diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 8ad33c94b..ba1669fcb 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -927,7 +927,33 @@ extension ConversationVC: dismissOnConfirm: false // Custom dismissal logic ) { [weak self] _ in dependencies.storage.writeAsync { db in - try messageDisappearingConfig.save(db) + let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies) + let currentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() + + let interactionId = try messageDisappearingConfig + .saved(db) + .insertControlMessage( + db, + threadVariant: cellViewModel.threadVariant, + authorId: userPublicKey, + timestampMs: currentTimestampMs, + serverHash: nil, + serverExpirationTimestamp: nil + ) + + let expirationTimerUpdateMessage: ExpirationTimerUpdate = ExpirationTimerUpdate() + .with(sentTimestamp: UInt64(currentTimestampMs)) + .with(messageDisappearingConfig) + + try MessageSender.send( + db, + message: expirationTimerUpdateMessage, + interactionId: interactionId, + threadId: cellViewModel.threadId, + threadVariant: cellViewModel.threadVariant, + using: dependencies + ) + try LibSession .update( db, diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index e5085535e..61ffd19f8 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -535,9 +535,8 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi dependencies.storage.write { db in try SessionThread.deleteOrLeave( db, + type: .leaveGroupAsync, threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - groupLeaveType: .standard, calledFromConfigHandling: false ) } diff --git a/Session/Home/Message Requests/MessageRequestsViewModel.swift b/Session/Home/Message Requests/MessageRequestsViewModel.swift index c1c41af59..407bf0ebf 100644 --- a/Session/Home/Message Requests/MessageRequestsViewModel.swift +++ b/Session/Home/Message Requests/MessageRequestsViewModel.swift @@ -204,22 +204,20 @@ class MessageRequestsViewModel: SessionTableViewModel, NavigatableStateHolder, O // Remove the one-to-one requests try SessionThread.deleteOrLeave( db, + type: .hideContactConversationAndDeleteContent, threadIds: threadInfo .filter { _, variant in variant == .contact } .map { id, _ in id }, - threadVariant: .contact, - groupLeaveType: .silent, calledFromConfigHandling: false ) // Remove the group requests try SessionThread.deleteOrLeave( db, + type: .deleteGroupAndContent, threadIds: threadInfo .filter { _, variant in variant == .legacyGroup || variant == .group } .map { id, _ in id }, - threadVariant: .group, - groupLeaveType: .silent, calledFromConfigHandling: false ) } diff --git a/Session/Home/New Conversation/NewMessageScreen.swift b/Session/Home/New Conversation/NewMessageScreen.swift index 8dca8308c..d4d5db363 100644 --- a/Session/Home/New Conversation/NewMessageScreen.swift +++ b/Session/Home/New Conversation/NewMessageScreen.swift @@ -148,7 +148,7 @@ struct EnterAccountIdScreen: View { ) ) { ZStack { - Text("\("messageNewDescriptionMobile".localized())\(Image(systemName: "questionmark.circle"))") + (Text("messageNewDescriptionMobile".localized()) + Text(Image(systemName: "questionmark.circle"))) .font(.system(size: Values.verySmallFontSize)) .foregroundColor(themeColor: .textSecondary) .multilineTextAlignment(.center) diff --git a/Session/Utilities/UIContextualAction+Utilities.swift b/Session/Utilities/UIContextualAction+Utilities.swift index aa3a8ee40..eb46ad1d3 100644 --- a/Session/Utilities/UIContextualAction+Utilities.swift +++ b/Session/Utilities/UIContextualAction+Utilities.swift @@ -140,9 +140,8 @@ public extension UIContextualAction { Storage.shared.writeAsync { db in try SessionThread.deleteOrLeave( db, + type: .hideContactConversationAndDeleteContent, threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - groupLeaveType: .silent, calledFromConfigHandling: false ) } @@ -187,9 +186,8 @@ public extension UIContextualAction { Storage.shared.writeAsync { db in try SessionThread.deleteOrLeave( db, + type: .hideContactConversationAndDeleteContent, threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - groupLeaveType: .silent, calledFromConfigHandling: false ) } @@ -345,9 +343,8 @@ public extension UIContextualAction { if threadIsMessageRequest { try SessionThread.deleteOrLeave( db, + type: .hideContactConversationAndDeleteContent, threadId: threadViewModel.threadId, - threadVariant: .contact, - groupLeaveType: .silent, calledFromConfigHandling: false ) } @@ -429,13 +426,19 @@ public extension UIContextualAction { cancelStyle: .alert_text, dismissOnConfirm: true, onConfirm: { _ in + let deletionType: SessionThread.DeletionType = { + switch threadViewModel.threadVariant { + case .legacyGroup, .group: return .leaveGroupAsync + default: return .deleteCommunityAndContent + } + }() + Storage.shared.writeAsync { db in do { try SessionThread.deleteOrLeave( db, + type: deletionType, threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - groupLeaveType: .standard, calledFromConfigHandling: false ) } catch { @@ -532,12 +535,23 @@ public extension UIContextualAction { cancelStyle: .alert_text, dismissOnConfirm: true, onConfirm: { _ in + let deletionType: SessionThread.DeletionType = { + switch (threadViewModel.threadVariant, isMessageRequest) { + case (.community, _): return .deleteCommunityAndContent + case (.group, true): return .deleteGroupAndContent + case (.group, _), (.legacyGroup, _): + return .leaveGroupAsync + + case (.contact, _): + return .hideContactConversationAndDeleteContent + } + }() + Storage.shared.writeAsync { db in try SessionThread.deleteOrLeave( db, + type: deletionType, threadId: threadViewModel.threadId, - threadVariant: threadViewModel.threadVariant, - groupLeaveType: (isMessageRequest ? .silent : .forced), calledFromConfigHandling: false ) } diff --git a/SessionMessagingKit/Database/Models/ClosedGroup.swift b/SessionMessagingKit/Database/Models/ClosedGroup.swift index 902fe1868..046d612a0 100644 --- a/SessionMessagingKit/Database/Models/ClosedGroup.swift +++ b/SessionMessagingKit/Database/Models/ClosedGroup.swift @@ -111,12 +111,6 @@ public extension ClosedGroup { // MARK: - Convenience public extension ClosedGroup { - enum LeaveType { - case standard - case silent - case forced - } - static func removeKeysAndUnsubscribe( _ db: Database? = nil, threadId: String, diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index c5b70f6d8..9dbe11538 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -276,35 +276,44 @@ public extension SessionThread { ) """) } +} + +// MARK: - Deletion + +public extension SessionThread { + enum DeletionType { + case hideContactConversationAndDeleteContent + case deleteContactConversationAndContact + case leaveGroupAsync + case deleteGroupAndContent + case deleteCommunityAndContent + } static func deleteOrLeave( _ db: Database, + type: SessionThread.DeletionType, threadId: String, - threadVariant: Variant, - groupLeaveType: ClosedGroup.LeaveType, calledFromConfigHandling: Bool ) throws { try deleteOrLeave( db, + type: type, threadIds: [threadId], - threadVariant: threadVariant, - groupLeaveType: groupLeaveType, calledFromConfigHandling: calledFromConfigHandling ) } static func deleteOrLeave( _ db: Database, + type: SessionThread.DeletionType, threadIds: [String], - threadVariant: Variant, - groupLeaveType: ClosedGroup.LeaveType, calledFromConfigHandling: Bool ) throws { let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) let remainingThreadIds: Set = threadIds.asSet().removing(currentUserPublicKey) - switch (threadVariant, groupLeaveType) { - case (.contact, .standard), (.contact, .silent): + switch type { + case .hideContactConversationAndDeleteContent: // Clear any interactions for the deleted thread _ = try Interaction .filter(threadIds.contains(Interaction.Columns.threadId)) @@ -334,28 +343,26 @@ public extension SessionThread { SessionThread.Columns.shouldBeVisible.set(to: false) ) - case (.contact, .forced): + case .deleteContactConversationAndContact: // If this wasn't called from config handling then we need to hide the conversation if !calledFromConfigHandling { - try LibSession - .remove(db, contactIds: Array(remainingThreadIds)) + try LibSession.remove(db, contactIds: Array(remainingThreadIds)) } _ = try SessionThread .filter(ids: remainingThreadIds) .deleteAll(db) - case (.legacyGroup, .standard), (.group, .standard): + case .leaveGroupAsync: try threadIds.forEach { threadId in - try MessageSender - .leave( - db, - groupPublicKey: threadId, - deleteThread: true - ) + try MessageSender.leave( + db, + groupPublicKey: threadId, + deleteThread: true + ) } - case (.legacyGroup, .silent), (.legacyGroup, .forced), (.group, .forced), (.group, .silent): + case .deleteGroupAndContent: try ClosedGroup.removeKeysAndUnsubscribe( db, threadIds: threadIds, @@ -363,7 +370,7 @@ public extension SessionThread { calledFromConfigHandling: calledFromConfigHandling ) - case (.community, _): + case .deleteCommunityAndContent: threadIds.forEach { threadId in OpenGroupManager.shared.delete( db, @@ -530,8 +537,8 @@ public extension SessionThread { profile: Profile? = nil ) -> String { switch variant { - case .legacyGroup, .group: return (closedGroupName ?? "Unknown Group") - case .community: return (openGroupName ?? "Unknown Community") + case .legacyGroup, .group: return (closedGroupName ?? "groupUnknown".localized()) + case .community: return (openGroupName ?? "communityUnknown".localized()) case .contact: guard !isNoteToSelf else { return "noteToSelf".localized() } guard let profile: Profile = profile else { diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index 6ce1f5e33..20793679d 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -248,9 +248,8 @@ internal extension LibSession { try SessionThread .deleteOrLeave( db, + type: .deleteContactConversationAndContact, threadIds: combinedIds, - threadVariant: .contact, - groupLeaveType: .forced, calledFromConfigHandling: true ) diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index e377f5387..d058e057f 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -177,14 +177,12 @@ internal extension LibSession { if !communityIdsToRemove.isEmpty { LibSession.kickFromConversationUIIfNeeded(removedThreadIds: Array(communityIdsToRemove)) - try SessionThread - .deleteOrLeave( - db, - threadIds: Array(communityIdsToRemove), - threadVariant: .community, - groupLeaveType: .forced, - calledFromConfigHandling: true - ) + try SessionThread.deleteOrLeave( + db, + type: .deleteCommunityAndContent, + threadIds: Array(communityIdsToRemove), + calledFromConfigHandling: true + ) } // MARK: -- Handle Legacy Group Changes @@ -370,9 +368,8 @@ internal extension LibSession { try SessionThread .deleteOrLeave( db, + type: .deleteGroupAndContent, threadIds: Array(legacyGroupIdsToRemove), - threadVariant: .legacyGroup, - groupLeaveType: .forced, calledFromConfigHandling: true ) } diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index 17b550a08..44b1f7d23 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -105,14 +105,12 @@ internal extension LibSession { // `deleteOrLeave` behaviour (for 'Note to Self' this will leave the conversation // but remove the associated interactions) if !LibSession.shouldBeVisible(priority: targetPriority) { - try SessionThread - .deleteOrLeave( - db, - threadId: userPublicKey, - threadVariant: .contact, - groupLeaveType: .silent, - calledFromConfigHandling: true - ) + try SessionThread.deleteOrLeave( + db, + type: .hideContactConversationAndDeleteContent, + threadId: userPublicKey, + calledFromConfigHandling: true + ) } } diff --git a/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift b/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift index 2f08780a0..69a5277da 100644 --- a/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift +++ b/SessionMessagingKit/Messages/Control Messages/ExpirationTimerUpdate.swift @@ -5,7 +5,37 @@ import GRDB import SessionUtilitiesKit public final class ExpirationTimerUpdate: ControlMessage { + private enum CodingKeys: String, CodingKey { + case syncTarget + } + + public var syncTarget: String? + public override var isSelfSendValid: Bool { true } + + public init(syncTarget: String? = nil) { + super.init() + + self.syncTarget = syncTarget + } + + // MARK: - Codable + + required init(from decoder: Decoder) throws { + try super.init(from: decoder) + + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + syncTarget = try? container.decode(String.self, forKey: .syncTarget) + } + + public override func encode(to encoder: Encoder) throws { + try super.encode(to: encoder) + + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encodeIfPresent(syncTarget, forKey: .syncTarget) + } // MARK: - Proto Conversion @@ -15,12 +45,15 @@ public final class ExpirationTimerUpdate: ControlMessage { let isExpirationTimerUpdate = (dataMessageProto.flags & UInt32(SNProtoDataMessage.SNProtoDataMessageFlags.expirationTimerUpdate.rawValue)) != 0 guard isExpirationTimerUpdate else { return nil } - return ExpirationTimerUpdate() + return ExpirationTimerUpdate( + syncTarget: dataMessageProto.syncTarget + ) } public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? { let dataMessageProto = SNProtoDataMessage.builder() dataMessageProto.setFlags(UInt32(SNProtoDataMessage.SNProtoDataMessageFlags.expirationTimerUpdate.rawValue)) + if let syncTarget = syncTarget { dataMessageProto.setSyncTarget(syncTarget) } let contentProto = SNProtoContent.builder() // DisappearingMessagesConfiguration @@ -39,7 +72,9 @@ public final class ExpirationTimerUpdate: ControlMessage { public var description: String { """ - ExpirationTimerUpdate() + ExpirationTimerUpdate( + syncTarget: \(syncTarget ?? "null"), + ) """ } } diff --git a/SessionMessagingKit/Messages/Message.swift b/SessionMessagingKit/Messages/Message.swift index 966366c47..53dd89546 100644 --- a/SessionMessagingKit/Messages/Message.swift +++ b/SessionMessagingKit/Messages/Message.swift @@ -269,6 +269,7 @@ public extension Message { static func shouldSync(message: Message) -> Bool { switch message { case is VisibleMessage: return true + case is ExpirationTimerUpdate: return true case is UnsendRequest: return true case let controlMessage as ClosedGroupControlMessage: @@ -295,6 +296,7 @@ public extension Message { switch message { case let message as VisibleMessage: maybeSyncTarget = message.syncTarget + case let message as ExpirationTimerUpdate: maybeSyncTarget = message.syncTarget default: maybeSyncTarget = nil } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 3bf1494d8..0b2aadae4 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -796,7 +796,12 @@ public final class OpenGroupManager { let syncTarget: String = (lookup.sessionId ?? message.recipient) switch messageInfo.variant { - case .visibleMessage: (messageInfo.message as? VisibleMessage)?.syncTarget = syncTarget + case .visibleMessage: + (messageInfo.message as? VisibleMessage)?.syncTarget = syncTarget + + case .expirationTimerUpdate: + (messageInfo.message as? ExpirationTimerUpdate)?.syncTarget = syncTarget + default: break } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index 8d51eddb9..ca9527618 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -104,9 +104,8 @@ extension MessageReceiver { _ = try SessionThread .deleteOrLeave( db, + type: .deleteContactConversationAndContact, // Blinded contact isn't synced anyway threadId: blindedIdLookup.blindedId, - threadVariant: .contact, - groupLeaveType: .forced, calledFromConfigHandling: false ) } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index bec01f071..6b699fc8f 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -108,6 +108,7 @@ public enum MessageReceiver { threadIdGenerator = { message in switch message { case let message as VisibleMessage: return (message.syncTarget ?? sender) + case let message as ExpirationTimerUpdate: return (message.syncTarget ?? sender) default: return sender } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index fb25ca0c9..67100fb46 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -1117,6 +1117,7 @@ public final class MessageSender { Message.shouldSync(message: message) { if let message = message as? VisibleMessage { message.syncTarget = publicKey } + if let message = message as? ExpirationTimerUpdate { message.syncTarget = publicKey } dependencies.jobRunner.add( db,