137 lines
3.4 KiB
TypeScript
137 lines
3.4 KiB
TypeScript
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
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 { SpinnerV2 } from './SpinnerV2';
|
|
|
|
const SPRING_CONFIG = {
|
|
mass: 0.5,
|
|
tension: 350,
|
|
friction: 20,
|
|
velocity: 0.01,
|
|
};
|
|
|
|
export type ButtonProps = {
|
|
context?: 'incoming' | 'outgoing';
|
|
variant: 'message' | 'mini' | 'draft';
|
|
mod: 'play' | 'pause' | 'not-downloaded' | 'downloading' | 'computing';
|
|
downloadFraction?: number;
|
|
label: string;
|
|
visible?: boolean;
|
|
onClick: () => void;
|
|
onMouseDown?: () => void;
|
|
onMouseUp?: () => void;
|
|
};
|
|
|
|
/** Handles animations, key events, and stopping event propagation */
|
|
export const PlaybackButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
function ButtonInner(props, ref) {
|
|
const {
|
|
context,
|
|
downloadFraction,
|
|
label,
|
|
mod,
|
|
onClick,
|
|
variant,
|
|
visible = true,
|
|
} = props;
|
|
let size = 36;
|
|
if (variant === 'mini') {
|
|
size = 14;
|
|
} else if (variant === 'draft') {
|
|
size = 18;
|
|
}
|
|
|
|
const reducedMotion = useReducedMotion();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
|
const [animProps] = useSpring(
|
|
{
|
|
immediate: reducedMotion,
|
|
config: SPRING_CONFIG,
|
|
to: { scale: visible ? 1 : 0 },
|
|
},
|
|
[visible]
|
|
);
|
|
|
|
// Clicking button toggle playback
|
|
const onButtonClick = useCallback(
|
|
(event: React.MouseEvent) => {
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
onClick();
|
|
},
|
|
[onClick]
|
|
);
|
|
|
|
// Keyboard playback toggle
|
|
const onButtonKeyDown = useCallback(
|
|
(event: React.KeyboardEvent) => {
|
|
if (event.key !== 'Enter' && event.key !== 'Space') {
|
|
return;
|
|
}
|
|
event.stopPropagation();
|
|
event.preventDefault();
|
|
|
|
onClick();
|
|
},
|
|
[onClick]
|
|
);
|
|
|
|
let content: JSX.Element | null = null;
|
|
const strokeWidth = variant === 'message' ? 2 : 1;
|
|
if (mod === 'downloading' && downloadFraction) {
|
|
content = (
|
|
<ProgressCircle
|
|
fractionComplete={downloadFraction}
|
|
width={size}
|
|
strokeWidth={strokeWidth}
|
|
/>
|
|
);
|
|
} else if (
|
|
mod === 'computing' ||
|
|
(mod === 'downloading' && !downloadFraction)
|
|
) {
|
|
content = (
|
|
<div className="PlaybackButton__SpinnerV2-container">
|
|
<SpinnerV2
|
|
className="PlaybackButton__SpinnerV2"
|
|
size={size}
|
|
strokeWidth={strokeWidth * 2}
|
|
marginRatio={1}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const buttonComponent = (
|
|
<button
|
|
type="button"
|
|
ref={ref}
|
|
className={classNames(
|
|
'PlaybackButton',
|
|
`PlaybackButton--variant-${variant}`,
|
|
context && `PlaybackButton--context-${context}`,
|
|
mod ? `PlaybackButton--${mod}` : undefined
|
|
)}
|
|
onClick={onButtonClick}
|
|
onKeyDown={onButtonKeyDown}
|
|
tabIndex={0}
|
|
aria-label={label}
|
|
>
|
|
{content}
|
|
</button>
|
|
);
|
|
|
|
if (variant === 'message') {
|
|
return <animated.div style={animProps}>{buttonComponent}</animated.div>;
|
|
}
|
|
|
|
return buttonComponent;
|
|
}
|
|
);
|