Redesign device link screens
This commit is contained in:
parent
a023fc1bb0
commit
364f00f37a
36 changed files with 1358 additions and 803 deletions
|
@ -8,7 +8,7 @@ import classNames from 'classnames';
|
|||
|
||||
import { AppViewType } from '../state/ducks/app';
|
||||
import { Inbox } from './Inbox';
|
||||
import { Install } from './Install';
|
||||
import { SmartInstallScreen } from '../state/smart/InstallScreen';
|
||||
import { StandaloneRegistration } from './StandaloneRegistration';
|
||||
import { ThemeType } from '../types/Util';
|
||||
import { usePageVisibility } from '../hooks/usePageVisibility';
|
||||
|
@ -50,7 +50,7 @@ export const App = ({
|
|||
let contents;
|
||||
|
||||
if (appView === AppViewType.Installer) {
|
||||
contents = <Install />;
|
||||
contents = <SmartInstallScreen />;
|
||||
} else if (appView === AppViewType.Standalone) {
|
||||
const onComplete = () => {
|
||||
window.removeSetupMenuItems();
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import { BackboneHost } from './BackboneHost';
|
||||
|
||||
export const Install = (): JSX.Element => {
|
||||
return (
|
||||
<BackboneHost
|
||||
className="full-screen-flow"
|
||||
View={window.Whisper.InstallView}
|
||||
/>
|
||||
);
|
||||
};
|
64
ts/components/InstallScreen.tsx
Normal file
64
ts/components/InstallScreen.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ComponentProps, ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
import { InstallScreenErrorStep } from './installScreen/InstallScreenErrorStep';
|
||||
import { InstallScreenChoosingDeviceNameStep } from './installScreen/InstallScreenChoosingDeviceNameStep';
|
||||
import { InstallScreenLinkInProgressStep } from './installScreen/InstallScreenLinkInProgressStep';
|
||||
import { InstallScreenQrCodeNotScannedStep } from './installScreen/InstallScreenQrCodeNotScannedStep';
|
||||
|
||||
export enum InstallScreenStep {
|
||||
Error,
|
||||
QrCodeNotScanned,
|
||||
ChoosingDeviceName,
|
||||
LinkInProgress,
|
||||
}
|
||||
|
||||
// We can't always use destructuring assignment because of the complexity of this props
|
||||
// type.
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
type PropsType =
|
||||
| {
|
||||
step: InstallScreenStep.Error;
|
||||
screenSpecificProps: ComponentProps<typeof InstallScreenErrorStep>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.QrCodeNotScanned;
|
||||
screenSpecificProps: ComponentProps<
|
||||
typeof InstallScreenQrCodeNotScannedStep
|
||||
>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.ChoosingDeviceName;
|
||||
screenSpecificProps: ComponentProps<
|
||||
typeof InstallScreenChoosingDeviceNameStep
|
||||
>;
|
||||
}
|
||||
| {
|
||||
step: InstallScreenStep.LinkInProgress;
|
||||
screenSpecificProps: ComponentProps<
|
||||
typeof InstallScreenLinkInProgressStep
|
||||
>;
|
||||
};
|
||||
|
||||
export function InstallScreen(props: Readonly<PropsType>): ReactElement {
|
||||
switch (props.step) {
|
||||
case InstallScreenStep.Error:
|
||||
return <InstallScreenErrorStep {...props.screenSpecificProps} />;
|
||||
case InstallScreenStep.QrCodeNotScanned:
|
||||
return (
|
||||
<InstallScreenQrCodeNotScannedStep {...props.screenSpecificProps} />
|
||||
);
|
||||
case InstallScreenStep.ChoosingDeviceName:
|
||||
return (
|
||||
<InstallScreenChoosingDeviceNameStep {...props.screenSpecificProps} />
|
||||
);
|
||||
case InstallScreenStep.LinkInProgress:
|
||||
return <InstallScreenLinkInProgressStep {...props.screenSpecificProps} />;
|
||||
default:
|
||||
throw missingCaseError(props);
|
||||
}
|
||||
}
|
9
ts/components/TitlebarDragArea.tsx
Normal file
9
ts/components/TitlebarDragArea.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export const TitlebarDragArea = (): ReactElement => (
|
||||
<div className="module-title-bar-drag-area" />
|
||||
);
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { InstallScreenChoosingDeviceNameStep } from './InstallScreenChoosingDeviceNameStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/InstallScreen/InstallScreenChoosingDeviceNameStep',
|
||||
module
|
||||
);
|
||||
|
||||
story.add('Default', () => {
|
||||
const Wrapper = () => {
|
||||
const [deviceName, setDeviceName] = useState<string>('Default value');
|
||||
|
||||
return (
|
||||
<InstallScreenChoosingDeviceNameStep
|
||||
i18n={i18n}
|
||||
deviceName={deviceName}
|
||||
setDeviceName={setDeviceName}
|
||||
onSubmit={action('onSubmit')}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return <Wrapper />;
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { normalizeDeviceName } from '../../util/normalizeDeviceName';
|
||||
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
|
||||
// This is the string's `.length`, which is the number of UTF-16 code points. Instead, we
|
||||
// want this to be either 50 graphemes or 256 encrypted bytes, whichever is smaller. See
|
||||
// DESKTOP-2844.
|
||||
export const MAX_DEVICE_NAME_LENGTH = 50;
|
||||
|
||||
type PropsType = {
|
||||
deviceName: string;
|
||||
i18n: LocalizerType;
|
||||
onSubmit: () => void;
|
||||
setDeviceName: (value: string) => void;
|
||||
};
|
||||
|
||||
export function InstallScreenChoosingDeviceNameStep({
|
||||
deviceName,
|
||||
i18n,
|
||||
onSubmit,
|
||||
setDeviceName,
|
||||
}: Readonly<PropsType>): ReactElement {
|
||||
const hasFocusedRef = useRef<boolean>(false);
|
||||
const focusRef = (el: null | HTMLElement) => {
|
||||
if (el) {
|
||||
el.focus();
|
||||
hasFocusedRef.current = true;
|
||||
}
|
||||
};
|
||||
|
||||
const normalizedName = normalizeDeviceName(deviceName);
|
||||
const canSubmit =
|
||||
normalizedName.length > 0 &&
|
||||
normalizedName.length <= MAX_DEVICE_NAME_LENGTH;
|
||||
|
||||
return (
|
||||
<form
|
||||
className="module-InstallScreenChoosingDeviceNameStep"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
onSubmit();
|
||||
}}
|
||||
>
|
||||
<TitlebarDragArea />
|
||||
|
||||
<InstallScreenSignalLogo />
|
||||
|
||||
<div className="module-InstallScreenChoosingDeviceNameStep__contents">
|
||||
<div className="module-InstallScreenChoosingDeviceNameStep__header">
|
||||
<h1>{i18n('chooseDeviceName')}</h1>
|
||||
<h2>{i18n('Install__choose-device-name__description')}</h2>
|
||||
</div>
|
||||
<div className="module-InstallScreenChoosingDeviceNameStep__inputs">
|
||||
<input
|
||||
className="module-InstallScreenChoosingDeviceNameStep__input"
|
||||
maxLength={MAX_DEVICE_NAME_LENGTH}
|
||||
onChange={event => {
|
||||
setDeviceName(event.target.value);
|
||||
}}
|
||||
placeholder={i18n('Install__choose-device-name__placeholder')}
|
||||
ref={focusRef}
|
||||
spellCheck={false}
|
||||
value={deviceName}
|
||||
/>
|
||||
<Button
|
||||
disabled={!canSubmit}
|
||||
variant={ButtonVariant.Primary}
|
||||
type="submit"
|
||||
>
|
||||
{i18n('finishLinkingPhone')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { InstallScreenErrorStep, InstallError } from './InstallScreenErrorStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/InstallScreen/InstallScreenErrorStep',
|
||||
module
|
||||
);
|
||||
|
||||
const defaultProps = {
|
||||
i18n,
|
||||
quit: action('quit'),
|
||||
tryAgain: action('tryAgain'),
|
||||
};
|
||||
|
||||
story.add('Too many devices', () => (
|
||||
<InstallScreenErrorStep
|
||||
{...defaultProps}
|
||||
error={InstallError.TooManyDevices}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Too old', () => (
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallError.TooOld} />
|
||||
));
|
||||
|
||||
story.add('Too old', () => (
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallError.TooOld} />
|
||||
));
|
||||
|
||||
story.add('Connection failed', () => (
|
||||
<InstallScreenErrorStep
|
||||
{...defaultProps}
|
||||
error={InstallError.ConnectionFailed}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Unknown error', () => (
|
||||
<InstallScreenErrorStep {...defaultProps} error={InstallError.UnknownError} />
|
||||
));
|
77
ts/components/installScreen/InstallScreenErrorStep.tsx
Normal file
77
ts/components/installScreen/InstallScreenErrorStep.tsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
|
||||
import { Button, ButtonVariant } from '../Button';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
|
||||
export enum InstallError {
|
||||
TooManyDevices,
|
||||
TooOld,
|
||||
ConnectionFailed,
|
||||
UnknownError,
|
||||
}
|
||||
|
||||
export function InstallScreenErrorStep({
|
||||
error,
|
||||
i18n,
|
||||
quit,
|
||||
tryAgain,
|
||||
}: Readonly<{
|
||||
error: InstallError;
|
||||
i18n: LocalizerType;
|
||||
quit: () => unknown;
|
||||
tryAgain: () => unknown;
|
||||
}>): ReactElement {
|
||||
let errorMessage: string;
|
||||
let buttonText = i18n('installTryAgain');
|
||||
let onClickButton = () => tryAgain();
|
||||
let shouldShowQuitButton = false;
|
||||
|
||||
switch (error) {
|
||||
case InstallError.TooManyDevices:
|
||||
errorMessage = i18n('installTooManyDevices');
|
||||
break;
|
||||
case InstallError.TooOld:
|
||||
errorMessage = i18n('installTooOld');
|
||||
buttonText = i18n('upgrade');
|
||||
onClickButton = () => {
|
||||
openLinkInWebBrowser('https://signal.org/download');
|
||||
};
|
||||
shouldShowQuitButton = true;
|
||||
break;
|
||||
case InstallError.ConnectionFailed:
|
||||
errorMessage = i18n('installConnectionFailed');
|
||||
break;
|
||||
case InstallError.UnknownError:
|
||||
errorMessage = i18n('installUnknownError');
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(error);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-InstallScreenErrorStep">
|
||||
<TitlebarDragArea />
|
||||
|
||||
<InstallScreenSignalLogo />
|
||||
|
||||
<h1>{i18n('installErrorHeader')}</h1>
|
||||
<h2>{errorMessage}</h2>
|
||||
|
||||
<div className="module-InstallScreenErrorStep__buttons">
|
||||
<Button onClick={onClickButton}>{buttonText}</Button>
|
||||
{shouldShowQuitButton && (
|
||||
<Button onClick={() => quit()} variant={ButtonVariant.Secondary}>
|
||||
{i18n('quit')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import { InstallScreenLinkInProgressStep } from './InstallScreenLinkInProgressStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/InstallScreen/InstallScreenLinkInProgressStep',
|
||||
module
|
||||
);
|
||||
|
||||
story.add('Default', () => <InstallScreenLinkInProgressStep i18n={i18n} />);
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { Spinner } from '../Spinner';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
|
||||
export const InstallScreenLinkInProgressStep = ({
|
||||
i18n,
|
||||
}: Readonly<{ i18n: LocalizerType }>): ReactElement => (
|
||||
<div className="module-InstallScreenLinkInProgressStep">
|
||||
<TitlebarDragArea />
|
||||
|
||||
<InstallScreenSignalLogo />
|
||||
|
||||
<Spinner size="50px" svgSize="normal" />
|
||||
<h1>{i18n('initialSync')}</h1>
|
||||
<h2>{i18n('initialSync__subtitle')}</h2>
|
||||
</div>
|
||||
);
|
|
@ -0,0 +1,91 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { setupI18n } from '../../util/setupI18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
import type { Loadable } from '../../util/loadable';
|
||||
import { LoadingState } from '../../util/loadable';
|
||||
import { InstallScreenQrCodeNotScannedStep } from './InstallScreenQrCodeNotScannedStep';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const story = storiesOf(
|
||||
'Components/InstallScreen/InstallScreenQrCodeNotScannedStep',
|
||||
module
|
||||
);
|
||||
|
||||
const Simulation = ({ finalResult }: { finalResult: Loadable<string> }) => {
|
||||
const [provisioningUrl, setProvisioningUrl] = useState<Loadable<string>>({
|
||||
loadingState: LoadingState.Loading,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timeout = setTimeout(() => {
|
||||
setProvisioningUrl(finalResult);
|
||||
}, 2000);
|
||||
return () => {
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}, [finalResult]);
|
||||
|
||||
return (
|
||||
<InstallScreenQrCodeNotScannedStep
|
||||
i18n={i18n}
|
||||
provisioningUrl={provisioningUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
story.add('QR code loading', () => (
|
||||
<InstallScreenQrCodeNotScannedStep
|
||||
i18n={i18n}
|
||||
provisioningUrl={{
|
||||
loadingState: LoadingState.Loading,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('QR code failed to load', () => (
|
||||
<InstallScreenQrCodeNotScannedStep
|
||||
i18n={i18n}
|
||||
provisioningUrl={{
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: new Error('uh oh'),
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('QR code loaded', () => (
|
||||
<InstallScreenQrCodeNotScannedStep
|
||||
i18n={i18n}
|
||||
provisioningUrl={{
|
||||
loadingState: LoadingState.Loaded,
|
||||
value:
|
||||
'https://example.com/fake-signal-link?uuid=56cdd548-e595-4962-9a27-3f1e8210a959&pub_key=SW4gdGhlIHZhc3QsIGRlZXAgZm9yZXN0IG9mIEh5cnVsZS4uLg%3D%3D',
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Simulated loading', () => (
|
||||
<Simulation
|
||||
finalResult={{
|
||||
loadingState: LoadingState.Loaded,
|
||||
value:
|
||||
'https://example.com/fake-signal-link?uuid=56cdd548-e595-4962-9a27-3f1e8210a959&pub_key=SW4gdGhlIHZhc3QsIGRlZXAgZm9yZXN0IG9mIEh5cnVsZS4uLg%3D%3D',
|
||||
}}
|
||||
/>
|
||||
));
|
||||
|
||||
story.add('Simulated failure', () => (
|
||||
<Simulation
|
||||
finalResult={{
|
||||
loadingState: LoadingState.LoadFailed,
|
||||
error: new Error('uh oh'),
|
||||
}}
|
||||
/>
|
||||
));
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import type { LocalizerType } from '../../types/Util';
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
import type { Loadable } from '../../util/loadable';
|
||||
import { LoadingState } from '../../util/loadable';
|
||||
|
||||
import { Intl } from '../Intl';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { TitlebarDragArea } from '../TitlebarDragArea';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
import { getClassNamesFor } from '../../util/getClassNamesFor';
|
||||
|
||||
// We can't always use destructuring assignment because of the complexity of this props
|
||||
// type.
|
||||
/* eslint-disable react/destructuring-assignment */
|
||||
type PropsType = {
|
||||
i18n: LocalizerType;
|
||||
provisioningUrl: Loadable<string>;
|
||||
};
|
||||
|
||||
// This should match the size in the CSS.
|
||||
const QR_CODE_SIZE = 256;
|
||||
const QR_CODE_FAILED_LINK =
|
||||
'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device';
|
||||
|
||||
const getQrCodeClassName = getClassNamesFor(
|
||||
'module-InstallScreenQrCodeNotScannedStep__qr-code'
|
||||
);
|
||||
|
||||
export const InstallScreenQrCodeNotScannedStep = ({
|
||||
i18n,
|
||||
provisioningUrl,
|
||||
}: Readonly<PropsType>): ReactElement => (
|
||||
<div className="module-InstallScreenQrCodeNotScannedStep">
|
||||
<TitlebarDragArea />
|
||||
|
||||
<InstallScreenSignalLogo />
|
||||
|
||||
<div className="module-InstallScreenQrCodeNotScannedStep__contents">
|
||||
<QrCode i18n={i18n} {...provisioningUrl} />
|
||||
<div className="module-InstallScreenQrCodeNotScannedStep__instructions">
|
||||
<h1>{i18n('Install__scan-this-code')}</h1>
|
||||
<ol>
|
||||
<li>{i18n('Install__instructions__1')}</li>
|
||||
<li>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="Install__instructions__2"
|
||||
components={{
|
||||
settings: (
|
||||
<strong>{i18n('Install__instructions__2__settings')}</strong>
|
||||
),
|
||||
linkedDevices: <strong>{i18n('linkedDevices')}</strong>,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="Install__instructions__3"
|
||||
components={{
|
||||
plusButton: (
|
||||
<div
|
||||
className="module-InstallScreenQrCodeNotScannedStep__android-plus"
|
||||
aria-label="+"
|
||||
/>
|
||||
),
|
||||
linkNewDevice: <strong>{i18n('linkNewDevice')}</strong>,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function QrCode(
|
||||
props: Loadable<string> & { i18n: LocalizerType }
|
||||
): ReactElement {
|
||||
const { i18n } = props;
|
||||
|
||||
const qrCodeElRef = useRef<null | HTMLDivElement>(null);
|
||||
|
||||
const valueToRender =
|
||||
props.loadingState === LoadingState.Loaded ? props.value : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
const qrCodeEl = qrCodeElRef.current;
|
||||
if (!qrCodeEl || !valueToRender) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
const qrCode = new window.QRCode(qrCodeEl, {
|
||||
text: valueToRender,
|
||||
width: QR_CODE_SIZE * window.devicePixelRatio,
|
||||
height: QR_CODE_SIZE * window.devicePixelRatio,
|
||||
});
|
||||
|
||||
return qrCode.clear.bind(qrCode);
|
||||
}, [valueToRender]);
|
||||
|
||||
let contents: ReactNode;
|
||||
switch (props.loadingState) {
|
||||
case LoadingState.Loading:
|
||||
contents = <Spinner size="24px" svgSize="small" />;
|
||||
break;
|
||||
case LoadingState.LoadFailed:
|
||||
contents = (
|
||||
<span className={classNames(getQrCodeClassName('__error-message'))}>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id="Install__qr-failed"
|
||||
components={[
|
||||
<a href={QR_CODE_FAILED_LINK}>
|
||||
{i18n('Install__qr-failed__learn-more')}
|
||||
</a>,
|
||||
]}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
break;
|
||||
case LoadingState.Loaded:
|
||||
contents = (
|
||||
<div className={getQrCodeClassName('__code')} ref={qrCodeElRef} />
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(props);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
getQrCodeClassName(''),
|
||||
props.loadingState === LoadingState.Loaded &&
|
||||
getQrCodeClassName('--loaded'),
|
||||
props.loadingState === LoadingState.LoadFailed &&
|
||||
getQrCodeClassName('--load-failed')
|
||||
)}
|
||||
>
|
||||
{contents}
|
||||
</div>
|
||||
);
|
||||
}
|
10
ts/components/installScreen/InstallScreenSignalLogo.tsx
Normal file
10
ts/components/installScreen/InstallScreenSignalLogo.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export const InstallScreenSignalLogo = (): ReactElement => (
|
||||
// Because "Signal" should be the same in every language, this is not localized.
|
||||
<div className="InstallScreenSignalLogo">Signal</div>
|
||||
);
|
Loading…
Add table
Add a link
Reference in a new issue