Skip to content

Commit

Permalink
DOE stories when they are part of deleted lists
Browse files Browse the repository at this point in the history
Co-authored-by: Josh Perez <60019601+josh-signal@users.noreply.github.com>
  • Loading branch information
automated-signal and josh-signal committed Oct 14, 2022
1 parent 7903828 commit 522634b
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 189 deletions.
2 changes: 1 addition & 1 deletion _locales/en/messages.json
Expand Up @@ -5516,7 +5516,7 @@
"description": "Button label to delete a private distribution list"
},
"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"
},
"StoriesSettings__choose-viewers": {
Expand Down
23 changes: 13 additions & 10 deletions ts/components/SendStoryModal.tsx
Expand Up @@ -214,8 +214,8 @@ export const SendStoryModal = ({
const [confirmRemoveGroupId, setConfirmRemoveGroupId] = useState<
string | undefined
>();
const [confirmDeleteListId, setConfirmDeleteListId] = useState<
string | undefined
const [confirmDeleteList, setConfirmDeleteList] = useState<
{ id: string; name: string } | undefined
>();

const [listIdToEdit, setListIdToEdit] = useState<string | undefined>();
Expand Down Expand Up @@ -397,7 +397,7 @@ export const SendStoryModal = ({
listToEdit={listToEdit}
onRemoveMember={onRemoveMember}
onRepliesNReactionsChanged={onRepliesNReactionsChanged}
setConfirmDeleteListId={setConfirmDeleteListId}
setConfirmDeleteList={setConfirmDeleteList}
setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections}
setPage={setPage}
setSelectedContacts={setSelectedContacts}
Expand Down Expand Up @@ -719,7 +719,7 @@ export const SendStoryModal = ({
{
label: i18n('SendStoryModal__delete-story'),
icon: 'SendStoryModal__icon--delete',
onClick: () => setConfirmDeleteListId(list.id),
onClick: () => setConfirmDeleteList(list),
},
]
}
Expand Down Expand Up @@ -897,30 +897,33 @@ export const SendStoryModal = ({
onClose={() => {
setConfirmRemoveGroupId(undefined);
}}
theme={Theme.Dark}
>
{i18n('SendStoryModal__confirm-remove-group')}
</ConfirmationDialog>
)}
{confirmDeleteListId && (
{confirmDeleteList && (
<ConfirmationDialog
dialogName="SendStoryModal.confirmDeleteList"
actions={[
{
action: () => {
onDeleteList(confirmDeleteListId);
setConfirmDeleteListId(undefined);
// setListToEditId(undefined);
onDeleteList(confirmDeleteList.id);
setConfirmDeleteList(undefined);
},
style: 'negative',
text: i18n('delete'),
},
]}
i18n={i18n}
onClose={() => {
setConfirmDeleteListId(undefined);
setConfirmDeleteList(undefined);
}}
theme={Theme.Dark}
>
{i18n('StoriesSettings__delete-list--confirm')}
{i18n('StoriesSettings__delete-list--confirm', [
confirmDeleteList.name,
])}
</ConfirmationDialog>
)}
{confirmDiscardModal}
Expand Down
24 changes: 14 additions & 10 deletions ts/components/StoriesSettingsModal.tsx
Expand Up @@ -126,8 +126,8 @@ export const StoriesSettingsModal = ({
setPage(Page.DistributionLists);
}, []);

const [confirmDeleteListId, setConfirmDeleteListId] = useState<
string | undefined
const [confirmDeleteList, setConfirmDeleteList] = useState<
{ id: string; name: string } | undefined
>();

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

<button
className="StoriesSettingsModal__delete-list"
onClick={() => setConfirmDeleteListId(listToEdit.id)}
onClick={() => setConfirmDeleteList(listToEdit)}
type="button"
>
{i18n('StoriesSettings__delete-list')}
Expand All @@ -530,6 +533,7 @@ export const DistributionListSettingsModal = ({
onClose={() => {
setConfirmRemoveMember(undefined);
}}
theme={Theme.Dark}
title={i18n('StoriesSettings__remove--title', [
confirmRemoveMember.title,
])}
Expand Down
174 changes: 7 additions & 167 deletions ts/state/ducks/stories.ts
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: AGPL-3.0-only

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 { BodyRangeType } from '../../types/Util';
import type { ConversationModel } from '../../models/conversations';
Expand All @@ -19,20 +19,18 @@ import type { SyncType } from '../../jobs/helpers/syncHelpers';
import type { UUIDStringType } from '../../types/UUID';
import * as log from '../../logging/log';
import dataInterface from '../../sql/Client';
import { DAY } from '../../util/durations';
import { ReadStatus } from '../../messages/MessageReadStatus';
import { SafetyNumberChangeSource } from '../../components/SafetyNumberChangeDialog';
import { StoryViewDirectionType, StoryViewModeType } from '../../types/Stories';
import { StoryRecipientUpdateEvent } from '../../textsecure/messageReceiverEvents';
import { ToastReactionFailed } from '../../components/ToastReactionFailed';
import { assertDev } from '../../util/assert';
import { blockSendUntilConversationsAreVerified } from '../../util/blockSendUntilConversationsAreVerified';
import { deleteStoryForEveryone as doDeleteStoryForEveryone } from '../../util/deleteStoryForEveryone';
import { enqueueReactionForSend } from '../../reactions/enqueueReactionForSend';
import { getMessageById } from '../../messages/getMessageById';
import { markViewed } from '../../services/MessageUpdater';
import { queueAttachmentDownloads } from '../../util/queueAttachmentDownloads';
import { replaceIndex } from '../../util/replaceIndex';
import { sendDeleteForEveryoneMessage } from '../../util/sendDeleteForEveryoneMessage';
import { showToast } from '../../util/showToast';
import {
hasFailed,
Expand All @@ -44,13 +42,11 @@ import {
getConversationSelector,
getHideStoryConversationIds,
} from '../selectors/conversations';
import { getSendOptions } from '../../util/getSendOptions';
import { getStories } from '../selectors/stories';
import { getStoryDataFromMessageAttributes } from '../../services/storyLoader';
import { isGroup } from '../../util/whatTypeOfConversation';
import { isNotNil } from '../../util/isNotNil';
import { isStory } from '../../messages/helpers';
import { onStoryRecipientUpdate } from '../../util/onStoryRecipientUpdate';
import { sendStoryMessage as doSendStoryMessage } from '../../util/sendStoryMessage';
import { useBoundActions } from '../../hooks/useBoundActions';
import { verifyStoryListMembers as doVerifyStoryListMembers } from '../../util/verifyStoryListMembers';
Expand Down Expand Up @@ -214,169 +210,13 @@ function deleteStoryForEveryone(
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;
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) {
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);
const storyData = stories.find(item => item.messageId === story.messageId);
if (!storyData) {
log.warn('deleteStoryForEveryone: Could not find story in redux data');
return;
}
await doDeleteStoryForEveryone(stories, storyData);

dispatch({
type: DOE_STORY,
Expand Down

0 comments on commit 522634b

Please sign in to comment.