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.
		
		
		
		
		
			
		
			
				
	
	
		
			366 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			366 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
| // tslint:disable:react-this-binding-issue
 | |
| 
 | |
| import React, { useState } 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';
 | |
| import { PubKey } from '../../session/types';
 | |
| import { ConversationTypeEnum } from '../../models/conversation';
 | |
| 
 | |
| import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
 | |
| 
 | |
| interface QuoteProps {
 | |
|   attachment?: QuotedAttachmentType;
 | |
|   authorPhoneNumber: string;
 | |
|   authorProfileName?: string;
 | |
|   authorName?: string;
 | |
|   i18n: LocalizerType;
 | |
|   isFromMe: boolean;
 | |
|   isIncoming: boolean;
 | |
|   conversationType: ConversationTypeEnum;
 | |
|   convoId: string;
 | |
|   isPublic?: boolean;
 | |
|   withContentAbove: boolean;
 | |
|   onClick?: (e: any) => void;
 | |
|   text: string;
 | |
|   referencedMessageNotFound: 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: QuoteProps): 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 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: any) => {
 | |
|   const { url, i18n, icon, contentType, handleImageErrorBound } = props;
 | |
| 
 | |
|   const { loading, urlToLoad } = useEncryptedFileFetch(url, contentType);
 | |
|   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={i18n('quoteThumbnailAlt')} onError={handleImageErrorBound} />
 | |
|       {iconElement}
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export const QuoteGenericFile = (props: any) => {
 | |
|   const { attachment, isIncoming } = props;
 | |
| 
 | |
|   if (!attachment) {
 | |
|     return <></>;
 | |
|   }
 | |
| 
 | |
|   const { fileName, contentType } = attachment;
 | |
|   const isGenericFile =
 | |
|     !GoogleChrome.isVideoTypeSupported(contentType) &&
 | |
|     !GoogleChrome.isImageTypeSupported(contentType) &&
 | |
|     !MIME.isAudio(contentType);
 | |
| 
 | |
|   if (!isGenericFile) {
 | |
|     return <></>;
 | |
|   }
 | |
| 
 | |
|   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: any) => {
 | |
|   const { attachment, i18n, 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} i18n={i18n} icon={'play'} />
 | |
|     ) : (
 | |
|       <QuoteIcon icon="movie" />
 | |
|     );
 | |
|   }
 | |
|   if (GoogleChrome.isImageTypeSupported(contentType)) {
 | |
|     return objectUrl && !imageBroken ? (
 | |
|       <QuoteImage
 | |
|         url={objectUrl}
 | |
|         i18n={i18n}
 | |
|         contentType={contentType}
 | |
|         handleImageErrorBound={handleImageErrorBound}
 | |
|       />
 | |
|     ) : (
 | |
|       <QuoteIcon icon="image" />
 | |
|     );
 | |
|   }
 | |
|   if (MIME.isAudio(contentType)) {
 | |
|     return <QuoteIcon icon="microphone" />;
 | |
|   }
 | |
|   return null;
 | |
| };
 | |
| 
 | |
| export const QuoteText = (props: any) => {
 | |
|   const { i18n, text, attachment, isIncoming, conversationType, convoId } = props;
 | |
|   const isGroup = conversationType === ConversationTypeEnum.GROUP;
 | |
| 
 | |
|   if (text) {
 | |
|     return (
 | |
|       <div
 | |
|         dir="auto"
 | |
|         className={classNames(
 | |
|           'module-quote__primary__text',
 | |
|           isIncoming ? 'module-quote__primary__text--incoming' : null
 | |
|         )}
 | |
|       >
 | |
|         <MessageBody
 | |
|           isGroup={isGroup}
 | |
|           convoId={convoId}
 | |
|           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;
 | |
| };
 | |
| 
 | |
| export const QuoteAuthor = (props: any) => {
 | |
|   const {
 | |
|     authorProfileName,
 | |
|     authorPhoneNumber,
 | |
|     authorName,
 | |
|     i18n,
 | |
|     isFromMe,
 | |
|     isIncoming,
 | |
|     isPublic,
 | |
|   } = props;
 | |
| 
 | |
|   return (
 | |
|     <div
 | |
|       className={classNames(
 | |
|         'module-quote__primary__author',
 | |
|         isIncoming ? 'module-quote__primary__author--incoming' : null
 | |
|       )}
 | |
|     >
 | |
|       {isFromMe ? (
 | |
|         i18n('you')
 | |
|       ) : (
 | |
|         <ContactName
 | |
|           phoneNumber={PubKey.shorten(authorPhoneNumber)}
 | |
|           name={authorName}
 | |
|           profileName={authorProfileName}
 | |
|           i18n={i18n}
 | |
|           compact={true}
 | |
|           shouldShowPubkey={Boolean(isPublic)}
 | |
|         />
 | |
|       )}
 | |
|     </div>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export const QuoteReferenceWarning = (props: any) => {
 | |
|   const { i18n, isIncoming, referencedMessageNotFound } = 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>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export const Quote = (props: QuoteProps) => {
 | |
|   const [imageBroken, setImageBroken] = useState(false);
 | |
| 
 | |
|   const handleImageErrorBound = null;
 | |
| 
 | |
|   const handleImageError = () => {
 | |
|     // tslint:disable-next-line no-console
 | |
|     console.log('Message: Image failed to load; failing over to placeholder');
 | |
|     setImageBroken(true);
 | |
|   };
 | |
| 
 | |
|   const { isIncoming, onClick, referencedMessageNotFound, withContentAbove } = props;
 | |
| 
 | |
|   if (!validateQuote(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',
 | |
|             !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">
 | |
|             <QuoteAuthor {...props} />
 | |
|             <QuoteGenericFile {...props} />
 | |
|             <QuoteText {...props} />
 | |
|           </div>
 | |
|           <QuoteIconContainer {...props} handleImageErrorBound={handleImageErrorBound} />
 | |
|         </div>
 | |
|         <QuoteReferenceWarning {...props} />
 | |
|       </div>
 | |
|     </>
 | |
|   );
 | |
| };
 |