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.
		
		
		
		
		
			
		
			
				
	
	
		
			635 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			635 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			TypeScript
		
	
| import _ from 'lodash';
 | |
| 
 | |
| import autoBind from 'auto-bind';
 | |
| import { blobToArrayBuffer } from 'blob-util';
 | |
| import loadImage from 'blueimp-load-image';
 | |
| import classNames from 'classnames';
 | |
| import { Component, RefObject, createRef } from 'react';
 | |
| import styled from 'styled-components';
 | |
| import {
 | |
|   CompositionBox,
 | |
|   SendMessageType,
 | |
|   StagedAttachmentType,
 | |
| } from './composition/CompositionBox';
 | |
| 
 | |
| import { perfEnd, perfStart } from '../../session/utils/Performance';
 | |
| 
 | |
| import { SessionMessagesListContainer } from './SessionMessagesListContainer';
 | |
| 
 | |
| import { SessionFileDropzone } from './SessionFileDropzone';
 | |
| 
 | |
| import { Data } from '../../data/data';
 | |
| import { markAllReadByConvoId } from '../../interactions/conversationInteractions';
 | |
| import { MAX_ATTACHMENT_FILESIZE_BYTES } from '../../session/constants';
 | |
| import { getConversationController } from '../../session/conversations';
 | |
| import { ToastUtils } from '../../session/utils';
 | |
| import {
 | |
|   ReduxConversationType,
 | |
|   SortedMessageModelProps,
 | |
|   openConversationToSpecificMessage,
 | |
|   quoteMessage,
 | |
|   resetSelectedMessageIds,
 | |
|   updateMentionsMembers,
 | |
| } from '../../state/ducks/conversations';
 | |
| import { updateConfirmModal } from '../../state/ducks/modalDialog';
 | |
| import { addStagedAttachmentsInConversation } from '../../state/ducks/stagedAttachments';
 | |
| import { MIME } from '../../types';
 | |
| import {
 | |
|   THUMBNAIL_CONTENT_TYPE,
 | |
|   getAudioDuration,
 | |
|   getVideoDuration,
 | |
|   makeImageThumbnailBuffer,
 | |
|   makeVideoScreenshot,
 | |
| } from '../../types/attachments/VisualAttachment';
 | |
| import { AttachmentUtil, GoogleChrome, arrayBufferToObjectURL } from '../../util';
 | |
| import { getCurrentRecoveryPhrase } from '../../util/storage';
 | |
| import { EmptyMessageView } from '../EmptyMessageView';
 | |
| import { SplitViewContainer } from '../SplitViewContainer';
 | |
| import { SessionButtonColor } from '../basic/SessionButton';
 | |
| import { InConversationCallContainer } from '../calling/InConversationCallContainer';
 | |
| import { NoMessageInConversation } from './SubtleNotification';
 | |
| import { ConversationHeaderWithDetails } from './header/ConversationHeader';
 | |
| 
 | |
| import { isAudio } from '../../types/MIME';
 | |
| import { NoticeBanner } from '../NoticeBanner';
 | |
| import { SessionSpinner } from '../loading';
 | |
| import { RightPanel, StyledRightPanelContainer } from './right-panel/RightPanel';
 | |
| import { HTMLDirection } from '../../util/i18n/rtlSupport';
 | |
| 
 | |
| const DEFAULT_JPEG_QUALITY = 0.85;
 | |
| 
 | |
| interface State {
 | |
|   isDraggingFile: boolean;
 | |
| }
 | |
| 
 | |
| interface Props {
 | |
|   ourDisplayNameInProfile: string;
 | |
|   ourNumber: string;
 | |
|   selectedConversationKey: string;
 | |
|   selectedConversation?: ReduxConversationType;
 | |
|   messagesProps: Array<SortedMessageModelProps>;
 | |
|   selectedMessages: Array<string>;
 | |
|   isRightPanelShowing: boolean;
 | |
|   hasOngoingCallWithFocusedConvo: boolean;
 | |
|   htmlDirection: HTMLDirection;
 | |
| 
 | |
|   stagedAttachments: Array<StagedAttachmentType>;
 | |
|   isSelectedConvoInitialLoadingInProgress: boolean;
 | |
| }
 | |
| 
 | |
| const StyledSpinnerContainer = styled.div`
 | |
|   display: flex;
 | |
|   justify-content: center;
 | |
|   width: 100%;
 | |
|   height: 100%;
 | |
|   align-items: center;
 | |
| `;
 | |
