You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			398 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			398 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			TypeScript
		
	
// 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<Props, State> {
 | 
						|
  public handleImageErrorBound: () => void;
 | 
						|
 | 
						|
  public constructor(props: Props) {
 | 
						|
    super(props);
 | 
						|
 | 
						|
    this.handleImageErrorBound = this.handleImageError.bind(this);
 | 
						|
 | 
						|
    this.state = {
 | 
						|
      imageBroken: false,
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  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 ? (
 | 
						|
      <div className="module-quote__icon-container__inner">
 | 
						|
        <div className="module-quote__icon-container__circle-background">
 | 
						|
          <div
 | 
						|
            className={classNames(
 | 
						|
              'module-quote__icon-container__icon',
 | 
						|
              `module-quote__icon-container__icon--${icon}`
 | 
						|
            )}
 | 
						|
          />
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    ) : null;
 | 
						|
 | 
						|
    return (
 | 
						|
      <div className="module-quote__icon-container">
 | 
						|
        <img
 | 
						|
          src={url}
 | 
						|
          alt={i18n('quoteThumbnailAlt')}
 | 
						|
          onError={this.handleImageErrorBound}
 | 
						|
        />
 | 
						|
        {iconElement}
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  public renderIcon(icon: string) {
 | 
						|
    return (
 | 
						|
      <div className="module-quote__icon-container">
 | 
						|
        <div className="module-quote__icon-container__inner">
 | 
						|
          <div className="module-quote__icon-container__circle-background">
 | 
						|
            <div
 | 
						|
              className={classNames(
 | 
						|
                'module-quote__icon-container__icon',
 | 
						|
                `module-quote__icon-container__icon--${icon}`
 | 
						|
              )}
 | 
						|
            />
 | 
						|
          </div>
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  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 (
 | 
						|
      <div className="module-quote__generic-file">
 | 
						|
        <div className="module-quote__generic-file__icon" />
 | 
						|
        <div
 | 
						|
          className={classNames(
 | 
						|
            'module-quote__generic-file__text',
 | 
						|
            isIncoming ? 'module-quote__generic-file__text--incoming' : null
 | 
						|
          )}
 | 
						|
        >
 | 
						|
          {fileName}
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  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 (
 | 
						|
        <div
 | 
						|
          dir="auto"
 | 
						|
          className={classNames(
 | 
						|
            'module-quote__primary__text',
 | 
						|
            isIncoming ? 'module-quote__primary__text--incoming' : null
 | 
						|
          )}
 | 
						|
        >
 | 
						|
          <MessageBody text={text} disableLinks={true} i18n={i18n} />
 | 
						|
        </div>
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (!attachment) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    const { contentType, isVoiceMessage } = attachment;
 | 
						|
 | 
						|
    const typeLabel = getTypeLabel({ i18n, contentType, isVoiceMessage });
 | 
						|
    if (typeLabel) {
 | 
						|
      return (
 | 
						|
        <div
 | 
						|
          className={classNames(
 | 
						|
            'module-quote__primary__type-label',
 | 
						|
            isIncoming ? 'module-quote__primary__type-label--incoming' : null
 | 
						|
          )}
 | 
						|
        >
 | 
						|
          {typeLabel}
 | 
						|
        </div>
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  public renderClose() {
 | 
						|
    const { onClose } = this.props;
 | 
						|
 | 
						|
    if (!onClose) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    // We don't want the overall click handler for the quote to fire, so we stop
 | 
						|
    //   propagation before handing control to the caller's callback.
 | 
						|
    const onClick = (e: React.MouseEvent<{}>): void => {
 | 
						|
      e.stopPropagation();
 | 
						|
      onClose();
 | 
						|
    };
 | 
						|
 | 
						|
    // We need the container to give us the flexibility to implement the iOS design.
 | 
						|
    return (
 | 
						|
      <div className="module-quote__close-container">
 | 
						|
        <div
 | 
						|
          className="module-quote__close-button"
 | 
						|
          role="button"
 | 
						|
          onClick={onClick}
 | 
						|
        />
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  public renderAuthor() {
 | 
						|
    const {
 | 
						|
      authorProfileName,
 | 
						|
      authorPhoneNumber,
 | 
						|
      authorName,
 | 
						|
      i18n,
 | 
						|
      isFromMe,
 | 
						|
      isIncoming,
 | 
						|
    } = this.props;
 | 
						|
 | 
						|
    return (
 | 
						|
      <div
 | 
						|
        className={classNames(
 | 
						|
          'module-quote__primary__author',
 | 
						|
          isIncoming ? 'module-quote__primary__author--incoming' : null
 | 
						|
        )}
 | 
						|
      >
 | 
						|
        {isFromMe ? (
 | 
						|
          i18n('you')
 | 
						|
        ) : (
 | 
						|
          <ContactName
 | 
						|
            phoneNumber={authorPhoneNumber}
 | 
						|
            name={authorName}
 | 
						|
            profileName={authorProfileName}
 | 
						|
            i18n={i18n}
 | 
						|
          />
 | 
						|
        )}
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  public renderReferenceWarning() {
 | 
						|
    const { i18n, isIncoming, referencedMessageNotFound } = this.props;
 | 
						|
 | 
						|
    if (!referencedMessageNotFound) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    return (
 | 
						|
      <div
 | 
						|
        className={classNames(
 | 
						|
          'module-quote__reference-warning',
 | 
						|
          isIncoming ? 'module-quote__reference-warning--incoming' : null
 | 
						|
        )}
 | 
						|
      >
 | 
						|
        <div
 | 
						|
          className={classNames(
 | 
						|
            'module-quote__reference-warning__icon',
 | 
						|
            isIncoming
 | 
						|
              ? 'module-quote__reference-warning__icon--incoming'
 | 
						|
              : null
 | 
						|
          )}
 | 
						|
        />
 | 
						|
        <div
 | 
						|
          className={classNames(
 | 
						|
            'module-quote__reference-warning__text',
 | 
						|
            isIncoming
 | 
						|
              ? 'module-quote__reference-warning__text--incoming'
 | 
						|
              : null
 | 
						|
          )}
 | 
						|
        >
 | 
						|
          {i18n('originalMessageNotFound')}
 | 
						|
        </div>
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  public render() {
 | 
						|
    const {
 | 
						|
      authorColor,
 | 
						|
      isIncoming,
 | 
						|
      onClick,
 | 
						|
      referencedMessageNotFound,
 | 
						|
      withContentAbove,
 | 
						|
    } = this.props;
 | 
						|
 | 
						|
    if (!validateQuote(this.props)) {
 | 
						|
      return null;
 | 
						|
    }
 | 
						|
 | 
						|
    return (
 | 
						|
      <div
 | 
						|
        className={classNames(
 | 
						|
          'module-quote-container',
 | 
						|
          withContentAbove ? 'module-quote-container--with-content-above' : null
 | 
						|
        )}
 | 
						|
      >
 | 
						|
        <div
 | 
						|
          onClick={onClick}
 | 
						|
          role="button"
 | 
						|
          className={classNames(
 | 
						|
            'module-quote',
 | 
						|
            isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing',
 | 
						|
            isIncoming
 | 
						|
              ? `module-quote--incoming-${authorColor}`
 | 
						|
              : `module-quote--outgoing-${authorColor}`,
 | 
						|
            !onClick ? 'module-quote--no-click' : null,
 | 
						|
            withContentAbove ? 'module-quote--with-content-above' : null,
 | 
						|
            referencedMessageNotFound
 | 
						|
              ? 'module-quote--with-reference-warning'
 | 
						|
              : null
 | 
						|
          )}
 | 
						|
        >
 | 
						|
          <div className="module-quote__primary">
 | 
						|
            {this.renderAuthor()}
 | 
						|
            {this.renderGenericFile()}
 | 
						|
            {this.renderText()}
 | 
						|
          </div>
 | 
						|
          {this.renderIconContainer()}
 | 
						|
          {this.renderClose()}
 | 
						|
        </div>
 | 
						|
        {this.renderReferenceWarning()}
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |