// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useCallback, useRef, useEffect, useState } from 'react'; import type { RefObject, ReactNode } from 'react'; import classNames from 'classnames'; import { noop } from 'lodash'; import { animated, useSpring } from '@react-spring/web'; import type { LocalizerType } from '../../types/Util'; import type { AttachmentType } from '../../types/Attachment'; import { isDownloaded } from '../../types/Attachment'; import type { DirectionType, MessageStatusType } from './Message'; import type { ComputePeaksResult } from '../GlobalAudioContext'; import { MessageMetadata } from './MessageMetadata'; import * as log from '../../logging/log'; import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer'; export type OwnProps = Readonly<{ active: ActiveAudioPlayerStateType | undefined; buttonRef: RefObject; renderingContext: string; i18n: LocalizerType; attachment: AttachmentType; collapseMetadata: boolean; withContentAbove: boolean; withContentBelow: boolean; // Message properties. Many are needed for rendering metadata direction: DirectionType; expirationLength?: number; expirationTimestamp?: number; id: string; conversationId: string; played: boolean; showMessageDetail: (id: string) => void; status?: MessageStatusType; textPending?: boolean; timestamp: number; kickOffAttachmentDownload(): void; onCorrupted(): void; computePeaks(url: string, barCount: number): Promise; }>; export type DispatchProps = Readonly<{ loadAndPlayMessageAudio: ( id: string, url: string, context: string, position: number, isConsecutive: boolean ) => void; setCurrentTime: (currentTime: number) => void; setPlaybackRate: (conversationId: string, rate: number) => void; setIsPlaying: (value: boolean) => void; }>; export type Props = OwnProps & DispatchProps; type ButtonProps = { i18n: LocalizerType; variant: 'play' | 'playback-rate'; mod?: string; label: string; visible?: boolean; animateClick?: boolean; onClick: () => void; onMouseDown?: () => void; onMouseUp?: () => void; children?: ReactNode; }; enum State { NotDownloaded = 'NotDownloaded', Pending = 'Pending', Computing = 'Computing', Normal = 'Normal', } // Constants const CSS_BASE = 'module-message__audio-attachment'; const BAR_COUNT = 47; const BAR_NOT_DOWNLOADED_HEIGHT = 2; const BAR_MIN_HEIGHT = 4; const BAR_MAX_HEIGHT = 20; const REWIND_BAR_COUNT = 2; // Increments for keyboard audio seek (in seconds) const SMALL_INCREMENT = 1; const BIG_INCREMENT = 5; const PLAYBACK_RATES = [1, 1.5, 2, 0.5]; const SPRING_CONFIG = { mass: 0.5, tension: 350, friction: 20, velocity: 0.01, }; const DOT_DIV_WIDTH = 14; // Utils const timeToText = (time: number): string => { const hours = Math.floor(time / 3600); let minutes = Math.floor((time % 3600) / 60).toString(); let seconds = Math.floor(time % 60).toString(); if (hours !== 0 && minutes.length < 2) { minutes = `0${minutes}`; } if (seconds.length < 2) { seconds = `0${seconds}`; } return hours ? `${hours}:${minutes}:${seconds}` : `${minutes}:${seconds}`; }; /** * Handles animations, key events, and stoping event propagation * for play button and playback rate button */ const Button = React.forwardRef( (props, ref) => { const { i18n, variant, mod, label, children, onClick, visible = true, animateClick = true, } = props; const [isDown, setIsDown] = useState(false); const [animProps] = useSpring( { config: SPRING_CONFIG, to: isDown && animateClick ? { scale: 1.3 } : { scale: visible ? 1 : 0 }, }, [visible, isDown, animateClick] ); // 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] ); return ( ); } ); const PlayedDot = ({ played, onHide, }: { played: boolean; onHide: () => void; }) => { const start = played ? 1 : 0; const end = played ? 0 : 1; const [animProps] = useSpring( { config: SPRING_CONFIG, from: { scale: start, opacity: start, width: start }, to: { scale: end, opacity: end, width: end * DOT_DIV_WIDTH }, onRest: () => { if (played) { onHide(); } }, }, [played] ); return (