DOE stories when they are part of deleted lists

This commit is contained in:
Josh Perez 2022-10-13 12:14:50 -04:00 committed by GitHub
parent b12de415f4
commit b2792639aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 226 additions and 189 deletions

View file

@ -5516,7 +5516,7 @@
"description": "Button label to delete a private distribution list" "description": "Button label to delete a private distribution list"
}, },
"StoriesSettings__delete-list--confirm": { "StoriesSettings__delete-list--confirm": {
"message": "Delete private story?", "message": "Are you sure you want to delete \"$name$\"? Updates shared to this story will also be deleted.",
"description": "Confirmation text to delete a private distribution list" "description": "Confirmation text to delete a private distribution list"
}, },
"StoriesSettings__choose-viewers": { "StoriesSettings__choose-viewers": {

View file

@ -214,8 +214,8 @@ export const SendStoryModal = ({
const [confirmRemoveGroupId, setConfirmRemoveGroupId] = useState< const [confirmRemoveGroupId, setConfirmRemoveGroupId] = useState<
string | undefined string | undefined
>(); >();
const [confirmDeleteListId, setConfirmDeleteListId] = useState< const [confirmDeleteList, setConfirmDeleteList] = useState<
string | undefined { id: string; name: string } | undefined
>(); >();
const [listIdToEdit, setListIdToEdit] = useState<string | undefined>(); const [listIdToEdit, setListIdToEdit] = useState<string | undefined>();
@ -397,7 +397,7 @@ export const SendStoryModal = ({
listToEdit={listToEdit} listToEdit={listToEdit}
onRemoveMember={onRemoveMember} onRemoveMember={onRemoveMember}
onRepliesNReactionsChanged={onRepliesNReactionsChanged} onRepliesNReactionsChanged={onRepliesNReactionsChanged}
setConfirmDeleteListId={setConfirmDeleteListId} setConfirmDeleteList={setConfirmDeleteList}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections} setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
setPage={setPage} setPage={setPage}
setSelectedContacts={setSelectedContacts} setSelectedContacts={setSelectedContacts}
@ -719,7 +719,7 @@ export const SendStoryModal = ({
{ {
label: i18n('SendStoryModal__delete-story'), label: i18n('SendStoryModal__delete-story'),
icon: 'SendStoryModal__icon--delete', icon: 'SendStoryModal__icon--delete',
onClick: () => setConfirmDeleteListId(list.id), onClick: () => setConfirmDeleteList(list),
}, },
] ]
} }
@ -897,19 +897,19 @@ export const SendStoryModal = ({
onClose={() => { onClose={() => {
setConfirmRemoveGroupId(undefined); setConfirmRemoveGroupId(undefined);
}} }}
theme={Theme.Dark}
> >
{i18n('SendStoryModal__confirm-remove-group')} {i18n('SendStoryModal__confirm-remove-group')}
</ConfirmationDialog> </ConfirmationDialog>
)} )}
{confirmDeleteListId && ( {confirmDeleteList && (
<ConfirmationDialog <ConfirmationDialog
dialogName="SendStoryModal.confirmDeleteList" dialogName="SendStoryModal.confirmDeleteList"
actions={[ actions={[
{ {
action: () => { action: () => {
onDeleteList(confirmDeleteListId); onDeleteList(confirmDeleteList.id);
setConfirmDeleteListId(undefined); setConfirmDeleteList(undefined);
// setListToEditId(undefined);
}, },
style: 'negative', style: 'negative',
text: i18n('delete'), text: i18n('delete'),
@ -917,10 +917,13 @@ export const SendStoryModal = ({
]} ]}
i18n={i18n} i18n={i18n}
onClose={() => { onClose={() => {
setConfirmDeleteListId(undefined); setConfirmDeleteList(undefined);
}} }}
theme={Theme.Dark}
> >
{i18n('StoriesSettings__delete-list--confirm')} {i18n('StoriesSettings__delete-list--confirm', [
confirmDeleteList.name,
])}
</ConfirmationDialog> </ConfirmationDialog>
)} )}
{confirmDiscardModal} {confirmDiscardModal}

View file

@ -126,8 +126,8 @@ export const StoriesSettingsModal = ({
setPage(Page.DistributionLists); setPage(Page.DistributionLists);
}, []); }, []);
const [confirmDeleteListId, setConfirmDeleteListId] = useState< const [confirmDeleteList, setConfirmDeleteList] = useState<
string | undefined { id: string; name: string } | undefined
>(); >();
let modal: RenderModalPage | null; let modal: RenderModalPage | null;
@ -188,7 +188,7 @@ export const StoriesSettingsModal = ({
listToEdit={listToEdit} listToEdit={listToEdit}
onRemoveMember={onRemoveMember} onRemoveMember={onRemoveMember}
onRepliesNReactionsChanged={onRepliesNReactionsChanged} onRepliesNReactionsChanged={onRepliesNReactionsChanged}
setConfirmDeleteListId={setConfirmDeleteListId} setConfirmDeleteList={setConfirmDeleteList}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections} setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
setPage={setPage} setPage={setPage}
setSelectedContacts={setSelectedContacts} setSelectedContacts={setSelectedContacts}
@ -297,13 +297,13 @@ export const StoriesSettingsModal = ({
{modal} {modal}
</PagedModal> </PagedModal>
)} )}
{confirmDeleteListId && ( {confirmDeleteList && (
<ConfirmationDialog <ConfirmationDialog
dialogName="StoriesSettings.deleteList" dialogName="StoriesSettings.deleteList"
actions={[ actions={[
{ {
action: () => { action: () => {
onDeleteList(confirmDeleteListId); onDeleteList(confirmDeleteList.id);
setListToEditId(undefined); setListToEditId(undefined);
}, },
style: 'negative', style: 'negative',
@ -312,10 +312,13 @@ export const StoriesSettingsModal = ({
]} ]}
i18n={i18n} i18n={i18n}
onClose={() => { onClose={() => {
setConfirmDeleteListId(undefined); setConfirmDeleteList(undefined);
}} }}
theme={Theme.Dark}
> >
{i18n('StoriesSettings__delete-list--confirm')} {i18n('StoriesSettings__delete-list--confirm', [
confirmDeleteList.name,
])}
</ConfirmationDialog> </ConfirmationDialog>
)} )}
{confirmDiscardModal} {confirmDiscardModal}
@ -326,7 +329,7 @@ export const StoriesSettingsModal = ({
type DistributionListSettingsModalPropsType = { type DistributionListSettingsModalPropsType = {
i18n: LocalizerType; i18n: LocalizerType;
listToEdit: StoryDistributionListWithMembersDataType; listToEdit: StoryDistributionListWithMembersDataType;
setConfirmDeleteListId: (id: string) => unknown; setConfirmDeleteList: (_: { id: string; name: string }) => unknown;
setPage: (page: Page) => unknown; setPage: (page: Page) => unknown;
setSelectedContacts: (contacts: Array<ConversationType>) => unknown; setSelectedContacts: (contacts: Array<ConversationType>) => unknown;
onBackButtonClick: (() => void) | undefined; onBackButtonClick: (() => void) | undefined;
@ -348,7 +351,7 @@ export const DistributionListSettingsModal = ({
onRepliesNReactionsChanged, onRepliesNReactionsChanged,
onBackButtonClick, onBackButtonClick,
onClose, onClose,
setConfirmDeleteListId, setConfirmDeleteList,
setMyStoriesToAllSignalConnections, setMyStoriesToAllSignalConnections,
setPage, setPage,
setSelectedContacts, setSelectedContacts,
@ -504,7 +507,7 @@ export const DistributionListSettingsModal = ({
<button <button
className="StoriesSettingsModal__delete-list" className="StoriesSettingsModal__delete-list"
onClick={() => setConfirmDeleteListId(listToEdit.id)} onClick={() => setConfirmDeleteList(listToEdit)}
type="button" type="button"
> >
{i18n('StoriesSettings__delete-list')} {i18n('StoriesSettings__delete-list')}
@ -530,6 +533,7 @@ export const DistributionListSettingsModal = ({
onClose={() => { onClose={() => {
setConfirmRemoveMember(undefined); setConfirmRemoveMember(undefined);
}} }}
theme={Theme.Dark}
title={i18n('StoriesSettings__remove--title', [ title={i18n('StoriesSettings__remove--title', [
confirmRemoveMember.title, confirmRemoveMember.title,
])} ])}

View file

@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; import type { ThunkAction, ThunkDispatch } from 'redux-thunk';
import { isEqual, noop, pick } from 'lodash'; import { isEqual, pick } from 'lodash';
import type { AttachmentType } from '../../types/Attachment'; import type { AttachmentType } from '../../types/Attachment';
import type { BodyRangeType } from '../../types/Util'; import type { BodyRangeType } from '../../types/Util';
import type { ConversationModel } from '../../models/conversations'; import type { ConversationModel } from '../../models/conversations';
@ -19,20 +19,18 @@ import type { SyncType } from '../../jobs/helpers/syncHelpers';
import type { UUIDStringType } from '../../types/UUID'; import type { UUIDStringType } from '../../types/UUID';
import * as log from '../../logging/log'; import * as log from '../../logging/log';
import dataInterface from '../../sql/Client'; import dataInterface from '../../sql/Client';
import { DAY } from '../../util/durations';
import { ReadStatus } from '../../messages/MessageReadStatus'; import { ReadStatus } from '../../messages/MessageReadStatus';
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog'; import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories'; import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
import { StoryRecipientUpdateEvent } from '../../textsecure/messageReceiverEvents';
import { ToastReactionFailed } from '../../components/ToastReactionFailed'; import { ToastReactionFailed } from '../../components/ToastReactionFailed';
import { assertDev } from '../../util/assert'; import { assertDev } from '../../util/assert';
import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified'; import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified';
import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/deleteStoryForEveryone';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend'; import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { getMessageById } from '../../messages/getMessageById'; import { getMessageById } from '../../messages/getMessageById';
import { markViewed } from '../../services/MessageUpdater'; import { markViewed } from '../../services/MessageUpdater';
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads'; import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
import { replaceIndex } from '../../util/replaceIndex'; import { replaceIndex } from '../../util/replaceIndex';
import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
import { showToast } from '../../util/showToast'; import { showToast } from '../../util/showToast';
import { import {
hasFailed, hasFailed,
@ -44,13 +42,11 @@ import {
getConversationSelector, getConversationSelector,
getHideStoryConversationIds, getHideStoryConversationIds,
} from '../selectors/conversations'; } from '../selectors/conversations';
import { getSendOptions } from '../../util/getSendOptions';
import { getStories } from '../selectors/stories'; import { getStories } from '../selectors/stories';
import { getStoryDataFromMessageAttributes } from '../../services/storyLoader'; import { getStoryDataFromMessageAttributes } from '../../services/storyLoader';
import { isGroup } from '../../util/whatTypeOfConversation'; import { isGroup } from '../../util/whatTypeOfConversation';
import { isNotNil } from '../../util/isNotNil'; import { isNotNil } from '../../util/isNotNil';
import { isStory } from '../../messages/helpers'; import { isStory } from '../../messages/helpers';
import { onStoryRecipientUpdate } from '../../util/onStoryRecipientUpdate';
import { sendStoryMessage as doSendStoryMessage } from '../../util/sendStoryMessage'; import { sendStoryMessage as doSendStoryMessage } from '../../util/sendStoryMessage';
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
import { verifyStoryListMembers as doVerifyStoryListMembers } from '../../util/verifyStoryListMembers'; import { verifyStoryListMembers as doVerifyStoryListMembers } from '../../util/verifyStoryListMembers';
@ -214,169 +210,13 @@ function deleteStoryForEveryone(
return; return;
} }
const conversationIds = new Set(
story.sendState.map(({ recipient }) => recipient.id)
);
const updatedStoryRecipients = new Map<
string,
{
distributionListIds: Set<string>;
isAllowedToReply: boolean;
}
>();
const ourConversation =
window.ConversationController.getOurConversationOrThrow();
// Remove ourselves from the DOE.
conversationIds.delete(ourConversation.id);
// Find stories that were sent to other distribution lists so that we don't
// send a DOE request to the members of those lists.
const { stories } = getState().stories; const { stories } = getState().stories;
stories.forEach(item => { const storyData = stories.find(item => item.messageId === story.messageId);
const { sendStateByConversationId } = item; if (!storyData) {
// We only want matching timestamp stories which are stories that were log.warn('deleteStoryForEveryone: Could not find story in redux data');
// sent to multi distribution lists. return;
// We don't want the story we just passed in.
// Don't need to check for stories that have already been deleted.
// And only for sent stories, not incoming.
if (
item.timestamp !== story.timestamp ||
item.messageId === story.messageId ||
item.deletedForEveryone ||
!sendStateByConversationId
) {
return;
}
Object.keys(sendStateByConversationId).forEach(conversationId => {
if (conversationId === ourConversation.id) {
return;
}
const destinationUuid =
window.ConversationController.get(conversationId)?.get('uuid');
if (!destinationUuid) {
return;
}
const distributionListIds =
updatedStoryRecipients.get(destinationUuid)?.distributionListIds ||
new Set();
// These are the remaining distribution list ids that the user has
// access to.
updatedStoryRecipients.set(destinationUuid, {
distributionListIds: item.storyDistributionListId
? new Set([...distributionListIds, item.storyDistributionListId])
: distributionListIds,
isAllowedToReply:
sendStateByConversationId[conversationId]
.isAllowedToReplyToStory !== false,
});
// Remove this conversationId so we don't send the DOE to those that
// still have access.
conversationIds.delete(conversationId);
});
});
// Send the DOE
conversationIds.forEach(cid => {
// Don't DOE yourself!
if (cid === ourConversation.id) {
return;
}
const conversation = window.ConversationController.get(cid);
if (!conversation) {
return;
}
sendDeleteForEveryoneMessage(conversation.attributes, {
deleteForEveryoneDuration: DAY,
id: story.messageId,
timestamp: story.timestamp,
});
});
// If it's the last story sent to a distribution list we don't have to send
// the sync message, but to be consistent let's build up the updated
// storyMessageRecipients and send the sync message.
if (!updatedStoryRecipients.size) {
story.sendState.forEach(item => {
if (item.recipient.id === ourConversation.id) {
return;
}
const destinationUuid = window.ConversationController.get(
item.recipient.id
)?.get('uuid');
if (!destinationUuid) {
return;
}
updatedStoryRecipients.set(destinationUuid, {
distributionListIds: new Set(),
isAllowedToReply: item.isAllowedToReplyToStory !== false,
});
});
}
// Send the sync message with the updated storyMessageRecipients list
const sender = window.textsecure.messaging;
if (sender) {
const options = await getSendOptions(ourConversation.attributes, {
syncMessage: true,
});
const storyMessageRecipients: Array<{
destinationUuid: string;
distributionListIds: Array<string>;
isAllowedToReply: boolean;
}> = [];
updatedStoryRecipients.forEach((recipientData, destinationUuid) => {
storyMessageRecipients.push({
destinationUuid,
distributionListIds: Array.from(recipientData.distributionListIds),
isAllowedToReply: recipientData.isAllowedToReply,
});
});
const destinationUuid = ourConversation.get('uuid');
if (!destinationUuid) {
return;
}
// Sync message for other devices
sender.sendSyncMessage({
destination: undefined,
destinationUuid,
storyMessageRecipients,
expirationStartTimestamp: null,
isUpdate: true,
options,
timestamp: story.timestamp,
urgent: false,
});
// Sync message for Desktop
const ev = new StoryRecipientUpdateEvent(
{
destinationUuid,
timestamp: story.timestamp,
storyMessageRecipients,
},
noop
);
onStoryRecipientUpdate(ev);
} }
await doDeleteStoryForEveryone(stories, storyData);
dispatch({ dispatch({
type: DOE_STORY, type: DOE_STORY,

View file

@ -10,6 +10,7 @@ import * as log from '../../logging/log';
import dataInterface from '../../sql/Client'; import dataInterface from '../../sql/Client';
import { MY_STORIES_ID } from '../../types/Stories'; import { MY_STORIES_ID } from '../../types/Stories';
import { UUID } from '../../types/UUID'; import { UUID } from '../../types/UUID';
import { deleteStoryForEveryone } from '../../util/deleteStoryForEveryone';
import { replaceIndex } from '../../util/replaceIndex'; import { replaceIndex } from '../../util/replaceIndex';
import { storageServiceUploadJob } from '../../services/storage'; import { storageServiceUploadJob } from '../../services/storage';
import { useBoundActions } from '../../hooks/useBoundActions'; import { useBoundActions } from '../../hooks/useBoundActions';
@ -201,7 +202,7 @@ function createDistributionList(
function deleteDistributionList( function deleteDistributionList(
listId: string listId: string
): ThunkAction<void, RootStateType, unknown, DeleteListActionType> { ): ThunkAction<void, RootStateType, unknown, DeleteListActionType> {
return async dispatch => { return async (dispatch, getState) => {
const deletedAtTimestamp = Date.now(); const deletedAtTimestamp = Date.now();
const storyDistribution = const storyDistribution =
@ -225,6 +226,14 @@ function deleteDistributionList(
} }
); );
const { stories } = getState().stories;
const storiesToDelete = stories.filter(
story => story.storyDistributionListId === listId
);
await Promise.all(
storiesToDelete.map(story => deleteStoryForEveryone(stories, story))
);
log.info( log.info(
'storyDistributionLists.deleteDistributionList: list deleted', 'storyDistributionLists.deleteDistributionList: list deleted',
listId listId

View file

@ -0,0 +1,181 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { noop } from 'lodash';
import type { StoryDataType } from '../state/ducks/stories';
import { DAY } from './durations';
import { StoryRecipientUpdateEvent } from '../textsecure/messageReceiverEvents';
import { getSendOptions } from './getSendOptions';
import { onStoryRecipientUpdate } from './onStoryRecipientUpdate';
import { sendDeleteForEveryoneMessage } from './sendDeleteForEveryoneMessage';
export async function deleteStoryForEveryone(
stories: Array<StoryDataType>,
story: StoryDataType
): Promise<void> {
if (!story.sendStateByConversationId) {
return;
}
const conversationIds = new Set(Object.keys(story.sendStateByConversationId));
const updatedStoryRecipients = new Map<
string,
{
distributionListIds: Set<string>;
isAllowedToReply: boolean;
}
>();
const ourConversation =
window.ConversationController.getOurConversationOrThrow();
// Remove ourselves from the DOE.
conversationIds.delete(ourConversation.id);
// Find stories that were sent to other distribution lists so that we don't
// send a DOE request to the members of those lists.
stories.forEach(item => {
const { sendStateByConversationId } = item;
// We only want matching timestamp stories which are stories that were
// sent to multi distribution lists.
// We don't want the story we just passed in.
// Don't need to check for stories that have already been deleted.
// And only for sent stories, not incoming.
if (
item.timestamp !== story.timestamp ||
item.messageId === story.messageId ||
item.deletedForEveryone ||
!sendStateByConversationId
) {
return;
}
Object.keys(sendStateByConversationId).forEach(conversationId => {
if (conversationId === ourConversation.id) {
return;
}
const destinationUuid =
window.ConversationController.get(conversationId)?.get('uuid');
if (!destinationUuid) {
return;
}
const distributionListIds =
updatedStoryRecipients.get(destinationUuid)?.distributionListIds ||
new Set();
// These are the remaining distribution list ids that the user has
// access to.
updatedStoryRecipients.set(destinationUuid, {
distributionListIds: item.storyDistributionListId
? new Set([...distributionListIds, item.storyDistributionListId])
: distributionListIds,
isAllowedToReply:
sendStateByConversationId[conversationId].isAllowedToReplyToStory !==
false,
});
// Remove this conversationId so we don't send the DOE to those that
// still have access.
conversationIds.delete(conversationId);
});
});
// Send the DOE
conversationIds.forEach(cid => {
// Don't DOE yourself!
if (cid === ourConversation.id) {
return;
}
const conversation = window.ConversationController.get(cid);
if (!conversation) {
return;
}
sendDeleteForEveryoneMessage(conversation.attributes, {
deleteForEveryoneDuration: DAY,
id: story.messageId,
timestamp: story.timestamp,
});
});
// If it's the last story sent to a distribution list we don't have to send
// the sync message, but to be consistent let's build up the updated
// storyMessageRecipients and send the sync message.
if (!updatedStoryRecipients.size) {
Object.entries(story.sendStateByConversationId).forEach(
([recipientId, sendState]) => {
if (recipientId === ourConversation.id) {
return;
}
const destinationUuid =
window.ConversationController.get(recipientId)?.get('uuid');
if (!destinationUuid) {
return;
}
updatedStoryRecipients.set(destinationUuid, {
distributionListIds: new Set(),
isAllowedToReply: sendState.isAllowedToReplyToStory !== false,
});
}
);
}
// Send the sync message with the updated storyMessageRecipients list
const sender = window.textsecure.messaging;
if (sender) {
const options = await getSendOptions(ourConversation.attributes, {
syncMessage: true,
});
const storyMessageRecipients: Array<{
destinationUuid: string;
distributionListIds: Array<string>;
isAllowedToReply: boolean;
}> = [];
updatedStoryRecipients.forEach((recipientData, destinationUuid) => {
storyMessageRecipients.push({
destinationUuid,
distributionListIds: Array.from(recipientData.distributionListIds),
isAllowedToReply: recipientData.isAllowedToReply,
});
});
const destinationUuid = ourConversation.get('uuid');
if (!destinationUuid) {
return;
}
// Sync message for other devices
sender.sendSyncMessage({
destination: undefined,
destinationUuid,
storyMessageRecipients,
expirationStartTimestamp: null,
isUpdate: true,
options,
timestamp: story.timestamp,
urgent: false,
});
// Sync message for Desktop
const ev = new StoryRecipientUpdateEvent(
{
destinationUuid,
timestamp: story.timestamp,
storyMessageRecipients,
},
noop
);
onStoryRecipientUpdate(ev);
}
}