diff --git a/app/main.ts b/app/main.ts index 2bed7f977db..cc0726820de 100644 --- a/app/main.ts +++ b/app/main.ts @@ -19,6 +19,7 @@ import { dialog, ipcMain as ipc, Menu, + nativeTheme, powerSaveBlocker, protocol as electronProtocol, screen, @@ -33,8 +34,11 @@ import * as GlobalErrors from './global_errors'; import { setup as setupCrashReports } from './crashReports'; import { setup as setupSpellChecker } from './spell_check'; import { redactAll, addSensitivePath } from '../ts/util/privacy'; +import { missingCaseError } from '../ts/util/missingCaseError'; import { strictAssert } from '../ts/util/assert'; import { consoleLogger } from '../ts/util/consoleLogger'; +import type { ThemeSettingType } from '../ts/types/Storage.d'; +import { ThemeType } from '../ts/types/Util'; import './startup_config'; @@ -230,6 +234,61 @@ async function getSpellCheckSetting() { return slowValue; } +type GetThemeSettingOptionsType = Readonly<{ + ephemeralOnly?: boolean; +}>; + +async function getThemeSetting({ + ephemeralOnly = false, +}: GetThemeSettingOptionsType = {}): Promise { + const fastValue = ephemeralConfig.get('theme-setting'); + if (fastValue !== undefined) { + getLogger().info('got fast theme-setting value', fastValue); + return fastValue as ThemeSettingType; + } + + if (ephemeralOnly) { + return 'system'; + } + + const json = await sql.sqlCall('getItemById', ['theme-setting']); + + // Default to `system` if setting doesn't exist yet + const slowValue = json ? json.value : 'system'; + + ephemeralConfig.set('theme-setting', slowValue); + + getLogger().info('got slow theme-setting value', slowValue); + + return slowValue; +} + +async function getResolvedThemeSetting( + options?: GetThemeSettingOptionsType +): Promise { + const theme = await getThemeSetting(options); + if (theme === 'system') { + return nativeTheme.shouldUseDarkColors ? ThemeType.dark : ThemeType.light; + } + return ThemeType[theme]; +} + +async function getBackgroundColor( + options?: GetThemeSettingOptionsType +): Promise { + const theme = await getResolvedThemeSetting(options); + + if (theme === 'light') { + return '#3a76f0'; + } + + if (theme === 'dark') { + return '#121212'; + } + + throw missingCaseError(theme); +} + let systemTrayService: SystemTrayService | undefined; const systemTraySettingCache = new SystemTraySettingCache( sql, @@ -479,7 +538,7 @@ async function createWindow() { : 'default', backgroundColor: isTestEnvironment(getEnvironment()) ? '#ffffff' // Tests should always be rendered on a white background - : '#3a76f0', + : await getBackgroundColor(), webPreferences: { ...defaultWebPrefs, nodeIntegration: false, @@ -599,6 +658,7 @@ async function createWindow() { const moreKeys = { isFullScreen: String(Boolean(mainWindow.isFullScreen())), + resolvedTheme: await getResolvedThemeSetting(), }; if (getEnvironment() === Environment.Test) { @@ -996,7 +1056,7 @@ function showScreenShareWindow(sourceName: string) { } let aboutWindow: BrowserWindow | undefined; -function showAbout() { +async function showAbout() { if (aboutWindow) { aboutWindow.show(); return; @@ -1008,7 +1068,7 @@ function showAbout() { resizable: false, title: getLocale().i18n('aboutSignalDesktop'), autoHideMenuBar: true, - backgroundColor: '#3a76f0', + backgroundColor: await getBackgroundColor(), show: false, webPreferences: { ...defaultWebPrefs, @@ -1038,7 +1098,7 @@ function showAbout() { } let settingsWindow: BrowserWindow | undefined; -function showSettingsWindow() { +async function showSettingsWindow() { if (settingsWindow) { settingsWindow.show(); return; @@ -1051,7 +1111,7 @@ function showSettingsWindow() { resizable: false, title: getLocale().i18n('signalDesktopPreferences'), autoHideMenuBar: true, - backgroundColor: '#3a76f0', + backgroundColor: await getBackgroundColor(), show: false, webPreferences: { ...defaultWebPrefs, @@ -1121,7 +1181,7 @@ async function showStickerCreator() { height: 650, title: getLocale().i18n('signalDesktopStickerCreator'), autoHideMenuBar: true, - backgroundColor: '#3a76f0', + backgroundColor: await getBackgroundColor(), show: false, webPreferences: { ...defaultWebPrefs, @@ -1172,16 +1232,14 @@ async function showDebugLogWindow() { return; } - const theme = settingsChannel - ? await settingsChannel.getSettingFromMainWindow('themeSetting') - : undefined; + const theme = await getThemeSetting(); const options = { width: 700, height: 500, resizable: false, title: getLocale().i18n('debugLog'), autoHideMenuBar: true, - backgroundColor: '#3a76f0', + backgroundColor: await getBackgroundColor(), show: false, webPreferences: { ...defaultWebPrefs, @@ -1235,9 +1293,7 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) { return; } - const theme = settingsChannel - ? await settingsChannel.getSettingFromMainWindow('themeSetting') - : undefined; + const theme = await getThemeSetting(); const size = mainWindow.getSize(); const options = { width: Math.min(400, size[0]), @@ -1245,7 +1301,7 @@ function showPermissionsPopupWindow(forCalling: boolean, forCamera: boolean) { resizable: false, title: getLocale().i18n('allowAccess'), autoHideMenuBar: true, - backgroundColor: '#3a76f0', + backgroundColor: await getBackgroundColor(), show: false, modal: true, webPreferences: { @@ -1509,6 +1565,12 @@ app.on('ready', async () => { const timeout = new Promise(resolveFn => setTimeout(resolveFn, 3000, 'timeout') ); + + // This color is to be used only in loading screen and in this case we should + // never wait for the database to be initialized. Thus the theme setting + // lookup should be done only in ephemeral config. + const backgroundColor = await getBackgroundColor({ ephemeralOnly: true }); + // eslint-disable-next-line more/no-then Promise.race([sqlInitPromise, timeout]).then(maybeTimeout => { if (maybeTimeout !== 'timeout') { @@ -1525,7 +1587,7 @@ app.on('ready', async () => { height: 265, resizable: false, frame: false, - backgroundColor: '#3a76f0', + backgroundColor, webPreferences: { ...defaultWebPrefs, nodeIntegration: false, diff --git a/preload.js b/preload.js index 6e0784aadb0..32c88d4676f 100644 --- a/preload.js +++ b/preload.js @@ -15,6 +15,7 @@ try { const { strictAssert } = require('./ts/util/assert'); const { parseIntWithFallback } = require('./ts/util/parseIntWithFallback'); const { UUIDKind } = require('./ts/types/UUID'); + const { ThemeType } = require('./ts/types/Util'); // It is important to call this as early as possible const { SignalContext } = require('./ts/windows/context'); @@ -247,6 +248,12 @@ try { }); } + if (config.resolvedTheme === 'light') { + window.initialTheme = ThemeType.light; + } else if (config.resolvedTheme === 'dark') { + window.initialTheme = ThemeType.dark; + } + // Settings-related events window.showSettings = () => ipc.send('show-settings'); diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 9665d2af017..e709364f773 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -235,7 +235,16 @@ $loading-height: 16px; right: 0; top: 0; bottom: 0; - background-color: $color-ultramarine-icon; + + /* Note: background-color is intentionally transparent until body has the + * theme class. + */ + @include explicit-light-theme { + background-color: $color-ultramarine-icon; + } + @include dark-theme { + background-color: $color-gray-95; + } color: $color-white; display: flex; flex-direction: column; diff --git a/stylesheets/_mixins.scss b/stylesheets/_mixins.scss index 16af1688898..5a87e596003 100644 --- a/stylesheets/_mixins.scss +++ b/stylesheets/_mixins.scss @@ -97,6 +97,12 @@ @content; } +@mixin explicit-light-theme() { + .light-theme & { + @content; + } +} + @mixin dark-theme() { .dark-theme & { @content; diff --git a/ts/background.ts b/ts/background.ts index 95979424203..d81ec041ad9 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -29,6 +29,7 @@ import type { Receipt } from './types/Receipt'; import { getTitleBarVisibility, TitleBarVisibility } from './types/Settings'; import { SocketStatus } from './types/SocketStatus'; import { DEFAULT_CONVERSATION_COLOR } from './types/Colors'; +import { ThemeType } from './types/Util'; import { ChallengeHandler } from './challenge'; import * as durations from './util/durations'; import { explodePromise } from './util/explodePromise'; @@ -166,6 +167,13 @@ export async function cleanupSessionResets(): Promise { } export async function startApp(): Promise { + if (window.initialTheme === ThemeType.light) { + document.body.classList.add('light-theme'); + } + if (window.initialTheme === ThemeType.dark) { + document.body.classList.add('dark-theme'); + } + const idleDetector = new IdleDetector(); await KeyboardLayout.initialize(); diff --git a/ts/main/settingsChannel.ts b/ts/main/settingsChannel.ts index e9cc1bffba5..bb06746f52b 100644 --- a/ts/main/settingsChannel.ts +++ b/ts/main/settingsChannel.ts @@ -17,6 +17,7 @@ import type { const EPHEMERAL_NAME_MAP = new Map([ ['spellCheck', 'spell-check'], ['systemTraySetting', 'system-tray-setting'], + ['themeSetting', 'theme-setting'], ]); type ResponseQueueEntry = Readonly<{ @@ -68,7 +69,9 @@ export class SettingsChannel { this.installSetting('readReceiptSetting', { setter: false }); this.installSetting('typingIndicatorSetting', { setter: false }); - this.installSetting('themeSetting'); + this.installSetting('themeSetting', { + isEphemeral: true, + }); this.installSetting('hideMenuBar'); this.installSetting('systemTraySetting', { isEphemeral: true, diff --git a/ts/window.d.ts b/ts/window.d.ts index 874acc50169..abf6e4e14a9 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -29,7 +29,7 @@ import * as Curve from './Curve'; import * as RemoteConfig from './RemoteConfig'; import * as OS from './OS'; import { getEnvironment } from './environment'; -import { LocalizerType } from './types/Util'; +import { LocalizerType, ThemeType } from './types/Util'; import type { Receipt } from './types/Receipt'; import { ConversationController } from './ConversationController'; import { ReduxActions } from './state/types'; @@ -212,6 +212,7 @@ declare global { isAfterVersion: (version: string, anotherVersion: string) => boolean; isBeforeVersion: (version: string, anotherVersion: string) => boolean; isFullScreen: () => boolean; + initialTheme?: ThemeType; libphonenumber: { util: { getRegionCodeForNumber: (number: string) => string;