From 2ec6c7f29cc7d82f5ec1c37d7d2f9cd4024126f1 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Tue, 4 Jun 2024 12:02:11 +1000 Subject: [PATCH] feat: handle isDestroyed flag + "you were removed from XXX" --- _locales/en/messages.json | 1 + .../conversation/SubtleNotification.tsx | 12 +- .../overlay/OverlayRightPanelSettings.tsx | 62 ++++++- ts/interactions/conversationInteractions.ts | 19 ++- ts/react.d.ts | 2 + ts/receiver/closedGroups.ts | 18 +- ts/receiver/configMessage.ts | 7 +- .../libsession/handleLibSessionMessage.ts | 4 +- .../apis/snode_api/SnodeRequestTypes.ts | 61 ++++++- .../snode_api/signature/groupSignature.ts | 11 +- ts/session/apis/snode_api/swarmPolling.ts | 19 ++- .../SwarmPollingGroupConfig.ts | 111 +++++++------ .../conversations/ConversationController.ts | 154 ++++++++++++++---- ts/session/onions/onionPath.ts | 2 +- ts/session/sending/MessageSender.ts | 14 +- .../utils/job_runners/jobs/GroupSyncJob.ts | 20 ++- ts/state/ducks/metaGroups.ts | 102 ++---------- ts/state/selectors/selectedConversation.ts | 2 +- ts/types/LocalizerKeys.ts | 1 + ts/types/sqlSharedTypes.ts | 55 ------- 20 files changed, 407 insertions(+), 270 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6f5e2a55f..e7d08665c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -38,6 +38,7 @@ "done": "Done", "youLeftTheGroup": "You have left the group.", "youGotKickedFromGroup": "You were removed from the group.", + "youWereRemovedFrom": "You were removed from $name$.", "unreadMessages": "Unread Messages", "debugLogExplanation": "This log will be saved to your desktop.", "reportIssue": "Report a Bug", diff --git a/ts/components/conversation/SubtleNotification.tsx b/ts/components/conversation/SubtleNotification.tsx index f0202bb8b..56719dcec 100644 --- a/ts/components/conversation/SubtleNotification.tsx +++ b/ts/components/conversation/SubtleNotification.tsx @@ -24,6 +24,7 @@ import { import { useLibGroupInviteGroupName, useLibGroupInvitePending, + useLibGroupKicked, } from '../../state/selectors/userGroups'; import { LocalizerKeys } from '../../types/LocalizerKeys'; import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer'; @@ -140,6 +141,7 @@ export const NoMessageInConversation = () => { const nameToRender = useSelectedNicknameOrProfileNameOrShortenedPubkey() || ''; const isPrivate = useSelectedIsPrivate(); const isIncomingRequest = useIsIncomingRequest(selectedConversation); + const isKickedFromGroup = useLibGroupKicked(selectedConversation); // groupV2 use its own invite logic as part of if ( @@ -160,11 +162,13 @@ export const NoMessageInConversation = () => { } else if (isMe) { localizedKey = 'noMessagesInNoteToSelf'; } + let dataTestId: SessionDataTestId = 'empty-conversation-notification'; + if (isGroupV2 && isKickedFromGroup) { + localizedKey = 'youWereRemovedFrom'; + dataTestId = 'group-control-message'; + } return ( - + ); }; diff --git a/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx b/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx index 78980902c..b78259066 100644 --- a/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx +++ b/ts/components/conversation/right-panel/overlay/OverlayRightPanelSettings.tsx @@ -24,8 +24,11 @@ import { triggerFakeAvatarUpdate, } from '../../../../interactions/conversationInteractions'; import { Constants } from '../../../../session'; -import { isDevProd } from '../../../../shared/env_vars'; +import { ConvoHub } from '../../../../session/conversations'; +import { PubKey } from '../../../../session/types'; +import { isDevProd, isTestIntegration } from '../../../../shared/env_vars'; import { closeRightPanel } from '../../../../state/ducks/conversations'; +import { updateConfirmModal } from '../../../../state/ducks/modalDialog'; import { resetRightOverlayMode, setRightOverlayMode } from '../../../../state/ducks/section'; import { useSelectedConversationKey, @@ -44,6 +47,7 @@ import { AttachmentTypeWithPath } from '../../../../types/Attachment'; import { getAbsoluteAttachmentPath } from '../../../../types/MessageAttachment'; import { Avatar, AvatarSize } from '../../../avatar/Avatar'; import { Flex } from '../../../basic/Flex'; +import { SessionButtonColor } from '../../../basic/SessionButton'; import { SpacerLG, SpacerMD, SpacerXL } from '../../../basic/Text'; import { PanelButtonGroup, PanelIconButton } from '../../../buttons'; import { MediaItemType } from '../../../lightbox/LightboxGallery'; @@ -192,6 +196,43 @@ const StyledName = styled.h4` font-size: var(--font-size-md); `; +const DestroyGroupForAllMembersButton = () => { + const dispatch = useDispatch(); + const groupPk = useSelectedConversationKey(); + if (groupPk && PubKey.is03Pubkey(groupPk) && (isDevProd() || isTestIntegration())) { + return ( + { + dispatch( + // TODO build the right UI for this (just adding buttons for QA for now) + updateConfirmModal({ + okText: window.i18n('delete'), + okTheme: SessionButtonColor.Danger, + title: window.i18n('editMenuDeleteGroup'), + conversationId: groupPk, + onClickOk: () => { + void ConvoHub.use().deleteGroup(groupPk, { + deleteAllMessagesOnSwarm: true, + emptyGroupButKeepAsKicked: false, + fromSyncMessage: false, + sendLeaveMessage: false, + forceDestroyForAllMembers: true, + }); + }, + }) + ); + }} + /> + ); + } + + return null; +}; + export const OverlayRightPanelSettings = () => { const [documents, setDocuments] = useState>([]); const [media, setMedia] = useState>([]); @@ -354,14 +395,17 @@ export const OverlayRightPanelSettings = () => { {isGroup && ( - void deleteConvoAction()} - color={'var(--danger-color)'} - iconType={'delete'} - /> + <> + void deleteConvoAction()} + color={'var(--danger-color)'} + iconType={'delete'} + /> + + )} diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 479133552..59b696cf9 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -458,11 +458,20 @@ async function leaveGroupOrCommunityByConvoId({ status: ConversationInteractionStatus.Start, }); - await ConvoHub.use().deleteClosedGroup(conversationId, { - fromSyncMessage: false, - sendLeaveMessage, - emptyGroupButKeepAsKicked: false, - }); + if (PubKey.is05Pubkey(conversationId)) { + await ConvoHub.use().deleteLegacyGroup(conversationId, { + fromSyncMessage: false, + sendLeaveMessage, + }); + } else if (PubKey.is03Pubkey(conversationId)) { + await ConvoHub.use().deleteGroup(conversationId, { + fromSyncMessage: false, + sendLeaveMessage, + deleteAllMessagesOnSwarm: false, + emptyGroupButKeepAsKicked: false, + forceDestroyForAllMembers: false, + }); + } await clearConversationInteractionState({ conversationId }); } catch (err) { window.log.warn(`showLeaveGroupByConvoId error: ${err}`); diff --git a/ts/react.d.ts b/ts/react.d.ts index 49c2de517..abfbdb49a 100644 --- a/ts/react.d.ts +++ b/ts/react.d.ts @@ -107,6 +107,7 @@ declare module 'react' { | 'conversation-request-explanation' | 'group-invite-control-message' | 'empty-conversation-notification' + | 'group-control-message' // call notification types | 'call-notification-missed-call' @@ -138,6 +139,7 @@ declare module 'react' { | 'remove-moderators' | 'add-moderators' | 'edit-group-name' + | 'delete-group-button' // SessionRadioGroup & SessionRadio | 'password-input-confirm' diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 50d99b60b..049c53af7 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -768,11 +768,13 @@ async function handleClosedGroupMembersRemoved( const ourPubKey = UserUtils.getOurPubKeyFromCache(); const wasCurrentUserKicked = !membersAfterUpdate.includes(ourPubKey.key); if (wasCurrentUserKicked) { + if (!PubKey.is05Pubkey(groupPubKey)) { + throw new Error('handleClosedGroupMembersRemoved expected a 05 groupPk'); + } // we now want to remove everything related to a group when we get kicked from it. - await ConvoHub.use().deleteClosedGroup(groupPubKey, { + await ConvoHub.use().deleteLegacyGroup(groupPubKey, { fromSyncMessage: false, sendLeaveMessage: false, - emptyGroupButKeepAsKicked: false, // legacy group case only here }); } else { // Note: we don't want to send a new encryption keypair when we get a member removed. @@ -854,21 +856,25 @@ function removeMemberFromZombies( } async function handleClosedGroupAdminMemberLeft(groupPublicKey: string, envelope: EnvelopePlus) { + if (!PubKey.is05Pubkey(groupPublicKey)) { + throw new Error('handleClosedGroupAdminMemberLeft excepted a 05 groupPk'); + } // if the admin was remove and we are the admin, it can only be voluntary - await ConvoHub.use().deleteClosedGroup(groupPublicKey, { + await ConvoHub.use().deleteLegacyGroup(groupPublicKey, { fromSyncMessage: false, sendLeaveMessage: false, - emptyGroupButKeepAsKicked: false, }); await IncomingMessageCache.removeFromCache(envelope); } async function handleClosedGroupLeftOurself(groupId: string, envelope: EnvelopePlus) { + if (!PubKey.is05Pubkey(groupId)) { + throw new Error('handleClosedGroupLeftOurself excepted a 05 groupPk'); + } // if we ourself left. It can only mean that another of our device left the group and we just synced that message through the swarm - await ConvoHub.use().deleteClosedGroup(groupId, { + await ConvoHub.use().deleteLegacyGroup(groupId, { fromSyncMessage: false, sendLeaveMessage: false, - emptyGroupButKeepAsKicked: false, }); await IncomingMessageCache.removeFromCache(envelope); } diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index cffadf693..66cc98b45 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -583,10 +583,9 @@ async function handleLegacyGroupUpdate(latestEnvelopeTimestamp: number) { ); const toLeaveFromDb = ConvoHub.use().get(toLeave.id); // the wrapper told us that this group is not tracked, so even if we left/got kicked from it, remove it from the DB completely - await ConvoHub.use().deleteClosedGroup(toLeaveFromDb.id, { + await ConvoHub.use().deleteLegacyGroup(toLeaveFromDb.id, { fromSyncMessage: true, sendLeaveMessage: false, // this comes from the wrapper, so we must have left/got kicked from that group already and our device already handled it. - emptyGroupButKeepAsKicked: false, }); } @@ -738,10 +737,12 @@ async function handleSingleGroupUpdateToLeave(toLeave: GroupPubkeyType) { `About to deleteGroup ${toLeave} via handleSingleGroupUpdateToLeave as in DB but not in wrapper` ); - await ConvoHub.use().deleteClosedGroup(toLeave, { + await ConvoHub.use().deleteGroup(toLeave, { fromSyncMessage: true, sendLeaveMessage: false, emptyGroupButKeepAsKicked: false, + deleteAllMessagesOnSwarm: false, + forceDestroyForAllMembers: false, }); } catch (e) { window.log.info('Failed to deleteClosedGroup with: ', e.message); diff --git a/ts/receiver/libsession/handleLibSessionMessage.ts b/ts/receiver/libsession/handleLibSessionMessage.ts index 2ed0db793..b24c06ea9 100644 --- a/ts/receiver/libsession/handleLibSessionMessage.ts +++ b/ts/receiver/libsession/handleLibSessionMessage.ts @@ -50,10 +50,12 @@ async function handleLibSessionKickedMessage({ } const inviteWasPending = (await UserGroupsWrapperActions.getGroup(groupPk))?.invitePending || false; - await ConvoHub.use().deleteClosedGroup(groupPk, { + await ConvoHub.use().deleteGroup(groupPk, { sendLeaveMessage: false, fromSyncMessage: false, emptyGroupButKeepAsKicked: !inviteWasPending, + deleteAllMessagesOnSwarm: false, + forceDestroyForAllMembers: false, }); } diff --git a/ts/session/apis/snode_api/SnodeRequestTypes.ts b/ts/session/apis/snode_api/SnodeRequestTypes.ts index 1bbded3b1..512af0ac5 100644 --- a/ts/session/apis/snode_api/SnodeRequestTypes.ts +++ b/ts/session/apis/snode_api/SnodeRequestTypes.ts @@ -1,7 +1,7 @@ import ByteBuffer from 'bytebuffer'; import { GroupPubkeyType, PubkeyType, WithGroupPubkey } from 'libsession_util_nodejs'; import { from_hex } from 'libsodium-wrappers-sumo'; -import { isEmpty } from 'lodash'; +import { isEmpty, isString } from 'lodash'; import { AwaitedReturn, assertUnreachable } from '../../../types/sqlSharedTypes'; import { UserGroupsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface'; import { concatUInt8Array } from '../../crypto'; @@ -310,7 +310,6 @@ abstract class AbstractRevokeSubRequest extends SnodeAPISubRequest { public readonly groupPk: GroupPubkeyType; public readonly timestamp: number; public readonly revokeTokenHex: Array; - protected readonly adminSecretKey: Uint8Array; constructor({ @@ -471,8 +470,49 @@ export class DeleteAllFromUserNodeSubRequest extends SnodeAPISubRequest { } } -// We don't need that one yet -// export class DeleteAllFromGroupNodeSubRequest extends DeleteAllFromUserNodeSubRequest {} +/** + * Delete all the messages and not the config messages for that group 03. + */ +export class DeleteAllFromGroupMsgNodeSubRequest extends SnodeAPISubRequest { + public method = 'delete_all' as const; + public readonly namespace = SnodeNamespaces.ClosedGroupMessages; + public readonly adminSecretKey: Uint8Array; + public readonly groupPk: GroupPubkeyType; + + constructor(args: WithGroupPubkey & WithSecretKey) { + super(); + this.groupPk = args.groupPk; + this.adminSecretKey = args.secretKey; + if (isEmpty(this.adminSecretKey)) { + throw new Error('DeleteAllFromGroupMsgNodeSubRequest needs an adminSecretKey'); + } + } + + public async buildAndSignParameters() { + const signDetails = await SnodeGroupSignature.getSnodeGroupSignature({ + method: this.method, + namespace: this.namespace, + group: { authData: null, pubkeyHex: this.groupPk, secretKey: this.adminSecretKey }, + }); + + if (!signDetails) { + throw new Error( + `[DeleteAllFromGroupMsgNodeSubRequest] SnodeSignature.getSnodeSignatureParamsUs returned an empty result` + ); + } + return { + method: this.method, + params: { + ...signDetails, + namespace: this.namespace, + }, + }; + } + + public loggingId(): string { + return `${this.method}-${ed25519Str(this.groupPk)}-${this.namespace}`; + } +} export class DeleteHashesFromUserNodeSubRequest extends SnodeAPISubRequest { public method = 'delete' as const; @@ -981,7 +1021,8 @@ export type RawSnodeSubRequests = | UpdateExpiryOnNodeGroupSubRequest | SubaccountRevokeSubRequest | SubaccountUnrevokeSubRequest - | GetExpiriesFromNodeSubRequest; + | GetExpiriesFromNodeSubRequest + | DeleteAllFromGroupMsgNodeSubRequest; export type BuiltSnodeSubRequests = | ReturnType @@ -1000,7 +1041,8 @@ export type BuiltSnodeSubRequests = | AwaitedReturn | AwaitedReturn | AwaitedReturn - | AwaitedReturn; + | AwaitedReturn + | AwaitedReturn; export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string { const { method, params } = request; @@ -1010,7 +1052,6 @@ export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string return `${method}`; case 'delete': - case 'delete_all': case 'expire': case 'get_expiries': case 'get_swarm': @@ -1019,6 +1060,12 @@ export function builtRequestToLoggingId(request: BuiltSnodeSubRequests): string const isUs = UserUtils.isUsFromCache(params.pubkey); return `${method}-${isUs ? 'us' : ed25519Str(params.pubkey)}`; } + case 'delete_all': { + const isUs = UserUtils.isUsFromCache(params.pubkey); + return `${method}-${isUs ? 'us' : ed25519Str(params.pubkey)}-${ + isString(params.namespace) ? params.namespace : SnodeNamespace.toRole(params.namespace) + }}`; + } case 'retrieve': case 'store': { diff --git a/ts/session/apis/snode_api/signature/groupSignature.ts b/ts/session/apis/snode_api/signature/groupSignature.ts index 49d823999..0843bc8a8 100644 --- a/ts/session/apis/snode_api/signature/groupSignature.ts +++ b/ts/session/apis/snode_api/signature/groupSignature.ts @@ -85,7 +85,7 @@ async function getGroupPromoteMessage({ type ParamsShared = { groupPk: GroupPubkeyType; namespace: SnodeNamespacesGroup; - method: 'retrieve' | 'store'; + method: 'retrieve' | 'store' | 'delete_all'; }; type SigParamsAdmin = ParamsShared & { @@ -140,13 +140,16 @@ export type GroupDetailsNeededForSignature = Pick< type StoreOrRetrieve = { method: 'store' | 'retrieve'; namespace: SnodeNamespacesGroup }; type DeleteHashes = { method: 'delete'; hashes: Array }; +type DeleteAllNonConfigs = { method: 'delete_all'; namespace: SnodeNamespacesGroup }; async function getSnodeGroupSignature({ group, ...args }: { group: GroupDetailsNeededForSignature | null; -} & (StoreOrRetrieve | DeleteHashes)): Promise { +} & (StoreOrRetrieve | DeleteHashes | DeleteAllNonConfigs)): Promise< + SigResultSubAccount | SigResultAdmin +> { if (!group) { throw new Error(`getSnodeGroupSignature: did not find group in wrapper`); } @@ -155,6 +158,10 @@ async function getSnodeGroupSignature({ const groupSecretKey = secretKey && !isEmpty(secretKey) ? secretKey : null; const groupAuthData = authData && !isEmpty(authData) ? authData : null; + if (args.method === 'delete_all' && isEmpty(secretKey)) { + throw new Error('getSnodeGroupSignature: delete_all needs an adminSecretKey'); + } + if (groupSecretKey) { if (args.method === 'delete') { return getGroupSignatureByHashesParams({ diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 3f2b6bde1..1ac35f380 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -667,11 +667,20 @@ export class SwarmPolling { window.log.debug( `notPollingForGroupAsNotInWrapper ${ed25519Str(pubkey)} with reason:"${reason}"` ); - await ConvoHub.use().deleteClosedGroup(pubkey, { - fromSyncMessage: true, - sendLeaveMessage: false, - emptyGroupButKeepAsKicked: false, - }); + if (PubKey.is05Pubkey(pubkey)) { + await ConvoHub.use().deleteLegacyGroup(pubkey, { + fromSyncMessage: true, + sendLeaveMessage: false, + }); + } else if (PubKey.is03Pubkey(pubkey)) { + await ConvoHub.use().deleteGroup(pubkey, { + fromSyncMessage: true, + sendLeaveMessage: false, + emptyGroupButKeepAsKicked: false, + deleteAllMessagesOnSwarm: false, + forceDestroyForAllMembers: false, + }); + } } private loadGroupIds() { diff --git a/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts b/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts index 00ddcaae6..1e2bac46c 100644 --- a/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts +++ b/ts/session/apis/snode_api/swarm_polling_config/SwarmPollingGroupConfig.ts @@ -9,6 +9,7 @@ import { GroupPendingRemovals } from '../../../utils/job_runners/jobs/GroupPendi import { LibSessionUtil } from '../../../utils/libsession/libsession_utils'; import { SnodeNamespaces } from '../namespaces'; import { RetrieveMessageItemWithNamespace } from '../types'; +import { ConvoHub } from '../../../conversations'; /** * This is a basic optimization to avoid running the logic when the `deleteBeforeSeconds` @@ -23,60 +24,70 @@ const lastAppliedRemoveAttachmentSentBeforeSeconds = new Map 0 && - (lastAppliedRemoveMsgSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) > - infos.deleteBeforeSeconds - ) { - // delete any messages in this conversation sent before that timestamp (in seconds) - const deletedMsgIds = await Data.removeAllMessagesInConversationSentBefore({ - deleteBeforeSeconds: infos.deleteBeforeSeconds, - conversationId: groupPk, - }); - window.log.info( - `removeAllMessagesInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteBeforeSeconds}: `, - deletedMsgIds - ); - window.inboxStore.dispatch( - messagesExpired(deletedMsgIds.map(messageId => ({ conversationKey: groupPk, messageId }))) - ); - lastAppliedRemoveMsgSentBeforeSeconds.set(groupPk, infos.deleteBeforeSeconds); - } + if (infos) { + if (infos.isDestroyed) { + window.log.info(`${ed25519Str(groupPk)} is marked as destroyed after merge. Removing it.`); + await ConvoHub.use().deleteGroup(groupPk, { + sendLeaveMessage: false, + fromSyncMessage: false, + emptyGroupButKeepAsKicked: true, // we just got something from the group's swarm, so it is not pendingInvite + deleteAllMessagesOnSwarm: false, + forceDestroyForAllMembers: false, + }); + } else { + if ( + isNumber(infos.deleteBeforeSeconds) && + isFinite(infos.deleteBeforeSeconds) && + infos.deleteBeforeSeconds > 0 && + (lastAppliedRemoveMsgSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) > + infos.deleteBeforeSeconds + ) { + // delete any messages in this conversation sent before that timestamp (in seconds) + const deletedMsgIds = await Data.removeAllMessagesInConversationSentBefore({ + deleteBeforeSeconds: infos.deleteBeforeSeconds, + conversationId: groupPk, + }); + window.log.info( + `removeAllMessagesInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteBeforeSeconds}: `, + deletedMsgIds + ); + window.inboxStore.dispatch( + messagesExpired(deletedMsgIds.map(messageId => ({ conversationKey: groupPk, messageId }))) + ); + lastAppliedRemoveMsgSentBeforeSeconds.set(groupPk, infos.deleteBeforeSeconds); + } - if ( - infos && - isNumber(infos.deleteAttachBeforeSeconds) && - isFinite(infos.deleteAttachBeforeSeconds) && - infos.deleteAttachBeforeSeconds > 0 && - (lastAppliedRemoveAttachmentSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) > - infos.deleteAttachBeforeSeconds - ) { - // delete any attachments in this conversation sent before that timestamp (in seconds) - const impactedMsgModels = await Data.getAllMessagesWithAttachmentsInConversationSentBefore({ - deleteAttachBeforeSeconds: infos.deleteAttachBeforeSeconds, - conversationId: groupPk, - }); - window.log.info( - `getAllMessagesWithAttachmentsInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteAttachBeforeSeconds}: impactedMsgModelsIds `, - impactedMsgModels.map(m => m.id) - ); + if ( + isNumber(infos.deleteAttachBeforeSeconds) && + isFinite(infos.deleteAttachBeforeSeconds) && + infos.deleteAttachBeforeSeconds > 0 && + (lastAppliedRemoveAttachmentSentBeforeSeconds.get(groupPk) || Number.MAX_SAFE_INTEGER) > + infos.deleteAttachBeforeSeconds + ) { + // delete any attachments in this conversation sent before that timestamp (in seconds) + const impactedMsgModels = await Data.getAllMessagesWithAttachmentsInConversationSentBefore({ + deleteAttachBeforeSeconds: infos.deleteAttachBeforeSeconds, + conversationId: groupPk, + }); + window.log.info( + `getAllMessagesWithAttachmentsInConversationSentBefore of ${ed25519Str(groupPk)} before ${infos.deleteAttachBeforeSeconds}: impactedMsgModelsIds `, + impactedMsgModels.map(m => m.id) + ); - for (let index = 0; index < impactedMsgModels.length; index++) { - const msg = impactedMsgModels[index]; + for (let index = 0; index < impactedMsgModels.length; index++) { + const msg = impactedMsgModels[index]; - // eslint-disable-next-line no-await-in-loop - // eslint-disable-next-line no-await-in-loop - await msg?.cleanup(); + // eslint-disable-next-line no-await-in-loop + await msg?.cleanup(); + } + lastAppliedRemoveAttachmentSentBeforeSeconds.set(groupPk, infos.deleteAttachBeforeSeconds); + } + } + const membersWithPendingRemovals = + await MetaGroupWrapperActions.memberGetAllPendingRemovals(groupPk); + if (membersWithPendingRemovals.length) { + await GroupPendingRemovals.addJob({ groupPk }); } - lastAppliedRemoveAttachmentSentBeforeSeconds.set(groupPk, infos.deleteAttachBeforeSeconds); - } - const membersWithPendingRemovals = - await MetaGroupWrapperActions.memberGetAllPendingRemovals(groupPk); - if (membersWithPendingRemovals.length) { - await GroupPendingRemovals.addJob({ groupPk }); } } diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 9f1ccf832..0d6736920 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -15,26 +15,34 @@ import { getOpenGroupManager } from '../apis/open_group_api/opengroupV2/OpenGrou import { PubKey } from '../types'; import { getMessageQueue } from '..'; +import { ConfigDumpData } from '../../data/configDump/configDump'; import { deleteAllMessagesByConvoIdNoConfirmation } from '../../interactions/conversationInteractions'; import { CONVERSATION_PRIORITIES, ConversationTypeEnum } from '../../models/conversationAttributes'; import { removeAllClosedGroupEncryptionKeyPairs } from '../../receiver/closedGroups'; -import { groupInfoActions } from '../../state/ducks/metaGroups'; import { getCurrentlySelectedConversationOutsideRedux } from '../../state/selectors/conversations'; import { assertUnreachable } from '../../types/sqlSharedTypes'; -import { UserGroupsWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface'; +import { + MetaGroupWrapperActions, + UserGroupsWrapperActions, +} from '../../webworker/workers/browser/libsession_worker_interface'; import { OpenGroupUtils } from '../apis/open_group_api/utils'; import { getSwarmPollingInstance } from '../apis/snode_api'; +import { DeleteAllFromGroupMsgNodeSubRequest } from '../apis/snode_api/SnodeRequestTypes'; import { GetNetworkTime } from '../apis/snode_api/getNetworkTime'; import { SnodeNamespaces } from '../apis/snode_api/namespaces'; import { ClosedGroupMemberLeftMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupMemberLeftMessage'; import { GroupUpdateMemberLeftMessage } from '../messages/outgoing/controlMessage/group_v2/to_group/GroupUpdateMemberLeftMessage'; import { UserUtils } from '../utils'; +import { ed25519Str } from '../utils/String'; +import { PreConditionFailed } from '../utils/errors'; +import { RunJobResult } from '../utils/job_runners/PersistedJob'; +import { GroupSync } from '../utils/job_runners/jobs/GroupSyncJob'; import { UserSync } from '../utils/job_runners/jobs/UserSyncJob'; import { LibSessionUtil } from '../utils/libsession/libsession_utils'; import { SessionUtilContact } from '../utils/libsession/libsession_utils_contacts'; import { SessionUtilConvoInfoVolatile } from '../utils/libsession/libsession_utils_convo_info_volatile'; import { SessionUtilUserGroups } from '../utils/libsession/libsession_utils_user_groups'; -import { ed25519Str } from '../utils/String'; +import { groupInfoActions } from '../../state/ducks/metaGroups'; let instance: ConvoController | null; @@ -205,57 +213,143 @@ class ConvoController { await conversation.commit(); } - public async deleteClosedGroup( - groupPk: string, + public async deleteLegacyGroup( + groupPk: PubkeyType, + { sendLeaveMessage, fromSyncMessage }: DeleteOptions & { sendLeaveMessage: boolean } + ) { + if (!PubKey.is05Pubkey(groupPk)) { + throw new PreConditionFailed('deleteLegacyGroup excepts a 05 group'); + } + + window.log.info( + `deleteLegacyGroup: ${ed25519Str(groupPk)}, sendLeaveMessage:${sendLeaveMessage}, fromSyncMessage:${fromSyncMessage}` + ); + + // this deletes all messages in the conversation + const conversation = await this.deleteConvoInitialChecks(groupPk, 'LegacyGroup', false); + if (!conversation || !conversation.isClosedGroup()) { + return; + } + // we don't need to keep polling anymore. + getSwarmPollingInstance().removePubkey(groupPk, 'deleteLegacyGroup'); + + // send the leave message before we delete everything for this group (including the key!) + if (sendLeaveMessage) { + await leaveClosedGroup(groupPk, fromSyncMessage); + } + + await removeLegacyGroupFromWrappers(groupPk); + + // we never keep a left legacy group. Only fully remove it. + await this.removeGroupOrCommunityFromDBAndRedux(groupPk); + await UserSync.queueNewJobIfNeeded(); + } + + public async deleteGroup( + groupPk: GroupPubkeyType, { sendLeaveMessage, fromSyncMessage, emptyGroupButKeepAsKicked, - }: DeleteOptions & { sendLeaveMessage: boolean; emptyGroupButKeepAsKicked: boolean } + deleteAllMessagesOnSwarm, + forceDestroyForAllMembers, + }: DeleteOptions & { + sendLeaveMessage: boolean; + emptyGroupButKeepAsKicked: boolean; + deleteAllMessagesOnSwarm: boolean; + forceDestroyForAllMembers: boolean; + } ) { - if (!PubKey.is03Pubkey(groupPk) && !PubKey.is05Pubkey(groupPk)) { - return; + if (!PubKey.is03Pubkey(groupPk)) { + throw new PreConditionFailed('deleteGroup excepts a 03-group'); } - const typeOfDelete: ConvoVolatileType = PubKey.is03Pubkey(groupPk) ? 'Group' : 'LegacyGroup'; window.log.info( - `deleteClosedGroup: ${ed25519Str(groupPk)}, sendLeaveMessage:${sendLeaveMessage}, fromSyncMessage:${fromSyncMessage}, emptyGroupButKeepAsKicked:${emptyGroupButKeepAsKicked}` + `deleteGroup: ${ed25519Str(groupPk)}, sendLeaveMessage:${sendLeaveMessage}, fromSyncMessage:${fromSyncMessage}, emptyGroupButKeepAsKicked:${emptyGroupButKeepAsKicked}, deleteAllMessagesOnSwarm:${deleteAllMessagesOnSwarm}, forceDestroyForAllMembers:${forceDestroyForAllMembers}` ); // this deletes all messages in the conversation - const conversation = await this.deleteConvoInitialChecks(groupPk, typeOfDelete, false); + const conversation = await this.deleteConvoInitialChecks(groupPk, 'Group', false); if (!conversation || !conversation.isClosedGroup()) { return; } // we don't need to keep polling anymore. - getSwarmPollingInstance().removePubkey(groupPk, 'deleteClosedGroup'); + getSwarmPollingInstance().removePubkey(groupPk, 'deleteGroup'); + + const group = await UserGroupsWrapperActions.getGroup(groupPk); // send the leave message before we delete everything for this group (including the key!) - if (sendLeaveMessage) { + // Note: if we were kicked, we already lost the authdata/secretKey for it, so no need to try to send our message. + if (sendLeaveMessage && !group?.kicked) { await leaveClosedGroup(groupPk, fromSyncMessage); } - if (PubKey.is03Pubkey(groupPk)) { - // a group 03 can be removed fully or kept empty as kicked. - // when it was pendingInvite, we delete it fully, - // when it was not, we empty the group but keep it with the "you have been kicked" message - // Note: the pendingInvite=true case cannot really happen as we wouldn't be polling from that group (and so, not get the message kicking us ) - if (emptyGroupButKeepAsKicked) { - window?.inboxStore?.dispatch(groupInfoActions.emptyGroupButKeepAsKicked({ groupPk })); - } else { - window?.inboxStore?.dispatch(groupInfoActions.destroyGroupDetails({ groupPk })); + // a group 03 can be removed fully or kept empty as kicked. + // when it was pendingInvite, we delete it fully, + // when it was not, we empty the group but keep it with the "you have been kicked" message + // Note: the pendingInvite=true case cannot really happen as we wouldn't be polling from that group (and so, not get the message kicking us) + if (emptyGroupButKeepAsKicked) { + // delete the secretKey/authData if we had it. If we need it for something, it has to be done before this call. + if (group) { + group.authData = null; + group.secretKey = null; + group.disappearingTimerSeconds = undefined; + group.kicked = true; + await UserGroupsWrapperActions.setGroup(group); } } else { - await removeLegacyGroupFromWrappers(groupPk); - } - // if we were kicked or sent our left message, we have nothing to do more with that group. - // Just delete everything related to it, not trying to add update message or send a left message. - if (!emptyGroupButKeepAsKicked) { + const us = UserUtils.getOurPubKeyStrFromCache(); + const allMembers = await MetaGroupWrapperActions.memberGetAll(groupPk); + const otherAdminsCount = allMembers + .filter(m => m.admin || m.promoted) + .filter(m => m.pubkeyHex !== us).length; + const weAreLastAdmin = otherAdminsCount === 0; + const infos = await MetaGroupWrapperActions.infoGet(groupPk); + const fromUserGroup = await UserGroupsWrapperActions.getGroup(groupPk); + if (!infos || !fromUserGroup || isEmpty(infos) || isEmpty(fromUserGroup)) { + throw new Error('deleteGroup: some required data not present'); + } + const { secretKey } = fromUserGroup; + + // check if we are the last admin + if (secretKey && !isEmpty(secretKey) && (weAreLastAdmin || forceDestroyForAllMembers)) { + const deleteAllMessagesSubRequest = deleteAllMessagesOnSwarm + ? new DeleteAllFromGroupMsgNodeSubRequest({ + groupPk, + secretKey, + }) + : null; + + // this marks the group info as deleted. We need to push those details + await MetaGroupWrapperActions.infoDestroy(groupPk); + const lastPushResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({ + groupPk, + revokeSubRequest: null, + unrevokeSubRequest: null, + supplementKeys: [], + deleteAllMessagesSubRequest, + }); + if (lastPushResult !== RunJobResult.Success) { + throw new Error(`Failed to destroyGroupDetails for pk ${ed25519Str(groupPk)}`); + } + } + + // this deletes the secretKey if we had it. If we need it for something, it has to be done before this call. + await UserGroupsWrapperActions.eraseGroup(groupPk); + + // we are on the emptyGroupButKeepAsKicked=false case, so we remove it all await this.removeGroupOrCommunityFromDBAndRedux(groupPk); } - if (!fromSyncMessage) { - await UserSync.queueNewJobIfNeeded(); - } + await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupPk); + // release the memory (and the current meta-dumps in memory for that group) + window.log.info(`freeing metagroup wrapper: ${ed25519Str(groupPk)}`); + await MetaGroupWrapperActions.free(groupPk); + // delete the dumps from the metagroup state only, not the details in the UserGroups wrapper itself. + await ConfigDumpData.deleteDumpFor(groupPk); + getSwarmPollingInstance().removePubkey(groupPk, 'deleteGroup'); + + window.inboxStore.dispatch(groupInfoActions.removeGroupDetailsFromSlice({ groupPk })); + await UserSync.queueNewJobIfNeeded(); } public async deleteCommunity(convoId: string, options: DeleteOptions) { diff --git a/ts/session/onions/onionPath.ts b/ts/session/onions/onionPath.ts index e22f2c5d6..4cfe09125 100644 --- a/ts/session/onions/onionPath.ts +++ b/ts/session/onions/onionPath.ts @@ -312,7 +312,7 @@ export async function testGuardNode(snode: Snode) { response = await insecureNodeFetch(url, fetchOptions); } catch (e) { if (e.type === 'request-timeout') { - window?.log?.warn('test :,', ed25519Str(snode.pubkey_ed25519)); + window?.log?.warn('testGuardNode request timedout for:', ed25519Str(snode.pubkey_ed25519)); } if (e.code === 'ENETUNREACH') { window?.log?.warn('no network on node,', snode); diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 464560b60..c327d5b65 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -16,6 +16,7 @@ import { } from '../apis/open_group_api/sogsv3/sogsV3SendMessage'; import { BuiltSnodeSubRequests, + DeleteAllFromGroupMsgNodeSubRequest, DeleteAllFromUserNodeSubRequest, DeleteHashesFromGroupNodeSubRequest, DeleteHashesFromUserNodeSubRequest, @@ -308,7 +309,8 @@ async function signSubRequests( p instanceof RetrieveGroupSubRequest || p instanceof UpdateExpiryOnNodeUserSubRequest || p instanceof UpdateExpiryOnNodeGroupSubRequest || - p instanceof GetExpiriesFromNodeSubRequest + p instanceof GetExpiriesFromNodeSubRequest || + p instanceof DeleteAllFromGroupMsgNodeSubRequest ) { return p.buildAndSignParameters(); } @@ -348,7 +350,11 @@ async function sendMessagesDataToSnode( messagesHashes: messagesToDelete, revokeSubRequest, unrevokeSubRequest, - }: WithMessagesHashes & WithRevokeSubRequest, + deleteAllMessagesSubRequest, + }: WithMessagesHashes & + WithRevokeSubRequest & { + deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null; + }, method: MethodBatchType ): Promise { if (!asssociatedWith) { @@ -375,6 +381,7 @@ async function sendMessagesDataToSnode( deleteHashesSubRequest, revokeSubRequest, unrevokeSubRequest, + deleteAllMessagesSubRequest, ]); const targetNode = await SnodePool.getNodeFromSwarmOrThrow(asssociatedWith); @@ -564,10 +571,12 @@ async function sendEncryptedDataToSnode({ messagesHashesToDelete, revokeSubRequest, unrevokeSubRequest, + deleteAllMessagesSubRequest, }: WithRevokeSubRequest & { storeRequests: Array; destination: GroupPubkeyType | PubkeyType; messagesHashesToDelete: Set | null; + deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null; }): Promise { try { const batchResults = await pRetry( @@ -579,6 +588,7 @@ async function sendEncryptedDataToSnode({ messagesHashes: [...(messagesHashesToDelete || [])], revokeSubRequest, unrevokeSubRequest, + deleteAllMessagesSubRequest, }, 'sequence' ); diff --git a/ts/session/utils/job_runners/jobs/GroupSyncJob.ts b/ts/session/utils/job_runners/jobs/GroupSyncJob.ts index 1cc7754b1..efc977d68 100644 --- a/ts/session/utils/job_runners/jobs/GroupSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/GroupSyncJob.ts @@ -7,6 +7,7 @@ import { assertUnreachable } from '../../../../types/sqlSharedTypes'; import { isSignInByLinking } from '../../../../util/storage'; import { MetaGroupWrapperActions } from '../../../../webworker/workers/browser/libsession_worker_interface'; import { + DeleteAllFromGroupMsgNodeSubRequest, StoreGroupConfigOrMessageSubRequest, StoreGroupExtraData, } from '../../../apis/snode_api/SnodeRequestTypes'; @@ -130,6 +131,7 @@ async function storeGroupUpdateMessages({ messagesHashesToDelete: null, revokeSubRequest: null, unrevokeSubRequest: null, + deleteAllMessagesSubRequest: null, }); const expectedReplyLength = updateMessagesRequests.length; // each of those messages are sent as a subrequest @@ -151,9 +153,11 @@ async function pushChangesToGroupSwarmIfNeeded({ unrevokeSubRequest, groupPk, supplementKeys, + deleteAllMessagesSubRequest, }: WithGroupPubkey & WithRevokeSubRequest & { supplementKeys: Array; + deleteAllMessagesSubRequest?: DeleteAllFromGroupMsgNodeSubRequest | null; }): Promise { // save the dumps to DB even before trying to push them, so at least we have an up to date dumps in the DB in case of crash, no network etc await LibSessionUtil.saveDumpsToDb(groupPk); @@ -161,7 +165,13 @@ async function pushChangesToGroupSwarmIfNeeded({ await LibSessionUtil.pendingChangesForGroup(groupPk); // If there are no pending changes then the job can just complete (next time something // is updated we want to try and run immediately so don't schedule another run in this case) - if (isEmpty(pendingConfigData) && !supplementKeys.length) { + if ( + isEmpty(pendingConfigData) && + !supplementKeys.length && + !revokeSubRequest && + !unrevokeSubRequest && + !deleteAllMessagesSubRequest + ) { return RunJobResult.Success; } @@ -233,14 +243,16 @@ async function pushChangesToGroupSwarmIfNeeded({ messagesHashesToDelete: allOldHashes, revokeSubRequest, unrevokeSubRequest, + deleteAllMessagesSubRequest, }); const expectedReplyLength = pendingConfigRequests.length + // each of those messages are sent as a subrequest keysEncryptedRequests.length + // each of those messages are sent as a subrequest - (allOldHashes.size ? 1 : 0) + // we are sending all hashes changes as a single request - (revokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single request - (unrevokeSubRequest ? 1 : 0); // we are sending all revoke updates as a single request + (allOldHashes.size ? 1 : 0) + // we are sending all hashes changes as a single subrequest + (revokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest + (unrevokeSubRequest ? 1 : 0) + // we are sending all revoke updates as a single subrequest + (deleteAllMessagesSubRequest ? 1 : 0); // a delete_all sub request is a single subrequest // we do a sequence call here. If we do not have the right expected number of results, consider it a failure if (!isArray(result) || result.length !== expectedReplyLength) { diff --git a/ts/state/ducks/metaGroups.ts b/ts/state/ducks/metaGroups.ts index 94e106444..ba978b05d 100644 --- a/ts/state/ducks/metaGroups.ts +++ b/ts/state/ducks/metaGroups.ts @@ -38,7 +38,6 @@ import { GroupSync } from '../../session/utils/job_runners/jobs/GroupSyncJob'; import { UserSync } from '../../session/utils/job_runners/jobs/UserSyncJob'; import { RunJobResult } from '../../session/utils/job_runners/PersistedJob'; import { LibSessionUtil } from '../../session/utils/libsession/libsession_utils'; -import { SessionUtilConvoInfoVolatile } from '../../session/utils/libsession/libsession_utils_convo_info_volatile'; import { getUserED25519KeyPairBytes } from '../../session/utils/User'; import { stringify, toFixedUint8ArrayOfLength } from '../../types/sqlSharedTypes'; import { @@ -52,7 +51,6 @@ import { import { StateType } from '../reducer'; import { openConversationWithMessages } from './conversations'; import { resetLeftOverlayMode } from './section'; -import { ed25519Str } from '../../session/utils/String'; type WithFromMemberLeftMessage = { fromMemberLeftMessage: boolean }; // there are some changes we want to skip when doing changes triggered from a memberLeft message. export type GroupState = { @@ -185,6 +183,7 @@ const initNewGroupInWrapper = createAsyncThunk( revokeSubRequest: null, unrevokeSubRequest: null, supplementKeys: [], + deleteAllMessagesSubRequest: null, }); if (result !== RunJobResult.Success) { window.log.warn('GroupSync.pushChangesToGroupSwarmIfNeeded during create failed'); @@ -247,10 +246,12 @@ const initNewGroupInWrapper = createAsyncThunk( await MetaGroupWrapperActions.infoDestroy(groupPk); const foundConvo = ConvoHub.use().get(groupPk); if (foundConvo) { - await ConvoHub.use().deleteClosedGroup(groupPk, { + await ConvoHub.use().deleteGroup(groupPk, { fromSyncMessage: false, sendLeaveMessage: false, emptyGroupButKeepAsKicked: false, + deleteAllMessagesOnSwarm: false, + forceDestroyForAllMembers: false, }); } throw e; @@ -392,7 +393,6 @@ const loadMetaDumpsFromDB = createAsyncThunk( /** * This action is to be called when we get a merge event from the network. * It refreshes the state of that particular group (info & members) with the state from the wrapper after the merge is done. - * */ const refreshGroupDetailsFromWrapper = createAsyncThunk( 'group/refreshGroupDetailsFromWrapper', @@ -415,69 +415,6 @@ const refreshGroupDetailsFromWrapper = createAsyncThunk( } ); -const destroyGroupDetails = createAsyncThunk( - 'group/destroyGroupDetails', - async ({ groupPk }: { groupPk: GroupPubkeyType }) => { - const us = UserUtils.getOurPubKeyStrFromCache(); - const weAreAdmin = await checkWeAreAdmin(groupPk); - const allMembers = await MetaGroupWrapperActions.memberGetAll(groupPk); - const otherAdminsCount = allMembers - .filter(m => m.admin || m.promoted) - .filter(m => m.pubkeyHex !== us).length; - - // we are the last admin promoted - if (weAreAdmin && otherAdminsCount === 0) { - // this marks the group info as deleted. We need to push those details - await MetaGroupWrapperActions.infoDestroy(groupPk); - const lastPushResult = await GroupSync.pushChangesToGroupSwarmIfNeeded({ - groupPk, - revokeSubRequest: null, - unrevokeSubRequest: null, - supplementKeys: [], - }); - if (lastPushResult !== RunJobResult.Success) { - throw new Error(`Failed to destroyGroupDetails for pk ${ed25519Str(groupPk)}`); - } - } - - // this deletes the secretKey if we had it. If we need it for something, it has to be done before this call. - await UserGroupsWrapperActions.eraseGroup(groupPk); - - await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupPk); - await ConfigDumpData.deleteDumpFor(groupPk); - - getSwarmPollingInstance().removePubkey(groupPk, 'destroyGroupDetails'); - - return { groupPk }; - } -); - -const emptyGroupButKeepAsKicked = createAsyncThunk( - 'group/emptyGroupButKeepAsKicked', - async ({ groupPk }: { groupPk: GroupPubkeyType }) => { - window.log.info(`emptyGroupButKeepAsKicked for ${ed25519Str(groupPk)}`); - getSwarmPollingInstance().removePubkey(groupPk, 'emptyGroupButKeepAsKicked'); - - // this deletes the secretKey if we had it. If we need it for something, it has to be done before this call. - const group = await UserGroupsWrapperActions.getGroup(groupPk); - if (group) { - group.authData = null; - group.secretKey = null; - group.disappearingTimerSeconds = undefined; - group.kicked = true; - - await UserGroupsWrapperActions.setGroup(group); - } - await SessionUtilConvoInfoVolatile.removeGroupFromWrapper(groupPk); - // release the memory (and the current meta-dumps in memory for that group) - await MetaGroupWrapperActions.free(groupPk); - // this deletes the dumps from the metagroup state only, not the details in the UserGroups wrapper itself. - await ConfigDumpData.deleteDumpFor(groupPk); - - return { groupPk }; - } -); - function validateMemberAddChange({ groupPk, withHistory: addMembersWithHistory, @@ -747,6 +684,7 @@ async function handleMemberAddedFromUI({ groupPk, supplementKeys, ...revokeUnrevokeParams, + deleteAllMessagesSubRequest: null, }); if (sequenceResult !== RunJobResult.Success) { throw new Error( @@ -868,6 +806,7 @@ async function handleMemberRemovedFromUI({ supplementKeys: [], revokeSubRequest: null, unrevokeSubRequest: null, + deleteAllMessagesSubRequest: null, }); if (sequenceResult !== RunJobResult.Success) { throw new Error( @@ -979,6 +918,7 @@ async function handleNameChangeFromUI({ supplementKeys: [], revokeSubRequest: null, unrevokeSubRequest: null, + deleteAllMessagesSubRequest: null, }); if (batchResult !== RunJobResult.Success) { @@ -1260,6 +1200,15 @@ const metaGroupSlice = createSlice({ ) { return applySendingStateChange({ changeType: 'promote', ...payload, state }); }, + removeGroupDetailsFromSlice( + state: GroupState, + { payload }: PayloadAction<{ groupPk: GroupPubkeyType }> + ) { + delete state.infos[payload.groupPk]; + delete state.members[payload.groupPk]; + delete state.membersInviteSending[payload.groupPk]; + delete state.membersPromoteSending[payload.groupPk]; + }, }, extraReducers: builder => { builder.addCase(initNewGroupInWrapper.fulfilled, (state, action) => { @@ -1316,22 +1265,7 @@ const metaGroupSlice = createSlice({ builder.addCase(refreshGroupDetailsFromWrapper.rejected, (_state, action) => { window.log.error('a refreshGroupDetailsFromWrapper was rejected', action.error); }); - builder.addCase(destroyGroupDetails.fulfilled, (state, action) => { - const { groupPk } = action.payload; - window.log.info(`removed 03 from metagroup wrapper ${ed25519Str(groupPk)}`); - deleteGroupPkEntriesFromState(state, groupPk); - }); - builder.addCase(destroyGroupDetails.rejected, (_state, action) => { - window.log.error('a destroyGroupDetails was rejected', action.error); - }); - builder.addCase(emptyGroupButKeepAsKicked.fulfilled, (state, action) => { - const { groupPk } = action.payload; - window.log.info(`markedAsKicked 03 from metagroup wrapper ${ed25519Str(groupPk)}`); - deleteGroupPkEntriesFromState(state, groupPk); - }); - builder.addCase(emptyGroupButKeepAsKicked.rejected, (_state, action) => { - window.log.error('a emptyGroupButKeepAsKicked was rejected', action.error); - }); + builder.addCase(handleUserGroupUpdate.fulfilled, (state, action) => { const { infos, members, groupPk } = action.payload; if (infos && members) { @@ -1437,8 +1371,6 @@ const metaGroupSlice = createSlice({ export const groupInfoActions = { initNewGroupInWrapper, loadMetaDumpsFromDB, - destroyGroupDetails, - emptyGroupButKeepAsKicked, refreshGroupDetailsFromWrapper, handleUserGroupUpdate, currentDeviceGroupMembersChange, diff --git a/ts/state/selectors/selectedConversation.ts b/ts/state/selectors/selectedConversation.ts index b8abcb0a8..a8c4b6b8b 100644 --- a/ts/state/selectors/selectedConversation.ts +++ b/ts/state/selectors/selectedConversation.ts @@ -402,7 +402,7 @@ export function useSelectedNicknameOrProfileNameOrShortenedPubkey() { return window.i18n('noteToSelf'); } if (selectedId && PubKey.is03Pubkey(selectedId)) { - return libGroupName; + return libGroupName || profileName || shortenedPubkey; } return nickname || profileName || shortenedPubkey; } diff --git a/ts/types/LocalizerKeys.ts b/ts/types/LocalizerKeys.ts index c3c802364..369aa218e 100644 --- a/ts/types/LocalizerKeys.ts +++ b/ts/types/LocalizerKeys.ts @@ -608,6 +608,7 @@ export type LocalizerKeys = | 'youLeftTheGroup' | 'youSetYourDisappearingMessages' | 'youWereInvitedToGroup' + | 'youWereRemovedFrom' | 'yourSessionID' | 'yourUniqueSessionID' | 'zoomFactorSettingTitle'; diff --git a/ts/types/sqlSharedTypes.ts b/ts/types/sqlSharedTypes.ts index 1257dd71e..36b8a7c6c 100644 --- a/ts/types/sqlSharedTypes.ts +++ b/ts/types/sqlSharedTypes.ts @@ -268,61 +268,6 @@ export function getLegacyGroupInfoFromDBValues({ return legacyGroup; } -/** - * This function should only be used to update the libsession fields of a 03-group. - * Most of the fields tracked in the usergroup wrapper in libsession are actually not updated - * once the entry is created, but some of them needs to be updated. - */ -export function getGroupInfoFromDBValues({ - id, - priority, - members: maybeMembers, - displayNameInProfile, - expirationMode, - expireTimer, - encPubkeyHex, - encSeckeyHex, - groupAdmins: maybeAdmins, - lastJoinedTimestamp, -}: { - id: string; - priority: number; - displayNameInProfile: string | undefined; - expirationMode: DisappearingMessageConversationModeType | undefined; - expireTimer: number | undefined; - encPubkeyHex: string; - encSeckeyHex: string; - members: string | Array; - groupAdmins: string | Array; - lastJoinedTimestamp: number; -}) { - const admins: Array = maybeArrayJSONtoArray(maybeAdmins); - const members: Array = maybeArrayJSONtoArray(maybeMembers); - - const wrappedMembers: Array = (members || []).map(m => { - return { - isAdmin: admins.includes(m), - pubkeyHex: m, - }; - }); - - const legacyGroup: LegacyGroupInfo = { - pubkeyHex: id, - name: displayNameInProfile || '', - priority: priority || 0, - members: wrappedMembers, - disappearingTimerSeconds: - expirationMode && expirationMode !== 'off' && !!expireTimer && expireTimer > 0 - ? expireTimer - : 0, - encPubkey: !isEmpty(encPubkeyHex) ? from_hex(encPubkeyHex) : new Uint8Array(), - encSeckey: !isEmpty(encSeckeyHex) ? from_hex(encSeckeyHex) : new Uint8Array(), - joinedAtSeconds: Math.floor(lastJoinedTimestamp / 1000), - }; - - return legacyGroup; -} - /** * This function can be used to make sure all the possible values as input of a switch as taken care off, without having a default case. *