Improvements to local backup UI

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
automated-signal
2026-05-04 15:22:12 -05:00
committed by GitHub
parent ec916dfcb0
commit a69f52d02f
6 changed files with 99 additions and 40 deletions
Binary file not shown.
+8 -2
View File
@@ -38,6 +38,8 @@ export const SIGNAL_BACKUPS_LEARN_MORE_URL =
const LOCAL_BACKUPS_PAGES = new Set([
SettingsPage.LocalBackups,
SettingsPage.LocalBackupsSetupKey,
SettingsPage.LocalBackupsSetupFolder,
SettingsPage.LocalBackupsKeyReference,
]);
@@ -291,13 +293,17 @@ export function PreferencesBackups({
disabled={isAuthPending}
onClick={async () => {
if (isLocalBackupsSetup) {
setSettingsLocation({ page: SettingsPage.LocalBackups });
setSettingsLocation({
page: SettingsPage.LocalBackups,
});
} else {
try {
setIsAuthPending(true);
const result = await promptOSAuth('enable-backups');
if (result === 'success' || result === 'unsupported') {
setSettingsLocation({ page: SettingsPage.LocalBackups });
setSettingsLocation({
page: SettingsPage.LocalBackupsSetupFolder,
});
}
} finally {
setIsAuthPending(false);
+84 -36
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
import type { ChangeEvent, JSX, MouseEvent } from 'react';
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { useCallback, useMemo, useState, useRef } from 'react';
import lodash from 'lodash';
import classNames from 'classnames';
@@ -85,24 +85,33 @@ export function PreferencesLocalBackups({
const [isShowingBackupKeyChangedModal, setIsShowingBackupKeyChangedModal] =
useState<boolean>(false);
if (!localBackupFolder) {
if (settingsLocation.page === SettingsPage.LocalBackupsSetupFolder) {
if (localBackupFolder) {
setSettingsLocation({ page: SettingsPage.LocalBackups });
}
return (
<LocalBackupsSetupFolderPicker
i18n={i18n}
pickLocalBackupFolder={pickLocalBackupFolder}
pickLocalBackupFolder={async () => {
const folder = await pickLocalBackupFolder();
if (folder) {
setSettingsLocation({ page: SettingsPage.LocalBackupsSetupKey });
}
}}
/>
);
}
const isReferencingBackupKey =
settingsLocation.page === SettingsPage.LocalBackupsKeyReference;
if (!previouslyViewedBackupKeyHash || isReferencingBackupKey) {
if (
settingsLocation.page === SettingsPage.LocalBackupsSetupKey ||
settingsLocation.page === SettingsPage.LocalBackupsKeyReference
) {
return (
<LocalBackupsBackupKeyViewer
backupKey={backupKey}
i18n={i18n}
isReferencing={
isReferencingBackupKey &&
settingsLocation.page === SettingsPage.LocalBackupsKeyReference &&
previouslyViewedBackupKeyHash === backupKeyHash
}
onBackupKeyViewed={() => {
@@ -116,6 +125,16 @@ export function PreferencesLocalBackups({
);
}
if (!localBackupFolder && !isDisablePending) {
setSettingsLocation({ page: SettingsPage.LocalBackupsSetupFolder });
return null;
}
if (!previouslyViewedBackupKeyHash && !isDisablePending) {
setSettingsLocation({ page: SettingsPage.LocalBackupsSetupKey });
return null;
}
async function showKeyReferenceWithAuth() {
setAuthError(undefined);
@@ -218,7 +237,11 @@ export function PreferencesLocalBackups({
<AxoButton.Root
variant="secondary"
size="lg"
onClick={() => openFileInFolder(localBackupFolder)}
onClick={() =>
localBackupFolder
? openFileInFolder(localBackupFolder)
: undefined
}
>
{showInFolderText}
</AxoButton.Root>
@@ -557,13 +580,10 @@ function LocalBackupsBackupKeyViewer({
);
const isStepViewOrReference = step === 'view' || step === 'reference';
const backupKeyForDisplay = useMemo(() => {
return backupKey
.replace(/\s/g, '')
.replace(/.{4}(?=.)/g, '$& ')
.toUpperCase();
}, [backupKey]);
const backupKeyForDisplay = useMemo(
() => formatBackupKeyForDisplay(backupKey, { convertAmbiguousChars: true }),
[backupKey]
);
const onCopyBackupKey = useCallback(
async function handleCopyBackupKey(e: MouseEvent) {
e.preventDefault();
@@ -645,8 +665,10 @@ function LocalBackupsBackupKeyViewer({
{step === 'caution' && (
<Modal
i18n={i18n}
modalName="CallingAdhocCallInfo.UnknownContactInfo"
modalName="LocalBackupsConfirmKeyModal"
moduleClassName="Preferences--LocalBackupsConfirmKeyModal"
noMouseClose
noEscapeClose
modalFooter={
<AxoButton.Root
variant="primary"
@@ -684,7 +706,8 @@ function LocalBackupsBackupKeyViewer({
<div className="Preferences--LocalBackupsSetupScreenPane">
<div className="Preferences--LocalBackupsSetupScreenPaneContent">
<LocalBackupsBackupKeyTextarea
backupKey={backupKeyForDisplay}
key={step === 'view' ? 'view' : 'reference'}
backupKeyForDisplay={backupKeyForDisplay}
i18n={i18n}
onValidate={(isValid: boolean) => setIsBackupKeyConfirmed(isValid)}
isStepViewOrReference={isStepViewOrReference}
@@ -715,13 +738,29 @@ function LocalBackupsBackupKeyViewer({
);
}
function formatBackupKeyForDisplay(
backupKey: string,
{ convertAmbiguousChars }: { convertAmbiguousChars: boolean }
): string {
const spacedAndUppercase = backupKey
.toUpperCase()
.replace(/\s/g, '')
.replace(/.{4}(?=.)/g, '$& ');
if (convertAmbiguousChars) {
return spacedAndUppercase.replace(/O/g, '#').replace(/0/g, '=');
}
return spacedAndUppercase;
}
function LocalBackupsBackupKeyTextarea({
backupKey,
backupKeyForDisplay,
i18n,
onValidate,
isStepViewOrReference,
}: {
backupKey: string;
backupKeyForDisplay: string;
i18n: LocalizerType;
onValidate: (isValid: boolean) => void;
isStepViewOrReference: boolean;
@@ -729,30 +768,39 @@ function LocalBackupsBackupKeyTextarea({
const backupKeyTextareaRef = useRef<HTMLTextAreaElement>(null);
const [backupKeyInput, setBackupKeyInput] = useState<string>('');
useEffect(() => {
if (backupKeyTextareaRef.current) {
backupKeyTextareaRef.current.focus();
}
}, [backupKeyTextareaRef, isStepViewOrReference]);
const backupKeyNoSpaces = useMemo(() => {
return backupKey.replace(/\s/g, '');
}, [backupKey]);
const handleTextareaChange = useCallback(
(ev: ChangeEvent<HTMLTextAreaElement>) => {
const { value } = ev.target;
const valueUppercaseNoSpaces = value.replace(/\s/g, '').toUpperCase();
const valueForUI = valueUppercaseNoSpaces.replace(/.{4}(?=.)/g, '$& ');
setBackupKeyInput(valueForUI);
onValidate(valueUppercaseNoSpaces === backupKeyNoSpaces);
const { selectionStart, value } = ev.target;
setBackupKeyInput(
formatBackupKeyForDisplay(value, { convertAmbiguousChars: false })
);
const currentCharIndex = value
.slice(0, selectionStart)
.replace(/\s/g, '').length;
const newCaretIndex =
currentCharIndex + Math.max(0, Math.floor((currentCharIndex - 1) / 4));
requestAnimationFrame(() => {
backupKeyTextareaRef.current?.setSelectionRange(
newCaretIndex,
newCaretIndex
);
});
onValidate(
formatBackupKeyForDisplay(value, { convertAmbiguousChars: true }) ===
backupKeyForDisplay
);
},
[backupKeyNoSpaces, onValidate]
[backupKeyForDisplay, onValidate]
);
return (
<textarea
aria-label={i18n('icu:Preferences--local-backups-recovery-key-text-box')}
autoFocus
className="Preferences--LocalBackupsBackupKey"
dir="ltr"
maxLength={79}
@@ -761,7 +809,7 @@ function LocalBackupsBackupKeyTextarea({
readOnly={isStepViewOrReference}
ref={backupKeyTextareaRef}
spellCheck="false"
value={isStepViewOrReference ? backupKey : backupKeyInput}
value={isStepViewOrReference ? backupKeyForDisplay : backupKeyInput}
/>
);
}
+1 -2
View File
@@ -70,9 +70,8 @@ export class Storage implements StorageInterface {
}
this.#items[key] = value;
await DataWriter.createOrUpdateItem({ id: key, value });
window.reduxActions?.items.putItemExternal(key, value);
await DataWriter.createOrUpdateItem({ id: key, value });
}
public async remove<K extends keyof Access>(key: K): Promise<void> {
+2
View File
@@ -90,6 +90,8 @@ export enum SettingsPage {
PNP = 'PNP',
BackupsDetails = 'BackupsDetails',
LocalBackups = 'LocalBackups',
LocalBackupsSetupFolder = 'LocalBackupsSetupFolder',
LocalBackupsSetupKey = 'LocalBackupsSetupKey',
LocalBackupsKeyReference = 'LocalBackupsKeyReference',
}
+4
View File
@@ -8,6 +8,8 @@ export type PreferencesBackupPage =
| SettingsPage.Backups
| SettingsPage.BackupsDetails
| SettingsPage.LocalBackups
| SettingsPage.LocalBackupsSetupFolder
| SettingsPage.LocalBackupsSetupKey
| SettingsPage.LocalBackupsKeyReference;
// Should be in sync with PreferencesBackupPage
@@ -18,6 +20,8 @@ export function isBackupPage(
page === SettingsPage.Backups ||
page === SettingsPage.BackupsDetails ||
page === SettingsPage.LocalBackups ||
page === SettingsPage.LocalBackupsSetupFolder ||
page === SettingsPage.LocalBackupsSetupKey ||
page === SettingsPage.LocalBackupsKeyReference
);
}