Fix error on message details screen with audio messages
This commit is contained in:
parent
5f9a75d9f4
commit
77c306843d
18 changed files with 417 additions and 224 deletions
|
@ -14,15 +14,26 @@ type Contents = {
|
|||
waveformCache: WaveformCache;
|
||||
};
|
||||
|
||||
export const GlobalAudioContext = React.createContext<Contents | null>(null);
|
||||
// This context's value is effectively global. This is not ideal but is necessary because
|
||||
// the app has multiple React roots. In the future, we should use a single React root
|
||||
// and instantiate these inside of `GlobalAudioProvider`. (We may wish to keep
|
||||
// `audioContext` global, however, as the browser limits the number that can be
|
||||
// created.)
|
||||
const globalContents: Contents = {
|
||||
audio: new Audio(),
|
||||
audioContext: new AudioContext(),
|
||||
waveformCache: new LRU({
|
||||
max: MAX_WAVEFORM_COUNT,
|
||||
}),
|
||||
};
|
||||
|
||||
export const GlobalAudioContext = React.createContext<Contents>(globalContents);
|
||||
|
||||
export type GlobalAudioProps = {
|
||||
conversationId: string;
|
||||
children?: React.ReactNode | React.ReactChildren;
|
||||
};
|
||||
|
||||
const audioContext = new AudioContext();
|
||||
|
||||
/**
|
||||
* A global context that holds Audio, AudioContext, LRU instances that are used
|
||||
* inside the conversation by ts/components/conversation/MessageAudio.tsx
|
||||
|
@ -31,37 +42,15 @@ export const GlobalAudioProvider: React.FC<GlobalAudioProps> = ({
|
|||
conversationId,
|
||||
children,
|
||||
}) => {
|
||||
const audio = React.useRef<HTMLAudioElement | null>(null);
|
||||
const waveformCache = React.useRef<WaveformCache | null>(null);
|
||||
|
||||
// NOTE: We don't want to construct these values on every re-render hence
|
||||
// the constructor calls have to be guarded by `if`s.
|
||||
if (!audio.current) {
|
||||
audio.current = new Audio();
|
||||
}
|
||||
if (!waveformCache.current) {
|
||||
waveformCache.current = new LRU({
|
||||
max: MAX_WAVEFORM_COUNT,
|
||||
});
|
||||
}
|
||||
|
||||
// When moving between conversations - stop audio
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (audio.current) {
|
||||
audio.current.pause();
|
||||
}
|
||||
globalContents.audio.pause();
|
||||
};
|
||||
}, [conversationId]);
|
||||
|
||||
const value = {
|
||||
audio: audio.current,
|
||||
audioContext,
|
||||
waveformCache: waveformCache.current,
|
||||
};
|
||||
|
||||
return (
|
||||
<GlobalAudioContext.Provider value={value}>
|
||||
<GlobalAudioContext.Provider value={globalContents}>
|
||||
{children}
|
||||
</GlobalAudioContext.Provider>
|
||||
);
|
||||
|
|
|
@ -101,7 +101,6 @@ export type PropsData = {
|
|||
isSticker?: boolean;
|
||||
isSelected?: boolean;
|
||||
isSelectedCounter?: number;
|
||||
interactionMode: InteractionModeType;
|
||||
direction: DirectionType;
|
||||
timestamp: number;
|
||||
status?: MessageStatusType;
|
||||
|
@ -150,16 +149,16 @@ export type PropsData = {
|
|||
isBlocked: boolean;
|
||||
isMessageRequestAccepted: boolean;
|
||||
bodyRanges?: BodyRangesType;
|
||||
|
||||
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
|
||||
};
|
||||
|
||||
export type PropsHousekeeping = {
|
||||
i18n: LocalizerType;
|
||||
interactionMode: InteractionModeType;
|
||||
theme?: ThemeType;
|
||||
disableMenu?: boolean;
|
||||
disableScroll?: boolean;
|
||||
collapseMetadata?: boolean;
|
||||
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
|
||||
};
|
||||
|
||||
export type PropsActions = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions';
|
|||
import { number } from '@storybook/addon-knobs';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import { Props as MessageProps } from './Message';
|
||||
import { PropsData as MessageDataPropsType } from './Message';
|
||||
import { MessageDetail, Props } from './MessageDetail';
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
@ -16,42 +16,19 @@ const i18n = setupI18n('en', enMessages);
|
|||
|
||||
const story = storiesOf('Components/Conversation/MessageDetail', module);
|
||||
|
||||
const defaultMessage: MessageProps = {
|
||||
const defaultMessage: MessageDataPropsType = {
|
||||
authorId: 'some-id',
|
||||
authorTitle: 'Max',
|
||||
canReply: true,
|
||||
canDeleteForEveryone: true,
|
||||
canDownload: true,
|
||||
clearSelectedMessage: () => null,
|
||||
conversationId: 'my-convo',
|
||||
conversationType: 'direct',
|
||||
deleteMessage: action('deleteMessage'),
|
||||
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
||||
direction: 'incoming',
|
||||
displayTapToViewMessage: () => null,
|
||||
downloadAttachment: () => null,
|
||||
i18n,
|
||||
id: 'my-message',
|
||||
interactionMode: 'keyboard',
|
||||
isBlocked: false,
|
||||
isMessageRequestAccepted: true,
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
openConversation: () => null,
|
||||
openLink: () => null,
|
||||
previews: [],
|
||||
reactToMessage: () => null,
|
||||
renderEmojiPicker: () => <div />,
|
||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||
replyToMessage: () => null,
|
||||
retrySend: () => null,
|
||||
scrollToQuotedMessage: () => null,
|
||||
showContactDetail: () => null,
|
||||
showContactModal: () => null,
|
||||
showExpiredIncomingTapToViewToast: () => null,
|
||||
showExpiredOutgoingTapToViewToast: () => null,
|
||||
showMessageDetail: () => null,
|
||||
showVisualAttachment: () => null,
|
||||
status: 'sent',
|
||||
text: 'A message from Max',
|
||||
timestamp: Date.now(),
|
||||
|
@ -70,10 +47,32 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
|||
},
|
||||
],
|
||||
errors: overrideProps.errors || [],
|
||||
i18n,
|
||||
message: overrideProps.message || defaultMessage,
|
||||
receivedAt: number('receivedAt', overrideProps.receivedAt || Date.now()),
|
||||
sentAt: number('sentAt', overrideProps.sentAt || Date.now()),
|
||||
|
||||
i18n,
|
||||
interactionMode: 'keyboard',
|
||||
|
||||
clearSelectedMessage: () => null,
|
||||
deleteMessage: action('deleteMessage'),
|
||||
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
||||
displayTapToViewMessage: () => null,
|
||||
downloadAttachment: () => null,
|
||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
||||
openConversation: () => null,
|
||||
openLink: () => null,
|
||||
reactToMessage: () => null,
|
||||
renderAudioAttachment: () => <div>*AudioAttachment*</div>,
|
||||
renderEmojiPicker: () => <div />,
|
||||
replyToMessage: () => null,
|
||||
retrySend: () => null,
|
||||
showContactDetail: () => null,
|
||||
showContactModal: () => null,
|
||||
showExpiredIncomingTapToViewToast: () => null,
|
||||
showExpiredOutgoingTapToViewToast: () => null,
|
||||
showVisualAttachment: () => null,
|
||||
});
|
||||
|
||||
story.add('Delivered Incoming', () => {
|
||||
|
|
|
@ -1,25 +1,32 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import moment from 'moment';
|
||||
|
||||
import { GlobalAudioProvider } from '../GlobalAudioContext';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Message, MessageStatusType, Props as MessageProps } from './Message';
|
||||
import {
|
||||
Message,
|
||||
MessageStatusType,
|
||||
Props as MessagePropsType,
|
||||
PropsData as MessagePropsDataType,
|
||||
} from './Message';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { ColorType } from '../../types/Colors';
|
||||
import { assert } from '../../util/assert';
|
||||
|
||||
type Contact = {
|
||||
status: MessageStatusType;
|
||||
export type Contact = {
|
||||
status: MessageStatusType | null;
|
||||
|
||||
title: string;
|
||||
phoneNumber?: string;
|
||||
name?: string;
|
||||
profileName?: string;
|
||||
avatarPath?: string;
|
||||
color: ColorType;
|
||||
color?: ColorType;
|
||||
isOutgoingKeyError: boolean;
|
||||
isUnidentifiedDelivery: boolean;
|
||||
|
||||
|
@ -30,15 +37,36 @@ type Contact = {
|
|||
};
|
||||
|
||||
export type Props = {
|
||||
sentAt: number;
|
||||
receivedAt: number;
|
||||
|
||||
message: MessageProps;
|
||||
errors: Array<Error>;
|
||||
contacts: Array<Contact>;
|
||||
errors: Array<Error>;
|
||||
message: MessagePropsDataType;
|
||||
receivedAt: number;
|
||||
sentAt: number;
|
||||
|
||||
i18n: LocalizerType;
|
||||
};
|
||||
} & Pick<
|
||||
MessagePropsType,
|
||||
| 'clearSelectedMessage'
|
||||
| 'deleteMessage'
|
||||
| 'deleteMessageForEveryone'
|
||||
| 'displayTapToViewMessage'
|
||||
| 'downloadAttachment'
|
||||
| 'interactionMode'
|
||||
| 'kickOffAttachmentDownload'
|
||||
| 'markAttachmentAsCorrupted'
|
||||
| 'openConversation'
|
||||
| 'openLink'
|
||||
| 'reactToMessage'
|
||||
| 'renderAudioAttachment'
|
||||
| 'renderEmojiPicker'
|
||||
| 'replyToMessage'
|
||||
| 'retrySend'
|
||||
| 'showContactDetail'
|
||||
| 'showContactModal'
|
||||
| 'showExpiredIncomingTapToViewToast'
|
||||
| 'showExpiredOutgoingTapToViewToast'
|
||||
| 'showVisualAttachment'
|
||||
>;
|
||||
|
||||
const _keyForError = (error: Error): string => {
|
||||
return `${error.name}-${error.message}`;
|
||||
|
@ -84,14 +112,14 @@ export class MessageDetail extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public renderDeleteButton(): JSX.Element {
|
||||
const { i18n, message } = this.props;
|
||||
const { deleteMessage, i18n, message } = this.props;
|
||||
|
||||
return (
|
||||
<div className="module-message-detail__delete-button-container">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
message.deleteMessage(message.id);
|
||||
deleteMessage(message.id);
|
||||
}}
|
||||
className="module-message-detail__delete-button"
|
||||
>
|
||||
|
@ -127,7 +155,9 @@ export class MessageDetail extends React.Component<Props> {
|
|||
<div
|
||||
className={classNames(
|
||||
'module-message-detail__contact__status-icon',
|
||||
`module-message-detail__contact__status-icon--${contact.status}`
|
||||
contact.status
|
||||
? `module-message-detail__contact__status-icon--${contact.status}`
|
||||
: undefined
|
||||
)}
|
||||
/>
|
||||
) : null;
|
||||
|
@ -179,13 +209,83 @@ export class MessageDetail extends React.Component<Props> {
|
|||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
const { errors, message, receivedAt, sentAt, i18n } = this.props;
|
||||
const {
|
||||
errors,
|
||||
message,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
|
||||
clearSelectedMessage,
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
i18n,
|
||||
interactionMode,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
openConversation,
|
||||
openLink,
|
||||
reactToMessage,
|
||||
renderAudioAttachment,
|
||||
renderEmojiPicker,
|
||||
replyToMessage,
|
||||
retrySend,
|
||||
showContactDetail,
|
||||
showContactModal,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showVisualAttachment,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
// 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">
|
||||
<Message {...message} i18n={i18n} />
|
||||
<GlobalAudioProvider conversationId={message.conversationId}>
|
||||
<Message
|
||||
{...message}
|
||||
clearSelectedMessage={clearSelectedMessage}
|
||||
deleteMessage={deleteMessage}
|
||||
deleteMessageForEveryone={deleteMessageForEveryone}
|
||||
disableMenu
|
||||
disableScroll
|
||||
displayTapToViewMessage={displayTapToViewMessage}
|
||||
downloadAttachment={downloadAttachment}
|
||||
i18n={i18n}
|
||||
interactionMode={interactionMode}
|
||||
kickOffAttachmentDownload={kickOffAttachmentDownload}
|
||||
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
|
||||
openConversation={openConversation}
|
||||
openLink={openLink}
|
||||
reactToMessage={reactToMessage}
|
||||
renderAudioAttachment={renderAudioAttachment}
|
||||
renderEmojiPicker={renderEmojiPicker}
|
||||
replyToMessage={replyToMessage}
|
||||
retrySend={retrySend}
|
||||
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>
|
||||
</div>
|
||||
<table className="module-message-detail__info">
|
||||
<tbody>
|
||||
|
|
|
@ -267,6 +267,7 @@ const renderItem = (id: string) => (
|
|||
renderEmojiPicker={() => <div />}
|
||||
item={items[id]}
|
||||
i18n={i18n}
|
||||
interactionMode="keyboard"
|
||||
conversationId=""
|
||||
conversationAccepted
|
||||
renderContact={() => '*ContactName*'}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from 'react';
|
||||
|
@ -38,6 +38,7 @@ const getDefaultProps = () => ({
|
|||
conversationAccepted: true,
|
||||
id: 'asdf',
|
||||
isSelected: false,
|
||||
interactionMode: 'keyboard' as const,
|
||||
selectMessage: action('selectMessage'),
|
||||
reactToMessage: action('reactToMessage'),
|
||||
clearSelectedMessage: action('clearSelectedMessage'),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
@ -6,6 +6,7 @@ import { LocalizerType, ThemeType } from '../../types/Util';
|
|||
|
||||
import {
|
||||
Message,
|
||||
InteractionModeType,
|
||||
Props as AllMessageProps,
|
||||
PropsActions as MessageActionsType,
|
||||
PropsData as MessageProps,
|
||||
|
@ -134,6 +135,7 @@ type PropsLocalType = {
|
|||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||
renderContact: SmartContactRendererType;
|
||||
i18n: LocalizerType;
|
||||
interactionMode: InteractionModeType;
|
||||
theme?: ThemeType;
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue