From 18fd44f504d7a064fc565448eb4eaec97e3c88d3 Mon Sep 17 00:00:00 2001 From: Josh Perez <60019601+josh-signal@users.noreply.github.com> Date: Wed, 12 Feb 2020 13:30:58 -0800 Subject: [PATCH] Move all status/alert dialogs into the Left Pane --- _locales/en/messages.json | 37 ++-- background.html | 28 --- images/error_red.svg | 1 - js/background.js | 39 +++- js/expire.js | 22 --- js/modules/signal.js | 24 +++ js/registration.js | 28 --- js/rotate_signed_prekey_listener.js | 2 +- js/views/app_view.js | 1 - js/views/conversation_view.js | 2 +- js/views/inbox_view.js | 32 ---- js/views/install_view.js | 2 +- js/views/network_status_view.js | 122 ------------- stylesheets/_global.scss | 22 --- stylesheets/_index.scss | 39 ---- stylesheets/_modules.scss | 81 ++++++++- test/i18n_test.js | 4 - test/index.html | 28 --- test/views/network_status_view_test.js | 180 ------------------- ts/components/ExpiredBuildDialog.stories.tsx | 21 +++ ts/components/ExpiredBuildDialog.tsx | 34 ++++ ts/components/LeftPane.tsx | 14 +- ts/components/NetworkStatus.stories.tsx | 98 ++++++++++ ts/components/NetworkStatus.tsx | 83 +++++++++ ts/components/UpdateDialog.stories.tsx | 73 ++++++++ ts/components/UpdateDialog.tsx | 135 ++++++++++++++ ts/services/networkObserver.ts | 40 +++++ ts/services/updateListener.ts | 13 ++ ts/shims/socketStatus.ts | 12 ++ ts/shims/updateIpc.ts | 9 + ts/state/actions.ts | 6 + ts/state/ducks/expiration.ts | 50 ++++++ ts/state/ducks/network.ts | 104 +++++++++++ ts/state/ducks/updates.ts | 106 +++++++++++ ts/state/reducer.ts | 26 ++- ts/state/selectors/network.ts | 21 +++ ts/state/selectors/registration.ts | 18 ++ ts/state/smart/ExpiredBuildDialog.tsx | 16 ++ ts/state/smart/LeftPane.tsx | 18 ++ ts/state/smart/NetworkStatus.tsx | 20 +++ ts/state/smart/UpdateDialog.tsx | 18 ++ ts/types/Dialogs.ts | 6 + ts/types/SocketStatus.ts | 8 + ts/updater/common.ts | 54 +++++- ts/updater/macos.ts | 43 +++-- ts/updater/windows.ts | 31 ++-- ts/util/hasExpired.ts | 47 +++++ ts/util/index.ts | 8 +- ts/util/lint/exceptions.json | 52 ++---- ts/util/registration.ts | 27 +++ 50 files changed, 1298 insertions(+), 607 deletions(-) delete mode 100644 images/error_red.svg delete mode 100644 js/expire.js delete mode 100644 js/registration.js delete mode 100644 js/views/network_status_view.js delete mode 100644 test/views/network_status_view_test.js create mode 100644 ts/components/ExpiredBuildDialog.stories.tsx create mode 100644 ts/components/ExpiredBuildDialog.tsx create mode 100644 ts/components/NetworkStatus.stories.tsx create mode 100644 ts/components/NetworkStatus.tsx create mode 100644 ts/components/UpdateDialog.stories.tsx create mode 100644 ts/components/UpdateDialog.tsx create mode 100644 ts/services/networkObserver.ts create mode 100644 ts/services/updateListener.ts create mode 100644 ts/shims/socketStatus.ts create mode 100644 ts/shims/updateIpc.ts create mode 100644 ts/state/ducks/expiration.ts create mode 100644 ts/state/ducks/network.ts create mode 100644 ts/state/ducks/updates.ts create mode 100644 ts/state/selectors/network.ts create mode 100644 ts/state/selectors/registration.ts create mode 100644 ts/state/smart/ExpiredBuildDialog.tsx create mode 100644 ts/state/smart/NetworkStatus.tsx create mode 100644 ts/state/smart/UpdateDialog.tsx create mode 100644 ts/types/Dialogs.ts create mode 100644 ts/types/SocketStatus.ts create mode 100644 ts/util/hasExpired.ts create mode 100644 ts/util/registration.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index aecaa43c82..52266265cf 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -575,6 +575,10 @@ "message": "Connecting", "description": "Displayed when the desktop client is currently connecting to the server." }, + "connectingHangOn": { + "message": "Shouldn't be long...", + "description": "Subtext description for when the client is connecting to the server." + }, "offline": { "message": "Offline", "description": "Displayed when the desktop client has no network connection." @@ -583,15 +587,6 @@ "message": "Check your network connection.", "description": "Obvious instructions for when a user's computer loses its network connection" }, - "attemptingReconnection": { - "message": "Attempting reconnect in $reconnect_duration_in_seconds$ seconds", - "placeholders": { - "reconnect_duration_in_seconds": { - "content": "$1", - "example": "10" - } - } - }, "submitDebugLog": { "message": "Debug log", "description": "Menu item and header text for debug log modal (sentence case)" @@ -839,12 +834,28 @@ "description": "Shown as the title of our update error dialogs on windows" }, "cannotUpdateDetail": { - "message": "Signal Desktop failed to update, but there is a new version available. Please go to https://signal.org/download and install the new version manually, then either contact support or file a bug about this problem.", - "description": "Shown if a general error happened while trying to install update package" + "message": "Signal Desktop failed to update, but there is a new version available. Please go to $url$ and install the new version manually, then either contact support or file a bug about this problem.", + "description": "Shown if a general error happened while trying to install update package", + "placeholders": { + "url": { + "content": "$1", + "example": "https://signal.org/download" + } + } }, "readOnlyVolume": { - "message": "Signal Desktop is likely in a macOS quarantine, and will not be able to auto-update. Please try moving Signal.app to /Applications with Finder.", - "description": "Shown on MacOS if running on a read-only volume and we cannot update" + "message": "Signal Desktop is likely in a macOS quarantine, and will not be able to auto-update. Please try moving $app$ to $folder$ with Finder.", + "description": "Shown on MacOS if running on a read-only volume and we cannot update", + "placeholders": { + "app": { + "content": "$1", + "example": "Signal.app" + }, + "folder": { + "content": "$2", + "example": "/Applications" + } + } }, "ok": { "message": "OK" diff --git a/background.html b/background.html index df383bf32c..9d3731b71d 100644 --- a/background.html +++ b/background.html @@ -54,7 +54,6 @@ - - - - - - @@ -479,7 +452,6 @@ - diff --git a/images/error_red.svg b/images/error_red.svg deleted file mode 100644 index cdfb9176da..0000000000 --- a/images/error_red.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/js/background.js b/js/background.js index b88e24c47c..0e2acf0d88 100644 --- a/js/background.js +++ b/js/background.js @@ -244,7 +244,7 @@ }; Whisper.events.trigger('userChanged', user); - Whisper.Registration.markDone(); + window.Signal.Util.Registration.markDone(); window.log.info('dispatching registration event'); Whisper.events.trigger('registration_done'); }); @@ -382,7 +382,10 @@ showStickerPack: async (packId, key) => { // We can get these events even if the user has never linked this instance. - if (Whisper.Import.isIncomplete() || !Whisper.Registration.everDone()) { + if ( + Whisper.Import.isIncomplete() || + !window.Signal.Util.Registration.everDone() + ) { return; } @@ -559,6 +562,15 @@ } finally { initializeRedux(); start(); + window.Signal.Services.initializeNetworkObserver( + window.reduxActions.network + ); + window.Signal.Services.initializeUpdateListener( + window.reduxActions.updates + ); + window.reduxActions.expiration.hydrateExpirationStatus( + window.Signal.Util.hasExpired() + ); } }); @@ -609,10 +621,22 @@ Signal.State.Ducks.emojis.actions, store.dispatch ); + actions.expiration = Signal.State.bindActionCreators( + Signal.State.Ducks.expiration.actions, + store.dispatch + ); actions.items = Signal.State.bindActionCreators( Signal.State.Ducks.items.actions, store.dispatch ); + actions.network = Signal.State.bindActionCreators( + Signal.State.Ducks.network.actions, + store.dispatch + ); + actions.updates = Signal.State.bindActionCreators( + Signal.State.Ducks.updates.actions, + store.dispatch + ); actions.user = Signal.State.bindActionCreators( Signal.State.Ducks.user.actions, store.dispatch @@ -1351,7 +1375,7 @@ if (Whisper.Import.isIncomplete()) { window.log.info('Import was interrupted, showing import error screen'); appView.openImporter(); - } else if (Whisper.Registration.everDone()) { + } else if (window.Signal.Util.Registration.everDone()) { // listeners Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion); window.Signal.RefreshSenderCertificate.initialize({ @@ -1377,9 +1401,6 @@ Whisper.events.on('unauthorized', () => { appView.inboxView.networkStatusView.update(); }); - Whisper.events.on('reconnectTimer', () => { - appView.inboxView.networkStatusView.setSocketReconnectInterval(60000); - }); Whisper.events.on('contactsync', () => { if (appView.installView) { appView.openInbox(); @@ -1479,7 +1500,7 @@ return; } - if (!Whisper.Registration.everDone()) { + if (!window.Signal.Util.Registration.everDone()) { return; } if (Whisper.Import.isIncomplete()) { @@ -2299,7 +2320,7 @@ window.log.warn( 'Client is no longer authorized; deleting local configuration' ); - Whisper.Registration.remove(); + window.Signal.Util.Registration.remove(); const NUMBER_ID_KEY = 'number_id'; const VERSION_KEY = 'version'; @@ -2317,7 +2338,7 @@ // These two bits of data are important to ensure that the app loads up // the conversation list, instead of showing just the QR code screen. - Whisper.Registration.markEverDone(); + window.Signal.Util.Registration.markEverDone(); textsecure.storage.put(NUMBER_ID_KEY, previousNumberId); // These two are important to ensure we don't rip through every message diff --git a/js/expire.js b/js/expire.js deleted file mode 100644 index 9caff73c19..0000000000 --- a/js/expire.js +++ /dev/null @@ -1,22 +0,0 @@ -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - let BUILD_EXPIRATION = 0; - try { - BUILD_EXPIRATION = parseInt(window.getExpiration(), 10); - if (BUILD_EXPIRATION) { - window.log.info( - 'Build expires: ', - new Date(BUILD_EXPIRATION).toISOString() - ); - } - } catch (e) { - // nothing - } - - window.extension = window.extension || {}; - - window.extension.expired = () => - BUILD_EXPIRATION && Date.now() > BUILD_EXPIRATION; -})(); diff --git a/js/modules/signal.js b/js/modules/signal.js index 1ea07874e0..94a80a616b 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -64,12 +64,16 @@ const { const { createStore } = require('../../ts/state/createStore'); const conversationsDuck = require('../../ts/state/ducks/conversations'); const emojisDuck = require('../../ts/state/ducks/emojis'); +const expirationDuck = require('../../ts/state/ducks/expiration'); const itemsDuck = require('../../ts/state/ducks/items'); +const networkDuck = require('../../ts/state/ducks/network'); const searchDuck = require('../../ts/state/ducks/search'); const stickersDuck = require('../../ts/state/ducks/stickers'); +const updatesDuck = require('../../ts/state/ducks/updates'); const userDuck = require('../../ts/state/ducks/user'); const conversationsSelectors = require('../../ts/state/selectors/conversations'); +const registrationSelectors = require('../../ts/state/selectors/registration'); const searchSelectors = require('../../ts/state/selectors/search'); // Migrations @@ -98,6 +102,14 @@ const Initialization = require('./views/initialization'); const { IdleDetector } = require('./idle_detector'); const MessageDataMigrator = require('./messages_data_migrator'); +// Processes / Services +const { + initializeNetworkObserver, +} = require('../../ts/services/networkObserver'); +const { + initializeUpdateListener, +} = require('../../ts/services/updateListener'); + function initializeMigrations({ userDataPath, getRegionCode, @@ -284,19 +296,30 @@ exports.setup = (options = {}) => { createStickerPreviewModal, createTimeline, }; + const Ducks = { conversations: conversationsDuck, emojis: emojisDuck, + expiration: expirationDuck, items: itemsDuck, + network: networkDuck, + updates: updatesDuck, user: userDuck, search: searchDuck, stickers: stickersDuck, }; + const Selectors = { conversations: conversationsSelectors, + registration: registrationSelectors, search: searchSelectors, }; + const Services = { + initializeNetworkObserver, + initializeUpdateListener, + }; + const State = { bindActionCreators, createStore, @@ -344,6 +367,7 @@ exports.setup = (options = {}) => { OS, RefreshSenderCertificate, Settings, + Services, State, Stickers, Types, diff --git a/js/registration.js b/js/registration.js deleted file mode 100644 index 499e981bf4..0000000000 --- a/js/registration.js +++ /dev/null @@ -1,28 +0,0 @@ -/* global storage, Whisper */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - Whisper.Registration = { - markEverDone() { - storage.put('chromiumRegistrationDoneEver', ''); - }, - markDone() { - this.markEverDone(); - storage.put('chromiumRegistrationDone', ''); - }, - isDone() { - return storage.get('chromiumRegistrationDone') === ''; - }, - everDone() { - return ( - storage.get('chromiumRegistrationDoneEver') === '' || - storage.get('chromiumRegistrationDone') === '' - ); - }, - remove() { - storage.remove('chromiumRegistrationDone'); - }, - }; -})(); diff --git a/js/rotate_signed_prekey_listener.js b/js/rotate_signed_prekey_listener.js index 76b9ce24ec..a3c21656ee 100644 --- a/js/rotate_signed_prekey_listener.js +++ b/js/rotate_signed_prekey_listener.js @@ -83,7 +83,7 @@ } events.on('timetravel', () => { - if (Whisper.Registration.isDone()) { + if (window.Signal.Util.Registration.isDone()) { setTimeoutForNextRun(); } }); diff --git a/js/views/app_view.js b/js/views/app_view.js index 9321cfd3a9..e616cc5e71 100644 --- a/js/views/app_view.js +++ b/js/views/app_view.js @@ -29,7 +29,6 @@ }); }, events: { - 'click .openInstaller': 'openInstaller', // NetworkStatusView has this button openInbox: 'openInbox', }, applyTheme() { diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 25bd680d28..c3b546d8a6 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -2642,7 +2642,7 @@ this.model.clearTypingTimers(); let ToastView; - if (extension.expired()) { + if (window.reduxStore.getState().expiration.hasExpired) { ToastView = Whisper.ExpiredToast; } if (this.model.isPrivate() && storage.isBlocked(this.model.id)) { diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index 6483ca7915..2a3800ca6b 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -1,7 +1,5 @@ /* global ConversationController, - extension, - getInboxCollection, i18n, Whisper, Signal @@ -95,25 +93,6 @@ this.setupLeftPane(); } - const inboxCollection = getInboxCollection(); - - this.listenTo(inboxCollection, 'messageError', () => { - if (this.networkStatusView) { - this.networkStatusView.update(); - } - }); - - this.networkStatusView = new Whisper.NetworkStatusView(); - this.$el - .find('.network-status-container') - .append(this.networkStatusView.render().el); - - if (extension.expired()) { - const banner = new Whisper.ExpiredAlertBanner().render(); - banner.$el.prependTo(this.$el); - this.$el.addClass('expired'); - } - Whisper.events.on('pack-install-failed', () => { const toast = new Whisper.StickerPackInstallFailedToast(); toast.$el.appendTo(this.$el); @@ -225,15 +204,4 @@ this.closeRecording(e); }, }); - - Whisper.ExpiredAlertBanner = Whisper.View.extend({ - templateName: 'expired_alert', - className: 'expiredAlert clearfix', - render_attributes() { - return { - expiredWarning: i18n('expiredWarning'), - upgrade: i18n('upgrade'), - }; - }, - }); })(); diff --git a/js/views/install_view.js b/js/views/install_view.js index ec4d004dea..2cea5a740c 100644 --- a/js/views/install_view.js +++ b/js/views/install_view.js @@ -38,7 +38,7 @@ // Keep data around if it's a re-link, or the middle of a light import this.shouldRetainData = - Whisper.Registration.everDone() || options.hasExistingData; + window.Signal.Util.Registration.everDone() || options.hasExistingData; }, render_attributes() { let errorMessage; diff --git a/js/views/network_status_view.js b/js/views/network_status_view.js deleted file mode 100644 index 54fa257007..0000000000 --- a/js/views/network_status_view.js +++ /dev/null @@ -1,122 +0,0 @@ -/* global Whisper, extension, Backbone, moment, i18n */ - -// eslint-disable-next-line func-names -(function() { - 'use strict'; - - window.Whisper = window.Whisper || {}; - - Whisper.NetworkStatusView = Whisper.View.extend({ - className: 'network-status', - templateName: 'networkStatus', - initialize() { - this.$el.hide(); - - this.renderIntervalHandle = setInterval(this.update.bind(this), 5000); - extension.windows.onClosed(() => { - clearInterval(this.renderIntervalHandle); - }); - - setTimeout(this.finishConnectingGracePeriod.bind(this), 5000); - - this.withinConnectingGracePeriod = true; - this.setSocketReconnectInterval(null); - - window.addEventListener('online', this.update.bind(this)); - window.addEventListener('offline', this.update.bind(this)); - - this.model = new Backbone.Model(); - this.listenTo(this.model, 'change', this.onChange); - }, - onReconnectTimer() { - this.setSocketReconnectInterval(60000); - }, - finishConnectingGracePeriod() { - this.withinConnectingGracePeriod = false; - }, - setSocketReconnectInterval(millis) { - this.socketReconnectWaitDuration = moment.duration(millis); - }, - navigatorOnLine() { - return navigator.onLine; - }, - getSocketStatus() { - return window.getSocketStatus(); - }, - getNetworkStatus() { - let message = ''; - let instructions = ''; - let hasInterruption = false; - let action = null; - let buttonClass = null; - - const socketStatus = this.getSocketStatus(); - switch (socketStatus) { - case WebSocket.CONNECTING: - message = i18n('connecting'); - this.setSocketReconnectInterval(null); - break; - case WebSocket.OPEN: - this.setSocketReconnectInterval(null); - break; - case WebSocket.CLOSED: - message = i18n('disconnected'); - instructions = i18n('checkNetworkConnection'); - hasInterruption = true; - break; - case WebSocket.CLOSING: - default: - message = i18n('disconnected'); - instructions = i18n('checkNetworkConnection'); - hasInterruption = true; - break; - } - - if ( - socketStatus === WebSocket.CONNECTING && - !this.withinConnectingGracePeriod - ) { - hasInterruption = true; - } - if (this.socketReconnectWaitDuration.asSeconds() > 0) { - instructions = i18n('attemptingReconnection', [ - this.socketReconnectWaitDuration.asSeconds(), - ]); - } - if (!this.navigatorOnLine()) { - hasInterruption = true; - message = i18n('offline'); - instructions = i18n('checkNetworkConnection'); - } else if (!Whisper.Registration.isDone()) { - hasInterruption = true; - message = i18n('unlinked'); - instructions = i18n('unlinkedWarning'); - action = i18n('relink'); - buttonClass = 'openInstaller'; - } - - return { - message, - instructions, - hasInterruption, - action, - buttonClass, - }; - }, - update() { - const status = this.getNetworkStatus(); - this.model.set(status); - }, - render_attributes() { - return this.model.attributes; - }, - onChange() { - this.render(); - if (this.model.attributes.hasInterruption) { - this.$el.slideDown(); - } else { - this.$el.hide(); - } - }, - }); -})(); diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index a5eddf01b9..c0bcdf8b4f 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -296,28 +296,6 @@ $loading-height: 16px; } } -.expiredAlert { - background: $color-accent-yellow; - padding: 10px; - - button { - float: right; - border: none; - border-radius: 5px; - font-weight: bold; - line-height: 36px; - padding: 0 20px; - margin-left: 20px; - - color: $color-white; - background: $color-signal-blue; - } - - .message { - padding: 10px 0; - } -} - @keyframes loading { 50% { transform: scale(1); diff --git a/stylesheets/_index.scss b/stylesheets/_index.scss index 198b4f5179..6b47f776aa 100644 --- a/stylesheets/_index.scss +++ b/stylesheets/_index.scss @@ -51,45 +51,6 @@ } } -.network-status-container { - .network-status { - background: url('../images/error_red.svg') no-repeat left 10px center; - background-size: 25px 25px; - background-color: $color-accent-yellow; - padding: 10px; - padding-left: 48px; - display: none; - - .network-status-message { - h3 { - padding: 0px; - margin: 0px; - margin-bottom: 2px; - font-size: 14px; - } - span { - display: inline-block; - font-size: 12px; - padding: 0.5em 0; - } - - @include dark-theme { - color: $color-gray-90; - } - } - .action { - button { - border-radius: 5px; - border: solid 1px $color-gray-25; - cursor: pointer; - font-family: inherit; - color: $color-white; - background: $color-signal-blue; - } - } - } -} - .left-pane-placeholder { height: 100%; } diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 5c37b3cf95..47a2ecf7ce 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -4685,8 +4685,10 @@ button.module-image__border-overlay:focus { .module-search-results__conversations-header { @include font-body-1-bold; - height: 36px; + height: 52px; margin-left: 16px; + padding-bottom: 8px; + padding-top: 8px; @include dark-theme { color: $color-gray-05; @@ -4716,8 +4718,10 @@ button.module-image__border-overlay:focus { .module-search-results__messages-header { @include font-body-1-bold; - height: 36px; + height: 52px; margin-left: 16px; + padding-bottom: 8px; + padding-top: 8px; @include dark-theme { color: $color-gray-05; @@ -6381,6 +6385,79 @@ button.module-image__border-overlay:focus { } } +.module-left-pane-dialog { + background: $color-accent-green; + color: $color-white; + padding: 16px; + + .module-left-pane-dialog__message { + h3 { + @include font-body-1-bold; + padding: 0px; + margin: 0px; + margin-bottom: 8px; + } + span { + @include font-body-1; + display: inline-block; + } + } + + .module-left-pane-dialog__actions { + margin-top: 8px; + text-align: right; + + .module-left-pane-dialog__link { + @include keyboard-mode { + display: inline-block; + outline: 0; + } + } + + button { + background: inherit; + border-radius: 20px; + border: solid 1px $color-white; + color: $color-white; + cursor: pointer; + font-family: inherit; + margin: 0 4px; + padding: 8px 16px; + outline: 0; + + &:focus { + @include keyboard-mode { + box-shadow: 0 0 0 3px $color-signal-blue; + } + } + + &:hover { + @include mouse-mode { + box-shadow: 0 0 0 3px $color-signal-blue; + } + } + } + + .module-left-pane-dialog__button--no-border { + border: none; + } + } + + &.module-left-pane-dialog--error { + background-color: $color-accent-red; + } + + &.module-left-pane-dialog--warning { + background-color: $color-accent-yellow; + color: $color-black; + + button { + border-color: $color-black; + color: $color-black; + } + } +} + // Module: Emoji Picker %module-emoji-picker--ribbon { diff --git a/test/i18n_test.js b/test/i18n_test.js index 8530b8cbfe..c351887e78 100644 --- a/test/i18n_test.js +++ b/test/i18n_test.js @@ -8,10 +8,6 @@ describe('i18n', () => { it('returns message for given string', () => { assert.equal(i18n('reportIssue'), 'Report an issue'); }); - it('returns message with single substitution', () => { - const actual = i18n('attemptingReconnection', 5); - assert.equal(actual, 'Attempting reconnect in 5 seconds'); - }); it('returns message with multiple substitutions', () => { const actual = i18n('theyChangedTheTimer', ['Someone', '5 minutes']); assert.equal( diff --git a/test/index.html b/test/index.html index 9c013d1a60..eebbd07596 100644 --- a/test/index.html +++ b/test/index.html @@ -35,7 +35,6 @@ - - - - - @@ -490,7 +464,6 @@ - @@ -500,7 +473,6 @@ - diff --git a/test/views/network_status_view_test.js b/test/views/network_status_view_test.js deleted file mode 100644 index a3daa7883e..0000000000 --- a/test/views/network_status_view_test.js +++ /dev/null @@ -1,180 +0,0 @@ -/* global _, $, Whisper */ - -describe('NetworkStatusView', () => { - describe('getNetworkStatus', () => { - let networkStatusView; - let socketStatus = WebSocket.OPEN; - - let oldGetSocketStatus; - - /* BEGIN stubbing globals */ - before(() => { - oldGetSocketStatus = window.getSocketStatus; - window.getSocketStatus = () => socketStatus; - }); - - after(() => { - window.getSocketStatus = oldGetSocketStatus; - - // It turns out that continued calls to window.getSocketStatus happen - // because we host NetworkStatusView in three mock interfaces, and the view - // checks every N seconds. That results in infinite errors unless there is - // something to call. - window.getSocketStatus = () => WebSocket.OPEN; - }); - /* END stubbing globals */ - - beforeEach(() => { - networkStatusView = new Whisper.NetworkStatusView(); - $('.network-status-container').append(networkStatusView.el); - }); - afterEach(() => { - // prevents huge number of errors on console after running tests - clearInterval(networkStatusView.renderIntervalHandle); - networkStatusView = null; - }); - - describe('initialization', () => { - it('should have an empty interval', () => { - assert.equal( - networkStatusView.socketReconnectWaitDuration.asSeconds(), - 0 - ); - }); - }); - describe('network status with no connection', () => { - beforeEach(() => { - networkStatusView.navigatorOnLine = () => false; - }); - it('should be interrupted', () => { - networkStatusView.update(); - const status = networkStatusView.getNetworkStatus(); - assert(status.hasInterruption); - assert.equal(status.instructions, 'Check your network connection.'); - }); - it('should display an offline message', () => { - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Offline/); - }); - it('should override socket status', () => { - _([ - WebSocket.CONNECTING, - WebSocket.OPEN, - WebSocket.CLOSING, - WebSocket.CLOSED, - ]).forEach(socketStatusVal => { - socketStatus = socketStatusVal; - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Offline/); - }); - }); - it('should override registration status', () => { - Whisper.Registration.remove(); - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Offline/); - }); - }); - describe('network status when registration is not done', () => { - beforeEach(() => { - Whisper.Registration.remove(); - }); - it('should display an unlinked message', () => { - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Relink/); - }); - it('should override socket status', () => { - _([ - WebSocket.CONNECTING, - WebSocket.OPEN, - WebSocket.CLOSING, - WebSocket.CLOSED, - ]).forEach(socketStatusVal => { - socketStatus = socketStatusVal; - networkStatusView.update(); - assert.match(networkStatusView.$el.text(), /Relink/); - }); - }); - }); - describe('network status when registration is done', () => { - beforeEach(() => { - networkStatusView.navigatorOnLine = () => true; - Whisper.Registration.markDone(); - networkStatusView.update(); - }); - it('should not display an unlinked message', () => { - networkStatusView.update(); - assert.notMatch(networkStatusView.$el.text(), /Relink/); - }); - }); - describe('network status when socket is connecting', () => { - beforeEach(() => { - Whisper.Registration.markDone(); - socketStatus = WebSocket.CONNECTING; - networkStatusView.update(); - }); - it('it should display a connecting string if connecting and not in the connecting grace period', () => { - networkStatusView.withinConnectingGracePeriod = false; - networkStatusView.getNetworkStatus(); - - assert.match(networkStatusView.$el.text(), /Connecting/); - }); - it('it should not be interrupted if in connecting grace period', () => { - assert(networkStatusView.withinConnectingGracePeriod); - const status = networkStatusView.getNetworkStatus(); - - assert.match(networkStatusView.$el.text(), /Connecting/); - assert(!status.hasInterruption); - }); - it('it should be interrupted if connecting grace period is over', () => { - networkStatusView.withinConnectingGracePeriod = false; - const status = networkStatusView.getNetworkStatus(); - - assert(status.hasInterruption); - }); - }); - describe('network status when socket is open', () => { - before(() => { - socketStatus = WebSocket.OPEN; - }); - it('should not be interrupted', () => { - const status = networkStatusView.getNetworkStatus(); - assert(!status.hasInterruption); - assert.match( - networkStatusView.$el - .find('.network-status-message') - .text() - .trim(), - /^$/ - ); - }); - }); - describe('network status when socket is closed or closing', () => { - _([WebSocket.CLOSED, WebSocket.CLOSING]).forEach(socketStatusVal => { - it('should be interrupted', () => { - socketStatus = socketStatusVal; - networkStatusView.update(); - const status = networkStatusView.getNetworkStatus(); - assert(status.hasInterruption); - }); - }); - }); - describe('the socket reconnect interval', () => { - beforeEach(() => { - socketStatus = WebSocket.CLOSED; - networkStatusView.setSocketReconnectInterval(61000); - networkStatusView.update(); - }); - it('should format the message based on the socketReconnectWaitDuration property', () => { - assert.equal( - networkStatusView.socketReconnectWaitDuration.asSeconds(), - 61 - ); - assert.match( - networkStatusView.$('.network-status-message:last').text(), - /Attempting reconnect/ - ); - }); - it('should be reset by changing the socketStatus to CONNECTING', () => {}); - }); - }); -}); diff --git a/ts/components/ExpiredBuildDialog.stories.tsx b/ts/components/ExpiredBuildDialog.stories.tsx new file mode 100644 index 0000000000..9e0484b8b3 --- /dev/null +++ b/ts/components/ExpiredBuildDialog.stories.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { ExpiredBuildDialog } from './ExpiredBuildDialog'; + +// @ts-ignore +import { setup as setupI18n } from '../../js/modules/i18n'; +// @ts-ignore +import enMessages from '../../_locales/en/messages.json'; + +import { storiesOf } from '@storybook/react'; +import { boolean } from '@storybook/addon-knobs'; + +const i18n = setupI18n('en', enMessages); + +storiesOf('Components/ExpiredBuildDialog', module).add( + 'ExpiredBuildDialog', + () => { + const hasExpired = boolean('hasExpired', true); + + return ; + } +); diff --git a/ts/components/ExpiredBuildDialog.tsx b/ts/components/ExpiredBuildDialog.tsx new file mode 100644 index 0000000000..6114577fb1 --- /dev/null +++ b/ts/components/ExpiredBuildDialog.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +import { LocalizerType } from '../types/Util'; + +interface PropsType { + hasExpired: boolean; + i18n: LocalizerType; +} + +export const ExpiredBuildDialog = ({ + hasExpired, + i18n, +}: PropsType): JSX.Element | null => { + if (!hasExpired) { + return null; + } + + return ( +
+ {i18n('expiredWarning')} +
+ + + +
+
+ ); +}; diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index e9a97c6a56..25cdac8866 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -32,8 +32,11 @@ export interface PropsType { showInbox: () => void; // Render Props + renderExpiredBuildDialog: () => JSX.Element; renderMainHeader: () => JSX.Element; renderMessageSearchResult: (id: string) => JSX.Element; + renderNetworkStatus: () => JSX.Element; + renderUpdateDialog: () => JSX.Element; } // from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5 @@ -378,13 +381,22 @@ export class LeftPane extends React.Component { }; public render(): JSX.Element { - const { renderMainHeader, showArchived } = this.props; + const { + renderExpiredBuildDialog, + renderMainHeader, + renderNetworkStatus, + renderUpdateDialog, + showArchived, + } = this.props; return (
{showArchived ? this.renderArchivedHeader() : renderMainHeader()}
+ {renderExpiredBuildDialog()} + {renderNetworkStatus()} + {renderUpdateDialog()} {this.renderList()}
); diff --git a/ts/components/NetworkStatus.stories.tsx b/ts/components/NetworkStatus.stories.tsx new file mode 100644 index 0000000000..762cb7df43 --- /dev/null +++ b/ts/components/NetworkStatus.stories.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import { NetworkStatus } from './NetworkStatus'; + +// @ts-ignore +import { setup as setupI18n } from '../../js/modules/i18n'; +// @ts-ignore +import enMessages from '../../_locales/en/messages.json'; + +import { storiesOf } from '@storybook/react'; +import { boolean, select } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; + +const i18n = setupI18n('en', enMessages); + +const defaultProps = { + hasNetworkDialog: true, + i18n, + isOnline: true, + isRegistrationDone: true, + socketStatus: 0, + relinkDevice: action('relink-device'), + withinConnectingGracePeriod: false, +}; + +const permutations = [ + { + title: 'Connecting', + props: { + socketStatus: 0, + }, + }, + { + title: 'Closing (online)', + props: { + socketStatus: 2, + }, + }, + { + title: 'Closed (online)', + props: { + socketStatus: 3, + }, + }, + { + title: 'Offline', + props: { + isOnline: false, + }, + }, + { + title: 'Unlinked (online)', + props: { + isRegistrationDone: false, + }, + }, + { + title: 'Unlinked (offline)', + props: { + isOnline: false, + isRegistrationDone: false, + }, + }, +]; + +storiesOf('Components/NetworkStatus', module) + .add('Knobs Playground', () => { + const hasNetworkDialog = boolean('hasNetworkDialog', true); + const isOnline = boolean('isOnline', true); + const isRegistrationDone = boolean('isRegistrationDone', true); + const socketStatus = select( + 'socketStatus', + { + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3, + }, + 0 + ); + + return ( + + ); + }) + .add('Iterations', () => { + return permutations.map(({ props, title }) => ( + <> +

