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
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2018-2020 Signal Messenger, LLC
|
||||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// The idea with this file is to make it webpackable for the style guide
|
||||
|
@ -82,6 +82,9 @@ const {
|
|||
createGroupV2JoinModal,
|
||||
} = require('../../ts/state/roots/createGroupV2JoinModal');
|
||||
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||
const {
|
||||
createMessageDetail,
|
||||
} = require('../../ts/state/roots/createMessageDetail');
|
||||
const {
|
||||
createGroupV2Permissions,
|
||||
} = require('../../ts/state/roots/createGroupV2Permissions');
|
||||
|
@ -345,6 +348,7 @@ exports.setup = (options = {}) => {
|
|||
createGroupV2JoinModal,
|
||||
createGroupV2Permissions,
|
||||
createLeftPane,
|
||||
createMessageDetail,
|
||||
createPendingInvites,
|
||||
createSafetyNumberViewer,
|
||||
createShortcutGuideModal,
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// Copyright 2020-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import {
|
||||
|
@ -14,7 +14,11 @@ import {
|
|||
} from '../state/ducks/conversations';
|
||||
import { getActiveCall } from '../state/ducks/calling';
|
||||
import { getCallSelector, isInCall } from '../state/selectors/calling';
|
||||
import { PropsData } from '../components/conversation/Message';
|
||||
import {
|
||||
MessageStatusType,
|
||||
PropsData,
|
||||
} from '../components/conversation/Message';
|
||||
import { OwnProps as SmartMessageDetailPropsType } from '../state/smart/MessageDetail';
|
||||
import { CallbackResultType } from '../textsecure/SendMessage';
|
||||
import { ExpirationTimerOptions } from '../util/ExpirationTimerOptions';
|
||||
import { missingCaseError } from '../util/missingCaseError';
|
||||
|
@ -319,7 +323,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
};
|
||||
}
|
||||
|
||||
getPropsForMessageDetail(): WhatIsThis {
|
||||
getPropsForMessageDetail(): Pick<
|
||||
SmartMessageDetailPropsType,
|
||||
'sentAt' | 'receivedAt' | 'message' | 'errors' | 'contacts'
|
||||
> {
|
||||
const newIdentity = window.i18n('newIdentity');
|
||||
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
|
||||
|
||||
|
@ -405,34 +412,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
return {
|
||||
sentAt: this.get('sent_at'),
|
||||
receivedAt: this.getReceivedAt(),
|
||||
message: {
|
||||
...this.getPropsForMessage(),
|
||||
disableMenu: true,
|
||||
disableScroll: true,
|
||||
// To ensure that group avatar doesn't show up
|
||||
conversationType: 'direct',
|
||||
downloadNewVersion: () => {
|
||||
this.trigger('download-new-version');
|
||||
},
|
||||
deleteMessage: (messageId: string) => {
|
||||
this.trigger('delete', messageId);
|
||||
},
|
||||
deleteMessageForEveryone: (messageId: string) => {
|
||||
this.trigger('delete-for-everyone', messageId);
|
||||
},
|
||||
showVisualAttachment: (options: unknown) => {
|
||||
this.trigger('show-visual-attachment', options);
|
||||
},
|
||||
displayTapToViewMessage: (messageId: string) => {
|
||||
this.trigger('display-tap-to-view-message', messageId);
|
||||
},
|
||||
openLink: (url: string) => {
|
||||
this.trigger('navigate-to', url);
|
||||
},
|
||||
reactWith: (emoji: string) => {
|
||||
this.trigger('react-with', emoji);
|
||||
},
|
||||
},
|
||||
message: this.getPropsForMessage(),
|
||||
errors,
|
||||
contacts: sortedContacts,
|
||||
};
|
||||
|
@ -834,10 +814,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
}
|
||||
|
||||
// Note: interactionMode is mixed in via selectors/conversations._messageSelector
|
||||
getPropsForMessage(): Omit<
|
||||
PropsData,
|
||||
'interactionMode' | 'renderAudioAttachment'
|
||||
> {
|
||||
getPropsForMessage(): Omit<PropsData, 'interactionMode'> {
|
||||
const sourceId = this.getContactId();
|
||||
const contact = this.findAndFormatContact(sourceId);
|
||||
const contactModel = this.findContact(sourceId);
|
||||
|
@ -1195,7 +1172,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
|||
};
|
||||
}
|
||||
|
||||
getStatus(identifier: string): string | null {
|
||||
private getStatus(identifier: string): MessageStatusType | null {
|
||||
const conversation = window.ConversationController.get(identifier);
|
||||
|
||||
if (!conversation) {
|
||||
|
|
18
ts/state/roots/createMessageDetail.tsx
Normal file
18
ts/state/roots/createMessageDetail.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { Store } from 'redux';
|
||||
|
||||
import { SmartMessageDetail, OwnProps } from '../smart/MessageDetail';
|
||||
|
||||
export const createMessageDetail = (
|
||||
store: Store,
|
||||
props: OwnProps
|
||||
): ReactElement => (
|
||||
<Provider store={store}>
|
||||
<SmartMessageDetail {...props} />
|
||||
</Provider>
|
||||
);
|
111
ts/state/smart/MessageDetail.tsx
Normal file
111
ts/state/smart/MessageDetail.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { ComponentProps } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
MessageDetail,
|
||||
Contact,
|
||||
} from '../../components/conversation/MessageDetail';
|
||||
import { PropsData as MessagePropsDataType } from '../../components/conversation/Message';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
|
||||
import { StateType } from '../reducer';
|
||||
import { getIntl, getInteractionMode } from '../selectors/user';
|
||||
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
|
||||
type MessageDetailProps = ComponentProps<typeof MessageDetail>;
|
||||
|
||||
export type OwnProps = {
|
||||
contacts: Array<Contact>;
|
||||
errors: Array<Error>;
|
||||
message: MessagePropsDataType;
|
||||
receivedAt: number;
|
||||
sentAt: number;
|
||||
} & Pick<
|
||||
MessageDetailProps,
|
||||
| 'clearSelectedMessage'
|
||||
| 'deleteMessage'
|
||||
| 'deleteMessageForEveryone'
|
||||
| 'displayTapToViewMessage'
|
||||
| 'downloadAttachment'
|
||||
| 'kickOffAttachmentDownload'
|
||||
| 'markAttachmentAsCorrupted'
|
||||
| 'openConversation'
|
||||
| 'openLink'
|
||||
| 'reactToMessage'
|
||||
| 'replyToMessage'
|
||||
| 'retrySend'
|
||||
| 'showContactDetail'
|
||||
| 'showContactModal'
|
||||
| 'showExpiredIncomingTapToViewToast'
|
||||
| 'showExpiredOutgoingTapToViewToast'
|
||||
| 'showVisualAttachment'
|
||||
>;
|
||||
|
||||
const mapStateToProps = (
|
||||
state: StateType,
|
||||
props: OwnProps
|
||||
): MessageDetailProps => {
|
||||
const {
|
||||
contacts,
|
||||
errors,
|
||||
message,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
|
||||
clearSelectedMessage,
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
openConversation,
|
||||
openLink,
|
||||
reactToMessage,
|
||||
replyToMessage,
|
||||
retrySend,
|
||||
showContactDetail,
|
||||
showContactModal,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showVisualAttachment,
|
||||
} = props;
|
||||
|
||||
return {
|
||||
contacts,
|
||||
errors,
|
||||
message,
|
||||
receivedAt,
|
||||
sentAt,
|
||||
|
||||
i18n: getIntl(state),
|
||||
interactionMode: getInteractionMode(state),
|
||||
|
||||
clearSelectedMessage,
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
openConversation,
|
||||
openLink,
|
||||
reactToMessage,
|
||||
renderAudioAttachment,
|
||||
renderEmojiPicker,
|
||||
replyToMessage,
|
||||
retrySend,
|
||||
showContactDetail,
|
||||
showContactModal,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showVisualAttachment,
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
export const SmartMessageDetail = smart(MessageDetail);
|
|
@ -1,13 +1,11 @@
|
|||
// Copyright 2019-2020 Signal Messenger, LLC
|
||||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
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';
|
||||
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
@ -23,8 +21,8 @@ import { SmartTypingBubble } from './TypingBubble';
|
|||
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
||||
import { SmartHeroRow } from './HeroRow';
|
||||
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
||||
import { SmartEmojiPicker } from './EmojiPicker';
|
||||
import { SmartMessageAudio, Props as MessageAudioProps } from './MessageAudio';
|
||||
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||
|
||||
// Workaround: A react component's required properties are filtering up through connect()
|
||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||
|
@ -43,11 +41,6 @@ type ExternalProps = {
|
|||
// are provided by ConversationView in setupTimeline().
|
||||
};
|
||||
|
||||
type AudioAttachmentProps = Omit<
|
||||
MessageAudioProps,
|
||||
'audio' | 'audioContext' | 'waveformCache'
|
||||
>;
|
||||
|
||||
function renderItem(
|
||||
messageId: string,
|
||||
conversationId: string,
|
||||
|
@ -64,35 +57,6 @@ function renderItem(
|
|||
);
|
||||
}
|
||||
|
||||
function renderAudioAttachment(props: AudioAttachmentProps) {
|
||||
return (
|
||||
<GlobalAudioContext.Consumer>
|
||||
{globalAudioProps => {
|
||||
return (
|
||||
globalAudioProps && (
|
||||
<SmartMessageAudio {...props} {...globalAudioProps} />
|
||||
)
|
||||
);
|
||||
}}
|
||||
</GlobalAudioContext.Consumer>
|
||||
);
|
||||
}
|
||||
|
||||
function renderEmojiPicker({
|
||||
ref,
|
||||
onPickEmoji,
|
||||
onClose,
|
||||
style,
|
||||
}: RenderEmojiPickerProps): JSX.Element {
|
||||
return (
|
||||
<SmartEmojiPicker
|
||||
ref={ref}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onClose={onClose}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function renderLastSeenIndicator(id: string): JSX.Element {
|
||||
return <FilteredSmartLastSeenIndicator id={id} />;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
@ -8,7 +8,7 @@ import { mapDispatchToProps } from '../actions';
|
|||
import { StateType } from '../reducer';
|
||||
|
||||
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
||||
import { getIntl, getTheme } from '../selectors/user';
|
||||
import { getIntl, getInteractionMode, getTheme } from '../selectors/user';
|
||||
import {
|
||||
getMessageSelector,
|
||||
getSelectedMessage,
|
||||
|
@ -47,6 +47,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
isSelected,
|
||||
renderContact,
|
||||
i18n: getIntl(state),
|
||||
interactionMode: getInteractionMode(state),
|
||||
theme: getTheme(state),
|
||||
};
|
||||
};
|
||||
|
|
27
ts/state/smart/renderAudioAttachment.tsx
Normal file
27
ts/state/smart/renderAudioAttachment.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { ReactElement } from 'react';
|
||||
import { GlobalAudioContext } from '../../components/GlobalAudioContext';
|
||||
import { SmartMessageAudio, Props as MessageAudioProps } from './MessageAudio';
|
||||
|
||||
type AudioAttachmentProps = Omit<
|
||||
MessageAudioProps,
|
||||
'audio' | 'audioContext' | 'waveformCache'
|
||||
>;
|
||||
|
||||
export function renderAudioAttachment(
|
||||
props: AudioAttachmentProps
|
||||
): ReactElement {
|
||||
return (
|
||||
<GlobalAudioContext.Consumer>
|
||||
{globalAudioProps => {
|
||||
return (
|
||||
globalAudioProps && (
|
||||
<SmartMessageAudio {...props} {...globalAudioProps} />
|
||||
)
|
||||
);
|
||||
}}
|
||||
</GlobalAudioContext.Consumer>
|
||||
);
|
||||
}
|
23
ts/state/smart/renderEmojiPicker.tsx
Normal file
23
ts/state/smart/renderEmojiPicker.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { RenderEmojiPickerProps } from '../../components/conversation/ReactionPicker';
|
||||
import { SmartEmojiPicker } from './EmojiPicker';
|
||||
|
||||
export function renderEmojiPicker({
|
||||
ref,
|
||||
onPickEmoji,
|
||||
onClose,
|
||||
style,
|
||||
}: RenderEmojiPickerProps): JSX.Element {
|
||||
return (
|
||||
<SmartEmojiPicker
|
||||
ref={ref}
|
||||
onPickEmoji={onPickEmoji}
|
||||
onClose={onClose}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -14398,24 +14398,6 @@
|
|||
"updated": "2020-11-11T21:56:04.179Z",
|
||||
"reasonDetail": "Needed to render the remote video element."
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/GlobalAudioContext.js",
|
||||
"line": " const audio = React.useRef(null);",
|
||||
"lineNumber": 38,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-03-11T17:20:05.355Z",
|
||||
"reasonDetail": "Need this to avoid re-creating Audio on every re-render"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/GlobalAudioContext.js",
|
||||
"line": " const waveformCache = React.useRef(null);",
|
||||
"lineNumber": 39,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-03-11T17:20:05.355Z",
|
||||
"reasonDetail": "Need this to avoid re-creating WaveformCache on every re-render"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/GroupCallOverflowArea.js",
|
||||
|
@ -14658,7 +14640,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||
"lineNumber": 242,
|
||||
"lineNumber": 241,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-03-05T19:57:01.431Z",
|
||||
"reasonDetail": "Used for managing focus only"
|
||||
|
@ -14667,7 +14649,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
||||
"lineNumber": 244,
|
||||
"lineNumber": 243,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-03-05T19:57:01.431Z",
|
||||
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
||||
|
@ -14676,7 +14658,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/Message.tsx",
|
||||
"line": " > = React.createRef();",
|
||||
"lineNumber": 248,
|
||||
"lineNumber": 247,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2021-03-05T19:57:01.431Z",
|
||||
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
||||
|
@ -14694,7 +14676,7 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/MessageDetail.js",
|
||||
"line": " this.focusRef = react_1.default.createRef();",
|
||||
"lineNumber": 21,
|
||||
"lineNumber": 23,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-11-01T22:46:33.013Z",
|
||||
"reasonDetail": "Used for setting focus only"
|
||||
|
|
|
@ -356,28 +356,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
this.showSafetyNumber
|
||||
);
|
||||
this.listenTo(this.model.messageCollection, 'force-send', this.forceSend);
|
||||
this.listenTo(this.model.messageCollection, 'delete', this.deleteMessage);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'delete-for-everyone',
|
||||
this.deleteMessageForEveryone
|
||||
);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'show-visual-attachment',
|
||||
this.showLightbox
|
||||
);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'display-tap-to-view-message',
|
||||
this.displayTapToViewMessage
|
||||
);
|
||||
this.listenTo(this.model.messageCollection, 'navigate-to', this.navigateTo);
|
||||
this.listenTo(
|
||||
this.model.messageCollection,
|
||||
'download-new-version',
|
||||
this.downloadNewVersion
|
||||
);
|
||||
|
||||
this.lazyUpdateVerified = window._.debounce(
|
||||
this.model.updateVerified.bind(this.model),
|
||||
|
@ -725,9 +703,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
});
|
||||
},
|
||||
|
||||
setupTimeline() {
|
||||
const { id } = this.model;
|
||||
|
||||
getMessageActions() {
|
||||
const reactToMessage = (messageId: any, reaction: any) => {
|
||||
this.sendReactionMessage(messageId, reaction);
|
||||
};
|
||||
|
@ -795,6 +771,33 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
const showExpiredOutgoingTapToViewToast = () => {
|
||||
this.showToast(Whisper.TapToViewExpiredOutgoingToast);
|
||||
};
|
||||
|
||||
return {
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
downloadNewVersion,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
openConversation,
|
||||
openLink,
|
||||
reactToMessage,
|
||||
replyToMessage,
|
||||
retrySend,
|
||||
showContactDetail,
|
||||
showContactModal,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
showIdentity,
|
||||
showMessageDetail,
|
||||
showVisualAttachment,
|
||||
};
|
||||
},
|
||||
|
||||
setupTimeline() {
|
||||
const { id } = this.model;
|
||||
|
||||
const contactSupport = () => {
|
||||
const baseUrl =
|
||||
'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed';
|
||||
|
@ -958,32 +961,15 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
JSX: window.Signal.State.Roots.createTimeline(window.reduxStore, {
|
||||
id,
|
||||
|
||||
...this.getMessageActions(),
|
||||
|
||||
contactSupport,
|
||||
deleteMessage,
|
||||
deleteMessageForEveryone,
|
||||
displayTapToViewMessage,
|
||||
downloadAttachment,
|
||||
downloadNewVersion,
|
||||
kickOffAttachmentDownload,
|
||||
markAttachmentAsCorrupted,
|
||||
loadNewerMessages,
|
||||
loadNewestMessages: this.loadNewestMessages.bind(this),
|
||||
loadAndScroll: this.loadAndScroll.bind(this),
|
||||
loadOlderMessages,
|
||||
markMessageRead,
|
||||
openConversation,
|
||||
openLink,
|
||||
reactToMessage,
|
||||
replyToMessage,
|
||||
retrySend,
|
||||
scrollToQuotedMessage,
|
||||
showContactDetail,
|
||||
showContactModal,
|
||||
showIdentity,
|
||||
showMessageDetail,
|
||||
showVisualAttachment,
|
||||
showExpiredIncomingTapToViewToast,
|
||||
showExpiredOutgoingTapToViewToast,
|
||||
updateSharedGroups: this.model.throttledUpdateSharedGroups,
|
||||
}),
|
||||
});
|
||||
|
@ -2940,7 +2926,8 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
},
|
||||
|
||||
showMessageDetail(messageId: any) {
|
||||
const message = this.model.messageCollection.get(messageId);
|
||||
const { model }: { model: ConversationModel } = this;
|
||||
const message = model.messageCollection?.get(messageId);
|
||||
if (!message) {
|
||||
throw new Error(
|
||||
`showMessageDetail: Did not find message for id ${messageId}`
|
||||
|
@ -2951,20 +2938,26 @@ Whisper.ConversationView = Whisper.View.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
const getProps = () => ({
|
||||
...message.getPropsForMessageDetail(),
|
||||
...this.getMessageActions(),
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
this.stopListening(message, 'change', update);
|
||||
this.resetPanel();
|
||||
};
|
||||
|
||||
const props = message.getPropsForMessageDetail();
|
||||
const view = new Whisper.ReactWrapperView({
|
||||
className: 'panel message-detail-wrapper',
|
||||
Component: window.Signal.Components.MessageDetail,
|
||||
props,
|
||||
JSX: window.Signal.State.Roots.createMessageDetail(
|
||||
window.reduxStore,
|
||||
getProps()
|
||||
),
|
||||
onClose,
|
||||
});
|
||||
|
||||
const update = () => view.update(message.getPropsForMessageDetail());
|
||||
const update = () => view.update(getProps());
|
||||
this.listenTo(message, 'change', update);
|
||||
this.listenTo(message, 'expired', onClose);
|
||||
// We could listen to all involved contacts, but we'll call that overkill
|
||||
|
|
2
ts/window.d.ts
vendored
2
ts/window.d.ts
vendored
|
@ -51,6 +51,7 @@ import { createGroupV1MigrationModal } from './state/roots/createGroupV1Migratio
|
|||
import { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
|
||||
import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions';
|
||||
import { createLeftPane } from './state/roots/createLeftPane';
|
||||
import { createMessageDetail } from './state/roots/createMessageDetail';
|
||||
import { createPendingInvites } from './state/roots/createPendingInvites';
|
||||
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
||||
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
||||
|
@ -488,6 +489,7 @@ declare global {
|
|||
createGroupV2JoinModal: typeof createGroupV2JoinModal;
|
||||
createGroupV2Permissions: typeof createGroupV2Permissions;
|
||||
createLeftPane: typeof createLeftPane;
|
||||
createMessageDetail: typeof createMessageDetail;
|
||||
createPendingInvites: typeof createPendingInvites;
|
||||
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
||||
createShortcutGuideModal: typeof createShortcutGuideModal;
|
||||
|
|
Loading…
Reference in a new issue