151 lines
		
	
	
	
		
			5.4 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			151 lines
		
	
	
	
		
			5.4 KiB
			
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
// Copyright 2025 Signal Messenger, LLC
 | 
						|
// SPDX-License-Identifier: AGPL-3.0-only
 | 
						|
import React, { memo, useCallback, useEffect, useState } from 'react';
 | 
						|
import { useSelector } from 'react-redux';
 | 
						|
import { getIntl, getTheme } from '../selectors/user';
 | 
						|
import type { DraftGifMessageSendModalProps } from '../../components/DraftGifMessageSendModal';
 | 
						|
import { DraftGifMessageSendModal } from '../../components/DraftGifMessageSendModal';
 | 
						|
import { strictAssert } from '../../util/assert';
 | 
						|
import type { HydratedBodyRangesType } from '../../types/BodyRange';
 | 
						|
import { SmartCompositionTextArea } from './CompositionTextArea';
 | 
						|
import { getDraftGifMessageSendModalProps } from '../selectors/globalModals';
 | 
						|
import { useGlobalModalActions } from '../ducks/globalModals';
 | 
						|
import { useComposerActions } from '../ducks/composer';
 | 
						|
import type { FunGifSelection } from '../../components/fun/panels/FunPanelGifs';
 | 
						|
import { tenorDownload } from '../../components/fun/data/tenor';
 | 
						|
import { drop } from '../../util/drop';
 | 
						|
import { processAttachment } from '../../util/processAttachment';
 | 
						|
import { SignalService as Proto } from '../../protobuf';
 | 
						|
import { writeDraftAttachment } from '../../util/writeDraftAttachment';
 | 
						|
import type { AttachmentDraftType } from '../../types/Attachment';
 | 
						|
import { createLogger } from '../../logging/log';
 | 
						|
import * as Errors from '../../types/errors';
 | 
						|
import { type Loadable, LoadingState } from '../../util/loadable';
 | 
						|
import { isAbortError } from '../../util/isAbortError';
 | 
						|
 | 
						|
const log = createLogger('DraftGifMessageSendModal');
 | 
						|
 | 
						|
type ReadyAttachmentDraftType = AttachmentDraftType & { pending: false };
 | 
						|
 | 
						|
export type GifDownloadState = Loadable<{
 | 
						|
  file: File;
 | 
						|
  attachment: ReadyAttachmentDraftType;
 | 
						|
}>;
 | 
						|
 | 
						|
export type SmartDraftGifMessageSendModalProps = Readonly<{
 | 
						|
  conversationId: string;
 | 
						|
  previousComposerDraftText: string;
 | 
						|
  previousComposerDraftBodyRanges: HydratedBodyRangesType;
 | 
						|
  gifSelection: FunGifSelection;
 | 
						|
}>;
 | 
						|
 | 
						|
