|  |  |  | // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import GRDB | 
					
						
							|  |  |  | import SessionUtilitiesKit | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | public struct MentionInfo: FetchableRecord, Decodable, ColumnExpressible { | 
					
						
							|  |  |  |     public typealias Columns = CodingKeys | 
					
						
							|  |  |  |     public enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { | 
					
						
							|  |  |  |         case profile | 
					
						
							|  |  |  |         case threadVariant | 
					
						
							|  |  |  |         case openGroupServer | 
					
						
							|  |  |  |         case openGroupRoomToken | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |      | 
					
						
							|  |  |  |     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, | 
					
						
							|  |  |  |         targetPrefixes: [SessionId.Prefix], | 
					
						
							|  |  |  |         pattern: FTS5Pattern? | 
					
						
							|  |  |  |     ) -> AdaptedFetchRequest<SQLRequest<MentionInfo>>? { | 
					
						
							|  |  |  |         guard threadVariant != .contact || userPublicKey != threadId else { return nil } | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let profile: TypedTableAlias<Profile> = TypedTableAlias() | 
					
						
							|  |  |  |         let interaction: TypedTableAlias<Interaction> = TypedTableAlias() | 
					
						
							|  |  |  |         let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias() | 
					
						
							|  |  |  |         let groupMember: TypedTableAlias<GroupMember> = TypedTableAlias() | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         let prefixesLiteral: SQLExpression = targetPrefixes | 
					
						
							|  |  |  |             .map { SQL("\(profile[.id]) LIKE '\(SQL(stringLiteral: "\($0.rawValue)%"))'") } | 
					
						
							|  |  |  |             .joined(operator: .or) | 
					
						
							|  |  |  |         let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName) | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         /// The query needs to differ depending on the thread variant because the behaviour should be different: | 
					
						
							|  |  |  |         /// | 
					
						
							|  |  |  |         /// **Contact:** We should show the profile directly (filtered out if the pattern doesn't match) | 
					
						
							|  |  |  |         /// **Group:** We should show all profiles within the group, filtered by the pattern | 
					
						
							|  |  |  |         /// **Community:** We should show only the 20 most recent profiles which match the pattern | 
					
						
							|  |  |  |         let request: SQLRequest<MentionInfo> = { | 
					
						
							|  |  |  |             let hasValidPattern: Bool = (pattern != nil && pattern?.rawPattern != "\"\"*") | 
					
						
							|  |  |  |             let targetJoin: SQL = { | 
					
						
							|  |  |  |                 guard hasValidPattern else { return "FROM \(Profile.self)" } | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |                 return """
 | 
					
						
							|  |  |  |                     FROM \(profileFullTextSearch) | 
					
						
							|  |  |  |                     JOIN \(Profile.self) ON ( | 
					
						
							|  |  |  |                         \(Profile.self).rowid = \(profileFullTextSearch).rowid AND | 
					
						
							|  |  |  |                         \(SQL("\(profile[.id]) != \(userPublicKey)")) AND ( | 
					
						
							|  |  |  |                             \(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR | 
					
						
							|  |  |  |                             \(prefixesLiteral) | 
					
						
							|  |  |  |                         ) | 
					
						
							|  |  |  |                     ) | 
					
						
							|  |  |  |                 """
 | 
					
						
							|  |  |  |             }() | 
					
						
							|  |  |  |             let targetWhere: SQL = { | 
					
						
							|  |  |  |                 guard let pattern: FTS5Pattern = pattern, pattern.rawPattern != "\"\"*" else { | 
					
						
							|  |  |  |                     return """
 | 
					
						
							|  |  |  |                         WHERE ( | 
					
						
							|  |  |  |                             \(SQL("\(profile[.id]) != \(userPublicKey)")) AND ( | 
					
						
							|  |  |  |                                 \(SQL("\(threadVariant) != \(SessionThread.Variant.community)")) OR | 
					
						
							|  |  |  |                                 \(prefixesLiteral) | 
					
						
							|  |  |  |                             ) | 
					
						
							|  |  |  |                         ) | 
					
						
							|  |  |  |                     """
 | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |                 let matchLiteral: SQL = SQL(stringLiteral: "\(Profile.Columns.nickname.name):\(pattern.rawPattern) OR \(Profile.Columns.name.name):\(pattern.rawPattern)") | 
					
						
							|  |  |  |                  | 
					
						
							|  |  |  |                 return "WHERE \(profileFullTextSearch) MATCH '\(matchLiteral)'" | 
					
						
							|  |  |  |             }() | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             switch threadVariant { | 
					
						
							|  |  |  |                 case .contact: | 
					
						
							|  |  |  |                     return SQLRequest("""
 | 
					
						
							|  |  |  |                         SELECT | 
					
						
							|  |  |  |                             \(Profile.self).*, | 
					
						
							|  |  |  |                             \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")) | 
					
						
							|  |  |  |                      | 
					
						
							|  |  |  |                         \(targetJoin) | 
					
						
							|  |  |  |                         \(targetWhere) AND \(SQL("\(profile[.id]) = \(threadId)")) | 
					
						
							|  |  |  |                     """)
 | 
					
						
							|  |  |  |                      | 
					
						
							|  |  |  |                 case .legacyGroup, .group: | 
					
						
							|  |  |  |                     return SQLRequest("""
 | 
					
						
							|  |  |  |                         SELECT | 
					
						
							|  |  |  |                             \(Profile.self).*, | 
					
						
							|  |  |  |                             \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")) | 
					
						
							|  |  |  |                      | 
					
						
							|  |  |  |                         \(targetJoin) | 
					
						
							|  |  |  |                         JOIN \(GroupMember.self) ON ( | 
					
						
							|  |  |  |                             \(SQL("\(groupMember[.groupId]) = \(threadId)")) AND | 
					
						
							|  |  |  |                             \(groupMember[.profileId]) = \(profile[.id]) AND | 
					
						
							|  |  |  |                             \(SQL("\(groupMember[.role]) = \(GroupMember.Role.standard)")) | 
					
						
							|  |  |  |                         ) | 
					
						
							|  |  |  |                         \(targetWhere) | 
					
						
							|  |  |  |                         GROUP BY \(profile[.id]) | 
					
						
							|  |  |  |                         ORDER BY IFNULL(\(profile[.nickname]), \(profile[.name])) ASC | 
					
						
							|  |  |  |                     """)
 | 
					
						
							|  |  |  |                      | 
					
						
							|  |  |  |                 case .community: | 
					
						
							|  |  |  |                     return SQLRequest("""
 | 
					
						
							|  |  |  |                         SELECT | 
					
						
							|  |  |  |                             \(Profile.self).*, | 
					
						
							|  |  |  |                             MAX(\(interaction[.timestampMs])),  -- Want the newest interaction (for sorting) | 
					
						
							|  |  |  |                             \(SQL("\(threadVariant) AS \(MentionInfo.Columns.threadVariant)")), | 
					
						
							|  |  |  |                             \(openGroup[.server]) AS \(MentionInfo.Columns.openGroupServer), | 
					
						
							|  |  |  |                             \(openGroup[.roomToken]) AS \(MentionInfo.Columns.openGroupRoomToken) | 
					
						
							|  |  |  |                      | 
					
						
							|  |  |  |                         \(targetJoin) | 
					
						
							|  |  |  |                         JOIN \(Interaction.self) ON ( | 
					
						
							|  |  |  |                             \(SQL("\(interaction[.threadId]) = \(threadId)")) AND | 
					
						
							|  |  |  |                             \(interaction[.authorId]) = \(profile[.id]) | 
					
						
							|  |  |  |                         ) | 
					
						
							|  |  |  |                         JOIN \(OpenGroup.self) ON \(SQL("\(openGroup[.threadId]) = \(threadId)")) | 
					
						
							|  |  |  |                         \(targetWhere) | 
					
						
							|  |  |  |                         GROUP BY \(profile[.id]) | 
					
						
							|  |  |  |                         ORDER BY \(interaction[.timestampMs].desc) | 
					
						
							|  |  |  |                         LIMIT 20 | 
					
						
							|  |  |  |                     """)
 | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         }() | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         return request.adapted { db in | 
					
						
							|  |  |  |             let adapters = try splittingRowAdapters(columnCounts: [ | 
					
						
							|  |  |  |                 Profile.numberOfSelectedColumns(db) | 
					
						
							|  |  |  |             ]) | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             return ScopeAdapter.with(MentionInfo.self, [ | 
					
						
							|  |  |  |                 .profile: adapters[0] | 
					
						
							|  |  |  |             ]) | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |