diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e8b6ee332a..264ee637ae 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -5742,6 +5742,14 @@ "messageformat": "Chat marked unread", "description": "A toast that shows up when user marks a conversation as unread" }, + "icu:SendEdit--dialog--title": { + "messageformat": "Signal beta only", + "description": "Title of the modal shown before sending your first edit message" + }, + "icu:SendEdit--dialog--body": { + "messageformat": "Editing messages is available to Signal beta users only. If you edit a message, it will only be visible to people who are on the latest version of Signal beta.", + "description": "Body text of the modal shown before sending your first edit message" + }, "icu:SendFormatting--dialog--title": { "messageformat": "Sending formatted text", "description": "Title of the modal shown before sending your first formatting message" diff --git a/ts/components/GlobalModalContainer.tsx b/ts/components/GlobalModalContainer.tsx index 43555627ac..aa4977457f 100644 --- a/ts/components/GlobalModalContainer.tsx +++ b/ts/components/GlobalModalContainer.tsx @@ -10,6 +10,7 @@ import type { FormattingWarningDataType, ForwardMessagesPropsType, SafetyNumberChangedBlockingDataType, + SendEditWarningDataType, UserNotFoundModalStateType, } from '../state/ducks/globalModals'; import type { LocalizerType, ThemeType } from '../types/Util'; @@ -19,6 +20,7 @@ import { missingCaseError } from '../util/missingCaseError'; import { ButtonVariant } from './Button'; import { ConfirmationDialog } from './ConfirmationDialog'; import { FormattingWarningModal } from './FormattingWarningModal'; +import { SendEditWarningModal } from './SendEditWarningModal'; import { SignalConnectionsModal } from './SignalConnectionsModal'; import { WhatsNewModal } from './WhatsNewModal'; @@ -59,6 +61,11 @@ export type PropsType = { // SafetyNumberModal safetyNumberModalContactId: string | undefined; renderSafetyNumber: () => JSX.Element; + // SendEditWarningModal + showSendEditWarningModal: ( + explodedPromise: ExplodePromiseResultType | undefined + ) => void; + sendEditWarningData: SendEditWarningDataType | undefined; // ShortcutGuideModal isShortcutGuideModalVisible: boolean; renderShortcutGuideModal: () => JSX.Element; @@ -119,6 +126,9 @@ export function GlobalModalContainer({ // SafetyNumberModal safetyNumberModalContactId, renderSafetyNumber, + // SendEditWarningDataType + showSendEditWarningModal, + sendEditWarningData, // ShortcutGuideModal isShortcutGuideModalVisible, renderShortcutGuideModal, @@ -205,6 +215,23 @@ export function GlobalModalContainer({ return renderProfileEditor(); } + if (sendEditWarningData) { + const { resolve } = sendEditWarningData.explodedPromise; + return ( + { + showSendEditWarningModal(undefined); + resolve(true); + }} + onCancel={() => { + showSendEditWarningModal(undefined); + resolve(false); + }} + /> + ); + } + if (isShortcutGuideModalVisible) { return renderShortcutGuideModal(); } diff --git a/ts/components/SendEditWarningModal.tsx b/ts/components/SendEditWarningModal.tsx new file mode 100644 index 0000000000..e73d39db32 --- /dev/null +++ b/ts/components/SendEditWarningModal.tsx @@ -0,0 +1,38 @@ +// Copyright 2021 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import React from 'react'; +import type { LocalizerType } from '../types/Util'; +import { ConfirmationDialog } from './ConfirmationDialog'; + +type PropsType = { + i18n: LocalizerType; + onSendAnyway: () => void; + onCancel: () => void; +}; + +export function SendEditWarningModal({ + i18n, + onSendAnyway, + onCancel, +}: PropsType): JSX.Element | null { + return ( + + {i18n('icu:SendEdit--dialog--body')} + + ); +} diff --git a/ts/state/ducks/composer.ts b/ts/state/ducks/composer.ts index 28730db597..436d0d29a1 100644 --- a/ts/state/ducks/composer.ts +++ b/ts/state/ducks/composer.ts @@ -90,6 +90,7 @@ import { strictAssert } from '../../util/assert'; import { makeQuote } from '../../util/makeQuote'; import { sendEditedMessage as doSendEditedMessage } from '../../util/sendEditedMessage'; import { maybeBlockSendForFormattingModal } from '../../util/maybeBlockSendForFormattingModal'; +import { maybeBlockSendForEditWarningModal } from '../../util/maybeBlockSendForEditWarningModal'; import { Sound, SoundType } from '../../util/Sound'; // State @@ -386,6 +387,7 @@ export function handleLeaveConversation( type WithPreSendChecksOptions = Readonly<{ bodyRanges?: DraftBodyRanges; message?: string; + isEditedMessage?: boolean; voiceNoteAttachment?: InMemoryAttachmentDraftType; }>; @@ -409,7 +411,7 @@ async function withPreSendChecks( conversation.attributes, ]); - const { bodyRanges, message, voiceNoteAttachment } = options; + const { bodyRanges, isEditedMessage, message, voiceNoteAttachment } = options; try { dispatch(setComposerDisabledState(conversationId, true)); @@ -449,6 +451,27 @@ async function withPreSendChecks( return; } + try { + if ( + isEditedMessage && + !window.storage.get('sendEditWarningShown') && + !window.SignalCI + ) { + const sendAnyway = await maybeBlockSendForEditWarningModal(); + if (!sendAnyway) { + dispatch(setComposerDisabledState(conversationId, false)); + return; + } + drop(window.storage.put('sendEditWarningShown', true)); + } + } catch (error) { + log.error( + 'withPreSendChecks block for send edit warning modal:', + Errors.toLogFormat(error) + ); + return; + } + const toast = shouldShowInvalidMessageToast(conversation.attributes); if (toast != null) { dispatch({ @@ -506,28 +529,33 @@ function sendEditedMessage( targetMessageId, } = options; - await withPreSendChecks(conversationId, options, dispatch, async () => { - try { - await doSendEditedMessage(conversationId, { - body: message, - bodyRanges, - preview: getLinkPreviewForSend(message), - quoteAuthorUuid, - quoteSentAt, - targetMessageId, - }); - } catch (error) { - log.error('sendEditedMessage', Errors.toLogFormat(error)); - if (error.toastType) { - dispatch({ - type: SHOW_TOAST, - payload: { - toastType: error.toastType, - }, + await withPreSendChecks( + conversationId, + { ...options, isEditedMessage: true }, + dispatch, + async () => { + try { + await doSendEditedMessage(conversationId, { + body: message, + bodyRanges, + preview: getLinkPreviewForSend(message), + quoteAuthorUuid, + quoteSentAt, + targetMessageId, }); + } catch (error) { + log.error('sendEditedMessage', Errors.toLogFormat(error)); + if (error.toastType) { + dispatch({ + type: SHOW_TOAST, + payload: { + toastType: error.toastType, + }, + }); + } } } - }); + ); }; } diff --git a/ts/state/ducks/globalModals.ts b/ts/state/ducks/globalModals.ts index bd9273251c..aeed4330ad 100644 --- a/ts/state/ducks/globalModals.ts +++ b/ts/state/ducks/globalModals.ts @@ -62,6 +62,9 @@ export type SafetyNumberChangedBlockingDataType = ReadonlyDeep<{ export type FormattingWarningDataType = ReadonlyDeep<{ explodedPromise: ExplodePromiseResultType; }>; +export type SendEditWarningDataType = ReadonlyDeep<{ + explodedPromise: ExplodePromiseResultType; +}>; export type AuthorizeArtCreatorDataType = ReadonlyDeep; @@ -96,6 +99,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{ profileEditorHasError: boolean; safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType; safetyNumberModalContactId?: string; + sendEditWarningData?: SendEditWarningDataType; stickerPackPreviewId?: string; userNotFoundModalState?: UserNotFoundModalStateType; }>; @@ -132,6 +136,8 @@ const CLOSE_ERROR_MODAL = 'globalModals/CLOSE_ERROR_MODAL'; const SHOW_ERROR_MODAL = 'globalModals/SHOW_ERROR_MODAL'; const SHOW_FORMATTING_WARNING_MODAL = 'globalModals/SHOW_FORMATTING_WARNING_MODAL'; +const SHOW_SEND_EDIT_WARNING_MODAL = + 'globalModals/SHOW_SEND_EDIT_WARNING_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'; @@ -234,6 +240,13 @@ type ShowFormattingWarningModalActionType = ReadonlyDeep<{ }; }>; +type ShowSendEditWarningModalActionType = ReadonlyDeep<{ + type: typeof SHOW_SEND_EDIT_WARNING_MODAL; + payload: { + explodedPromise: ExplodePromiseResultType | undefined; + }; +}>; + type HideStoriesSettingsActionType = ReadonlyDeep<{ type: typeof HIDE_STORIES_SETTINGS; }>; @@ -338,6 +351,7 @@ export type GlobalModalsActionType = ReadonlyDeep< | ShowErrorModalActionType | ShowFormattingWarningModalActionType | ShowSendAnywayDialogActionType + | ShowSendEditWarningModalActionType | ShowShortcutGuideModalActionType | ShowStickerPackPreviewActionType | ShowStoriesSettingsActionType @@ -375,6 +389,7 @@ export const actions = { showEditHistoryModal, showErrorModal, showFormattingWarningModal, + showSendEditWarningModal, showGV2MigrationDialog, showShortcutGuideModal, showStickerPackPreview, @@ -455,6 +470,12 @@ function showFormattingWarningModal( return { type: SHOW_FORMATTING_WARNING_MODAL, payload: { explodedPromise } }; } +function showSendEditWarningModal( + explodedPromise: ExplodePromiseResultType | undefined +): ShowSendEditWarningModalActionType { + return { type: SHOW_SEND_EDIT_WARNING_MODAL, payload: { explodedPromise } }; +} + function showGV2MigrationDialog( conversationId: string ): ThunkAction { @@ -984,6 +1005,21 @@ export function reducer( }; } + if (action.type === SHOW_SEND_EDIT_WARNING_MODAL) { + const { explodedPromise } = action.payload; + if (!explodedPromise) { + return { + ...state, + sendEditWarningData: undefined, + }; + } + + return { + ...state, + sendEditWarningData: { explodedPromise }, + }; + } + if (action.type === SHOW_STICKER_PACK_PREVIEW) { return { ...state, diff --git a/ts/state/smart/GlobalModalContainer.tsx b/ts/state/smart/GlobalModalContainer.tsx index acaa7bf123..0215bf37eb 100644 --- a/ts/state/smart/GlobalModalContainer.tsx +++ b/ts/state/smart/GlobalModalContainer.tsx @@ -64,12 +64,14 @@ export function SmartGlobalModalContainer(): JSX.Element { const { addUserToAnotherGroupModalContactId, + authArtCreatorData, contactModalState, + deleteMessagesProps, editHistoryMessages, errorModalProps, - deleteMessagesProps, formattingWarningData, forwardMessagesProps, + isAuthorizingArtCreator, isProfileEditorVisible, isShortcutGuideModalVisible, isSignalConnectionsVisible, @@ -77,10 +79,9 @@ export function SmartGlobalModalContainer(): JSX.Element { isWhatsNewVisible, safetyNumberChangedBlockingData, safetyNumberModalContactId, + sendEditWarningData, stickerPackPreviewId, userNotFoundModalState, - isAuthorizingArtCreator, - authArtCreatorData, } = useSelector( state => state.globalModals ); @@ -92,6 +93,7 @@ export function SmartGlobalModalContainer(): JSX.Element { hideUserNotFoundModal, hideWhatsNewModal, showFormattingWarningModal, + showSendEditWarningModal, toggleSignalConnectionsModal, } = useGlobalModalActions(); @@ -162,7 +164,9 @@ export function SmartGlobalModalContainer(): JSX.Element { renderStoriesSettings={renderStoriesSettings} safetyNumberChangedBlockingData={safetyNumberChangedBlockingData} safetyNumberModalContactId={safetyNumberModalContactId} + sendEditWarningData={sendEditWarningData} showFormattingWarningModal={showFormattingWarningModal} + showSendEditWarningModal={showSendEditWarningModal} stickerPackPreviewId={stickerPackPreviewId} theme={theme} toggleSignalConnectionsModal={toggleSignalConnectionsModal} diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index 20b3d63b1f..ab5e839d91 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -88,6 +88,7 @@ export type StorageAccessType = { regionCode: string; registrationIdMap: Record; remoteBuildExpiration: number; + sendEditWarningShown: boolean; sessionResets: SessionResetsType; showStickerPickerHint: boolean; showStickersIntroduction: boolean; diff --git a/ts/util/maybeBlockSendForEditWarningModal.ts b/ts/util/maybeBlockSendForEditWarningModal.ts new file mode 100644 index 0000000000..6516f29872 --- /dev/null +++ b/ts/util/maybeBlockSendForEditWarningModal.ts @@ -0,0 +1,10 @@ +// Copyright 2023 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import { explodePromise } from './explodePromise'; + +export async function maybeBlockSendForEditWarningModal(): Promise { + const explodedPromise = explodePromise(); + window.reduxActions.globalModals.showSendEditWarningModal(explodedPromise); + return explodedPromise.promise; +}