Support for local deletes synced to all your devices

This commit is contained in:
Scott Nonnenberg 2024-05-29 01:56:00 +10:00 committed by GitHub
parent 06f71a7ef8
commit 11eb1782a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 2094 additions and 72 deletions

View file

@ -0,0 +1,78 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { Meta, StoryFn } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import enMessages from '../../_locales/en/messages.json';
import { setupI18n } from '../util/setupI18n';
import DeleteMessagesModal from './DeleteMessagesModal';
import type { DeleteMessagesModalProps } from './DeleteMessagesModal';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/DeleteMessagesModal',
component: DeleteMessagesModal,
args: {
i18n,
isMe: false,
isDeleteSyncSendEnabled: false,
canDeleteForEveryone: true,
messageCount: 1,
onClose: action('onClose'),
onDeleteForMe: action('onDeleteForMe'),
onDeleteForEveryone: action('onDeleteForEveryone'),
showToast: action('showToast'),
},
} satisfies Meta<DeleteMessagesModalProps>;
function createProps(args: Partial<DeleteMessagesModalProps>) {
return {
i18n,
isMe: false,
isDeleteSyncSendEnabled: false,
canDeleteForEveryone: true,
messageCount: 1,
onClose: action('onClose'),
onDeleteForMe: action('onDeleteForMe'),
onDeleteForEveryone: action('onDeleteForEveryone'),
showToast: action('showToast'),
...args,
};
}
// eslint-disable-next-line react/function-component-definition
const Template: StoryFn<DeleteMessagesModalProps> = args => {
return <DeleteMessagesModal {...args} />;
};
export const OneMessage = Template.bind({});
export const ThreeMessages = Template.bind({});
ThreeMessages.args = createProps({
messageCount: 3,
});
export const IsMe = Template.bind({});
IsMe.args = createProps({
isMe: true,
});
export const IsMeThreeMessages = Template.bind({});
IsMeThreeMessages.args = createProps({
isMe: true,
messageCount: 3,
});
export const DeleteSyncEnabled = Template.bind({});
DeleteSyncEnabled.args = createProps({
isDeleteSyncSendEnabled: true,
});
export const IsMeDeleteSyncEnabled = Template.bind({});
IsMeDeleteSyncEnabled.args = createProps({
isDeleteSyncSendEnabled: true,
isMe: true,
});

View file

@ -8,8 +8,9 @@ import type { LocalizerType } from '../types/Util';
import type { ShowToastAction } from '../state/ducks/toast';
import { ToastType } from '../types/Toast';
type DeleteMessagesModalProps = Readonly<{
export type DeleteMessagesModalProps = Readonly<{
isMe: boolean;
isDeleteSyncSendEnabled: boolean;
canDeleteForEveryone: boolean;
i18n: LocalizerType;
messageCount: number;
@ -23,6 +24,7 @@ const MAX_DELETE_FOR_EVERYONE = 30;
export default function DeleteMessagesModal({
isMe,
isDeleteSyncSendEnabled,
canDeleteForEveryone,
i18n,
messageCount,
@ -33,15 +35,22 @@ export default function DeleteMessagesModal({
}: DeleteMessagesModalProps): JSX.Element {
const actions: Array<ActionSpec> = [];
const syncNoteToSelfDelete = isMe && isDeleteSyncSendEnabled;
let deleteForMeText = i18n('icu:DeleteMessagesModal--deleteForMe');
if (syncNoteToSelfDelete) {
deleteForMeText = i18n('icu:DeleteMessagesModal--noteToSelf--deleteSync');
} else if (isMe) {
deleteForMeText = i18n('icu:DeleteMessagesModal--deleteFromThisDevice');
}
actions.push({
action: onDeleteForMe,
style: 'negative',
text: isMe
? i18n('icu:DeleteMessagesModal--deleteFromThisDevice')
: i18n('icu:DeleteMessagesModal--deleteForMe'),
text: deleteForMeText,
});
if (canDeleteForEveryone) {
if (canDeleteForEveryone && !syncNoteToSelfDelete) {
const tooManyMessages = messageCount > MAX_DELETE_FOR_EVERYONE;
actions.push({
'aria-disabled': tooManyMessages,
@ -63,6 +72,20 @@ export default function DeleteMessagesModal({
});
}
let descriptionText = i18n('icu:DeleteMessagesModal--description', {
count: messageCount,
});
if (syncNoteToSelfDelete) {
descriptionText = i18n(
'icu:DeleteMessagesModal--description--noteToSelf--deleteSync',
{ count: messageCount }
);
} else if (isMe) {
descriptionText = i18n('icu:DeleteMessagesModal--description--noteToSelf', {
count: messageCount,
});
}
return (
<ConfirmationDialog
actions={actions}
@ -74,13 +97,7 @@ export default function DeleteMessagesModal({
})}
moduleClassName="DeleteMessagesModal"
>
{isMe
? i18n('icu:DeleteMessagesModal--description--noteToSelf', {
count: messageCount,
})
: i18n('icu:DeleteMessagesModal--description', {
count: messageCount,
})}
{descriptionText}
</ConfirmationDialog>
);
}

View file

@ -0,0 +1,30 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { Meta, StoryFn } from '@storybook/react';
import React from 'react';
import { action } from '@storybook/addon-actions';
import type { PropsType } from './LocalDeleteWarningModal';
import enMessages from '../../_locales/en/messages.json';
import { LocalDeleteWarningModal } from './LocalDeleteWarningModal';
import { setupI18n } from '../util/setupI18n';
const i18n = setupI18n('en', enMessages);
export default {
title: 'Components/LocalDeleteWarningModal',
component: LocalDeleteWarningModal,
args: {
i18n,
onClose: action('onClose'),
},
} satisfies Meta<PropsType>;
// eslint-disable-next-line react/function-component-definition
const Template: StoryFn<PropsType> = args => (
<LocalDeleteWarningModal {...args} />
);
export const Modal = Template.bind({});
Modal.args = {};

View file

@ -0,0 +1,53 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import type { LocalizerType } from '../types/Util';
import { Button, ButtonVariant } from './Button';
import { I18n } from './I18n';
import { Modal } from './Modal';
export type PropsType = {
i18n: LocalizerType;
onClose: () => unknown;
};
export function LocalDeleteWarningModal({
i18n,
onClose,
}: PropsType): JSX.Element {
return (
<Modal
modalName="LocalDeleteWarningModal"
moduleClassName="LocalDeleteWarningModal"
i18n={i18n}
onClose={onClose}
>
<div className="LocalDeleteWarningModal">
<div className="LocalDeleteWarningModal__image">
<img
src="images/local-delete-sync.svg"
height="92"
width="138"
alt=""
/>
</div>
<div className="LocalDeleteWarningModal__header">
<I18n i18n={i18n} id="icu:LocalDeleteWarningModal__header" />
</div>
<div className="LocalDeleteWarningModal__description">
<I18n i18n={i18n} id="icu:LocalDeleteWarningModal__description" />
</div>
<div className="LocalDeleteWarningModal__button">
<Button onClick={onClose} variant={ButtonVariant.Primary}>
<I18n i18n={i18n} id="icu:LocalDeleteWarningModal__confirm" />
</Button>
</div>
</div>
</Modal>
);
}

View file

@ -46,6 +46,10 @@ const commonProps = {
i18n,
localDeleteWarningShown: true,
isDeleteSyncSendEnabled: true,
setLocalDeleteWarningShown: action('setLocalDeleteWarningShown'),
onConversationAccept: action('onConversationAccept'),
onConversationArchive: action('onConversationArchive'),
onConversationBlock: action('onConversationBlock'),
@ -412,3 +416,32 @@ export function Unaccepted(): JSX.Element {
</>
);
}
export function NeedsDeleteConfirmation(): JSX.Element {
const [localDeleteWarningShown, setLocalDeleteWarningShown] =
React.useState(false);
const props = {
...commonProps,
conversation: getDefaultConversation(),
localDeleteWarningShown,
setLocalDeleteWarningShown: () => setLocalDeleteWarningShown(true),
};
const theme = useContext(StorybookThemeContext);
return <ConversationHeader {...props} theme={theme} />;
}
export function NeedsDeleteConfirmationButNotEnabled(): JSX.Element {
const [localDeleteWarningShown, setLocalDeleteWarningShown] =
React.useState(false);
const props = {
...commonProps,
conversation: getDefaultConversation(),
localDeleteWarningShown,
isDeleteSyncSendEnabled: false,
setLocalDeleteWarningShown: () => setLocalDeleteWarningShown(true),
};
const theme = useContext(StorybookThemeContext);
return <ConversationHeader {...props} theme={theme} />;
}

View file

@ -38,6 +38,7 @@ import {
MessageRequestState,
} from './MessageRequestActionsConfirmation';
import type { MinimalConversation } from '../../hooks/useMinimalConversation';
import { LocalDeleteWarningModal } from '../LocalDeleteWarningModal';
function HeaderInfoTitle({
name,
@ -92,6 +93,8 @@ export type PropsDataType = {
conversationName: ContactNameData;
hasPanelShowing?: boolean;
hasStories?: HasStories;
localDeleteWarningShown: boolean;
isDeleteSyncSendEnabled: boolean;
isMissingMandatoryProfileSharing?: boolean;
isSelectMode: boolean;
isSignalConversation?: boolean;
@ -102,6 +105,8 @@ export type PropsDataType = {
};
export type PropsActionsType = {
setLocalDeleteWarningShown: () => void;
onConversationAccept: () => void;
onConversationArchive: () => void;
onConversationBlock: () => void;
@ -147,10 +152,12 @@ export const ConversationHeader = memo(function ConversationHeader({
hasPanelShowing,
hasStories,
i18n,
isDeleteSyncSendEnabled,
isMissingMandatoryProfileSharing,
isSelectMode,
isSignalConversation,
isSMSOnly,
localDeleteWarningShown,
onConversationAccept,
onConversationArchive,
onConversationBlock,
@ -174,6 +181,7 @@ export const ConversationHeader = memo(function ConversationHeader({
onViewRecentMedia,
onViewUserStories,
outgoingCallButtonStyle,
setLocalDeleteWarningShown,
sharedGroupNames,
theme,
}: PropsType): JSX.Element | null {
@ -223,13 +231,16 @@ export const ConversationHeader = memo(function ConversationHeader({
{hasDeleteMessagesConfirmation && (
<DeleteMessagesConfirmationDialog
i18n={i18n}
onDestoryMessages={() => {
isDeleteSyncSendEnabled={isDeleteSyncSendEnabled}
localDeleteWarningShown={localDeleteWarningShown}
onDestroyMessages={() => {
setHasDeleteMessagesConfirmation(false);
onConversationDeleteMessages();
}}
onClose={() => {
setHasDeleteMessagesConfirmation(false);
}}
setLocalDeleteWarningShown={setLocalDeleteWarningShown}
/>
)}
{hasLeaveGroupConfirmation && (
@ -923,14 +934,29 @@ function CannotLeaveGroupBecauseYouAreLastAdminAlert({
}
function DeleteMessagesConfirmationDialog({
isDeleteSyncSendEnabled,
i18n,
onDestoryMessages,
localDeleteWarningShown,
onDestroyMessages,
onClose,
setLocalDeleteWarningShown,
}: {
isDeleteSyncSendEnabled: boolean;
i18n: LocalizerType;
onDestoryMessages: () => void;
localDeleteWarningShown: boolean;
onDestroyMessages: () => void;
onClose: () => void;
setLocalDeleteWarningShown: () => void;
}) {
if (!localDeleteWarningShown && isDeleteSyncSendEnabled) {
return (
<LocalDeleteWarningModal
i18n={i18n}
onClose={setLocalDeleteWarningShown}
/>
);
}
return (
<ConfirmationDialog
dialogName="ConversationHeader.destroyMessages"
@ -939,7 +965,7 @@ function DeleteMessagesConfirmationDialog({
)}
actions={[
{
action: onDestoryMessages,
action: onDestroyMessages,
style: 'negative',
text: i18n('icu:delete'),
},