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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
// The idea with this file is to make it webpackable for the style guide
|
// The idea with this file is to make it webpackable for the style guide
|
||||||
|
@ -82,6 +82,9 @@ const {
|
||||||
createGroupV2JoinModal,
|
createGroupV2JoinModal,
|
||||||
} = require('../../ts/state/roots/createGroupV2JoinModal');
|
} = require('../../ts/state/roots/createGroupV2JoinModal');
|
||||||
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
|
||||||
|
const {
|
||||||
|
createMessageDetail,
|
||||||
|
} = require('../../ts/state/roots/createMessageDetail');
|
||||||
const {
|
const {
|
||||||
createGroupV2Permissions,
|
createGroupV2Permissions,
|
||||||
} = require('../../ts/state/roots/createGroupV2Permissions');
|
} = require('../../ts/state/roots/createGroupV2Permissions');
|
||||||
|
@ -345,6 +348,7 @@ exports.setup = (options = {}) => {
|
||||||
createGroupV2JoinModal,
|
createGroupV2JoinModal,
|
||||||
createGroupV2Permissions,
|
createGroupV2Permissions,
|
||||||
createLeftPane,
|
createLeftPane,
|
||||||
|
createMessageDetail,
|
||||||
createPendingInvites,
|
createPendingInvites,
|
||||||
createSafetyNumberViewer,
|
createSafetyNumberViewer,
|
||||||
createShortcutGuideModal,
|
createShortcutGuideModal,
|
||||||
|
|
|
@ -14,15 +14,26 @@ type Contents = {
|
||||||
waveformCache: WaveformCache;
|
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 = {
|
export type GlobalAudioProps = {
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
children?: React.ReactNode | React.ReactChildren;
|
children?: React.ReactNode | React.ReactChildren;
|
||||||
};
|
};
|
||||||
|
|
||||||
const audioContext = new AudioContext();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A global context that holds Audio, AudioContext, LRU instances that are used
|
* A global context that holds Audio, AudioContext, LRU instances that are used
|
||||||
* inside the conversation by ts/components/conversation/MessageAudio.tsx
|
* inside the conversation by ts/components/conversation/MessageAudio.tsx
|
||||||
|
@ -31,37 +42,15 @@ export const GlobalAudioProvider: React.FC<GlobalAudioProps> = ({
|
||||||
conversationId,
|
conversationId,
|
||||||
children,
|
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
|
// When moving between conversations - stop audio
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (audio.current) {
|
globalContents.audio.pause();
|
||||||
audio.current.pause();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [conversationId]);
|
}, [conversationId]);
|
||||||
|
|
||||||
const value = {
|
|
||||||
audio: audio.current,
|
|
||||||
audioContext,
|
|
||||||
waveformCache: waveformCache.current,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlobalAudioContext.Provider value={value}>
|
<GlobalAudioContext.Provider value={globalContents}>
|
||||||
{children}
|
{children}
|
||||||
</GlobalAudioContext.Provider>
|
</GlobalAudioContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -101,7 +101,6 @@ export type PropsData = {
|
||||||
isSticker?: boolean;
|
isSticker?: boolean;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
isSelectedCounter?: number;
|
isSelectedCounter?: number;
|
||||||
interactionMode: InteractionModeType;
|
|
||||||
direction: DirectionType;
|
direction: DirectionType;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
status?: MessageStatusType;
|
status?: MessageStatusType;
|
||||||
|
@ -150,16 +149,16 @@ export type PropsData = {
|
||||||
isBlocked: boolean;
|
isBlocked: boolean;
|
||||||
isMessageRequestAccepted: boolean;
|
isMessageRequestAccepted: boolean;
|
||||||
bodyRanges?: BodyRangesType;
|
bodyRanges?: BodyRangesType;
|
||||||
|
|
||||||
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsHousekeeping = {
|
export type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
interactionMode: InteractionModeType;
|
||||||
theme?: ThemeType;
|
theme?: ThemeType;
|
||||||
disableMenu?: boolean;
|
disableMenu?: boolean;
|
||||||
disableScroll?: boolean;
|
disableScroll?: boolean;
|
||||||
collapseMetadata?: boolean;
|
collapseMetadata?: boolean;
|
||||||
|
renderAudioAttachment: (props: AudioAttachmentProps) => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PropsActions = {
|
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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -7,7 +7,7 @@ import { action } from '@storybook/addon-actions';
|
||||||
import { number } from '@storybook/addon-knobs';
|
import { number } from '@storybook/addon-knobs';
|
||||||
import { storiesOf } from '@storybook/react';
|
import { storiesOf } from '@storybook/react';
|
||||||
|
|
||||||
import { Props as MessageProps } from './Message';
|
import { PropsData as MessageDataPropsType } from './Message';
|
||||||
import { MessageDetail, Props } from './MessageDetail';
|
import { MessageDetail, Props } from './MessageDetail';
|
||||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||||
import enMessages from '../../../_locales/en/messages.json';
|
import enMessages from '../../../_locales/en/messages.json';
|
||||||
|
@ -16,42 +16,19 @@ const i18n = setupI18n('en', enMessages);
|
||||||
|
|
||||||
const story = storiesOf('Components/Conversation/MessageDetail', module);
|
const story = storiesOf('Components/Conversation/MessageDetail', module);
|
||||||
|
|
||||||
const defaultMessage: MessageProps = {
|
const defaultMessage: MessageDataPropsType = {
|
||||||
authorId: 'some-id',
|
authorId: 'some-id',
|
||||||
authorTitle: 'Max',
|
authorTitle: 'Max',
|
||||||
canReply: true,
|
canReply: true,
|
||||||
canDeleteForEveryone: true,
|
canDeleteForEveryone: true,
|
||||||
canDownload: true,
|
canDownload: true,
|
||||||
clearSelectedMessage: () => null,
|
|
||||||
conversationId: 'my-convo',
|
conversationId: 'my-convo',
|
||||||
conversationType: 'direct',
|
conversationType: 'direct',
|
||||||
deleteMessage: action('deleteMessage'),
|
|
||||||
deleteMessageForEveryone: action('deleteMessageForEveryone'),
|
|
||||||
direction: 'incoming',
|
direction: 'incoming',
|
||||||
displayTapToViewMessage: () => null,
|
|
||||||
downloadAttachment: () => null,
|
|
||||||
i18n,
|
|
||||||
id: 'my-message',
|
id: 'my-message',
|
||||||
interactionMode: 'keyboard',
|
|
||||||
isBlocked: false,
|
isBlocked: false,
|
||||||
isMessageRequestAccepted: true,
|
isMessageRequestAccepted: true,
|
||||||
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
|
|
||||||
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
|
|
||||||
openConversation: () => null,
|
|
||||||
openLink: () => null,
|
|
||||||
previews: [],
|
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',
|
status: 'sent',
|
||||||
text: 'A message from Max',
|
text: 'A message from Max',
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
@ -70,10 +47,32 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
errors: overrideProps.errors || [],
|
errors: overrideProps.errors || [],
|
||||||
i18n,
|
|
||||||
message: overrideProps.message || defaultMessage,
|
message: overrideProps.message || defaultMessage,
|
||||||
receivedAt: number('receivedAt', overrideProps.receivedAt || Date.now()),
|
receivedAt: number('receivedAt', overrideProps.receivedAt || Date.now()),
|
||||||
sentAt: number('sentAt', overrideProps.sentAt || 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', () => {
|
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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
|
import { GlobalAudioProvider } from '../GlobalAudioContext';
|
||||||
import { Avatar } from '../Avatar';
|
import { Avatar } from '../Avatar';
|
||||||
import { ContactName } from './ContactName';
|
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 { LocalizerType } from '../../types/Util';
|
||||||
import { ColorType } from '../../types/Colors';
|
import { ColorType } from '../../types/Colors';
|
||||||
|
import { assert } from '../../util/assert';
|
||||||
|
|
||||||
type Contact = {
|
export type Contact = {
|
||||||
status: MessageStatusType;
|
status: MessageStatusType | null;
|
||||||
|
|
||||||
title: string;
|
title: string;
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
color: ColorType;
|
color?: ColorType;
|
||||||
isOutgoingKeyError: boolean;
|
isOutgoingKeyError: boolean;
|
||||||
isUnidentifiedDelivery: boolean;
|
isUnidentifiedDelivery: boolean;
|
||||||
|
|
||||||
|
@ -30,15 +37,36 @@ type Contact = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
sentAt: number;
|
|
||||||
receivedAt: number;
|
|
||||||
|
|
||||||
message: MessageProps;
|
|
||||||
errors: Array<Error>;
|
|
||||||
contacts: Array<Contact>;
|
contacts: Array<Contact>;
|
||||||
|
errors: Array<Error>;
|
||||||
|
message: MessagePropsDataType;
|
||||||
|
receivedAt: number;
|
||||||
|
sentAt: number;
|
||||||
|
|
||||||
i18n: LocalizerType;
|
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 => {
|
const _keyForError = (error: Error): string => {
|
||||||
return `${error.name}-${error.message}`;
|
return `${error.name}-${error.message}`;
|
||||||
|
@ -84,14 +112,14 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public renderDeleteButton(): JSX.Element {
|
public renderDeleteButton(): JSX.Element {
|
||||||
const { i18n, message } = this.props;
|
const { deleteMessage, i18n, message } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="module-message-detail__delete-button-container">
|
<div className="module-message-detail__delete-button-container">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
message.deleteMessage(message.id);
|
deleteMessage(message.id);
|
||||||
}}
|
}}
|
||||||
className="module-message-detail__delete-button"
|
className="module-message-detail__delete-button"
|
||||||
>
|
>
|
||||||
|
@ -127,7 +155,9 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'module-message-detail__contact__status-icon',
|
'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;
|
) : null;
|
||||||
|
@ -179,13 +209,83 @@ export class MessageDetail extends React.Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
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 (
|
return (
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
|
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
|
||||||
<div className="module-message-detail__message-container">
|
<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>
|
</div>
|
||||||
<table className="module-message-detail__info">
|
<table className="module-message-detail__info">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -267,6 +267,7 @@ const renderItem = (id: string) => (
|
||||||
renderEmojiPicker={() => <div />}
|
renderEmojiPicker={() => <div />}
|
||||||
item={items[id]}
|
item={items[id]}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
|
interactionMode="keyboard"
|
||||||
conversationId=""
|
conversationId=""
|
||||||
conversationAccepted
|
conversationAccepted
|
||||||
renderContact={() => '*ContactName*'}
|
renderContact={() => '*ContactName*'}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -38,6 +38,7 @@ const getDefaultProps = () => ({
|
||||||
conversationAccepted: true,
|
conversationAccepted: true,
|
||||||
id: 'asdf',
|
id: 'asdf',
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
|
interactionMode: 'keyboard' as const,
|
||||||
selectMessage: action('selectMessage'),
|
selectMessage: action('selectMessage'),
|
||||||
reactToMessage: action('reactToMessage'),
|
reactToMessage: action('reactToMessage'),
|
||||||
clearSelectedMessage: action('clearSelectedMessage'),
|
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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -6,6 +6,7 @@ import { LocalizerType, ThemeType } from '../../types/Util';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Message,
|
Message,
|
||||||
|
InteractionModeType,
|
||||||
Props as AllMessageProps,
|
Props as AllMessageProps,
|
||||||
PropsActions as MessageActionsType,
|
PropsActions as MessageActionsType,
|
||||||
PropsData as MessageProps,
|
PropsData as MessageProps,
|
||||||
|
@ -134,6 +135,7 @@ type PropsLocalType = {
|
||||||
selectMessage: (messageId: string, conversationId: string) => unknown;
|
selectMessage: (messageId: string, conversationId: string) => unknown;
|
||||||
renderContact: SmartContactRendererType;
|
renderContact: SmartContactRendererType;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
interactionMode: InteractionModeType;
|
||||||
theme?: ThemeType;
|
theme?: ThemeType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020-2021 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -14,7 +14,11 @@ import {
|
||||||
} from '../state/ducks/conversations';
|
} from '../state/ducks/conversations';
|
||||||
import { getActiveCall } from '../state/ducks/calling';
|
import { getActiveCall } from '../state/ducks/calling';
|
||||||
import { getCallSelector, isInCall } from '../state/selectors/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 { CallbackResultType } from '../textsecure/SendMessage';
|
||||||
import { ExpirationTimerOptions } from '../util/ExpirationTimerOptions';
|
import { ExpirationTimerOptions } from '../util/ExpirationTimerOptions';
|
||||||
import { missingCaseError } from '../util/missingCaseError';
|
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 newIdentity = window.i18n('newIdentity');
|
||||||
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
|
const OUTGOING_KEY_ERROR = 'OutgoingIdentityKeyError';
|
||||||
|
|
||||||
|
@ -405,34 +412,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
return {
|
return {
|
||||||
sentAt: this.get('sent_at'),
|
sentAt: this.get('sent_at'),
|
||||||
receivedAt: this.getReceivedAt(),
|
receivedAt: this.getReceivedAt(),
|
||||||
message: {
|
message: this.getPropsForMessage(),
|
||||||
...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);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errors,
|
errors,
|
||||||
contacts: sortedContacts,
|
contacts: sortedContacts,
|
||||||
};
|
};
|
||||||
|
@ -834,10 +814,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: interactionMode is mixed in via selectors/conversations._messageSelector
|
// Note: interactionMode is mixed in via selectors/conversations._messageSelector
|
||||||
getPropsForMessage(): Omit<
|
getPropsForMessage(): Omit<PropsData, 'interactionMode'> {
|
||||||
PropsData,
|
|
||||||
'interactionMode' | 'renderAudioAttachment'
|
|
||||||
> {
|
|
||||||
const sourceId = this.getContactId();
|
const sourceId = this.getContactId();
|
||||||
const contact = this.findAndFormatContact(sourceId);
|
const contact = this.findAndFormatContact(sourceId);
|
||||||
const contactModel = this.findContact(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);
|
const conversation = window.ConversationController.get(identifier);
|
||||||
|
|
||||||
if (!conversation) {
|
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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { pick } from 'lodash';
|
import { pick } from 'lodash';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { mapDispatchToProps } from '../actions';
|
import { mapDispatchToProps } from '../actions';
|
||||||
import { GlobalAudioContext } from '../../components/GlobalAudioContext';
|
|
||||||
import { Timeline } from '../../components/conversation/Timeline';
|
import { Timeline } from '../../components/conversation/Timeline';
|
||||||
import { RenderEmojiPickerProps } from '../../components/conversation/ReactionPicker';
|
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
|
@ -23,8 +21,8 @@ import { SmartTypingBubble } from './TypingBubble';
|
||||||
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
||||||
import { SmartHeroRow } from './HeroRow';
|
import { SmartHeroRow } from './HeroRow';
|
||||||
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
||||||
import { SmartEmojiPicker } from './EmojiPicker';
|
import { renderAudioAttachment } from './renderAudioAttachment';
|
||||||
import { SmartMessageAudio, Props as MessageAudioProps } from './MessageAudio';
|
import { renderEmojiPicker } from './renderEmojiPicker';
|
||||||
|
|
||||||
// Workaround: A react component's required properties are filtering up through connect()
|
// Workaround: A react component's required properties are filtering up through connect()
|
||||||
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
|
||||||
|
@ -43,11 +41,6 @@ type ExternalProps = {
|
||||||
// are provided by ConversationView in setupTimeline().
|
// are provided by ConversationView in setupTimeline().
|
||||||
};
|
};
|
||||||
|
|
||||||
type AudioAttachmentProps = Omit<
|
|
||||||
MessageAudioProps,
|
|
||||||
'audio' | 'audioContext' | 'waveformCache'
|
|
||||||
>;
|
|
||||||
|
|
||||||
function renderItem(
|
function renderItem(
|
||||||
messageId: string,
|
messageId: string,
|
||||||
conversationId: 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 {
|
function renderLastSeenIndicator(id: string): JSX.Element {
|
||||||
return <FilteredSmartLastSeenIndicator id={id} />;
|
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
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
@ -8,7 +8,7 @@ import { mapDispatchToProps } from '../actions';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
import { TimelineItem } from '../../components/conversation/TimelineItem';
|
||||||
import { getIntl, getTheme } from '../selectors/user';
|
import { getIntl, getInteractionMode, getTheme } from '../selectors/user';
|
||||||
import {
|
import {
|
||||||
getMessageSelector,
|
getMessageSelector,
|
||||||
getSelectedMessage,
|
getSelectedMessage,
|
||||||
|
@ -47,6 +47,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||||
isSelected,
|
isSelected,
|
||||||
renderContact,
|
renderContact,
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
|
interactionMode: getInteractionMode(state),
|
||||||
theme: getTheme(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",
|
"updated": "2020-11-11T21:56:04.179Z",
|
||||||
"reasonDetail": "Needed to render the remote video element."
|
"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",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/GroupCallOverflowArea.js",
|
"path": "ts/components/GroupCallOverflowArea.js",
|
||||||
|
@ -14658,7 +14640,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
|
||||||
"lineNumber": 242,
|
"lineNumber": 241,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for managing focus only"
|
"reasonDetail": "Used for managing focus only"
|
||||||
|
@ -14667,7 +14649,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
"line": " public audioButtonRef: React.RefObject<HTMLButtonElement> = React.createRef();",
|
||||||
"lineNumber": 244,
|
"lineNumber": 243,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
"reasonDetail": "Used for propagating click from the Message to MessageAudio's button"
|
||||||
|
@ -14676,7 +14658,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/Message.tsx",
|
"path": "ts/components/conversation/Message.tsx",
|
||||||
"line": " > = React.createRef();",
|
"line": " > = React.createRef();",
|
||||||
"lineNumber": 248,
|
"lineNumber": 247,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2021-03-05T19:57:01.431Z",
|
"updated": "2021-03-05T19:57:01.431Z",
|
||||||
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
"reasonDetail": "Used for detecting clicks outside reaction viewer"
|
||||||
|
@ -14694,7 +14676,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/MessageDetail.js",
|
"path": "ts/components/conversation/MessageDetail.js",
|
||||||
"line": " this.focusRef = react_1.default.createRef();",
|
"line": " this.focusRef = react_1.default.createRef();",
|
||||||
"lineNumber": 21,
|
"lineNumber": 23,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-11-01T22:46:33.013Z",
|
"updated": "2019-11-01T22:46:33.013Z",
|
||||||
"reasonDetail": "Used for setting focus only"
|
"reasonDetail": "Used for setting focus only"
|
||||||
|
|
|
@ -356,28 +356,6 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
this.showSafetyNumber
|
this.showSafetyNumber
|
||||||
);
|
);
|
||||||
this.listenTo(this.model.messageCollection, 'force-send', this.forceSend);
|
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.lazyUpdateVerified = window._.debounce(
|
||||||
this.model.updateVerified.bind(this.model),
|
this.model.updateVerified.bind(this.model),
|
||||||
|
@ -725,9 +703,7 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setupTimeline() {
|
getMessageActions() {
|
||||||
const { id } = this.model;
|
|
||||||
|
|
||||||
const reactToMessage = (messageId: any, reaction: any) => {
|
const reactToMessage = (messageId: any, reaction: any) => {
|
||||||
this.sendReactionMessage(messageId, reaction);
|
this.sendReactionMessage(messageId, reaction);
|
||||||
};
|
};
|
||||||
|
@ -795,6 +771,33 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
const showExpiredOutgoingTapToViewToast = () => {
|
const showExpiredOutgoingTapToViewToast = () => {
|
||||||
this.showToast(Whisper.TapToViewExpiredOutgoingToast);
|
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 contactSupport = () => {
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
'https://support.signal.org/hc/LOCALE/requests/new?desktop&chat_refreshed';
|
'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, {
|
JSX: window.Signal.State.Roots.createTimeline(window.reduxStore, {
|
||||||
id,
|
id,
|
||||||
|
|
||||||
|
...this.getMessageActions(),
|
||||||
|
|
||||||
contactSupport,
|
contactSupport,
|
||||||
deleteMessage,
|
|
||||||
deleteMessageForEveryone,
|
|
||||||
displayTapToViewMessage,
|
|
||||||
downloadAttachment,
|
|
||||||
downloadNewVersion,
|
|
||||||
kickOffAttachmentDownload,
|
|
||||||
markAttachmentAsCorrupted,
|
|
||||||
loadNewerMessages,
|
loadNewerMessages,
|
||||||
loadNewestMessages: this.loadNewestMessages.bind(this),
|
loadNewestMessages: this.loadNewestMessages.bind(this),
|
||||||
loadAndScroll: this.loadAndScroll.bind(this),
|
loadAndScroll: this.loadAndScroll.bind(this),
|
||||||
loadOlderMessages,
|
loadOlderMessages,
|
||||||
markMessageRead,
|
markMessageRead,
|
||||||
openConversation,
|
|
||||||
openLink,
|
|
||||||
reactToMessage,
|
|
||||||
replyToMessage,
|
|
||||||
retrySend,
|
|
||||||
scrollToQuotedMessage,
|
scrollToQuotedMessage,
|
||||||
showContactDetail,
|
|
||||||
showContactModal,
|
|
||||||
showIdentity,
|
|
||||||
showMessageDetail,
|
|
||||||
showVisualAttachment,
|
|
||||||
showExpiredIncomingTapToViewToast,
|
|
||||||
showExpiredOutgoingTapToViewToast,
|
|
||||||
updateSharedGroups: this.model.throttledUpdateSharedGroups,
|
updateSharedGroups: this.model.throttledUpdateSharedGroups,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -2940,7 +2926,8 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
showMessageDetail(messageId: any) {
|
showMessageDetail(messageId: any) {
|
||||||
const message = this.model.messageCollection.get(messageId);
|
const { model }: { model: ConversationModel } = this;
|
||||||
|
const message = model.messageCollection?.get(messageId);
|
||||||
if (!message) {
|
if (!message) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`showMessageDetail: Did not find message for id ${messageId}`
|
`showMessageDetail: Did not find message for id ${messageId}`
|
||||||
|
@ -2951,20 +2938,26 @@ Whisper.ConversationView = Whisper.View.extend({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getProps = () => ({
|
||||||
|
...message.getPropsForMessageDetail(),
|
||||||
|
...this.getMessageActions(),
|
||||||
|
});
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
this.stopListening(message, 'change', update);
|
this.stopListening(message, 'change', update);
|
||||||
this.resetPanel();
|
this.resetPanel();
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = message.getPropsForMessageDetail();
|
|
||||||
const view = new Whisper.ReactWrapperView({
|
const view = new Whisper.ReactWrapperView({
|
||||||
className: 'panel message-detail-wrapper',
|
className: 'panel message-detail-wrapper',
|
||||||
Component: window.Signal.Components.MessageDetail,
|
JSX: window.Signal.State.Roots.createMessageDetail(
|
||||||
props,
|
window.reduxStore,
|
||||||
|
getProps()
|
||||||
|
),
|
||||||
onClose,
|
onClose,
|
||||||
});
|
});
|
||||||
|
|
||||||
const update = () => view.update(message.getPropsForMessageDetail());
|
const update = () => view.update(getProps());
|
||||||
this.listenTo(message, 'change', update);
|
this.listenTo(message, 'change', update);
|
||||||
this.listenTo(message, 'expired', onClose);
|
this.listenTo(message, 'expired', onClose);
|
||||||
// We could listen to all involved contacts, but we'll call that overkill
|
// 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 { createGroupV2JoinModal } from './state/roots/createGroupV2JoinModal';
|
||||||
import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions';
|
import { createGroupV2Permissions } from './state/roots/createGroupV2Permissions';
|
||||||
import { createLeftPane } from './state/roots/createLeftPane';
|
import { createLeftPane } from './state/roots/createLeftPane';
|
||||||
|
import { createMessageDetail } from './state/roots/createMessageDetail';
|
||||||
import { createPendingInvites } from './state/roots/createPendingInvites';
|
import { createPendingInvites } from './state/roots/createPendingInvites';
|
||||||
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
import { createSafetyNumberViewer } from './state/roots/createSafetyNumberViewer';
|
||||||
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
import { createShortcutGuideModal } from './state/roots/createShortcutGuideModal';
|
||||||
|
@ -488,6 +489,7 @@ declare global {
|
||||||
createGroupV2JoinModal: typeof createGroupV2JoinModal;
|
createGroupV2JoinModal: typeof createGroupV2JoinModal;
|
||||||
createGroupV2Permissions: typeof createGroupV2Permissions;
|
createGroupV2Permissions: typeof createGroupV2Permissions;
|
||||||
createLeftPane: typeof createLeftPane;
|
createLeftPane: typeof createLeftPane;
|
||||||
|
createMessageDetail: typeof createMessageDetail;
|
||||||
createPendingInvites: typeof createPendingInvites;
|
createPendingInvites: typeof createPendingInvites;
|
||||||
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
createSafetyNumberViewer: typeof createSafetyNumberViewer;
|
||||||
createShortcutGuideModal: typeof createShortcutGuideModal;
|
createShortcutGuideModal: typeof createShortcutGuideModal;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue