Fix error on message details screen with audio messages

This commit is contained in:
Evan Hahn 2021-03-24 17:06:12 -05:00 committed by GitHub
parent 5f9a75d9f4
commit 77c306843d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 417 additions and 224 deletions

View file

@ -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,

View file

@ -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>
);

View file

@ -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 = {

View file

@ -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', () => {

View file

@ -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>

View file

@ -267,6 +267,7 @@ const renderItem = (id: string) => (
renderEmojiPicker={() => <div />}
item={items[id]}
i18n={i18n}
interactionMode="keyboard"
conversationId=""
conversationAccepted
renderContact={() => '*ContactName*'}

View file

@ -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'),

View file

@ -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;
};

View file

@ -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) {

View 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>
);

View 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);

View file

@ -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} />;
}

View file

@ -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),
};
};

View 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>
);
}

View 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}
/>
);
}

View file

@ -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"

View file

@ -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
View file

@ -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;