// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ReactElement, ReactNode } from 'react'; import React, { useCallback, useState, useEffect } from 'react'; import classNames from 'classnames'; import { noop } from 'lodash'; import type { LocalizerType } from '../../types/Util'; import { InstallScreenStep, InstallScreenQRCodeError, } from '../../types/InstallScreen'; import { missingCaseError } from '../../util/missingCaseError'; import type { Loadable } from '../../util/loadable'; import { LoadingState } from '../../util/loadable'; import { drop } from '../../util/drop'; import { getEnvironment, Environment } from '../../environment'; import { I18n } from '../I18n'; import { Spinner } from '../Spinner'; import { BrandedQRCode } from '../BrandedQRCode'; import { TitlebarDragArea } from '../TitlebarDragArea'; import { InstallScreenSignalLogo } from './InstallScreenSignalLogo'; import { InstallScreenUpdateDialog } from './InstallScreenUpdateDialog'; import { getClassNamesFor } from '../../util/getClassNamesFor'; import type { UpdatesStateType } from '../../state/ducks/updates'; // We can't always use destructuring assignment because of the complexity of this props // type. export type PropsType = Readonly<{ i18n: LocalizerType; provisioningUrl: Loadable; hasExpired?: boolean; updates: UpdatesStateType; currentVersion: string; OS: string; isStaging: boolean; retryGetQrCode: () => void; startUpdate: () => void; forceUpdate: () => void; }>; const getQrCodeClassName = getClassNamesFor( 'module-InstallScreenQrCodeNotScannedStep__qr-code' ); const SUPPORT_PAGE = 'https://support.signal.org/hc/articles/360007320451#desktop_multiple_device'; export function InstallScreenQrCodeNotScannedStep({ currentVersion, hasExpired, i18n, isStaging, OS, provisioningUrl, retryGetQrCode, startUpdate, forceUpdate, updates, }: Readonly): ReactElement { return (
{hasExpired && ( )}

{i18n('icu:Install__scan-this-code')}

  1. {i18n('icu:Install__instructions__1')}
  2. {i18n('icu:Install__instructions__2__settings')} ), linkedDevices: {i18n('icu:linkedDevices')}, }} />
  3. {i18n('icu:linkNewDevice')}, }} />
{isStaging ? ( 'THIS IS A STAGING DESKTOP' ) : ( {i18n('icu:Install__support-link')} )}
); } function InstallScreenQrCode( props: Loadable & { i18n: LocalizerType; retryGetQrCode: () => void; } ): ReactElement { const { i18n, retryGetQrCode } = props; let contents: ReactNode; const loadError = props.loadingState === LoadingState.LoadFailed ? props.error : undefined; useEffect(() => { if (loadError !== InstallScreenQRCodeError.MaxRotations) { return noop; } const cleanup = () => { document.removeEventListener('visibilitychange', onVisibilityChange); }; const onVisibilityChange = () => { if (document.hidden) { return; } cleanup(); retryGetQrCode(); }; document.addEventListener('visibilitychange', onVisibilityChange); return cleanup; }, [retryGetQrCode, loadError]); let isJustButton = false; switch (props.loadingState) { case LoadingState.Loading: contents = ; break; case LoadingState.LoadFailed: switch (props.error) { case InstallScreenQRCodeError.Timeout: contents = ( <> {i18n('icu:Install__qr-failed-load__error--timeout')} {i18n('icu:Install__qr-failed-load__retry')} ); break; case InstallScreenQRCodeError.Unknown: contents = ( <> {i18n('icu:Install__qr-failed-load__retry')} ); break; case InstallScreenQRCodeError.NetworkIssue: contents = ( <> {i18n('icu:Install__qr-failed-load__error--network')} {i18n('icu:Install__qr-failed-load__get-help')} ); break; case InstallScreenQRCodeError.MaxRotations: isJustButton = true; contents = ( {i18n('icu:Install__qr-max-rotations__retry')} ); break; default: throw missingCaseError(props.error); } break; case LoadingState.Loaded: contents = ; break; default: throw missingCaseError(props); } return (
{contents}
); } function QRCodeImage({ i18n, link, }: { i18n: LocalizerType; link: string; }): JSX.Element { const [isCopying, setIsCopying] = useState(false); // Add a development-only feature to copy a QR code to the clipboard by double-clicking. // This can be used to quickly inspect the code, or to link this Desktop with an iOS // simulator primary, which has a debug-only option to paste the linking URL instead of // scanning it. (By the time you read this comment Android may have a similar feature.) const onDoubleClick = useCallback(() => { if (getEnvironment() === Environment.PackagedApp) { return; } drop(navigator.clipboard.writeText(link)); setIsCopying(true); }, [link]); useEffect(() => { if (!isCopying) { return noop; } const timer = setTimeout(() => { setIsCopying(false); }, 250); return () => clearTimeout(timer); }, [isCopying]); return ( ); } function RetryButton({ onClick, children, }: { onClick: () => void; children: ReactNode; }): JSX.Element { const onKeyDown = useCallback( (ev: React.KeyboardEvent) => { if (ev.key === 'Enter') { ev.preventDefault(); ev.stopPropagation(); onClick(); } }, [onClick] ); return ( ); } function Paragraph(children: React.ReactNode): JSX.Element { return

{children}

; }