// 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 MessagePropsType, PropsData as MessagePropsDataType, } from './Message'; import { LocalizerType } from '../../types/Util'; import { ConversationType } from '../../state/ducks/conversations'; import { assert } from '../../util/assert'; export type Contact = Pick< ConversationType, | 'acceptedMessageRequest' | 'avatarPath' | 'color' | 'isMe' | 'name' | 'phoneNumber' | 'profileName' | 'sharedGroupNames' | 'title' | 'unblurredAvatarPath' > & { status: MessageStatusType | null; isOutgoingKeyError: boolean; isUnidentifiedDelivery: boolean; unblurredAvatarPath?: string; errors?: Array; onSendAnyway: () => void; onShowSafetyNumber: () => void; }; export type Props = { contacts: Array; errors: Array; 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' | 'showForwardMessageModal' | 'showVisualAttachment' >; const _keyForError = (error: Error): string => { return `${error.name}-${error.message}`; }; export class MessageDetail extends React.Component { private readonly focusRef = 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 renderDeleteButton(): JSX.Element { const { deleteMessage, i18n, message } = this.props; return (
); } public renderContact(contact: Contact): JSX.Element { const { i18n } = this.props; const errors = contact.errors || []; const errorComponent = contact.isOutgoingKeyError ? (
) : null; const statusComponent = !contact.isOutgoingKeyError ? (
) : null; const unidentifiedDeliveryComponent = contact.isUnidentifiedDelivery ? (
) : null; return (
{this.renderAvatar(contact)}
{errors.map(error => (
{error.message}
))}
{errorComponent} {unidentifiedDeliveryComponent} {statusComponent}
); } public renderContacts(): JSX.Element | null { const { contacts } = this.props; if (!contacts || !contacts.length) { return null; } return (
{contacts.map(contact => this.renderContact(contact))}
); } public render(): JSX.Element { 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, showForwardMessageModal, showVisualAttachment, } = this.props; return ( // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
{ 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} />
{(errors || []).map(error => ( ))} {receivedAt ? ( ) : null}
{i18n('error')} {' '} {error.message}{' '}
{i18n('sent')} {moment(sentAt).format('LLLL')}{' '} ({sentAt})
{i18n('received')} {moment(receivedAt).format('LLLL')}{' '} ({receivedAt})
{message.direction === 'incoming' ? i18n('from') : i18n('to')}
{this.renderContacts()} {this.renderDeleteButton()}
); } }