From d0f17a1398fd092cc8055ad652ff78ed56ccbe5b Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Fri, 24 Mar 2023 14:16:48 -0700 Subject: [PATCH] Add shortcuts for forward/delete selected/targeted messages --- _locales/en/messages.json | 22 ++++++++++++-- ts/background.ts | 25 +++++++++++----- ts/components/ShortcutGuide.tsx | 2 +- .../conversation/ConversationView.tsx | 14 ++++----- .../conversation/SelectModeActions.tsx | 8 ++--- .../conversation/TimelineMessage.tsx | 11 +++++-- ts/state/ducks/globalModals.ts | 29 +++++++++++++++++++ ts/state/smart/ConversationView.tsx | 17 ++++++----- ts/util/showConfirmationDialog.tsx | 13 +++++++-- 9 files changed, 106 insertions(+), 35 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a981c376c2..7fc305a6ac 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2717,6 +2717,10 @@ }, "Keyboard--delete-message": { "message": "Delete selected message", + "description": "(deleted 03/24/2023) Shown in the shortcuts guide" + }, + "Keyboard--delete-messages": { + "message": "Delete selected messages", "description": "Shown in the shortcuts guide" }, "Keyboard--add-newline": { @@ -4653,15 +4657,27 @@ }, "icu:SelectModeActions__confirmDelete--title": { "messageformat": "Delete {count, plural, one {# message} other {# messages}}?", - "description": "conversation > in select mode > composition area actions > delete selected messages > confirmation modal > title" + "description": "(deleted 03/24/2023) conversation > in select mode > composition area actions > delete selected messages > confirmation modal > title" + }, + "icu:ConfirmDeleteForMeModal--title": { + "messageformat": "Delete {count, plural, one {# message} other {# messages}}?", + "description": "delete selected messages > confirmation modal > title" }, "icu:SelectModeActions__confirmDelete--description": { "messageformat": "{count, plural, one {This message} other {These messages}} will be deleted from this device.", - "description": "conversation > in select mode > composition area actions > delete selected messages > confirmation modal > message" + "description": "(deleted 03/24/2023) conversation > in select mode > composition area actions > delete selected messages > confirmation modal > message" + }, + "icu:ConfirmDeleteForMeModal--description": { + "messageformat": "{count, plural, one {This message} other {These messages}} will be deleted from this device.", + "description": "delete selected messages > confirmation modal > description" }, "icu:SelectModeActions__confirmDelete--confirm": { "messageformat": "Delete for me", - "description": "conversation > in select mode > composition area actions > delete selected messages > confirmation modal > button" + "description": "(deleted 03/24/2023) conversation > in select mode > composition area actions > delete selected messages > confirmation modal > button" + }, + "icu:ConfirmDeleteForMeModal--confirm": { + "messageformat": "Delete for me", + "description": "delete selected messages > confirmation modal > button" }, "icu:SelectModeActions__toast--TooManyMessagesToForward": { "messageformat": "You can only forward up to 30 messages", diff --git a/ts/background.ts b/ts/background.ts index be6302bbb2..94ee1d23a4 100644 --- a/ts/background.ts +++ b/ts/background.ts @@ -628,7 +628,7 @@ export async function startApp(): Promise { onTopOfEverything: true, cancelText: window.i18n('quit'), confirmStyle: 'negative', - message: window.i18n('deleteOldIndexedDBData'), + title: window.i18n('deleteOldIndexedDBData'), okText: window.i18n('deleteOldData'), reject: () => reject(), resolve: () => resolve(), @@ -1712,21 +1712,32 @@ export async function startApp(): Promise { shiftKey && (key === 'd' || key === 'D') ) { - const { targetedMessage } = state.conversations; + const { forwardMessagesProps } = state.globalModals; + const { targetedMessage, selectedMessageIds } = state.conversations; - if (targetedMessage) { + const messageIds = + selectedMessageIds ?? + (targetedMessage != null ? [targetedMessage] : null); + + if (forwardMessagesProps == null && messageIds != null) { event.preventDefault(); event.stopPropagation(); showConfirmationDialog({ - dialogName: 'deleteMessage', + dialogName: 'ConfirmDeleteForMeModal', confirmStyle: 'negative', - message: window.i18n('deleteWarning'), - okText: window.i18n('delete'), + 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: [targetedMessage], + messageIds, }); }, }); diff --git a/ts/components/ShortcutGuide.tsx b/ts/components/ShortcutGuide.tsx index a436ddadc1..34dad13bb6 100644 --- a/ts/components/ShortcutGuide.tsx +++ b/ts/components/ShortcutGuide.tsx @@ -163,7 +163,7 @@ const MESSAGE_SHORTCUTS: Array = [ keys: [['commandOrCtrl', 'S']], }, { - description: 'Keyboard--delete-message', + description: 'Keyboard--delete-messages', keys: [['commandOrCtrl', 'shift', 'D']], }, ]; diff --git a/ts/components/conversation/ConversationView.tsx b/ts/components/conversation/ConversationView.tsx index 1b23e70aff..8947017f0d 100644 --- a/ts/components/conversation/ConversationView.tsx +++ b/ts/components/conversation/ConversationView.tsx @@ -6,6 +6,9 @@ import { useEscapeHandling } from '../../hooks/useEscapeHandling'; export type PropsType = { conversationId: string; + hasOpenModal: boolean; + isSelectMode: boolean; + onExitSelectMode: () => void; processAttachments: (options: { conversationId: string; files: ReadonlyArray; @@ -14,21 +17,18 @@ export type PropsType = { renderConversationHeader: () => JSX.Element; renderTimeline: () => JSX.Element; renderPanel: () => JSX.Element | undefined; - isSelectMode: boolean; - isForwardModalOpen: boolean; - onExitSelectMode: () => void; }; export function ConversationView({ conversationId, + hasOpenModal, + isSelectMode, + onExitSelectMode, processAttachments, renderCompositionArea, renderConversationHeader, renderTimeline, renderPanel, - isSelectMode, - isForwardModalOpen, - onExitSelectMode, }: PropsType): JSX.Element { const onDrop = React.useCallback( (event: React.DragEvent) => { @@ -88,7 +88,7 @@ export function ConversationView({ ); useEscapeHandling( - isSelectMode && !isForwardModalOpen ? onExitSelectMode : undefined + isSelectMode && !hasOpenModal ? onExitSelectMode : undefined ); return ( diff --git a/ts/components/conversation/SelectModeActions.tsx b/ts/components/conversation/SelectModeActions.tsx index e4a13e1f12..d3bebf5558 100644 --- a/ts/components/conversation/SelectModeActions.tsx +++ b/ts/components/conversation/SelectModeActions.tsx @@ -103,11 +103,11 @@ export default function SelectModeActions({ onDeleteMessages(); }, style: 'negative', - text: i18n('icu:SelectModeActions__confirmDelete--confirm'), + text: i18n('icu:ConfirmDeleteForMeModal--confirm'), }, ]} - dialogName="TimelineMessage/deleteMessage" - title={i18n('icu:SelectModeActions__confirmDelete--title', { + dialogName="ConfirmDeleteForMeModal" + title={i18n('icu:ConfirmDeleteForMeModal--title', { count: selectedMessageIds.length, })} i18n={i18n} @@ -115,7 +115,7 @@ export default function SelectModeActions({ setConfirmDelete(false); }} > - {i18n('icu:SelectModeActions__confirmDelete--description', { + {i18n('icu:ConfirmDeleteForMeModal--description', { count: selectedMessageIds.length, })} diff --git a/ts/components/conversation/TimelineMessage.tsx b/ts/components/conversation/TimelineMessage.tsx index 5b77718dd5..4de7664560 100644 --- a/ts/components/conversation/TimelineMessage.tsx +++ b/ts/components/conversation/TimelineMessage.tsx @@ -369,14 +369,19 @@ export function TimelineMessage(props: Props): JSX.Element { messageIds: [id], }), style: 'negative', - text: i18n('delete'), + text: i18n('icu:ConfirmDeleteForMeModal--confirm'), }, ]} - dialogName="TimelineMessage/deleteMessage" + dialogName="ConfirmDeleteForMeModal" i18n={i18n} onClose={() => setHasDeleteConfirmation(false)} + title={i18n('icu:ConfirmDeleteForMeModal--title', { + count: 1, + })} > - {i18n('deleteWarning')} + {i18n('icu:ConfirmDeleteForMeModal--description', { + count: 1, + })} )} diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index 6e851c6c70..9c4c6d0e40 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -29,6 +29,9 @@ import type { ShowToastActionType } from './toast'; // State +export type ConfirmDeleteForMeModalProps = ReadonlyDeep<{ + count: number; +}>; export type ForwardMessagePropsType = ReadonlyDeep< Omit >; @@ -60,6 +63,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{ }; forwardMessagesProps?: ForwardMessagesPropsType; gv2MigrationProps?: MigrateToGV2PropsType; + hasConfirmationModal: boolean; isProfileEditorVisible: boolean; isSignalConnectionsVisible: boolean; isShortcutGuideModalVisible: boolean; @@ -105,6 +109,7 @@ const SHOW_ERROR_MODAL = 'globalModals/SHOW_ERROR_MODAL'; const CLOSE_SHORTCUT_GUIDE_MODAL = 'globalModals/CLOSE_SHORTCUT_GUIDE_MODAL'; const SHOW_SHORTCUT_GUIDE_MODAL = 'globalModals/SHOW_SHORTCUT_GUIDE_MODAL'; const SHOW_AUTH_ART_CREATOR = 'globalModals/SHOW_AUTH_ART_CREATOR'; +const TOGGLE_CONFIRMATION_MODAL = 'globalModals/TOGGLE_CONFIRMATION_MODAL'; const CANCEL_AUTH_ART_CREATOR = 'globalModals/CANCEL_AUTH_ART_CREATOR'; const CONFIRM_AUTH_ART_CREATOR_PENDING = 'globalModals/CONFIRM_AUTH_ART_CREATOR_PENDING'; @@ -180,6 +185,11 @@ type ToggleSignalConnectionsModalActionType = ReadonlyDeep<{ type: typeof TOGGLE_SIGNAL_CONNECTIONS_MODAL; }>; +type ToggleConfirmationModalActionType = ReadonlyDeep<{ + type: typeof TOGGLE_CONFIRMATION_MODAL; + payload: boolean; +}>; + type ShowStoriesSettingsActionType = ReadonlyDeep<{ type: typeof SHOW_STORIES_SETTINGS; }>; @@ -283,6 +293,7 @@ export type GlobalModalsActionType = ReadonlyDeep< | ToggleSafetyNumberModalActionType | ToggleAddUserToAnotherGroupModalActionType | ToggleSignalConnectionsModalActionType + | ToggleConfirmationModalActionType >; // Action Creators @@ -304,6 +315,7 @@ export const actions = { toggleSafetyNumberModal, toggleAddUserToAnotherGroupModal, toggleSignalConnectionsModal, + toggleConfirmationModal, showGV2MigrationDialog, closeGV2MigrationDialog, showStickerPackPreview, @@ -500,6 +512,15 @@ function toggleSignalConnectionsModal(): ToggleSignalConnectionsModalActionType }; } +function toggleConfirmationModal( + isOpen: boolean +): ToggleConfirmationModalActionType { + return { + type: TOGGLE_CONFIRMATION_MODAL, + payload: isOpen, + }; +} + function showBlockingSafetyNumberChangeDialog( untrustedByConversation: RecipientsByConversation, explodedPromise: ExplodePromiseResultType, @@ -663,6 +684,7 @@ export function confirmAuthorizeArtCreator(): ThunkAction< export function getEmptyState(): GlobalModalsStateType { return { + hasConfirmationModal: false, isProfileEditorVisible: false, isShortcutGuideModalVisible: false, isSignalConnectionsVisible: false, @@ -776,6 +798,13 @@ export function reducer( }; } + if (action.type === TOGGLE_CONFIRMATION_MODAL) { + return { + ...state, + hasConfirmationModal: action.payload, + }; + } + if (action.type === SHOW_SEND_ANYWAY_DIALOG) { const { promiseUuid, source } = action.payload; diff --git a/ts/state/smart/ConversationView.tsx b/ts/state/smart/ConversationView.tsx index 70e316add1..1216f6f19d 100644 --- a/ts/state/smart/ConversationView.tsx +++ b/ts/state/smart/ConversationView.tsx @@ -48,13 +48,21 @@ export function SmartConversationView(): JSX.Element { const { processAttachments } = useComposerActions(); const i18n = useSelector(getIntl); - const isForwardModalOpen = useSelector((state: StateType) => { - return state.globalModals.forwardMessagesProps != null; + const hasOpenModal = useSelector((state: StateType) => { + return ( + state.globalModals.forwardMessagesProps != null || + state.globalModals.hasConfirmationModal + ); }); return ( { + toggleSelectMode(false); + }} processAttachments={processAttachments} renderCompositionArea={() => } renderConversationHeader={() => ( @@ -179,11 +187,6 @@ export function SmartConversationView(): JSX.Element { return undefined; }} - isSelectMode={isSelectMode} - isForwardModalOpen={isForwardModalOpen} - onExitSelectMode={() => { - toggleSelectMode(false); - }} /> ); } diff --git a/ts/util/showConfirmationDialog.tsx b/ts/util/showConfirmationDialog.tsx index 59d1d1bd0f..ed29532423 100644 --- a/ts/util/showConfirmationDialog.tsx +++ b/ts/util/showConfirmationDialog.tsx @@ -10,7 +10,8 @@ type ConfirmationDialogViewProps = { dialogName: string; cancelText?: string; confirmStyle?: 'affirmative' | 'negative'; - message: string; + title: string; + description?: string; okText: string; reject?: (error: Error) => void; resolve: () => void; @@ -24,6 +25,8 @@ function removeConfirmationDialog() { return; } + window.reduxActions.globalModals.toggleConfirmationModal(false); + unmountComponentAtNode(confirmationDialogViewNode); document.body.removeChild(confirmationDialogViewNode); @@ -43,6 +46,8 @@ export function showConfirmationDialog( removeConfirmationDialog(); } + window.reduxActions.globalModals.toggleConfirmationModal(true); + confirmationDialogViewNode = document.createElement('div'); document.body.appendChild(confirmationDialogViewNode); @@ -71,8 +76,10 @@ export function showConfirmationDialog( onClose={() => { removeConfirmationDialog(); }} - title={options.message} - />, + title={options.title} + > + {options.description} + , confirmationDialogViewNode ); }