Merge delete for me/everyone into one modal

This commit is contained in:
Jamie Kyle 2023-04-10 14:38:34 -07:00 committed by GitHub
parent c956c0e025
commit 822b162136
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 658 additions and 672 deletions

View file

@ -2383,7 +2383,7 @@
},
"icu:deleteMessage": {
"messageformat": "Delete message for me",
"description": "Shown on the drop-down menu for an individual message, deletes single message"
"description": "(deleted 04/06/2023) Shown on the drop-down menu for an individual message, deletes single message"
},
"deleteMessageForEveryone": {
"message": "Delete message for everyone",
@ -2391,7 +2391,11 @@
},
"icu:deleteMessageForEveryone": {
"messageformat": "Delete message for everyone",
"description": "Shown on the drop-down menu for an individual message, deletes single message for everyone"
"description": "(deleted 04/06/2023) Shown on the drop-down menu for an individual message, deletes single message for everyone"
},
"icu:MessageContextMenu__deleteMessage": {
"messageformat": "Delete message",
"description": "Show on the drop-down menu for an individual message, opens a modal to select if you want to 'delete for me' or 'delete for everyone'"
},
"deleteMessages": {
"message": "Delete",
@ -8953,6 +8957,10 @@
},
"icu:ConfirmDeleteForMeModal--title": {
"messageformat": "Delete {count, plural, one {# message} other {# messages}}?",
"description": "(deleted 04/06/2023) delete selected messages > confirmation modal > title"
},
"icu:DeleteMessagesModal--title": {
"messageformat": "Delete {count, plural, one {message} other {# messages}}?",
"description": "delete selected messages > confirmation modal > title"
},
"icu:SelectModeActions__confirmDelete--description": {
@ -8961,6 +8969,10 @@
},
"icu:ConfirmDeleteForMeModal--description": {
"messageformat": "{count, plural, one {This message} other {These messages}} will be deleted from this device.",
"description": "(deleted 04/06/2023) delete selected messages > confirmation modal > description"
},
"icu:DeleteMessagesModal--description": {
"messageformat": "Who would you like to delete {count, plural, one {this message} other {these messages}} for?",
"description": "delete selected messages > confirmation modal > description"
},
"icu:SelectModeActions__confirmDelete--confirm": {
@ -8969,7 +8981,19 @@
},
"icu:ConfirmDeleteForMeModal--confirm": {
"messageformat": "Delete for me",
"description": "delete selected messages > confirmation modal > button"
"description": "(deleted 03/24/2023) delete selected messages > confirmation modal > button"
},
"icu:DeleteMessagesModal--deleteForMe": {
"messageformat": "Delete for me",
"description": "delete selected messages > confirmation modal > delete for me"
},
"icu:DeleteMessagesModal--deleteForEveryone": {
"messageformat": "Delete for everyone",
"description": "delete selected messages > confirmation modal > delete for everyone"
},
"icu:DeleteMessagesModal__toast--TooManyMessagesToDeleteForEveryone": {
"messageformat": "You can only select up to {count, plural, one {# message} other {# messages}} to delete for everyone",
"description": "delete selected messages > confirmation modal > deleted for everyone (disabled) > toast > too many messages to 'delete for everyone'"
},
"icu:SelectModeActions__toast--TooManyMessagesToForward": {
"messageformat": "You can only forward up to 30 messages",

View file

@ -0,0 +1,6 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
.DeleteMessagesModal__ModalHost__width-container {
min-width: fit-content;
}

View file

@ -71,6 +71,7 @@
@import './components/CustomColorEditor.scss';
@import './components/CustomizingPreferredReactionsModal.scss';
@import './components/DebugLogWindow.scss';
@import './components/DeleteMessagesModal.scss';
@import './components/DisappearingTimeDialog.scss';
@import './components/DisappearingTimerSelect.scss';
@import './components/EditConversationAttributesModal.scss';

View file

@ -1730,22 +1730,13 @@ export async function startApp(): Promise<void> {
event.preventDefault();
event.stopPropagation();
showConfirmationDialog({
dialogName: 'ConfirmDeleteForMeModal',
confirmStyle: 'negative',
title: window.i18n('icu:ConfirmDeleteForMeModal--title', {
count: messageIds.length,
}),
description: window.i18n(
'icu:ConfirmDeleteForMeModal--description',
{ count: messageIds.length }
),
okText: window.i18n('icu:ConfirmDeleteForMeModal--confirm'),
resolve: () => {
window.reduxActions.conversations.deleteMessages({
window.reduxActions.globalModals.toggleDeleteMessagesModal({
conversationId: conversation.id,
messageIds,
});
onDelete() {
if (selectedMessageIds != null) {
window.reduxActions.conversations.toggleSelectMode(false);
}
},
});
@ -1771,7 +1762,12 @@ export async function startApp(): Promise<void> {
event.stopPropagation();
window.reduxActions.globalModals.toggleForwardMessagesModal(
messageIds
messageIds,
() => {
if (selectedMessageIds != null) {
window.reduxActions.conversations.toggleSelectMode(false);
}
}
);
return;

View file

@ -8,11 +8,7 @@ import Measure from 'react-measure';
import type { ListRowProps } from 'react-virtualized';
import type { ConversationType } from '../state/ducks/conversations';
import type {
LocalizerType,
ReplacementValuesType,
ThemeType,
} from '../types/Util';
import type { LocalizerType, ThemeType } from '../types/Util';
import { ToastType } from '../types/Toast';
import { filterAndSortConversationsByRecent } from '../util/filterAndSortConversations';
import { ConfirmationDialog } from './ConfirmationDialog';
@ -26,6 +22,7 @@ import { SearchInput } from './SearchInput';
import { useRestoreFocus } from '../hooks/useRestoreFocus';
import { ListView } from './ListView';
import { ListTile } from './ListTile';
import type { ShowToastAction } from '../state/ducks/toast';
type OwnProps = {
i18n: LocalizerType;
@ -45,7 +42,7 @@ type DispatchProps = {
onFailure?: () => unknown;
}
) => void;
showToast: (toastType: ToastType, parameters?: ReplacementValuesType) => void;
showToast: ShowToastAction;
};
export type Props = OwnProps & DispatchProps;
@ -225,14 +222,20 @@ export function AddUserToAnotherGroupModal({
text: i18n('icu:AddUserToAnotherGroupModal__confirm-add'),
style: 'affirmative',
action: () => {
showToast(ToastType.AddingUserToGroup, {
showToast({
toastType: ToastType.AddingUserToGroup,
parameters: {
contact: contact.title,
},
});
addMembersToGroup(selectedGroupId, [contact.id], {
onSuccess: () =>
showToast(ToastType.UserAddedToGroup, {
showToast({
toastType: ToastType.UserAddedToGroup,
parameters: {
contact: contact.title,
group: selectedGroup.title,
},
}),
});
toggleAddUserToAnotherGroupModal(undefined);

View file

@ -7,9 +7,9 @@ import classNames from 'classnames';
import type { ExecuteMenuRoleType } from './TitleBarContainer';
import type { MenuOptionsType, MenuActionType } from '../types/menu';
import type { ToastType } from '../types/Toast';
import type { AnyToast } from '../types/Toast';
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
import type { LocalizerType, ReplacementValuesType } from '../types/Util';
import type { LocalizerType } from '../types/Util';
import { ThemeType } from '../types/Util';
import { AppViewType } from '../state/ducks/app';
import { SmartInstallScreen } from '../state/smart/InstallScreen';
@ -51,10 +51,7 @@ type PropsType = {
executeMenuAction: (action: MenuActionType) => void;
hideToast: () => unknown;
titleBarDoubleClick: () => void;
toast?: {
toastType: ToastType;
parameters?: ReplacementValuesType;
};
toast?: AnyToast;
scrollToMessage: (conversationId: string, messageId: string) => unknown;
toggleStoriesView: () => unknown;
viewStory: ViewStoryActionCreatorType;

View file

@ -132,7 +132,6 @@ const useProps = (overrideProps: Partial<Props> = {}): Props => ({
renderSmartCompositionRecordingDraft: _ => <div>RECORDING DRAFT</div>,
// Select mode
selectedMessageIds: undefined,
lastSelectedMessage: undefined,
toggleSelectMode: action('toggleSelectMode'),
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
});

View file

@ -39,7 +39,6 @@ import { AudioCapture } from './conversation/AudioCapture';
import { CompositionUpload } from './CompositionUpload';
import type {
ConversationType,
MessageTimestamps,
PushPanelForConversationActionType,
ShowConversationType,
} from '../state/ducks/conversations';
@ -149,7 +148,6 @@ export type OwnProps = Readonly<{
props: SmartCompositionRecordingDraftProps
) => JSX.Element | null;
selectedMessageIds: ReadonlyArray<string> | undefined;
lastSelectedMessage: MessageTimestamps | undefined;
toggleSelectMode: (on: boolean) => void;
toggleForwardMessagesModal: (
messageIds: ReadonlyArray<string>,
@ -287,7 +285,6 @@ export function CompositionArea({
renderSmartCompositionRecordingDraft,
// Selected messages
selectedMessageIds,
lastSelectedMessage,
toggleSelectMode,
toggleForwardMessagesModal,
}: Props): JSX.Element | null {
@ -560,12 +557,13 @@ export function CompositionArea({
toggleSelectMode(false);
}}
onDeleteMessages={() => {
window.reduxActions.conversations.deleteMessages({
window.reduxActions.globalModals.toggleDeleteMessagesModal({
conversationId,
lastSelectedMessage,
messageIds: selectedMessageIds,
});
onDelete() {
toggleSelectMode(false);
},
});
}}
onForwardMessages={() => {
if (selectedMessageIds.length > 0) {

View file

@ -16,6 +16,8 @@ export type ActionSpec = {
action: () => unknown;
style?: 'affirmative' | 'negative';
autoClose?: boolean;
disabled?: boolean;
'aria-disabled'?: boolean;
} & (
| {
text: string;
@ -130,7 +132,8 @@ export const ConfirmationDialog = React.memo(function ConfirmationDialogInner({
? action.id ?? action.text
: action.id
}
disabled={isSpinning}
disabled={action.disabled || isSpinning}
aria-disabled={action['aria-disabled']}
onClick={() => {
action.action();
if (action.autoClose !== false) {
@ -165,6 +168,9 @@ export const ConfirmationDialog = React.memo(function ConfirmationDialogInner({
onTopOfEverything={onTopOfEverything}
overlayStyles={overlayStyles}
theme={theme}
moduleClassName={
moduleClassName ? `${moduleClassName}__ModalHost` : undefined
}
>
<animated.div style={modalStyles}>
<ModalPage

View file

@ -0,0 +1,76 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { ActionSpec } from './ConfirmationDialog';
import { ConfirmationDialog } from './ConfirmationDialog';
import type { LocalizerType } from '../types/Util';
import type { ShowToastAction } from '../state/ducks/toast';
import { ToastType } from '../types/Toast';
type DeleteMessagesModalProps = Readonly<{
canDeleteForEveryone: boolean;
i18n: LocalizerType;
messageCount: number;
onClose: () => void;
onDeleteForMe: () => void;
onDeleteForEveryone: () => void;
showToast: ShowToastAction;
}>;
const MAX_DELETE_FOR_EVERYONE = 30;
export default function DeleteMessagesModal({
canDeleteForEveryone,
i18n,
messageCount,
onClose,
onDeleteForMe,
onDeleteForEveryone,
showToast,
}: DeleteMessagesModalProps): JSX.Element {
const actions: Array<ActionSpec> = [];
actions.push({
action: onDeleteForMe,
style: 'negative',
text: i18n('icu:DeleteMessagesModal--deleteForMe'),
});
if (canDeleteForEveryone) {
const tooManyMessages = messageCount > MAX_DELETE_FOR_EVERYONE;
actions.push({
'aria-disabled': tooManyMessages,
autoClose: !tooManyMessages,
action: () => {
if (tooManyMessages) {
showToast({
toastType: ToastType.TooManyMessagesToDeleteForEveryone,
parameters: { count: MAX_DELETE_FOR_EVERYONE },
});
} else {
onDeleteForEveryone();
}
},
style: 'negative',
text: i18n('icu:DeleteMessagesModal--deleteForEveryone'),
});
}
return (
<ConfirmationDialog
actions={actions}
dialogName="ConfirmDeleteForMeModal"
i18n={i18n}
onClose={onClose}
title={i18n('icu:DeleteMessagesModal--title', {
count: messageCount,
})}
moduleClassName="DeleteMessagesModal"
>
{i18n('icu:DeleteMessagesModal--description', {
count: messageCount,
})}
</ConfirmationDialog>
);
}

View file

@ -38,7 +38,7 @@ export class ErrorBoundary extends React.PureComponent<Props, State> {
`\nerrorInfo: ${errorInfo.componentStack}`
);
if (window.reduxActions) {
window.reduxActions.toast.showToast(ToastType.Error);
window.reduxActions.toast.showToast({ toastType: ToastType.Error });
}
if (closeView) {
closeView();

View file

@ -127,7 +127,7 @@ export function ForwardMessagesModal({
const forwardMessages = React.useCallback(() => {
if (!canForwardMessages) {
showToast(ToastType.CannotForwardEmptyMessage);
showToast({ toastType: ToastType.CannotForwardEmptyMessage });
return;
}
const conversationIds = selectedContacts.map(contact => contact.id);

View file

@ -5,6 +5,7 @@ import React from 'react';
import type {
AuthorizeArtCreatorDataType,
ContactModalStateType,
DeleteMessagesPropsType,
EditHistoryMessagesType,
ForwardMessagesPropsType,
SafetyNumberChangedBlockingDataType,
@ -38,6 +39,9 @@ export type PropsType = {
description?: string;
title?: string;
}) => JSX.Element;
// DeleteMessageModal
deleteMessagesProps: DeleteMessagesPropsType | undefined;
renderDeleteMessagesModal: () => JSX.Element;
// ForwardMessageModal
forwardMessagesProps: ForwardMessagesPropsType | undefined;
renderForwardMessagesModal: () => JSX.Element;
@ -92,6 +96,9 @@ export function GlobalModalContainer({
// ErrorModal
errorModalProps,
renderErrorModal,
// DeleteMessageModal
deleteMessagesProps,
renderDeleteMessagesModal,
// ForwardMessageModal
forwardMessagesProps,
renderForwardMessagesModal,
@ -158,6 +165,10 @@ export function GlobalModalContainer({
return renderEditHistoryMessagesModal();
}
if (deleteMessagesProps) {
return renderDeleteMessagesModal();
}
if (forwardMessagesProps) {
return renderForwardMessagesModal();
}

View file

@ -6,7 +6,7 @@ import classNames from 'classnames';
import type { ConversationType } from '../state/ducks/conversations';
import type { LocalizerType } from '../types/Util';
import type { MyStoryType, StoryViewType } from '../types/Stories';
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
import type { ShowToastAction } from '../state/ducks/toast';
import { Avatar, AvatarSize } from './Avatar';
import { HasStories, ResolvedSendStatus } from '../types/Stories';
import { MessageTimestamp } from './conversation/MessageTimestamp';
@ -24,7 +24,7 @@ export type PropsType = {
onClick: () => unknown;
onMediaPlaybackStart: () => void;
queueStoryDownload: (storyId: string) => unknown;
showToast: ShowToastActionCreatorType;
showToast: ShowToastAction;
};
function getNewestMyStory(story: MyStoryType): StoryViewType {

View file

@ -28,7 +28,7 @@ import { PanelRow } from './conversation/conversation-details/PanelRow';
import type { ProfileDataType } from '../state/ducks/conversations';
import { UsernameEditState } from '../state/ducks/usernameEnums';
import { ToastType } from '../types/Toast';
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
import type { ShowToastAction } from '../state/ducks/toast';
import { getEmojiData, unifiedToEmoji } from './emoji/lib';
import { assertDev } from '../util/assert';
import { missingCaseError } from '../util/missingCaseError';
@ -85,7 +85,7 @@ type PropsActionType = {
saveAvatarToDisk: SaveAvatarToDiskActionType;
setUsernameEditState: (editState: UsernameEditState) => void;
deleteUsername: () => void;
showToast: ShowToastActionCreatorType;
showToast: ShowToastAction;
openUsernameReservationModal: () => void;
};
@ -525,7 +525,7 @@ export function ProfileEditor({
'Should not be visible without username'
);
void window.navigator.clipboard.writeText(username);
showToast(ToastType.CopiedUsername);
showToast({ toastType: ToastType.CopiedUsername });
},
},
{
@ -540,7 +540,7 @@ export function ProfileEditor({
void window.navigator.clipboard.writeText(
generateUsernameLink(username)
);
showToast(ToastType.CopiedUsernameLink);
showToast({ toastType: ToastType.CopiedUsernameLink });
},
},
{

View file

@ -14,7 +14,7 @@ import type {
} from '../types/Stories';
import type { LocalizerType } from '../types/Util';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
import type { ShowToastAction } from '../state/ducks/toast';
import type {
AddStoryData,
ViewUserStoriesActionCreatorType,
@ -48,7 +48,7 @@ export type PropsType = {
setAddStoryData: (data: AddStoryData) => unknown;
showConversation: ShowConversationType;
showStoriesSettings: () => unknown;
showToast: ShowToastActionCreatorType;
showToast: ShowToastAction;
stories: Array<ConversationStoryType>;
toggleHideStories: (conversationId: string) => unknown;
toggleStoriesView: () => unknown;

View file

@ -5,7 +5,7 @@ import type { ReactNode } from 'react';
import React, { useState, useCallback } from 'react';
import type { LocalizerType } from '../types/Util';
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
import type { ShowToastAction } from '../state/ducks/toast';
import { ContextMenu } from './ContextMenu';
import { Theme } from '../util/theme';
import { ToastType } from '../types/Toast';
@ -22,7 +22,7 @@ export type PropsType = {
moduleClassName?: string;
onAddStory: (file?: File) => unknown;
onContextMenuShowingChanged?: (value: boolean) => void;
showToast: ShowToastActionCreatorType;
showToast: ShowToastAction;
};
export function StoriesAddStoryButton({
@ -55,7 +55,7 @@ export function StoriesAddStoryButton({
result.reason === ReasonVideoNotGood.UnsupportedCodec ||
result.reason === ReasonVideoNotGood.UnsupportedContainer
) {
showToast(ToastType.StoryVideoUnsupported);
showToast({ toastType: ToastType.StoryVideoUnsupported });
return;
}
@ -79,7 +79,7 @@ export function StoriesAddStoryButton({
}
if (result.reason !== ReasonVideoNotGood.AllGoodNevermind) {
showToast(ToastType.StoryVideoError);
showToast({ toastType: ToastType.StoryVideoError });
return;
}

View file

@ -12,7 +12,7 @@ import type {
import type { ConversationStoryType, MyStoryType } from '../types/Stories';
import type { LocalizerType } from '../types/Util';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
import type { ShowToastAction } from '../state/ducks/toast';
import type { ViewUserStoriesActionCreatorType } from '../state/ducks/stories';
import { ContextMenu } from './ContextMenu';
import { MyStoryButton } from './MyStoryButton';
@ -68,7 +68,7 @@ export type PropsType = {
onMediaPlaybackStart: () => void;
queueStoryDownload: (storyId: string) => unknown;
showConversation: ShowConversationType;
showToast: ShowToastActionCreatorType;
showToast: ShowToastAction;
stories: Array<ConversationStoryType>;
toggleHideStories: (conversationId: string) => unknown;
toggleStoriesView: () => unknown;

View file

@ -21,7 +21,7 @@ import type { EmojiPickDataType } from './emoji/EmojiPicker';
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
import type { RenderEmojiPickerProps } from './conversation/ReactionPicker';
import type { ReplyStateType, StoryViewType } from '../types/Stories';
import type { ShowToastActionCreatorType } from '../state/ducks/toast';
import type { ShowToastAction } from '../state/ducks/toast';
import type { ViewStoryActionCreatorType } from '../state/ducks/stories';
import * as log from '../logging/log';
import { AnimatedEmojiGalore } from './AnimatedEmojiGalore';
@ -113,7 +113,7 @@ export type PropsType = {
saveAttachment: SaveAttachmentActionCreatorType;
setHasAllStoriesUnmuted: (isUnmuted: boolean) => unknown;
showContactModal: (contactId: string, conversationId?: string) => void;
showToast: ShowToastActionCreatorType;
showToast: ShowToastAction;
skinTone?: number;
story: StoryViewType;
storyViewMode: StoryViewModeType;
@ -786,7 +786,7 @@ export function StoryViewer({
onClick={
hasAudio
? () => setHasAllStoriesUnmuted(!hasAllStoriesUnmuted)
: () => showToast(ToastType.StoryMuted)
: () => showToast({ toastType: ToastType.StoryMuted })
}
type="button"
/>
@ -940,14 +940,14 @@ export function StoryViewer({
onReactToStory(emoji, story);
if (!isGroupStory) {
setCurrentViewTarget(null);
showToast(ToastType.StoryReact);
showToast({ toastType: ToastType.StoryReact });
}
setReactionEmoji(emoji);
}}
onReply={(message, mentions, replyTimestamp) => {
if (!isGroupStory) {
setCurrentViewTarget(null);
showToast(ToastType.StoryReply);
showToast({ toastType: ToastType.StoryReply });
}
onReplyToStory(message, mentions, replyTimestamp, story);
}}

View file

@ -4,14 +4,136 @@
import type { Meta, Story } from '@storybook/react';
import React from 'react';
import type { PropsType } from './ToastManager';
import enMessages from '../../_locales/en/messages.json';
import { ToastManager } from './ToastManager';
import type { AnyToast } from '../types/Toast';
import { ToastType } from '../types/Toast';
import { setupI18n } from '../util/setupI18n';
import { missingCaseError } from '../util/missingCaseError';
import type { PropsType } from './ToastManager';
const i18n = setupI18n('en', enMessages);
function getToast(toastType: ToastType): AnyToast {
switch (toastType) {
case ToastType.AddingUserToGroup:
return { toastType, parameters: { contact: 'Sam Mirete' } };
case ToastType.AlreadyGroupMember:
return { toastType: ToastType.AlreadyGroupMember };
case ToastType.AlreadyRequestedToJoin:
return { toastType: ToastType.AlreadyRequestedToJoin };
case ToastType.Blocked:
return { toastType: ToastType.Blocked };
case ToastType.BlockedGroup:
return { toastType: ToastType.BlockedGroup };
case ToastType.CannotForwardEmptyMessage:
return { toastType: ToastType.CannotForwardEmptyMessage };
case ToastType.CannotMixMultiAndNonMultiAttachments:
return { toastType: ToastType.CannotMixMultiAndNonMultiAttachments };
case ToastType.CannotOpenGiftBadgeIncoming:
return { toastType: ToastType.CannotOpenGiftBadgeIncoming };
case ToastType.CannotOpenGiftBadgeOutgoing:
return { toastType: ToastType.CannotOpenGiftBadgeOutgoing };
case ToastType.CannotStartGroupCall:
return { toastType: ToastType.CannotStartGroupCall };
case ToastType.ConversationArchived:
return {
toastType: ToastType.ConversationArchived,
parameters: { conversationId: 'some-conversation-id' },
};
case ToastType.ConversationMarkedUnread:
return { toastType: ToastType.ConversationMarkedUnread };
case ToastType.ConversationRemoved:
return {
toastType: ToastType.ConversationRemoved,
parameters: { title: 'Alice' },
};
case ToastType.ConversationUnarchived:
return { toastType: ToastType.ConversationUnarchived };
case ToastType.CopiedUsername:
return { toastType: ToastType.CopiedUsername };
case ToastType.CopiedUsernameLink:
return { toastType: ToastType.CopiedUsernameLink };
case ToastType.DangerousFileType:
return { toastType: ToastType.DangerousFileType };
case ToastType.DeleteForEveryoneFailed:
return { toastType: ToastType.DeleteForEveryoneFailed };
case ToastType.Error:
return { toastType: ToastType.Error };
case ToastType.Expired:
return { toastType: ToastType.Expired };
case ToastType.FailedToDeleteUsername:
return { toastType: ToastType.FailedToDeleteUsername };
case ToastType.FileSaved:
return {
toastType: ToastType.FileSaved,
parameters: { fullPath: '/image.png' },
};
case ToastType.FileSize:
return {
toastType: ToastType.FileSize,
parameters: { limit: 100, units: 'MB' },
};
case ToastType.InvalidConversation:
return { toastType: ToastType.InvalidConversation };
case ToastType.LeftGroup:
return { toastType: ToastType.LeftGroup };
case ToastType.MaxAttachments:
return { toastType: ToastType.MaxAttachments };
case ToastType.MessageBodyTooLong:
return { toastType: ToastType.MessageBodyTooLong };
case ToastType.OriginalMessageNotFound:
return { toastType: ToastType.OriginalMessageNotFound };
case ToastType.PinnedConversationsFull:
return { toastType: ToastType.PinnedConversationsFull };
case ToastType.ReactionFailed:
return { toastType: ToastType.ReactionFailed };
case ToastType.ReportedSpamAndBlocked:
return { toastType: ToastType.ReportedSpamAndBlocked };
case ToastType.StoryMuted:
return { toastType: ToastType.StoryMuted };
case ToastType.StoryReact:
return { toastType: ToastType.StoryReact };
case ToastType.StoryReply:
return { toastType: ToastType.StoryReply };
case ToastType.StoryVideoError:
return { toastType: ToastType.StoryVideoError };
case ToastType.StoryVideoUnsupported:
return { toastType: ToastType.StoryVideoUnsupported };
case ToastType.TapToViewExpiredIncoming:
return { toastType: ToastType.TapToViewExpiredIncoming };
case ToastType.TapToViewExpiredOutgoing:
return { toastType: ToastType.TapToViewExpiredOutgoing };
case ToastType.TooManyMessagesToDeleteForEveryone:
return {
toastType: ToastType.TooManyMessagesToDeleteForEveryone,
parameters: { count: 30 },
};
case ToastType.TooManyMessagesToForward:
return { toastType: ToastType.TooManyMessagesToForward };
case ToastType.UnableToLoadAttachment:
return { toastType: ToastType.UnableToLoadAttachment };
case ToastType.UnsupportedMultiAttachment:
return { toastType: ToastType.UnsupportedMultiAttachment };
case ToastType.UnsupportedOS:
return { toastType: ToastType.UnsupportedOS };
case ToastType.UserAddedToGroup:
return {
toastType: ToastType.UserAddedToGroup,
parameters: {
contact: 'Sam Mirete',
group: 'Hike Group 🏔',
},
};
default:
throw missingCaseError(toastType);
}
}
type Args = Omit<PropsType, 'toast'> & {
toastType: ToastType;
};
export default {
title: 'Components/ToastManager',
component: ToastManager,
@ -22,331 +144,26 @@ export default {
i18n: {
defaultValue: i18n,
},
toast: {
defaultValue: undefined,
toastType: {
defaultValue: ToastType.AddingUserToGroup,
options: ToastType,
control: { type: 'select' },
},
OS: {
defaultValue: 'macOS',
},
},
} as Meta;
} as Meta<Args>;
// eslint-disable-next-line react/function-component-definition
const Template: Story<PropsType> = args => <ToastManager {...args} />;
export const UndefinedToast = Template.bind({});
UndefinedToast.args = {};
export const InvalidToast = Template.bind({});
InvalidToast.args = {
toast: {
toastType: 'this is a toast that does not exist' as ToastType,
},
const Template: Story<Args> = args => {
const { toastType, ...rest } = args;
return (
<>
<p>Select a toast type in controls</p>
<ToastManager toast={getToast(toastType)} {...rest} />
</>
);
};
export const AddingUserToGroup = Template.bind({});
AddingUserToGroup.args = {
toast: {
toastType: ToastType.AddingUserToGroup,
parameters: {
contact: 'Sam Mirete',
},
},
};
export const AlreadyGroupMember = Template.bind({});
AlreadyGroupMember.args = {
toast: {
toastType: ToastType.AlreadyGroupMember,
},
};
export const AlreadyRequestedToJoin = Template.bind({});
AlreadyRequestedToJoin.args = {
toast: {
toastType: ToastType.AlreadyRequestedToJoin,
},
};
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: {
toastType: ToastType.CannotMixMultiAndNonMultiAttachments,
},
};
export const CannotOpenGiftBadgeIncoming = Template.bind({});
CannotOpenGiftBadgeIncoming.args = {
toast: {
toastType: ToastType.CannotOpenGiftBadgeIncoming,
},
};
export const CannotOpenGiftBadgeOutgoing = Template.bind({});
CannotOpenGiftBadgeOutgoing.args = {
toast: {
toastType: ToastType.CannotOpenGiftBadgeOutgoing,
},
};
export const CannotStartGroupCall = Template.bind({});
CannotStartGroupCall.args = {
toast: {
toastType: ToastType.CannotStartGroupCall,
},
};
export const ConversationArchived = Template.bind({});
ConversationArchived.args = {
toast: {
toastType: ToastType.ConversationArchived,
parameters: {
conversationId: 'some-conversation-id',
},
},
};
export const ConversationMarkedUnread = Template.bind({});
ConversationMarkedUnread.args = {
toast: {
toastType: ToastType.ConversationMarkedUnread,
},
};
export const ConversationRemoved = Template.bind({});
ConversationRemoved.args = {
toast: {
toastType: ToastType.ConversationRemoved,
parameters: {
title: 'Alice',
},
},
};
export const ConversationUnarchived = Template.bind({});
ConversationUnarchived.args = {
toast: {
toastType: ToastType.ConversationUnarchived,
},
};
export const CopiedUsername = Template.bind({});
CopiedUsername.args = {
toast: {
toastType: ToastType.CopiedUsername,
},
};
export const CopiedUsernameLink = Template.bind({});
CopiedUsernameLink.args = {
toast: {
toastType: ToastType.CopiedUsernameLink,
},
};
export const DangerousFileType = Template.bind({});
DangerousFileType.args = {
toast: {
toastType: ToastType.DangerousFileType,
},
};
export const DeleteForEveryoneFailed = Template.bind({});
DeleteForEveryoneFailed.args = {
toast: {
toastType: ToastType.DeleteForEveryoneFailed,
},
};
export const Error = Template.bind({});
Error.args = {
toast: {
toastType: ToastType.Error,
},
};
export const Expired = Template.bind({});
Expired.args = {
toast: {
toastType: ToastType.Expired,
},
};
export const FailedToDeleteUsername = Template.bind({});
FailedToDeleteUsername.args = {
toast: {
toastType: ToastType.FailedToDeleteUsername,
},
};
export const FileSaved = Template.bind({});
FileSaved.args = {
toast: {
toastType: ToastType.FileSaved,
parameters: {
fullPath: '/image.png',
},
},
};
export const FileSize = Template.bind({});
FileSize.args = {
toast: {
toastType: ToastType.FileSize,
parameters: {
limit: '100',
units: 'MB',
},
},
};
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: {
toastType: ToastType.MaxAttachments,
},
};
export const OriginalMessageNotFound = Template.bind({});
OriginalMessageNotFound.args = {
toast: {
toastType: ToastType.OriginalMessageNotFound,
},
};
export const MessageBodyTooLong = Template.bind({});
MessageBodyTooLong.args = {
toast: {
toastType: ToastType.MessageBodyTooLong,
},
};
export const PinnedConversationsFull = Template.bind({});
PinnedConversationsFull.args = {
toast: {
toastType: ToastType.PinnedConversationsFull,
},
};
export const ReactionFailed = Template.bind({});
ReactionFailed.args = {
toast: {
toastType: ToastType.ReactionFailed,
},
};
export const ReportedSpamAndBlocked = Template.bind({});
ReportedSpamAndBlocked.args = {
toast: {
toastType: ToastType.ReportedSpamAndBlocked,
},
};
export const StoryMuted = Template.bind({});
StoryMuted.args = {
toast: {
toastType: ToastType.StoryMuted,
},
};
export const StoryReact = Template.bind({});
StoryReact.args = {
toast: {
toastType: ToastType.StoryReact,
},
};
export const StoryReply = Template.bind({});
StoryReply.args = {
toast: {
toastType: ToastType.StoryReply,
},
};
export const StoryVideoError = Template.bind({});
StoryVideoError.args = {
toast: {
toastType: ToastType.StoryVideoError,
},
};
export const StoryVideoUnsupported = Template.bind({});
StoryVideoUnsupported.args = {
toast: {
toastType: ToastType.StoryVideoUnsupported,
},
};
export const TapToViewExpiredIncoming = Template.bind({});
TapToViewExpiredIncoming.args = {
toast: {
toastType: ToastType.TapToViewExpiredIncoming,
},
};
export const TapToViewExpiredOutgoing = Template.bind({});
TapToViewExpiredOutgoing.args = {
toast: {
toastType: ToastType.TapToViewExpiredOutgoing,
},
};
export const UnableToLoadAttachment = Template.bind({});
UnableToLoadAttachment.args = {
toast: {
toastType: ToastType.UnableToLoadAttachment,
},
};
export const UnsupportedMultiAttachment = Template.bind({});
UnsupportedMultiAttachment.args = {
toast: {
toastType: ToastType.UnsupportedMultiAttachment,
},
};
export const UnsupportedOS = Template.bind({});
UnsupportedOS.args = {
toast: {
toastType: ToastType.UnsupportedOS,
},
};
export const UserAddedToGroup = Template.bind({});
UserAddedToGroup.args = {
toast: {
toastType: ToastType.UserAddedToGroup,
parameters: {
contact: 'Sam Mirete',
group: 'Hike Group 🏔',
},
},
};
export const BasicUsage = Template.bind({});

View file

@ -2,10 +2,11 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType, ReplacementValuesType } from '../types/Util';
import type { LocalizerType } from '../types/Util';
import { SECOND } from '../util/durations';
import { Toast } from './Toast';
import { missingCaseError } from '../util/missingCaseError';
import type { AnyToast } from '../types/Toast';
import { ToastType } from '../types/Toast';
export type PropsType = {
@ -14,10 +15,7 @@ export type PropsType = {
openFileInFolder: (target: string) => unknown;
OS: string;
onUndoArchive: (conversaetionId: string) => unknown;
toast?: {
toastType: ToastType;
parameters?: ReplacementValuesType;
};
toast?: AnyToast;
};
const SHORT_TIMEOUT = 3 * SECOND;
@ -40,7 +38,7 @@ export function ToastManager({
return (
<Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
{i18n('icu:AddUserToAnotherGroupModal__toast--adding-user-to-group', {
contact: toast.parameters?.contact,
contact: toast.parameters.contact,
})}
</Toast>
);
@ -117,9 +115,7 @@ export function ToastManager({
toastAction={{
label: i18n('icu:conversationArchivedUndo'),
onClick: () => {
if (toast.parameters && 'conversationId' in toast.parameters) {
onUndoArchive(String(toast.parameters.conversationId));
}
},
}}
>
@ -138,7 +134,7 @@ export function ToastManager({
return (
<Toast onClose={hideToast}>
{i18n('icu:Toast--ConversationRemoved', {
title: toast?.parameters?.title ?? '',
title: toast.parameters.title,
})}
</Toast>
);
@ -212,9 +208,7 @@ export function ToastManager({
toastAction={{
label: i18n('icu:attachmentSavedShow'),
onClick: () => {
if (toast.parameters && 'fullPath' in toast.parameters) {
openFileInFolder(String(toast.parameters.fullPath));
}
openFileInFolder(toast.parameters.fullPath);
},
}}
>
@ -227,8 +221,8 @@ export function ToastManager({
return (
<Toast onClose={hideToast}>
{i18n('icu:fileSizeWarning', {
limit: toast.parameters?.limit,
units: toast.parameters?.units,
limit: toast.parameters.limit,
units: toast.parameters.units,
})}
</Toast>
);
@ -330,6 +324,17 @@ export function ToastManager({
);
}
if (toastType === ToastType.TooManyMessagesToDeleteForEveryone) {
return (
<Toast onClose={hideToast}>
{i18n(
'icu:DeleteMessagesModal__toast--TooManyMessagesToDeleteForEveryone',
{ count: toast.parameters.count }
)}
</Toast>
);
}
if (toastType === ToastType.TooManyMessagesToForward) {
return (
<Toast onClose={hideToast}>
@ -364,8 +369,8 @@ export function ToastManager({
return (
<Toast onClose={hideToast}>
{i18n('icu:AddUserToAnotherGroupModal__toast--user-added-to-group', {
contact: toast.parameters?.contact,
group: toast.parameters?.group,
contact: toast.parameters.contact,
group: toast.parameters.group,
})}
</Toast>
);

View file

@ -97,8 +97,6 @@ const defaultMessageProps: TimelineMessagesProps = {
conversationId: 'conversationId',
conversationTitle: 'Conversation Title',
conversationType: 'direct', // override
deleteMessages: action('default--deleteMessages'),
deleteMessageForEveryone: action('default--deleteMessageForEveryone'),
direction: 'incoming',
showLightboxForViewOnceMedia: action('default--showLightboxForViewOnceMedia'),
doubleCheckMissingQuoteReference: action(
@ -145,6 +143,7 @@ const defaultMessageProps: TimelineMessagesProps = {
showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast'
),
toggleDeleteMessagesModal: action('default--toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('default--toggleForwardMessagesModal'),
showLightbox: action('default--showLightbox'),
startConversation: action('default--startConversation'),

View file

@ -2,11 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-only
import classNames from 'classnames';
import React, { useState } from 'react';
import React from 'react';
import type { ShowToastAction } from '../../state/ducks/toast';
import { ToastType } from '../../types/Toast';
import type { LocalizerType } from '../../types/Util';
import { ConfirmationDialog } from '../ConfirmationDialog';
// Keep this in sync with iOS and Android
const MAX_FORWARD_COUNT = 30;
@ -28,8 +27,6 @@ export default function SelectModeActions({
showToast,
i18n,
}: SelectModeActionsProps): JSX.Element {
const [confirmDelete, setConfirmDelete] = useState(false);
const hasSelectedMessages = selectedMessageIds.length >= 1;
const tooManyMessagesToForward =
selectedMessageIds.length > MAX_FORWARD_COUNT;
@ -38,7 +35,6 @@ export default function SelectModeActions({
const canDelete = hasSelectedMessages;
return (
<>
<div className="SelectModeActions">
<button
type="button"
@ -62,9 +58,7 @@ export default function SelectModeActions({
'SelectModeActions__button--disabled': !canDelete,
})}
disabled={!canDelete}
onClick={() => {
setConfirmDelete(true);
}}
onClick={onDeleteMessages}
aria-label={i18n('icu:SelectModeActions--deleteSelectedMessages')}
>
<span
@ -82,9 +76,7 @@ export default function SelectModeActions({
if (canForward) {
onForwardMessages();
} else if (tooManyMessagesToForward) {
showToast(ToastType.TooManyMessagesToForward, {
count: MAX_FORWARD_COUNT,
});
showToast({ toastType: ToastType.TooManyMessagesToForward });
}
}}
aria-label={i18n('icu:SelectModeActions--forwardSelectedMessages')}
@ -95,31 +87,5 @@ export default function SelectModeActions({
/>
</button>
</div>
{confirmDelete && (
<ConfirmationDialog
actions={[
{
action: () => {
onDeleteMessages();
},
style: 'negative',
text: i18n('icu:ConfirmDeleteForMeModal--confirm'),
},
]}
dialogName="ConfirmDeleteForMeModal"
title={i18n('icu:ConfirmDeleteForMeModal--title', {
count: selectedMessageIds.length,
})}
i18n={i18n}
onClose={() => {
setConfirmDelete(false);
}}
>
{i18n('icu:ConfirmDeleteForMeModal--description', {
count: selectedMessageIds.length,
})}
</ConfirmationDialog>
)}
</>
);
}

View file

@ -282,8 +282,6 @@ const actions = () => ({
setQuoteByMessageId: action('setQuoteByMessageId'),
retryDeleteForEveryone: action('retryDeleteForEveryone'),
retryMessageSend: action('retryMessageSend'),
deleteMessages: action('deleteMessages'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
saveAttachment: action('saveAttachment'),
pushPanelForConversation: action('pushPanelForConversation'),
showContactDetail: action('showContactDetail'),
@ -305,6 +303,7 @@ const actions = () => ({
showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast'
),
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
toggleSafetyNumberModal: action('toggleSafetyNumberModal'),

View file

@ -71,8 +71,6 @@ const getDefaultProps = () => ({
retryDeleteForEveryone: action('retryDeleteForEveryone'),
retryMessageSend: action('retryMessageSend'),
blockGroupLinkRequests: action('blockGroupLinkRequests'),
deleteMessages: action('deleteMessages'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
markAttachmentAsCorrupted: action('markAttachmentAsCorrupted'),
messageExpanded: action('messageExpanded'),
@ -82,6 +80,7 @@ const getDefaultProps = () => ({
pushPanelForConversation: action('pushPanelForConversation'),
showContactModal: action('showContactModal'),
showLightbox: action('showLightbox'),
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
showLightboxForViewOnceMedia: action('showLightboxForViewOnceMedia'),
doubleCheckMissingQuoteReference: action('doubleCheckMissingQuoteReference'),

View file

@ -265,8 +265,6 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
conversationType: overrideProps.conversationType || 'direct',
contact: overrideProps.contact,
deletedForEveryone: overrideProps.deletedForEveryone,
deleteMessages: action('deleteMessages'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
// disableMenu: overrideProps.disableMenu,
disableScroll: overrideProps.disableScroll,
direction: overrideProps.direction || 'incoming',
@ -350,6 +348,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
showExpiredOutgoingTapToViewToast: action(
'showExpiredOutgoingTapToViewToast'
),
toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'),
toggleForwardMessagesModal: action('toggleForwardMessagesModal'),
showLightbox: action('showLightbox'),
startConversation: action('startConversation'),

View file

@ -26,9 +26,9 @@ import type {
import type { PushPanelForConversationActionType } from '../../state/ducks/conversations';
import { doesMessageBodyOverflow } from './MessageBodyReadMore';
import type { Props as ReactionPickerProps } from './ReactionPicker';
import { ConfirmationDialog } from '../ConfirmationDialog';
import { useToggleReactionPicker } from '../../hooks/useKeyboardShortcuts';
import { PanelType } from '../../types/Panels';
import type { DeleteMessagesPropsType } from '../../state/ducks/globalModals';
export type PropsData = {
canDownload: boolean;
@ -41,13 +41,9 @@ export type PropsData = {
} & Omit<MessagePropsData, 'renderingContext' | 'menu'>;
export type PropsActions = {
deleteMessages: (options: {
conversationId: string;
messageIds: ReadonlyArray<string>;
}) => void;
deleteMessageForEveryone: (id: string) => void;
pushPanelForConversation: PushPanelForConversationActionType;
toggleForwardMessagesModal: (id: Array<string>) => void;
toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void;
toggleForwardMessagesModal: (messageIds: Array<string>) => void;
reactToMessage: (
id: string,
{ emoji, remove }: { emoji: string; remove: boolean }
@ -83,7 +79,6 @@ export function TimelineMessage(props: Props): JSX.Element {
const {
attachments,
author,
canDeleteForEveryone,
canDownload,
canReact,
canReply,
@ -93,8 +88,6 @@ export function TimelineMessage(props: Props): JSX.Element {
containerElementRef,
containerWidthBreakpoint,
conversationId,
deleteMessages,
deleteMessageForEveryone,
deletedForEveryone,
direction,
giftBadge,
@ -116,6 +109,7 @@ export function TimelineMessage(props: Props): JSX.Element {
setQuoteByMessageId,
text,
timestamp,
toggleDeleteMessagesModal,
toggleForwardMessagesModal,
toggleSelectMessage,
} = props;
@ -259,9 +253,6 @@ export function TimelineMessage(props: Props): JSX.Element {
}
}, [canReact, toggleReactionPicker]);
const [hasDOEConfirmation, setHasDOEConfirmation] = useState(false);
const [hasDeleteConfirmation, setHasDeleteConfirmation] = useState(false);
const toggleReactionPickerKeyboard = useToggleReactionPicker(
handleReact || noop
);
@ -343,48 +334,6 @@ export function TimelineMessage(props: Props): JSX.Element {
return (
<>
{hasDOEConfirmation && canDeleteForEveryone && (
<ConfirmationDialog
actions={[
{
action: () => deleteMessageForEveryone(id),
style: 'negative',
text: i18n('icu:delete'),
},
]}
dialogName="TimelineMessage/deleteMessageForEveryone"
i18n={i18n}
onClose={() => setHasDOEConfirmation(false)}
>
{i18n('icu:deleteForEveryoneWarning')}
</ConfirmationDialog>
)}
{hasDeleteConfirmation && (
<ConfirmationDialog
actions={[
{
action: () =>
deleteMessages({
conversationId,
messageIds: [id],
}),
style: 'negative',
text: i18n('icu:ConfirmDeleteForMeModal--confirm'),
},
]}
dialogName="ConfirmDeleteForMeModal"
i18n={i18n}
onClose={() => setHasDeleteConfirmation(false)}
title={i18n('icu:ConfirmDeleteForMeModal--title', {
count: 1,
})}
>
{i18n('icu:ConfirmDeleteForMeModal--description', {
count: 1,
})}
</ConfirmationDialog>
)}
<Message
{...props}
renderingContext="conversation/TimelineItem"
@ -413,10 +362,12 @@ export function TimelineMessage(props: Props): JSX.Element {
onForward={
canForward ? () => toggleForwardMessagesModal([id]) : undefined
}
onDeleteForMe={() => setHasDeleteConfirmation(true)}
onDeleteForEveryone={
canDeleteForEveryone ? () => setHasDOEConfirmation(true) : undefined
}
onDeleteMessage={() => {
toggleDeleteMessagesModal({
conversationId,
messageIds: [id],
});
}}
onMoreInfo={() =>
pushPanelForConversation({
type: PanelType.MessageDetails,
@ -594,8 +545,7 @@ type MessageContextProps = {
onRetryMessageSend: (() => void) | undefined;
onRetryDeleteForEveryone: (() => void) | undefined;
onForward: (() => void) | undefined;
onDeleteForMe: () => void;
onDeleteForEveryone: (() => void) | undefined;
onDeleteMessage: () => void;
onMoreInfo: () => void;
onSelect: () => void;
};
@ -612,8 +562,7 @@ const MessageContextMenu = ({
onRetryMessageSend,
onRetryDeleteForEveryone,
onForward,
onDeleteForMe,
onDeleteForEveryone,
onDeleteMessage,
}: MessageContextProps): JSX.Element => {
const menu = (
<ContextMenu id={triggerId}>
@ -746,27 +695,11 @@ const MessageContextMenu = ({
event.stopPropagation();
event.preventDefault();
onDeleteForMe();
onDeleteMessage();
}}
>
{i18n('icu:deleteMessage')}
{i18n('icu:MessageContextMenu__deleteMessage')}
</MenuItem>
{onDeleteForEveryone && (
<MenuItem
attributes={{
className:
'module-message__context--icon module-message__context__delete-message-for-everyone',
}}
onClick={(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();
onDeleteForEveryone();
}}
>
{i18n('icu:deleteMessageForEveryone')}
</MenuItem>
)}
</ContextMenu>
);

View file

@ -72,7 +72,9 @@ export async function joinViaLink(hash: string): Promise<void> {
window.reduxActions.conversations.showConversation({
conversationId: existingConversation.id,
});
window.reduxActions.toast.showToast(ToastType.AlreadyGroupMember);
window.reduxActions.toast.showToast({
toastType: ToastType.AlreadyGroupMember,
});
return;
}
@ -166,7 +168,9 @@ export async function joinViaLink(hash: string): Promise<void> {
conversationId: existingConversation.id,
});
window.reduxActions.toast.showToast(ToastType.AlreadyRequestedToJoin);
window.reduxActions.toast.showToast({
toastType: ToastType.AlreadyRequestedToJoin,
});
return;
}

View file

@ -18,7 +18,6 @@ import type {
} from '../../types/Attachment';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import type { DraftBodyRangeMention } from '../../types/BodyRange';
import type { ReplacementValuesType } from '../../types/Util';
import type { LinkPreviewType } from '../../types/message/LinkPreviews';
import type { MessageAttributesType } from '../../model-types.d';
import type { NoopActionType } from './noop';
@ -35,6 +34,7 @@ import { LinkPreviewSourceType } from '../../types/LinkPreview';
import { completeRecording } from './audioRecorder';
import { RecordingState } from '../../types/AudioRecorder';
import { SHOW_TOAST } from './toast';
import type { AnyToast } from '../../types/Toast';
import { ToastType } from '../../types/Toast';
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import { UUID } from '../../types/UUID';
@ -436,13 +436,11 @@ function sendMultiMediaMessage(
conversation.clearTypingTimers();
const toastType = shouldShowInvalidMessageToast(conversation.attributes);
if (toastType) {
const toast = shouldShowInvalidMessageToast(conversation.attributes);
if (toast != null) {
dispatch({
type: SHOW_TOAST,
payload: {
toastType,
},
payload: toast,
});
dispatch(setComposerDisabledState(conversationId, false));
return;
@ -561,13 +559,11 @@ function sendStickerMessage(
return;
}
const toastType = shouldShowInvalidMessageToast(conversation.attributes);
if (toastType) {
const toast = shouldShowInvalidMessageToast(conversation.attributes);
if (toast != null) {
dispatch({
type: SHOW_TOAST,
payload: {
toastType,
},
payload: toast,
});
return;
}
@ -906,9 +902,7 @@ function processAttachments({
return;
}
let toastToShow:
| { toastType: ToastType; parameters?: ReplacementValuesType }
| undefined;
let toastToShow: AnyToast | undefined;
const nextDraftAttachments = (
conversation.get('draftAttachments') || []
@ -986,7 +980,7 @@ function processAttachments({
function preProcessAttachment(
file: File,
draftAttachments: Array<AttachmentDraftType>
): { toastType: ToastType; parameters?: ReplacementValuesType } | undefined {
): AnyToast | undefined {
if (!file) {
return;
}

View file

@ -1001,7 +1001,7 @@ export const actions = {
deleteAvatarFromDisk,
deleteConversation,
deleteMessages,
deleteMessageForEveryone,
deleteMessagesForEveryone,
destroyMessages,
discardMessages,
doubleCheckMissingQuoteReference,
@ -2857,8 +2857,8 @@ function popPanelForConversation(): ThunkAction<
};
}
function deleteMessageForEveryone(
messageId: string
function deleteMessagesForEveryone(
messageIds: ReadonlyArray<string>
): ThunkAction<
void,
RootStateType,
@ -2866,6 +2866,11 @@ function deleteMessageForEveryone(
NoopActionType | ShowToastActionType
> {
return async dispatch => {
let hasError = false;
await Promise.all(
messageIds.map(async messageId => {
try {
const message = window.MessageController.getById(messageId);
if (!message) {
throw new Error(
@ -2878,27 +2883,33 @@ function deleteMessageForEveryone(
throw new Error('deleteMessageForEveryone: no conversation');
}
try {
await sendDeleteForEveryoneMessage(conversation.attributes, {
id: message.id,
timestamp: message.get('sent_at'),
});
dispatch({
type: 'NOOP',
payload: null,
});
} catch (error) {
hasError = true;
log.error(
'Error sending delete-for-everyone',
'Error queuing delete-for-everyone job',
Errors.toLogFormat(error),
messageId
);
}
})
);
if (hasError) {
dispatch({
type: SHOW_TOAST,
payload: {
toastType: ToastType.DeleteForEveryoneFailed,
},
});
} else {
dispatch({
type: 'NOOP',
payload: null,
});
}
};
}

View file

@ -45,8 +45,10 @@ import type { ShowToastActionType } from './toast';
export type EditHistoryMessagesType = ReadonlyDeep<
Array<MessageAttributesType>
>;
export type ConfirmDeleteForMeModalProps = ReadonlyDeep<{
count: number;
export type DeleteMessagesPropsType = ReadonlyDeep<{
conversationId: string;
messageIds: ReadonlyArray<string>;
onDelete?: () => void;
}>;
export type ForwardMessagePropsType = ReadonlyDeep<MessagePropsType>;
export type ForwardMessagesPropsType = ReadonlyDeep<{
@ -76,6 +78,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
description?: string;
title?: string;
};
deleteMessagesProps?: DeleteMessagesPropsType;
forwardMessagesProps?: ForwardMessagesPropsType;
gv2MigrationProps?: MigrateToGV2PropsType;
hasConfirmationModal: boolean;
@ -103,6 +106,8 @@ const HIDE_UUID_NOT_FOUND_MODAL = 'globalModals/HIDE_UUID_NOT_FOUND_MODAL';
const SHOW_UUID_NOT_FOUND_MODAL = 'globalModals/SHOW_UUID_NOT_FOUND_MODAL';
const SHOW_STORIES_SETTINGS = 'globalModals/SHOW_STORIES_SETTINGS';
const HIDE_STORIES_SETTINGS = 'globalModals/HIDE_STORIES_SETTINGS';
const TOGGLE_DELETE_MESSAGES_MODAL =
'globalModals/TOGGLE_DELETE_MESSAGES_MODAL';
const TOGGLE_FORWARD_MESSAGES_MODAL =
'globalModals/TOGGLE_FORWARD_MESSAGES_MODAL';
const TOGGLE_PROFILE_EDITOR = 'globalModals/TOGGLE_PROFILE_EDITOR';
@ -175,6 +180,11 @@ export type ShowUserNotFoundModalActionType = ReadonlyDeep<{
payload: UserNotFoundModalStateType;
}>;
type ToggleDeleteMessagesModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_DELETE_MESSAGES_MODAL;
payload: DeleteMessagesPropsType | undefined;
}>;
type ToggleForwardMessagesModalActionType = ReadonlyDeep<{
type: typeof TOGGLE_FORWARD_MESSAGES_MODAL;
payload: ForwardMessagesPropsType | undefined;
@ -321,6 +331,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| ShowWhatsNewModalActionType
| StartMigrationToGV2ActionType
| ToggleAddUserToAnotherGroupModalActionType
| ToggleDeleteMessagesModalActionType
| ToggleForwardMessagesModalActionType
| ToggleProfileEditorActionType
| ToggleProfileEditorErrorActionType
@ -357,6 +368,7 @@ export const actions = {
showWhatsNewModal,
toggleAddUserToAnotherGroupModal,
toggleConfirmationModal,
toggleDeleteMessagesModal,
toggleForwardMessagesModal,
toggleProfileEditor,
toggleProfileEditorHasError,
@ -473,6 +485,15 @@ function closeGV2MigrationDialog(): CloseGV2MigrationDialogActionType {
};
}
function toggleDeleteMessagesModal(
props: DeleteMessagesPropsType | undefined
): ToggleDeleteMessagesModalActionType {
return {
type: TOGGLE_DELETE_MESSAGES_MODAL,
payload: props,
};
}
function toggleForwardMessagesModal(
messageIds?: ReadonlyArray<string>,
onForward?: () => void
@ -855,6 +876,13 @@ export function reducer(
};
}
if (action.type === TOGGLE_DELETE_MESSAGES_MODAL) {
return {
...state,
deleteMessagesProps: action.payload,
};
}
if (action.type === TOGGLE_FORWARD_MESSAGES_MODAL) {
return {
...state,

View file

@ -6,18 +6,14 @@ import { ipcRenderer } from 'electron';
import type { ReadonlyDeep } from 'type-fest';
import type { BoundActionCreatorsMapObject } from '../../hooks/useBoundActions';
import type { NoopActionType } from './noop';
import type { ReplacementValuesType } from '../../types/Util';
import { useBoundActions } from '../../hooks/useBoundActions';
import type { ToastType } from '../../types/Toast';
import type { AnyToast } from '../../types/Toast';
// State
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type ToastStateType = {
toast?: {
toastType: ToastType;
parameters?: ReplacementValuesType;
};
toast?: AnyToast;
};
// Actions
@ -32,10 +28,7 @@ type HideToastActionType = ReadonlyDeep<{
// eslint-disable-next-line local-rules/type-alias-readonlydeep
export type ShowToastActionType = {
type: typeof SHOW_TOAST;
payload: {
toastType: ToastType;
parameters?: ReplacementValuesType;
};
payload: AnyToast;
};
// eslint-disable-next-line local-rules/type-alias-readonlydeep
@ -57,29 +50,14 @@ function openFileInFolder(target: string): NoopActionType {
};
}
export type ShowToastActionCreatorType = ReadonlyDeep<
(
toastType: ToastType,
parameters?: ReplacementValuesType
) => ShowToastActionType
>;
export type ShowToastAction = ReadonlyDeep<(toast: AnyToast) => void>;
export type ShowToastAction = ReadonlyDeep<
(toastType: ToastType, parameters?: ReplacementValuesType) => void
>;
export const showToast: ShowToastActionCreatorType = (
toastType,
parameters
) => {
export function showToast(toast: AnyToast): ShowToastActionType {
return {
type: SHOW_TOAST,
payload: {
toastType,
parameters,
},
};
payload: toast,
};
}
export const actions = {
hideToast,

View file

@ -244,7 +244,7 @@ export function deleteUsername({
try {
await doDeleteUsername(username);
} catch {
dispatch(showToast(ToastType.FailedToDeleteUsername));
dispatch(showToast({ toastType: ToastType.FailedToDeleteUsername }));
}
};

View file

@ -74,6 +74,7 @@ import {
getSelectedMessageIds,
getTargetedMessage,
isMissingRequiredProfileSharing,
getMessages,
} from './conversations';
import {
getIntl,
@ -1799,6 +1800,16 @@ export function canDeleteForEveryone(
);
}
export const canDeleteMessagesForEveryone = createSelector(
[getMessages, (_state, messageIds: ReadonlyArray<string>) => messageIds],
(messagesLookup, messageIds) => {
return messageIds.every(messageId => {
const message = getOwn(messagesLookup, messageId);
return message != null && canDeleteForEveryone(message);
});
}
);
export function canRetryDeleteForEveryone(
message: Pick<
MessageWithUIFieldsType,

View file

@ -19,7 +19,6 @@ import { getEmojiSkinTone } from '../selectors/items';
import {
getConversationSelector,
getGroupAdminsSelector,
getLastSelectedMessage,
getSelectedMessageIds,
isMissingRequiredProfileSharing,
} from '../selectors/conversations';
@ -93,7 +92,6 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const recentEmojis = selectRecentEmojis(state);
const selectedMessageIds = getSelectedMessageIds(state);
const lastSelectedMessage = getLastSelectedMessage(state);
return {
// Base
@ -170,7 +168,6 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
// Select Mode
selectedMessageIds,
lastSelectedMessage,
};
};

View file

@ -51,6 +51,7 @@ export function SmartConversationView(): JSX.Element {
const hasOpenModal = useSelector((state: StateType) => {
return (
state.globalModals.forwardMessagesProps != null ||
state.globalModals.deleteMessagesProps != null ||
state.globalModals.hasConfirmationModal
);
});

View file

@ -0,0 +1,61 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { useSelector } from 'react-redux';
import type { DeleteMessagesPropsType } from '../ducks/globalModals';
import type { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import DeleteMessagesModal from '../../components/DeleteMessagesModal';
import { strictAssert } from '../../util/assert';
import { canDeleteMessagesForEveryone } from '../selectors/message';
import { useConversationsActions } from '../ducks/conversations';
import { useToastActions } from '../ducks/toast';
export function SmartDeleteMessagesModal(): JSX.Element | null {
const deleteMessagesProps = useSelector<
StateType,
DeleteMessagesPropsType | undefined
>(state => state.globalModals.deleteMessagesProps);
strictAssert(
deleteMessagesProps != null,
'Cannot render delete messages modal without messages'
);
const { conversationId, messageIds, onDelete } = deleteMessagesProps;
const canDeleteForEveryone = useSelector((state: StateType) => {
return canDeleteMessagesForEveryone(state, messageIds);
});
const lastSelectedMessage = useSelector((state: StateType) => {
return state.conversations.lastSelectedMessage;
});
const i18n = useSelector(getIntl);
const { toggleDeleteMessagesModal } = useGlobalModalActions();
const { deleteMessages, deleteMessagesForEveryone } =
useConversationsActions();
const { showToast } = useToastActions();
return (
<DeleteMessagesModal
canDeleteForEveryone={canDeleteForEveryone}
i18n={i18n}
messageCount={deleteMessagesProps.messageIds.length}
onClose={() => {
toggleDeleteMessagesModal(undefined);
}}
onDeleteForMe={() => {
deleteMessages({
conversationId,
messageIds,
lastSelectedMessage,
});
onDelete?.();
}}
onDeleteForEveryone={() => {
deleteMessagesForEveryone(messageIds);
onDelete?.();
}}
showToast={showToast}
/>
);
}

View file

@ -21,6 +21,7 @@ import { SmartStoriesSettingsModal } from './StoriesSettingsModal';
import { getConversationsStoppingSend } from '../selectors/conversations';
import { getIntl, getTheme } from '../selectors/user';
import { useGlobalModalActions } from '../ducks/globalModals';
import { SmartDeleteMessagesModal } from './DeleteMessagesModal';
function renderEditHistoryMessagesModal(): JSX.Element {
return <SmartEditHistoryMessagesModal />;
@ -34,6 +35,10 @@ function renderContactModal(): JSX.Element {
return <SmartContactModal />;
}
function renderDeleteMessagesModal(): JSX.Element {
return <SmartDeleteMessagesModal />;
}
function renderForwardMessagesModal(): JSX.Element {
return <SmartForwardMessagesModal />;
}
@ -62,6 +67,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
contactModalState,
editHistoryMessages,
errorModalProps,
deleteMessagesProps,
forwardMessagesProps,
isProfileEditorVisible,
isShortcutGuideModalVisible,
@ -128,6 +134,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
contactModalState={contactModalState}
editHistoryMessages={editHistoryMessages}
errorModalProps={errorModalProps}
deleteMessagesProps={deleteMessagesProps}
forwardMessagesProps={forwardMessagesProps}
hasSafetyNumberChangeModal={hasSafetyNumberChangeModal}
hideUserNotFoundModal={hideUserNotFoundModal}
@ -142,6 +149,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
renderContactModal={renderContactModal}
renderEditHistoryMessagesModal={renderEditHistoryMessagesModal}
renderErrorModal={renderErrorModal}
renderDeleteMessagesModal={renderDeleteMessagesModal}
renderForwardMessagesModal={renderForwardMessagesModal}
renderProfileEditor={renderProfileEditor}
renderSafetyNumber={renderSafetyNumber}

View file

@ -139,7 +139,9 @@ export function SmartStoryViewer(): JSX.Element | null {
);
}}
onSetSkinTone={onSetSkinTone}
onTextTooLong={() => showToast(ToastType.MessageBodyTooLong)}
onTextTooLong={() => {
showToast({ toastType: ToastType.MessageBodyTooLong });
}}
onUseEmoji={onUseEmoji}
onMediaPlaybackStart={pauseVoiceNotePlayer}
preferredReactionEmoji={preferredReactionEmoji}

View file

@ -112,8 +112,6 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
const {
blockGroupLinkRequests,
clearTargetedMessage: clearSelectedMessage,
deleteMessages,
deleteMessageForEveryone,
doubleCheckMissingQuoteReference,
kickOffAttachmentDownload,
markAttachmentAsCorrupted,
@ -138,6 +136,7 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
const {
showContactModal,
showEditHistoryModal,
toggleDeleteMessagesModal,
toggleForwardMessagesModal,
toggleSafetyNumberModal,
} = useGlobalModalActions();
@ -177,8 +176,6 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
blockGroupLinkRequests={blockGroupLinkRequests}
checkForAccount={checkForAccount}
clearTargetedMessage={clearSelectedMessage}
deleteMessages={deleteMessages}
deleteMessageForEveryone={deleteMessageForEveryone}
doubleCheckMissingQuoteReference={doubleCheckMissingQuoteReference}
kickOffAttachmentDownload={kickOffAttachmentDownload}
markAttachmentAsCorrupted={markAttachmentAsCorrupted}
@ -202,6 +199,7 @@ export function SmartTimelineItem(props: ExternalProps): JSX.Element {
showSpoiler={showSpoiler}
startCallingLobby={startCallingLobby}
startConversation={startConversation}
toggleDeleteMessagesModal={toggleDeleteMessagesModal}
toggleForwardMessagesModal={toggleForwardMessagesModal}
toggleSafetyNumberModal={toggleSafetyNumberModal}
viewStory={viewStory}

View file

@ -441,7 +441,6 @@ describe('electron/state/ducks/username', () => {
type: 'toast/SHOW_TOAST',
payload: {
toastType: ToastType.FailedToDeleteUsername,
parameters: undefined,
},
});
});

View file

@ -40,9 +40,68 @@ export enum ToastType {
StoryVideoUnsupported = 'StoryVideoUnsupported',
TapToViewExpiredIncoming = 'TapToViewExpiredIncoming',
TapToViewExpiredOutgoing = 'TapToViewExpiredOutgoing',
TooManyMessagesToDeleteForEveryone = 'TooManyMessagesToDeleteForEveryone',
TooManyMessagesToForward = 'TooManyMessagesToForward',
UnableToLoadAttachment = 'UnableToLoadAttachment',
UnsupportedMultiAttachment = 'UnsupportedMultiAttachment',
UnsupportedOS = 'UnsupportedOS',
UserAddedToGroup = 'UserAddedToGroup',
}
export type AnyToast =
| { toastType: ToastType.AddingUserToGroup; parameters: { contact: string } }
| { toastType: ToastType.AlreadyGroupMember }
| { toastType: ToastType.AlreadyRequestedToJoin }
| { toastType: ToastType.Blocked }
| { toastType: ToastType.BlockedGroup }
| { toastType: ToastType.CannotForwardEmptyMessage }
| { toastType: ToastType.CannotMixMultiAndNonMultiAttachments }
| { toastType: ToastType.CannotOpenGiftBadgeIncoming }
| { toastType: ToastType.CannotOpenGiftBadgeOutgoing }
| { toastType: ToastType.CannotStartGroupCall }
| {
toastType: ToastType.ConversationArchived;
parameters: { conversationId: string };
}
| { toastType: ToastType.ConversationMarkedUnread }
| { toastType: ToastType.ConversationRemoved; parameters: { title: string } }
| { toastType: ToastType.ConversationUnarchived }
| { toastType: ToastType.CopiedUsername }
| { toastType: ToastType.CopiedUsernameLink }
| { toastType: ToastType.DangerousFileType }
| { toastType: ToastType.DeleteForEveryoneFailed }
| { toastType: ToastType.Error }
| { toastType: ToastType.Expired }
| { toastType: ToastType.FailedToDeleteUsername }
| { toastType: ToastType.FileSaved; parameters: { fullPath: string } }
| {
toastType: ToastType.FileSize;
parameters: { limit: number; units: string };
}
| { toastType: ToastType.InvalidConversation }
| { toastType: ToastType.LeftGroup }
| { toastType: ToastType.MaxAttachments }
| { toastType: ToastType.MessageBodyTooLong }
| { toastType: ToastType.OriginalMessageNotFound }
| { toastType: ToastType.PinnedConversationsFull }
| { toastType: ToastType.ReactionFailed }
| { toastType: ToastType.ReportedSpamAndBlocked }
| { toastType: ToastType.StoryMuted }
| { toastType: ToastType.StoryReact }
| { toastType: ToastType.StoryReply }
| { toastType: ToastType.StoryVideoError }
| { toastType: ToastType.StoryVideoUnsupported }
| { toastType: ToastType.TapToViewExpiredIncoming }
| { toastType: ToastType.TapToViewExpiredOutgoing }
| {
toastType: ToastType.TooManyMessagesToDeleteForEveryone;
parameters: { count: number };
}
| { toastType: ToastType.TooManyMessagesToForward }
| { toastType: ToastType.UnableToLoadAttachment }
| { toastType: ToastType.UnsupportedMultiAttachment }
| { toastType: ToastType.UnsupportedOS }
| {
toastType: ToastType.UserAddedToGroup;
parameters: { contact: string; group: string };
};

View file

@ -5,6 +5,7 @@ import type { ConversationAttributesType } from '../model-types';
import { hasExpired } from '../state/selectors/expiration';
import { isOSUnsupported } from '../state/selectors/updates';
import type { AnyToast } from '../types/Toast';
import { ToastType } from '../types/Toast';
import {
isDirectConversation,
@ -17,13 +18,13 @@ const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
export function shouldShowInvalidMessageToast(
conversationAttributes: ConversationAttributesType,
messageText?: string
): ToastType | undefined {
): AnyToast | undefined {
const state = window.reduxStore.getState();
if (hasExpired(state)) {
if (isOSUnsupported(state)) {
return ToastType.UnsupportedOS;
return { toastType: ToastType.UnsupportedOS };
}
return ToastType.Expired;
return { toastType: ToastType.Expired };
}
const isValid =
@ -32,7 +33,7 @@ export function shouldShowInvalidMessageToast(
isGroupV2(conversationAttributes);
if (!isValid) {
return ToastType.InvalidConversation;
return { toastType: ToastType.InvalidConversation };
}
const { e164, uuid } = conversationAttributes;
@ -41,7 +42,7 @@ export function shouldShowInvalidMessageToast(
((e164 && window.storage.blocked.isBlocked(e164)) ||
(uuid && window.storage.blocked.isUuidBlocked(uuid)))
) {
return ToastType.Blocked;
return { toastType: ToastType.Blocked };
}
const { groupId } = conversationAttributes;
@ -50,18 +51,18 @@ export function shouldShowInvalidMessageToast(
groupId &&
window.storage.blocked.isGroupBlocked(groupId)
) {
return ToastType.BlockedGroup;
return { toastType: ToastType.BlockedGroup };
}
if (
!isDirectConversation(conversationAttributes) &&
conversationAttributes.left
) {
return ToastType.LeftGroup;
return { toastType: ToastType.LeftGroup };
}
if (messageText && messageText.length > MAX_MESSAGE_BODY_LENGTH) {
return ToastType.MessageBodyTooLong;
return { toastType: ToastType.MessageBodyTooLong };
}
return undefined;