diff --git a/ts/components/basic/SessionRadioGroup.tsx b/ts/components/basic/SessionRadioGroup.tsx
index fa79464484..377b33069b 100644
--- a/ts/components/basic/SessionRadioGroup.tsx
+++ b/ts/components/basic/SessionRadioGroup.tsx
@@ -10,6 +10,7 @@ export type SessionRadioItems = Array<{
label: string;
inputDataTestId: SessionDataTestId;
labelDataTestId: SessionDataTestId;
+ disabled?: boolean;
}>;
interface Props {
@@ -50,6 +51,7 @@ export const SessionRadioGroup = (props: Props) => {
label={item.label}
active={itemIsActive}
value={item.value}
+ disabled={item.disabled}
inputDataTestId={item.inputDataTestId}
labelDataTestId={item.labelDataTestId}
inputName={group}
diff --git a/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx b/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx
index af004d7aa9..19501586ba 100644
--- a/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx
+++ b/ts/components/conversation/header/ConversationHeaderSelectionOverlay.tsx
@@ -1,13 +1,9 @@
import { useDispatch, useSelector } from 'react-redux';
import useKey from 'react-use/lib/useKey';
-import { deleteMessagesForX } from '../../../interactions/conversations/unsendingInteractions';
import { resetSelectedMessageIds } from '../../../state/ducks/conversations';
import { getSelectedMessageIds } from '../../../state/selectors/conversations';
-import {
- useSelectedConversationKey,
- useSelectedIsPublic,
-} from '../../../state/selectors/selectedConversation';
+import { useSelectedConversationKey } from '../../../state/selectors/selectedConversation';
import {
SessionButton,
SessionButtonColor,
@@ -18,13 +14,15 @@ import { SessionFocusTrap } from '../../SessionFocusTrap';
import { tr } from '../../../localization/localeTools';
import { SessionLucideIconButton } from '../../icon/SessionIconButton';
import { LUCIDE_ICONS_UNICODE } from '../../icon/lucide';
+import { useDeleteMessagesCb } from '../../menuAndSettingsHooks/useDeleteMessagesCb';
export const SelectionOverlay = () => {
const selectedMessageIds = useSelector(getSelectedMessageIds);
const selectedConversationKey = useSelectedConversationKey();
- const isPublic = useSelectedIsPublic();
const dispatch = useDispatch();
+ const deleteMessagesCb = useDeleteMessagesCb(selectedConversationKey);
+
function onCloseOverlay() {
dispatch(resetSelectedMessageIds());
}
@@ -51,8 +49,8 @@ export const SelectionOverlay = () => {
return true;
case 'Backspace':
case 'Delete':
- if (selectionMode && selectedConversationKey) {
- void deleteMessagesForX(selectedMessageIds, selectedConversationKey, isPublic);
+ if (selectionMode) {
+ void deleteMessagesCb?.(selectedMessageIds);
}
return true;
default:
@@ -61,12 +59,6 @@ export const SelectionOverlay = () => {
}
);
- // `enforceDeleteServerSide` should check for message statuses too, but when we have multiple selected,
- // some might be sent and some in an error state. We default to trying to delete all of them server side first,
- // which might fail. If that fails, the user will need to do a delete for all the ones sent already, and a manual delete
- // for each ones which is in an error state.
- const enforceDeleteServerSide = isPublic;
-
const classNameAndId = 'message-selection-overlay';
return (
@@ -88,14 +80,8 @@ export const SelectionOverlay = () => {
buttonShape={SessionButtonShape.Square}
buttonType={SessionButtonType.Solid}
text={tr('delete')}
- onClick={async () => {
- if (selectedConversationKey) {
- await deleteMessagesForX(
- selectedMessageIds,
- selectedConversationKey,
- enforceDeleteServerSide
- );
- }
+ onClick={() => {
+ void deleteMessagesCb?.(selectedMessageIds);
}}
/>
diff --git a/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx b/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx
index b33e163cff..8ad6f9f9a2 100644
--- a/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx
+++ b/ts/components/conversation/right-panel/overlay/message-info/OverlayMessageInfo.tsx
@@ -17,7 +17,6 @@ import {
replyToMessage,
resendMessage,
} from '../../../../../interactions/conversationInteractions';
-import { deleteMessagesById } from '../../../../../interactions/conversations/unsendingInteractions';
import {
useMessageAttachments,
useMessageAuthor,
@@ -54,6 +53,7 @@ import { useShowCopyAccountIdCb } from '../../../../menuAndSettingsHooks/useCopy
import { sectionActions } from '../../../../../state/ducks/section';
import { useIsIncomingRequest } from '../../../../../hooks/useParamSelector';
import { tr } from '../../../../../localization/localeTools';
+import { useDeleteMessagesCb } from '../../../../menuAndSettingsHooks/useDeleteMessagesCb';
// NOTE we override the default max-widths when in the detail isDetailView
const StyledMessageBody = styled.div`
@@ -289,6 +289,8 @@ export const OverlayMessageInfo = () => {
}
}, [sender, closePanel]);
+ const deleteMessagesCb = useDeleteMessagesCb(convoId);
+
if (!rightOverlayMode || !messageInfo || !convoId || !messageId || !sender) {
return null;
}
@@ -404,14 +406,14 @@ export const OverlayMessageInfo = () => {
/>
)}
{/* Deleting messages sends a "delete message" message so it must be disabled for message requests. */}
- {isDeletable && !isLegacyGroup && !isIncomingMessageRequest && (
+ {isDeletable && !isLegacyGroup && !isIncomingMessageRequest && deleteMessagesCb && (
}
color={'var(--danger-color)'}
dataTestId="delete-from-details"
onClick={() => {
- void deleteMessagesById([messageId], convoId);
+ void deleteMessagesCb?.(messageId);
}}
/>
)}
diff --git a/ts/components/menu/items/DeleteMessage/DeleteMessageMenuItem.tsx b/ts/components/menu/items/DeleteMessage/DeleteMessageMenuItem.tsx
index a079e04e38..6ca05e845d 100644
--- a/ts/components/menu/items/DeleteMessage/DeleteMessageMenuItem.tsx
+++ b/ts/components/menu/items/DeleteMessage/DeleteMessageMenuItem.tsx
@@ -1,36 +1,24 @@
-import { useCallback } from 'react';
-import { deleteMessagesForX } from '../../../../interactions/conversations/unsendingInteractions';
-import {
- useMessageIsDeletable,
- useMessageIsDeletableForEveryone,
- useMessageStatus,
-} from '../../../../state/selectors';
-import {
- useSelectedConversationKey,
- useSelectedIsPublic,
-} from '../../../../state/selectors/selectedConversation';
+import { useSelectedConversationKey } from '../../../../state/selectors/selectedConversation';
import { ItemWithDataTestId } from '../MenuItemWithDataTestId';
import { tr } from '../../../../localization/localeTools';
+import { useDeleteMessagesCb } from '../../../menuAndSettingsHooks/useDeleteMessagesCb';
export const DeleteItem = ({ messageId }: { messageId: string }) => {
const convoId = useSelectedConversationKey();
- const isPublic = useSelectedIsPublic();
- const isDeletable = useMessageIsDeletable(messageId);
- const isDeletableForEveryone = useMessageIsDeletableForEveryone(messageId);
- const messageStatus = useMessageStatus(messageId);
+ const deleteMessagesCb = useDeleteMessagesCb(convoId);
- const enforceDeleteServerSide = isPublic && messageStatus !== 'error';
-
- const onDelete = useCallback(() => {
- if (convoId) {
- void deleteMessagesForX([messageId], convoId, enforceDeleteServerSide);
- }
- }, [convoId, enforceDeleteServerSide, messageId]);
-
- if (!convoId || (isPublic && !isDeletableForEveryone) || (!isPublic && !isDeletable)) {
+ if (!deleteMessagesCb) {
return null;
}
- return {tr('delete')};
+ return (
+ {
+ void deleteMessagesCb?.(messageId);
+ }}
+ >
+ {tr('delete')}
+
+ );
};
diff --git a/ts/components/menuAndSettingsHooks/useDeleteMessagesCb.tsx b/ts/components/menuAndSettingsHooks/useDeleteMessagesCb.tsx
new file mode 100644
index 0000000000..32c4ea132b
--- /dev/null
+++ b/ts/components/menuAndSettingsHooks/useDeleteMessagesCb.tsx
@@ -0,0 +1,421 @@
+import type { PubkeyType, GroupPubkeyType } from 'libsession_util_nodejs';
+import { compact, isArray } from 'lodash';
+import { useDispatch } from 'react-redux';
+import { updateConfirmModal } from '../../state/ducks/modalDialog';
+import { useIsMe, useIsPublic, useWeAreAdmin } from '../../hooks/useParamSelector';
+import type { LocalizerProps } from '../basic/Localizer';
+import { SessionButtonColor } from '../basic/SessionButton';
+import { closeRightPanel, resetSelectedMessageIds } from '../../state/ducks/conversations';
+import { tr } from '../../localization/localeTools';
+import { useWeAreCommunityAdminOrModerator } from '../../state/selectors/conversations';
+import type { ConversationModel } from '../../models/conversation';
+import type { MessageModel } from '../../models/message';
+import { PubKey } from '../../session/types';
+import { ToastUtils, UserUtils } from '../../session/utils';
+import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
+import { Data } from '../../data/data';
+import { MessageQueue } from '../../session/sending';
+import {
+ deleteMessagesFromSwarmAndCompletelyLocally,
+ deleteMessagesFromSwarmAndMarkAsDeletedLocally,
+} from '../../interactions/conversations/unsendingInteractions';
+import { deleteSogsMessageByServerIds } from '../../session/apis/open_group_api/sogsv3/sogsV3DeleteMessages';
+import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces';
+import type { SessionRadioItems } from '../basic/SessionRadioGroup';
+import { getSodiumRenderer } from '../../session/crypto';
+import { GroupUpdateDeleteMemberContentMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateDeleteMemberContentMessage';
+import { UnsendMessage } from '../../session/messages/outgoing/controlMessage/UnsendMessage';
+import { NetworkTime } from '../../util/NetworkTime';
+import { deleteMessagesLocallyOnly } from '../../interactions/conversations/deleteMessagesLocallyOnly';
+import { sectionActions } from '../../state/ducks/section';
+import { ConvoHub } from '../../session/conversations';
+import { useNetworkStakedTokens } from '../../state/selectors/networkData';
+
+const deleteMessageDeviceOnly = 'deleteMessageDeviceOnly';
+const deleteMessageDevicesAll = 'deleteMessageDevicesAll';
+const deleteMessageEveryone = 'deleteMessageEveryone';
+
+type MessageDeletionType =
+ | typeof deleteMessageDeviceOnly
+ | typeof deleteMessageDevicesAll
+ | typeof deleteMessageEveryone;
+
+/**
+ * Offer to delete for everyone or not, based on what is currently selected and our role in the corresponding conversation.
+ *
+ */
+export function useDeleteMessagesCb(conversationId: string | undefined) {
+ const dispatch = useDispatch();
+
+ const isMe = useIsMe(conversationId);
+ const isPublic = useIsPublic(conversationId);
+ const weAreAdminOrModCommunity = useWeAreCommunityAdminOrModerator(conversationId);
+ const weAreAdminGroup = useWeAreAdmin(conversationId);
+
+ const closeDialog = () => dispatch(updateConfirmModal(null));
+
+ if (!conversationId) {
+ return null;
+ }
+
+ return async (messageIds: string | Array | undefined) => {
+ const count = isArray(messageIds) ? messageIds.length : messageIds ? 1 : 0;
+ const convo = ConvoHub.use().get(conversationId);
+
+ if (!convo || !messageIds || (!isArray(messageIds) && !messageIds.length)) {
+ return;
+ }
+ const messageIdsArr = isArray(messageIds) ? messageIds : [messageIds];
+
+ const canDeleteAllForEveryoneAsAdmin =
+ (isPublic && weAreAdminOrModCommunity) || (!isPublic && weAreAdminGroup);
+
+ const msgModels = await Data.getMessagesById(messageIdsArr);
+ const senders = compact(msgModels.map(m => m.getSource()));
+ const us = UserUtils.getOurPubKeyStrFromCache();
+
+ const anyAreMarkAsDeleted = msgModels.some(m => m.get('isDeleted'));
+ const anyAreControlMessages = msgModels.some(m => m.isControlMessage());
+
+ const canDeleteAllForEveryoneAsMe = senders.every(s => s === us);
+ const canDeleteAllForEveryone =
+ (canDeleteAllForEveryoneAsMe || canDeleteAllForEveryoneAsAdmin) &&
+ !anyAreControlMessages &&
+ !anyAreMarkAsDeleted;
+
+ // Note: the isMe case has no radio buttons, so we just show the description below
+ const i18nMessage: LocalizerProps | undefined = isMe
+ ? { token: 'deleteMessageDescriptionDevice', args: { count } }
+ : undefined;
+
+ const canDeleteFromAllDevices = isMe && !anyAreControlMessages && !anyAreMarkAsDeleted;
+
+ const radioOptions: SessionRadioItems | undefined = [
+ {
+ label: tr(deleteMessageDeviceOnly),
+ value: deleteMessageDeviceOnly,
+ inputDataTestId: `input-${deleteMessageDeviceOnly}` as const,
+ labelDataTestId: `label-${deleteMessageDeviceOnly}` as const,
+ disabled: false, // we can always delete message locally
+ },
+ isMe
+ ? {
+ label: tr(deleteMessageDevicesAll),
+ value: deleteMessageDevicesAll,
+ inputDataTestId: `input-${deleteMessageDevicesAll}` as const,
+ labelDataTestId: `label-${deleteMessageDevicesAll}` as const,
+ disabled: !canDeleteFromAllDevices,
+ }
+ : {
+ label: tr(deleteMessageEveryone),
+ value: deleteMessageEveryone,
+ inputDataTestId: `input-${deleteMessageEveryone}` as const,
+ labelDataTestId: `label-${deleteMessageEveryone}` as const,
+ disabled: !canDeleteAllForEveryone,
+ },
+ ];
+
+ dispatch(
+ updateConfirmModal({
+ title: tr('deleteMessage', { count }),
+ radioOptions,
+ i18nMessage,
+ okText: tr('delete'),
+
+ okTheme: SessionButtonColor.Danger,
+ onClickOk: async args => {
+ if (
+ args !== deleteMessageEveryone &&
+ args !== deleteMessageDevicesAll &&
+ args !== deleteMessageDeviceOnly
+ ) {
+ throw new Error('doDeleteSelectedMessages: invalid args onClickOk');
+ }
+
+ await doDeleteSelectedMessages({
+ selectedMessages: msgModels,
+ conversation: convo,
+ deletionType: args,
+ });
+ dispatch(updateConfirmModal(null));
+ dispatch(closeRightPanel());
+ dispatch(sectionActions.resetRightOverlayMode());
+ },
+ onClickClose: closeDialog,
+ })
+ );
+ };
+}
+
+/**
+ * Delete the messages from the conversation.
+ * Also deletes messages from the swarm/sogs if needed, sends unsend requests for syncing etc...
+ *
+ * Note: this function does not check if the user is allowed to delete the messages.
+ * The call will just fail if the user is not allowed to delete the messages, silently.
+ * So make sure to check the user permissions before calling this function and to display only valid actions for the user's permissions.
+ *
+ */
+const doDeleteSelectedMessages = async ({
+ conversation,
+ selectedMessages,
+ deletionType,
+}: {
+ selectedMessages: Array;
+ conversation: ConversationModel;
+ deletionType: MessageDeletionType;
+}) => {
+ // legacy groups are read only
+ if (conversation.isClosedGroup() && PubKey.is05Pubkey(conversation.id)) {
+ window.log.info('doDeleteSelectedMessages: legacy groups are deprecated');
+ return;
+ }
+
+ if (deletionType === deleteMessageDeviceOnly) {
+ // Mark those messages as deleted only locally
+ await deleteMessagesLocallyOnly({
+ conversation,
+ messages: selectedMessages,
+ deletionType: 'markDeleted',
+ });
+ ToastUtils.pushDeleted(selectedMessages.length);
+
+ return;
+ }
+
+ // device only was handled above, so this isPublic can only mean delete for everyone in a community
+ if (conversation.isPublic()) {
+ await doDeleteSelectedMessagesInSOGS(selectedMessages, conversation);
+ return;
+ }
+
+ if (deletionType === deleteMessageDevicesAll) {
+ // Delete those messages locally, from our swarm and from our other devices, but not for anyone else in the conversation
+ await unsendMessageJustForThisUserAllDevices(conversation, selectedMessages);
+ return;
+ }
+
+ note: this is all done but untested
+ // Note: a groupv2 member can delete messages for everyone if they are the admin, or if that message is theirs.
+
+ if (deletionType !== deleteMessageEveryone) {
+ throw new Error('doDeleteSelectedMessages: invalid deletionType');
+ }
+
+ if (conversation.isPrivate()) {
+ // Note: we cannot delete for everyone a message in non 05-private chat
+ if (!PubKey.is05Pubkey(conversation.id)) {
+ throw new Error('unsendMessagesForEveryone1o1AndLegacy requires a 05 key');
+ }
+ // private chats: we want to delete those messages completely (not just marked as deleted)
+ await unsendMessagesForEveryone1o1(conversation, conversation.id, selectedMessages);
+ await deleteMessagesFromSwarmAndCompletelyLocally(conversation, selectedMessages);
+ ToastUtils.pushDeleted(selectedMessages.length);
+ window.inboxStore?.dispatch(resetSelectedMessageIds());
+
+ return;
+ }
+
+ if (!conversation.isClosedGroupV2() || !PubKey.is03Pubkey(conversation.id)) {
+ // considering the above, the only valid case here is 03 groupv2
+ throw new Error('doDeleteSelectedMessages: invalid conversation type');
+ }
+
+ await unsendMessagesForEveryoneGroupV2({
+ groupPk: conversation.id,
+ msgsToDelete: selectedMessages,
+ allMessagesFrom: [], // currently we cannot remove all the messages from a specific pubkey but we do already handle them on the receiving side
+ });
+
+ // 03 groups: mark as deleted
+ await deleteMessagesFromSwarmAndMarkAsDeletedLocally(conversation, selectedMessages);
+
+ window.inboxStore?.dispatch(resetSelectedMessageIds());
+ ToastUtils.pushDeleted(selectedMessages.length);
+};
+
+/**
+ * Send an UnsendMessage synced message so our devices removes those messages locally,
+ * and send an unsend request on our swarm so this message is effectively removed from it.
+ * Then, deletes completely the messages locally.
+ *
+ * Show a toast on error/success and reset the selection
+ */
+async function unsendMessageJustForThisUserAllDevices(
+ conversation: ConversationModel,
+ msgsToDelete: Array
+) {
+ window?.log?.warn('Deleting messages just for this user');
+
+ const unsendMsgObjects = getUnsendMessagesObjects1o1OrLegacyGroups(msgsToDelete);
+
+ // sending to our other devices all the messages separately for now
+ await Promise.all(
+ unsendMsgObjects.map(unsendObject =>
+ MessageQueue.use()
+ .sendSyncMessage({ namespace: SnodeNamespaces.Default, message: unsendObject })
+ .catch(window?.log?.error)
+ )
+ );
+ await deleteMessagesFromSwarmAndCompletelyLocally(conversation, msgsToDelete);
+
+ // Update view and trigger update
+ window.inboxStore?.dispatch(resetSelectedMessageIds());
+ ToastUtils.pushDeleted(unsendMsgObjects.length);
+}
+
+/**
+ * Attempt to delete the messages from the SOGS.
+ * Note: this function does not check if the user is allowed to delete the messages.
+ * The call will just fail if the user is not allowed to delete the messages, silently, so make
+ * sure to check the user permissions before calling this function and to display only valid actions for the user's permissions.
+ */
+async function doDeleteSelectedMessagesInSOGS(
+ selectedMessages: Array,
+ conversation: ConversationModel
+) {
+ const toDeleteLocallyIds = await deleteOpenGroupMessages(selectedMessages, conversation);
+ if (toDeleteLocallyIds.length === 0) {
+ // Failed to delete those messages from the sogs.
+ ToastUtils.pushToastError('errorGeneric', tr('errorGeneric'));
+ return;
+ }
+
+ await deleteMessagesLocallyOnly({
+ conversation,
+ messages: selectedMessages,
+ deletionType: 'complete',
+ });
+
+ // successful deletion
+ ToastUtils.pushDeleted(toDeleteLocallyIds.length);
+ window.inboxStore?.dispatch(resetSelectedMessageIds());
+}
+
+/**
+ *
+ * @param messages the list of MessageModel to delete
+ * @param convo the conversation to delete from (only v2 opengroups are supported)
+ */
+async function deleteOpenGroupMessages(
+ messages: Array,
+ convo: ConversationModel
+): Promise> {
+ if (!convo.isPublic()) {
+ throw new Error('cannot delete public message on a non public groups');
+ }
+
+ const roomInfos = convo.toOpenGroupV2();
+ // on v2 servers we can only remove a single message per request..
+ // so logic here is to delete each messages and get which one where not removed
+ const validServerIdsToRemove = compact(
+ messages.map(msg => {
+ return msg.get('serverId');
+ })
+ );
+
+ const validMessageModelsToRemove = compact(
+ messages.map(msg => {
+ const serverId = msg.get('serverId');
+ if (serverId) {
+ return msg;
+ }
+ return undefined;
+ })
+ );
+
+ let allMessagesAreDeleted: boolean = false;
+ if (validServerIdsToRemove.length) {
+ allMessagesAreDeleted = await deleteSogsMessageByServerIds(validServerIdsToRemove, roomInfos);
+ }
+ // remove only the messages we managed to remove on the server
+ if (allMessagesAreDeleted) {
+ window?.log?.info('Removed all those serverIds messages successfully');
+ return validMessageModelsToRemove.map(m => m.id);
+ }
+ window?.log?.info(
+ 'failed to remove all those serverIds message. not removing them locally neither'
+ );
+ return [];
+}
+
+async function unsendMessagesForEveryone1o1(
+ conversation: ConversationModel,
+ destination: PubkeyType,
+ msgsToDelete: Array
+) {
+ const unsendMsgObjects = getUnsendMessagesObjects1o1OrLegacyGroups(msgsToDelete);
+
+ if (!conversation.isPrivate()) {
+ throw new Error('unsendMessagesForEveryone1o1 only works with private conversations');
+ }
+
+ // sending to recipient all the messages separately for now
+ await Promise.all(
+ unsendMsgObjects.map(unsendObject =>
+ MessageQueue.use()
+ .sendToPubKey(new PubKey(destination), unsendObject, SnodeNamespaces.Default)
+ .catch(window?.log?.error)
+ )
+ );
+ await Promise.all(
+ unsendMsgObjects.map(unsendObject =>
+ MessageQueue.use()
+ .sendSyncMessage({ namespace: SnodeNamespaces.Default, message: unsendObject })
+ .catch(window?.log?.error)
+ )
+ );
+}
+
+async function unsendMessagesForEveryoneGroupV2({
+ allMessagesFrom,
+ groupPk,
+ msgsToDelete,
+}: {
+ groupPk: GroupPubkeyType;
+ msgsToDelete: Array;
+ allMessagesFrom: Array;
+}) {
+ const messageHashesToUnsend = compact(msgsToDelete.map(m => m.getMessageHash()));
+ const group = await UserGroupsWrapperActions.getGroup(groupPk);
+
+ if (!messageHashesToUnsend.length && !allMessagesFrom.length) {
+ window.log.info('unsendMessagesForEveryoneGroupV2: no hashes nor author to remove');
+ return;
+ }
+
+ await MessageQueue.use().sendToGroupV2NonDurably({
+ message: new GroupUpdateDeleteMemberContentMessage({
+ createAtNetworkTimestamp: NetworkTime.now(),
+ expirationType: 'unknown', // GroupUpdateDeleteMemberContentMessage is not displayed so not expiring.
+ expireTimer: 0,
+ groupPk,
+ memberSessionIds: allMessagesFrom,
+ messageHashes: messageHashesToUnsend,
+ sodium: await getSodiumRenderer(),
+ secretKey: group?.secretKey || undefined,
+ }),
+ });
+}
+
+function getUnsendMessagesObjects1o1OrLegacyGroups(messages: Array) {
+ return compact(
+ messages.map((message, index) => {
+ const author = message.get('source');
+
+ // call getPropsForMessage here so we get the received_at or sent_at timestamp in timestamp
+ const referencedMessageTimestamp = message.getPropsForMessage().timestamp;
+ if (!referencedMessageTimestamp) {
+ window?.log?.error('cannot find timestamp - aborting unsend request');
+ return undefined;
+ }
+
+ return new UnsendMessage({
+ // this isn't pretty, but we need a unique timestamp for Android to not drop the message as a duplicate
+ createAtNetworkTimestamp: NetworkTime.now() + index,
+ referencedMessageTimestamp,
+ author,
+ });
+ })
+ );
+}
diff --git a/ts/interactions/conversations/deleteMessagesFromSwarmOnly.ts b/ts/interactions/conversations/deleteMessagesFromSwarmOnly.ts
new file mode 100644
index 0000000000..fcfec51854
--- /dev/null
+++ b/ts/interactions/conversations/deleteMessagesFromSwarmOnly.ts
@@ -0,0 +1,47 @@
+import type { PubkeyType, GroupPubkeyType } from 'libsession_util_nodejs';
+import { compact, isEmpty } from 'lodash';
+import type { MessageModel } from '../../models/message';
+import { SnodeAPI } from '../../session/apis/snode_api/SNodeAPI';
+import { PubKey } from '../../session/types';
+import { ed25519Str } from '../../session/utils/String';
+import { isStringArray } from '../../types/isStringArray';
+
+/**
+ * Do a single request to the swarm with all the message hashes to delete from the swarm.
+ * Does not delete anything locally.
+ * Should only be used when we are deleting a
+ *
+ * Returns true if no errors happened, false in an error happened
+ */
+export async function deleteMessagesFromSwarmOnly(
+ messages: Array | Array,
+ pubkey: PubkeyType | GroupPubkeyType
+) {
+ const deletionMessageHashes = isStringArray(messages)
+ ? messages
+ : compact(messages.map(m => m.getMessageHash()));
+
+ try {
+ if (isEmpty(messages)) {
+ return false;
+ }
+
+ if (!deletionMessageHashes.length) {
+ window.log?.warn(
+ 'deleteMessagesFromSwarmOnly: We do not have hashes for some of those messages'
+ );
+ return false;
+ }
+ const hashesAsSet = new Set(deletionMessageHashes);
+ if (PubKey.is03Pubkey(pubkey)) {
+ return await SnodeAPI.networkDeleteMessagesForGroup(hashesAsSet, pubkey);
+ }
+ return await SnodeAPI.networkDeleteMessageOurSwarm(hashesAsSet, pubkey);
+ } catch (e) {
+ window.log?.error(
+ `deleteMessagesFromSwarmOnly: Error deleting message from swarm of ${ed25519Str(pubkey)}, hashes: ${deletionMessageHashes}`,
+ e
+ );
+ return false;
+ }
+}
diff --git a/ts/interactions/conversations/deleteMessagesLocallyOnly.ts b/ts/interactions/conversations/deleteMessagesLocallyOnly.ts
new file mode 100644
index 0000000000..74f82075bc
--- /dev/null
+++ b/ts/interactions/conversations/deleteMessagesLocallyOnly.ts
@@ -0,0 +1,32 @@
+import type { ConversationModel } from '../../models/conversation';
+import type { MessageModel } from '../../models/message';
+import type { WithLocalMessageDeletionType } from '../../session/types/with';
+
+/**
+ * Deletes a message completely or mark it as deleted. Does not interact with the swarm at all
+ * @param message Message to delete
+ * @param deletionType 'complete' means completely delete the item from the database, markDeleted means empty the message content but keep an entry
+ */
+export async function deleteMessagesLocallyOnly({
+ conversation,
+ messages,
+ deletionType,
+}: WithLocalMessageDeletionType & {
+ conversation: ConversationModel;
+ messages: Array;
+}) {
+ for (let index = 0; index < messages.length; index++) {
+ const message = messages[index];
+ if (deletionType === 'complete') {
+ // remove the message from the database
+ // eslint-disable-next-line no-await-in-loop
+ await conversation.removeMessage(message.id);
+ } else {
+ // just mark the message as deleted but still show in conversation
+ // eslint-disable-next-line no-await-in-loop
+ await message.markAsDeleted();
+ }
+ }
+
+ conversation.updateLastMessage();
+}
diff --git a/ts/interactions/conversations/unsendingInteractions.ts b/ts/interactions/conversations/unsendingInteractions.ts
index f272ac1a46..6bd246ec00 100644
--- a/ts/interactions/conversations/unsendingInteractions.ts
+++ b/ts/interactions/conversations/unsendingInteractions.ts
@@ -1,215 +1,11 @@
-import { GroupPubkeyType, PubkeyType } from 'libsession_util_nodejs';
-import { compact, isEmpty } from 'lodash';
-import { SessionButtonColor } from '../../components/basic/SessionButton';
-import { Data } from '../../data/data';
import { ConversationModel } from '../../models/conversation';
import { MessageModel } from '../../models/message';
-import { deleteSogsMessageByServerIds } from '../../session/apis/open_group_api/sogsv3/sogsV3DeleteMessages';
-import { SnodeAPI } from '../../session/apis/snode_api/SNodeAPI';
-import { SnodeNamespaces } from '../../session/apis/snode_api/namespaces';
-import { ConvoHub } from '../../session/conversations';
-import { getSodiumRenderer } from '../../session/crypto';
-import { UnsendMessage } from '../../session/messages/outgoing/controlMessage/UnsendMessage';
-import { GroupUpdateDeleteMemberContentMessage } from '../../session/messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateDeleteMemberContentMessage';
import { PubKey } from '../../session/types';
-import { ToastUtils, UserUtils } from '../../session/utils';
-import { closeRightPanel, resetSelectedMessageIds } from '../../state/ducks/conversations';
-import { updateConfirmModal } from '../../state/ducks/modalDialog';
+import { UserUtils } from '../../session/utils';
import { ed25519Str } from '../../session/utils/String';
-import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
-import { NetworkTime } from '../../util/NetworkTime';
-import { MessageQueue } from '../../session/sending';
-import { WithLocalMessageDeletionType } from '../../session/types/with';
-import { tr } from '../../localization/localeTools';
-import { sectionActions } from '../../state/ducks/section';
-import type { LocalizerProps } from '../../components/basic/Localizer';
-
-async function unsendMessagesForEveryone1o1AndLegacy(
- conversation: ConversationModel,
- destination: PubkeyType,
- msgsToDelete: Array
-) {
- const unsendMsgObjects = getUnsendMessagesObjects1o1OrLegacyGroups(msgsToDelete);
-
- if (conversation.isClosedGroupV2()) {
- throw new Error('unsendMessagesForEveryone1o1AndLegacy not compatible with group v2');
- }
-
- if (conversation.isPrivate()) {
- // sending to recipient all the messages separately for now
- await Promise.all(
- unsendMsgObjects.map(unsendObject =>
- MessageQueue.use()
- .sendToPubKey(new PubKey(destination), unsendObject, SnodeNamespaces.Default)
- .catch(window?.log?.error)
- )
- );
- await Promise.all(
- unsendMsgObjects.map(unsendObject =>
- MessageQueue.use()
- .sendSyncMessage({ namespace: SnodeNamespaces.Default, message: unsendObject })
- .catch(window?.log?.error)
- )
- );
- return;
- }
- if (conversation.isClosedGroup()) {
- // legacy groups are readonly
- }
-}
-
-export async function unsendMessagesForEveryoneGroupV2({
- allMessagesFrom,
- groupPk,
- msgsToDelete,
-}: {
- groupPk: GroupPubkeyType;
- msgsToDelete: Array;
- allMessagesFrom: Array;
-}) {
- const messageHashesToUnsend = getMessageHashes(msgsToDelete);
- const group = await UserGroupsWrapperActions.getGroup(groupPk);
-
- if (!messageHashesToUnsend.length && !allMessagesFrom.length) {
- window.log.info('unsendMessagesForEveryoneGroupV2: no hashes nor author to remove');
- return;
- }
-
- await MessageQueue.use().sendToGroupV2NonDurably({
- message: new GroupUpdateDeleteMemberContentMessage({
- createAtNetworkTimestamp: NetworkTime.now(),
- expirationType: 'unknown', // GroupUpdateDeleteMemberContentMessage is not displayed so not expiring.
- expireTimer: 0,
- groupPk,
- memberSessionIds: allMessagesFrom,
- messageHashes: messageHashesToUnsend,
- sodium: await getSodiumRenderer(),
- secretKey: group?.secretKey || undefined,
- }),
- });
-}
-
-/**
- * Deletes messages for everyone in a 1-1 or everyone in a closed group conversation.
- */
-async function unsendMessagesForEveryone(
- conversation: ConversationModel,
- msgsToDelete: Array,
- { deletionType }: WithLocalMessageDeletionType
-) {
- window?.log?.info('Deleting messages for all users in this conversation');
- const destinationId = conversation.id;
- if (!destinationId) {
- return;
- }
- if (conversation.isOpenGroupV2()) {
- throw new Error(
- 'Cannot unsend a message for an opengroup v2. This has to be a deleteMessage api call'
- );
- }
-
- if (
- conversation.isPrivate() ||
- (conversation.isClosedGroup() && !conversation.isClosedGroupV2())
- ) {
- if (!PubKey.is05Pubkey(conversation.id)) {
- throw new Error('unsendMessagesForEveryone1o1AndLegacy requires a 05 key');
- }
- await unsendMessagesForEveryone1o1AndLegacy(conversation, conversation.id, msgsToDelete);
- } else if (conversation.isClosedGroupV2()) {
- if (!PubKey.is03Pubkey(destinationId)) {
- throw new Error('invalid conversation id (03) for unsendMessageForEveryone');
- }
- await unsendMessagesForEveryoneGroupV2({
- groupPk: destinationId,
- msgsToDelete,
- allMessagesFrom: [], // currently we cannot remove all the messages from a specific pubkey but we do already handle them on the receiving side
- });
- }
- if (deletionType === 'complete') {
- await deleteMessagesFromSwarmAndCompletelyLocally(conversation, msgsToDelete);
- } else {
- await deleteMessagesFromSwarmAndMarkAsDeletedLocally(conversation, msgsToDelete);
- }
-
- window.inboxStore?.dispatch(resetSelectedMessageIds());
- ToastUtils.pushDeleted(msgsToDelete.length);
-}
-
-function getUnsendMessagesObjects1o1OrLegacyGroups(messages: Array) {
- // #region building request
- return compact(
- messages.map((message, index) => {
- const author = message.get('source');
-
- // call getPropsForMessage here so we get the received_at or sent_at timestamp in timestamp
- const referencedMessageTimestamp = message.getPropsForMessage().timestamp;
- if (!referencedMessageTimestamp) {
- window?.log?.error('cannot find timestamp - aborting unsend request');
- return undefined;
- }
-
- return new UnsendMessage({
- // this isn't pretty, but we need a unique timestamp for Android to not drop the message as a duplicate
- createAtNetworkTimestamp: NetworkTime.now() + index,
- referencedMessageTimestamp,
- author,
- });
- })
- );
- // #endregion
-}
-
-function getMessageHashes(messages: Array) {
- return compact(
- messages.map(message => {
- return message.get('messageHash');
- })
- );
-}
-
-function isStringArray(value: unknown): value is Array {
- return Array.isArray(value) && value.every(val => typeof val === 'string');
-}
-
-/**
- * Do a single request to the swarm with all the message hashes to delete from the swarm.
- *
- * It does not delete anything locally.
- *
- * Returns true if no errors happened, false in an error happened
- */
-export async function deleteMessagesFromSwarmOnly(
- messages: Array | Array,
- pubkey: PubkeyType | GroupPubkeyType
-) {
- const deletionMessageHashes = isStringArray(messages) ? messages : getMessageHashes(messages);
-
- try {
- if (isEmpty(messages)) {
- return false;
- }
-
- if (!deletionMessageHashes.length) {
- window.log?.warn(
- 'deleteMessagesFromSwarmOnly: We do not have hashes for some of those messages'
- );
- return false;
- }
- const hashesAsSet = new Set(deletionMessageHashes);
- if (PubKey.is03Pubkey(pubkey)) {
- return await SnodeAPI.networkDeleteMessagesForGroup(hashesAsSet, pubkey);
- }
- return await SnodeAPI.networkDeleteMessageOurSwarm(hashesAsSet, pubkey);
- } catch (e) {
- window.log?.error(
- `deleteMessagesFromSwarmOnly: Error deleting message from swarm of ${ed25519Str(pubkey)}, hashes: ${deletionMessageHashes}`,
- e
- );
- return false;
- }
-}
+import { deleteMessagesLocallyOnly } from './deleteMessagesLocallyOnly';
+import { deleteMessagesFromSwarmOnly } from './deleteMessagesFromSwarmOnly';
/**
* Delete the messages from the swarm with an unsend request and if it worked, delete those messages locally.
@@ -230,10 +26,10 @@ export async function deleteMessagesFromSwarmAndCompletelyLocally(
);
return;
}
- // LEGACY GROUPS -- we cannot delete on the swarm (just unsend which is done separately)
+ // LEGACY GROUPS are deprecated
if (conversation.isClosedGroup() && PubKey.is05Pubkey(pubkey)) {
- window.log.info('Cannot delete message from a closed group swarm, so we just complete delete.');
- await deleteMessagesLocallyOnly({ conversation, messages, deletionType: 'complete' });
+ window.log.info('legacy groups are deprecated.');
+
return;
}
window.log.info(
@@ -252,19 +48,16 @@ export async function deleteMessagesFromSwarmAndCompletelyLocally(
}
/**
- * Delete the messages from the swarm with an unsend request and if it worked, mark those messages locally as deleted but do not remove them.
+ * Delete the messages from the swarm with an unsend request and mark those messages locally as deleted but do not remove them.
* If an error happened, we still mark the message locally as deleted.
*/
export async function deleteMessagesFromSwarmAndMarkAsDeletedLocally(
conversation: ConversationModel,
messages: Array
) {
- // legacy groups cannot delete messages on the swarm (just "unsend")
+ // legacy groups are deprecated
if (conversation.isClosedGroup() && PubKey.is05Pubkey(conversation.id)) {
- window.log.info(
- 'Cannot delete messages from a legacy closed group swarm, so we just markDeleted.'
- );
- await deleteMessagesLocallyOnly({ conversation, messages, deletionType: 'markDeleted' });
+ window.log.info('legacy groups are deprecated. Not deleting anything');
return;
}
@@ -283,339 +76,3 @@ export async function deleteMessagesFromSwarmAndMarkAsDeletedLocally(
}
await deleteMessagesLocallyOnly({ conversation, messages, deletionType: 'markDeleted' });
}
-
-/**
- * Deletes a message completely or mark it as deleted only. Does not interact with the swarm at all
- * @param message Message to delete
- * @param deletionType 'complete' means completely delete the item from the database, markDeleted means empty the message content but keep an entry
- */
-async function deleteMessagesLocallyOnly({
- conversation,
- messages,
- deletionType,
-}: WithLocalMessageDeletionType & {
- conversation: ConversationModel;
- messages: Array;
-}) {
- for (let index = 0; index < messages.length; index++) {
- const message = messages[index];
- if (deletionType === 'complete') {
- // remove the message from the database
- // eslint-disable-next-line no-await-in-loop
- await conversation.removeMessage(message.id);
- } else {
- // just mark the message as deleted but still show in conversation
- // eslint-disable-next-line no-await-in-loop
- await message.markAsDeleted();
- }
- }
-
- conversation.updateLastMessage();
-}
-
-/**
- * Send an UnsendMessage synced message so our devices removes those messages locally,
- * and send an unsend request on our swarm so this message is effectively removed.
- *
- * Show a toast on error/success and reset the selection
- */
-async function unsendMessageJustForThisUser(
- conversation: ConversationModel,
- msgsToDelete: Array
-) {
- window?.log?.warn('Deleting messages just for this user');
-
- const unsendMsgObjects = getUnsendMessagesObjects1o1OrLegacyGroups(msgsToDelete);
-
- // sending to our other devices all the messages separately for now
- await Promise.all(
- unsendMsgObjects.map(unsendObject =>
- MessageQueue.use()
- .sendSyncMessage({ namespace: SnodeNamespaces.Default, message: unsendObject })
- .catch(window?.log?.error)
- )
- );
- await deleteMessagesFromSwarmAndCompletelyLocally(conversation, msgsToDelete);
-
- // Update view and trigger update
- window.inboxStore?.dispatch(resetSelectedMessageIds());
- ToastUtils.pushDeleted(unsendMsgObjects.length);
-}
-
-const doDeleteSelectedMessagesInSOGS = async (
- selectedMessages: Array,
- conversation: ConversationModel,
- isAllOurs: boolean
-) => {
- const ourDevicePubkey = UserUtils.getOurPubKeyStrFromCache();
- if (!ourDevicePubkey) {
- return;
- }
- // #region open group v2 deletion
- // Get our Moderator status
- const isAdmin = conversation.weAreAdminUnblinded();
- const isModerator = conversation.isModerator(ourDevicePubkey);
-
- if (!isAllOurs && !(isAdmin || isModerator)) {
- ToastUtils.pushMessageDeleteForbidden();
- window.inboxStore?.dispatch(resetSelectedMessageIds());
- return;
- }
-
- const toDeleteLocallyIds = await deleteOpenGroupMessages(selectedMessages, conversation);
- if (toDeleteLocallyIds.length === 0) {
- // Message failed to delete from server, show error?
- return;
- }
- await Promise.all(
- toDeleteLocallyIds.map(async id => {
- const msgToDeleteLocally = await Data.getMessageById(id);
- if (msgToDeleteLocally) {
- return deleteMessagesLocallyOnly({
- conversation,
- messages: [msgToDeleteLocally],
- deletionType: 'complete',
- });
- }
- return null;
- })
- );
- // successful deletion
- ToastUtils.pushDeleted(toDeleteLocallyIds.length);
- window.inboxStore?.dispatch(resetSelectedMessageIds());
- // #endregion
-};
-
-/**
- * Effectively delete the messages from a conversation.
- * This call is to be called by the user on a confirmation dialog for instance.
- *
- * It does what needs to be done on a user action to delete messages for each conversation type
- */
-const doDeleteSelectedMessages = async ({
- conversation,
- selectedMessages,
- deleteForEveryone,
-}: {
- selectedMessages: Array;
- conversation: ConversationModel;
- deleteForEveryone: boolean;
-}) => {
- const ourDevicePubkey = UserUtils.getOurPubKeyStrFromCache();
- if (!ourDevicePubkey) {
- return;
- }
-
- const areAllOurs = selectedMessages.every(message => message.getSource() === ourDevicePubkey);
- if (conversation.isPublic()) {
- await doDeleteSelectedMessagesInSOGS(selectedMessages, conversation, areAllOurs);
- return;
- }
-
- // Note: a groupv2 member can delete messages for everyone if they are the admin, or if that message is theirs.
-
- if (deleteForEveryone) {
- if (conversation.isClosedGroupV2()) {
- const convoId = conversation.id;
- if (!PubKey.is03Pubkey(convoId)) {
- throw new Error('unsend request for groupv2 but not a 03 key is impossible possible');
- }
- // only lookup adminKey if we need to
- if (!areAllOurs) {
- const group = await UserGroupsWrapperActions.getGroup(convoId);
- const weHaveAdminKey = !isEmpty(group?.secretKey);
- if (!weHaveAdminKey) {
- ToastUtils.pushMessageDeleteForbidden();
- window.inboxStore?.dispatch(resetSelectedMessageIds());
- return;
- }
- }
- // if they are all ours, of not but we are an admin, we can move forward
- await unsendMessagesForEveryone(conversation, selectedMessages, {
- deletionType: 'markDeleted', // 03 groups: mark as deleted
- });
- return;
- }
-
- if (!areAllOurs) {
- ToastUtils.pushMessageDeleteForbidden();
- window.inboxStore?.dispatch(resetSelectedMessageIds());
- return;
- }
- await unsendMessagesForEveryone(conversation, selectedMessages, { deletionType: 'complete' }); // not 03 group: delete completely
- return;
- }
-
- // delete just for me in a groupv2 only means delete locally (not even synced to our other devices)
- if (conversation.isClosedGroupV2()) {
- await deleteMessagesLocallyOnly({
- conversation,
- messages: selectedMessages,
- deletionType: 'markDeleted',
- });
- ToastUtils.pushDeleted(selectedMessages.length);
-
- return;
- }
-
- // delete just for me in a legacy closed group only means delete locally
- if (conversation.isClosedGroup()) {
- await deleteMessagesFromSwarmAndMarkAsDeletedLocally(conversation, selectedMessages);
-
- // Update view and trigger update
- window.inboxStore?.dispatch(resetSelectedMessageIds());
- ToastUtils.pushDeleted(selectedMessages.length);
- return;
- }
- // otherwise, delete that message locally, from our swarm and from our other devices
- await unsendMessageJustForThisUser(conversation, selectedMessages);
-};
-
-/**
- * Either delete for everyone or not, based on the props
- */
-export async function deleteMessagesForX(
- messageIds: Array,
- conversationId: string,
- /** should only be enforced for messages successfully sent on communities */
- enforceDeleteServerSide: boolean
-) {
- if (conversationId) {
- if (enforceDeleteServerSide) {
- await deleteMessagesByIdForEveryone(messageIds, conversationId);
- } else {
- await deleteMessagesById(messageIds, conversationId);
- }
- }
-}
-
-export async function deleteMessagesByIdForEveryone(
- messageIds: Array,
- conversationId: string
-) {
- const conversation = ConvoHub.use().getOrThrow(conversationId);
- const isMe = conversation.isMe();
- const selectedMessages = compact(
- await Promise.all(messageIds.map(m => Data.getMessageById(m, false)))
- );
-
- const closeDialog = () => window.inboxStore?.dispatch(updateConfirmModal(null));
-
- window.inboxStore?.dispatch(
- updateConfirmModal({
- title: isMe ? tr('deleteMessageDevicesAll') : tr('clearMessagesForEveryone'),
- i18nMessage: { token: 'deleteMessageConfirm', args: { count: selectedMessages.length } },
- okText: isMe ? tr('deleteMessageDevicesAll') : tr('clearMessagesForEveryone'),
- okTheme: SessionButtonColor.Danger,
- onClickOk: async () => {
- await doDeleteSelectedMessages({ selectedMessages, conversation, deleteForEveryone: true });
-
- // explicitly close modal for this case.
- closeDialog();
- },
- onClickCancel: closeDialog,
- onClickClose: closeDialog,
- })
- );
-}
-
-export async function deleteMessagesById(messageIds: Array, conversationId: string) {
- const conversation = ConvoHub.use().getOrThrow(conversationId);
- const selectedMessages = compact(
- await Promise.all(messageIds.map(m => Data.getMessageById(m, false)))
- );
-
- const isMe = conversation.isMe();
- const count = messageIds.length;
-
- const closeDialog = () => window.inboxStore?.dispatch(updateConfirmModal(null));
- const clearMessagesForEveryone = 'clearMessagesForEveryone';
-
- // Note: the isMe case has no radio buttons, so we just show the description below
- const i18nMessage: LocalizerProps | undefined = isMe
- ? { token: 'deleteMessageDescriptionDevice', args: { count } }
- : undefined;
-
- window.inboxStore?.dispatch(
- updateConfirmModal({
- title: tr('deleteMessage', { count: selectedMessages.length }),
- radioOptions: !isMe
- ? [
- {
- label: tr('clearMessagesForMe'),
- value: 'clearMessagesForMe' as const,
- inputDataTestId: 'input-deleteJustForMe' as const,
- labelDataTestId: 'label-deleteJustForMe' as const,
- },
- {
- label: tr('clearMessagesForEveryone'),
- value: clearMessagesForEveryone,
- inputDataTestId: 'input-deleteForEveryone' as const,
- labelDataTestId: 'label-deleteForEveryone' as const,
- },
- ]
- : undefined,
- i18nMessage,
- okText: tr('delete'),
- okTheme: SessionButtonColor.Danger,
- onClickOk: async args => {
- await doDeleteSelectedMessages({
- selectedMessages,
- conversation,
- deleteForEveryone: args === clearMessagesForEveryone,
- });
- window.inboxStore?.dispatch(updateConfirmModal(null));
- window.inboxStore?.dispatch(closeRightPanel());
- window.inboxStore?.dispatch(sectionActions.resetRightOverlayMode());
- },
- onClickClose: closeDialog,
- })
- );
-}
-
-/**
- *
- * @param messages the list of MessageModel to delete
- * @param convo the conversation to delete from (only v2 opengroups are supported)
- */
-async function deleteOpenGroupMessages(
- messages: Array,
- convo: ConversationModel
-): Promise> {
- if (!convo.isPublic()) {
- throw new Error('cannot delete public message on a non public groups');
- }
-
- const roomInfos = convo.toOpenGroupV2();
- // on v2 servers we can only remove a single message per request..
- // so logic here is to delete each messages and get which one where not removed
- const validServerIdsToRemove = compact(
- messages.map(msg => {
- return msg.get('serverId');
- })
- );
-
- const validMessageModelsToRemove = compact(
- messages.map(msg => {
- const serverId = msg.get('serverId');
- if (serverId) {
- return msg;
- }
- return undefined;
- })
- );
-
- let allMessagesAreDeleted: boolean = false;
- if (validServerIdsToRemove.length) {
- allMessagesAreDeleted = await deleteSogsMessageByServerIds(validServerIdsToRemove, roomInfos);
- }
- // remove only the messages we managed to remove on the server
- if (allMessagesAreDeleted) {
- window?.log?.info('Removed all those serverIds messages successfully');
- return validMessageModelsToRemove.map(m => m.id);
- }
- window?.log?.info(
- 'failed to remove all those serverIds message. not removing them locally neither'
- );
- return [];
-}
diff --git a/ts/models/message.ts b/ts/models/message.ts
index d76385ae23..6361899ea2 100644
--- a/ts/models/message.ts
+++ b/ts/models/message.ts
@@ -183,7 +183,9 @@ export class MessageModel extends Model {
this.isExpirationTimerUpdate() ||
this.isDataExtractionNotification() ||
this.isMessageRequestResponse() ||
- this.isGroupUpdate()
+ this.isGroupUpdate() ||
+ this.isCallNotification() ||
+ this.isInteractionNotification()
);
}
diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts
index 43d3f7cc32..aed236934e 100644
--- a/ts/models/messageType.ts
+++ b/ts/models/messageType.ts
@@ -54,7 +54,6 @@ export interface MessageAttributes {
* You can use the values from READ_MESSAGE_STATE.unread and READ_MESSAGE_STATE.read
*/
unread: number;
- group?: any;
/**
* timestamp is the sent_at timestamp, which is the envelope.timestamp
*/
diff --git a/ts/react.d.ts b/ts/react.d.ts
index 169a836237..cbb0c413cc 100644
--- a/ts/react.d.ts
+++ b/ts/react.d.ts
@@ -75,6 +75,18 @@ declare module 'react' {
// left pane section types
type Sections = 'theme' | 'settings' | 'message' | 'privacy' | 'debug-menu';
+ type RadioOptions =
+ | 'device_and_network'
+ | 'device_only'
+ | 'deleteMessageEveryone'
+ | 'deleteMessageDevicesAll'
+ | 'deleteMessageDeviceOnly'
+ | 'enterForSend'
+ | 'enterForNewLine'
+ | 'message'
+ | 'name'
+ | 'count';
+
type SettingsMenuItems =
| 'message-requests'
| 'recovery-password'
@@ -223,24 +235,8 @@ declare module 'react' {
// SessionRadioGroup & SessionRadio
| 'password-input-confirm'
| 'msg-status'
- | 'input-device_and_network'
- | 'label-device_and_network'
- | 'input-device_only'
- | 'label-device_only'
- | 'input-deleteForEveryone'
- | 'label-deleteForEveryone'
- | 'input-deleteJustForMe'
- | 'label-deleteJustForMe'
- | 'input-enterForSend'
- | 'label-enterForSend'
- | 'input-enterForNewLine'
- | 'label-enterForNewLine'
- | 'input-message'
- | 'label-message'
- | 'input-name'
- | 'label-name'
- | 'input-count'
- | 'label-count'
+ | `input-${RadioOptions}`
+ | `label-${RadioOptions}`
| 'clear-everyone-radio-option'
| 'clear-device-radio-option'
| 'clear-everyone-radio-option-label'
diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts
index f9039dfbf1..d0568a7ffb 100644
--- a/ts/receiver/contentMessage.ts
+++ b/ts/receiver/contentMessage.ts
@@ -676,7 +676,7 @@ async function handleUnsendMessage(envelope: EnvelopePlus, unsendMessage: Signal
return;
}
if (messageToDelete.getSource() === UserUtils.getOurPubKeyStrFromCache()) {
- // a message we sent is completely removed when we get a unsend request
+ // a message we sent is completely removed when we get a unsend request for it
void deleteMessagesFromSwarmAndCompletelyLocally(conversation, [messageToDelete]);
} else {
void deleteMessagesFromSwarmAndMarkAsDeletedLocally(conversation, [messageToDelete]);
diff --git a/ts/receiver/groupv2/handleGroupV2Message.ts b/ts/receiver/groupv2/handleGroupV2Message.ts
index 735fcf0731..7fc6813643 100644
--- a/ts/receiver/groupv2/handleGroupV2Message.ts
+++ b/ts/receiver/groupv2/handleGroupV2Message.ts
@@ -2,7 +2,6 @@ import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_no
import { isEmpty, isFinite, isNumber } from 'lodash';
import { Data } from '../../data/data';
import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions';
-import { deleteMessagesFromSwarmOnly } from '../../interactions/conversations/unsendingInteractions';
import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/types';
import { HexString } from '../../node/hexStrings';
import { SignalService } from '../../protobuf';
@@ -27,6 +26,7 @@ import {
UserGroupsWrapperActions,
} from '../../webworker/workers/browser/libsession_worker_interface';
import { sendInviteResponseToGroup } from '../../session/sending/group/GroupInviteResponse';
+import { deleteMessagesFromSwarmOnly } from '../../interactions/conversations/deleteMessagesFromSwarmOnly';
type WithSignatureTimestamp = { signatureTimestamp: number };
type WithAuthor = { author: PubkeyType };
diff --git a/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts b/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts
index ce4f65d6ab..c9d4e062ec 100644
--- a/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts
+++ b/ts/session/utils/job_runners/jobs/GroupPendingRemovalsJob.ts
@@ -5,7 +5,7 @@ import { v4 } from 'uuid';
import AbortController from 'abort-controller';
import { StringUtils } from '../..';
import { Data } from '../../../../data/data';
-import { deleteMessagesFromSwarmOnly } from '../../../../interactions/conversations/unsendingInteractions';
+import { deleteMessagesFromSwarmAndMarkAsDeletedLocally } from '../../../../interactions/conversations/unsendingInteractions';
import {
MetaGroupWrapperActions,
MultiEncryptWrapperActions,
@@ -226,20 +226,9 @@ class GroupPendingRemovalsJob extends PersistedJob m.getMessageHash()));
- if (messageHashes.length) {
- await deleteMessagesFromSwarmOnly(messageHashes, groupPk);
- }
- for (let index = 0; index < models.length; index++) {
- const messageModel = models[index];
- try {
- // eslint-disable-next-line no-await-in-loop
- await messageModel.markAsDeleted();
- } catch (e) {
- window.log.warn(
- `GroupPendingRemoval markAsDeleted of ${messageModel.getMessageHash()} failed with`,
- e.message
- );
- }
+ const convo = models?.[0].getConversation();
+ if (convo && messageHashes.length) {
+ await deleteMessagesFromSwarmAndMarkAsDeletedLocally(convo, models);
}
}
} catch (e) {
diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts
index 39500d2b9a..862ed28682 100644
--- a/ts/state/ducks/conversations.ts
+++ b/ts/state/ducks/conversations.ts
@@ -186,7 +186,6 @@ export type PropsForMessageWithConvoProps = PropsForMessageWithoutConvoProps & {
weAreAdmin: boolean;
isSenderAdmin: boolean;
isDeletable: boolean;
- isDeletableForEveryone: boolean;
isBlocked: boolean;
isDeleted?: boolean;
};
diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts
index b955d21cdf..2f3357e6e5 100644
--- a/ts/state/selectors/conversations.ts
+++ b/ts/state/selectors/conversations.ts
@@ -738,12 +738,6 @@ export const getMessagePropsByMessageId = createSelector(
const isDeletable =
sender === ourPubkey || !isPublic || (isPublic && (weAreAdmin || weAreModerator));
- // A message is deletable for everyone if
- // either we sent it no matter what the conversation type,
- // or the convo is public and we are an admin or moderator
- const isDeletableForEveryone =
- sender === ourPubkey || (isPublic && (weAreAdmin || weAreModerator)) || false;
-
const isSenderAdmin = groupAdmins.includes(sender);
const messageProps: MessageModelPropsWithConvoProps = {
@@ -754,7 +748,6 @@ export const getMessagePropsByMessageId = createSelector(
isPublic: !!isPublic,
isSenderAdmin,
isDeletable,
- isDeletableForEveryone,
weAreAdmin,
conversationType: selectedConvo.type,
sender,
diff --git a/ts/state/selectors/messages.ts b/ts/state/selectors/messages.ts
index 79bf875dc1..75741a7035 100644
--- a/ts/state/selectors/messages.ts
+++ b/ts/state/selectors/messages.ts
@@ -123,9 +123,6 @@ export function useMessageSender(messageId: string | undefined) {
return useMessagePropsByMessageId(messageId)?.propsForMessage.sender;
}
-export function useMessageIsDeletableForEveryone(messageId: string | undefined) {
- return useMessagePropsByMessageId(messageId)?.propsForMessage.isDeletableForEveryone;
-}
export function useMessageServerTimestamp(messageId: string | undefined) {
return useMessagePropsByMessageId(messageId)?.propsForMessage.serverTimestamp;
diff --git a/ts/types/isStringArray.ts b/ts/types/isStringArray.ts
new file mode 100644
index 0000000000..03d9b60e68
--- /dev/null
+++ b/ts/types/isStringArray.ts
@@ -0,0 +1,3 @@
+export function isStringArray(value: unknown): value is Array {
+ return Array.isArray(value) && value.every(val => typeof val === 'string');
+}