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.
		
		
		
		
		
			
		
			
				
	
	
		
			389 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			389 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			TypeScript
		
	
import classNames from 'classnames';
 | 
						|
import React, { useState } from 'react';
 | 
						|
import { noop } from 'lodash';
 | 
						|
 | 
						|
import * as MIME from '../../../../types/MIME';
 | 
						|
import * as GoogleChrome from '../../../../util/GoogleChrome';
 | 
						|
 | 
						|
import { useDisableDrag } from '../../../../hooks/useDisableDrag';
 | 
						|
import { useEncryptedFileFetch } from '../../../../hooks/useEncryptedFileFetch';
 | 
						|
import { PubKey } from '../../../../session/types';
 | 
						|
import {
 | 
						|
  useSelectedIsPrivate,
 | 
						|
  useSelectedIsPublic,
 | 
						|
} from '../../../../state/selectors/selectedConversation';
 | 
						|
import { ContactName } from '../../ContactName';
 | 
						|
import { MessageBody } from './MessageBody';
 | 
						|
 | 
						|
export type QuotePropsWithoutListener = {
 | 
						|
  attachment?: QuotedAttachmentType;
 | 
						|
  sender: string;
 | 
						|
  authorProfileName?: string;
 | 
						|
  authorName?: string;
 | 
						|
  isFromMe: boolean;
 | 
						|
  isIncoming: boolean;
 | 
						|
  text: string | null;
 | 
						|
  referencedMessageNotFound: boolean;
 | 
						|
};
 | 
						|
 | 
						|
export type QuotePropsWithListener = QuotePropsWithoutListener & {
 | 
						|
  onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
 | 
						|
};
 | 
						|
 | 
						|
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: QuotePropsWithoutListener): 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 undefined;
 | 
						|
}
 | 
						|
 | 
						|
function getTypeLabel({
 | 
						|
  contentType,
 | 
						|
  isVoiceMessage,
 | 
						|
}: {
 | 
						|
  contentType: MIME.MIMEType;
 | 
						|
  isVoiceMessage: boolean;
 | 
						|
}): string | undefined {
 | 
						|
  if (GoogleChrome.isVideoTypeSupported(contentType)) {
 | 
						|
    return window.i18n('video');
 | 
						|
  }
 | 
						|
  if (GoogleChrome.isImageTypeSupported(contentType)) {
 | 
						|
    return window.i18n('photo');
 | 
						|
  }
 | 
						|
  if (MIME.isAudio(contentType) && isVoiceMessage) {
 | 
						|
    return window.i18n('voiceMessage');
 | 
						|
  }
 | 
						|
  if (MIME.isAudio(contentType)) {
 | 
						|
    return window.i18n('audio');
 | 
						|
  }
 | 
						|
 | 
						|
  return undefined;
 | 
						|
}
 | 
						|
