Pause, cancel & resume backup media download
This commit is contained in:
parent
65539b1419
commit
028a3f3ef0
28 changed files with 958 additions and 141 deletions
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
|
||||
export function BackupMediaDownloadCancelConfirmationDialog({
|
||||
i18n,
|
||||
handleConfirmCancel,
|
||||
handleDialogClose,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
handleConfirmCancel: VoidFunction;
|
||||
handleDialogClose: VoidFunction;
|
||||
}): JSX.Element | null {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
moduleClassName="BackupMediaDownloadCancelConfirmation"
|
||||
dialogName="BackupMediaDownloadCancelConfirmation"
|
||||
cancelText={i18n(
|
||||
'icu:BackupMediaDownloadCancelConfirmation__button-continue'
|
||||
)}
|
||||
actions={[
|
||||
{
|
||||
text: i18n(
|
||||
'icu:BackupMediaDownloadCancelConfirmation__button-confirm-cancel'
|
||||
),
|
||||
action: handleConfirmCancel,
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
i18n={i18n}
|
||||
onClose={handleDialogClose}
|
||||
title={i18n('icu:BackupMediaDownloadCancelConfirmation__title')}
|
||||
>
|
||||
{i18n('icu:BackupMediaDownloadCancelConfirmation__description')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
|
@ -2,26 +2,65 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { type ComponentProps } from 'react';
|
||||
import type { Meta, StoryFn } from '@storybook/react';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { setupI18n } from '../util/setupI18n';
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
import { BackupMediaDownloadProgressBanner } from './BackupMediaDownloadProgress';
|
||||
import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress';
|
||||
import { KIBIBYTE } from '../types/AttachmentSize';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
type PropsType = ComponentProps<typeof BackupMediaDownloadProgressBanner>;
|
||||
type PropsType = ComponentProps<typeof BackupMediaDownloadProgress>;
|
||||
|
||||
export default {
|
||||
title: 'Components/BackupMediaDownloadProgress',
|
||||
args: {
|
||||
isPaused: false,
|
||||
downloadedBytes: 600 * KIBIBYTE,
|
||||
totalBytes: 1000 * KIBIBYTE,
|
||||
handleClose: action('handleClose'),
|
||||
handlePause: action('handlePause'),
|
||||
handleResume: action('handleResume'),
|
||||
handleCancel: action('handleCancel'),
|
||||
i18n,
|
||||
},
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
// eslint-disable-next-line react/function-component-definition
|
||||
const Template: StoryFn<PropsType> = (args: PropsType) => (
|
||||
<BackupMediaDownloadProgressBanner {...args} i18n={i18n} />
|
||||
);
|
||||
export function InProgress(args: PropsType): JSX.Element {
|
||||
return <BackupMediaDownloadProgress {...args} />;
|
||||
}
|
||||
|
||||
export const InProgress = Template.bind({});
|
||||
InProgress.args = {
|
||||
downloadedBytes: 92048023,
|
||||
totalBytes: 1024102532,
|
||||
};
|
||||
export function Increasing(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<BackupMediaDownloadProgress
|
||||
{...args}
|
||||
{...useIncreasingFractionComplete()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Paused(args: PropsType): JSX.Element {
|
||||
return <BackupMediaDownloadProgress {...args} isPaused />;
|
||||
}
|
||||
|
||||
export function Complete(args: PropsType): JSX.Element {
|
||||
return (
|
||||
<BackupMediaDownloadProgress {...args} downloadedBytes={args.totalBytes} />
|
||||
);
|
||||
}
|
||||
|
||||
function useIncreasingFractionComplete() {
|
||||
const [fractionComplete, setFractionComplete] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
if (fractionComplete >= 1) {
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
setFractionComplete(cur => Math.min(1, cur + 0.1));
|
||||
}, 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [fractionComplete]);
|
||||
return { downloadedBytes: 1e10 * fractionComplete, totalBytes: 1e10 };
|
||||
}
|
||||
|
|
|
@ -1,48 +1,160 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import type { LocalizerType } from '../types/Util';
|
||||
import { formatFileSize } from '../util/formatFileSize';
|
||||
import { ProgressBar } from './ProgressBar';
|
||||
import { roundFractionForProgressBar } from '../util/numbers';
|
||||
import { ProgressCircle } from './ProgressCircle';
|
||||
import { ContextMenu } from './ContextMenu';
|
||||
import { BackupMediaDownloadCancelConfirmationDialog } from './BackupMediaDownloadCancelConfirmationDialog';
|
||||
|
||||
export type PropsType = Readonly<{
|
||||
i18n: LocalizerType;
|
||||
downloadedBytes: number;
|
||||
totalBytes: number;
|
||||
isPaused: boolean;
|
||||
handleCancel: VoidFunction;
|
||||
handleClose: VoidFunction;
|
||||
handleResume: VoidFunction;
|
||||
handlePause: VoidFunction;
|
||||
}>;
|
||||
|
||||
export function BackupMediaDownloadProgressBanner({
|
||||
export function BackupMediaDownloadProgress({
|
||||
i18n,
|
||||
downloadedBytes,
|
||||
totalBytes,
|
||||
isPaused,
|
||||
handleCancel: handleConfirmedCancel,
|
||||
handleClose,
|
||||
handleResume,
|
||||
handlePause,
|
||||
}: PropsType): JSX.Element | null {
|
||||
const [isShowingCancelConfirmation, setIsShowingCancelConfirmation] =
|
||||
useState(false);
|
||||
if (totalBytes === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const fractionComplete = Math.max(
|
||||
0,
|
||||
Math.min(1, downloadedBytes / totalBytes)
|
||||
function handleCancel() {
|
||||
setIsShowingCancelConfirmation(true);
|
||||
}
|
||||
|
||||
const fractionComplete = roundFractionForProgressBar(
|
||||
downloadedBytes / totalBytes
|
||||
);
|
||||
|
||||
let content: JSX.Element | undefined;
|
||||
let icon: JSX.Element | undefined;
|
||||
let actionButton: JSX.Element | undefined;
|
||||
if (fractionComplete === 1) {
|
||||
icon = <div className="BackupMediaDownloadProgress__icon--complete" />;
|
||||
content = (
|
||||
<>
|
||||
<div className="BackupMediaDownloadProgress__title">
|
||||
{i18n('icu:BackupMediaDownloadProgress__title-complete')}
|
||||
</div>
|
||||
<div className="BackupMediaDownloadProgress__progressbar-hint">
|
||||
{formatFileSize(downloadedBytes)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
actionButton = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClose}
|
||||
className="BackupMediaDownloadProgress__button-close"
|
||||
aria-label={i18n('icu:close')}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
icon = <ProgressCircle fractionComplete={fractionComplete} />;
|
||||
|
||||
if (isPaused) {
|
||||
content = (
|
||||
<>
|
||||
<div className="BackupMediaDownloadProgress__title">
|
||||
{i18n('icu:BackupMediaDownloadProgress__title-paused')}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResume}
|
||||
className="BackupMediaDownloadProgress__button"
|
||||
aria-label={i18n('icu:BackupMediaDownloadProgress__button-resume')}
|
||||
>
|
||||
{i18n('icu:BackupMediaDownloadProgress__button-resume')}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<>
|
||||
<div className="BackupMediaDownloadProgress__title">
|
||||
{i18n('icu:BackupMediaDownloadProgress__title-in-progress')}
|
||||
</div>
|
||||
|
||||
<div className="BackupMediaDownloadProgress__progressbar-hint">
|
||||
{i18n('icu:BackupMediaDownloadProgress__progressbar-hint', {
|
||||
currentSize: formatFileSize(downloadedBytes),
|
||||
totalSize: formatFileSize(totalBytes),
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
actionButton = (
|
||||
<ContextMenu
|
||||
i18n={i18n}
|
||||
menuOptions={[
|
||||
isPaused
|
||||
? {
|
||||
label: i18n('icu:BackupMediaDownloadProgress__button-resume'),
|
||||
onClick: handleResume,
|
||||
}
|
||||
: {
|
||||
label: i18n('icu:BackupMediaDownloadProgress__button-pause'),
|
||||
onClick: handlePause,
|
||||
},
|
||||
{
|
||||
label: i18n('icu:BackupMediaDownloadProgress__button-cancel'),
|
||||
onClick: handleCancel,
|
||||
},
|
||||
]}
|
||||
moduleClassName="Stories__pane__settings"
|
||||
popperOptions={{
|
||||
placement: 'bottom-end',
|
||||
strategy: 'absolute',
|
||||
}}
|
||||
portalToRoot
|
||||
>
|
||||
{({ onClick }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="BackupMediaDownloadProgress__button-more"
|
||||
aria-label={i18n('icu:BackupMediaDownloadProgress__button-more')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="BackupMediaDownloadProgressBanner">
|
||||
<div className="BackupMediaDownloadProgressBanner__icon" />
|
||||
<div className="BackupMediaDownloadProgressBanner__content">
|
||||
<div className="BackupMediaDownloadProgressBanner__title">
|
||||
{i18n('icu:BackupMediaDownloadProgress__title')}
|
||||
</div>
|
||||
<ProgressBar fractionComplete={fractionComplete} />
|
||||
<div className="BackupMediaDownloadProgressBanner__progressbar-hint">
|
||||
{i18n('icu:BackupMediaDownloadProgress__progressbar-hint', {
|
||||
currentSize: formatFileSize(downloadedBytes),
|
||||
totalSize: formatFileSize(totalBytes),
|
||||
fractionComplete,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="BackupMediaDownloadProgress">
|
||||
{icon}
|
||||
<div className="BackupMediaDownloadProgress__content">{content}</div>
|
||||
{actionButton}
|
||||
{isShowingCancelConfirmation ? (
|
||||
<BackupMediaDownloadCancelConfirmationDialog
|
||||
i18n={i18n}
|
||||
handleDialogClose={() => setIsShowingCancelConfirmation(false)}
|
||||
handleConfirmCancel={handleConfirmedCancel}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -137,7 +137,12 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
|||
unreadMentionsCount: 0,
|
||||
markedUnread: false,
|
||||
},
|
||||
backupMediaDownloadProgress: { totalBytes: 0, downloadedBytes: 0 },
|
||||
backupMediaDownloadProgress: {
|
||||
downloadBannerDismissed: false,
|
||||
isPaused: false,
|
||||
totalBytes: 0,
|
||||
downloadedBytes: 0,
|
||||
},
|
||||
clearConversationSearch: action('clearConversationSearch'),
|
||||
clearGroupCreationError: action('clearGroupCreationError'),
|
||||
clearSearch: action('clearSearch'),
|
||||
|
@ -147,6 +152,12 @@ const useProps = (overrideProps: OverridePropsType = {}): PropsType => {
|
|||
composeReplaceAvatar: action('composeReplaceAvatar'),
|
||||
composeSaveAvatarToDisk: action('composeSaveAvatarToDisk'),
|
||||
createGroup: action('createGroup'),
|
||||
dismissBackupMediaDownloadBanner: action(
|
||||
'dismissBackupMediaDownloadBanner'
|
||||
),
|
||||
pauseBackupMediaDownload: action('pauseBackupMediaDownload'),
|
||||
resumeBackupMediaDownload: action('resumeBackupMediaDownload'),
|
||||
cancelBackupMediaDownload: action('cancelBackupMediaDownload'),
|
||||
endConversationSearch: action('endConversationSearch'),
|
||||
endSearch: action('endSearch'),
|
||||
getPreferredBadge: () => undefined,
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
// Copyright 2019 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, {
|
||||
useEffect,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import React, { useEffect, useCallback, useMemo, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
|
@ -62,10 +56,15 @@ import {
|
|||
import { ContextMenu } from './ContextMenu';
|
||||
import { EditState as ProfileEditorEditState } from './ProfileEditor';
|
||||
import type { UnreadStats } from '../util/countUnreadStats';
|
||||
import { BackupMediaDownloadProgressBanner } from './BackupMediaDownloadProgress';
|
||||
import { BackupMediaDownloadProgress } from './BackupMediaDownloadProgress';
|
||||
|
||||
export type PropsType = {
|
||||
backupMediaDownloadProgress: { totalBytes: number; downloadedBytes: number };
|
||||
backupMediaDownloadProgress: {
|
||||
totalBytes: number;
|
||||
downloadedBytes: number;
|
||||
isPaused: boolean;
|
||||
downloadBannerDismissed: boolean;
|
||||
};
|
||||
otherTabsUnreadStats: UnreadStats;
|
||||
hasExpiredDialog: boolean;
|
||||
hasFailedStorySends: boolean;
|
||||
|
@ -128,6 +127,10 @@ export type PropsType = {
|
|||
composeReplaceAvatar: ReplaceAvatarActionType;
|
||||
composeSaveAvatarToDisk: SaveAvatarToDiskActionType;
|
||||
createGroup: () => void;
|
||||
dismissBackupMediaDownloadBanner: () => void;
|
||||
pauseBackupMediaDownload: () => void;
|
||||
resumeBackupMediaDownload: () => void;
|
||||
cancelBackupMediaDownload: () => void;
|
||||
endConversationSearch: () => void;
|
||||
endSearch: () => void;
|
||||
navTabsCollapsed: boolean;
|
||||
|
@ -184,6 +187,7 @@ export function LeftPane({
|
|||
backupMediaDownloadProgress,
|
||||
otherTabsUnreadStats,
|
||||
blockConversation,
|
||||
cancelBackupMediaDownload,
|
||||
challengeStatus,
|
||||
clearConversationSearch,
|
||||
clearGroupCreationError,
|
||||
|
@ -214,6 +218,7 @@ export function LeftPane({
|
|||
onOutgoingVideoCallInConversation,
|
||||
|
||||
openUsernameReservationModal,
|
||||
pauseBackupMediaDownload,
|
||||
preferredWidthFromStorage,
|
||||
preloadConversation,
|
||||
removeConversation,
|
||||
|
@ -226,6 +231,7 @@ export function LeftPane({
|
|||
renderRelinkDialog,
|
||||
renderUpdateDialog,
|
||||
renderToastManager,
|
||||
resumeBackupMediaDownload,
|
||||
savePreferredLeftPaneWidth,
|
||||
searchInConversation,
|
||||
selectedConversationId,
|
||||
|
@ -256,6 +262,7 @@ export function LeftPane({
|
|||
usernameCorrupted,
|
||||
usernameLinkCorrupted,
|
||||
updateSearchTerm,
|
||||
dismissBackupMediaDownloadBanner,
|
||||
}: PropsType): JSX.Element {
|
||||
const previousModeSpecificProps = usePrevious(
|
||||
modeSpecificProps,
|
||||
|
@ -645,27 +652,25 @@ export function LeftPane({
|
|||
|
||||
// We'll show the backup media download progress banner if the download is currently or
|
||||
// was ongoing at some point during the lifecycle of this component
|
||||
const [
|
||||
hasMediaBackupDownloadBeenOngoing,
|
||||
setHasMediaBackupDownloadBeenOngoing,
|
||||
] = useState(false);
|
||||
|
||||
const isMediaBackupDownloadOngoing =
|
||||
const isMediaBackupDownloadIncomplete =
|
||||
backupMediaDownloadProgress?.totalBytes > 0 &&
|
||||
backupMediaDownloadProgress.downloadedBytes <
|
||||
backupMediaDownloadProgress.totalBytes;
|
||||
|
||||
if (isMediaBackupDownloadOngoing && !hasMediaBackupDownloadBeenOngoing) {
|
||||
setHasMediaBackupDownloadBeenOngoing(true);
|
||||
}
|
||||
|
||||
if (hasMediaBackupDownloadBeenOngoing) {
|
||||
if (
|
||||
isMediaBackupDownloadIncomplete &&
|
||||
!backupMediaDownloadProgress.downloadBannerDismissed
|
||||
) {
|
||||
dialogs.push({
|
||||
key: 'backupMediaDownload',
|
||||
dialog: (
|
||||
<BackupMediaDownloadProgressBanner
|
||||
<BackupMediaDownloadProgress
|
||||
i18n={i18n}
|
||||
{...backupMediaDownloadProgress}
|
||||
handleClose={dismissBackupMediaDownloadBanner}
|
||||
handlePause={pauseBackupMediaDownload}
|
||||
handleResume={resumeBackupMediaDownload}
|
||||
handleCancel={cancelBackupMediaDownload}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
|
53
ts/components/ProgressBar.stories.tsx
Normal file
53
ts/components/ProgressBar.stories.tsx
Normal file
|
@ -0,0 +1,53 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ProgressBar } from './ProgressBar';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
|
||||
type Props = React.ComponentProps<typeof ProgressBar>;
|
||||
export default {
|
||||
title: 'Components/ProgressBar',
|
||||
component: ProgressBar,
|
||||
args: {
|
||||
fractionComplete: 0,
|
||||
isRTL: false,
|
||||
},
|
||||
} satisfies ComponentMeta<Props>;
|
||||
|
||||
export function Zero(args: Props): JSX.Element {
|
||||
return <ProgressBar {...args} />;
|
||||
}
|
||||
|
||||
export function Thirty(args: Props): JSX.Element {
|
||||
return <ProgressBar {...args} fractionComplete={0.3} />;
|
||||
}
|
||||
|
||||
export function Done(args: Props): JSX.Element {
|
||||
return <ProgressBar {...args} fractionComplete={1} />;
|
||||
}
|
||||
|
||||
export function Increasing(args: Props): JSX.Element {
|
||||
const fractionComplete = useIncreasingFractionComplete();
|
||||
return <ProgressBar {...args} fractionComplete={fractionComplete} />;
|
||||
}
|
||||
|
||||
export function RTLIncreasing(args: Props): JSX.Element {
|
||||
const fractionComplete = useIncreasingFractionComplete();
|
||||
return <ProgressBar {...args} fractionComplete={fractionComplete} isRTL />;
|
||||
}
|
||||
|
||||
function useIncreasingFractionComplete() {
|
||||
const [fractionComplete, setFractionComplete] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
if (fractionComplete >= 1) {
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
setFractionComplete(cur => Math.min(1, cur + 0.1));
|
||||
}, 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [fractionComplete]);
|
||||
return fractionComplete;
|
||||
}
|
|
@ -5,15 +5,17 @@ import React from 'react';
|
|||
|
||||
export function ProgressBar({
|
||||
fractionComplete,
|
||||
isRTL,
|
||||
}: {
|
||||
fractionComplete: number;
|
||||
isRTL: boolean;
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<div className="ProgressBar">
|
||||
<div
|
||||
className="ProgressBar__fill"
|
||||
style={{
|
||||
marginInlineEnd: `${(1 - fractionComplete) * 100}%`,
|
||||
transform: `translateX(${(isRTL ? -1 : 1) * (fractionComplete - 1) * 100}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
44
ts/components/ProgressCircle.stories.tsx
Normal file
44
ts/components/ProgressCircle.stories.tsx
Normal file
|
@ -0,0 +1,44 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
||||
import { ProgressCircle } from './ProgressCircle';
|
||||
import type { ComponentMeta } from '../storybook/types';
|
||||
|
||||
type Props = React.ComponentProps<typeof ProgressCircle>;
|
||||
export default {
|
||||
title: 'Components/ProgressCircle',
|
||||
component: ProgressCircle,
|
||||
args: { fractionComplete: 0, width: undefined, strokeWidth: undefined },
|
||||
} satisfies ComponentMeta<Props>;
|
||||
|
||||
export function Zero(args: Props): JSX.Element {
|
||||
return <ProgressCircle {...args} />;
|
||||
}
|
||||
|
||||
export function Thirty(args: Props): JSX.Element {
|
||||
return <ProgressCircle {...args} fractionComplete={0.3} />;
|
||||
}
|
||||
|
||||
export function Done(args: Props): JSX.Element {
|
||||
return <ProgressCircle {...args} fractionComplete={1} />;
|
||||
}
|
||||
export function Increasing(args: Props): JSX.Element {
|
||||
const fractionComplete = useIncreasingFractionComplete();
|
||||
return <ProgressCircle {...args} fractionComplete={fractionComplete} />;
|
||||
}
|
||||
|
||||
function useIncreasingFractionComplete() {
|
||||
const [fractionComplete, setFractionComplete] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
if (fractionComplete >= 1) {
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
setFractionComplete(cur => Math.min(1, cur + 0.1));
|
||||
}, 300);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [fractionComplete]);
|
||||
return fractionComplete;
|
||||
}
|
41
ts/components/ProgressCircle.tsx
Normal file
41
ts/components/ProgressCircle.tsx
Normal file
|
@ -0,0 +1,41 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export function ProgressCircle({
|
||||
fractionComplete,
|
||||
width = 24,
|
||||
strokeWidth = 3,
|
||||
}: {
|
||||
fractionComplete: number;
|
||||
width?: number;
|
||||
strokeWidth?: number;
|
||||
}): JSX.Element {
|
||||
const radius = width / 2 - strokeWidth / 2;
|
||||
const circumference = radius * 2 * Math.PI;
|
||||
return (
|
||||
<svg className="ProgressCircle" width={width} height={width}>
|
||||
<circle
|
||||
className="ProgressCircle__background"
|
||||
strokeWidth={strokeWidth}
|
||||
r={radius}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
/>
|
||||
<circle
|
||||
className="ProgressCircle__fill"
|
||||
r={radius}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
strokeWidth={strokeWidth}
|
||||
// setting the strokeDashArray to be the circumference of the ring means each dash
|
||||
// will cover the whole ring
|
||||
strokeDasharray={circumference}
|
||||
// offsetting the dash as a fraction of the circumference allows showing the
|
||||
// progress
|
||||
strokeDashoffset={(1 - fractionComplete) * circumference}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -9,6 +9,7 @@ import { TitlebarDragArea } from '../TitlebarDragArea';
|
|||
import { ProgressBar } from '../ProgressBar';
|
||||
import { ConfirmationDialog } from '../ConfirmationDialog';
|
||||
import { InstallScreenSignalLogo } from './InstallScreenSignalLogo';
|
||||
import { roundFractionForProgressBar } from '../../util/numbers';
|
||||
|
||||
// We can't always use destructuring assignment because of the complexity of this props
|
||||
// type.
|
||||
|
@ -41,29 +42,26 @@ export function InstallScreenBackupImportStep({
|
|||
setIsConfirmingCancel(false);
|
||||
}, [onCancel]);
|
||||
|
||||
let percentage = 0;
|
||||
let progress: JSX.Element;
|
||||
let isCancelPossible = true;
|
||||
if (currentBytes != null && totalBytes != null) {
|
||||
isCancelPossible = currentBytes !== totalBytes;
|
||||
|
||||
percentage = Math.max(0, Math.min(1, currentBytes / totalBytes));
|
||||
if (percentage > 0 && percentage <= 0.01) {
|
||||
percentage = 0.01;
|
||||
} else if (percentage >= 0.99 && percentage < 1) {
|
||||
percentage = 0.99;
|
||||
} else {
|
||||
percentage = Math.round(percentage * 100) / 100;
|
||||
}
|
||||
const fractionComplete = roundFractionForProgressBar(
|
||||
currentBytes / totalBytes
|
||||
);
|
||||
|
||||
progress = (
|
||||
<>
|
||||
<ProgressBar fractionComplete={percentage} />
|
||||
<ProgressBar
|
||||
fractionComplete={fractionComplete}
|
||||
isRTL={i18n.getLocaleDirection() === 'rtl'}
|
||||
/>
|
||||
<div className="InstallScreenBackupImportStep__progressbar-hint">
|
||||
{i18n('icu:BackupImportScreen__progressbar-hint', {
|
||||
currentSize: formatFileSize(currentBytes),
|
||||
totalSize: formatFileSize(totalBytes),
|
||||
fractionComplete: percentage,
|
||||
fractionComplete,
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
|
@ -71,7 +69,10 @@ export function InstallScreenBackupImportStep({
|
|||
} else {
|
||||
progress = (
|
||||
<>
|
||||
<ProgressBar fractionComplete={0} />
|
||||
<ProgressBar
|
||||
fractionComplete={0}
|
||||
isRTL={i18n.getLocaleDirection() === 'rtl'}
|
||||
/>
|
||||
<div className="InstallScreenBackupImportStep__progressbar-hint">
|
||||
{i18n('icu:BackupImportScreen__progressbar-hint--preparing')}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue