Move all status/alert dialogs into the Left Pane
This commit is contained in:
parent
101070bf42
commit
18fd44f504
50 changed files with 1298 additions and 607 deletions
|
@ -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"
|
||||
|
|
|
@ -54,7 +54,6 @@
|
|||
|
||||
<script type='text/x-tmpl-mustache' id='two-column'>
|
||||
<div class='gutter'>
|
||||
<div class='network-status-container' aria-live='assertive'></div>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
</div>
|
||||
<div class='conversation-stack'>
|
||||
|
@ -72,13 +71,6 @@
|
|||
<div class='lightbox-container'></div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='expired_alert'>
|
||||
<a target='_blank' href='https://signal.org/download/'>
|
||||
<button class='upgrade'>{{ upgrade }}</button>
|
||||
</a>
|
||||
{{ expiredWarning }}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='banner'>
|
||||
<div class='body'>
|
||||
<span class='icon warning'></span>
|
||||
|
@ -227,23 +219,6 @@
|
|||
{{/isStep2}}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='networkStatus'>
|
||||
<div class='network-status-message'>
|
||||
<h3>{{ message }}</h3>
|
||||
<span>{{ instructions }}</span>
|
||||
</div>
|
||||
{{ #reconnectDurationAsSeconds }}
|
||||
<div class="network-status-message">
|
||||
{{ attemptingReconnectionMessage }}
|
||||
</div>
|
||||
{{/reconnectDurationAsSeconds }}
|
||||
{{ #action }}
|
||||
<div class="action">
|
||||
<button class='small blue {{ buttonClass }}'>{{ action }}</button>
|
||||
</div>
|
||||
{{/action }}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='import-flow-template'>
|
||||
{{#isStep2}}
|
||||
<div id='step2' class='step'>
|
||||
|
@ -464,8 +439,6 @@
|
|||
<script type='text/javascript' src='js/expiring_tap_to_view_messages.js'></script>
|
||||
|
||||
<script type='text/javascript' src='js/chromium.js'></script>
|
||||
<script type='text/javascript' src='js/registration.js'></script>
|
||||
<script type='text/javascript' src='js/expire.js'></script>
|
||||
<script type='text/javascript' src='js/conversation_controller.js'></script>
|
||||
<script type='text/javascript' src='js/message_controller.js'></script>
|
||||
|
||||
|
@ -479,7 +452,6 @@
|
|||
<script type='text/javascript' src='js/views/recorder_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/conversation_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/inbox_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/network_status_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/confirmation_dialog_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/identicon_svg_view.js'></script>
|
||||
<script type='text/javascript' src='js/views/install_view.js'></script>
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><circle cx="11" cy="-1041.36" r="8" transform="matrix(1 0 0-1 0-1030.36)" opacity=".98" fill="#da4453"/><path d="m-26.309 18.07c-1.18 0-2.135.968-2.135 2.129v12.82c0 1.176.948 2.129 2.135 2.129 1.183 0 2.135-.968 2.135-2.129v-12.82c0-1.176-.946-2.129-2.135-2.129zm0 21.348c-1.18 0-2.135.954-2.135 2.135 0 1.18.954 2.135 2.135 2.135 1.181 0 2.135-.954 2.135-2.135 0-1.18-.952-2.135-2.135-2.135z" transform="matrix(.30056 0 0 .30056 18.902 1.728)" fill="#fff" stroke="#fff"/></svg>
|
Before Width: | Height: | Size: 539 B |
|
@ -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
|
||||
|
|
22
js/expire.js
22
js/expire.js
|
@ -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;
|
||||
})();
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -83,7 +83,7 @@
|
|||
}
|
||||
|
||||
events.on('timetravel', () => {
|
||||
if (Whisper.Registration.isDone()) {
|
||||
if (window.Signal.Util.Registration.isDone()) {
|
||||
setTimeoutForNextRun();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -29,7 +29,6 @@
|
|||
});
|
||||
},
|
||||
events: {
|
||||
'click .openInstaller': 'openInstaller', // NetworkStatusView has this button
|
||||
openInbox: 'openInbox',
|
||||
},
|
||||
applyTheme() {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -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);
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -35,7 +35,6 @@
|
|||
|
||||
<script type='text/x-tmpl-mustache' id='two-column'>
|
||||
<div class='gutter'>
|
||||
<div class='network-status-container'></div>
|
||||
<div class='left-pane-placeholder'></div>
|
||||
</div>
|
||||
<div class='conversation-stack'>
|
||||
|
@ -66,13 +65,6 @@
|
|||
</div>
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='expired_alert'>
|
||||
<a target='_blank' href='https://signal.org/download/'>
|
||||
<button class='upgrade'>{{ upgrade }}</button>
|
||||
</a>
|
||||
{{ expiredWarning }}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='banner'>
|
||||
<div class='body'>
|
||||
<span class='icon warning'></span>
|
||||
|
@ -237,23 +229,6 @@
|
|||
{{/isStep2}}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='networkStatus'>
|
||||
<div class='network-status-message'>
|
||||
<h3>{{ message }}</h3>
|
||||
<span>{{ instructions }}</span>
|
||||
</div>
|
||||
{{ #reconnectDurationAsSeconds }}
|
||||
<div class="network-status-message">
|
||||
{{ attemptingReconnectionMessage }}
|
||||
</div>
|
||||
{{/reconnectDurationAsSeconds }}
|
||||
{{ #action }}
|
||||
<div class="action">
|
||||
<button class='small blue {{ buttonClass }}'>{{ action }}</button>
|
||||
</div>
|
||||
{{/action }}
|
||||
</script>
|
||||
|
||||
<script type='text/x-tmpl-mustache' id='import-flow-template'>
|
||||
{{#isStep2}}
|
||||
<div id='step2' class='step'>
|
||||
|
@ -459,7 +434,6 @@
|
|||
<script type="text/javascript" src="test.js"></script>
|
||||
|
||||
<script type='text/javascript' src='../js/registration.js' data-cover></script>
|
||||
<script type="text/javascript" src="../js/expire.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/chromium.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/database.js" data-cover></script>
|
||||
<script type="text/javascript" src="../js/storage.js" data-cover></script>
|
||||
|
@ -490,7 +464,6 @@
|
|||
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/conversation_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/inbox_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/network_status_view.js'></script>
|
||||
<script type='text/javascript' src='../js/views/confirmation_dialog_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/identicon_svg_view.js' data-cover></script>
|
||||
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
|
||||
|
@ -500,7 +473,6 @@
|
|||
|
||||
<script type="text/javascript" src="views/whisper_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/list_view_test.js"></script>
|
||||
<script type="text/javascript" src="views/network_status_view_test.js"></script>
|
||||
|
||||
<script type="text/javascript" src="models/messages_test.js"></script>
|
||||
|
||||
|
|
|
@ -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', () => {});
|
||||
});
|
||||
});
|
||||
});
|
21
ts/components/ExpiredBuildDialog.stories.tsx
Normal file
21
ts/components/ExpiredBuildDialog.stories.tsx
Normal file
|
@ -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 <ExpiredBuildDialog hasExpired={hasExpired} i18n={i18n} />;
|
||||
}
|
||||
);
|
34
ts/components/ExpiredBuildDialog.tsx
Normal file
34
ts/components/ExpiredBuildDialog.tsx
Normal file
|
@ -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 (
|
||||
<div className="module-left-pane-dialog module-left-pane-dialog--error">
|
||||
{i18n('expiredWarning')}
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<a
|
||||
className="module-left-pane-dialog__link"
|
||||
href="https://signal.org/download/"
|
||||
rel="noreferrer"
|
||||
tabIndex={-1}
|
||||
target="_blank"
|
||||
>
|
||||
<button className="upgrade">{i18n('upgrade')}</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -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<PropsType> {
|
|||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { renderMainHeader, showArchived } = this.props;
|
||||
const {
|
||||
renderExpiredBuildDialog,
|
||||
renderMainHeader,
|
||||
renderNetworkStatus,
|
||||
renderUpdateDialog,
|
||||
showArchived,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-left-pane">
|
||||
<div className="module-left-pane__header">
|
||||
{showArchived ? this.renderArchivedHeader() : renderMainHeader()}
|
||||
</div>
|
||||
{renderExpiredBuildDialog()}
|
||||
{renderNetworkStatus()}
|
||||
{renderUpdateDialog()}
|
||||
{this.renderList()}
|
||||
</div>
|
||||
);
|
||||
|
|
98
ts/components/NetworkStatus.stories.tsx
Normal file
98
ts/components/NetworkStatus.stories.tsx
Normal file
|
@ -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 (
|
||||
<NetworkStatus
|
||||
{...defaultProps}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
isOnline={isOnline}
|
||||
isRegistrationDone={isRegistrationDone}
|
||||
socketStatus={socketStatus}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<NetworkStatus {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
83
ts/components/NetworkStatus.tsx
Normal file
83
ts/components/NetworkStatus.tsx
Normal file
|
@ -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 (
|
||||
<div className="module-left-pane-dialog module-left-pane-dialog--warning">
|
||||
<div className="module-left-pane-dialog__message">
|
||||
<h3>{title}</h3>
|
||||
<span>{subtext}</span>
|
||||
</div>
|
||||
{renderActionableButton && renderActionableButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 => (
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<button onClick={relinkDevice}>{i18n('relink')}</button>
|
||||
</div>
|
||||
),
|
||||
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,
|
||||
});
|
||||
};
|
73
ts/components/UpdateDialog.stories.tsx
Normal file
73
ts/components/UpdateDialog.stories.tsx
Normal file
|
@ -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 (
|
||||
<UpdateDialog
|
||||
{...defaultProps}
|
||||
dialogType={dialogType}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<UpdateDialog {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
135
ts/components/UpdateDialog.tsx
Normal file
135
ts/components/UpdateDialog.tsx
Normal file
|
@ -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<React.SetStateAction<MaybeMoment>>;
|
||||
|
||||
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<MaybeMoment>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
ackRender();
|
||||
});
|
||||
|
||||
if (hasNetworkDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === Dialogs.None || isSnoozed(snoozeUntil)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === Dialogs.Cannot_Update) {
|
||||
return (
|
||||
<div className="module-left-pane-dialog module-left-pane-dialog--warning">
|
||||
<div className="module-left-pane-dialog__message">
|
||||
<h3>{i18n('cannotUpdate')}</h3>
|
||||
<span>
|
||||
<Intl
|
||||
components={[
|
||||
<a
|
||||
key="signal-download"
|
||||
href="https://signal.org/download/"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
https://signal.org/download/
|
||||
</a>,
|
||||
]}
|
||||
i18n={i18n}
|
||||
id="cannotUpdateDetail"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (dialogType === Dialogs.MacOS_Read_Only) {
|
||||
return (
|
||||
<div className="module-left-pane-dialog module-left-pane-dialog--warning">
|
||||
<div className="module-left-pane-dialog__message">
|
||||
<h3>{i18n('cannotUpdate')}</h3>
|
||||
<span>
|
||||
<Intl
|
||||
components={[
|
||||
<strong key="app">Signal.app</strong>,
|
||||
<strong key="folder">/Applications</strong>,
|
||||
]}
|
||||
i18n={i18n}
|
||||
id="readOnlyVolume"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<button onClick={dismissDialog}>{i18n('ok')}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-left-pane-dialog">
|
||||
<div className="module-left-pane-dialog__message">
|
||||
<h3>{i18n('autoUpdateNewVersionTitle')}</h3>
|
||||
<span>{i18n('autoUpdateNewVersionMessage')}</span>
|
||||
</div>
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
{canSnooze(snoozeUntil) && (
|
||||
<button
|
||||
className="module-left-pane-dialog__button--no-border"
|
||||
onClick={() => {
|
||||
handleSnooze(setSnoozeForLater);
|
||||
}}
|
||||
>
|
||||
{i18n('autoUpdateLaterButtonLabel')}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={startUpdate}>
|
||||
{i18n('autoUpdateRestartButtonLabel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
40
ts/services/networkObserver.ts
Normal file
40
ts/services/networkObserver.ts
Normal file
|
@ -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);
|
||||
}
|
13
ts/services/updateListener.ts
Normal file
13
ts/services/updateListener.ts
Normal file
|
@ -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);
|
||||
});
|
||||
}
|
12
ts/shims/socketStatus.ts
Normal file
12
ts/shims/socketStatus.ts
Normal file
|
@ -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();
|
||||
}
|
9
ts/shims/updateIpc.ts
Normal file
9
ts/shims/updateIpc.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { ipcRenderer } from 'electron';
|
||||
|
||||
export function startUpdate() {
|
||||
ipcRenderer.send('start-update');
|
||||
}
|
||||
|
||||
export function ackRender() {
|
||||
ipcRenderer.send('show-update-dialog-ack');
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
50
ts/state/ducks/expiration.ts
Normal file
50
ts/state/ducks/expiration.ts
Normal file
|
@ -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;
|
||||
}
|
104
ts/state/ducks/network.ts
Normal file
104
ts/state/ducks/network.ts
Normal file
|
@ -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;
|
||||
}
|
106
ts/state/ducks/updates.ts
Normal file
106
ts/state/ducks/updates.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
21
ts/state/selectors/network.ts
Normal file
21
ts/state/selectors/network.ts
Normal file
|
@ -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
|
||||
);
|
18
ts/state/selectors/registration.ts
Normal file
18
ts/state/selectors/registration.ts
Normal file
|
@ -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 === ''
|
||||
);
|
16
ts/state/smart/ExpiredBuildDialog.tsx
Normal file
16
ts/state/smart/ExpiredBuildDialog.tsx
Normal file
|
@ -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);
|
|
@ -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 <FilteredSmartExpiredBuildDialog />;
|
||||
}
|
||||
function renderMainHeader(): JSX.Element {
|
||||
return <FilteredSmartMainHeader />;
|
||||
}
|
||||
function renderMessageSearchResult(id: string): JSX.Element {
|
||||
return <FilteredSmartMessageSearchResult id={id} />;
|
||||
}
|
||||
function renderUpdateDialog(): JSX.Element {
|
||||
return <FilteredSmartUpdateDialog />;
|
||||
}
|
||||
function renderNetworkStatus(): JSX.Element {
|
||||
return <FilteredSmartNetworkStatus />;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
20
ts/state/smart/NetworkStatus.tsx
Normal file
20
ts/state/smart/NetworkStatus.tsx
Normal file
|
@ -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);
|
18
ts/state/smart/UpdateDialog.tsx
Normal file
18
ts/state/smart/UpdateDialog.tsx
Normal file
|
@ -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);
|
6
ts/types/Dialogs.ts
Normal file
6
ts/types/Dialogs.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export enum Dialogs {
|
||||
None,
|
||||
Update,
|
||||
Cannot_Update,
|
||||
MacOS_Read_Only,
|
||||
}
|
8
ts/types/SocketStatus.ts
Normal file
8
ts/types/SocketStatus.ts
Normal file
|
@ -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,
|
||||
}
|
|
@ -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<boolean> {
|
||||
) {
|
||||
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<any> {
|
||||
) {
|
||||
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 {
|
||||
|
|
|
@ -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> {
|
||||
): 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],
|
||||
|
|
|
@ -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 {
|
||||
|
|
47
ts/util/hasExpired.ts
Normal file
47
ts/util/hasExpired.ts
Normal file
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
27
ts/util/registration.ts
Normal file
27
ts/util/registration.ts
Normal file
|
@ -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());
|
||||
}
|
Loading…
Add table
Reference in a new issue