diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b932a9448830..c56618be61cf 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1246,7 +1246,7 @@ "message": "Link New Device", "description": "The menu option shown in Signal iOS to add a new linked device" }, - "deviceName": { + "Preferences--device-name": { "message": "Device name", "description": "The label in settings panel shown for the user-provided name for this desktop instance" }, @@ -1278,7 +1278,7 @@ "installTryAgain": { "message": "Try again" }, - "theme": { + "Preferences--theme": { "message": "Theme", "description": "Header for theme settings" }, @@ -1614,7 +1614,7 @@ "message": "Relay all calls through the Signal server to avoid revealing your IP address to your contact. Enabling will reduce call quality.", "description": "Details describing the always relay calls setting" }, - "permissions": { + "Preferences--permissions": { "message": "Permissions", "description": "Header for permissions section of settings" }, @@ -1631,7 +1631,7 @@ "description": "Header for general options on the settings screen" }, "spellCheckDescription": { - "message": "Enable spell check", + "message": "Spell check text entered in message composition box", "description": "Description of the spell check setting" }, "spellCheckWillBeEnabled": { @@ -1655,7 +1655,7 @@ "description": "Description for the automatic launch setting" }, "clearDataHeader": { - "message": "Clear Data", + "message": "Clear application data", "description": "Header in the settings dialog for the section dealing with data deletion" }, "clearDataExplanation": { @@ -1803,7 +1803,7 @@ "description": "Error message displayed when sending to an unregistered user." }, "sync": { - "message": "Contacts", + "message": "Import contacts", "description": "Label for contact and group sync settings" }, "syncExplanation": { @@ -1963,7 +1963,7 @@ } }, "audioNotificationDescription": { - "message": "Play audio notification", + "message": "Play notification sounds", "description": "Description for audio notification setting" }, "callRingtoneNotificationDescription": { @@ -5009,7 +5009,7 @@ "description": "Displayed while checking if the contact is SMS-only" }, "countMutedConversationsDescription": { - "message": "Count muted conversations in badge count", + "message": "Include muted conversations in badge count", "description": "Description for counting muted conversations in badge setting" }, "ContactModal--message": { @@ -5965,5 +5965,131 @@ "LeftPaneSetGroupMetadataHelper__avatar-modal-title": { "message": "Group Avatar", "description": "Title for the avatar picker in the group creation flow" + }, + "Preferences__button--general": { + "message": "General", + "description": "Button to switch the settings view" + }, + "Preferences__button--appearance": { + "message": "Appearance", + "description": "Button to switch the settings view" + }, + "Preferences__button--chats": { + "message": "Chats", + "description": "Button to switch the settings view" + }, + "Preferences__button--calls": { + "message": "Calls", + "description": "Button to switch the settings view" + }, + "Preferences__button--notifications": { + "message": "Notifications", + "description": "Button to switch the settings view" + }, + "Preferences__button--privacy": { + "message": "Privacy", + "description": "Button to switch the settings view" + }, + "Preferences--lastSynced": { + "message": "Last import at $date$ $time$", + "description": "Label for date and time of last sync operation", + "placeholders": { + "date": { + "content": "$1", + "example": "6/9/2020" + }, + "time": { + "content": "$2", + "example": "7:37:25 PM" + } + } + }, + "Preferences--system": { + "message": "System", + "description": "Title for system type settings" + }, + "Preferences--zoom": { + "message": "Zoom level", + "description": "Label for changing the zoom level" + }, + "Preferences__link-previews--title": { + "message": "Generate link previews", + "description": "Title for the generate link previews setting" + }, + "Preferences__link-previews--description": { + "message": "To change this setting, open the Signal app on your mobile device and navigate to Settings > Chats", + "description": "Description for the generate link previews setting" + }, + "Preferences--advanced": { + "message": "Advanced", + "description": "Title for advanced settings" + }, + "Preferences--notification-content": { + "message": "Notification content", + "description": "Label for the notification content setting select box" + }, + "Preferences--blocked": { + "message": "Blocked", + "description": "Label for blocked contacts setting" + }, + "Preferences--blocked-count-singular": { + "message": "$num$ contact", + "description": "Number of contacts blocked singular", + "placeholders": { + "num": { + "content": "$1", + "example": "1" + } + } + }, + "Preferences--blocked-count-plural": { + "message": "$num$ contacts", + "description": "Number of contacts blocked plural", + "placeholders": { + "num": { + "content": "$1", + "example": "20" + } + } + }, + "Preferences__who-can--title": { + "message": "Who can...", + "description": "Title for the 'who can do X' setting" + }, + "Preferences__privacy--description": { + "message": "To change these settings, open the Signal app on your mobile device and navigate to Settings > Privacy", + "description": "Description for the 'who can do X' setting" + }, + "Preferences__who-can--everybody": { + "message": "Everybody", + "description": "Option for who can see my X select" + }, + "Preferences__who-can--contacts": { + "message": "My Contacts", + "description": "Option for who can see my X select" + }, + "Preferences__who-can--nobody": { + "message": "Nobody", + "description": "Option for who can see my X select" + }, + "Preferences--messaging": { + "message": "Messaging", + "description": "Title for the messaging settings" + }, + "Preferences--see-me": { + "message": "See my phone number", + "description": "Label for the see my phone number setting" + }, + "Preferences--find-me": { + "message": "Find me by my phone number", + "description": "Label for the find me by my phone number setting" + }, + "Preferences--read-receipts": { + "message": "Read receipts", + "description": "Label for the read receipts setting" + }, + "Preferences--typing-indicators": { + "message": "Typing indicators", + "description": "Label for the typing indicators setting" } } diff --git a/app/ephemeral_config.ts b/app/ephemeral_config.ts index 9f8d3b0cbed0..c207d2b4af7d 100644 --- a/app/ephemeral_config.ts +++ b/app/ephemeral_config.ts @@ -10,7 +10,7 @@ import { start } from './base_config'; const userDataPath = app.getPath('userData'); const targetPath = join(userDataPath, 'ephemeral.json'); -const ephemeralConfig = start('ephemeral', targetPath, { +export const ephemeralConfig = start('ephemeral', targetPath, { allowMalformedOnStartup: true, }); diff --git a/app/permissions.ts b/app/permissions.ts index 1acfa5ee74e4..0e869e91b00d 100644 --- a/app/permissions.ts +++ b/app/permissions.ts @@ -24,7 +24,7 @@ const PERMISSIONS: Record = { }; function _createPermissionHandler( - userConfig: ConfigType + userConfig: Pick ): Parameters[0] { return (_webContents, permission, callback, details): void => { // We default 'media' permission to false, but the user can override that for @@ -75,7 +75,7 @@ export function installPermissionsHandler({ userConfig, }: { session: typeof ElectronSession; - userConfig: ConfigType; + userConfig: Pick; }): void { // Setting the permission request handler to null first forces any permissions to be // requested again. Without this, revoked permissions might still be available if diff --git a/app/user_config.ts b/app/user_config.ts index ca128a6d2ae9..b1ffddd8a287 100644 --- a/app/user_config.ts +++ b/app/user_config.ts @@ -24,7 +24,7 @@ console.log(`userData: ${app.getPath('userData')}`); const userDataPath = app.getPath('userData'); const targetPath = join(userDataPath, 'config.json'); -const userConfig = start('user', targetPath); +export const userConfig = start('user', targetPath); export const get = userConfig.get.bind(userConfig); export const remove = userConfig.remove.bind(userConfig); diff --git a/debug_log_preload.js b/debug_log_preload.js index cfc5c856854b..ed669910b108 100644 --- a/debug_log_preload.js +++ b/debug_log_preload.js @@ -6,6 +6,10 @@ const { ipcRenderer } = require('electron'); const url = require('url'); const copyText = require('copy-text-to-clipboard'); + +// It is important to call this as early as possible +require('./ts/windows/context'); + const i18n = require('./js/modules/i18n'); const { getEnvironment, @@ -13,10 +17,6 @@ const { parseEnvironment, } = require('./ts/environment'); -const { Context: SignalContext } = require('./ts/context'); - -window.SignalContext = new SignalContext(ipcRenderer); - const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); diff --git a/images/icons/v2/appearance-outline-24.svg b/images/icons/v2/appearance-outline-24.svg new file mode 100644 index 000000000000..e8a3530d72e7 --- /dev/null +++ b/images/icons/v2/appearance-outline-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/appearance-solid-24.svg b/images/icons/v2/appearance-solid-24.svg new file mode 100644 index 000000000000..e0ab19b55099 --- /dev/null +++ b/images/icons/v2/appearance-solid-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/bell-outline-24.svg b/images/icons/v2/bell-outline-24.svg new file mode 100644 index 000000000000..cdfea27b95c6 --- /dev/null +++ b/images/icons/v2/bell-outline-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/bell-solid-24.svg b/images/icons/v2/bell-solid-24.svg new file mode 100644 index 000000000000..12a5a70ad3f4 --- /dev/null +++ b/images/icons/v2/bell-solid-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/icons/v2/lock-solid-24.svg b/images/icons/v2/lock-solid-24.svg new file mode 100644 index 000000000000..0b2da89c62cb --- /dev/null +++ b/images/icons/v2/lock-solid-24.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/permissions_popup_start.js b/js/permissions_popup_start.js index 7f8fdf8d3125..c512a193498b 100644 --- a/js/permissions_popup_start.js +++ b/js/permissions_popup_start.js @@ -12,7 +12,7 @@ $(document).on('keydown', e => { const $body = $(document.body); async function applyTheme() { - const theme = await window.getThemeSetting(); + const theme = await window.Settings.themeSetting.getValue(); $body.removeClass('light-theme'); $body.removeClass('dark-theme'); $body.addClass(`${theme === 'system' ? window.systemTheme : theme}-theme`); @@ -41,9 +41,9 @@ window.showConfirmationDialog({ okText: i18n('allowAccess'), resolve: () => { if (!window.forCamera) { - window.setMediaPermissions(true); + window.Settings.mediaPermissions.setValue(true); } else { - window.setMediaCameraPermissions(true); + window.Settings.mediaCameraPermissions.setValue(true); } window.closePermissionsPopup(); }, diff --git a/js/settings_start.js b/js/settings_start.js deleted file mode 100644 index eb19481654e4..000000000000 --- a/js/settings_start.js +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2018-2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global $, Whisper */ - -$(document).on('keydown', e => { - if (e.keyCode === 27) { - window.closeSettings(); - } -}); - -const $body = $(document.body); - -async function applyTheme() { - const theme = await window.getThemeSetting(); - $body.removeClass('light-theme'); - $body.removeClass('dark-theme'); - $body.addClass(`${theme === 'system' ? window.systemTheme : theme}-theme`); -} - -applyTheme(); - -window.SignalContext.nativeThemeListener.subscribe(() => { - applyTheme(); -}); - -const getInitialData = async () => ({ - deviceName: await window.getDeviceName(), - - themeSetting: await window.getThemeSetting(), - hideMenuBar: await window.getHideMenuBar(), - systemTray: await window.getSystemTraySetting(), - - notificationSetting: await window.getNotificationSetting(), - audioNotification: await window.getAudioNotification(), - notificationDrawAttention: await window.getNotificationDrawAttention(), - countMutedConversations: await window.getCountMutedConversations(), - - spellCheck: await window.getSpellCheck(), - autoLaunch: await window.getAutoLaunch(), - - incomingCallNotification: await window.getIncomingCallNotification(), - callRingtoneNotification: await window.getCallRingtoneNotification(), - callSystemNotification: await window.getCallSystemNotification(), - alwaysRelayCalls: await window.getAlwaysRelayCalls(), - - mediaPermissions: await window.getMediaPermissions(), - mediaCameraPermissions: await window.getMediaCameraPermissions(), - - isPrimary: await window.isPrimary(), - lastSyncTime: await window.getLastSyncTime(), - universalExpireTimer: await window.getUniversalExpireTimer(), -}); - -window.initialRequest = getInitialData(); - -// eslint-disable-next-line more/no-then -window.initialRequest.then( - data => { - window.initialData = data; - window.view = new Whisper.SettingsView(); - window.view.$el.appendTo($body); - }, - error => { - window.log.error( - 'settings.initialRequest error:', - error && error.stack ? error.stack : error - ); - window.closeSettings(); - } -); diff --git a/js/views/settings_view.js b/js/views/settings_view.js deleted file mode 100644 index 65e583fbc7cb..000000000000 --- a/js/views/settings_view.js +++ /dev/null @@ -1,476 +0,0 @@ -// Copyright 2016-2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global i18n: false */ -/* global Whisper: false */ -/* global $: false */ - -/* eslint-disable no-new */ - -// eslint-disable-next-line func-names -(function () { - window.Whisper = window.Whisper || {}; - const { Settings } = window.Signal.Types; - - const { - DEFAULT_DURATIONS_IN_SECONDS, - DEFAULT_DURATIONS_SET, - format: formatExpirationTimer, - } = window.Signal.Util.expirationTimer; - - const CheckboxView = Whisper.View.extend({ - initialize(options) { - this.name = options.name; - this.setFn = options.setFn; - this.value = options.value; - this.populate(); - }, - events: { - change: 'change', - }, - change(e) { - const value = e.target.checked; - this.setFn(value); - window.log.info(this.name, 'changed to', value); - }, - populate() { - this.$('input').prop('checked', !!this.value); - }, - }); - - const MediaPermissionsSettingView = Whisper.View.extend({ - initialize(options) { - this.value = options.value; - this.setFn = options.setFn; - this.populate(); - }, - events: { - change: 'change', - }, - change(e) { - this.value = e.target.checked; - this.setFn(this.value); - window.log.info('media-permissions changed to', this.value); - }, - populate() { - this.$('input').prop('checked', Boolean(this.value)); - }, - }); - - const MediaCameraPermissionsSettingView = Whisper.View.extend({ - initialize(options) { - this.value = options.value; - this.setFn = options.setFn; - this.populate(); - }, - events: { - change: 'change', - }, - change(e) { - this.value = e.target.checked; - this.setFn(this.value); - window.log.info('media-camera-permissions changed to', this.value); - }, - populate() { - this.$('input').prop('checked', Boolean(this.value)); - }, - }); - - const DisappearingMessagesView = Whisper.View.extend({ - template: () => $('#disappearingMessagesSettings').html(), - initialize(options) { - this.timeDialog = null; - - this.value = options.value || 0; - - this.render(); - }, - - render_attributes() { - const isCustomValue = this.isCustomValue(); - - return { - title: i18n('disappearingMessages'), - timerValues: DEFAULT_DURATIONS_IN_SECONDS.map(seconds => { - const text = formatExpirationTimer(i18n, seconds, { - capitalizeOff: true, - }); - return { - selected: seconds === this.value ? 'selected' : undefined, - value: seconds, - text, - }; - }), - customSelected: isCustomValue ? 'selected' : undefined, - customText: i18n( - isCustomValue - ? 'selectedCustomDisappearingTimeOption' - : 'customDisappearingTimeOption' - ), - customInfo: isCustomValue - ? { - text: formatExpirationTimer(i18n, this.value), - } - : undefined, - timerLabel: i18n('settings__DisappearingMessages__timer__label'), - footer: i18n('settings__DisappearingMessages__footer'), - }; - }, - - events: { - change: 'change', - }, - - change(e) { - const value = parseInt(e.target.value, 10); - - if (value === -1) { - this.showDialog(); - return; - } - - this.updateValue(value); - window.log.info('disappearing-messages-timer changed to', this.value); - }, - - isCustomValue() { - return this.value && !DEFAULT_DURATIONS_SET.has(this.value); - }, - - showDialog() { - this.closeDialog(); - - this.timeDialog = new window.Whisper.ReactWrapperView({ - className: 'disappearing-time-dialog-wrapper', - Component: window.Signal.Components.DisappearingTimeDialog, - props: { - i18n, - initialValue: this.value, - onSubmit: newValue => { - this.updateValue(newValue); - this.closeDialog(); - - window.log.info( - 'disappearing-messages-timer changed to custom value', - this.value - ); - }, - onClose: () => { - this.closeDialog(); - }, - }, - }); - }, - - closeDialog() { - if (this.timeDialog) { - this.timeDialog.remove(); - } - this.timeDialog = null; - }, - - updateValue(newValue) { - this.value = newValue; - window.setUniversalExpireTimer(newValue); - this.render(); - }, - }); - - const RadioButtonGroupView = Whisper.View.extend({ - initialize(options) { - this.name = options.name; - this.setFn = options.setFn; - this.value = options.value; - this.populate(); - }, - events: { - change: 'change', - }, - change(e) { - const value = this.$(e.target).val(); - this.setFn(value); - window.log.info(this.name, 'changed to', value); - }, - populate() { - this.$(`#${this.name}-${this.value}`).attr('checked', 'checked'); - }, - }); - Whisper.SettingsView = Whisper.View.extend({ - className: 'settings modal expand', - template: () => $('#settings').html(), - initialize() { - this.render(); - new RadioButtonGroupView({ - el: this.$('.notification-settings'), - name: 'notification-setting', - value: window.initialData.notificationSetting, - setFn: window.setNotificationSetting, - }); - new RadioButtonGroupView({ - el: this.$('.theme-settings'), - name: 'theme-setting', - value: window.initialData.themeSetting, - setFn: theme => { - $(document.body) - .removeClass('dark-theme') - .removeClass('light-theme') - .addClass( - `${theme === 'system' ? window.systemTheme : theme}-theme` - ); - window.setThemeSetting(theme); - }, - }); - if (Settings.isDrawAttentionSupported()) { - new CheckboxView({ - el: this.$('.draw-attention-setting'), - name: 'draw-attention-setting', - value: window.initialData.notificationDrawAttention, - setFn: window.setNotificationDrawAttention, - }); - } - if (Settings.isAudioNotificationSupported()) { - new CheckboxView({ - el: this.$('.audio-notification-setting'), - name: 'audio-notification-setting', - value: window.initialData.audioNotification, - setFn: window.setAudioNotification, - }); - } - new CheckboxView({ - el: this.$('.badge-count-muted-conversations-setting'), - name: 'badge-count-muted-conversations-setting', - value: window.initialData.countMutedConversations, - setFn: window.setCountMutedConversations, - }); - new CheckboxView({ - el: this.$('.spell-check-setting'), - name: 'spell-check-setting', - value: window.initialData.spellCheck, - setFn: val => { - const $msg = this.$('.spell-check-setting-message'); - if (val !== window.appStartInitialSpellcheckSetting) { - $msg.show(); - $msg.attr('aria-hidden', false); - } else { - $msg.hide(); - $msg.attr('aria-hidden', true); - } - window.setSpellCheck(val); - }, - }); - if (Settings.isAutoLaunchSupported()) { - new CheckboxView({ - el: this.$('.auto-launch-setting'), - name: 'auto-launch-setting', - value: window.initialData.autoLaunch, - setFn: window.setAutoLaunch, - }); - } - if (Settings.isHideMenuBarSupported()) { - new CheckboxView({ - el: this.$('.menu-bar-setting'), - name: 'menu-bar-setting', - value: window.initialData.hideMenuBar, - setFn: window.setHideMenuBar, - }); - } - new window.Whisper.ReactWrapperView({ - el: this.$('.system-tray-setting-container'), - Component: window.Signal.Components.SystemTraySettingsCheckboxes, - props: { - i18n, - initialValue: window.initialData.systemTray, - isSystemTraySupported: Settings.isSystemTraySupported( - window.getVersion() - ), - onChange: window.setSystemTraySetting, - }, - }); - new CheckboxView({ - el: this.$('.always-relay-calls-setting'), - name: 'always-relay-calls-setting', - value: window.initialData.alwaysRelayCalls, - setFn: window.setAlwaysRelayCalls, - }); - new CheckboxView({ - el: this.$('.call-ringtone-notification-setting'), - name: 'call-ringtone-notification-setting', - value: window.initialData.callRingtoneNotification, - setFn: window.setCallRingtoneNotification, - }); - new CheckboxView({ - el: this.$('.call-system-notification-setting'), - name: 'call-system-notification-setting', - value: window.initialData.callSystemNotification, - setFn: window.setCallSystemNotification, - }); - new CheckboxView({ - el: this.$('.incoming-call-notification-setting'), - name: 'incoming-call-notification-setting', - value: window.initialData.incomingCallNotification, - setFn: window.setIncomingCallNotification, - }); - new MediaPermissionsSettingView({ - el: this.$('.media-permissions'), - value: window.initialData.mediaPermissions, - setFn: window.setMediaPermissions, - }); - new MediaCameraPermissionsSettingView({ - el: this.$('.media-camera-permissions'), - value: window.initialData.mediaCameraPermissions, - setFn: window.setMediaCameraPermissions, - }); - - const disappearingMessagesView = new DisappearingMessagesView({ - value: window.initialData.universalExpireTimer, - name: 'disappearing-messages-setting', - }); - this.$('.disappearing-messages-setting').append( - disappearingMessagesView.el - ); - - if (!window.initialData.isPrimary) { - const syncView = new SyncView().render(); - this.$('.sync-setting').append(syncView.el); - } - }, - events: { - 'click .close': 'onClose', - 'click .clear-data': 'onClearData', - }, - render_attributes() { - const appStartSpellCheck = window.appStartInitialSpellcheckSetting; - const spellCheckDirty = - window.initialData.spellCheck !== appStartSpellCheck; - - return { - deviceNameLabel: i18n('deviceName'), - deviceName: window.initialData.deviceName, - theme: i18n('theme'), - notifications: i18n('notifications'), - notificationSettingsDialog: i18n('notificationSettingsDialog'), - settings: i18n('Keyboard--preferences'), - disableNotifications: i18n('disableNotifications'), - nameAndMessage: i18n('nameAndMessage'), - noNameOrMessage: i18n('noNameOrMessage'), - nameOnly: i18n('nameOnly'), - notificationDrawAttention: i18n('notificationDrawAttention'), - audioNotificationDescription: i18n('audioNotificationDescription'), - isAudioNotificationSupported: Settings.isAudioNotificationSupported(), - isHideMenuBarSupported: Settings.isHideMenuBarSupported(), - isDrawAttentionSupported: Settings.isDrawAttentionSupported(), - isAutoLaunchSupported: Settings.isAutoLaunchSupported(), - hasSystemTheme: true, - themeLight: i18n('themeLight'), - themeDark: i18n('themeDark'), - themeSystem: i18n('themeSystem'), - hideMenuBar: i18n('hideMenuBar'), - clearDataHeader: i18n('clearDataHeader'), - clearDataButton: i18n('clearDataButton'), - clearDataExplanation: i18n('clearDataExplanation'), - calling: i18n('calling'), - countMutedConversationsDescription: i18n( - 'countMutedConversationsDescription' - ), - alwaysRelayCallsDescription: i18n('alwaysRelayCallsDescription'), - alwaysRelayCallsDetail: i18n('alwaysRelayCallsDetail'), - callRingtoneNotificationDescription: i18n( - 'callRingtoneNotificationDescription' - ), - callSystemNotificationDescription: i18n( - 'callSystemNotificationDescription' - ), - incomingCallNotificationDescription: i18n( - 'incomingCallNotificationDescription' - ), - permissions: i18n('permissions'), - mediaPermissionsDescription: i18n('mediaPermissionsDescription'), - mediaCameraPermissionsDescription: i18n( - 'mediaCameraPermissionsDescription' - ), - generalHeader: i18n('general'), - spellCheckDescription: i18n('spellCheckDescription'), - spellCheckHidden: spellCheckDirty ? 'false' : 'true', - spellCheckDisplay: spellCheckDirty ? 'inherit' : 'none', - spellCheckDirtyText: appStartSpellCheck - ? i18n('spellCheckWillBeDisabled') - : i18n('spellCheckWillBeEnabled'), - autoLaunchDescription: i18n('autoLaunchDescription'), - }; - }, - onClose() { - window.closeSettings(); - }, - onClearData() { - window.deleteAllData(); - window.closeSettings(); - }, - }); - - const SyncView = Whisper.View.extend({ - template: () => $('#syncSettings').html(), - className: 'syncSettings', - events: { - 'click .sync': 'sync', - }, - initialize() { - this.lastSyncTime = window.initialData.lastSyncTime; - }, - enable() { - this.$('.sync').text(i18n('syncNow')); - this.$('.sync').removeAttr('disabled'); - }, - disable() { - this.$('.sync').attr('disabled', 'disabled'); - this.$('.sync').text(i18n('syncing')); - }, - onsuccess() { - window.setLastSyncTime(Date.now()); - this.lastSyncTime = Date.now(); - window.log.info('sync successful'); - this.enable(); - this.render(); - }, - ontimeout() { - window.log.error('sync timed out'); - this.$('.synced_at').hide(); - this.$('.sync_failed').show(); - this.enable(); - }, - async sync() { - this.$('.sync_failed').hide(); - if (window.initialData.isPrimary) { - window.log.warn('Tried to sync from device 1'); - return; - } - - this.disable(); - try { - await window.makeSyncRequest(); - this.onsuccess(); - } catch (error) { - window.log.error( - 'settings sync timeout error:', - error && error.stack ? error.stack : error - ); - this.ontimeout(); - } - }, - render_attributes() { - const attrs = { - sync: i18n('sync'), - syncNow: i18n('syncNow'), - syncExplanation: i18n('syncExplanation'), - syncFailed: i18n('syncFailed'), - }; - let date = this.lastSyncTime; - if (date) { - date = new Date(date); - attrs.lastSynced = i18n('lastSynced'); - attrs.syncDate = date.toLocaleDateString(); - attrs.syncTime = date.toLocaleTimeString(); - } - return attrs; - }, - }); -})(); diff --git a/main.js b/main.js index b55533a0115e..45f15e0465a2 100644 --- a/main.js +++ b/main.js @@ -37,7 +37,6 @@ const { ipcMain: ipc, Menu, protocol: electronProtocol, - session, shell, systemPreferences, } = electron; @@ -102,7 +101,6 @@ const { installFileHandler, installWebHandler, } = require('./app/protocol_filter'); -const { installPermissionsHandler } = require('./app/permissions'); const OS = require('./ts/OS'); const { isProduction } = require('./ts/util/version'); const { @@ -124,6 +122,7 @@ const { Environment, isTestEnvironment } = require('./ts/environment'); const { ChallengeMainHandler } = require('./ts/main/challengeMain'); const { NativeThemeNotifier } = require('./ts/main/NativeThemeNotifier'); const { PowerChannel } = require('./ts/main/powerChannel'); +const { SettingsChannel } = require('./ts/main/settingsChannel'); const { maybeParseUrl, setUrlSearchParams } = require('./ts/util/url'); const { getHeicConverter } = require('./ts/workers/heicConverterMain'); @@ -235,6 +234,7 @@ const loadLocale = require('./app/locale').load; // Both of these will be set after app fires the 'ready' event let logger; let locale; +let settingsChannel; function prepareFileUrl( pathSegments /* : ReadonlyArray */, @@ -425,6 +425,8 @@ async function createWindow() { // Create the browser window. mainWindow = new BrowserWindow(windowOptions); + settingsChannel.setMainWindow(mainWindow); + mainWindowCreated = true; setupSpellChecker(mainWindow, locale.messages); if (!startInTray && windowConfig && windowConfig.maximized) { @@ -567,6 +569,7 @@ async function createWindow() { // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. mainWindow = undefined; + settingsChannel.setMainWindow(mainWindow); if (systemTrayService) { systemTrayService.setMainWindow(mainWindow); } @@ -875,40 +878,26 @@ function showSettingsWindow() { settingsWindow.show(); return; } - if (!mainWindow) { - return; - } - addDarkOverlay(); - - const size = mainWindow.getSize(); - // center settings window over main window - const settingwidth = Math.min(500, size[0]); - const settingheight = Math.max(size[1] - 100, MIN_HEIGHT); - const mainPos = mainWindow.getPosition(); - const mainSize = mainWindow.getSize(); const options = { - x: Math.round(mainPos[0] + mainSize[0] / 2 - settingwidth / 2), - y: Math.round(mainPos[1] + mainSize[1] / 2 - settingheight / 2), - width: settingwidth, - height: settingheight, - frame: false, + width: 700, + height: 700, + frame: true, resizable: false, title: locale.messages.signalDesktopPreferences.message, autoHideMenuBar: true, backgroundColor: '#3a76f0', show: false, - modal: true, + modal: false, webPreferences: { ...defaultWebPrefs, nodeIntegration: false, nodeIntegrationInWorker: false, contextIsolation: false, enableRemoteModule: true, - preload: path.join(__dirname, 'settings_preload.js'), + preload: path.join(__dirname, 'ts', 'windows', 'settings', 'preload.js'), nativeWindowOpen: true, }, - parent: mainWindow, }; settingsWindow = new BrowserWindow(options); @@ -924,6 +913,11 @@ function showSettingsWindow() { settingsWindow.once('ready-to-show', () => { settingsWindow.show(); + settingsWindow.webContents.send('render'); + + if (config.get('openDevTools')) { + settingsWindow.webContents.openDevTools(); + } }); } @@ -1015,7 +1009,7 @@ async function showDebugLogWindow() { return; } - const theme = await pify(getDataFromMainWindow)('theme-setting'); + const theme = await settingsChannel.getSettingFromMainWindow('themeSetting'); const size = mainWindow.getSize(); const options = { width: Math.max(size[0] - 100, MIN_WIDTH), @@ -1068,7 +1062,9 @@ function showPermissionsPopupWindow(forCalling, forCamera) { reject(new Error('No main window')); } - const theme = await pify(getDataFromMainWindow)('theme-setting'); + const theme = await settingsChannel.getSettingFromMainWindow( + 'themeSetting' + ); const size = mainWindow.getSize(); const options = { width: Math.min(400, size[0]), @@ -1154,6 +1150,9 @@ let ready = false; app.on('ready', async () => { const startTime = Date.now(); + settingsChannel = new SettingsChannel(); + settingsChannel.install(); + // We use this event only a single time to log the startup time of the app // from when it's first ready until the loading screen disappears. ipc.once('signal-app-loaded', (event, info) => { @@ -1201,8 +1200,6 @@ app.on('ready', async () => { protocol: electronProtocol, }); - installPermissionsHandler({ session, userConfig }); - logger = await logging.initialize(getMainWindow); logger.info('app ready'); logger.info(`starting version ${packageJson.version}`); @@ -1668,79 +1665,6 @@ ipc.on('close-settings', () => { } }); -installSettingsGetter('device-name'); - -installSettingsGetter('theme-setting'); -installSettingsSetter('theme-setting'); -installSettingsGetter('hide-menu-bar'); -installSettingsSetter('hide-menu-bar'); -installSettingsGetter('system-tray-setting'); -installSettingsSetter('system-tray-setting'); - -installSettingsGetter('notification-setting'); -installSettingsSetter('notification-setting'); -installSettingsGetter('notification-draw-attention'); -installSettingsSetter('notification-draw-attention'); -installSettingsGetter('audio-notification'); -installSettingsSetter('audio-notification'); -installSettingsGetter('badge-count-muted-conversations'); -installSettingsSetter('badge-count-muted-conversations'); - -installSettingsGetter('spell-check'); -installSettingsSetter('spell-check', true); - -installSettingsGetter('auto-launch'); -installSettingsSetter('auto-launch'); - -installSettingsGetter('always-relay-calls'); -installSettingsSetter('always-relay-calls'); -installSettingsGetter('call-ringtone-notification'); -installSettingsSetter('call-ringtone-notification'); -installSettingsGetter('call-system-notification'); -installSettingsSetter('call-system-notification'); -installSettingsGetter('incoming-call-notification'); -installSettingsSetter('incoming-call-notification'); - -// These ones are different because its single source of truth is userConfig, -// not IndexedDB -ipc.on('get-media-permissions', event => { - event.sender.send( - 'get-success-media-permissions', - null, - userConfig.get('mediaPermissions') || false - ); -}); -ipc.on('get-media-camera-permissions', event => { - event.sender.send( - 'get-success-media-camera-permissions', - null, - userConfig.get('mediaCameraPermissions') || false - ); -}); -ipc.on('set-media-permissions', (event, value) => { - userConfig.set('mediaPermissions', value); - - // We reinstall permissions handler to ensure that a revoked permission takes effect - installPermissionsHandler({ session, userConfig }); - - event.sender.send('set-success-media-permissions', null); -}); -ipc.on('set-media-camera-permissions', (event, value) => { - userConfig.set('mediaCameraPermissions', value); - - // We reinstall permissions handler to ensure that a revoked permission takes effect - installPermissionsHandler({ session, userConfig }); - - event.sender.send('set-success-media-camera-permissions', null); -}); - -installSettingsGetter('is-primary'); -installSettingsGetter('sync-request'); -installSettingsGetter('sync-time'); -installSettingsSetter('sync-time'); -installSettingsGetter('universal-expire-timer'); -installSettingsSetter('universal-expire-timer'); - ipc.on('delete-all-data', () => { if (mainWindow && mainWindow.webContents) { mainWindow.webContents.send('delete-all-data'); @@ -1776,47 +1700,12 @@ ipc.on('get-user-data-path', event => { event.returnValue = app.getPath('userData'); }); -function getDataFromMainWindow(name, callback) { - ipc.once(`get-success-${name}`, (_event, error, value) => - callback(error, value) - ); - mainWindow.webContents.send(`get-${name}`); -} - -function installSettingsGetter(name) { - ipc.on(`get-${name}`, event => { - if (mainWindow && mainWindow.webContents) { - getDataFromMainWindow(name, (error, value) => { - const contents = event.sender; - if (contents.isDestroyed()) { - return; - } - - contents.send(`get-success-${name}`, error, value); - }); - } - }); -} - -function installSettingsSetter(name, isEphemeral = false) { - ipc.on(`set-${name}`, (event, value) => { - if (isEphemeral) { - ephemeralConfig.set('spell-check', value); - } - - if (mainWindow && mainWindow.webContents) { - ipc.once(`set-success-${name}`, (_event, error) => { - const contents = event.sender; - if (contents.isDestroyed()) { - return; - } - - contents.send(`set-success-${name}`, error); - }); - mainWindow.webContents.send(`set-${name}`, value); - } - }); -} +// Refresh the settings window whenever preferences change +ipc.on('preferences-changed', () => { + if (settingsWindow && settingsWindow.webContents) { + settingsWindow.webContents.send('render'); + } +}); function getIncomingHref(argv) { return argv.find(arg => isSgnlHref(arg, logger)); diff --git a/package.json b/package.json index 829ca11262fa..06632cf1c869 100644 --- a/package.json +++ b/package.json @@ -416,7 +416,6 @@ "preload_utils.js", "about_preload.js", "screenShare_preload.js", - "settings_preload.js", "permissions_popup_preload.js", "debug_log_preload.js", "main.js", diff --git a/permissions_popup_preload.js b/permissions_popup_preload.js index cff4fb816934..490ae100437c 100644 --- a/permissions_popup_preload.js +++ b/permissions_popup_preload.js @@ -8,23 +8,24 @@ window.ReactDOM = require('react-dom'); const { ipcRenderer } = require('electron'); const url = require('url'); + +// It is important to call this as early as possible +require('./ts/windows/context'); + const i18n = require('./js/modules/i18n'); const { ConfirmationDialog } = require('./ts/components/ConfirmationDialog'); -const { makeGetter, makeSetter } = require('./preload_utils'); const { getEnvironment, setEnvironment, parseEnvironment, } = require('./ts/environment'); -const { Context: SignalContext } = require('./ts/context'); - const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); setEnvironment(parseEnvironment(config.environment)); -window.SignalContext = new SignalContext(ipcRenderer); +const { createSetting } = require('./ts/util/preload'); window.getEnvironment = getEnvironment; window.getVersion = () => config.version; @@ -43,8 +44,14 @@ require('./ts/logging/set_up_renderer_logging').initialize(); window.closePermissionsPopup = () => ipcRenderer.send('close-permissions-popup'); -window.setMediaPermissions = makeSetter('media-permissions'); -window.setMediaCameraPermissions = makeSetter('media-camera-permissions'); -window.getThemeSetting = makeGetter('theme-setting'); -window.setThemeSetting = makeSetter('theme-setting'); window.Backbone = require('backbone'); + +window.Settings = { + mediaCameraPermissions: createSetting('mediaCameraPermissions', { + getter: false, + }), + mediaPermissions: createSetting('mediaPermissions', { + getter: false, + }), + themeSetting: createSetting('themeSetting', { setter: false }), +}; diff --git a/preload.js b/preload.js index 13ba15a5b6a3..8413eb2b3ea1 100644 --- a/preload.js +++ b/preload.js @@ -12,7 +12,10 @@ try { const electron = require('electron'); const semver = require('semver'); const _ = require('lodash'); - const { installGetter, installSetter } = require('./preload_utils'); + + // It is important to call this as early as possible + require('./ts/windows/context'); + const { getEnvironment, setEnvironment, @@ -24,10 +27,6 @@ try { const { remote } = electron; const { app } = remote; - const { Context: SignalContext } = require('./ts/context'); - - window.SignalContext = new SignalContext(ipc); - window.sqlInitializer = require('./ts/sql/initialize'); const config = require('url').parse(window.location.toString(), true).query; @@ -254,146 +253,7 @@ try { window.Events.removeDarkOverlay(); }); - installGetter('device-name', 'getDeviceName'); - - installGetter('theme-setting', 'getThemeSetting'); - installSetter('theme-setting', 'setThemeSetting'); - installGetter('hide-menu-bar', 'getHideMenuBar'); - installSetter('hide-menu-bar', 'setHideMenuBar'); - installGetter('system-tray-setting', 'getSystemTraySetting'); - installSetter('system-tray-setting', 'setSystemTraySetting'); - - installGetter('notification-setting', 'getNotificationSetting'); - installSetter('notification-setting', 'setNotificationSetting'); - installGetter('notification-draw-attention', 'getNotificationDrawAttention'); - installSetter('notification-draw-attention', 'setNotificationDrawAttention'); - installGetter('audio-notification', 'getAudioNotification'); - installSetter('audio-notification', 'setAudioNotification'); - installGetter( - 'badge-count-muted-conversations', - 'getCountMutedConversations' - ); - installSetter( - 'badge-count-muted-conversations', - 'setCountMutedConversations' - ); - - window.getCountMutedConversations = () => - new Promise((resolve, reject) => { - ipc.once( - 'get-success-badge-count-muted-conversations', - (_event, error, value) => { - if (error) { - return reject(new Error(error)); - } - - return resolve(value); - } - ); - ipc.send('get-badge-count-muted-conversations'); - }); - - installGetter('spell-check', 'getSpellCheck'); - installSetter('spell-check', 'setSpellCheck'); - - installGetter('auto-launch', 'getAutoLaunch'); - installSetter('auto-launch', 'setAutoLaunch'); - - installGetter('always-relay-calls', 'getAlwaysRelayCalls'); - installSetter('always-relay-calls', 'setAlwaysRelayCalls'); - - installGetter('call-ringtone-notification', 'getCallRingtoneNotification'); - installSetter('call-ringtone-notification', 'setCallRingtoneNotification'); - - window.getCallRingtoneNotification = () => - new Promise((resolve, reject) => { - ipc.once( - 'get-success-call-ringtone-notification', - (_event, error, value) => { - if (error) { - return reject(new Error(error)); - } - - return resolve(value); - } - ); - ipc.send('get-call-ringtone-notification'); - }); - - installGetter('call-system-notification', 'getCallSystemNotification'); - installSetter('call-system-notification', 'setCallSystemNotification'); - - window.getCallSystemNotification = () => - new Promise((resolve, reject) => { - ipc.once( - 'get-success-call-system-notification', - (_event, error, value) => { - if (error) { - return reject(new Error(error)); - } - - return resolve(value); - } - ); - ipc.send('get-call-system-notification'); - }); - - installGetter('incoming-call-notification', 'getIncomingCallNotification'); - installSetter('incoming-call-notification', 'setIncomingCallNotification'); - - window.getIncomingCallNotification = () => - new Promise((resolve, reject) => { - ipc.once( - 'get-success-incoming-call-notification', - (_event, error, value) => { - if (error) { - return reject(new Error(error)); - } - - return resolve(value); - } - ); - ipc.send('get-incoming-call-notification'); - }); - - window.getAlwaysRelayCalls = () => - new Promise((resolve, reject) => { - ipc.once('get-success-always-relay-calls', (_event, error, value) => { - if (error) { - return reject(new Error(error)); - } - - return resolve(value); - }); - ipc.send('get-always-relay-calls'); - }); - - window.getMediaPermissions = () => - new Promise((resolve, reject) => { - ipc.once('get-success-media-permissions', (_event, error, value) => { - if (error) { - return reject(new Error(error)); - } - - return resolve(value); - }); - ipc.send('get-media-permissions'); - }); - - window.getMediaCameraPermissions = () => - new Promise((resolve, reject) => { - ipc.once( - 'get-success-media-camera-permissions', - (_event, error, value) => { - if (error) { - return reject(new Error(error)); - } - - return resolve(value); - } - ); - ipc.send('get-media-camera-permissions'); - }); + require('./ts/windows/preload'); window.getBuiltInImages = () => new Promise((resolve, reject) => { @@ -407,13 +267,6 @@ try { ipc.send('get-built-in-images'); }); - installGetter('is-primary', 'isPrimary'); - installGetter('sync-request', 'getSyncRequest'); - installGetter('sync-time', 'getLastSyncTime'); - installSetter('sync-time', 'setLastSyncTime'); - installGetter('universal-expire-timer', 'getUniversalExpireTimer'); - installSetter('universal-expire-timer', 'setUniversalExpireTimer'); - ipc.on('delete-all-data', async () => { const { deleteAllData } = window.Events; if (!deleteAllData) { diff --git a/preload_utils.js b/preload_utils.js deleted file mode 100644 index 9d665b02d52a..000000000000 --- a/preload_utils.js +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2019-2020 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global window */ - -const { ipcRenderer: ipc } = require('electron'); - -exports.installGetter = function installGetter(name, functionName) { - ipc.on(`get-${name}`, async () => { - const getFn = window.Events[functionName]; - if (!getFn) { - ipc.send( - `get-success-${name}`, - `installGetter: ${functionName} not found for event ${name}` - ); - return; - } - try { - ipc.send(`get-success-${name}`, null, await getFn()); - } catch (error) { - ipc.send( - `get-success-${name}`, - error && error.stack ? error.stack : error - ); - } - }); -}; - -exports.installSetter = function installSetter(name, functionName) { - ipc.on(`set-${name}`, async (_event, value) => { - const setFn = window.Events[functionName]; - if (!setFn) { - ipc.send( - `set-success-${name}`, - `installSetter: ${functionName} not found for event ${name}` - ); - return; - } - try { - await setFn(value); - ipc.send(`set-success-${name}`); - } catch (error) { - ipc.send( - `set-success-${name}`, - error && error.stack ? error.stack : error - ); - } - }); -}; - -exports.makeGetter = function makeGetter(name) { - return () => - new Promise((resolve, reject) => { - ipc.once(`get-success-${name}`, (event, error, value) => { - if (error) { - return reject(error); - } - - return resolve(value); - }); - ipc.send(`get-${name}`); - }); -}; - -exports.makeSetter = function makeSetter(name) { - return value => - new Promise((resolve, reject) => { - ipc.once(`set-success-${name}`, (event, error) => { - if (error) { - return reject(error); - } - - return resolve(); - }); - ipc.send(`set-${name}`, value); - }); -}; diff --git a/screenShare_preload.js b/screenShare_preload.js index 70f40de813ba..c347ddfdb79c 100644 --- a/screenShare_preload.js +++ b/screenShare_preload.js @@ -8,6 +8,9 @@ const ReactDOM = require('react-dom'); const url = require('url'); const { ipcRenderer } = require('electron'); +// It is important to call this as early as possible +require('./ts/windows/context'); + const i18n = require('./js/modules/i18n'); const { getEnvironment, @@ -18,10 +21,6 @@ const { CallingScreenSharingController, } = require('./ts/components/CallingScreenSharingController'); -const { Context: SignalContext } = require('./ts/context'); - -window.SignalContext = new SignalContext(ipcRenderer); - const config = url.parse(window.location.toString(), true).query; const { locale } = config; const localeMessages = ipcRenderer.sendSync('locale-data'); diff --git a/settings.html b/settings.html index 637fb25d6abe..93ce26b8deb7 100644 --- a/settings.html +++ b/settings.html @@ -1,4 +1,4 @@ - + @@ -6,11 +6,7 @@ - - - - - - - - - - + +
+ + diff --git a/settings_preload.js b/settings_preload.js deleted file mode 100644 index 7f0534e1b2ad..000000000000 --- a/settings_preload.js +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2018-2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -/* global window */ - -const { ipcRenderer } = require('electron'); - -const url = require('url'); -const i18n = require('./js/modules/i18n'); -const { - getEnvironment, - setEnvironment, - parseEnvironment, -} = require('./ts/environment'); - -const config = url.parse(window.location.toString(), true).query; -const { locale } = config; -const localeMessages = ipcRenderer.sendSync('locale-data'); -setEnvironment(parseEnvironment(config.environment)); - -const { Context: SignalContext } = require('./ts/context'); - -window.SignalContext = new SignalContext(ipcRenderer); - -window.platform = process.platform; -window.theme = config.theme; -window.i18n = i18n.setup(locale, localeMessages); -window.appStartInitialSpellcheckSetting = - config.appStartInitialSpellcheckSetting === 'true'; - -window.getEnvironment = getEnvironment; -window.getVersion = () => config.version; -window.getAppInstance = () => config.appInstance; - -// So far we're only using this for Signal.Types -const Signal = require('./js/modules/signal'); - -window.Signal = Signal.setup({ - Attachments: null, - userDataPath: null, - getRegionCode: () => null, -}); - -window.closeSettings = () => ipcRenderer.send('close-settings'); - -window.getDeviceName = makeGetter('device-name'); - -window.getThemeSetting = makeGetter('theme-setting'); -window.setThemeSetting = makeSetter('theme-setting'); -window.getHideMenuBar = makeGetter('hide-menu-bar'); -window.setHideMenuBar = makeSetter('hide-menu-bar'); -window.getSystemTraySetting = makeGetter('system-tray-setting'); -window.setSystemTraySetting = makeSetter('system-tray-setting'); - -window.getSpellCheck = makeGetter('spell-check'); -window.setSpellCheck = makeSetter('spell-check'); - -window.getAutoLaunch = makeGetter('auto-launch'); -window.setAutoLaunch = makeSetter('auto-launch'); - -window.getAlwaysRelayCalls = makeGetter('always-relay-calls'); -window.setAlwaysRelayCalls = makeSetter('always-relay-calls'); - -window.getNotificationSetting = makeGetter('notification-setting'); -window.setNotificationSetting = makeSetter('notification-setting'); -window.getNotificationDrawAttention = makeGetter('notification-draw-attention'); -window.setNotificationDrawAttention = makeSetter('notification-draw-attention'); -window.getAudioNotification = makeGetter('audio-notification'); -window.setAudioNotification = makeSetter('audio-notification'); -window.getCallRingtoneNotification = makeGetter('call-ringtone-notification'); -window.setCallRingtoneNotification = makeSetter('call-ringtone-notification'); -window.getCallSystemNotification = makeGetter('call-system-notification'); -window.setCallSystemNotification = makeSetter('call-system-notification'); -window.getIncomingCallNotification = makeGetter('incoming-call-notification'); -window.setIncomingCallNotification = makeSetter('incoming-call-notification'); -window.getCountMutedConversations = makeGetter( - 'badge-count-muted-conversations' -); -window.setCountMutedConversations = makeSetter( - 'badge-count-muted-conversations' -); - -window.getMediaPermissions = makeGetter('media-permissions'); -window.setMediaPermissions = makeSetter('media-permissions'); -window.getMediaCameraPermissions = makeGetter('media-camera-permissions'); -window.setMediaCameraPermissions = makeSetter('media-camera-permissions'); - -window.isPrimary = makeGetter('is-primary'); -window.makeSyncRequest = makeGetter('sync-request'); -window.getLastSyncTime = makeGetter('sync-time'); -window.setLastSyncTime = makeSetter('sync-time'); -window.getUniversalExpireTimer = makeGetter('universal-expire-timer'); -window.setUniversalExpireTimer = makeSetter('universal-expire-timer'); - -window.deleteAllData = () => ipcRenderer.send('delete-all-data'); - -function makeGetter(name) { - return () => - new Promise((resolve, reject) => { - ipcRenderer.once(`get-success-${name}`, (event, error, value) => { - if (error) { - return reject(error); - } - - return resolve(value); - }); - ipcRenderer.send(`get-${name}`); - }); -} - -function makeSetter(name) { - return value => - new Promise((resolve, reject) => { - ipcRenderer.once(`set-success-${name}`, (event, error) => { - if (error) { - return reject(error); - } - - return resolve(); - }); - ipcRenderer.send(`set-${name}`, value); - }); -} - -window.Backbone = require('backbone'); -window.React = require('react'); -window.ReactDOM = require('react-dom'); - -require('./ts/backbone/views/whisper_view'); -require('./ts/backbone/views/toast_view'); -require('./ts/logging/set_up_renderer_logging').initialize(); diff --git a/sticker-creator/preload.js b/sticker-creator/preload.js index 67f35a717e37..e10da9a97170 100644 --- a/sticker-creator/preload.js +++ b/sticker-creator/preload.js @@ -10,6 +10,10 @@ const config = require('url').parse(window.location.toString(), true).query; const { noop, uniqBy } = require('lodash'); const pMap = require('p-map'); const client = require('@signalapp/signal-client'); + +// It is important to call this as early as possible +require('../ts/windows/context'); + const { deriveStickerPackKey } = require('../ts/Crypto'); const { SignalService: Proto } = require('../ts/protobuf'); const { @@ -17,20 +21,16 @@ const { setEnvironment, parseEnvironment, } = require('../ts/environment'); -const { makeGetter } = require('../preload_utils'); +const { createSetting } = require('../ts/util/preload'); const { dialog } = remote; -const { Context: SignalContext } = require('../ts/context'); - const STICKER_SIZE = 512; const MIN_STICKER_DIMENSION = 10; const MAX_STICKER_DIMENSION = STICKER_SIZE; const MAX_WEBP_STICKER_BYTE_LENGTH = 100 * 1024; const MAX_ANIMATED_STICKER_BYTE_LENGTH = 300 * 1024; -window.SignalContext = new SignalContext(ipc); - setEnvironment(parseEnvironment(config.environment)); window.sqlInitializer = require('../ts/sql/initialize'); @@ -274,10 +274,10 @@ async function encrypt(data, key, iv) { return ciphertext; } -const getThemeSetting = makeGetter('theme-setting'); +const getThemeSetting = createSetting('theme-setting'); async function resolveTheme() { - const theme = (await getThemeSetting()) || 'system'; + const theme = (await getThemeSetting.getValue()) || 'system'; if (process.platform === 'darwin' && theme === 'system') { const { theme: nativeTheme } = window.SignalContext.nativeThemeListener; return nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index b54e72487588..8ec66f64dfea 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -8755,17 +8755,6 @@ button.module-image__border-overlay:focus { ); } } -.module-avatar-popup__item__icon-colors { - @include light-theme { - @include color-svg( - '../images/icons/v2/color-outline-24.svg', - $color-gray-75 - ); - } - @include dark-theme { - @include color-svg('../images/icons/v2/color-solid-24.svg', $color-gray-15); - } -} .module-avatar-popup__item__icon-archive { @include light-theme { @include color-svg( diff --git a/stylesheets/components/Checkbox.scss b/stylesheets/components/Checkbox.scss new file mode 100644 index 000000000000..130284f5bc41 --- /dev/null +++ b/stylesheets/components/Checkbox.scss @@ -0,0 +1,30 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.Checkbox { + &__container { + align-items: center; + display: flex; + + input { + height: 18px; + width: 18px; + } + } + + &__checkbox { + height: 18px; + margin-right: 20px; + width: 18px; + } + + &__description { + @include font-subtitle; + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-gray-25; + } + } +} diff --git a/stylesheets/components/Preferences.scss b/stylesheets/components/Preferences.scss new file mode 100644 index 000000000000..ae3361857582 --- /dev/null +++ b/stylesheets/components/Preferences.scss @@ -0,0 +1,241 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +@mixin preferences-icon($light_svg, $dark_svg) { + &:before { + @include light-theme { + @include color-svg($light_svg, $color-gray-75); + } + @include dark-theme { + @include color-svg($dark_svg, $color-gray-15); + } + } +} + +.Preferences { + display: flex; + overflow: hidden; + @include light-theme { + background: $color-white; + } + @include dark-theme { + background: $color-gray-95; + } + + &__page-selector { + padding-top: 76px; + min-width: 240px; + @include light-theme { + background: $color-gray-02; + } + @include dark-theme { + background: $color-gray-80; + } + } + + &__padding { + padding: 0 24px; + } + + &__button { + @include button-reset; + @include font-body-1; + align-items: center; + display: flex; + height: 48px; + width: 100%; + padding: 14px 0; + + &--selected { + @include light-theme { + background: $color-gray-15; + } + @include dark-theme { + background: $color-gray-65; + } + } + + &:before { + content: ''; + display: block; + height: 22px; + margin-left: 18px; + margin-right: 14px; + width: 22px; + } + + &--general { + @include preferences-icon( + '../images/icons/v2/settings-outline-16.svg', + '../images/icons/v2/settings-outline-16.svg' + ); + } + + &--appearance { + @include preferences-icon( + '../images/icons/v2/appearance-outline-24.svg', + '../images/icons/v2/appearance-solid-24.svg' + ); + } + + &--chats { + @include preferences-icon( + '../images/icons/v2/message-outline-24.svg', + '../images/icons/v2/message-solid-24.svg' + ); + } + + &--calls { + @include preferences-icon( + '../images/icons/v2/video-outline-24.svg', + '../images/icons/v2/video-solid-24.svg' + ); + } + + &--notifications { + @include preferences-icon( + '../images/icons/v2/bell-outline-24.svg', + '../images/icons/v2/bell-solid-24.svg' + ); + } + + &--privacy { + @include preferences-icon( + '../images/icons/v2/lock-outline-24.svg', + '../images/icons/v2/lock-solid-24.svg' + ); + &:before { + -webkit-mask-size: 75%; + } + } + } + + &__settings-pane { + height: 100vh; + overflow: scroll; + width: 100%; + } + + &__title { + @include font-body-1-bold; + align-items: center; + display: flex; + height: 76px; + padding: 42px 0 14px 0; + text-align: center; + + &--header { + flex-grow: 1; + text-align: center; + } + } + + &__settings-row { + padding-bottom: 12px; + + h3 { + @include font-body-1-bold; + margin: 0; + margin-bottom: 8px; + } + } + + &__settings-row:not(:last-child) { + border-bottom: 1px solid $color-gray-15; + @include light-theme { + border-color: $color-gray-15; + } + @include dark-theme { + border-color: $color-gray-65; + } + margin-bottom: 24px; + } + + &__control { + align-items: center; + display: flex; + justify-content: space-between; + min-height: 48px; + padding: 4px 24px; + + &--key { + flex-grow: 1; + padding-right: 20px; + } + + &--value { + color: $color-gray-45; + } + + &--clickable { + @include button-reset; + padding: 4px 24px; + width: 100%; + &:hover { + @include light-theme { + background: $color-gray-02; + } + @include dark-theme { + background: $color-gray-80; + } + } + } + } + + &__checkbox { + padding: 10px 24px; + } + + &__description { + @include font-subtitle; + @include light-theme { + color: $color-gray-60; + } + @include dark-theme { + color: $color-gray-25; + } + + &--error { + color: $color-accent-red !important; + } + } + + &__select { + width: 100%; + } + + &__select-title { + display: block; + margin-bottom: 8px; + } + + &__right-button { + display: flex; + justify-content: flex-end; + min-width: 120px; + } + + &__back-icon { + @include button-reset; + + display: inline-block; + height: 24px; + margin-left: 12px; + min-width: 24px; + vertical-align: text-bottom; + width: 24px; + + @include light-theme { + @include color-svg( + '../images/icons/v2/chevron-left-24.svg', + $color-gray-90 + ); + } + @include dark-theme { + @include color-svg( + '../images/icons/v2/chevron-left-24.svg', + $color-gray-02 + ); + } + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index b0bea0f931d0..957a8fd35139 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -43,6 +43,7 @@ @import './components/CallingScreenSharingController.scss'; @import './components/CallingSelectPresentingSourcesModal.scss'; @import './components/ChatColorPicker.scss'; +@import './components/Checkbox.scss'; @import './components/CompositionArea.scss'; @import './components/ContactName.scss'; @import './components/ContactPill.scss'; @@ -65,6 +66,7 @@ @import './components/MessageAudio.scss'; @import './components/MessageDetail.scss'; @import './components/Modal.scss'; +@import './components/Preferences.scss'; @import './components/ProfileEditor.scss'; @import './components/SafetyNumberChangeDialog.scss'; @import './components/SafetyNumberViewer.scss'; diff --git a/ts/background.ts b/ts/background.ts index 2f6b13864b73..5373f5a939b9 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1,6 +1,7 @@ // Copyright 2020-2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { webFrame } from 'electron'; import { isNumber, noop } from 'lodash'; import { bindActionCreators } from 'redux'; import { render, unstable_batchedUpdates as batchedUpdates } from 'react-dom'; @@ -64,7 +65,6 @@ import { EnvelopeEvent, } from './textsecure/messageReceiverEvents'; import type { WebAPIType } from './textsecure/WebAPI'; -import * as universalExpireTimer from './util/universalExpireTimer'; import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation'; import { getSendOptions } from './util/getSendOptions'; import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff'; @@ -88,13 +88,11 @@ import { SendStatus, } from './messages/MessageSendState'; import * as AttachmentDownloads from './messageModifiers/AttachmentDownloads'; -import { - SystemTraySetting, - parseSystemTraySetting, -} from './types/SystemTraySetting'; import * as Stickers from './types/Stickers'; import { SignalService as Proto } from './protobuf'; import { onRetryRequest, onDecryptionError } from './util/handleRetry'; +import { themeChanged } from './shims/themeChanged'; +import { createIPCEvents } from './util/createIPCEvents'; const MAX_ATTACHMENT_DOWNLOAD_AGE = 3600 * 72 * 1000; @@ -582,7 +580,11 @@ export async function startApp(): Promise { window.log.info('Storage fetch'); window.storage.fetch(); - function mapOldThemeToNew(theme: Readonly) { + function mapOldThemeToNew( + theme: Readonly< + 'system' | 'light' | 'dark' | 'android' | 'ios' | 'android-dark' + > + ): 'system' | 'light' | 'dark' { switch (theme) { case 'dark': case 'light': @@ -611,129 +613,7 @@ export async function startApp(): Promise { cleanupSessionResets(); // These make key operations available to IPC handlers created in preload.js - window.Events = { - getDeviceName: () => window.textsecure.storage.user.getDeviceName(), - - getThemeSetting: (): 'light' | 'dark' | 'system' => - window.storage.get( - 'theme-setting', - window.platform === 'darwin' ? 'system' : 'light' - ), - setThemeSetting: (value: 'light' | 'dark' | 'system') => { - window.storage.put('theme-setting', value); - onChangeTheme(); - }, - getHideMenuBar: () => window.storage.get('hide-menu-bar'), - setHideMenuBar: (value: boolean) => { - window.storage.put('hide-menu-bar', value); - window.setAutoHideMenuBar(value); - window.setMenuBarVisibility(!value); - }, - getSystemTraySetting: (): SystemTraySetting => - parseSystemTraySetting(window.storage.get('system-tray-setting')), - setSystemTraySetting: (value: Readonly) => { - window.storage.put('system-tray-setting', value); - window.updateSystemTraySetting(value); - }, - - getNotificationSetting: () => - window.storage.get('notification-setting', 'message'), - setNotificationSetting: (value: 'message' | 'name' | 'count' | 'off') => - window.storage.put('notification-setting', value), - getNotificationDrawAttention: () => - window.storage.get('notification-draw-attention', true), - setNotificationDrawAttention: (value: boolean) => - window.storage.put('notification-draw-attention', value), - getAudioNotification: () => window.storage.get('audio-notification'), - setAudioNotification: (value: boolean) => - window.storage.put('audio-notification', value), - getCountMutedConversations: () => - window.storage.get('badge-count-muted-conversations', false), - setCountMutedConversations: (value: boolean) => { - window.storage.put('badge-count-muted-conversations', value); - window.Whisper.events.trigger('updateUnreadCount'); - }, - getCallRingtoneNotification: () => - window.storage.get('call-ringtone-notification', true), - setCallRingtoneNotification: (value: boolean) => - window.storage.put('call-ringtone-notification', value), - getCallSystemNotification: () => - window.storage.get('call-system-notification', true), - setCallSystemNotification: (value: boolean) => - window.storage.put('call-system-notification', value), - getIncomingCallNotification: () => - window.storage.get('incoming-call-notification', true), - setIncomingCallNotification: (value: boolean) => - window.storage.put('incoming-call-notification', value), - - getSpellCheck: () => window.storage.get('spell-check', true), - setSpellCheck: (value: boolean) => { - window.storage.put('spell-check', value); - }, - - getAlwaysRelayCalls: () => window.storage.get('always-relay-calls'), - setAlwaysRelayCalls: (value: boolean) => - window.storage.put('always-relay-calls', value), - - getAutoLaunch: () => window.getAutoLaunch(), - setAutoLaunch: (value: boolean) => window.setAutoLaunch(value), - - isPrimary: () => window.textsecure.storage.user.getDeviceId() === 1, - getSyncRequest: () => - new Promise((resolve, reject) => { - const FIVE_MINUTES = 5 * 60 * 60 * 1000; - const syncRequest = window.getSyncRequest(FIVE_MINUTES); - syncRequest.addEventListener('success', () => resolve()); - syncRequest.addEventListener('timeout', () => - reject(new Error('timeout')) - ); - }), - getLastSyncTime: () => window.storage.get('synced_at'), - setLastSyncTime: (value: number) => - window.storage.put('synced_at', value), - getUniversalExpireTimer: (): number | undefined => { - return universalExpireTimer.get(); - }, - setUniversalExpireTimer: async ( - newValue: number | undefined - ): Promise => { - await universalExpireTimer.set(newValue); - - // Update account in Storage Service - const conversationId = window.ConversationController.getOurConversationIdOrThrow(); - const account = window.ConversationController.get(conversationId); - assert(account, "Account wasn't found"); - - account.captureChange('universalExpireTimer'); - - // Add a notification to the currently open conversation - const state = window.reduxStore.getState(); - const selectedId = state.conversations.selectedConversationId; - if (selectedId) { - const conversation = window.ConversationController.get(selectedId); - assert(conversation, "Conversation wasn't found"); - - await conversation.updateLastMessage(); - } - }, - - addDarkOverlay: () => { - if ($('.dark-overlay').length) { - return; - } - $(document.body).prepend('
'); - $('.dark-overlay').on('click', () => $('.dark-overlay').remove()); - }, - removeDarkOverlay: () => $('.dark-overlay').remove(), - showKeyboardShortcuts: () => window.showKeyboardShortcuts(), - - deleteAllData: async () => { - await window.sqlInitializer.goBackToMainProcess(); - - const clearDataView = new window.Whisper.ClearDataView().render(); - $('body').append(clearDataView.el); - }, - + window.Events = createIPCEvents({ shutdown: async () => { window.log.info('background/shutdown'); // Stop background processing @@ -763,112 +643,9 @@ export async function startApp(): Promise { // Shut down the data interface cleanly await window.Signal.Data.shutdown(); }, + }); - showStickerPack: (packId: string, key: string) => { - // We can get these events even if the user has never linked this instance. - if (!window.Signal.Util.Registration.everDone()) { - window.log.warn('showStickerPack: Not registered, returning early'); - return; - } - if (window.isShowingModal) { - window.log.warn( - 'showStickerPack: Already showing modal, returning early' - ); - return; - } - try { - window.isShowingModal = true; - - // Kick off the download - Stickers.downloadEphemeralPack(packId, key); - - const props = { - packId, - onClose: async () => { - window.isShowingModal = false; - stickerPreviewModalView.remove(); - await Stickers.removeEphemeralPack(packId); - }, - }; - - const stickerPreviewModalView = new window.Whisper.ReactWrapperView({ - className: 'sticker-preview-modal-wrapper', - JSX: window.Signal.State.Roots.createStickerPreviewModal( - window.reduxStore, - props - ), - }); - } catch (error) { - window.isShowingModal = false; - window.log.error( - 'showStickerPack: Ran into an error!', - error && error.stack ? error.stack : error - ); - const errorView = new window.Whisper.ReactWrapperView({ - className: 'error-modal-wrapper', - Component: window.Signal.Components.ErrorModal, - props: { - onClose: () => { - errorView.remove(); - }, - }, - }); - } - }, - showGroupViaLink: async (hash: string) => { - // We can get these events even if the user has never linked this instance. - if (!window.Signal.Util.Registration.everDone()) { - window.log.warn('showGroupViaLink: Not registered, returning early'); - return; - } - if (window.isShowingModal) { - window.log.warn( - 'showGroupViaLink: Already showing modal, returning early' - ); - return; - } - try { - await window.Signal.Groups.joinViaLink(hash); - } catch (error) { - window.log.error( - 'showGroupViaLink: Ran into an error!', - error && error.stack ? error.stack : error - ); - const errorView = new window.Whisper.ReactWrapperView({ - className: 'error-modal-wrapper', - Component: window.Signal.Components.ErrorModal, - props: { - title: window.i18n('GroupV2--join--general-join-failure--title'), - description: window.i18n('GroupV2--join--general-join-failure'), - onClose: () => { - errorView.remove(); - }, - }, - }); - } - window.isShowingModal = false; - }, - - unknownSignalLink: () => { - window.log.warn('unknownSignalLink: Showing error dialog'); - const errorView = new window.Whisper.ReactWrapperView({ - className: 'error-modal-wrapper', - Component: window.Signal.Components.ErrorModal, - props: { - description: window.i18n('unknown-sgnl-link'), - onClose: () => { - errorView.remove(); - }, - }, - }); - }, - - installStickerPack: async (packId: string, key: string) => { - Stickers.downloadStickerPack(packId, key, { - finalStatus: 'installed', - }); - }, - }; + webFrame.setZoomFactor(window.Events.getZoomFactor()); // How long since we were last running? const lastHeartbeat = window.storage.get('lastHeartbeat', 0); @@ -2373,7 +2150,7 @@ export async function startApp(): Promise { 'theme-setting', await window.Events.getThemeSetting() ); - onChangeTheme(); + themeChanged(); } const syncRequest = window.getSyncRequest(); window.Whisper.events.trigger('contactsync:begin'); @@ -2457,18 +2234,7 @@ export async function startApp(): Promise { } } - function onChangeTheme() { - if (window.reduxActions && window.reduxActions.user) { - const theme = window.Events.getThemeSetting(); - window.reduxActions.user.userChanged({ - theme: theme === 'system' ? window.systemTheme : theme, - }); - } - } - - window.SignalContext.nativeThemeListener.subscribe(() => { - onChangeTheme(); - }); + window.SignalContext.nativeThemeListener.subscribe(themeChanged); const FIVE_MINUTES = 5 * 60 * 1000; diff --git a/ts/components/AvatarPopup.stories.tsx b/ts/components/AvatarPopup.stories.tsx index 341940292c5f..d8f20fb0d051 100644 --- a/ts/components/AvatarPopup.stories.tsx +++ b/ts/components/AvatarPopup.stories.tsx @@ -41,7 +41,6 @@ const createProps = (overrideProps: Partial = {}): Props => ({ name: text('name', overrideProps.name || ''), noteToSelf: boolean('noteToSelf', overrideProps.noteToSelf || false), onEditProfile: action('onEditProfile'), - onSetChatColor: action('onSetChatColor'), onViewArchive: action('onViewArchive'), onViewPreferences: action('onViewPreferences'), phoneNumber: text('phoneNumber', overrideProps.phoneNumber || ''), diff --git a/ts/components/AvatarPopup.tsx b/ts/components/AvatarPopup.tsx index 99314232687d..757132bfa580 100644 --- a/ts/components/AvatarPopup.tsx +++ b/ts/components/AvatarPopup.tsx @@ -13,7 +13,6 @@ export type Props = { readonly i18n: LocalizerType; onEditProfile: () => unknown; - onSetChatColor: () => unknown; onViewPreferences: () => unknown; onViewArchive: () => unknown; @@ -30,7 +29,6 @@ export const AvatarPopup = (props: Props): JSX.Element => { phoneNumber, title, onEditProfile, - onSetChatColor, onViewPreferences, onViewArchive, style, @@ -79,21 +77,6 @@ export const AvatarPopup = (props: Props): JSX.Element => { {i18n('mainMenuSettings')} - + + } + /> + + )} + + ); + } else if (page === Page.Calls) { + settings = ( + <> +
+
+ {i18n('Preferences__button--calls')} +
+
+ + + + + + + + ({ + text: localizeDefault(i18n, device.name), + value: device.index, + })) + : [ + { + text: i18n( + 'callingDeviceSelection__select--no-device' + ), + value: 'undefined', + }, + ] + } + value={selectedMicrophone?.index} + /> + + } + right={
} + /> + + + { + if ( + value === String(universalExpireTimer) || + value === '-1' + ) { + setShowDisappearingTimerDialog(true); + return; + } + + onUniversalExpireTimerChange(parseInt(value, 10)); + }} + options={DEFAULT_DURATIONS_IN_SECONDS.map(seconds => { + const text = formatExpirationTimer(i18n, seconds, { + capitalizeOff: true, + }); + return { + value: seconds, + text, + }; + }).concat([ + { + value: isCustomDisappearingMessageValue + ? universalExpireTimer + : -1, + text: isCustomDisappearingMessageValue + ? formatExpirationTimer(i18n, universalExpireTimer) + : i18n('selectedCustomDisappearingTimeOption'), + }, + ])} + value={universalExpireTimer} + /> + } + /> + + + +
{i18n('clearDataHeader')}
+
+ {i18n('clearDataExplanation')} +
+ + } + right={ +
+ +
+ } + /> +
+ + ); + } else if (page === Page.ChatColor) { + settings = ( + <> +
+
+ + + ); + } + + return ( +
+
+ + + + + + +
+
{settings}
+
+ ); +}; + +const SettingsRow = ({ + children, + title, +}: { + children: ReactNode; + title?: string; +}): JSX.Element => { + return ( +
+ {title &&

{title}

} + {children} +
+ ); +}; + +const Control = ({ + left, + onClick, + right, +}: { + left: ReactNode; + onClick?: () => unknown; + right: ReactNode; +}): JSX.Element => { + const content = ( + <> +
{left}
+
{right}
+ + ); + + if (onClick) { + return ( + + ); + } + + return
{content}
; +}; + +function localizeDefault(i18n: LocalizerType, deviceLabel: string): string { + return deviceLabel.toLowerCase().startsWith('default') + ? deviceLabel.replace( + /default/i, + i18n('callingDeviceSelection__select--default') + ) + : deviceLabel; +} diff --git a/ts/components/Select.tsx b/ts/components/Select.tsx index 65d2bcd43cc7..29c967b2259f 100644 --- a/ts/components/Select.tsx +++ b/ts/components/Select.tsx @@ -11,34 +11,48 @@ export type Option = Readonly<{ }>; export type PropsType = Readonly<{ + disabled?: boolean; moduleClassName?: string; + name?: string; options: ReadonlyArray