Add backup media download progress to settings pane

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2025-07-09 11:50:17 -05:00 committed by GitHub
parent fd5b977190
commit de847714e2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 355 additions and 189 deletions

View file

@ -5565,11 +5565,11 @@
"description": "Text of the retry button of the error modal in the backup import screen"
},
"icu:BackupMediaDownloadProgress__title-in-progress": {
"messageformat": "Restoring media",
"messageformat": "Syncing media",
"description": "Label next to a progress bar showing active media (attachment) download progress after restoring from backup"
},
"icu:BackupMediaDownloadProgress__title-paused": {
"messageformat": "Restore paused",
"messageformat": "Syncing paused",
"description": "Label indicating media (attachment) download progress has been paused (due to user interaction)"
},
"icu:BackupMediaDownloadProgress__title-idle": {
@ -5589,15 +5589,15 @@
"description": "Description text when attachment download in progress but offline"
},
"icu:BackupMediaDownloadProgress__button-pause": {
"messageformat": "Pause transfer",
"messageformat": "Pause",
"description": "Text for button to pause media (attachment) download after backup impor"
},
"icu:BackupMediaDownloadProgress__button-resume": {
"messageformat": "Resume transfer",
"messageformat": "Resume",
"description": "Text for button to resume media (attachment) download after backup import"
},
"icu:BackupMediaDownloadProgress__button-cancel": {
"messageformat": "Cancel transfer",
"messageformat": "Cancel syncing",
"description": "Text for button to cancel (pause) media (attachment) download after backup import"
},
"icu:BackupMediaDownloadProgress__button-more": {
@ -5613,21 +5613,45 @@
"description": "Hint under the progressbar showing media (attachment) download progress after restoring from backup"
},
"icu:BackupMediaDownloadCancelConfirmation__title": {
"messageformat": "Cancel media transfer?",
"description": "Text for button to cancel (pause) media (attachment) download after backup import"
"messageformat": "Cancel media syncing?",
"description": "Text for button to cancel media (attachment) download after backup import"
},
"icu:BackupMediaDownloadCancelConfirmation__description": {
"messageformat": "Your messages and media have not completed restoring. If you choose to cancel, you can transfer again from Settings.",
"description": "Text for button to cancel (pause) media (attachment) download after backup import"
"messageformat": "Your media has not completed syncing. If you choose to cancel, you can sync again by re-linking this device. <learnMoreLink>Learn more</learnMoreLink>.",
"description": "Text for button to cancel media (attachment) download after backup import"
},
"icu:BackupMediaDownloadCancelConfirmation__button-continue": {
"messageformat": "Continue transfer",
"messageformat": "Continue syncing",
"description": "Text for button to close confirmation dialog and continue media (attachment) download"
},
"icu:BackupMediaDownloadCancelConfirmation__button-confirm-cancel": {
"messageformat": "Cancel transfer",
"messageformat": "Cancel syncing",
"description": "Text for button to confirm cancellation of media (attachment) download"
},
"icu:BackupMediaDownloadProgressSettings__paused--title": {
"messageformat": "Syncing paused",
"description": "Label indicating media (attachment) download progress when the transfer has been paused"
},
"icu:BackupMediaDownloadProgressSettings__paused--description": {
"messageformat": "Click “{resumeButtonText}” to continue syncing",
"description": "Shown below progress bar when the transfer has been paused. {resumeButtonText} will be 'icu:BackupMediaDownloadProgressSettings__button-resume'"
},
"icu:BackupMediaDownloadProgressSettings__button-resume": {
"messageformat": "Resume",
"description": "Text for button to resume media (attachment) download after backup import"
},
"icu:BackupMediaDownloadProgressSettings__button-pause": {
"messageformat": "Pause",
"description": "Text for button to pause media (attachment) download after backup import"
},
"icu:BackupMediaDownloadProgressSettings__title-in-progress": {
"messageformat": "Syncing media",
"description": "Label next to a progress bar showing active media (attachment) download progress after restoring from backup"
},
"icu:BackupMediaDownloadProgressSettings__progressbar-hint": {
"messageformat": "{currentSize} of {totalSize} ({fractionComplete, number, percent})...",
"description": "Hint under the progressbar in the backup import screen"
},
"icu:CriticalIdlePrimaryDeviceModal__title": {
"messageformat": "Account action required",
"description": "Title for modal shown when a user must use their idle primary device to retain their account"

View file

@ -138,4 +138,7 @@ button.BackupMediaDownloadProgress__button-close {
.BackupMediaDownloadCancelConfirmation {
min-width: 440px;
a {
text-decoration: none;
}
}

View file

@ -0,0 +1,31 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
@use '../mixins';
@use '../variables';
.BackupMediaDownloadProgressSettings {
display: flex;
flex-direction: row;
align-items: center;
gap: 20px;
&__buttons {
display: flex;
gap: 12px;
}
}
.BackupMediaDownloadProgressSettings__content {
display: flex;
flex-direction: column;
justify-content: center;
gap: 5px;
flex: 1;
}
.BackupMediaDownloadProgressSettings__description {
color: light-dark(variables.$color-gray-60, variables.$color-gray-25);
font-size: 12px;
line-height: 16px;
}

View file

@ -33,6 +33,7 @@
@use 'components/AvatarTextEditor.scss';
@use 'components/BackfillFailureModal.scss';
@use 'components/BackupMediaDownloadProgress.scss';
@use 'components/BackupMediaDownloadProgressSettings.scss';
@use 'components/BadgeCarouselIndex.scss';
@use 'components/BadgeDialog.scss';
@use 'components/BadgeSustainerInstructionsDialog.scss';

View file

@ -5,7 +5,10 @@ import React from 'react';
import { ConfirmationDialog } from './ConfirmationDialog';
import type { LocalizerType } from '../types/I18N';
import { I18n } from './I18n';
const BACKUP_AND_RESTORE_SUPPORT_PAGE =
'https://support.signal.org/hc/articles/360007059752-Backup-and-Restore-Messages';
export function BackupMediaDownloadCancelConfirmationDialog({
i18n,
handleConfirmCancel,
@ -15,6 +18,11 @@ export function BackupMediaDownloadCancelConfirmationDialog({
handleConfirmCancel: VoidFunction;
handleDialogClose: VoidFunction;
}): JSX.Element | null {
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
<a href={BACKUP_AND_RESTORE_SUPPORT_PAGE} rel="noreferrer" target="_blank">
{parts}
</a>
);
return (
<ConfirmationDialog
moduleClassName="BackupMediaDownloadCancelConfirmation"
@ -35,7 +43,13 @@ export function BackupMediaDownloadCancelConfirmationDialog({
onClose={handleDialogClose}
title={i18n('icu:BackupMediaDownloadCancelConfirmation__title')}
>
{i18n('icu:BackupMediaDownloadCancelConfirmation__description')}
<I18n
id="icu:BackupMediaDownloadCancelConfirmation__description"
i18n={i18n}
components={{
learnMoreLink,
}}
/>
</ConfirmationDialog>
);
}

View file

@ -0,0 +1,128 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState } from 'react';
import type { LocalizerType } from '../types/Util';
import { formatFileSize } from '../util/formatFileSize';
import { roundFractionForProgressBar } from '../util/numbers';
import { ProgressBar } from './ProgressBar';
import { Button, ButtonSize, ButtonVariant } from './Button';
import { BackupMediaDownloadCancelConfirmationDialog } from './BackupMediaDownloadCancelConfirmationDialog';
export type PropsType = Readonly<{
i18n: LocalizerType;
completedBytes: number;
totalBytes: number;
isPaused: boolean;
handleCancel: VoidFunction;
handleResume: VoidFunction;
handlePause: VoidFunction;
}>;
export function BackupMediaDownloadProgressSettings({
i18n,
completedBytes,
totalBytes,
isPaused,
handleCancel,
handleResume,
handlePause,
}: PropsType): JSX.Element | null {
const [isShowingCancelConfirmation, setIsShowingCancelConfirmation] =
useState(false);
const isRTL = i18n.getLocaleDirection() === 'rtl';
if (totalBytes <= 0) {
return null;
}
const fractionComplete = roundFractionForProgressBar(
completedBytes / totalBytes
);
const isCompleted = fractionComplete === 1;
if (isCompleted) {
return null;
}
let title: string;
let description: string;
let actionButton: JSX.Element | undefined;
if (isPaused) {
title = i18n('icu:BackupMediaDownloadProgressSettings__paused--title');
description = i18n(
'icu:BackupMediaDownloadProgressSettings__paused--description',
{
resumeButtonText: i18n(
'icu:BackupMediaDownloadProgressSettings__button-resume'
),
}
);
actionButton = (
<Button
onClick={handleResume}
variant={ButtonVariant.Secondary}
size={ButtonSize.Small}
className="BackupMediaDownloadProgressSettings__button"
>
{i18n('icu:BackupMediaDownloadProgressSettings__button-resume')}
</Button>
);
} else {
title = i18n('icu:BackupMediaDownloadProgressSettings__title-in-progress');
description = i18n(
'icu:BackupMediaDownloadProgressSettings__progressbar-hint',
{
currentSize: formatFileSize(completedBytes),
totalSize: formatFileSize(totalBytes),
fractionComplete,
}
);
actionButton = (
<Button
onClick={handlePause}
variant={ButtonVariant.Secondary}
size={ButtonSize.Small}
className="BackupMediaDownloadProgressSettings__button"
>
{i18n('icu:BackupMediaDownloadProgressSettings__button-pause')}
</Button>
);
}
return (
<div className="BackupMediaDownloadProgressSettings">
<div className="BackupMediaDownloadProgressSettings__content">
<div className="BackupMediaDownloadProgressSettings__title">
{title}
</div>
<div className="BackupMediaDownloadProgressSettings__ProgressBar">
<ProgressBar fractionComplete={fractionComplete} isRTL={isRTL} />
</div>
<div className="BackupMediaDownloadProgressSettings__description">
{description}
</div>
</div>
<div className="BackupMediaDownloadProgressSettings__buttons">
{actionButton}
<Button
onClick={() => setIsShowingCancelConfirmation(true)}
variant={ButtonVariant.SecondaryDestructive}
className="BackupMediaDownloadProgressSettings__button"
size={ButtonSize.Small}
>
{i18n('icu:cancel')}
</Button>
</div>
{isShowingCancelConfirmation ? (
<BackupMediaDownloadCancelConfirmationDialog
i18n={i18n}
handleConfirmCancel={handleCancel}
handleDialogClose={() => setIsShowingCancelConfirmation(false)}
/>
) : null}
</div>
);
}

View file

@ -10,20 +10,16 @@ import { PRODUCTION_DOWNLOAD_URL, BETA_DOWNLOAD_URL } from '../types/support';
import { I18n } from './I18n';
import { LeftPaneDialog } from './LeftPaneDialog';
import { formatFileSize } from '../util/formatFileSize';
import { getLocalizedUrl } from '../util/getLocalizedUrl';
import type { LocalizerType } from '../types/Util';
import type { DismissOptions } from './LeftPaneDialog';
import type { WidthBreakpoint } from './_util';
function contactSupportLink(parts: ReactNode): JSX.Element {
const localizedSupportLink = getLocalizedUrl(
'https://support.signal.org/hc/LOCALE/requests/new?desktop'
);
return (
<a
key="signal-support"
href={localizedSupportLink}
href="https://support.signal.org/hc/requests/new?desktop"
rel="noreferrer"
target="_blank"
>

View file

@ -415,6 +415,9 @@ export default {
removeCustomColorOnConversations: action(
'removeCustomColorOnConversations'
),
resumeBackupMediaDownload: action('resumeBackupMediaDownload'),
pauseBackupMediaDownload: action('pauseBackupMediaDownload'),
cancelBackupMediaDownload: action('cancelBackupMediaDownload'),
resetAllChatColors: action('resetAllChatColors'),
resetDefaultChatColor: action('resetDefaultChatColor'),
savePreferredLeftPaneWidth: action('savePreferredLeftPaneWidth'),
@ -528,6 +531,55 @@ PNPDiscoverabilityDisabled.args = {
page: Page.PNP,
};
export const BackupsMediaDownloadActive = Template.bind({});
BackupsMediaDownloadActive.args = {
page: Page.BackupsDetails,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
cloudBackupStatus: {
protoSize: 100_000_000,
createdTimestamp: Date.now() - WEEK,
},
backupSubscriptionStatus: {
status: 'active',
cost: {
amount: 22.99,
currencyCode: 'USD',
},
renewalTimestamp: Date.now() + 20 * DAY,
},
backupMediaDownloadStatus: {
completedBytes: 123_456_789,
totalBytes: 987_654_321,
isPaused: false,
isIdle: false,
},
};
export const BackupsMediaDownloadPaused = Template.bind({});
BackupsMediaDownloadPaused.args = {
page: Page.BackupsDetails,
backupFeatureEnabled: true,
backupLocalBackupsEnabled: true,
cloudBackupStatus: {
protoSize: 100_000_000,
createdTimestamp: Date.now() - WEEK,
},
backupSubscriptionStatus: {
status: 'active',
cost: {
amount: 22.99,
currencyCode: 'USD',
},
renewalTimestamp: Date.now() + 20 * DAY,
},
backupMediaDownloadStatus: {
completedBytes: 123_456_789,
totalBytes: 987_654_321,
isPaused: true,
isIdle: false,
},
};
export const BackupsPaidActive = Template.bind({});
BackupsPaidActive.args = {
page: Page.Backups,

View file

@ -74,6 +74,7 @@ import type {
ThemeType,
} from '../types/Util';
import type {
BackupMediaDownloadStatusType,
BackupsSubscriptionType,
BackupStatusType,
} from '../types/backups';
@ -118,6 +119,10 @@ export type PropsDataType = {
localBackupFolder: string | undefined;
cloudBackupStatus?: BackupStatusType;
backupSubscriptionStatus: BackupsSubscriptionType;
backupMediaDownloadStatus?: BackupMediaDownloadStatusType;
pauseBackupMediaDownload: VoidFunction;
cancelBackupMediaDownload: VoidFunction;
resumeBackupMediaDownload: VoidFunction;
blockedCount: number;
customColors: Record<string, CustomColorType>;
defaultConversationColor: DefaultConversationColorType;
@ -225,6 +230,8 @@ type PropsFunctionType = {
getMessageSampleForSchemaVersion: (
version: number
) => Promise<Array<MessageAttributesType>>;
resumeBackupMediaDownload: () => void;
pauseBackupMediaDownload: () => void;
getConversationsWithCustomColor: (colorId: string) => Array<ConversationType>;
getPreferredBadge: PreferredBadgeSelectorType;
makeSyncRequest: () => unknown;
@ -386,6 +393,10 @@ export function Preferences({
availableMicrophones,
availableSpeakers,
backupFeatureEnabled,
backupMediaDownloadStatus,
pauseBackupMediaDownload,
resumeBackupMediaDownload,
cancelBackupMediaDownload,
backupKeyViewed,
backupSubscriptionStatus,
backupLocalBackupsEnabled,
@ -2160,6 +2171,10 @@ export function Preferences({
accountEntropyPool={accountEntropyPool}
backupKeyViewed={backupKeyViewed}
backupSubscriptionStatus={backupSubscriptionStatus}
backupMediaDownloadStatus={backupMediaDownloadStatus}
cancelBackupMediaDownload={cancelBackupMediaDownload}
pauseBackupMediaDownload={pauseBackupMediaDownload}
resumeBackupMediaDownload={resumeBackupMediaDownload}
cloudBackupStatus={cloudBackupStatus}
i18n={i18n}
locale={resolvedLocale}

View file

@ -5,6 +5,7 @@ import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import type {
BackupMediaDownloadStatusType,
BackupsSubscriptionType,
BackupStatusType,
} from '../types/backups';
@ -28,6 +29,7 @@ import type {
PromptOSAuthResultType,
} from '../util/os/promptOSAuthMain';
import { ConfirmationDialog } from './ConfirmationDialog';
import { BackupMediaDownloadProgressSettings } from './BackupMediaDownloadProgressSettings';
export const SIGNAL_BACKUPS_LEARN_MORE_URL =
'https://support.signal.org/hc/articles/360007059752-Backup-and-Restore-Messages';
@ -42,6 +44,10 @@ export function PreferencesBackups({
localBackupFolder,
onBackupKeyViewedChange,
pickLocalBackupFolder,
backupMediaDownloadStatus,
cancelBackupMediaDownload,
pauseBackupMediaDownload,
resumeBackupMediaDownload,
page,
promptOSAuth,
refreshCloudBackupStatus,
@ -58,6 +64,10 @@ export function PreferencesBackups({
locale: string;
onBackupKeyViewedChange: (keyViewed: boolean) => void;
page: PreferencesBackupPage;
backupMediaDownloadStatus: BackupMediaDownloadStatusType | undefined;
cancelBackupMediaDownload: () => void;
pauseBackupMediaDownload: () => void;
resumeBackupMediaDownload: () => void;
pickLocalBackupFolder: () => Promise<string | undefined>;
promptOSAuth: (
reason: PromptOSAuthReasonType
@ -90,6 +100,10 @@ export function PreferencesBackups({
i18n={i18n}
cloudBackupStatus={cloudBackupStatus}
backupSubscriptionStatus={backupSubscriptionStatus}
backupMediaDownloadStatus={backupMediaDownloadStatus}
cancelBackupMediaDownload={cancelBackupMediaDownload}
pauseBackupMediaDownload={pauseBackupMediaDownload}
resumeBackupMediaDownload={resumeBackupMediaDownload}
locale={locale}
/>
);
@ -468,12 +482,25 @@ function BackupsDetailsPage({
backupSubscriptionStatus,
i18n,
locale,
cancelBackupMediaDownload,
pauseBackupMediaDownload,
resumeBackupMediaDownload,
backupMediaDownloadStatus,
}: {
cloudBackupStatus?: BackupStatusType;
backupSubscriptionStatus: BackupsSubscriptionType;
i18n: LocalizerType;
locale: string;
cancelBackupMediaDownload: () => void;
pauseBackupMediaDownload: () => void;
resumeBackupMediaDownload: () => void;
backupMediaDownloadStatus?: BackupMediaDownloadStatusType;
}): JSX.Element {
const shouldShowMediaProgress =
backupMediaDownloadStatus &&
backupMediaDownloadStatus.completedBytes <
backupMediaDownloadStatus.totalBytes;
return (
<>
<div className="Preferences--backups-summary__container">
@ -484,12 +511,12 @@ function BackupsDetailsPage({
})}
</div>
{cloudBackupStatus ? (
{cloudBackupStatus || shouldShowMediaProgress ? (
<SettingsRow
className="Preferences--backup-details"
title={i18n('icu:Preferences--backup-details__header')}
>
{cloudBackupStatus.createdTimestamp ? (
{cloudBackupStatus?.createdTimestamp ? (
<div className="Preferences--backup-details__row">
<label>{i18n('icu:Preferences--backup-created-at__label')}</label>
<div
@ -506,6 +533,17 @@ function BackupsDetailsPage({
</div>
</div>
) : null}
{shouldShowMediaProgress && backupMediaDownloadStatus ? (
<div className="Preferences--backup-details__row">
<BackupMediaDownloadProgressSettings
{...backupMediaDownloadStatus}
handleCancel={cancelBackupMediaDownload}
handlePause={pauseBackupMediaDownload}
handleResume={resumeBackupMediaDownload}
i18n={i18n}
/>
</div>
) : null}
</SettingsRow>
) : null}
</>

View file

@ -10,7 +10,6 @@ import { Button, ButtonSize, ButtonVariant } from '../Button';
import { SystemMessage } from './SystemMessage';
import { ChatSessionRefreshedDialog } from './ChatSessionRefreshedDialog';
import { openLinkInWebBrowser } from '../../util/openLinkInWebBrowser';
import { getLocalizedUrl } from '../../util/getLocalizedUrl';
type PropsHousekeepingType = {
i18n: LocalizerType;
@ -34,9 +33,8 @@ export function ChatSessionRefreshedNotification(
const wrappedContactSupport = useCallback(() => {
setIsDialogOpen(false);
const url = getLocalizedUrl(
'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed'
);
const url =
'https://support.signal.org/hc/requests/new?desktop&chat_refreshed';
openLinkInWebBrowser(url);
}, [setIsDialogOpen]);

View file

@ -15,7 +15,6 @@ import type { SmartChooseGroupMembersModalPropsType } from '../../../state/smart
import type { SmartConfirmAdditionsModalPropsType } from '../../../state/smart/ConfirmAdditionsModal';
import { assertDev } from '../../../util/assert';
import { getMutedUntilText } from '../../../util/getMutedUntilText';
import { getLocalizedUrl } from '../../../util/getLocalizedUrl';
import type { LocalizerType, ThemeType } from '../../../types/Util';
import type { BadgeType } from '../../../badges/types';
@ -521,9 +520,7 @@ export function ConversationDetails({
}
label={i18n('icu:ConversationDetails--support-center')}
onClick={() => {
openLinkInWebBrowser(
getLocalizedUrl('https://support.signal.org/hc/LOCALE')
);
openLinkInWebBrowser('https://support.signal.org');
}}
/>
<PanelRow
@ -536,9 +533,7 @@ export function ConversationDetails({
label={i18n('icu:contactUs')}
onClick={() => {
openLinkInWebBrowser(
getLocalizedUrl(
'https://support.signal.org/hc/LOCALE/requests/new?desktop'
)
'https://support.signal.org/hc/requests/new?desktop'
);
}}
/>

View file

@ -79,6 +79,11 @@ import type { WidthBreakpoint } from '../../components/_util';
import { DialogType } from '../../types/Dialogs';
import { promptOSAuth } from '../../util/promptOSAuth';
import type { StateType } from '../reducer';
import {
pauseBackupMediaDownload,
resumeBackupMediaDownload,
cancelBackupMediaDownload,
} from '../../util/backupMediaDownload';
const DEFAULT_NOTIFICATION_SETTING = 'message';
@ -490,8 +495,15 @@ export function SmartPreferences(): JSX.Element | null {
// Simple, one-way items
const { backupSubscriptionStatus, cloudBackupStatus, localBackupFolder } =
items;
const {
backupSubscriptionStatus,
cloudBackupStatus,
localBackupFolder,
backupMediaDownloadCompletedBytes,
backupMediaDownloadTotalBytes,
attachmentDownloadManagerIdled,
backupMediaDownloadPaused,
} = items;
const defaultConversationColor =
items.defaultConversationColor || DEFAULT_CONVERSATION_COLOR;
const hasLinkPreviews = items.linkPreviews ?? false;
@ -712,6 +724,12 @@ export function SmartPreferences(): JSX.Element | null {
backupFeatureEnabled={backupFeatureEnabled}
backupKeyViewed={backupKeyViewed}
backupSubscriptionStatus={backupSubscriptionStatus ?? { status: 'off' }}
backupMediaDownloadStatus={{
completedBytes: backupMediaDownloadCompletedBytes ?? 0,
totalBytes: backupMediaDownloadTotalBytes ?? 0,
isPaused: Boolean(backupMediaDownloadPaused),
isIdle: Boolean(attachmentDownloadManagerIdled),
}}
backupLocalBackupsEnabled={backupLocalBackupsEnabled}
badge={badge}
blockedCount={blockedCount}
@ -837,6 +855,9 @@ export function SmartPreferences(): JSX.Element | null {
resetDefaultChatColor={resetDefaultChatColor}
resolvedLocale={resolvedLocale}
savePreferredLeftPaneWidth={savePreferredLeftPaneWidth}
resumeBackupMediaDownload={resumeBackupMediaDownload}
pauseBackupMediaDownload={pauseBackupMediaDownload}
cancelBackupMediaDownload={cancelBackupMediaDownload}
selectedCamera={selectedCamera}
selectedMicrophone={selectedMicrophone}
selectedSpeaker={selectedSpeaker}

View file

@ -40,6 +40,13 @@ export type BackupStatusType = {
protoSize?: number;
};
export type BackupMediaDownloadStatusType = {
totalBytes: number;
completedBytes: number;
isPaused: boolean;
isIdle: boolean;
};
export type BackupsSubscriptionType =
| {
status: 'off' | 'not-found' | 'expired';

View file

@ -1,26 +0,0 @@
// Copyright 2025 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { mapToSupportLocale } from './mapToSupportLocale';
/**
* Ensures the provided string contains "LOCALE".
* If not, produces a readable TypeScript error.
*/
type RequiresLocale<T extends string> = T extends `${string}LOCALE${string}`
? T
: `Error: The URL must contain "LOCALE" but got "${T}"`;
/**
* Replaces "LOCALE" in a URL with the appropriate localized support locale.
*
* @param url The URL string containing "LOCALE" to be replaced
* @returns The URL with "LOCALE" replaced with the appropriate locale
*/
export function getLocalizedUrl<T extends string>(
url: RequiresLocale<T>
): string {
const locale = window.SignalContext.getResolvedMessagesLocale();
const supportLocale = mapToSupportLocale(locale);
return url.replace('LOCALE', supportLocale);
}

View file

@ -1,131 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export type SupportLocaleType =
| 'ar'
| 'de'
| 'en-us'
| 'es'
| 'fr'
| 'it'
| 'ja'
| 'pl'
| 'pt-br'
| 'ru'
| 'sq'
| 'zh-tw';
// See https://source.chromium.org/chromium/chromium/src/+/main:ui/base/l10n/l10n_util.cc
export type ElectronLocaleType =
| 'af'
| 'ar'
| 'bg'
| 'bn'
| 'ca'
| 'cs'
| 'cy'
| 'da'
| 'de'
| 'de-AT'
| 'de-CH'
| 'de-DE'
| 'de-LI'
| 'el'
| 'en'
| 'en-AU'
| 'en-CA'
| 'en-GB'
| 'en-GB-oxendict'
| 'en-IN'
| 'en-NZ'
| 'en-US'
| 'eo'
| 'es'
| 'es-419'
| 'et'
| 'eu'
| 'fa'
| 'fi'
| 'fr'
| 'fr-CA'
| 'fr-CH'
| 'fr-FR'
| 'he'
| 'hi'
| 'hr'
| 'hu'
| 'id'
| 'it'
| 'it-CH'
| 'it-IT'
| 'ja'
| 'km'
| 'kn'
| 'ko'
| 'lt'
| 'mk'
| 'mr'
| 'ms'
| 'nb'
| 'nl'
| 'nn'
| 'no'
| 'pl'
| 'pt-BR'
| 'pt-PT'
| 'ro'
| 'ru'
| 'sk'
| 'sl'
| 'sq'
| 'sr'
| 'sv'
| 'sw'
| 'ta'
| 'te'
| 'th'
| 'tr'
| 'uk'
| 'ur'
| 'vi'
| 'zh-CN'
| 'zh-HK'
| 'zh-TW';
export function mapToSupportLocale(ourLocale: string): SupportLocaleType {
if (ourLocale === 'ar') {
return ourLocale;
}
if (ourLocale === 'de') {
return ourLocale;
}
if (ourLocale === 'es') {
return ourLocale;
}
if (ourLocale === 'fr') {
return ourLocale;
}
if (ourLocale === 'it') {
return ourLocale;
}
if (ourLocale === 'ja') {
return ourLocale;
}
if (ourLocale === 'pl') {
return ourLocale;
}
if (ourLocale === 'pt-BR') {
return 'pt-br';
}
if (ourLocale === 'ru') {
return ourLocale;
}
if (ourLocale === 'sq') {
return ourLocale;
}
if (ourLocale === 'zh-TW') {
return 'zh-tw';
}
return 'en-us';
}