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'),
|
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'),
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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({});
|
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: {
|
||||||
|
|
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -27,9 +27,7 @@ export type PropsType = {
|
||||||
| 'onClickAddPack'
|
| 'onClickAddPack'
|
||||||
| 'onCloseLinkPreview'
|
| 'onCloseLinkPreview'
|
||||||
| 'onEditorStateChange'
|
| 'onEditorStateChange'
|
||||||
| 'onPickSticker'
|
|
||||||
| 'onSelectMediaQuality'
|
| 'onSelectMediaQuality'
|
||||||
| 'onSendMessage'
|
|
||||||
| 'onTextTooLong'
|
| 'onTextTooLong'
|
||||||
| 'openConversation'
|
| 'openConversation'
|
||||||
>;
|
>;
|
||||||
|
|
|
@ -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,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
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
|
// 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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
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 { 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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue