diff --git a/ts/components/CompositionArea.stories.tsx b/ts/components/CompositionArea.stories.tsx index 69bcbb452..55acd9a8c 100644 --- a/ts/components/CompositionArea.stories.tsx +++ b/ts/components/CompositionArea.stories.tsx @@ -35,7 +35,9 @@ const useProps = (overrideProps: Partial = {}): Props => ({ addAttachment: action('addAttachment'), conversationId: '123', i18n, - onSendMessage: action('onSendMessage'), + isDisabled: false, + messageCompositionId: '456', + sendMultiMediaMessage: action('sendMultiMediaMessage'), processAttachments: action('processAttachments'), removeAttachment: action('removeAttachment'), theme: React.useContext(StorybookThemeContext), @@ -89,7 +91,7 @@ const useProps = (overrideProps: Partial = {}): Props => ({ recentStickers: [], clearInstalledStickerPack: action('clearInstalledStickerPack'), onClickAddPack: action('onClickAddPack'), - onPickSticker: action('onPickSticker'), + sendStickerMessage: action('sendStickerMessage'), clearShowIntroduction: action('clearShowIntroduction'), showPickerHint: false, clearShowPickerHint: action('clearShowPickerHint'), diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index fcc51c67b..7607cea34 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -63,8 +63,6 @@ export type CompositionAPIType = | { focusInput: () => void; isDirty: () => boolean; - setDisabled: (disabled: boolean) => void; - reset: InputApi['reset']; resetEmojiResults: InputApi['resetEmojiResults']; } | undefined; @@ -94,11 +92,13 @@ export type OwnProps = Readonly<{ groupVersion?: 1 | 2; i18n: LocalizerType; imageToBlurHash: typeof imageToBlurHash; + isDisabled: boolean; isFetchingUUID?: boolean; isGroupV1AndDisabled?: boolean; isMissingMandatoryProfileSharing?: boolean; isSignalConversation?: boolean; recordingState: RecordingState; + messageCompositionId: string; isSMSOnly?: boolean; left?: boolean; linkPreviewLoading: boolean; @@ -112,13 +112,20 @@ export type OwnProps = Readonly<{ files: ReadonlyArray; }) => unknown; onSelectMediaQuality(isHQ: boolean): unknown; - onSendMessage(options: { - draftAttachments?: ReadonlyArray; - mentions?: DraftBodyRangesType; - message?: string; - timestamp?: number; - voiceNoteAttachment?: InMemoryAttachmentDraftType; - }): unknown; + sendStickerMessage( + id: string, + opts: { packId: string; stickerId: number } + ): unknown; + sendMultiMediaMessage( + conversationId: string, + options: { + draftAttachments?: ReadonlyArray; + mentions?: DraftBodyRangesType; + message?: string; + timestamp?: number; + voiceNoteAttachment?: InMemoryAttachmentDraftType; + } + ): unknown; openConversation(conversationId: string): unknown; quotedMessageProps?: Omit< QuoteProps, @@ -156,7 +163,6 @@ export type Props = Pick< | 'recentStickers' | 'clearInstalledStickerPack' | 'onClickAddPack' - | 'onPickSticker' | 'clearShowIntroduction' | 'showPickerHint' | 'clearShowPickerHint' @@ -171,12 +177,14 @@ export function CompositionArea({ addAttachment, conversationId, i18n, - onSendMessage, imageToBlurHash, + isDisabled, + isSignalConversation, processAttachments, removeAttachment, + messageCompositionId, + sendMultiMediaMessage, theme, - isSignalConversation, // AttachmentList draftAttachments, @@ -223,7 +231,7 @@ export function CompositionArea({ recentStickers, clearInstalledStickerPack, onClickAddPack, - onPickSticker, + sendStickerMessage, clearShowIntroduction, showPickerHint, clearShowPickerHint, @@ -255,7 +263,6 @@ export function CompositionArea({ isSMSOnly, isFetchingUUID, }: Props): JSX.Element { - const [disabled, setDisabled] = useState(false); const [dirty, setDirty] = useState(false); const [large, setLarge] = useState(false); const [attachmentToEdit, setAttachmentToEdit] = useState< @@ -275,7 +282,7 @@ export function CompositionArea({ const handleSubmit = useCallback( (message: string, mentions: DraftBodyRangesType, timestamp: number) => { emojiButtonRef.current?.close(); - onSendMessage({ + sendMultiMediaMessage(conversationId, { draftAttachments, mentions, message, @@ -283,7 +290,7 @@ export function CompositionArea({ }); setLarge(false); }, - [draftAttachments, onSendMessage, setLarge] + [conversationId, draftAttachments, sendMultiMediaMessage, setLarge] ); const launchAttachmentPicker = useCallback(() => { @@ -327,12 +334,6 @@ export function CompositionArea({ compositionApi.current = { isDirty: () => dirty, focusInput, - setDisabled, - reset: () => { - if (inputApiRef.current) { - inputApiRef.current.reset(); - } - }, resetEmojiResults: () => { if (inputApiRef.current) { inputApiRef.current.resetEmojiResults(); @@ -341,6 +342,14 @@ export function CompositionArea({ }; } + useEffect(() => { + if (!inputApiRef.current) { + return; + } + + inputApiRef.current.reset(); + }, [messageCompositionId]); + const insertEmoji = useCallback( (e: EmojiPickDataType) => { if (inputApiRef.current) { @@ -400,7 +409,7 @@ export function CompositionArea({ voiceNoteAttachment: InMemoryAttachmentDraftType ) => { emojiButtonRef.current?.close(); - onSendMessage({ voiceNoteAttachment }); + sendMultiMediaMessage(conversationId, { voiceNoteAttachment }); }} startRecording={startRecording} /> @@ -447,7 +456,9 @@ export function CompositionArea({ recentStickers={recentStickers} clearInstalledStickerPack={clearInstalledStickerPack} onClickAddPack={onClickAddPack} - onPickSticker={onPickSticker} + onPickSticker={(packId, stickerId) => + sendStickerMessage(conversationId, { packId, stickerId }) + } clearShowIntroduction={clearShowIntroduction} showPickerHint={showPickerHint} clearShowPickerHint={clearShowPickerHint} @@ -690,7 +701,7 @@ export function CompositionArea({ > ( - -); - -_ToastBlocked.story = { - name: 'ToastBlocked', -}; diff --git a/ts/components/ToastBlocked.tsx b/ts/components/ToastBlocked.tsx deleted file mode 100644 index b40e2831b..000000000 --- a/ts/components/ToastBlocked.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastBlocked({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('unblockToSend')}; -} diff --git a/ts/components/ToastBlockedGroup.stories.tsx b/ts/components/ToastBlockedGroup.stories.tsx deleted file mode 100644 index 3934f96fc..000000000 --- a/ts/components/ToastBlockedGroup.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastBlockedGroup } from './ToastBlockedGroup'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastBlockedGroup', -}; - -export const _ToastBlockedGroup = (): JSX.Element => ( - -); - -_ToastBlockedGroup.story = { - name: 'ToastBlockedGroup', -}; diff --git a/ts/components/ToastBlockedGroup.tsx b/ts/components/ToastBlockedGroup.tsx deleted file mode 100644 index 0c5ebdcf7..000000000 --- a/ts/components/ToastBlockedGroup.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastBlockedGroup({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('unblockGroupToSend')}; -} diff --git a/ts/components/ToastExpired.stories.tsx b/ts/components/ToastExpired.stories.tsx deleted file mode 100644 index 18b495c7f..000000000 --- a/ts/components/ToastExpired.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastExpired } from './ToastExpired'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastExpired', -}; - -export const _ToastExpired = (): JSX.Element => ( - -); - -_ToastExpired.story = { - name: 'ToastExpired', -}; diff --git a/ts/components/ToastExpired.tsx b/ts/components/ToastExpired.tsx deleted file mode 100644 index b1c98851c..000000000 --- a/ts/components/ToastExpired.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastExpired({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('expiredWarning')}; -} diff --git a/ts/components/ToastInvalidConversation.stories.tsx b/ts/components/ToastInvalidConversation.stories.tsx deleted file mode 100644 index 63059871f..000000000 --- a/ts/components/ToastInvalidConversation.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastInvalidConversation } from './ToastInvalidConversation'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastInvalidConversation', -}; - -export const _ToastInvalidConversation = (): JSX.Element => ( - -); - -_ToastInvalidConversation.story = { - name: 'ToastInvalidConversation', -}; diff --git a/ts/components/ToastInvalidConversation.tsx b/ts/components/ToastInvalidConversation.tsx deleted file mode 100644 index f3040494d..000000000 --- a/ts/components/ToastInvalidConversation.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastInvalidConversation({ - i18n, - onClose, -}: PropsType): JSX.Element { - return {i18n('invalidConversation')}; -} diff --git a/ts/components/ToastLeftGroup.stories.tsx b/ts/components/ToastLeftGroup.stories.tsx deleted file mode 100644 index 057af203d..000000000 --- a/ts/components/ToastLeftGroup.stories.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import { action } from '@storybook/addon-actions'; -import { ToastLeftGroup } from './ToastLeftGroup'; - -import { setupI18n } from '../util/setupI18n'; -import enMessages from '../../_locales/en/messages.json'; - -const i18n = setupI18n('en', enMessages); - -const defaultProps = { - i18n, - onClose: action('onClose'), -}; - -export default { - title: 'Components/ToastLeftGroup', -}; - -export const _ToastLeftGroup = (): JSX.Element => ( - -); - -_ToastLeftGroup.story = { - name: 'ToastLeftGroup', -}; diff --git a/ts/components/ToastLeftGroup.tsx b/ts/components/ToastLeftGroup.tsx deleted file mode 100644 index c07fc5d37..000000000 --- a/ts/components/ToastLeftGroup.tsx +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import React from 'react'; -import type { LocalizerType } from '../types/Util'; -import { Toast } from './Toast'; - -type PropsType = { - i18n: LocalizerType; - onClose: () => unknown; -}; - -export function ToastLeftGroup({ i18n, onClose }: PropsType): JSX.Element { - return {i18n('youLeftTheGroup')}; -} diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 181a8f279..08dfc8ef6 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -49,6 +49,20 @@ AddingUserToGroup.args = { }, }; +export const Blocked = Template.bind({}); +Blocked.args = { + toast: { + toastType: ToastType.Blocked, + }, +}; + +export const BlockedGroup = Template.bind({}); +BlockedGroup.args = { + toast: { + toastType: ToastType.BlockedGroup, + }, +}; + export const CannotMixMultiAndNonMultiAttachments = Template.bind({}); CannotMixMultiAndNonMultiAttachments.args = { toast: { @@ -98,6 +112,13 @@ Error.args = { }, }; +export const Expired = Template.bind({}); +Expired.args = { + toast: { + toastType: ToastType.Expired, + }, +}; + export const FailedToDeleteUsername = Template.bind({}); FailedToDeleteUsername.args = { toast: { @@ -116,6 +137,20 @@ FileSize.args = { }, }; +export const InvalidConversation = Template.bind({}); +InvalidConversation.args = { + toast: { + toastType: ToastType.InvalidConversation, + }, +}; + +export const LeftGroup = Template.bind({}); +LeftGroup.args = { + toast: { + toastType: ToastType.LeftGroup, + }, +}; + export const MaxAttachments = Template.bind({}); MaxAttachments.args = { toast: { diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index d050d7274..af0027392 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -42,6 +42,14 @@ export function ToastManager({ ); } + if (toastType === ToastType.Blocked) { + return {i18n('unblockToSend')}; + } + + if (toastType === ToastType.BlockedGroup) { + return {i18n('unblockGroupToSend')}; + } + if (toastType === ToastType.CannotMixMultiAndNonMultiAttachments) { return ( @@ -97,6 +105,10 @@ export function ToastManager({ ); } + if (toastType === ToastType.Expired) { + return {i18n('expiredWarning')}; + } + if (toastType === ToastType.FailedToDeleteUsername) { return ( @@ -113,6 +125,14 @@ export function ToastManager({ ); } + if (toastType === ToastType.InvalidConversation) { + return {i18n('invalidConversation')}; + } + + if (toastType === ToastType.LeftGroup) { + return {i18n('youLeftTheGroup')}; + } + if (toastType === ToastType.MaxAttachments) { return {i18n('maximumAttachments')}; } diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index d26a9f680..b3952a71b 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -1,52 +1,73 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import path from 'path'; import type { ThunkAction } from 'redux-thunk'; -import * as log from '../../logging/log'; -import type { NoopActionType } from './noop'; -import type { StateType as RootStateType } from '../reducer'; -import type { - AttachmentDraftType, - InMemoryAttachmentDraftType, -} from '../../types/Attachment'; -import type { MessageAttributesType } from '../../model-types.d'; -import type { LinkPreviewType } from '../../types/message/LinkPreviews'; -import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation'; import type { AddLinkPreviewActionType, RemoveLinkPreviewActionType, } from './linkPreviews'; +import type { + AttachmentType, + AttachmentDraftType, + InMemoryAttachmentDraftType, +} from '../../types/Attachment'; +import type { + DraftBodyRangesType, + ReplacementValuesType, +} from '../../types/Util'; +import type { LinkPreviewType } from '../../types/message/LinkPreviews'; +import type { MessageAttributesType } from '../../model-types.d'; +import type { NoopActionType } from './noop'; +import type { ShowToastActionType } from './toast'; +import type { StateType as RootStateType } from '../reducer'; +import type { UUIDStringType } from '../../types/UUID'; +import * as log from '../../logging/log'; +import * as Errors from '../../types/errors'; import { ADD_PREVIEW as ADD_LINK_PREVIEW, REMOVE_PREVIEW as REMOVE_LINK_PREVIEW, } from './linkPreviews'; -import { writeDraftAttachment } from '../../util/writeDraftAttachment'; -import { deleteDraftAttachment } from '../../util/deleteDraftAttachment'; -import { replaceIndex } from '../../util/replaceIndex'; -import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk'; import { LinkPreviewSourceType } from '../../types/LinkPreview'; import { RecordingState } from './audioRecorder'; -import { hasLinkPreviewLoaded } from '../../services/LinkPreview'; import { SHOW_TOAST, ToastType } from './toast'; -import type { ShowToastActionType } from './toast'; +import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog'; +import { UUID } from '../../types/UUID'; +import { assignWithNoUnnecessaryAllocation } from '../../util/assignWithNoUnnecessaryAllocation'; +import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified'; +import { clearConversationDraftAttachments } from '../../util/clearConversationDraftAttachments'; +import { deleteDraftAttachment } from '../../util/deleteDraftAttachment'; +import { + hasLinkPreviewLoaded, + getLinkPreviewForSend, + resetLinkPreview, +} from '../../services/LinkPreview'; import { getMaximumAttachmentSize } from '../../util/attachments'; -import { isFileDangerous } from '../../util/isFileDangerous'; -import { isImage, isVideo, stringToMIMEType } from '../../types/MIME'; +import { getRecipientsByConversation } from '../../util/getRecipientsByConversation'; import { getRenderDetailsForLimit, processAttachment, } from '../../util/processAttachment'; -import type { ReplacementValuesType } from '../../types/Util'; +import { hasDraftAttachments } from '../../util/hasDraftAttachments'; +import { isFileDangerous } from '../../util/isFileDangerous'; +import { isImage, isVideo, stringToMIMEType } from '../../types/MIME'; +import { isNotNil } from '../../util/isNotNil'; +import { replaceIndex } from '../../util/replaceIndex'; +import { resolveAttachmentDraftData } from '../../util/resolveAttachmentDraftData'; +import { resolveDraftAttachmentOnDisk } from '../../util/resolveDraftAttachmentOnDisk'; +import { shouldShowInvalidMessageToast } from '../../util/shouldShowInvalidMessageToast'; +import { writeDraftAttachment } from '../../util/writeDraftAttachment'; // State export type ComposerStateType = { attachments: ReadonlyArray; + isDisabled: boolean; linkPreviewLoading: boolean; linkPreviewResult?: LinkPreviewType; + messageCompositionId: UUIDStringType; quotedMessage?: Pick; shouldSendHighQualityAttachments?: boolean; }; @@ -58,6 +79,7 @@ const REPLACE_ATTACHMENTS = 'composer/REPLACE_ATTACHMENTS'; const RESET_COMPOSER = 'composer/RESET_COMPOSER'; const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING'; const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE'; +const SET_COMPOSER_DISABLED = 'composer/SET_COMPOSER_DISABLED'; type AddPendingAttachmentActionType = { type: typeof ADD_PENDING_ATTACHMENT; @@ -73,6 +95,11 @@ type ResetComposerActionType = { type: typeof RESET_COMPOSER; }; +type SetComposerDisabledStateActionType = { + type: typeof SET_COMPOSER_DISABLED; + payload: boolean; +}; + type SetHighQualitySettingActionType = { type: typeof SET_HIGH_QUALITY_SETTING; payload: boolean; @@ -89,6 +116,7 @@ type ComposerActionType = | RemoveLinkPreviewActionType | ReplaceAttachmentsActionType | ResetComposerActionType + | SetComposerDisabledStateActionType | SetHighQualitySettingActionType | SetQuotedMessageActionType; @@ -101,10 +129,204 @@ export const actions = { removeAttachment, replaceAttachments, resetComposer, + setComposerDisabledState, + sendMultiMediaMessage, + sendStickerMessage, setMediaQualitySetting, setQuotedMessage, }; +function sendMultiMediaMessage( + conversationId: string, + options: { + draftAttachments?: ReadonlyArray; + mentions?: DraftBodyRangesType; + message?: string; + timestamp?: number; + voiceNoteAttachment?: InMemoryAttachmentDraftType; + } +): ThunkAction< + void, + RootStateType, + unknown, + | NoopActionType + | ResetComposerActionType + | SetComposerDisabledStateActionType + | SetQuotedMessageActionType + | ShowToastActionType +> { + return async (dispatch, getState) => { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('sendMultiMediaMessage: No conversation found'); + } + + const { + draftAttachments, + message = '', + mentions, + timestamp = Date.now(), + voiceNoteAttachment, + } = options; + + const state = getState(); + + const sendStart = Date.now(); + const recipientsByConversation = getRecipientsByConversation([ + conversation.attributes, + ]); + + try { + dispatch(setComposerDisabledState(true)); + + const sendAnyway = await blockSendUntilConversationsAreVerified( + recipientsByConversation, + SafetyNumberChangeSource.MessageSend + ); + if (!sendAnyway) { + dispatch(setComposerDisabledState(false)); + return; + } + } catch (error) { + dispatch(setComposerDisabledState(false)); + log.error('sendMessage error:', Errors.toLogFormat(error)); + return; + } + + conversation.clearTypingTimers(); + + const toastType = shouldShowInvalidMessageToast(conversation.attributes); + if (toastType) { + dispatch({ + type: SHOW_TOAST, + payload: { + toastType, + }, + }); + return; + } + + try { + if ( + !message.length && + !hasDraftAttachments(conversation.attributes.draftAttachments, { + includePending: false, + }) && + !voiceNoteAttachment + ) { + return; + } + + let attachments: Array = []; + if (voiceNoteAttachment) { + attachments = [voiceNoteAttachment]; + } else if (draftAttachments) { + attachments = ( + await Promise.all(draftAttachments.map(resolveAttachmentDraftData)) + ).filter(isNotNil); + } + + const quote = state.composer.quotedMessage?.quote; + + const shouldSendHighQualityAttachments = window.reduxStore + ? state.composer.shouldSendHighQualityAttachments + : undefined; + + const sendHQImages = + shouldSendHighQualityAttachments !== undefined + ? shouldSendHighQualityAttachments + : state.items['sent-media-quality'] === 'high'; + + const sendDelta = Date.now() - sendStart; + + log.info('Send pre-checks took', sendDelta, 'milliseconds'); + + await conversation.enqueueMessageForSend( + { + body: message, + attachments, + quote, + preview: getLinkPreviewForSend(message), + mentions, + }, + { + sendHQImages, + timestamp, + extraReduxActions: () => { + conversation.setMarkedUnread(false); + resetLinkPreview(); + clearConversationDraftAttachments(conversationId, draftAttachments); + dispatch(setQuotedMessage(undefined)); + dispatch(resetComposer()); + }, + } + ); + } catch (error) { + log.error( + 'Error pulling attached files before send', + Errors.toLogFormat(error) + ); + } finally { + dispatch(setComposerDisabledState(false)); + } + }; +} + +function sendStickerMessage( + conversationId: string, + options: { + packId: string; + stickerId: number; + } +): ThunkAction< + void, + RootStateType, + unknown, + NoopActionType | ShowToastActionType +> { + return async dispatch => { + const conversation = window.ConversationController.get(conversationId); + if (!conversation) { + throw new Error('sendStickerMessage: No conversation found'); + } + + const recipientsByConversation = getRecipientsByConversation([ + conversation.attributes, + ]); + + try { + const sendAnyway = await blockSendUntilConversationsAreVerified( + recipientsByConversation, + SafetyNumberChangeSource.MessageSend + ); + if (!sendAnyway) { + return; + } + + const toastType = shouldShowInvalidMessageToast(conversation.attributes); + if (toastType) { + dispatch({ + type: SHOW_TOAST, + payload: { + toastType, + }, + }); + return; + } + + const { packId, stickerId } = options; + conversation.sendStickerMessage(packId, stickerId); + } catch (error) { + log.error('clickSend error:', Errors.toLogFormat(error)); + } + + dispatch({ + type: 'NOOP', + payload: null, + }); + }; +} + // Not cool that we have to pull from ConversationModel here // but if the current selected conversation isn't the one that we're operating // on then we won't be able to grab attachments from state so we resort to the @@ -440,6 +662,15 @@ function resetComposer(): ResetComposerActionType { }; } +function setComposerDisabledState( + value: boolean +): SetComposerDisabledStateActionType { + return { + type: SET_COMPOSER_DISABLED, + payload: value, + }; +} + function setMediaQualitySetting( payload: boolean ): SetHighQualitySettingActionType { @@ -463,7 +694,9 @@ function setQuotedMessage( export function getEmptyState(): ComposerStateType { return { attachments: [], + isDisabled: false, linkPreviewLoading: false, + messageCompositionId: UUID.generate().toString(), }; } @@ -526,5 +759,12 @@ export function reducer( }; } + if (action.type === SET_COMPOSER_DISABLED) { + return { + ...state, + isDisabled: action.payload, + }; + } + return state; } diff --git a/ts/state/ducks/toast.ts b/ts/state/ducks/toast.ts index 950045cc6..4621fb613 100644 --- a/ts/state/ducks/toast.ts +++ b/ts/state/ducks/toast.ts @@ -6,6 +6,8 @@ import type { ReplacementValuesType } from '../../types/Util'; export enum ToastType { AddingUserToGroup = 'AddingUserToGroup', + Blocked = 'Blocked', + BlockedGroup = 'BlockedGroup', CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments', CannotStartGroupCall = 'CannotStartGroupCall', CopiedUsername = 'CopiedUsername', @@ -13,8 +15,11 @@ export enum ToastType { DangerousFileType = 'DangerousFileType', DeleteForEveryoneFailed = 'DeleteForEveryoneFailed', Error = 'Error', + Expired = 'Expired', FailedToDeleteUsername = 'FailedToDeleteUsername', FileSize = 'FileSize', + InvalidConversation = 'InvalidConversation', + LeftGroup = 'LeftGroup', MaxAttachments = 'MaxAttachments', MessageBodyTooLong = 'MessageBodyTooLong', PinnedConversationsFull = 'PinnedConversationsFull', diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index 450f4b409..2f16c0b35 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -70,8 +70,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { const { attachments: draftAttachments, + isDisabled, linkPreviewLoading, linkPreviewResult, + messageCompositionId, quotedMessage, shouldSendHighQualityAttachments, } = state.composer; @@ -81,9 +83,11 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => { return { // Base conversationId: id, - i18n: getIntl(state), - theme: getTheme(state), getPreferredBadge: getPreferredBadgeSelector(state), + i18n: getIntl(state), + isDisabled, + messageCompositionId, + theme: getTheme(state), // AudioCapture errorDialogAudioRecorderType: state.audioRecorder.errorDialogAudioRecorderType, diff --git a/ts/state/smart/ConversationView.tsx b/ts/state/smart/ConversationView.tsx index bbfae6696..ea17c9c97 100644 --- a/ts/state/smart/ConversationView.tsx +++ b/ts/state/smart/ConversationView.tsx @@ -27,9 +27,7 @@ export type PropsType = { | 'onClickAddPack' | 'onCloseLinkPreview' | 'onEditorStateChange' - | 'onPickSticker' | 'onSelectMediaQuality' - | 'onSendMessage' | 'onTextTooLong' | 'openConversation' >; diff --git a/ts/test-both/state/ducks/composer_test.ts b/ts/test-both/state/ducks/composer_test.ts index 519fea6c8..8309b18ff 100644 --- a/ts/test-both/state/ducks/composer_test.ts +++ b/ts/test-both/state/ducks/composer_test.ts @@ -103,17 +103,23 @@ describe('both/state/ducks/composer', () => { describe('resetComposer', () => { it('returns composer back to empty state', () => { const { resetComposer } = actions; + const emptyState = getEmptyState(); const nextState = reducer( { attachments: [], + isDisabled: false, linkPreviewLoading: true, + messageCompositionId: emptyState.messageCompositionId, quotedMessage: QUOTED_MESSAGE, shouldSendHighQualityAttachments: true, }, resetComposer() ); - assert.deepEqual(nextState, getEmptyState()); + assert.deepEqual(nextState, { + ...getEmptyState(), + messageCompositionId: nextState.messageCompositionId, + }); }); }); diff --git a/ts/util/clearConversationDraftAttachments.ts b/ts/util/clearConversationDraftAttachments.ts new file mode 100644 index 000000000..522bb623b --- /dev/null +++ b/ts/util/clearConversationDraftAttachments.ts @@ -0,0 +1,29 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { AttachmentDraftType } from '../types/Attachment'; +import { strictAssert } from './assert'; +import { deleteDraftAttachment } from './deleteDraftAttachment'; + +export async function clearConversationDraftAttachments( + conversationId: string, + draftAttachments: ReadonlyArray = [] +): Promise { + const conversation = window.ConversationController.get(conversationId); + strictAssert(conversation, 'no conversation found'); + + conversation.set({ + draftAttachments: [], + draftChanged: true, + }); + + window.reduxActions.composer.replaceAttachments(conversationId, []); + + // We're fine doing this all at once; at most it should be 32 attachments + await Promise.all([ + window.Signal.Data.updateConversation(conversation.attributes), + Promise.all( + draftAttachments.map(attachment => deleteDraftAttachment(attachment)) + ), + ]); +} diff --git a/ts/util/getRecipientsByConversation.ts b/ts/util/getRecipientsByConversation.ts new file mode 100644 index 000000000..1e16b1a6e --- /dev/null +++ b/ts/util/getRecipientsByConversation.ts @@ -0,0 +1,27 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ConversationAttributesType } from '../model-types'; +import type { RecipientsByConversation } from '../state/ducks/stories'; + +import { getConversationMembers } from './getConversationMembers'; +import { UUID } from '../types/UUID'; +import { isNotNil } from './isNotNil'; + +export function getRecipientsByConversation( + conversations: Array +): RecipientsByConversation { + const recipientsByConversation: RecipientsByConversation = {}; + + conversations.forEach(attributes => { + recipientsByConversation[attributes.id] = { + uuids: getConversationMembers(attributes) + .map(member => + member.uuid ? UUID.checkedLookup(member.uuid).toString() : undefined + ) + .filter(isNotNil), + }; + }); + + return recipientsByConversation; +} diff --git a/ts/util/hasDraftAttachments.ts b/ts/util/hasDraftAttachments.ts new file mode 100644 index 000000000..b6f2e98d9 --- /dev/null +++ b/ts/util/hasDraftAttachments.ts @@ -0,0 +1,19 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { AttachmentDraftType } from '../types/Attachment'; + +export function hasDraftAttachments( + draftAttachments: Array | undefined, + options: { includePending: boolean } +): boolean { + if (!draftAttachments) { + return false; + } + + if (options.includePending) { + return draftAttachments.length > 0; + } + + return draftAttachments.some(item => !item.pending); +} diff --git a/ts/util/isCallSafe.ts b/ts/util/isCallSafe.ts index 1d8b9d52b..a66555655 100644 --- a/ts/util/isCallSafe.ts +++ b/ts/util/isCallSafe.ts @@ -2,27 +2,16 @@ // SPDX-License-Identifier: AGPL-3.0-only import type { ConversationAttributesType } from '../model-types'; -import type { RecipientsByConversation } from '../state/ducks/stories'; import * as log from '../logging/log'; import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog'; import { blockSendUntilConversationsAreVerified } from './blockSendUntilConversationsAreVerified'; -import { getConversationMembers } from './getConversationMembers'; -import { UUID } from '../types/UUID'; -import { isNotNil } from './isNotNil'; +import { getRecipientsByConversation } from './getRecipientsByConversation'; export async function isCallSafe( attributes: ConversationAttributesType ): Promise { - const recipientsByConversation: RecipientsByConversation = { - [attributes.id]: { - uuids: getConversationMembers(attributes) - .map(member => - member.uuid ? UUID.checkedLookup(member.uuid).toString() : undefined - ) - .filter(isNotNil), - }, - }; + const recipientsByConversation = getRecipientsByConversation([attributes]); const callAnyway = await blockSendUntilConversationsAreVerified( recipientsByConversation, diff --git a/ts/util/maybeForwardMessage.ts b/ts/util/maybeForwardMessage.ts index 1f6b57e66..6302054c8 100644 --- a/ts/util/maybeForwardMessage.ts +++ b/ts/util/maybeForwardMessage.ts @@ -10,7 +10,7 @@ import { blockSendUntilConversationsAreVerified } from './blockSendUntilConversa import { getMessageIdForLogging } from './idForLogging'; import { isNotNil } from './isNotNil'; import { resetLinkPreview } from '../services/LinkPreview'; -import type { RecipientsByConversation } from '../state/ducks/stories'; +import { getRecipientsByConversation } from './getRecipientsByConversation'; export async function maybeForwardMessage( messageAttributes: MessageAttributesType, @@ -41,12 +41,9 @@ export async function maybeForwardMessage( throw new Error('Cannot send to group'); } - const recipientsByConversation: RecipientsByConversation = {}; - conversations.forEach(conversation => { - recipientsByConversation[conversation.id] = { - uuids: conversation.getMemberUuids().map(uuid => uuid.toString()), - }; - }); + const recipientsByConversation = getRecipientsByConversation( + conversations.map(x => x.attributes) + ); // Verify that all contacts that we're forwarding // to are verified and trusted. diff --git a/ts/util/shouldShowInvalidMessageToast.ts b/ts/util/shouldShowInvalidMessageToast.ts new file mode 100644 index 000000000..1833614a2 --- /dev/null +++ b/ts/util/shouldShowInvalidMessageToast.ts @@ -0,0 +1,62 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { ConversationAttributesType } from '../model-types'; + +import { ToastType } from '../state/ducks/toast'; +import { + isDirectConversation, + isGroupV1, + isGroupV2, +} from './whatTypeOfConversation'; + +const MAX_MESSAGE_BODY_LENGTH = 64 * 1024; + +export function shouldShowInvalidMessageToast( + conversationAttributes: ConversationAttributesType, + messageText?: string +): ToastType | undefined { + if (window.reduxStore.getState().expiration.hasExpired) { + return ToastType.Expired; + } + + const isValid = + isDirectConversation(conversationAttributes) || + isGroupV1(conversationAttributes) || + isGroupV2(conversationAttributes); + + if (!isValid) { + return ToastType.InvalidConversation; + } + + const { e164, uuid } = conversationAttributes; + if ( + isDirectConversation(conversationAttributes) && + ((e164 && window.storage.blocked.isBlocked(e164)) || + (uuid && window.storage.blocked.isUuidBlocked(uuid))) + ) { + return ToastType.Blocked; + } + + const { groupId } = conversationAttributes; + if ( + !isDirectConversation(conversationAttributes) && + groupId && + window.storage.blocked.isGroupBlocked(groupId) + ) { + return ToastType.BlockedGroup; + } + + if ( + !isDirectConversation(conversationAttributes) && + conversationAttributes.left + ) { + return ToastType.LeftGroup; + } + + if (messageText && messageText.length > MAX_MESSAGE_BODY_LENGTH) { + return ToastType.MessageBodyTooLong; + } + + return undefined; +} diff --git a/ts/util/showToast.tsx b/ts/util/showToast.tsx index 5b6005c30..724d9d29c 100644 --- a/ts/util/showToast.tsx +++ b/ts/util/showToast.tsx @@ -6,8 +6,6 @@ import { render, unmountComponentAtNode } from 'react-dom'; import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember'; import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin'; -import type { ToastBlocked } from '../components/ToastBlocked'; -import type { ToastBlockedGroup } from '../components/ToastBlockedGroup'; import type { ToastCannotOpenGiftBadge, ToastPropsType as ToastCannotOpenGiftBadgePropsType, @@ -24,7 +22,6 @@ import type { ToastInternalError, ToastPropsType as ToastInternalErrorPropsType, } from '../components/ToastInternalError'; -import type { ToastExpired } from '../components/ToastExpired'; import type { ToastFileSaved, ToastPropsType as ToastFileSavedPropsType, @@ -34,8 +31,6 @@ import type { ToastPropsType as ToastFileSizePropsType, } from '../components/ToastFileSize'; import type { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied'; -import type { ToastInvalidConversation } from '../components/ToastInvalidConversation'; -import type { ToastLeftGroup } from '../components/ToastLeftGroup'; import type { ToastLinkCopied } from '../components/ToastLinkCopied'; import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs'; import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong'; @@ -51,8 +46,6 @@ import type { ToastVoiceNoteMustBeOnlyAttachment } from '../components/ToastVoic export function showToast(Toast: typeof ToastAlreadyGroupMember): void; export function showToast(Toast: typeof ToastAlreadyRequestedToJoin): void; -export function showToast(Toast: typeof ToastBlocked): void; -export function showToast(Toast: typeof ToastBlockedGroup): void; export function showToast( Toast: typeof ToastCannotOpenGiftBadge, props: Omit @@ -69,7 +62,6 @@ export function showToast( Toast: typeof ToastInternalError, props: ToastInternalErrorPropsType ): void; -export function showToast(Toast: typeof ToastExpired): void; export function showToast( Toast: typeof ToastFileSaved, props: ToastFileSavedPropsType @@ -79,8 +71,6 @@ export function showToast( props: ToastFileSizePropsType ): void; export function showToast(Toast: typeof ToastGroupLinkCopied): void; -export function showToast(Toast: typeof ToastInvalidConversation): void; -export function showToast(Toast: typeof ToastLeftGroup): void; export function showToast(Toast: typeof ToastLinkCopied): void; export function showToast(Toast: typeof ToastLoadingFullLogs): void; export function showToast(Toast: typeof ToastMessageBodyTooLong): void; diff --git a/ts/views/conversation_view.tsx b/ts/views/conversation_view.tsx index 90cca66b1..4b634919e 100644 --- a/ts/views/conversation_view.tsx +++ b/ts/views/conversation_view.tsx @@ -12,7 +12,6 @@ import { render } from 'mustache'; import type { AttachmentType } from '../types/Attachment'; import { isGIF } from '../types/Attachment'; import * as Stickers from '../types/Stickers'; -import * as Errors from '../types/errors'; import type { DraftBodyRangesType } from '../types/Util'; import type { MIMEType } from '../types/MIME'; import type { ConversationModel } from '../models/conversations'; @@ -45,15 +44,10 @@ import * as log from '../logging/log'; import type { EmbeddedContactType } from '../types/EmbeddedContact'; import { createConversationView } from '../state/roots/createConversationView'; import type { CompositionAPIType } from '../components/CompositionArea'; -import { ToastBlocked } from '../components/ToastBlocked'; -import { ToastBlockedGroup } from '../components/ToastBlockedGroup'; import { ToastConversationArchived } from '../components/ToastConversationArchived'; import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread'; import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived'; import { ToastDangerousFileType } from '../components/ToastDangerousFileType'; -import { ToastExpired } from '../components/ToastExpired'; -import { ToastInvalidConversation } from '../components/ToastInvalidConversation'; -import { ToastLeftGroup } from '../components/ToastLeftGroup'; import { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong'; import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound'; import { ToastReactionFailed } from '../components/ToastReactionFailed'; @@ -65,7 +59,6 @@ import { deleteDraftAttachment } from '../util/deleteDraftAttachment'; import { retryMessageSend } from '../util/retryMessageSend'; import { isNotNil } from '../util/isNotNil'; import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; -import { resolveAttachmentDraftData } from '../util/resolveAttachmentDraftData'; import { showToast } from '../util/showToast'; import { UUIDKind } from '../types/UUID'; import type { UUIDStringType } from '../types/UUID'; @@ -74,18 +67,14 @@ import { ContactDetail } from '../components/conversation/ContactDetail'; import { MediaGallery } from '../components/conversation/media-gallery/MediaGallery'; import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent'; import { - getLinkPreviewForSend, maybeGrabLinkPreview, removeLinkPreview, - resetLinkPreview, suspendLinkPreviews, } from '../services/LinkPreview'; import { LinkPreviewSourceType } from '../types/LinkPreview'; import { closeLightbox, showLightbox } from '../util/showLightbox'; import { saveAttachment } from '../util/saveAttachment'; import { SECOND } from '../util/durations'; -import { blockSendUntilConversationsAreVerified } from '../util/blockSendUntilConversationsAreVerified'; -import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog'; import { startConversation } from '../util/startConversation'; import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper'; @@ -170,8 +159,6 @@ type MediaType = { }; }; -const MAX_MESSAGE_BODY_LENGTH = 64 * 1024; - export class ConversationView extends window.Backbone.View { private debouncedSaveDraft: ( messageText: string, @@ -182,7 +169,6 @@ export class ConversationView extends window.Backbone.View { private compositionApi: { current: CompositionAPIType; } = { current: undefined }; - private sendStart?: number; // Sub-views private contactModalView?: Backbone.View; @@ -432,8 +418,6 @@ export class ConversationView extends window.Backbone.View { id: this.model.id, compositionApi: this.compositionApi, onClickAddPack: () => this.showStickerManager(), - onPickSticker: (packId: string, stickerId: number) => - this.sendStickerMessage({ packId, stickerId }), onEditorStateChange: ( msg: string, bodyRanges: DraftBodyRangesType, @@ -473,26 +457,6 @@ export class ConversationView extends window.Backbone.View { }, openConversation: this.openConversation.bind(this), - - onSendMessage: ({ - draftAttachments, - mentions = [], - message = '', - timestamp, - voiceNoteAttachment, - }: { - draftAttachments?: ReadonlyArray; - mentions?: DraftBodyRangesType; - message?: string; - timestamp?: number; - voiceNoteAttachment?: AttachmentType; - }): void => { - this.sendMessage(message, mentions, { - draftAttachments, - timestamp, - voiceNoteAttachment, - }); - }, }; // createConversationView root @@ -1655,198 +1619,6 @@ export class ConversationView extends window.Backbone.View { ); } - async sendStickerMessage(options: { - packId: string; - stickerId: number; - }): Promise { - const recipientsByConversation = { - [this.model.id]: { - uuids: this.model.getMemberUuids().map(uuid => uuid.toString()), - }, - }; - - try { - const sendAnyway = await blockSendUntilConversationsAreVerified( - recipientsByConversation, - SafetyNumberChangeSource.MessageSend - ); - if (!sendAnyway) { - return; - } - - if (this.showInvalidMessageToast()) { - return; - } - - const { packId, stickerId } = options; - this.model.sendStickerMessage(packId, stickerId); - } catch (error) { - log.error('clickSend error:', Errors.toLogFormat(error)); - } - } - - showInvalidMessageToast(messageText?: string): boolean { - const { model }: { model: ConversationModel } = this; - - let toastView: - | undefined - | typeof ToastBlocked - | typeof ToastBlockedGroup - | typeof ToastExpired - | typeof ToastInvalidConversation - | typeof ToastLeftGroup - | typeof ToastMessageBodyTooLong; - - if (window.reduxStore.getState().expiration.hasExpired) { - toastView = ToastExpired; - } - if (!model.isValid()) { - toastView = ToastInvalidConversation; - } - - const e164 = this.model.get('e164'); - const uuid = this.model.get('uuid'); - if ( - isDirectConversation(this.model.attributes) && - ((e164 && window.storage.blocked.isBlocked(e164)) || - (uuid && window.storage.blocked.isUuidBlocked(uuid))) - ) { - toastView = ToastBlocked; - } - - const groupId = this.model.get('groupId'); - if ( - !isDirectConversation(this.model.attributes) && - groupId && - window.storage.blocked.isGroupBlocked(groupId) - ) { - toastView = ToastBlockedGroup; - } - - if (!isDirectConversation(model.attributes) && model.get('left')) { - toastView = ToastLeftGroup; - } - if (messageText && messageText.length > MAX_MESSAGE_BODY_LENGTH) { - toastView = ToastMessageBodyTooLong; - } - - if (toastView) { - showToast(toastView); - return true; - } - - return false; - } - - async sendMessage( - message = '', - mentions: DraftBodyRangesType = [], - options: { - draftAttachments?: ReadonlyArray; - timestamp?: number; - voiceNoteAttachment?: AttachmentType; - } = {} - ): Promise { - const timestamp = options.timestamp || Date.now(); - - this.sendStart = Date.now(); - const recipientsByConversation = { - [this.model.id]: { - uuids: this.model.getMemberUuids().map(uuid => uuid.toString()), - }, - }; - - try { - this.disableMessageField(); - - const sendAnyway = await blockSendUntilConversationsAreVerified( - recipientsByConversation, - SafetyNumberChangeSource.MessageSend - ); - if (!sendAnyway) { - this.enableMessageField(); - return; - } - } catch (error) { - this.enableMessageField(); - log.error('sendMessage error:', Errors.toLogFormat(error)); - return; - } - - this.model.clearTypingTimers(); - - if (this.showInvalidMessageToast(message)) { - this.enableMessageField(); - return; - } - - try { - if ( - !message.length && - !this.hasFiles({ includePending: false }) && - !options.voiceNoteAttachment - ) { - return; - } - - let attachments: Array = []; - if (options.voiceNoteAttachment) { - attachments = [options.voiceNoteAttachment]; - } else if (options.draftAttachments) { - attachments = ( - await Promise.all( - options.draftAttachments.map(resolveAttachmentDraftData) - ) - ).filter(isNotNil); - } - - const composerState = window.reduxStore - ? window.reduxStore.getState().composer - : undefined; - const shouldSendHighQualityAttachments = - composerState?.shouldSendHighQualityAttachments; - const quote = composerState?.quotedMessage?.quote; - - const sendHQImages = - shouldSendHighQualityAttachments !== undefined - ? shouldSendHighQualityAttachments - : window.storage.get('sent-media-quality') === 'high'; - - const sendDelta = Date.now() - this.sendStart; - - log.info('Send pre-checks took', sendDelta, 'milliseconds'); - - await this.model.enqueueMessageForSend( - { - body: message, - attachments, - quote, - preview: getLinkPreviewForSend(message), - mentions, - }, - { - sendHQImages, - timestamp, - extraReduxActions: () => { - this.compositionApi.current?.reset(); - this.model.setMarkedUnread(false); - this.setQuoteMessage(undefined); - resetLinkPreview(); - this.clearAttachments(); - window.reduxActions.composer.resetComposer(); - }, - } - ); - } catch (error) { - log.error( - 'Error pulling attached files before send', - Errors.toLogFormat(error) - ); - } finally { - this.enableMessageField(); - } - } - focusMessageField(): void { if (this.panels && this.panels.length) { return; @@ -1855,14 +1627,6 @@ export class ConversationView extends window.Backbone.View { this.compositionApi.current?.focusInput(); } - disableMessageField(): void { - this.compositionApi.current?.setDisabled(true); - } - - enableMessageField(): void { - this.compositionApi.current?.setDisabled(false); - } - resetEmojiResults(): void { this.compositionApi.current?.resetEmojiResults(); } @@ -1974,7 +1738,7 @@ export class ConversationView extends window.Backbone.View { quote, }); - this.enableMessageField(); + window.reduxActions.composer.setComposerDisabledState(false); this.focusMessageField(); } else { window.reduxActions.composer.setQuotedMessage(undefined);