diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e8adb8eea8..6ef183a22e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -4723,14 +4723,54 @@ "messageformat": "Cancel transfer", "description": "Text of the confirmation button of the cancel confirmation modal in the backup import screen" }, - "icu:BackupMediaDownloadProgress__title": { + "icu:BackupMediaDownloadProgress__title-in-progress": { "messageformat": "Restoring media", - "description": "Label above a progress bar showing media (attachment) download progress after restoring from backup" + "description": "Label next to a progress bar showing active media (attachment) download progress after restoring from backup" + }, + "icu:BackupMediaDownloadProgress__title-paused": { + "messageformat": "Restore paused", + "description": "Label indicating media (attachment) download progress has been paused (due to user interaction)" + }, + "icu:BackupMediaDownloadProgress__button-pause": { + "messageformat": "Pause transfer", + "description": "Text for button to pause media (attachment) download after backup impor" + }, + "icu:BackupMediaDownloadProgress__button-resume": { + "messageformat": "Resume transfer", + "description": "Text for button to resume media (attachment) download after backup import" + }, + "icu:BackupMediaDownloadProgress__button-cancel": { + "messageformat": "Cancel transfer", + "description": "Text for button to cancel (pause) media (attachment) download after backup import" + }, + "icu:BackupMediaDownloadProgress__button-more": { + "messageformat": "More options", + "description": "Alt text for button that opens menu to allow user to select to pause or cancel media download" + }, + "icu:BackupMediaDownloadProgress__title-complete": { + "messageformat": "Restore complete", + "description": "Label above a progress bar showing active media (attachment) download progress after restoring from backup" }, "icu:BackupMediaDownloadProgress__progressbar-hint": { - "messageformat": "{currentSize} of {totalSize} ({fractionComplete, number, percent})", + "messageformat": "{currentSize} of {totalSize}", "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" + }, + "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" + }, + "icu:BackupMediaDownloadCancelConfirmation__button-continue": { + "messageformat": "Continue transfer", + "description": "Text for button to close confirmation dialog and continue media (attachment) download" + }, + "icu:BackupMediaDownloadCancelConfirmation__button-confirm-cancel": { + "messageformat": "Cancel transfer", + "description": "Text for button to confirm cancellation of media (attachment) download" + }, "icu:CompositionArea--expand": { "messageformat": "Expand", "description": "Aria label for expanding composition area" diff --git a/stylesheets/components/BackupMediaDownloadProgress.scss b/stylesheets/components/BackupMediaDownloadProgress.scss index 1752fd2b22..40ec849b97 100644 --- a/stylesheets/components/BackupMediaDownloadProgress.scss +++ b/stylesheets/components/BackupMediaDownloadProgress.scss @@ -1,16 +1,20 @@ // Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -.BackupMediaDownloadProgressBanner { - @include font-body-2; - +.BackupMediaDownloadProgress { border-radius: 10px; display: flex; - gap: 12px; - padding: 12px; + align-items: center; + gap: 10px; + padding: 11px; padding-inline-end: 16px; margin-inline: 10px; user-select: none; + position: relative; + + &__title { + @include font-body-2-bold; + } @include light-theme { background-color: $color-white; @@ -22,69 +26,84 @@ } } -.BackupMediaDownloadProgressBanner__icon { - background: rgba($color-ultramarine, 0.2); - width: 30px; - height: 30px; - padding: 6px; - border-radius: 50%; - @include dark-theme { - background: $color-gray-60; - } +.BackupMediaDownloadProgress__icon--complete { &::after { content: ''; - display: inline-block; - width: 18px; - height: 18px; + display: block; + width: 24px; + height: 24px; @include light-theme { @include color-svg( - '../images/icons/v3/backup/backup-bold.svg', + '../images/icons/v3/check/check-circle.svg', $color-ultramarine ); } @include dark-theme { @include color-svg( - '../images/icons/v3/backup/backup-bold.svg', - $color-ultramarine-pale + '../images/icons/v3/check/check-circle.svg', + $color-ultramarine-light ); } } } +button.BackupMediaDownloadProgress__button { + @include button-reset; + @include font-subtitle-bold; + @include light-theme { + color: $color-ultramarine; + } + @include dark-theme { + color: $color-ultramarine-light; + } +} -.BackupMediaDownloadProgressBanner__content { +button.BackupMediaDownloadProgress__button-more { + position: absolute; + inset-inline-end: 14px; + inset-block-start: 10px; + @include button-reset; + &::after { + content: ''; + display: block; + width: 20px; + height: 20px; + @include light-theme { + @include color-svg('../images/icons/v3/more/more.svg', $color-gray-75); + } + @include dark-theme { + @include color-svg('../images/icons/v3/more/more.svg', $color-gray-20); + } + } +} +button.BackupMediaDownloadProgress__button-close { + position: absolute; + inset-inline-end: 14px; + inset-block-start: 10px; + @include button-reset; + &::after { + content: ''; + display: block; + width: 20px; + height: 20px; + @include light-theme { + @include color-svg('../images/icons/v3/x/x.svg', $color-gray-75); + } + @include dark-theme { + @include color-svg('../images/icons/v3/x/x.svg', $color-gray-20); + } + } +} + +.BackupMediaDownloadProgress__content { display: flex; flex-direction: column; - flex: 1; - gap: 7px; + justify-content: center; + gap: 2px; + min-height: 36px; } -.BackupMediaDownloadProgressBanner .Progressbar { - overflow: hidden; - - background: rgba($color-ultramarine, 0.2); - height: 5px; - border-radius: 2px; -} - -.BackupMediaDownloadProgressBanner__progressbar__fill { - background-color: $color-ultramarine; - border-radius: 2px; - display: block; - height: 100%; - width: 100%; - &:dir(ltr) { - /* stylelint-disable-next-line declaration-property-value-disallowed-list */ - transform: translateX(-100%); - } - &:dir(rtl) { - /* stylelint-disable-next-line declaration-property-value-disallowed-list */ - transform: translateX(100%); - } - transition: transform 500ms ease-out; -} - -.BackupMediaDownloadProgressBanner__progressbar-hint { - @include font-caption; +.BackupMediaDownloadProgress__progressbar-hint { + @include font-subtitle; @include light-theme { color: rgba($color-gray-60, 0.8); @@ -94,3 +113,7 @@ color: $color-gray-25; } } + +.BackupMediaDownloadCancelConfirmation { + min-width: 440px; +} diff --git a/stylesheets/components/ProgressBar.scss b/stylesheets/components/ProgressBar.scss index a46b9dc6af..aa0a47b019 100644 --- a/stylesheets/components/ProgressBar.scss +++ b/stylesheets/components/ProgressBar.scss @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-only .ProgressBar { + position: relative; overflow: hidden; background: rgba($color-ultramarine, 0.2); height: 5px; @@ -9,9 +10,11 @@ } .ProgressBar__fill { + position: absolute; background-color: $color-ultramarine; border-radius: 2px; display: block; height: 100%; - transition: margin 500ms ease-out; + width: 100%; + transition: transform 500ms ease-out; } diff --git a/stylesheets/components/ProgressCircle.scss b/stylesheets/components/ProgressCircle.scss new file mode 100644 index 0000000000..ed12815846 --- /dev/null +++ b/stylesheets/components/ProgressCircle.scss @@ -0,0 +1,25 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.ProgressCircle { + fill: none; + transform: rotate(-90deg); + + .ProgressCircle__fill, + .ProgressCircle__background { + fill: none; + } + + .ProgressCircle__background { + stroke: $color-gray-20; + @include dark-theme() { + stroke: $color-gray-60; + } + } + + .ProgressCircle__fill { + stroke: $color-ultramarine; + stroke-linecap: round; + transition: stroke-dashoffset 500ms ease-out; + } +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index b07d243c0a..748b2be9e5 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -140,6 +140,7 @@ @import './components/Preferences.scss'; @import './components/ProfileEditor.scss'; @import './components/ProgressBar.scss'; +@import './components/ProgressCircle.scss'; @import './components/Quote.scss'; @import './components/ReactionPickerPicker.scss'; @import './components/RecordingComposer.scss'; diff --git a/ts/components/BackupMediaDownloadCancelConfirmationDialog.tsx b/ts/components/BackupMediaDownloadCancelConfirmationDialog.tsx new file mode 100644 index 0000000000..6cf1034184 --- /dev/null +++ b/ts/components/BackupMediaDownloadCancelConfirmationDialog.tsx @@ -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 ( + + {i18n('icu:BackupMediaDownloadCancelConfirmation__description')} + + ); +} diff --git a/ts/components/BackupMediaDownloadProgress.stories.tsx b/ts/components/BackupMediaDownloadProgress.stories.tsx index 8421849915..bae36c8aca 100644 --- a/ts/components/BackupMediaDownloadProgress.stories.tsx +++ b/ts/components/BackupMediaDownloadProgress.stories.tsx @@ -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; +type PropsType = ComponentProps; 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; -// eslint-disable-next-line react/function-component-definition -const Template: StoryFn = (args: PropsType) => ( - -); +export function InProgress(args: PropsType): JSX.Element { + return ; +} -export const InProgress = Template.bind({}); -InProgress.args = { - downloadedBytes: 92048023, - totalBytes: 1024102532, -}; +export function Increasing(args: PropsType): JSX.Element { + return ( + + ); +} + +export function Paused(args: PropsType): JSX.Element { + return ; +} + +export function Complete(args: PropsType): JSX.Element { + return ( + + ); +} + +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 }; +} diff --git a/ts/components/BackupMediaDownloadProgress.tsx b/ts/components/BackupMediaDownloadProgress.tsx index 06fd9e26b7..ca9b44b0f9 100644 --- a/ts/components/BackupMediaDownloadProgress.tsx +++ b/ts/components/BackupMediaDownloadProgress.tsx @@ -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 =
; + content = ( + <> +
+ {i18n('icu:BackupMediaDownloadProgress__title-complete')} +
+
+ {formatFileSize(downloadedBytes)} +
+ + ); + actionButton = ( + + + ); + } else { + content = ( + <> +
+ {i18n('icu:BackupMediaDownloadProgress__title-in-progress')} +
+ +
+ {i18n('icu:BackupMediaDownloadProgress__progressbar-hint', { + currentSize: formatFileSize(downloadedBytes), + totalSize: formatFileSize(totalBytes), + })} +
+ + ); + } + + actionButton = ( + + {({ onClick }) => { + return ( +