Show a warning dialog when sending first edited message
This commit is contained in:
parent
23b058fe10
commit
4d354c8005
8 changed files with 175 additions and 23 deletions
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
38
ts/components/SendEditWarningModal.tsx
Normal file
38
ts/components/SendEditWarningModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
1
ts/types/Storage.d.ts
vendored
1
ts/types/Storage.d.ts
vendored
|
@ -88,6 +88,7 @@ export type StorageAccessType = {
|
|||
regionCode: string;
|
||||
registrationIdMap: Record<string, number>;
|
||||
remoteBuildExpiration: number;
|
||||
sendEditWarningShown: boolean;
|
||||
sessionResets: SessionResetsType;
|
||||
showStickerPickerHint: boolean;
|
||||
showStickersIntroduction: boolean;
|
||||
|
|
10
ts/util/maybeBlockSendForEditWarningModal.ts
Normal file
10
ts/util/maybeBlockSendForEditWarningModal.ts
Normal 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;
|
||||
}
|
Loading…
Add table
Reference in a new issue