Moves sendMessage and friends to redux

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

View file

@ -35,7 +35,9 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
addAttachment: action('addAttachment'),
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'),

View file

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

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastBlocked } from './ToastBlocked';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastBlocked',
};
export const _ToastBlocked = (): JSX.Element => (
<ToastBlocked {...defaultProps} />
);
_ToastBlocked.story = {
name: 'ToastBlocked',
};

View file

@ -1,15 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastBlocked({ i18n, onClose }: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('unblockToSend')}</Toast>;
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastBlockedGroup } from './ToastBlockedGroup';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastBlockedGroup',
};
export const _ToastBlockedGroup = (): JSX.Element => (
<ToastBlockedGroup {...defaultProps} />
);
_ToastBlockedGroup.story = {
name: 'ToastBlockedGroup',
};

View file

@ -1,15 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastBlockedGroup({ i18n, onClose }: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('unblockGroupToSend')}</Toast>;
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastExpired } from './ToastExpired';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastExpired',
};
export const _ToastExpired = (): JSX.Element => (
<ToastExpired {...defaultProps} />
);
_ToastExpired.story = {
name: 'ToastExpired',
};

View file

@ -1,15 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastExpired({ i18n, onClose }: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('expiredWarning')}</Toast>;
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastInvalidConversation } from './ToastInvalidConversation';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastInvalidConversation',
};
export const _ToastInvalidConversation = (): JSX.Element => (
<ToastInvalidConversation {...defaultProps} />
);
_ToastInvalidConversation.story = {
name: 'ToastInvalidConversation',
};

View file

@ -1,18 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastInvalidConversation({
i18n,
onClose,
}: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('invalidConversation')}</Toast>;
}

View file

@ -1,28 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import { ToastLeftGroup } from './ToastLeftGroup';
import { setupI18n } from '../util/setupI18n';
import enMessages from '../../_locales/en/messages.json';
const i18n = setupI18n('en', enMessages);
const defaultProps = {
i18n,
onClose: action('onClose'),
};
export default {
title: 'Components/ToastLeftGroup',
};
export const _ToastLeftGroup = (): JSX.Element => (
<ToastLeftGroup {...defaultProps} />
);
_ToastLeftGroup.story = {
name: 'ToastLeftGroup',
};

View file

@ -1,15 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Toast } from './Toast';
type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function ToastLeftGroup({ i18n, onClose }: PropsType): JSX.Element {
return <Toast onClose={onClose}>{i18n('youLeftTheGroup')}</Toast>;
}

View file

@ -49,6 +49,20 @@ AddingUserToGroup.args = {
},
};
export const Blocked = Template.bind({});
Blocked.args = {
toast: {
toastType: ToastType.Blocked,
},
};
export const BlockedGroup = Template.bind({});
BlockedGroup.args = {
toast: {
toastType: ToastType.BlockedGroup,
},
};
export const CannotMixMultiAndNonMultiAttachments = Template.bind({});
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: {

View file

@ -42,6 +42,14 @@ export function ToastManager({
);
}
if (toastType === ToastType.Blocked) {
return <Toast onClose={hideToast}>{i18n('unblockToSend')}</Toast>;
}
if (toastType === ToastType.BlockedGroup) {
return <Toast onClose={hideToast}>{i18n('unblockGroupToSend')}</Toast>;
}
if (toastType === ToastType.CannotMixMultiAndNonMultiAttachments) {
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>;
}

View file

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

View file

@ -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',

View file

@ -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,

View file

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

View file

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

View file

@ -0,0 +1,29 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentDraftType } from '../types/Attachment';
import { strictAssert } from './assert';
import { deleteDraftAttachment } from './deleteDraftAttachment';
export async function clearConversationDraftAttachments(
conversationId: string,
draftAttachments: ReadonlyArray<AttachmentDraftType> = []
): Promise<void> {
const conversation = window.ConversationController.get(conversationId);
strictAssert(conversation, 'no conversation found');
conversation.set({
draftAttachments: [],
draftChanged: true,
});
window.reduxActions.composer.replaceAttachments(conversationId, []);
// We're fine doing this all at once; at most it should be 32 attachments
await Promise.all([
window.Signal.Data.updateConversation(conversation.attributes),
Promise.all(
draftAttachments.map(attachment => deleteDraftAttachment(attachment))
),
]);
}

View file

@ -0,0 +1,27 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationAttributesType } from '../model-types';
import type { RecipientsByConversation } from '../state/ducks/stories';
import { getConversationMembers } from './getConversationMembers';
import { UUID } from '../types/UUID';
import { isNotNil } from './isNotNil';
export function getRecipientsByConversation(
conversations: Array<ConversationAttributesType>
): RecipientsByConversation {
const recipientsByConversation: RecipientsByConversation = {};
conversations.forEach(attributes => {
recipientsByConversation[attributes.id] = {
uuids: getConversationMembers(attributes)
.map(member =>
member.uuid ? UUID.checkedLookup(member.uuid).toString() : undefined
)
.filter(isNotNil),
};
});
return recipientsByConversation;
}

View file

@ -0,0 +1,19 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { AttachmentDraftType } from '../types/Attachment';
export function hasDraftAttachments(
draftAttachments: Array<AttachmentDraftType> | undefined,
options: { includePending: boolean }
): boolean {
if (!draftAttachments) {
return false;
}
if (options.includePending) {
return draftAttachments.length > 0;
}
return draftAttachments.some(item => !item.pending);
}

View file

@ -2,27 +2,16 @@
// SPDX-License-Identifier: AGPL-3.0-only
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,

View file

@ -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.

View file

@ -0,0 +1,62 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { ConversationAttributesType } from '../model-types';
import { ToastType } from '../state/ducks/toast';
import {
isDirectConversation,
isGroupV1,
isGroupV2,
} from './whatTypeOfConversation';
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
export function shouldShowInvalidMessageToast(
conversationAttributes: ConversationAttributesType,
messageText?: string
): ToastType | undefined {
if (window.reduxStore.getState().expiration.hasExpired) {
return ToastType.Expired;
}
const isValid =
isDirectConversation(conversationAttributes) ||
isGroupV1(conversationAttributes) ||
isGroupV2(conversationAttributes);
if (!isValid) {
return ToastType.InvalidConversation;
}
const { e164, uuid } = conversationAttributes;
if (
isDirectConversation(conversationAttributes) &&
((e164 && window.storage.blocked.isBlocked(e164)) ||
(uuid && window.storage.blocked.isUuidBlocked(uuid)))
) {
return ToastType.Blocked;
}
const { groupId } = conversationAttributes;
if (
!isDirectConversation(conversationAttributes) &&
groupId &&
window.storage.blocked.isGroupBlocked(groupId)
) {
return ToastType.BlockedGroup;
}
if (
!isDirectConversation(conversationAttributes) &&
conversationAttributes.left
) {
return ToastType.LeftGroup;
}
if (messageText && messageText.length > MAX_MESSAGE_BODY_LENGTH) {
return ToastType.MessageBodyTooLong;
}
return undefined;
}

View file

@ -6,8 +6,6 @@ import { render, unmountComponentAtNode } from 'react-dom';
import type { ToastAlreadyGroupMember } from '../components/ToastAlreadyGroupMember';
import type { 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;

View file

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