Show backup status in Settings window

This commit is contained in:
trevor-signal
2025-04-02 14:57:29 -04:00
committed by GitHub
parent 51647fef95
commit aba0e028d4
25 changed files with 1136 additions and 191 deletions

View File

@@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { AudioDevice } from '@signalapp/ringrtc';
import type { ReactNode } from 'react';
import React, {
useCallback,
useEffect,
@@ -12,7 +11,6 @@ import React, {
} from 'react';
import { noop, partition } from 'lodash';
import classNames from 'classnames';
import { v4 as uuid } from 'uuid';
import * as LocaleMatcher from '@formatjs/intl-localematcher';
import type { MediaDeviceSettings } from '../types/Calling';
@@ -41,10 +39,7 @@ import { Button, ButtonVariant } from './Button';
import { ChatColorPicker } from './ChatColorPicker';
import { Checkbox } from './Checkbox';
import { WidthBreakpoint } from './_util';
import {
CircleCheckbox,
Variant as CircleCheckboxVariant,
} from './CircleCheckbox';
import { ConfirmationDialog } from './ConfirmationDialog';
import { DisappearingTimeDialog } from './DisappearingTimeDialog';
import { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability';
@@ -70,6 +65,16 @@ import { assertDev } from '../util/assert';
import { I18n } from './I18n';
import { FunSkinTonesList } from './fun/FunSkinTones';
import { emojiParentKeyConstant, type EmojiSkinTone } from './fun/data/emojis';
import type {
BackupsSubscriptionType,
BackupStatusType,
} from '../types/backups';
import {
SettingsControl as Control,
SettingsRadio,
SettingsRow,
} from './PreferencesUtil';
import { PreferencesBackups } from './PreferencesBackups';
type CheckboxChangeHandlerType = (value: boolean) => unknown;
type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
@@ -77,7 +82,10 @@ type SelectChangeHandlerType<T = string | number> = (value: T) => unknown;
export type PropsDataType = {
// Settings
autoDownloadAttachment: AutoDownloadAttachmentType;
backupFeatureEnabled: boolean;
blockedCount: number;
cloudBackupStatus?: BackupStatusType;
backupSubscriptionStatus?: BackupsSubscriptionType;
customColors: Record<string, CustomColorType>;
defaultConversationColor: DefaultConversationColorType;
deviceName?: string;
@@ -105,6 +113,7 @@ export type PropsDataType = {
hasStoriesDisabled: boolean;
hasTextFormatting: boolean;
hasTypingIndicators: boolean;
initialPage?: Page;
lastSyncTime?: number;
notificationContent: NotificationSettingType;
phoneNumber: string | undefined;
@@ -152,6 +161,8 @@ type PropsFunctionType = {
colorId: string
) => Promise<Array<ConversationType>>;
makeSyncRequest: () => unknown;
refreshCloudBackupStatus: () => void;
refreshBackupSubscriptionStatus: () => void;
removeCustomColor: (colorId: string) => unknown;
removeCustomColorOnConversations: (colorId: string) => unknown;
resetAllChatColors: () => unknown;
@@ -210,7 +221,7 @@ export type PropsType = PropsDataType & PropsFunctionType;
export type PropsPreloadType = Omit<PropsType, 'i18n'>;
enum Page {
export enum Page {
// Accessible through left nav
General = 'General',
Appearance = 'Appearance',
@@ -219,6 +230,7 @@ enum Page {
Notifications = 'Notifications',
Privacy = 'Privacy',
DataUsage = 'DataUsage',
Backups = 'Backups',
// Sub pages
ChatColor = 'ChatColor',
@@ -260,8 +272,11 @@ export function Preferences({
availableLocales,
availableMicrophones,
availableSpeakers,
backupFeatureEnabled,
backupSubscriptionStatus,
blockedCount,
closeSettings,
cloudBackupStatus,
customColors,
defaultConversationColor,
deviceName = '',
@@ -294,6 +309,7 @@ export function Preferences({
hasTextFormatting,
hasTypingIndicators,
i18n,
initialPage = Page.General,
initialSpellCheckSetting,
isAutoDownloadUpdatesSupported,
isAutoLaunchSupported,
@@ -341,6 +357,8 @@ export function Preferences({
onZoomFactorChange,
phoneNumber = '',
preferredSystemLocales,
refreshCloudBackupStatus,
refreshBackupSubscriptionStatus,
removeCustomColor,
removeCustomColorOnConversations,
resetAllChatColors,
@@ -365,7 +383,7 @@ export function Preferences({
const [confirmDelete, setConfirmDelete] = useState(false);
const [confirmStoriesOff, setConfirmStoriesOff] = useState(false);
const [page, setPage] = useState<Page>(Page.General);
const [page, setPage] = useState<Page>(initialPage);
const [showSyncFailed, setShowSyncFailed] = useState(false);
const [nowSyncing, setNowSyncing] = useState(false);
const [showDisappearingTimerDialog, setShowDisappearingTimerDialog] =
@@ -385,6 +403,19 @@ export function Preferences({
setLanguageDialog(null);
setSelectedLanguageLocale(localeOverride);
}
const shouldShowBackupsPage =
backupFeatureEnabled && backupSubscriptionStatus != null;
if (page === Page.Backups && !shouldShowBackupsPage) {
setPage(Page.General);
}
useEffect(() => {
if (page === Page.Backups) {
refreshCloudBackupStatus();
refreshBackupSubscriptionStatus();
}
}, [page, refreshCloudBackupStatus, refreshBackupSubscriptionStatus]);
useEffect(() => {
doneRendering();
@@ -1687,6 +1718,15 @@ export function Preferences({
)}
</>
);
} else if (page === Page.Backups) {
settings = (
<PreferencesBackups
i18n={i18n}
cloudBackupStatus={cloudBackupStatus}
backupSubscriptionStatus={backupSubscriptionStatus}
locale={resolvedLocale}
/>
);
}
return (
@@ -1775,6 +1815,19 @@ export function Preferences({
>
{i18n('icu:Preferences__button--data-usage')}
</button>
{shouldShowBackupsPage ? (
<button
type="button"
className={classNames({
Preferences__button: true,
'Preferences__button--backups': true,
'Preferences__button--selected': page === Page.Backups,
})}
onClick={() => setPage(Page.Backups)}
>
{i18n('icu:Preferences__button--backups')}
</button>
) : null}
</div>
<div className="Preferences__settings-pane" ref={settingsPaneRef}>
{settings}
@@ -1796,113 +1849,6 @@ export function Preferences({
);
}
function SettingsRow({
children,
title,
className,
}: {
children: ReactNode;
title?: string;
className?: string;
}): JSX.Element {
return (
<fieldset className={classNames('Preferences__settings-row', className)}>
{title && <legend className="Preferences__padding">{title}</legend>}
{children}
</fieldset>
);
}
function Control({
icon,
left,
onClick,
right,
}: {
/** A className or `true` to leave room for icon */
icon?: string | true;
left: ReactNode;
onClick?: () => unknown;
right: ReactNode;
}): JSX.Element {
const content = (
<>
{icon && (
<div
className={classNames(
'Preferences__control--icon',
icon === true ? null : icon
)}
/>
)}
<div className="Preferences__control--key">{left}</div>
<div className="Preferences__control--value">{right}</div>
</>
);
if (onClick) {
return (
<button
className="Preferences__control Preferences__control--clickable"
type="button"
onClick={onClick}
>
{content}
</button>
);
}
return <div className="Preferences__control">{content}</div>;
}
type SettingsRadioOptionType<Enum> = Readonly<{
text: string;
value: Enum;
readOnly?: boolean;
onClick?: () => void;
}>;
function SettingsRadio<Enum>({
value,
options,
onChange,
}: {
value: Enum;
options: ReadonlyArray<SettingsRadioOptionType<Enum>>;
onChange: (value: Enum) => void;
}): JSX.Element {
const htmlIds = useMemo(() => {
return Array.from({ length: options.length }, () => uuid());
}, [options.length]);
return (
<div className="Preferences__padding">
{options.map(({ text, value: optionValue, readOnly, onClick }, i) => {
const htmlId = htmlIds[i];
return (
<label
className={classNames('Preferences__settings-radio__label', {
'Preferences__settings-radio__label--readonly': readOnly,
})}
key={htmlId}
htmlFor={htmlId}
>
<CircleCheckbox
isRadio
variant={CircleCheckboxVariant.Small}
id={htmlId}
checked={value === optionValue}
onClick={onClick}
onChange={readOnly ? noop : () => onChange(optionValue)}
/>
{text}
</label>
);
})}
</div>
);
}
function localizeDefault(i18n: LocalizerType, deviceLabel: string): string {
return deviceLabel.toLowerCase().startsWith('default')
? deviceLabel.replace(