| 
 | |
| const ConvoLoadingSpinner = () => {
 | |
|   return (
 | |
|     <StyledSpinnerContainer>
 | |
|       <SessionSpinner loading={true} />
 | |
|     </StyledSpinnerContainer>
 | |
|   );
 | |
| };
 | |
| 
 | |
| export class SessionConversation extends Component<Props, State> {
 | |
|   private readonly messageContainerRef: RefObject<HTMLDivElement>;
 | |
|   private dragCounter: number;
 | |
|   private publicMembersRefreshTimeout?: NodeJS.Timeout;
 | |
|   private readonly updateMemberList: () => any;
 | |
| 
 | |
|   constructor(props: any) {
 | |
|     super(props);
 | |
| 
 | |
|     this.state = {
 | |
|       isDraggingFile: false,
 | |
|     };
 | |
|     this.messageContainerRef = createRef();
 | |
|     this.dragCounter = 0;
 | |
|     this.updateMemberList = _.debounce(this.updateMemberListBouncy.bind(this), 10000);
 | |
| 
 | |
|     autoBind(this);
 | |
|   }
 | |
| 
 | |
|   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
|   // ~~~~~~~~~~~~~~~~ LIFECYCLES ~~~~~~~~~~~~~~~~
 | |
|   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
| 
 | |
|   public componentDidUpdate(prevProps: Props, _prevState: State) {
 | |
|     const { selectedConversationKey: newConversationKey, selectedConversation: newConversation } =
 | |
|       this.props;
 | |
|     const { selectedConversationKey: oldConversationKey } = prevProps;
 | |
| 
 | |
|     // if the convo is valid, and it changed, register for drag events
 | |
|     if (newConversationKey && newConversation && newConversationKey !== oldConversationKey) {
 | |
|       // Pause thread to wait for rendering to complete
 | |
|       setTimeout(() => {
 | |
|         const div = this.messageContainerRef.current;
 | |
|         div?.addEventListener('dragenter', this.handleDragIn);
 | |
|         div?.addEventListener('dragleave', this.handleDragOut);
 | |
|         div?.addEventListener('dragover', this.handleDrag);
 | |
|         div?.addEventListener('drop', this.handleDrop);
 | |
|       }, 100);
 | |
| 
 | |
|       // if the conversation changed, we have to stop our refresh of member list
 | |
|       if (this.publicMembersRefreshTimeout) {
 | |
|         global.clearInterval(this.publicMembersRefreshTimeout);
 | |
|         this.publicMembersRefreshTimeout = undefined;
 | |
|       }
 | |
|       // if the newConversation changed, and is public, start our refresh members list
 | |
|       if (newConversation.isPublic) {
 | |
|         // this is a debounced call.
 | |
|         void this.updateMemberListBouncy();
 | |
|         // run this only once every minute if we don't change the visible conversation.
 | |
|         // this is a heavy operation (like a few thousands members can be here)
 | |
|         this.publicMembersRefreshTimeout = global.setInterval(this.updateMemberList, 60000);
 | |
|       }
 | |
|     }
 | |
|     // if we do not have a model, unregister for events
 | |
|     if (!newConversation) {
 | |
|       const div = this.messageContainerRef.current;
 | |
|       div?.removeEventListener('dragenter', this.handleDragIn);
 | |
|       div?.removeEventListener('dragleave', this.handleDragOut);
 | |
|       div?.removeEventListener('dragover', this.handleDrag);
 | |
|       div?.removeEventListener('drop', this.handleDrop);
 | |
|       if (this.publicMembersRefreshTimeout) {
 | |
|         global.clearInterval(this.publicMembersRefreshTimeout);
 | |
|         this.publicMembersRefreshTimeout = undefined;
 | |
|       }
 | |
|     }
 | |
|     if (newConversationKey !== oldConversationKey) {
 | |
|       this.setState({
 | |
|         isDraggingFile: false,
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   public componentWillUnmount() {
 | |
|     const div = this.messageContainerRef.current;
 | |
|     div?.removeEventListener('dragenter', this.handleDragIn);
 | |
|     div?.removeEventListener('dragleave', this.handleDragOut);
 | |
|     div?.removeEventListener('dragover', this.handleDrag);
 | |
|     div?.removeEventListener('drop', this.handleDrop);
 | |
| 
 | |
|     if (this.publicMembersRefreshTimeout) {
 | |
|       global.clearInterval(this.publicMembersRefreshTimeout);
 | |
|       this.publicMembersRefreshTimeout = undefined;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   public sendMessageFn(msg: SendMessageType) {
 | |
|     const { selectedConversationKey } = this.props;
 | |
|     const conversationModel = getConversationController().get(selectedConversationKey);
 | |
| 
 | |
|     if (!conversationModel) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const sendAndScroll = async () => {
 | |
|       // this needs to be awaited otherwise, the scrollToNow won't find the new message in the db.
 | |
|       // and this make the showScrollButton to be visible (even if we just scrolled to now)
 | |
|       await conversationModel.sendMessage(msg);
 | |
|       await this.scrollToNow();
 | |
|     };
 | |
| 
 | |
|     const recoveryPhrase = getCurrentRecoveryPhrase();
 | |
| 
 | |
|     // string replace to fix case where pasted text contains invis characters causing false negatives
 | |
|     if (msg.body.replace(/\s/g, '').includes(recoveryPhrase.replace(/\s/g, ''))) {
 | |
|       window.inboxStore?.dispatch(
 | |
|         updateConfirmModal({
 | |
|           title: window.i18n('warning'),
 | |
|           i18nMessage: { token: 'recoveryPasswordWarningSendDescription' },
 | |
|           okTheme: SessionButtonColor.Danger,
 | |
|           onClickOk: () => {
 | |
|             void sendAndScroll();
 | |
|           },
 | |
|           onClickClose: () => {
 | |
|             window.inboxStore?.dispatch(updateConfirmModal(null));
 | |
|           },
 | |
|         })
 | |
|       );
 | |
|     } else {
 | |
|       void sendAndScroll();
 | |
|     }
 | |
| 
 | |
|     window.inboxStore?.dispatch(quoteMessage(undefined));
 | |
|   }
 | |
| 
 | |
|   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
|   // ~~~~~~~~~~~~~~ RENDER METHODS ~~~~~~~~~~~~~~
 | |
|   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
|   public render() {
 | |
|     const { isDraggingFile } = this.state;
 | |
| 
 | |
|     const {
 | |
|       ourDisplayNameInProfile,
 | |
|       selectedConversation,
 | |
|       messagesProps,
 | |
|       selectedMessages,
 | |
|       isRightPanelShowing,
 | |
|       isSelectedConvoInitialLoadingInProgress,
 | |
|     } = this.props;
 | |
| 
 | |
|     if (!selectedConversation || !messagesProps) {
 | |
|       return <EmptyMessageView />;
 | |
|     }
 | |
|     // TODOLATER break selectionMode into it's own container component so we can use hooks to fetch relevant state from the store
 | |
|     const selectionMode = selectedMessages.length > 0;
 | |
| 
 | |
|     const bannerText =
 | |
|       selectedConversation.hasOutdatedClient &&
 | |
|       selectedConversation.hasOutdatedClient !== ourDisplayNameInProfile
 | |
|         ? window.i18n('disappearingMessagesLegacy', {
 | |
|             name: selectedConversation.hasOutdatedClient, // we store this guy's display name in here.
 | |
|           })
 | |
|         : window.i18n('deleteAfterGroupFirstReleaseConfigOutdated');
 | |
| 
 | |
|     return (
 | |
|       <>
 | |
|         <div className="conversation-header">
 | |
|           <ConversationHeaderWithDetails />
 | |
|           {selectedConversation?.hasOutdatedClient?.length ? (
 | |
|             <NoticeBanner
 | |
|               text={bannerText}
 | |
|               dismissCallback={() => {
 | |
|                 const conversation = getConversationController().get(selectedConversation.id);
 | |
|                 conversation.set({ hasOutdatedClient: undefined });
 | |
|                 void conversation.commit();
 | |
|               }}
 | |
|             />
 | |
|           ) : null}
 | |
|         </div>
 | |
|         {isSelectedConvoInitialLoadingInProgress ? (
 | |
|           <ConvoLoadingSpinner />
 | |
|         ) : (
 | |
|           <>
 | |
|             <div
 | |
|               // if you change the classname, also update it on onKeyDown
 | |
|               className={classNames('conversation-content', selectionMode && 'selection-mode')}
 | |
|               tabIndex={0}
 | |
|               onKeyDown={this.onKeyDown}
 | |
|               role="navigation"
 | |
|             >
 | |
|               <div className="conversation-messages">
 | |
|                 <NoMessageInConversation />
 | |
| 
 | |
|                 <SplitViewContainer
 | |
|                   top={<InConversationCallContainer />}
 | |
|                   bottom={
 | |
|                     <SessionMessagesListContainer
 | |
|                       messageContainerRef={this.messageContainerRef}
 | |
|                       scrollToNow={this.scrollToNow}
 | |
|                     />
 | |
|                   }
 | |
|                   disableTop={!this.props.hasOngoingCallWithFocusedConvo}
 | |
|                 />
 | |
|                 {isDraggingFile && <SessionFileDropzone />}
 | |
|               </div>
 | |
| 
 | |
|               <CompositionBox
 | |
|                 sendMessage={this.sendMessageFn}
 | |
|                 stagedAttachments={this.props.stagedAttachments}
 | |
|                 // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | |
|                 onChoseAttachments={this.onChoseAttachments}
 | |
|                 htmlDirection={this.props.htmlDirection}
 | |
|               />
 | |
|             </div>
 | |
|             <StyledRightPanelContainer className={classNames(isRightPanelShowing && 'show')}>
 | |
|               <RightPanel />
 | |
|             </StyledRightPanelContainer>
 | |
|           </>
 | |
|         )}
 | |
|       </>
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   private async scrollToNow() {
 | |
|     const conversationKey = this.props.selectedConversationKey;
 | |
|     if (!conversationKey) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     await markAllReadByConvoId(conversationKey);
 | |
|     const mostRecentMessage = await Data.getLastMessageInConversation(conversationKey);
 | |
| 
 | |
|     if (mostRecentMessage) {
 | |
|       await openConversationToSpecificMessage({
 | |
|         conversationKey,
 | |
|         messageIdToNavigateTo: mostRecentMessage.id,
 | |
|         shouldHighlightMessage: false,
 | |
|       });
 | |
|       const messageContainer = this.messageContainerRef.current;
 | |
|       if (!messageContainer) {
 | |
|         return;
 | |
|       }
 | |
|       messageContainer.scrollTop = messageContainer.scrollHeight - messageContainer.clientHeight;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
|   // ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~
 | |
|   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 | |
|   private onKeyDown(event: any) {
 | |
|     const selectionMode = !!this.props.selectedMessages.length;
 | |
| 
 | |
|     if (event.target.classList.contains('conversation-content')) {
 | |
|       switch (event.key) {
 | |
|         case 'Escape':
 | |
|           if (selectionMode) {
 | |
|             window.inboxStore?.dispatch(resetSelectedMessageIds());
 | |
|           }
 | |
|           break;
 | |
|         default:
 | |
|           break;
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private async onChoseAttachments(attachmentsFileList: Array<File>) {
 | |
|     if (!attachmentsFileList || attachmentsFileList.length === 0) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     for (let i = 0; i < attachmentsFileList.length; i++) {
 | |
|       // eslint-disable-next-line no-await-in-loop
 | |
|       await this.maybeAddAttachment(attachmentsFileList[i]);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private async maybeAddAttachment(file: any) {
 | |
|     if (!file) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const fileName = file.name;
 | |
|     const contentType = file.type;
 | |
| 
 | |
|     const { stagedAttachments } = this.props;
 | |
| 
 | |
|     if (stagedAttachments.length >= 32) {
 | |
|       ToastUtils.pushMaximumAttachmentsError();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     const haveNonImage = _.some(
 | |
|       stagedAttachments,
 | |
|       attachment => !MIME.isImage(attachment.contentType)
 | |
|     );
 | |
|     // You can't add another attachment if you already have a non-image staged
 | |
|     if (haveNonImage) {
 | |
|       ToastUtils.pushMultipleNonImageError();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // You can't add a non-image attachment if you already have attachments staged
 | |
|     if (!MIME.isImage(contentType) && stagedAttachments.length > 0) {
 | |
|       ToastUtils.pushCannotMixError();
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     let blob = null;
 | |
| 
 | |
|     try {
 | |
|       blob = await AttachmentUtil.autoScale({
 | |
|         contentType,
 | |
|         blob: file,
 | |
|       });
 | |
| 
 | |
|       if (blob.blob.size > MAX_ATTACHMENT_FILESIZE_BYTES) {
 | |
|         ToastUtils.pushFileSizeErrorAsByte();
 | |
|         return;
 | |
|       }
 | |
|     } catch (error) {
 | |
|       window?.log?.error(
 | |
|         'Error ensuring that image is properly sized:',
 | |
|         error && error.stack ? error.stack : error
 | |
|       );
 | |
| 
 | |
|       ToastUtils.pushLoadAttachmentFailure(error?.message);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|       if (GoogleChrome.isImageTypeSupported(contentType)) {
 | |
|         // this does not add the preview to the message outgoing
 | |
|         // this is just for us, for the list of attachments we are sending
 | |
|         // the files are scaled down under getFiles()
 | |
| 
 | |
|         const attachmentWithPreview = await renderImagePreview(contentType, file, fileName);
 | |
|         this.addAttachments([attachmentWithPreview]);
 | |
|       } else if (GoogleChrome.isVideoTypeSupported(contentType)) {
 | |
|         const attachmentWithVideoPreview = await renderVideoPreview(contentType, file, fileName);
 | |
|         this.addAttachments([attachmentWithVideoPreview]);
 | |
|       } else {
 | |
|         const attachment: StagedAttachmentType = {
 | |
|           file,
 | |
|           size: file.size,
 | |
|           contentType,
 | |
|           fileName,
 | |
|           url: '',
 | |
|           isVoiceMessage: false,
 | |
|           fileSize: null,
 | |
|           screenshot: null,
 | |
|           thumbnail: null,
 | |
|         };
 | |
| 
 | |
|         if (isAudio(contentType)) {
 | |
|           const objectUrl = URL.createObjectURL(file);
 | |
| 
 | |
|           try {
 | |
|             const duration = await getAudioDuration({ objectUrl, contentType });
 | |
|             attachment.duration = duration;
 | |
|           } finally {
 | |
|             URL.revokeObjectURL(objectUrl);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         this.addAttachments([attachment]);
 | |
|       }
 | |
|     } catch (e) {
 | |
|       window?.log?.error(
 | |
|         `Was unable to generate thumbnail for file type ${contentType}`,
 | |
|         e && e.stack ? e.stack : e
 | |
|       );
 | |
|       this.addAttachments([
 | |
|         {
 | |
|           file,
 | |
|           size: file.size,
 | |
|           contentType,
 | |
|           fileName,
 | |
|           isVoiceMessage: false,
 | |
|           url: '',
 | |
|           fileSize: null,
 | |
|           screenshot: null,
 | |
|           thumbnail: null,
 | |
|         },
 | |
|       ]);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private addAttachments(newAttachments: Array<StagedAttachmentType>) {
 | |
|     window.inboxStore?.dispatch(
 | |
|       addStagedAttachmentsInConversation({
 | |
|         conversationKey: this.props.selectedConversationKey,
 | |
|         newAttachments,
 | |
|       })
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   private handleDrag(e: any) {
 | |
|     e.preventDefault();
 | |
|     e.stopPropagation();
 | |
|   }
 | |
| 
 | |
|   private handleDragIn(e: any) {
 | |
|     e.preventDefault();
 | |
|     e.stopPropagation();
 | |
|     this.dragCounter++;
 | |
|     if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
 | |
|       this.setState({ isDraggingFile: true });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private handleDragOut(e: any) {
 | |
|     e.preventDefault();
 | |
|     e.stopPropagation();
 | |
|     this.dragCounter--;
 | |
| 
 | |
|     if (this.dragCounter === 0) {
 | |
|       this.setState({ isDraggingFile: false });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private handleDrop(e: DragEvent) {
 | |
|     e.preventDefault();
 | |
|     e.stopPropagation();
 | |
| 
 | |
|     if (e?.dataTransfer?.files && e.dataTransfer.files.length > 0) {
 | |
|       void this.onChoseAttachments(Array.from(e.dataTransfer.files));
 | |
|       e.dataTransfer.clearData();
 | |
|       this.dragCounter = 0;
 | |
|       this.setState({ isDraggingFile: false });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private async updateMemberListBouncy() {
 | |
|     const start = Date.now();
 | |
|     const allPubKeys = await Data.getPubkeysInPublicConversation(
 | |
|       this.props.selectedConversationKey
 | |
|     );
 | |
| 
 | |
|     window?.log?.debug(
 | |
|       `[perf] getPubkeysInPublicConversation returned '${
 | |
|         allPubKeys?.length
 | |
|       }' members in ${Date.now() - start}ms`
 | |
|     );
 | |
| 
 | |
|     const allMembers = allPubKeys.map((pubKey: string) => {
 | |
|       const conv = getConversationController().get(pubKey);
 | |
|       const profileName = conv?.getNicknameOrRealUsernameOrPlaceholder();
 | |
| 
 | |
|       return {
 | |
|         id: pubKey,
 | |
|         authorProfileName: profileName,
 | |
|       };
 | |
|     });
 | |
| 
 | |
|     window.inboxStore?.dispatch(updateMentionsMembers(allMembers));
 | |
|   }
 | |
| }
 | |
| 
 | |
| const renderVideoPreview = async (contentType: string, file: File, fileName: string) => {
 | |
|   const objectUrl = URL.createObjectURL(file);
 | |
|   try {
 | |
|     const type = THUMBNAIL_CONTENT_TYPE;
 | |
| 
 | |
|     const thumbnail = await makeVideoScreenshot({
 | |
|       objectUrl,
 | |
|       contentType: type,
 | |
|     });
 | |
|     const duration = await getVideoDuration({
 | |
|       objectUrl,
 | |
|       contentType: type,
 | |
|     });
 | |
|     const data = await blobToArrayBuffer(thumbnail);
 | |
|     const url = arrayBufferToObjectURL({
 | |
|       data,
 | |
|       type,
 | |
|     });
 | |
|     return {
 | |
|       file,
 | |
|       size: file.size,
 | |
|       fileName,
 | |
|       contentType,
 | |
|       duration,
 | |
|       videoUrl: objectUrl,
 | |
|       url,
 | |
|       isVoiceMessage: false,
 | |
|       fileSize: null,
 | |
|       screenshot: null,
 | |
|       thumbnail: null,
 | |
|     };
 | |
|   } catch (error) {
 | |
|     URL.revokeObjectURL(objectUrl);
 | |
|     throw error;
 | |
|   }
 | |
| };
 | |
| 
 | |
| const autoOrientJpegImage = async (fileOrBlobOrURL: File): Promise<string> => {
 | |
|   perfStart('autoOrientJpegImage');
 | |
|   const loadedImage = await loadImage(fileOrBlobOrURL, { orientation: true, canvas: true });
 | |
|   perfEnd('autoOrientJpegImage', 'autoOrientJpegImage');
 | |
|   const dataURL = (loadedImage.image as HTMLCanvasElement).toDataURL(
 | |
|     MIME.IMAGE_JPEG,
 | |
|     DEFAULT_JPEG_QUALITY
 | |
|   );
 | |
| 
 | |
|   return dataURL;
 | |
| };
 | |
| 
 | |
| const renderImagePreview = async (contentType: string, file: File, fileName: string) => {
 | |
|   if (!MIME.isJPEG(contentType)) {
 | |
|     const urlImage = URL.createObjectURL(file);
 | |
|     if (!urlImage) {
 | |
|       throw new Error('Failed to create object url for image!');
 | |
|     }
 | |
|     return {
 | |
|       file,
 | |
|       size: file.size,
 | |
|       fileName,
 | |
|       contentType,
 | |
|       url: urlImage,
 | |
|       isVoiceMessage: false,
 | |
|       fileSize: null,
 | |
|       screenshot: null,
 | |
|       thumbnail: null,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   // orient the image correctly based on the EXIF data, if needed
 | |
|   const orientedImageUrl = await autoOrientJpegImage(file);
 | |
| 
 | |
|   const thumbnailBuffer = await makeImageThumbnailBuffer({
 | |
|     objectUrl: orientedImageUrl,
 | |
|     contentType,
 | |
|   });
 | |
|   const url = arrayBufferToObjectURL({
 | |
|     data: thumbnailBuffer,
 | |
|     type: THUMBNAIL_CONTENT_TYPE,
 | |
|   });
 | |
| 
 | |
|   return {
 | |
|     file,
 | |
|     size: file.size,
 | |
|     fileName,
 | |
|     contentType,
 | |
|     url,
 | |
|     isVoiceMessage: false,
 | |
|     fileSize: null,
 | |
|     screenshot: null,
 | |
|     thumbnail: null,
 | |
|   };
 | |
| };
 |