New option for control over update downloads
This commit is contained in:
parent
80c1ad6ee3
commit
e9308bbafb
49 changed files with 1230 additions and 803 deletions
|
@ -183,6 +183,10 @@
|
|||
"message": "Chat Color",
|
||||
"description": "One of the menu options available in the Avatar Popup menu"
|
||||
},
|
||||
"avatarMenuUpdateAvailable": {
|
||||
"message": "Update Signal",
|
||||
"description": "One of the menu options available in the Avatar Popup menu"
|
||||
},
|
||||
"loading": {
|
||||
"message": "Loading...",
|
||||
"description": "Message shown on the loading screen before we've loaded any messages"
|
||||
|
@ -640,15 +644,15 @@
|
|||
"description": "Displayed when the desktop client cannot connect to the server."
|
||||
},
|
||||
"connecting": {
|
||||
"message": "Connecting",
|
||||
"message": "Connecting...",
|
||||
"description": "Displayed when the desktop client is currently connecting to the server."
|
||||
},
|
||||
"connect": {
|
||||
"message": "Connect",
|
||||
"message": "Click to reconnect.",
|
||||
"description": "Shown to allow the user to manually attempt a reconnect."
|
||||
},
|
||||
"connectingHangOn": {
|
||||
"message": "Shouldn't be long...",
|
||||
"message": "Shouldn't be long",
|
||||
"description": "Subtext description for when the client is connecting to the server."
|
||||
},
|
||||
"offline": {
|
||||
|
@ -796,6 +800,20 @@
|
|||
"welcomeToSignal": {
|
||||
"message": "Welcome to Signal"
|
||||
},
|
||||
"whatsNew": {
|
||||
"message": "See $whatsNew$ in this update",
|
||||
"description": "Shown in the main window",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"content": "$1",
|
||||
"example": "what's new"
|
||||
}
|
||||
}
|
||||
},
|
||||
"viewReleaseNotes": {
|
||||
"message": "what's new",
|
||||
"description": "Clickable link that displays the latest release notes"
|
||||
},
|
||||
"selectAContact": {
|
||||
"message": "Select a contact or group to start chatting."
|
||||
},
|
||||
|
@ -1791,7 +1809,7 @@
|
|||
"description": "Warning notification that this version of the app has expired"
|
||||
},
|
||||
"upgrade": {
|
||||
"message": "Upgrade",
|
||||
"message": "Click to go to signal.org/download",
|
||||
"description": "Label text for button to upgrade the app to the latest version"
|
||||
},
|
||||
"mediaMessage": {
|
||||
|
@ -2210,10 +2228,13 @@
|
|||
"message": "Relink"
|
||||
},
|
||||
"autoUpdateNewVersionTitle": {
|
||||
"message": "Signal update available"
|
||||
"message": "Update available"
|
||||
},
|
||||
"autoUpdateNewVersionMessage": {
|
||||
"message": "There is a new version of Signal available."
|
||||
"message": "Click to restart Signal"
|
||||
},
|
||||
"downloadNewVersionMessage": {
|
||||
"message": "Click to download update"
|
||||
},
|
||||
"autoUpdateNewVersionInstructions": {
|
||||
"message": "Press Restart Signal to apply the updates."
|
||||
|
@ -6091,5 +6112,43 @@
|
|||
"Preferences--typing-indicators": {
|
||||
"message": "Typing indicators",
|
||||
"description": "Label for the typing indicators setting"
|
||||
},
|
||||
"Preferences--updates": {
|
||||
"message": "Updates",
|
||||
"description": "Header for settings having to do with updates"
|
||||
},
|
||||
"Preferences__download-update": {
|
||||
"message": "Automatically download updates",
|
||||
"description": "Label for checkbox for the auto download updates setting"
|
||||
},
|
||||
"DialogUpdate--version-available": {
|
||||
"message": "Update to version $version$ available",
|
||||
"description": "Tooltip for new update available",
|
||||
"placeholders": {
|
||||
"status": {
|
||||
"content": "$1",
|
||||
"example": "v7.7.7"
|
||||
}
|
||||
}
|
||||
},
|
||||
"WhatsNew__v5.15--1": {
|
||||
"message": "No that's not speck of dust you need to flick off your monitor, there's now a dot for unplayed incoming audio messages.",
|
||||
"description": "Release notes for v5.15"
|
||||
},
|
||||
"WhatsNew__v5.15--2": {
|
||||
"message": "The calling lobby got some remodeling and renovations done and we didn't even have to refinance.",
|
||||
"description": "Release notes for v5.15"
|
||||
},
|
||||
"WhatsNew__v5.15--3": {
|
||||
"message": "The new preferences window is better and faster. Go ahead and change your zoom level, toggle the theme, set a custom disappearing timer.",
|
||||
"description": "Release notes for v5.15"
|
||||
},
|
||||
"WhatsNew__v5.15--4": {
|
||||
"message": "You can now choose when to download and apply new updates for Signal. The dialogs got a small makeover too. Check out the setting in the new preferences window.",
|
||||
"description": "Release notes for v5.15"
|
||||
},
|
||||
"WhatsNew__v5.15--5": {
|
||||
"message": "Squashed lots of bugs and there are some performance improvements as well. Thank you all for your reports!",
|
||||
"description": "Release notes for v5.15"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,7 @@
|
|||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p class="whats-new-placeholder"></p>
|
||||
<p>{{ selectAContact }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
1
images/icons/v2/offline-22.svg
Normal file
1
images/icons/v2/offline-22.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><path d="M19.62,8.32A.85.85,0,0,1,19,8.07a11.51,11.51,0,0,0-4.26-2.69.87.87,0,1,1,.57-1.65,13.23,13.23,0,0,1,4.92,3.1.87.87,0,0,1,0,1.24A.86.86,0,0,1,19.62,8.32ZM3,8.07A11.51,11.51,0,0,1,7.25,5.38a.87.87,0,1,0-.57-1.65,13.23,13.23,0,0,0-4.92,3.1.87.87,0,0,0,0,1.24A.88.88,0,0,0,3,8.07Zm14.18,3.07a.88.88,0,0,0,0-1.24,8.71,8.71,0,0,0-2-1.5.87.87,0,1,0-.83,1.54,6.7,6.7,0,0,1,1.6,1.2.88.88,0,0,0,.62.25A.86.86,0,0,0,17.17,11.14Zm-11.1,0a6.7,6.7,0,0,1,1.6-1.2A.87.87,0,0,0,6.84,8.4a8.71,8.71,0,0,0-2,1.5.88.88,0,0,0,0,1.24.86.86,0,0,0,.62.25A.88.88,0,0,0,6.07,11.14ZM11,16a1.5,1.5,0,1,0,1.5,1.5A1.5,1.5,0,0,0,11,16ZM12.26,2.4a.85.85,0,0,0-.64-.28H10.38a.85.85,0,0,0-.64.28.88.88,0,0,0-.24.65l.63,10a.87.87,0,0,0,1.74,0l.63-10A.88.88,0,0,0,12.26,2.4Zm1,11.19a.88.88,0,1,0,1.75,0,.88.88,0,0,0-1.75,0ZM7,13.59a.88.88,0,1,0,.87-.87A.88.88,0,0,0,7,13.59Z"/></svg>
|
After Width: | Height: | Size: 927 B |
|
@ -59,6 +59,7 @@ const {
|
|||
const {
|
||||
SystemTraySettingsCheckboxes,
|
||||
} = require('../../ts/components/conversation/SystemTraySettingsCheckboxes');
|
||||
const { WhatsNew } = require('../../ts/components/WhatsNew');
|
||||
|
||||
// State
|
||||
const { createTimeline } = require('../../ts/state/roots/createTimeline');
|
||||
|
@ -359,6 +360,7 @@ exports.setup = (options = {}) => {
|
|||
Types: {
|
||||
Message: MediaGalleryMessage,
|
||||
},
|
||||
WhatsNew,
|
||||
};
|
||||
|
||||
const Roots = {
|
||||
|
|
|
@ -93,6 +93,8 @@
|
|||
model: { window: options.window },
|
||||
});
|
||||
|
||||
this.renderWhatsNew();
|
||||
|
||||
Whisper.events.on('refreshConversation', ({ oldId, newId }) => {
|
||||
const convo = this.conversation_stack.lastConversation;
|
||||
if (convo && convo.get('id') === oldId) {
|
||||
|
@ -153,6 +155,18 @@
|
|||
events: {
|
||||
click: 'onClick',
|
||||
},
|
||||
renderWhatsNew() {
|
||||
if (this.whatsNewView) {
|
||||
return;
|
||||
}
|
||||
this.whatsNewView = new Whisper.ReactWrapperView({
|
||||
Component: window.Signal.Components.WhatsNew,
|
||||
props: {
|
||||
i18n: window.i18n,
|
||||
},
|
||||
});
|
||||
this.$('.whats-new-placeholder').append(this.whatsNewView.el);
|
||||
},
|
||||
setupLeftPane() {
|
||||
if (this.leftPaneView) {
|
||||
return;
|
||||
|
|
2
main.js
2
main.js
|
@ -666,7 +666,7 @@ async function readyForUpdates() {
|
|||
|
||||
// Second, start checking for app updates
|
||||
try {
|
||||
await updater.start(getMainWindow, locale, logger);
|
||||
await updater.start(getMainWindow, logger);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Error starting update checks:',
|
||||
|
|
|
@ -3683,6 +3683,21 @@ button.module-conversation-details__action-button {
|
|||
|
||||
&__avatar {
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&--container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&--badged {
|
||||
background: $color-ultramarine;
|
||||
border-radius: 100%;
|
||||
border: 1px solid $color-white;
|
||||
height: 8px;
|
||||
width: 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__search {
|
||||
|
@ -7781,79 +7796,6 @@ 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-ultramarine;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include mouse-mode {
|
||||
box-shadow: 0 0 0 3px $color-ultramarine;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@ -8740,6 +8682,15 @@ button.module-image__border-overlay:focus {
|
|||
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
&--update {
|
||||
@include light-theme {
|
||||
@include color-svg('../images/icons/v2/refresh-24.svg', $color-gray-75);
|
||||
}
|
||||
@include dark-theme {
|
||||
@include color-svg('../images/icons/v2/refresh-24.svg', $color-gray-15);
|
||||
}
|
||||
}
|
||||
}
|
||||
.module-avatar-popup__item__icon-settings {
|
||||
@include light-theme {
|
||||
|
@ -8771,9 +8722,18 @@ button.module-image__border-overlay:focus {
|
|||
}
|
||||
|
||||
.module-avatar-popup__item__text {
|
||||
flex-grow: 1;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.module-avatar-popup__item--badge {
|
||||
background: $color-ultramarine;
|
||||
border-radius: 100%;
|
||||
height: 8px;
|
||||
margin-right: 10px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
// Module: Shortcut Guide Modal
|
||||
|
||||
.module-shortcut-guide-modal {
|
||||
|
|
156
stylesheets/components/LeftPaneDialog.scss
Normal file
156
stylesheets/components/LeftPaneDialog.scss
Normal file
|
@ -0,0 +1,156 @@
|
|||
@keyframes progress-animation {
|
||||
0% {
|
||||
background-position: 100%;
|
||||
}
|
||||
100% {
|
||||
background-position: -100%;
|
||||
}
|
||||
}
|
||||
|
||||
.LeftPaneDialog {
|
||||
align-items: center;
|
||||
background: $color-ultramarine;
|
||||
color: $color-white;
|
||||
display: flex;
|
||||
min-height: 64px;
|
||||
padding: 12px 18px;
|
||||
|
||||
&__container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__container-close {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
&__spinner-container {
|
||||
margin-right: 18px;
|
||||
}
|
||||
|
||||
&__spinner {
|
||||
&__arc {
|
||||
background-color: $color-black;
|
||||
}
|
||||
|
||||
&__circle {
|
||||
background-color: $color-accent-yellow;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 18px;
|
||||
background-color: $color-white;
|
||||
|
||||
&--network {
|
||||
-webkit-mask: url('../images/icons/v2/offline-22.svg') no-repeat center;
|
||||
}
|
||||
|
||||
&--update {
|
||||
-webkit-mask: url('../images/icons/v2/refresh-24.svg') no-repeat center;
|
||||
}
|
||||
}
|
||||
|
||||
&__action-text {
|
||||
@include button-reset;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&__close-button {
|
||||
@include button-reset;
|
||||
|
||||
border-radius: 4px;
|
||||
float: right;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
|
||||
&::before {
|
||||
-webkit-mask: url('../images/icons/v2/x-24.svg') no-repeat center;
|
||||
background-color: $color-white;
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $color-white-alpha-20;
|
||||
}
|
||||
&:active {
|
||||
background-color: $color-white-alpha-20;
|
||||
}
|
||||
}
|
||||
|
||||
&__message {
|
||||
width: 100%;
|
||||
|
||||
h3 {
|
||||
@include font-body-1-bold;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
span {
|
||||
@include font-body-1;
|
||||
display: inline-block;
|
||||
}
|
||||
a {
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
background-color: $color-accent-red;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background-color: $color-accent-yellow;
|
||||
color: $color-black;
|
||||
|
||||
a {
|
||||
color: $color-black;
|
||||
}
|
||||
|
||||
.LeftPaneDialog__icon {
|
||||
background-color: $color-black;
|
||||
}
|
||||
|
||||
.LeftPaneDialog__close-button::before {
|
||||
background-color: $color-black;
|
||||
}
|
||||
}
|
||||
|
||||
&__progress {
|
||||
&--container {
|
||||
background: $color-white-alpha-20;
|
||||
border-radius: 2px;
|
||||
height: 4px;
|
||||
max-width: 210px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&--bar {
|
||||
animation: progress-animation 2s linear infinite;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
$color-white-alpha-40,
|
||||
$color-white-alpha-60,
|
||||
$color-white-alpha-90,
|
||||
$color-white-alpha-60,
|
||||
$color-white-alpha-40
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
height: 100%;
|
||||
transition: width 500ms ease-out;
|
||||
}
|
||||
}
|
||||
}
|
7
stylesheets/components/WhatsNew.scss
Normal file
7
stylesheets/components/WhatsNew.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
.WhatsNew {
|
||||
@include button-reset;
|
||||
color: $color-ultramarine;
|
||||
}
|
|
@ -62,6 +62,7 @@
|
|||
@import './components/GroupInput.scss';
|
||||
@import './components/IncomingCallBar.scss';
|
||||
@import './components/Input.scss';
|
||||
@import './components/LeftPaneDialog.scss';
|
||||
@import './components/MediaQualitySelector.scss';
|
||||
@import './components/MessageAudio.scss';
|
||||
@import './components/MessageDetail.scss';
|
||||
|
@ -78,3 +79,4 @@
|
|||
@import './components/Tabs.scss';
|
||||
@import './components/TimelineWarning.scss';
|
||||
@import './components/TimelineWarnings.scss';
|
||||
@import './components/WhatsNew.scss';
|
||||
|
|
|
@ -53,6 +53,7 @@
|
|||
<div class='content'>
|
||||
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
|
||||
<h3>{{ welcomeToSignal }}</h3>
|
||||
<p class="whats-new-placeholder"></p>
|
||||
<p>{{ selectAContact }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -888,8 +888,7 @@ export async function startApp(): Promise<void> {
|
|||
window.reduxActions.network
|
||||
);
|
||||
window.Signal.Services.initializeUpdateListener(
|
||||
window.reduxActions.updates,
|
||||
window.Whisper.events
|
||||
window.reduxActions.updates
|
||||
);
|
||||
window.Signal.Services.calling.initialize(
|
||||
window.reduxActions.calling,
|
||||
|
|
|
@ -36,6 +36,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
conversationTypeMap,
|
||||
overrideProps.conversationType || 'direct'
|
||||
),
|
||||
hasPendingUpdate: Boolean(overrideProps.hasPendingUpdate),
|
||||
i18n,
|
||||
isMe: true,
|
||||
name: text('name', overrideProps.name || ''),
|
||||
|
@ -47,6 +48,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
profileName: text('profileName', overrideProps.profileName || ''),
|
||||
sharedGroupNames: [],
|
||||
size: 80,
|
||||
startUpdate: action('startUpdate'),
|
||||
style: {},
|
||||
title: text('title', overrideProps.title || ''),
|
||||
});
|
||||
|
@ -83,3 +85,11 @@ stories.add('Phone Number', () => {
|
|||
|
||||
return <AvatarPopup {...props} />;
|
||||
});
|
||||
|
||||
stories.add('Update Available', () => {
|
||||
const props = createProps({
|
||||
hasPendingUpdate: true,
|
||||
});
|
||||
|
||||
return <AvatarPopup {...props} />;
|
||||
});
|
||||
|
|
|
@ -12,6 +12,9 @@ import { LocalizerType } from '../types/Util';
|
|||
export type Props = {
|
||||
readonly i18n: LocalizerType;
|
||||
|
||||
hasPendingUpdate: boolean;
|
||||
startUpdate: () => unknown;
|
||||
|
||||
onEditProfile: () => unknown;
|
||||
onViewPreferences: () => unknown;
|
||||
onViewArchive: () => unknown;
|
||||
|
@ -23,15 +26,17 @@ export type Props = {
|
|||
|
||||
export const AvatarPopup = (props: Props): JSX.Element => {
|
||||
const {
|
||||
hasPendingUpdate,
|
||||
i18n,
|
||||
name,
|
||||
profileName,
|
||||
phoneNumber,
|
||||
title,
|
||||
onEditProfile,
|
||||
onViewPreferences,
|
||||
onViewArchive,
|
||||
onViewPreferences,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
startUpdate,
|
||||
style,
|
||||
title,
|
||||
} = props;
|
||||
|
||||
const shouldShowNumber = Boolean(name || profileName);
|
||||
|
@ -92,6 +97,24 @@ export const AvatarPopup = (props: Props): JSX.Element => {
|
|||
{i18n('avatarMenuViewArchive')}
|
||||
</div>
|
||||
</button>
|
||||
{hasPendingUpdate && (
|
||||
<button
|
||||
type="button"
|
||||
className="module-avatar-popup__item"
|
||||
onClick={startUpdate}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'module-avatar-popup__item__icon',
|
||||
'module-avatar-popup__item__icon--update'
|
||||
)}
|
||||
/>
|
||||
<div className="module-avatar-popup__item__text">
|
||||
{i18n('avatarMenuUpdateAvailable')}
|
||||
</div>
|
||||
<div className="module-avatar-popup__item--badge" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -5,17 +5,17 @@ import * as React from 'react';
|
|||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
|
||||
import { ExpiredBuildDialog } from './ExpiredBuildDialog';
|
||||
import { DialogExpiredBuild } from './DialogExpiredBuild';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
storiesOf('Components/ExpiredBuildDialog', module).add(
|
||||
'ExpiredBuildDialog',
|
||||
storiesOf('Components/DialogExpiredBuild', module).add(
|
||||
'DialogExpiredBuild',
|
||||
() => {
|
||||
const hasExpired = boolean('hasExpired', true);
|
||||
|
||||
return <ExpiredBuildDialog hasExpired={hasExpired} i18n={i18n} />;
|
||||
return <DialogExpiredBuild hasExpired={hasExpired} i18n={i18n} />;
|
||||
}
|
||||
);
|
|
@ -10,7 +10,7 @@ type PropsType = {
|
|||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export const ExpiredBuildDialog = ({
|
||||
export const DialogExpiredBuild = ({
|
||||
hasExpired,
|
||||
i18n,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
|
@ -19,19 +19,17 @@ export const ExpiredBuildDialog = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="module-left-pane-dialog module-left-pane-dialog--error">
|
||||
{i18n('expiredWarning')}
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<div className="LeftPaneDialog LeftPaneDialog--error">
|
||||
<div className="LeftPaneDialog__message">
|
||||
{i18n('expiredWarning')}{' '}
|
||||
<a
|
||||
className="module-left-pane-dialog__link"
|
||||
className="LeftPaneDialog__action-text"
|
||||
href="https://signal.org/download/"
|
||||
rel="noreferrer"
|
||||
tabIndex={-1}
|
||||
target="_blank"
|
||||
>
|
||||
<button type="button" className="upgrade">
|
||||
{i18n('upgrade')}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
69
ts/components/DialogNetworkStatus.stories.tsx
Normal file
69
ts/components/DialogNetworkStatus.stories.tsx
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { DialogNetworkStatus } from './DialogNetworkStatus';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
hasNetworkDialog: true,
|
||||
i18n,
|
||||
isOnline: true,
|
||||
socketStatus: SocketStatus.CONNECTING,
|
||||
manualReconnect: action('manual-reconnect'),
|
||||
withinConnectingGracePeriod: false,
|
||||
challengeStatus: 'idle' as const,
|
||||
};
|
||||
|
||||
const story = storiesOf('Components/DialogNetworkStatus', module);
|
||||
|
||||
story.add('Knobs Playground', () => {
|
||||
const hasNetworkDialog = boolean('hasNetworkDialog', true);
|
||||
const isOnline = boolean('isOnline', true);
|
||||
const socketStatus = select(
|
||||
'socketStatus',
|
||||
{
|
||||
CONNECTING: SocketStatus.CONNECTING,
|
||||
OPEN: SocketStatus.OPEN,
|
||||
CLOSING: SocketStatus.CLOSING,
|
||||
CLOSED: SocketStatus.CLOSED,
|
||||
},
|
||||
SocketStatus.CONNECTING
|
||||
);
|
||||
|
||||
return (
|
||||
<DialogNetworkStatus
|
||||
{...defaultProps}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
isOnline={isOnline}
|
||||
socketStatus={socketStatus}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Connecting', () => (
|
||||
<DialogNetworkStatus
|
||||
{...defaultProps}
|
||||
socketStatus={SocketStatus.CONNECTING}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Closing', () => (
|
||||
<DialogNetworkStatus {...defaultProps} socketStatus={SocketStatus.CLOSING} />
|
||||
));
|
||||
|
||||
story.add('Closed', () => (
|
||||
<DialogNetworkStatus {...defaultProps} socketStatus={SocketStatus.CLOSED} />
|
||||
));
|
||||
|
||||
story.add('Offline', () => (
|
||||
<DialogNetworkStatus {...defaultProps} isOnline={false} />
|
||||
));
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { Spinner } from './Spinner';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { NetworkStateType } from '../state/ducks/network';
|
||||
|
@ -16,28 +17,42 @@ export type PropsType = NetworkStateType & {
|
|||
};
|
||||
|
||||
type RenderDialogTypes = {
|
||||
isConnecting?: boolean;
|
||||
title: string;
|
||||
subtext: string;
|
||||
renderActionableButton?: () => JSX.Element;
|
||||
};
|
||||
|
||||
function renderDialog({
|
||||
isConnecting,
|
||||
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">
|
||||
<div className="LeftPaneDialog LeftPaneDialog--warning">
|
||||
{isConnecting ? (
|
||||
<div className="LeftPaneDialog__spinner-container">
|
||||
<Spinner
|
||||
direction="on-avatar"
|
||||
moduleClassName="LeftPaneDialog__spinner"
|
||||
size="22px"
|
||||
svgSize="small"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="LeftPaneDialog__icon LeftPaneDialog__icon--network" />
|
||||
)}
|
||||
<div className="LeftPaneDialog__message">
|
||||
<h3>{title}</h3>
|
||||
<span>{subtext}</span>
|
||||
<div>{renderActionableButton && renderActionableButton()}</div>
|
||||
</div>
|
||||
{renderActionableButton && renderActionableButton()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const NetworkStatus = ({
|
||||
export const DialogNetworkStatus = ({
|
||||
hasNetworkDialog,
|
||||
i18n,
|
||||
isOnline,
|
||||
|
@ -75,19 +90,23 @@ export const NetworkStatus = ({
|
|||
};
|
||||
|
||||
const manualReconnectButton = (): JSX.Element => (
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<button onClick={reconnect} type="button">
|
||||
<button
|
||||
className="LeftPaneDialog__action-text"
|
||||
onClick={reconnect}
|
||||
type="button"
|
||||
>
|
||||
{i18n('connect')}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (isConnecting) {
|
||||
return renderDialog({
|
||||
isConnecting: true,
|
||||
subtext: i18n('connectingHangOn'),
|
||||
title: i18n('connecting'),
|
||||
});
|
||||
}
|
||||
|
||||
if (!isOnline) {
|
||||
return renderDialog({
|
||||
renderActionableButton: manualReconnectButton,
|
||||
|
@ -114,6 +133,7 @@ export const NetworkStatus = ({
|
|||
}
|
||||
|
||||
return renderDialog({
|
||||
isConnecting: socketStatus === SocketStatus.CONNECTING,
|
||||
renderActionableButton,
|
||||
subtext,
|
||||
title,
|
64
ts/components/DialogUpdate.stories.tsx
Normal file
64
ts/components/DialogUpdate.stories.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { DialogUpdate } from './DialogUpdate';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
dismissDialog: action('dismiss-dialog'),
|
||||
downloadSize: 116504357,
|
||||
downloadedSize: 61003110,
|
||||
hasNetworkDialog: false,
|
||||
i18n,
|
||||
didSnooze: false,
|
||||
showEventsCount: 0,
|
||||
snoozeUpdate: action('snooze-update'),
|
||||
startUpdate: action('start-update'),
|
||||
version: 'v7.7.7',
|
||||
};
|
||||
|
||||
const story = storiesOf('Components/DialogUpdate', module);
|
||||
|
||||
story.add('Knobs Playground', () => {
|
||||
const dialogType = select('dialogType', DialogType, DialogType.Update);
|
||||
const hasNetworkDialog = boolean('hasNetworkDialog', false);
|
||||
const didSnooze = boolean('didSnooze', false);
|
||||
|
||||
return (
|
||||
<DialogUpdate
|
||||
{...defaultProps}
|
||||
dialogType={dialogType}
|
||||
didSnooze={didSnooze}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
story.add('Update', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.Update} />
|
||||
));
|
||||
|
||||
story.add('Download Ready', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.DownloadReady} />
|
||||
));
|
||||
|
||||
story.add('Downloading', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.Downloading} />
|
||||
));
|
||||
|
||||
story.add('Cannot Update', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.Cannot_Update} />
|
||||
));
|
||||
|
||||
story.add('macOS RO Error', () => (
|
||||
<DialogUpdate {...defaultProps} dialogType={DialogType.MacOS_Read_Only} />
|
||||
));
|
179
ts/components/DialogUpdate.tsx
Normal file
179
ts/components/DialogUpdate.tsx
Normal file
|
@ -0,0 +1,179 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import formatFileSize from 'filesize';
|
||||
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { Intl } from './Intl';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
dialogType: DialogType;
|
||||
didSnooze: boolean;
|
||||
dismissDialog: () => void;
|
||||
downloadSize?: number;
|
||||
downloadedSize?: number;
|
||||
hasNetworkDialog: boolean;
|
||||
i18n: LocalizerType;
|
||||
showEventsCount: number;
|
||||
snoozeUpdate: () => void;
|
||||
startUpdate: () => void;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export const DialogUpdate = ({
|
||||
dialogType,
|
||||
didSnooze,
|
||||
dismissDialog,
|
||||
downloadSize,
|
||||
downloadedSize,
|
||||
hasNetworkDialog,
|
||||
i18n,
|
||||
snoozeUpdate,
|
||||
startUpdate,
|
||||
version,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
if (hasNetworkDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === DialogType.None) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (didSnooze) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === DialogType.Cannot_Update) {
|
||||
return (
|
||||
<div className="LeftPaneDialog LeftPaneDialog--warning">
|
||||
<div className="LeftPaneDialog__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 === DialogType.MacOS_Read_Only) {
|
||||
return (
|
||||
<div className="LeftPaneDialog LeftPaneDialog--warning">
|
||||
<div className="LeftPaneDialog__container">
|
||||
<div className="LeftPaneDialog__message">
|
||||
<h3>{i18n('cannotUpdate')}</h3>
|
||||
<span>
|
||||
<Intl
|
||||
components={{
|
||||
app: <strong key="app">Signal.app</strong>,
|
||||
folder: <strong key="folder">/Applications</strong>,
|
||||
}}
|
||||
i18n={i18n}
|
||||
id="readOnlyVolume"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="LeftPaneDialog__container-close">
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
className="LeftPaneDialog__close-button"
|
||||
onClick={dismissDialog}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let size: string | undefined;
|
||||
if (
|
||||
downloadSize &&
|
||||
(dialogType === DialogType.DownloadReady ||
|
||||
dialogType === DialogType.Downloading)
|
||||
) {
|
||||
size = `(${formatFileSize(downloadSize, { round: 0 })})`;
|
||||
}
|
||||
|
||||
let updateSubText: JSX.Element;
|
||||
if (dialogType === DialogType.DownloadReady) {
|
||||
updateSubText = (
|
||||
<button
|
||||
className="LeftPaneDialog__action-text"
|
||||
onClick={startUpdate}
|
||||
type="button"
|
||||
>
|
||||
{i18n('downloadNewVersionMessage')}
|
||||
</button>
|
||||
);
|
||||
} else if (dialogType === DialogType.Downloading) {
|
||||
const width = Math.ceil(
|
||||
((downloadedSize || 1) / (downloadSize || 1)) * 100
|
||||
);
|
||||
|
||||
updateSubText = (
|
||||
<div className="LeftPaneDialog__progress--container">
|
||||
<div
|
||||
className="LeftPaneDialog__progress--bar"
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
updateSubText = (
|
||||
<button
|
||||
className="LeftPaneDialog__action-text"
|
||||
onClick={startUpdate}
|
||||
type="button"
|
||||
>
|
||||
{i18n('autoUpdateNewVersionMessage')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const versionTitle = version
|
||||
? i18n('DialogUpdate--version-available', [version])
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="LeftPaneDialog" title={versionTitle}>
|
||||
<div className="LeftPaneDialog__container">
|
||||
<div className="LeftPaneDialog__icon LeftPaneDialog__icon--update" />
|
||||
<div className="LeftPaneDialog__message">
|
||||
<h3>
|
||||
{i18n('autoUpdateNewVersionTitle')} {size}
|
||||
</h3>
|
||||
{updateSubText}
|
||||
</div>
|
||||
</div>
|
||||
<div className="LeftPaneDialog__container-close">
|
||||
{dialogType !== DialogType.Downloading && (
|
||||
<button
|
||||
aria-label={i18n('close')}
|
||||
className="LeftPaneDialog__close-button"
|
||||
onClick={snoozeUpdate}
|
||||
tabIndex={0}
|
||||
type="button"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -45,6 +45,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
title: requiredText('title', overrideProps.title),
|
||||
name: optionalText('name', overrideProps.name),
|
||||
avatarPath: optionalText('avatarPath', overrideProps.avatarPath),
|
||||
hasPendingUpdate: Boolean(overrideProps.hasPendingUpdate),
|
||||
|
||||
i18n,
|
||||
|
||||
|
@ -55,6 +56,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
|||
searchInConversation: action('searchInConversation'),
|
||||
clearConversationSearch: action('clearConversationSearch'),
|
||||
clearSearch: action('clearSearch'),
|
||||
startUpdate: action('startUpdate'),
|
||||
|
||||
showArchivedConversations: action('showArchivedConversations'),
|
||||
startComposing: action('startComposing'),
|
||||
|
@ -115,3 +117,9 @@ story.add('Searching Conversation with Term', () => {
|
|||
|
||||
return <MainHeader {...props} />;
|
||||
});
|
||||
|
||||
story.add('Update Available', () => {
|
||||
const props = createProps({ hasPendingUpdate: true });
|
||||
|
||||
return <MainHeader {...props} />;
|
||||
});
|
||||
|
|
|
@ -37,6 +37,7 @@ export type PropsType = {
|
|||
profileName?: string;
|
||||
title: string;
|
||||
avatarPath?: string;
|
||||
hasPendingUpdate: boolean;
|
||||
|
||||
i18n: LocalizerType;
|
||||
|
||||
|
@ -59,6 +60,7 @@ export type PropsType = {
|
|||
noteToSelf: string;
|
||||
}
|
||||
) => void;
|
||||
startUpdate: () => unknown;
|
||||
clearConversationSearch: () => void;
|
||||
clearSearch: () => void;
|
||||
|
||||
|
@ -342,16 +344,18 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
avatarPath,
|
||||
color,
|
||||
disabled,
|
||||
hasPendingUpdate,
|
||||
i18n,
|
||||
name,
|
||||
startComposing,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
title,
|
||||
searchConversationId,
|
||||
searchConversationName,
|
||||
searchTerm,
|
||||
showArchivedConversations,
|
||||
startComposing,
|
||||
startUpdate,
|
||||
title,
|
||||
toggleProfileEditor,
|
||||
} = this.props;
|
||||
const { showingAvatarPopup, popperRoot } = this.state;
|
||||
|
@ -369,6 +373,7 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div className="module-main-header__avatar--container">
|
||||
<Avatar
|
||||
acceptedMessageRequest
|
||||
avatarPath={avatarPath}
|
||||
|
@ -381,13 +386,17 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
phoneNumber={phoneNumber}
|
||||
profileName={profileName}
|
||||
title={title}
|
||||
// `sharedGroupNames` makes no sense for yourself, but `<Avatar>` needs it
|
||||
// to determine blurring.
|
||||
// `sharedGroupNames` makes no sense for yourself, but
|
||||
// `<Avatar>` needs it to determine blurring.
|
||||
sharedGroupNames={[]}
|
||||
size={28}
|
||||
innerRef={ref}
|
||||
onClick={this.showAvatarPopup}
|
||||
/>
|
||||
{hasPendingUpdate && (
|
||||
<div className="module-main-header__avatar--badged" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
{showingAvatarPopup && popperRoot
|
||||
|
@ -408,6 +417,8 @@ export class MainHeader extends React.Component<PropsType, StateType> {
|
|||
title={title}
|
||||
avatarPath={avatarPath}
|
||||
size={28}
|
||||
hasPendingUpdate={hasPendingUpdate}
|
||||
startUpdate={startUpdate}
|
||||
// See the comment above about `sharedGroupNames`.
|
||||
sharedGroupNames={[]}
|
||||
onEditProfile={() => {
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { NetworkStatus } from './NetworkStatus';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
hasNetworkDialog: true,
|
||||
i18n,
|
||||
isOnline: true,
|
||||
socketStatus: SocketStatus.CONNECTING,
|
||||
manualReconnect: action('manual-reconnect'),
|
||||
withinConnectingGracePeriod: false,
|
||||
challengeStatus: 'idle' as const,
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Connecting',
|
||||
props: {
|
||||
socketStatus: SocketStatus.CONNECTING,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Closing (online)',
|
||||
props: {
|
||||
socketStatus: SocketStatus.CLOSING,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Closed (online)',
|
||||
props: {
|
||||
socketStatus: SocketStatus.CLOSED,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Offline',
|
||||
props: {
|
||||
isOnline: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
storiesOf('Components/NetworkStatus', module)
|
||||
.add('Knobs Playground', () => {
|
||||
const hasNetworkDialog = boolean('hasNetworkDialog', true);
|
||||
const isOnline = boolean('isOnline', true);
|
||||
const socketStatus = select(
|
||||
'socketStatus',
|
||||
{
|
||||
CONNECTING: SocketStatus.CONNECTING,
|
||||
OPEN: SocketStatus.OPEN,
|
||||
CLOSING: SocketStatus.CLOSING,
|
||||
CLOSED: SocketStatus.CLOSED,
|
||||
},
|
||||
SocketStatus.CONNECTING
|
||||
);
|
||||
|
||||
return (
|
||||
<NetworkStatus
|
||||
{...defaultProps}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
isOnline={isOnline}
|
||||
socketStatus={socketStatus}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<NetworkStatus {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
|
@ -69,6 +69,7 @@ const createProps = (): PropsType => ({
|
|||
defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
|
||||
deviceName: 'Work Windows ME',
|
||||
hasAudioNotifications: true,
|
||||
hasAutoDownloadUpdate: true,
|
||||
hasAutoLaunch: true,
|
||||
hasCallNotifications: true,
|
||||
hasCallRingtoneNotification: false,
|
||||
|
@ -125,6 +126,7 @@ const createProps = (): PropsType => ({
|
|||
isSystemTraySupported: true,
|
||||
|
||||
onAudioNotificationsChange: action('onAudioNotificationsChange'),
|
||||
onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'),
|
||||
onAutoLaunchChange: action('onAutoLaunchChange'),
|
||||
onCallNotificationsChange: action('onCallNotificationsChange'),
|
||||
onCallRingtoneNotificationChange: action('onCallRingtoneNotificationChange'),
|
||||
|
|
|
@ -43,6 +43,7 @@ export type PropsType = {
|
|||
defaultConversationColor: DefaultConversationColorType;
|
||||
deviceName?: string;
|
||||
hasAudioNotifications?: boolean;
|
||||
hasAutoDownloadUpdate: boolean;
|
||||
hasAutoLaunch: boolean;
|
||||
hasCallNotifications: boolean;
|
||||
hasCallRingtoneNotification: boolean;
|
||||
|
@ -104,6 +105,7 @@ export type PropsType = {
|
|||
|
||||
// Change handlers
|
||||
onAudioNotificationsChange: CheckboxChangeHandlerType;
|
||||
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
|
||||
onAutoLaunchChange: CheckboxChangeHandlerType;
|
||||
onCallNotificationsChange: CheckboxChangeHandlerType;
|
||||
onCallRingtoneNotificationChange: CheckboxChangeHandlerType;
|
||||
|
@ -161,6 +163,7 @@ export const Preferences = ({
|
|||
editCustomColor,
|
||||
getConversationsWithCustomColor,
|
||||
hasAudioNotifications,
|
||||
hasAutoDownloadUpdate,
|
||||
hasAutoLaunch,
|
||||
hasCallNotifications,
|
||||
hasCallRingtoneNotification,
|
||||
|
@ -191,6 +194,7 @@ export const Preferences = ({
|
|||
makeSyncRequest,
|
||||
notificationContent,
|
||||
onAudioNotificationsChange,
|
||||
onAutoDownloadUpdateChange,
|
||||
onAutoLaunchChange,
|
||||
onCallNotificationsChange,
|
||||
onCallRingtoneNotificationChange,
|
||||
|
@ -340,6 +344,15 @@ export const Preferences = ({
|
|||
onChange={onMediaCameraPermissionsChange}
|
||||
/>
|
||||
</SettingsRow>
|
||||
<SettingsRow title={i18n('Preferences--updates')}>
|
||||
<Checkbox
|
||||
checked={hasAutoDownloadUpdate}
|
||||
label={i18n('Preferences__download-update')}
|
||||
moduleClassName="Preferences__checkbox"
|
||||
name="autoDownloadUpdate"
|
||||
onChange={onAutoDownloadUpdateChange}
|
||||
/>
|
||||
</SettingsRow>
|
||||
</>
|
||||
);
|
||||
} else if (page === Page.Appearance) {
|
||||
|
|
|
@ -21,12 +21,12 @@ export const RelinkDialog = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="module-left-pane-dialog module-left-pane-dialog--warning">
|
||||
<div className="module-left-pane-dialog__message">
|
||||
<div className="LeftPaneDialog LeftPaneDialog--warning">
|
||||
<div className="LeftPaneDialog__message">
|
||||
<h3>{i18n('unlinked')}</h3>
|
||||
<span>{i18n('unlinkedWarning')}</span>
|
||||
</div>
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<div className="LeftPaneDialog__actions">
|
||||
<button onClick={relinkDevice} type="button">
|
||||
{i18n('relink')}
|
||||
</button>
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { UpdateDialog } from './UpdateDialog';
|
||||
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps = {
|
||||
ackRender: action('ack-render'),
|
||||
dismissDialog: action('dismiss-dialog'),
|
||||
hasNetworkDialog: false,
|
||||
i18n,
|
||||
didSnooze: false,
|
||||
showEventsCount: 0,
|
||||
snoozeUpdate: action('snooze-update'),
|
||||
startUpdate: action('start-update'),
|
||||
};
|
||||
|
||||
const permutations = [
|
||||
{
|
||||
title: 'Update',
|
||||
props: {
|
||||
dialogType: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Update (didSnooze=true)',
|
||||
props: {
|
||||
dialogType: 1,
|
||||
didSnooze: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
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);
|
||||
const didSnooze = boolean('didSnooze', false);
|
||||
|
||||
return (
|
||||
<UpdateDialog
|
||||
{...defaultProps}
|
||||
dialogType={dialogType}
|
||||
didSnooze={didSnooze}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.add('Iterations', () => {
|
||||
return permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<UpdateDialog {...defaultProps} {...props} />
|
||||
</>
|
||||
));
|
||||
});
|
|
@ -1,117 +0,0 @@
|
|||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Dialogs } from '../types/Dialogs';
|
||||
import { Intl } from './Intl';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
ackRender: () => void;
|
||||
dialogType: Dialogs;
|
||||
didSnooze: boolean;
|
||||
dismissDialog: () => void;
|
||||
hasNetworkDialog: boolean;
|
||||
i18n: LocalizerType;
|
||||
showEventsCount: number;
|
||||
snoozeUpdate: () => void;
|
||||
startUpdate: () => void;
|
||||
};
|
||||
|
||||
export const UpdateDialog = ({
|
||||
ackRender,
|
||||
dialogType,
|
||||
didSnooze,
|
||||
dismissDialog,
|
||||
hasNetworkDialog,
|
||||
i18n,
|
||||
snoozeUpdate,
|
||||
startUpdate,
|
||||
}: PropsType): JSX.Element | null => {
|
||||
React.useEffect(() => {
|
||||
ackRender();
|
||||
});
|
||||
|
||||
if (hasNetworkDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === Dialogs.None) {
|
||||
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={{
|
||||
app: <strong key="app">Signal.app</strong>,
|
||||
folder: <strong key="folder">/Applications</strong>,
|
||||
}}
|
||||
i18n={i18n}
|
||||
id="readOnlyVolume"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className="module-left-pane-dialog__actions">
|
||||
<button type="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">
|
||||
{!didSnooze && (
|
||||
<button
|
||||
type="button"
|
||||
className="module-left-pane-dialog__button--no-border"
|
||||
onClick={snoozeUpdate}
|
||||
>
|
||||
{i18n('autoUpdateLaterButtonLabel')}
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={startUpdate}>
|
||||
{i18n('autoUpdateRestartButtonLabel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
75
ts/components/WhatsNew.tsx
Normal file
75
ts/components/WhatsNew.tsx
Normal file
|
@ -0,0 +1,75 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import { Modal } from './Modal';
|
||||
import { Intl } from './Intl';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
type ReleaseNotesType = {
|
||||
date: Date;
|
||||
version: string;
|
||||
features: Array<string>;
|
||||
};
|
||||
|
||||
export const WhatsNew = ({ i18n }: PropsType): JSX.Element => {
|
||||
const [releaseNotes, setReleaseNotes] = useState<
|
||||
ReleaseNotesType | undefined
|
||||
>();
|
||||
|
||||
const viewReleaseNotes = () => {
|
||||
setReleaseNotes({
|
||||
date: new Date('08/17/2021'),
|
||||
version: window.getVersion(),
|
||||
features: [
|
||||
'WhatsNew__v5.15--1',
|
||||
'WhatsNew__v5.15--2',
|
||||
'WhatsNew__v5.15--3',
|
||||
'WhatsNew__v5.15--4',
|
||||
'WhatsNew__v5.15--5',
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{releaseNotes && (
|
||||
<Modal
|
||||
hasXButton
|
||||
i18n={i18n}
|
||||
onClose={() => setReleaseNotes(undefined)}
|
||||
title={i18n('WhatsNew__modal-title')}
|
||||
>
|
||||
<>
|
||||
<span>
|
||||
{moment(releaseNotes.date).format('LL')} ·{' '}
|
||||
{releaseNotes.version}
|
||||
</span>
|
||||
<ul>
|
||||
{releaseNotes.features.map(featureKey => (
|
||||
<li key={featureKey}>
|
||||
<Intl i18n={i18n} id={featureKey} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
</Modal>
|
||||
)}
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="whatsNew"
|
||||
components={[
|
||||
<button className="WhatsNew" type="button" onClick={viewReleaseNotes}>
|
||||
{i18n('viewReleaseNotes')}
|
||||
</button>,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -61,6 +61,7 @@ export class SettingsChannel {
|
|||
isEphemeral: true,
|
||||
});
|
||||
|
||||
this.installSetting('autoDownloadUpdate');
|
||||
this.installSetting('autoLaunch');
|
||||
|
||||
this.installSetting('alwaysRelayCalls');
|
||||
|
|
|
@ -2,26 +2,24 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Dialogs } from '../types/Dialogs';
|
||||
import { ShowUpdateDialogAction } from '../state/ducks/updates';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import {
|
||||
UpdateDialogOptionsType,
|
||||
ShowUpdateDialogAction,
|
||||
} from '../state/ducks/updates';
|
||||
|
||||
type UpdatesActions = {
|
||||
showUpdateDialog: (x: Dialogs) => ShowUpdateDialogAction;
|
||||
showUpdateDialog: (
|
||||
x: DialogType,
|
||||
options: UpdateDialogOptionsType
|
||||
) => ShowUpdateDialogAction;
|
||||
};
|
||||
|
||||
type EventsType = {
|
||||
once: (ev: string, f: () => void) => void;
|
||||
};
|
||||
|
||||
export function initializeUpdateListener(
|
||||
updatesActions: UpdatesActions,
|
||||
events: EventsType
|
||||
): void {
|
||||
ipcRenderer.on('show-update-dialog', (_, dialogType: Dialogs) => {
|
||||
updatesActions.showUpdateDialog(dialogType);
|
||||
});
|
||||
|
||||
events.once('snooze-update', () => {
|
||||
updatesActions.showUpdateDialog(Dialogs.Update);
|
||||
});
|
||||
export function initializeUpdateListener(updatesActions: UpdatesActions): void {
|
||||
ipcRenderer.on(
|
||||
'show-update-dialog',
|
||||
(_, dialogType: DialogType, options: UpdateDialogOptionsType = {}) => {
|
||||
updatesActions.showUpdateDialog(dialogType, options);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,3 @@ import { ipcRenderer } from 'electron';
|
|||
export function startUpdate(): void {
|
||||
ipcRenderer.send('start-update');
|
||||
}
|
||||
|
||||
export function ackRender(): void {
|
||||
ipcRenderer.send('show-update-dialog-ack');
|
||||
}
|
||||
|
|
|
@ -1,86 +1,110 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Dialogs } from '../../types/Dialogs';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import * as updateIpc from '../../shims/updateIpc';
|
||||
import { trigger } from '../../shims/events';
|
||||
import { DialogType } from '../../types/Dialogs';
|
||||
import { StateType as RootStateType } from '../reducer';
|
||||
import { onTimeout } from '../../services/timers';
|
||||
|
||||
// State
|
||||
|
||||
export type UpdatesStateType = {
|
||||
dialogType: Dialogs;
|
||||
dialogType: DialogType;
|
||||
didSnooze: boolean;
|
||||
downloadSize?: number;
|
||||
downloadedSize?: number;
|
||||
showEventsCount: number;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
// Actions
|
||||
|
||||
const ACK_RENDER = 'updates/ACK_RENDER';
|
||||
const DISMISS_DIALOG = 'updates/DISMISS_DIALOG';
|
||||
const SHOW_UPDATE_DIALOG = 'updates/SHOW_UPDATE_DIALOG';
|
||||
const SNOOZE_UPDATE = 'updates/SNOOZE_UPDATE';
|
||||
const START_UPDATE = 'updates/START_UPDATE';
|
||||
const UNSNOOZE_UPDATE = 'updates/UNSNOOZE_UPDATE';
|
||||
|
||||
type AckRenderAction = {
|
||||
type: 'updates/ACK_RENDER';
|
||||
export type UpdateDialogOptionsType = {
|
||||
downloadSize?: number;
|
||||
downloadedSize?: number;
|
||||
version?: string;
|
||||
};
|
||||
|
||||
type DismissDialogAction = {
|
||||
type: 'updates/DISMISS_DIALOG';
|
||||
type: typeof DISMISS_DIALOG;
|
||||
};
|
||||
|
||||
export type ShowUpdateDialogAction = {
|
||||
type: 'updates/SHOW_UPDATE_DIALOG';
|
||||
payload: Dialogs;
|
||||
type: typeof SHOW_UPDATE_DIALOG;
|
||||
payload: {
|
||||
dialogType: DialogType;
|
||||
otherState: UpdateDialogOptionsType;
|
||||
};
|
||||
};
|
||||
|
||||
type SnoozeUpdateActionType = {
|
||||
type: 'updates/SNOOZE_UPDATE';
|
||||
type: typeof SNOOZE_UPDATE;
|
||||
};
|
||||
|
||||
type StartUpdateAction = {
|
||||
type: 'updates/START_UPDATE';
|
||||
type: typeof START_UPDATE;
|
||||
};
|
||||
|
||||
type UnsnoozeUpdateActionType = {
|
||||
type: typeof UNSNOOZE_UPDATE;
|
||||
payload: DialogType;
|
||||
};
|
||||
|
||||
export type UpdatesActionType =
|
||||
| AckRenderAction
|
||||
| DismissDialogAction
|
||||
| ShowUpdateDialogAction
|
||||
| SnoozeUpdateActionType
|
||||
| StartUpdateAction;
|
||||
| StartUpdateAction
|
||||
| UnsnoozeUpdateActionType;
|
||||
|
||||
// Action Creators
|
||||
|
||||
function ackRender(): AckRenderAction {
|
||||
updateIpc.ackRender();
|
||||
|
||||
return {
|
||||
type: ACK_RENDER,
|
||||
};
|
||||
}
|
||||
|
||||
function dismissDialog(): DismissDialogAction {
|
||||
return {
|
||||
type: DISMISS_DIALOG,
|
||||
};
|
||||
}
|
||||
|
||||
function showUpdateDialog(dialogType: Dialogs): ShowUpdateDialogAction {
|
||||
function showUpdateDialog(
|
||||
dialogType: DialogType,
|
||||
updateDialogOptions: UpdateDialogOptionsType = {}
|
||||
): ShowUpdateDialogAction {
|
||||
return {
|
||||
type: SHOW_UPDATE_DIALOG,
|
||||
payload: dialogType,
|
||||
payload: {
|
||||
dialogType,
|
||||
otherState: updateDialogOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const SNOOZE_TIMER = 60 * 1000 * 30;
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
function snoozeUpdate(): SnoozeUpdateActionType {
|
||||
setTimeout(() => {
|
||||
trigger('snooze-update');
|
||||
}, SNOOZE_TIMER);
|
||||
function snoozeUpdate(): ThunkAction<
|
||||
void,
|
||||
RootStateType,
|
||||
unknown,
|
||||
SnoozeUpdateActionType | UnsnoozeUpdateActionType
|
||||
> {
|
||||
return (dispatch, getState) => {
|
||||
const { dialogType } = getState().updates;
|
||||
onTimeout(Date.now() + ONE_DAY, () => {
|
||||
dispatch({
|
||||
type: UNSNOOZE_UPDATE,
|
||||
payload: dialogType,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
dispatch({
|
||||
type: SNOOZE_UPDATE,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -93,7 +117,6 @@ function startUpdate(): StartUpdateAction {
|
|||
}
|
||||
|
||||
export const actions = {
|
||||
ackRender,
|
||||
dismissDialog,
|
||||
showUpdateDialog,
|
||||
snoozeUpdate,
|
||||
|
@ -104,7 +127,7 @@ export const actions = {
|
|||
|
||||
function getEmptyState(): UpdatesStateType {
|
||||
return {
|
||||
dialogType: Dialogs.None,
|
||||
dialogType: DialogType.None,
|
||||
didSnooze: false,
|
||||
showEventsCount: 0,
|
||||
};
|
||||
|
@ -115,37 +138,46 @@ export function reducer(
|
|||
action: Readonly<UpdatesActionType>
|
||||
): UpdatesStateType {
|
||||
if (action.type === SHOW_UPDATE_DIALOG) {
|
||||
const { dialogType, otherState } = action.payload;
|
||||
|
||||
return {
|
||||
dialogType: action.payload,
|
||||
didSnooze: state.didSnooze,
|
||||
...state,
|
||||
...otherState,
|
||||
dialogType,
|
||||
showEventsCount: state.showEventsCount + 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === SNOOZE_UPDATE) {
|
||||
return {
|
||||
dialogType: Dialogs.None,
|
||||
...state,
|
||||
dialogType: DialogType.None,
|
||||
didSnooze: true,
|
||||
showEventsCount: state.showEventsCount,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === START_UPDATE) {
|
||||
return {
|
||||
dialogType: Dialogs.None,
|
||||
didSnooze: state.didSnooze,
|
||||
showEventsCount: state.showEventsCount,
|
||||
...state,
|
||||
dialogType: DialogType.None,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
action.type === DISMISS_DIALOG &&
|
||||
state.dialogType === Dialogs.MacOS_Read_Only
|
||||
state.dialogType === DialogType.MacOS_Read_Only
|
||||
) {
|
||||
return {
|
||||
dialogType: Dialogs.None,
|
||||
didSnooze: state.didSnooze,
|
||||
showEventsCount: state.showEventsCount,
|
||||
...state,
|
||||
dialogType: DialogType.None,
|
||||
};
|
||||
}
|
||||
|
||||
if (action.type === UNSNOOZE_UPDATE) {
|
||||
return {
|
||||
...state,
|
||||
dialogType: action.payload,
|
||||
didSnooze: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { ExpiredBuildDialog } from '../../components/ExpiredBuildDialog';
|
||||
import { DialogExpiredBuild } from '../../components/DialogExpiredBuild';
|
||||
import { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
|
@ -16,4 +16,4 @@ const mapStateToProps = (state: StateType) => {
|
|||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartExpiredBuildDialog = smart(ExpiredBuildDialog);
|
||||
export const SmartExpiredBuildDialog = smart(DialogExpiredBuild);
|
||||
|
|
|
@ -25,6 +25,7 @@ import { getMe, getSelectedConversation } from '../selectors/conversations';
|
|||
const mapStateToProps = (state: StateType) => {
|
||||
return {
|
||||
disabled: state.network.challengeStatus !== 'idle',
|
||||
hasPendingUpdate: Boolean(state.updates.didSnooze),
|
||||
searchTerm: getQuery(state),
|
||||
searchConversationId: getSearchConversationId(state),
|
||||
searchConversationName: getSearchConversationName(state),
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { NetworkStatus } from '../../components/NetworkStatus';
|
||||
import { DialogNetworkStatus } from '../../components/DialogNetworkStatus';
|
||||
import { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { hasNetworkDialog } from '../selectors/network';
|
||||
|
@ -18,4 +18,4 @@ const mapStateToProps = (state: StateType) => {
|
|||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartNetworkStatus = smart(NetworkStatus);
|
||||
export const SmartNetworkStatus = smart(DialogNetworkStatus);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { UpdateDialog } from '../../components/UpdateDialog';
|
||||
import { DialogUpdate } from '../../components/DialogUpdate';
|
||||
import { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { hasNetworkDialog } from '../selectors/network';
|
||||
|
@ -18,4 +18,4 @@ const mapStateToProps = (state: StateType) => {
|
|||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartUpdateDialog = smart(UpdateDialog);
|
||||
export const SmartUpdateDialog = smart(DialogUpdate);
|
||||
|
|
|
@ -9,10 +9,11 @@ import {
|
|||
getVersion,
|
||||
isUpdateFileNameValid,
|
||||
validatePath,
|
||||
parseYaml,
|
||||
} from '../../updater/common';
|
||||
|
||||
describe('updater/signatures', () => {
|
||||
const windows = `version: 1.23.2
|
||||
const windows = parseYaml(`version: 1.23.2
|
||||
files:
|
||||
- url: signal-desktop-win-1.23.2.exe
|
||||
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
|
||||
|
@ -20,8 +21,8 @@ files:
|
|||
path: signal-desktop-win-1.23.2.exe
|
||||
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
|
||||
releaseDate: '2019-03-29T16:58:08.210Z'
|
||||
`;
|
||||
const mac = `version: 1.23.2
|
||||
`);
|
||||
const mac = parseYaml(`version: 1.23.2
|
||||
files:
|
||||
- url: signal-desktop-mac-1.23.2.zip
|
||||
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
|
||||
|
@ -30,8 +31,8 @@ files:
|
|||
path: signal-desktop-mac-1.23.2.zip
|
||||
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
|
||||
releaseDate: '2019-03-29T16:57:16.997Z'
|
||||
`;
|
||||
const windowsBeta = `version: 1.23.2-beta.1
|
||||
`);
|
||||
const windowsBeta = parseYaml(`version: 1.23.2-beta.1
|
||||
files:
|
||||
- url: signal-desktop-beta-win-1.23.2-beta.1.exe
|
||||
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
|
||||
|
@ -39,8 +40,8 @@ files:
|
|||
path: signal-desktop-beta-win-1.23.2-beta.1.exe
|
||||
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
|
||||
releaseDate: '2019-03-29T01:56:00.544Z'
|
||||
`;
|
||||
const macBeta = `version: 1.23.2-beta.1
|
||||
`);
|
||||
const macBeta = parseYaml(`version: 1.23.2-beta.1
|
||||
files:
|
||||
- url: signal-desktop-beta-mac-1.23.2-beta.1.zip
|
||||
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
|
||||
|
@ -49,7 +50,7 @@ files:
|
|||
path: signal-desktop-beta-mac-1.23.2-beta.1.zip
|
||||
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
|
||||
releaseDate: '2019-03-29T01:53:23.881Z'
|
||||
`;
|
||||
`);
|
||||
|
||||
describe('#getVersion', () => {
|
||||
it('successfully gets version', () => {
|
||||
|
|
|
@ -3,9 +3,11 @@
|
|||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export enum Dialogs {
|
||||
None,
|
||||
Update,
|
||||
Cannot_Update,
|
||||
MacOS_Read_Only,
|
||||
export enum DialogType {
|
||||
None = 'None',
|
||||
Update = 'Update',
|
||||
Cannot_Update = 'Cannot_Update',
|
||||
MacOS_Read_Only = 'MacOS_Read_Only',
|
||||
DownloadReady = 'DownloadReady',
|
||||
Downloading = 'Downloading',
|
||||
}
|
||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -33,6 +33,7 @@ export type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
|||
export type StorageAccessType = {
|
||||
'always-relay-calls': boolean;
|
||||
'audio-notification': boolean;
|
||||
'auto-download-update': boolean;
|
||||
'badge-count-muted-conversations': boolean;
|
||||
'blocked-groups': Array<string>;
|
||||
'blocked-uuids': Array<string>;
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from 'fs';
|
||||
import { join, normalize } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
import { createParser, ParserConfiguration } from 'dashdash';
|
||||
import ProxyAgent from 'proxy-agent';
|
||||
|
@ -20,10 +21,10 @@ import { v4 as getGuid } from 'uuid';
|
|||
import pify from 'pify';
|
||||
import mkdirp from 'mkdirp';
|
||||
import rimraf from 'rimraf';
|
||||
import { app, BrowserWindow, dialog, ipcMain } from 'electron';
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
|
||||
import { getTempPath } from '../../app/attachments';
|
||||
import { Dialogs } from '../types/Dialogs';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { getUserAgent } from '../util/getUserAgent';
|
||||
import { isAlpha, isBeta } from '../util/version';
|
||||
|
||||
|
@ -31,7 +32,6 @@ import * as packageJson from '../../package.json';
|
|||
import { getSignatureFileName } from './signature';
|
||||
import { isPathInside } from '../util/isPathInside';
|
||||
|
||||
import { LocaleType } from '../types/I18N';
|
||||
import { LoggerType } from '../types/Logging';
|
||||
|
||||
const writeFile = pify(writeFileCallback);
|
||||
|
@ -39,24 +39,40 @@ const mkdirpPromise = pify(mkdirp);
|
|||
const rimrafPromise = pify(rimraf);
|
||||
const { platform } = process;
|
||||
|
||||
export const ACK_RENDER_TIMEOUT = 10000;
|
||||
export const GOT_CONNECT_TIMEOUT = 2 * 60 * 1000;
|
||||
export const GOT_LOOKUP_TIMEOUT = 2 * 60 * 1000;
|
||||
export const GOT_SOCKET_TIMEOUT = 2 * 60 * 1000;
|
||||
|
||||
type JSONUpdateSchema = {
|
||||
version: string;
|
||||
files: Array<{
|
||||
url: string;
|
||||
sha512: string;
|
||||
size: string;
|
||||
blockMapSize?: string;
|
||||
}>;
|
||||
path: string;
|
||||
sha512: string;
|
||||
releaseDate: string;
|
||||
};
|
||||
|
||||
export type UpdaterInterface = {
|
||||
force(): Promise<void>;
|
||||
};
|
||||
|
||||
export type UpdateInformationType = {
|
||||
fileName: string;
|
||||
size: number;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export async function checkForUpdates(
|
||||
logger: LoggerType,
|
||||
forceUpdate = false
|
||||
): Promise<{
|
||||
fileName: string;
|
||||
version: string;
|
||||
} | null> {
|
||||
): Promise<UpdateInformationType | null> {
|
||||
const yaml = await getUpdateYaml();
|
||||
const version = getVersion(yaml);
|
||||
const parsedYaml = parseYaml(yaml);
|
||||
const version = getVersion(parsedYaml);
|
||||
|
||||
if (!version) {
|
||||
logger.warn('checkForUpdates: no version extracted from downloaded yaml');
|
||||
|
@ -70,8 +86,11 @@ export async function checkForUpdates(
|
|||
`forceUpdate=${forceUpdate}`
|
||||
);
|
||||
|
||||
const fileName = getUpdateFileName(parsedYaml);
|
||||
|
||||
return {
|
||||
fileName: getUpdateFileName(yaml),
|
||||
fileName,
|
||||
size: getSize(parsedYaml, fileName),
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
@ -95,7 +114,8 @@ export function validatePath(basePath: string, targetPath: string): void {
|
|||
|
||||
export async function downloadUpdate(
|
||||
fileName: string,
|
||||
logger: LoggerType
|
||||
logger: LoggerType,
|
||||
mainWindow?: BrowserWindow
|
||||
): Promise<string> {
|
||||
const baseUrl = getUpdatesBase();
|
||||
const updateFileUrl = `${baseUrl}/${fileName}`;
|
||||
|
@ -121,6 +141,23 @@ export async function downloadUpdate(
|
|||
const writeStream = createWriteStream(targetUpdatePath);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
if (mainWindow) {
|
||||
let downloadedSize = 0;
|
||||
|
||||
const throttledSend = throttle(() => {
|
||||
mainWindow.webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.Downloading,
|
||||
{ downloadedSize }
|
||||
);
|
||||
}, 500);
|
||||
|
||||
downloadStream.on('data', data => {
|
||||
downloadedSize += data.length;
|
||||
throttledSend();
|
||||
});
|
||||
}
|
||||
|
||||
downloadStream.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
|
@ -144,106 +181,6 @@ export async function downloadUpdate(
|
|||
}
|
||||
}
|
||||
|
||||
let showingUpdateDialog = false;
|
||||
|
||||
async function showFallbackUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
): Promise<boolean> {
|
||||
if (showingUpdateDialog) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const RESTART_BUTTON = 0;
|
||||
const LATER_BUTTON = 1;
|
||||
const options = {
|
||||
type: 'info',
|
||||
buttons: [
|
||||
locale.messages.autoUpdateRestartButtonLabel.message,
|
||||
locale.messages.autoUpdateLaterButtonLabel.message,
|
||||
],
|
||||
title: locale.messages.autoUpdateNewVersionTitle.message,
|
||||
message: locale.messages.autoUpdateNewVersionMessage.message,
|
||||
detail: locale.messages.autoUpdateNewVersionInstructions.message,
|
||||
defaultId: LATER_BUTTON,
|
||||
cancelId: LATER_BUTTON,
|
||||
};
|
||||
|
||||
showingUpdateDialog = true;
|
||||
|
||||
const { response } = await dialog.showMessageBox(mainWindow, options);
|
||||
|
||||
showingUpdateDialog = false;
|
||||
|
||||
return response === RESTART_BUTTON;
|
||||
}
|
||||
|
||||
export function showUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType,
|
||||
performUpdateCallback: () => void
|
||||
): void {
|
||||
let ack = false;
|
||||
|
||||
ipcMain.once('show-update-dialog-ack', () => {
|
||||
ack = true;
|
||||
});
|
||||
|
||||
mainWindow.webContents.send('show-update-dialog', Dialogs.Update);
|
||||
|
||||
setTimeout(async () => {
|
||||
if (!ack) {
|
||||
const shouldUpdate = await showFallbackUpdateDialog(mainWindow, locale);
|
||||
if (shouldUpdate) {
|
||||
performUpdateCallback();
|
||||
}
|
||||
}
|
||||
}, ACK_RENDER_TIMEOUT);
|
||||
}
|
||||
|
||||
let showingCannotUpdateDialog = false;
|
||||
|
||||
async function showFallbackCannotUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
): Promise<void> {
|
||||
if (showingCannotUpdateDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
type: 'error',
|
||||
buttons: [locale.messages.ok.message],
|
||||
title: locale.messages.cannotUpdate.message,
|
||||
message: locale.i18n('cannotUpdateDetail', ['https://signal.org/download']),
|
||||
};
|
||||
|
||||
showingCannotUpdateDialog = true;
|
||||
|
||||
await dialog.showMessageBox(mainWindow, options);
|
||||
|
||||
showingCannotUpdateDialog = false;
|
||||
}
|
||||
|
||||
export function showCannotUpdateDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
): 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, locale);
|
||||
}
|
||||
}, ACK_RENDER_TIMEOUT);
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
export function getUpdateCheckUrl(): string {
|
||||
|
@ -288,9 +225,7 @@ function isVersionNewer(newVersion: string): boolean {
|
|||
return gt(newVersion, version);
|
||||
}
|
||||
|
||||
export function getVersion(yaml: string): string | null {
|
||||
const info = parseYaml(yaml);
|
||||
|
||||
export function getVersion(info: JSONUpdateSchema): string | null {
|
||||
return info && info.version;
|
||||
}
|
||||
|
||||
|
@ -299,11 +234,7 @@ export function isUpdateFileNameValid(name: string): boolean {
|
|||
return validFile.test(name);
|
||||
}
|
||||
|
||||
// Reliant on third party parser that returns any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function getUpdateFileName(yaml: string): any {
|
||||
const info = parseYaml(yaml);
|
||||
|
||||
export function getUpdateFileName(info: JSONUpdateSchema): string {
|
||||
if (!info || !info.path) {
|
||||
throw new Error('getUpdateFileName: No path present in YAML file');
|
||||
}
|
||||
|
@ -318,9 +249,17 @@ export function getUpdateFileName(yaml: string): any {
|
|||
return path;
|
||||
}
|
||||
|
||||
// Reliant on third party parser that returns any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
function parseYaml(yaml: string): any {
|
||||
function getSize(info: JSONUpdateSchema, fileName: string): number {
|
||||
if (!info || !info.files) {
|
||||
throw new Error('getUpdateFileName: No files present in YAML file');
|
||||
}
|
||||
|
||||
const foundFile = info.files.find(file => file.url === fileName);
|
||||
|
||||
return Number(foundFile?.size) || 0;
|
||||
}
|
||||
|
||||
export function parseYaml(yaml: string): JSONUpdateSchema {
|
||||
return safeLoad(yaml, { schema: FAILSAFE_SCHEMA, json: true });
|
||||
}
|
||||
|
||||
|
@ -413,3 +352,21 @@ export function getCliOptions<T>(options: ParserConfiguration['options']): T {
|
|||
export function setUpdateListener(performUpdateCallback: () => void): void {
|
||||
ipcMain.once('start-update', performUpdateCallback);
|
||||
}
|
||||
|
||||
export function getAutoDownloadUpdateSetting(
|
||||
mainWindow: BrowserWindow
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcMain.once(
|
||||
'settings:get-success:autoDownloadUpdate',
|
||||
(_, error, value: boolean) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(value);
|
||||
}
|
||||
}
|
||||
);
|
||||
mainWindow.webContents.send('settings:get:autoDownloadUpdate');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { BrowserWindow } from 'electron';
|
|||
import { UpdaterInterface } from './common';
|
||||
import { start as startMacOS } from './macos';
|
||||
import { start as startWindows } from './windows';
|
||||
import { LocaleType } from '../types/I18N';
|
||||
import { LoggerType } from '../types/Logging';
|
||||
|
||||
let initialized = false;
|
||||
|
@ -16,7 +15,6 @@ let updater: UpdaterInterface | undefined;
|
|||
|
||||
export async function start(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale?: LocaleType,
|
||||
logger?: LoggerType
|
||||
): Promise<void> {
|
||||
const { platform } = process;
|
||||
|
@ -26,9 +24,6 @@ export async function start(
|
|||
}
|
||||
initialized = true;
|
||||
|
||||
if (!locale) {
|
||||
throw new Error('updater/start: Must provide locale!');
|
||||
}
|
||||
if (!logger) {
|
||||
throw new Error('updater/start: Must provide logger!');
|
||||
}
|
||||
|
@ -42,9 +37,9 @@ export async function start(
|
|||
}
|
||||
|
||||
if (platform === 'win32') {
|
||||
updater = await startWindows(getMainWindow, locale, logger);
|
||||
updater = await startWindows(getMainWindow, logger);
|
||||
} else if (platform === 'darwin') {
|
||||
updater = await startMacOS(getMainWindow, locale, logger);
|
||||
updater = await startMacOS(getMainWindow, logger);
|
||||
} else {
|
||||
throw new Error('updater/start: Unsupported platform');
|
||||
}
|
||||
|
|
|
@ -7,27 +7,25 @@ import { AddressInfo } from 'net';
|
|||
import { dirname } from 'path';
|
||||
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
import { app, autoUpdater, BrowserWindow, dialog, ipcMain } from 'electron';
|
||||
import { app, autoUpdater, BrowserWindow } from 'electron';
|
||||
import { get as getFromConfig } from 'config';
|
||||
import { gt } from 'semver';
|
||||
import got from 'got';
|
||||
|
||||
import {
|
||||
ACK_RENDER_TIMEOUT,
|
||||
checkForUpdates,
|
||||
deleteTempDir,
|
||||
downloadUpdate,
|
||||
getAutoDownloadUpdateSetting,
|
||||
getPrintableError,
|
||||
setUpdateListener,
|
||||
showCannotUpdateDialog,
|
||||
showUpdateDialog,
|
||||
UpdaterInterface,
|
||||
UpdateInformationType,
|
||||
} from './common';
|
||||
import { LocaleType } from '../types/I18N';
|
||||
import { LoggerType } from '../types/Logging';
|
||||
import { hexToBinary, verifySignature } from './signature';
|
||||
import { markShouldQuit } from '../../app/window_state';
|
||||
import { Dialogs } from '../types/Dialogs';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
|
||||
const SECOND = 1000;
|
||||
const MINUTE = SECOND * 60;
|
||||
|
@ -35,7 +33,6 @@ const INTERVAL = MINUTE * 30;
|
|||
|
||||
export async function start(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
logger: LoggerType
|
||||
): Promise<UpdaterInterface> {
|
||||
logger.info('macos/start: starting checks...');
|
||||
|
@ -45,19 +42,17 @@ export async function start(
|
|||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await checkDownloadAndInstall(getMainWindow, locale, logger);
|
||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
||||
} catch (error) {
|
||||
logger.error('macos/start: error:', getPrintableError(error));
|
||||
logger.error(`macos/start: ${getPrintableError(error)}`);
|
||||
}
|
||||
}, INTERVAL);
|
||||
|
||||
setUpdateListener(createUpdater(logger));
|
||||
|
||||
await checkDownloadAndInstall(getMainWindow, locale, logger);
|
||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
||||
|
||||
return {
|
||||
async force(): Promise<void> {
|
||||
return checkDownloadAndInstall(getMainWindow, locale, logger, true);
|
||||
return checkForUpdatesMaybeInstall(getMainWindow, logger, true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -67,21 +62,48 @@ let version: string;
|
|||
let updateFilePath: string;
|
||||
let loggerForQuitHandler: LoggerType;
|
||||
|
||||
async function checkDownloadAndInstall(
|
||||
async function checkForUpdatesMaybeInstall(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
logger: LoggerType,
|
||||
force = false
|
||||
) {
|
||||
logger.info('checkDownloadAndInstall: checking for update...');
|
||||
try {
|
||||
logger.info('checkForUpdatesMaybeInstall: checking for update...');
|
||||
const result = await checkForUpdates(logger, force);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fileName: newFileName, version: newVersion } = result;
|
||||
|
||||
setUpdateListener(createUpdater(getMainWindow, result, logger));
|
||||
|
||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
||||
const autoDownloadUpdates = await getAutoDownloadUpdateSetting(
|
||||
getMainWindow()
|
||||
);
|
||||
if (!autoDownloadUpdates) {
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.DownloadReady,
|
||||
{
|
||||
downloadSize: result.size,
|
||||
version: result.version,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
await downloadAndInstall(newFileName, newVersion, getMainWindow, logger);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAndInstall(
|
||||
newFileName: string,
|
||||
newVersion: string,
|
||||
getMainWindow: () => BrowserWindow,
|
||||
logger: LoggerType,
|
||||
updateOnProgress?: boolean
|
||||
) {
|
||||
try {
|
||||
const oldFileName = fileName;
|
||||
const oldVersion = version;
|
||||
|
||||
|
@ -89,17 +111,20 @@ async function checkDownloadAndInstall(
|
|||
fileName = newFileName;
|
||||
version = newVersion;
|
||||
try {
|
||||
updateFilePath = await downloadUpdate(fileName, logger);
|
||||
updateFilePath = await downloadUpdate(
|
||||
fileName,
|
||||
logger,
|
||||
updateOnProgress ? getMainWindow() : undefined
|
||||
);
|
||||
} catch (error) {
|
||||
// Restore state in case of download error
|
||||
fileName = oldFileName;
|
||||
version = oldVersion;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateFilePath) {
|
||||
logger.info('checkDownloadAndInstall: no update file path. Skipping!');
|
||||
logger.info('downloadAndInstall: no update file path. Skipping!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -109,7 +134,7 @@ async function checkDownloadAndInstall(
|
|||
// Note: We don't delete the cache here, because we don't want to continually
|
||||
// re-download the broken release. We will download it only once per launch.
|
||||
throw new Error(
|
||||
`checkDownloadAndInstall: Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
|
||||
`downloadAndInstall: Downloaded update did not pass signature verification (version: '${version}'; fileName: '${fileName}')`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -119,13 +144,19 @@ async function checkDownloadAndInstall(
|
|||
const readOnly = 'Cannot update while running on a read-only volume';
|
||||
const message: string = error.message || '';
|
||||
if (message.includes(readOnly)) {
|
||||
logger.info('checkDownloadAndInstall: showing read-only dialog...');
|
||||
showReadOnlyDialog(getMainWindow(), locale);
|
||||
logger.info('downloadAndInstall: showing read-only dialog...');
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.MacOS_Read_Only
|
||||
);
|
||||
} else {
|
||||
logger.info(
|
||||
'checkDownloadAndInstall: showing general update failure dialog...'
|
||||
'downloadAndInstall: showing general update failure dialog...'
|
||||
);
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.Cannot_Update
|
||||
);
|
||||
showCannotUpdateDialog(getMainWindow(), locale);
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
@ -133,12 +164,13 @@ async function checkDownloadAndInstall(
|
|||
|
||||
// At this point, closing the app will cause the update to be installed automatically
|
||||
// because Squirrel has cached the update file and will do the right thing.
|
||||
logger.info('downloadAndInstall: showing update dialog...');
|
||||
|
||||
logger.info('checkDownloadAndInstall: showing update dialog...');
|
||||
|
||||
showUpdateDialog(getMainWindow(), locale, createUpdater(logger));
|
||||
getMainWindow().webContents.send('show-update-dialog', DialogType.Update, {
|
||||
version,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
|
||||
logger.error(`downloadAndInstall: ${getPrintableError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,10 +184,7 @@ function deleteCache(filePath: string | null, logger: LoggerType) {
|
|||
if (filePath) {
|
||||
const tempDir = dirname(filePath);
|
||||
deleteTempDir(tempDir).catch(error => {
|
||||
logger.error(
|
||||
'quitHandler: error deleting temporary directory:',
|
||||
getPrintableError(error)
|
||||
);
|
||||
logger.error(`quitHandler: ${getPrintableError(error)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -171,10 +200,7 @@ async function handToAutoUpdate(
|
|||
let serverUrl: string;
|
||||
|
||||
server.on('error', (error: Error) => {
|
||||
logger.error(
|
||||
'handToAutoUpdate: server had error',
|
||||
getPrintableError(error)
|
||||
);
|
||||
logger.error(`handToAutoUpdate: ${getPrintableError(error)}`);
|
||||
shutdown(server, logger);
|
||||
reject(error);
|
||||
});
|
||||
|
@ -254,8 +280,9 @@ function pipeUpdateToSquirrel(
|
|||
|
||||
response.on('error', (error: Error) => {
|
||||
logger.error(
|
||||
'pipeUpdateToSquirrel: update file download request had an error',
|
||||
getPrintableError(error)
|
||||
`pipeUpdateToSquirrel: update file download request had an error ${getPrintableError(
|
||||
error
|
||||
)}`
|
||||
);
|
||||
shutdown(server, logger);
|
||||
reject(error);
|
||||
|
@ -263,8 +290,9 @@ function pipeUpdateToSquirrel(
|
|||
|
||||
readStream.on('error', (error: Error) => {
|
||||
logger.error(
|
||||
'pipeUpdateToSquirrel: read stream error response:',
|
||||
getPrintableError(error)
|
||||
`pipeUpdateToSquirrel: read stream error response: ${getPrintableError(
|
||||
error
|
||||
)}`
|
||||
);
|
||||
shutdown(server, logger, response);
|
||||
reject(error);
|
||||
|
@ -339,7 +367,7 @@ function shutdown(
|
|||
server.close();
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('shutdown: Error closing server', getPrintableError(error));
|
||||
logger.error(`shutdown: Error closing server ${getPrintableError(error)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -348,62 +376,32 @@ function shutdown(
|
|||
}
|
||||
} catch (endError) {
|
||||
logger.error(
|
||||
"shutdown: couldn't end response",
|
||||
getPrintableError(endError)
|
||||
`shutdown: couldn't end response ${getPrintableError(endError)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function showReadOnlyDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
): 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, locale);
|
||||
}
|
||||
}, ACK_RENDER_TIMEOUT);
|
||||
}
|
||||
|
||||
let showingReadOnlyDialog = false;
|
||||
|
||||
async function showFallbackReadOnlyDialog(
|
||||
mainWindow: BrowserWindow,
|
||||
locale: LocaleType
|
||||
function createUpdater(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
info: Pick<UpdateInformationType, 'fileName' | 'version'>,
|
||||
logger: LoggerType
|
||||
) {
|
||||
if (showingReadOnlyDialog) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
type: 'warning',
|
||||
buttons: [locale.messages.ok.message],
|
||||
title: locale.messages.cannotUpdate.message,
|
||||
message: locale.i18n('readOnlyVolume', {
|
||||
app: 'Signal.app',
|
||||
folder: '/Applications',
|
||||
}),
|
||||
};
|
||||
|
||||
showingReadOnlyDialog = true;
|
||||
|
||||
await dialog.showMessageBox(mainWindow, options);
|
||||
|
||||
showingReadOnlyDialog = false;
|
||||
}
|
||||
|
||||
function createUpdater(logger: LoggerType) {
|
||||
return () => {
|
||||
return async () => {
|
||||
if (updateFilePath) {
|
||||
logger.info('performUpdate: calling quitAndInstall...');
|
||||
markShouldQuit();
|
||||
autoUpdater.quitAndInstall();
|
||||
} else {
|
||||
logger.info(
|
||||
'performUpdate: have not downloaded update, going to download'
|
||||
);
|
||||
await downloadAndInstall(
|
||||
info.fileName,
|
||||
info.version,
|
||||
getMainWindow,
|
||||
logger,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,16 +14,16 @@ import {
|
|||
checkForUpdates,
|
||||
deleteTempDir,
|
||||
downloadUpdate,
|
||||
getAutoDownloadUpdateSetting,
|
||||
getPrintableError,
|
||||
setUpdateListener,
|
||||
showCannotUpdateDialog,
|
||||
showUpdateDialog,
|
||||
UpdaterInterface,
|
||||
UpdateInformationType,
|
||||
} from './common';
|
||||
import { LocaleType } from '../types/I18N';
|
||||
import { LoggerType } from '../types/Logging';
|
||||
import { hexToBinary, verifySignature } from './signature';
|
||||
import { markShouldQuit } from '../../app/window_state';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
|
||||
const readdir = pify(readdirCallback);
|
||||
const unlink = pify(unlinkCallback);
|
||||
|
@ -40,7 +40,6 @@ let loggerForQuitHandler: LoggerType;
|
|||
|
||||
export async function start(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
logger: LoggerType
|
||||
): Promise<UpdaterInterface> {
|
||||
logger.info('windows/start: starting checks...');
|
||||
|
@ -48,41 +47,66 @@ export async function start(
|
|||
loggerForQuitHandler = logger;
|
||||
app.once('quit', quitHandler);
|
||||
|
||||
setUpdateListener(createUpdater(getMainWindow, locale, logger));
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
await checkDownloadAndInstall(getMainWindow, locale, logger);
|
||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
||||
} catch (error) {
|
||||
logger.error('windows/start: error:', getPrintableError(error));
|
||||
logger.error(`windows/start: ${getPrintableError(error)}`);
|
||||
}
|
||||
}, INTERVAL);
|
||||
|
||||
await deletePreviousInstallers(logger);
|
||||
await checkDownloadAndInstall(getMainWindow, locale, logger);
|
||||
await checkForUpdatesMaybeInstall(getMainWindow, logger);
|
||||
|
||||
return {
|
||||
async force(): Promise<void> {
|
||||
return checkDownloadAndInstall(getMainWindow, locale, logger, true);
|
||||
return checkForUpdatesMaybeInstall(getMainWindow, logger, true);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function checkDownloadAndInstall(
|
||||
async function checkForUpdatesMaybeInstall(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
logger: LoggerType,
|
||||
force = false
|
||||
) {
|
||||
try {
|
||||
logger.info('checkDownloadAndInstall: checking for update...');
|
||||
logger.info('checkForUpdatesMaybeInstall: checking for update...');
|
||||
const result = await checkForUpdates(logger, force);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { fileName: newFileName, version: newVersion } = result;
|
||||
|
||||
setUpdateListener(createUpdater(getMainWindow, result, logger));
|
||||
|
||||
if (fileName !== newFileName || !version || gt(newVersion, version)) {
|
||||
const autoDownloadUpdates = await getAutoDownloadUpdateSetting(
|
||||
getMainWindow()
|
||||
);
|
||||
if (!autoDownloadUpdates) {
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.DownloadReady,
|
||||
{
|
||||
downloadSize: result.size,
|
||||
version: result.version,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
await downloadAndInstall(newFileName, newVersion, getMainWindow, logger);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadAndInstall(
|
||||
newFileName: string,
|
||||
newVersion: string,
|
||||
getMainWindow: () => BrowserWindow,
|
||||
logger: LoggerType,
|
||||
updateOnProgress?: boolean
|
||||
) {
|
||||
try {
|
||||
const oldFileName = fileName;
|
||||
const oldVersion = version;
|
||||
|
||||
|
@ -91,14 +115,17 @@ async function checkDownloadAndInstall(
|
|||
version = newVersion;
|
||||
|
||||
try {
|
||||
updateFilePath = await downloadUpdate(fileName, logger);
|
||||
updateFilePath = await downloadUpdate(
|
||||
fileName,
|
||||
logger,
|
||||
updateOnProgress ? getMainWindow() : undefined
|
||||
);
|
||||
} catch (error) {
|
||||
// Restore state in case of download error
|
||||
fileName = oldFileName;
|
||||
version = oldVersion;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
|
||||
const verified = await verifySignature(updateFilePath, version, publicKey);
|
||||
|
@ -110,14 +137,12 @@ async function checkDownloadAndInstall(
|
|||
);
|
||||
}
|
||||
|
||||
logger.info('checkDownloadAndInstall: showing dialog...');
|
||||
showUpdateDialog(
|
||||
getMainWindow(),
|
||||
locale,
|
||||
createUpdater(getMainWindow, locale, logger)
|
||||
);
|
||||
logger.info('downloadAndInstall: showing dialog...');
|
||||
getMainWindow().webContents.send('show-update-dialog', DialogType.Update, {
|
||||
version,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('checkDownloadAndInstall: error', getPrintableError(error));
|
||||
logger.error(`downloadAndInstall: ${getPrintableError(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,10 +150,7 @@ function quitHandler() {
|
|||
if (updateFilePath && !installing) {
|
||||
verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch(
|
||||
error => {
|
||||
loggerForQuitHandler.error(
|
||||
'quitHandler: error installing:',
|
||||
getPrintableError(error)
|
||||
);
|
||||
loggerForQuitHandler.error(`quitHandler: ${getPrintableError(error)}`);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -208,10 +230,7 @@ function deleteCache(filePath: string | null, logger: LoggerType) {
|
|||
if (filePath) {
|
||||
const tempDir = dirname(filePath);
|
||||
deleteTempDir(tempDir).catch(error => {
|
||||
logger.error(
|
||||
'deleteCache: error deleting temporary directory',
|
||||
getPrintableError(error)
|
||||
);
|
||||
logger.error(`deleteCache: ${getPrintableError(error)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -237,23 +256,37 @@ async function spawn(
|
|||
|
||||
function createUpdater(
|
||||
getMainWindow: () => BrowserWindow,
|
||||
locale: LocaleType,
|
||||
info: Pick<UpdateInformationType, 'fileName' | 'version'>,
|
||||
logger: LoggerType
|
||||
) {
|
||||
return async () => {
|
||||
if (updateFilePath) {
|
||||
try {
|
||||
await verifyAndInstall(updateFilePath, version, logger);
|
||||
installing = true;
|
||||
} catch (error) {
|
||||
logger.info(
|
||||
'checkDownloadAndInstall: showing general update failure dialog...'
|
||||
logger.info('createUpdater: showing general update failure dialog...');
|
||||
getMainWindow().webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.Cannot_Update
|
||||
);
|
||||
showCannotUpdateDialog(getMainWindow(), locale);
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
markShouldQuit();
|
||||
app.quit();
|
||||
} else {
|
||||
logger.info(
|
||||
'performUpdate: have not downloaded update, going to download'
|
||||
);
|
||||
await downloadAndInstall(
|
||||
info.fileName,
|
||||
info.version,
|
||||
getMainWindow,
|
||||
logger,
|
||||
true
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
|
|||
export type IPCEventsValuesType = {
|
||||
alwaysRelayCalls: boolean | undefined;
|
||||
audioNotification: boolean | undefined;
|
||||
autoDownloadUpdate: boolean;
|
||||
autoLaunch: boolean;
|
||||
callRingtoneNotification: boolean;
|
||||
callSystemNotification: boolean;
|
||||
|
@ -252,6 +253,10 @@ export function createIPCEvents(
|
|||
window.storage.get('typingIndicators', false),
|
||||
|
||||
// Configurable settings
|
||||
getAutoDownloadUpdate: () =>
|
||||
window.storage.get('auto-download-update', true),
|
||||
setAutoDownloadUpdate: value =>
|
||||
window.storage.put('auto-download-update', value),
|
||||
getThemeSetting: () =>
|
||||
window.storage.get(
|
||||
'theme-setting',
|
||||
|
|
|
@ -431,6 +431,13 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-06-15T23:46:51.629Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-$(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.whats-new-placeholder').append(this.whatsNewView.el);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-08-17T01:37:13.116Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
|
@ -439,6 +446,13 @@
|
|||
"updated": "2021-02-26T18:44:56.450Z",
|
||||
"reasonDetail": "Adding sub-view to DOM"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
"line": " this.$('.whats-new-placeholder').append(this.whatsNewView.el);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-08-17T01:37:13.116Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-appendTo(",
|
||||
"path": "js/views/inbox_view.js",
|
||||
|
|
11
ts/window.d.ts
vendored
11
ts/window.d.ts
vendored
|
@ -103,6 +103,7 @@ import { ProgressModal } from './components/ProgressModal';
|
|||
import { Quote } from './components/conversation/Quote';
|
||||
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
|
||||
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
|
||||
import { WhatsNew } from './components/WhatsNew';
|
||||
import { MIMEType } from './types/MIME';
|
||||
import { DownloadedAttachmentType } from './types/Attachment';
|
||||
import { ElectronLocaleType } from './util/mapToSupportLocale';
|
||||
|
@ -298,11 +299,8 @@ declare global {
|
|||
enableStorageService: () => boolean;
|
||||
eraseAllStorageServiceState: () => Promise<void>;
|
||||
initializeGroupCredentialFetcher: () => void;
|
||||
initializeNetworkObserver: (network: WhatIsThis) => void;
|
||||
initializeUpdateListener: (
|
||||
updates: WhatIsThis,
|
||||
events: WhatIsThis
|
||||
) => void;
|
||||
initializeNetworkObserver: (network: ReduxActions['network']) => void;
|
||||
initializeUpdateListener: (updates: ReduxActions['updates']) => void;
|
||||
onTimeout: (timestamp: number, cb: () => void, id?: string) => string;
|
||||
removeTimeout: (uuid: string) => void;
|
||||
retryPlaceholders?: Util.RetryPlaceholders;
|
||||
|
@ -420,6 +418,7 @@ declare global {
|
|||
ConfirmationDialog: typeof ConfirmationDialog;
|
||||
ContactDetail: typeof ContactDetail;
|
||||
ContactModal: typeof ContactModal;
|
||||
DisappearingTimeDialog: typeof DisappearingTimeDialog;
|
||||
ErrorModal: typeof ErrorModal;
|
||||
Lightbox: typeof Lightbox;
|
||||
LightboxGallery: typeof LightboxGallery;
|
||||
|
@ -428,7 +427,7 @@ declare global {
|
|||
ProgressModal: typeof ProgressModal;
|
||||
Quote: typeof Quote;
|
||||
StagedLinkPreview: typeof StagedLinkPreview;
|
||||
DisappearingTimeDialog: typeof DisappearingTimeDialog;
|
||||
WhatsNew: typeof WhatsNew;
|
||||
};
|
||||
OS: typeof OS;
|
||||
Workflow: {
|
||||
|
|
|
@ -39,6 +39,7 @@ installSetting('typingIndicatorSetting', {
|
|||
|
||||
installSetting('alwaysRelayCalls');
|
||||
installSetting('audioNotification');
|
||||
installSetting('autoDownloadUpdate');
|
||||
installSetting('autoLaunch');
|
||||
installSetting('countMutedConversations');
|
||||
installSetting('callRingtoneNotification');
|
||||
|
|
|
@ -45,6 +45,7 @@ window.getVersion = () => String(config.version);
|
|||
window.i18n = i18n.setup(locale, localeMessages);
|
||||
|
||||
const settingAudioNotification = createSetting('audioNotification');
|
||||
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
|
||||
const settingAutoLaunch = createSetting('autoLaunch');
|
||||
const settingCallRingtoneNotification = createSetting(
|
||||
'callRingtoneNotification'
|
||||
|
@ -166,6 +167,7 @@ async function renderPreferences() {
|
|||
blockedCount,
|
||||
deviceName,
|
||||
hasAudioNotifications,
|
||||
hasAutoDownloadUpdate,
|
||||
hasAutoLaunch,
|
||||
hasCallNotifications,
|
||||
hasCallRingtoneNotification,
|
||||
|
@ -201,6 +203,7 @@ async function renderPreferences() {
|
|||
blockedCount: settingBlockedCount.getValue(),
|
||||
deviceName: settingDeviceName.getValue(),
|
||||
hasAudioNotifications: settingAudioNotification.getValue(),
|
||||
hasAutoDownloadUpdate: settingAutoDownloadUpdate.getValue(),
|
||||
hasAutoLaunch: settingAutoLaunch.getValue(),
|
||||
hasCallNotifications: settingCallSystemNotification.getValue(),
|
||||
hasCallRingtoneNotification: settingCallRingtoneNotification.getValue(),
|
||||
|
@ -256,6 +259,7 @@ async function renderPreferences() {
|
|||
defaultConversationColor,
|
||||
deviceName,
|
||||
hasAudioNotifications,
|
||||
hasAutoDownloadUpdate,
|
||||
hasAutoLaunch,
|
||||
hasCallNotifications,
|
||||
hasCallRingtoneNotification,
|
||||
|
@ -310,6 +314,7 @@ async function renderPreferences() {
|
|||
|
||||
// Change handlers
|
||||
onAudioNotificationsChange: reRender(settingAudioNotification.setValue),
|
||||
onAutoDownloadUpdateChange: reRender(settingAutoDownloadUpdate.setValue),
|
||||
onAutoLaunchChange: reRender(settingAutoLaunch.setValue),
|
||||
onCallNotificationsChange: reRender(settingCallSystemNotification.setValue),
|
||||
onCallRingtoneNotificationChange: reRender(
|
||||
|
|
Loading…
Add table
Reference in a new issue