diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4328c4e1bed8..e88fedb02052 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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", diff --git a/stylesheets/components/DeleteMessagesModal.scss b/stylesheets/components/DeleteMessagesModal.scss new file mode 100644 index 000000000000..3470efd9ec0d --- /dev/null +++ b/stylesheets/components/DeleteMessagesModal.scss @@ -0,0 +1,6 @@ +// Copyright 2022 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +.DeleteMessagesModal__ModalHost__width-container { + min-width: fit-content; +} diff --git a/stylesheets/manifest.scss b/stylesheets/manifest.scss index 4991b4954f44..4836c9b26016 100644 --- a/stylesheets/manifest.scss +++ b/stylesheets/manifest.scss @@ -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'; diff --git a/ts/background.ts b/ts/background.ts index fe270021c656..9d6914f82b72 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -1730,22 +1730,13 @@ export async function startApp(): Promise { 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({ - conversationId: conversation.id, - messageIds, - }); + 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 { event.stopPropagation(); window.reduxActions.globalModals.toggleForwardMessagesModal( - messageIds + messageIds, + () => { + if (selectedMessageIds != null) { + window.reduxActions.conversations.toggleSelectMode(false); + } + } ); return; diff --git a/ts/components/AddUserToAnotherGroupModal.tsx b/ts/components/AddUserToAnotherGroupModal.tsx index 3867ba33fb04..0d44b3b41376 100644 --- a/ts/components/AddUserToAnotherGroupModal.tsx +++ b/ts/components/AddUserToAnotherGroupModal.tsx @@ -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, { - contact: contact.title, + showToast({ + toastType: ToastType.AddingUserToGroup, + parameters: { + contact: contact.title, + }, }); addMembersToGroup(selectedGroupId, [contact.id], { onSuccess: () => - showToast(ToastType.UserAddedToGroup, { - contact: contact.title, - group: selectedGroup.title, + showToast({ + toastType: ToastType.UserAddedToGroup, + parameters: { + contact: contact.title, + group: selectedGroup.title, + }, }), }); toggleAddUserToAnotherGroupModal(undefined); diff --git a/ts/components/App.tsx b/ts/components/App.tsx index cbc82c26b140..f6a6c7992415 100644 --- a/ts/components/App.tsx +++ b/ts/components/App.tsx @@ -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; diff --git a/ts/components/CompositionArea.stories.tsx b/ts/components/CompositionArea.stories.tsx index 99f4e0d2f089..3724eb25f723 100644 --- a/ts/components/CompositionArea.stories.tsx +++ b/ts/components/CompositionArea.stories.tsx @@ -132,7 +132,6 @@ const useProps = (overrideProps: Partial = {}): Props => ({ renderSmartCompositionRecordingDraft: _ =>
RECORDING DRAFT
, // Select mode selectedMessageIds: undefined, - lastSelectedMessage: undefined, toggleSelectMode: action('toggleSelectMode'), toggleForwardMessagesModal: action('toggleForwardMessagesModal'), }); diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx index eecb58d8af1c..ad180a7f8789 100644 --- a/ts/components/CompositionArea.tsx +++ b/ts/components/CompositionArea.tsx @@ -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 | undefined; - lastSelectedMessage: MessageTimestamps | undefined; toggleSelectMode: (on: boolean) => void; toggleForwardMessagesModal: ( messageIds: ReadonlyArray, @@ -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); + }, }); - toggleSelectMode(false); }} onForwardMessages={() => { if (selectedMessageIds.length > 0) { diff --git a/ts/components/ConfirmationDialog.tsx b/ts/components/ConfirmationDialog.tsx index 0e9a2f09a85e..bec64023157c 100644 --- a/ts/components/ConfirmationDialog.tsx +++ b/ts/components/ConfirmationDialog.tsx @@ -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 + } > 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 = []; + + 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 ( + + {i18n('icu:DeleteMessagesModal--description', { + count: messageCount, + })} + + ); +} diff --git a/ts/components/ErrorBoundary.tsx b/ts/components/ErrorBoundary.tsx index 64c828b14600..58aaa11ae45e 100644 --- a/ts/components/ErrorBoundary.tsx +++ b/ts/components/ErrorBoundary.tsx @@ -38,7 +38,7 @@ export class ErrorBoundary extends React.PureComponent { `\nerrorInfo: ${errorInfo.componentStack}` ); if (window.reduxActions) { - window.reduxActions.toast.showToast(ToastType.Error); + window.reduxActions.toast.showToast({ toastType: ToastType.Error }); } if (closeView) { closeView(); diff --git a/ts/components/ForwardMessagesModal.tsx b/ts/components/ForwardMessagesModal.tsx index e94ca58fa33e..3b942e9b906f 100644 --- a/ts/components/ForwardMessagesModal.tsx +++ b/ts/components/ForwardMessagesModal.tsx @@ -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); diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx index 4b4053e7cc79..27d568337ce3 100644 --- a/ts/components/GlobalModalContainer.tsx +++ b/ts/components/GlobalModalContainer.tsx @@ -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(); } diff --git a/ts/components/MyStoryButton.tsx b/ts/components/MyStoryButton.tsx index bd15c6ef932d..5cecfb847ecb 100644 --- a/ts/components/MyStoryButton.tsx +++ b/ts/components/MyStoryButton.tsx @@ -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 { diff --git a/ts/components/ProfileEditor.tsx b/ts/components/ProfileEditor.tsx index 8eddaa0d578a..2ab087f47597 100644 --- a/ts/components/ProfileEditor.tsx +++ b/ts/components/ProfileEditor.tsx @@ -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 }); }, }, { diff --git a/ts/components/Stories.tsx b/ts/components/Stories.tsx index 8ffe03d3c2a2..80bde45a0efe 100644 --- a/ts/components/Stories.tsx +++ b/ts/components/Stories.tsx @@ -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; toggleHideStories: (conversationId: string) => unknown; toggleStoriesView: () => unknown; diff --git a/ts/components/StoriesAddStoryButton.tsx b/ts/components/StoriesAddStoryButton.tsx index 20daac2cdda4..477f0bfd12bb 100644 --- a/ts/components/StoriesAddStoryButton.tsx +++ b/ts/components/StoriesAddStoryButton.tsx @@ -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; } diff --git a/ts/components/StoriesPane.tsx b/ts/components/StoriesPane.tsx index 84183348f218..518398d2063b 100644 --- a/ts/components/StoriesPane.tsx +++ b/ts/components/StoriesPane.tsx @@ -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; toggleHideStories: (conversationId: string) => unknown; toggleStoriesView: () => unknown; diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx index 80f7a3af0d7d..eabf563cc9c2 100644 --- a/ts/components/StoryViewer.tsx +++ b/ts/components/StoryViewer.tsx @@ -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); }} diff --git a/ts/components/ToastManager.stories.tsx b/ts/components/ToastManager.stories.tsx index 8f7a8666e714..3f02c9c1e38d 100644 --- a/ts/components/ToastManager.stories.tsx +++ b/ts/components/ToastManager.stories.tsx @@ -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 & { + 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; // eslint-disable-next-line react/function-component-definition -const Template: Story = 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 => { + const { toastType, ...rest } = args; + return ( + <> +

