Unify audio playback under App component

This commit is contained in:
Fedor Indutny 2021-06-29 12:58:29 -07:00 committed by GitHub
parent 8b30fc17cd
commit 2cd4160422
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 290 additions and 80 deletions

View file

@ -175,7 +175,8 @@ const globalContents: Contents = {
export const GlobalAudioContext = React.createContext<Contents>(globalContents);
export type GlobalAudioProps = {
conversationId: string;
conversationId: string | undefined;
isPaused: boolean;
children?: React.ReactNode | React.ReactChildren;
};
@ -185,6 +186,7 @@ export type GlobalAudioProps = {
*/
export const GlobalAudioProvider: React.FC<GlobalAudioProps> = ({
conversationId,
isPaused,
children,
}) => {
// When moving between conversations - stop audio
@ -194,6 +196,13 @@ export const GlobalAudioProvider: React.FC<GlobalAudioProps> = ({
};
}, [conversationId]);
// Pause when requested by parent
React.useEffect(() => {
if (isPaused) {
globalContents.audio.pause();
}
}, [isPaused]);
return (
<GlobalAudioContext.Provider value={globalContents}>
{children}

View file

@ -47,19 +47,22 @@ const renderEmojiPicker: Props['renderEmojiPicker'] = ({
);
const MessageAudioContainer: React.FC<AudioAttachmentProps> = props => {
const [activeAudioID, setActiveAudioID] = React.useState<string | undefined>(
undefined
);
const [active, setActive] = React.useState<{
id?: string;
context?: string;
}>({});
const audio = React.useMemo(() => new Audio(), []);
return (
<MessageAudio
{...props}
id="storybook"
renderingContext="storybook"
audio={audio}
computePeaks={computePeaks}
setActiveAudioID={setActiveAudioID}
activeAudioID={activeAudioID}
setActiveAudioID={(id, context) => setActive({ id, context })}
activeAudioID={active.id}
activeAudioContext={active.context}
/>
);
};
@ -101,6 +104,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
undefined,
i18n,
id: text('id', overrideProps.id || ''),
renderingContext: 'storybook',
interactionMode: overrideProps.interactionMode || 'keyboard',
isSticker: isBoolean(overrideProps.isSticker)
? overrideProps.isSticker

View file

@ -89,6 +89,7 @@ export type DirectionType = typeof Directions[number];
export type AudioAttachmentProps = {
id: string;
renderingContext: string;
i18n: LocalizerType;
buttonRef: React.RefObject<HTMLButtonElement>;
direction: DirectionType;
@ -103,6 +104,7 @@ export type AudioAttachmentProps = {
export type PropsData = {
id: string;
renderingContext: string;
contactNameColor?: ContactNameColorType;
conversationColor: ConversationColorType;
customColor?: CustomColorType;
@ -751,6 +753,7 @@ export class Message extends React.Component<Props, State> {
direction,
i18n,
id,
renderingContext,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
quote,
@ -849,6 +852,7 @@ export class Message extends React.Component<Props, State> {
i18n,
buttonRef: this.audioButtonRef,
id,
renderingContext,
direction,
theme,
attachment: firstAttachment,

View file

@ -14,6 +14,7 @@ import { ComputePeaksResult } from '../GlobalAudioContext';
export type Props = {
direction?: 'incoming' | 'outgoing';
id: string;
renderingContext: string;
i18n: LocalizerType;
attachment: AttachmentType;
withContentAbove: boolean;
@ -28,7 +29,8 @@ export type Props = {
computePeaks(url: string, barCount: number): Promise<ComputePeaksResult>;
activeAudioID: string | undefined;
setActiveAudioID: (id: string | undefined) => void;
activeAudioContext: string | undefined;
setActiveAudioID: (id: string | undefined, context: string) => void;
};
type ButtonProps = {
@ -121,14 +123,19 @@ const Button: React.FC<ButtonProps> = props => {
* toggle Play/Pause button.
*
* A global audio player is used for playback and access is managed by the
* `activeAudioID` property. Whenever `activeAudioID` property is equal to `id`
* the instance of the `MessageAudio` assumes the ownership of the `Audio`
* instance and fully manages it.
* `activeAudioID` and `activeAudioContext` properties. Whenever both
* `activeAudioID` and `activeAudioContext` are equal to `id` and `context`
* respectively the instance of the `MessageAudio` assumes the ownership of the
* `Audio` instance and fully manages it.
*
* `context` is required for displaying separate MessageAudio instances in
* MessageDetails and Message React components.
*/
export const MessageAudio: React.FC<Props> = (props: Props) => {
const {
i18n,
id,
renderingContext,
direction,
attachment,
withContentAbove,
@ -142,12 +149,14 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
computePeaks,
activeAudioID,
activeAudioContext,
setActiveAudioID,
} = props;
assert(audio !== null, 'GlobalAudioContext always provides audio');
const isActive = activeAudioID === id;
const isActive =
activeAudioID === id && activeAudioContext === renderingContext;
const waveformRef = useRef<HTMLDivElement | null>(null);
const [isPlaying, setIsPlaying] = useState(isActive && !audio.paused);
@ -317,7 +326,7 @@ export const MessageAudio: React.FC<Props> = (props: Props) => {
if (!isActive && !isPlaying) {
window.log.info('MessageAudio: changing owner', id);
setActiveAudioID(id);
setActiveAudioID(id, renderingContext);
// Pause old audio
if (!audio.paused) {

View file

@ -30,6 +30,7 @@ const defaultMessage: MessageDataPropsType = {
conversationType: 'direct',
direction: 'incoming',
id: 'my-message',
renderingContext: 'storybook',
isBlocked: false,
isMessageRequestAccepted: true,
previews: [],

View file

@ -6,7 +6,6 @@ import classNames from 'classnames';
import moment from 'moment';
import { noop } from 'lodash';
import { GlobalAudioProvider } from '../GlobalAudioContext';
import { Avatar } from '../Avatar';
import { ContactName } from './ContactName';
import {
@ -46,7 +45,7 @@ export type Props = {
contacts: Array<Contact>;
contactNameColor?: ContactNameColorType;
errors: Array<Error>;
message: MessagePropsDataType;
message: Omit<MessagePropsDataType, 'renderingContext'>;
receivedAt: number;
sentAt: number;
@ -266,57 +265,54 @@ export class MessageDetail extends React.Component<Props> {
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
<div className="module-message-detail__message-container">
<GlobalAudioProvider conversationId={message.conversationId}>
<Message
{...message}
checkForAccount={checkForAccount}
clearSelectedMessage={clearSelectedMessage}
contactNameColor={contactNameColor}
deleteMessage={deleteMessage}
deleteMessageForEveryone={deleteMessageForEveryone}
disableMenu
disableScroll
displayTapToViewMessage={displayTapToViewMessage}
downloadAttachment={downloadAttachment}
doubleCheckMissingQuoteReference={
doubleCheckMissingQuoteReference
}
i18n={i18n}
interactionMode={interactionMode}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
onHeightChange={noop}
openConversation={openConversation}
openLink={openLink}
reactToMessage={reactToMessage}
renderAudioAttachment={renderAudioAttachment}
renderEmojiPicker={renderEmojiPicker}
replyToMessage={replyToMessage}
retrySend={retrySend}
showForwardMessageModal={showForwardMessageModal}
scrollToQuotedMessage={() => {
assert(
false,
'scrollToQuotedMessage should never be called because scrolling is disabled'
);
}}
showContactDetail={showContactDetail}
showContactModal={showContactModal}
showExpiredIncomingTapToViewToast={
showExpiredIncomingTapToViewToast
}
showExpiredOutgoingTapToViewToast={
showExpiredOutgoingTapToViewToast
}
showMessageDetail={() => {
assert(
false,
"showMessageDetail should never be called because the menu is disabled (and we're already in the message detail!)"
);
}}
showVisualAttachment={showVisualAttachment}
/>
</GlobalAudioProvider>
<Message
{...message}
renderingContext="conversation/MessageDetail"
checkForAccount={checkForAccount}
clearSelectedMessage={clearSelectedMessage}
contactNameColor={contactNameColor}
deleteMessage={deleteMessage}
deleteMessageForEveryone={deleteMessageForEveryone}
disableMenu
disableScroll
displayTapToViewMessage={displayTapToViewMessage}
downloadAttachment={downloadAttachment}
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
i18n={i18n}
interactionMode={interactionMode}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
onHeightChange={noop}
openConversation={openConversation}
openLink={openLink}
reactToMessage={reactToMessage}
renderAudioAttachment={renderAudioAttachment}
renderEmojiPicker={renderEmojiPicker}
replyToMessage={replyToMessage}
retrySend={retrySend}
showForwardMessageModal={showForwardMessageModal}
scrollToQuotedMessage={() => {
assert(
false,
'scrollToQuotedMessage should never be called because scrolling is disabled'
);
}}
showContactDetail={showContactDetail}
showContactModal={showContactModal}
showExpiredIncomingTapToViewToast={
showExpiredIncomingTapToViewToast
}
showExpiredOutgoingTapToViewToast={
showExpiredOutgoingTapToViewToast
}
showMessageDetail={() => {
assert(
false,
"showMessageDetail should never be called because the menu is disabled (and we're already in the message detail!)"
);
}}
showVisualAttachment={showVisualAttachment}
/>
</div>
<table className="module-message-detail__info">
<tbody>

View file

@ -50,6 +50,7 @@ const defaultMessageProps: MessagesProps = {
),
i18n,
id: 'messageId',
renderingContext: 'storybook',
interactionMode: 'keyboard',
isBlocked: false,
isMessageRequestAccepted: true,

View file

@ -15,8 +15,6 @@ import Measure from 'react-measure';
import { ScrollDownButton } from './ScrollDownButton';
import { GlobalAudioProvider } from '../GlobalAudioContext';
import { LocalizerType } from '../../types/Util';
import { ConversationType } from '../../state/ducks/conversations';
import { assert } from '../../util/assert';
@ -1424,9 +1422,8 @@ export class Timeline extends React.PureComponent<PropsType, StateType> {
>
{timelineWarning}
<GlobalAudioProvider conversationId={id}>
{autoSizer}
</GlobalAudioProvider>
{autoSizer}
{shouldShowScrollDownButton ? (
<ScrollDownButton
conversationId={id}

View file

@ -80,7 +80,7 @@ type LinkNotificationType = {
};
type MessageType = {
type: 'message';
data: MessageProps;
data: Omit<MessageProps, 'renderingContext'>;
};
type UnsupportedMessageType = {
type: 'unsupportedMessage';
@ -189,7 +189,13 @@ export class TimelineItem extends React.PureComponent<PropsType> {
if (item.type === 'message') {
return (
<Message {...this.props} {...item.data} i18n={i18n} theme={theme} />
<Message
{...this.props}
{...item.data}
i18n={i18n}
theme={theme}
renderingContext="conversation/TimelineItem"
/>
);
}