Unsupported OS Dialog

This commit is contained in:
Fedor Indutny 2023-01-18 15:31:10 -08:00 committed by GitHub
parent c6e184016b
commit ac50af52d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 776 additions and 224 deletions

View file

@ -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 youd like to chat with. If you change usernames youll 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 computers version of {OS} soon. To keep using Signal, update your computers 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 computers 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 computers 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"

View file

@ -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"
]

View file

@ -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:

View file

@ -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 {

View file

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

View file

@ -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) {

View file

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

View file

@ -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>

View file

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

View file

@ -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">

View file

@ -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 (
<>

View file

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

View file

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

View file

@ -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 (

View file

@ -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'),

View file

@ -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>

View file

@ -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: {

View file

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

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

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

View file

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

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

View file

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

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

View file

@ -108,3 +108,8 @@ export const getMenuOptions = createSelector(
getUser,
(state: UserStateType): MenuOptionsType => state.menuOptions
);
export const getIsMacOS = createSelector(
getPlatform,
(platform: string): boolean => platform === 'darwin'
);

View file

@ -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: () => (

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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',

View file

@ -41,5 +41,6 @@ export enum ToastType {
TapToViewExpiredOutgoing = 'TapToViewExpiredOutgoing',
UnableToLoadAttachment = 'UnableToLoadAttachment',
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
UnsupportedOS = 'UnsupportedOS',
UserAddedToGroup = 'UserAddedToGroup',
}

View file

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

View file

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

View file

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

View file

@ -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,

View file

@ -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
View file

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

View file

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

View file

@ -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',