Signal-Desktop/ts/state/smart/DraftGifMessageSendModal.tsx

152 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}
/>
);
}
);