signal-desktop/ts/components/conversation/MessageDetail.tsx

394 lines
12 KiB
TypeScript
Raw Normal View History

// Copyright 2018-2021 Signal Messenger, LLC
2020-10-30 20:34:04 +00:00
// SPDX-License-Identifier: AGPL-3.0-only
import React, { ReactChild, ReactNode } from 'react';
import classNames from 'classnames';
import moment from 'moment';
import { noop } from 'lodash';
import { Avatar, AvatarSize } from '../Avatar';
import { ContactName } from './ContactName';
import {
Message,
Props as MessagePropsType,
PropsData as MessagePropsDataType,
} from './Message';
import { LocalizerType } from '../../types/Util';
2021-05-07 22:21:10 +00:00
import { ConversationType } from '../../state/ducks/conversations';
import { groupBy } from '../../util/mapUtil';
2021-06-07 16:50:18 +00:00
import { ContactNameColorType } from '../../types/Colors';
import { SendStatus } from '../../messages/MessageSendState';
2021-05-07 22:21:10 +00:00
export type Contact = Pick<
ConversationType,
| 'acceptedMessageRequest'
| 'avatarPath'
| 'color'
| 'id'
2021-05-07 22:21:10 +00:00
| 'isMe'
| 'name'
| 'phoneNumber'
| 'profileName'
| 'sharedGroupNames'
| 'title'
| 'unblurredAvatarPath'
> & {
status?: SendStatus;
2020-07-24 01:35:32 +00:00
isOutgoingKeyError: boolean;
isUnidentifiedDelivery: boolean;
errors?: Array<Error>;
};
2021-08-30 21:32:56 +00:00
export type PropsData = {
// An undefined status means they were the sender and it's an incoming message. If
// `undefined` is a status, there should be no other items in the array; if there are
// any defined statuses, `undefined` shouldn't be present.
contacts: ReadonlyArray<Contact>;
2021-06-07 16:50:18 +00:00
contactNameColor?: ContactNameColorType;
errors: Array<Error>;
message: Omit<MessagePropsDataType, 'renderingContext'>;
receivedAt: number;
sentAt: number;
sendAnyway: (contactId: string, messageId: string) => unknown;
showSafetyNumber: (contactId: string) => void;
2019-01-14 21:49:58 +00:00
i18n: LocalizerType;
2021-08-30 21:32:56 +00:00
} & Pick<MessagePropsType, 'interactionMode'>;
export type PropsBackboneActions = Pick<
MessagePropsType,
| 'displayTapToViewMessage'
| 'kickOffAttachmentDownload'
| 'markAttachmentAsCorrupted'
| 'markViewed'
| 'openConversation'
| 'openLink'
| 'reactToMessage'
| 'renderAudioAttachment'
| 'renderEmojiPicker'
| 'replyToMessage'
| 'retrySend'
| 'showContactDetail'
| 'showContactModal'
| 'showExpiredIncomingTapToViewToast'
| 'showExpiredOutgoingTapToViewToast'
2021-04-27 22:35:35 +00:00
| 'showForwardMessageModal'
| 'showVisualAttachment'
>;
2021-08-30 21:32:56 +00:00
export type PropsReduxActions = Pick<
MessagePropsType,
| 'clearSelectedMessage'
| 'doubleCheckMissingQuoteReference'
| 'checkForAccount'
>;
export type ExternalProps = PropsData & PropsBackboneActions;
export type Props = PropsData & PropsBackboneActions & PropsReduxActions;
const contactSortCollator = new Intl.Collator();
2020-09-14 19:51:27 +00:00
const _keyForError = (error: Error): string => {
return `${error.name}-${error.message}`;
};
export class MessageDetail extends React.Component<Props> {
private readonly focusRef = React.createRef<HTMLDivElement>();
2019-11-07 21:36:16 +00:00
private readonly messageContainerRef = React.createRef<HTMLDivElement>();
2020-09-14 19:51:27 +00:00
public componentDidMount(): void {
2019-11-07 21:36:16 +00:00
// When this component is created, it's initially not part of the DOM, and then it's
// added off-screen and animated in. This ensures that the focus takes.
setTimeout(() => {
if (this.focusRef.current) {
this.focusRef.current.focus();
}
});
}
2020-09-14 19:51:27 +00:00
public renderAvatar(contact: Contact): JSX.Element {
const { i18n } = this.props;
2020-07-24 01:35:32 +00:00
const {
2021-05-04 23:19:36 +00:00
acceptedMessageRequest,
2020-07-24 01:35:32 +00:00
avatarPath,
color,
2021-05-07 22:21:10 +00:00
isMe,
2020-07-24 01:35:32 +00:00
name,
2021-05-04 23:19:36 +00:00
phoneNumber,
2020-07-24 01:35:32 +00:00
profileName,
2021-05-04 23:19:36 +00:00
sharedGroupNames,
2020-07-24 01:35:32 +00:00
title,
2021-05-04 23:19:36 +00:00
unblurredAvatarPath,
2020-07-24 01:35:32 +00:00
} = contact;
return (
<Avatar
2021-05-04 23:19:36 +00:00
acceptedMessageRequest={acceptedMessageRequest}
avatarPath={avatarPath}
color={color}
conversationType="direct"
i18n={i18n}
2021-05-07 22:21:10 +00:00
isMe={isMe}
name={name}
phoneNumber={phoneNumber}
profileName={profileName}
2020-07-24 01:35:32 +00:00
title={title}
2021-05-04 23:19:36 +00:00
sharedGroupNames={sharedGroupNames}
size={AvatarSize.THIRTY_SIX}
2021-05-04 23:19:36 +00:00
unblurredAvatarPath={unblurredAvatarPath}
/>
);
}
2020-09-14 19:51:27 +00:00
public renderContact(contact: Contact): JSX.Element {
const { i18n, message, showSafetyNumber, sendAnyway } = this.props;
const errors = contact.errors || [];
const errorComponent = contact.isOutgoingKeyError ? (
<div className="module-message-detail__contact__error-buttons">
<button
2020-09-14 19:51:27 +00:00
type="button"
className="module-message-detail__contact__show-safety-number"
onClick={() => showSafetyNumber(contact.id)}
>
{i18n('showSafetyNumber')}
</button>
<button
2020-09-14 19:51:27 +00:00
type="button"
className="module-message-detail__contact__send-anyway"
onClick={() => sendAnyway(contact.id, message.id)}
>
{i18n('sendAnyway')}
</button>
</div>
) : null;
const unidentifiedDeliveryComponent = contact.isUnidentifiedDelivery ? (
<div className="module-message-detail__contact__unidentified-delivery-icon" />
) : null;
return (
<div key={contact.id} className="module-message-detail__contact">
{this.renderAvatar(contact)}
<div className="module-message-detail__contact__text">
<div className="module-message-detail__contact__name">
<ContactName
phoneNumber={contact.phoneNumber}
name={contact.name}
profileName={contact.profileName}
2020-07-24 01:35:32 +00:00
title={contact.title}
i18n={i18n}
/>
</div>
2020-09-14 19:51:27 +00:00
{errors.map(error => (
<div
key={_keyForError(error)}
className="module-message-detail__contact__error"
>
{error.message}
</div>
))}
</div>
{errorComponent}
{unidentifiedDeliveryComponent}
</div>
);
}
private renderContactGroup(
sendStatus: undefined | SendStatus,
contacts: undefined | ReadonlyArray<Contact>
): ReactNode {
const { i18n } = this.props;
if (!contacts || !contacts.length) {
return null;
}
const i18nKey =
sendStatus === undefined ? 'from' : `MessageDetailsHeader--${sendStatus}`;
const sortedContacts = [...contacts].sort((a, b) =>
contactSortCollator.compare(a.title, b.title)
);
return (
<div key={i18nKey} className="module-message-detail__contact-group">
<div
className={classNames(
'module-message-detail__contact-group__header',
sendStatus &&
`module-message-detail__contact-group__header--${sendStatus}`
)}
>
{i18n(i18nKey)}
</div>
{sortedContacts.map(contact => this.renderContact(contact))}
</div>
);
}
private renderContacts(): ReactChild {
// This assumes that the list either contains one sender (a status of `undefined`) or
// 1+ contacts with `SendStatus`es, but it doesn't check that assumption.
const { contacts } = this.props;
const contactsBySendStatus = groupBy(contacts, contact => contact.status);
return (
<div className="module-message-detail__contact-container">
{[
undefined,
SendStatus.Failed,
SendStatus.Viewed,
SendStatus.Read,
SendStatus.Delivered,
SendStatus.Sent,
SendStatus.Pending,
].map(sendStatus =>
this.renderContactGroup(
sendStatus,
contactsBySendStatus.get(sendStatus)
)
)}
</div>
);
}
2020-09-14 19:51:27 +00:00
public render(): JSX.Element {
const {
errors,
message,
receivedAt,
sentAt,
checkForAccount,
clearSelectedMessage,
2021-06-07 16:50:18 +00:00
contactNameColor,
displayTapToViewMessage,
doubleCheckMissingQuoteReference,
i18n,
interactionMode,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
markViewed,
openConversation,
openLink,
reactToMessage,
renderAudioAttachment,
renderEmojiPicker,
replyToMessage,
retrySend,
showContactDetail,
showContactModal,
showExpiredIncomingTapToViewToast,
showExpiredOutgoingTapToViewToast,
2021-04-27 22:35:35 +00:00
showForwardMessageModal,
showVisualAttachment,
} = this.props;
return (
2020-09-14 19:51:27 +00:00
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
2019-11-07 21:36:16 +00:00
<div className="module-message-detail" tabIndex={0} ref={this.focusRef}>
<div
className="module-message-detail__message-container"
ref={this.messageContainerRef}
>
<Message
{...message}
renderingContext="conversation/MessageDetail"
checkForAccount={checkForAccount}
clearSelectedMessage={clearSelectedMessage}
contactNameColor={contactNameColor}
containerElementRef={this.messageContainerRef}
2021-08-30 21:32:56 +00:00
deleteMessage={() =>
window.log.warn('MessageDetail: deleteMessage called!')
}
deleteMessageForEveryone={() =>
window.log.warn('MessageDetail: deleteMessageForEveryone called!')
}
disableMenu
disableScroll
displayTapToViewMessage={displayTapToViewMessage}
2021-08-30 21:32:56 +00:00
downloadAttachment={() =>
window.log.warn('MessageDetail: deleteMessageForEveryone called!')
}
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
i18n={i18n}
interactionMode={interactionMode}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
markViewed={markViewed}
onHeightChange={noop}
openConversation={openConversation}
openLink={openLink}
reactToMessage={reactToMessage}
renderAudioAttachment={renderAudioAttachment}
renderEmojiPicker={renderEmojiPicker}
replyToMessage={replyToMessage}
retrySend={retrySend}
showForwardMessageModal={showForwardMessageModal}
scrollToQuotedMessage={() => {
2021-08-30 21:32:56 +00:00
window.log.warn('MessageDetail: scrollToQuotedMessage called!');
}}
showContactDetail={showContactDetail}
showContactModal={showContactModal}
showExpiredIncomingTapToViewToast={
showExpiredIncomingTapToViewToast
}
showExpiredOutgoingTapToViewToast={
showExpiredOutgoingTapToViewToast
}
showMessageDetail={() => {
2021-08-30 21:32:56 +00:00
window.log.warn(
'MessageDetail: deleteMessageForEveryone called!'
);
}}
showVisualAttachment={showVisualAttachment}
/>
</div>
<table className="module-message-detail__info">
<tbody>
2020-09-14 19:51:27 +00:00
{(errors || []).map(error => (
<tr key={_keyForError(error)}>
<td className="module-message-detail__label">
{i18n('error')}
</td>
<td>
{' '}
<span className="error-message">{error.message}</span>{' '}
</td>
</tr>
))}
<tr>
<td className="module-message-detail__label">{i18n('sent')}</td>
<td>
{moment(sentAt).format('LLLL')}{' '}
<span className="module-message-detail__unix-timestamp">
({sentAt})
</span>
</td>
</tr>
{receivedAt ? (
<tr>
<td className="module-message-detail__label">
{i18n('received')}
</td>
<td>
{moment(receivedAt).format('LLLL')}{' '}
<span className="module-message-detail__unix-timestamp">
({receivedAt})
</span>
</td>
</tr>
) : null}
</tbody>
</table>
{this.renderContacts()}
</div>
);
}
}