// Copyright 2018-2021 Signal Messenger, LLC // 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'; import { ConversationType } from '../../state/ducks/conversations'; import { groupBy } from '../../util/mapUtil'; import { ContactNameColorType } from '../../types/Colors'; import { SendStatus } from '../../messages/MessageSendState'; import * as log from '../../logging/log'; export type Contact = Pick< ConversationType, | 'acceptedMessageRequest' | 'avatarPath' | 'color' | 'id' | 'isMe' | 'name' | 'phoneNumber' | 'profileName' | 'sharedGroupNames' | 'title' | 'unblurredAvatarPath' > & { status?: SendStatus; isOutgoingKeyError: boolean; isUnidentifiedDelivery: boolean; errors?: Array; }; 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; contactNameColor?: ContactNameColorType; errors: Array; message: Omit; receivedAt: number; sentAt: number; showSafetyNumber: (contactId: string) => void; i18n: LocalizerType; } & Pick; export type PropsBackboneActions = Pick< MessagePropsType, | 'displayTapToViewMessage' | 'kickOffAttachmentDownload' | 'markAttachmentAsCorrupted' | 'markViewed' | 'openConversation' | 'openLink' | 'reactToMessage' | 'renderAudioAttachment' | 'renderEmojiPicker' | 'renderReactionPicker' | 'replyToMessage' | 'retrySend' | 'showContactDetail' | 'showContactModal' | 'showExpiredIncomingTapToViewToast' | 'showExpiredOutgoingTapToViewToast' | 'showForwardMessageModal' | 'showVisualAttachment' >; export type PropsReduxActions = Pick< MessagePropsType, | 'clearSelectedMessage' | 'doubleCheckMissingQuoteReference' | 'checkForAccount' >; export type ExternalProps = PropsData & PropsBackboneActions; export type Props = PropsData & PropsBackboneActions & PropsReduxActions; const contactSortCollator = new Intl.Collator(); const _keyForError = (error: Error): string => { return `${error.name}-${error.message}`; }; export class MessageDetail extends React.Component { private readonly focusRef = React.createRef(); private readonly messageContainerRef = React.createRef(); public componentDidMount(): void { // 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(); } }); } public renderAvatar(contact: Contact): JSX.Element { const { i18n } = this.props; const { acceptedMessageRequest, avatarPath, color, isMe, name, phoneNumber, profileName, sharedGroupNames, title, unblurredAvatarPath, } = contact; return ( ); } public renderContact(contact: Contact): JSX.Element { const { i18n, showSafetyNumber } = this.props; const errors = contact.errors || []; const errorComponent = contact.isOutgoingKeyError ? (
) : null; const unidentifiedDeliveryComponent = contact.isUnidentifiedDelivery ? (
) : null; return (
{this.renderAvatar(contact)}
{errors.map(error => (
{error.message}
))}
{errorComponent} {unidentifiedDeliveryComponent}
); } private renderContactGroup( sendStatus: undefined | SendStatus, contacts: undefined | ReadonlyArray ): 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 (
{i18n(i18nKey)}
{sortedContacts.map(contact => this.renderContact(contact))}
); } 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 (
{[ undefined, SendStatus.Failed, SendStatus.Viewed, SendStatus.Read, SendStatus.Delivered, SendStatus.Sent, SendStatus.Pending, ].map(sendStatus => this.renderContactGroup( sendStatus, contactsBySendStatus.get(sendStatus) ) )}
); } public render(): JSX.Element { const { errors, message, receivedAt, sentAt, checkForAccount, clearSelectedMessage, contactNameColor, displayTapToViewMessage, doubleCheckMissingQuoteReference, i18n, interactionMode, kickOffAttachmentDownload, markAttachmentAsCorrupted, markViewed, openConversation, openLink, reactToMessage, renderAudioAttachment, renderEmojiPicker, renderReactionPicker, replyToMessage, retrySend, showContactDetail, showContactModal, showExpiredIncomingTapToViewToast, showExpiredOutgoingTapToViewToast, showForwardMessageModal, showVisualAttachment, } = this.props; return ( // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
log.warn('MessageDetail: deleteMessage called!') } deleteMessageForEveryone={() => log.warn('MessageDetail: deleteMessageForEveryone called!') } disableMenu disableScroll displayTapToViewMessage={displayTapToViewMessage} downloadAttachment={() => 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} renderReactionPicker={renderReactionPicker} replyToMessage={replyToMessage} retrySend={retrySend} showForwardMessageModal={showForwardMessageModal} scrollToQuotedMessage={() => { log.warn('MessageDetail: scrollToQuotedMessage called!'); }} showContactDetail={showContactDetail} showContactModal={showContactModal} showExpiredIncomingTapToViewToast={ showExpiredIncomingTapToViewToast } showExpiredOutgoingTapToViewToast={ showExpiredOutgoingTapToViewToast } showMessageDetail={() => { log.warn('MessageDetail: deleteMessageForEveryone called!'); }} showVisualAttachment={showVisualAttachment} />
{(errors || []).map(error => ( ))} {receivedAt ? ( ) : null}
{i18n('error')} {' '} {error.message}{' '}
{i18n('sent')} {moment(sentAt).format('LLLL')}{' '} ({sentAt})
{i18n('received')} {moment(receivedAt).format('LLLL')}{' '} ({receivedAt})
{this.renderContacts()}
); } }