Select a toast type in controls

+ + + ); }; -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({}); diff --git a/ts/components/ToastManager.tsx b/ts/components/ToastManager.tsx index c64bc7ecc378..4f316034acb7 100644 --- a/ts/components/ToastManager.tsx +++ b/ts/components/ToastManager.tsx @@ -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 ( {i18n('icu:AddUserToAnotherGroupModal__toast--adding-user-to-group', { - contact: toast.parameters?.contact, + contact: toast.parameters.contact, })} ); @@ -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)); - } + onUndoArchive(String(toast.parameters.conversationId)); }, }} > @@ -138,7 +134,7 @@ export function ToastManager({ return ( {i18n('icu:Toast--ConversationRemoved', { - title: toast?.parameters?.title ?? '', + title: toast.parameters.title, })} ); @@ -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 ( {i18n('icu:fileSizeWarning', { - limit: toast.parameters?.limit, - units: toast.parameters?.units, + limit: toast.parameters.limit, + units: toast.parameters.units, })} ); @@ -330,6 +324,17 @@ export function ToastManager({ ); } + if (toastType === ToastType.TooManyMessagesToDeleteForEveryone) { + return ( + + {i18n( + 'icu:DeleteMessagesModal__toast--TooManyMessagesToDeleteForEveryone', + { count: toast.parameters.count } + )} + + ); + } + if (toastType === ToastType.TooManyMessagesToForward) { return ( @@ -364,8 +369,8 @@ export function ToastManager({ return ( {i18n('icu:AddUserToAnotherGroupModal__toast--user-added-to-group', { - contact: toast.parameters?.contact, - group: toast.parameters?.group, + contact: toast.parameters.contact, + group: toast.parameters.group, })} ); diff --git a/ts/components/conversation/Quote.stories.tsx b/ts/components/conversation/Quote.stories.tsx index 3cde3ffef107..9a5639074438 100644 --- a/ts/components/conversation/Quote.stories.tsx +++ b/ts/components/conversation/Quote.stories.tsx @@ -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'), diff --git a/ts/components/conversation/SelectModeActions.tsx b/ts/components/conversation/SelectModeActions.tsx index d3bebf555838..bf51ec1fc97f 100644 --- a/ts/components/conversation/SelectModeActions.tsx +++ b/ts/components/conversation/SelectModeActions.tsx @@ -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,88 +35,57 @@ export default function SelectModeActions({ const canDelete = hasSelectedMessages; return ( - <> -
- -
- {i18n('icu:SelectModeActions--selectedMessages', { - count: selectedMessageIds.length, - })} -
- - +
+ +
+ {i18n('icu:SelectModeActions--selectedMessages', { + count: selectedMessageIds.length, + })}
- {confirmDelete && ( - { - 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, - })} - - )} - + + +
); } diff --git a/ts/components/conversation/Timeline.stories.tsx b/ts/components/conversation/Timeline.stories.tsx index 4c42ee100c7a..3a5a692a85c8 100644 --- a/ts/components/conversation/Timeline.stories.tsx +++ b/ts/components/conversation/Timeline.stories.tsx @@ -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'), diff --git a/ts/components/conversation/TimelineItem.stories.tsx b/ts/components/conversation/TimelineItem.stories.tsx index e94841e10ea4..96e0e1e3ff7b 100644 --- a/ts/components/conversation/TimelineItem.stories.tsx +++ b/ts/components/conversation/TimelineItem.stories.tsx @@ -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'), diff --git a/ts/components/conversation/TimelineMessage.stories.tsx b/ts/components/conversation/TimelineMessage.stories.tsx index 0bbde2ce5d32..76c0943e2bf2 100644 --- a/ts/components/conversation/TimelineMessage.stories.tsx +++ b/ts/components/conversation/TimelineMessage.stories.tsx @@ -265,8 +265,6 @@ const createProps = (overrideProps: Partial = {}): 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 => ({ showExpiredOutgoingTapToViewToast: action( 'showExpiredOutgoingTapToViewToast' ), + toggleDeleteMessagesModal: action('toggleDeleteMessagesModal'), toggleForwardMessagesModal: action('toggleForwardMessagesModal'), showLightbox: action('showLightbox'), startConversation: action('startConversation'), diff --git a/ts/components/conversation/TimelineMessage.tsx b/ts/components/conversation/TimelineMessage.tsx index 8cb2d63b3e8e..5a44d16c91ff 100644 --- a/ts/components/conversation/TimelineMessage.tsx +++ b/ts/components/conversation/TimelineMessage.tsx @@ -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; export type PropsActions = { - deleteMessages: (options: { - conversationId: string; - messageIds: ReadonlyArray; - }) => void; - deleteMessageForEveryone: (id: string) => void; pushPanelForConversation: PushPanelForConversationActionType; - toggleForwardMessagesModal: (id: Array) => void; + toggleDeleteMessagesModal: (props: DeleteMessagesPropsType) => void; + toggleForwardMessagesModal: (messageIds: Array) => 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 && ( - deleteMessageForEveryone(id), - style: 'negative', - text: i18n('icu:delete'), - }, - ]} - dialogName="TimelineMessage/deleteMessageForEveryone" - i18n={i18n} - onClose={() => setHasDOEConfirmation(false)} - > - {i18n('icu:deleteForEveryoneWarning')} - - )} - {hasDeleteConfirmation && ( - - 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, - })} - - )} - 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 = ( @@ -746,27 +695,11 @@ const MessageContextMenu = ({ event.stopPropagation(); event.preventDefault(); - onDeleteForMe(); + onDeleteMessage(); }} > - {i18n('icu:deleteMessage')} + {i18n('icu:MessageContextMenu__deleteMessage')} - {onDeleteForEveryone && ( - { - event.stopPropagation(); - event.preventDefault(); - - onDeleteForEveryone(); - }} - > - {i18n('icu:deleteMessageForEveryone')} - - )} ); diff --git a/ts/groups/joinViaLink.ts b/ts/groups/joinViaLink.ts index 7663aa9e1514..2008e2447bf3 100644 --- a/ts/groups/joinViaLink.ts +++ b/ts/groups/joinViaLink.ts @@ -72,7 +72,9 @@ export async function joinViaLink(hash: string): Promise { 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 { conversationId: existingConversation.id, }); - window.reduxActions.toast.showToast(ToastType.AlreadyRequestedToJoin); + window.reduxActions.toast.showToast({ + toastType: ToastType.AlreadyRequestedToJoin, + }); return; } diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index b36641d856d7..065dc3a4f326 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -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 -): { toastType: ToastType; parameters?: ReplacementValuesType } | undefined { +): AnyToast | undefined { if (!file) { return; } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index eac82a7b6dc2..97d11a0fe11a 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -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 ): ThunkAction< void, RootStateType, @@ -2866,39 +2866,50 @@ function deleteMessageForEveryone( NoopActionType | ShowToastActionType > { return async dispatch => { - const message = window.MessageController.getById(messageId); - if (!message) { - throw new Error( - `deleteMessageForEveryone: Message ${messageId} missing!` - ); - } + let hasError = false; - const conversation = message.getConversation(); - if (!conversation) { - throw new Error('deleteMessageForEveryone: no conversation'); - } + await Promise.all( + messageIds.map(async messageId => { + try { + const message = window.MessageController.getById(messageId); + if (!message) { + throw new Error( + `deleteMessageForEveryone: Message ${messageId} missing!` + ); + } - try { - await sendDeleteForEveryoneMessage(conversation.attributes, { - id: message.id, - timestamp: message.get('sent_at'), - }); - dispatch({ - type: 'NOOP', - payload: null, - }); - } catch (error) { - log.error( - 'Error sending delete-for-everyone', - Errors.toLogFormat(error), - messageId - ); + const conversation = message.getConversation(); + if (!conversation) { + throw new Error('deleteMessageForEveryone: no conversation'); + } + + await sendDeleteForEveryoneMessage(conversation.attributes, { + id: message.id, + timestamp: message.get('sent_at'), + }); + } catch (error) { + hasError = true; + log.error( + '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, + }); } }; } diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index c229453d977a..e8181a8dc0e5 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -45,8 +45,10 @@ import type { ShowToastActionType } from './toast'; export type EditHistoryMessagesType = ReadonlyDeep< Array >; -export type ConfirmDeleteForMeModalProps = ReadonlyDeep<{ - count: number; +export type DeleteMessagesPropsType = ReadonlyDeep<{ + conversationId: string; + messageIds: ReadonlyArray; + onDelete?: () => void; }>; export type ForwardMessagePropsType = ReadonlyDeep; 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, 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, diff --git a/ts/state/ducks/toast.ts b/ts/state/ducks/toast.ts index 4310f5028062..b4d6879f30f0 100644 --- a/ts/state/ducks/toast.ts +++ b/ts/state/ducks/toast.ts @@ -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, diff --git a/ts/state/ducks/username.ts b/ts/state/ducks/username.ts index 35a05dff95a6..7d20f0b27910 100644 --- a/ts/state/ducks/username.ts +++ b/ts/state/ducks/username.ts @@ -244,7 +244,7 @@ export function deleteUsername({ try { await doDeleteUsername(username); } catch { - dispatch(showToast(ToastType.FailedToDeleteUsername)); + dispatch(showToast({ toastType: ToastType.FailedToDeleteUsername })); } }; diff --git a/ts/state/selectors/message.ts b/ts/state/selectors/message.ts index abbc773e96b8..03e4b118c182 100644 --- a/ts/state/selectors/message.ts +++ b/ts/state/selectors/message.ts @@ -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) => messageIds], + (messagesLookup, messageIds) => { + return messageIds.every(messageId => { + const message = getOwn(messagesLookup, messageId); + return message != null && canDeleteForEveryone(message); + }); + } +); + export function canRetryDeleteForEveryone( message: Pick< MessageWithUIFieldsType, diff --git a/ts/state/smart/CompositionArea.tsx b/ts/state/smart/CompositionArea.tsx index 84ea7e20ce71..797e22067517 100644 --- a/ts/state/smart/CompositionArea.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -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, }; }; diff --git a/ts/state/smart/ConversationView.tsx b/ts/state/smart/ConversationView.tsx index 1216f6f19deb..08f99f75292f 100644 --- a/ts/state/smart/ConversationView.tsx +++ b/ts/state/smart/ConversationView.tsx @@ -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 ); }); diff --git a/ts/state/smart/DeleteMessagesModal.tsx b/ts/state/smart/DeleteMessagesModal.tsx new file mode 100644 index 000000000000..041c6bddf697 --- /dev/null +++ b/ts/state/smart/DeleteMessagesModal.tsx @@ -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 ( + { + toggleDeleteMessagesModal(undefined); + }} + onDeleteForMe={() => { + deleteMessages({ + conversationId, + messageIds, + lastSelectedMessage, + }); + onDelete?.(); + }} + onDeleteForEveryone={() => { + deleteMessagesForEveryone(messageIds); + onDelete?.(); + }} + showToast={showToast} + /> + ); +} diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx index dfa5cdf3c93e..b3acea598471 100644 --- a/ts/state/smart/GlobalModalContainer.tsx +++ b/ts/state/smart/GlobalModalContainer.tsx @@ -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 ; @@ -34,6 +35,10 @@ function renderContactModal(): JSX.Element { return ; } +function renderDeleteMessagesModal(): JSX.Element { + return ; +} + function renderForwardMessagesModal(): JSX.Element { return ; } @@ -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} diff --git a/ts/state/smart/StoryViewer.tsx b/ts/state/smart/StoryViewer.tsx index bad08f96989f..c5673cc5e6c2 100644 --- a/ts/state/smart/StoryViewer.tsx +++ b/ts/state/smart/StoryViewer.tsx @@ -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} diff --git a/ts/state/smart/TimelineItem.tsx b/ts/state/smart/TimelineItem.tsx index d43894889e21..a80d849e8188 100644 --- a/ts/state/smart/TimelineItem.tsx +++ b/ts/state/smart/TimelineItem.tsx @@ -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} diff --git a/ts/test-electron/state/ducks/username_test.ts b/ts/test-electron/state/ducks/username_test.ts index 6f6fd4e2621c..885d490d7348 100644 --- a/ts/test-electron/state/ducks/username_test.ts +++ b/ts/test-electron/state/ducks/username_test.ts @@ -441,7 +441,6 @@ describe('electron/state/ducks/username', () => { type: 'toast/SHOW_TOAST', payload: { toastType: ToastType.FailedToDeleteUsername, - parameters: undefined, }, }); }); diff --git a/ts/types/Toast.tsx b/ts/types/Toast.tsx index ffcbd5e84492..20abde4e9cd0 100644 --- a/ts/types/Toast.tsx +++ b/ts/types/Toast.tsx @@ -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 }; + }; diff --git a/ts/util/shouldShowInvalidMessageToast.ts b/ts/util/shouldShowInvalidMessageToast.ts index 35005069f85c..c7a2c6320246 100644 --- a/ts/util/shouldShowInvalidMessageToast.ts +++ b/ts/util/shouldShowInvalidMessageToast.ts @@ -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;