import React from 'react'; import classNames from 'classnames'; import moment from 'moment'; import { padStart } from 'lodash'; import { formatRelativeTime } from '../../util/formatRelativeTime'; import { isImageTypeSupported, isVideoTypeSupported, } from '../../util/GoogleChrome'; import { MessageBody } from './MessageBody'; import { Emojify } from './Emojify'; import { Quote, QuotedAttachment } from './Quote'; import { EmbeddedContact } from './EmbeddedContact'; import { Contact } from '../../types/Contact'; import { Localizer } from '../../types/Util'; import * as MIME from '../../../ts/types/MIME'; interface Attachment { contentType: MIME.MIMEType; fileName: string; /** Not included in protobuf, needs to be pulled from flags */ isVoiceMessage: boolean; /** For messages not already on disk, this will be a data url */ url: string; fileSize?: string; } interface Props { text?: string; id?: string; collapseMetadata?: boolean; direction: 'incoming' | 'outgoing'; timestamp: number; status?: 'sending' | 'sent' | 'delivered' | 'read'; contacts?: Array; color: | 'gray' | 'blue' | 'cyan' | 'deep-orange' | 'green' | 'indigo' | 'pink' | 'purple' | 'red' | 'teal'; i18n: Localizer; authorName?: string; authorProfileName?: string; /** Note: this should be formatted for display */ authorPhoneNumber?: string; conversationType: 'group' | 'direct'; attachment?: Attachment; quote?: { text: string; attachments: Array; isFromMe: boolean; authorName?: string; authorPhoneNumber?: string; authorProfileName?: string; }; authorAvatarPath?: string; contactHasSignalAccount: boolean; expirationLength?: number; expirationTimestamp?: number; onClickQuote?: () => void; onSendMessageToContact?: () => void; onClickContact?: () => void; onClickAttachment?: () => void; } function isImage(attachment?: Attachment) { return ( attachment && attachment.contentType && isImageTypeSupported(attachment.contentType) ); } function isVideo(attachment?: Attachment) { return ( attachment && attachment.contentType && isVideoTypeSupported(attachment.contentType) ); } function isAudio(attachment?: Attachment) { return ( attachment && attachment.contentType && MIME.isAudio(attachment.contentType) ); } function getTimerBucket(expiration: number, length: number): string { const delta = expiration - Date.now(); if (delta < 0) { return '00'; } if (delta > length) { return '60'; } const increment = Math.round(delta / length * 12); return padStart(String(increment * 5), 2, '0'); } function getExtension({ fileName, contentType, }: { fileName: string; contentType: MIME.MIMEType; }): string | null { if (fileName && fileName.indexOf('.') >= 0) { const lastPeriod = fileName.lastIndexOf('.'); const extension = fileName.slice(lastPeriod + 1); if (extension.length) { return extension; } } const slash = contentType.indexOf('/'); if (slash >= 0) { return contentType.slice(slash + 1); } return null; } export class Message extends React.Component { public renderTimer() { const { attachment, direction, expirationLength, expirationTimestamp, text, } = this.props; if (!expirationLength || !expirationTimestamp) { return null; } const withImageNoCaption = !text && isImage(attachment); const bucket = getTimerBucket(expirationTimestamp, expirationLength); return (
); } public renderMetadata() { const { collapseMetadata, color, direction, i18n, status, timestamp, text, attachment, } = this.props; if (collapseMetadata) { return null; } // We're not showing metadata on top of videos since they still have native controls if (!text && isVideo(attachment)) { return null; } const withImageNoCaption = !text && isImage(attachment); return (
{formatRelativeTime(timestamp, { i18n, extended: true })} {this.renderTimer()} {direction === 'outgoing' ? (
) : null}
); } public renderAuthor() { const { authorName, conversationType, direction, i18n, authorPhoneNumber, authorProfileName, } = this.props; const title = authorName ? authorName : authorPhoneNumber; if (direction !== 'incoming' || conversationType !== 'group' || !title) { return null; } const profileElement = authorProfileName && !authorName ? ( ~ ) : null; return (
{profileElement}
); } // tslint:disable-next-line max-func-body-length public renderAttachment() { const { i18n, attachment, text, collapseMetadata, conversationType, direction, quote, onClickAttachment, } = this.props; if (!attachment) { return null; } const withCaption = Boolean(text); // For attachments which aren't full-frame const withContentBelow = withCaption || !collapseMetadata; const withContentAbove = quote || (conversationType === 'group' && direction === 'incoming'); if (isImage(attachment)) { return (
{i18n('imageAttachmentAlt')} {!withCaption && !collapseMetadata ? (
) : null}
); } else if (isVideo(attachment)) { return ( ); } else if (isAudio(attachment)) { return ( ); } else { const { fileName, fileSize, contentType } = attachment; const extension = getExtension({ contentType, fileName }); return (
{extension ? (
{extension}
) : null}
{fileName}
{fileSize}
); } } public renderQuote() { const { color, conversationType, direction, i18n, onClickQuote, quote, } = this.props; if (!quote) { return null; } const authorTitle = quote.authorName ? quote.authorName : quote.authorPhoneNumber; const authorProfileName = !quote.authorName ? quote.authorProfileName : undefined; const withContentAbove = conversationType === 'group' && direction === 'incoming'; return ( ); } public renderEmbeddedContact() { const { collapseMetadata, contactHasSignalAccount, contacts, conversationType, direction, i18n, onClickContact, onSendMessageToContact, text, } = this.props; const first = contacts && contacts[0]; if (!first) { return null; } const withCaption = Boolean(text); const withContentAbove = conversationType === 'group' && direction === 'incoming'; const withContentBelow = withCaption || !collapseMetadata; return ( ); } public renderSendMessageButton() { const { contactHasSignalAccount, contacts, i18n, onSendMessageToContact, } = this.props; const first = contacts && contacts[0]; if (!first || !contactHasSignalAccount) { return null; } return (
{i18n('sendMessageToContact')}
); } public renderAvatar() { const { authorName, authorPhoneNumber, authorProfileName, authorAvatarPath, collapseMetadata, color, conversationType, direction, i18n, } = this.props; const title = `${authorName || authorPhoneNumber}${ !authorName && authorProfileName ? ` ~${authorProfileName}` : '' }`; if ( collapseMetadata || conversationType !== 'group' || direction === 'outgoing' ) { return; } if (!authorAvatarPath) { return (
#
); } return (
{i18n('contactAvatarAlt',
); } public renderText() { const { text, i18n, direction } = this.props; if (!text) { return null; } return (
); } public render() { const { attachment, color, conversationType, direction, id, quote, text, } = this.props; const imageAndNothingElse = !text && isImage(attachment) && conversationType !== 'group' && !quote; return (
  • {this.renderAuthor()} {this.renderQuote()} {this.renderAttachment()} {this.renderEmbeddedContact()} {this.renderText()} {this.renderMetadata()} {this.renderSendMessageButton()} {this.renderAvatar()}
  • ); } }