Unsupported OS Dialog
This commit is contained in:
parent
c6e184016b
commit
ac50af52d2
44 changed files with 776 additions and 224 deletions
|
@ -2161,7 +2161,8 @@
|
|||
"description": "Shown in update dialog when partial update fails and we have to ask user to download full update"
|
||||
},
|
||||
"autoUpdateNewVersionInstructions": {
|
||||
"message": "Press Restart Signal to apply the updates."
|
||||
"message": "Press Restart Signal to apply the updates.",
|
||||
"description": "(deleted 2023/01/18)"
|
||||
},
|
||||
"autoUpdateRestartButtonLabel": {
|
||||
"message": "Restart Signal"
|
||||
|
@ -6256,6 +6257,18 @@
|
|||
"message": "These digits help keep your username private so you can avoid unwanted messages. Share your username with only the people and groups you’d like to chat with. If you change usernames you’ll get a new set of digits.",
|
||||
"description": "Body of the popup with information about discriminator in username"
|
||||
},
|
||||
"icu:UnsupportedOSWarningDialog__body": {
|
||||
"messageformat": "Signal desktop will no longer support your computer’s version of {OS} soon. To keep using Signal, update your computer’s operating system by {expirationDate}. <learnMoreLink>Learn more</learnMoreLink>",
|
||||
"description": "Body of a dialog displayed on unsupported operating systems"
|
||||
},
|
||||
"icu:UnsupportedOSErrorDialog__body": {
|
||||
"messageformat": "Signal desktop no longer works on this computer. To use Signal desktop again, update your computer’s version of {OS}. <learnMoreLink>Learn more</learnMoreLink>",
|
||||
"description": "Body of a dialog displayed on unsupported operating systems"
|
||||
},
|
||||
"icu:UnsupportedOSErrorToast": {
|
||||
"messageformat": "Signal desktop no longer works on this computer. To use Signal desktop again, update your computer’s version of {OS}.",
|
||||
"description": "Body of a dialog displayed on unsupported operating systems"
|
||||
},
|
||||
"WhatsNew__modal-title": {
|
||||
"message": "What's New",
|
||||
"description": "Title for the whats new modal"
|
||||
|
|
10
package.json
10
package.json
|
@ -336,6 +336,11 @@
|
|||
}
|
||||
],
|
||||
"mergeASARs": true,
|
||||
"releaseInfo": {
|
||||
"vendor": {
|
||||
"minOSVersion": "17.0.0"
|
||||
}
|
||||
},
|
||||
"singleArchFiles": "node_modules/@signalapp/{libsignal-client/prebuilds/**,ringrtc/build/**}",
|
||||
"target": [
|
||||
{
|
||||
|
@ -371,6 +376,11 @@
|
|||
"url": "https://updates.signal.org/desktop"
|
||||
}
|
||||
],
|
||||
"releaseInfo": {
|
||||
"vendor": {
|
||||
"minOSVersion": "6.1.0"
|
||||
}
|
||||
},
|
||||
"target": [
|
||||
"nsis"
|
||||
]
|
||||
|
|
|
@ -11,6 +11,21 @@ index ffcc8bd..bafab0e 100644
|
|||
}
|
||||
}
|
||||
const desktopMeta = {
|
||||
diff --git a/node_modules/app-builder-lib/scheme.json b/node_modules/app-builder-lib/scheme.json
|
||||
index 4ce62d9..e9729c1 100644
|
||||
--- a/node_modules/app-builder-lib/scheme.json
|
||||
+++ b/node_modules/app-builder-lib/scheme.json
|
||||
@@ -4749,6 +4749,10 @@
|
||||
"null",
|
||||
"string"
|
||||
]
|
||||
+ },
|
||||
+ "vendor": {
|
||||
+ "description": "Vendor-specific informaton",
|
||||
+ "type": "object"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
diff --git a/node_modules/app-builder-lib/templates/linux/after-install.tpl b/node_modules/app-builder-lib/templates/linux/after-install.tpl
|
||||
index 1536059..555f8f5 100644
|
||||
--- a/node_modules/app-builder-lib/templates/linux/after-install.tpl
|
||||
|
@ -25,7 +40,7 @@ index 1536059..555f8f5 100644
|
|||
update-mime-database /usr/share/mime || true
|
||||
update-desktop-database /usr/share/applications || true
|
||||
diff --git a/node_modules/app-builder-lib/templates/nsis/messages.yml b/node_modules/app-builder-lib/templates/nsis/messages.yml
|
||||
index 6527c99..695444c 100644
|
||||
index 87fa6b5..ad560bd 100644
|
||||
--- a/node_modules/app-builder-lib/templates/nsis/messages.yml
|
||||
+++ b/node_modules/app-builder-lib/templates/nsis/messages.yml
|
||||
@@ -45,7 +45,7 @@ x64WinRequired:
|
||||
|
|
|
@ -168,6 +168,8 @@
|
|||
|
||||
&__message,
|
||||
&__tooltip {
|
||||
max-width: 250px;
|
||||
|
||||
h3 {
|
||||
@include font-body-1-bold;
|
||||
padding: 0px;
|
||||
|
@ -187,6 +189,10 @@
|
|||
&--error {
|
||||
background-color: $error-background-color;
|
||||
color: $error-text-color;
|
||||
|
||||
a {
|
||||
color: $error-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
&--warning {
|
||||
|
@ -253,6 +259,10 @@
|
|||
&--error {
|
||||
--tooltip-text-color: #{$error-text-color};
|
||||
--tooltip-background-color: #{$error-background-color};
|
||||
|
||||
a {
|
||||
color: #{$error-text-color};
|
||||
}
|
||||
}
|
||||
|
||||
&--warning {
|
||||
|
|
37
ts/OS.ts
37
ts/OS.ts
|
@ -1,21 +1,38 @@
|
|||
// Copyright 2018 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import os from 'os';
|
||||
import { release as osRelease } from 'os';
|
||||
import semver from 'semver';
|
||||
|
||||
export const isMacOS = (): boolean => process.platform === 'darwin';
|
||||
export const isLinux = (): boolean => process.platform === 'linux';
|
||||
export const isWindows = (minVersion?: string): boolean => {
|
||||
const osRelease = os.release();
|
||||
const createIsPlatform = (
|
||||
platform: typeof process.platform
|
||||
): ((minVersion?: string) => boolean) => {
|
||||
return minVersion => {
|
||||
if (process.platform !== platform) {
|
||||
return false;
|
||||
}
|
||||
if (minVersion === undefined) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return minVersion === undefined ? true : semver.gte(osRelease, minVersion);
|
||||
return semver.gte(osRelease(), minVersion);
|
||||
};
|
||||
};
|
||||
|
||||
export const isMacOS = createIsPlatform('darwin');
|
||||
export const isLinux = createIsPlatform('linux');
|
||||
export const isWindows = createIsPlatform('win32');
|
||||
|
||||
// Windows 10 and above
|
||||
export const hasCustomTitleBar = (): boolean =>
|
||||
isWindows('10.0.0') || Boolean(process.env.CUSTOM_TITLEBAR);
|
||||
|
||||
export const getName = (): string => {
|
||||
if (isMacOS()) {
|
||||
return 'macOS';
|
||||
}
|
||||
if (isWindows()) {
|
||||
return 'Windows';
|
||||
}
|
||||
return 'Linux';
|
||||
};
|
||||
|
|
|
@ -1054,7 +1054,7 @@ export async function startApp(): Promise<void> {
|
|||
);
|
||||
} finally {
|
||||
initializeRedux({ mainWindowStats, menuOptions });
|
||||
void start();
|
||||
drop(start());
|
||||
window.Signal.Services.initializeNetworkObserver(
|
||||
window.reduxActions.network
|
||||
);
|
||||
|
@ -1070,7 +1070,7 @@ export async function startApp(): Promise<void> {
|
|||
window.getSfuUrl()
|
||||
);
|
||||
window.reduxActions.expiration.hydrateExpirationStatus(
|
||||
window.Signal.Util.hasExpired()
|
||||
window.getBuildExpiration()
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -1958,9 +1958,6 @@ export async function startApp(): Promise<void> {
|
|||
remoteBuildExpirationTimestamp
|
||||
)
|
||||
);
|
||||
window.reduxActions.expiration.hydrateExpirationStatus(
|
||||
window.Signal.Util.hasExpired()
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
@ -2143,9 +2140,6 @@ export async function startApp(): Promise<void> {
|
|||
'remoteBuildExpiration',
|
||||
remoteBuildExpirationTimestamp
|
||||
);
|
||||
window.reduxActions.expiration.hydrateExpirationStatus(
|
||||
window.Signal.Util.hasExpired()
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -43,6 +43,7 @@ type PropsType = {
|
|||
onUndoArchive: (conversationId: string) => unknown;
|
||||
openFileInFolder: (target: string) => unknown;
|
||||
hasCustomTitleBar: boolean;
|
||||
OS: string;
|
||||
osClassName: string;
|
||||
hideMenuBar: boolean;
|
||||
|
||||
|
@ -76,6 +77,7 @@ export function App({
|
|||
onUndoArchive,
|
||||
openFileInFolder,
|
||||
openInbox,
|
||||
OS,
|
||||
osClassName,
|
||||
registerSingleDevice,
|
||||
renderCallManager,
|
||||
|
@ -173,6 +175,7 @@ export function App({
|
|||
})}
|
||||
>
|
||||
<ToastManager
|
||||
OS={OS}
|
||||
hideToast={hideToast}
|
||||
i18n={i18n}
|
||||
onUndoArchive={onUndoArchive}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
|
||||
import { DialogExpiredBuild } from './DialogExpiredBuild';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
|
@ -22,13 +22,11 @@ export const _DialogExpiredBuild = (): JSX.Element => {
|
|||
WidthBreakpoint,
|
||||
WidthBreakpoint.Wide
|
||||
);
|
||||
const hasExpired = boolean('hasExpired', true);
|
||||
|
||||
return (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
<DialogExpiredBuild
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
hasExpired={hasExpired}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
|
|
|
@ -9,21 +9,15 @@ import type { WidthBreakpoint } from './_util';
|
|||
import { LeftPaneDialog } from './LeftPaneDialog';
|
||||
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
|
||||
|
||||
type PropsType = {
|
||||
export type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
hasExpired: boolean;
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
|
||||
export function DialogExpiredBuild({
|
||||
containerWidthBreakpoint,
|
||||
hasExpired,
|
||||
i18n,
|
||||
}: PropsType): JSX.Element | null {
|
||||
if (!hasExpired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
|
|
|
@ -13,16 +13,14 @@ import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary';
|
|||
|
||||
const FIVE_SECONDS = 5 * 1000;
|
||||
|
||||
export type PropsType = NetworkStateType & {
|
||||
export type PropsType = Pick<NetworkStateType, 'isOnline' | 'socketStatus'> & {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
hasNetworkDialog: boolean;
|
||||
i18n: LocalizerType;
|
||||
manualReconnect: () => void;
|
||||
};
|
||||
|
||||
export function DialogNetworkStatus({
|
||||
containerWidthBreakpoint,
|
||||
hasNetworkDialog,
|
||||
i18n,
|
||||
isOnline,
|
||||
socketStatus,
|
||||
|
@ -32,10 +30,6 @@ export function DialogNetworkStatus({
|
|||
socketStatus === SocketStatus.CONNECTING
|
||||
);
|
||||
useEffect(() => {
|
||||
if (!hasNetworkDialog) {
|
||||
return () => null;
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
if (isConnecting) {
|
||||
|
@ -47,17 +41,13 @@ export function DialogNetworkStatus({
|
|||
return () => {
|
||||
clearTimeoutIfNecessary(timeout);
|
||||
};
|
||||
}, [hasNetworkDialog, isConnecting, setIsConnecting]);
|
||||
}, [isConnecting, setIsConnecting]);
|
||||
|
||||
const reconnect = () => {
|
||||
setIsConnecting(true);
|
||||
manualReconnect();
|
||||
};
|
||||
|
||||
if (!hasNetworkDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isConnecting) {
|
||||
const spinner = (
|
||||
<div className="LeftPaneDialog__spinner-container">
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { DialogRelink } from './DialogRelink';
|
||||
|
@ -16,7 +15,6 @@ const i18n = setupI18n('en', enMessages);
|
|||
const defaultProps = {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
i18n,
|
||||
isRegistrationDone: true,
|
||||
relinkDevice: action('relink-device'),
|
||||
};
|
||||
|
||||
|
@ -25,14 +23,12 @@ const permutations = [
|
|||
title: 'Unlinked (wide container)',
|
||||
props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
isRegistrationDone: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Unlinked (narrow container)',
|
||||
props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Narrow,
|
||||
isRegistrationDone: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -41,14 +37,6 @@ export default {
|
|||
title: 'Components/DialogRelink',
|
||||
};
|
||||
|
||||
export function KnobsPlayground(): JSX.Element {
|
||||
const isRegistrationDone = boolean('isRegistrationDone', false);
|
||||
|
||||
return (
|
||||
<DialogRelink {...defaultProps} isRegistrationDone={isRegistrationDone} />
|
||||
);
|
||||
}
|
||||
|
||||
export function Iterations(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -11,20 +11,14 @@ import { LeftPaneDialog } from './LeftPaneDialog';
|
|||
export type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
i18n: LocalizerType;
|
||||
isRegistrationDone: boolean;
|
||||
relinkDevice: () => void;
|
||||
};
|
||||
|
||||
export function DialogRelink({
|
||||
containerWidthBreakpoint,
|
||||
i18n,
|
||||
isRegistrationDone,
|
||||
relinkDevice,
|
||||
}: PropsType): JSX.Element | null {
|
||||
if (isRegistrationDone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { DialogUpdate } from './DialogUpdate';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
|
@ -20,9 +20,7 @@ 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'),
|
||||
|
@ -40,8 +38,6 @@ export function KnobsPlayground(): JSX.Element {
|
|||
WidthBreakpoint.Wide
|
||||
);
|
||||
const dialogType = select('dialogType', DialogType, DialogType.Update);
|
||||
const hasNetworkDialog = boolean('hasNetworkDialog', false);
|
||||
const didSnooze = boolean('didSnooze', false);
|
||||
|
||||
return (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={containerWidthBreakpoint}>
|
||||
|
@ -49,8 +45,6 @@ export function KnobsPlayground(): JSX.Element {
|
|||
{...defaultProps}
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
dialogType={dialogType}
|
||||
didSnooze={didSnooze}
|
||||
hasNetworkDialog={hasNetworkDialog}
|
||||
currentVersion="5.24.0"
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
|
@ -231,6 +225,23 @@ MacOSReadOnlyWide.story = {
|
|||
name: 'MacOS_Read_Only (Wide)',
|
||||
};
|
||||
|
||||
export function UnsupportedOSWide(): JSX.Element {
|
||||
return (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={WidthBreakpoint.Wide}>
|
||||
<DialogUpdate
|
||||
{...defaultProps}
|
||||
containerWidthBreakpoint={WidthBreakpoint.Wide}
|
||||
currentVersion="5.24.0"
|
||||
dialogType={DialogType.UnsupportedOS}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
);
|
||||
}
|
||||
|
||||
UnsupportedOSWide.story = {
|
||||
name: 'UnsupportedOS (Wide)',
|
||||
};
|
||||
|
||||
export function UpdateNarrow(): JSX.Element {
|
||||
return (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={WidthBreakpoint.Narrow}>
|
||||
|
@ -385,3 +396,20 @@ export function MacOSReadOnlyNarrow(): JSX.Element {
|
|||
MacOSReadOnlyNarrow.story = {
|
||||
name: 'MacOS_Read_Only (Narrow)',
|
||||
};
|
||||
|
||||
export function UnsupportedOSNarrow(): JSX.Element {
|
||||
return (
|
||||
<FakeLeftPaneContainer containerWidthBreakpoint={WidthBreakpoint.Narrow}>
|
||||
<DialogUpdate
|
||||
{...defaultProps}
|
||||
containerWidthBreakpoint={WidthBreakpoint.Narrow}
|
||||
currentVersion="5.24.0"
|
||||
dialogType={DialogType.UnsupportedOS}
|
||||
/>
|
||||
</FakeLeftPaneContainer>
|
||||
);
|
||||
}
|
||||
|
||||
UnsupportedOSNarrow.story = {
|
||||
name: 'UnsupportedOS (Narrow)',
|
||||
};
|
||||
|
|
|
@ -14,13 +14,10 @@ import type { WidthBreakpoint } from './_util';
|
|||
export type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
dialogType: DialogType;
|
||||
didSnooze: boolean;
|
||||
dismissDialog: () => void;
|
||||
downloadSize?: number;
|
||||
downloadedSize?: number;
|
||||
hasNetworkDialog: boolean;
|
||||
i18n: LocalizerType;
|
||||
showEventsCount: number;
|
||||
snoozeUpdate: () => void;
|
||||
startUpdate: () => void;
|
||||
version?: string;
|
||||
|
@ -33,29 +30,15 @@ const BETA_DOWNLOAD_URL = 'https://support.signal.org/beta';
|
|||
export function DialogUpdate({
|
||||
containerWidthBreakpoint,
|
||||
dialogType,
|
||||
didSnooze,
|
||||
dismissDialog,
|
||||
downloadSize,
|
||||
downloadedSize,
|
||||
hasNetworkDialog,
|
||||
i18n,
|
||||
snoozeUpdate,
|
||||
startUpdate,
|
||||
version,
|
||||
currentVersion,
|
||||
}: PropsType): JSX.Element | null {
|
||||
if (hasNetworkDialog) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === DialogType.None) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (didSnooze) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dialogType === DialogType.Cannot_Update) {
|
||||
const url = isBeta(currentVersion)
|
||||
? BETA_DOWNLOAD_URL
|
||||
|
@ -174,6 +157,11 @@ export function DialogUpdate({
|
|||
);
|
||||
}
|
||||
|
||||
if (dialogType === DialogType.UnsupportedOS) {
|
||||
// Displayed as UnsupportedOSDialog in LeftPane
|
||||
return null;
|
||||
}
|
||||
|
||||
let title = i18n('autoUpdateNewVersionTitle');
|
||||
|
||||
if (
|
||||
|
|
|
@ -4,18 +4,27 @@
|
|||
import * as React from 'react';
|
||||
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import { boolean, select } from '@storybook/addon-knobs';
|
||||
|
||||
import type { PropsType } from './LeftPane';
|
||||
import { LeftPane, LeftPaneMode } from './LeftPane';
|
||||
import { CaptchaDialog } from './CaptchaDialog';
|
||||
import { CrashReportDialog } from './CrashReportDialog';
|
||||
import type { PropsType as DialogNetworkStatusPropsType } from './DialogNetworkStatus';
|
||||
import { DialogExpiredBuild } from './DialogExpiredBuild';
|
||||
import { DialogNetworkStatus } from './DialogNetworkStatus';
|
||||
import { DialogRelink } from './DialogRelink';
|
||||
import type { PropsType as DialogUpdatePropsType } from './DialogUpdate';
|
||||
import { DialogUpdate } from './DialogUpdate';
|
||||
import { UnsupportedOSDialog } from './UnsupportedOSDialog';
|
||||
import type { ConversationType } from '../state/ducks/conversations';
|
||||
import { MessageSearchResult } from './conversationList/MessageSearchResult';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import { DurationInSeconds } from '../util/durations';
|
||||
import { DurationInSeconds, DAY } from '../util/durations';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import { SocketStatus } from '../types/SocketStatus';
|
||||
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
|
||||
import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext';
|
||||
import {
|
||||
|
@ -25,6 +34,11 @@ import {
|
|||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
type OverridePropsType = Partial<PropsType> & {
|
||||
dialogNetworkStatus?: Partial<DialogNetworkStatusPropsType>;
|
||||
dialogUpdate?: Partial<DialogUpdatePropsType>;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: 'Components/LeftPane',
|
||||
};
|
||||
|
@ -95,7 +109,7 @@ const defaultModeSpecificProps = {
|
|||
|
||||
const emptySearchResultsGroup = { isLoading: false, results: [] };
|
||||
|
||||
const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
||||
const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
||||
let modeSpecificProps =
|
||||
overrideProps.modeSpecificProps ?? defaultModeSpecificProps;
|
||||
|
||||
|
@ -112,6 +126,8 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
|||
};
|
||||
}
|
||||
|
||||
const isUpdateDownloaded = boolean('isUpdateDownloaded', false);
|
||||
|
||||
return {
|
||||
clearConversationSearch: action('clearConversationSearch'),
|
||||
clearGroupCreationError: action('clearGroupCreationError'),
|
||||
|
@ -124,6 +140,7 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
|||
createGroup: action('createGroup'),
|
||||
getPreferredBadge: () => undefined,
|
||||
i18n,
|
||||
isMacOS: boolean('isMacOS', false),
|
||||
preferredWidthFromStorage: 320,
|
||||
regionCode: 'US',
|
||||
challengeStatus: select(
|
||||
|
@ -132,12 +149,23 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
|||
'idle'
|
||||
),
|
||||
crashReportCount: select('challengeReportCount', [0, 1], 0),
|
||||
|
||||
hasNetworkDialog: boolean('hasNetworkDialog', false),
|
||||
hasExpiredDialog: boolean('hasExpiredDialog', false),
|
||||
hasRelinkDialog: boolean('hasRelinkDialog', false),
|
||||
hasUpdateDialog: boolean('hasUpdateDialog', false),
|
||||
unsupportedOSDialogType: select(
|
||||
'unsupportedOSDialogType',
|
||||
['error', 'warning', undefined],
|
||||
undefined
|
||||
),
|
||||
isUpdateDownloaded,
|
||||
|
||||
setChallengeStatus: action('setChallengeStatus'),
|
||||
lookupConversationWithoutUuid: makeFakeLookupConversationWithoutUuid(),
|
||||
showUserNotFoundModal: action('showUserNotFoundModal'),
|
||||
setIsFetchingUUID,
|
||||
showConversation: action('showConversation'),
|
||||
renderExpiredBuildDialog: () => <div />,
|
||||
renderMainHeader: () => <div />,
|
||||
renderMessageSearchResult: (id: string) => (
|
||||
<MessageSearchResult
|
||||
|
@ -155,9 +183,39 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
|||
to={defaultConversations[1]}
|
||||
/>
|
||||
),
|
||||
renderNetworkStatus: () => <div />,
|
||||
renderRelinkDialog: () => <div />,
|
||||
renderUpdateDialog: () => <div />,
|
||||
|
||||
renderNetworkStatus: props => (
|
||||
<DialogNetworkStatus
|
||||
i18n={i18n}
|
||||
socketStatus={SocketStatus.CLOSED}
|
||||
isOnline={false}
|
||||
manualReconnect={action('manualReconnect')}
|
||||
{...overrideProps.dialogNetworkStatus}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
renderRelinkDialog: props => (
|
||||
<DialogRelink
|
||||
i18n={i18n}
|
||||
relinkDevice={action('relinkDevice')}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
renderUpdateDialog: props => (
|
||||
<DialogUpdate
|
||||
i18n={i18n}
|
||||
dialogType={
|
||||
isUpdateDownloaded ? DialogType.Update : DialogType.DownloadReady
|
||||
}
|
||||
dismissDialog={action('dismissUpdate')}
|
||||
snoozeUpdate={action('snoozeUpdate')}
|
||||
startUpdate={action('startUpdate')}
|
||||
currentVersion="1.0.0"
|
||||
{...overrideProps.dialogUpdate}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
||||
renderCaptchaDialog: () => (
|
||||
<CaptchaDialog
|
||||
i18n={i18n}
|
||||
|
@ -174,6 +232,15 @@ const useProps = (overrideProps: Partial<PropsType> = {}): PropsType => {
|
|||
eraseCrashReports={action('eraseCrashReports')}
|
||||
/>
|
||||
),
|
||||
renderExpiredBuildDialog: props => <DialogExpiredBuild {...props} />,
|
||||
renderUnsupportedOSDialog: props => (
|
||||
<UnsupportedOSDialog
|
||||
i18n={i18n}
|
||||
OS="macOS"
|
||||
expirationTimestamp={Date.now() + 5 * DAY}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
selectedConversationId: undefined,
|
||||
selectedMessageId: undefined,
|
||||
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
|
||||
|
|
|
@ -22,7 +22,6 @@ import { LeftPaneChooseGroupMembersHelper } from './leftPane/LeftPaneChooseGroup
|
|||
import type { LeftPaneSetGroupMetadataPropsType } from './leftPane/LeftPaneSetGroupMetadataHelper';
|
||||
import { LeftPaneSetGroupMetadataHelper } from './leftPane/LeftPaneSetGroupMetadataHelper';
|
||||
|
||||
import * as OS from '../OS';
|
||||
import type { LocalizerType, ThemeType } from '../types/Util';
|
||||
import { ScrollBehavior } from '../types/Util';
|
||||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
|
||||
|
@ -41,9 +40,11 @@ import {
|
|||
} from '../util/leftPaneWidth';
|
||||
import type { LookupConversationWithoutUuidActionsType } from '../util/lookupConversationWithoutUuid';
|
||||
import type { ShowConversationType } from '../state/ducks/conversations';
|
||||
import type { PropsType as UnsupportedOSDialogPropsType } from '../state/smart/UnsupportedOSDialog';
|
||||
|
||||
import { ConversationList } from './ConversationList';
|
||||
import { ContactCheckboxDisabledReason } from './conversationList/ContactCheckbox';
|
||||
import type { PropsType as DialogExpiredBuildPropsType } from './DialogExpiredBuild';
|
||||
|
||||
import type {
|
||||
DeleteAvatarFromDiskActionType,
|
||||
|
@ -61,6 +62,13 @@ export enum LeftPaneMode {
|
|||
}
|
||||
|
||||
export type PropsType = {
|
||||
hasExpiredDialog: boolean;
|
||||
hasNetworkDialog: boolean;
|
||||
hasRelinkDialog: boolean;
|
||||
hasUpdateDialog: boolean;
|
||||
isUpdateDownloaded: boolean;
|
||||
unsupportedOSDialogType: 'error' | 'warning' | undefined;
|
||||
|
||||
// These help prevent invalid states. For example, we don't need the list of pinned
|
||||
// conversations if we're trying to start a new conversation. Ideally these would be
|
||||
// at the top level, but this is not supported by react-redux + TypeScript.
|
||||
|
@ -85,6 +93,7 @@ export type PropsType = {
|
|||
} & LeftPaneSetGroupMetadataPropsType);
|
||||
getPreferredBadge: PreferredBadgeSelectorType;
|
||||
i18n: LocalizerType;
|
||||
isMacOS: boolean;
|
||||
preferredWidthFromStorage: number;
|
||||
selectedConversationId: undefined | string;
|
||||
selectedMessageId: undefined | string;
|
||||
|
@ -122,14 +131,14 @@ export type PropsType = {
|
|||
updateSearchTerm: (_: string) => void;
|
||||
|
||||
// Render Props
|
||||
renderExpiredBuildDialog: (
|
||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
) => JSX.Element;
|
||||
renderMainHeader: () => JSX.Element;
|
||||
renderMessageSearchResult: (id: string) => JSX.Element;
|
||||
renderNetworkStatus: (
|
||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
) => JSX.Element;
|
||||
renderUnsupportedOSDialog: (
|
||||
_: Readonly<UnsupportedOSDialogPropsType>
|
||||
) => JSX.Element;
|
||||
renderRelinkDialog: (
|
||||
_: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
) => JSX.Element;
|
||||
|
@ -138,6 +147,7 @@ export type PropsType = {
|
|||
) => JSX.Element;
|
||||
renderCaptchaDialog: (props: { onSkip(): void }) => JSX.Element;
|
||||
renderCrashReportDialog: () => JSX.Element;
|
||||
renderExpiredBuildDialog: (_: DialogExpiredBuildPropsType) => JSX.Element;
|
||||
} & LookupConversationWithoutUuidActionsType;
|
||||
|
||||
export function LeftPane({
|
||||
|
@ -153,8 +163,14 @@ export function LeftPane({
|
|||
crashReportCount,
|
||||
createGroup,
|
||||
getPreferredBadge,
|
||||
hasExpiredDialog,
|
||||
hasNetworkDialog,
|
||||
hasRelinkDialog,
|
||||
hasUpdateDialog,
|
||||
i18n,
|
||||
lookupConversationWithoutUuid,
|
||||
isMacOS,
|
||||
isUpdateDownloaded,
|
||||
modeSpecificProps,
|
||||
preferredWidthFromStorage,
|
||||
renderCaptchaDialog,
|
||||
|
@ -163,6 +179,7 @@ export function LeftPane({
|
|||
renderMainHeader,
|
||||
renderMessageSearchResult,
|
||||
renderNetworkStatus,
|
||||
renderUnsupportedOSDialog,
|
||||
renderRelinkDialog,
|
||||
renderUpdateDialog,
|
||||
savePreferredLeftPaneWidth,
|
||||
|
@ -186,6 +203,7 @@ export function LeftPane({
|
|||
theme,
|
||||
toggleComposeEditingAvatar,
|
||||
toggleConversationInChooseMembers,
|
||||
unsupportedOSDialogType,
|
||||
updateSearchTerm,
|
||||
}: PropsType): JSX.Element {
|
||||
const [preferredWidth, setPreferredWidth] = useState(
|
||||
|
@ -289,7 +307,7 @@ export function LeftPane({
|
|||
useEffect(() => {
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
const { ctrlKey, shiftKey, altKey, metaKey } = event;
|
||||
const commandOrCtrl = OS.isMacOS() ? metaKey : ctrlKey;
|
||||
const commandOrCtrl = isMacOS ? metaKey : ctrlKey;
|
||||
const key = KeyboardLayout.lookup(event);
|
||||
|
||||
if (key === 'Escape') {
|
||||
|
@ -383,6 +401,7 @@ export function LeftPane({
|
|||
}, [
|
||||
clearSearch,
|
||||
helper,
|
||||
isMacOS,
|
||||
searchInConversation,
|
||||
selectedConversationId,
|
||||
selectedMessageId,
|
||||
|
@ -527,6 +546,60 @@ export function LeftPane({
|
|||
|
||||
const widthBreakpoint = getConversationListWidthBreakpoint(width);
|
||||
|
||||
const commonDialogProps = {
|
||||
i18n,
|
||||
containerWidthBreakpoint: widthBreakpoint,
|
||||
};
|
||||
|
||||
// Yellow dialogs
|
||||
let maybeYellowDialog: JSX.Element | undefined;
|
||||
|
||||
if (unsupportedOSDialogType === 'warning') {
|
||||
maybeYellowDialog = renderUnsupportedOSDialog({
|
||||
type: 'warning',
|
||||
...commonDialogProps,
|
||||
});
|
||||
} else if (hasNetworkDialog) {
|
||||
maybeYellowDialog = renderNetworkStatus(commonDialogProps);
|
||||
} else if (hasRelinkDialog) {
|
||||
maybeYellowDialog = renderRelinkDialog(commonDialogProps);
|
||||
}
|
||||
|
||||
// Update dialog
|
||||
let maybeUpdateDialog: JSX.Element | undefined;
|
||||
if (hasUpdateDialog && (!hasNetworkDialog || isUpdateDownloaded)) {
|
||||
maybeUpdateDialog = renderUpdateDialog(commonDialogProps);
|
||||
}
|
||||
|
||||
// Red dialogs
|
||||
let maybeRedDialog: JSX.Element | undefined;
|
||||
if (unsupportedOSDialogType === 'error') {
|
||||
maybeRedDialog = renderUnsupportedOSDialog({
|
||||
type: 'error',
|
||||
...commonDialogProps,
|
||||
});
|
||||
} else if (hasExpiredDialog) {
|
||||
maybeRedDialog = renderExpiredBuildDialog(commonDialogProps);
|
||||
}
|
||||
|
||||
const dialogs = new Array<{ key: string; dialog: JSX.Element }>();
|
||||
|
||||
if (maybeRedDialog) {
|
||||
dialogs.push({ key: 'red', dialog: maybeRedDialog });
|
||||
if (maybeUpdateDialog) {
|
||||
dialogs.push({ key: 'update', dialog: maybeUpdateDialog });
|
||||
} else if (maybeYellowDialog) {
|
||||
dialogs.push({ key: 'yellow', dialog: maybeYellowDialog });
|
||||
}
|
||||
} else {
|
||||
if (maybeUpdateDialog) {
|
||||
dialogs.push({ key: 'update', dialog: maybeUpdateDialog });
|
||||
}
|
||||
if (maybeYellowDialog) {
|
||||
dialogs.push({ key: 'yellow', dialog: maybeYellowDialog });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -556,12 +629,9 @@ export function LeftPane({
|
|||
showConversation,
|
||||
})}
|
||||
<div className="module-left-pane__dialogs">
|
||||
{renderExpiredBuildDialog({
|
||||
containerWidthBreakpoint: widthBreakpoint,
|
||||
})}
|
||||
{renderRelinkDialog({ containerWidthBreakpoint: widthBreakpoint })}
|
||||
{renderNetworkStatus({ containerWidthBreakpoint: widthBreakpoint })}
|
||||
{renderUpdateDialog({ containerWidthBreakpoint: widthBreakpoint })}
|
||||
{dialogs.map(({ key, dialog }) => (
|
||||
<React.Fragment key={key}>{dialog}</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
{preRowsNode && <React.Fragment key={0}>{preRowsNode}</React.Fragment>}
|
||||
<Measure bounds>
|
||||
|
|
|
@ -25,6 +25,9 @@ export default {
|
|||
toast: {
|
||||
defaultValue: undefined,
|
||||
},
|
||||
OS: {
|
||||
defaultValue: 'macOS',
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
|
@ -327,6 +330,13 @@ UnsupportedMultiAttachment.args = {
|
|||
},
|
||||
};
|
||||
|
||||
export const UnsupportedOS = Template.bind({});
|
||||
UnsupportedOS.args = {
|
||||
toast: {
|
||||
toastType: ToastType.UnsupportedOS,
|
||||
},
|
||||
};
|
||||
|
||||
export const UserAddedToGroup = Template.bind({});
|
||||
UserAddedToGroup.args = {
|
||||
toast: {
|
||||
|
|
|
@ -12,6 +12,7 @@ export type PropsType = {
|
|||
hideToast: () => unknown;
|
||||
i18n: LocalizerType;
|
||||
openFileInFolder: (target: string) => unknown;
|
||||
OS: string;
|
||||
onUndoArchive: (conversaetionId: string) => unknown;
|
||||
toast?: {
|
||||
toastType: ToastType;
|
||||
|
@ -26,6 +27,7 @@ export function ToastManager({
|
|||
i18n,
|
||||
openFileInFolder,
|
||||
onUndoArchive,
|
||||
OS,
|
||||
toast,
|
||||
}: PropsType): JSX.Element | null {
|
||||
if (toast === undefined) {
|
||||
|
@ -320,6 +322,14 @@ export function ToastManager({
|
|||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.UnsupportedOS) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
{i18n('icu:UnsupportedOSErrorToast', { OS })}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
if (toastType === ToastType.UserAddedToGroup) {
|
||||
return (
|
||||
<Toast onClose={hideToast}>
|
||||
|
|
79
ts/components/UnsupportedOSDialog.stories.tsx
Normal file
79
ts/components/UnsupportedOSDialog.stories.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { UnsupportedOSDialog } from './UnsupportedOSDialog';
|
||||
import type { PropsType } from './UnsupportedOSDialog';
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import { DAY } from '../util/durations';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { WidthBreakpoint } from './_util';
|
||||
import { FakeLeftPaneContainer } from '../test-both/helpers/FakeLeftPaneContainer';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const defaultProps: PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
OS: 'macOS',
|
||||
expirationTimestamp: Date.now() + 5 * DAY,
|
||||
i18n,
|
||||
type: 'warning',
|
||||
};
|
||||
|
||||
const permutations: ReadonlyArray<{
|
||||
title: string;
|
||||
props: Partial<PropsType>;
|
||||
}> = [
|
||||
{
|
||||
title: 'Warning (wide container)',
|
||||
props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
type: 'warning',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Warning (narrow container)',
|
||||
props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Narrow,
|
||||
type: 'warning',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Error (wide container)',
|
||||
props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Wide,
|
||||
type: 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Error (narrow container)',
|
||||
props: {
|
||||
containerWidthBreakpoint: WidthBreakpoint.Narrow,
|
||||
type: 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default {
|
||||
title: 'Components/UnsupportedOSDialog',
|
||||
};
|
||||
|
||||
export function Iterations(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{permutations.map(({ props, title }) => (
|
||||
<>
|
||||
<h3>{title}</h3>
|
||||
<FakeLeftPaneContainer
|
||||
containerWidthBreakpoint={
|
||||
props.containerWidthBreakpoint ?? WidthBreakpoint.Wide
|
||||
}
|
||||
>
|
||||
<UnsupportedOSDialog {...defaultProps} {...props} />
|
||||
</FakeLeftPaneContainer>
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
74
ts/components/UnsupportedOSDialog.tsx
Normal file
74
ts/components/UnsupportedOSDialog.tsx
Normal file
|
@ -0,0 +1,74 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import type { FormatXMLElementFn } from 'intl-messageformat';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import type { WidthBreakpoint } from './_util';
|
||||
import { Intl } from './Intl';
|
||||
|
||||
import { LeftPaneDialog } from './LeftPaneDialog';
|
||||
|
||||
export type PropsType = {
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
expirationTimestamp: number;
|
||||
i18n: LocalizerType;
|
||||
type: 'warning' | 'error';
|
||||
OS: string;
|
||||
};
|
||||
|
||||
const SUPPORT_URL = 'https://support.signal.org/hc/articles/5109141421850';
|
||||
|
||||
export function UnsupportedOSDialog({
|
||||
containerWidthBreakpoint,
|
||||
expirationTimestamp,
|
||||
i18n,
|
||||
type,
|
||||
OS,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const learnMoreLink: FormatXMLElementFn<JSX.Element | string> = children => (
|
||||
<a key="signal-support" href={SUPPORT_URL} rel="noreferrer" target="_blank">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
|
||||
let body: JSX.Element;
|
||||
if (type === 'error') {
|
||||
body = (
|
||||
<Intl
|
||||
id="icu:UnsupportedOSErrorDialog__body"
|
||||
i18n={i18n}
|
||||
components={{
|
||||
OS,
|
||||
learnMoreLink,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (type === 'warning') {
|
||||
body = (
|
||||
<Intl
|
||||
id="icu:UnsupportedOSWarningDialog__body"
|
||||
i18n={i18n}
|
||||
components={{
|
||||
OS,
|
||||
expirationDate: moment(expirationTimestamp).format('ll'),
|
||||
learnMoreLink,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
throw missingCaseError(type);
|
||||
}
|
||||
|
||||
return (
|
||||
<LeftPaneDialog
|
||||
containerWidthBreakpoint={containerWidthBreakpoint}
|
||||
type={type}
|
||||
>
|
||||
<span>{body}</span>
|
||||
</LeftPaneDialog>
|
||||
);
|
||||
}
|
|
@ -6,7 +6,7 @@ import type { ReadonlyDeep } from 'type-fest';
|
|||
// State
|
||||
|
||||
export type ExpirationStateType = ReadonlyDeep<{
|
||||
hasExpired: boolean;
|
||||
buildExpiration: number;
|
||||
}>;
|
||||
|
||||
// Actions
|
||||
|
@ -15,7 +15,7 @@ const HYDRATE_EXPIRATION_STATUS = 'expiration/HYDRATE_EXPIRATION_STATUS';
|
|||
|
||||
type HyrdateExpirationStatusActionType = ReadonlyDeep<{
|
||||
type: 'expiration/HYDRATE_EXPIRATION_STATUS';
|
||||
payload: boolean;
|
||||
payload: { buildExpiration: number };
|
||||
}>;
|
||||
|
||||
export type ExpirationActionType =
|
||||
|
@ -23,10 +23,12 @@ export type ExpirationActionType =
|
|||
|
||||
// Action Creators
|
||||
|
||||
function hydrateExpirationStatus(hasExpired: boolean): ExpirationActionType {
|
||||
function hydrateExpirationStatus(
|
||||
buildExpiration: number
|
||||
): ExpirationActionType {
|
||||
return {
|
||||
type: HYDRATE_EXPIRATION_STATUS,
|
||||
payload: hasExpired,
|
||||
payload: { buildExpiration },
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -38,7 +40,7 @@ export const actions = {
|
|||
|
||||
export function getEmptyState(): ExpirationStateType {
|
||||
return {
|
||||
hasExpired: false,
|
||||
buildExpiration: 0,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -48,7 +50,7 @@ export function reducer(
|
|||
): ExpirationStateType {
|
||||
if (action.type === HYDRATE_EXPIRATION_STATUS) {
|
||||
return {
|
||||
hasExpired: action.payload,
|
||||
buildExpiration: action.payload.buildExpiration,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
77
ts/state/selectors/expiration.ts
Normal file
77
ts/state/selectors/expiration.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { Environment, getEnvironment } from '../../environment';
|
||||
import { isInPast } from '../../util/timestamp';
|
||||
import { DAY } from '../../util/durations';
|
||||
import * as log from '../../logging/log';
|
||||
import type { StateType } from '../reducer';
|
||||
import type { ExpirationStateType } from '../ducks/expiration';
|
||||
import { getRemoteBuildExpiration, getAutoDownloadUpdate } from './items';
|
||||
|
||||
const NINETY_ONE_DAYS = 91 * DAY;
|
||||
const THIRTY_ONE_DAYS = 31 * DAY;
|
||||
const SIXTY_DAYS = 60 * DAY;
|
||||
|
||||
export const getExpiration = (state: StateType): ExpirationStateType =>
|
||||
state.expiration;
|
||||
|
||||
export const getExpirationTimestamp = createSelector(
|
||||
getExpiration,
|
||||
getRemoteBuildExpiration,
|
||||
getAutoDownloadUpdate,
|
||||
(
|
||||
{ buildExpiration }: Readonly<ExpirationStateType>,
|
||||
remoteBuildExpiration: number | undefined,
|
||||
autoDownloadUpdate: boolean
|
||||
): number => {
|
||||
const localBuildExpiration = autoDownloadUpdate
|
||||
? buildExpiration
|
||||
: buildExpiration - SIXTY_DAYS;
|
||||
|
||||
if (remoteBuildExpiration) {
|
||||
return Math.min(remoteBuildExpiration, localBuildExpiration);
|
||||
}
|
||||
|
||||
return localBuildExpiration;
|
||||
}
|
||||
);
|
||||
|
||||
export type HasExpiredOptionsType = Readonly<{
|
||||
now?: number;
|
||||
}>;
|
||||
|
||||
export const hasExpired = createSelector(
|
||||
getExpirationTimestamp,
|
||||
getAutoDownloadUpdate,
|
||||
(_: StateType, { now = Date.now() }: HasExpiredOptionsType = {}) => now,
|
||||
(buildExpiration: number, autoDownloadUpdate: boolean, now: number) => {
|
||||
if (getEnvironment() !== Environment.Production && buildExpiration === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info('Build expires: ', new Date(buildExpiration).toISOString());
|
||||
|
||||
if (isInPast(buildExpiration)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const safeExpirationMs = autoDownloadUpdate
|
||||
? NINETY_ONE_DAYS
|
||||
: THIRTY_ONE_DAYS;
|
||||
|
||||
const buildExpirationDuration = buildExpiration - now;
|
||||
const tooFarIntoFuture = buildExpirationDuration > safeExpirationMs;
|
||||
|
||||
if (tooFarIntoFuture) {
|
||||
log.error(
|
||||
'Build expiration is set too far into the future',
|
||||
buildExpiration
|
||||
);
|
||||
}
|
||||
|
||||
return tooFarIntoFuture || isInPast(buildExpiration);
|
||||
}
|
||||
);
|
|
@ -189,3 +189,15 @@ export const getHasStoryViewReceiptSetting = createSelector(
|
|||
state.storyViewReceiptsEnabled ?? state['read-receipt-setting'] ?? false
|
||||
)
|
||||
);
|
||||
|
||||
export const getRemoteBuildExpiration = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): number | undefined =>
|
||||
Number(state.remoteBuildExpiration)
|
||||
);
|
||||
|
||||
export const getAutoDownloadUpdate = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): boolean =>
|
||||
Boolean(state['auto-download-update'] ?? true)
|
||||
);
|
||||
|
|
42
ts/state/selectors/updates.ts
Normal file
42
ts/state/selectors/updates.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2022 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { DialogType } from '../../types/Dialogs';
|
||||
|
||||
import type { StateType } from '../reducer';
|
||||
import type { UpdatesStateType } from '../ducks/updates';
|
||||
|
||||
const getUpdatesState = (state: Readonly<StateType>): UpdatesStateType =>
|
||||
state.updates;
|
||||
|
||||
export const isUpdateDialogVisible = createSelector(
|
||||
getUpdatesState,
|
||||
({ dialogType, didSnooze }) => {
|
||||
if (dialogType === DialogType.None) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Displayed as UnsupportedOSDialog in LeftPane
|
||||
if (dialogType === DialogType.UnsupportedOS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (didSnooze) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
export const isUpdateDownloaded = createSelector(
|
||||
getUpdatesState,
|
||||
({ dialogType }) => dialogType === DialogType.Update
|
||||
);
|
||||
|
||||
export const isOSUnsupported = createSelector(
|
||||
getUpdatesState,
|
||||
({ dialogType }) => dialogType === DialogType.UnsupportedOS
|
||||
);
|
|
@ -108,3 +108,8 @@ export const getMenuOptions = createSelector(
|
|||
getUser,
|
||||
(state: UserStateType): MenuOptionsType => state.menuOptions
|
||||
);
|
||||
|
||||
export const getIsMacOS = createSelector(
|
||||
getPlatform,
|
||||
(platform: string): boolean => platform === 'darwin'
|
||||
);
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { MenuItemConstructorOptions } from 'electron';
|
|||
|
||||
import type { MenuActionType } from '../../types/menu';
|
||||
import { App } from '../../components/App';
|
||||
import { getName as getOSName } from '../../OS';
|
||||
import { SmartCallManager } from './CallManager';
|
||||
import { SmartGlobalModalContainer } from './GlobalModalContainer';
|
||||
import { SmartLightbox } from './Lightbox';
|
||||
|
@ -58,6 +59,7 @@ const mapStateToProps = (state: StateType) => {
|
|||
isFullScreen: getIsMainWindowFullScreen(state),
|
||||
menuOptions: getMenuOptions(state),
|
||||
hasCustomTitleBar: window.SignalContext.OS.hasCustomTitleBar(),
|
||||
OS: getOSName(),
|
||||
osClassName,
|
||||
hideMenuBar: getHideMenuBar(state),
|
||||
renderCallManager: () => (
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
import { DialogExpiredBuild } from '../../components/DialogExpiredBuild';
|
||||
import type { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
||||
|
||||
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
|
||||
return {
|
||||
hasExpired: state.expiration.hasExpired,
|
||||
i18n: getIntl(state),
|
||||
...ownProps,
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartExpiredBuildDialog = smart(DialogExpiredBuild);
|
|
@ -7,9 +7,12 @@ import { get } from 'lodash';
|
|||
import { mapDispatchToProps } from '../actions';
|
||||
import type { PropsType as LeftPanePropsType } from '../../components/LeftPane';
|
||||
import { LeftPane, LeftPaneMode } from '../../components/LeftPane';
|
||||
import { DialogExpiredBuild } from '../../components/DialogExpiredBuild';
|
||||
import type { PropsType as DialogExpiredBuildPropsType } from '../../components/DialogExpiredBuild';
|
||||
import type { StateType } from '../reducer';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { lookupConversationWithoutUuid } from '../../util/lookupConversationWithoutUuid';
|
||||
import { isDone as isRegistrationDone } from '../../util/registration';
|
||||
|
||||
import { ComposerStep, OneTimeModalState } from '../ducks/conversationsEnums';
|
||||
import {
|
||||
|
@ -20,8 +23,20 @@ import {
|
|||
getStartSearchCounter,
|
||||
isSearching,
|
||||
} from '../selectors/search';
|
||||
import { getIntl, getRegionCode, getTheme } from '../selectors/user';
|
||||
import {
|
||||
getIntl,
|
||||
getRegionCode,
|
||||
getTheme,
|
||||
getIsMacOS,
|
||||
} from '../selectors/user';
|
||||
import { hasExpired } from '../selectors/expiration';
|
||||
import {
|
||||
isUpdateDialogVisible,
|
||||
isUpdateDownloaded,
|
||||
isOSUnsupported,
|
||||
} from '../selectors/updates';
|
||||
import { getPreferredBadgeSelector } from '../selectors/badges';
|
||||
import { hasNetworkDialog } from '../selectors/network';
|
||||
import {
|
||||
getPreferredLeftPaneWidth,
|
||||
getUsernamesEnabled,
|
||||
|
@ -54,20 +69,16 @@ import {
|
|||
getGroupSizeHardLimit,
|
||||
} from '../../groups/limits';
|
||||
|
||||
import { SmartExpiredBuildDialog } from './ExpiredBuildDialog';
|
||||
import { SmartMainHeader } from './MainHeader';
|
||||
import { SmartMessageSearchResult } from './MessageSearchResult';
|
||||
import { SmartNetworkStatus } from './NetworkStatus';
|
||||
import { SmartRelinkDialog } from './RelinkDialog';
|
||||
import { SmartUnsupportedOSDialog } from './UnsupportedOSDialog';
|
||||
import type { PropsType as SmartUnsupportedOSDialogPropsType } from './UnsupportedOSDialog';
|
||||
import { SmartUpdateDialog } from './UpdateDialog';
|
||||
import { SmartCaptchaDialog } from './CaptchaDialog';
|
||||
import { SmartCrashReportDialog } from './CrashReportDialog';
|
||||
|
||||
function renderExpiredBuildDialog(
|
||||
props: Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>
|
||||
): JSX.Element {
|
||||
return <SmartExpiredBuildDialog {...props} />;
|
||||
}
|
||||
function renderMainHeader(): JSX.Element {
|
||||
return <SmartMainHeader />;
|
||||
}
|
||||
|
@ -95,6 +106,16 @@ function renderCaptchaDialog({ onSkip }: { onSkip(): void }): JSX.Element {
|
|||
function renderCrashReportDialog(): JSX.Element {
|
||||
return <SmartCrashReportDialog />;
|
||||
}
|
||||
function renderExpiredBuildDialog(
|
||||
props: DialogExpiredBuildPropsType
|
||||
): JSX.Element {
|
||||
return <DialogExpiredBuild {...props} />;
|
||||
}
|
||||
function renderUnsupportedOSDialog(
|
||||
props: Readonly<SmartUnsupportedOSDialogPropsType>
|
||||
): JSX.Element {
|
||||
return <SmartUnsupportedOSDialog {...props} />;
|
||||
}
|
||||
|
||||
const getModeSpecificProps = (
|
||||
state: StateType
|
||||
|
@ -183,7 +204,29 @@ const getModeSpecificProps = (
|
|||
};
|
||||
|
||||
const mapStateToProps = (state: StateType) => {
|
||||
const hasUpdateDialog = isUpdateDialogVisible(state);
|
||||
const hasUnsupportedOS = isOSUnsupported(state);
|
||||
|
||||
let hasExpiredDialog = false;
|
||||
let unsupportedOSDialogType: 'error' | 'warning' | undefined;
|
||||
if (hasExpired(state)) {
|
||||
if (hasUnsupportedOS) {
|
||||
unsupportedOSDialogType = 'error';
|
||||
} else {
|
||||
hasExpiredDialog = true;
|
||||
}
|
||||
} else if (hasUnsupportedOS) {
|
||||
unsupportedOSDialogType = 'warning';
|
||||
}
|
||||
|
||||
return {
|
||||
hasNetworkDialog: hasNetworkDialog(state),
|
||||
hasExpiredDialog,
|
||||
hasRelinkDialog: !isRegistrationDone(),
|
||||
hasUpdateDialog,
|
||||
isUpdateDownloaded: isUpdateDownloaded(state),
|
||||
unsupportedOSDialogType,
|
||||
|
||||
modeSpecificProps: getModeSpecificProps(state),
|
||||
preferredWidthFromStorage: getPreferredLeftPaneWidth(state),
|
||||
selectedConversationId: getSelectedConversationId(state),
|
||||
|
@ -191,10 +234,10 @@ const mapStateToProps = (state: StateType) => {
|
|||
showArchived: getShowArchived(state),
|
||||
getPreferredBadge: getPreferredBadgeSelector(state),
|
||||
i18n: getIntl(state),
|
||||
isMacOS: getIsMacOS(state),
|
||||
regionCode: getRegionCode(state),
|
||||
challengeStatus: state.network.challengeStatus,
|
||||
crashReportCount: state.crashReports.count,
|
||||
renderExpiredBuildDialog,
|
||||
renderMainHeader,
|
||||
renderMessageSearchResult,
|
||||
renderNetworkStatus,
|
||||
|
@ -202,6 +245,8 @@ const mapStateToProps = (state: StateType) => {
|
|||
renderUpdateDialog,
|
||||
renderCaptchaDialog,
|
||||
renderCrashReportDialog,
|
||||
renderExpiredBuildDialog,
|
||||
renderUnsupportedOSDialog,
|
||||
lookupConversationWithoutUuid,
|
||||
theme: getTheme(state),
|
||||
};
|
||||
|
|
|
@ -6,7 +6,6 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { DialogNetworkStatus } from '../../components/DialogNetworkStatus';
|
||||
import type { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { hasNetworkDialog } from '../selectors/network';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
||||
|
@ -14,7 +13,6 @@ type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
|||
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
|
||||
return {
|
||||
...state.network,
|
||||
hasNetworkDialog: hasNetworkDialog(state),
|
||||
i18n: getIntl(state),
|
||||
...ownProps,
|
||||
};
|
||||
|
|
|
@ -6,7 +6,6 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { DialogRelink } from '../../components/DialogRelink';
|
||||
import type { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { isDone } from '../../util/registration';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
|
||||
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
||||
|
@ -14,7 +13,6 @@ type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
|||
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
|
||||
return {
|
||||
i18n: getIntl(state),
|
||||
isRegistrationDone: isDone(),
|
||||
...ownProps,
|
||||
};
|
||||
};
|
||||
|
|
31
ts/state/smart/UnsupportedOSDialog.tsx
Normal file
31
ts/state/smart/UnsupportedOSDialog.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { UnsupportedOSDialog } from '../../components/UnsupportedOSDialog';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { getExpirationTimestamp } from '../selectors/expiration';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
import { getName as getOSName } from '../../OS';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
type: 'warning' | 'error';
|
||||
containerWidthBreakpoint: WidthBreakpoint;
|
||||
}>;
|
||||
|
||||
export function SmartUnsupportedOSDialog(ownProps: PropsType): JSX.Element {
|
||||
const i18n = useSelector(getIntl);
|
||||
const expirationTimestamp = useSelector(getExpirationTimestamp);
|
||||
const OS = getOSName();
|
||||
|
||||
return (
|
||||
<UnsupportedOSDialog
|
||||
{...ownProps}
|
||||
i18n={i18n}
|
||||
expirationTimestamp={expirationTimestamp}
|
||||
OS={OS}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -6,17 +6,19 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { DialogUpdate } from '../../components/DialogUpdate';
|
||||
import type { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
import { hasNetworkDialog } from '../selectors/network';
|
||||
import { getExpirationTimestamp } from '../selectors/expiration';
|
||||
import type { WidthBreakpoint } from '../../components/_util';
|
||||
import { getName as getOSName } from '../../OS';
|
||||
|
||||
type PropsType = Readonly<{ containerWidthBreakpoint: WidthBreakpoint }>;
|
||||
|
||||
const mapStateToProps = (state: StateType, ownProps: PropsType) => {
|
||||
return {
|
||||
...state.updates,
|
||||
hasNetworkDialog: hasNetworkDialog(state),
|
||||
i18n: getIntl(state),
|
||||
currentVersion: window.getVersion(),
|
||||
expirationTimestamp: getExpirationTimestamp(state),
|
||||
OS: getOSName(),
|
||||
...ownProps,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@ export async function handleStatusCode(status: number): Promise<void> {
|
|||
if (status === 499) {
|
||||
log.error('Got 499 from Signal Server. Build is expired.');
|
||||
await window.storage.put('remoteBuildExpiration', Date.now());
|
||||
window.reduxActions.expiration.hydrateExpirationStatus(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ export enum DialogType {
|
|||
Update = 'Update',
|
||||
Cannot_Update = 'Cannot_Update',
|
||||
Cannot_Update_Require_Manual = 'Cannot_Update_Require_Manual',
|
||||
UnsupportedOS = 'UnsupportedOS',
|
||||
MacOS_Read_Only = 'MacOS_Read_Only',
|
||||
DownloadReady = 'DownloadReady',
|
||||
FullDownloadReady = 'FullDownloadReady',
|
||||
|
|
|
@ -41,5 +41,6 @@ export enum ToastType {
|
|||
TapToViewExpiredOutgoing = 'TapToViewExpiredOutgoing',
|
||||
UnableToLoadAttachment = 'UnableToLoadAttachment',
|
||||
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
|
||||
UnsupportedOS = 'UnsupportedOS',
|
||||
UserAddedToGroup = 'UserAddedToGroup',
|
||||
}
|
||||
|
|
|
@ -8,13 +8,13 @@ import { readdir, stat, writeFile, mkdir } from 'fs/promises';
|
|||
import { promisify } from 'util';
|
||||
import { execFile } from 'child_process';
|
||||
import { join, normalize, extname } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { tmpdir, release as osRelease } from 'os';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
import type { ParserConfiguration } from 'dashdash';
|
||||
import { createParser } from 'dashdash';
|
||||
import { FAILSAFE_SCHEMA, safeLoad } from 'js-yaml';
|
||||
import { gt } from 'semver';
|
||||
import { gt, lt } from 'semver';
|
||||
import config from 'config';
|
||||
import got from 'got';
|
||||
import { v4 as getGuid } from 'uuid';
|
||||
|
@ -65,7 +65,10 @@ type JSONUpdateSchema = {
|
|||
path: string;
|
||||
sha512: string;
|
||||
releaseDate: string;
|
||||
requireManualUpdate?: boolean;
|
||||
vendor?: {
|
||||
requireManualUpdate?: boolean;
|
||||
minOSVersion?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type UpdateInformationType = {
|
||||
|
@ -365,13 +368,28 @@ export abstract class Updater {
|
|||
const yaml = await getUpdateYaml();
|
||||
const parsedYaml = parseYaml(yaml);
|
||||
|
||||
if (parsedYaml.requireManualUpdate) {
|
||||
this.logger.warn('checkForUpdates: manual update required');
|
||||
this.markCannotUpdate(
|
||||
new Error('yaml file has requireManualUpdate flag'),
|
||||
DialogType.Cannot_Update_Require_Manual
|
||||
);
|
||||
return;
|
||||
const { vendor } = parsedYaml;
|
||||
if (vendor) {
|
||||
if (vendor.requireManualUpdate) {
|
||||
this.logger.warn('checkForUpdates: manual update required');
|
||||
this.markCannotUpdate(
|
||||
new Error('yaml file has requireManualUpdate flag'),
|
||||
DialogType.Cannot_Update_Require_Manual
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (vendor.minOSVersion && lt(osRelease(), vendor.minOSVersion)) {
|
||||
this.logger.warn(
|
||||
`checkForUpdates: OS version ${osRelease()} is less than the ` +
|
||||
`minimum supported version ${vendor.minOSVersion}`
|
||||
);
|
||||
this.markCannotUpdate(
|
||||
new Error('yaml file has unsatisfied minOSVersion value'),
|
||||
DialogType.UnsupportedOS
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const version = getVersion(parsedYaml);
|
||||
|
|
|
@ -7,7 +7,9 @@ import type { BrowserWindow } from 'electron';
|
|||
import type { Updater } from './common';
|
||||
import { MacOSUpdater } from './macos';
|
||||
import { WindowsUpdater } from './windows';
|
||||
import { isLinuxVersionSupported } from './linux';
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
import { DialogType } from '../types/Dialogs';
|
||||
import type { SettingsChannel } from '../main/settingsChannel';
|
||||
|
||||
let initialized = false;
|
||||
|
@ -30,6 +32,15 @@ export async function start(
|
|||
throw new Error('updater/start: Must provide logger!');
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
if (!isLinuxVersionSupported(logger)) {
|
||||
getMainWindow()?.webContents.send(
|
||||
'show-update-dialog',
|
||||
DialogType.UnsupportedOS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (autoUpdateDisabled()) {
|
||||
logger.info(
|
||||
'updater/start: Updates disabled - not starting new version checks'
|
||||
|
@ -46,7 +57,7 @@ export async function start(
|
|||
throw new Error('updater/start: Unsupported platform');
|
||||
}
|
||||
|
||||
await updater.start();
|
||||
await updater?.start();
|
||||
}
|
||||
|
||||
export async function force(): Promise<void> {
|
||||
|
|
33
ts/updater/linux.ts
Normal file
33
ts/updater/linux.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2023 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { version as osVersion } from 'os';
|
||||
|
||||
import type { LoggerType } from '../types/Logging';
|
||||
|
||||
const MIN_UBUNTU_VERSION = '16.04';
|
||||
|
||||
export function getUbuntuVersion(): string | undefined {
|
||||
if (process.platform !== 'linux') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const match = osVersion().match(/^#\d+~([\d.]+)-Ubuntu\s/);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return match[1];
|
||||
}
|
||||
|
||||
export function isLinuxVersionSupported(logger?: LoggerType): boolean {
|
||||
const ubuntu = getUbuntuVersion();
|
||||
if (ubuntu !== undefined && ubuntu < MIN_UBUNTU_VERSION) {
|
||||
logger?.warn(
|
||||
`updater/isLinuxVersionSupported: unsupported Ubuntu version ${ubuntu}`
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { Environment, getEnvironment } from '../environment';
|
||||
import { isInPast } from './timestamp';
|
||||
import * as log from '../logging/log';
|
||||
import * as Errors from '../types/errors';
|
||||
|
||||
const ONE_DAY_MS = 86400 * 1000;
|
||||
const NINETY_ONE_DAYS = 91 * ONE_DAY_MS;
|
||||
const THIRTY_ONE_DAYS = 31 * ONE_DAY_MS;
|
||||
|
||||
export function hasExpired(): boolean {
|
||||
let buildExpiration = 0;
|
||||
|
||||
try {
|
||||
buildExpiration = window.getExpiration();
|
||||
if (buildExpiration) {
|
||||
log.info('Build expires: ', new Date(buildExpiration).toISOString());
|
||||
}
|
||||
} catch (e) {
|
||||
log.error('Error retrieving build expiration date', Errors.toLogFormat(e));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (getEnvironment() === Environment.Production) {
|
||||
const safeExpirationMs = window.Events.getAutoDownloadUpdate()
|
||||
? NINETY_ONE_DAYS
|
||||
: THIRTY_ONE_DAYS;
|
||||
|
||||
const buildExpirationDuration = buildExpiration - Date.now();
|
||||
const tooFarIntoFuture = buildExpirationDuration > safeExpirationMs;
|
||||
|
||||
if (tooFarIntoFuture) {
|
||||
log.error(
|
||||
'Build expiration is set too far into the future',
|
||||
buildExpiration
|
||||
);
|
||||
}
|
||||
|
||||
return tooFarIntoFuture || isInPast(buildExpiration);
|
||||
}
|
||||
|
||||
return buildExpiration !== 0 && isInPast(buildExpiration);
|
||||
}
|
|
@ -14,7 +14,6 @@ import { getStringForProfileChange } from './getStringForProfileChange';
|
|||
import { getTextWithMentions } from './getTextWithMentions';
|
||||
import { getUuidsForE164s } from './getUuidsForE164s';
|
||||
import { getUserAgent } from './getUserAgent';
|
||||
import { hasExpired } from './hasExpired';
|
||||
import {
|
||||
initializeMessageCounter,
|
||||
incrementMessageCounter,
|
||||
|
@ -58,7 +57,6 @@ export {
|
|||
getStringForProfileChange,
|
||||
getTextWithMentions,
|
||||
getUserAgent,
|
||||
hasExpired,
|
||||
incrementMessageCounter,
|
||||
initializeMessageCounter,
|
||||
isFileDangerous,
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationAttributesType } from '../model-types';
|
||||
import { hasExpired } from '../state/selectors/expiration';
|
||||
import { isOSUnsupported } from '../state/selectors/updates';
|
||||
|
||||
import { ToastType } from '../types/Toast';
|
||||
import {
|
||||
|
@ -16,7 +18,11 @@ export function shouldShowInvalidMessageToast(
|
|||
conversationAttributes: ConversationAttributesType,
|
||||
messageText?: string
|
||||
): ToastType | undefined {
|
||||
if (window.reduxStore.getState().expiration.hasExpired) {
|
||||
const state = window.reduxStore.getState();
|
||||
if (hasExpired(state)) {
|
||||
if (isOSUnsupported(state)) {
|
||||
return ToastType.UnsupportedOS;
|
||||
}
|
||||
return ToastType.Expired;
|
||||
}
|
||||
|
||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -150,8 +150,8 @@ declare global {
|
|||
getAppInstance: () => string | undefined;
|
||||
getConversations: () => ConversationModelCollectionType;
|
||||
getBuildCreation: () => number;
|
||||
getBuildExpiration: () => number;
|
||||
getEnvironment: typeof getEnvironment;
|
||||
getExpiration: () => number;
|
||||
getHostName: () => string;
|
||||
getInteractionMode: () => 'mouse' | 'keyboard';
|
||||
getLocale: () => string;
|
||||
|
|
|
@ -43,22 +43,7 @@ window.getEnvironment = getEnvironment;
|
|||
window.getAppInstance = () => config.appInstance;
|
||||
window.getVersion = () => config.version;
|
||||
window.getBuildCreation = () => parseIntWithFallback(config.buildCreation, 0);
|
||||
window.getExpiration = () => {
|
||||
const sixtyDays = 60 * 86400 * 1000;
|
||||
const remoteBuildExpiration = window.storage.get('remoteBuildExpiration');
|
||||
const { buildExpiration } = config;
|
||||
|
||||
const localBuildExpiration = window.Events.getAutoDownloadUpdate()
|
||||
? buildExpiration
|
||||
: buildExpiration - sixtyDays;
|
||||
|
||||
if (remoteBuildExpiration) {
|
||||
return remoteBuildExpiration < localBuildExpiration
|
||||
? remoteBuildExpiration
|
||||
: localBuildExpiration;
|
||||
}
|
||||
return localBuildExpiration;
|
||||
};
|
||||
window.getBuildExpiration = () => config.buildExpiration;
|
||||
window.getHostName = () => config.hostname;
|
||||
window.getServerTrustRoot = () => config.serverTrustRoot;
|
||||
window.getServerPublicParams = () => config.serverPublicParams;
|
||||
|
|
|
@ -94,9 +94,12 @@ if (isTestElectron) {
|
|||
);
|
||||
contextBridge.exposeInMainWorld('getAppInstance', window.getAppInstance);
|
||||
contextBridge.exposeInMainWorld('getBuildCreation', window.getBuildCreation);
|
||||
contextBridge.exposeInMainWorld(
|
||||
'getBuildExpiration',
|
||||
window.getBuildExpiration
|
||||
);
|
||||
contextBridge.exposeInMainWorld('getConversations', window.getConversations);
|
||||
contextBridge.exposeInMainWorld('getEnvironment', window.getEnvironment);
|
||||
contextBridge.exposeInMainWorld('getExpiration', window.getExpiration);
|
||||
contextBridge.exposeInMainWorld('getHostName', window.getHostName);
|
||||
contextBridge.exposeInMainWorld(
|
||||
'getInteractionMode',
|
||||
|
|
Loading…
Reference in a new issue