// Copyright 2021 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ThunkAction } from 'redux-thunk'; import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions'; import { Sound } from '../../util/Sound'; import * as Errors from '../../types/errors'; import type { StateType as RootStateType } from '../reducer'; import { selectNextConsecutiveVoiceNoteMessageId } from '../selectors/audioPlayer'; import { getConversationByIdSelector, getSelectedConversationId, } from '../selectors/conversations'; import type { MessageDeletedActionType, MessageChangedActionType, SelectedConversationChangedActionType, ConversationChangedActionType, } from './conversations'; import { SELECTED_CONVERSATION_CHANGED, setVoiceNotePlaybackRate, markViewed, } from './conversations'; import * as log from '../../logging/log'; import { strictAssert } from '../../util/assert'; import { globalMessageAudio } from '../../services/globalMessageAudio'; import { isPlayed } from '../../types/Attachment'; import { getMessageIdForLogging } from '../../util/idForLogging'; import { getMessagePropStatus } from '../selectors/message'; // State export type ActiveAudioPlayerStateType = { readonly playing: boolean; readonly currentTime: number; readonly playbackRate: number; readonly duration: number; }; export type AudioPlayerStateType = { readonly active: | (ActiveAudioPlayerStateType & { id: string; context: string }) | undefined; }; // Actions /** * Sets the current "active" message audio for a particular rendering "context" */ export type SetMessageAudioAction = { type: 'audioPlayer/SET_MESSAGE_AUDIO'; payload: | { id: string; context: string; playbackRate: number; duration: number; } | undefined; }; type SetPlaybackRate = { type: 'audioPlayer/SET_PLAYBACK_RATE'; payload: number; }; type SetIsPlayingAction = { type: 'audioPlayer/SET_IS_PLAYING'; payload: boolean; }; type CurrentTimeUpdated = { type: 'audioPlayer/CURRENT_TIME_UPDATED'; payload: number; }; type MessageAudioEnded = { type: 'audioPlayer/MESSAGE_AUDIO_ENDED'; }; type DurationChanged = { type: 'audioPlayer/DURATION_CHANGED'; payload: number; }; type AudioPlayerActionType = | SetMessageAudioAction | SetIsPlayingAction | SetPlaybackRate | MessageAudioEnded | CurrentTimeUpdated | DurationChanged; // Action Creators export const actions = { loadAndPlayMessageAudio, unloadMessageAudio, setPlaybackRate, setCurrentTime, setIsPlaying, }; export const useActions = (): BoundActionCreatorsMapObject => useBoundActions(actions); function setCurrentTime(value: number): CurrentTimeUpdated { globalMessageAudio.currentTime = value; return { type: 'audioPlayer/CURRENT_TIME_UPDATED', payload: value, }; } function setIsPlaying(value: boolean): SetIsPlayingAction { if (!value) { globalMessageAudio.pause(); } else { void globalMessageAudio.play(); } return { type: 'audioPlayer/SET_IS_PLAYING', payload: value, }; } function setPlaybackRate( conversationId: string, rate: number ): ThunkAction< void, RootStateType, unknown, SetPlaybackRate | ConversationChangedActionType > { return dispatch => { globalMessageAudio.playbackRate = rate; dispatch({ type: 'audioPlayer/SET_PLAYBACK_RATE', payload: rate, }); // update the preference for the conversation dispatch( setVoiceNotePlaybackRate({ conversationId, rate, }) ); }; } function unloadMessageAudio(): SetMessageAudioAction { globalMessageAudio.pause(); return { type: 'audioPlayer/SET_MESSAGE_AUDIO', payload: undefined, }; } const stateChangeConfirmUpSound = new Sound({ src: 'sounds/state-change_confirm-up.ogg', }); const stateChangeConfirmDownSound = new Sound({ src: 'sounds/state-change_confirm-down.ogg', }); /** * @param isConsecutive Is this part of a consecutive group (not first though) */ function loadAndPlayMessageAudio( id: string, url: string, context: string, position: number, isConsecutive: boolean ): ThunkAction< void, RootStateType, unknown, | SetMessageAudioAction | MessageAudioEnded | CurrentTimeUpdated | SetIsPlayingAction | DurationChanged > { return (dispatch, getState) => { // set source to new message and start playing globalMessageAudio.load({ src: url, onTimeUpdate: () => { dispatch({ type: 'audioPlayer/CURRENT_TIME_UPDATED', payload: globalMessageAudio.currentTime, }); }, onLoadedMetadata: () => { strictAssert( !Number.isNaN(globalMessageAudio.duration), 'Audio should have definite duration on `loadedmetadata` event' ); log.info('MessageAudio: `loadedmetadata` event', id); // Sync-up audio's time in case if