+
+ {isConnecting ? (
+
+
+
+ ) : (
+
+ )}
+
{title}
{subtext}
+
{renderActionableButton && renderActionableButton()}
- {renderActionableButton && renderActionableButton()}
);
}
-export const NetworkStatus = ({
+export const DialogNetworkStatus = ({
hasNetworkDialog,
i18n,
isOnline,
@@ -75,19 +90,23 @@ export const NetworkStatus = ({
};
const manualReconnectButton = (): JSX.Element => (
-
-
-
+
);
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,
diff --git a/ts/components/DialogUpdate.stories.tsx b/ts/components/DialogUpdate.stories.tsx
new file mode 100644
index 0000000000..66c5d2eacf
--- /dev/null
+++ b/ts/components/DialogUpdate.stories.tsx
@@ -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 (
+
+ );
+});
+
+story.add('Update', () => (
+
+));
+
+story.add('Download Ready', () => (
+
+));
+
+story.add('Downloading', () => (
+
+));
+
+story.add('Cannot Update', () => (
+
+));
+
+story.add('macOS RO Error', () => (
+
+));
diff --git a/ts/components/DialogUpdate.tsx b/ts/components/DialogUpdate.tsx
new file mode 100644
index 0000000000..689c60a32c
--- /dev/null
+++ b/ts/components/DialogUpdate.tsx
@@ -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 (
+
+
+
{i18n('cannotUpdate')}
+
+
+ https://signal.org/download/
+ ,
+ ]}
+ i18n={i18n}
+ id="cannotUpdateDetail"
+ />
+
+
+
+ );
+ }
+
+ if (dialogType === DialogType.MacOS_Read_Only) {
+ return (
+
+
+
+
{i18n('cannotUpdate')}
+
+ Signal.app,
+ folder: /Applications,
+ }}
+ i18n={i18n}
+ id="readOnlyVolume"
+ />
+
+
+
+
+
+
+
+ );
+ }
+
+ 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 = (
+
+ );
+ } else if (dialogType === DialogType.Downloading) {
+ const width = Math.ceil(
+ ((downloadedSize || 1) / (downloadSize || 1)) * 100
+ );
+
+ updateSubText = (
+
+ );
+ } else {
+ updateSubText = (
+
+ );
+ }
+
+ const versionTitle = version
+ ? i18n('DialogUpdate--version-available', [version])
+ : undefined;
+
+ return (
+
+
+
+
+
+ {i18n('autoUpdateNewVersionTitle')} {size}
+
+ {updateSubText}
+
+
+
+ {dialogType !== DialogType.Downloading && (
+
+ )}
+
+
+ );
+};
diff --git a/ts/components/MainHeader.stories.tsx b/ts/components/MainHeader.stories.tsx
index f43dd8b392..f3fed291df 100644
--- a/ts/components/MainHeader.stories.tsx
+++ b/ts/components/MainHeader.stories.tsx
@@ -45,6 +45,7 @@ const createProps = (overrideProps: Partial
= {}): 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 => ({
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 ;
});
+
+story.add('Update Available', () => {
+ const props = createProps({ hasPendingUpdate: true });
+
+ return ;
+});
diff --git a/ts/components/MainHeader.tsx b/ts/components/MainHeader.tsx
index 0b72ae1aa3..d3923aeed1 100644
--- a/ts/components/MainHeader.tsx
+++ b/ts/components/MainHeader.tsx
@@ -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 {
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 {
{({ ref }) => (
- ` needs it
- // to determine blurring.
- sharedGroupNames={[]}
- size={28}
- innerRef={ref}
- onClick={this.showAvatarPopup}
- />
+
+
` needs it to determine blurring.
+ sharedGroupNames={[]}
+ size={28}
+ innerRef={ref}
+ onClick={this.showAvatarPopup}
+ />
+ {hasPendingUpdate && (
+
+ )}
+
)}
{showingAvatarPopup && popperRoot
@@ -408,6 +417,8 @@ export class MainHeader extends React.Component {
title={title}
avatarPath={avatarPath}
size={28}
+ hasPendingUpdate={hasPendingUpdate}
+ startUpdate={startUpdate}
// See the comment above about `sharedGroupNames`.
sharedGroupNames={[]}
onEditProfile={() => {
diff --git a/ts/components/NetworkStatus.stories.tsx b/ts/components/NetworkStatus.stories.tsx
deleted file mode 100644
index e8e0ed71fa..0000000000
--- a/ts/components/NetworkStatus.stories.tsx
+++ /dev/null
@@ -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 (
-
- );
- })
- .add('Iterations', () => {
- return permutations.map(({ props, title }) => (
- <>
- {title}
-
- >
- ));
- });
diff --git a/ts/components/Preferences.stories.tsx b/ts/components/Preferences.stories.tsx
index d52e7f267c..43d41b1b66 100644
--- a/ts/components/Preferences.stories.tsx
+++ b/ts/components/Preferences.stories.tsx
@@ -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'),
diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx
index fba133f2e4..04c643ccf3 100644
--- a/ts/components/Preferences.tsx
+++ b/ts/components/Preferences.tsx
@@ -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}
/>
+
+
+
>
);
} else if (page === Page.Appearance) {
diff --git a/ts/components/RelinkDialog.tsx b/ts/components/RelinkDialog.tsx
index 3005657a07..9d2af3fcd3 100644
--- a/ts/components/RelinkDialog.tsx
+++ b/ts/components/RelinkDialog.tsx
@@ -21,12 +21,12 @@ export const RelinkDialog = ({
}
return (
-
-
+
+
{i18n('unlinked')}
{i18n('unlinkedWarning')}
-
+
diff --git a/ts/components/UpdateDialog.stories.tsx b/ts/components/UpdateDialog.stories.tsx
deleted file mode 100644
index 57932c9b70..0000000000
--- a/ts/components/UpdateDialog.stories.tsx
+++ /dev/null
@@ -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 (
-
- );
- })
- .add('Iterations', () => {
- return permutations.map(({ props, title }) => (
- <>
-
{title}
-
- >
- ));
- });
diff --git a/ts/components/UpdateDialog.tsx b/ts/components/UpdateDialog.tsx
deleted file mode 100644
index ec5ed7f704..0000000000
--- a/ts/components/UpdateDialog.tsx
+++ /dev/null
@@ -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 (
-
-
-
{i18n('cannotUpdate')}
-
-
- https://signal.org/download/
- ,
- ]}
- i18n={i18n}
- id="cannotUpdateDetail"
- />
-
-
-
- );
- }
-
- if (dialogType === Dialogs.MacOS_Read_Only) {
- return (
-
-
-
{i18n('cannotUpdate')}
-
- Signal.app,
- folder: /Applications,
- }}
- i18n={i18n}
- id="readOnlyVolume"
- />
-
-
-
-
-
-
- );
- }
-
- return (
-
-
-
{i18n('autoUpdateNewVersionTitle')}
- {i18n('autoUpdateNewVersionMessage')}
-
-
- {!didSnooze && (
-
- )}
-
-
-
- );
-};
diff --git a/ts/components/WhatsNew.tsx b/ts/components/WhatsNew.tsx
new file mode 100644
index 0000000000..2005b916dc
--- /dev/null
+++ b/ts/components/WhatsNew.tsx
@@ -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
;
+};
+
+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 && (
+ setReleaseNotes(undefined)}
+ title={i18n('WhatsNew__modal-title')}
+ >
+ <>
+
+ {moment(releaseNotes.date).format('LL')} ·{' '}
+ {releaseNotes.version}
+
+
+ {releaseNotes.features.map(featureKey => (
+ -
+
+
+ ))}
+
+ >
+
+ )}
+
+ {i18n('viewReleaseNotes')}
+ ,
+ ]}
+ />
+ >
+ );
+};
diff --git a/ts/main/settingsChannel.ts b/ts/main/settingsChannel.ts
index 304b2ad64f..0805536699 100644
--- a/ts/main/settingsChannel.ts
+++ b/ts/main/settingsChannel.ts
@@ -61,6 +61,7 @@ export class SettingsChannel {
isEphemeral: true,
});
+ this.installSetting('autoDownloadUpdate');
this.installSetting('autoLaunch');
this.installSetting('alwaysRelayCalls');
diff --git a/ts/services/updateListener.ts b/ts/services/updateListener.ts
index 57f34e4a27..3b2ddf63c7 100644
--- a/ts/services/updateListener.ts
+++ b/ts/services/updateListener.ts
@@ -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);
+ }
+ );
}
diff --git a/ts/shims/updateIpc.ts b/ts/shims/updateIpc.ts
index a0cceb22cc..6b33a4f659 100644
--- a/ts/shims/updateIpc.ts
+++ b/ts/shims/updateIpc.ts
@@ -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');
-}
diff --git a/ts/state/ducks/updates.ts b/ts/state/ducks/updates.ts
index 9bd20446ff..46f7c15b0a 100644
--- a/ts/state/ducks/updates.ts
+++ b/ts/state/ducks/updates.ts
@@ -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
): 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,
};
}
diff --git a/ts/state/smart/ExpiredBuildDialog.tsx b/ts/state/smart/ExpiredBuildDialog.tsx
index 43bc87ae1b..79541a7c3b 100644
--- a/ts/state/smart/ExpiredBuildDialog.tsx
+++ b/ts/state/smart/ExpiredBuildDialog.tsx
@@ -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);
diff --git a/ts/state/smart/MainHeader.tsx b/ts/state/smart/MainHeader.tsx
index f71abdec6b..4fbb965f26 100644
--- a/ts/state/smart/MainHeader.tsx
+++ b/ts/state/smart/MainHeader.tsx
@@ -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),
diff --git a/ts/state/smart/NetworkStatus.tsx b/ts/state/smart/NetworkStatus.tsx
index 6a22b5b17a..521c717505 100644
--- a/ts/state/smart/NetworkStatus.tsx
+++ b/ts/state/smart/NetworkStatus.tsx
@@ -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);
diff --git a/ts/state/smart/UpdateDialog.tsx b/ts/state/smart/UpdateDialog.tsx
index d90ed16882..0605ca1ad6 100644
--- a/ts/state/smart/UpdateDialog.tsx
+++ b/ts/state/smart/UpdateDialog.tsx
@@ -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);
diff --git a/ts/test-node/updater/common_test.ts b/ts/test-node/updater/common_test.ts
index 70db040365..30ba116187 100644
--- a/ts/test-node/updater/common_test.ts
+++ b/ts/test-node/updater/common_test.ts
@@ -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', () => {
diff --git a/ts/types/Dialogs.ts b/ts/types/Dialogs.ts
index 7fd594d4e4..c6f90280fb 100644
--- a/ts/types/Dialogs.ts
+++ b/ts/types/Dialogs.ts
@@ -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',
}
diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts
index 530289d7e8..0d8a5f240c 100644
--- a/ts/types/Storage.d.ts
+++ b/ts/types/Storage.d.ts
@@ -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;
'blocked-uuids': Array;
diff --git a/ts/updater/common.ts b/ts/updater/common.ts
index f5e626e12f..2bd6702fe0 100644
--- a/ts/updater/common.ts
+++ b/ts/updater/common.ts
@@ -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;
};
+export type UpdateInformationType = {
+ fileName: string;
+ size: number;
+ version: string;
+};
+
export async function checkForUpdates(
logger: LoggerType,
forceUpdate = false
-): Promise<{
- fileName: string;
- version: string;
-} | null> {
+): Promise {
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 {
const baseUrl = getUpdatesBase();
const updateFileUrl = `${baseUrl}/${fileName}`;
@@ -121,6 +141,23 @@ export async function downloadUpdate(
const writeStream = createWriteStream(targetUpdatePath);
await new Promise((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 {
- 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 {
- 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(options: ParserConfiguration['options']): T {
export function setUpdateListener(performUpdateCallback: () => void): void {
ipcMain.once('start-update', performUpdateCallback);
}
+
+export function getAutoDownloadUpdateSetting(
+ mainWindow: BrowserWindow
+): Promise {
+ 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');
+ });
+}
diff --git a/ts/updater/index.ts b/ts/updater/index.ts
index 1ab3005aec..dda3ec79f0 100644
--- a/ts/updater/index.ts
+++ b/ts/updater/index.ts
@@ -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 {
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');
}
diff --git a/ts/updater/macos.ts b/ts/updater/macos.ts
index 3e912f4c1e..ea5d16c243 100644
--- a/ts/updater/macos.ts
+++ b/ts/updater/macos.ts
@@ -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 {
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 {
- 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,
+ 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
+ );
+ }
};
}
diff --git a/ts/updater/windows.ts b/ts/updater/windows.ts
index ba4496f7ba..3d401e7e10 100644
--- a/ts/updater/windows.ts
+++ b/ts/updater/windows.ts
@@ -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 {
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 {
- 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,
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();
};
}
diff --git a/ts/util/createIPCEvents.ts b/ts/util/createIPCEvents.ts
index 994d24bd80..bc3db15b30 100644
--- a/ts/util/createIPCEvents.ts
+++ b/ts/util/createIPCEvents.ts
@@ -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',
diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json
index 62ef275af5..1ae15d8759 100644
--- a/ts/util/lint/exceptions.json
+++ b/ts/util/lint/exceptions.json
@@ -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",
diff --git a/ts/window.d.ts b/ts/window.d.ts
index dce2a49b5f..e3b14e76ab 100644
--- a/ts/window.d.ts
+++ b/ts/window.d.ts
@@ -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;
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: {
diff --git a/ts/windows/preload.ts b/ts/windows/preload.ts
index 42a7176440..565dbb4441 100644
--- a/ts/windows/preload.ts
+++ b/ts/windows/preload.ts
@@ -39,6 +39,7 @@ installSetting('typingIndicatorSetting', {
installSetting('alwaysRelayCalls');
installSetting('audioNotification');
+installSetting('autoDownloadUpdate');
installSetting('autoLaunch');
installSetting('countMutedConversations');
installSetting('callRingtoneNotification');
diff --git a/ts/windows/settings/preload.ts b/ts/windows/settings/preload.ts
index c74f0db9b6..057e0eeb83 100644
--- a/ts/windows/settings/preload.ts
+++ b/ts/windows/settings/preload.ts
@@ -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(