Unify audio playback under App component
This commit is contained in:
parent
8b30fc17cd
commit
2cd4160422
19 changed files with 290 additions and 80 deletions
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -30,6 +30,7 @@ const defaultMessage: MessageDataPropsType = {
|
|||
conversationType: 'direct',
|
||||
direction: 'incoming',
|
||||
id: 'my-message',
|
||||
renderingContext: 'storybook',
|
||||
isBlocked: false,
|
||||
isMessageRequestAccepted: true,
|
||||
previews: [],
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -50,6 +50,7 @@ const defaultMessageProps: MessagesProps = {
|
|||
),
|
||||
i18n,
|
||||
id: 'messageId',
|
||||
renderingContext: 'storybook',
|
||||
interactionMode: 'keyboard',
|
||||
isBlocked: false,
|
||||
isMessageRequestAccepted: true,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue