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());
+}