New option for control over update downloads

This commit is contained in:
Josh Perez 2021-08-19 18:56:29 -04:00 committed by GitHub
parent 80c1ad6ee3
commit e9308bbafb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 1230 additions and 803 deletions

View File

@ -183,6 +183,10 @@
"message": "Chat Color", "message": "Chat Color",
"description": "One of the menu options available in the Avatar Popup menu" "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": { "loading": {
"message": "Loading...", "message": "Loading...",
"description": "Message shown on the loading screen before we've loaded any messages" "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." "description": "Displayed when the desktop client cannot connect to the server."
}, },
"connecting": { "connecting": {
"message": "Connecting", "message": "Connecting...",
"description": "Displayed when the desktop client is currently connecting to the server." "description": "Displayed when the desktop client is currently connecting to the server."
}, },
"connect": { "connect": {
"message": "Connect", "message": "Click to reconnect.",
"description": "Shown to allow the user to manually attempt a reconnect." "description": "Shown to allow the user to manually attempt a reconnect."
}, },
"connectingHangOn": { "connectingHangOn": {
"message": "Shouldn't be long...", "message": "Shouldn't be long",
"description": "Subtext description for when the client is connecting to the server." "description": "Subtext description for when the client is connecting to the server."
}, },
"offline": { "offline": {
@ -796,6 +800,20 @@
"welcomeToSignal": { "welcomeToSignal": {
"message": "Welcome to Signal" "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": { "selectAContact": {
"message": "Select a contact or group to start chatting." "message": "Select a contact or group to start chatting."
}, },
@ -1791,7 +1809,7 @@
"description": "Warning notification that this version of the app has expired" "description": "Warning notification that this version of the app has expired"
}, },
"upgrade": { "upgrade": {
"message": "Upgrade", "message": "Click to go to signal.org/download",
"description": "Label text for button to upgrade the app to the latest version" "description": "Label text for button to upgrade the app to the latest version"
}, },
"mediaMessage": { "mediaMessage": {
@ -2210,10 +2228,13 @@
"message": "Relink" "message": "Relink"
}, },
"autoUpdateNewVersionTitle": { "autoUpdateNewVersionTitle": {
"message": "Signal update available" "message": "Update available"
}, },
"autoUpdateNewVersionMessage": { "autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available." "message": "Click to restart Signal"
},
"downloadNewVersionMessage": {
"message": "Click to download update"
}, },
"autoUpdateNewVersionInstructions": { "autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates." "message": "Press Restart Signal to apply the updates."
@ -6091,5 +6112,43 @@
"Preferences--typing-indicators": { "Preferences--typing-indicators": {
"message": "Typing indicators", "message": "Typing indicators",
"description": "Label for the typing indicators setting" "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"
} }
} }

View File

@ -84,6 +84,7 @@
<div class='content'> <div class='content'>
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div> <div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
<h3>{{ welcomeToSignal }}</h3> <h3>{{ welcomeToSignal }}</h3>
<p class="whats-new-placeholder"></p>
<p>{{ selectAContact }}</p> <p>{{ selectAContact }}</p>
</div> </div>
</div> </div>

View 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

View File

