// Copyright 2019 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import React, { memo, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import classNames from 'classnames'; import type { ReadonlyDeep } from 'type-fest'; import type { DraftBodyRanges, HydratedBodyRangesType, } from '../types/BodyRange.std.js'; import type { LocalizerType, ThemeType } from '../types/Util.std.js'; import type { ErrorDialogAudioRecorderType } from '../types/AudioRecorder.std.js'; import { RecordingState } from '../types/AudioRecorder.std.js'; import type { imageToBlurHash } from '../util/imageToBlurHash.dom.js'; import { dropNull } from '../util/dropNull.std.js'; import { Spinner } from './Spinner.dom.js'; import type { InputApi, Props as CompositionInputProps, } from './CompositionInput.dom.js'; import { CompositionInput } from './CompositionInput.dom.js'; import type { Props as MessageRequestActionsProps } from './conversation/MessageRequestActions.dom.js'; import { MessageRequestActions } from './conversation/MessageRequestActions.dom.js'; import type { PropsType as GroupV1DisabledActionsPropsType } from './conversation/GroupV1DisabledActions.dom.js'; import { GroupV1DisabledActions } from './conversation/GroupV1DisabledActions.dom.js'; import type { PropsType as GroupV2PendingApprovalActionsPropsType } from './conversation/GroupV2PendingApprovalActions.dom.js'; import { GroupV2PendingApprovalActions } from './conversation/GroupV2PendingApprovalActions.dom.js'; import { AnnouncementsOnlyGroupBanner } from './AnnouncementsOnlyGroupBanner.dom.js'; import { AttachmentList } from './conversation/AttachmentList.dom.js'; import type { AttachmentDraftType, InMemoryAttachmentDraftType, } from '../types/Attachment.std.js'; import { isImageAttachment, isVoiceMessage } from '../util/Attachment.std.js'; import { isViewOnceEligible } from '../util/viewOnceEligibility.std.js'; import type { AciString } from '../types/ServiceId.std.js'; import { AudioCapture } from './conversation/AudioCapture.dom.js'; import { CompositionUpload } from './CompositionUpload.dom.js'; import type { ConversationRemovalStage, ConversationType, PushPanelForConversationActionType, ShowConversationType, } from '../state/ducks/conversations.preload.js'; import type { GetConversationByIdType } from '../state/selectors/conversations.dom.js'; import type { GetSharedGroupNamesType } from '../util/sharedGroupNames.dom.js'; import type { LinkPreviewForUIType } from '../types/message/LinkPreviews.std.js'; import { isSameLinkPreview } from '../types/message/LinkPreviews.std.js'; import { MandatoryProfileSharingActions } from './conversation/MandatoryProfileSharingActions.dom.js'; import { MediaQualitySelector } from './MediaQualitySelector.dom.js'; import type { Props as QuoteProps } from './conversation/Quote.dom.js'; import { Quote } from './conversation/Quote.dom.js'; import { useAttachFileShortcut, useEditLastMessageSent, } from '../hooks/useKeyboardShortcuts.dom.js'; import { MediaEditor } from './MediaEditor.dom.js'; import { isImageTypeSupported } from '../util/GoogleChrome.std.js'; import * as KeyboardLayout from '../services/keyboardLayout.dom.js'; import { usePrevious } from '../hooks/usePrevious.std.js'; import { PanelType } from '../types/Panels.std.js'; import type { SmartCompositionRecordingDraftProps } from '../state/smart/CompositionRecordingDraft.preload.js'; import { useEscapeHandling } from '../hooks/useEscapeHandling.dom.js'; import SelectModeActions from './conversation/SelectModeActions.dom.js'; import type { ShowToastAction } from '../state/ducks/toast.preload.js'; import type { DraftEditMessageType } from '../model-types.d.ts'; import type { ForwardMessagesPayload } from '../state/ducks/globalModals.preload.js'; import { ForwardMessagesModalType } from './ForwardMessagesModal.dom.js'; import { SignalConversationMuteToggle } from './conversation/SignalConversationMuteToggle.dom.js'; import { FunPicker } from './fun/FunPicker.dom.js'; import type { FunEmojiSelection } from './fun/panels/FunPanelEmojis.dom.js'; import type { FunStickerSelection } from './fun/panels/FunPanelStickers.dom.js'; import type { FunGifSelection } from './fun/panels/FunPanelGifs.dom.js'; import type { SmartDraftGifMessageSendModalProps } from '../state/smart/DraftGifMessageSendModal.preload.js'; import { strictAssert } from '../util/assert.std.js'; import { ConfirmationDialog } from './ConfirmationDialog.dom.js'; import type { EmojiSkinTone } from './fun/data/emojis.std.js'; import { FunPickerButton } from './fun/FunButton.dom.js'; import { AxoDropdownMenu } from '../axo/AxoDropdownMenu.dom.js'; import { AxoIconButton } from '../axo/AxoIconButton.dom.js'; import { tw } from '../axo/tw.dom.js'; import type { PollCreateType } from '../types/Polls.dom.js'; import { PollCreateModal } from './PollCreateModal.dom.js'; import { useDocumentKeyDown } from '../hooks/useDocumentKeyDown.dom.js'; export type OwnProps = Readonly<{ acceptedMessageRequest: boolean | null; removalStage: ConversationRemovalStage | null; addAttachment: ( conversationId: string, attachment: InMemoryAttachmentDraftType ) => unknown; announcementsOnly: boolean | null; areWeAdmin: boolean | null; areWePending: boolean | null; areWePendingApproval: boolean | null; getSharedGroupNames: GetSharedGroupNamesType; cancelRecording: () => unknown; completeRecording: ( conversationId: string, onRecordingComplete: (rec: InMemoryAttachmentDraftType) => unknown ) => unknown; convertDraftBodyRangesIntoHydrated: ( bodyRanges: DraftBodyRanges | undefined ) => HydratedBodyRangesType | undefined; conversationId: string; conversationSelector: GetConversationByIdType; discardEditMessage: (id: string) => unknown; draftEditMessage: DraftEditMessageType | null; draftAttachments: ReadonlyArray; errorDialogAudioRecorderType: ErrorDialogAudioRecorderType | null; errorRecording: (e: ErrorDialogAudioRecorderType) => unknown; focusCounter: number; groupAdmins: Array<{ member: ConversationType; labelEmoji: string | undefined; labelString: string | undefined; }>; groupVersion: 1 | 2 | null; i18n: LocalizerType; imageToBlurHash: typeof imageToBlurHash; isDisabled: boolean; isFetchingUUID: boolean | null; isFormattingEnabled: boolean; isGroupV1AndDisabled: boolean | null; isMissingMandatoryProfileSharing: boolean | null; isPollSend1to1Enabled: boolean; isSignalConversation: boolean | null; isActive: boolean; lastEditableMessageId: string | null; recordingState: RecordingState; messageCompositionId: string; memberColors: Map; shouldHidePopovers: boolean | null; isMuted: boolean; isSmsOnlyOrUnregistered: boolean | null; left: boolean | null; linkPreviewLoading: boolean; linkPreviewResult: LinkPreviewForUIType | null; onClearAttachments(conversationId: string): unknown; onCloseLinkPreview(conversationId: string): unknown; platform: string; showToast: ShowToastAction; processAttachments: (options: { conversationId: string; files: ReadonlyArray; flags: number | null; }) => unknown; setMuteExpiration(conversationId: string, muteExpiresAt: number): unknown; setMediaQualitySetting(conversationId: string, isHQ: boolean): unknown; sendStickerMessage( id: string, opts: { packId: string; stickerId: number } ): unknown; sendEditedMessage( conversationId: string, options: { bodyRanges?: DraftBodyRanges; message?: string; quoteAuthorAci?: AciString; quoteSentAt?: number; targetMessageId: string; } ): unknown; sendMultiMediaMessage( conversationId: string, options: { draftAttachments?: ReadonlyArray; bodyRanges?: DraftBodyRanges; isViewOnce?: boolean; message?: string; timestamp?: number; voiceNoteAttachment?: InMemoryAttachmentDraftType; } ): unknown; sendPoll(conversationId: string, poll: PollCreateType): unknown; quotedMessageId: string | null; quotedMessageProps: null | ReadonlyDeep< Omit< QuoteProps, 'i18n' | 'onClick' | 'onClose' | 'withContentAbove' | 'isCompose' > >; quotedMessageAuthorAci: AciString | null; quotedMessageSentAt: number | null; removeAttachment: ( conversationId: string, attachment: AttachmentDraftType ) => unknown; scrollToMessage: (conversationId: string, messageId: string) => unknown; setComposerFocus: (conversationId: string) => unknown; setMessageToEdit(conversationId: string, messageId: string): unknown; setQuoteByMessageId( conversationId: string, messageId: string | undefined ): unknown; isViewOnce: boolean; setViewOnce(options: { conversationId: string; value: boolean; toastNotify: boolean; }): unknown; shouldSendHighQualityAttachments: boolean; showConversation: ShowConversationType; startRecording: (id: string) => unknown; theme: ThemeType; renderSmartCompositionRecording: () => React.JSX.Element; renderSmartCompositionRecordingDraft: ( props: SmartCompositionRecordingDraftProps ) => React.JSX.Element | null; selectedMessageIds: ReadonlyArray | undefined; areSelectedMessagesForwardable: boolean | undefined; toggleSelectMode: (on: boolean) => void; toggleForwardMessagesModal: ( payload: ForwardMessagesPayload, onForward: () => void ) => void; toggleDraftGifMessageSendModal: ( props: SmartDraftGifMessageSendModalProps | null ) => void; onSelectEmoji: (emojiSelection: FunEmojiSelection) => void; emojiSkinToneDefault: EmojiSkinTone | null; }>; export type Props = Pick< CompositionInputProps, | 'draftText' | 'draftBodyRanges' | 'getPreferredBadge' | 'onEditorStateChange' | 'onTextTooLong' | 'ourConversationId' | 'quotedMessageId' | 'sendCounter' | 'sortedGroupMembers' > & MessageRequestActionsProps & Pick & Pick & { pushPanelForConversation: PushPanelForConversationActionType; } & OwnProps; export const CompositionArea = memo(function CompositionArea({ // Base props addAttachment, conversationId, convertDraftBodyRangesIntoHydrated, discardEditMessage, draftEditMessage, focusCounter, i18n, imageToBlurHash, isDisabled, isPollSend1to1Enabled, isSignalConversation, isMuted, isActive, lastEditableMessageId, messageCompositionId, pushPanelForConversation, platform, processAttachments, removeAttachment, sendEditedMessage, sendMultiMediaMessage, sendPoll, setComposerFocus, setMessageToEdit, setQuoteByMessageId, shouldHidePopovers, showToast, theme, setMuteExpiration, // AttachmentList draftAttachments, onClearAttachments, // AudioCapture recordingState, startRecording, // StagedLinkPreview linkPreviewLoading, linkPreviewResult, onCloseLinkPreview, // Quote quotedMessageId, quotedMessageProps, quotedMessageAuthorAci, quotedMessageSentAt, scrollToMessage, // View Once isViewOnce, setViewOnce, // MediaQualitySelector setMediaQualitySetting, shouldSendHighQualityAttachments, // CompositionInput draftBodyRanges, draftText, getPreferredBadge, isFormattingEnabled, onEditorStateChange, onTextTooLong, ourConversationId, sendCounter, sortedGroupMembers, // FunPicker onSelectEmoji, emojiSkinToneDefault, sendStickerMessage, // Message Requests acceptedMessageRequest, areWePending, areWePendingApproval, conversationType, getSharedGroupNames, groupVersion, isBlocked, isHidden, isReported, isMissingMandatoryProfileSharing, left, removalStage, acceptConversation, blockConversation, reportSpam, blockAndReportSpam, deleteConversation, conversationName, addedByName, // GroupV1 Disabled Actions isGroupV1AndDisabled, showGV2MigrationDialog, // GroupV2 announcementsOnly, areWeAdmin, groupAdmins, memberColors, cancelJoinRequest, showConversation, // SMS-only contacts isSmsOnlyOrUnregistered, isFetchingUUID, renderSmartCompositionRecording, renderSmartCompositionRecordingDraft, // Selected messages selectedMessageIds, areSelectedMessagesForwardable, toggleSelectMode, toggleForwardMessagesModal, // DraftGifMessageSendModal toggleDraftGifMessageSendModal, }: Props): React.JSX.Element | null { const [dirty, setDirty] = useState(false); const [large, setLarge] = useState(false); const [attachmentToEdit, setAttachmentToEdit] = useState< AttachmentDraftType | undefined >(); const [isPollModalOpen, setIsPollModalOpen] = useState(false); const inputApiRef = useRef(); const fileInputRef = useRef(null); const photoVideoInputRef = useRef(null); const handleForceSend = useCallback(() => { setLarge(false); if (inputApiRef.current) { inputApiRef.current.submit(); } }, [inputApiRef, setLarge]); const draftEditMessageBody = draftEditMessage?.body; const editedMessageId = draftEditMessage?.targetMessageId; let canSend = // Text or link preview edited dirty || // Quote of edited message changed (draftEditMessage != null && dropNull(draftEditMessage.quote?.messageId) !== dropNull(quotedMessageId)) || // Link preview of edited message changed (draftEditMessage != null && !isSameLinkPreview(linkPreviewResult, draftEditMessage?.preview)) || // Not edit message, but has attachments (draftEditMessage == null && draftAttachments.length !== 0); // Draft attachments should finish loading if (draftAttachments.some(attachment => attachment.pending)) { canSend = false; } const handleSubmit = useCallback( ( message: string, bodyRanges: DraftBodyRanges, timestamp: number ): boolean => { if (!canSend) { return false; } if (editedMessageId) { sendEditedMessage(conversationId, { bodyRanges, message, // sent timestamp for the quote quoteSentAt: quotedMessageSentAt ?? undefined, quoteAuthorAci: quotedMessageAuthorAci ?? undefined, targetMessageId: editedMessageId, }); } else { sendMultiMediaMessage(conversationId, { draftAttachments, bodyRanges, message, timestamp, isViewOnce, }); } setLarge(false); return true; }, [ conversationId, canSend, draftAttachments, editedMessageId, isViewOnce, quotedMessageSentAt, quotedMessageAuthorAci, sendEditedMessage, sendMultiMediaMessage, setLarge, ] ); const launchAttachmentPicker = useCallback((type?: 'media' | 'file') => { const inputRef = type === 'media' ? photoVideoInputRef : fileInputRef; const fileInput = inputRef.current; if (fileInput) { // Setting the value to empty so that onChange always fires in case // you add multiple photos. fileInput.value = ''; fileInput.click(); } }, []); const launchMediaPicker = useCallback( () => launchAttachmentPicker('media'), [launchAttachmentPicker] ); const launchFilePicker = useCallback( () => launchAttachmentPicker('file'), [launchAttachmentPicker] ); const handleOpenPollModal = useCallback(() => { setIsPollModalOpen(true); }, []); const handleClosePollModal = useCallback(() => { setIsPollModalOpen(false); }, []); const handleSendPoll = useCallback( (poll: PollCreateType) => { sendPoll(conversationId, poll); handleClosePollModal(); }, [conversationId, sendPoll, handleClosePollModal] ); function maybeEditAttachment(attachment: AttachmentDraftType) { if (!isImageTypeSupported(attachment.contentType)) { return; } setAttachmentToEdit(attachment); } const isComposerEmpty = !draftAttachments.length && !draftText && !draftEditMessage; const maybeEditMessage = useCallback(() => { if (!isComposerEmpty || !lastEditableMessageId) { return false; } setMessageToEdit(conversationId, lastEditableMessageId); return true; }, [ conversationId, isComposerEmpty, lastEditableMessageId, setMessageToEdit, ]); const attachFileShortcut = useAttachFileShortcut(launchFilePicker); const editLastMessageSent = useEditLastMessageSent(maybeEditMessage); useDocumentKeyDown(event => { const hasFocus = inputApiRef.current?.hasFocus() ?? false; if (hasFocus) { attachFileShortcut(event); editLastMessageSent(event); } }); // Focus input on first mount const previousFocusCounter = usePrevious( focusCounter, focusCounter ); useEffect(() => { if (inputApiRef.current) { inputApiRef.current.focus(); } }, []); // Focus input whenever explicitly requested useEffect(() => { if (focusCounter !== previousFocusCounter && inputApiRef.current) { inputApiRef.current.focus(); } }, [inputApiRef, focusCounter, previousFocusCounter]); const previousMessageCompositionId = usePrevious( messageCompositionId, messageCompositionId ); const previousSendCounter = usePrevious(sendCounter, sendCounter); useEffect(() => { if (!inputApiRef.current) { return; } if ( previousMessageCompositionId !== messageCompositionId || previousSendCounter !== sendCounter ) { inputApiRef.current.reset(); } }, [ messageCompositionId, sendCounter, previousMessageCompositionId, previousSendCounter, ]); // We want to reset the state of Quill only if: // // - Our other device edits the message (edit history length would change) // - User begins editing another message. const editHistoryLength = draftEditMessage?.editHistoryLength; const hasEditHistoryChanged = usePrevious(editHistoryLength, editHistoryLength) !== editHistoryLength; const hasEditedMessageChanged = usePrevious(editedMessageId, editedMessageId) !== editedMessageId; const hasEditDraftChanged = hasEditHistoryChanged || hasEditedMessageChanged; useEffect(() => { if (!hasEditDraftChanged) { return; } inputApiRef.current?.setContents( draftEditMessageBody ?? '', draftBodyRanges ?? undefined, true ); }, [draftBodyRanges, draftEditMessageBody, hasEditDraftChanged]); const previousConversationId = usePrevious(conversationId, conversationId); useEffect(() => { if (conversationId === previousConversationId) { return; } if (!draftText) { inputApiRef.current?.setContents(''); return; } inputApiRef.current?.setContents( draftText, draftBodyRanges ?? undefined, true ); }, [conversationId, draftBodyRanges, draftText, previousConversationId]); const handleToggleLarge = useCallback(() => { setLarge(l => !l); }, [setLarge]); const shouldShowMicrophone = !large && isComposerEmpty; const showMediaQualitySelector = draftAttachments.some(isImageAttachment); const showViewOnceToggle = isViewOnceEligible( draftAttachments, Boolean(quotedMessageId) ); const isViewOnceActive = isViewOnce && showViewOnceToggle; let draftEditMessageForInput = draftEditMessage; let largeForInput = large; let linkPreviewLoadingForInput = linkPreviewLoading; let linkPreviewResultForInput = linkPreviewResult; let quotedMessageIdForInput = quotedMessageId; if (isViewOnceActive) { draftEditMessageForInput = null; largeForInput = false; linkPreviewLoadingForInput = false; linkPreviewResultForInput = null; quotedMessageIdForInput = null; } const [funPickerOpen, setFunPickerOpen] = useState(false); const handleToggleViewOnce = useCallback(() => { setFunPickerOpen(false); setViewOnce({ conversationId, value: !isViewOnce, toastNotify: true, }); }, [conversationId, isViewOnce, setViewOnce]); const handleFunPickerOpenChange = useCallback( (open: boolean) => { setFunPickerOpen(open); if (!open) { setComposerFocus(conversationId); } }, [conversationId, setComposerFocus] ); const handleFunPickerSelectEmoji = useCallback( (emojiSelection: FunEmojiSelection) => { if (inputApiRef.current) { inputApiRef.current.insertEmoji(emojiSelection); } }, [] ); const handleFunPickerSelectSticker = useCallback( (stickerSelection: FunStickerSelection) => { sendStickerMessage(conversationId, { packId: stickerSelection.stickerPackId, stickerId: stickerSelection.stickerId, }); }, [sendStickerMessage, conversationId] ); const [confirmGifSelection, setConfirmGifSelection] = useState(null); const handleFunPickerSelectGif = useCallback( async (gifSelection: FunGifSelection) => { if (draftAttachments.length > 0) { setConfirmGifSelection(gifSelection); } else { toggleDraftGifMessageSendModal({ conversationId, previousComposerDraftText: draftText ?? '', previousComposerDraftBodyRanges: draftBodyRanges ?? [], gifSelection, }); } }, [ conversationId, toggleDraftGifMessageSendModal, draftText, draftBodyRanges, draftAttachments, ] ); const handleConfirmGifSelection = useCallback(() => { strictAssert(confirmGifSelection != null, 'Need selected gif to confirm'); onClearAttachments(conversationId); toggleDraftGifMessageSendModal({ conversationId, previousComposerDraftText: draftText ?? '', previousComposerDraftBodyRanges: draftBodyRanges ?? [], gifSelection: confirmGifSelection, }); }, [ confirmGifSelection, conversationId, toggleDraftGifMessageSendModal, draftText, draftBodyRanges, onClearAttachments, ]); const handleCancelGifSelection = useCallback(() => { setConfirmGifSelection(null); }, []); const handleFunPickerAddStickerPack = useCallback(() => { pushPanelForConversation({ type: PanelType.StickerManager, }); }, [pushPanelForConversation]); const mediaQualitySelectorFragment = useMemo( () => showMediaQualitySelector ? (
) : null, [ conversationId, i18n, setMediaQualitySetting, shouldSendHighQualityAttachments, showMediaQualitySelector, ] ); const leftHandSideButtonsFragment = ( <> {confirmGifSelection && ( {i18n('icu:CompositionArea__ConfirmGifSelection__Body')} )}
{mediaQualitySelectorFragment} ); const micButtonFragment = shouldShowMicrophone ? (
) : null; const editMessageFragment = draftEditMessage ? ( <> {large &&
}
) : null; const isRecording = recordingState === RecordingState.Recording; const actionSlotClassName = tw( 'flex size-8 shrink-0 items-center justify-center' ); const composerAddMenuButton = draftEditMessage || linkPreviewResult || isRecording ? null : (
{i18n('icu:CompositionArea__AttachMenu__PhotosAndVideos')} {i18n('icu:CompositionArea__AttachMenu__File')} {(conversationType === 'group' || isPollSend1to1Enabled) && ( {i18n('icu:CompositionArea__AttachMenu__Poll')} )}
); const sendButtonFragment = !draftEditMessage ? ( <>
) : null; // Listen for cmd/ctrl-shift-x to toggle large composition mode useEffect(() => { const handler = (e: KeyboardEvent) => { const { shiftKey, ctrlKey, metaKey } = e; const key = KeyboardLayout.lookup(e); // When using the ctrl key, `key` is `'K'`. When using the cmd key, `key` is `'k'` const targetKey = key === 'k' || key === 'K'; const commandKey = platform === 'darwin' && metaKey; const controlKey = platform !== 'darwin' && ctrlKey; const commandOrCtrl = commandKey || controlKey; // cmd/ctrl-shift-k if (targetKey && shiftKey && commandOrCtrl) { e.preventDefault(); setLarge(x => !x); } }; document.addEventListener('keydown', handler); return () => { document.removeEventListener('keydown', handler); }; }, [platform, setLarge]); const handleEscape = useCallback(() => { if (linkPreviewResult) { onCloseLinkPreview(conversationId); } else if (quotedMessageId) { setQuoteByMessageId(conversationId, undefined); } else if (draftEditMessage) { discardEditMessage(conversationId); } }, [ conversationId, discardEditMessage, draftEditMessage, linkPreviewResult, onCloseLinkPreview, quotedMessageId, setQuoteByMessageId, ]); useEscapeHandling(handleEscape); if (isSignalConversation) { return ( ); } if (selectedMessageIds != null) { return ( { toggleSelectMode(false); }} onDeleteMessages={() => { window.reduxActions.globalModals.toggleDeleteMessagesModal({ conversationId, messageIds: selectedMessageIds, onDelete() { toggleSelectMode(false); }, }); }} onForwardMessages={() => { if (selectedMessageIds.length > 0) { toggleForwardMessagesModal( { type: ForwardMessagesModalType.Forward, messageIds: selectedMessageIds, }, () => { toggleSelectMode(false); } ); } }} showToast={showToast} /> ); } if ( isBlocked || areWePending || (!acceptedMessageRequest && removalStage !== 'justNotification') ) { return ( ); } if (conversationType === 'direct' && isSmsOnlyOrUnregistered) { return (
{isFetchingUUID ? ( ) : ( <>

{i18n('icu:CompositionArea--sms-only__title')}

{i18n('icu:CompositionArea--sms-only__body')}

)}
); } // If no message request, but we haven't shared profile yet, we show profile-sharing UI if ( !left && (conversationType === 'direct' || (conversationType === 'group' && groupVersion === 1)) && isMissingMandatoryProfileSharing ) { return ( ); } // If this is a V1 group, now disabled entirely, we show UI to help them upgrade if (!left && isGroupV1AndDisabled) { return ( ); } if (areWePendingApproval) { return ( ); } if (announcementsOnly && !areWeAdmin) { return ( ); } if (isRecording) { return renderSmartCompositionRecording(); } if (draftAttachments.length === 1 && isVoiceMessage(draftAttachments[0])) { const voiceNoteAttachment = draftAttachments[0]; if (!voiceNoteAttachment.pending && voiceNoteAttachment.url) { return renderSmartCompositionRecordingDraft({ voiceNoteAttachment }); } } return (
{attachmentToEdit && 'url' in attachmentToEdit && attachmentToEdit.url && ( setAttachmentToEdit(undefined)} onDone={({ caption, captionBodyRanges, data, contentType, blurHash, isViewOnce: editorIsViewOnce, isHighQuality: editorIsHighQuality, }) => { const newAttachment = { ...attachmentToEdit, contentType, blurHash, data, size: data.byteLength, }; addAttachment(conversationId, newAttachment); setAttachmentToEdit(undefined); if ( editorIsViewOnce !== undefined && editorIsViewOnce !== isViewOnce ) { setViewOnce({ conversationId, value: editorIsViewOnce, toastNotify: false, }); } if ( editorIsHighQuality !== undefined && editorIsHighQuality !== shouldSendHighQualityAttachments ) { setMediaQualitySetting(conversationId, editorIsHighQuality); } onEditorStateChange?.({ bodyRanges: captionBodyRanges ?? [], conversationId, messageText: caption ?? '', sendCounter, }); inputApiRef.current?.setContents( caption ?? '', convertDraftBodyRangesIntoHydrated(captionBodyRanges), true ); }} onSelectEmoji={onSelectEmoji} onTextTooLong={onTextTooLong} ourConversationId={ourConversationId} platform={platform} emojiSkinToneDefault={emojiSkinToneDefault} sortedGroupMembers={sortedGroupMembers} /> )} {isViewOnceActive ? null : (
)}
{isViewOnceActive ? null : quotedMessageProps && (
scrollToMessage(conversationId, quotedMessageId) : undefined } onClose={() => { setQuoteByMessageId(conversationId, undefined); }} />
)} {draftAttachments.length ? (
onClearAttachments(conversationId)} onCloseAttachment={attachment => { removeAttachment(conversationId, attachment); }} />
) : null}
{!large ? leftHandSideButtonsFragment : null}
{isViewOnceActive && (
)} {!isViewOnceActive && !large && ( <> {!dirty ? micButtonFragment : null} {editMessageFragment} {composerAddMenuButton} )}
{!isViewOnceActive && large ? (
{leftHandSideButtonsFragment} {composerAddMenuButton} {!dirty ? micButtonFragment : null} {editMessageFragment} {dirty || !shouldShowMicrophone ? sendButtonFragment : null}
) : null} {isPollModalOpen && ( )}
); });