diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e3c2c2602..1c7f6ac9a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -246,8 +246,11 @@ "nicknamePlaceholder": "New Nickname", "changeNicknameMessage": "Enter a nickname for this user", "noteToSelf": "Note to Self", + "savedMessages": "Saved Messages", "hideMenuBarTitle": "Hide Menu Bar", "hideMenuBarDescription": "Toggle system menu bar visibility.", + "matchThemeSystemSettingTitle": "Auto dark-mode", + "matchThemeSystemSettingDescription": "Match system settings", "startConversation": "Start New Conversation", "invalidNumberError": "Please check the Session ID or ONS name and try again", "failedResolveOns": "Failed to resolve ONS name", @@ -462,6 +465,10 @@ "open": "Open", "audioMessageAutoplayTitle": "Autoplay Audio Messages", "audioMessageAutoplayDescription": "Autoplay consecutive audio messages.", + "enterKeySettingTitle": "Enter Key", + "enterKeySettingDescription": "Function of the enter key when typing in a conversation.", + "enterSendNewMessageDescription": "ENTER sends a message, SHIFT + ENTER starts a new line", + "enterNewLineDescription": "SHIFT + ENTER sends a message, ENTER starts a new line", "clickToTrustContact": "Click to download media", "trustThisContactDialogTitle": "Trust $name$?", "trustThisContactDialogDescription": "Are you sure you want to download media sent by $name$?", diff --git a/package.json b/package.json index 94c4a8224..adc32a799 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.11.4", + "version": "1.11.5", "license": "GPL-3.0", "author": { "name": "Oxen Labs", diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 7166870aa..7df119bd4 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -811,8 +811,8 @@ font-size: 20px; font-weight: normal; letter-spacing: 0; - text-align: center; + background-color: rgba(0, 0, 0, 0.7); } .module-image__close-button { diff --git a/ts/components/MemberListItem.tsx b/ts/components/MemberListItem.tsx index c27742974..043ad60d0 100644 --- a/ts/components/MemberListItem.tsx +++ b/ts/components/MemberListItem.tsx @@ -141,12 +141,12 @@ const GroupStatusText = ({ groupPk, pubkey }: { pubkey: PubkeyType; groupPk: Gro const statusText = groupPromotionFailed ? window.i18n('promotionFailed') : groupInviteFailed - ? window.i18n('inviteFailed') - : groupInvitePending - ? window.i18n('invitePending') - : groupPromotionPending - ? window.i18n('promotionPending') - : null; + ? window.i18n('inviteFailed') + : groupInvitePending + ? window.i18n('invitePending') + : groupPromotionPending + ? window.i18n('promotionPending') + : null; if (!statusText) { return null; diff --git a/ts/components/basic/SessionButton.tsx b/ts/components/basic/SessionButton.tsx index e30a75ecb..10a1d33e8 100644 --- a/ts/components/basic/SessionButton.tsx +++ b/ts/components/basic/SessionButton.tsx @@ -82,8 +82,8 @@ const StyledButton = styled.div<{ props.buttonType === SessionButtonType.Solid ? 'background-color: var(--button-solid-disabled-color)' : props.buttonType === SessionButtonType.Outline - ? 'border: 1px solid var(--button-outline-disabled-color)' - : ''}; + ? 'border: 1px solid var(--button-outline-disabled-color)' + : ''}; color: ${props => props.buttonType === SessionButtonType.Solid ? 'var(--button-solid-text-color)' diff --git a/ts/components/basic/SessionRadio.tsx b/ts/components/basic/SessionRadio.tsx index 6da751018..f1bd2e27c 100644 --- a/ts/components/basic/SessionRadio.tsx +++ b/ts/components/basic/SessionRadio.tsx @@ -18,8 +18,8 @@ const StyledInput = styled.input<{ props.disabled ? 'var(--disabled-color)' : props.selectedColor - ? props.selectedColor - : 'var(--primary-color)'}; + ? props.selectedColor + : 'var(--primary-color)'}; } `; @@ -116,8 +116,8 @@ const StyledLabelOutlineSelected = styled(StyledLabel)<{ selectedColor: string } props.disabled ? 'var(--disabled-color)' : props.selectedColor - ? props.selectedColor - : 'var(--primary-color)'}; + ? props.selectedColor + : 'var(--primary-color)'}; outline: 1px solid transparent; /* CSS variables don't work here */ } `; diff --git a/ts/components/basic/SessionRadioGroup.tsx b/ts/components/basic/SessionRadioGroup.tsx index e07c0b19c..aa49fa0c8 100644 --- a/ts/components/basic/SessionRadioGroup.tsx +++ b/ts/components/basic/SessionRadioGroup.tsx @@ -8,7 +8,7 @@ interface Props { initialItem: string; items: Array<{ value: string; label: string }>; group: string; - onClick: (selectedValue: string) => any; + onClick: (selectedValue: string) => void; style?: CSSProperties; } diff --git a/ts/components/basic/Text.tsx b/ts/components/basic/Text.tsx index fd6e7a879..a99acf97a 100644 --- a/ts/components/basic/Text.tsx +++ b/ts/components/basic/Text.tsx @@ -42,23 +42,23 @@ const SpacerStyled = styled.div` props.size === 'xl' ? 'var(--margins-xl)' : props.size === 'lg' - ? 'var(--margins-lg)' - : props.size === 'md' - ? 'var(--margins-md)' - : props.size === 'sm' - ? 'var(--margins-sm)' - : 'var(--margins-xs)'}; + ? 'var(--margins-lg)' + : props.size === 'md' + ? 'var(--margins-md)' + : props.size === 'sm' + ? 'var(--margins-sm)' + : 'var(--margins-xs)'}; width: ${props => props.size === 'xl' ? 'var(--margins-xl)' : props.size === 'lg' - ? 'var(--margins-lg)' - : props.size === 'md' - ? 'var(--margins-md)' - : props.size === 'sm' - ? 'var(--margins-sm)' - : 'var(--margins-xs)'}; + ? 'var(--margins-lg)' + : props.size === 'md' + ? 'var(--margins-md)' + : props.size === 'sm' + ? 'var(--margins-sm)' + : 'var(--margins-xs)'}; `; const Spacer = (props: SpacerProps) => { diff --git a/ts/components/conversation/SessionQuotedMessageComposition.tsx b/ts/components/conversation/SessionQuotedMessageComposition.tsx index 6c54f5f78..fc5371209 100644 --- a/ts/components/conversation/SessionQuotedMessageComposition.tsx +++ b/ts/components/conversation/SessionQuotedMessageComposition.tsx @@ -70,14 +70,14 @@ function renderSubtitleText( return quoteText && quoteText !== '' ? quoteText : hasAudioAttachment - ? window.i18n('audio') - : isGenericFile - ? window.i18n('document') - : isVideo - ? window.i18n('video') - : isImage - ? window.i18n('image') - : null; + ? window.i18n('audio') + : isGenericFile + ? window.i18n('document') + : isVideo + ? window.i18n('video') + : isImage + ? window.i18n('image') + : null; } export const SessionQuotedMessageComposition = () => { diff --git a/ts/components/conversation/TimerNotification.tsx b/ts/components/conversation/TimerNotification.tsx index 121b9031a..758e4a6c7 100644 --- a/ts/components/conversation/TimerNotification.tsx +++ b/ts/components/conversation/TimerNotification.tsx @@ -144,8 +144,8 @@ function useTextToRender(props: PropsForExpirationTimer) { const mode = isLegacyDisappearingModeEnabled(expirationMode) ? null : expirationMode === 'deleteAfterRead' - ? window.i18n('timerModeRead') - : window.i18n('timerModeSent'); + ? window.i18n('timerModeRead') + : window.i18n('timerModeSent'); switch (type) { case 'fromOther': return disabled @@ -154,12 +154,12 @@ function useTextToRender(props: PropsForExpirationTimer) { [contact, timespanText] ) : mode - ? window.i18n(ownSideOnly ? 'theySetTheirDisappearingMessages' : 'theyChangedTheTimer', [ - contact, - timespanText, - mode, - ]) - : window.i18n('theyChangedTheTimerLegacy', [contact, timespanText]); + ? window.i18n(ownSideOnly ? 'theySetTheirDisappearingMessages' : 'theyChangedTheTimer', [ + contact, + timespanText, + mode, + ]) + : window.i18n('theyChangedTheTimerLegacy', [contact, timespanText]); case 'fromMe': case 'fromSync': return disabled @@ -167,11 +167,11 @@ function useTextToRender(props: PropsForExpirationTimer) { ownSideOnly ? 'youDisabledYourDisappearingMessages' : 'youDisabledDisappearingMessages' ) : mode - ? window.i18n(ownSideOnly ? 'youSetYourDisappearingMessages' : 'youChangedTheTimer', [ - timespanText, - mode, - ]) - : window.i18n('youChangedTheTimerLegacy', [timespanText]); + ? window.i18n(ownSideOnly ? 'youSetYourDisappearingMessages' : 'youChangedTheTimer', [ + timespanText, + mode, + ]) + : window.i18n('youChangedTheTimerLegacy', [timespanText]); default: assertUnreachable(type, `TimerNotification: Missing case error "${type}"`); } diff --git a/ts/components/conversation/TypingAnimation.tsx b/ts/components/conversation/TypingAnimation.tsx index 61af18a01..7ec13f7f5 100644 --- a/ts/components/conversation/TypingAnimation.tsx +++ b/ts/components/conversation/TypingAnimation.tsx @@ -63,8 +63,8 @@ const StyledTypingDot = styled.div<{ index: number }>` props.index === 0 ? 'typing-animation-first' : props.index === 1 - ? 'typing-animation-second' - : 'typing-animation-third'} + ? 'typing-animation-second' + : 'typing-animation-third'} 1600ms ease infinite; `; diff --git a/ts/components/conversation/composition/CompositionBox.tsx b/ts/components/conversation/composition/CompositionBox.tsx index eedf5114d..b0aaa25c7 100644 --- a/ts/components/conversation/composition/CompositionBox.tsx +++ b/ts/components/conversation/composition/CompositionBox.tsx @@ -836,8 +836,19 @@ class CompositionBoxInner extends React.Component { } private async onKeyDown(event: any) { - if (event.key === 'Enter' && !event.shiftKey && !event.nativeEvent.isComposing) { - // If shift, newline. If in IME composing mode, leave it to IME. Else send message. + const isEnter = event.key === 'Enter'; + const isShiftEnter = event.shiftKey && isEnter; + const isShiftSendEnabled = window.getSettingValue(SettingsKey.hasShiftSendEnabled) as boolean; + const isNotComposing = !event.nativeEvent.isComposing; + + if (isShiftSendEnabled && isEnter && isNotComposing) { + event.preventDefault(); + if (isShiftEnter) { + await this.onSendMessage(); + } else { + this.insertNewLine(); + } + } else if (isEnter && !event.shiftKey && isNotComposing) { event.preventDefault(); await this.onSendMessage(); } else if (event.key === 'Escape' && this.state.showEmojiPanel) { @@ -849,6 +860,34 @@ class CompositionBoxInner extends React.Component { } } + private insertNewLine() { + const messageBox = this.textarea.current; + if (!messageBox) { + return; + } + + const { draft } = this.state; + const { selectedConversationKey } = this.props; + + if (!selectedConversationKey) { + return; // add this check to prevent undefined from being used + } + + const currentSelectionStart = Number(messageBox.selectionStart); + const realSelectionStart = getSelectionBasedOnMentions(draft, currentSelectionStart); + + const before = draft.slice(0, realSelectionStart); + const after = draft.slice(realSelectionStart); + + const updatedDraft = `${before}\n${after}`; + + this.setState({ draft: updatedDraft }); + updateDraftForConversation({ + conversationKey: selectedConversationKey, + draft: updatedDraft, + }); + } + private async onKeyUp() { if (!this.props.selectedConversationKey) { throw new Error('selectedConversationKey is needed'); diff --git a/ts/components/conversation/message/message-content/MessageText.tsx b/ts/components/conversation/message/message-content/MessageText.tsx index 15ef56bd1..18a0f39b6 100644 --- a/ts/components/conversation/message/message-content/MessageText.tsx +++ b/ts/components/conversation/message/message-content/MessageText.tsx @@ -32,8 +32,8 @@ export const MessageText = (props: Props) => { const contents = isDeleted ? window.i18n('messageDeletedPlaceholder') : direction === 'incoming' && status === 'error' - ? window.i18n('incomingError') - : text?.trim(); + ? window.i18n('incomingError') + : text?.trim(); if (!contents) { return null; diff --git a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx index fa17c8beb..2e8a93131 100644 --- a/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx +++ b/ts/components/conversation/message/message-item/GroupUpdateMessage.tsx @@ -69,10 +69,10 @@ function changeOfMembersV2({ sortedWithUsFirst.length === 1 && sortedWithUsFirst[0].sessionId === us ? 'You' : sortedWithUsFirst.length === 1 - ? 'One' - : sortedWithUsFirst.length === 2 - ? 'Two' - : 'Others'; + ? 'One' + : sortedWithUsFirst.length === 2 + ? 'Two' + : 'Others'; const action = type === 'added' ? 'Joined' : type === 'promoted' ? 'Promoted' : ('Removed' as const); diff --git a/ts/components/conversation/message/reactions/ReactionPopup.tsx b/ts/components/conversation/message/reactions/ReactionPopup.tsx index f8a13f587..f438266cf 100644 --- a/ts/components/conversation/message/reactions/ReactionPopup.tsx +++ b/ts/components/conversation/message/reactions/ReactionPopup.tsx @@ -111,8 +111,8 @@ const Contacts = (contacts: Array, count: number) => { reactors === 1 ? 'reactionPopupOne' : reactors === 2 - ? 'reactionPopupTwo' - : 'reactionPopupThree', + ? 'reactionPopupTwo' + : 'reactionPopupThree', contacts )}{' '} {window.i18n('reactionPopup')} diff --git a/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx b/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx index f3cf6bd4c..312ff0021 100644 --- a/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx +++ b/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx @@ -269,10 +269,10 @@ export const OverlayRightPanelSettings = () => { const leaveGroupString = isPublic ? window.i18n('leaveGroup') : isKickedFromGroup - ? window.i18n('youGotKickedFromGroup') - : left - ? window.i18n('youLeftTheGroup') - : window.i18n('leaveGroup'); + ? window.i18n('youGotKickedFromGroup') + : left + ? window.i18n('youLeftTheGroup') + : window.i18n('leaveGroup'); const showUpdateGroupNameButton = isGroup && weAreAdmin && !commonNoShow; // legacy groups non-admin cannot change groupname anymore const showAddRemoveModeratorsButton = weAreAdmin && !commonNoShow && isPublic; diff --git a/ts/components/conversation/right-panel/overlay/disappearing-messages/DisappearingModes.tsx b/ts/components/conversation/right-panel/overlay/disappearing-messages/DisappearingModes.tsx index 083a8733a..3683119cc 100644 --- a/ts/components/conversation/right-panel/overlay/disappearing-messages/DisappearingModes.tsx +++ b/ts/components/conversation/right-panel/overlay/disappearing-messages/DisappearingModes.tsx @@ -42,19 +42,19 @@ export const DisappearingModes = (props: DisappearingModesProps) => { mode === 'legacy' ? window.i18n('disappearingMessagesModeLegacy') : mode === 'deleteAfterRead' - ? window.i18n('disappearingMessagesModeAfterRead') - : mode === 'deleteAfterSend' - ? window.i18n('disappearingMessagesModeAfterSend') - : window.i18n('disappearingMessagesModeOff'); + ? window.i18n('disappearingMessagesModeAfterRead') + : mode === 'deleteAfterSend' + ? window.i18n('disappearingMessagesModeAfterSend') + : window.i18n('disappearingMessagesModeOff'); const subtitleI18n = mode === 'legacy' ? window.i18n('disappearingMessagesModeLegacySubtitle') : mode === 'deleteAfterRead' - ? window.i18n('disappearingMessagesModeAfterReadSubtitle') - : mode === 'deleteAfterSend' - ? window.i18n('disappearingMessagesModeAfterSendSubtitle') - : undefined; + ? window.i18n('disappearingMessagesModeAfterReadSubtitle') + : mode === 'deleteAfterSend' + ? window.i18n('disappearingMessagesModeAfterSendSubtitle') + : undefined; return ( { singleMode === 'deleteAfterRead' ? window.i18n('disappearingMessagesModeAfterReadSubtitle') : singleMode === 'deleteAfterSend' - ? window.i18n('disappearingMessagesModeAfterSendSubtitle') - : window.i18n('settingAppliesToYourMessages') + ? window.i18n('disappearingMessagesModeAfterSendSubtitle') + : window.i18n('settingAppliesToYourMessages') } /> { singleMode ? disappearingModeOptions[singleMode] : modeSelected - ? disappearingModeOptions[modeSelected] - : undefined + ? disappearingModeOptions[modeSelected] + : undefined } /> @@ -217,8 +217,8 @@ export const OverlayDisappearingMessages = () => { singleMode ? disappearingModeOptions[singleMode] : modeSelected - ? disappearingModeOptions[modeSelected] - : undefined + ? disappearingModeOptions[modeSelected] + : undefined } dataTestId={'disappear-set-button'} > diff --git a/ts/components/dialog/SessionPasswordDialog.tsx b/ts/components/dialog/SessionPasswordDialog.tsx index b358f40e3..b2a77618e 100644 --- a/ts/components/dialog/SessionPasswordDialog.tsx +++ b/ts/components/dialog/SessionPasswordDialog.tsx @@ -82,10 +82,10 @@ export class SessionPasswordDialog extends React.Component { passwordAction === 'change' ? 'changePassword' : passwordAction === 'remove' - ? 'removePassword' - : passwordAction === 'enter' - ? 'passwordViewTitle' - : 'setPassword'; + ? 'removePassword' + : passwordAction === 'enter' + ? 'passwordViewTitle' + : 'setPassword'; return ( diff --git a/ts/components/icon/DropDownAndToggleButton.tsx b/ts/components/icon/DropDownAndToggleButton.tsx index fccb8a3db..98db7b081 100644 --- a/ts/components/icon/DropDownAndToggleButton.tsx +++ b/ts/components/icon/DropDownAndToggleButton.tsx @@ -18,14 +18,14 @@ const StyledRoundedButton = styled.div<{ isFullScreen: boolean; isMuted: boolean props.isMuted ? 'var(--call-buttons-background-disabled-color)' : props.isFullScreen - ? 'var(--call-buttons-action-background-color)' - : 'var(--call-buttons-background-color)'}; + ? 'var(--call-buttons-action-background-color)' + : 'var(--call-buttons-background-color)'}; color: ${props => props.isMuted ? 'var(--call-buttons-icon-disabled-color)' : props.isFullScreen - ? 'var(--call-buttons-action-icon-color)' - : 'var(--call-buttons-icon-color)'}; + ? 'var(--call-buttons-action-icon-color)' + : 'var(--call-buttons-icon-color)'}; ${props => props.isFullScreen && 'opacity: 0.4;'} &:hover { diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index 28d5f4c55..c91065fae 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -44,8 +44,9 @@ import { import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob'; import { forceSyncConfigurationNowIfNeeded } from '../../session/utils/sync/syncUtils'; import { isDarkTheme } from '../../state/selectors/theme'; -import { ThemeStateType } from '../../themes/constants/colors'; +import { ensureThemeConsistency } from '../../themes/SessionTheme'; import { switchThemeTo } from '../../themes/switchTheme'; +import { getOppositeTheme } from '../../util/theme'; import { ReleasedFeatures } from '../../util/releaseFeature'; @@ -63,11 +64,8 @@ const Section = (props: { type: SectionType }) => { if (type === SectionType.Profile) { dispatch(editProfileModal({})); } else if (type === SectionType.ColorMode) { - const currentTheme = String(window.Events.getThemeSetting()); - const newTheme = ( - isDarkMode ? currentTheme.replace('dark', 'light') : currentTheme.replace('light', 'dark') - ) as ThemeStateType; - + const currentTheme = window.Events.getThemeSetting(); + const newTheme = getOppositeTheme(currentTheme); // We want to persist the primary color when using the color mode button void switchThemeTo({ theme: newTheme, @@ -151,14 +149,26 @@ const cleanUpMediasInterval = DURATION.MINUTES * 60; const fetchReleaseFromFileServerInterval = 1000 * 60; // try to fetch the latest release from the fileserver every minute const setupTheme = async () => { + const shouldFollowSystemTheme = window.getSettingValue(SettingsKey.hasFollowSystemThemeEnabled); const theme = window.Events.getThemeSetting(); - // We don't want to reset the primary color on startup - await switchThemeTo({ + const themeConfig = { theme, mainWindow: true, usePrimaryColor: true, dispatch: window?.inboxStore?.dispatch || undefined, - }); + }; + + if (shouldFollowSystemTheme) { + // Check if system theme matches currently set theme, if not switch it and return true, if matching return false + const wasThemeSwitched = await ensureThemeConsistency(); + if (!wasThemeSwitched) { + // if theme wasn't switched them set theme to default + await switchThemeTo(themeConfig); + } + return; + } + + await switchThemeTo(themeConfig); }; // Do this only if we created a new Session ID, or if we already received the initial configuration message diff --git a/ts/components/leftpane/conversation-list-item/UserItem.tsx b/ts/components/leftpane/conversation-list-item/UserItem.tsx index 9e6e6f8e5..2c01a1fbd 100644 --- a/ts/components/leftpane/conversation-list-item/UserItem.tsx +++ b/ts/components/leftpane/conversation-list-item/UserItem.tsx @@ -28,8 +28,8 @@ export const UserItem = () => { const displayName = isMe ? window.i18n('noteToSelf') : isSearchResultsMode && hasNickname && realName - ? `${realName} (${username})` - : username; + ? `${realName} (${username})` + : username; let shouldShowPubkey = false; if (isEmpty(username) && isEmpty(displayName)) { diff --git a/ts/components/menu/Menu.tsx b/ts/components/menu/Menu.tsx index fa3fba6df..f98274a00 100644 --- a/ts/components/menu/Menu.tsx +++ b/ts/components/menu/Menu.tsx @@ -589,8 +589,8 @@ export const NotificationForConvoMenuItem = (): JSX.Element | null => { n === 'all' || !n ? 'notificationForConvo_all' : n === 'disabled' - ? 'notificationForConvo_disabled' - : 'notificationForConvo_mentions_only'; + ? 'notificationForConvo_disabled' + : 'notificationForConvo_mentions_only'; return { value: n, name: window.i18n(keyToUse) }; }); diff --git a/ts/components/settings/section/CategoryAppearance.tsx b/ts/components/settings/section/CategoryAppearance.tsx index 8ad4400fc..9d889dedf 100644 --- a/ts/components/settings/section/CategoryAppearance.tsx +++ b/ts/components/settings/section/CategoryAppearance.tsx @@ -3,13 +3,15 @@ import React from 'react'; import useUpdate from 'react-use/lib/useUpdate'; import { SettingsKey } from '../../../data/settings-key'; import { isHideMenuBarSupported } from '../../../types/Settings'; - +import { useHasFollowSystemThemeEnabled } from '../../../state/selectors/settings'; +import { ensureThemeConsistency } from '../../../themes/SessionTheme'; import { SessionToggleWithDescription } from '../SessionSettingListItem'; import { SettingsThemeSwitcher } from '../SettingsThemeSwitcher'; import { ZoomingSessionSlider } from '../ZoomingSessionSlider'; export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null }) => { const forceUpdate = useUpdate(); + const isFollowSystemThemeEnabled = useHasFollowSystemThemeEnabled(); if (props.hasPassword !== null) { const isHideMenuBarActive = @@ -32,6 +34,20 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null active={isHideMenuBarActive} /> )} + { + const toggledValue = !isFollowSystemThemeEnabled; + await window.setSettingValue(SettingsKey.hasFollowSystemThemeEnabled, toggledValue); + if (!isFollowSystemThemeEnabled) { + await ensureThemeConsistency(); + } + }} + title={window.i18n('matchThemeSystemSettingTitle')} + description={window.i18n('matchThemeSystemSettingDescription')} + active={isFollowSystemThemeEnabled} + dataTestId="enable-follow-system-theme" + /> ); } diff --git a/ts/components/settings/section/CategoryConversations.tsx b/ts/components/settings/section/CategoryConversations.tsx index 3830883ae..4fd0a2074 100644 --- a/ts/components/settings/section/CategoryConversations.tsx +++ b/ts/components/settings/section/CategoryConversations.tsx @@ -6,10 +6,13 @@ import { SettingsKey } from '../../../data/settings-key'; import { ToastUtils } from '../../../session/utils'; import { toggleAudioAutoplay } from '../../../state/ducks/userConfig'; import { getAudioAutoplay } from '../../../state/selectors/userConfig'; - +import { SessionRadioGroup } from '../../basic/SessionRadioGroup'; import { BlockedContactsList } from '../BlockedList'; - -import { SessionToggleWithDescription } from '../SessionSettingListItem'; +import { + SessionSettingsItemWrapper, + SessionToggleWithDescription, +} from '../SessionSettingListItem'; +import { useHasEnterSendEnabled } from '../../../state/selectors/settings'; async function toggleCommunitiesPruning() { try { @@ -81,13 +84,49 @@ const AudioMessageAutoPlaySetting = () => { ); }; +const EnterKeyFunctionSetting = () => { + const initialSetting = useHasEnterSendEnabled(); + const selectedWithSettingTrue = 'enterForNewLine'; + + const items = [ + { + label: window.i18n('enterSendNewMessageDescription'), + value: 'enterForSend', + }, + { + label: window.i18n('enterNewLineDescription'), + value: selectedWithSettingTrue, + }, + ]; + + return ( + + { + void window.setSettingValue( + SettingsKey.hasShiftSendEnabled, + selectedRadioValue === selectedWithSettingTrue + ); + }} + /> + + ); +}; + export const CategoryConversations = () => { return ( <> - + ); diff --git a/ts/data/settings-key.ts b/ts/data/settings-key.ts index 345e6b081..872f57e7f 100644 --- a/ts/data/settings-key.ts +++ b/ts/data/settings-key.ts @@ -1,7 +1,7 @@ const settingsReadReceipt = 'read-receipt-setting'; const settingsTypingIndicator = 'typing-indicators-setting'; const settingsAutoUpdate = 'auto-update'; - +const hasShiftSendEnabled = 'hasShiftSendEnabled'; const settingsMenuBar = 'hide-menu-bar'; const settingsSpellCheck = 'spell-check'; const settingsLinkPreview = 'link-preview-setting'; @@ -14,6 +14,7 @@ const someDeviceOutdatedSyncing = 'someDeviceOutdatedSyncing'; const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem'; const lastAvatarUploadTimestamp = 'lastAvatarUploadTimestamp'; const hasLinkPreviewPopupBeenDisplayed = 'hasLinkPreviewPopupBeenDisplayed'; +const hasFollowSystemThemeEnabled = 'hasFollowSystemThemeEnabled'; // user config tracking timestamps (to discard incoming messages which would make a change we reverted in the last config message we merged) const latestUserProfileEnvelopeTimestamp = 'latestUserProfileEnvelopeTimestamp'; @@ -24,6 +25,7 @@ export const SettingsKey = { settingsReadReceipt, settingsTypingIndicator, settingsAutoUpdate, + hasShiftSendEnabled, settingsMenuBar, settingsSpellCheck, settingsLinkPreview, @@ -39,6 +41,7 @@ export const SettingsKey = { latestUserProfileEnvelopeTimestamp, latestUserGroupEnvelopeTimestamp, latestUserContactsEnvelopeTimestamp, + hasFollowSystemThemeEnabled, } as const; export const KNOWN_BLINDED_KEYS_ITEM = 'KNOWN_BLINDED_KEYS_ITEM'; diff --git a/ts/hooks/useParamSelector.ts b/ts/hooks/useParamSelector.ts index df6630aa7..979f41412 100644 --- a/ts/hooks/useParamSelector.ts +++ b/ts/hooks/useParamSelector.ts @@ -452,8 +452,8 @@ export function useQuoteAuthorName(authorId?: string): { const authorName = isMe ? window.i18n('you') : convoProps?.nickname || convoProps?.isPrivate - ? convoProps?.displayNameInProfile - : undefined; + ? convoProps?.displayNameInProfile + : undefined; return { authorName, isMe }; } diff --git a/ts/mains/main_node.ts b/ts/mains/main_node.ts index e845e2098..46b850a65 100644 --- a/ts/mains/main_node.ts +++ b/ts/mains/main_node.ts @@ -10,6 +10,7 @@ import { dialog, ipcMain as ipc, Menu, + nativeTheme, protocol as electronProtocol, screen, shell, @@ -1119,6 +1120,15 @@ ipc.on('set-auto-update-setting', async (_event, enabled) => { } }); +ipc.on('get-native-theme', event => { + event.sender.send('send-native-theme', nativeTheme.shouldUseDarkColors); +}); + +nativeTheme.on('updated', () => { + // Inform all renderer processes of the theme change + mainWindow?.webContents.send('native-theme-update', nativeTheme.shouldUseDarkColors); +}); + async function getThemeFromMainWindow() { return new Promise(resolve => { ipc.once('get-success-theme-setting', (_event, value) => { diff --git a/ts/mains/main_renderer.tsx b/ts/mains/main_renderer.tsx index 8bae8fa02..6149b240e 100644 --- a/ts/mains/main_renderer.tsx +++ b/ts/mains/main_renderer.tsx @@ -3,7 +3,9 @@ import _, { toPairs } from 'lodash'; import ReactDOM from 'react-dom'; import nativeEmojiData from '@emoji-mart/data'; -import React from 'react'; +import { ipcRenderer } from 'electron'; +// eslint-disable-next-line import/no-named-default +import { default as React } from 'react'; import { isMacOS } from '../OS'; import { SessionInboxView } from '../components/SessionInboxView'; @@ -22,11 +24,13 @@ import { getOurPubKeyStrFromCache } from '../session/utils/User'; import { runners } from '../session/utils/job_runners/JobRunner'; import { LibSessionUtil } from '../session/utils/libsession/libsession_utils'; import { switchPrimaryColorTo } from '../themes/switchPrimaryColor'; +import { switchThemeTo } from '../themes/switchTheme'; import { BlockedNumberController } from '../util'; import { initialiseEmojiData } from '../util/emoji'; import { Notifications } from '../util/notifications'; import { Registration } from '../util/registration'; import { Storage, isSignInByLinking } from '../util/storage'; +import { getOppositeTheme, isThemeMismatched } from '../util/theme'; // Globally disable drag and drop document.body.addEventListener( @@ -109,6 +113,23 @@ function mapOldThemeToNew(theme: string) { return theme; } } +// using __unused as lodash is imported using _ +ipcRenderer.on('native-theme-update', (__unused, shouldUseDarkColors) => { + const shouldFollowSystemTheme = window.getSettingValue(SettingsKey.hasFollowSystemThemeEnabled); + + if (shouldFollowSystemTheme) { + const theme = window.Events.getThemeSetting(); + if (isThemeMismatched(theme, shouldUseDarkColors)) { + const newTheme = getOppositeTheme(theme); + void switchThemeTo({ + theme: newTheme, + mainWindow: true, + usePrimaryColor: true, + dispatch: window?.inboxStore?.dispatch, + }); + } + } +}); async function startJobRunners() { // start the job runners diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index b3208624d..1af32139d 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -17,6 +17,7 @@ import { xor, } from 'lodash'; +import { v4 } from 'uuid'; import { SignalService } from '../protobuf'; import { getMessageQueue } from '../session'; import { ConvoHub } from '../session/conversations'; @@ -126,7 +127,6 @@ import { getSubscriberCountOutsideRedux, } from '../state/selectors/sogsRoomInfo'; // decide it it makes sense to move this to a redux slice? -import { v4 } from 'uuid'; import { DisappearingMessages } from '../session/disappearing_messages'; import { DisappearingMessageConversationModeType } from '../session/disappearing_messages/types'; import { FetchMsgExpirySwarm } from '../session/utils/job_runners/jobs/FetchMsgExpirySwarmJob'; @@ -2569,10 +2569,10 @@ export class ConversationModel extends Backbone.Model { mode === 'deleteAfterRead' ? ours === 'deleteAfterRead' : mode === 'deleteAfterSend' - ? ours === 'deleteAfterSend' - : mode === 'off' - ? ours === 'off' - : false; + ? ours === 'deleteAfterSend' + : mode === 'off' + ? ours === 'off' + : false; return success; } diff --git a/ts/node/migration/helpers/v31.ts b/ts/node/migration/helpers/v31.ts index d1314b3cb..b8bef0bad 100644 --- a/ts/node/migration/helpers/v31.ts +++ b/ts/node/migration/helpers/v31.ts @@ -309,14 +309,8 @@ function insertLegacyGroupIntoWrapper( ) { checkTargetMigration(version, targetVersion); - const { - priority, - id, - groupAdmins, - members, - displayNameInProfile, - lastJoinedTimestamp, - } = legacyGroup; + const { priority, id, groupAdmins, members, displayNameInProfile, lastJoinedTimestamp } = + legacyGroup; const latestEncryptionKeyPairHex = sqlNode.getLatestClosedGroupEncryptionKeyPair( legacyGroup.id, diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 90aa722f1..867efb03a 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -16,15 +16,16 @@ import { ConversationTypeEnum } from '../models/conversationAttributes'; import { getSwarmPollingInstance } from '../session/apis/snode_api'; import { GetNetworkTime } from '../session/apis/snode_api/getNetworkTime'; import { SnodeNamespaces } from '../session/apis/snode_api/namespaces'; +import { DisappearingMessageUpdate } from '../session/disappearing_messages/types'; import { ClosedGroupEncryptionPairReplyMessage } from '../session/messages/outgoing/controlMessage/group/ClosedGroupEncryptionPairReplyMessage'; import { UserUtils } from '../session/utils'; import { perfEnd, perfStart } from '../session/utils/Performance'; import { ReleasedFeatures } from '../util/releaseFeature'; import { Storage } from '../util/storage'; // eslint-disable-next-line import/no-unresolved, import/extensions -import { DisappearingMessageUpdate } from '../session/disappearing_messages/types'; -import { ClosedGroup, GroupDiff, GroupInfo } from '../session/group/closed-group'; import { ConfigWrapperUser } from '../webworker/workers/browser/libsession_worker_functions'; + +import { ClosedGroup, GroupDiff, GroupInfo } from '../session/group/closed-group'; import { IncomingMessageCache } from './cache'; import { getSettingsKeyFromLibsessionWrapper } from './configMessage'; import { ECKeyPair, HexKeyPair } from './keypairs'; @@ -318,8 +319,8 @@ export async function handleNewClosedGroup( expireTimer === 0 ? 'off' : ReleasedFeatures.isDisappearMessageV2FeatureReleasedCached() - ? 'deleteAfterSend' - : 'legacy', + ? 'deleteAfterSend' + : 'legacy', providedExpireTimer: expireTimer, providedSource: sender, receivedAt: GetNetworkTime.now(), diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 0b2985ac5..7218bba57 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -15,6 +15,7 @@ import { ProfileManager } from '../session/profile_manager/ProfileManager'; import { PubKey } from '../session/types'; import { StringUtils, UserUtils } from '../session/utils'; import { toHex } from '../session/utils/String'; +import { FetchMsgExpirySwarm } from '../session/utils/job_runners/jobs/FetchMsgExpirySwarmJob'; import { UserSync } from '../session/utils/job_runners/jobs/UserSyncJob'; import { LibSessionUtil } from '../session/utils/libsession/libsession_utils'; import { SessionUtilContact } from '../session/utils/libsession/libsession_utils_contacts'; @@ -43,8 +44,9 @@ import { } from '../webworker/workers/browser/libsession_worker_functions'; // eslint-disable-next-line import/no-unresolved, import/extensions import { Data } from '../data/data'; -import { FetchMsgExpirySwarm } from '../session/utils/job_runners/jobs/FetchMsgExpirySwarmJob'; import { ReleasedFeatures } from '../util/releaseFeature'; + +// eslint-disable-next-line import/no-unresolved import { ContactsWrapperActions, ConvoInfoVolatileWrapperActions, diff --git a/ts/receiver/groupv2/handleGroupV2Message.ts b/ts/receiver/groupv2/handleGroupV2Message.ts index 8d62b0630..b3910a070 100644 --- a/ts/receiver/groupv2/handleGroupV2Message.ts +++ b/ts/receiver/groupv2/handleGroupV2Message.ts @@ -13,6 +13,7 @@ import { GroupUpdateInviteResponseMessage } from '../../session/messages/outgoin import { ed25519Str } from '../../session/onions/onionPath'; import { PubKey } from '../../session/types'; import { UserUtils } from '../../session/utils'; +import { sleepFor } from '../../session/utils/Promise'; import { stringToUint8Array } from '../../session/utils/String'; import { PreConditionFailed } from '../../session/utils/errors'; import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob'; @@ -63,15 +64,16 @@ async function handleGroupInviteMessage({ author, envelopeTimestamp, }: GroupInviteDetails) { - if (!PubKey.is03Pubkey(inviteMessage.groupSessionId)) { + const groupPk = inviteMessage.groupSessionId; + if (!PubKey.is03Pubkey(groupPk)) { return; } if (BlockedNumberController.isBlocked(author)) { window.log.info( - `received invite to group ${ed25519Str( - inviteMessage.groupSessionId - )} by blocked user:${ed25519Str(author)}... dropping it` + `received invite to group ${ed25519Str(groupPk)} by blocked user:${ed25519Str( + author + )}... dropping it` ); return; } @@ -79,7 +81,7 @@ async function handleGroupInviteMessage({ const authorIsApproved = ConvoHub.use().get(author)?.isApproved() || false; const sigValid = await verifySig({ - pubKey: HexString.fromHexStringNoPrefix(inviteMessage.groupSessionId), + pubKey: HexString.fromHexStringNoPrefix(groupPk), signature: inviteMessage.adminSignature, data: stringToUint8Array(`INVITE${UserUtils.getOurPubKeyStrFromCache()}${envelopeTimestamp}`), }); @@ -89,15 +91,8 @@ async function handleGroupInviteMessage({ return; } - window.log.debug( - `received invite to group ${ed25519Str(inviteMessage.groupSessionId)} by user:${ed25519Str( - author - )}` - ); - const convo = await ConvoHub.use().getOrCreateAndWait( - inviteMessage.groupSessionId, - ConversationTypeEnum.GROUPV2 - ); + window.log.debug(`received invite to group ${ed25519Str(groupPk)} by user:${ed25519Str(author)}`); + const convo = await ConvoHub.use().getOrCreateAndWait(groupPk, ConversationTypeEnum.GROUPV2); convo.set({ active_at: envelopeTimestamp, didApproveMe: true, @@ -112,14 +107,14 @@ async function handleGroupInviteMessage({ await convo.commit(); const userEd25519Secretkey = (await UserUtils.getUserED25519KeyPairBytes()).privKeyBytes; - let found = await UserGroupsWrapperActions.getGroup(inviteMessage.groupSessionId); + let found = await UserGroupsWrapperActions.getGroup(groupPk); if (!found) { found = { authData: null, joinedAtSeconds: Date.now(), name: inviteMessage.name, priority: 0, - pubkeyHex: inviteMessage.groupSessionId, + pubkeyHex: groupPk, secretKey: null, kicked: false, invitePending: true, @@ -136,24 +131,24 @@ async function handleGroupInviteMessage({ found.authData = inviteMessage.memberAuthData; await UserGroupsWrapperActions.setGroup(found); - await MetaGroupWrapperActions.init(inviteMessage.groupSessionId, { + await MetaGroupWrapperActions.init(groupPk, { metaDumped: null, groupEd25519Secretkey: null, userEd25519Secretkey: toFixedUint8ArrayOfLength(userEd25519Secretkey, 64).buffer, - groupEd25519Pubkey: toFixedUint8ArrayOfLength( - HexString.fromHexStringNoPrefix(inviteMessage.groupSessionId), - 32 - ).buffer, + groupEd25519Pubkey: toFixedUint8ArrayOfLength(HexString.fromHexStringNoPrefix(groupPk), 32) + .buffer, }); + await LibSessionUtil.saveDumpsToDb(UserUtils.getOurPubKeyStrFromCache()); await UserSync.queueNewJobIfNeeded(); if (!found.invitePending) { // if this group should already be polling - getSwarmPollingInstance().addGroupId(inviteMessage.groupSessionId); - console.warn( - 'we need to do a first poll to fetch the keys etc before we can send our invite response...' - ); - await sendInviteResponseToGroup({ groupPk: inviteMessage.groupSessionId }); + getSwarmPollingInstance().addGroupId(groupPk, async () => { + // we need to do a first poll to fetch the keys etc before we can send our invite response + // this is pretty hacky, but also an admin seeing a message from that user in the group will mark it as not pending anymore + await sleepFor(2000); + await sendInviteResponseToGroup({ groupPk }); + }); } } diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 7c4afac72..3dde14498 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -80,7 +80,11 @@ export const getSwarmPollingInstance = () => { return instance; }; -type GroupPollingEntry = { pubkey: PubKey; lastPolledTimestamp: number }; +type GroupPollingEntry = { + pubkey: PubKey; + lastPolledTimestamp: number; + callbackFirstPoll?: () => Promise; +}; function entryToKey(entry: GroupPollingEntry) { return entry.pubkey.key; @@ -134,11 +138,11 @@ export class SwarmPolling { } } - public addGroupId(pubkey: PubKey | string) { + public addGroupId(pubkey: PubKey | string, callbackFirstPoll?: () => Promise) { const pk = PubKey.cast(pubkey); if (this.groupPolling.findIndex(m => m.pubkey.key === pk.key) === -1) { window?.log?.info('Swarm addGroupId: adding pubkey to polling', pk.key); - this.groupPolling.push({ pubkey: pk, lastPolledTimestamp: 0 }); + this.groupPolling.push({ pubkey: pk, lastPolledTimestamp: 0, callbackFirstPoll }); } } @@ -386,6 +390,11 @@ export class SwarmPolling { const shouldDiscardMessages = await this.shouldLeaveNotPolledGroup({ type, pubkey }); if (shouldDiscardMessages) { + window.log.info( + `polled a pk which should not be polled anymore: ${ed25519Str( + pubkey + )}. Discarding polling result` + ); return; } @@ -394,6 +403,13 @@ export class SwarmPolling { // groupv2 messages are not stored in the cache, so for each that we process, we also add it as seen message. // this is to take care of a crash half way through processing messages. We'd get the same 100 messages back, and we'd skip up to the first not seen message await handleMessagesForGroupV2(newMessages, pubkey); + // if a callback was registered for the first poll of that group pk, call it + const groupEntry = this.groupPolling.find(m => m.pubkey.key === pubkey); + if (groupEntry && groupEntry.callbackFirstPoll) { + void groupEntry.callbackFirstPoll(); + groupEntry.callbackFirstPoll = undefined; + } + return; } diff --git a/ts/session/disappearing_messages/types.ts b/ts/session/disappearing_messages/types.ts index 550df0663..39c5574cd 100644 --- a/ts/session/disappearing_messages/types.ts +++ b/ts/session/disappearing_messages/types.ts @@ -1,5 +1,5 @@ // NOTE this must match Content.ExpirationType in the protobuf -export type DisappearingMessageType = typeof DisappearingMessageMode[number]; +export type DisappearingMessageType = (typeof DisappearingMessageMode)[number]; export const DisappearingMessageMode = ['unknown', 'deleteAfterRead', 'deleteAfterSend'] as const; export type DisappearAfterSendOnly = Exclude; @@ -11,7 +11,8 @@ export const DisappearingMessageConversationModes = [ // TODO legacy messages support will be removed in a future release 'legacy', ] as const; -export type DisappearingMessageConversationModeType = typeof DisappearingMessageConversationModes[number]; +export type DisappearingMessageConversationModeType = + (typeof DisappearingMessageConversationModes)[number]; // TODO legacy messages support will be removed in a future release // expirationType will no longer have an undefined option diff --git a/ts/session/utils/Promise.ts b/ts/session/utils/Promise.ts index c9de5b730..005e44c0e 100644 --- a/ts/session/utils/Promise.ts +++ b/ts/session/utils/Promise.ts @@ -203,7 +203,6 @@ export async function timeout(promise: Promise, timeoutMs: number): Promis return Promise.race([timeoutPromise, promise]); } - export const sleepFor = async (ms: number, showLog = false) => { if (showLog) { // eslint-disable-next-line no-console diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts index 12e6380c9..a4cecb067 100644 --- a/ts/session/utils/calling/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -145,16 +145,17 @@ const iceServersFullArray = [ username: 'session202111', credential: '053c268164bc7bd7', }, - { - urls: 'turn:fenrir.getsession.org', - username: 'session202111', - credential: '053c268164bc7bd7', - }, - { - urls: 'turn:frigg.getsession.org', - username: 'session202111', - credential: '053c268164bc7bd7', - }, + // excluding those two (fenrir & frigg) as they are TCP only for now + // { + // urls: 'turn:fenrir.getsession.org', + // username: 'session202111', + // credential: '053c268164bc7bd7', + // }, + // { + // urls: 'turn:frigg.getsession.org', + // username: 'session202111', + // credential: '053c268164bc7bd7', + // }, { urls: 'turn:angus.getsession.org', username: 'session202111', diff --git a/ts/session/utils/job_runners/jobs/GroupInviteJob.ts b/ts/session/utils/job_runners/jobs/GroupInviteJob.ts index bfa75c18b..4fb1636a5 100644 --- a/ts/session/utils/job_runners/jobs/GroupInviteJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupInviteJob.ts @@ -153,7 +153,6 @@ class GroupInviteJob extends PersistedJob { } finally { updateFailedStateForMember(groupPk, member, failed); try { - debugger; await MetaGroupWrapperActions.memberSetInvited(groupPk, member, failed); } catch (e) { window.log.warn('GroupInviteJob memberSetInvited failed with', e.message); diff --git a/ts/session/utils/libsession/libsession_utils_convo_info_volatile.ts b/ts/session/utils/libsession/libsession_utils_convo_info_volatile.ts index 2ee2ecd4d..269fc1336 100644 --- a/ts/session/utils/libsession/libsession_utils_convo_info_volatile.ts +++ b/ts/session/utils/libsession/libsession_utils_convo_info_volatile.ts @@ -56,8 +56,8 @@ function getConvoType(convo: ConversationModel): ConvoVolatileType { SessionUtilUserProfile.isUserProfileToStoreInWrapper(convo.id) ? '1o1' : SessionUtilUserGroups.isCommunityToStoreInWrapper(convo) - ? 'Community' - : 'LegacyGroup'; + ? 'Community' + : 'LegacyGroup'; return convoType; } diff --git a/ts/session/utils/libsession/libsession_utils_user_groups.ts b/ts/session/utils/libsession/libsession_utils_user_groups.ts index 3b67d81a4..52f553c39 100644 --- a/ts/session/utils/libsession/libsession_utils_user_groups.ts +++ b/ts/session/utils/libsession/libsession_utils_user_groups.ts @@ -82,8 +82,8 @@ async function insertGroupsFromDBIntoWrapperAndRefresh( const convoType: UserGroupsType = SessionUtilUserGroups.isCommunityToStoreInWrapper(foundConvo) ? 'Community' : PubKey.is03Pubkey(convoId) - ? 'Group' - : 'LegacyGroup'; + ? 'Group' + : 'LegacyGroup'; switch (convoType) { case 'Community': diff --git a/ts/state/ducks/search.ts b/ts/state/ducks/search.ts index b868d6c82..379bdd927 100644 --- a/ts/state/ducks/search.ts +++ b/ts/state/ducks/search.ts @@ -70,6 +70,7 @@ export function search(query: string): SearchResultsKickoffActionType { async function doSearch(query: string): Promise { const options: SearchOptions = { noteToSelf: window.i18n('noteToSelf').toLowerCase(), + savedMessages: window.i18n('savedMessages').toLowerCase(), ourNumber: UserUtils.getOurPubKeyStrFromCache(), }; const advancedSearchOptions = getAdvancedSearchOptionsFromQuery(query); @@ -200,7 +201,7 @@ async function queryMessages(query: string): Promise> } async function queryConversationsAndContacts(providedQuery: string, options: SearchOptions) { - const { ourNumber, noteToSelf } = options; + const { ourNumber, noteToSelf, savedMessages } = options; const query = providedQuery.replace(/[+-.()]*/g, ''); const searchResults: Array = await Data.searchConversations(query); @@ -224,9 +225,11 @@ async function queryConversationsAndContacts(providedQuery: string, options: Sea conversations.push(conversation.id); } } + + const queryLowered = providedQuery.toLowerCase(); // Inject synthetic Note to Self entry if query matches localized 'Note to Self' - if (noteToSelf.indexOf(providedQuery.toLowerCase()) !== -1) { - // ensure that we don't have duplicates in our results + if (noteToSelf.includes(queryLowered) || savedMessages.includes(queryLowered)) { + // Ensure that we don't have duplicates in our results contacts = contacts.filter(id => id !== ourNumber); conversations = conversations.filter(id => id !== ourNumber); diff --git a/ts/state/ducks/settings.tsx b/ts/state/ducks/settings.tsx index e8de3e436..318e7fc23 100644 --- a/ts/state/ducks/settings.tsx +++ b/ts/state/ducks/settings.tsx @@ -8,6 +8,8 @@ const SettingsBoolsKeyTrackedInRedux = [ SettingsKey.someDeviceOutdatedSyncing, SettingsKey.settingsLinkPreview, SettingsKey.hasBlindedMsgRequestsEnabled, + SettingsKey.hasFollowSystemThemeEnabled, + SettingsKey.hasShiftSendEnabled, ] as const; export type SettingsState = { @@ -20,6 +22,8 @@ export function getSettingsInitialState() { someDeviceOutdatedSyncing: false, 'link-preview-setting': false, // this is the value of SettingsKey.settingsLinkPreview hasBlindedMsgRequestsEnabled: false, + hasFollowSystemThemeEnabled: false, + hasShiftSendEnabled: false, }, }; } @@ -47,6 +51,11 @@ const settingsSlice = createSlice({ SettingsKey.hasBlindedMsgRequestsEnabled, false ); + const hasFollowSystemThemeEnabled = Storage.get( + SettingsKey.hasFollowSystemThemeEnabled, + false + ); + const hasShiftSendEnabled = Storage.get(SettingsKey.hasShiftSendEnabled, false); state.settingsBools.someDeviceOutdatedSyncing = isBoolean(outdatedSync) ? outdatedSync : false; @@ -54,6 +63,15 @@ const settingsSlice = createSlice({ state.settingsBools.hasBlindedMsgRequestsEnabled = isBoolean(hasBlindedMsgRequestsEnabled) ? hasBlindedMsgRequestsEnabled : false; + + state.settingsBools.hasFollowSystemThemeEnabled = isBoolean(hasFollowSystemThemeEnabled) + ? hasFollowSystemThemeEnabled + : false; + + state.settingsBools.hasShiftSendEnabled = isBoolean(hasShiftSendEnabled) + ? hasShiftSendEnabled + : false; + return state; }, updateSettingsBoolValue(state, action: PayloadAction<{ id: string; value: boolean }>) { diff --git a/ts/state/selectors/settings.ts b/ts/state/selectors/settings.ts index eb70ef9b1..ff0ccb3ec 100644 --- a/ts/state/selectors/settings.ts +++ b/ts/state/selectors/settings.ts @@ -11,6 +11,12 @@ const getHasDeviceOutdatedSyncing = (state: StateType) => const getHasBlindedMsgRequestsEnabled = (state: StateType) => state.settings.settingsBools[SettingsKey.hasBlindedMsgRequestsEnabled]; +const getHasFollowSystemThemeEnabled = (state: StateType) => + state.settings.settingsBools[SettingsKey.hasFollowSystemThemeEnabled]; + +const getHasShiftSendEnabled = (state: StateType) => + state.settings.settingsBools[SettingsKey.hasShiftSendEnabled]; + export const useHasLinkPreviewEnabled = () => { const value = useSelector(getLinkPreviewEnabled); return Boolean(value); @@ -25,3 +31,14 @@ export const useHasBlindedMsgRequestsEnabled = () => { const value = useSelector(getHasBlindedMsgRequestsEnabled); return Boolean(value); }; + +export const useHasFollowSystemThemeEnabled = () => { + const value = useSelector(getHasFollowSystemThemeEnabled); + return Boolean(value); +}; + +export const useHasEnterSendEnabled = () => { + const value = useSelector(getHasShiftSendEnabled); + + return Boolean(value); +}; diff --git a/ts/state/selectors/theme.ts b/ts/state/selectors/theme.ts index 1cfdcc7fc..b1878f359 100644 --- a/ts/state/selectors/theme.ts +++ b/ts/state/selectors/theme.ts @@ -1,8 +1,9 @@ import { ThemeStateType } from '../../themes/constants/colors'; import { StateType } from '../reducer'; +import { checkDarkTheme, checkLightTheme } from '../../util/theme'; export const getTheme = (state: StateType): ThemeStateType => state.theme; -export const isDarkTheme = (state: StateType): boolean => state.theme.includes('dark'); +export const isDarkTheme = (state: StateType): boolean => checkDarkTheme(state.theme); -export const isLightTheme = (state: StateType): boolean => state.theme.includes('light'); +export const isLightTheme = (state: StateType): boolean => checkLightTheme(state.theme); diff --git a/ts/themes/SessionTheme.tsx b/ts/themes/SessionTheme.tsx index d06f553e2..3337df271 100644 --- a/ts/themes/SessionTheme.tsx +++ b/ts/themes/SessionTheme.tsx @@ -1,7 +1,9 @@ +import { ipcRenderer } from 'electron'; import React from 'react'; - import { createGlobalStyle } from 'styled-components'; +import { switchThemeTo } from './switchTheme'; import { classicDark } from './classicDark'; +import { getOppositeTheme, isThemeMismatched } from '../util/theme'; import { declareCSSVariables, THEME_GLOBALS } from './globals'; // Defaults to Classic Dark theme @@ -18,3 +20,26 @@ export const SessionTheme = ({ children }: { children: any }) => ( {children} ); + +export async function ensureThemeConsistency(): Promise { + const theme = window.Events.getThemeSetting(); + + return new Promise(resolve => { + ipcRenderer.send('get-native-theme'); + ipcRenderer.once('send-native-theme', (_, shouldUseDarkColors) => { + const isMismatchedTheme = isThemeMismatched(theme, shouldUseDarkColors); + if (isMismatchedTheme) { + const newTheme = getOppositeTheme(theme); + void switchThemeTo({ + theme: newTheme, + mainWindow: true, + usePrimaryColor: true, + dispatch: window?.inboxStore?.dispatch, + }); + resolve(true); // Theme was switched + } else { + resolve(false); // Theme was not switched + } + }); + }); +} diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index 1707ea03c..6fc59115e 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -178,9 +178,13 @@ export type LocalizerKeys = | 'endCall' | 'enterAnOpenGroupURL' | 'enterDisplayName' + | 'enterKeySettingDescription' + | 'enterKeySettingTitle' + | 'enterNewLineDescription' | 'enterNewPassword' | 'enterPassword' | 'enterRecoveryPhrase' + | 'enterSendNewMessageDescription' | 'enterSessionID' | 'enterSessionIDOfRecipient' | 'enterSessionIDOrONSName' @@ -276,6 +280,8 @@ export type LocalizerKeys = | 'mainMenuWindow' | 'markAllAsRead' | 'markUnread' + | 'matchThemeSystemSettingDescription' + | 'matchThemeSystemSettingTitle' | 'maxPasswordAttempts' | 'maximumAttachments' | 'media' @@ -422,6 +428,7 @@ export type LocalizerKeys = | 'save' | 'saveLogToDesktop' | 'saved' + | 'savedMessages' | 'savedTheFile' | 'searchFor...' | 'searchForContactsOnly' diff --git a/ts/types/Search.ts b/ts/types/Search.ts index 282142267..0206be8a3 100644 --- a/ts/types/Search.ts +++ b/ts/types/Search.ts @@ -1,6 +1,7 @@ export type SearchOptions = { ourNumber: string; noteToSelf: string; + savedMessages: string; }; export type AdvancedSearchOptions = { diff --git a/ts/util/theme.ts b/ts/util/theme.ts new file mode 100644 index 000000000..e84baf84e --- /dev/null +++ b/ts/util/theme.ts @@ -0,0 +1,21 @@ +import { ThemeStateType } from '../themes/constants/colors'; + +export const checkDarkTheme = (theme: ThemeStateType): boolean => theme.includes('dark'); +export const checkLightTheme = (theme: ThemeStateType): boolean => theme.includes('light'); + +export function getOppositeTheme(themeName: ThemeStateType): ThemeStateType { + if (checkDarkTheme(themeName)) { + return themeName.replace('dark', 'light') as ThemeStateType; + } + if (checkLightTheme(themeName)) { + return themeName.replace('light', 'dark') as ThemeStateType; + } + // If neither 'dark' nor 'light' is in the theme name, return the original theme name. + return themeName as ThemeStateType; +} + +export function isThemeMismatched(themeName: ThemeStateType, prefersDark: boolean): boolean { + const systemLightTheme = checkLightTheme(themeName); + const systemDarkTheme = checkDarkTheme(themeName); + return (prefersDark && systemLightTheme) || (!prefersDark && systemDarkTheme); +} diff --git a/ts/webworker/workers/node/libsession/libsession.worker.ts b/ts/webworker/workers/node/libsession/libsession.worker.ts index 59f2cdda9..214b3d4ce 100644 --- a/ts/webworker/workers/node/libsession/libsession.worker.ts +++ b/ts/webworker/workers/node/libsession/libsession.worker.ts @@ -230,8 +230,8 @@ onmessage = async (e: { data: [number, ConfigWrapperObjectTypesMeta, string, ... const wrapper = isUserConfigWrapperType(config) ? getCorrespondingUserWrapper(config) : isMetaWrapperType(config) - ? getCorrespondingGroupWrapper(config) - : undefined; + ? getCorrespondingGroupWrapper(config) + : undefined; if (!wrapper) { throw new Error(`did not find an already built wrapper for config: "${config}"`); }