New UI for audio playback and global audio player

Introduce new UI and behavior for playing audio attachments in
conversations. Previously, playback stopped unexpectedly during window
resizes and scrolling through the messages due to the row height
recomputation in `react-virtualized`.

With this commit we introduce `<GlobalAudioContext/>` instance that
wraps whole conversation and provides an `<audio/>` element that
doesn't get re-rendered (or destroyed) whenever `react-virtualized`
recomputes messages. The audio players (with a freshly designed UI) now
share this global `<audio/>` instance and manage access to it using
`audioPlayer.owner` state from the redux.

New UI computes on the fly, caches, and displays waveforms for each
audio attachment. Storybook had to be slightly modified to accomodate
testing of Android bubbles by introducing the new knob for
`authorColor`.
This commit is contained in:
Fedor Indutny 2021-03-10 12:36:58 -08:00 committed by Josh Perez
parent 1ca4960924
commit 12d7f24d0f
30 changed files with 1176 additions and 102 deletions

View file

@ -1,6 +1,7 @@
// Copyright 2019-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { actions as audioPlayer } from './ducks/audioPlayer';
import { actions as calling } from './ducks/calling';
import { actions as conversations } from './ducks/conversations';
import { actions as emojis } from './ducks/emojis';
@ -14,6 +15,7 @@ import { actions as updates } from './ducks/updates';
import { actions as user } from './ducks/user';
export const mapDispatchToProps = {
...audioPlayer,
...calling,
...conversations,
...emojis,

View file

@ -0,0 +1,70 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { useBoundActions } from '../../util/hooks';
import { SwitchToAssociatedViewActionType } from './conversations';
// State
export type AudioPlayerStateType = {
readonly activeAudioID: string | undefined;
};
// Actions
type SetActiveAudioIDAction = {
type: 'audioPlayer/SET_ACTIVE_AUDIO_ID';
payload: {
id: string | undefined;
};
};
type AudioPlayerActionType = SetActiveAudioIDAction;
// Action Creators
export const actions = {
setActiveAudioID,
};
export const useActions = (): typeof actions => useBoundActions(actions);
function setActiveAudioID(id: string | undefined): SetActiveAudioIDAction {
return {
type: 'audioPlayer/SET_ACTIVE_AUDIO_ID',
payload: { id },
};
}
// Reducer
function getEmptyState(): AudioPlayerStateType {
return {
activeAudioID: undefined,
};
}
export function reducer(
state: Readonly<AudioPlayerStateType> = getEmptyState(),
action: Readonly<AudioPlayerActionType | SwitchToAssociatedViewActionType>
): AudioPlayerStateType {
if (action.type === 'audioPlayer/SET_ACTIVE_AUDIO_ID') {
const { payload } = action;
return {
...state,
activeAudioID: payload.id,
};
}
// Reset activeAudioID on conversation change.
if (action.type === 'SWITCH_TO_ASSOCIATED_VIEW') {
return {
...state,
activeAudioID: undefined,
};
}
return state;
}

View file

@ -3,6 +3,7 @@
import { combineReducers } from 'redux';
import { reducer as audioPlayer } from './ducks/audioPlayer';
import { reducer as calling } from './ducks/calling';
import { reducer as conversations } from './ducks/conversations';
import { reducer as emojis } from './ducks/emojis';
@ -16,6 +17,7 @@ import { reducer as updates } from './ducks/updates';
import { reducer as user } from './ducks/user';
export const reducer = combineReducers({
audioPlayer,
calling,
conversations,
emojis,

View file

@ -0,0 +1,36 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { connect } from 'react-redux';
import { MessageAudio } from '../../components/conversation/MessageAudio';
import { mapDispatchToProps } from '../actions';
import { StateType } from '../reducer';
import { WaveformCache } from '../../types/Audio';
import { LocalizerType } from '../../types/Util';
export type Props = {
audio: HTMLAudioElement;
audioContext: AudioContext;
waveformCache: WaveformCache;
direction?: 'incoming' | 'outgoing';
id: string;
i18n: LocalizerType;
url: string;
withContentAbove: boolean;
withContentBelow: boolean;
buttonRef: React.RefObject<HTMLButtonElement>;
};
const mapStateToProps = (state: StateType, props: Props) => {
return {
...props,
...state.audioPlayer,
};
};
const smart = connect(mapStateToProps, mapDispatchToProps);
export const SmartMessageAudio = smart(MessageAudio);

View file

@ -5,6 +5,7 @@ import { pick } from 'lodash';
import React from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { GlobalAudioContext } from '../../components/GlobalAudioContext';
import { Timeline } from '../../components/conversation/Timeline';
import { RenderEmojiPickerProps } from '../../components/conversation/ReactionPicker';
import { StateType } from '../reducer';
@ -23,6 +24,7 @@ import { SmartLastSeenIndicator } from './LastSeenIndicator';
import { SmartHeroRow } from './HeroRow';
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
import { SmartEmojiPicker } from './EmojiPicker';
import { SmartMessageAudio, Props as MessageAudioProps } from './MessageAudio';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
@ -41,6 +43,11 @@ type ExternalProps = {
// are provided by ConversationView in setupTimeline().
};
type AudioAttachmentProps = Omit<
MessageAudioProps,
'audio' | 'audioContext' | 'waveformCache'
>;
function renderItem(
messageId: string,
conversationId: string,
@ -52,9 +59,25 @@ function renderItem(
conversationId={conversationId}
id={messageId}
renderEmojiPicker={renderEmojiPicker}
renderAudioAttachment={renderAudioAttachment}
/>
);
}
function renderAudioAttachment(props: AudioAttachmentProps) {
return (
<GlobalAudioContext.Consumer>
{globalAudioProps => {
return (
globalAudioProps && (
<SmartMessageAudio {...props} {...globalAudioProps} />
)
);
}}
</GlobalAudioContext.Consumer>
);
}
function renderEmojiPicker({
ref,
onPickEmoji,

View file

@ -1,6 +1,7 @@
// Copyright 2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { actions as audioPlayer } from './ducks/audioPlayer';
import { actions as calling } from './ducks/calling';
import { actions as conversations } from './ducks/conversations';
import { actions as emojis } from './ducks/emojis';
@ -14,6 +15,7 @@ import { actions as updates } from './ducks/updates';
import { actions as user } from './ducks/user';
export type ReduxActions = {
audioPlayer: typeof audioPlayer;
calling: typeof calling;
conversations: typeof conversations;
emojis: typeof emojis;