{title}

+ + + )); + }); diff --git a/ts/components/NetworkStatus.tsx b/ts/components/NetworkStatus.tsx new file mode 100644 index 0000000000..93142bf666 --- /dev/null +++ b/ts/components/NetworkStatus.tsx @@ -0,0 +1,83 @@ +import React from 'react'; + +import { LocalizerType } from '../types/Util'; +import { NetworkStateType } from '../state/ducks/network'; + +export interface PropsType extends NetworkStateType { + hasNetworkDialog: boolean; + i18n: LocalizerType; + isRegistrationDone: boolean; + relinkDevice: () => void; +} + +type RenderDialogTypes = { + title: string; + subtext: string; + renderActionableButton?: () => JSX.Element; +}; + +function renderDialog({ + title, + subtext, + renderActionableButton, +}: RenderDialogTypes): JSX.Element { + return ( +
+
+

{title}

+ {subtext} +
+ {renderActionableButton && renderActionableButton()} +
+ ); +} + +export const NetworkStatus = ({ + hasNetworkDialog, + i18n, + isOnline, + isRegistrationDone, + socketStatus, + relinkDevice, +}: PropsType): JSX.Element | null => { + if (!hasNetworkDialog) { + return null; + } + + if (!isOnline) { + return renderDialog({ + subtext: i18n('checkNetworkConnection'), + title: i18n('offline'), + }); + } else if (!isRegistrationDone) { + return renderDialog({ + renderActionableButton: (): JSX.Element => ( +
+ +
+ ), + subtext: i18n('unlinkedWarning'), + title: i18n('unlinked'), + }); + } + + let subtext = ''; + let title = ''; + + switch (socketStatus) { + case WebSocket.CONNECTING: + subtext = i18n('connectingHangOn'); + title = i18n('connecting'); + break; + case WebSocket.CLOSED: + case WebSocket.CLOSING: + default: + title = i18n('disconnected'); + subtext = i18n('checkNetworkConnection'); + } + + return renderDialog({ + subtext, + title, + }); +}; diff --git a/ts/components/UpdateDialog.stories.tsx b/ts/components/UpdateDialog.stories.tsx new file mode 100644 index 0000000000..7028d1e88d --- /dev/null +++ b/ts/components/UpdateDialog.stories.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { UpdateDialog } from './UpdateDialog'; + +// @ts-ignore +import { setup as setupI18n } from '../../js/modules/i18n'; +// @ts-ignore +import enMessages from '../../_locales/en/messages.json'; + +import { storiesOf } from '@storybook/react'; +import { boolean, select } from '@storybook/addon-knobs'; +import { action } from '@storybook/addon-actions'; + +const i18n = setupI18n('en', enMessages); + +const defaultProps = { + ackRender: action('ack-render'), + dismissDialog: action('dismiss-dialog'), + hasNetworkDialog: false, + i18n, + startUpdate: action('start-update'), +}; + +const permutations = [ + { + title: 'Update', + props: { + dialogType: 1, + }, + }, + { + title: 'Cannot Update', + props: { + dialogType: 2, + }, + }, + { + title: 'MacOS Read Only Error', + props: { + dialogType: 3, + }, + }, +]; + +storiesOf('Components/UpdateDialog', module) + .add('Knobs Playground', () => { + const dialogType = select( + 'dialogType', + { + None: 0, + Update: 1, + Cannot_Update: 2, + MacOS_Read_Only: 3, + }, + 1 + ); + const hasNetworkDialog = boolean('hasNetworkDialog', false); + + return ( + + ); + }) + .add('Iterations', () => { + return permutations.map(({ props, title }) => ( + <> +

