mirror of
https://github.com/lux-tel/session-ios.git
synced 2026-10-30 20:38:32 +00:00
The `SessionThreadViewModel` has been renamed to `ConversationInfoViewModel` and is now a simple struct (as opposed to a FetchableRecord with a handful of behemoth database queries) The `ConversationInfoViewModel` and `MessageViewModel` now get initialised with a `ConversationDataCache` which is an object that holds a bunch of [ID -> Data] stores for various types that are needed to construct the aforementioned view models. The `ConversationDataCache` will generally be populated via the `ConversationDataHelper` which, when combined with our `ObservableEvent` system, is where the majority of the work for this refactoring has gone. This helper essentially has 4 parts: - `determineFetchRequirements`: Reviews any events which have come through and determines whether anything needs to be fetched from the database - `applyNonDatabaseEvents`: Updates the cached data directly based on delta information included in the `ObservableEvent` instances - `fetchFromDatabase`: Fetches any data from the database based on the info returned from `determineFetchRequirements`, this function will also resolve any dependant data that newly fetched objects need if they aren't already in the cache - `fetchFromLibSession`: Retrieves requested data from libSession Using the above setup all screens which contain conversation or message data now use these two view models, and can share the state of the view models between each other to reduce the need to refetch data when entering a new screen Some additional changes included due to the above are: • No longer need to pass `displayNameRetriever` throughout • Removed deprecated display name retrieval functions • Removed some duplicate code • Removed some synchronous database query hacks we needed until refactoring had been completed • Refactored mentions to be handled by tags • Refactored and optimised the searching logic (also added some caching) • Refactored the ThreadSettingsViewModel to use the new ObservationBuilder system • Refactored the ThreadPickerViewModel to use the new ObservationBuilder system • Refactored the HomeViewModel to retrieve data using the `ConversationDataHelper` • Refactored the ConversationViewModel to retrieve data using the `ConversationDataHelper` • Fixed an issue where communities weren't getting the correct "joined" timestamp
210 lines
9.0 KiB
Swift
210 lines
9.0 KiB
Swift
// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
|
|
//
|
|
// stringlint:disable
|
|
|
|
import GRDB
|
|
import SessionUIKit
|
|
import SessionUtilitiesKit
|
|
|
|
public extension MentionSelectionView.ViewModel {
|
|
static func mentions(
|
|
profiles: [Profile],
|
|
threadVariant: SessionThread.Variant,
|
|
currentUserSessionIds: Set<String>,
|
|
adminModIds: [String],
|
|
using dependencies: Dependencies
|
|
) -> [MentionSelectionView.ViewModel] {
|
|
return profiles.compactMap { profile -> MentionSelectionView.ViewModel? in
|
|
guard let info: ProfilePictureView.Info = ProfilePictureView.Info.generateInfoFrom(
|
|
size: MentionSelectionView.profilePictureViewSize,
|
|
publicKey: profile.id,
|
|
threadVariant: .contact, /// Always show the display picture in 'contact' mode
|
|
displayPictureUrl: nil,
|
|
profile: profile,
|
|
profileIcon: (adminModIds.contains(profile.id) ? .crown : .none),
|
|
using: dependencies
|
|
).front else { return nil }
|
|
|
|
return MentionSelectionView.ViewModel(
|
|
profileId: profile.id,
|
|
displayName: profile.displayName(
|
|
showYouForCurrentUser: true,
|
|
currentUserSessionIds: currentUserSessionIds,
|
|
includeSessionIdSuffix: (threadVariant == .community)
|
|
),
|
|
profilePictureInfo: info
|
|
)
|
|
}
|
|
}
|
|
|
|
static func mentions(
|
|
for query: String = "",
|
|
threadId: String,
|
|
threadVariant: SessionThread.Variant,
|
|
currentUserSessionIds: Set<String>,
|
|
communityInfo: (server: String, roomToken: String)?,
|
|
using dependencies: Dependencies
|
|
) async throws -> [MentionSelectionView.ViewModel] {
|
|
let profiles: [Profile] = try await dependencies[singleton: .storage].readAsync { db in
|
|
let pattern: FTS5Pattern? = try? GlobalSearch.pattern(db, searchTerm: query, forTable: Profile.self)
|
|
let targetPrefixes: [SessionId.Prefix] = {
|
|
switch threadVariant {
|
|
case .contact, .legacyGroup, .group: return [.standard]
|
|
case .community:
|
|
let capabilities: Set<Capability.Variant> = ((try? Capability
|
|
.select(.variant)
|
|
.filter(Capability.Columns.openGroupServer == communityInfo?.server)
|
|
.asRequest(of: Capability.Variant.self)
|
|
.fetchSet(db)) ?? [])
|
|
|
|
guard capabilities.contains(.blind) else {
|
|
return [.standard]
|
|
}
|
|
|
|
return [.blinded15, .blinded25]
|
|
}
|
|
}()
|
|
|
|
return try mentionsQuery(
|
|
threadId: threadId,
|
|
threadVariant: threadVariant,
|
|
targetPrefixes: targetPrefixes,
|
|
currentUserSessionIds: currentUserSessionIds,
|
|
pattern: pattern
|
|
).fetchAll(db)
|
|
}
|
|
|
|
let adminModIds: [String] = await {
|
|
switch (threadVariant, communityInfo) {
|
|
case (.contact, _), (.group, _), (.legacyGroup, _), (.community, .none): return []
|
|
case (.community, .some(let communityInfo)):
|
|
guard let server: CommunityManager.Server = await dependencies[singleton: .communityManager].server(communityInfo.server) else {
|
|
return []
|
|
}
|
|
|
|
return (
|
|
(server.rooms[communityInfo.roomToken]?.admins ?? []) +
|
|
(server.rooms[communityInfo.roomToken]?.moderators ?? [])
|
|
)
|
|
}
|
|
}()
|
|
|
|
return mentions(
|
|
profiles: profiles,
|
|
threadVariant: threadVariant,
|
|
currentUserSessionIds: currentUserSessionIds,
|
|
adminModIds: adminModIds,
|
|
using: dependencies
|
|
)
|
|
}
|
|
|
|
// stringlint:ignore_contents
|
|
private static func mentionsQuery(
|
|
threadId: String,
|
|
threadVariant: SessionThread.Variant,
|
|
targetPrefixes: [SessionId.Prefix],
|
|
currentUserSessionIds: Set<String>,
|
|
pattern: FTS5Pattern?
|
|
) -> SQLRequest<Profile> {
|
|
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 { prefix in
|
|
SQL(
|
|
"""
|
|
(
|
|
\(profile[.id]) > '\(SQL(stringLiteral: "\(prefix.rawValue)"))' AND
|
|
\(profile[.id]) < '\(SQL(stringLiteral: "\(prefix.endOfRangeString)"))'
|
|
)
|
|
""")
|
|
}
|
|
.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 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("\(threadVariant) != \(SessionThread.Variant.community)")) OR
|
|
\(prefixesLiteral)
|
|
)
|
|
)
|
|
"""
|
|
}()
|
|
let targetWhere: SQL = {
|
|
guard let pattern: FTS5Pattern = pattern, pattern.rawPattern != "\"\"*" else {
|
|
return """
|
|
WHERE (
|
|
\(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).*
|
|
\(targetJoin)
|
|
\(targetWhere) AND (
|
|
\(SQL("\(profile[.id]) = \(threadId)")) OR
|
|
\(SQL("\(profile[.id]) IN \(currentUserSessionIds)"))
|
|
)
|
|
ORDER BY \(SQL("\(profile[.id]) IN \(currentUserSessionIds)")) DESC
|
|
""")
|
|
|
|
case .legacyGroup, .group:
|
|
return SQLRequest("""
|
|
SELECT \(Profile.self).*
|
|
\(targetJoin)
|
|
JOIN \(GroupMember.self) ON (
|
|
\(SQL("\(groupMember[.groupId]) = \(threadId)")) AND
|
|
\(groupMember[.profileId]) = \(profile[.id]) AND
|
|
\(SQL("\(groupMember[.role]) != \(GroupMember.Role.zombie)"))
|
|
)
|
|
\(targetWhere)
|
|
GROUP BY \(profile[.id])
|
|
ORDER BY
|
|
\(SQL("\(profile[.id]) IN \(currentUserSessionIds)")) DESC,
|
|
IFNULL(\(profile[.nickname]), \(profile[.name])) ASC
|
|
""")
|
|
|
|
case .community:
|
|
return SQLRequest("""
|
|
SELECT
|
|
\(Profile.self).*,
|
|
MAX(\(interaction[.timestampMs])) -- Want the newest interaction (for sorting)
|
|
|
|
\(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
|
|
\(SQL("\(profile[.id]) IN \(currentUserSessionIds)")) DESC,
|
|
\(interaction[.timestampMs].desc)
|
|
LIMIT 20
|
|
""")
|
|
}
|
|
}
|
|
}
|