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
pull/612/head
Morgan Pretty 3 years ago
parent 0db74ce1e3
commit 333849c32e

@ -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<Profile> = 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)

@ -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];

@ -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

@ -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<GroupMember> = TypedTableAlias()
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
let unreadInteractions: TableAlias = TableAlias()
let unreadMentions: TableAlias = TableAlias()
let lastInteraction: TableAlias = TableAlias()
let lastInteractionThread: TypedTableAlias<SessionThread> = TypedTableAlias()
let linkPreview: TypedTableAlias<LinkPreview> = 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

@ -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)

@ -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) } :

@ -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,

@ -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<String> = [], excludeCurrentUser: Bool = true) -> [Profile] {
return GRDBStorage.shared
.read { db in
let idsToExclude: Set<String> = 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)

@ -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" }
return displayName
.defaulting(to: "Anonymous", useDefaultIfEmpty: true)
guard !isNoteToSelf else { return "NOTE_TO_SELF".localized() }
guard let profile: Profile = profile else {
return Profile.truncated(id: threadId, truncating: .middle)
}
case .closedGroup, .openGroup:
return displayName
.defaulting(to: "Group", useDefaultIfEmpty: true)
return profile.displayName()
}
}
}

@ -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<String> = Set(groupMembers.map { $0.profileId }).union(addedMembers)
try addedMembers.forEach { memberId in
try GroupMember(
groupId: id,
profileId: memberId,
role: .standard
).save(db)
}
let currentMemberIds: Set<String> = groupMembers.map { $0.profileId }.asSet()
let members: Set<String> = currentMemberIds.union(addedMembers)
// 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

@ -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)

@ -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)
}

@ -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
)
)
}

Loading…
Cancel
Save