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",
"description": "One of the menu options available in the Avatar Popup menu"
},
"avatarMenuUpdateAvailable": {
"message": "Update Signal",
"description": "One of the menu options available in the Avatar Popup menu"
},
"loading": {
"message": "Loading...",
"description": "Message shown on the loading screen before we've loaded any messages"
@ -640,15 +644,15 @@
"description": "Displayed when the desktop client cannot connect to the server."
},
"connecting": {
"message": "Connecting",
"message": "Connecting...",
"description": "Displayed when the desktop client is currently connecting to the server."
},
"connect": {
"message": "Connect",
"message": "Click to reconnect.",
"description": "Shown to allow the user to manually attempt a reconnect."
},
"connectingHangOn": {
"message": "Shouldn't be long...",
"message": "Shouldn't be long",
"description": "Subtext description for when the client is connecting to the server."
},
"offline": {
@ -796,6 +800,20 @@
"welcomeToSignal": {
"message": "Welcome to Signal"
},
"whatsNew": {
"message": "See $whatsNew$ in this update",
"description": "Shown in the main window",
"placeholders": {
"name": {
"content": "$1",
"example": "what's new"
}
}
},
"viewReleaseNotes": {
"message": "what's new",
"description": "Clickable link that displays the latest release notes"
},
"selectAContact": {
"message": "Select a contact or group to start chatting."
},
@ -1791,7 +1809,7 @@
"description": "Warning notification that this version of the app has expired"
},
"upgrade": {
"message": "Upgrade",
"message": "Click to go to signal.org/download",
"description": "Label text for button to upgrade the app to the latest version"
},
"mediaMessage": {
@ -2210,10 +2228,13 @@
"message": "Relink"
},
"autoUpdateNewVersionTitle": {
"message": "Signal update available"
"message": "Update available"
},
"autoUpdateNewVersionMessage": {
"message": "There is a new version of Signal available."
"message": "Click to restart Signal"
},
"downloadNewVersionMessage": {
"message": "Click to download update"
},
"autoUpdateNewVersionInstructions": {
"message": "Press Restart Signal to apply the updates."
@ -6091,5 +6112,43 @@
"Preferences--typing-indicators": {
"message": "Typing indicators",
"description": "Label for the typing indicators setting"
},
"Preferences--updates": {
"message": "Updates",
"description": "Header for settings having to do with updates"
},
"Preferences__download-update": {
"message": "Automatically download updates",
"description": "Label for checkbox for the auto download updates setting"
},
"DialogUpdate--version-available": {
"message": "Update to version $version$ available",
"description": "Tooltip for new update available",
"placeholders": {
"status": {
"content": "$1",
"example": "v7.7.7"
}
}
},
"WhatsNew__v5.15--1": {
"message": "No that's not speck of dust you need to flick off your monitor, there's now a dot for unplayed incoming audio messages.",
"description": "Release notes for v5.15"
},
"WhatsNew__v5.15--2": {
"message": "The calling lobby got some remodeling and renovations done and we didn't even have to refinance.",
"description": "Release notes for v5.15"
},
"WhatsNew__v5.15--3": {
"message": "The new preferences window is better and faster. Go ahead and change your zoom level, toggle the theme, set a custom disappearing timer.",
"description": "Release notes for v5.15"
},
"WhatsNew__v5.15--4": {
"message": "You can now choose when to download and apply new updates for Signal. The dialogs got a small makeover too. Check out the setting in the new preferences window.",
"description": "Release notes for v5.15"
},
"WhatsNew__v5.15--5": {
"message": "Squashed lots of bugs and there are some performance improvements as well. Thank you all for your reports!",
"description": "Release notes for v5.15"
}
}

View File

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

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

View File

