Update link & sync UI

This commit is contained in:
trevor-signal 2024-12-19 15:46:50 -05:00 committed by GitHub
parent 1c933af6ce
commit 6f1d767c72
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 293 additions and 119 deletions

View file

@ -16,6 +16,9 @@ export default {
},
} satisfies ComponentMeta<Props>;
export function Spinning(args: Props): JSX.Element {
return <ProgressBar {...args} fractionComplete={null} />;
}
export function Zero(args: Props): JSX.Element {
return <ProgressBar {...args} />;
}

View file

@ -7,9 +7,16 @@ export function ProgressBar({
fractionComplete,
isRTL,
}: {
fractionComplete: number;
fractionComplete: number | null;
isRTL: boolean;
}): JSX.Element {
if (fractionComplete == null) {
return (
<div className="ProgressBar">
<div className="ProgressBar__fill ProgressBar__fill--spinning" />
</div>
);
}
return (
<div className="ProgressBar">
<div

View file

@ -1,7 +1,7 @@
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useCallback } from 'react';
import React, { useState, useCallback, useEffect } from 'react';
import type { Meta, StoryFn } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { setupI18n } from '../../util/setupI18n';
@ -73,11 +73,48 @@ const Template: StoryFn<PropsType> = (args: PropsType) => {
);
};
export const NoBytes = Template.bind({});
NoBytes.args = {
backupStep: InstallScreenBackupStep.Download,
currentBytes: undefined,
totalBytes: undefined,
export function FullFlow(): JSX.Element {
const [backupStep, setBackupStep] = useState<InstallScreenBackupStep>(
InstallScreenBackupStep.WaitForBackup
);
const [currentBytes, setCurrentBytes] = useState<number>(0);
const [totalBytes, setTotalBytes] = useState<number>(0);
const TOTAL_BYTES = 1024 * 1024;
useEffect(() => {
setTimeout(() => {
setBackupStep(InstallScreenBackupStep.Download);
setCurrentBytes(0);
setTotalBytes(TOTAL_BYTES);
for (let i = 0; i < 4; i += 1) {
setTimeout(() => {
setCurrentBytes(TOTAL_BYTES / (4 - i));
}, i * 900);
}
}, 1000);
}, [TOTAL_BYTES]);
return (
<InstallScreenBackupImportStep
i18n={i18n}
updates={DEFAULT_UPDATES}
currentVersion="v6.0.0"
OS="macOS"
startUpdate={action('startUpdate')}
forceUpdate={action('forceUpdate')}
onCancel={action('onCancel')}
onRetry={action('onRetry')}
currentBytes={currentBytes}
totalBytes={totalBytes}
backupStep={backupStep}
onRestartLink={action('onRestartLink')}
/>
);
}
export const Waiting = Template.bind({});
Waiting.args = {
backupStep: InstallScreenBackupStep.WaitForBackup,
};
export const Bytes = Template.bind({});

View file

@ -24,40 +24,50 @@ import { InstallScreenUpdateDialog } from './InstallScreenUpdateDialog';
// We can't always use destructuring assignment because of the complexity of this props
// type.
export type PropsType = Readonly<{
i18n: LocalizerType;
backupStep: InstallScreenBackupStep;
currentBytes?: number;
totalBytes?: number;
error?: InstallScreenBackupError;
onCancel: () => void;
onRetry: () => void;
onRestartLink: () => void;
export type PropsType = Readonly<
{
i18n: LocalizerType;
// Updater UI
updates: UpdatesStateType;
currentVersion: string;
OS: string;
startUpdate: () => void;
forceUpdate: () => void;
}>;
error?: InstallScreenBackupError;
onCancel: () => void;
onRetry: () => void;
onRestartLink: () => void;
export function InstallScreenBackupImportStep({
i18n,
backupStep,
currentBytes,
totalBytes,
error,
onCancel,
onRetry,
onRestartLink,
// Updater UI
updates: UpdatesStateType;
currentVersion: string;
OS: string;
startUpdate: () => void;
forceUpdate: () => void;
} & (
| {
backupStep: InstallScreenBackupStep.WaitForBackup;
}
| {
backupStep:
| InstallScreenBackupStep.Download
| InstallScreenBackupStep.Process;
currentBytes: number;
totalBytes: number;
}
)
>;
export function InstallScreenBackupImportStep(props: PropsType): JSX.Element {
const {
i18n,
backupStep,
error,
onCancel,
onRetry,
onRestartLink,
updates,
currentVersion,
OS,
startUpdate,
forceUpdate,
} = props;
updates,
currentVersion,
OS,
startUpdate,
forceUpdate,
}: PropsType): JSX.Element {
const [isConfirmingCancel, setIsConfirmingCancel] = useState(false);
const [isConfirmingSkip, setIsConfirmingSkip] = useState(false);
@ -92,52 +102,6 @@ export function InstallScreenBackupImportStep({
setIsConfirmingSkip(false);
}, [onRetry]);
let progress: JSX.Element;
if (currentBytes != null && totalBytes != null) {
const fractionComplete = roundFractionForProgressBar(
currentBytes / totalBytes
);
let hint: string;
if (backupStep === InstallScreenBackupStep.Download) {
hint = i18n('icu:BackupImportScreen__progressbar-hint', {
currentSize: formatFileSize(currentBytes),
totalSize: formatFileSize(totalBytes),
fractionComplete,
});
} else if (backupStep === InstallScreenBackupStep.Process) {
hint = i18n('icu:BackupImportScreen__progressbar-hint--processing');
} else {
throw missingCaseError(backupStep);
}
progress = (
<>
<ProgressBar
key={backupStep}
fractionComplete={fractionComplete}
isRTL={i18n.getLocaleDirection() === 'rtl'}
/>
<div className="InstallScreenBackupImportStep__progressbar-hint">
{hint}
</div>
</>
);
} else {
progress = (
<>
<ProgressBar
key={backupStep}
fractionComplete={0}
isRTL={i18n.getLocaleDirection() === 'rtl'}
/>
<div className="InstallScreenBackupImportStep__progressbar-hint">
{i18n('icu:BackupImportScreen__progressbar-hint--preparing')}
</div>
</>
);
}
const learnMoreLink = (parts: Array<string | JSX.Element>) => (
<a href={SYNCING_MESSAGES_SECURITY_URL} rel="noreferrer" target="_blank">
{parts}
@ -209,13 +173,12 @@ export function InstallScreenBackupImportStep({
return (
<div className="InstallScreenBackupImportStep">
<TitlebarDragArea />
<InstallScreenSignalLogo />
<div className="InstallScreenBackupImportStep__content">
<h3 className="InstallScreenBackupImportStep__title">
{i18n('icu:BackupImportScreen__title')}
</h3>
{progress}
<ProgressBarAndDescription {...props} />
<div className="InstallScreenBackupImportStep__description">
{i18n('icu:BackupImportScreen__description')}
</div>
@ -289,3 +252,76 @@ export function InstallScreenBackupImportStep({
</div>
);
}
type ProgressBarPropsType = Readonly<
{
i18n: LocalizerType;
} & (
| {
backupStep: InstallScreenBackupStep.WaitForBackup;
}
| {
backupStep:
| InstallScreenBackupStep.Download
| InstallScreenBackupStep.Process;
currentBytes: number;
totalBytes: number;
}
)
>;
function ProgressBarAndDescription(props: ProgressBarPropsType): JSX.Element {
const { backupStep, i18n } = props;
if (backupStep === InstallScreenBackupStep.WaitForBackup) {
return (
<>
<ProgressBar
fractionComplete={null}
isRTL={i18n.getLocaleDirection() === 'rtl'}
/>
<div className="InstallScreenBackupImportStep__progressbar-hint">
{i18n('icu:BackupImportScreen__progressbar-hint--preparing')}
</div>
</>
);
}
const { currentBytes, totalBytes } = props;
const fractionComplete = roundFractionForProgressBar(
currentBytes / totalBytes
);
if (backupStep === InstallScreenBackupStep.Download) {
return (
<>
<ProgressBar
fractionComplete={fractionComplete}
isRTL={i18n.getLocaleDirection() === 'rtl'}
/>
<div className="InstallScreenBackupImportStep__progressbar-hint">
{i18n('icu:BackupImportScreen__progressbar-hint', {
currentSize: formatFileSize(currentBytes),
totalSize: formatFileSize(totalBytes),
fractionComplete,
})}
</div>
</>
);
// eslint-disable-next-line no-else-return
} else if (backupStep === InstallScreenBackupStep.Process) {
return (
<>
<ProgressBar
fractionComplete={fractionComplete}
isRTL={i18n.getLocaleDirection() === 'rtl'}
/>
<div className="InstallScreenBackupImportStep__progressbar-hint">
{i18n('icu:BackupImportScreen__progressbar-hint--processing')}
</div>
</>
);
} else {
throw missingCaseError(backupStep);
}
}

View file

@ -5,5 +5,5 @@ import type { ReactElement } from 'react';
import React from 'react';
export function InstallScreenSignalLogo(): ReactElement {
return <div className="InstallScreenSignalLogo">Signal</div>;
return <div className="InstallScreenSignalLogo" />;
}