// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import GRDB import SessionUtilitiesKit public struct MentionInfo: FetchableRecord, Decodable { fileprivate static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue) fileprivate static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue) fileprivate static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue) fileprivate static let profileString: String = CodingKeys.profile.stringValue public let profile: Profile public let threadVariant: SessionThread.Variant public let openGroupServer: String? public let openGroupRoomToken: String? } public extension MentionInfo { static func query( userPublicKey: String, threadId: String, threadVariant: SessionThread.Variant, targetPrefix: SessionId.Prefix, pattern: FTS5Pattern? ) -> AdaptedFetchRequest>? { guard threadVariant != .contact || userPublicKey != threadId else { return nil } let profile: TypedTableAlias = TypedTableAlias() let interaction: TypedTableAlias = TypedTableAlias() let openGroup: TypedTableAlias = TypedTableAlias() let prefixLiteral: SQL = SQL(stringLiteral: "\(targetPrefix.rawValue)%") let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName) /// **Note:** The `\(MentionInfo.profileKey).*` value **MUST** be first let limitSQL: SQL? = (threadVariant == .openGroup ? SQL("LIMIT 20") : nil) let request: SQLRequest = { guard let pattern: FTS5Pattern = pattern else { let finalLimitSQL: SQL = (limitSQL ?? "") return """ SELECT \(Profile.self).*, MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")), \(openGroup[.server]) AS \(MentionInfo.openGroupServerKey), \(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey) FROM \(Profile.self) JOIN \(Interaction.self) ON ( \(SQL("\(interaction[.threadId]) = \(threadId)")) AND \(interaction[.authorId]) = \(profile[.id]) ) LEFT JOIN \(OpenGroup.self) ON \(SQL("\(openGroup[.threadId]) = \(threadId)")) WHERE ( \(SQL("\(profile[.id]) != \(userPublicKey)")) AND ( \(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR \(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'")) ) ) GROUP BY \(profile[.id]) ORDER BY \(interaction[.timestampMs].desc) \(finalLimitSQL) """ } // If we do have a search patern then use FTS let matchLiteral: SQL = SQL(stringLiteral: "\(Profile.Columns.nickname.name):\(pattern.rawPattern) OR \(Profile.Columns.name.name):\(pattern.rawPattern)") let finalLimitSQL: SQL = (limitSQL ?? "") return """ SELECT \(Profile.self).*, MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting) \(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")), \(openGroup[.server]) AS \(MentionInfo.openGroupServerKey), \(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey) FROM \(profileFullTextSearch) JOIN \(Profile.self) ON ( \(Profile.self).rowid = \(profileFullTextSearch).rowid AND \(SQL("\(profile[.id]) != \(userPublicKey)")) AND ( \(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR \(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'")) ) ) JOIN \(Interaction.self) ON ( \(SQL("\(interaction[.threadId]) = \(threadId)")) AND \(interaction[.authorId]) = \(profile[.id]) ) LEFT JOIN \(OpenGroup.self) ON \(SQL("\(openGroup[.threadId]) = \(threadId)")) WHERE \(profileFullTextSearch) MATCH '\(matchLiteral)' GROUP BY \(profile[.id]) ORDER BY \(interaction[.timestampMs].desc) \(finalLimitSQL) """ }() return request.adapted { db in let adapters = try splittingRowAdapters(columnCounts: [ Profile.numberOfSelectedColumns(db) ]) return ScopeAdapter([ MentionInfo.profileString: adapters[0] ]) } } }