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

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