diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2e7f67311d9e..a58162586d68 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1625,6 +1625,10 @@ "message": "Dark", "description": "Label text for dark theme" }, + "themeSystem": { + "message": "System", + "description": "Label text for system theme" + }, "noteToSelf": { "message": "Note to Self", "description": "Name for the conversation with your own phone number" diff --git a/js/background.js b/js/background.js index def21856baa8..78c29039efda 100644 --- a/js/background.js +++ b/js/background.js @@ -208,6 +208,7 @@ switch (theme) { case 'dark': case 'light': + case 'system': return theme; case 'android-dark': return 'dark'; @@ -231,7 +232,11 @@ window.Events = { getDeviceName: () => textsecure.storage.user.getDeviceName(), - getThemeSetting: () => storage.get('theme-setting', 'light'), + getThemeSetting: () => + storage.get( + 'theme-setting', + window.platform === 'darwin' ? 'system' : 'light' + ), setThemeSetting: value => { storage.put('theme-setting', value); onChangeTheme(); @@ -326,6 +331,19 @@ `New version detected: ${currentVersion}; previous: ${lastVersion}` ); + const themeSetting = window.Events.getThemeSetting(); + const newThemeSetting = mapOldThemeToNew(themeSetting); + + if ( + window.isBeforeVersion(lastVersion, 'v1.25.0') && + window.platform === 'darwin' && + newThemeSetting === window.systemTheme + ) { + window.Events.setThemeSetting('system'); + } else { + window.Events.setThemeSetting(newThemeSetting); + } + if (window.isBeforeVersion(lastVersion, 'v1.25.0')) { // Stickers flags await Promise.all([ @@ -334,6 +352,7 @@ ]); } + // This one should always be last - it could restart the app if (window.isBeforeVersion(lastVersion, 'v1.15.0-beta.5')) { await window.Signal.Logs.deleteAll(); window.restart(); @@ -411,10 +430,6 @@ }; startSpellCheck(); - const themeSetting = window.Events.getThemeSetting(); - const newThemeSetting = mapOldThemeToNew(themeSetting); - window.Events.setThemeSetting(newThemeSetting); - try { await Promise.all([ ConversationController.load(), diff --git a/js/permissions_popup_start.js b/js/permissions_popup_start.js index f153a0bf96d3..668a934c964b 100644 --- a/js/permissions_popup_start.js +++ b/js/permissions_popup_start.js @@ -9,7 +9,23 @@ $(document).on('keyup', e => { }); const $body = $(document.body); -$body.addClass(`${window.theme}-theme`); + +async function applyTheme() { + 'use strict'; + + const theme = await window.getThemeSetting(); + $body.removeClass('light-theme'); + $body.removeClass('dark-theme'); + $body.addClass(`${theme === 'system' ? window.systemTheme : theme}-theme`); +} + +applyTheme(); + +window.subscribeToSystemThemeChange(() => { + 'use strict'; + + applyTheme(); +}); window.view = new Whisper.ConfirmationDialogView({ message: i18n('audioPermissionNeeded'), diff --git a/js/settings_start.js b/js/settings_start.js index eeb85a66ac10..789a81fa8e75 100644 --- a/js/settings_start.js +++ b/js/settings_start.js @@ -9,7 +9,23 @@ $(document).on('keyup', e => { }); const $body = $(document.body); -$body.addClass(`${window.theme}-theme`); + +async function applyTheme() { + 'use strict'; + + const theme = await window.getThemeSetting(); + $body.removeClass('light-theme'); + $body.removeClass('dark-theme'); + $body.addClass(`${theme === 'system' ? window.systemTheme : theme}-theme`); +} + +applyTheme(); + +window.subscribeToSystemThemeChange(() => { + 'use strict'; + + applyTheme(); +}); // eslint-disable-next-line strict const getInitialData = async () => ({ diff --git a/js/views/app_view.js b/js/views/app_view.js index 3fd906e98534..162f0b69f1a1 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -8,6 +8,14 @@ window.Whisper = window.Whisper || {}; + function resolveTheme() { + const theme = storage.get('theme-setting') || 'light'; + if (window.platform === 'darwin' && theme === 'system') { + return window.systemTheme; + } + return theme; + } + Whisper.AppView = Backbone.View.extend({ initialize() { this.inboxView = null; @@ -15,14 +23,18 @@ this.applyTheme(); this.applyHideMenu(); + + window.subscribeToSystemThemeChange(() => { + this.applyTheme(); + }); }, events: { 'click .openInstaller': 'openInstaller', // NetworkStatusView has this button openInbox: 'openInbox', }, applyTheme() { + const theme = resolveTheme(); const iOS = storage.get('userAgent') === 'OWI'; - const theme = storage.get('theme-setting') || 'light'; this.$el .removeClass('light-theme') .removeClass('dark-theme') diff --git a/js/views/settings_view.js b/js/views/settings_view.js index e17304a294d7..0b02a7fc835d 100644 --- a/js/views/settings_view.js +++ b/js/views/settings_view.js @@ -88,7 +88,9 @@ $(document.body) .removeClass('dark-theme') .removeClass('light-theme') - .addClass(`${theme}-theme`); + .addClass( + `${theme === 'system' ? window.systemTheme : theme}-theme` + ); window.setThemeSetting(theme); }, }); @@ -143,8 +145,10 @@ audioNotificationDescription: i18n('audioNotificationDescription'), isAudioNotificationSupported: Settings.isAudioNotificationSupported(), isHideMenuBarSupported: Settings.isHideMenuBarSupported(), + hasSystemTheme: window.platform === 'darwin', themeLight: i18n('themeLight'), themeDark: i18n('themeDark'), + themeSystem: i18n('themeSystem'), hideMenuBar: i18n('hideMenuBar'), clearDataHeader: i18n('clearDataHeader'), clearDataButton: i18n('clearDataButton'), diff --git a/main.js b/main.js index b1a06001e84c..ce4cd3829116 100644 --- a/main.js +++ b/main.js @@ -507,7 +507,6 @@ async function showSettingsWindow() { return; } - const theme = await pify(getDataFromMainWindow)('theme-setting'); const size = mainWindow.getSize(); const options = { width: Math.min(500, size[0]), @@ -532,7 +531,7 @@ async function showSettingsWindow() { captureClicks(settingsWindow); - settingsWindow.loadURL(prepareURL([__dirname, 'settings.html'], { theme })); + settingsWindow.loadURL(prepareURL([__dirname, 'settings.html'])); settingsWindow.on('closed', () => { removeDarkOverlay(); diff --git a/package.json b/package.json index f825c1829539..799b3beb23b3 100644 --- a/package.json +++ b/package.json @@ -181,6 +181,7 @@ "mac": { "artifactName": "${name}-mac-${version}.${ext}", "category": "public.app-category.social-networking", + "darkModeSupport": true, "icon": "build/icons/mac/icon.icns", "publish": [ { diff --git a/permissions_popup_preload.js b/permissions_popup_preload.js index 621a5fc7ce2b..bf8efc299bde 100644 --- a/permissions_popup_preload.js +++ b/permissions_popup_preload.js @@ -1,9 +1,11 @@ /* global window */ -const { ipcRenderer } = require('electron'); +const { ipcRenderer, remote } = require('electron'); const url = require('url'); const i18n = require('./js/modules/i18n'); +const { systemPreferences } = remote.require('electron'); + const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); @@ -12,6 +14,25 @@ window.getVersion = () => config.version; window.theme = config.theme; window.i18n = i18n.setup(locale, localeMessages); +function setSystemTheme() { + window.systemTheme = systemPreferences.isDarkMode() ? 'dark' : 'light'; +} + +setSystemTheme(); + +window.subscribeToSystemThemeChange = fn => { + if (!systemPreferences.subscribeNotification) { + return; + } + systemPreferences.subscribeNotification( + 'AppleInterfaceThemeChangedNotification', + () => { + setSystemTheme(); + fn(); + } + ); +}; + require('./js/logging'); window.closePermissionsPopup = () => @@ -19,6 +40,8 @@ window.closePermissionsPopup = () => window.getMediaPermissions = makeGetter('media-permissions'); window.setMediaPermissions = makeSetter('media-permissions'); +window.getThemeSetting = makeGetter('theme-setting'); +window.setThemeSetting = makeSetter('theme-setting'); function makeGetter(name) { return () => diff --git a/preload.js b/preload.js index 48a516565808..69a34b2d04a8 100644 --- a/preload.js +++ b/preload.js @@ -7,6 +7,7 @@ const semver = require('semver'); const { deferredToPromise } = require('./js/modules/deferred_to_promise'); const { app } = electron.remote; +const { systemPreferences } = electron.remote.require('electron'); window.PROTO_ROOT = 'protos'; const config = require('url').parse(window.location.toString(), true).query; @@ -31,6 +32,25 @@ window.getHostName = () => config.hostname; window.getServerTrustRoot = () => config.serverTrustRoot; window.isBehindProxy = () => Boolean(config.proxyUrl); +function setSystemTheme() { + window.systemTheme = systemPreferences.isDarkMode() ? 'dark' : 'light'; +} + +setSystemTheme(); + +window.subscribeToSystemThemeChange = fn => { + if (!systemPreferences.subscribeNotification) { + return; + } + systemPreferences.subscribeNotification( + 'AppleInterfaceThemeChangedNotification', + () => { + setSystemTheme(); + fn(); + } + ); +}; + window.isBeforeVersion = (toCheck, baseVersion) => { try { return semver.lt(toCheck, baseVersion); diff --git a/settings.html b/settings.html index bf14f2444012..184b117a211c 100644 --- a/settings.html +++ b/settings.html @@ -44,6 +44,12 @@