Merge pull request #2957 from KeeJef/follow-system-theme

feat: Add setting to follow system theming
This commit is contained in:
Audric Ackermann 2023-11-14 15:25:15 +11:00 committed by GitHub
commit 173d01c4e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 150 additions and 22 deletions

View File

@ -223,6 +223,8 @@
"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",

View File

@ -834,11 +834,11 @@
color: var(--white-color);
font-size: 20px;
font-weight: normal;
letter-spacing: 0;
text-align: center;
background-color: rgba(0, 0, 0, 0.7);
font-size: 20px;
font-weight: normal;
letter-spacing: 0;
text-align: center;
background-color: rgba(0, 0, 0, 0.7);
}
.module-image__close-button {

View File

@ -43,7 +43,8 @@ import {
getFreshSwarmFor,
} from '../../session/apis/snode_api/snodePool';
import { isDarkTheme } from '../../state/selectors/theme';
import { ThemeStateType } from '../../themes/constants/colors';
import { ensureThemeConsistency } from '../../themes/SessionTheme';
import { getOppositeTheme } from '../../util/theme';
import { switchThemeTo } from '../../themes/switchTheme';
import { ConfigurationSync } from '../../session/utils/job_runners/jobs/ConfigurationSyncJob';
@ -61,11 +62,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,
@ -149,14 +147,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

View File

@ -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}
/>
)}
<SessionToggleWithDescription
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onClickToggle={async () => {
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"
/>
</>
);
}

View File

@ -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';
@ -40,6 +41,7 @@ export const SettingsKey = {
latestUserProfileEnvelopeTimestamp,
latestUserGroupEnvelopeTimestamp,
latestUserContactsEnvelopeTimestamp,
hasFollowSystemThemeEnabled,
} as const;
export const KNOWN_BLINDED_KEYS_ITEM = 'KNOWN_BLINDED_KEYS_ITEM';

View File

@ -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) => {

View File

@ -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,8 @@ 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, isThemeMismatched } from '../util/theme';
import { switchThemeTo } from '../themes/switchTheme';
// Globally disable drag and drop
document.body.addEventListener(
@ -109,6 +111,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

View File

@ -8,6 +8,7 @@ const SettingsBoolsKeyTrackedInRedux = [
SettingsKey.someDeviceOutdatedSyncing,
SettingsKey.settingsLinkPreview,
SettingsKey.hasBlindedMsgRequestsEnabled,
SettingsKey.hasFollowSystemThemeEnabled,
SettingsKey.hasShiftSendEnabled,
] as const;
@ -21,6 +22,7 @@ export function getSettingsInitialState() {
someDeviceOutdatedSyncing: false,
'link-preview-setting': false, // this is the value of SettingsKey.settingsLinkPreview
hasBlindedMsgRequestsEnabled: false,
hasFollowSystemThemeEnabled: false,
hasShiftSendEnabled: false,
},
};
@ -49,8 +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;
@ -58,9 +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 }>) {

View File

@ -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];
const getHasShiftSendEnabled = (state: StateType) =>
state.settings.settingsBools[SettingsKey.hasShiftSendEnabled];
@ -29,7 +32,13 @@ export const useHasBlindedMsgRequestsEnabled = () => {
return Boolean(value);
};
export const useHasEnterSendEnabled = () => {
const value = useSelector(getHasShiftSendEnabled);
export const useHasFollowSystemThemeEnabled = () => {
const value = useSelector(getHasFollowSystemThemeEnabled);
return Boolean(value);
};
export const useHasEnterSendEnabled = () => {
const value = useSelector(getHasShiftSendEnabled);
return Boolean(value);
};

View File

@ -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);

View File

@ -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<boolean> {
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
}
});
});
}

View File

@ -245,6 +245,8 @@ export type LocalizerKeys =
| 'mainMenuWindow'
| 'markAllAsRead'
| 'markUnread'
| 'matchThemeSystemSettingDescription'
| 'matchThemeSystemSettingTitle'
| 'maxPasswordAttempts'
| 'maximumAttachments'
| 'media'

21
ts/util/theme.ts Normal file
View File

@ -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);
}