{title}

+ + + )); + }); diff --git a/ts/components/UpdateDialog.tsx b/ts/components/UpdateDialog.tsx new file mode 100644 index 0000000000..13c0943334 --- /dev/null +++ b/ts/components/UpdateDialog.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import moment from 'moment'; + +import { Dialogs } from '../types/Dialogs'; +import { Intl } from './Intl'; +import { LocalizerType } from '../types/Util'; + +export interface PropsType { + ackRender: () => void; + dialogType: Dialogs; + dismissDialog: () => void; + hasNetworkDialog: boolean; + i18n: LocalizerType; + startUpdate: () => void; +} + +type MaybeMoment = moment.Moment | null; +type ReactSnoozeHook = React.Dispatch>; + +const SNOOZE_TIMER = 60 * 1000 * 30; + +function handleSnooze(setSnoozeForLater: ReactSnoozeHook) { + setSnoozeForLater(moment().add(SNOOZE_TIMER)); + setTimeout(() => { + setSnoozeForLater(moment()); + }, SNOOZE_TIMER); +} + +function canSnooze(snoozeUntil: MaybeMoment) { + return snoozeUntil === null; +} + +function isSnoozed(snoozeUntil: MaybeMoment) { + if (snoozeUntil === null) { + return false; + } + + return moment().isBefore(snoozeUntil); +} + +export const UpdateDialog = ({ + ackRender, + dialogType, + dismissDialog, + hasNetworkDialog, + i18n, + startUpdate, +}: PropsType): JSX.Element | null => { + const [snoozeUntil, setSnoozeForLater] = React.useState(null); + + React.useEffect(() => { + ackRender(); + }); + + if (hasNetworkDialog) { + return null; + } + + if (dialogType === Dialogs.None || isSnoozed(snoozeUntil)) { + return null; + } + + if (dialogType === Dialogs.Cannot_Update) { + return ( +
+
+

{i18n('cannotUpdate')}

+ + + https://signal.org/download/ + , + ]} + i18n={i18n} + id="cannotUpdateDetail" + /> + +
+
+ ); + } + + if (dialogType === Dialogs.MacOS_Read_Only) { + return ( +
+
+

{i18n('cannotUpdate')}

+ + Signal.app, + /Applications, + ]} + i18n={i18n} + id="readOnlyVolume" + /> + +
+
+ +
+
+ ); + } + + return ( +
+
+

{i18n('autoUpdateNewVersionTitle')}

+ {i18n('autoUpdateNewVersionMessage')} +
+
+ {canSnooze(snoozeUntil) && ( + + )} + +
+
+ ); +}; diff --git a/ts/services/networkObserver.ts b/ts/services/networkObserver.ts new file mode 100644 index 0000000000..11f096d30b --- /dev/null +++ b/ts/services/networkObserver.ts @@ -0,0 +1,40 @@ +import { + CheckNetworkStatusPayloadType, + NetworkActionType, +} from '../state/ducks/network'; +import { getSocketStatus } from '../shims/socketStatus'; + +type NetworkActions = { + checkNetworkStatus: (x: CheckNetworkStatusPayloadType) => NetworkActionType; + closeConnectingGracePeriod: () => NetworkActionType; +}; + +const REFRESH_INTERVAL = 5000; + +interface ShimmedWindow extends Window { + log: { + info: (...args: any) => void; + }; +} + +const unknownWindow = window as unknown; +const shimmedWindow = unknownWindow as ShimmedWindow; + +export function initializeNetworkObserver(networkActions: NetworkActions) { + const { log } = shimmedWindow; + log.info(`Initializing network observer every ${REFRESH_INTERVAL}ms`); + + const refresh = () => { + networkActions.checkNetworkStatus({ + isOnline: navigator.onLine, + socketStatus: getSocketStatus(), + }); + }; + + window.addEventListener('online', refresh); + window.addEventListener('offline', refresh); + window.setInterval(refresh, REFRESH_INTERVAL); + window.setTimeout(() => { + networkActions.closeConnectingGracePeriod(); + }, REFRESH_INTERVAL); +} diff --git a/ts/services/updateListener.ts b/ts/services/updateListener.ts new file mode 100644 index 0000000000..19d76858af --- /dev/null +++ b/ts/services/updateListener.ts @@ -0,0 +1,13 @@ +import { ipcRenderer } from 'electron'; +import { Dialogs } from '../types/Dialogs'; +import { ShowUpdateDialogAction } from '../state/ducks/updates'; + +type UpdatesActions = { + showUpdateDialog: (x: Dialogs) => ShowUpdateDialogAction; +}; + +export function initializeUpdateListener(updatesActions: UpdatesActions) { + ipcRenderer.on('show-update-dialog', (_, dialogType: Dialogs) => { + updatesActions.showUpdateDialog(dialogType); + }); +} diff --git a/ts/shims/socketStatus.ts b/ts/shims/socketStatus.ts new file mode 100644 index 0000000000..d463e25071 --- /dev/null +++ b/ts/shims/socketStatus.ts @@ -0,0 +1,12 @@ +interface ShimmedWindow extends Window { + getSocketStatus: () => number; +} + +const unknownWindow = window as unknown; +const shimmedWindow = unknownWindow as ShimmedWindow; + +export function getSocketStatus() { + const { getSocketStatus: getMessageReceiverStatus } = shimmedWindow; + + return getMessageReceiverStatus(); +} diff --git a/ts/shims/updateIpc.ts b/ts/shims/updateIpc.ts new file mode 100644 index 0000000000..a9a0e8078d --- /dev/null +++ b/ts/shims/updateIpc.ts @@ -0,0 +1,9 @@ +import { ipcRenderer } from 'electron'; + +export function startUpdate() { + ipcRenderer.send('start-update'); +} + +export function ackRender() { + ipcRenderer.send('show-update-dialog-ack'); +} diff --git a/ts/state/actions.ts b/ts/state/actions.ts index 5a92f9b7bf..92d82dfaba 100644 --- a/ts/state/actions.ts +++ b/ts/state/actions.ts @@ -1,15 +1,21 @@ import { actions as conversations } from './ducks/conversations'; import { actions as emojis } from './ducks/emojis'; +import { actions as expiration } from './ducks/expiration'; import { actions as items } from './ducks/items'; +import { actions as network } from './ducks/network'; import { actions as search } from './ducks/search'; import { actions as stickers } from './ducks/stickers'; +import { actions as updates } from './ducks/updates'; import { actions as user } from './ducks/user'; export const mapDispatchToProps = { ...conversations, ...emojis, + ...expiration, ...items, + ...network, ...search, ...stickers, + ...updates, ...user, }; diff --git a/ts/state/ducks/expiration.ts b/ts/state/ducks/expiration.ts new file mode 100644 index 0000000000..44eae25ab9 --- /dev/null +++ b/ts/state/ducks/expiration.ts @@ -0,0 +1,50 @@ +// State + +export type ExpirationStateType = { + hasExpired: boolean; +}; + +// Actions + +const HYDRATE_EXPIRATION_STATUS = 'expiration/HYDRATE_EXPIRATION_STATUS'; + +type HyrdateExpirationStatusActionType = { + type: 'expiration/HYDRATE_EXPIRATION_STATUS'; + payload: boolean; +}; + +export type ExpirationActionType = HyrdateExpirationStatusActionType; + +// Action Creators + +function hydrateExpirationStatus(hasExpired: boolean): ExpirationActionType { + return { + type: HYDRATE_EXPIRATION_STATUS, + payload: hasExpired, + }; +} + +export const actions = { + hydrateExpirationStatus, +}; + +// Reducer + +function getEmptyState(): ExpirationStateType { + return { + hasExpired: false, + }; +} + +export function reducer( + state: ExpirationStateType = getEmptyState(), + action: ExpirationActionType +): ExpirationStateType { + if (action.type === HYDRATE_EXPIRATION_STATUS) { + return { + hasExpired: action.payload, + }; + } + + return state; +} diff --git a/ts/state/ducks/network.ts b/ts/state/ducks/network.ts new file mode 100644 index 0000000000..368c1851ab --- /dev/null +++ b/ts/state/ducks/network.ts @@ -0,0 +1,104 @@ +import { SocketStatus } from '../../types/SocketStatus'; +import { trigger } from '../../shims/events'; + +// State + +export type NetworkStateType = { + isOnline: boolean; + socketStatus: SocketStatus; + withinConnectingGracePeriod: boolean; +}; + +// Actions + +const CHECK_NETWORK_STATUS = 'network/CHECK_NETWORK_STATUS'; +const CLOSE_CONNECTING_GRACE_PERIOD = 'network/CLOSE_CONNECTING_GRACE_PERIOD'; +const RELINK_DEVICE = 'network/RELINK_DEVICE'; + +export type CheckNetworkStatusPayloadType = { + isOnline: boolean; + socketStatus: SocketStatus; +}; + +type CheckNetworkStatusAction = { + type: 'network/CHECK_NETWORK_STATUS'; + payload: CheckNetworkStatusPayloadType; +}; + +type CloseConnectingGracePeriodActionType = { + type: 'network/CLOSE_CONNECTING_GRACE_PERIOD'; +}; + +type RelinkDeviceActionType = { + type: 'network/RELINK_DEVICE'; +}; + +export type NetworkActionType = + | CheckNetworkStatusAction + | CloseConnectingGracePeriodActionType + | RelinkDeviceActionType; + +// Action Creators + +function checkNetworkStatus( + payload: CheckNetworkStatusPayloadType +): CheckNetworkStatusAction { + return { + type: CHECK_NETWORK_STATUS, + payload, + }; +} + +function closeConnectingGracePeriod(): CloseConnectingGracePeriodActionType { + return { + type: CLOSE_CONNECTING_GRACE_PERIOD, + }; +} + +function relinkDevice(): RelinkDeviceActionType { + trigger('setupAsNewDevice'); + + return { + type: RELINK_DEVICE, + }; +} + +export const actions = { + checkNetworkStatus, + closeConnectingGracePeriod, + relinkDevice, +}; + +// Reducer + +function getEmptyState(): NetworkStateType { + return { + isOnline: navigator.onLine, + socketStatus: WebSocket.OPEN, + withinConnectingGracePeriod: true, + }; +} + +export function reducer( + state: NetworkStateType = getEmptyState(), + action: NetworkActionType +): NetworkStateType { + if (action.type === CHECK_NETWORK_STATUS) { + const { isOnline, socketStatus } = action.payload; + + return { + ...state, + isOnline, + socketStatus, + }; + } + + if (action.type === CLOSE_CONNECTING_GRACE_PERIOD) { + return { + ...state, + withinConnectingGracePeriod: false, + }; + } + + return state; +} diff --git a/ts/state/ducks/updates.ts b/ts/state/ducks/updates.ts new file mode 100644 index 0000000000..cc29788ece --- /dev/null +++ b/ts/state/ducks/updates.ts @@ -0,0 +1,106 @@ +import { Dialogs } from '../../types/Dialogs'; +import * as updateIpc from '../../shims/updateIpc'; + +// State + +export type UpdatesStateType = { + dialogType: Dialogs; +}; + +// Actions + +const ACK_RENDER = 'updates/ACK_RENDER'; +const DISMISS_DIALOG = 'updates/DISMISS_DIALOG'; +const SHOW_UPDATE_DIALOG = 'updates/SHOW_UPDATE_DIALOG'; +const START_UPDATE = 'updates/START_UPDATE'; + +type AckRenderAction = { + type: 'updates/ACK_RENDER'; +}; + +type DismissDialogAction = { + type: 'updates/DISMISS_DIALOG'; +}; + +export type ShowUpdateDialogAction = { + type: 'updates/SHOW_UPDATE_DIALOG'; + payload: Dialogs; +}; + +type StartUpdateAction = { + type: 'updates/START_UPDATE'; +}; + +export type UpdatesActionType = + | AckRenderAction + | DismissDialogAction + | ShowUpdateDialogAction + | StartUpdateAction; + +// Action Creators + +function ackRender(): AckRenderAction { + updateIpc.ackRender(); + + return { + type: ACK_RENDER, + }; +} + +function dismissDialog(): DismissDialogAction { + return { + type: DISMISS_DIALOG, + }; +} + +function showUpdateDialog(dialogType: Dialogs): ShowUpdateDialogAction { + return { + type: SHOW_UPDATE_DIALOG, + payload: dialogType, + }; +} + +function startUpdate(): StartUpdateAction { + updateIpc.startUpdate(); + + return { + type: START_UPDATE, + }; +} + +export const actions = { + ackRender, + dismissDialog, + showUpdateDialog, + startUpdate, +}; + +// Reducer + +function getEmptyState(): UpdatesStateType { + return { + dialogType: Dialogs.None, + }; +} + +export function reducer( + state: UpdatesStateType = getEmptyState(), + action: UpdatesActionType +): UpdatesStateType { + if (action.type === SHOW_UPDATE_DIALOG) { + return { + dialogType: action.payload, + }; + } + + if ( + action.type === DISMISS_DIALOG && + state.dialogType === Dialogs.MacOS_Read_Only + ) { + return { + dialogType: Dialogs.None, + }; + } + + return state; +} diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index 28fbb39845..3251899902 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -10,11 +10,21 @@ import { EmojisStateType, reducer as emojis, } from './ducks/emojis'; +import { + ExpirationActionType, + ExpirationStateType, + reducer as expiration, +} from './ducks/expiration'; import { ItemsActionType, ItemsStateType, reducer as items, } from './ducks/items'; +import { + NetworkActionType, + NetworkStateType, + reducer as network, +} from './ducks/network'; import { reducer as search, SEARCH_TYPES as SearchActionType, @@ -25,30 +35,44 @@ import { StickersActionType, StickersStateType, } from './ducks/stickers'; +import { + reducer as updates, + UpdatesActionType, + UpdatesStateType, +} from './ducks/updates'; import { reducer as user, UserStateType } from './ducks/user'; export type StateType = { conversations: ConversationsStateType; emojis: EmojisStateType; + expiration: ExpirationStateType; items: ItemsStateType; + network: NetworkStateType; search: SearchStateType; stickers: StickersStateType; + updates: UpdatesStateType; user: UserStateType; }; export type ActionsType = | EmojisActionType + | ExpirationActionType | ConversationActionType | ItemsActionType + | NetworkActionType | StickersActionType - | SearchActionType; + | SearchActionType + | UpdatesActionType; export const reducers = { conversations, emojis, + expiration, items, + network, search, stickers, + updates, user, }; diff --git a/ts/state/selectors/network.ts b/ts/state/selectors/network.ts new file mode 100644 index 0000000000..8a169f7e27 --- /dev/null +++ b/ts/state/selectors/network.ts @@ -0,0 +1,21 @@ +import { createSelector } from 'reselect'; + +import { StateType } from '../reducer'; +import { NetworkStateType } from '../ducks/network'; +import { isDone } from './registration'; + +const getNetwork = (state: StateType): NetworkStateType => state.network; + +export const hasNetworkDialog = createSelector( + getNetwork, + isDone, + ( + { isOnline, socketStatus, withinConnectingGracePeriod }: NetworkStateType, + isRegistrationDone: boolean + ): boolean => + !isOnline || + !isRegistrationDone || + (socketStatus === WebSocket.CONNECTING && !withinConnectingGracePeriod) || + socketStatus === WebSocket.CLOSED || + socketStatus === WebSocket.CLOSING +); diff --git a/ts/state/selectors/registration.ts b/ts/state/selectors/registration.ts new file mode 100644 index 0000000000..3b8bc66e9b --- /dev/null +++ b/ts/state/selectors/registration.ts @@ -0,0 +1,18 @@ +import { createSelector } from 'reselect'; + +import { StateType } from '../reducer'; +import { ItemsStateType } from '../ducks/items'; + +const getItems = (state: StateType): ItemsStateType => state.items; + +export const isDone = createSelector( + getItems, + (state: ItemsStateType): boolean => state.chromiumRegistrationDone === '' +); + +export const everDone = createSelector( + getItems, + (state: ItemsStateType): boolean => + state.chromiumRegistrationDoneEver === '' || + state.chromiumRegistrationDone === '' +); diff --git a/ts/state/smart/ExpiredBuildDialog.tsx b/ts/state/smart/ExpiredBuildDialog.tsx new file mode 100644 index 0000000000..771803d63b --- /dev/null +++ b/ts/state/smart/ExpiredBuildDialog.tsx @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { mapDispatchToProps } from '../actions'; +import { ExpiredBuildDialog } from '../../components/ExpiredBuildDialog'; +import { StateType } from '../reducer'; +import { getIntl } from '../selectors/user'; + +const mapStateToProps = (state: StateType) => { + return { + hasExpired: state.expiration.hasExpired, + i18n: getIntl(state), + }; +}; + +const smart = connect(mapStateToProps, mapDispatchToProps); + +export const SmartExpiredBuildDialog = smart(ExpiredBuildDialog); diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx index b4bb92dced..5ed4453915 100644 --- a/ts/state/smart/LeftPane.tsx +++ b/ts/state/smart/LeftPane.tsx @@ -12,20 +12,35 @@ import { getShowArchived, } from '../selectors/conversations'; +import { SmartExpiredBuildDialog } from './ExpiredBuildDialog'; import { SmartMainHeader } from './MainHeader'; import { SmartMessageSearchResult } from './MessageSearchResult'; +import { SmartNetworkStatus } from './NetworkStatus'; +import { SmartUpdateDialog } from './UpdateDialog'; // Workaround: A react component's required properties are filtering up through connect() // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 const FilteredSmartMainHeader = SmartMainHeader as any; const FilteredSmartMessageSearchResult = SmartMessageSearchResult as any; +const FilteredSmartNetworkStatus = SmartNetworkStatus as any; +const FilteredSmartUpdateDialog = SmartUpdateDialog as any; +const FilteredSmartExpiredBuildDialog = SmartExpiredBuildDialog as any; +function renderExpiredBuildDialog(): JSX.Element { + return ; +} function renderMainHeader(): JSX.Element { return ; } function renderMessageSearchResult(id: string): JSX.Element { return ; } +function renderUpdateDialog(): JSX.Element { + return ; +} +function renderNetworkStatus(): JSX.Element { + return ; +} const mapStateToProps = (state: StateType) => { const showSearch = isSearching(state); @@ -40,8 +55,11 @@ const mapStateToProps = (state: StateType) => { selectedConversationId, showArchived: getShowArchived(state), i18n: getIntl(state), + renderExpiredBuildDialog, renderMainHeader, renderMessageSearchResult, + renderNetworkStatus, + renderUpdateDialog, }; }; diff --git a/ts/state/smart/NetworkStatus.tsx b/ts/state/smart/NetworkStatus.tsx new file mode 100644 index 0000000000..2445517c0d --- /dev/null +++ b/ts/state/smart/NetworkStatus.tsx @@ -0,0 +1,20 @@ +import { connect } from 'react-redux'; +import { mapDispatchToProps } from '../actions'; +import { NetworkStatus } from '../../components/NetworkStatus'; +import { StateType } from '../reducer'; +import { getIntl } from '../selectors/user'; +import { hasNetworkDialog } from '../selectors/network'; +import { isDone } from '../selectors/registration'; + +const mapStateToProps = (state: StateType) => { + return { + ...state.network, + hasNetworkDialog: hasNetworkDialog(state), + i18n: getIntl(state), + isRegistrationDone: isDone(state), + }; +}; + +const smart = connect(mapStateToProps, mapDispatchToProps); + +export const SmartNetworkStatus = smart(NetworkStatus); diff --git a/ts/state/smart/UpdateDialog.tsx b/ts/state/smart/UpdateDialog.tsx new file mode 100644 index 0000000000..044277bf0b --- /dev/null +++ b/ts/state/smart/UpdateDialog.tsx @@ -0,0 +1,18 @@ +import { connect } from 'react-redux'; +import { mapDispatchToProps } from '../actions'; +import { UpdateDialog } from '../../components/UpdateDialog'; +import { StateType } from '../reducer'; +import { getIntl } from '../selectors/user'; +import { hasNetworkDialog } from '../selectors/network'; + +const mapStateToProps = (state: StateType) => { + return { + ...state.updates, + hasNetworkDialog: hasNetworkDialog(state), + i18n: getIntl(state), + }; +}; + +const smart = connect(mapStateToProps, mapDispatchToProps); + +export const SmartUpdateDialog = smart(UpdateDialog); diff --git a/ts/types/Dialogs.ts b/ts/types/Dialogs.ts new file mode 100644 index 0000000000..75ec754573 --- /dev/null +++ b/ts/types/Dialogs.ts @@ -0,0 +1,6 @@ +export enum Dialogs { + None, + Update, + Cannot_Update, + MacOS_Read_Only, +} diff --git a/ts/types/SocketStatus.ts b/ts/types/SocketStatus.ts new file mode 100644 index 0000000000..13c02e6712 --- /dev/null +++ b/ts/types/SocketStatus.ts @@ -0,0 +1,8 @@ +// Maps to values found here: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/readyState +// which are returned by libtextsecure's MessageReceiver +export enum SocketStatus { + CONNECTING, + OPEN, + CLOSING, + CLOSED, +} diff --git a/ts/updater/common.ts b/ts/updater/common.ts index adf126c4af..bb5881d5f8 100644 --- a/ts/updater/common.ts +++ b/ts/updater/common.ts @@ -18,9 +18,10 @@ import { v4 as getGuid } from 'uuid'; import pify from 'pify'; import mkdirp from 'mkdirp'; import rimraf from 'rimraf'; -import { app, BrowserWindow, dialog } from 'electron'; +import { app, BrowserWindow, dialog, ipcMain } from 'electron'; import { getTempPath } from '../../app/attachments'; +import { Dialogs } from '../types/Dialogs'; // @ts-ignore import * as packageJson from '../../package.json'; @@ -49,6 +50,8 @@ const mkdirpPromise = pify(mkdirp); const rimrafPromise = pify(rimraf); const { platform } = process; +export const ACK_RENDER_TIMEOUT = 10000; + export async function checkForUpdates( logger: LoggerType ): Promise<{ @@ -141,10 +144,10 @@ export async function downloadUpdate( } } -export async function showUpdateDialog( +async function showFallbackUpdateDialog( mainWindow: BrowserWindow, messages: MessagesType -): Promise { +) { const RESTART_BUTTON = 0; const LATER_BUTTON = 1; const options = { @@ -165,10 +168,32 @@ export async function showUpdateDialog( return response === RESTART_BUTTON; } -export async function showCannotUpdateDialog( +export function showUpdateDialog( + mainWindow: BrowserWindow, + messages: MessagesType, + performUpdateCallback: () => void +): void { + let ack = false; + + ipcMain.once('start-update', performUpdateCallback); + + ipcMain.once('show-update-dialog-ack', () => { + ack = true; + }); + + mainWindow.webContents.send('show-update-dialog', Dialogs.Update); + + setTimeout(async () => { + if (!ack) { + await showFallbackUpdateDialog(mainWindow, messages); + } + }, ACK_RENDER_TIMEOUT); +} + +async function showFallbackCannotUpdateDialog( mainWindow: BrowserWindow, messages: MessagesType -): Promise { +) { const options = { type: 'error', buttons: [messages.ok.message], @@ -179,6 +204,25 @@ export async function showCannotUpdateDialog( await dialog.showMessageBox(mainWindow, options); } +export function showCannotUpdateDialog( + mainWindow: BrowserWindow, + messages: MessagesType +): void { + let ack = false; + + ipcMain.once('show-update-dialog-ack', () => { + ack = true; + }); + + mainWindow.webContents.send('show-update-dialog', Dialogs.Cannot_Update); + + setTimeout(async () => { + if (!ack) { + await showFallbackCannotUpdateDialog(mainWindow, messages); + } + }, ACK_RENDER_TIMEOUT); +} + // Helper functions export function getUpdateCheckUrl(): string { diff --git a/ts/updater/macos.ts b/ts/updater/macos.ts index 3e75d37435..cfd697d356 100644 --- a/ts/updater/macos.ts +++ b/ts/updater/macos.ts @@ -4,12 +4,13 @@ import { AddressInfo } from 'net'; import { dirname } from 'path'; import { v4 as getGuid } from 'uuid'; -import { app, autoUpdater, BrowserWindow, dialog } from 'electron'; +import { app, autoUpdater, BrowserWindow, dialog, ipcMain } from 'electron'; import { get as getFromConfig } from 'config'; import { gt } from 'semver'; import got from 'got'; import { + ACK_RENDER_TIMEOUT, checkForUpdates, deleteTempDir, downloadUpdate, @@ -21,6 +22,7 @@ import { } from './common'; import { hexToBinary, verifySignature } from './signature'; import { markShouldQuit } from '../../app/window_state'; +import { Dialogs } from '../types/Dialogs'; let isChecking = false; const SECOND = 1000; @@ -96,12 +98,12 @@ async function checkDownloadAndInstall( const message: string = error.message || ''; if (message.includes(readOnly)) { logger.info('checkDownloadAndInstall: showing read-only dialog...'); - await showReadOnlyDialog(getMainWindow(), messages); + showReadOnlyDialog(getMainWindow(), messages); } else { logger.info( 'checkDownloadAndInstall: showing general update failure dialog...' ); - await showCannotUpdateDialog(getMainWindow(), messages); + showCannotUpdateDialog(getMainWindow(), messages); } throw error; @@ -111,14 +113,12 @@ async function checkDownloadAndInstall( // because Squirrel has cached the update file and will do the right thing. logger.info('checkDownloadAndInstall: showing update dialog...'); - const shouldUpdate = await showUpdateDialog(getMainWindow(), messages); - if (!shouldUpdate) { - return; - } - logger.info('checkDownloadAndInstall: calling quitAndInstall...'); - markShouldQuit(); - autoUpdater.quitAndInstall(); + showUpdateDialog(getMainWindow(), messages, () => { + logger.info('checkDownloadAndInstall: calling quitAndInstall...'); + markShouldQuit(); + autoUpdater.quitAndInstall(); + }); } catch (error) { logger.error('checkDownloadAndInstall: error', getPrintableError(error)); } finally { @@ -339,10 +339,29 @@ function shutdown( } } -export async function showReadOnlyDialog( +export function showReadOnlyDialog( mainWindow: BrowserWindow, messages: MessagesType -): Promise { +): void { + let ack = false; + + ipcMain.once('show-update-dialog-ack', () => { + ack = true; + }); + + mainWindow.webContents.send('show-update-dialog', Dialogs.MacOS_Read_Only); + + setTimeout(async () => { + if (!ack) { + await showFallbackReadOnlyDialog(mainWindow, messages); + } + }, ACK_RENDER_TIMEOUT); +} + +async function showFallbackReadOnlyDialog( + mainWindow: BrowserWindow, + messages: MessagesType +) { const options = { type: 'warning', buttons: [messages.ok.message], diff --git a/ts/updater/windows.ts b/ts/updater/windows.ts index 82c5f086bf..e1295b85a8 100644 --- a/ts/updater/windows.ts +++ b/ts/updater/windows.ts @@ -93,25 +93,22 @@ async function checkDownloadAndInstall( } logger.info('checkDownloadAndInstall: showing dialog...'); - const shouldUpdate = await showUpdateDialog(getMainWindow(), messages); - if (!shouldUpdate) { - return; - } + showUpdateDialog(getMainWindow(), messages, async () => { + try { + await verifyAndInstall(updateFilePath, version, logger); + installing = true; + } catch (error) { + logger.info( + 'checkDownloadAndInstall: showing general update failure dialog...' + ); + showCannotUpdateDialog(getMainWindow(), messages); - try { - await verifyAndInstall(updateFilePath, version, logger); - installing = true; - } catch (error) { - logger.info( - 'checkDownloadAndInstall: showing general update failure dialog...' - ); - await showCannotUpdateDialog(getMainWindow(), messages); + throw error; + } - throw error; - } - - markShouldQuit(); - app.quit(); + markShouldQuit(); + app.quit(); + }); } catch (error) { logger.error('checkDownloadAndInstall: error', getPrintableError(error)); } finally { diff --git a/ts/util/hasExpired.ts b/ts/util/hasExpired.ts new file mode 100644 index 0000000000..7913a15eff --- /dev/null +++ b/ts/util/hasExpired.ts @@ -0,0 +1,47 @@ +interface ShimmedWindow extends Window { + getExpiration: () => string; + log: { + info: (...args: any) => void; + error: (...args: any) => void; + }; +} + +const unknownWindow = window as unknown; +const shimmedWindow = unknownWindow as ShimmedWindow; + +// @ts-ignore +const env = window.getEnvironment(); + +const NINETY_ONE_DAYS = 86400 * 91 * 1000; + +export function hasExpired() { + const { getExpiration, log } = shimmedWindow; + + let buildExpiration = 0; + + try { + buildExpiration = parseInt(getExpiration(), 10); + if (buildExpiration) { + log.info('Build expires: ', new Date(buildExpiration).toISOString()); + } + } catch (e) { + log.error('Error retrieving build expiration date', e.stack); + + return true; + } + + const tooFarIntoFuture = Date.now() + NINETY_ONE_DAYS < buildExpiration; + + if (tooFarIntoFuture) { + log.error( + 'Build expiration is set too far into the future', + buildExpiration + ); + } + + if (env === 'production') { + return Date.now() > buildExpiration && tooFarIntoFuture; + } + + return buildExpiration && Date.now() > buildExpiration; +} diff --git a/ts/util/index.ts b/ts/util/index.ts index 3d0726a84e..83a2a97069 100644 --- a/ts/util/index.ts +++ b/ts/util/index.ts @@ -1,12 +1,14 @@ import * as GoogleChrome from './GoogleChrome'; +import * as Registration from './registration'; import { arrayBufferToObjectURL } from './arrayBufferToObjectURL'; import { combineNames } from './combineNames'; import { createBatcher } from './batcher'; import { createWaitBatcher } from './waitBatcher'; +import { hasExpired } from './hasExpired'; import { isFileDangerous } from './isFileDangerous'; -import { missingCaseError } from './missingCaseError'; -import { migrateColor } from './migrateColor'; import { makeLookup } from './makeLookup'; +import { migrateColor } from './migrateColor'; +import { missingCaseError } from './missingCaseError'; export { arrayBufferToObjectURL, @@ -14,8 +16,10 @@ export { createBatcher, createWaitBatcher, GoogleChrome, + hasExpired, isFileDangerous, makeLookup, migrateColor, missingCaseError, + Registration, }; diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index e9cbda6803..06282612f6 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -309,7 +309,7 @@ "rule": "DOM-innerHTML", "path": "js/views/app_view.js", "line": " this.el.innerHTML = '';", - "lineNumber": 55, + "lineNumber": 54, "reasonCategory": "usageTrusted", "updated": "2018-09-15T00:38:04.183Z", "reasonDetail": "Hard-coded string" @@ -318,7 +318,7 @@ "rule": "jQuery-append(", "path": "js/views/app_view.js", "line": " this.el.append(view.el);", - "lineNumber": 56, + "lineNumber": 55, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes" @@ -327,7 +327,7 @@ "rule": "jQuery-appendTo(", "path": "js/views/app_view.js", "line": " this.debugLogView.$el.appendTo(this.el);", - "lineNumber": 62, + "lineNumber": 61, "reasonCategory": "usageTrusted", "updated": "2018-09-19T18:13:29.628Z", "reasonDetail": "Interacting with already-existing DOM nodes" @@ -461,7 +461,7 @@ "rule": "jQuery-appendTo(", "path": "js/views/inbox_view.js", "line": " view.$el.appendTo(this.el);", - "lineNumber": 35, + "lineNumber": 33, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Known DOM elements" @@ -470,7 +470,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.message').text(message);", - "lineNumber": 69, + "lineNumber": 67, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Hardcoded selector" @@ -479,7 +479,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " el: this.$('.conversation-stack'),", - "lineNumber": 85, + "lineNumber": 83, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Hardcoded selector" @@ -488,25 +488,7 @@ "rule": "jQuery-prependTo(", "path": "js/views/inbox_view.js", "line": " this.appLoadingScreen.$el.prependTo(this.el);", - "lineNumber": 92, - "reasonCategory": "usageTrusted", - "updated": "2019-10-21T22:30:15.622Z", - "reasonDetail": "Known DOM elements" - }, - { - "rule": "jQuery-append(", - "path": "js/views/inbox_view.js", - "line": " .append(this.networkStatusView.render().el);", - "lineNumber": 109, - "reasonCategory": "usageTrusted", - "updated": "2019-10-21T22:30:15.622Z", - "reasonDetail": "Known DOM elements" - }, - { - "rule": "jQuery-prependTo(", - "path": "js/views/inbox_view.js", - "line": " banner.$el.prependTo(this.$el);", - "lineNumber": 113, + "lineNumber": 90, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Known DOM elements" @@ -515,7 +497,7 @@ "rule": "jQuery-appendTo(", "path": "js/views/inbox_view.js", "line": " toast.$el.appendTo(this.$el);", - "lineNumber": 119, + "lineNumber": 98, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Known DOM elements" @@ -524,7 +506,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);", - "lineNumber": 139, + "lineNumber": 118, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Known DOM elements" @@ -533,7 +515,7 @@ "rule": "jQuery-append(", "path": "js/views/inbox_view.js", "line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);", - "lineNumber": 139, + "lineNumber": 118, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Known DOM elements" @@ -542,7 +524,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " if (e && this.$(e.target).closest('.placeholder').length) {", - "lineNumber": 189, + "lineNumber": 168, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Known DOM elements" @@ -551,7 +533,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('#header, .gutter').addClass('inactive');", - "lineNumber": 193, + "lineNumber": 172, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Hardcoded selector" @@ -560,7 +542,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.conversation-stack').addClass('inactive');", - "lineNumber": 197, + "lineNumber": 176, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Hardcoded selector" @@ -569,7 +551,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.conversation:first .menu').trigger('close');", - "lineNumber": 199, + "lineNumber": 178, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Hardcoded selector" @@ -578,7 +560,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {", - "lineNumber": 219, + "lineNumber": 198, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Known DOM elements" @@ -587,7 +569,7 @@ "rule": "jQuery-$(", "path": "js/views/inbox_view.js", "line": " this.$('.conversation:first .recorder').trigger('close');", - "lineNumber": 222, + "lineNumber": 201, "reasonCategory": "usageTrusted", "updated": "2019-10-21T22:30:15.622Z", "reasonDetail": "Hardcoded selector" @@ -9323,4 +9305,4 @@ "reasonCategory": "falseMatch", "updated": "2020-02-07T19:52:28.522Z" } -] \ No newline at end of file +] diff --git a/ts/util/registration.ts b/ts/util/registration.ts new file mode 100644 index 0000000000..a45c5beac4 --- /dev/null +++ b/ts/util/registration.ts @@ -0,0 +1,27 @@ +import * as RegistrationSelectors from '../state/selectors/registration'; + +export function markEverDone() { + // @ts-ignore + window.storage.put('chromiumRegistrationDoneEver', ''); +} + +export function markDone() { + markEverDone(); + // @ts-ignore + window.storage.put('chromiumRegistrationDone', ''); +} + +export function remove() { + // @ts-ignore + window.storage.remove('chromiumRegistrationDone'); +} + +export function isDone() { + // @ts-ignore + return RegistrationSelectors.isDone(window.reduxStore.getState()); +} + +export function everDone() { + // @ts-ignore + return RegistrationSelectors.everDone(window.reduxStore.getState()); +}