From ad4387803b4d85a3f24d90b6d38f0ad5ff618fb2 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Tue, 3 Jul 2018 15:33:50 -0700 Subject: [PATCH] New media permission, show dialog when not enabled for voice msg UI now in separate renderer: - the permissions popup - settings dialog - debug log dialog - about window Couple bug fixes: - About Window: Fix 'escape' to close window - Remove outdated dist/copy tasks from Gruntfile Eslintified settings_view.js --- .eslintignore | 1 + Gruntfile.js | 8 +- _locales/en/messages.json | 24 +++ about.html | 98 +++++------- about_preload.js | 4 +- app/permissions.js | 36 +++-- background.html | 111 +------------- debug_log.html | 53 +++++++ debug_log_preload.js | 19 +++ js/about_start.js | 24 +++ js/background.js | 58 +++++-- js/debug_log_start.js | 13 ++ js/modules/signal.js | 60 +++++--- js/notifications.js | 2 + js/permissions_popup_start.js | 19 +++ js/settings_start.js | 29 ++++ js/views/app_view.js | 2 - js/views/clear_data_view.js | 69 +++++++++ js/views/debug_log_view.js | 5 +- js/views/inbox_view.js | 7 - js/views/recorder_view.js | 25 ++- js/views/settings_view.js | 224 ++++++++++++--------------- main.js | 268 ++++++++++++++++++++++++++++++--- package.json | 6 + permissions_popup.html | 38 +++++ permissions_popup_preload.js | 45 ++++++ preload.js | 87 ++++++++++- settings.html | 116 ++++++++++++++ settings_preload.js | 76 ++++++++++ stylesheets/_conversation.scss | 18 +++ stylesheets/_global.scss | 11 ++ stylesheets/_settings.scss | 16 +- test/index.html | 3 +- 33 files changed, 1192 insertions(+), 383 deletions(-) create mode 100644 debug_log.html create mode 100644 debug_log_preload.js create mode 100644 js/about_start.js create mode 100644 js/debug_log_start.js create mode 100644 js/permissions_popup_start.js create mode 100644 js/settings_start.js create mode 100644 js/views/clear_data_view.js create mode 100644 permissions_popup.html create mode 100644 permissions_popup_preload.js create mode 100644 settings.html create mode 100644 settings_preload.js diff --git a/.eslintignore b/.eslintignore index ca4534c62dec..cc4758e2c4e3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -40,6 +40,7 @@ ts/**/*.js !js/expiring_messages.js !js/views/attachment_view.js !js/views/backbone_wrapper_view.js +!js/views/clear_data_view.js !js/views/conversation_search_view.js !js/views/conversation_view.js !js/views/debug_log_view.js diff --git a/Gruntfile.js b/Gruntfile.js index 6ca9e4397dd2..8531037b42e9 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -105,12 +105,15 @@ module.exports = function(grunt) { '!js/expiring_messages.js', '!js/modules/**/*.js', '!js/Mp3LameEncoder.min.js', + '!js/settings_start.js', '!js/signal_protocol_store.js', + '!js/views/clear_data_view.js', '!js/views/conversation_search_view.js', '!js/views/conversation_view.js', '!js/views/debug_log_view.js', '!js/views/file_input_view.js', '!js/views/message_view.js', + '!js/views/settings_view.js', '!js/models/conversations.js', '!js/models/messages.js', '!js/WebAudioRecorderMp3.js', @@ -134,10 +137,6 @@ module.exports = function(grunt) { }, }, watch: { - dist: { - files: ['<%= dist.src %>', '<%= dist.res %>'], - tasks: ['copy_dist'], - }, libtextsecure: { files: ['./libtextsecure/*.js', './libtextsecure/storage/*.js'], tasks: ['concat:libtextsecure'], @@ -461,7 +460,6 @@ module.exports = function(grunt) { grunt.registerTask('dev', ['default', 'watch']); grunt.registerTask('lint', ['jshint']); grunt.registerTask('test', ['unit-tests', 'lib-unit-tests']); - grunt.registerTask('copy_dist', ['gitinfo', 'copy:res', 'copy:src']); grunt.registerTask('date', ['gitinfo', 'getExpireTime']); grunt.registerTask('default', [ 'exec:build-protobuf', diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 3cbd56ce04a3..918ee0a58638 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -514,6 +514,10 @@ "message": "Report an Issue", "description": "Item under the Help menu, takes you to GitHub new issue form (title case)" }, + "signalDesktopPreferences": { + "message": "Signal Desktop Preferences", + "description": "Title of the window that pops up with Signal Desktop preferences in it" + }, "aboutSignalDesktop": { "message": "About Signal Desktop", "description": "Item under the Help menu, which opens a small about window" @@ -610,6 +614,18 @@ } } }, + "audioPermissionNeeded": { + "message": "To send audio messages, allow Signal Desktop to access your microphone.", + "description": "Shown if the user attempts to send an audio message without audio permssions turned on" + }, + "allowAccess": { + "message": "Allow Access", + "description": "Button shown in popup asking to enable microphon/video permissions to send audio messages" + }, + "showSettings": { + "message": "Show Settings", + "description": "A button shown in dialog requesting the user to turn on audio permissions" + }, "audio": { "message": "Audio", "description": "Shown in a quotation of a message containing an audio attachment if no text was originally provided with that attachment" @@ -795,6 +811,14 @@ "message": "Theme", "description": "Header for theme settings" }, + "permissions": { + "message": "Permissions", + "description": "Header for permissions section of settings" + }, + "mediaPermissionsDescription": { + "message": "Allow access to camera and microphone", + "description": "Description of the media permission description" + }, "clearDataHeader": { "message": "Clear Data", "description": "Header in the settings dialog for the section dealing with data deletion" diff --git a/about.html b/about.html index b638426f3c67..f291edfd832f 100644 --- a/about.html +++ b/about.html @@ -1,68 +1,50 @@ - - + a { + color: white; + } + + - - -
- -
-
- -
-
- signal.org -
-
-
- Terms & Privacy Policy -
- - - - +
+
+
+ signal.org +
+
+
+ Terms & Privacy Policy +
+ + - diff --git a/about_preload.js b/about_preload.js index 0c5b164fda66..1c76457d1c23 100644 --- a/about_preload.js +++ b/about_preload.js @@ -10,6 +10,8 @@ window.getEnvironment = () => config.environment; window.getVersion = () => config.version; window.getAppInstance = () => config.appInstance; -window.closeAbout = () => ipc.send('close-about'); +window.closeAbout = () => ipcRenderer.send('close-about'); window.i18n = i18n.setup(locale, localeMessages); + +require('./js/logging'); diff --git a/app/permissions.js b/app/permissions.js index 6493034d539f..b56efc3aae43 100644 --- a/app/permissions.js +++ b/app/permissions.js @@ -4,9 +4,11 @@ const PERMISSIONS = { // Allowed fullscreen: true, // required to show videos in full-screen - media: true, // required for access to microphone, used for voice notes notifications: true, // required to show OS notifications for new messages + // Off by default, can be enabled by user + media: false, // required for access to microphone, used for voice notes + // Not allowed geolocation: false, midiSysex: false, @@ -14,18 +16,32 @@ const PERMISSIONS = { pointerLock: false, }; -function _permissionHandler(webContents, permission, callback) { - if (PERMISSIONS[permission]) { - console.log(`Approving request for permission '${permission}'`); - return callback(true); - } +function _createPermissionHandler(userConfig) { + return (webContents, permission, callback) => { + // We default 'media' permission to false, but the user can override that + if (permission === 'media' && userConfig.get('mediaPermissions')) { + return true; + } - console.log(`Denying request for permission '${permission}'`); - return callback(false); + if (PERMISSIONS[permission]) { + console.log(`Approving request for permission '${permission}'`); + return callback(true); + } + + console.log(`Denying request for permission '${permission}'`); + return callback(false); + }; } -function installPermissionsHandler({ session }) { - session.defaultSession.setPermissionRequestHandler(_permissionHandler); +function installPermissionsHandler({ session, userConfig }) { + // Setting the permission request handler to null first forces any permissions to be + // requested again. Without this, revoked permissions might still be available if + // they've already been used successfully. + session.defaultSession.setPermissionRequestHandler(null); + + session.defaultSession.setPermissionRequestHandler( + _createPermissionHandler(userConfig) + ); } module.exports = { diff --git a/background.html b/background.html index f9a4aeb1befc..17f724d6a529 100644 --- a/background.html +++ b/background.html @@ -519,114 +519,6 @@ {{ learnMore }} - - - - - @@ -929,7 +820,6 @@ - @@ -937,6 +827,7 @@ + diff --git a/debug_log.html b/debug_log.html new file mode 100644 index 000000000000..34c0ec950df3 --- /dev/null +++ b/debug_log.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + diff --git a/debug_log_preload.js b/debug_log_preload.js new file mode 100644 index 000000000000..158e602bdb60 --- /dev/null +++ b/debug_log_preload.js @@ -0,0 +1,19 @@ +const { ipcRenderer } = require('electron'); +const url = require('url'); +const i18n = require('./js/modules/i18n'); + +const config = url.parse(window.location.toString(), true).query; +const { locale } = config; +const localeMessages = ipcRenderer.sendSync('locale-data'); + +window.i18n = i18n.setup(locale, localeMessages); + +// got.js appears to need this to successfully submit debug logs to the cloud +window.nodeSetImmediate = setImmediate; + +window.getNodeVersion = () => config.node_version; +window.getEnvironment = () => config.environment; + +require('./js/logging'); + +window.closeDebugLog = () => ipcRenderer.send('close-debug-log'); diff --git a/js/about_start.js b/js/about_start.js new file mode 100644 index 000000000000..01fe36f10de3 --- /dev/null +++ b/js/about_start.js @@ -0,0 +1,24 @@ +// Add version +$('.version').text(`v${window.getVersion()}`); + +// Add debugging metadata - environment if not production, app instance name +const states = []; + +if (window.getEnvironment() !== 'production') { + states.push(window.getEnvironment()); +} +if (window.getAppInstance()) { + states.push(window.getAppInstance()); +} + +$('.environment').text(states.join(' - ')); + +// Install the 'dismiss with escape key' handler +$(document).on('keyup', function(e) { + if (e.keyCode === 27) { + window.closeAbout(); + } +}); + +// Localize the privacy string +$('.privacy').text(window.i18n('privacyPolicy')); diff --git a/js/background.js b/js/background.js index e0a7930c01a1..43b11d5ef96d 100644 --- a/js/background.js +++ b/js/background.js @@ -123,6 +123,54 @@ } first = false; + // These make key operations available to IPC handlers created in preload.js + window.Events = { + getDeviceName: () => textsecure.storage.user.getDeviceName(), + + getThemeSetting: () => storage.get('theme-setting'), + setThemeSetting: value => { + storage.put('theme-setting', value); + onChangeTheme(); + }, + getHideMenuBar: () => storage.get('hide-menu-bar'), + setHideMenuBar: value => { + storage.get('hide-menu-bar', value); + window.setAutoHideMenuBar(value); + window.setMenuBarVisibility(!value); + }, + + getNotificationSetting: () => + storage.get('notification-setting', 'message'), + setNotificationSetting: value => + storage.get('notification-setting', value), + getAudioNotification: () => storage.get('audio-notification'), + setAudioNotification: value => storage.put('audio-notification', value), + + // eslint-disable-next-line eqeqeq + isPrimary: () => textsecure.storage.user.getDeviceId() == '1', + getSyncRequest: () => + new Promise((resolve, reject) => { + const syncRequest = window.getSyncRequest(); + syncRequest.addEventListener('success', resolve); + syncRequest.addEventListener('timeout', reject); + }), + getLastSyncTime: () => storage.get('synced_at'), + setLastSyncTime: value => storage.put('synced_at', value), + + addDarkOverlay: () => { + if ($('.dark-overlay').length) { + return; + } + $(document.body).prepend('
'); + $('.dark-overlay').on('click', () => $('.dark-overlay').remove()); + }, + removeDarkOverlay: () => $('.dark-overlay').remove(), + deleteAllData: () => { + const clearDataView = new window.Whisper.ClearDataView().render(); + $('body').append(clearDataView.el); + }, + }; + try { await ConversationController.load(); } finally { @@ -208,16 +256,6 @@ Whisper.events.on('showDebugLog', () => { appView.openDebugLog(); }); - Whisper.events.on('showSettings', () => { - if (!appView || !appView.inboxView) { - console.log( - "background: Event: 'showSettings':" + - ' Expected `appView.inboxView` to exist.' - ); - return; - } - appView.inboxView.showSettings(); - }); Whisper.events.on('unauthorized', () => { appView.inboxView.networkStatusView.update(); }); diff --git a/js/debug_log_start.js b/js/debug_log_start.js new file mode 100644 index 000000000000..e17ab912e89c --- /dev/null +++ b/js/debug_log_start.js @@ -0,0 +1,13 @@ +$(document).on('keyup', function(e) { + if (e.keyCode === 27) { + window.closeDebugLog(); + } +}); + +const $body = $(document.body); + +// got.js appears to need this to successfully submit debug logs to the cloud +window.setImmediate = window.nodeSetImmediate; + +window.view = new Whisper.DebugLogView(); +window.view.$el.appendTo($body); diff --git a/js/modules/signal.js b/js/modules/signal.js index 471dfed9e92f..ae2dceef9e6f 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -55,32 +55,23 @@ const Initialization = require('./views/initialization'); const { IdleDetector } = require('./idle_detector'); const MessageDataMigrator = require('./messages_data_migrator'); -exports.setup = (options = {}) => { - const { Attachments, userDataPath, getRegionCode } = options; - - const Components = { - ContactDetail, - ContactName, - ConversationTitle, - EmbeddedContact, - Emojify, - Lightbox, - LightboxGallery, - MediaGallery, - MessageBody, - Types: { - Message: MediaGalleryMessage, - }, - Quote, - }; +function initializeMigrations({ + Attachments, + userDataPath, + Type, + getRegionCode, +}) { + if (!Attachments) { + return null; + } const attachmentsPath = Attachments.getPath(userDataPath); const readAttachmentData = Attachments.createReader(attachmentsPath); - const loadAttachmentData = AttachmentType.loadData(readAttachmentData); + const loadAttachmentData = Type.loadData(readAttachmentData); - const Migrations = { + return { attachmentsPath, - deleteAttachmentData: AttachmentType.deleteData( + deleteAttachmentData: Type.deleteData( Attachments.createDeleter(attachmentsPath) ), getAbsoluteAttachmentPath: Attachments.createAbsolutePathGetter( @@ -100,6 +91,33 @@ exports.setup = (options = {}) => { Attachments.createWriterForExisting(attachmentsPath) ), }; +} + +exports.setup = (options = {}) => { + const { Attachments, userDataPath, getRegionCode } = options; + + const Migrations = initializeMigrations({ + Attachments, + userDataPath, + Type: AttachmentType, + getRegionCode, + }); + + const Components = { + ContactDetail, + ContactName, + ConversationTitle, + EmbeddedContact, + Emojify, + Lightbox, + LightboxGallery, + MediaGallery, + MessageBody, + Types: { + Message: MediaGalleryMessage, + }, + Quote, + }; const Types = { Attachment: AttachmentType, diff --git a/js/notifications.js b/js/notifications.js index b367c57b30f6..9d7f014e3554 100644 --- a/js/notifications.js +++ b/js/notifications.js @@ -134,6 +134,8 @@ break; } + console.log({ title, message, iconUrl }); + const shouldHideExpiringMessageBody = last.isExpiringMessage && Signal.OS.isMacOS(); if (shouldHideExpiringMessageBody) { diff --git a/js/permissions_popup_start.js b/js/permissions_popup_start.js new file mode 100644 index 000000000000..765f35b30a36 --- /dev/null +++ b/js/permissions_popup_start.js @@ -0,0 +1,19 @@ +$(document).on('keyup', function(e) { + if (e.keyCode === 27) { + window.closePermissionsPopup(); + } +}); + +const $body = $(document.body); + +window.view = new Whisper.ConfirmationDialogView({ + message: i18n('audioPermissionNeeded'), + okText: i18n('allowAccess'), + resolve: () => { + window.setMediaPermissions(true); + window.closePermissionsPopup(); + }, + reject: window.closePermissionsPopup, +}); + +window.view.$el.appendTo($body); diff --git a/js/settings_start.js b/js/settings_start.js new file mode 100644 index 000000000000..d8ae874abc18 --- /dev/null +++ b/js/settings_start.js @@ -0,0 +1,29 @@ +$(document).on('keyup', function(e) { + if (e.keyCode === 27) { + window.closeSettings(); + } +}); + +const $body = $(document.body); + +const getInitialData = async () => ({ + deviceName: await window.getDeviceName(), + + themeSetting: await window.getThemeSetting(), + hideMenuBar: await window.getHideMenuBar(), + + notificationSetting: await window.getNotificationSetting(), + audioNotification: await window.getAudioNotification(), + + mediaPermissions: await window.getMediaPermissions(), + + isPrimary: await window.isPrimary(), + lastSyncTime: await window.getLastSyncTime(), +}); + +window.initialRequest = getInitialData(); +window.initialRequest.then(data => { + window.initialData = data; + window.view = new Whisper.SettingsView(); + window.view.$el.appendTo($body); +}); diff --git a/js/views/app_view.js b/js/views/app_view.js index d03c0917a6c6..c11c08eefc21 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -14,8 +14,6 @@ events: { 'click .openInstaller': 'openInstaller', // NetworkStatusView has this button openInbox: 'openInbox', - 'change-theme': 'applyTheme', - 'change-hide-menu': 'applyHideMenu', }, applyTheme: function() { var theme = storage.get('theme-setting') || 'android'; diff --git a/js/views/clear_data_view.js b/js/views/clear_data_view.js new file mode 100644 index 000000000000..515996920808 --- /dev/null +++ b/js/views/clear_data_view.js @@ -0,0 +1,69 @@ +/* global i18n: false */ +/* global Whisper: false */ + +/* eslint-disable no-new */ + +// eslint-disable-next-line func-names +(function() { + 'use strict'; + + window.Whisper = window.Whisper || {}; + const { Database } = window.Whisper; + const { Logs } = window.Signal; + + const CLEAR_DATA_STEPS = { + CHOICE: 1, + DELETING: 2, + }; + window.Whisper.ClearDataView = Whisper.View.extend({ + templateName: 'clear-data', + className: 'full-screen-flow overlay', + events: { + 'click .cancel': 'onCancel', + 'click .delete-all-data': 'onDeleteAllData', + }, + initialize() { + this.step = CLEAR_DATA_STEPS.CHOICE; + }, + onCancel() { + this.remove(); + }, + async onDeleteAllData() { + console.log('Deleting everything!'); + this.step = CLEAR_DATA_STEPS.DELETING; + this.render(); + + try { + await Database.close(); + console.log('All database connections closed. Starting delete.'); + } catch (error) { + console.log('Something went wrong closing all database connections.'); + } + + this.clearAllData(); + }, + async clearAllData() { + try { + await Promise.all([Logs.deleteAll(), Database.drop()]); + } catch (error) { + console.log( + 'Something went wrong deleting all data:', + error && error.stack ? error.stack : error + ); + } + window.restart(); + }, + render_attributes() { + return { + isStep1: this.step === CLEAR_DATA_STEPS.CHOICE, + header: i18n('deleteAllDataHeader'), + body: i18n('deleteAllDataBody'), + cancelButton: i18n('cancel'), + deleteButton: i18n('deleteAllDataButton'), + + isStep2: this.step === CLEAR_DATA_STEPS.DELETING, + deleting: i18n('deleteAllDataProgress'), + }; + }, + }); +})(); diff --git a/js/views/debug_log_view.js b/js/views/debug_log_view.js index 8230d5ad8c42..bcb71ee1b36d 100644 --- a/js/views/debug_log_view.js +++ b/js/views/debug_log_view.js @@ -42,9 +42,8 @@ close: i18n('gotIt'), debugLogExplanation: i18n('debugLogExplanation'), }, - close(e) { - e.preventDefault(); - this.remove(); + close() { + window.closeDebugLog(); }, async submit(e) { e.preventDefault(); diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 987814294245..50bea574463d 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -233,13 +233,6 @@ reloadBackgroundPage() { window.location.reload(); }, - showSettings() { - if (this.$el.find('.settings').length) { - return; - } - const view = new Whisper.SettingsView(); - view.$el.appendTo(this.el); - }, filterContacts(e) { this.searchView.filterContacts(e); const input = this.$('input.search'); diff --git a/js/views/recorder_view.js b/js/views/recorder_view.js index ba28761963b3..dba138e2eb48 100644 --- a/js/views/recorder_view.js +++ b/js/views/recorder_view.js @@ -31,17 +31,25 @@ if (this.recorder.isRecording()) { this.recorder.cancelRecording(); } + this.recorder = null; + if (this.interval) { clearInterval(this.interval); } + this.interval = null; + if (this.source) { this.source.disconnect(); } + this.source = null; + if (this.context) { this.context.close().then(function() { console.log('audio context closed'); }); } + this.context = null; + this.remove(); this.trigger('closed'); }, @@ -74,8 +82,23 @@ this.recorder.startRecording(); }, onError: function(error) { - console.log(error.stack); + // Protect against out-of-band errors, which can happen if the user revokes media + // permissions after successfully accessing the microphone. + if (!this.recorder) { + return; + } + this.close(); + + if (error && error.name === 'PermissionDeniedError') { + console.log('RecorderView.onError: Microphone access is not allowed!'); + window.showPermissionsPopup(); + } else { + console.log( + 'RecorderView.onError:', + error && error.stack ? error.stack : error + ); + } }, }); })(); diff --git a/js/views/settings_view.js b/js/views/settings_view.js index a8345d21053c..f3713a8d8efc 100644 --- a/js/views/settings_view.js +++ b/js/views/settings_view.js @@ -1,106 +1,119 @@ -/* global storage: false */ -/* global textsecure: false */ /* global i18n: false */ /* global Whisper: false */ -/* eslint-disable */ +/* eslint-disable no-new */ +// eslint-disable-next-line func-names (function() { 'use strict'; + window.Whisper = window.Whisper || {}; - const { Database } = window.Whisper; - const { OS, Logs } = window.Signal; const { Settings } = window.Signal.Types; - var CheckboxView = Whisper.View.extend({ - initialize: function(options) { - this.name = options.name; - this.defaultValue = options.defaultValue; - this.event = options.event; + const CheckboxView = Whisper.View.extend({ + initialize(options) { + this.setFn = options.setFn; + this.value = options.value; this.populate(); }, events: { change: 'change', }, - change: function(e) { - var value = e.target.checked; - storage.put(this.name, value); + change(e) { + const value = e.target.checked; + this.setFn(value); console.log(this.name, 'changed to', value); - if (this.event) { - this.$el.trigger(this.event); - } }, - populate: function() { - var value = storage.get(this.name, this.defaultValue); - this.$('input').prop('checked', !!value); + populate() { + this.$('input').prop('checked', !!this.value); }, }); - var RadioButtonGroupView = Whisper.View.extend({ - initialize: function(options) { - this.name = options.name; - this.defaultValue = options.defaultValue; - this.event = options.event; + + const MediaPermissionsSettingView = Whisper.View.extend({ + initialize(options) { + this.value = options.value; + this.setFn = options.setFn; this.populate(); }, events: { change: 'change', }, - change: function(e) { - var value = this.$(e.target).val(); - storage.put(this.name, value); - console.log(this.name, 'changed to', value); - if (this.event) { - this.$el.trigger(this.event); - } + change(e) { + this.value = e.target.checked; + this.setFn(this.value); + console.log('media-permissions changed to', this.value); }, - populate: function() { - var value = storage.get(this.name, this.defaultValue); - this.$('#' + this.name + '-' + value).attr('checked', 'checked'); + populate() { + this.$('input').prop('checked', Boolean(this.value)); + }, + }); + + 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); + console.log(this.name, 'changed to', value); + }, + populate() { + this.$(`#${this.name}-${this.value}`).attr('checked', 'checked'); }, }); Whisper.SettingsView = Whisper.View.extend({ className: 'settings modal expand', templateName: 'settings', - initialize: function() { - this.deviceName = textsecure.storage.user.getDeviceName(); + initialize() { this.render(); new RadioButtonGroupView({ el: this.$('.notification-settings'), - defaultValue: 'message', name: 'notification-setting', + value: window.initialData.notificationSetting, + setFn: window.setNotificationSetting, }); new RadioButtonGroupView({ el: this.$('.theme-settings'), - defaultValue: 'android', name: 'theme-setting', - event: 'change-theme', + value: window.initialData.themeSetting, + setFn: window.setThemeSetting, }); if (Settings.isAudioNotificationSupported()) { new CheckboxView({ el: this.$('.audio-notification-setting'), - defaultValue: false, - name: 'audio-notification', + value: window.initialData.audioNotification, + setFn: window.setAudioNotification, }); } new CheckboxView({ el: this.$('.menu-bar-setting'), - defaultValue: false, - name: 'hide-menu-bar', - event: 'change-hide-menu', + value: window.initialData.hideMenuBar, + setFn: window.setHideMenuBar, }); - if (textsecure.storage.user.getDeviceId() != '1') { - var syncView = new SyncView().render(); + new MediaPermissionsSettingView({ + el: this.$('.media-permissions'), + value: window.initialData.mediaPermissions, + setFn: window.setMediaPermissions, + }); + if (!window.initialData.isPrimary) { + const syncView = new SyncView().render(); this.$('.sync-setting').append(syncView.el); } }, events: { - 'click .close': 'remove', + 'click .close': 'onClose', 'click .clear-data': 'onClearData', }, - render_attributes: function() { + render_attributes() { return { deviceNameLabel: i18n('deviceName'), - deviceName: this.deviceName, + deviceName: window.initialData.deviceName, theme: i18n('theme'), notifications: i18n('notifications'), notificationSettingsDialog: i18n('notificationSettingsDialog'), @@ -116,121 +129,72 @@ clearDataHeader: i18n('clearDataHeader'), clearDataButton: i18n('clearDataButton'), clearDataExplanation: i18n('clearDataExplanation'), + permissions: i18n('permissions'), + mediaPermissionsDescription: i18n('mediaPermissionsDescription'), }; }, - onClearData: function() { - var clearDataView = new ClearDataView().render(); - $('body').append(clearDataView.el); + onClose() { + window.closeSettings(); + }, + onClearData() { + window.deleteAllData(); + window.closeSettings(); }, }); - /* jshint ignore:start */ - /* eslint-enable */ - - const CLEAR_DATA_STEPS = { - CHOICE: 1, - DELETING: 2, - }; - const ClearDataView = Whisper.View.extend({ - templateName: 'clear-data', - className: 'full-screen-flow overlay', - events: { - 'click .cancel': 'onCancel', - 'click .delete-all-data': 'onDeleteAllData', - }, - initialize() { - this.step = CLEAR_DATA_STEPS.CHOICE; - }, - onCancel() { - this.remove(); - }, - async onDeleteAllData() { - console.log('Deleting everything!'); - this.step = CLEAR_DATA_STEPS.DELETING; - this.render(); - - try { - await Database.close(); - console.log('All database connections closed. Starting delete.'); - } catch (error) { - console.log('Something went wrong closing all database connections.'); - } - - this.clearAllData(); - }, - async clearAllData() { - try { - await Promise.all([Logs.deleteAll(), Database.drop()]); - } catch (error) { - console.log( - 'Something went wrong deleting all data:', - error && error.stack ? error.stack : error - ); - } - window.restart(); - }, - render_attributes() { - return { - isStep1: this.step === CLEAR_DATA_STEPS.CHOICE, - header: i18n('deleteAllDataHeader'), - body: i18n('deleteAllDataBody'), - cancelButton: i18n('cancel'), - deleteButton: i18n('deleteAllDataButton'), - - isStep2: this.step === CLEAR_DATA_STEPS.DELETING, - deleting: i18n('deleteAllDataProgress'), - }; - }, - }); - - /* eslint-disable */ - /* jshint ignore:end */ - - var SyncView = Whisper.View.extend({ + const SyncView = Whisper.View.extend({ templateName: 'syncSettings', className: 'syncSettings', events: { 'click .sync': 'sync', }, - enable: function() { + initialize() { + this.lastSyncTime = window.initialData.lastSyncTime; + }, + enable() { this.$('.sync').text(i18n('syncNow')); this.$('.sync').removeAttr('disabled'); }, - disable: function() { + disable() { this.$('.sync').attr('disabled', 'disabled'); this.$('.sync').text(i18n('syncing')); }, - onsuccess: function() { - storage.put('synced_at', Date.now()); + onsuccess() { + window.setLastSyncTime(Date.now()); + this.lastSyncTime = Date.now(); console.log('sync successful'); this.enable(); this.render(); }, - ontimeout: function() { + ontimeout() { console.log('sync timed out'); this.$('.synced_at').hide(); this.$('.sync_failed').show(); this.enable(); }, - sync: function() { + async sync() { this.$('.sync_failed').hide(); - if (textsecure.storage.user.getDeviceId() != '1') { - this.disable(); - var syncRequest = window.getSyncRequest(); - syncRequest.addEventListener('success', this.onsuccess.bind(this)); - syncRequest.addEventListener('timeout', this.ontimeout.bind(this)); - } else { + if (window.initialData.isPrimary) { console.log('Tried to sync from device 1'); + return; + } + + this.disable(); + try { + await window.makeSyncRequest(); + this.onsuccess(); + } catch (error) { + this.ontimeout(); } }, - render_attributes: function() { - var attrs = { + render_attributes() { + const attrs = { sync: i18n('sync'), syncNow: i18n('syncNow'), syncExplanation: i18n('syncExplanation'), syncFailed: i18n('syncFailed'), }; - var date = storage.get('synced_at'); + let date = this.lastSyncTime; if (date) { date = new Date(date); attrs.lastSynced = i18n('lastSynced'); diff --git a/main.js b/main.js index ee96eb787f7b..f0a7f208bd58 100644 --- a/main.js +++ b/main.js @@ -349,20 +349,6 @@ function createWindow() { }); } -function showDebugLog() { - if (mainWindow) { - mainWindow.webContents.send('debug-log'); - } -} - -function showSettings() { - if (!mainWindow) { - return; - } - - mainWindow.webContents.send('show-settings'); -} - function openReleaseNotes() { shell.openExternal( `https://github.com/signalapp/Signal-Desktop/releases/tag/v${app.getVersion()}` @@ -441,6 +427,146 @@ function showAbout() { }); } +let settingsWindow; +function showSettingsWindow() { + if (settingsWindow) { + settingsWindow.show(); + return; + } + if (!mainWindow) { + return; + } + + const size = mainWindow.getSize(); + const options = { + width: Math.min(500, size[0]), + height: Math.max(size[1] - 100, MIN_HEIGHT), + resizable: false, + title: locale.messages.signalDesktopPreferences.message, + autoHideMenuBar: true, + backgroundColor: '#FFFFFF', + show: false, + modal: true, + webPreferences: { + nodeIntegration: false, + nodeIntegrationInWorker: false, + preload: path.join(__dirname, 'settings_preload.js'), + // sandbox: true, + nativeWindowOpen: true, + }, + parent: mainWindow, + }; + + settingsWindow = new BrowserWindow(options); + + captureClicks(settingsWindow); + + settingsWindow.loadURL(prepareURL([__dirname, 'settings.html'])); + + settingsWindow.on('closed', () => { + removeDarkOverlay(); + settingsWindow = null; + }); + + settingsWindow.once('ready-to-show', () => { + addDarkOverlay(); + settingsWindow.show(); + }); +} + +let debugLogWindow; +function showDebugLogWindow() { + if (debugLogWindow) { + debugLogWindow.show(); + return; + } + + const size = mainWindow.getSize(); + const options = { + width: Math.max(size[0] - 100, MIN_WIDTH), + height: Math.max(size[1] - 100, MIN_HEIGHT), + resizable: false, + title: locale.messages.signalDesktopPreferences.message, + autoHideMenuBar: true, + backgroundColor: '#FFFFFF', + show: false, + modal: true, + webPreferences: { + nodeIntegration: false, + nodeIntegrationInWorker: false, + preload: path.join(__dirname, 'debug_log_preload.js'), + // sandbox: true, + nativeWindowOpen: true, + }, + parent: mainWindow, + }; + + debugLogWindow = new BrowserWindow(options); + + captureClicks(debugLogWindow); + + debugLogWindow.loadURL(prepareURL([__dirname, 'debug_log.html'])); + + debugLogWindow.on('closed', () => { + removeDarkOverlay(); + debugLogWindow = null; + }); + + debugLogWindow.once('ready-to-show', () => { + addDarkOverlay(); + debugLogWindow.show(); + }); +} + +let permissionsPopupWindow; +function showPermissionsPopupWindow() { + if (permissionsPopupWindow) { + permissionsPopupWindow.show(); + return; + } + if (!mainWindow) { + return; + } + + const size = mainWindow.getSize(); + const options = { + width: Math.min(400, size[0]), + height: Math.min(150, size[1]), + resizable: false, + title: locale.messages.signalDesktopPreferences.message, + autoHideMenuBar: true, + backgroundColor: '#FFFFFF', + show: false, + modal: true, + webPreferences: { + nodeIntegration: false, + nodeIntegrationInWorker: false, + preload: path.join(__dirname, 'permissions_popup_preload.js'), + // sandbox: true, + nativeWindowOpen: true, + }, + parent: mainWindow, + }; + + permissionsPopupWindow = new BrowserWindow(options); + + captureClicks(permissionsPopupWindow); + + permissionsPopupWindow.loadURL( + prepareURL([__dirname, 'permissions_popup.html']) + ); + + permissionsPopupWindow.on('closed', () => { + removeDarkOverlay(); + permissionsPopupWindow = null; + }); + + permissionsPopupWindow.once('ready-to-show', () => { + addDarkOverlay(); + permissionsPopupWindow.show(); + }); +} + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. @@ -462,7 +588,7 @@ app.on('ready', async () => { protocol: electronProtocol, }); - installPermissionsHandler({ session }); + installPermissionsHandler({ session, userConfig }); // NOTE: Temporarily allow `then` until we convert the entire file to `async` / `await`: /* eslint-disable more/no-then */ @@ -508,9 +634,10 @@ function setupMenu(options) { const { platform } = process; const menuOptions = Object.assign({}, options, { development, - showDebugLog, + showDebugLog: showDebugLogWindow, showWindow, showAbout, + showSettings: showSettingsWindow, openReleaseNotes, openNewBugForm, openSupportPage, @@ -519,7 +646,6 @@ function setupMenu(options) { setupWithImport, setupAsNewDevice, setupAsStandalone, - showSettings, }); const template = createTemplate(menuOptions, locale.messages); const menu = Menu.buildFromTemplate(template); @@ -591,6 +717,14 @@ ipc.on('draw-attention', () => { } }); +ipc.on('set-media-permissions', (event, enabled) => { + userConfig.set('mediaPermissions', enabled); +}); +ipc.on('get-media-permissions', event => { + // eslint-disable-next-line no-param-reassign + event.returnValue = userConfig.get('mediaPermissions') || false; +}); + ipc.on('restart', () => { app.relaunch(); app.quit(); @@ -619,3 +753,103 @@ ipc.on('update-tray-icon', (event, unreadCount) => { tray.updateIcon(unreadCount); } }); + +// Debug Log-related IPC calls + +ipc.on('show-debug-log', showDebugLogWindow); +ipc.on('close-debug-log', () => { + if (debugLogWindow) { + debugLogWindow.close(); + } +}); + +// Permissions Popup-related IPC calls + +ipc.on('show-permissions-popup', showPermissionsPopupWindow); +ipc.on('close-permissions-popup', () => { + if (permissionsPopupWindow) { + permissionsPopupWindow.close(); + } +}); + +// Settings-related IPC calls + +function addDarkOverlay() { + if (mainWindow && mainWindow.webContents) { + mainWindow.webContents.send('add-dark-overlay'); + } +} +function removeDarkOverlay() { + if (mainWindow && mainWindow.webContents) { + mainWindow.webContents.send('remove-dark-overlay'); + } +} + +ipc.on('show-settings', showSettingsWindow); +ipc.on('close-settings', () => { + if (settingsWindow) { + settingsWindow.close(); + } +}); + +installSettingsGetter('device-name'); + +installSettingsGetter('theme-setting'); +installSettingsSetter('theme-setting'); +installSettingsGetter('hide-menu-bar'); +installSettingsSetter('hide-menu-bar'); + +installSettingsGetter('notification-setting'); +installSettingsSetter('notification-setting'); +installSettingsGetter('audio-notification'); +installSettingsSetter('audio-notification'); + +// This one is 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('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); +}); + +installSettingsGetter('is-primary'); +installSettingsGetter('sync-request'); +installSettingsGetter('sync-time'); +installSettingsSetter('sync-time'); + +ipc.on('delete-all-data', () => { + if (mainWindow && mainWindow.webContents) { + mainWindow.webContents.send('delete-all-data'); + } +}); + +function installSettingsGetter(name) { + ipc.on(`get-${name}`, event => { + if (mainWindow && mainWindow.webContents) { + ipc.once(`get-success-${name}`, (_event, error, value) => + event.sender.send(`get-success-${name}`, error, value) + ); + mainWindow.webContents.send(`get-${name}`); + } + }); +} + +function installSettingsSetter(name) { + ipc.on(`set-${name}`, (event, value) => { + if (mainWindow && mainWindow.webContents) { + ipc.once(`set-success-${name}`, (_event, error) => + event.sender.send(`set-success-${name}`, error) + ); + mainWindow.webContents.send(`set-${name}`, value); + } + }); +} diff --git a/package.json b/package.json index 47f27eaaa65e..48d8d5c0df20 100644 --- a/package.json +++ b/package.json @@ -214,6 +214,9 @@ "config/local-${env.SIGNAL_ENV}.json", "background.html", "about.html", + "settings.html", + "permissions_popup.html", + "debug_log.html", "_locales/**", "protos/*", "js/**", @@ -225,6 +228,9 @@ "app/*", "preload.js", "about_preload.js", + "settings_preload.js", + "permissions_preload.js", + "debug_log_preload.js", "main.js", "images/**", "fonts/*", diff --git a/permissions_popup.html b/permissions_popup.html new file mode 100644 index 000000000000..e95b75ef0643 --- /dev/null +++ b/permissions_popup.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + diff --git a/permissions_popup_preload.js b/permissions_popup_preload.js new file mode 100644 index 000000000000..2d44f554b110 --- /dev/null +++ b/permissions_popup_preload.js @@ -0,0 +1,45 @@ +const { ipcRenderer } = require('electron'); +const url = require('url'); +const i18n = require('./js/modules/i18n'); + +const config = url.parse(window.location.toString(), true).query; +const { locale } = config; +const localeMessages = ipcRenderer.sendSync('locale-data'); + +window.i18n = i18n.setup(locale, localeMessages); + +require('./js/logging'); + +window.closePermissionsPopup = () => + ipcRenderer.send('close-permissions-popup'); + +window.getMediaPermissions = makeGetter('media-permissions'); +window.setMediaPermissions = makeSetter('media-permissions'); + +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); + }); +} diff --git a/preload.js b/preload.js index f583721d37e9..d271b51fb345 100644 --- a/preload.js +++ b/preload.js @@ -61,6 +61,10 @@ window.restart = () => { ipc.send('restart'); }; +window.setMediaPermissions = enabled => + ipc.send('set-media-permissions', enabled); +window.getMediaPermissions = () => ipc.sendSync('get-media-permissions'); + window.closeAbout = () => ipc.send('close-about'); window.updateTrayIcon = unreadCount => @@ -82,12 +86,89 @@ ipc.on('set-up-as-standalone', () => { Whisper.events.trigger('setupAsStandalone'); }); -ipc.on('show-settings', () => { - Whisper.events.trigger('showSettings'); +// Settings-related events + +window.showSettings = () => ipc.send('show-settings'); +window.showPermissionsPopup = () => ipc.send('show-permissions-popup'); + +ipc.on('add-dark-overlay', () => { + const { addDarkOverlay } = window.Events; + if (addDarkOverlay) { + addDarkOverlay(); + } +}); +ipc.on('remove-dark-overlay', () => { + const { removeDarkOverlay } = window.Events; + if (removeDarkOverlay) { + removeDarkOverlay(); + } }); -window.addSetupMenuItems = () => ipc.send('add-setup-menu-items'); +installGetter('device-name', 'getDeviceName'); +installGetter('theme-setting', 'getThemeSetting'); +installSetter('theme-setting', 'setThemeSetting'); +installGetter('hide-menu-bar', 'getHideMenuBar'); +installSetter('hide-menu-bar', 'setHideMenuBar'); + +installGetter('notification-setting', 'getNotificationSetting'); +installSetter('notification-setting', 'setNotificationSetting'); +installGetter('audio-notification', 'getAudioNotification'); +installSetter('audio-notification', 'setAudioNotification'); + +window.getMediaPermissions = () => + new Promise((resolve, reject) => { + ipc.once('get-success-media-permissions', (_event, error, value) => { + if (error) { + return reject(error); + } + + return resolve(value); + }); + ipc.send('get-media-permissions'); + }); + +installGetter('is-primary', 'isPrimary'); +installGetter('sync-request', 'getSyncRequest'); +installGetter('sync-time', 'getLastSyncTime'); +installSetter('sync-time', 'setLastSyncTime'); + +ipc.on('delete-all-data', () => { + const { deleteAllData } = window.Events; + if (deleteAllData) { + deleteAllData(); + } +}); + +function installGetter(name, functionName) { + ipc.on(`get-${name}`, async () => { + const getFn = window.Events[functionName]; + if (getFn) { + // eslint-disable-next-line no-param-reassign + try { + ipc.send(`get-success-${name}`, null, await getFn()); + } catch (error) { + ipc.send(`get-success-${name}`, error); + } + } + }); +} + +function installSetter(name, functionName) { + ipc.on(`set-${name}`, async (_event, value) => { + const setFn = window.Events[functionName]; + if (setFn) { + try { + await setFn(value); + ipc.send(`set-success-${name}`); + } catch (error) { + ipc.send(`set-success-${name}`, error); + } + } + }); +} + +window.addSetupMenuItems = () => ipc.send('add-setup-menu-items'); window.removeSetupMenuItems = () => ipc.send('remove-setup-menu-items'); // We pull these dependencies in now, from here, because they have Node.js dependencies diff --git a/settings.html b/settings.html new file mode 100644 index 000000000000..a5f0beb54f0e --- /dev/null +++ b/settings.html @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + diff --git a/settings_preload.js b/settings_preload.js new file mode 100644 index 000000000000..3f0b0ffd2fe5 --- /dev/null +++ b/settings_preload.js @@ -0,0 +1,76 @@ +const { ipcRenderer } = require('electron'); +const url = require('url'); +const i18n = require('./js/modules/i18n'); + +const config = url.parse(window.location.toString(), true).query; +const { locale } = config; +const localeMessages = ipcRenderer.sendSync('locale-data'); + +window.i18n = i18n.setup(locale, localeMessages); + +require('./js/logging'); + +// 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.getEnvironment = () => config.environment; +window.getVersion = () => config.version; +window.getAppInstance = () => config.appInstance; + +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.getNotificationSetting = makeGetter('notification-setting'); +window.setNotificationSetting = makeSetter('notification-setting'); +window.getAudioNotification = makeGetter('audio-notification'); +window.setAudioNotification = makeSetter('audio-notification'); + +window.getMediaPermissions = makeGetter('media-permissions'); +window.setMediaPermissions = makeSetter('media-permissions'); + +window.isPrimary = makeGetter('is-primary'); +window.makeSyncRequest = makeGetter('sync-request'); +window.getLastSyncTime = makeGetter('sync-time'); +window.setLastSyncTime = makeSetter('sync-time'); + +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); + }); +} diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index 13c933bdd4ad..c97eadd5338a 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -1320,6 +1320,24 @@ span.status { } } } + +.permissions-popup, +.debug-log-window { + .modal { + background-color: transparent; + padding: 0; + } + + .confirmation-dialog .content { + box-shadow: 0px 0px 0px 0px; + max-width: 1000px; + margin: 0; + margin-left: auto; + margin-right: auto; + margin-top: 15px; + } +} + .advisory .icon { height: 1.25em; width: 1.25em; diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 31b811daf293..fceef5adac3e 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -16,6 +16,17 @@ body { color: $grey_d; } +.dark-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: black; + opacity: 0.25; + z-index: 200; +} + .clearfix:before, .clearfix:after { display: table; diff --git a/stylesheets/_settings.scss b/stylesheets/_settings.scss index bed01b43041a..6ef8ec909319 100644 --- a/stylesheets/_settings.scss +++ b/stylesheets/_settings.scss @@ -1,11 +1,17 @@ .settings { &.modal { - padding: 50px; + padding: 0; + background-color: transparent; .content { - margin: 0 auto; + margin: 0; + margin-left: auto; + margin-right: auto; + width: 100%; - max-width: 500px; + max-width: 450px; + border-radius: 0; + box-shadow: 0px 0px 0px 0px; } } hr { @@ -32,6 +38,10 @@ color: red; } } + .restart-needed { + margin-top: 1em; + } + .clear-data-settings { button { float: right; diff --git a/test/index.html b/test/index.html index 5bfff87b70ea..44e25acf4201 100644 --- a/test/index.html +++ b/test/index.html @@ -618,11 +618,11 @@ - + @@ -636,7 +636,6 @@ -