diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 526082fbf..a2ce32596 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -764,6 +764,30 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi return UISwipeActionsConfiguration(actions: [ delete, mute, pin ]) case .openGroup, .closedGroup: + if threadViewModel.currentUserIsClosedGroupMember == false { + let delete: UIContextualAction = UIContextualAction( + title: "TXT_DELETE_TITLE".localized(), + icon: UIImage(named: "icon_bin")?.resizedImage(to: CGSize(width: Values.mediumFontSize, height: Values.mediumFontSize)), + iconHeight: Values.mediumFontSize, + themeTintColor: .white, + themeBackgroundColor: .conversationButton_swipeDestructive, + side: .trailing, + actionIndex: 2, + indexPath: indexPath, + tableView: tableView + ) { [weak self] _, _, completionHandler in + self?.viewModel.delete( + threadId: threadViewModel.threadId, + threadVariant: threadViewModel.threadVariant, + force: true + ) + + completionHandler(true) + } + + return UISwipeActionsConfiguration(actions: [ delete, mute, pin ]) + } + let leave: UIContextualAction = UIContextualAction( title: "LEAVE_BUTTON_TITLE".localized(), icon: UIImage(systemName: "rectangle.portrait.and.arrow.right"), diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 33aed908b..f515444f3 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -301,23 +301,42 @@ public class HomeViewModel { // MARK: - Functions - public func delete(threadId: String, threadVariant: SessionThread.Variant) { + public func delete(threadId: String, threadVariant: SessionThread.Variant, force: Bool = false) { + + func delete(_ db: Database, threadId: String) throws { + _ = try SessionThread + .filter(id: threadId) + .deleteAll(db) + } + Storage.shared.writeAsync { db in switch threadVariant { case .closedGroup: - try MessageSender.leave( - db, - groupPublicKey: threadId, - deleteThread: true - ) + if force { + if let thread: SessionThread = try? SessionThread.fetchOne(db, id: threadId), + let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) + { + try MessageSender.performClosedGroupCleanUp( + db, + for: closedGroup, + in: thread + ) + } + try delete(db, threadId: threadId) + } else { + try MessageSender.leave( + db, + groupPublicKey: threadId, + deleteThread: true + ) + } case .openGroup: OpenGroupManager.shared.delete(db, openGroupId: threadId) - _ = try SessionThread - .filter(id: threadId) - .deleteAll(db) + try delete(db, threadId: threadId) - default: break + default: + try delete(db, threadId: threadId) } } } diff --git a/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift b/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift index 5ebe5a9cd..bf6360a26 100644 --- a/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift +++ b/SessionMessagingKit/Jobs/Types/GroupLeavingJob.swift @@ -50,21 +50,8 @@ public enum GroupLeavingJob: JobExecutor { ) } .done(on: queue) { _ in - // Remove the group from the database and unsubscribe from PNs - ClosedGroupPoller.shared.stopPolling(for: details.groupPublicKey) - Storage.shared.writeAsync { db in - let userPublicKey: String = getUserHexEncodedPublicKey(db) - - try closedGroup - .keyPairs - .deleteAll(db) - - let _ = PushNotificationAPI.performOperation( - .unsubscribe, - for: details.groupPublicKey, - publicKey: userPublicKey - ) + try MessageSender.performClosedGroupCleanUp(db, for: closedGroup, in: thread) try Interaction .filter(id: interactionId) @@ -76,25 +63,6 @@ public enum GroupLeavingJob: JobExecutor { ] ) - // Update the group (if the admin leaves the group is disbanded) - let wasAdminUser: Bool = try GroupMember - .filter(GroupMember.Columns.groupId == thread.id) - .filter(GroupMember.Columns.profileId == userPublicKey) - .filter(GroupMember.Columns.role == GroupMember.Role.admin) - .isNotEmpty(db) - - if wasAdminUser { - try GroupMember - .filter(GroupMember.Columns.groupId == thread.id) - .deleteAll(db) - } - else { - try GroupMember - .filter(GroupMember.Columns.groupId == thread.id) - .filter(GroupMember.Columns.profileId == userPublicKey) - .deleteAll(db) - } - if details.deleteThread { _ = try SessionThread .filter(id: thread.id) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index 20f1c8574..415e99275 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -582,4 +582,40 @@ extension MessageSender { } catch {} } + + /// Remove the group from the database and unsubscribe from PNs + public static func performClosedGroupCleanUp(_ db: Database, for closedGroup: ClosedGroup, in thread: SessionThread) throws { + ClosedGroupPoller.shared.stopPolling(for: closedGroup.publicKey) + + let userPublicKey: String = getUserHexEncodedPublicKey(db) + + try closedGroup + .keyPairs + .deleteAll(db) + + let _ = PushNotificationAPI.performOperation( + .unsubscribe, + for: closedGroup.publicKey, + publicKey: userPublicKey + ) + + // Update the group (if the admin leaves the group is disbanded) + let wasAdminUser: Bool = try GroupMember + .filter(GroupMember.Columns.groupId == thread.id) + .filter(GroupMember.Columns.profileId == userPublicKey) + .filter(GroupMember.Columns.role == GroupMember.Role.admin) + .isNotEmpty(db) + + if wasAdminUser { + try GroupMember + .filter(GroupMember.Columns.groupId == thread.id) + .deleteAll(db) + } + else { + try GroupMember + .filter(GroupMember.Columns.groupId == thread.id) + .filter(GroupMember.Columns.profileId == userPublicKey) + .deleteAll(db) + } + } } diff --git a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift index 0b66cd78f..491d43747 100644 --- a/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift +++ b/SessionMessagingKit/Shared Models/SessionThreadViewModel.swift @@ -457,6 +457,9 @@ public extension SessionThreadViewModel { let interactionAttachmentAttachmentIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.attachmentId.name) let interactionAttachmentInteractionIdColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.interactionId.name) let interactionAttachmentAlbumIndexColumnLiteral: SQL = SQL(stringLiteral: InteractionAttachment.Columns.albumIndex.name) + let groupMemberProfileIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.profileId.name) + let groupMemberRoleColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.role.name) + let groupMemberGroupIdColumnLiteral: SQL = SQL(stringLiteral: GroupMember.Columns.groupId.name) /// **Note:** The `numColumnsBeforeProfiles` value **MUST** match the number of fields before /// the `ViewModel.contactProfileKey` entry below otherwise the query will fail to @@ -464,7 +467,7 @@ public extension SessionThreadViewModel { /// /// Explicitly set default values for the fields ignored for search results let numColumnsBeforeProfiles: Int = 12 - let numColumnsBetweenProfilesAndAttachmentInfo: Int = 11 // The attachment info columns will be combined + let numColumnsBetweenProfilesAndAttachmentInfo: Int = 12 // The attachment info columns will be combined let request: SQLRequest = """ SELECT @@ -488,7 +491,8 @@ public extension SessionThreadViewModel { \(ViewModel.closedGroupProfileBackKey).*, \(ViewModel.closedGroupProfileBackFallbackKey).*, \(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey), - (\(groupMember[.profileId]) IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupAdminKey), + (\(ViewModel.currentUserIsClosedGroupMemberKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey), + (\(ViewModel.currentUserIsClosedGroupAdminKey).profileId IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupAdminKey), \(openGroup[.name]) AS \(ViewModel.openGroupNameKey), \(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey), @@ -563,10 +567,15 @@ public extension SessionThreadViewModel { LEFT JOIN \(Profile.self) AS \(ViewModel.contactProfileKey) ON \(ViewModel.contactProfileKey).\(profileIdColumnLiteral) = \(thread[.id]) LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(thread[.id]) LEFT JOIN \(ClosedGroup.self) ON \(closedGroup[.threadId]) = \(thread[.id]) - LEFT JOIN \(GroupMember.self) ON ( - \(SQL("\(groupMember[.role]) = \(GroupMember.Role.admin)")) AND - \(groupMember[.groupId]) = \(closedGroup[.threadId]) AND - \(SQL("\(groupMember[.profileId]) = \(userPublicKey)")) + LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupMemberKey) ON ( + \(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberRoleColumnLiteral) != \(GroupMember.Role.zombie)")) AND + \(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND + \(SQL("\(ViewModel.currentUserIsClosedGroupMemberKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)")) + ) + LEFT JOIN \(GroupMember.self) AS \(ViewModel.currentUserIsClosedGroupAdminKey) ON ( + \(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberRoleColumnLiteral) = \(GroupMember.Role.admin)")) AND + \(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberGroupIdColumnLiteral) = \(closedGroup[.threadId]) AND + \(SQL("\(ViewModel.currentUserIsClosedGroupAdminKey).\(groupMemberProfileIdColumnLiteral) = \(userPublicKey)")) ) LEFT JOIN \(Profile.self) AS \(ViewModel.closedGroupProfileFrontKey) ON (