// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { useCallback, useRef, useEffect, useState } from 'react'; import type { RefObject } 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 type { PushPanelForConversationActionType } from '../../state/ducks/conversations'; import { isDownloaded } from '../../types/Attachment'; import type { DirectionType, MessageStatusType } from './Message'; import type { ComputePeaksResult } from '../VoiceNotesPlaybackContext'; import { MessageMetadata } from './MessageMetadata'; import * as log from '../../logging/log'; import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer'; import { PlaybackRateButton } from '../PlaybackRateButton'; import { durationToPlaybackText } from '../../util/durationToPlaybackText'; export type OwnProps = Readonly<{ active: | Pick< ActiveAudioPlayerStateType, 'currentTime' | 'duration' | 'playing' | 'playbackRate' > | undefined; buttonRef: RefObject; 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; played: boolean; status?: MessageStatusType; textPending?: boolean; timestamp: number; kickOffAttachmentDownload(): void; onCorrupted(): void; computePeaks(url: string, barCount: number): Promise; onPlayMessage: (id: string, position: number) => void; }>; export type DispatchProps = Readonly<{ pushPanelForConversation: PushPanelForConversationActionType; setPosition: (positionAsRatio: number) => void; setPlaybackRate: (rate: number) => void; setIsPlaying: (value: boolean) => void; }>; export type Props = OwnProps & DispatchProps; type ButtonProps = { mod?: string; label: string; visible?: boolean; onClick: () => void; onMouseDown?: () => void; onMouseUp?: () => void; }; 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 SPRING_CONFIG = { mass: 0.5, tension: 350, friction: 20, velocity: 0.01, }; const DOT_DIV_WIDTH = 14; /** Handles animations, key events, and stopping event propagation */ const PlaybackButton = React.forwardRef( function ButtonInner(props, ref) { const { mod, label, onClick, visible = true } = props; const [animProps] = useSpring( { 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] ); return (