@ -93,6 +93,8 @@
model: { window: options.window },
});
this.renderWhatsNew();
Whisper.events.on('refreshConversation', ({ oldId, newId }) => {
const convo = this.conversation_stack.lastConversation;
if (convo && convo.get('id') === oldId) {
@ -153,6 +155,18 @@
events: {
click: 'onClick',
},
renderWhatsNew() {
if (this.whatsNewView) {
return;
}
this.whatsNewView = new Whisper.ReactWrapperView({
Component: window.Signal.Components.WhatsNew,
props: {
i18n: window.i18n,
},
});
this.$('.whats-new-placeholder').append(this.whatsNewView.el);
},
setupLeftPane() {
if (this.leftPaneView) {
return;

View File

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

View File

@ -3683,6 +3683,21 @@ button.module-conversation-details__action-button {
&__avatar {
-webkit-app-region: no-drag;
&--container {
position: relative;
}
&--badged {
background: $color-ultramarine;
border-radius: 100%;
border: 1px solid $color-white;
height: 8px;
width: 8px;
position: absolute;
top: 0;
right: 0;
}
}
&__search {
@ -7781,79 +7796,6 @@ button.module-image__border-overlay:focus {
}
}
.module-left-pane-dialog {
background: $color-accent-green;
color: $color-white;
padding: 16px;
.module-left-pane-dialog__message {
h3 {
@include font-body-1-bold;
padding: 0px;
margin: 0px;
margin-bottom: 8px;
}
span {
@include font-body-1;
display: inline-block;
}
}
.module-left-pane-dialog__actions {
margin-top: 8px;
text-align: right;
.module-left-pane-dialog__link {
@include keyboard-mode {
display: inline-block;
outline: 0;
}
}
button {
background: inherit;
border-radius: 20px;
border: solid 1px $color-white;
color: $color-white;
cursor: pointer;
font-family: inherit;
margin: 0 4px;
padding: 8px 16px;
outline: 0;
&:focus {
@include keyboard-mode {
box-shadow: 0 0 0 3px $color-ultramarine;
}
}
&:hover {
@include mouse-mode {
box-shadow: 0 0 0 3px $color-ultramarine;
}
}
}
.module-left-pane-dialog__button--no-border {
border: none;
}
}
&.module-left-pane-dialog--error {
background-color: $color-accent-red;
}
&.module-left-pane-dialog--warning {
background-color: $color-accent-yellow;
color: $color-black;
button {
border-color: $color-black;
color: $color-black;
}
}
}
// Module: Emoji Picker
%module-emoji-picker--ribbon {
@ -8740,6 +8682,15 @@ button.module-image__border-overlay:focus {
height: 16px;
width: 16px;
&--update {
@include light-theme {
@include color-svg('../images/icons/v2/refresh-24.svg', $color-gray-75);
}
@include dark-theme {
@include color-svg('../images/icons/v2/refresh-24.svg', $color-gray-15);
}
}
}
.module-avatar-popup__item__icon-settings {
@include light-theme {
@ -8771,9 +8722,18 @@ button.module-image__border-overlay:focus {
}
.module-avatar-popup__item__text {
flex-grow: 1;
margin-left: 8px;
}
.module-avatar-popup__item--badge {
background: $color-ultramarine;
border-radius: 100%;
height: 8px;
margin-right: 10px;
width: 8px;
}
// Module: Shortcut Guide Modal
.module-shortcut-guide-modal {

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/IncomingCallBar.scss';
@import './components/Input.scss';
@import './components/LeftPaneDialog.scss';
@import './components/MediaQualitySelector.scss';
@import './components/MessageAudio.scss';
@import './components/MessageDetail.scss';
@ -78,3 +79,4 @@
@import './components/Tabs.scss';
@import './components/TimelineWarning.scss';
@import './components/TimelineWarnings.scss';
@import './components/WhatsNew.scss';

View File

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

View File

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

View File

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

View File

@ -12,6 +12,9 @@ import { LocalizerType } from '../types/Util';
export type Props = {
readonly i18n: LocalizerType;
hasPendingUpdate: boolean;
startUpdate: () => unknown;
onEditProfile: () => unknown;
onViewPreferences: () => unknown;
onViewArchive: () => unknown;
@ -23,15 +26,17 @@ export type Props = {
export const AvatarPopup = (props: Props): JSX.Element => {
const {
hasPendingUpdate,
i18n,
name,
profileName,
phoneNumber,
title,
onEditProfile,
onViewPreferences,
onViewArchive,
onViewPreferences,
phoneNumber,
profileName,
startUpdate,
style,
title,
} = props;
const shouldShowNumber = Boolean(name || profileName);
@ -92,6 +97,24 @@ export const AvatarPopup = (props: Props): JSX.Element => {
{i18n('avatarMenuViewArchive')}
</div>
</button>
{hasPendingUpdate && (
<button
type="button"
className="module-avatar-popup__item"
onClick={startUpdate}
>
<div
className={classNames(
'module-avatar-popup__item__icon',
'module-avatar-popup__item__icon--update'
)}
/>
<div className="module-avatar-popup__item__text">
{i18n('avatarMenuUpdateAvailable')}
</div>
<div className="module-avatar-popup__item--badge" />
</button>
)}
</div>
);
};

View File

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

View File

@ -10,7 +10,7 @@ type PropsType = {
i18n: LocalizerType;
};
export const ExpiredBuildDialog = ({
export const DialogExpiredBuild = ({
hasExpired,
i18n,
}: PropsType): JSX.Element | null => {
@ -19,19 +19,17 @@ export const ExpiredBuildDialog = ({
}
return (
<div className="module-left-pane-dialog module-left-pane-dialog--error">
{i18n('expiredWarning')}
<div className="module-left-pane-dialog__actions">
<div className="LeftPaneDialog LeftPaneDialog--error">
<div className="LeftPaneDialog__message">
{i18n('expiredWarning')}{' '}
<a
className="module-left-pane-dialog__link"
className="LeftPaneDialog__action-text"
href="https://signal.org/download/"
rel="noreferrer"
tabIndex={-1}
target="_blank"
>
<button type="button" className="upgrade">
{i18n('upgrade')}
</button>
{i18n('upgrade')}
</a>
</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 { Spinner } from './Spinner';
import { LocalizerType } from '../types/Util';
import { SocketStatus } from '../types/SocketStatus';
import { NetworkStateType } from '../state/ducks/network';
@ -16,28 +17,42 @@ export type PropsType = NetworkStateType & {
};
type RenderDialogTypes = {
isConnecting?: boolean;
title: string;
subtext: string;
renderActionableButton?: () => JSX.Element;
};
function renderDialog({
isConnecting,
title,
subtext,
renderActionableButton,
}: RenderDialogTypes): JSX.Element {
return (
<div className="module-left-pane-dialog module-left-pane-dialog--warning">
<div className="module-left-pane-dialog__message">
<div className="LeftPaneDialog LeftPaneDialog--warning">
{isConnecting ? (
<div className="LeftPaneDialog__spinner-container">
<Spinner
direction="on-avatar"
moduleClassName="LeftPaneDialog__spinner"
size="22px"
svgSize="small"
/>
</div>
) : (
<div className="LeftPaneDialog__icon LeftPaneDialog__icon--network" />
)}
<div className="LeftPaneDialog__message">
<h3>{title}</h3>
<span>{subtext}</span>
<div>{renderActionableButton && renderActionableButton()}</div>
</div>
{renderActionableButton && renderActionableButton()}
</div>
);
}
export const NetworkStatus = ({
export const DialogNetworkStatus = ({
hasNetworkDialog,
i18n,
isOnline,
@ -75,19 +90,23 @@ export const NetworkStatus = ({
};
const manualReconnectButton = (): JSX.Element => (
<div className="module-left-pane-dialog__actions">
<button onClick={reconnect} type="button">
{i18n('connect')}
</button>
</div>
<button
className="LeftPaneDialog__action-text"
onClick={reconnect}
type="button"
>
{i18n('connect')}
</button>
);
if (isConnecting) {
return renderDialog({
isConnecting: true,
subtext: i18n('connectingHangOn'),
title: i18n('connecting'),
});
}
if (!isOnline) {
return renderDialog({
renderActionableButton: manualReconnectButton,
@ -114,6 +133,7 @@ export const NetworkStatus = ({
}
return renderDialog({
isConnecting: socketStatus === SocketStatus.CONNECTING,
renderActionableButton,
subtext,
title,

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

View File

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

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

View File

@ -43,6 +43,7 @@ export type PropsType = {
defaultConversationColor: DefaultConversationColorType;
deviceName?: string;
hasAudioNotifications?: boolean;
hasAutoDownloadUpdate: boolean;
hasAutoLaunch: boolean;
hasCallNotifications: boolean;
hasCallRingtoneNotification: boolean;
@ -104,6 +105,7 @@ export type PropsType = {
// Change handlers
onAudioNotificationsChange: CheckboxChangeHandlerType;
onAutoDownloadUpdateChange: CheckboxChangeHandlerType;
onAutoLaunchChange: CheckboxChangeHandlerType;
onCallNotificationsChange: CheckboxChangeHandlerType;
onCallRingtoneNotificationChange: CheckboxChangeHandlerType;
@ -161,6 +163,7 @@ export const Preferences = ({
editCustomColor,
getConversationsWithCustomColor,
hasAudioNotifications,
hasAutoDownloadUpdate,
hasAutoLaunch,
hasCallNotifications,
hasCallRingtoneNotification,
@ -191,6 +194,7 @@ export const Preferences = ({
makeSyncRequest,
notificationContent,
onAudioNotificationsChange,
onAutoDownloadUpdateChange,
onAutoLaunchChange,
onCallNotificationsChange,
onCallRingtoneNotificationChange,
@ -340,6 +344,15 @@ export const Preferences = ({
onChange={onMediaCameraPermissionsChange}
/>
</SettingsRow>
<SettingsRow title={i18n('Preferences--updates')}>
<Checkbox
checked={hasAutoDownloadUpdate}
label={i18n('Preferences__download-update')}
moduleClassName="Preferences__checkbox"
name="autoDownloadUpdate"
onChange={onAutoDownloadUpdateChange}
/>
</SettingsRow>
</>
);
} else if (page === Page.Appearance) {

View File

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

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,
});
this.installSetting('autoDownloadUpdate');
this.installSetting('autoLaunch');
this.installSetting('alwaysRelayCalls');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import {
} from 'fs';
import { join, normalize } from 'path';
import { tmpdir } from 'os';
import { throttle } from 'lodash';
import { createParser, ParserConfiguration } from 'dashdash';
import ProxyAgent from 'proxy-agent';
@ -20,10 +21,10 @@ import { v4 as getGuid } from 'uuid';
import pify from 'pify';
import mkdirp from 'mkdirp';
import rimraf from 'rimraf';
import { app, BrowserWindow, dialog, ipcMain } from 'electron';
import { app, BrowserWindow, ipcMain } from 'electron';
import { getTempPath } from '../../app/attachments';
import { Dialogs } from '../types/Dialogs';
import { DialogType } from '../types/Dialogs';
import { getUserAgent } from '../util/getUserAgent';
import { isAlpha, isBeta } from '../util/version';
@ -31,7 +32,6 @@ import * as packageJson from '../../package.json';
import { getSignatureFileName } from './signature';
import { isPathInside } from '../util/isPathInside';
import { LocaleType } from '../types/I18N';
import { LoggerType } from '../types/Logging';
const writeFile = pify(writeFileCallback);
@ -39,24 +39,40 @@ const mkdirpPromise = pify(mkdirp);
const rimrafPromise = pify(rimraf);
const { platform } = process;
export const ACK_RENDER_TIMEOUT = 10000;
export const GOT_CONNECT_TIMEOUT = 2 * 60 * 1000;
export const GOT_LOOKUP_TIMEOUT = 2 * 60 * 1000;
export const GOT_SOCKET_TIMEOUT = 2 * 60 * 1000;
type JSONUpdateSchema = {
version: string;
files: Array<{
url: string;
sha512: string;
size: string;
blockMapSize?: string;
}>;
path: string;
sha512: string;
releaseDate: string;
};
export type UpdaterInterface = {
force(): Promise<void>;
};
export type UpdateInformationType = {
fileName: string;
size: number;
version: string;
};
export async function checkForUpdates(
logger: LoggerType,
forceUpdate = false
): Promise<{
fileName: string;
version: string;
} | null> {
): Promise<UpdateInformationType | null> {
const yaml = await getUpdateYaml();
const version = getVersion(yaml);
const parsedYaml = parseYaml(yaml);
const version = getVersion(parsedYaml);
if (!version) {
logger.warn('checkForUpdates: no version extracted from downloaded yaml');
@ -70,8 +86,11 @@ export async function checkForUpdates(
`forceUpdate=${forceUpdate}`
);
const fileName = getUpdateFileName(parsedYaml);
return {
fileName: getUpdateFileName(yaml),
fileName,
size: getSize(parsedYaml, fileName),
version,
};
}
@ -95,7 +114,8 @@ export function validatePath(basePath: string, targetPath: string): void {
export async function downloadUpdate(
fileName: string,
logger: LoggerType
logger: LoggerType,
mainWindow?: BrowserWindow
): Promise<string> {
const baseUrl = getUpdatesBase();
const updateFileUrl = `${baseUrl}/${fileName}`;
@ -121,6 +141,23 @@ export async function downloadUpdate(
const writeStream = createWriteStream(targetUpdatePath);
await new Promise<void>((resolve, reject) => {
if (mainWindow) {
let downloadedSize = 0;
const throttledSend = throttle(() => {
mainWindow.webContents.send(
'show-update-dialog',
DialogType.Downloading,
{ downloadedSize }
);
}, 500);
downloadStream.on('data', data => {
downloadedSize += data.length;
throttledSend();
});
}
downloadStream.on('error', error => {
reject(error);
});
@ -144,106 +181,6 @@ export async function downloadUpdate(
}
}
let showingUpdateDialog = false;
async function showFallbackUpdateDialog(
mainWindow: BrowserWindow,
locale: LocaleType
): Promise<boolean> {
if (showingUpdateDialog) {
return false;
}
const RESTART_BUTTON = 0;
const LATER_BUTTON = 1;
const options = {
type: 'info',
buttons: [
locale.messages.autoUpdateRestartButtonLabel.message,
locale.messages.autoUpdateLaterButtonLabel.message,
],
title: locale.messages.autoUpdateNewVersionTitle.message,
message: locale.messages.autoUpdateNewVersionMessage.message,
detail: locale.messages.autoUpdateNewVersionInstructions.message,
defaultId: LATER_BUTTON,
cancelId: LATER_BUTTON,
};
showingUpdateDialog = true;
const { response } = await dialog.showMessageBox(mainWindow, options);
showingUpdateDialog = false;
return response === RESTART_BUTTON;
}
export function showUpdateDialog(
mainWindow: BrowserWindow,
locale: LocaleType,
performUpdateCallback: () => void
): void {
let ack = false;
ipcMain.once('show-update-dialog-ack', () => {
ack = true;
});
mainWindow.webContents.send('show-update-dialog', Dialogs.Update);
setTimeout(async () => {
if (!ack) {
const shouldUpdate = await showFallbackUpdateDialog(mainWindow, locale);
if (shouldUpdate) {
performUpdateCallback();
}
}
}, ACK_RENDER_TIMEOUT);
}
let showingCannotUpdateDialog = false;
async function showFallbackCannotUpdateDialog(
mainWindow: BrowserWindow,
locale: LocaleType
): Promise<void> {
if (showingCannotUpdateDialog) {
return;
}
const options = {
type: 'error',
buttons: [locale.messages.ok.message],
title: locale.messages.cannotUpdate.message,
message: locale.i18n('cannotUpdateDetail', ['https://signal.org/download']),
};
showingCannotUpdateDialog = true;
await dialog.showMessageBox(mainWindow, options);
showingCannotUpdateDialog = false;
}
export function showCannotUpdateDialog(
mainWindow: BrowserWindow,
locale: LocaleType
): void {
let ack = false;
ipcMain.once('show-update-dialog-ack', () => {
ack = true;
});
mainWindow.webContents.send('show-update-dialog', Dialogs.Cannot_Update);
setTimeout(async () => {
if (!ack) {
await showFallbackCannotUpdateDialog(mainWindow, locale);
}
}, ACK_RENDER_TIMEOUT);
}
// Helper functions
export function getUpdateCheckUrl(): string {
@ -288,9 +225,7 @@ function isVersionNewer(newVersion: string): boolean {
return gt(newVersion, version);
}
export function getVersion(yaml: string): string | null {
const info = parseYaml(yaml);
export function getVersion(info: JSONUpdateSchema): string | null {
return info && info.version;
}
@ -299,11 +234,7 @@ export function isUpdateFileNameValid(name: string): boolean {
return validFile.test(name);
}
// Reliant on third party parser that returns any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function getUpdateFileName(yaml: string): any {
const info = parseYaml(yaml);
export function getUpdateFileName(info: JSONUpdateSchema): string {
if (!info || !info.path) {
throw new Error('getUpdateFileName: No path present in YAML file');
}
@ -318,9 +249,17 @@ export function getUpdateFileName(yaml: string): any {
return path;
}
// Reliant on third party parser that returns any
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function parseYaml(yaml: string): any {
function getSize(info: JSONUpdateSchema, fileName: string): number {
if (!info || !info.files) {
throw new Error('getUpdateFileName: No files present in YAML file');
}
const foundFile = info.files.find(file => file.url === fileName);
return Number(foundFile?.size) || 0;
}
export function parseYaml(yaml: string): JSONUpdateSchema {
return safeLoad(yaml, { schema: FAILSAFE_SCHEMA, json: true });
}
@ -413,3 +352,21 @@ export function getCliOptions<T>(options: ParserConfiguration['options']): T {
export function setUpdateListener(performUpdateCallback: () => void): void {
ipcMain.once('start-update', performUpdateCallback);
}
export function getAutoDownloadUpdateSetting(
mainWindow: BrowserWindow
): Promise<boolean> {
return new Promise((resolve, reject) => {
ipcMain.once(
'settings:get-success:autoDownloadUpdate',
(_, error, value: boolean) => {
if (error) {
reject(error);
} else {
resolve(value);
}
}
);
mainWindow.webContents.send('settings:get:autoDownloadUpdate');
});
}

View File

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

View File

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

View File

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

View File

@ -431,6 +431,13 @@
"reasonCategory": "usageTrusted",
"updated": "2021-06-15T23:46:51.629Z"
},
{
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewView.el);",
"reasonCategory": "usageTrusted",
"updated": "2021-08-17T01:37:13.116Z"
},
{
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
@ -439,6 +446,13 @@
"updated": "2021-02-26T18:44:56.450Z",
"reasonDetail": "Adding sub-view to DOM"
},
{
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
"line": " this.$('.whats-new-placeholder').append(this.whatsNewView.el);",
"reasonCategory": "usageTrusted",
"updated": "2021-08-17T01:37:13.116Z"
},
{
"rule": "jQuery-appendTo(",
"path": "js/views/inbox_view.js",

11
ts/window.d.ts vendored
View File

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

View File

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

View File

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