export const SmartDraftGifMessageSendModal = memo(
 | 
						|
  function SmartDraftGifMessageSendModal() {
 | 
						|
    const props = useSelector(getDraftGifMessageSendModalProps);
 | 
						|
    strictAssert(props != null, 'Missing props');
 | 
						|
    const { conversationId, gifSelection } = props;
 | 
						|
 | 
						|
    const i18n = useSelector(getIntl);
 | 
						|
    const theme = useSelector(getTheme);
 | 
						|
 | 
						|
    const { toggleDraftGifMessageSendModal } = useGlobalModalActions();
 | 
						|
    const { sendMultiMediaMessage } = useComposerActions();
 | 
						|
 | 
						|
    const [draftText, setDraftText] = useState(props.previousComposerDraftText);
 | 
						|
    const [draftBodyRanges, setDraftBodyRanges] = useState(
 | 
						|
      props.previousComposerDraftBodyRanges
 | 
						|
    );
 | 
						|
 | 
						|
    const [gifDownloadState, setGifDownloadState] = useState<GifDownloadState>({
 | 
						|
      loadingState: LoadingState.Loading,
 | 
						|
    });
 | 
						|
 | 
						|
    const handleChange: DraftGifMessageSendModalProps['onChange'] = useCallback(
 | 
						|
      (updatedDraftText, updatedBodyRanges) => {
 | 
						|
        setDraftText(updatedDraftText);
 | 
						|
        setDraftBodyRanges(updatedBodyRanges);
 | 
						|
      },
 | 
						|
      []
 | 
						|
    );
 | 
						|
 | 
						|
    const handleSubmit = useCallback(() => {
 | 
						|
      strictAssert(
 | 
						|
        gifDownloadState.loadingState === LoadingState.Loaded,
 | 
						|
        'Gif must be already downloaded'
 | 
						|
      );
 | 
						|
      const draftAttachment = gifDownloadState.value.attachment;
 | 
						|
      toggleDraftGifMessageSendModal(null);
 | 
						|
      sendMultiMediaMessage(conversationId, {
 | 
						|
        message: draftText,
 | 
						|
        bodyRanges: draftBodyRanges,
 | 
						|
        draftAttachments: [draftAttachment],
 | 
						|
        timestamp: Date.now(),
 | 
						|
      });
 | 
						|
    }, [
 | 
						|
      gifDownloadState,
 | 
						|
      draftText,
 | 
						|
      draftBodyRanges,
 | 
						|
      conversationId,
 | 
						|
      toggleDraftGifMessageSendModal,
 | 
						|
      sendMultiMediaMessage,
 | 
						|
    ]);
 | 
						|
 | 
						|
    const handleClose = useCallback(() => {
 | 
						|
      toggleDraftGifMessageSendModal(null);
 | 
						|
    }, [toggleDraftGifMessageSendModal]);
 | 
						|
 | 
						|
    const gifUrl = gifSelection.gif.attachmentMedia.url;
 | 
						|
 | 
						|
    useEffect(() => {
 | 
						|
      const controller = new AbortController();
 | 
						|
      async function download() {
 | 
						|
        setGifDownloadState({ loadingState: LoadingState.Loading });
 | 
						|
        try {
 | 
						|
          const bytes = await tenorDownload(gifUrl, controller.signal);
 | 
						|
          const file = new File([bytes], 'gif.mp4', {
 | 
						|
            type: 'video/mp4',
 | 
						|
          });
 | 
						|
          const inMemoryAttachment = await processAttachment(file, {
 | 
						|
            generateScreenshot: false,
 | 
						|
            flags: Proto.AttachmentPointer.Flags.GIF,
 | 
						|
          });
 | 
						|
          strictAssert(
 | 
						|
            inMemoryAttachment != null,
 | 
						|
            'Attachment should not be null'
 | 
						|
          );
 | 
						|
          const attachment = await writeDraftAttachment(inMemoryAttachment);
 | 
						|
          strictAssert(!attachment.pending, 'Attachment should not be pending');
 | 
						|
          setGifDownloadState({
 | 
						|
            loadingState: LoadingState.Loaded,
 | 
						|
            value: { file, attachment },
 | 
						|
          });
 | 
						|
        } catch (error) {
 | 
						|
          if (isAbortError(error)) {
 | 
						|
            return;
 | 
						|
          }
 | 
						|
          log.error('Error while downloading gif', Errors.toLogFormat(error));
 | 
						|
          setGifDownloadState({ loadingState: LoadingState.LoadFailed, error });
 | 
						|
        }
 | 
						|
      }
 | 
						|
      drop(download());
 | 
						|
      return () => {
 | 
						|
        controller.abort();
 | 
						|
      };
 | 
						|
    }, [gifUrl]);
 | 
						|
 | 
						|
    return (
 | 
						|
      <DraftGifMessageSendModal
 | 
						|
        i18n={i18n}
 | 
						|
        RenderCompositionTextArea={SmartCompositionTextArea}
 | 
						|
        draftText={draftText ?? ''}
 | 
						|
        draftBodyRanges={draftBodyRanges}
 | 
						|
        gifSelection={gifSelection}
 | 
						|
        gifDownloadState={gifDownloadState}
 | 
						|
        theme={theme}
 | 
						|
        onChange={handleChange}
 | 
						|
        onSubmit={handleSubmit}
 | 
						|
        onClose={handleClose}
 | 
						|
      />
 | 
						|
    );
 | 
						|
  }
 | 
						|
);
 |