Moves sendMessage and friends to redux
This commit is contained in:
parent
7ea38bb1a9
commit
2378776e1b
27 changed files with 517 additions and 537 deletions
|
@ -35,7 +35,9 @@ const useProps = (overrideProps: Partial<Props> = {}): 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> = {}): Props => ({
|
|||
recentStickers: [],
|
||||
clearInstalledStickerPack: action('clearInstalledStickerPack'),
|
||||
onClickAddPack: action('onClickAddPack'),
|
||||
onPickSticker: action('onPickSticker'),
|
||||
sendStickerMessage: action('sendStickerMessage'),
|
||||
clearShowIntroduction: action('clearShowIntroduction'),
|
||||
showPickerHint: false,
|
||||
clearShowPickerHint: action('clearShowPickerHint'),
|
||||
|
|
|
@ -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<File>;
|
||||
}) => unknown;
|
||||
onSelectMediaQuality(isHQ: boolean): unknown;
|
||||
onSendMessage(options: {
|
||||
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
|
||||
mentions?: DraftBodyRangesType;
|
||||
message?: string;
|
||||
timestamp?: number;
|
||||
voiceNoteAttachment?: InMemoryAttachmentDraftType;
|
||||
}): unknown;
|
||||
sendStickerMessage(
|
||||
id: string,
|
||||
opts: { packId: string; stickerId: number }
|
||||
): unknown;
|
||||
sendMultiMediaMessage(
|
||||
conversationId: string,
|
||||
options: {
|
||||
draftAttachments?: ReadonlyArray<AttachmentDraftType>;
|
||||
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({
|
|||
>
|
||||
<CompositionInput
|
||||
clearQuotedMessage={clearQuotedMessage}
|
||||
disabled={disabled}
|
||||
disabled={isDisabled}
|
||||
draftBodyRanges={draftBodyRanges}
|
||||
draftText={draftText}
|
||||
getPreferredBadge={getPreferredBadge}
|
||||
|
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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',
|
||||
};
|
|
@ -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>;
|
||||
}
|
|
@ -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: {
|
||||
|
|
|
@ -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) {
|
||||
return (
|
||||
<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) {
|
||||
return (
|
||||
<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) {
|
||||
return <Toast onClose={hideToast}>{i18n('maximumAttachments')}</Toast>;
|
||||
}
|
||||
|
|
|
@ -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<AttachmentDraftType>;
|
||||
isDisabled: boolean;
|
||||
linkPreviewLoading: boolean;
|
||||
linkPreviewResult?: LinkPreviewType;
|
||||
messageCompositionId: UUIDStringType;
|
||||
quotedMessage?: Pick<MessageAttributesType, 'conversationId' | 'quote'>;
|
||||
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<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
|
||||
// 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;
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -27,9 +27,7 @@ export type PropsType = {
|
|||
| 'onClickAddPack'
|
||||
| 'onCloseLinkPreview'
|
||||
| 'onEditorStateChange'
|
||||
| 'onPickSticker'
|
||||
| 'onSelectMediaQuality'
|
||||
| 'onSendMessage'
|
||||
| 'onTextTooLong'
|
||||
| 'openConversation'
|
||||
>;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
29
ts/util/clearConversationDraftAttachments.ts
Normal file
29
ts/util/clearConversationDraftAttachments.ts
Normal 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))
|
||||
),
|
||||
]);
|
||||
}
|
27
ts/util/getRecipientsByConversation.ts
Normal file
27
ts/util/getRecipientsByConversation.ts
Normal 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;
|
||||
}
|
19
ts/util/hasDraftAttachments.ts
Normal file
19
ts/util/hasDraftAttachments.ts
Normal 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);
|
||||
}
|
|
@ -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<boolean> {
|
||||
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,
|
||||
|
|
|
@ -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.
|
||||
|
|
62
ts/util/shouldShowInvalidMessageToast.ts
Normal file
62
ts/util/shouldShowInvalidMessageToast.ts
Normal 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;
|
||||
}
|
|
@ -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<ToastCannotOpenGiftBadgePropsType, 'i18n' | 'onClose'>
|
||||
|
@ -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;
|
||||
|
|
|
@ -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<ConversationModel> {
|
||||
private debouncedSaveDraft: (
|
||||
messageText: string,
|
||||
|
@ -182,7 +169,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
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<ConversationModel> {
|
|||
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<ConversationModel> {
|
|||
},
|
||||
|
||||
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
|
||||
|
@ -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 {
|
||||
if (this.panels && this.panels.length) {
|
||||
return;
|
||||
|
@ -1855,14 +1627,6 @@ export class ConversationView extends window.Backbone.View<ConversationModel> {
|
|||
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<ConversationModel> {
|
|||
quote,
|
||||
});
|
||||
|
||||
this.enableMessageField();
|
||||
window.reduxActions.composer.setComposerDisabledState(false);
|
||||
this.focusMessageField();
|
||||
} else {
|
||||
window.reduxActions.composer.setQuotedMessage(undefined);
|
||||
|
|
Loading…
Reference in a new issue