Moves sendMessage and friends to redux

This commit is contained in:
Josh Perez 2022-12-08 02:43:48 -05:00 committed by GitHub
parent 7ea38bb1a9
commit 2378776e1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 517 additions and 537 deletions

View file

@ -35,7 +35,9 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
addAttachment: action('addAttachment'), addAttachment: action('addAttachment'),
conversationId: '123', conversationId: '123',
i18n, i18n,
onSendMessage: action('onSendMessage'), isDisabled: false,
messageCompositionId: '456',
sendMultiMediaMessage: action('sendMultiMediaMessage'),
processAttachments: action('processAttachments'), processAttachments: action('processAttachments'),
removeAttachment: action('removeAttachment'), removeAttachment: action('removeAttachment'),
theme: React.useContext(StorybookThemeContext), theme: React.useContext(StorybookThemeContext),
@ -89,7 +91,7 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
recentStickers: [], recentStickers: [],
clearInstalledStickerPack: action('clearInstalledStickerPack'), clearInstalledStickerPack: action('clearInstalledStickerPack'),
onClickAddPack: action('onClickAddPack'), onClickAddPack: action('onClickAddPack'),
onPickSticker: action('onPickSticker'), sendStickerMessage: action('sendStickerMessage'),
clearShowIntroduction: action('clearShowIntroduction'), clearShowIntroduction: action('clearShowIntroduction'),
showPickerHint: false, showPickerHint: false,
clearShowPickerHint: action('clearShowPickerHint'), clearShowPickerHint: action('clearShowPickerHint'),

View file

@ -63,8 +63,6 @@ export type CompositionAPIType =
| { | {
focusInput: () => void; focusInput: () => void;
isDirty: () => boolean; isDirty: () => boolean;
setDisabled: (disabled: boolean) => void;
reset: InputApi['reset'];
resetEmojiResults: InputApi['resetEmojiResults']; resetEmojiResults: InputApi['resetEmojiResults'];
} }
| undefined; | undefined;
@ -94,11 +92,13 @@ export type OwnProps = Readonly<{
groupVersion?: 1 | 2; groupVersion?: 1 | 2;
i18n: LocalizerType; i18n: LocalizerType;
imageToBlurHash: typeof imageToBlurHash; imageToBlurHash: typeof imageToBlurHash;
isDisabled: boolean;
isFetchingUUID?: boolean; isFetchingUUID?: boolean;
isGroupV1AndDisabled?: boolean; isGroupV1AndDisabled?: boolean;
isMissingMandatoryProfileSharing?: boolean; isMissingMandatoryProfileSharing?: boolean;
isSignalConversation?: boolean; isSignalConversation?: boolean;
recordingState: RecordingState; recordingState: RecordingState;
messageCompositionId: string;
isSMSOnly?: boolean; isSMSOnly?: boolean;
left?: boolean; left?: boolean;
linkPreviewLoading: boolean; linkPreviewLoading: boolean;
@ -112,13 +112,20 @@ export type OwnProps = Readonly<{
files: ReadonlyArray<File>; files: ReadonlyArray<File>;
}) => unknown; }) => unknown;
onSelectMediaQuality(isHQ: boolean): unknown; onSelectMediaQuality(isHQ: boolean): unknown;
onSendMessage(options: { sendStickerMessage(
draftAttachments?: ReadonlyArray<AttachmentDraftType>; id: string,
mentions?: DraftBodyRangesType; opts: { packId: string; stickerId: number }
message?: string; ): unknown;
timestamp?: number; sendMultiMediaMessage(
voiceNoteAttachment?: InMemoryAttachmentDraftType; conversationId: string,
}): unknown; options: {
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
mentions?: DraftBodyRangesType;
message?: string;
timestamp?: number;
voiceNoteAttachment?: InMemoryAttachmentDraftType;
}
): unknown;
openConversation(conversationId: string): unknown; openConversation(conversationId: string): unknown;
quotedMessageProps?: Omit< quotedMessageProps?: Omit<
QuoteProps, QuoteProps,
@ -156,7 +163,6 @@ export type Props = Pick<
| 'recentStickers' | 'recentStickers'
| 'clearInstalledStickerPack' | 'clearInstalledStickerPack'
| 'onClickAddPack' | 'onClickAddPack'
| 'onPickSticker'
| 'clearShowIntroduction' | 'clearShowIntroduction'
| 'showPickerHint' | 'showPickerHint'
| 'clearShowPickerHint' | 'clearShowPickerHint'
@ -171,12 +177,14 @@ export function CompositionArea({
addAttachment, addAttachment,
conversationId, conversationId,
i18n, i18n,
onSendMessage,
imageToBlurHash, imageToBlurHash,
isDisabled,
isSignalConversation,
processAttachments, processAttachments,
removeAttachment, removeAttachment,
messageCompositionId,
sendMultiMediaMessage,
theme, theme,
isSignalConversation,
// AttachmentList // AttachmentList
draftAttachments, draftAttachments,
@ -223,7 +231,7 @@ export function CompositionArea({
recentStickers, recentStickers,
clearInstalledStickerPack, clearInstalledStickerPack,
onClickAddPack, onClickAddPack,
onPickSticker, sendStickerMessage,
clearShowIntroduction, clearShowIntroduction,
showPickerHint, showPickerHint,
clearShowPickerHint, clearShowPickerHint,
@ -255,7 +263,6 @@ export function CompositionArea({
isSMSOnly, isSMSOnly,
isFetchingUUID, isFetchingUUID,
}: Props): JSX.Element { }: Props): JSX.Element {
const [disabled, setDisabled] = useState(false);
const [dirty, setDirty] = useState(false); const [dirty, setDirty] = useState(false);
const [large, setLarge] = useState(false); const [large, setLarge] = useState(false);
const [attachmentToEdit, setAttachmentToEdit] = useState< const [attachmentToEdit, setAttachmentToEdit] = useState<
@ -275,7 +282,7 @@ export function CompositionArea({
const handleSubmit = useCallback( const handleSubmit = useCallback(
(message: string, mentions: DraftBodyRangesType, timestamp: number) => { (message: string, mentions: DraftBodyRangesType, timestamp: number) => {
emojiButtonRef.current?.close(); emojiButtonRef.current?.close();
onSendMessage({ sendMultiMediaMessage(conversationId, {
draftAttachments, draftAttachments,
mentions, mentions,
message, message,
@ -283,7 +290,7 @@ export function CompositionArea({
}); });
setLarge(false); setLarge(false);
}, },
[draftAttachments, onSendMessage, setLarge] [conversationId, draftAttachments, sendMultiMediaMessage, setLarge]
); );
const launchAttachmentPicker = useCallback(() => { const launchAttachmentPicker = useCallback(() => {
@ -327,12 +334,6 @@ export function CompositionArea({
compositionApi.current = { compositionApi.current = {
isDirty: () => dirty, isDirty: () => dirty,
focusInput, focusInput,
setDisabled,
reset: () => {
if (inputApiRef.current) {
inputApiRef.current.reset();
}
},
resetEmojiResults: () => { resetEmojiResults: () => {
if (inputApiRef.current) { if (inputApiRef.current) {
inputApiRef.current.resetEmojiResults(); inputApiRef.current.resetEmojiResults();
@ -341,6 +342,14 @@ export function CompositionArea({
}; };
} }
useEffect(() => {
if (!inputApiRef.current) {
return;
}
inputApiRef.current.reset();
}, [messageCompositionId]);
const insertEmoji = useCallback( const insertEmoji = useCallback(
(e: EmojiPickDataType) => { (e: EmojiPickDataType) => {
if (inputApiRef.current) { if (inputApiRef.current) {
@ -400,7 +409,7 @@ export function CompositionArea({
voiceNoteAttachment: InMemoryAttachmentDraftType voiceNoteAttachment: InMemoryAttachmentDraftType
) => { ) => {
emojiButtonRef.current?.close(); emojiButtonRef.current?.close();
onSendMessage({ voiceNoteAttachment }); sendMultiMediaMessage(conversationId, { voiceNoteAttachment });
}} }}
startRecording={startRecording} startRecording={startRecording}
/> />
@ -447,7 +456,9 @@ export function CompositionArea({
recentStickers={recentStickers} recentStickers={recentStickers}
clearInstalledStickerPack={clearInstalledStickerPack} clearInstalledStickerPack={clearInstalledStickerPack}
onClickAddPack={onClickAddPack} onClickAddPack={onClickAddPack}
onPickSticker={onPickSticker} onPickSticker={(packId, stickerId) =>
sendStickerMessage(conversationId, { packId, stickerId })
}
clearShowIntroduction={clearShowIntroduction} clearShowIntroduction={clearShowIntroduction}
showPickerHint={showPickerHint} showPickerHint={showPickerHint}
clearShowPickerHint={clearShowPickerHint} clearShowPickerHint={clearShowPickerHint}
@ -690,7 +701,7 @@ export function CompositionArea({
> >
<CompositionInput <CompositionInput
clearQuotedMessage={clearQuotedMessage} clearQuotedMessage={clearQuotedMessage}
disabled={disabled} disabled={isDisabled}
draftBodyRanges={draftBodyRanges} draftBodyRanges={draftBodyRanges}
draftText={draftText} draftText={draftText}
getPreferredBadge={getPreferredBadge} getPreferredBadge={getPreferredBadge}

View file

@ -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 { ToastBlocked } from './ToastBlocked';
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/ToastBlocked',
};
export const _ToastBlocked = (): JSX.Element => (
<ToastBlocked {...defaultProps} />
);
_ToastBlocked.story = {
name: 'ToastBlocked',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('unblockToSend')}</Toast>;
}

View file

@ -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 {...defaultProps} />
);
_ToastBlockedGroup.story = {
name: 'ToastBlockedGroup',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('unblockGroupToSend')}</Toast>;
}

View file

@ -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 {...defaultProps} />
);
_ToastExpired.story = {
name: 'ToastExpired',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('expiredWarning')}</Toast>;
}

View file

@ -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 {...defaultProps} />
);
_ToastInvalidConversation.story = {
name: 'ToastInvalidConversation',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('invalidConversation')}</Toast>;
}

View file

@ -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 {...defaultProps} />
);
_ToastLeftGroup.story = {
name: 'ToastLeftGroup',
};

View file

@ -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 <Toast onClose={onClose}>{i18n('youLeftTheGroup')}</Toast>;
}

View file

@ -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({}); export const CannotMixMultiAndNonMultiAttachments = Template.bind({});
CannotMixMultiAndNonMultiAttachments.args = { CannotMixMultiAndNonMultiAttachments.args = {
toast: { toast: {
@ -98,6 +112,13 @@ Error.args = {
}, },
}; };
export const Expired = Template.bind({});
Expired.args = {
toast: {
toastType: ToastType.Expired,
},
};
export const FailedToDeleteUsername = Template.bind({}); export const FailedToDeleteUsername = Template.bind({});
FailedToDeleteUsername.args = { FailedToDeleteUsername.args = {
toast: { 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({}); export const MaxAttachments = Template.bind({});
MaxAttachments.args = { MaxAttachments.args = {
toast: { toast: {

View file

@ -42,6 +42,14 @@ export function ToastManager({
); );
} }
if (toastType === ToastType.Blocked) {
return <Toast onClose={hideToast}>{i18n('unblockToSend')}</Toast>;
}
if (toastType === ToastType.BlockedGroup) {
return <Toast onClose={hideToast}>{i18n('unblockGroupToSend')}</Toast>;
}
if (toastType === ToastType.CannotMixMultiAndNonMultiAttachments) { if (toastType === ToastType.CannotMixMultiAndNonMultiAttachments) {
return ( return (
<Toast onClose={hideToast}> <Toast onClose={hideToast}>
@ -97,6 +105,10 @@ export function ToastManager({
); );
} }
if (toastType === ToastType.Expired) {
return <Toast onClose={hideToast}>{i18n('expiredWarning')}</Toast>;
}
if (toastType === ToastType.FailedToDeleteUsername) { if (toastType === ToastType.FailedToDeleteUsername) {
return ( return (
<Toast onClose={hideToast}> <Toast onClose={hideToast}>
@ -113,6 +125,14 @@ export function ToastManager({
); );
} }
if (toastType === ToastType.InvalidConversation) {
return <Toast onClose={hideToast}>{i18n('invalidConversation')}</Toast>;
}
if (toastType === ToastType.LeftGroup) {
return <Toast onClose={hideToast}>{i18n('youLeftTheGroup')}</Toast>;
}
if (toastType === ToastType.MaxAttachments) { if (toastType === ToastType.MaxAttachments) {
return <Toast onClose={hideToast}>{i18n('maximumAttachments')}</Toast>; return <Toast onClose={hideToast}>{i18n('maximumAttachments')}</Toast>;
} }

View file

@ -1,52 +1,73 @@
// Copyright 2021 Signal Messenger, LLC // Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import path from 'path'; import path from 'path';
import type { ThunkAction } from 'redux-thunk'; 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 { import type {
AddLinkPreviewActionType, AddLinkPreviewActionType,
RemoveLinkPreviewActionType, RemoveLinkPreviewActionType,
} from './linkPreviews'; } 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 { import {
ADD_PREVIEW as ADD_LINK_PREVIEW, ADD_PREVIEW as ADD_LINK_PREVIEW,
REMOVE_PREVIEW as REMOVE_LINK_PREVIEW, REMOVE_PREVIEW as REMOVE_LINK_PREVIEW,
} from './linkPreviews'; } 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 { LinkPreviewSourceType } from '../../types/LinkPreview';
import { RecordingState } from './audioRecorder'; import { RecordingState } from './audioRecorder';
import { hasLinkPreviewLoaded } from '../../services/LinkPreview';
import { SHOW_TOAST, ToastType } from './toast'; 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 { getMaximumAttachmentSize } from '../../util/attachments';
import { isFileDangerous } from '../../util/isFileDangerous'; import { getRecipientsByConversation } from '../../util/getRecipientsByConversation';
import { isImage, isVideo, stringToMIMEType } from '../../types/MIME';
import { import {
getRenderDetailsForLimit, getRenderDetailsForLimit,
processAttachment, processAttachment,
} from '../../util/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 // State
export type ComposerStateType = { export type ComposerStateType = {
attachments: ReadonlyArray<AttachmentDraftType>; attachments: ReadonlyArray<AttachmentDraftType>;
isDisabled: boolean;
linkPreviewLoading: boolean; linkPreviewLoading: boolean;
linkPreviewResult?: LinkPreviewType; linkPreviewResult?: LinkPreviewType;
messageCompositionId: UUIDStringType;
quotedMessage?: Pick<MessageAttributesType, 'conversationId' | 'quote'>; quotedMessage?: Pick<MessageAttributesType, 'conversationId' | 'quote'>;
shouldSendHighQualityAttachments?: boolean; shouldSendHighQualityAttachments?: boolean;
}; };
@ -58,6 +79,7 @@ const REPLACE_ATTACHMENTS = 'composer/REPLACE_ATTACHMENTS';
const RESET_COMPOSER = 'composer/RESET_COMPOSER'; const RESET_COMPOSER = 'composer/RESET_COMPOSER';
const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING'; const SET_HIGH_QUALITY_SETTING = 'composer/SET_HIGH_QUALITY_SETTING';
const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE'; const SET_QUOTED_MESSAGE = 'composer/SET_QUOTED_MESSAGE';
const SET_COMPOSER_DISABLED = 'composer/SET_COMPOSER_DISABLED';
type AddPendingAttachmentActionType = { type AddPendingAttachmentActionType = {
type: typeof ADD_PENDING_ATTACHMENT; type: typeof ADD_PENDING_ATTACHMENT;
@ -73,6 +95,11 @@ type ResetComposerActionType = {
type: typeof RESET_COMPOSER; type: typeof RESET_COMPOSER;
}; };
type SetComposerDisabledStateActionType = {
type: typeof SET_COMPOSER_DISABLED;
payload: boolean;
};
type SetHighQualitySettingActionType = { type SetHighQualitySettingActionType = {
type: typeof SET_HIGH_QUALITY_SETTING; type: typeof SET_HIGH_QUALITY_SETTING;
payload: boolean; payload: boolean;
@ -89,6 +116,7 @@ type ComposerActionType =
| RemoveLinkPreviewActionType | RemoveLinkPreviewActionType
| ReplaceAttachmentsActionType | ReplaceAttachmentsActionType
| ResetComposerActionType | ResetComposerActionType
| SetComposerDisabledStateActionType
| SetHighQualitySettingActionType | SetHighQualitySettingActionType
| SetQuotedMessageActionType; | SetQuotedMessageActionType;
@ -101,10 +129,204 @@ export const actions = {
removeAttachment, removeAttachment,
replaceAttachments, replaceAttachments,
resetComposer, resetComposer,
setComposerDisabledState,
sendMultiMediaMessage,
sendStickerMessage,
setMediaQualitySetting, setMediaQualitySetting,
setQuotedMessage, setQuotedMessage,
}; };
function sendMultiMediaMessage(
conversationId: string,
options: {
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
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<AttachmentType> = [];
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 // Not cool that we have to pull from ConversationModel here
// but if the current selected conversation isn't the one that we're operating // 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 // 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( function setMediaQualitySetting(
payload: boolean payload: boolean
): SetHighQualitySettingActionType { ): SetHighQualitySettingActionType {
@ -463,7 +694,9 @@ function setQuotedMessage(
export function getEmptyState(): ComposerStateType { export function getEmptyState(): ComposerStateType {
return { return {
attachments: [], attachments: [],
isDisabled: false,
linkPreviewLoading: 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; return state;
} }

View file

@ -6,6 +6,8 @@ import type { ReplacementValuesType } from '../../types/Util';
export enum ToastType { export enum ToastType {
AddingUserToGroup = 'AddingUserToGroup', AddingUserToGroup = 'AddingUserToGroup',
Blocked = 'Blocked',
BlockedGroup = 'BlockedGroup',
CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments', CannotMixMultiAndNonMultiAttachments = 'CannotMixMultiAndNonMultiAttachments',
CannotStartGroupCall = 'CannotStartGroupCall', CannotStartGroupCall = 'CannotStartGroupCall',
CopiedUsername = 'CopiedUsername', CopiedUsername = 'CopiedUsername',
@ -13,8 +15,11 @@ export enum ToastType {
DangerousFileType = 'DangerousFileType', DangerousFileType = 'DangerousFileType',
DeleteForEveryoneFailed = 'DeleteForEveryoneFailed', DeleteForEveryoneFailed = 'DeleteForEveryoneFailed',
Error = 'Error', Error = 'Error',
Expired = 'Expired',
FailedToDeleteUsername = 'FailedToDeleteUsername', FailedToDeleteUsername = 'FailedToDeleteUsername',
FileSize = 'FileSize', FileSize = 'FileSize',
InvalidConversation = 'InvalidConversation',
LeftGroup = 'LeftGroup',
MaxAttachments = 'MaxAttachments', MaxAttachments = 'MaxAttachments',
MessageBodyTooLong = 'MessageBodyTooLong', MessageBodyTooLong = 'MessageBodyTooLong',
PinnedConversationsFull = 'PinnedConversationsFull', PinnedConversationsFull = 'PinnedConversationsFull',

View file

@ -70,8 +70,10 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const { const {
attachments: draftAttachments, attachments: draftAttachments,
isDisabled,
linkPreviewLoading, linkPreviewLoading,
linkPreviewResult, linkPreviewResult,
messageCompositionId,
quotedMessage, quotedMessage,
shouldSendHighQualityAttachments, shouldSendHighQualityAttachments,
} = state.composer; } = state.composer;
@ -81,9 +83,11 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
return { return {
// Base // Base
conversationId: id, conversationId: id,
i18n: getIntl(state),
theme: getTheme(state),
getPreferredBadge: getPreferredBadgeSelector(state), getPreferredBadge: getPreferredBadgeSelector(state),
i18n: getIntl(state),
isDisabled,
messageCompositionId,
theme: getTheme(state),
// AudioCapture // AudioCapture
errorDialogAudioRecorderType: errorDialogAudioRecorderType:
state.audioRecorder.errorDialogAudioRecorderType, state.audioRecorder.errorDialogAudioRecorderType,

View file

@ -27,9 +27,7 @@ export type PropsType = {
| 'onClickAddPack' | 'onClickAddPack'
| 'onCloseLinkPreview' | 'onCloseLinkPreview'
| 'onEditorStateChange' | 'onEditorStateChange'
| 'onPickSticker'
| 'onSelectMediaQuality' | 'onSelectMediaQuality'
| 'onSendMessage'
| 'onTextTooLong' | 'onTextTooLong'
| 'openConversation' | 'openConversation'
>; >;

View file

@ -103,17 +103,23 @@ describe('both/state/ducks/composer', () => {
describe('resetComposer', () => { describe('resetComposer', () => {
it('returns composer back to empty state', () => { it('returns composer back to empty state', () => {
const { resetComposer } = actions; const { resetComposer } = actions;
const emptyState = getEmptyState();
const nextState = reducer( const nextState = reducer(
{ {
attachments: [], attachments: [],
isDisabled: false,
linkPreviewLoading: true, linkPreviewLoading: true,
messageCompositionId: emptyState.messageCompositionId,
quotedMessage: QUOTED_MESSAGE, quotedMessage: QUOTED_MESSAGE,
shouldSendHighQualityAttachments: true, shouldSendHighQualityAttachments: true,
}, },
resetComposer() resetComposer()
); );
assert.deepEqual(nextState, getEmptyState()); assert.deepEqual(nextState, {
...getEmptyState(),
messageCompositionId: nextState.messageCompositionId,
});
}); });
}); });

View file

@ -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<AttachmentDraftType> = []
): Promise<void> {
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))
),
]);
}

View file

@ -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<ConversationAttributesType>
): 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;
}

View file

@ -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<AttachmentDraftType> | undefined,
options: { includePending: boolean }
): boolean {
if (!draftAttachments) {
return false;
}
if (options.includePending) {
return draftAttachments.length > 0;
}
return draftAttachments.some(item => !item.pending);
}

View file

@ -2,27 +2,16 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationAttributesType } from '../model-types'; import type { ConversationAttributesType } from '../model-types';
import type { RecipientsByConversation } from '../state/ducks/stories';
import * as log from '../logging/log'; import * as log from '../logging/log';
import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog'; import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
import { blockSendUntilConversationsAreVerified } from './blockSendUntilConversationsAreVerified'; import { blockSendUntilConversationsAreVerified } from './blockSendUntilConversationsAreVerified';
import { getConversationMembers } from './getConversationMembers'; import { getRecipientsByConversation } from './getRecipientsByConversation';
import { UUID } from '../types/UUID';
import { isNotNil } from './isNotNil';
export async function isCallSafe( export async function isCallSafe(
attributes: ConversationAttributesType attributes: ConversationAttributesType
): Promise<boolean> { ): Promise<boolean> {
const recipientsByConversation: RecipientsByConversation = { const recipientsByConversation = getRecipientsByConversation([attributes]);
[attributes.id]: {
uuids: getConversationMembers(attributes)
.map(member =>
member.uuid ? UUID.checkedLookup(member.uuid).toString() : undefined
)
.filter(isNotNil),
},
};
const callAnyway = await blockSendUntilConversationsAreVerified( const callAnyway = await blockSendUntilConversationsAreVerified(
recipientsByConversation, recipientsByConversation,

View file

@ -10,7 +10,7 @@ import { blockSendUntilConversationsAreVerified } from './blockSendUntilConversa
import { getMessageIdForLogging } from './idForLogging'; import { getMessageIdForLogging } from './idForLogging';
import { isNotNil } from './isNotNil'; import { isNotNil } from './isNotNil';
import { resetLinkPreview } from '../services/LinkPreview'; import { resetLinkPreview } from '../services/LinkPreview';
import type { RecipientsByConversation } from '../state/ducks/stories'; import { getRecipientsByConversation } from './getRecipientsByConversation';
export async function maybeForwardMessage( export async function maybeForwardMessage(
messageAttributes: MessageAttributesType, messageAttributes: MessageAttributesType,
@ -41,12 +41,9 @@ export async function maybeForwardMessage(
throw new Error('Cannot send to group'); throw new Error('Cannot send to group');
} }
const recipientsByConversation: RecipientsByConversation = {}; const recipientsByConversation = getRecipientsByConversation(
conversations.forEach(conversation => { conversations.map(x => x.attributes)
recipientsByConversation[conversation.id] = { );
uuids: conversation.getMemberUuids().map(uuid => uuid.toString()),
};
});
// Verify that all contacts that we're forwarding // Verify that all contacts that we're forwarding
// to are verified and trusted. // to are verified and trusted.

View file

@ -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;
}

View file

@ -6,8 +6,6 @@ import { render, unmountComponentAtNode } from 'react-dom';
import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember'; import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember';
import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin'; import type { ToastAlreadyRequestedToJoin } from '../components/ToastAlreadyRequestedToJoin';
import type { ToastBlocked } from '../components/ToastBlocked';
import type { ToastBlockedGroup } from '../components/ToastBlockedGroup';
import type { import type {
ToastCannotOpenGiftBadge, ToastCannotOpenGiftBadge,
ToastPropsType as ToastCannotOpenGiftBadgePropsType, ToastPropsType as ToastCannotOpenGiftBadgePropsType,
@ -24,7 +22,6 @@ import type {
ToastInternalError, ToastInternalError,
ToastPropsType as ToastInternalErrorPropsType, ToastPropsType as ToastInternalErrorPropsType,
} from '../components/ToastInternalError'; } from '../components/ToastInternalError';
import type { ToastExpired } from '../components/ToastExpired';
import type { import type {
ToastFileSaved, ToastFileSaved,
ToastPropsType as ToastFileSavedPropsType, ToastPropsType as ToastFileSavedPropsType,
@ -34,8 +31,6 @@ import type {
ToastPropsType as ToastFileSizePropsType, ToastPropsType as ToastFileSizePropsType,
} from '../components/ToastFileSize'; } from '../components/ToastFileSize';
import type { ToastGroupLinkCopied } from '../components/ToastGroupLinkCopied'; 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 { ToastLinkCopied } from '../components/ToastLinkCopied';
import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs'; import type { ToastLoadingFullLogs } from '../components/ToastLoadingFullLogs';
import type { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong'; 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 ToastAlreadyGroupMember): void;
export function showToast(Toast: typeof ToastAlreadyRequestedToJoin): 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( export function showToast(
Toast: typeof ToastCannotOpenGiftBadge, Toast: typeof ToastCannotOpenGiftBadge,
props: Omit<ToastCannotOpenGiftBadgePropsType, 'i18n' | 'onClose'> props: Omit<ToastCannotOpenGiftBadgePropsType, 'i18n' | 'onClose'>
@ -69,7 +62,6 @@ export function showToast(
Toast: typeof ToastInternalError, Toast: typeof ToastInternalError,
props: ToastInternalErrorPropsType props: ToastInternalErrorPropsType
): void; ): void;
export function showToast(Toast: typeof ToastExpired): void;
export function showToast( export function showToast(
Toast: typeof ToastFileSaved, Toast: typeof ToastFileSaved,
props: ToastFileSavedPropsType props: ToastFileSavedPropsType
@ -79,8 +71,6 @@ export function showToast(
props: ToastFileSizePropsType props: ToastFileSizePropsType
): void; ): void;
export function showToast(Toast: typeof ToastGroupLinkCopied): 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 ToastLinkCopied): void;
export function showToast(Toast: typeof ToastLoadingFullLogs): void; export function showToast(Toast: typeof ToastLoadingFullLogs): void;
export function showToast(Toast: typeof ToastMessageBodyTooLong): void; export function showToast(Toast: typeof ToastMessageBodyTooLong): void;

View file

@ -12,7 +12,6 @@ import { render } from 'mustache';
import type { AttachmentType } from '../types/Attachment'; import type { AttachmentType } from '../types/Attachment';
import { isGIF } from '../types/Attachment'; import { isGIF } from '../types/Attachment';
import * as Stickers from '../types/Stickers'; import * as Stickers from '../types/Stickers';
import * as Errors from '../types/errors';
import type { DraftBodyRangesType } from '../types/Util'; import type { DraftBodyRangesType } from '../types/Util';
import type { MIMEType } from '../types/MIME'; import type { MIMEType } from '../types/MIME';
import type { ConversationModel } from '../models/conversations'; import type { ConversationModel } from '../models/conversations';
@ -45,15 +44,10 @@ import * as log from '../logging/log';
import type { EmbeddedContactType } from '../types/EmbeddedContact'; import type { EmbeddedContactType } from '../types/EmbeddedContact';
import { createConversationView } from '../state/roots/createConversationView'; import { createConversationView } from '../state/roots/createConversationView';
import type { CompositionAPIType } from '../components/CompositionArea'; import type { CompositionAPIType } from '../components/CompositionArea';
import { ToastBlocked } from '../components/ToastBlocked';
import { ToastBlockedGroup } from '../components/ToastBlockedGroup';
import { ToastConversationArchived } from '../components/ToastConversationArchived'; import { ToastConversationArchived } from '../components/ToastConversationArchived';
import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread'; import { ToastConversationMarkedUnread } from '../components/ToastConversationMarkedUnread';
import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived'; import { ToastConversationUnarchived } from '../components/ToastConversationUnarchived';
import { ToastDangerousFileType } from '../components/ToastDangerousFileType'; 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 { ToastMessageBodyTooLong } from '../components/ToastMessageBodyTooLong';
import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound'; import { ToastOriginalMessageNotFound } from '../components/ToastOriginalMessageNotFound';
import { ToastReactionFailed } from '../components/ToastReactionFailed'; import { ToastReactionFailed } from '../components/ToastReactionFailed';
@ -65,7 +59,6 @@ import { deleteDraftAttachment } from '../util/deleteDraftAttachment';
import { retryMessageSend } from '../util/retryMessageSend'; import { retryMessageSend } from '../util/retryMessageSend';
import { isNotNil } from '../util/isNotNil'; import { isNotNil } from '../util/isNotNil';
import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser'; import { openLinkInWebBrowser } from '../util/openLinkInWebBrowser';
import { resolveAttachmentDraftData } from '../util/resolveAttachmentDraftData';
import { showToast } from '../util/showToast'; import { showToast } from '../util/showToast';
import { UUIDKind } from '../types/UUID'; import { UUIDKind } from '../types/UUID';
import type { UUIDStringType } 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 { MediaGallery } from '../components/conversation/media-gallery/MediaGallery';
import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent'; import type { ItemClickEvent } from '../components/conversation/media-gallery/types/ItemClickEvent';
import { import {
getLinkPreviewForSend,
maybeGrabLinkPreview, maybeGrabLinkPreview,
removeLinkPreview, removeLinkPreview,
resetLinkPreview,
suspendLinkPreviews, suspendLinkPreviews,
} from '../services/LinkPreview'; } from '../services/LinkPreview';
import { LinkPreviewSourceType } from '../types/LinkPreview'; import { LinkPreviewSourceType } from '../types/LinkPreview';
import { closeLightbox, showLightbox } from '../util/showLightbox'; import { closeLightbox, showLightbox } from '../util/showLightbox';
import { saveAttachment } from '../util/saveAttachment'; import { saveAttachment } from '../util/saveAttachment';
import { SECOND } from '../util/durations'; import { SECOND } from '../util/durations';
import { blockSendUntilConversationsAreVerified } from '../util/blockSendUntilConversationsAreVerified';
import { SafetyNumberChangeSource } from '../components/SafetyNumberChangeDialog';
import { startConversation } from '../util/startConversation'; import { startConversation } from '../util/startConversation';
import { longRunningTaskWrapper } from '../util/longRunningTaskWrapper'; 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<ConversationModel> { export class ConversationView extends window.Backbone.View<ConversationModel> {
private debouncedSaveDraft: ( private debouncedSaveDraft: (
messageText: string, messageText: string,
@ -182,7 +169,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
private compositionApi: { private compositionApi: {
current: CompositionAPIType; current: CompositionAPIType;
} = { current: undefined }; } = { current: undefined };
private sendStart?: number;
// Sub-views // Sub-views
private contactModalView?: Backbone.View; private contactModalView?: Backbone.View;
@ -432,8 +418,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
id: this.model.id, id: this.model.id,
compositionApi: this.compositionApi, compositionApi: this.compositionApi,
onClickAddPack: () => this.showStickerManager(), onClickAddPack: () => this.showStickerManager(),
onPickSticker: (packId: string, stickerId: number) =>
this.sendStickerMessage({ packId, stickerId }),
onEditorStateChange: ( onEditorStateChange: (
msg: string, msg: string,
bodyRanges: DraftBodyRangesType, bodyRanges: DraftBodyRangesType,
@ -473,26 +457,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
}, },
openConversation: this.openConversation.bind(this), openConversation: this.openConversation.bind(this),
onSendMessage: ({
draftAttachments,
mentions = [],
message = '',
timestamp,
voiceNoteAttachment,
}: {
draftAttachments?: ReadonlyArray<AttachmentType>;
mentions?: DraftBodyRangesType;
message?: string;
timestamp?: number;
voiceNoteAttachment?: AttachmentType;
}): void => {
this.sendMessage(message, mentions, {
draftAttachments,
timestamp,
voiceNoteAttachment,
});
},
}; };
// createConversationView root // createConversationView root
@ -1655,198 +1619,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
); );
} }
async sendStickerMessage(options: {
packId: string;
stickerId: number;
}): Promise<void> {
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<AttachmentType>;
timestamp?: number;
voiceNoteAttachment?: AttachmentType;
} = {}
): Promise<void> {
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<AttachmentType> = [];
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 { focusMessageField(): void {
if (this.panels && this.panels.length) { if (this.panels && this.panels.length) {
return; return;
@ -1855,14 +1627,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
this.compositionApi.current?.focusInput(); this.compositionApi.current?.focusInput();
} }
disableMessageField(): void {
this.compositionApi.current?.setDisabled(true);
}
enableMessageField(): void {
this.compositionApi.current?.setDisabled(false);
}
resetEmojiResults(): void { resetEmojiResults(): void {
this.compositionApi.current?.resetEmojiResults(); this.compositionApi.current?.resetEmojiResults();
} }
@ -1974,7 +1738,7 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
quote, quote,
}); });
this.enableMessageField(); window.reduxActions.composer.setComposerDisabledState(false);
this.focusMessageField(); this.focusMessageField();
} else { } else {
window.reduxActions.composer.setQuotedMessage(undefined); window.reduxActions.composer.setQuotedMessage(undefined);