diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 5f2f9bbeba..8efa2ec7cd 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -2792,13 +2792,6 @@ button.ConversationDetails__action-button { .module-image__progress-circle-wrapper { @include mixins.position-absolute-center; - - .ProgressCircle .ProgressCircle__background { - stroke: variables.$color-white-alpha-20; - } - .ProgressCircle .ProgressCircle__fill { - stroke: variables.$color-white; - } } .module-image__spinner-container { @@ -2954,7 +2947,7 @@ button.module-image__border-overlay:focus { width: 24px; @include mixins.color-svg( '../images/icons/v3/play/play-fill.svg', - variables.$color-white + var(--color-label-primary-on-color) ); } .module-image__stop-icon { @@ -2965,7 +2958,7 @@ button.module-image__border-overlay:focus { width: 24px; @include mixins.color-svg( '../images/icons/v3/stop/stop-fill.svg', - variables.$color-white + var(--color-label-primary-on-color) ); } .module-image__download-icon { @@ -2975,7 +2968,7 @@ button.module-image__border-overlay:focus { width: 24px; @include mixins.color-svg( '../images/icons/v3/arrow/arrow-down.svg', - variables.$color-white + var(--color-label-primary-on-color) ); } .module-image__undownloadable-icon { @@ -2985,7 +2978,7 @@ button.module-image__border-overlay:focus { width: 24px; @include mixins.color-svg( '../images/icons/v3/photo/photo-slash-compact.svg', - variables.$color-white + var(--color-label-primary-on-color) ); } diff --git a/stylesheets/components/AttachmentDetailPill.scss b/stylesheets/components/AttachmentDetailPill.scss index 94f00d54ef..f5f9d92be3 100644 --- a/stylesheets/components/AttachmentDetailPill.scss +++ b/stylesheets/components/AttachmentDetailPill.scss @@ -25,7 +25,7 @@ z-index: variables.$z-index-above-base; @include mixins.font-caption; - color: variables.$color-white; + color: var(--color-label-primary-on-color); transition: width 400ms ease-out; } @@ -34,21 +34,6 @@ position: relative; margin: 4px; margin-inline-end: -4px; - - .ProgressCircle .ProgressCircle__background { - stroke: variables.$color-white-alpha-20; - } - .ProgressCircle .ProgressCircle__fill { - stroke: variables.$color-white; - } - - .module-spinner__circle { - background-color: variables.$color-white-alpha-20; - } - - .module-spinner__arc { - background-color: variables.$color-white; - } } .AttachmentDetailPill__text-wrapper { @@ -72,7 +57,7 @@ width: 12px; @include mixins.color-svg( '../images/icons/v3/stop/stop-fill.svg', - variables.$color-white + var(--color-label-primary-on-color) ); } .AttachmentDetailPill__download-icon { @@ -82,6 +67,6 @@ width: 16px; @include mixins.color-svg( '../images/icons/v3/arrow/arrow-down.svg', - variables.$color-white + var(--color-label-primary-on-color) ); } diff --git a/stylesheets/components/AttachmentStatusIcon.scss b/stylesheets/components/AttachmentStatusIcon.scss index a39384aa7f..74ad1b2938 100644 --- a/stylesheets/components/AttachmentStatusIcon.scss +++ b/stylesheets/components/AttachmentStatusIcon.scss @@ -111,54 +111,22 @@ .AttachmentStatusIcon__circle-icon--x { @include mixins.color-svg-themed( '../images/icons/v3/x/x-bold.svg', - variables.$color-white, - variables.$color-white-alpha-90 + var(--color-label-primary-on-color), + var(--color-label-primary-on-color) ); } .AttachmentStatusIcon__circle-icon--arrow-down { @include mixins.color-svg-themed( '../images/icons/v3/arrow/arrow-down-bold.svg', - variables.$color-white, - variables.$color-white-alpha-90 + var(--color-label-primary-on-color), + var(--color-label-primary-on-color) ); } .AttachmentStatusIcon__circle-icon--incoming { @include mixins.light-theme { - background-color: variables.$color-gray-90; + background-color: var(--color-label-primary); } @include mixins.dark-theme { - background-color: variables.$color-white-alpha-90; - } -} - -.AttachmentStatusIcon__progress-container { - .ProgressCircle .ProgressCircle__background { - @include mixins.light-theme { - stroke: none; - fill: none; - } - @include mixins.dark-theme { - stroke: none; - fill: none; - } - } - - .ProgressCircle .ProgressCircle__fill { - @include mixins.light-theme { - stroke: variables.$color-white; - } - @include mixins.dark-theme { - stroke: variables.$color-white-alpha-90; - } - } -} -.AttachmentStatusIcon__progress-container--incoming { - .ProgressCircle .ProgressCircle__fill { - @include mixins.light-theme { - stroke: variables.$color-gray-90; - } - @include mixins.dark-theme { - stroke: variables.$color-white-alpha-90; - } + background-color: var(--color-label-primary); } } diff --git a/stylesheets/components/CallingLobby.scss b/stylesheets/components/CallingLobby.scss index f5323ee597..843bc3846d 100644 --- a/stylesheets/components/CallingLobby.scss +++ b/stylesheets/components/CallingLobby.scss @@ -76,9 +76,8 @@ width: auto; } -.CallingLobby__CallLinkJoinRequestPendingSpinner { - margin-inline-end: 8px; - color: variables.$color-gray-15; +.CallingLobby__CallLinkJoinRequestPendingText { + margin-inline-start: 8px; } .CallingLobby__Footer { diff --git a/stylesheets/components/DonationProgressModal.scss b/stylesheets/components/DonationProgressModal.scss index 3209c181e4..48a3aca36f 100644 --- a/stylesheets/components/DonationProgressModal.scss +++ b/stylesheets/components/DonationProgressModal.scss @@ -21,10 +21,7 @@ @include mixins.font-body-2; } -.DonationProgressModal .SpinnerV2 { +.DonationProgressModal__SpinnerV2 { + display: inline-block; margin-inline: auto; } - -.DonationProgressModal .SpinnerV2__Path { - color: variables.$color-ultramarine; -} diff --git a/stylesheets/components/PlaybackButton.scss b/stylesheets/components/PlaybackButton.scss index 302c6c4ed0..727549f217 100644 --- a/stylesheets/components/PlaybackButton.scss +++ b/stylesheets/components/PlaybackButton.scss @@ -67,29 +67,11 @@ &--computing { cursor: auto; } - &__SpinnerV2-container { + &__Spinner-container { @include mixins.position-absolute-center; } - .ProgressCircle { - @include mixins.position-absolute-center; - .ProgressCircle__background { - stroke: none; - } - } - @include mixins.dark-theme { - .ProgressCircle .ProgressCircle__background { - stroke: none; - } - } @include mixins.light-theme { - .ProgressCircle .ProgressCircle__fill { - stroke: variables.$color-white; - } - .SpinnerV2 .SpinnerV2__Path { - stroke: variables.$color-white; - } - - @include all-audio-icons(variables.$color-gray-90); + @include all-audio-icons(var(--color-label-primary)); &--context-incoming { &.PlaybackButton--variant-message { @@ -101,24 +83,11 @@ background: variables.$color-white-alpha-40; } } - .ProgressCircle .ProgressCircle__fill { - stroke: variables.$color-gray-90; - } - .SpinnerV2 .SpinnerV2__Path { - stroke: variables.$color-gray-90; - } } } @include mixins.dark-theme { - .ProgressCircle .ProgressCircle__fill { - stroke: variables.$color-white-alpha-90; - } - .SpinnerV2 .SpinnerV2__Path { - stroke: variables.$color-white-alpha-90; - } - - @include all-audio-icons(variables.$color-white-alpha-90); + @include all-audio-icons(var(--color-label-primary)); &--context-incoming { &.PlaybackButton--variant-message { @@ -130,12 +99,6 @@ background: variables.$color-white-alpha-40; } } - .ProgressCircle .ProgressCircle__fill { - stroke: variables.$color-white-alpha-90; - } - .SpinnerV2 .SpinnerV2__Path { - stroke: variables.$color-white-alpha-90; - } } } @@ -149,12 +112,12 @@ background: variables.$color-white-alpha-40; } } - @include all-audio-icons(variables.$color-white); + @include all-audio-icons(var(--color-label-primary-on-color)); } @include mixins.dark-theme { &--context-outgoing { - @include all-audio-icons(variables.$color-white-alpha-90); + @include all-audio-icons(var(--color-label-primary-on-color)); } } } diff --git a/stylesheets/components/ProgressCircle.scss b/stylesheets/components/ProgressCircle.scss deleted file mode 100644 index 6e115dbab0..0000000000 --- a/stylesheets/components/ProgressCircle.scss +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -@use '../mixins'; -@use '../variables'; - -.ProgressCircle { - fill: none; - transform: rotate(-90deg); - - .ProgressCircle__fill, - .ProgressCircle__background { - fill: none; - } - - .ProgressCircle__background { - stroke: variables.$color-gray-20; - @include mixins.dark-theme() { - stroke: variables.$color-gray-60; - } - } - - .ProgressCircle__fill { - stroke: variables.$color-ultramarine; - stroke-linecap: round; - transition: stroke-dashoffset 500ms ease-out; - } -} diff --git a/stylesheets/components/SpinnerV2.scss b/stylesheets/components/SpinnerV2.scss deleted file mode 100644 index c547007f84..0000000000 --- a/stylesheets/components/SpinnerV2.scss +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -@use '../variables'; - -.SpinnerV2 { - animation: SpinnerV2-rotate 2s linear infinite; -} - -.SpinnerV2__Path { - stroke: currentColor; - stroke-linecap: round; - animation: SpinnerV2-dash 1.5s ease-in-out infinite; -} - -@keyframes SpinnerV2-rotate { - 100% { - transform: rotate(360deg); - } -} - -@keyframes SpinnerV2-dash { - 0% { - stroke-dasharray: 2%, 300%; - stroke-dashoffset: 0; - } - 50% { - stroke-dasharray: 180%, 300%; - stroke-dashoffset: -70%; - } - 100% { - stroke-dasharray: 180%, 300%; - stroke-dashoffset: -248%; - } -} diff --git a/stylesheets/components/fun/FunGif.scss b/stylesheets/components/fun/FunGif.scss index 2f9cd0e475..1a24d273ac 100644 --- a/stylesheets/components/fun/FunGif.scss +++ b/stylesheets/components/fun/FunGif.scss @@ -39,10 +39,6 @@ border-radius: 8px; } -.FunGifPreview__Spinner { - color: light-dark(variables.$color-gray-25, variables.$color-gray-45); -} - .FunGifPreview__ErrorIcon { width: 36px; height: 36px; diff --git a/stylesheets/components/fun/FunResults.scss b/stylesheets/components/fun/FunResults.scss index 890e358742..e4cf032c0f 100644 --- a/stylesheets/components/fun/FunResults.scss +++ b/stylesheets/components/fun/FunResults.scss @@ -49,7 +49,3 @@ } } } - -.FunResults__Spinner { - color: light-dark(variables.$color-gray-25, variables.$color-gray-45); -} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 450776647c..91812b7c5a 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -155,7 +155,6 @@ @use 'components/ProfileMovedModal.scss'; @use 'components/ProfileNameWarningModal.scss'; @use 'components/ProgressBar.scss'; -@use 'components/ProgressCircle.scss'; @use 'components/Quote.scss'; @use 'components/ReactionPickerPicker.scss'; @use 'components/RecordingComposer.scss'; @@ -174,7 +173,6 @@ @use 'components/SignalConnectionsModal.scss'; @use 'components/SignalConversationMuteToggle.scss'; @use 'components/Slider.scss'; -@use 'components/SpinnerV2.scss'; @use 'components/StagedLinkPreview.scss'; @use 'components/StickerManager.scss'; @use 'components/Stories.scss'; diff --git a/stylesheets/tailwind-config.css b/stylesheets/tailwind-config.css index cb683accca..f2f8435bff 100644 --- a/stylesheets/tailwind-config.css +++ b/stylesheets/tailwind-config.css @@ -365,6 +365,8 @@ @theme { --animate-*: initial; /* reset defaults */ --animate-fade-out: animate-fade-out 120ms var(--ease-out-cubic); + --animate-spinner-v2-rotate: animate-spinner-v2-rotate 2s linear infinite; + --animate-spinner-v2-dash: animate-spinner-v2-dash 1.5s ease-in-out infinite; } @layer base { @@ -373,4 +375,28 @@ opacity: 0; } } + + @keyframes animate-spinner-v2-rotate { + 0% { + transform: rotate(-180deg); + } + 100% { + transform: rotate(180deg); + } + } + + @keyframes animate-spinner-v2-dash { + 0% { + stroke-dasharray: 2%, 300%; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 180%, 300%; + stroke-dashoffset: -70%; + } + 100% { + stroke-dasharray: 180%, 300%; + stroke-dashoffset: -248%; + } + } } diff --git a/ts/components/BackupMediaDownloadProgress.tsx b/ts/components/BackupMediaDownloadProgress.tsx index b806a1ea29..1b83d4bf1c 100644 --- a/ts/components/BackupMediaDownloadProgress.tsx +++ b/ts/components/BackupMediaDownloadProgress.tsx @@ -5,8 +5,7 @@ import React, { useState } from 'react'; import type { LocalizerType } from '../types/Util'; import { formatFileSize } from '../util/formatFileSize'; -import { roundFractionForProgressBar } from '../util/numbers'; -import { ProgressCircle } from './ProgressCircle'; +import { SpinnerV2 } from './SpinnerV2'; import { ContextMenu } from './ContextMenu'; import { BackupMediaDownloadCancelConfirmationDialog } from './BackupMediaDownloadCancelConfirmationDialog'; import { LeftPaneDialog } from './LeftPaneDialog'; @@ -50,14 +49,10 @@ export function BackupMediaDownloadProgress({ setIsShowingCancelConfirmation(true); } - const fractionComplete = roundFractionForProgressBar( - downloadedBytes / totalBytes - ); - let content: JSX.Element | undefined; let icon: JSX.Element | undefined; - const isCompleted = fractionComplete === 1; + const isCompleted = downloadedBytes === totalBytes; const actionButton = isCompleted || isIdle ? ( @@ -141,8 +136,14 @@ export function BackupMediaDownloadProgress({ ); icon = (
-
@@ -181,8 +182,14 @@ export function BackupMediaDownloadProgress({ ); icon = (
-
@@ -204,8 +211,14 @@ export function BackupMediaDownloadProgress({ ); icon = (
-
diff --git a/ts/components/CallingLobby.tsx b/ts/components/CallingLobby.tsx index 6c7c6b3c4c..9e14fec298 100644 --- a/ts/components/CallingLobby.tsx +++ b/ts/components/CallingLobby.tsx @@ -303,12 +303,10 @@ export function CallingLobby({ {callMode === CallMode.Adhoc ? ( isAdhocJoinRequestPending ? (
- - {i18n('icu:CallingLobby__CallLinkNotice--join-request-pending')} + + + {i18n('icu:CallingLobby__CallLinkNotice--join-request-pending')} +
) : (
diff --git a/ts/components/DonationProgressModal.tsx b/ts/components/DonationProgressModal.tsx index d7b17315c4..29747b455b 100644 --- a/ts/components/DonationProgressModal.tsx +++ b/ts/components/DonationProgressModal.tsx @@ -37,7 +37,9 @@ export function DonationProgressModal(props: PropsType): JSX.Element { noEscapeClose noMouseClose > - +
+ +
{i18n('icu:Donations__Processing')}
diff --git a/ts/components/PlaybackButton.stories.tsx b/ts/components/PlaybackButton.stories.tsx index a7e072ee29..f75685ab2a 100644 --- a/ts/components/PlaybackButton.stories.tsx +++ b/ts/components/PlaybackButton.stories.tsx @@ -9,6 +9,7 @@ import type { ButtonProps } from './PlaybackButton'; import { PlaybackButton } from './PlaybackButton'; import { StorybookThemeContext } from '../../.storybook/StorybookThemeContext'; import { ThemeType } from '../types/Util'; +import { AUDIO_MP3 } from '../types/MIME'; export default { title: 'components/PlaybackButton', @@ -50,6 +51,16 @@ export function Default(): JSX.Element { key={`${variant}_${context}_${mod}`} variant={variant} label="playback" + attachment={ + mod === 'downloading' + ? undefined + : { + contentType: AUDIO_MP3, + size: 3000, + totalDownloaded: 1000, + isPermanentlyUndownloadable: false, + } + } onClick={action('click')} context={context} mod={mod} diff --git a/ts/components/PlaybackButton.tsx b/ts/components/PlaybackButton.tsx index d217fd1890..db82b17f82 100644 --- a/ts/components/PlaybackButton.tsx +++ b/ts/components/PlaybackButton.tsx @@ -5,7 +5,7 @@ import { animated, useSpring } from '@react-spring/web'; import classNames from 'classnames'; import React, { useCallback } from 'react'; import { useReducedMotion } from '../hooks/useReducedMotion'; -import { ProgressCircle } from './ProgressCircle'; +import type { AttachmentForUIType } from '../types/Attachment'; import { SpinnerV2 } from './SpinnerV2'; const SPRING_CONFIG = { @@ -19,7 +19,7 @@ export type ButtonProps = { context?: 'incoming' | 'outgoing'; variant: 'message' | 'mini' | 'draft'; mod: 'play' | 'pause' | 'not-downloaded' | 'downloading' | 'computing'; - downloadFraction?: number; + attachment?: AttachmentForUIType; label: string; visible?: boolean; onClick: () => void; @@ -32,7 +32,7 @@ export const PlaybackButton = React.forwardRef( function ButtonInner(props, ref) { const { context, - downloadFraction, + attachment, label, mod, onClick, @@ -84,25 +84,24 @@ export const PlaybackButton = React.forwardRef( let content: JSX.Element | null = null; const strokeWidth = variant === 'message' ? 2 : 1; - if (mod === 'downloading' && downloadFraction) { + if (mod === 'computing' || mod === 'downloading') { content = ( - - ); - } else if ( - mod === 'computing' || - (mod === 'downloading' && !downloadFraction) - ) { - content = ( -
+
); diff --git a/ts/components/ProgressCircle.stories.tsx b/ts/components/ProgressCircle.stories.tsx deleted file mode 100644 index 2868bb8362..0000000000 --- a/ts/components/ProgressCircle.stories.tsx +++ /dev/null @@ -1,49 +0,0 @@ -// 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; -export default { - title: 'Components/ProgressCircle', - component: ProgressCircle, - args: { - fractionComplete: 0, - width: undefined, - strokeWidth: undefined, - ariaLabel: undefined, - }, -} satisfies ComponentMeta; - -export function Zero(args: Props): JSX.Element { - return ; -} - -export function Thirty(args: Props): JSX.Element { - return ; -} - -export function Done(args: Props): JSX.Element { - return ; -} -export function Increasing(args: Props): JSX.Element { - const fractionComplete = useIncreasingFractionComplete(); - 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 fractionComplete; -} diff --git a/ts/components/ProgressCircle.tsx b/ts/components/ProgressCircle.tsx deleted file mode 100644 index b227c4d720..0000000000 --- a/ts/components/ProgressCircle.tsx +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2024 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; - -export function ProgressCircle({ - fractionComplete, - width = 24, - strokeWidth = 3, - ariaLabel, -}: { - fractionComplete: number; - width?: number; - strokeWidth?: number; - ariaLabel?: string; -}): JSX.Element { - const radius = width / 2 - strokeWidth / 2; - const circumference = radius * 2 * Math.PI; - const widthInPixels = `${width}px`; - - return ( - - - - - ); -} diff --git a/ts/components/SpinnerV2.stories.tsx b/ts/components/SpinnerV2.stories.tsx index d94a8bb4f5..60615c6de5 100644 --- a/ts/components/SpinnerV2.stories.tsx +++ b/ts/components/SpinnerV2.stories.tsx @@ -1,9 +1,10 @@ // Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import * as React from 'react'; +import React, { useEffect, useState } from 'react'; import { SpinnerV2 } from './SpinnerV2'; +import { tw } from '../axo/tw'; import type { ComponentMeta } from '../storybook/types'; import type { Props } from './SpinnerV2'; @@ -12,42 +13,85 @@ export default { title: 'Components/SpinnerV2', component: SpinnerV2, argTypes: { + variant: { + options: ['normal', 'no-background', 'no-background-incoming', 'brand'], + control: { type: 'select' }, + }, size: { control: { type: 'number' } }, + value: { control: { type: 'range', min: 0, max: 1, step: 0.1 } }, strokeWidth: { control: { type: 'number' } }, marginRatio: { control: { type: 'number' } }, }, - args: { size: 36, strokeWidth: 2, className: undefined, marginRatio: 0.8 }, + args: { + size: 36, + strokeWidth: 2, + marginRatio: 0.8, + min: 0, + max: 1, + value: undefined, + variant: 'normal', + ariaLabel: 'label', + }, } satisfies ComponentMeta; export function Default(args: Props): JSX.Element { - return ; -} - -export function Thin(args: Props): JSX.Element { - return ; -} - -export function Thick(args: Props): JSX.Element { - return ; -} - -export function NoMargin(args: Props): JSX.Element { - return ; -} - -export function BigMargin(args: Props): JSX.Element { - return ; -} - -export function Styled(args: Props): JSX.Element { return ( -
- - +
+ +
+ ); +} + +export function Thin(args: Props): JSX.Element { + return ( +
+ +
+ ); +} + +export function Thick(args: Props): JSX.Element { + return ( +
+ +
+ ); +} + +export function NoMargin(args: Props): JSX.Element { + return ( +
+ +
+ ); +} + +export function BigMargin(args: Props): JSX.Element { + return ( +
+ +
+ ); +} + +export function SpinnerToProgress(args: Props): JSX.Element { + const [value, setValue] = useState(); + useEffect(() => { + const timer = setInterval(() => { + setValue(v => { + if (v == null) { + return 0.3; + } + return undefined; + }); + }, 2000); + return () => { + clearInterval(timer); + }; + }); + return ( +
+
); } diff --git a/ts/components/SpinnerV2.tsx b/ts/components/SpinnerV2.tsx index fd6c3b9008..031923bfb8 100644 --- a/ts/components/SpinnerV2.tsx +++ b/ts/components/SpinnerV2.tsx @@ -2,39 +2,135 @@ // SPDX-License-Identifier: AGPL-3.0-only import React from 'react'; -import classNames from 'classnames'; + +import { tw, type TailwindStyles } from '../axo/tw'; +import { roundFractionForProgressBar } from '../util/numbers'; export type Props = { - className?: string; + value?: number | 'indeterminate'; // default: 'indeterminate' + min?: number; // default: 0 + max?: number; // default: 1 + variant?: SpinnerVariant; + ariaLabel?: string; marginRatio?: number; size: number; strokeWidth: number; }; +type SpinnerVariantStyles = Readonly<{ + fg: TailwindStyles; + bg: TailwindStyles; +}>; + +const SpinnerVariants = { + normal: { + bg: tw('stroke-label-disabled-on-color'), + fg: tw('stroke-label-primary-on-color'), + }, + 'no-background': { + bg: tw('stroke-none'), + fg: tw('stroke-label-primary-on-color'), + }, + 'no-background-incoming': { + bg: tw('stroke-none'), + fg: tw('stroke-label-primary'), + }, + brand: { + bg: tw('stroke-fill-secondary'), + fg: tw('stroke-border-selected'), + }, +} as const satisfies Record; + +export type SpinnerVariant = keyof typeof SpinnerVariants; + export function SpinnerV2({ - className, + value = 'indeterminate', + min = 0, + max = 1, + variant = 'normal', marginRatio, size, strokeWidth, + ariaLabel, }: Props): JSX.Element { - const radius = Math.min(size - strokeWidth / 2, size * (marginRatio ?? 0.8)); + const sizeInPixels = `${size}px`; + + const radius = Math.min( + size / 2 - strokeWidth / 2, + (size / 2) * (marginRatio ?? 0.8) + ); + const circumference = radius * 2 * Math.PI; + + const { bg, fg } = SpinnerVariants[variant]; + + const bgElem = ( + + ); + + if (value === 'indeterminate') { + return ( + + {bgElem} + + + + + ); + } + const fractionComplete = roundFractionForProgressBar( + (value - min) / (max - min) + ); + return ( - + {bgElem} + + + ); } diff --git a/ts/components/conversation/AttachmentDetailPill.tsx b/ts/components/conversation/AttachmentDetailPill.tsx index 7645ca8c8c..3e56f6c468 100644 --- a/ts/components/conversation/AttachmentDetailPill.tsx +++ b/ts/components/conversation/AttachmentDetailPill.tsx @@ -5,13 +5,11 @@ import React from 'react'; import classNames from 'classnames'; import { formatFileSize } from '../../util/formatFileSize'; -import { ProgressCircle } from '../ProgressCircle'; +import { SpinnerV2 } from '../SpinnerV2'; import type { AttachmentForUIType } from '../../types/Attachment'; import type { LocalizerType } from '../../types/I18N'; -import { Spinner } from '../Spinner'; import { isKeyboardActivation } from '../../hooks/useKeyboardShortcuts'; -import { roundFractionForProgressBar } from '../../util/numbers'; export type PropsType = { attachments: ReadonlyArray; @@ -130,20 +128,21 @@ export function AttachmentDetailPill({ {formatFileSize(totalSize)}
); - } else if (totalDownloadedSize > 0) { - const downloadFraction = roundFractionForProgressBar( - totalDownloadedSize / totalSize - ); + } else { + const isDownloading = totalDownloadedSize > 0; ariaLabel = i18n('icu:cancelDownload'); onClick = cancelDownloadClick; onKeyDown = cancelDownloadKeyDown; control = (
-
@@ -156,21 +155,6 @@ export function AttachmentDetailPill({ {formatFileSize(totalSize)}
); - } else { - ariaLabel = i18n('icu:cancelDownload'); - onClick = cancelDownloadClick; - onKeyDown = cancelDownloadKeyDown; - control = ( -
- -
-
- ); - text = ( -
- {formatFileSize(totalSize)} -
- ); } return ( diff --git a/ts/components/conversation/AttachmentStatusIcon.tsx b/ts/components/conversation/AttachmentStatusIcon.tsx index 6e6c2ef94c..3bb60c40ad 100644 --- a/ts/components/conversation/AttachmentStatusIcon.tsx +++ b/ts/components/conversation/AttachmentStatusIcon.tsx @@ -4,11 +4,10 @@ import React, { useRef, useState } from 'react'; import classNames from 'classnames'; -import { ProgressCircle } from '../ProgressCircle'; +import { SpinnerV2 } from '../SpinnerV2'; import { usePrevious } from '../../hooks/usePrevious'; import type { AttachmentForUIType } from '../../types/Attachment'; -import { roundFractionForProgressBar } from '../../util/numbers'; const TRANSITION_DELAY = 200; @@ -110,15 +109,12 @@ export function AttachmentStatusIcon({ (state === IconState.Downloaded && isWaiting)) ) { const { size, totalDownloaded } = attachment; - let downloadFraction = - size && totalDownloaded - ? roundFractionForProgressBar(totalDownloaded / size) - : undefined; + let spinnerValue = (size && totalDownloaded) || undefined; if (state === IconState.Downloading && isWaiting) { - downloadFraction = undefined; + spinnerValue = undefined; } if (state === IconState.Downloaded && isWaiting) { - downloadFraction = 1; + spinnerValue = size; } return ( @@ -131,22 +127,24 @@ export function AttachmentStatusIcon({ : undefined )} > - {downloadFraction ? ( -
- -
- ) : undefined} +
+ +
-
-
- -
- - ); - } - - if (!attachment.pending) { - return undefined; - } + const spinnerValue = + (attachment.pending && + !isIncremental(attachment) && + attachment.size && + attachment.totalDownloaded) || + undefined; return ( ); diff --git a/ts/components/conversation/MessageAudio.tsx b/ts/components/conversation/MessageAudio.tsx index 9e5071a16e..59a1b9f0e6 100644 --- a/ts/components/conversation/MessageAudio.tsx +++ b/ts/components/conversation/MessageAudio.tsx @@ -8,7 +8,7 @@ import { noop } from 'lodash'; import { animated, useSpring } from '@react-spring/web'; import type { LocalizerType } from '../../types/Util'; -import type { AttachmentType } from '../../types/Attachment'; +import type { AttachmentForUIType } from '../../types/Attachment'; import type { PushPanelForConversationActionType } from '../../state/ducks/conversations'; import { isDownloaded } from '../../types/Attachment'; import type { DirectionType, MessageStatusType } from './Message'; @@ -24,7 +24,6 @@ import { useComputePeaks } from '../../hooks/useComputePeaks'; import { durationToPlaybackText } from '../../util/durationToPlaybackText'; import { shouldNeverBeCalled } from '../../util/shouldNeverBeCalled'; import { formatFileSize } from '../../util/formatFileSize'; -import { roundFractionForProgressBar } from '../../util/numbers'; const log = createLogger('MessageAudio'); @@ -37,7 +36,7 @@ export type OwnProps = Readonly<{ | undefined; buttonRef: RefObject; i18n: LocalizerType; - attachment: AttachmentType; + attachment: AttachmentForUIType; collapseMetadata: boolean; withContentAbove: boolean; withContentBelow: boolean; @@ -293,18 +292,11 @@ export function MessageAudio(props: Props): JSX.Element { /> ); } else if (state === State.Pending) { - // Not really a button, but who cares? - const downloadFraction = - attachment.size && attachment.totalDownloaded - ? roundFractionForProgressBar( - attachment.totalDownloaded / attachment.size - ) - : undefined; button = (
- {spinner && !hasError && ( - - )} + {spinner && !hasError && } {hasError &&
}
{props.src != null && ( diff --git a/ts/components/fun/base/FunResults.tsx b/ts/components/fun/base/FunResults.tsx index a7cc9088f9..027356437d 100644 --- a/ts/components/fun/base/FunResults.tsx +++ b/ts/components/fun/base/FunResults.tsx @@ -52,7 +52,5 @@ export function FunResultsButton(props: FunResultsButtonProps): JSX.Element { } export function FunResultsSpinner(): JSX.Element { - return ( - - ); + return ; }