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:
parent
1ca4960924
commit
12d7f24d0f
30 changed files with 1176 additions and 102 deletions
|
@ -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,
|
||||
|
|
70
ts/state/ducks/audioPlayer.ts
Normal file
70
ts/state/ducks/audioPlayer.ts
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
36
ts/state/smart/MessageAudio.tsx
Normal file
36
ts/state/smart/MessageAudio.tsx
Normal 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);
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue