diff --git a/.eslintignore b/.eslintignore
index ca4534c62..cc4758e2c 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 6ca9e4397..8531037b4 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 3cbd56ce0..918ee0a58 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 b638426f3..f291edfd8 100644
--- a/about.html
+++ b/about.html
@@ -1,68 +1,50 @@
-
-
+ a {
+ color: white;
+ }
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
diff --git a/about_preload.js b/about_preload.js
index 0c5b164fd..1c76457d1 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 6493034d5..b56efc3aa 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 f9a4aeb1b..17f724d6a 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 000000000..34c0ec950
--- /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 000000000..158e602bd
--- /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 000000000..01fe36f10
--- /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 e0a7930c0..43b11d5ef 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 000000000..e17ab912e
--- /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 471dfed9e..ae2dceef9 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 b367c57b3..9d7f014e3 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 000000000..765f35b30
--- /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 000000000..d8ae874ab
--- /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 d03c0917a..c11c08eef 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 000000000..515996920
--- /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 8230d5ad8..bcb71ee1b 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 987814294..50bea5744 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 ba2876196..dba138e2e 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 a8345d210..f3713a8d8 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 ee96eb787..f0a7f208b 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 47f27eaaa..48d8d5c0d 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 000000000..e95b75ef0
--- /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 000000000..2d44f554b
--- /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 f583721d3..d271b51fb 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 000000000..a5f0beb54
--- /dev/null
+++ b/settings.html
@@ -0,0 +1,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/settings_preload.js b/settings_preload.js
new file mode 100644
index 000000000..3f0b0ffd2
--- /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 13c933bdd..c97eadd53 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 31b811daf..fceef5ada 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 bed01b430..6ef8ec909 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 5bfff87b7..44e25acf4 100644
--- a/test/index.html
+++ b/test/index.html
@@ -618,11 +618,11 @@
-
+
@@ -636,7 +636,6 @@
-