From 2fe29ca30e55b26f9b9226731a15c1a950d4d65d Mon Sep 17 00:00:00 2001 From: Kee Jefferys Date: Fri, 13 Oct 2023 18:08:55 +1100 Subject: [PATCH] feat: implement setting to follow system theme feat: check theme congruence on startup and on native theme update fix: make toggle and startup following work fix: should return here, but this breaks things --- _locales/en/messages.json | 2 + ts/components/leftpane/ActionsPanel.tsx | 23 +++++++---- .../settings/section/CategoryAppearance.tsx | 16 +++++++- ts/data/settings-key.ts | 2 + ts/mains/main_node.ts | 10 +++++ ts/mains/main_renderer.tsx | 25 +++++++++++- ts/state/ducks/settings.tsx | 9 +++++ ts/state/selectors/settings.ts | 8 ++++ ts/themes/SessionTheme.tsx | 40 ++++++++++++++++++- ts/types/LocalizerKeys.ts | 2 + 10 files changed, 127 insertions(+), 10 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 09a67aa7b..8862edd2f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -222,6 +222,8 @@ "noteToSelf": "Note to Self", "hideMenuBarTitle": "Hide Menu Bar", "hideMenuBarDescription": "Toggle system menu bar visibility.", + "matchThemeSystemSettingTitle": "Auto night-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", diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index f94fc50f1..188056574 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -43,6 +43,7 @@ import { getFreshSwarmFor, } from '../../session/apis/snode_api/snodePool'; import { isDarkTheme } from '../../state/selectors/theme'; +import { getOppositeTheme, checkThemeCongruency } from '../../themes/SessionTheme'; import { ThemeStateType } from '../../themes/constants/colors'; import { switchThemeTo } from '../../themes/switchTheme'; import { ConfigurationSync } from '../../session/utils/job_runners/jobs/ConfigurationSyncJob'; @@ -62,10 +63,7 @@ const Section = (props: { type: SectionType }) => { 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 newTheme = getOppositeTheme(currentTheme) as ThemeStateType; // We want to persist the primary color when using the color mode button void switchThemeTo({ theme: newTheme, @@ -149,14 +147,25 @@ 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) { + const themeIsCongruent = await checkThemeCongruency(); + // Only set theme if it matches with native theme, otherwise handled by checkThemeCongruency() + if (!themeIsCongruent) { + 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/settings/section/CategoryAppearance.tsx b/ts/components/settings/section/CategoryAppearance.tsx index 8ad4400fc..fa057947a 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 { checkThemeCongruency } 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,18 @@ export const SettingsCategoryAppearance = (props: { hasPassword: boolean | null active={isHideMenuBarActive} /> )} + { + const toggledValue = !isFollowSystemThemeEnabled; + void window.setSettingValue(SettingsKey.hasFollowSystemThemeEnabled, toggledValue); + if (!isFollowSystemThemeEnabled) { + void checkThemeCongruency(); + } + }} + title={window.i18n('matchThemeSystemSettingTitle')} + description={window.i18n('matchThemeSystemSettingDescription')} + active={isFollowSystemThemeEnabled} + /> ); } diff --git a/ts/data/settings-key.ts b/ts/data/settings-key.ts index 345e6b081..d48024014 100644 --- a/ts/data/settings-key.ts +++ b/ts/data/settings-key.ts @@ -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'; @@ -39,6 +40,7 @@ export const SettingsKey = { latestUserProfileEnvelopeTimestamp, latestUserGroupEnvelopeTimestamp, latestUserContactsEnvelopeTimestamp, + hasFollowSystemThemeEnabled, } as const; export const KNOWN_BLINDED_KEYS_ITEM = 'KNOWN_BLINDED_KEYS_ITEM'; diff --git a/ts/mains/main_node.ts b/ts/mains/main_node.ts index e8c5cc966..09aff3747 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, @@ -1117,6 +1118,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 ba2a7b516..80658b431 100644 --- a/ts/mains/main_renderer.tsx +++ b/ts/mains/main_renderer.tsx @@ -1,7 +1,7 @@ +import { ipcRenderer } from 'electron'; import _ from 'lodash'; import ReactDOM from 'react-dom'; import Backbone from 'backbone'; - import React from 'react'; import nativeEmojiData from '@emoji-mart/data'; @@ -27,6 +27,9 @@ import { switchPrimaryColorTo } from '../themes/switchPrimaryColor'; import { LibSessionUtil } from '../session/utils/libsession/libsession_utils'; import { runners } from '../session/utils/job_runners/JobRunner'; import { SettingsKey } from '../data/settings-key'; +import { getOppositeTheme } from '../themes/SessionTheme'; +import { switchThemeTo } from '../themes/switchTheme'; +import { ThemeStateType } from '../themes/constants/colors'; // Globally disable drag and drop document.body.addEventListener( @@ -109,6 +112,26 @@ 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 ( + (shouldUseDarkColors && theme.includes('light')) || + (!shouldUseDarkColors && theme.includes('dark')) + ) { + const newTheme = getOppositeTheme(theme) as ThemeStateType; + void switchThemeTo({ + theme: newTheme, + mainWindow: true, + usePrimaryColor: true, + dispatch: window?.inboxStore?.dispatch || undefined, + }); + } + } +}); async function startJobRunners() { // start the job runners diff --git a/ts/state/ducks/settings.tsx b/ts/state/ducks/settings.tsx index 43868f4c1..16efb2041 100644 --- a/ts/state/ducks/settings.tsx +++ b/ts/state/ducks/settings.tsx @@ -8,6 +8,7 @@ const SettingsBoolsKeyTrackedInRedux = [ SettingsKey.someDeviceOutdatedSyncing, SettingsKey.settingsLinkPreview, SettingsKey.hasBlindedMsgRequestsEnabled, + SettingsKey.hasFollowSystemThemeEnabled, ] as const; export type SettingsState = { @@ -20,6 +21,7 @@ export function getSettingsInitialState() { someDeviceOutdatedSyncing: false, 'link-preview-setting': false, // this is the value of SettingsKey.settingsLinkPreview hasBlindedMsgRequestsEnabled: false, + hasFollowSystemThemeEnabled: false, }, }; } @@ -47,6 +49,10 @@ const settingsSlice = createSlice({ SettingsKey.hasBlindedMsgRequestsEnabled, false ); + const hasFollowSystemThemeEnabled = Storage.get( + SettingsKey.hasFollowSystemThemeEnabled, + false + ); state.settingsBools.someDeviceOutdatedSyncing = isBoolean(outdatedSync) ? outdatedSync : false; @@ -54,6 +60,9 @@ const settingsSlice = createSlice({ state.settingsBools.hasBlindedMsgRequestsEnabled = isBoolean(hasBlindedMsgRequestsEnabled) ? hasBlindedMsgRequestsEnabled : false; + state.settingsBools.hasFollowSystemThemeEnabled = isBoolean(hasFollowSystemThemeEnabled) + ? hasFollowSystemThemeEnabled + : 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..7c3614224 100644 --- a/ts/state/selectors/settings.ts +++ b/ts/state/selectors/settings.ts @@ -11,6 +11,9 @@ const getHasDeviceOutdatedSyncing = (state: StateType) => const getHasBlindedMsgRequestsEnabled = (state: StateType) => state.settings.settingsBools[SettingsKey.hasBlindedMsgRequestsEnabled]; +const getHasFollowSystemThemeEnabled = (state: StateType) => + state.settings.settingsBools[SettingsKey.hasFollowSystemThemeEnabled]; + export const useHasLinkPreviewEnabled = () => { const value = useSelector(getLinkPreviewEnabled); return Boolean(value); @@ -25,3 +28,8 @@ export const useHasBlindedMsgRequestsEnabled = () => { const value = useSelector(getHasBlindedMsgRequestsEnabled); return Boolean(value); }; + +export const useHasFollowSystemThemeEnabled = () => { + const value = useSelector(getHasFollowSystemThemeEnabled); + return Boolean(value); +}; diff --git a/ts/themes/SessionTheme.tsx b/ts/themes/SessionTheme.tsx index d06f553e2..fda5eb1f2 100644 --- a/ts/themes/SessionTheme.tsx +++ b/ts/themes/SessionTheme.tsx @@ -1,6 +1,8 @@ +import { ipcRenderer } from 'electron'; import React from 'react'; - import { createGlobalStyle } from 'styled-components'; +import { switchThemeTo } from './switchTheme'; +import { ThemeStateType } from './constants/colors'; import { classicDark } from './classicDark'; import { declareCSSVariables, THEME_GLOBALS } from './globals'; @@ -18,3 +20,39 @@ export const SessionTheme = ({ children }: { children: any }) => ( {children} ); + +export function getOppositeTheme(themeName: string) { + if (themeName.includes('dark')) { + return themeName.replace('dark', 'light'); + } + if (themeName.includes('light')) { + return themeName.replace('light', 'dark'); + } + // If neither 'dark' nor 'light' is in the theme name, return the original theme name. + return themeName; +} + +export async function checkThemeCongruency(): Promise { + const theme = window.Events.getThemeSetting(); + + return new Promise(resolve => { + ipcRenderer.send('get-native-theme'); + ipcRenderer.once('send-native-theme', (_, shouldUseDarkColors) => { + const isMismatchedTheme = + (shouldUseDarkColors && theme.includes('light')) || + (!shouldUseDarkColors && theme.includes('dark')); + if (isMismatchedTheme) { + const newTheme = getOppositeTheme(theme) as ThemeStateType; + void switchThemeTo({ + theme: newTheme, + mainWindow: true, + usePrimaryColor: true, + dispatch: window?.inboxStore?.dispatch || undefined, + }); + 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 ed5d4ad7f..1edfb83d7 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -241,6 +241,8 @@ export type LocalizerKeys = | 'mainMenuWindow' | 'markAllAsRead' | 'markUnread' + | 'matchThemeSystemSettingDescription' + | 'matchThemeSystemSettingTitle' | 'maxPasswordAttempts' | 'maximumAttachments' | 'media'