export const QuoteIcon = (props: any) => {
 | 
						|
  const { icon } = props;
 | 
						|
 | 
						|
  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>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const QuoteImage = (props: {
 | 
						|
  handleImageErrorBound: () => void;
 | 
						|
  url: string;
 | 
						|
  contentType: string;
 | 
						|
  icon?: string;
 | 
						|
}) => {
 | 
						|
  const { url, icon, contentType, handleImageErrorBound } = props;
 | 
						|
  const disableDrag = useDisableDrag();
 | 
						|
 | 
						|
  const { loading, urlToLoad } = useEncryptedFileFetch(url, contentType, false);
 | 
						|
  const srcData = !loading ? urlToLoad : '';
 | 
						|
 | 
						|
  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={srcData}
 | 
						|
        alt={window.i18n('quoteThumbnailAlt')}
 | 
						|
        onDragStart={disableDrag}
 | 
						|
        onError={handleImageErrorBound}
 | 
						|
      />
 | 
						|
      {iconElement}
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const QuoteGenericFile = (
 | 
						|
  props: Pick<QuotePropsWithoutListener, 'attachment' | 'isIncoming'>
 | 
						|
) => {
 | 
						|
  const { attachment, isIncoming } = props;
 | 
						|
 | 
						|
  if (!attachment) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  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>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const QuoteIconContainer = (
 | 
						|
  props: Pick<QuotePropsWithoutListener, 'attachment'> & {
 | 
						|
    handleImageErrorBound: () => void;
 | 
						|
    imageBroken: boolean;
 | 
						|
  }
 | 
						|
) => {
 | 
						|
  const { attachment, imageBroken, handleImageErrorBound } = props;
 | 
						|
 | 
						|
  if (!attachment) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  const { contentType, thumbnail } = attachment;
 | 
						|
  const objectUrl = getObjectUrl(thumbnail);
 | 
						|
 | 
						|
  if (GoogleChrome.isVideoTypeSupported(contentType)) {
 | 
						|
    return objectUrl && !imageBroken ? (
 | 
						|
      <QuoteImage
 | 
						|
        url={objectUrl}
 | 
						|
        contentType={MIME.IMAGE_JPEG}
 | 
						|
        icon="play"
 | 
						|
        handleImageErrorBound={noop}
 | 
						|
      />
 | 
						|
    ) : (
 | 
						|
      <QuoteIcon icon="movie" />
 | 
						|
    );
 | 
						|
  }
 | 
						|
  if (GoogleChrome.isImageTypeSupported(contentType)) {
 | 
						|
    return objectUrl && !imageBroken ? (
 | 
						|
      <QuoteImage
 | 
						|
        url={objectUrl}
 | 
						|
        contentType={contentType}
 | 
						|
        handleImageErrorBound={handleImageErrorBound}
 | 
						|
      />
 | 
						|
    ) : (
 | 
						|
      <QuoteIcon icon="image" />
 | 
						|
    );
 | 
						|
  }
 | 
						|
  if (MIME.isAudio(contentType)) {
 | 
						|
    return <QuoteIcon icon="microphone" />;
 | 
						|
  }
 | 
						|
  return null;
 | 
						|
};
 | 
						|
 | 
						|
export const QuoteText = (
 | 
						|
  props: Pick<QuotePropsWithoutListener, 'text' | 'attachment' | 'isIncoming'>
 | 
						|
) => {
 | 
						|
  const { text, attachment, isIncoming } = props;
 | 
						|
 | 
						|
  const isGroup = !useSelectedIsPrivate();
 | 
						|
 | 
						|
  if (text) {
 | 
						|
    return (
 | 
						|
      <div
 | 
						|
        dir="auto"
 | 
						|
        className={classNames(
 | 
						|
          'module-quote__primary__text',
 | 
						|
          isIncoming ? 'module-quote__primary__text--incoming' : null
 | 
						|
        )}
 | 
						|
      >
 | 
						|
        <MessageBody text={text} disableLinks={true} disableJumbomoji={true} isGroup={isGroup} />
 | 
						|
      </div>
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  if (!attachment) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  const { contentType, isVoiceMessage } = attachment;
 | 
						|
 | 
						|
  const typeLabel = getTypeLabel({ 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;
 | 
						|
};
 | 
						|
 | 
						|
type QuoteAuthorProps = {
 | 
						|
  author: string;
 | 
						|
  authorProfileName?: string;
 | 
						|
  authorName?: string;
 | 
						|
  isFromMe: boolean;
 | 
						|
  isIncoming: boolean;
 | 
						|
  showPubkeyForAuthor?: boolean;
 | 
						|
};
 | 
						|
 | 
						|
const QuoteAuthor = (props: QuoteAuthorProps) => {
 | 
						|
  const { authorProfileName, author, authorName, isFromMe, isIncoming } = props;
 | 
						|
 | 
						|
  return (
 | 
						|
    <div
 | 
						|
      className={classNames(
 | 
						|
        'module-quote__primary__author',
 | 
						|
        isIncoming ? 'module-quote__primary__author--incoming' : null
 | 
						|
      )}
 | 
						|
    >
 | 
						|
      {isFromMe ? (
 | 
						|
        window.i18n('you')
 | 
						|
      ) : (
 | 
						|
        <ContactName
 | 
						|
          pubkey={PubKey.shorten(author)}
 | 
						|
          name={authorName}
 | 
						|
          profileName={authorProfileName}
 | 
						|
          compact={true}
 | 
						|
          shouldShowPubkey={Boolean(props.showPubkeyForAuthor)}
 | 
						|
        />
 | 
						|
      )}
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const QuoteReferenceWarning = (
 | 
						|
  props: Pick<QuotePropsWithoutListener, 'isIncoming' | 'referencedMessageNotFound'>
 | 
						|
) => {
 | 
						|
  const { isIncoming, referencedMessageNotFound } = props;
 | 
						|
 | 
						|
  if (!referencedMessageNotFound) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  return (
 | 
						|
    <div
 | 
						|
      className={classNames(
 | 
						|
        'module-quote__reference-warning',
 | 
						|
        isIncoming
 | 
						|
          ? 'module-quote__reference-warning--incoming'
 | 
						|
          : 'module-quote__reference-warning--outgoing'
 | 
						|
      )}
 | 
						|
    >
 | 
						|
      <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
 | 
						|
        )}
 | 
						|
      >
 | 
						|
        {window.i18n('originalMessageNotFound')}
 | 
						|
      </div>
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
};
 | 
						|
 | 
						|
export const Quote = (props: QuotePropsWithListener) => {
 | 
						|
  const [imageBroken, setImageBroken] = useState(false);
 | 
						|
  const handleImageErrorBound = () => {
 | 
						|
    setImageBroken(true);
 | 
						|
  };
 | 
						|
 | 
						|
  const isPublic = useSelectedIsPublic();
 | 
						|
 | 
						|
  if (!validateQuote(props)) {
 | 
						|
    return null;
 | 
						|
  }
 | 
						|
 | 
						|
  const { isIncoming, referencedMessageNotFound, attachment, text, onClick } = props;
 | 
						|
 | 
						|
  return (
 | 
						|
    <div className={classNames('module-quote-container')}>
 | 
						|
      <div
 | 
						|
        onClick={onClick}
 | 
						|
        role="button"
 | 
						|
        className={classNames(
 | 
						|
          'module-quote',
 | 
						|
          isIncoming ? 'module-quote--incoming' : 'module-quote--outgoing',
 | 
						|
          !onClick ? 'module-quote--no-click' : null,
 | 
						|
          referencedMessageNotFound ? 'module-quote--with-reference-warning' : null
 | 
						|
        )}
 | 
						|
      >
 | 
						|
        <div className="module-quote__primary">
 | 
						|
          <QuoteAuthor
 | 
						|
            authorName={props.authorName}
 | 
						|
            author={props.sender}
 | 
						|
            authorProfileName={props.authorProfileName}
 | 
						|
            isFromMe={props.isFromMe}
 | 
						|
            isIncoming={props.isIncoming}
 | 
						|
            showPubkeyForAuthor={isPublic}
 | 
						|
          />
 | 
						|
          <QuoteGenericFile {...props} />
 | 
						|
          <QuoteText isIncoming={isIncoming} text={text} attachment={attachment} />
 | 
						|
        </div>
 | 
						|
        <QuoteIconContainer
 | 
						|
          attachment={attachment}
 | 
						|
          handleImageErrorBound={handleImageErrorBound}
 | 
						|
          imageBroken={imageBroken}
 | 
						|
        />
 | 
						|
      </div>
 | 
						|
      <QuoteReferenceWarning
 | 
						|
        isIncoming={isIncoming}
 | 
						|
        referencedMessageNotFound={referencedMessageNotFound}
 | 
						|
      />
 | 
						|
    </div>
 | 
						|
  );
 | 
						|
};
 |