mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2025-12-24 20:26:24 +00:00
Merge ProgressCircle into SpinnerV2
This commit is contained in:
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -49,7 +49,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.FunResults__Spinner {
|
||||
color: light-dark(variables.$color-gray-25, variables.$color-gray-45);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = (
|
||||
<div className="BackupMediaDownloadProgress__icon">
|
||||
<ProgressCircle
|
||||
fractionComplete={fractionComplete}
|
||||
<SpinnerV2
|
||||
size={24}
|
||||
strokeWidth={3}
|
||||
marginRatio={1}
|
||||
min={0}
|
||||
max={totalBytes}
|
||||
value={downloadedBytes}
|
||||
variant="brand"
|
||||
ariaLabel={i18n('icu:BackupMediaDownloadProgress__title-paused')}
|
||||
/>
|
||||
</div>
|
||||
@@ -181,8 +182,14 @@ export function BackupMediaDownloadProgress({
|
||||
);
|
||||
icon = (
|
||||
<div className="BackupMediaDownloadProgress__icon">
|
||||
<ProgressCircle
|
||||
fractionComplete={fractionComplete}
|
||||
<SpinnerV2
|
||||
size={24}
|
||||
strokeWidth={3}
|
||||
marginRatio={1}
|
||||
min={0}
|
||||
max={totalBytes}
|
||||
value={downloadedBytes}
|
||||
variant="brand"
|
||||
ariaLabel={i18n('icu:BackupMediaDownloadProgress__title-offline')}
|
||||
/>
|
||||
</div>
|
||||
@@ -204,8 +211,14 @@ export function BackupMediaDownloadProgress({
|
||||
);
|
||||
icon = (
|
||||
<div className="BackupMediaDownloadProgress__icon">
|
||||
<ProgressCircle
|
||||
fractionComplete={fractionComplete}
|
||||
<SpinnerV2
|
||||
size={24}
|
||||
strokeWidth={3}
|
||||
marginRatio={1}
|
||||
min={0}
|
||||
max={totalBytes}
|
||||
value={downloadedBytes}
|
||||
variant="brand"
|
||||
ariaLabel={i18n('icu:BackupMediaDownloadProgress__title-in-progress')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -303,12 +303,10 @@ export function CallingLobby({
|
||||
{callMode === CallMode.Adhoc ? (
|
||||
isAdhocJoinRequestPending ? (
|
||||
<div className="CallingLobby__CallLinkNotice CallingLobby__CallLinkNotice--join-request-pending">
|
||||
<SpinnerV2
|
||||
className="CallingLobby__CallLinkJoinRequestPendingSpinner"
|
||||
size={16}
|
||||
strokeWidth={3}
|
||||
/>
|
||||
{i18n('icu:CallingLobby__CallLinkNotice--join-request-pending')}
|
||||
<SpinnerV2 size={16} strokeWidth={1.5} />
|
||||
<span className="CallingLobby__CallLinkJoinRequestPendingText">
|
||||
{i18n('icu:CallingLobby__CallLinkNotice--join-request-pending')}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="CallingLobby__CallLinkNotice">
|
||||
|
||||
@@ -37,7 +37,9 @@ export function DonationProgressModal(props: PropsType): JSX.Element {
|
||||
noEscapeClose
|
||||
noMouseClose
|
||||
>
|
||||
<SpinnerV2 size={58} strokeWidth={8} />
|
||||
<div className="DonationProgressModal__SpinnerV2">
|
||||
<SpinnerV2 size={58} strokeWidth={4} variant="brand" />
|
||||
</div>
|
||||
<div className="DonationProgressModal__text">
|
||||
{i18n('icu:Donations__Processing')}
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<HTMLButtonElement, ButtonProps>(
|
||||
function ButtonInner(props, ref) {
|
||||
const {
|
||||
context,
|
||||
downloadFraction,
|
||||
attachment,
|
||||
label,
|
||||
mod,
|
||||
onClick,
|
||||
@@ -84,25 +84,24 @@ export const PlaybackButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
|
||||
let content: JSX.Element | null = null;
|
||||
const strokeWidth = variant === 'message' ? 2 : 1;
|
||||
if (mod === 'downloading' && downloadFraction) {
|
||||
if (mod === 'computing' || mod === 'downloading') {
|
||||
content = (
|
||||
<ProgressCircle
|
||||
fractionComplete={downloadFraction}
|
||||
width={size}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
);
|
||||
} else if (
|
||||
mod === 'computing' ||
|
||||
(mod === 'downloading' && !downloadFraction)
|
||||
) {
|
||||
content = (
|
||||
<div className="PlaybackButton__SpinnerV2-container">
|
||||
<div className="PlaybackButton__Spinner-container">
|
||||
<SpinnerV2
|
||||
className="PlaybackButton__SpinnerV2"
|
||||
variant={
|
||||
context === 'incoming'
|
||||
? 'no-background-incoming'
|
||||
: 'no-background'
|
||||
}
|
||||
min={0}
|
||||
max={attachment?.size}
|
||||
value={
|
||||
attachment?.size && attachment?.totalDownloaded
|
||||
? attachment.totalDownloaded
|
||||
: undefined
|
||||
}
|
||||
size={size}
|
||||
strokeWidth={strokeWidth * 2}
|
||||
marginRatio={1}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<typeof ProgressCircle>;
|
||||
export default {
|
||||
title: 'Components/ProgressCircle',
|
||||
component: ProgressCircle,
|
||||
args: {
|
||||
fractionComplete: 0,
|
||||
width: undefined,
|
||||
strokeWidth: undefined,
|
||||
ariaLabel: 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;
|
||||
}
|
||||
@@ -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 (
|
||||
<svg
|
||||
className="ProgressCircle"
|
||||
width={widthInPixels}
|
||||
height={widthInPixels}
|
||||
role="progressbar"
|
||||
aria-label={ariaLabel}
|
||||
aria-valuenow={Math.trunc(fractionComplete * 100)}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={100}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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<Props>;
|
||||
|
||||
export function Default(args: Props): JSX.Element {
|
||||
return <SpinnerV2 {...args} />;
|
||||
}
|
||||
|
||||
export function Thin(args: Props): JSX.Element {
|
||||
return <SpinnerV2 {...args} strokeWidth={1} />;
|
||||
}
|
||||
|
||||
export function Thick(args: Props): JSX.Element {
|
||||
return <SpinnerV2 {...args} strokeWidth={6} />;
|
||||
}
|
||||
|
||||
export function NoMargin(args: Props): JSX.Element {
|
||||
return <SpinnerV2 {...args} marginRatio={1} strokeWidth={6} />;
|
||||
}
|
||||
|
||||
export function BigMargin(args: Props): JSX.Element {
|
||||
return <SpinnerV2 {...args} marginRatio={0.5} strokeWidth={6} />;
|
||||
}
|
||||
|
||||
export function Styled(args: Props): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
<style>{`
|
||||
.red-spinner {
|
||||
color: light-dark(hsl(0deg 100% 70%), hsl(0deg 100% 30%));
|
||||
}
|
||||
`}</style>
|
||||
<SpinnerV2 {...args} className="red-spinner" />
|
||||
<div className={tw('bg-background-overlay')}>
|
||||
<SpinnerV2 {...args} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Thin(args: Props): JSX.Element {
|
||||
return (
|
||||
<div className={tw('bg-background-overlay')}>
|
||||
<SpinnerV2 {...args} strokeWidth={1} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Thick(args: Props): JSX.Element {
|
||||
return (
|
||||
<div className={tw('bg-background-overlay')}>
|
||||
<SpinnerV2 {...args} strokeWidth={6} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoMargin(args: Props): JSX.Element {
|
||||
return (
|
||||
<div className={tw('bg-background-overlay')}>
|
||||
<SpinnerV2 {...args} marginRatio={1} strokeWidth={6} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function BigMargin(args: Props): JSX.Element {
|
||||
return (
|
||||
<div className={tw('bg-background-overlay')}>
|
||||
<SpinnerV2 {...args} marginRatio={0.5} strokeWidth={6} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SpinnerToProgress(args: Props): JSX.Element {
|
||||
const [value, setValue] = useState<number | undefined>();
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setValue(v => {
|
||||
if (v == null) {
|
||||
return 0.3;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}, 2000);
|
||||
return () => {
|
||||
clearInterval(timer);
|
||||
};
|
||||
});
|
||||
return (
|
||||
<div className={tw('bg-background-overlay')}>
|
||||
<SpinnerV2 {...args} value={value} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, SpinnerVariantStyles>;
|
||||
|
||||
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 = (
|
||||
<circle
|
||||
className={tw(bg, 'fill-none')}
|
||||
strokeWidth={strokeWidth}
|
||||
r={radius}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
/>
|
||||
);
|
||||
|
||||
if (value === 'indeterminate') {
|
||||
return (
|
||||
<svg
|
||||
className={tw('fill-none')}
|
||||
width={sizeInPixels}
|
||||
height={sizeInPixels}
|
||||
>
|
||||
{bgElem}
|
||||
<g className={tw('origin-center animate-spinner-v2-rotate')}>
|
||||
<circle
|
||||
className={tw(fg, 'animate-spinner-v2-dash fill-none')}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
style={{
|
||||
strokeLinecap: 'round',
|
||||
}}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
const fractionComplete = roundFractionForProgressBar(
|
||||
(value - min) / (max - min)
|
||||
);
|
||||
|
||||
return (
|
||||
<svg
|
||||
className={classNames('SpinnerV2', className)}
|
||||
viewBox={`0 0 ${size * 2} ${size * 2}`}
|
||||
style={{
|
||||
height: size,
|
||||
width: size,
|
||||
}}
|
||||
className={tw('fill-none')}
|
||||
width={sizeInPixels}
|
||||
height={sizeInPixels}
|
||||
role="progressbar"
|
||||
aria-label={ariaLabel}
|
||||
aria-valuenow={value}
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
>
|
||||
<circle
|
||||
className="SpinnerV2__Path"
|
||||
cx={size}
|
||||
cy={size}
|
||||
r={radius}
|
||||
fill="none"
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
{bgElem}
|
||||
<g className={tw('origin-center -rotate-90')}>
|
||||
<circle
|
||||
className={tw(
|
||||
fg,
|
||||
'fill-none transition-[stroke-dashoffset] duration-500 ease-out-cubic'
|
||||
)}
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
style={{ strokeLinecap: 'round' }}
|
||||
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}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<AttachmentForUIType>;
|
||||
@@ -130,20 +128,21 @@ export function AttachmentDetailPill({
|
||||
{formatFileSize(totalSize)}
|
||||
</div>
|
||||
);
|
||||
} else if (totalDownloadedSize > 0) {
|
||||
const downloadFraction = roundFractionForProgressBar(
|
||||
totalDownloadedSize / totalSize
|
||||
);
|
||||
} else {
|
||||
const isDownloading = totalDownloadedSize > 0;
|
||||
|
||||
ariaLabel = i18n('icu:cancelDownload');
|
||||
onClick = cancelDownloadClick;
|
||||
onKeyDown = cancelDownloadKeyDown;
|
||||
control = (
|
||||
<div className="AttachmentDetailPill__spinner-wrapper">
|
||||
<ProgressCircle
|
||||
fractionComplete={downloadFraction}
|
||||
width={24}
|
||||
<SpinnerV2
|
||||
min={0}
|
||||
max={totalSize}
|
||||
value={isDownloading ? totalDownloadedSize : 'indeterminate'}
|
||||
size={24}
|
||||
strokeWidth={2}
|
||||
marginRatio={1}
|
||||
/>
|
||||
<div className="AttachmentDetailPill__stop-icon" />
|
||||
</div>
|
||||
@@ -156,21 +155,6 @@ export function AttachmentDetailPill({
|
||||
{formatFileSize(totalSize)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
ariaLabel = i18n('icu:cancelDownload');
|
||||
onClick = cancelDownloadClick;
|
||||
onKeyDown = cancelDownloadKeyDown;
|
||||
control = (
|
||||
<div className="AttachmentDetailPill__spinner-wrapper">
|
||||
<Spinner svgSize="small" size="24px" />
|
||||
<div className="AttachmentDetailPill__stop-icon" />
|
||||
</div>
|
||||
);
|
||||
text = (
|
||||
<div className="AttachmentDetailPill__text-wrapper">
|
||||
{formatFileSize(totalSize)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -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 ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'AttachmentStatusIcon__progress-container',
|
||||
isIncoming
|
||||
? 'AttachmentStatusIcon__progress-container--incoming'
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<ProgressCircle
|
||||
fractionComplete={downloadFraction}
|
||||
width={36}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</div>
|
||||
) : undefined}
|
||||
<div
|
||||
className={classNames(
|
||||
'AttachmentStatusIcon__progress-container',
|
||||
isIncoming
|
||||
? 'AttachmentStatusIcon__progress-container--incoming'
|
||||
: undefined
|
||||
)}
|
||||
>
|
||||
<SpinnerV2
|
||||
min={0}
|
||||
max={size}
|
||||
value={spinnerValue}
|
||||
variant={isIncoming ? 'no-background-incoming' : 'no-background'}
|
||||
size={36}
|
||||
strokeWidth={2}
|
||||
marginRatio={1}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'AttachmentStatusIcon__circle-icon',
|
||||
|
||||
@@ -6,7 +6,6 @@ import React, { useCallback } from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { ImageOrBlurhash } from '../ImageOrBlurhash';
|
||||
import { Spinner } from '../Spinner';
|
||||
import type { LocalizerType, ThemeType } from '../../types/Util';
|
||||
import type { AttachmentForUIType } from '../../types/Attachment';
|
||||
import {
|
||||
@@ -14,9 +13,8 @@ import {
|
||||
isIncremental,
|
||||
isReadyToView,
|
||||
} from '../../types/Attachment';
|
||||
import { ProgressCircle } from '../ProgressCircle';
|
||||
import { SpinnerV2 } from '../SpinnerV2';
|
||||
import { useUndownloadableMediaHandler } from '../../hooks/useUndownloadableMediaHandler';
|
||||
import { roundFractionForProgressBar } from '../../util/numbers';
|
||||
|
||||
export enum CurveType {
|
||||
None = 0,
|
||||
@@ -334,41 +332,12 @@ export function getSpinner({
|
||||
i18n: LocalizerType;
|
||||
tabIndex: number | undefined;
|
||||
}): JSX.Element | undefined {
|
||||
const downloadFraction =
|
||||
attachment.pending &&
|
||||
!isIncremental(attachment) &&
|
||||
attachment.size &&
|
||||
attachment.totalDownloaded
|
||||
? roundFractionForProgressBar(
|
||||
attachment.totalDownloaded / attachment.size
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (downloadFraction) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="module-image__overlay-circle"
|
||||
aria-label={i18n('icu:cancelDownload')}
|
||||
onClick={cancelDownloadClick}
|
||||
onKeyDown={cancelDownloadKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__stop-icon" />
|
||||
<div className="module-image__progress-circle-wrapper">
|
||||
<ProgressCircle
|
||||
fractionComplete={downloadFraction}
|
||||
width={44}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (!attachment.pending) {
|
||||
return undefined;
|
||||
}
|
||||
const spinnerValue =
|
||||
(attachment.pending &&
|
||||
!isIncremental(attachment) &&
|
||||
attachment.size &&
|
||||
attachment.totalDownloaded) ||
|
||||
undefined;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -379,13 +348,16 @@ export function getSpinner({
|
||||
onKeyDown={cancelDownloadKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
>
|
||||
<div className="module-image__spinner-container">
|
||||
<Spinner
|
||||
moduleClassName="module-image-spinner"
|
||||
svgSize="normal"
|
||||
size="44px"
|
||||
<div className="module-image__stop-icon" />
|
||||
<div className="module-image__progress-circle-wrapper">
|
||||
<SpinnerV2
|
||||
min={0}
|
||||
max={attachment.size}
|
||||
value={spinnerValue}
|
||||
size={44}
|
||||
strokeWidth={2}
|
||||
marginRatio={1}
|
||||
/>
|
||||
<div className="module-image__stop-icon" />
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -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<HTMLButtonElement>;
|
||||
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 = (
|
||||
<PlaybackButton
|
||||
variant="message"
|
||||
mod="downloading"
|
||||
downloadFraction={downloadFraction}
|
||||
attachment={attachment}
|
||||
onClick={cancelAttachmentDownload}
|
||||
label={i18n('icu:MessageAudio--pending')}
|
||||
context={direction}
|
||||
|
||||
@@ -154,13 +154,7 @@ export function FunGifPreview(props: FunGifPreviewProps): JSX.Element {
|
||||
}
|
||||
/>
|
||||
<div className="FunGifPreview__Backdrop" role="status">
|
||||
{spinner && !hasError && (
|
||||
<SpinnerV2
|
||||
className="FunGifPreview__Spinner"
|
||||
size={36}
|
||||
strokeWidth={4}
|
||||
/>
|
||||
)}
|
||||
{spinner && !hasError && <SpinnerV2 size={36} strokeWidth={2} />}
|
||||
{hasError && <div className="FunGifPreview__ErrorIcon" />}
|
||||
</div>
|
||||
{props.src != null && (
|
||||
|
||||
@@ -52,7 +52,5 @@ export function FunResultsButton(props: FunResultsButtonProps): JSX.Element {
|
||||
}
|
||||
|
||||
export function FunResultsSpinner(): JSX.Element {
|
||||
return (
|
||||
<SpinnerV2 className="FunResults__Spinner" size={36} strokeWidth={4} />
|
||||
);
|
||||
return <SpinnerV2 size={36} strokeWidth={2} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user