@ -59,6 +59,7 @@ const {
const { const {
SystemTraySettingsCheckboxes, SystemTraySettingsCheckboxes,
} = require('../../ts/components/conversation/SystemTraySettingsCheckboxes'); } = require('../../ts/components/conversation/SystemTraySettingsCheckboxes');
const { WhatsNew } = require('../../ts/components/WhatsNew');
// State // State
const { createTimeline } = require('../../ts/state/roots/createTimeline'); const { createTimeline } = require('../../ts/state/roots/createTimeline');
@ -359,6 +360,7 @@ exports.setup = (options = {}) => {
Types: { Types: {
Message: MediaGalleryMessage, Message: MediaGalleryMessage,
}, },
WhatsNew,
}; };
const Roots = { const Roots = {

View File

@ -93,6 +93,8 @@
model: { window: options.window }, model: { window: options.window },
}); });
this.renderWhatsNew();
Whisper.events.on('refreshConversation', ({ oldId, newId }) => { Whisper.events.on('refreshConversation', ({ oldId, newId }) => {
const convo = this.conversation_stack.lastConversation; const convo = this.conversation_stack.lastConversation;
if (convo && convo.get('id') === oldId) { if (convo && convo.get('id') === oldId) {
@ -153,6 +155,18 @@
events: { events: {
click: 'onClick', 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() { setupLeftPane() {
if (this.leftPaneView) { if (this.leftPaneView) {
return; return;

View File

@ -666,7 +666,7 @@ async function readyForUpdates() {
// Second, start checking for app updates // Second, start checking for app updates
try { try {
await updater.start(getMainWindow, locale, logger); await updater.start(getMainWindow, logger);
} catch (error) { } catch (error) {
logger.error( logger.error(
'Error starting update checks:', 'Error starting update checks:',

View File

@ -3683,6 +3683,21 @@ button.module-conversation-details__action-button {
&__avatar { &__avatar {
-webkit-app-region: no-drag; -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 { &__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
%module-emoji-picker--ribbon { %module-emoji-picker--ribbon {
@ -8740,6 +8682,15 @@ button.module-image__border-overlay:focus {
height: 16px; height: 16px;
width: 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 { .module-avatar-popup__item__icon-settings {
@include light-theme { @include light-theme {
@ -8771,9 +8722,18 @@ button.module-image__border-overlay:focus {
} }
.module-avatar-popup__item__text { .module-avatar-popup__item__text {
flex-grow: 1;
margin-left: 8px; 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
.module-shortcut-guide-modal { .module-shortcut-guide-modal {

View 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;
}
}
}

View File

@ -0,0 +1,7 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.WhatsNew {
@include button-reset;
color: $color-ultramarine;
}

View File

@ -62,6 +62,7 @@
@import './components/GroupInput.scss'; @import './components/GroupInput.scss';
@import './components/IncomingCallBar.scss'; @import './components/IncomingCallBar.scss';
@import './components/Input.scss'; @import './components/Input.scss';
@import './components/LeftPaneDialog.scss';
@import './components/MediaQualitySelector.scss'; @import './components/MediaQualitySelector.scss';
@import './components/MessageAudio.scss'; @import './components/MessageAudio.scss';
@import './components/MessageDetail.scss'; @import './components/MessageDetail.scss';
@ -78,3 +79,4 @@
@import './components/Tabs.scss'; @import './components/Tabs.scss';
@import './components/TimelineWarning.scss'; @import './components/TimelineWarning.scss';
@import './components/TimelineWarnings.scss'; @import './components/TimelineWarnings.scss';
@import './components/WhatsNew.scss';

View File

@ -53,6 +53,7 @@
<div class='content'> <div class='content'>
<div class="module-splash-screen__logo module-img--128 module-logo-blue"></div> <div class="module-splash-screen__logo module-img--128 module-logo-blue"></div>
<h3>{{ welcomeToSignal }}</h3> <h3>{{ welcomeToSignal }}</h3>
<p class="whats-new-placeholder"></p>
<p>{{ selectAContact }}</p> <p>{{ selectAContact }}</p>
</div> </div>
</div> </div>

View File

@ -888,8 +888,7 @@ export async function startApp(): Promise<void> {
window.reduxActions.network window.reduxActions.network
); );
window.Signal.Services.initializeUpdateListener( window.Signal.Services.initializeUpdateListener(
window.reduxActions.updates, window.reduxActions.updates
window.Whisper.events
); );
window.Signal.Services.calling.initialize( window.Signal.Services.calling.initialize(
window.reduxActions.calling, window.reduxActions.calling,

View File

@ -36,6 +36,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
conversationTypeMap, conversationTypeMap,
overrideProps.conversationType || 'direct' overrideProps.conversationType || 'direct'
), ),
hasPendingUpdate: Boolean(overrideProps.hasPendingUpdate),
i18n, i18n,
isMe: true, isMe: true,
name: text('name', overrideProps.name || ''), name: text('name', overrideProps.name || ''),
@ -47,6 +48,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
profileName: text('profileName', overrideProps.profileName || ''), profileName: text('profileName', overrideProps.profileName || ''),
sharedGroupNames: [], sharedGroupNames: [],
size: 80, size: 80,
startUpdate: action('startUpdate'),
style: {}, style: {},
title: text('title', overrideProps.title || ''), title: text('title', overrideProps.title || ''),
}); });
@ -83,3 +85,11 @@ stories.add('Phone Number', () => {
return <AvatarPopup {...props} />; return <AvatarPopup {...props} />;
}); });
stories.add('Update Available', () => {
const props = createProps({
hasPendingUpdate: true,
});
return <AvatarPopup {...props} />;
});

View File

@ -12,6 +12,9 @@ import { LocalizerType } from '../types/Util';
export type Props = { export type Props = {
readonly i18n: LocalizerType; readonly i18n: LocalizerType;
hasPendingUpdate: boolean;
startUpdate: () => unknown;
onEditProfile: () => unknown; onEditProfile: () => unknown;
onViewPreferences: () => unknown; onViewPreferences: () => unknown;
onViewArchive: () => unknown; onViewArchive: () => unknown;
@ -23,15 +26,17 @@ export type Props = {
export const AvatarPopup = (props: Props): JSX.Element => { export const AvatarPopup = (props: Props): JSX.Element => {
const { const {
hasPendingUpdate,
i18n, i18n,
name, name,
profileName,
phoneNumber,
title,
onEditProfile, onEditProfile,
onViewPreferences,
onViewArchive, onViewArchive,
onViewPreferences,
phoneNumber,
profileName,
startUpdate,
style, style,
title,
} = props; } = props;
const shouldShowNumber = Boolean(name || profileName); const shouldShowNumber = Boolean(name || profileName);
@ -92,6 +97,24 @@ export const AvatarPopup = (props: Props): JSX.Element => {
{i18n('avatarMenuViewArchive')} {i18n('avatarMenuViewArchive')}
</div> </div>
</button> </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> </div>
); );
}; };

View File

@ -5,17 +5,17 @@ import * as React from 'react';
import { storiesOf } from '@storybook/react'; import { storiesOf } from '@storybook/react';
import { boolean } from '@storybook/addon-knobs'; import { boolean } from '@storybook/addon-knobs';
import { ExpiredBuildDialog } from './ExpiredBuildDialog'; import { DialogExpiredBuild } from './DialogExpiredBuild';
import { setup as setupI18n } from '../../js/modules/i18n'; import { setup as setupI18n } from '../../js/modules/i18n';
import enMessages from '../../_locales/en/messages.json'; import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages); const i18n = setupI18n('en', enMessages);
storiesOf('Components/ExpiredBuildDialog', module).add( storiesOf('Components/DialogExpiredBuild', module).add(
'ExpiredBuildDialog', 'DialogExpiredBuild',
() => { () => {
const hasExpired = boolean('hasExpired', true); const hasExpired = boolean('hasExpired', true);
return <ExpiredBuildDialog hasExpired={hasExpired} i18n={i18n} />; return <DialogExpiredBuild hasExpired={hasExpired} i18n={i18n} />;
} }
); );

View File

@ -10,7 +10,7 @@ type PropsType = {
i18n: LocalizerType; i18n: LocalizerType;
}; };
export const ExpiredBuildDialog = ({ export const DialogExpiredBuild = ({
hasExpired, hasExpired,
i18n, i18n,
}: PropsType): JSX.Element | null => { }: PropsType): JSX.Element | null => {
@ -19,19 +19,17 @@ export const ExpiredBuildDialog = ({
} }
return ( return (
<div className="module-left-pane-dialog module-left-pane-dialog--error"> <div className="LeftPaneDialog LeftPaneDialog--error">
{i18n('expiredWarning')} <div className="LeftPaneDialog__message">
<div className="module-left-pane-dialog__actions"> {i18n('expiredWarning')}{' '}
<a <a
className="module-left-pane-dialog__link" className="LeftPaneDialog__action-text"
href="https://signal.org/download/" href="https://signal.org/download/"
rel="noreferrer" rel="noreferrer"
tabIndex={-1} tabIndex={-1}
target="_blank" target="_blank"
> >
<button type="button" className="upgrade"> {i18n('upgrade')}
{i18n('upgrade')}
</button>
</a> </a>
</div> </div>
</div> </div>

View 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} />
));

View File

@ -3,6 +3,7 @@
import React from 'react'; import React from 'react';
import { Spinner } from './Spinner';
import { LocalizerType } from '../types/Util'; import { LocalizerType } from '../types/Util';
import { SocketStatus } from '../types/SocketStatus'; import { SocketStatus } from '../types/SocketStatus';
import { NetworkStateType } from '../state/ducks/network'; import { NetworkStateType } from '../state/ducks/network';
@ -16,28 +17,42 @@ export type PropsType = NetworkStateType & {
}; };
type RenderDialogTypes = { type RenderDialogTypes = {
isConnecting?: boolean;
title: string; title: string;
subtext: string; subtext: string;
renderActionableButton?: () => JSX.Element; renderActionableButton?: () => JSX.Element;
}; };
function renderDialog({ function renderDialog({
isConnecting,
title, title,
subtext, subtext,
renderActionableButton, renderActionableButton,
}: RenderDialogTypes): JSX.Element { }: RenderDialogTypes): JSX.Element {
return ( return (
<div className="module-left-pane-dialog module-left-pane-dialog--warning"> <div className="LeftPaneDialog LeftPaneDialog--warning">
<div className="module-left-pane-dialog__message"> {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> <h3>{title}</h3>
<span>{subtext}</span> <span>{subtext}</span>
<div>{renderActionableButton && renderActionableButton()}</div>
</div> </div>
{renderActionableButton && renderActionableButton()}
</div> </div>
); );
} }
export const NetworkStatus = ({ export const DialogNetworkStatus = ({
hasNetworkDialog, hasNetworkDialog,
i18n, i18n,
isOnline, isOnline,
@ -75,19 +90,23 @@ export const NetworkStatus = ({
}; };
const manualReconnectButton = (): JSX.Element => ( const manualReconnectButton = (): JSX.Element => (
<div className="module-left-pane-dialog__actions"> <button
<button onClick={reconnect} type="button"> className="LeftPaneDialog__action-text"
{i18n('connect')} onClick={reconnect}
</button> type="button"
</div> >
{i18n('connect')}
</button>
); );
if (isConnecting) { if (isConnecting) {
return renderDialog({ return renderDialog({
isConnecting: true,
subtext: i18n('connectingHangOn'), subtext: i18n('connectingHangOn'),
title: i18n('connecting'), title: i18n('connecting'),
}); });
} }
if (!isOnline) { if (!isOnline) {
return renderDialog({ return renderDialog({
renderActionableButton: manualReconnectButton, renderActionableButton: manualReconnectButton,
@ -114,6 +133,7 @@ export const NetworkStatus = ({
} }
return renderDialog({ return renderDialog({
isConnecting: socketStatus === SocketStatus.CONNECTING,
renderActionableButton, renderActionableButton,
subtext, subtext,
title, title,

View 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} />
));

View 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>
);
};

View File

@ -45,6 +45,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
title: requiredText('title', overrideProps.title), title: requiredText('title', overrideProps.title),
name: optionalText('name', overrideProps.name), name: optionalText('name', overrideProps.name),
avatarPath: optionalText('avatarPath', overrideProps.avatarPath), avatarPath: optionalText('avatarPath', overrideProps.avatarPath),
hasPendingUpdate: Boolean(overrideProps.hasPendingUpdate),
i18n, i18n,
@ -55,6 +56,7 @@ const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
searchInConversation: action('searchInConversation'), searchInConversation: action('searchInConversation'),
clearConversationSearch: action('clearConversationSearch'), clearConversationSearch: action('clearConversationSearch'),
clearSearch: action('clearSearch'), clearSearch: action('clearSearch'),
startUpdate: action('startUpdate'),
showArchivedConversations: action('showArchivedConversations'), showArchivedConversations: action('showArchivedConversations'),
startComposing: action('startComposing'), startComposing: action('startComposing'),
@ -115,3 +117,9 @@ story.add('Searching Conversation with Term', () => {
return <MainHeader {...props} />; return <MainHeader {...props} />;
}); });
story.add('Update Available', () => {
const props = createProps({ hasPendingUpdate: true });
return <MainHeader {...props} />;
});

View File

@ -37,6 +37,7 @@ export type PropsType = {
profileName?: string; profileName?: string;
title: string; title: string;
avatarPath?: string; avatarPath?: string;
hasPendingUpdate: boolean;
i18n: LocalizerType; i18n: LocalizerType;
@ -59,6 +60,7 @@ export type PropsType = {
noteToSelf: string; noteToSelf: string;
} }
) => void; ) => void;
startUpdate: () => unknown;
clearConversationSearch: () => void; clearConversationSearch: () => void;
clearSearch: () => void; clearSearch: () => void;
@ -342,16 +344,18 @@ export class MainHeader extends React.Component<PropsType, StateType> {
avatarPath, avatarPath,
color, color,
disabled, disabled,
hasPendingUpdate,
i18n, i18n,
name, name,
startComposing,
phoneNumber, phoneNumber,
profileName, profileName,
title,
searchConversationId, searchConversationId,
searchConversationName, searchConversationName,
searchTerm, searchTerm,
showArchivedConversations, showArchivedConversations,
startComposing,
startUpdate,
title,
toggleProfileEditor, toggleProfileEditor,
} = this.props; } = this.props;
const { showingAvatarPopup, popperRoot } = this.state; const { showingAvatarPopup, popperRoot } = this.state;
@ -369,25 +373,30 @@ export class MainHeader extends React.Component<PropsType, StateType> {
<Manager> <Manager>
<Reference> <Reference>
{({ ref }) => ( {({ ref }) => (
<Avatar <div className="module-main-header__avatar--container">
acceptedMessageRequest <Avatar
avatarPath={avatarPath} acceptedMessageRequest
className="module-main-header__avatar" avatarPath={avatarPath}
color={color} className="module-main-header__avatar"
conversationType="direct" color={color}
i18n={i18n} conversationType="direct"
isMe i18n={i18n}
name={name} isMe
phoneNumber={phoneNumber} name={name}
profileName={profileName} phoneNumber={phoneNumber}
title={title} profileName={profileName}
// `sharedGroupNames` makes no sense for yourself, but `<Avatar>` needs it title={title}
// to determine blurring. // `sharedGroupNames` makes no sense for yourself, but
sharedGroupNames={[]} // `<Avatar>` needs it to determine blurring.
size={28} sharedGroupNames={[]}
innerRef={ref} size={28}
onClick={this.showAvatarPopup} innerRef={ref}
/> onClick={this.showAvatarPopup}
/>
{hasPendingUpdate && (
<div className="module-main-header__avatar--badged" />
)}
</div>
)} )}
</Reference> </Reference>
{showingAvatarPopup && popperRoot {showingAvatarPopup && popperRoot
@ -408,6 +417,8 @@ export class MainHeader extends React.Component<PropsType, StateType> {
title={title} title={title}
avatarPath={avatarPath} avatarPath={avatarPath}
size={28} size={28}
hasPendingUpdate={hasPendingUpdate}
startUpdate={startUpdate}
// See the comment above about `sharedGroupNames`. // See the comment above about `sharedGroupNames`.
sharedGroupNames={[]} sharedGroupNames={[]}
onEditProfile={() => { onEditProfile={() => {

View File

@ -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} />
</>
));
});

View File

@ -69,6 +69,7 @@ const createProps = (): PropsType => ({
defaultConversationColor: DEFAULT_CONVERSATION_COLOR, defaultConversationColor: DEFAULT_CONVERSATION_COLOR,
deviceName: 'Work Windows ME', deviceName: 'Work Windows ME',
hasAudioNotifications: true, hasAudioNotifications: true,
hasAutoDownloadUpdate: true,
hasAutoLaunch: true, hasAutoLaunch: true,
hasCallNotifications: true, hasCallNotifications: true,
hasCallRingtoneNotification: false, hasCallRingtoneNotification: false,
@ -125,6 +126,7 @@ const createProps = (): PropsType => ({
isSystemTraySupported: true, isSystemTraySupported: true,
onAudioNotificationsChange: action('onAudioNotificationsChange'), onAudioNotificationsChange: action('onAudioNotificationsChange'),
onAutoDownloadUpdateChange: action('onAutoDownloadUpdateChange'),
onAutoLaunchChange: action('onAutoLaunchChange'), onAutoLaunchChange: action('onAutoLaunchChange'),
onCallNotificationsChange: action('onCallNotificationsChange'), onCallNotificationsChange: action('onCallNotificationsChange'),
onCallRingtoneNotificationChange: action('onCallRingtoneNotificationChange'), onCallRingtoneNotificationChange: action('onCallRingtoneNotificationChange'),

View File

@ -43,6 +43,7 @@ export type PropsType = {
defaultConversationColor: DefaultConversationColorType; defaultConversationColor: DefaultConversationColorType;
deviceName?: string; deviceName?: string;
hasAudioNotifications?: boolean; hasAudioNotifications?: boolean;
hasAutoDownloadUpdate: boolean;
hasAutoLaunch: boolean; hasAutoLaunch: boolean;
hasCallNotifications: boolean; hasCallNotifications: boolean;
hasCallRingtoneNotification: boolean; hasCallRingtoneNotification: boolean;
@ -104,6 +105,7 @@ export type PropsType = {
// Change handlers // Change handlers
onAudioNotificationsChange: CheckboxChangeHandlerType; onAudioNotificationsChange: CheckboxChangeHandlerType;
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
onAutoLaunchChange: CheckboxChangeHandlerType; onAutoLaunchChange: CheckboxChangeHandlerType;
onCallNotificationsChange: CheckboxChangeHandlerType; onCallNotificationsChange: CheckboxChangeHandlerType;
onCallRingtoneNotificationChange: CheckboxChangeHandlerType; onCallRingtoneNotificationChange: CheckboxChangeHandlerType;
@ -161,6 +163,7 @@ export const Preferences = ({
editCustomColor, editCustomColor,
getConversationsWithCustomColor, getConversationsWithCustomColor,
hasAudioNotifications, hasAudioNotifications,
hasAutoDownloadUpdate,
hasAutoLaunch, hasAutoLaunch,
hasCallNotifications, hasCallNotifications,
hasCallRingtoneNotification, hasCallRingtoneNotification,
@ -191,6 +194,7 @@ export const Preferences = ({
makeSyncRequest, makeSyncRequest,
notificationContent, notificationContent,
onAudioNotificationsChange, onAudioNotificationsChange,
onAutoDownloadUpdateChange,
onAutoLaunchChange, onAutoLaunchChange,
onCallNotificationsChange, onCallNotificationsChange,
onCallRingtoneNotificationChange, onCallRingtoneNotificationChange,
@ -340,6 +344,15 @@ export const Preferences = ({
onChange={onMediaCameraPermissionsChange} onChange={onMediaCameraPermissionsChange}
/> />
</SettingsRow> </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) { } else if (page === Page.Appearance) {

View File

@ -21,12 +21,12 @@ export const RelinkDialog = ({
} }
return ( return (
<div className="module-left-pane-dialog module-left-pane-dialog--warning"> <div className="LeftPaneDialog LeftPaneDialog--warning">
<div className="module-left-pane-dialog__message"> <div className="LeftPaneDialog__message">
<h3>{i18n('unlinked')}</h3> <h3>{i18n('unlinked')}</h3>
<span>{i18n('unlinkedWarning')}</span> <span>{i18n('unlinkedWarning')}</span>
</div> </div>
<div className="module-left-pane-dialog__actions"> <div className="LeftPaneDialog__actions">
<button onClick={relinkDevice} type="button"> <button onClick={relinkDevice} type="button">
{i18n('relink')} {i18n('relink')}
</button> </button>

View File

@ -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} />
</>
));
});

View File

@ -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>
);
};

View 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')} &middot;{' '}
{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>,
]}
/>
</>
);
};

View File

@ -61,6 +61,7 @@ export class SettingsChannel {
isEphemeral: true, isEphemeral: true,
}); });
this.installSetting('autoDownloadUpdate');
this.installSetting('autoLaunch'); this.installSetting('autoLaunch');
this.installSetting('alwaysRelayCalls'); this.installSetting('alwaysRelayCalls');

View File

@ -2,26 +2,24 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { Dialogs } from '../types/Dialogs'; import { DialogType } from '../types/Dialogs';
import { ShowUpdateDialogAction } from '../state/ducks/updates'; import {
UpdateDialogOptionsType,
ShowUpdateDialogAction,
} from '../state/ducks/updates';
type UpdatesActions = { type UpdatesActions = {
showUpdateDialog: (x: Dialogs) => ShowUpdateDialogAction; showUpdateDialog: (
x: DialogType,
options: UpdateDialogOptionsType
) => ShowUpdateDialogAction;
}; };
type EventsType = { export function initializeUpdateListener(updatesActions: UpdatesActions): void {
once: (ev: string, f: () => void) => void; ipcRenderer.on(
}; 'show-update-dialog',
(_, dialogType: DialogType, options: UpdateDialogOptionsType = {}) => {
export function initializeUpdateListener( updatesActions.showUpdateDialog(dialogType, options);
updatesActions: UpdatesActions, }
events: EventsType );
): void {
ipcRenderer.on('show-update-dialog', (_, dialogType: Dialogs) => {
updatesActions.showUpdateDialog(dialogType);
});
events.once('snooze-update', () => {
updatesActions.showUpdateDialog(Dialogs.Update);
});
} }

View File

@ -6,7 +6,3 @@ import { ipcRenderer } from 'electron';
export function startUpdate(): void { export function startUpdate(): void {
ipcRenderer.send('start-update'); ipcRenderer.send('start-update');
} }
export function ackRender(): void {
ipcRenderer.send('show-update-dialog-ack');
}

View File

@ -1,86 +1,110 @@
// Copyright 2020 Signal Messenger, LLC // Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import { Dialogs } from '../../types/Dialogs'; import { ThunkAction } from 'redux-thunk';
import * as updateIpc from '../../shims/updateIpc'; 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 // State
export type UpdatesStateType = { export type UpdatesStateType = {
dialogType: Dialogs; dialogType: DialogType;
didSnooze: boolean; didSnooze: boolean;
downloadSize?: number;
downloadedSize?: number;
showEventsCount: number; showEventsCount: number;
version?: string;
}; };
// Actions // Actions
const ACK_RENDER = 'updates/ACK_RENDER';
const DISMISS_DIALOG = 'updates/DISMISS_DIALOG'; const DISMISS_DIALOG = 'updates/DISMISS_DIALOG';
const SHOW_UPDATE_DIALOG = 'updates/SHOW_UPDATE_DIALOG'; const SHOW_UPDATE_DIALOG = 'updates/SHOW_UPDATE_DIALOG';
const SNOOZE_UPDATE = 'updates/SNOOZE_UPDATE'; const SNOOZE_UPDATE = 'updates/SNOOZE_UPDATE';
const START_UPDATE = 'updates/START_UPDATE'; const START_UPDATE = 'updates/START_UPDATE';
const UNSNOOZE_UPDATE = 'updates/UNSNOOZE_UPDATE';
type AckRenderAction = { export type UpdateDialogOptionsType = {
type: 'updates/ACK_RENDER'; downloadSize?: number;
downloadedSize?: number;
version?: string;
}; };
type DismissDialogAction = { type DismissDialogAction = {
type: 'updates/DISMISS_DIALOG'; type: typeof DISMISS_DIALOG;
}; };
export type ShowUpdateDialogAction = { export type ShowUpdateDialogAction = {
type: 'updates/SHOW_UPDATE_DIALOG'; type: typeof SHOW_UPDATE_DIALOG;
payload: Dialogs; payload: {
dialogType: DialogType;
otherState: UpdateDialogOptionsType;
};
}; };
type SnoozeUpdateActionType = { type SnoozeUpdateActionType = {
type: 'updates/SNOOZE_UPDATE'; type: typeof SNOOZE_UPDATE;
}; };
type StartUpdateAction = { type StartUpdateAction = {
type: 'updates/START_UPDATE'; type: typeof START_UPDATE;
};
type UnsnoozeUpdateActionType = {
type: typeof UNSNOOZE_UPDATE;
payload: DialogType;
}; };
export type UpdatesActionType = export type UpdatesActionType =
| AckRenderAction
| DismissDialogAction | DismissDialogAction
| ShowUpdateDialogAction | ShowUpdateDialogAction
| SnoozeUpdateActionType | SnoozeUpdateActionType
| StartUpdateAction; | StartUpdateAction
| UnsnoozeUpdateActionType;
// Action Creators // Action Creators
function ackRender(): AckRenderAction {
updateIpc.ackRender();
return {
type: ACK_RENDER,
};
}
function dismissDialog(): DismissDialogAction { function dismissDialog(): DismissDialogAction {
return { return {
type: DISMISS_DIALOG, type: DISMISS_DIALOG,
}; };
} }
function showUpdateDialog(dialogType: Dialogs): ShowUpdateDialogAction { function showUpdateDialog(
dialogType: DialogType,
updateDialogOptions: UpdateDialogOptionsType = {}
): ShowUpdateDialogAction {
return { return {
type: SHOW_UPDATE_DIALOG, 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 { function snoozeUpdate(): ThunkAction<
setTimeout(() => { void,
trigger('snooze-update'); RootStateType,
}, SNOOZE_TIMER); 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, type: SNOOZE_UPDATE,
});
}; };
} }
@ -93,7 +117,6 @@ function startUpdate(): StartUpdateAction {
} }
export const actions = { export const actions = {
ackRender,
dismissDialog, dismissDialog,
showUpdateDialog, showUpdateDialog,
snoozeUpdate, snoozeUpdate,
@ -104,7 +127,7 @@ export const actions = {
function getEmptyState(): UpdatesStateType { function getEmptyState(): UpdatesStateType {
return { return {
dialogType: Dialogs.None, dialogType: DialogType.None,
didSnooze: false, didSnooze: false,
showEventsCount: 0, showEventsCount: 0,
}; };
@ -115,37 +138,46 @@ export function reducer(
action: Readonly<UpdatesActionType> action: Readonly<UpdatesActionType>
): UpdatesStateType { ): UpdatesStateType {
if (action.type === SHOW_UPDATE_DIALOG) { if (action.type === SHOW_UPDATE_DIALOG) {
const { dialogType, otherState } = action.payload;
return { return {
dialogType: action.payload, ...state,
didSnooze: state.didSnooze, ...otherState,
dialogType,
showEventsCount: state.showEventsCount + 1, showEventsCount: state.showEventsCount + 1,
}; };
} }
if (action.type === SNOOZE_UPDATE) { if (action.type === SNOOZE_UPDATE) {
return { return {
dialogType: Dialogs.None, ...state,
dialogType: DialogType.None,
didSnooze: true, didSnooze: true,
showEventsCount: state.showEventsCount,
}; };
} }
if (action.type === START_UPDATE) { if (action.type === START_UPDATE) {
return { return {
dialogType: Dialogs.None, ...state,
didSnooze: state.didSnooze, dialogType: DialogType.None,
showEventsCount: state.showEventsCount,
}; };
} }
if ( if (
action.type === DISMISS_DIALOG && action.type === DISMISS_DIALOG &&
state.dialogType === Dialogs.MacOS_Read_Only state.dialogType === DialogType.MacOS_Read_Only
) { ) {
return { return {
dialogType: Dialogs.None, ...state,
didSnooze: state.didSnooze, dialogType: DialogType.None,
showEventsCount: state.showEventsCount, };
}
if (action.type === UNSNOOZE_UPDATE) {
return {
...state,
dialogType: action.payload,
didSnooze: false,
}; };
} }

View File

@ -3,7 +3,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
import { ExpiredBuildDialog } from '../../components/ExpiredBuildDialog'; import { DialogExpiredBuild } from '../../components/DialogExpiredBuild';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
@ -16,4 +16,4 @@ const mapStateToProps = (state: StateType) => {
const smart = connect(mapStateToProps, mapDispatchToProps); const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartExpiredBuildDialog = smart(ExpiredBuildDialog); export const SmartExpiredBuildDialog = smart(DialogExpiredBuild);

View File

@ -25,6 +25,7 @@ import { getMe, getSelectedConversation } from '../selectors/conversations';
const mapStateToProps = (state: StateType) => { const mapStateToProps = (state: StateType) => {
return { return {
disabled: state.network.challengeStatus !== 'idle', disabled: state.network.challengeStatus !== 'idle',
hasPendingUpdate: Boolean(state.updates.didSnooze),
searchTerm: getQuery(state), searchTerm: getQuery(state),
searchConversationId: getSearchConversationId(state), searchConversationId: getSearchConversationId(state),
searchConversationName: getSearchConversationName(state), searchConversationName: getSearchConversationName(state),

View File

@ -3,7 +3,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
import { NetworkStatus } from '../../components/NetworkStatus'; import { DialogNetworkStatus } from '../../components/DialogNetworkStatus';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { hasNetworkDialog } from '../selectors/network'; import { hasNetworkDialog } from '../selectors/network';
@ -18,4 +18,4 @@ const mapStateToProps = (state: StateType) => {
const smart = connect(mapStateToProps, mapDispatchToProps); const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartNetworkStatus = smart(NetworkStatus); export const SmartNetworkStatus = smart(DialogNetworkStatus);

View File

@ -3,7 +3,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions'; import { mapDispatchToProps } from '../actions';
import { UpdateDialog } from '../../components/UpdateDialog'; import { DialogUpdate } from '../../components/DialogUpdate';
import { StateType } from '../reducer'; import { StateType } from '../reducer';
import { getIntl } from '../selectors/user'; import { getIntl } from '../selectors/user';
import { hasNetworkDialog } from '../selectors/network'; import { hasNetworkDialog } from '../selectors/network';
@ -18,4 +18,4 @@ const mapStateToProps = (state: StateType) => {
const smart = connect(mapStateToProps, mapDispatchToProps); const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartUpdateDialog = smart(UpdateDialog); export const SmartUpdateDialog = smart(DialogUpdate);

View File

@ -9,10 +9,11 @@ import {
getVersion, getVersion,
isUpdateFileNameValid, isUpdateFileNameValid,
validatePath, validatePath,
parseYaml,
} from '../../updater/common'; } from '../../updater/common';
describe('updater/signatures', () => { describe('updater/signatures', () => {
const windows = `version: 1.23.2 const windows = parseYaml(`version: 1.23.2
files: files:
- url: signal-desktop-win-1.23.2.exe - url: signal-desktop-win-1.23.2.exe
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ== sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
@ -20,8 +21,8 @@ files:
path: signal-desktop-win-1.23.2.exe path: signal-desktop-win-1.23.2.exe
sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ== sha512: hhK+cVAb+QOK/Ln0RBcq8Rb1iPcUC0KZeT4NwLB25PMGoPmakY27XE1bXq4QlkASJN1EkYTbKf3oUJtcllziyQ==
releaseDate: '2019-03-29T16:58:08.210Z' releaseDate: '2019-03-29T16:58:08.210Z'
`; `);
const mac = `version: 1.23.2 const mac = parseYaml(`version: 1.23.2
files: files:
- url: signal-desktop-mac-1.23.2.zip - url: signal-desktop-mac-1.23.2.zip
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg== sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
@ -30,8 +31,8 @@ files:
path: signal-desktop-mac-1.23.2.zip path: signal-desktop-mac-1.23.2.zip
sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg== sha512: f4pPo3WulTVi9zBWGsJPNIlvPOTCxPibPPDmRFDoXMmFm6lqJpXZQ9DSWMJumfc4BRp4y/NTQLGYI6b4WuJwhg==
releaseDate: '2019-03-29T16:57:16.997Z' 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: files:
- url: signal-desktop-beta-win-1.23.2-beta.1.exe - url: signal-desktop-beta-win-1.23.2-beta.1.exe
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ== sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
@ -39,8 +40,8 @@ files:
path: signal-desktop-beta-win-1.23.2-beta.1.exe path: signal-desktop-beta-win-1.23.2-beta.1.exe
sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ== sha512: ZHM1F3y/Y6ulP5NhbFuh7t2ZCpY4lD9BeBhPV+g2B/0p/66kp0MJDeVxTgjR49OakwpMAafA1d6y2QBail4hSQ==
releaseDate: '2019-03-29T01:56:00.544Z' 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: files:
- url: signal-desktop-beta-mac-1.23.2-beta.1.zip - url: signal-desktop-beta-mac-1.23.2-beta.1.zip
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw== sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
@ -49,7 +50,7 @@ files:
path: signal-desktop-beta-mac-1.23.2-beta.1.zip path: signal-desktop-beta-mac-1.23.2-beta.1.zip
sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw== sha512: h/01N0DD5Jw2Q6M1n4uLGLTCrMFxcn8QOPtLR3HpABsf3w9b2jFtKb56/2cbuJXP8ol8TkTDWKnRV6mnqnLBDw==
releaseDate: '2019-03-29T01:53:23.881Z' releaseDate: '2019-03-29T01:53:23.881Z'
`; `);
describe('#getVersion', () => { describe('#getVersion', () => {
it('successfully gets version', () => { it('successfully gets version', () => {

View File

@ -3,9 +3,11 @@
/* eslint-disable camelcase */ /* eslint-disable camelcase */
export enum Dialogs { export enum DialogType {
None, None = 'None',
Update, Update = 'Update',
Cannot_Update, Cannot_Update = 'Cannot_Update',
MacOS_Read_Only, MacOS_Read_Only = 'MacOS_Read_Only',
DownloadReady = 'DownloadReady',
Downloading = 'Downloading',
} }

View File

@ -33,6 +33,7 @@ export type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
export type StorageAccessType = { export type StorageAccessType = {
'always-relay-calls': boolean; 'always-relay-calls': boolean;
'audio-notification': boolean; 'audio-notification': boolean;
'auto-download-update': boolean;
'badge-count-muted-conversations': boolean; 'badge-count-muted-conversations': boolean;
'blocked-groups': Array<string>; 'blocked-groups': Array<string>;
'blocked-uuids': Array<string>; 'blocked-uuids': Array<string>;

View File

@ -9,6 +9,7 @@ import {
} from 'fs'; } from 'fs';
import { join, normalize } from 'path'; import { join, normalize } from 'path';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import { throttle } from 'lodash';
import { createParser, ParserConfiguration } from 'dashdash'; import { createParser, ParserConfiguration } from 'dashdash';
import ProxyAgent from 'proxy-agent'; import ProxyAgent from 'proxy-agent';
@ -20,10 +21,10 @@ import { v4 as getGuid } from 'uuid';
import pify from 'pify'; import pify from 'pify';
import mkdirp from 'mkdirp'; import mkdirp from 'mkdirp';
import rimraf from 'rimraf'; import rimraf from 'rimraf';
import { app, BrowserWindow, dialog, ipcMain } from 'electron'; import { app, BrowserWindow, ipcMain } from 'electron';
import { getTempPath } from '../../app/attachments'; import { getTempPath } from '../../app/attachments';
import { Dialogs } from '../types/Dialogs'; import { DialogType } from '../types/Dialogs';
import { getUserAgent } from '../util/getUserAgent'; import { getUserAgent } from '../util/getUserAgent';
import { isAlpha, isBeta } from '../util/version'; import { isAlpha, isBeta } from '../util/version';
@ -31,7 +32,6 @@ import * as packageJson from '../../package.json';
import { getSignatureFileName } from './signature'; import { getSignatureFileName } from './signature';
import { isPathInside } from '../util/isPathInside'; import { isPathInside } from '../util/isPathInside';
import { LocaleType } from '../types/I18N';
import { LoggerType } from '../types/Logging'; import { LoggerType } from '../types/Logging';
const writeFile = pify(writeFileCallback); const writeFile = pify(writeFileCallback);
@ -39,24 +39,40 @@ const mkdirpPromise = pify(mkdirp);
const rimrafPromise = pify(rimraf); const rimrafPromise = pify(rimraf);
const { platform } = process; const { platform } = process;
export const ACK_RENDER_TIMEOUT = 10000;
export const GOT_CONNECT_TIMEOUT = 2 * 60 * 1000; export const GOT_CONNECT_TIMEOUT = 2 * 60 * 1000;
export const GOT_LOOKUP_TIMEOUT = 2 * 60 * 1000; export const GOT_LOOKUP_TIMEOUT = 2 * 60 * 1000;
export const GOT_SOCKET_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 = { export type UpdaterInterface = {
force(): Promise<void>; force(): Promise<void>;
}; };
export type UpdateInformationType = {
fileName: string;
size: number;
version: string;
};
export async function checkForUpdates( export async function checkForUpdates(
logger: LoggerType, logger: LoggerType,
forceUpdate = false forceUpdate = false
): Promise<{ ): Promise<UpdateInformationType | null> {
fileName: string;
version: string;
} | null> {
const yaml = await getUpdateYaml(); const yaml = await getUpdateYaml();
const version = getVersion(yaml); const parsedYaml = parseYaml(yaml);
const version = getVersion(parsedYaml);
if (!version) { if (!version) {
logger.warn('checkForUpdates: no version extracted from downloaded yaml'); logger.warn('checkForUpdates: no version extracted from downloaded yaml');
@ -70,8 +86,11 @@ export async function checkForUpdates(
`forceUpdate=${forceUpdate}` `forceUpdate=${forceUpdate}`
); );
const fileName = getUpdateFileName(parsedYaml);
return { return {
fileName: getUpdateFileName(yaml), fileName,
size: getSize(parsedYaml, fileName),
version, version,
}; };
} }
@ -95,7 +114,8 @@ export function validatePath(basePath: string, targetPath: string): void {
export async function downloadUpdate( export async function downloadUpdate(
fileName: string, fileName: string,
logger: LoggerType logger: LoggerType,
mainWindow?: BrowserWindow
): Promise<string> { ): Promise<string> {
const baseUrl = getUpdatesBase(); const baseUrl = getUpdatesBase();
const updateFileUrl = `${baseUrl}/${fileName}`; const updateFileUrl = `${baseUrl}/${fileName}`;
@ -121,6 +141,23 @@ export async function downloadUpdate(
const writeStream = createWriteStream(targetUpdatePath); const writeStream = createWriteStream(targetUpdatePath);
await new Promise<void>((resolve, reject) => { 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 => { downloadStream.on('error', error => {
reject(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 // Helper functions
export function getUpdateCheckUrl(): string { export function getUpdateCheckUrl(): string {
@ -288,9 +225,7 @@ function isVersionNewer(newVersion: string): boolean {
return gt(newVersion, version); return gt(newVersion, version);
} }
export function getVersion(yaml: string): string | null { export function getVersion(info: JSONUpdateSchema): string | null {
const info = parseYaml(yaml);
return info && info.version; return info && info.version;
} }
@ -299,11 +234,7 @@ export function isUpdateFileNameValid(name: string): boolean {
return validFile.test(name); return validFile.test(name);
} }
// Reliant on third party parser that returns any export function getUpdateFileName(info: JSONUpdateSchema): string {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getUpdateFileName(yaml: string): any {
const info = parseYaml(yaml);
if (!info || !info.path) { if (!info || !info.path) {
throw new Error('getUpdateFileName: No path present in YAML file'); throw new Error('getUpdateFileName: No path present in YAML file');
} }
@ -318,9 +249,17 @@ export function getUpdateFileName(yaml: string): any {
return path; return path;
} }
// Reliant on third party parser that returns any function getSize(info: JSONUpdateSchema, fileName: string): number {
// eslint-disable-next-line @typescript-eslint/no-explicit-any if (!info || !info.files) {
function parseYaml(yaml: string): any { 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 }); 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 { export function setUpdateListener(performUpdateCallback: () => void): void {
ipcMain.once('start-update', performUpdateCallback); 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');
});
}

View File

@ -7,7 +7,6 @@ import { BrowserWindow } from 'electron';
import { UpdaterInterface } from './common'; import { UpdaterInterface } from './common';
import { start as startMacOS } from './macos'; import { start as startMacOS } from './macos';
import { start as startWindows } from './windows'; import { start as startWindows } from './windows';
import { LocaleType } from '../types/I18N';
import { LoggerType } from '../types/Logging'; import { LoggerType } from '../types/Logging';
let initialized = false; let initialized = false;
@ -16,7 +15,6 @@ let updater: UpdaterInterface | undefined;
export async function start( export async function start(
getMainWindow: () => BrowserWindow, getMainWindow: () => BrowserWindow,
locale?: LocaleType,
logger?: LoggerType logger?: LoggerType
): Promise<void> { ): Promise<void> {
const { platform } = process; const { platform } = process;
@ -26,9 +24,6 @@ export async function start(
} }
initialized = true; initialized = true;
if (!locale) {
throw new Error('updater/start: Must provide locale!');
}
if (!logger) { if (!logger) {
throw new Error('updater/start: Must provide logger!'); throw new Error('updater/start: Must provide logger!');
} }
@ -42,9 +37,9 @@ export async function start(
} }
if (platform === 'win32') { if (platform === 'win32') {
updater = await startWindows(getMainWindow, locale, logger); updater = await startWindows(getMainWindow, logger);
} else if (platform === 'darwin') { } else if (platform === 'darwin') {
updater = await startMacOS(getMainWindow, locale, logger); updater = await startMacOS(getMainWindow, logger);
} else { } else {
throw new Error('updater/start: Unsupported platform'); throw new Error('updater/start: Unsupported platform');
} }

View File

@ -7,27 +7,25 @@ import { AddressInfo } from 'net';
import { dirname } from 'path'; import { dirname } from 'path';
import { v4 as getGuid } from 'uuid'; 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 { get as getFromConfig } from 'config';
import { gt } from 'semver'; import { gt } from 'semver';
import got from 'got'; import got from 'got';
import { import {
ACK_RENDER_TIMEOUT,
checkForUpdates, checkForUpdates,
deleteTempDir, deleteTempDir,
downloadUpdate, downloadUpdate,
getAutoDownloadUpdateSetting,
getPrintableError, getPrintableError,
setUpdateListener, setUpdateListener,
showCannotUpdateDialog,
showUpdateDialog,
UpdaterInterface, UpdaterInterface,
UpdateInformationType,
} from './common'; } from './common';
import { LocaleType } from '../types/I18N';
import { LoggerType } from '../types/Logging'; import { LoggerType } from '../types/Logging';
import { hexToBinary, verifySignature } from './signature'; import { hexToBinary, verifySignature } from './signature';
import { markShouldQuit } from '../../app/window_state'; import { markShouldQuit } from '../../app/window_state';
import { Dialogs } from '../types/Dialogs'; import { DialogType } from '../types/Dialogs';
const SECOND = 1000; const SECOND = 1000;
const MINUTE = SECOND * 60; const MINUTE = SECOND * 60;
@ -35,7 +33,6 @@ const INTERVAL = MINUTE * 30;
export async function start( export async function start(
getMainWindow: () => BrowserWindow, getMainWindow: () => BrowserWindow,
locale: LocaleType,
logger: LoggerType logger: LoggerType
): Promise<UpdaterInterface> { ): Promise<UpdaterInterface> {
logger.info('macos/start: starting checks...'); logger.info('macos/start: starting checks...');
@ -45,19 +42,17 @@ export async function start(
setInterval(async () => { setInterval(async () => {
try { try {
await checkDownloadAndInstall(getMainWindow, locale, logger); await checkForUpdatesMaybeInstall(getMainWindow, logger);
} catch (error) { } catch (error) {
logger.error('macos/start: error:', getPrintableError(error)); logger.error(`macos/start: ${getPrintableError(error)}`);
} }
}, INTERVAL); }, INTERVAL);
setUpdateListener(createUpdater(logger)); await checkForUpdatesMaybeInstall(getMainWindow, logger);
await checkDownloadAndInstall(getMainWindow, locale, logger);
return { return {
async force(): Promise<void> { async force(): Promise<void> {
return checkDownloadAndInstall(getMainWindow, locale, logger, true); return checkForUpdatesMaybeInstall(getMainWindow, logger, true);
}, },
}; };
} }
@ -67,39 +62,69 @@ let version: string;
let updateFilePath: string; let updateFilePath: string;
let loggerForQuitHandler: LoggerType; let loggerForQuitHandler: LoggerType;
async function checkDownloadAndInstall( async function checkForUpdatesMaybeInstall(
getMainWindow: () => BrowserWindow, getMainWindow: () => BrowserWindow,
locale: LocaleType,
logger: LoggerType, logger: LoggerType,
force = false force = false
) { ) {
logger.info('checkDownloadAndInstall: checking for update...'); logger.info('checkForUpdatesMaybeInstall: checking for update...');
try { const result = await checkForUpdates(logger, force);
const result = await checkForUpdates(logger, force); if (!result) {
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; return;
} }
await downloadAndInstall(newFileName, newVersion, getMainWindow, logger);
}
}
const { fileName: newFileName, version: newVersion } = result; async function downloadAndInstall(
if (fileName !== newFileName || !version || gt(newVersion, version)) { newFileName: string,
const oldFileName = fileName; newVersion: string,
const oldVersion = version; getMainWindow: () => BrowserWindow,
logger: LoggerType,
updateOnProgress?: boolean
) {
try {
const oldFileName = fileName;
const oldVersion = version;
deleteCache(updateFilePath, logger); deleteCache(updateFilePath, logger);
fileName = newFileName; fileName = newFileName;
version = newVersion; version = newVersion;
try { try {
updateFilePath = await downloadUpdate(fileName, logger); updateFilePath = await downloadUpdate(
} catch (error) { fileName,
// Restore state in case of download error logger,
fileName = oldFileName; updateOnProgress ? getMainWindow() : undefined
version = oldVersion; );
throw error; } catch (error) {
} // Restore state in case of download error
fileName = oldFileName;
version = oldVersion;
throw error;
} }
if (!updateFilePath) { if (!updateFilePath) {
logger.info('checkDownloadAndInstall: no update file path. Skipping!'); logger.info('downloadAndInstall: no update file path. Skipping!');
return; return;
} }
@ -109,7 +134,7 @@ async function checkDownloadAndInstall(
// Note: We don't delete the cache here, because we don't want to continually // 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. // re-download the broken release. We will download it only once per launch.
throw new Error( 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 readOnly = 'Cannot update while running on a read-only volume';
const message: string = error.message || ''; const message: string = error.message || '';
if (message.includes(readOnly)) { if (message.includes(readOnly)) {
logger.info('checkDownloadAndInstall: showing read-only dialog...'); logger.info('downloadAndInstall: showing read-only dialog...');
showReadOnlyDialog(getMainWindow(), locale); getMainWindow().webContents.send(
'show-update-dialog',
DialogType.MacOS_Read_Only
);
} else { } else {
logger.info( 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; throw error;
@ -133,12 +164,13 @@ async function checkDownloadAndInstall(
// At this point, closing the app will cause the update to be installed automatically // 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. // 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...'); getMainWindow().webContents.send('show-update-dialog', DialogType.Update, {
version,
showUpdateDialog(getMainWindow(), locale, createUpdater(logger)); });
} catch (error) { } 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) { if (filePath) {
const tempDir = dirname(filePath); const tempDir = dirname(filePath);
deleteTempDir(tempDir).catch(error => { deleteTempDir(tempDir).catch(error => {
logger.error( logger.error(`quitHandler: ${getPrintableError(error)}`);
'quitHandler: error deleting temporary directory:',
getPrintableError(error)
);
}); });
} }
} }
@ -171,10 +200,7 @@ async function handToAutoUpdate(
let serverUrl: string; let serverUrl: string;
server.on('error', (error: Error) => { server.on('error', (error: Error) => {
logger.error( logger.error(`handToAutoUpdate: ${getPrintableError(error)}`);
'handToAutoUpdate: server had error',
getPrintableError(error)
);
shutdown(server, logger); shutdown(server, logger);
reject(error); reject(error);
}); });
@ -254,8 +280,9 @@ function pipeUpdateToSquirrel(
response.on('error', (error: Error) => { response.on('error', (error: Error) => {
logger.error( logger.error(
'pipeUpdateToSquirrel: update file download request had an error', `pipeUpdateToSquirrel: update file download request had an error ${getPrintableError(
getPrintableError(error) error
)}`
); );
shutdown(server, logger); shutdown(server, logger);
reject(error); reject(error);
@ -263,8 +290,9 @@ function pipeUpdateToSquirrel(
readStream.on('error', (error: Error) => { readStream.on('error', (error: Error) => {
logger.error( logger.error(
'pipeUpdateToSquirrel: read stream error response:', `pipeUpdateToSquirrel: read stream error response: ${getPrintableError(
getPrintableError(error) error
)}`
); );
shutdown(server, logger, response); shutdown(server, logger, response);
reject(error); reject(error);
@ -339,7 +367,7 @@ function shutdown(
server.close(); server.close();
} }
} catch (error) { } catch (error) {
logger.error('shutdown: Error closing server', getPrintableError(error)); logger.error(`shutdown: Error closing server ${getPrintableError(error)}`);
} }
try { try {
@ -348,62 +376,32 @@ function shutdown(
} }
} catch (endError) { } catch (endError) {
logger.error( logger.error(
"shutdown: couldn't end response", `shutdown: couldn't end response ${getPrintableError(endError)}`
getPrintableError(endError)
); );
} }
} }
export function showReadOnlyDialog( function createUpdater(
mainWindow: BrowserWindow, getMainWindow: () => BrowserWindow,
locale: LocaleType info: Pick<UpdateInformationType, 'fileName' | 'version'>,
): void { logger: LoggerType
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
) { ) {
if (showingReadOnlyDialog) { return async () => {
return; if (updateFilePath) {
} logger.info('performUpdate: calling quitAndInstall...');
markShouldQuit();
const options = { autoUpdater.quitAndInstall();
type: 'warning', } else {
buttons: [locale.messages.ok.message], logger.info(
title: locale.messages.cannotUpdate.message, 'performUpdate: have not downloaded update, going to download'
message: locale.i18n('readOnlyVolume', { );
app: 'Signal.app', await downloadAndInstall(
folder: '/Applications', info.fileName,
}), info.version,
}; getMainWindow,
logger,
showingReadOnlyDialog = true; true
);
await dialog.showMessageBox(mainWindow, options); }
showingReadOnlyDialog = false;
}
function createUpdater(logger: LoggerType) {
return () => {
logger.info('performUpdate: calling quitAndInstall...');
markShouldQuit();
autoUpdater.quitAndInstall();
}; };
} }

View File

@ -14,16 +14,16 @@ import {
checkForUpdates, checkForUpdates,
deleteTempDir, deleteTempDir,
downloadUpdate, downloadUpdate,
getAutoDownloadUpdateSetting,
getPrintableError, getPrintableError,
setUpdateListener, setUpdateListener,
showCannotUpdateDialog,
showUpdateDialog,
UpdaterInterface, UpdaterInterface,
UpdateInformationType,
} from './common'; } from './common';
import { LocaleType } from '../types/I18N';
import { LoggerType } from '../types/Logging'; import { LoggerType } from '../types/Logging';
import { hexToBinary, verifySignature } from './signature'; import { hexToBinary, verifySignature } from './signature';
import { markShouldQuit } from '../../app/window_state'; import { markShouldQuit } from '../../app/window_state';
import { DialogType } from '../types/Dialogs';
const readdir = pify(readdirCallback); const readdir = pify(readdirCallback);
const unlink = pify(unlinkCallback); const unlink = pify(unlinkCallback);
@ -40,7 +40,6 @@ let loggerForQuitHandler: LoggerType;
export async function start( export async function start(
getMainWindow: () => BrowserWindow, getMainWindow: () => BrowserWindow,
locale: LocaleType,
logger: LoggerType logger: LoggerType
): Promise<UpdaterInterface> { ): Promise<UpdaterInterface> {
logger.info('windows/start: starting checks...'); logger.info('windows/start: starting checks...');
@ -48,56 +47,84 @@ export async function start(
loggerForQuitHandler = logger; loggerForQuitHandler = logger;
app.once('quit', quitHandler); app.once('quit', quitHandler);
setUpdateListener(createUpdater(getMainWindow, locale, logger));
setInterval(async () => { setInterval(async () => {
try { try {
await checkDownloadAndInstall(getMainWindow, locale, logger); await checkForUpdatesMaybeInstall(getMainWindow, logger);
} catch (error) { } catch (error) {
logger.error('windows/start: error:', getPrintableError(error)); logger.error(`windows/start: ${getPrintableError(error)}`);
} }
}, INTERVAL); }, INTERVAL);
await deletePreviousInstallers(logger); await deletePreviousInstallers(logger);
await checkDownloadAndInstall(getMainWindow, locale, logger); await checkForUpdatesMaybeInstall(getMainWindow, logger);
return { return {
async force(): Promise<void> { async force(): Promise<void> {
return checkDownloadAndInstall(getMainWindow, locale, logger, true); return checkForUpdatesMaybeInstall(getMainWindow, logger, true);
}, },
}; };
} }
async function checkDownloadAndInstall( async function checkForUpdatesMaybeInstall(
getMainWindow: () => BrowserWindow, getMainWindow: () => BrowserWindow,
locale: LocaleType,
logger: LoggerType, logger: LoggerType,
force = false force = false
) { ) {
try { logger.info('checkForUpdatesMaybeInstall: checking for update...');
logger.info('checkDownloadAndInstall: checking for update...'); const result = await checkForUpdates(logger, force);
const result = await checkForUpdates(logger, force); if (!result) {
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; return;
} }
await downloadAndInstall(newFileName, newVersion, getMainWindow, logger);
}
}
const { fileName: newFileName, version: newVersion } = result; async function downloadAndInstall(
if (fileName !== newFileName || !version || gt(newVersion, version)) { newFileName: string,
const oldFileName = fileName; newVersion: string,
const oldVersion = version; getMainWindow: () => BrowserWindow,
logger: LoggerType,
updateOnProgress?: boolean
) {
try {
const oldFileName = fileName;
const oldVersion = version;
deleteCache(updateFilePath, logger); deleteCache(updateFilePath, logger);
fileName = newFileName; fileName = newFileName;
version = newVersion; version = newVersion;
try { try {
updateFilePath = await downloadUpdate(fileName, logger); updateFilePath = await downloadUpdate(
} catch (error) { fileName,
// Restore state in case of download error logger,
fileName = oldFileName; updateOnProgress ? getMainWindow() : undefined
version = oldVersion; );
throw error; } catch (error) {
} // Restore state in case of download error
fileName = oldFileName;
version = oldVersion;
throw error;
} }
const publicKey = hexToBinary(getFromConfig('updatesPublicKey')); const publicKey = hexToBinary(getFromConfig('updatesPublicKey'));
@ -110,14 +137,12 @@ async function checkDownloadAndInstall(
); );
} }
logger.info('checkDownloadAndInstall: showing dialog...'); logger.info('downloadAndInstall: showing dialog...');
showUpdateDialog( getMainWindow().webContents.send('show-update-dialog', DialogType.Update, {
getMainWindow(), version,
locale, });
createUpdater(getMainWindow, locale, logger)
);
} catch (error) { } catch (error) {
logger.error('checkDownloadAndInstall: error', getPrintableError(error)); logger.error(`downloadAndInstall: ${getPrintableError(error)}`);
} }
} }
@ -125,10 +150,7 @@ function quitHandler() {
if (updateFilePath && !installing) { if (updateFilePath && !installing) {
verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch( verifyAndInstall(updateFilePath, version, loggerForQuitHandler).catch(
error => { error => {
loggerForQuitHandler.error( loggerForQuitHandler.error(`quitHandler: ${getPrintableError(error)}`);
'quitHandler: error installing:',
getPrintableError(error)
);
} }
); );
} }
@ -208,10 +230,7 @@ function deleteCache(filePath: string | null, logger: LoggerType) {
if (filePath) { if (filePath) {
const tempDir = dirname(filePath); const tempDir = dirname(filePath);
deleteTempDir(tempDir).catch(error => { deleteTempDir(tempDir).catch(error => {
logger.error( logger.error(`deleteCache: ${getPrintableError(error)}`);
'deleteCache: error deleting temporary directory',
getPrintableError(error)
);
}); });
} }
} }
@ -237,23 +256,37 @@ async function spawn(
function createUpdater( function createUpdater(
getMainWindow: () => BrowserWindow, getMainWindow: () => BrowserWindow,
locale: LocaleType, info: Pick<UpdateInformationType, 'fileName' | 'version'>,
logger: LoggerType logger: LoggerType
) { ) {
return async () => { return async () => {
try { if (updateFilePath) {
await verifyAndInstall(updateFilePath, version, logger); try {
installing = true; await verifyAndInstall(updateFilePath, version, logger);
} catch (error) { installing = true;
} catch (error) {
logger.info('createUpdater: showing general update failure dialog...');
getMainWindow().webContents.send(
'show-update-dialog',
DialogType.Cannot_Update
);
throw error;
}
markShouldQuit();
app.quit();
} else {
logger.info( logger.info(
'checkDownloadAndInstall: showing general update failure dialog...' 'performUpdate: have not downloaded update, going to download'
);
await downloadAndInstall(
info.fileName,
info.version,
getMainWindow,
logger,
true
); );
showCannotUpdateDialog(getMainWindow(), locale);
throw error;
} }
markShouldQuit();
app.quit();
}; };
} }

View File

@ -35,6 +35,7 @@ type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
export type IPCEventsValuesType = { export type IPCEventsValuesType = {
alwaysRelayCalls: boolean | undefined; alwaysRelayCalls: boolean | undefined;
audioNotification: boolean | undefined; audioNotification: boolean | undefined;
autoDownloadUpdate: boolean;
autoLaunch: boolean; autoLaunch: boolean;
callRingtoneNotification: boolean; callRingtoneNotification: boolean;
callSystemNotification: boolean; callSystemNotification: boolean;
@ -252,6 +253,10 @@ export function createIPCEvents(
window.storage.get('typingIndicators', false), window.storage.get('typingIndicators', false),
// Configurable settings // Configurable settings
getAutoDownloadUpdate: () =>
window.storage.get('auto-download-update', true),
setAutoDownloadUpdate: value =>
window.storage.put('auto-download-update', value),
getThemeSetting: () => getThemeSetting: () =>
window.storage.get( window.storage.get(
'theme-setting', 'theme-setting',

View File

@ -431,6 +431,13 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-06-15T23:46:51.629Z" "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(", "rule": "jQuery-append(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",
@ -439,6 +446,13 @@
"updated": "2021-02-26T18:44:56.450Z", "updated": "2021-02-26T18:44:56.450Z",
"reasonDetail": "Adding sub-view to DOM" "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(", "rule": "jQuery-appendTo(",
"path": "js/views/inbox_view.js", "path": "js/views/inbox_view.js",

11
ts/window.d.ts vendored
View File

@ -103,6 +103,7 @@ import { ProgressModal } from './components/ProgressModal';
import { Quote } from './components/conversation/Quote'; import { Quote } from './components/conversation/Quote';
import { StagedLinkPreview } from './components/conversation/StagedLinkPreview'; import { StagedLinkPreview } from './components/conversation/StagedLinkPreview';
import { DisappearingTimeDialog } from './components/DisappearingTimeDialog'; import { DisappearingTimeDialog } from './components/DisappearingTimeDialog';
import { WhatsNew } from './components/WhatsNew';
import { MIMEType } from './types/MIME'; import { MIMEType } from './types/MIME';
import { DownloadedAttachmentType } from './types/Attachment'; import { DownloadedAttachmentType } from './types/Attachment';
import { ElectronLocaleType } from './util/mapToSupportLocale'; import { ElectronLocaleType } from './util/mapToSupportLocale';
@ -298,11 +299,8 @@ declare global {
enableStorageService: () => boolean; enableStorageService: () => boolean;
eraseAllStorageServiceState: () => Promise<void>; eraseAllStorageServiceState: () => Promise<void>;
initializeGroupCredentialFetcher: () => void; initializeGroupCredentialFetcher: () => void;
initializeNetworkObserver: (network: WhatIsThis) => void; initializeNetworkObserver: (network: ReduxActions['network']) => void;
initializeUpdateListener: ( initializeUpdateListener: (updates: ReduxActions['updates']) => void;
updates: WhatIsThis,
events: WhatIsThis
) => void;
onTimeout: (timestamp: number, cb: () => void, id?: string) => string; onTimeout: (timestamp: number, cb: () => void, id?: string) => string;
removeTimeout: (uuid: string) => void; removeTimeout: (uuid: string) => void;
retryPlaceholders?: Util.RetryPlaceholders; retryPlaceholders?: Util.RetryPlaceholders;
@ -420,6 +418,7 @@ declare global {
ConfirmationDialog: typeof ConfirmationDialog; ConfirmationDialog: typeof ConfirmationDialog;
ContactDetail: typeof ContactDetail; ContactDetail: typeof ContactDetail;
ContactModal: typeof ContactModal; ContactModal: typeof ContactModal;
DisappearingTimeDialog: typeof DisappearingTimeDialog;
ErrorModal: typeof ErrorModal; ErrorModal: typeof ErrorModal;
Lightbox: typeof Lightbox; Lightbox: typeof Lightbox;
LightboxGallery: typeof LightboxGallery; LightboxGallery: typeof LightboxGallery;
@ -428,7 +427,7 @@ declare global {
ProgressModal: typeof ProgressModal; ProgressModal: typeof ProgressModal;
Quote: typeof Quote; Quote: typeof Quote;
StagedLinkPreview: typeof StagedLinkPreview; StagedLinkPreview: typeof StagedLinkPreview;
DisappearingTimeDialog: typeof DisappearingTimeDialog; WhatsNew: typeof WhatsNew;
}; };
OS: typeof OS; OS: typeof OS;
Workflow: { Workflow: {

View File

@ -39,6 +39,7 @@ installSetting('typingIndicatorSetting', {
installSetting('alwaysRelayCalls'); installSetting('alwaysRelayCalls');
installSetting('audioNotification'); installSetting('audioNotification');
installSetting('autoDownloadUpdate');
installSetting('autoLaunch'); installSetting('autoLaunch');
installSetting('countMutedConversations'); installSetting('countMutedConversations');
installSetting('callRingtoneNotification'); installSetting('callRingtoneNotification');

View File

@ -45,6 +45,7 @@ window.getVersion = () => String(config.version);
window.i18n = i18n.setup(locale, localeMessages); window.i18n = i18n.setup(locale, localeMessages);
const settingAudioNotification = createSetting('audioNotification'); const settingAudioNotification = createSetting('audioNotification');
const settingAutoDownloadUpdate = createSetting('autoDownloadUpdate');
const settingAutoLaunch = createSetting('autoLaunch'); const settingAutoLaunch = createSetting('autoLaunch');
const settingCallRingtoneNotification = createSetting( const settingCallRingtoneNotification = createSetting(
'callRingtoneNotification' 'callRingtoneNotification'
@ -166,6 +167,7 @@ async function renderPreferences() {
blockedCount, blockedCount,
deviceName, deviceName,
hasAudioNotifications, hasAudioNotifications,
hasAutoDownloadUpdate,
hasAutoLaunch, hasAutoLaunch,
hasCallNotifications, hasCallNotifications,
hasCallRingtoneNotification, hasCallRingtoneNotification,
@ -201,6 +203,7 @@ async function renderPreferences() {
blockedCount: settingBlockedCount.getValue(), blockedCount: settingBlockedCount.getValue(),
deviceName: settingDeviceName.getValue(), deviceName: settingDeviceName.getValue(),
hasAudioNotifications: settingAudioNotification.getValue(), hasAudioNotifications: settingAudioNotification.getValue(),
hasAutoDownloadUpdate: settingAutoDownloadUpdate.getValue(),
hasAutoLaunch: settingAutoLaunch.getValue(), hasAutoLaunch: settingAutoLaunch.getValue(),
hasCallNotifications: settingCallSystemNotification.getValue(), hasCallNotifications: settingCallSystemNotification.getValue(),
hasCallRingtoneNotification: settingCallRingtoneNotification.getValue(), hasCallRingtoneNotification: settingCallRingtoneNotification.getValue(),
@ -256,6 +259,7 @@ async function renderPreferences() {
defaultConversationColor, defaultConversationColor,
deviceName, deviceName,
hasAudioNotifications, hasAudioNotifications,
hasAutoDownloadUpdate,
hasAutoLaunch, hasAutoLaunch,
hasCallNotifications, hasCallNotifications,
hasCallRingtoneNotification, hasCallRingtoneNotification,
@ -310,6 +314,7 @@ async function renderPreferences() {
// Change handlers // Change handlers
onAudioNotificationsChange: reRender(settingAudioNotification.setValue), onAudioNotificationsChange: reRender(settingAudioNotification.setValue),
onAutoDownloadUpdateChange: reRender(settingAutoDownloadUpdate.setValue),
onAutoLaunchChange: reRender(settingAutoLaunch.setValue), onAutoLaunchChange: reRender(settingAutoLaunch.setValue),
onCallNotificationsChange: reRender(settingCallSystemNotification.setValue), onCallNotificationsChange: reRender(settingCallSystemNotification.setValue),
onCallRingtoneNotificationChange: reRender( onCallRingtoneNotificationChange: reRender(