From 333849c32ed5935ad494601ef423292885220926 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 9 May 2022 14:45:14 +1000 Subject: [PATCH] Logic for interactions with user mentions and closed group tweaks Added logic to flag interactions that mention the current user Fixed up closed group member handling --- Session/Closed Groups/EditClosedGroupVC.swift | 18 ++++-- .../OWSConversationSettingsViewController.m | 7 +- Session/Home/HomeVC.swift | 4 +- Session/Home/HomeViewModel.swift | 34 +++++++++- .../_001_InitialSetupMigration.swift | 4 ++ .../Migrations/_003_YDBToGRDBMigration.swift | 4 ++ .../Database/Models/Interaction.swift | 11 ++++ .../Database/Models/Profile.swift | 8 ++- .../Database/Models/SessionThread.swift | 44 +++++-------- .../MessageReceiver+Handling.swift | 64 +++++++++++++------ .../MessageSender+Convenience.swift | 2 + .../Pollers/ClosedGroupPoller.swift | 9 ++- .../NSENotificationPresenter.swift | 11 ++-- 13 files changed, 148 insertions(+), 72 deletions(-) diff --git a/Session/Closed Groups/EditClosedGroupVC.swift b/Session/Closed Groups/EditClosedGroupVC.swift index 56b7da6b4..a6c2e0731 100644 --- a/Session/Closed Groups/EditClosedGroupVC.swift +++ b/Session/Closed Groups/EditClosedGroupVC.swift @@ -130,6 +130,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat setUpViewHierarchy() updateNavigationBarButtons() + handleMembersChanged() } private func setUpViewHierarchy() { @@ -230,6 +231,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat let removeAction = UITableViewRowAction(style: .destructive, title: "Remove") { [weak self] _, _ in self?.adminIds.remove(profileId) self?.membersAndZombies.remove(at: indexPath.row) + self?.handleMembersChanged() } removeAction.backgroundColor = Colors.destructive @@ -320,13 +322,16 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat .asSet() ) { [weak self] selectedUserIds in GRDBStorage.shared.read { [weak self] db in - let profileAlias: TypedTableAlias = TypedTableAlias() - let selectedGroupMembers: [GroupMemberDisplayInfo] = try GroupMember - .filter(selectedUserIds.contains(GroupMember.Columns.profileId)) - .including(optional: GroupMember.profile.aliased(profileAlias)) - .asRequest(of: GroupMemberDisplayInfo.self) + let selectedGroupMembers: [GroupMemberDisplayInfo] = try Profile + .filter(selectedUserIds.contains(Profile.Columns.id)) .fetchAll(db) - + .map { profile in + GroupMemberDisplayInfo( + profileId: profile.id, + role: .standard, + profile: profile + ) + } self?.membersAndZombies = (self?.membersAndZombies ?? []) .appending(contentsOf: selectedGroupMembers) .sorted(by: { lhs, rhs in @@ -368,6 +373,7 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat self?.addMembersButton.isUserInteractionEnabled = (self?.hasContactsToAdd == true) self?.addMembersButton.layer.borderColor = color.cgColor self?.addMembersButton.setTitleColor(color, for: UIControl.State.normal) + self?.handleMembersChanged() } navigationController?.pushViewController(userSelectionVC, animated: true, completion: nil) diff --git a/Session/Conversations/Settings/OWSConversationSettingsViewController.m b/Session/Conversations/Settings/OWSConversationSettingsViewController.m index 0d7749665..d4d21a049 100644 --- a/Session/Conversations/Settings/OWSConversationSettingsViewController.m +++ b/Session/Conversations/Settings/OWSConversationSettingsViewController.m @@ -766,11 +766,8 @@ CGFloat kIconViewLength = 24; - (void)leaveGroup { - TSGroupThread *gThread = (TSGroupThread *)self.thread; - - if (gThread.isClosedGroup) { - NSString *groupPublicKey = [LKGroupUtilities getDecodedGroupID:gThread.groupModel.groupId]; - [[SNMessageSender leaveClosedGroupWithPublicKey:groupPublicKey] retainUntilComplete]; + if (self.isClosedGroup) { + [[SNMessageSender leaveClosedGroupWithPublicKey:self.threadId] retainUntilComplete]; } [self.navigationController popViewControllerAnimated:YES]; diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index 8a1724e6f..ff816bf36 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -48,8 +48,8 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, NewConve right: 0 ) result.showsVerticalScrollIndicator = false - result.register(MessageRequestsCell.self, forCellReuseIdentifier: MessageRequestsCell.reuseIdentifier) - result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier) + result.register(view: MessageRequestsCell.self) + result.register(view: ConversationCell.self) result.dataSource = self result.delegate = self diff --git a/Session/Home/HomeViewModel.swift b/Session/Home/HomeViewModel.swift index 5ddf938ca..3fc9969e2 100644 --- a/Session/Home/HomeViewModel.swift +++ b/Session/Home/HomeViewModel.swift @@ -83,6 +83,7 @@ public class HomeViewModel { fileprivate static let isNoteToSelfKey = CodingKeys.isNoteToSelf.stringValue fileprivate static let currentUserIsClosedGroupAdminKey = CodingKeys.currentUserIsClosedGroupAdmin.stringValue fileprivate static let threadUnreadCountKey = CodingKeys.threadUnreadCount.stringValue + fileprivate static let threadUnreadMentionCountKey = CodingKeys.threadUnreadMentionCount.stringValue fileprivate static let lastInteractionInfoKey = CodingKeys.lastInteractionInfo.stringValue public var differenceIdentifier: String { id } @@ -109,7 +110,7 @@ public class HomeViewModel { private let currentUserIsClosedGroupAdmin: Bool? private let threadUnreadCount: UInt? - public let unreadMentionCount: UInt = 0 // TODO: This + private let threadUnreadMentionCount: UInt? public let lastInteractionInfo: InteractionInfo? @@ -167,6 +168,10 @@ public class HomeViewModel { return (threadUnreadCount ?? 0) } + public var unreadMentionCount: UInt { + return (threadUnreadMentionCount ?? 0) + } + fileprivate init() { self.id = "FALLBACK" self.variant = .contact @@ -184,6 +189,7 @@ public class HomeViewModel { self.isNoteToSelf = false self.currentUserIsClosedGroupAdmin = nil self.threadUnreadCount = nil + self.threadUnreadMentionCount = nil self.lastInteractionInfo = nil } @@ -196,6 +202,7 @@ public class HomeViewModel { let closedGroupMember: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() let unreadInteractions: TableAlias = TableAlias() + let unreadMentions: TableAlias = TableAlias() let lastInteraction: TableAlias = TableAlias() let lastInteractionThread: TypedTableAlias = TypedTableAlias() let linkPreview: TypedTableAlias = TypedTableAlias() @@ -214,6 +221,17 @@ public class HomeViewModel { .filter(Interaction.Columns.wasRead == false) .group(Interaction.Columns.threadId) ) + let unreadMentionsExpression: CommonTableExpression = CommonTableExpression( + named: ThreadInfo.threadUnreadMentionCountKey, + request: Interaction + .select( + count(Interaction.Columns.id).forKey(ThreadInfo.threadUnreadMentionCountKey), + Interaction.Columns.threadId + ) + .filter(Interaction.Columns.wasRead == false) + .filter(Interaction.Columns.hasMention == true) + .group(Interaction.Columns.threadId) + ) let lastInteractionExpression: CommonTableExpression = CommonTableExpression( named: ThreadInfo.lastInteractionInfoKey, request: Interaction @@ -261,7 +279,8 @@ public class HomeViewModel { SessionThread.isNoteToSelf(userPublicKey: userPublicKey).forKey(ThreadInfo.isNoteToSelfKey), (closedGroupMember[.profileId] != nil).forKey(ThreadInfo.currentUserIsClosedGroupAdminKey), - unreadInteractions[ThreadInfo.threadUnreadCountKey] + unreadInteractions[ThreadInfo.threadUnreadCountKey], + unreadMentions[ThreadInfo.threadUnreadMentionCountKey] ) .aliased(thread) .joining( @@ -308,6 +327,17 @@ public class HomeViewModel { ) .aliased(unreadInteractions) ) + .with(unreadMentionsExpression) + .joining( + optional: SessionThread + .association( + to: unreadMentionsExpression, + on: { thread, unreadMentions in + thread[SessionThread.Columns.id] == unreadMentions[Interaction.Columns.threadId] + } + ) + .aliased(unreadMentions) + ) .with(lastInteractionExpression) .including( optional: SessionThread diff --git a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift index 85da33703..532622989 100644 --- a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -153,6 +153,10 @@ enum _001_InitialSetupMigration: Migration { .notNull() .indexed() // Quicker querying .defaults(to: false) + t.column(.hasMention, .boolean) + .notNull() + .indexed() // Quicker querying + .defaults(to: false) t.column(.expiresInSeconds, .double) t.column(.expiresStartedAtMs, .double) t.column(.linkPreviewUrl, .text) diff --git a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift index 6ae96c531..d830e6982 100644 --- a/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift +++ b/SessionMessagingKit/Database/Migrations/_003_YDBToGRDBMigration.swift @@ -586,6 +586,10 @@ enum _003_YDBToGRDBMigration: Migration { timestampMs: Int64(legacyInteraction.timestamp), receivedAtTimestampMs: Int64(legacyInteraction.receivedAtTimestamp), wasRead: wasRead, + hasMention: ( + body?.contains("@\(currentUserPublicKey)") == true || + quotedMessage?.authorId == currentUserPublicKey + ), // For both of these '0' used to be equivalent to null expiresInSeconds: ((expiresInSeconds ?? 0) > 0 ? expiresInSeconds.map { TimeInterval($0) } : diff --git a/SessionMessagingKit/Database/Models/Interaction.swift b/SessionMessagingKit/Database/Models/Interaction.swift index 0f9371eea..74555c658 100644 --- a/SessionMessagingKit/Database/Models/Interaction.swift +++ b/SessionMessagingKit/Database/Models/Interaction.swift @@ -47,6 +47,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu case timestampMs case receivedAtTimestampMs case wasRead + case hasMention case expiresInSeconds case expiresStartedAtMs @@ -136,6 +137,9 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu /// **Note:** This flag is not applicable to standardOutgoing or standardIncomingDeleted interactions public let wasRead: Bool + /// A flag indicating whether the current user was mentioned in this interaction (or the associated quote) + public let hasMention: Bool + /// The number of seconds until this message should expire public let expiresInSeconds: TimeInterval? @@ -208,6 +212,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu timestampMs: Int64, receivedAtTimestampMs: Int64, wasRead: Bool, + hasMention: Bool, expiresInSeconds: TimeInterval?, expiresStartedAtMs: Double?, linkPreviewUrl: String?, @@ -224,6 +229,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu self.timestampMs = timestampMs self.receivedAtTimestampMs = receivedAtTimestampMs self.wasRead = wasRead + self.hasMention = hasMention self.expiresInSeconds = expiresInSeconds self.expiresStartedAtMs = expiresStartedAtMs self.linkPreviewUrl = linkPreviewUrl @@ -240,6 +246,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu body: String? = nil, timestampMs: Int64 = 0, wasRead: Bool = false, + hasMention: Bool = false, expiresInSeconds: TimeInterval? = nil, expiresStartedAtMs: Double? = nil, linkPreviewUrl: String? = nil, @@ -262,6 +269,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu } }() self.wasRead = wasRead + self.hasMention = hasMention self.expiresInSeconds = expiresInSeconds self.expiresStartedAtMs = expiresStartedAtMs self.linkPreviewUrl = linkPreviewUrl @@ -367,6 +375,7 @@ public extension Interaction { authorId: String? = nil, timestampMs: Int64? = nil, wasRead: Bool? = nil, + hasMention: Bool? = nil, expiresInSeconds: TimeInterval? = nil, expiresStartedAtMs: Double? = nil, openGroupServerMessageId: Int64? = nil @@ -381,6 +390,7 @@ public extension Interaction { timestampMs: (timestampMs ?? self.timestampMs), receivedAtTimestampMs: receivedAtTimestampMs, wasRead: (wasRead ?? self.wasRead), + hasMention: (hasMention ?? self.hasMention), expiresInSeconds: (expiresInSeconds ?? self.expiresInSeconds), expiresStartedAtMs: (expiresStartedAtMs ?? self.expiresStartedAtMs), linkPreviewUrl: linkPreviewUrl, @@ -524,6 +534,7 @@ public extension Interaction { timestampMs: timestampMs, receivedAtTimestampMs: receivedAtTimestampMs, wasRead: wasRead, + hasMention: hasMention, expiresInSeconds: expiresInSeconds, expiresStartedAtMs: expiresStartedAtMs, linkPreviewUrl: linkPreviewUrl, diff --git a/SessionMessagingKit/Database/Models/Profile.swift b/SessionMessagingKit/Database/Models/Profile.swift index a7f97da8f..c3721383e 100644 --- a/SessionMessagingKit/Database/Models/Profile.swift +++ b/SessionMessagingKit/Database/Models/Profile.swift @@ -224,14 +224,18 @@ public extension Profile { // MARK: - GRDB Interactions public extension Profile { - static func fetchAllContactProfiles(excludeCurrentUser: Bool = true) -> [Profile] { + static func fetchAllContactProfiles(excluding: Set = [], excludeCurrentUser: Bool = true) -> [Profile] { return GRDBStorage.shared .read { db in + let idsToExclude: Set = excluding + .inserting(excludeCurrentUser ? getUserHexEncodedPublicKey(db) : nil) + // Sort the contacts by their displayName value return try Profile - .filter(Profile.Columns.id != (excludeCurrentUser ? "" : getUserHexEncodedPublicKey(db))) + .filter(!idsToExclude.contains(Profile.Columns.id)) .joining( required: Profile.contact + .filter(Contact.Columns.isApproved == true) .filter(Contact.Columns.didApproveMe == true) ) .fetchAll(db) diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index f411e030d..6dba259b7 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -262,38 +262,24 @@ public extension SessionThread { ) } - func name(_ db: Database) -> String { - switch variant { - case .contact: - guard !isNoteToSelf(db) else { return name(isNoteToSelf: true) } - - return name( - displayName: Profile.displayName( - db, - id: id, - customFallback: Profile.truncated(id: id, truncating: .middle) - ) - ) - - case .closedGroup: - return name(displayName: try? String.fetchOne(db, closedGroup.select(.name))) - - case .openGroup: - return name(displayName: try? String.fetchOne(db, openGroup.select(.name))) - } - } - - func name(isNoteToSelf: Bool = false, displayName: String? = nil) -> String { + static func displayName( + threadId: String, + variant: Variant, + closedGroupName: String? = nil, + openGroupName: String? = nil, + isNoteToSelf: Bool = false, + profile: Profile? = nil + ) -> String { switch variant { + case .closedGroup: return (closedGroupName ?? "Unknown Group") + case .openGroup: return (openGroupName ?? "Unknown Group") case .contact: - guard !isNoteToSelf else { return "Note to Self" } + guard !isNoteToSelf else { return "NOTE_TO_SELF".localized() } + guard let profile: Profile = profile else { + return Profile.truncated(id: threadId, truncating: .middle) + } - return displayName - .defaulting(to: "Anonymous", useDefaultIfEmpty: true) - - case .closedGroup, .openGroup: - return displayName - .defaulting(to: "Group", useDefaultIfEmpty: true) + return profile.displayName() } } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift index 5423294c6..1f6f9de5f 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver+Handling.swift @@ -459,17 +459,14 @@ extension MessageReceiver { throw MessageReceiverError.noThread } + // Store the message variant so we can run variant-specific behaviours + let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) let thread: SessionThread = try SessionThread .fetchOrCreate(db, id: threadInfo.id, variant: threadInfo.variant) - - // Store the message variant so we can run variant-specific behaviours - let variant: Interaction.Variant = { - if sender == getUserHexEncodedPublicKey(db) { - return .standardOutgoing - } - - return .standardIncoming - }() + let variant: Interaction.Variant = (sender == currentUserPublicKey ? + .standardOutgoing : + .standardIncoming + ) // Retrieve the disappearing messages config to set the 'expiresInSeconds' value // accoring to the config @@ -491,6 +488,10 @@ extension MessageReceiver { variant: variant, body: message.text, timestampMs: Int64(messageSentTimestamp * 1000), + hasMention: ( + message.text?.contains("@\(currentUserPublicKey)") == true || + dataMessage.quote?.author == currentUserPublicKey + ), // Note: Ensure we don't ever expire open group messages expiresInSeconds: (disappearingMessagesConfiguration.isEnabled && message.openGroupServerMessageId == nil ? disappearingMessagesConfiguration.durationSeconds : @@ -836,6 +837,23 @@ extension MessageReceiver { // Notify the user if !groupAlreadyExisted { + // Create the GroupMember records + try members.forEach { memberId in + try GroupMember( + groupId: groupPublicKey, + profileId: memberId, + role: .standard + ).save(db) + } + + try admins.forEach { adminId in + try GroupMember( + groupId: groupPublicKey, + profileId: adminId, + role: .admin + ).save(db) + } + // Note: We don't provide a `serverHash` in this case as we want to allow duplicates // to avoid the following situation: // • The app performed a background poll or received a push notification @@ -972,15 +990,19 @@ extension MessageReceiver { // Update the group let addedMembers: [String] = membersAsData.map { $0.toHexString() } - let members: Set = Set(groupMembers.map { $0.profileId }).union(addedMembers) + let currentMemberIds: Set = groupMembers.map { $0.profileId }.asSet() + let members: Set = currentMemberIds.union(addedMembers) - try addedMembers.forEach { memberId in - try GroupMember( - groupId: id, - profileId: memberId, - role: .standard - ).save(db) - } + // Create records for any new members + try addedMembers + .filter { !currentMemberIds.contains($0) } + .forEach { memberId in + try GroupMember( + groupId: id, + profileId: memberId, + role: .standard + ).insert(db) + } // Send the latest encryption key pair to the added members if the current user is // the admin of the group @@ -1148,11 +1170,17 @@ extension MessageReceiver { ) } else { + // Delete all old user roles and re-add them as a zombie + try closedGroup + .allMembers + .filter(GroupMember.Columns.profileId == sender) + .deleteAll(db) + try GroupMember( groupId: id, profileId: sender, role: .zombie - ).save(db) + ).insert(db) } // Update the group diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index de137163a..4fff3a2a0 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -22,6 +22,8 @@ extension MessageSender { } public static func send(_ db: Database, interaction: Interaction, in thread: SessionThread) throws { + // Only 'VisibleMessage' types can be sent via this method + guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage } guard let interactionId: Int64 = interaction.id else { throw GRDBStorageError.objectNotSaved } return try send(db, message: VisibleMessage.from(db, interaction: interaction), interactionId: interactionId, in: thread) diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift index 7c441a95e..a2b65c1f8 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift @@ -43,13 +43,16 @@ public final class ClosedGroupPoller: NSObject { assert(Thread.current.isMainThread) // Timers don't do well on background queues #endif - // Fetch all closed groups (excluding any which have no key pairs as the user is - // no longer a member of those + // Fetch all closed groups (excluding any don't contain the current user as a + // GroupMemeber as the user is no longer a member of those) GRDBStorage.shared .read { db in try ClosedGroup .select(.threadId) - .joining(required: ClosedGroup.keyPairs) + .joining( + required: ClosedGroup.members + .filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db)) + ) .asRequest(of: String.self) .fetchAll(db) } diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index 55fe20d65..88c964f9d 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -53,14 +53,15 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { return } - var groupName = thread.name(db) - if groupName.count < 1 { - groupName = MessageStrings.newGroupDefaultTitle - } notificationTitle = String( format: NotificationStrings.incomingGroupMessageTitleFormat, senderName, - groupName + SessionThread.displayName( + threadId: thread.id, + variant: thread.variant, + closedGroupName: (try? thread.closedGroup.fetchOne(db))?.name, + openGroupName: (try? thread.openGroup.fetchOne(db))?.name + ) ) }