Show a warning dialog when sending first edited message

This commit is contained in:
Josh Perez 2023-06-14 15:20:06 -07:00 committed by GitHub
parent 23b058fe10
commit 4d354c8005
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 175 additions and 23 deletions

View file

@ -5742,6 +5742,14 @@
"messageformat": "Chat marked unread", "messageformat": "Chat marked unread",
"description": "A toast that shows up when user marks a conversation as 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": { "icu:SendFormatting--dialog--title": {
"messageformat": "Sending formatted text", "messageformat": "Sending formatted text",
"description": "Title of the modal shown before sending your first formatting message" "description": "Title of the modal shown before sending your first formatting message"

View file

@ -10,6 +10,7 @@ import type {
FormattingWarningDataType, FormattingWarningDataType,
ForwardMessagesPropsType, ForwardMessagesPropsType,
SafetyNumberChangedBlockingDataType, SafetyNumberChangedBlockingDataType,
SendEditWarningDataType,
UserNotFoundModalStateType, UserNotFoundModalStateType,
} from '../state/ducks/globalModals'; } from '../state/ducks/globalModals';
import type { LocalizerType, ThemeType } from '../types/Util'; import type { LocalizerType, ThemeType } from '../types/Util';
@ -19,6 +20,7 @@ import { missingCaseError } from '../util/missingCaseError';
import { ButtonVariant } from './Button'; import { ButtonVariant } from './Button';
import { ConfirmationDialog } from './ConfirmationDialog'; import { ConfirmationDialog } from './ConfirmationDialog';
import { FormattingWarningModal } from './FormattingWarningModal'; import { FormattingWarningModal } from './FormattingWarningModal';
import { SendEditWarningModal } from './SendEditWarningModal';
import { SignalConnectionsModal } from './SignalConnectionsModal'; import { SignalConnectionsModal } from './SignalConnectionsModal';
import { WhatsNewModal } from './WhatsNewModal'; import { WhatsNewModal } from './WhatsNewModal';
@ -59,6 +61,11 @@ export type PropsType = {
// SafetyNumberModal // SafetyNumberModal
safetyNumberModalContactId: string | undefined; safetyNumberModalContactId: string | undefined;
renderSafetyNumber: () => JSX.Element; renderSafetyNumber: () => JSX.Element;
// SendEditWarningModal
showSendEditWarningModal: (
explodedPromise: ExplodePromiseResultType<boolean> | undefined
) => void;
sendEditWarningData: SendEditWarningDataType | undefined;
// ShortcutGuideModal // ShortcutGuideModal
isShortcutGuideModalVisible: boolean; isShortcutGuideModalVisible: boolean;
renderShortcutGuideModal: () => JSX.Element; renderShortcutGuideModal: () => JSX.Element;
@ -119,6 +126,9 @@ export function GlobalModalContainer({
// SafetyNumberModal // SafetyNumberModal
safetyNumberModalContactId, safetyNumberModalContactId,
renderSafetyNumber, renderSafetyNumber,
// SendEditWarningDataType
showSendEditWarningModal,
sendEditWarningData,
// ShortcutGuideModal // ShortcutGuideModal
isShortcutGuideModalVisible, isShortcutGuideModalVisible,
renderShortcutGuideModal, renderShortcutGuideModal,
@ -205,6 +215,23 @@ export function GlobalModalContainer({
return renderProfileEditor(); return renderProfileEditor();
} }
if (sendEditWarningData) {
const { resolve } = sendEditWarningData.explodedPromise;
return (
<SendEditWarningModal
i18n={i18n}
onSendAnyway={() => {
showSendEditWarningModal(undefined);
resolve(true);
}}
onCancel={() => {
showSendEditWarningModal(undefined);
resolve(false);
}}
/>
);
}
if (isShortcutGuideModalVisible) { if (isShortcutGuideModalVisible) {
return renderShortcutGuideModal(); return renderShortcutGuideModal();
} }

View file

@ -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 (
<ConfirmationDialog
actions={[
{
action: onSendAnyway,
autoClose: true,
style: 'affirmative',
text: i18n('icu:sendAnyway'),
},
]}
dialogName="SendEditWarningModal"
i18n={i18n}
onCancel={onCancel}
onClose={onCancel}
title={i18n('icu:SendEdit--dialog--title')}
>
{i18n('icu:SendEdit--dialog--body')}
</ConfirmationDialog>
);
}

View file

@ -90,6 +90,7 @@ import { strictAssert } from '../../util/assert';
import { makeQuote } from '../../util/makeQuote'; import { makeQuote } from '../../util/makeQuote';
import { sendEditedMessage as doSendEditedMessage } from '../../util/sendEditedMessage'; import { sendEditedMessage as doSendEditedMessage } from '../../util/sendEditedMessage';
import { maybeBlockSendForFormattingModal } from '../../util/maybeBlockSendForFormattingModal'; import { maybeBlockSendForFormattingModal } from '../../util/maybeBlockSendForFormattingModal';
import { maybeBlockSendForEditWarningModal } from '../../util/maybeBlockSendForEditWarningModal';
import { Sound, SoundType } from '../../util/Sound'; import { Sound, SoundType } from '../../util/Sound';
// State // State
@ -386,6 +387,7 @@ export function handleLeaveConversation(
type WithPreSendChecksOptions = Readonly<{ type WithPreSendChecksOptions = Readonly<{
bodyRanges?: DraftBodyRanges; bodyRanges?: DraftBodyRanges;
message?: string; message?: string;
isEditedMessage?: boolean;
voiceNoteAttachment?: InMemoryAttachmentDraftType; voiceNoteAttachment?: InMemoryAttachmentDraftType;
}>; }>;
@ -409,7 +411,7 @@ async function withPreSendChecks(
conversation.attributes, conversation.attributes,
]); ]);
const { bodyRanges, message, voiceNoteAttachment } = options; const { bodyRanges, isEditedMessage, message, voiceNoteAttachment } = options;
try { try {
dispatch(setComposerDisabledState(conversationId, true)); dispatch(setComposerDisabledState(conversationId, true));
@ -449,6 +451,27 @@ async function withPreSendChecks(
return; 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); const toast = shouldShowInvalidMessageToast(conversation.attributes);
if (toast != null) { if (toast != null) {
dispatch({ dispatch({
@ -506,28 +529,33 @@ function sendEditedMessage(
targetMessageId, targetMessageId,
} = options; } = options;
await withPreSendChecks(conversationId, options, dispatch, async () => { await withPreSendChecks(
try { conversationId,
await doSendEditedMessage(conversationId, { { ...options, isEditedMessage: true },
body: message, dispatch,
bodyRanges, async () => {
preview: getLinkPreviewForSend(message), try {
quoteAuthorUuid, await doSendEditedMessage(conversationId, {
quoteSentAt, body: message,
targetMessageId, bodyRanges,
}); preview: getLinkPreviewForSend(message),
} catch (error) { quoteAuthorUuid,
log.error('sendEditedMessage', Errors.toLogFormat(error)); quoteSentAt,
if (error.toastType) { targetMessageId,
dispatch({
type: SHOW_TOAST,
payload: {
toastType: error.toastType,
},
}); });
} catch (error) {
log.error('sendEditedMessage', Errors.toLogFormat(error));
if (error.toastType) {
dispatch({
type: SHOW_TOAST,
payload: {
toastType: error.toastType,
},
});
}
} }
} }
}); );
}; };
} }

View file

@ -62,6 +62,9 @@ export type SafetyNumberChangedBlockingDataType = ReadonlyDeep<{
export type FormattingWarningDataType = ReadonlyDeep<{ export type FormattingWarningDataType = ReadonlyDeep<{
explodedPromise: ExplodePromiseResultType<boolean>; explodedPromise: ExplodePromiseResultType<boolean>;
}>; }>;
export type SendEditWarningDataType = ReadonlyDeep<{
explodedPromise: ExplodePromiseResultType<boolean>;
}>;
export type AuthorizeArtCreatorDataType = export type AuthorizeArtCreatorDataType =
ReadonlyDeep<AuthorizeArtCreatorOptionsType>; ReadonlyDeep<AuthorizeArtCreatorOptionsType>;
@ -96,6 +99,7 @@ export type GlobalModalsStateType = ReadonlyDeep<{
profileEditorHasError: boolean; profileEditorHasError: boolean;
safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType; safetyNumberChangedBlockingData?: SafetyNumberChangedBlockingDataType;
safetyNumberModalContactId?: string; safetyNumberModalContactId?: string;
sendEditWarningData?: SendEditWarningDataType;
stickerPackPreviewId?: string; stickerPackPreviewId?: string;
userNotFoundModalState?: UserNotFoundModalStateType; userNotFoundModalState?: UserNotFoundModalStateType;
}>; }>;
@ -132,6 +136,8 @@ const CLOSE_ERROR_MODAL = 'globalModals/CLOSE_ERROR_MODAL';
const SHOW_ERROR_MODAL = 'globalModals/SHOW_ERROR_MODAL'; const SHOW_ERROR_MODAL = 'globalModals/SHOW_ERROR_MODAL';
const SHOW_FORMATTING_WARNING_MODAL = const SHOW_FORMATTING_WARNING_MODAL =
'globalModals/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 CLOSE_SHORTCUT_GUIDE_MODAL = 'globalModals/CLOSE_SHORTCUT_GUIDE_MODAL';
const SHOW_SHORTCUT_GUIDE_MODAL = 'globalModals/SHOW_SHORTCUT_GUIDE_MODAL'; const SHOW_SHORTCUT_GUIDE_MODAL = 'globalModals/SHOW_SHORTCUT_GUIDE_MODAL';
const SHOW_AUTH_ART_CREATOR = 'globalModals/SHOW_AUTH_ART_CREATOR'; 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<boolean> | undefined;
};
}>;
type HideStoriesSettingsActionType = ReadonlyDeep<{ type HideStoriesSettingsActionType = ReadonlyDeep<{
type: typeof HIDE_STORIES_SETTINGS; type: typeof HIDE_STORIES_SETTINGS;
}>; }>;
@ -338,6 +351,7 @@ export type GlobalModalsActionType = ReadonlyDeep<
| ShowErrorModalActionType | ShowErrorModalActionType
| ShowFormattingWarningModalActionType | ShowFormattingWarningModalActionType
| ShowSendAnywayDialogActionType | ShowSendAnywayDialogActionType
| ShowSendEditWarningModalActionType
| ShowShortcutGuideModalActionType | ShowShortcutGuideModalActionType
| ShowStickerPackPreviewActionType | ShowStickerPackPreviewActionType
| ShowStoriesSettingsActionType | ShowStoriesSettingsActionType
@ -375,6 +389,7 @@ export const actions = {
showEditHistoryModal, showEditHistoryModal,
showErrorModal, showErrorModal,
showFormattingWarningModal, showFormattingWarningModal,
showSendEditWarningModal,
showGV2MigrationDialog, showGV2MigrationDialog,
showShortcutGuideModal, showShortcutGuideModal,
showStickerPackPreview, showStickerPackPreview,
@ -455,6 +470,12 @@ function showFormattingWarningModal(
return { type: SHOW_FORMATTING_WARNING_MODAL, payload: { explodedPromise } }; return { type: SHOW_FORMATTING_WARNING_MODAL, payload: { explodedPromise } };
} }
function showSendEditWarningModal(
explodedPromise: ExplodePromiseResultType<boolean> | undefined
): ShowSendEditWarningModalActionType {
return { type: SHOW_SEND_EDIT_WARNING_MODAL, payload: { explodedPromise } };
}
function showGV2MigrationDialog( function showGV2MigrationDialog(
conversationId: string conversationId: string
): ThunkAction<void, RootStateType, unknown, StartMigrationToGV2ActionType> { ): ThunkAction<void, RootStateType, unknown, StartMigrationToGV2ActionType> {
@ -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) { if (action.type === SHOW_STICKER_PACK_PREVIEW) {
return { return {
...state, ...state,

View file

@ -64,12 +64,14 @@ export function SmartGlobalModalContainer(): JSX.Element {
const { const {
addUserToAnotherGroupModalContactId, addUserToAnotherGroupModalContactId,
authArtCreatorData,
contactModalState, contactModalState,
deleteMessagesProps,
editHistoryMessages, editHistoryMessages,
errorModalProps, errorModalProps,
deleteMessagesProps,
formattingWarningData, formattingWarningData,
forwardMessagesProps, forwardMessagesProps,
isAuthorizingArtCreator,
isProfileEditorVisible, isProfileEditorVisible,
isShortcutGuideModalVisible, isShortcutGuideModalVisible,
isSignalConnectionsVisible, isSignalConnectionsVisible,
@ -77,10 +79,9 @@ export function SmartGlobalModalContainer(): JSX.Element {
isWhatsNewVisible, isWhatsNewVisible,
safetyNumberChangedBlockingData, safetyNumberChangedBlockingData,
safetyNumberModalContactId, safetyNumberModalContactId,
sendEditWarningData,
stickerPackPreviewId, stickerPackPreviewId,
userNotFoundModalState, userNotFoundModalState,
isAuthorizingArtCreator,
authArtCreatorData,
} = useSelector<StateType, GlobalModalsStateType>( } = useSelector<StateType, GlobalModalsStateType>(
state => state.globalModals state => state.globalModals
); );
@ -92,6 +93,7 @@ export function SmartGlobalModalContainer(): JSX.Element {
hideUserNotFoundModal, hideUserNotFoundModal,
hideWhatsNewModal, hideWhatsNewModal,
showFormattingWarningModal, showFormattingWarningModal,
showSendEditWarningModal,
toggleSignalConnectionsModal, toggleSignalConnectionsModal,
} = useGlobalModalActions(); } = useGlobalModalActions();
@ -162,7 +164,9 @@ export function SmartGlobalModalContainer(): JSX.Element {
renderStoriesSettings={renderStoriesSettings} renderStoriesSettings={renderStoriesSettings}
safetyNumberChangedBlockingData={safetyNumberChangedBlockingData} safetyNumberChangedBlockingData={safetyNumberChangedBlockingData}
safetyNumberModalContactId={safetyNumberModalContactId} safetyNumberModalContactId={safetyNumberModalContactId}
sendEditWarningData={sendEditWarningData}
showFormattingWarningModal={showFormattingWarningModal} showFormattingWarningModal={showFormattingWarningModal}
showSendEditWarningModal={showSendEditWarningModal}
stickerPackPreviewId={stickerPackPreviewId} stickerPackPreviewId={stickerPackPreviewId}
theme={theme} theme={theme}
toggleSignalConnectionsModal={toggleSignalConnectionsModal} toggleSignalConnectionsModal={toggleSignalConnectionsModal}

View file

@ -88,6 +88,7 @@ export type StorageAccessType = {
regionCode: string; regionCode: string;
registrationIdMap: Record<string, number>; registrationIdMap: Record<string, number>;
remoteBuildExpiration: number; remoteBuildExpiration: number;
sendEditWarningShown: boolean;
sessionResets: SessionResetsType; sessionResets: SessionResetsType;
showStickerPickerHint: boolean; showStickerPickerHint: boolean;
showStickersIntroduction: boolean; showStickersIntroduction: boolean;

View file

@ -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<boolean> {
const explodedPromise = explodePromise<boolean>();
window.reduxActions.globalModals.showSendEditWarningModal(explodedPromise);
return explodedPromise.promise;
}