// tslint:disable:react-this-binding-issue import React from 'react'; import classNames from 'classnames'; import * as MIME from '../../../ts/types/MIME'; import * as GoogleChrome from '../../../ts/util/GoogleChrome'; import { MessageBody } from './MessageBody'; import { ColorType, LocalizerType } from '../../types/Util'; import { ContactName } from './ContactName'; interface Props { attachment?: QuotedAttachmentType; authorPhoneNumber: string; authorProfileName?: string; authorName?: string; authorColor?: ColorType; i18n: LocalizerType; isFromMe: boolean; isIncoming: boolean; withContentAbove: boolean; onClick?: () => void; onClose?: () => void; text: string; referencedMessageNotFound: boolean; } interface State { imageBroken: boolean; } export interface QuotedAttachmentType { contentType: MIME.MIMEType; fileName: string; /** Not included in protobuf */ isVoiceMessage: boolean; thumbnail?: Attachment; } interface Attachment { contentType: MIME.MIMEType; /** Not included in protobuf, and is loaded asynchronously */ objectUrl?: string; } function validateQuote(quote: Props): boolean { if (quote.text) { return true; } if (quote.attachment) { return true; } return false; } function getObjectUrl(thumbnail: Attachment | undefined): string | undefined { if (thumbnail && thumbnail.objectUrl) { return thumbnail.objectUrl; } return; } function getTypeLabel({ i18n, contentType, isVoiceMessage, }: { i18n: LocalizerType; contentType: MIME.MIMEType; isVoiceMessage: boolean; }): string | undefined { if (GoogleChrome.isVideoTypeSupported(contentType)) { return i18n('video'); } if (GoogleChrome.isImageTypeSupported(contentType)) { return i18n('photo'); } if (MIME.isAudio(contentType) && isVoiceMessage) { return i18n('voiceMessage'); } if (MIME.isAudio(contentType)) { return i18n('audio'); } return; } export class Quote extends React.Component { public state = { imageBroken: false, }; public handleKeyDown = (event: React.KeyboardEvent) => { const { onClick } = this.props; // This is important to ensure that using this quote to navigate to the referenced // message doesn't also trigger its parent message's keydown. if (onClick && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); event.stopPropagation(); onClick(); } }; public handleClick = (event: React.MouseEvent) => { const { onClick } = this.props; if (onClick) { event.preventDefault(); event.stopPropagation(); onClick(); } }; public handleImageError = () => { // tslint:disable-next-line no-console console.log('Message: Image failed to load; failing over to placeholder'); this.setState({ imageBroken: true, }); }; public renderImage(url: string, i18n: LocalizerType, icon?: string) { const iconElement = icon ? (
) : null; return (
{i18n('quoteThumbnailAlt')} {iconElement}
); } public renderIcon(icon: string) { return (
); } public renderGenericFile() { const { attachment, isIncoming } = this.props; if (!attachment) { return; } const { fileName, contentType } = attachment; const isGenericFile = !GoogleChrome.isVideoTypeSupported(contentType) && !GoogleChrome.isImageTypeSupported(contentType) && !MIME.isAudio(contentType); if (!isGenericFile) { return null; } return (
{fileName}
); } public renderIconContainer() { const { attachment, i18n } = this.props; const { imageBroken } = this.state; if (!attachment) { return null; } const { contentType, thumbnail } = attachment; const objectUrl = getObjectUrl(thumbnail); if (GoogleChrome.isVideoTypeSupported(contentType)) { return objectUrl && !imageBroken ? this.renderImage(objectUrl, i18n, 'play') : this.renderIcon('movie'); } if (GoogleChrome.isImageTypeSupported(contentType)) { return objectUrl && !imageBroken ? this.renderImage(objectUrl, i18n) : this.renderIcon('image'); } if (MIME.isAudio(contentType)) { return this.renderIcon('microphone'); } return null; } public renderText() { const { i18n, text, attachment, isIncoming } = this.props; if (text) { return (
); } if (!attachment) { return null; } const { contentType, isVoiceMessage } = attachment; const typeLabel = getTypeLabel({ i18n, contentType, isVoiceMessage }); if (typeLabel) { return (
{typeLabel}
); } return null; } public renderClose() { const { onClose } = this.props; if (!onClose) { return null; } const clickHandler = (e: React.MouseEvent): void => { e.stopPropagation(); e.preventDefault(); onClose(); }; const keyDownHandler = (e: React.KeyboardEvent): void => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); e.preventDefault(); onClose(); } }; // We need the container to give us the flexibility to implement the iOS design. return (
); } public renderAuthor() { const { authorProfileName, authorPhoneNumber, authorName, i18n, isFromMe, isIncoming, } = this.props; return (
{isFromMe ? ( i18n('you') ) : ( )}
); } public renderReferenceWarning() { const { i18n, isIncoming, referencedMessageNotFound } = this.props; if (!referencedMessageNotFound) { return null; } return (
{i18n('originalMessageNotFound')}
); } public render() { const { authorColor, isIncoming, onClick, referencedMessageNotFound, withContentAbove, } = this.props; if (!validateQuote(this.props)) { return null; } return (
{this.renderReferenceWarning()}
); } }