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",
"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"

View file

@ -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<boolean> | 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 (
<SendEditWarningModal
i18n={i18n}
onSendAnyway={() => {
showSendEditWarningModal(undefined);
resolve(true);
}}
onCancel={() => {
showSendEditWarningModal(undefined);
resolve(false);
}}
/>
);
}
if (isShortcutGuideModalVisible) {
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 { 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,
},
});
}
}
}
});
);
};
}

View file

@ -62,6 +62,9 @@ export type SafetyNumberChangedBlockingDataType = ReadonlyDeep<{
export type FormattingWarningDataType = ReadonlyDeep<{
explodedPromise: ExplodePromiseResultType<boolean>;
}>;
export type SendEditWarningDataType = ReadonlyDeep<{
explodedPromise: ExplodePromiseResultType<boolean>;
}>;
export type AuthorizeArtCreatorDataType =
ReadonlyDeep<AuthorizeArtCreatorOptionsType>;
@ -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<boolean> | 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<boolean> | undefined
): ShowSendEditWarningModalActionType {
return { type: SHOW_SEND_EDIT_WARNING_MODAL, payload: { explodedPromise } };
}
function showGV2MigrationDialog(
conversationId: string
): 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) {
return {
...state,

View file

@ -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<StateType, GlobalModalsStateType>(
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}

View file

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