Files
Morgan Pretty ac9816b118 Massive refactors of the SessionThreadViewModel and MessageViewModel
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
2025-12-11 16:47:37 +11:00

168 lines
5.9 KiB
Swift

// Copyright © 2025 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public struct EventHandlingStrategy: OptionSet, Hashable {
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
public static let none: EventHandlingStrategy = []
public static let databaseQuery: EventHandlingStrategy = EventHandlingStrategy(rawValue: 1 << 0)
public static let libSessionQuery: EventHandlingStrategy = EventHandlingStrategy(rawValue: 1 << 1)
public static let directCacheUpdate: EventHandlingStrategy = EventHandlingStrategy(rawValue: 1 << 2)
}
public struct EventChangeset {
public static let empty: EventChangeset = EventChangeset(eventsByKey: [:], eventsByStrategy: [:])
private let eventsByKey: [GenericObservableKey: [ObservedEvent]]
private let eventsByStrategy: [EventHandlingStrategy: Set<ObservedEvent>]
fileprivate init(
eventsByKey: [GenericObservableKey: [ObservedEvent]],
eventsByStrategy: [EventHandlingStrategy: Set<ObservedEvent>]
) {
self.eventsByKey = eventsByKey
self.eventsByStrategy = eventsByStrategy
}
// MARK: - Generic Event Accessors
public func events(matching strategy: EventHandlingStrategy) -> Set<ObservedEvent> {
var result: Set<ObservedEvent> = []
eventsByStrategy.forEach { key, events in
if key.contains(strategy) {
result.formUnion(events)
}
}
return result
}
public var databaseEvents: Set<ObservedEvent> {
return events(matching: .databaseQuery)
}
public var libSessionEvents: Set<ObservedEvent> {
return events(matching: .libSessionQuery)
}
/// Checks if any event matches the generic key
public func containsGeneric(_ key: GenericObservableKey) -> Bool {
return containsAnyGeneric(key)
}
public func containsAnyGeneric(_ keys: GenericObservableKey...) -> Bool {
return !Set(eventsByKey.keys).isDisjoint(with: Set(keys))
}
/// Returns the most recent value for a specific key, cast to T
public func latestGeneric<T>(_ key: GenericObservableKey, as type: T.Type = T.self) -> T? {
return eventsByKey[key]?.last?.value as? T /// The `last` event should be the newest
}
/// Returns the most recent value for a specific key, cast to T that matches the condition
public func latestGeneric<T>(_ key: GenericObservableKey, as type: T.Type = T.self, where condition: (T) -> Bool) -> T? {
return eventsByKey[key]?
.reversed() /// The `last` event should be the newest so iterate backwards
.first(where: {
guard let value: T = $0.value as? T else { return false }
return condition(value)
}) as? T
}
/// Iterates over all events matching the key, casting them to T
public func forEach<T>(
_ key: GenericObservableKey,
as type: T.Type = T.self,
_ body: (T) -> Void
) {
eventsByKey[key]?.forEach { event in
if let value = event.value as? T {
body(value)
}
}
}
/// Iterates over events matching the key, providing the full event (useful if you need the specific key ID)
public func forEachEvent<T>(
_ key: GenericObservableKey,
as valueType: T.Type = T.self,
_ body: (ObservedEvent, T) -> Void
) {
eventsByKey[key]?.forEach { event in
if let value = event.value as? T {
body(event, value)
}
}
}
// MARK: - Explicit Event Accessors
/// Checks if any event matches the generic key
public func contains(_ key: ObservableKey) -> Bool {
return containsAny(key)
}
public func containsAny(_ keys: ObservableKey...) -> Bool {
return keys.contains { key in
eventsByKey[key.generic]?.first(where: { $0.key == key }) != nil
}
}
/// Returns the most recent value for a specific key, cast to T
public func latest<T>(_ key: ObservableKey, as type: T.Type = T.self) -> T? {
return eventsByKey[key.generic]?
.reversed() /// The `last` event should be the newest
.first(where: { $0.key == key })?
.value as? T
}
/// Returns the most recent value for a specific key, cast to T that matches the condition
public func latest<T>(_ key: ObservableKey, as type: T.Type = T.self, where condition: (T) -> Bool) -> T? {
return eventsByKey[key.generic]?
.reversed() /// The `last` event should be the newest so iterate backwards
.first(where: {
guard
$0.key == key,
let value: T = $0.value as? T
else { return false }
return condition(value)
}) as? T
}
}
public extension Collection where Element == ObservedEvent {
func split() -> EventChangeset {
var allEvents: [GenericObservableKey: [ObservedEvent]] = [:]
for event in self {
allEvents[event.key.generic, default: []].append(event)
}
return EventChangeset(eventsByKey: allEvents, eventsByStrategy: [:])
}
func split(
by classifier: (ObservedEvent) -> EventHandlingStrategy
) -> EventChangeset {
var allEvents: [GenericObservableKey: [ObservedEvent]] = [:]
var eventsByStrategy: [EventHandlingStrategy: Set<ObservedEvent>] = [:]
for event in self {
allEvents[event.key.generic, default: []].append(event)
let strategy: EventHandlingStrategy = classifier(event)
eventsByStrategy[strategy, default: []].insert(event)
}
return EventChangeset(eventsByKey: allEvents, eventsByStrategy: eventsByStrategy)
}
}