From fd367200792eb51fa658663db49e52e167af8c8d Mon Sep 17 00:00:00 2001 From: Ken Powers Date: Thu, 16 May 2019 15:32:38 -0700 Subject: [PATCH] Add "system" theme setting for MacOS --- _locales/en/messages.json | 4 ++++ js/background.js | 25 ++++++++++++++++---- js/permissions_popup_start.js | 18 +++++++++++++- js/settings_start.js | 18 +++++++++++++- js/views/app_view.js | 14 ++++++++++- js/views/settings_view.js | 6 ++++- main.js | 3 +-- package.json | 1 + permissions_popup_preload.js | 25 +++++++++++++++++++- preload.js | 20 ++++++++++++++++ settings.html | 6 +++++ settings_preload.js | 25 +++++++++++++++++++- ts/util/lint/exceptions.json | 44 +++++++++++++++++------------------ 13 files changed, 174 insertions(+), 35 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2e7f67311..a58162586 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 def21856b..78c29039e 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 f153a0bf9..668a934c9 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 eeb85a66a..789a81fa8 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 3fd906e98..162f0b69f 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 e17304a29..0b02a7fc8 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 b1a06001e..ce4cd3829 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 f825c1829..799b3beb2 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 621a5fc7c..bf8efc299 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 48a516565..69a34b2d0 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 bf14f2444..184b117a2 100644 --- a/settings.html +++ b/settings.html @@ -44,6 +44,12 @@

{{ theme }}

+ {{#hasSystemTheme}} +
+ + +
+ {{/hasSystemTheme}}
diff --git a/settings_preload.js b/settings_preload.js index 858a88c29..9018fa1ca 100644 --- a/settings_preload.js +++ b/settings_preload.js @@ -1,6 +1,7 @@ /* global window */ -const { ipcRenderer } = require('electron'); +const { ipcRenderer, remote } = require('electron'); + const url = require('url'); const i18n = require('./js/modules/i18n'); @@ -8,9 +9,31 @@ const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); +const { systemPreferences } = remote.require('electron'); + +window.platform = process.platform; 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(); + } + ); +}; + window.getEnvironment = () => config.environment; window.getVersion = () => config.version; window.getAppInstance = () => config.appInstance; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index 2e7cfe0c3..5a31a3798 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -257,7 +257,7 @@ "rule": "jQuery-appendTo(", "path": "js/permissions_popup_start.js", "line": "window.view.$el.appendTo($body);", - "lineNumber": 26, + "lineNumber": 42, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes" @@ -284,7 +284,7 @@ "rule": "jQuery-appendTo(", "path": "js/settings_start.js", "line": " window.view.$el.appendTo($body);", - "lineNumber": 41, + "lineNumber": 57, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes" @@ -317,7 +317,7 @@ "rule": "DOM-innerHTML", "path": "js/views/app_view.js", "line": " this.el.innerHTML = '';", - "lineNumber": 43, + "lineNumber": 55, "reasonCategory": "usageTrusted", "updated": "2018-09-15T00:38:04.183Z", "reasonDetail": "Hard-coded string" @@ -326,7 +326,7 @@ "rule": "jQuery-append(", "path": "js/views/app_view.js", "line": " this.el.append(view.el);", - "lineNumber": 44, + "lineNumber": 56, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes" @@ -335,7 +335,7 @@ "rule": "jQuery-appendTo(", "path": "js/views/app_view.js", "line": " this.debugLogView.$el.appendTo(this.el);", - "lineNumber": 50, + "lineNumber": 62, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes" @@ -1008,7 +1008,7 @@ "rule": "jQuery-$(", "path": "js/views/settings_view.js", "line": " el: this.$('.audio-notification-setting'),", - "lineNumber": 97, + "lineNumber": 99, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -1017,7 +1017,7 @@ "rule": "jQuery-$(", "path": "js/views/settings_view.js", "line": " el: this.$('.spell-check-setting'),", - "lineNumber": 104, + "lineNumber": 106, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -1026,7 +1026,7 @@ "rule": "jQuery-$(", "path": "js/views/settings_view.js", "line": " el: this.$('.menu-bar-setting'),", - "lineNumber": 111, + "lineNumber": 113, "reasonCategory": "usageTrusted", "updated": "2019-04-08T18:24:35.255Z", "reasonDetail": "Protected from arbitrary input" @@ -1035,7 +1035,7 @@ "rule": "jQuery-$(", "path": "js/views/settings_view.js", "line": " el: this.$('.media-permissions'),", - "lineNumber": 118, + "lineNumber": 120, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -1044,7 +1044,7 @@ "rule": "jQuery-$(", "path": "js/views/settings_view.js", "line": " this.$('.sync-setting').append(syncView.el);", - "lineNumber": 124, + "lineNumber": 126, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -1053,7 +1053,7 @@ "rule": "jQuery-append(", "path": "js/views/settings_view.js", "line": " this.$('.sync-setting').append(syncView.el);", - "lineNumber": 124, + "lineNumber": 126, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes" @@ -1062,25 +1062,25 @@ "rule": "jQuery-$(", "path": "js/views/settings_view.js", "line": " this.$('.sync').text(i18n('syncNow'));", - "lineNumber": 179, - "reasonCategory": "usageTrusted", - "updated": "2018-09-19T21:59:32.770Z", - "reasonDetail": "Protected from arbitrary input" - }, - { - "rule": "jQuery-$(", - "path": "js/views/settings_view.js", - "line": " this.$('.sync').attr('disabled', 'disabled');", "lineNumber": 183, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" }, + { + "rule": "jQuery-$(", + "path": "js/views/settings_view.js", + "line": " this.$('.sync').attr('disabled', 'disabled');", + "lineNumber": 187, + "reasonCategory": "usageTrusted", + "updated": "2018-09-19T21:59:32.770Z", + "reasonDetail": "Protected from arbitrary input" + }, { "rule": "jQuery-$(", "path": "js/views/settings_view.js", "line": " this.$('.synced_at').hide();", - "lineNumber": 195, + "lineNumber": 199, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input" @@ -1089,7 +1089,7 @@ "rule": "jQuery-$(", "path": "js/views/settings_view.js", "line": " this.$('.sync_failed').hide();", - "lineNumber": 200, + "lineNumber": 204, "reasonCategory": "usageTrusted", "updated": "2018-09-19T21:59:32.770Z", "reasonDetail": "Protected from arbitrary input"