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
201 lines
7.8 KiB
Swift
201 lines
7.8 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||
|
||
import Foundation
|
||
import GRDB
|
||
import SessionUtilitiesKit
|
||
|
||
/// This lookup is created when the user interacts with a blinded id
|
||
public struct BlindedIdLookup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||
public static var databaseTableName: String { "blindedIdLookup" }
|
||
|
||
public typealias Columns = CodingKeys
|
||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||
case blindedId
|
||
case sessionId
|
||
case openGroupServer
|
||
case openGroupPublicKey
|
||
}
|
||
|
||
public var id: String { blindedId }
|
||
|
||
/// The blinded id for the user on this open group server
|
||
public let blindedId: String
|
||
|
||
/// The standard sessionId which can be used to generate this blindedId on this open group server
|
||
///
|
||
/// **Note:** This value will be null if the user owning the blinded id hasn’t accepted the message request
|
||
public let sessionId: String?
|
||
|
||
/// The server for the Open Group server this blinded id belongs to
|
||
public let openGroupServer: String
|
||
|
||
/// The public key for the Open Group server this blinded id belongs to
|
||
public let openGroupPublicKey: String
|
||
|
||
// MARK: - Initialization
|
||
|
||
public init(
|
||
blindedId: String,
|
||
sessionId: String? = nil,
|
||
openGroupServer: String,
|
||
openGroupPublicKey: String
|
||
) {
|
||
self.blindedId = blindedId
|
||
self.sessionId = sessionId
|
||
self.openGroupServer = openGroupServer
|
||
self.openGroupPublicKey = openGroupPublicKey
|
||
}
|
||
}
|
||
|
||
// MARK: - Mutation
|
||
|
||
public extension BlindedIdLookup {
|
||
func with(sessionId: String) -> BlindedIdLookup {
|
||
return BlindedIdLookup(
|
||
blindedId: self.blindedId,
|
||
sessionId: sessionId,
|
||
openGroupServer: self.openGroupServer,
|
||
openGroupPublicKey: self.openGroupPublicKey
|
||
)
|
||
}
|
||
}
|
||
|
||
// MARK: - GRDB Interactions
|
||
|
||
public extension BlindedIdLookup {
|
||
/// Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard sessionId, as a result in order
|
||
/// to see if there is an unblinded contact for this blindedId we can only really generate blinded ids for each contact and check
|
||
/// if any match
|
||
///
|
||
/// If we can't find a match this method will still store a lookup, just with no standard sessionId value (this gives us a method to
|
||
/// link back to the open group the blindedId originated from)
|
||
static func fetchOrCreate(
|
||
_ db: ObservingDatabase,
|
||
blindedId: String,
|
||
sessionId: String? = nil,
|
||
openGroupServer: String,
|
||
openGroupPublicKey: String,
|
||
isCheckingForOutbox: Bool,
|
||
using dependencies: Dependencies
|
||
) throws -> BlindedIdLookup {
|
||
var lookup: BlindedIdLookup = (try? BlindedIdLookup
|
||
.fetchOne(db, id: blindedId))
|
||
.defaulting(
|
||
to: BlindedIdLookup(
|
||
blindedId: blindedId,
|
||
openGroupServer: openGroupServer.lowercased(),
|
||
openGroupPublicKey: openGroupPublicKey
|
||
)
|
||
)
|
||
|
||
// If the lookup already has a resolved sessionId then just return it immediately
|
||
guard lookup.sessionId == nil else { return lookup }
|
||
|
||
// If we we given a sessionId then validate it is correct and if so save it
|
||
if
|
||
let sessionId: String = sessionId,
|
||
dependencies[singleton: .crypto].verify(
|
||
.sessionId(
|
||
sessionId,
|
||
matchesBlindedId: blindedId,
|
||
serverPublicKey: openGroupPublicKey
|
||
)
|
||
)
|
||
{
|
||
lookup = try lookup
|
||
.with(sessionId: sessionId)
|
||
.upserted(db)
|
||
db.addContactEvent(
|
||
id: lookup.blindedId,
|
||
change: .unblinded(blindedId: lookup.blindedId, unblindedId: sessionId)
|
||
)
|
||
return lookup
|
||
}
|
||
|
||
// We now need to try to match the blinded id to an existing contact, this can only be done by looping
|
||
// through all approved contacts and generating a blinded id for the provided open group for each to
|
||
// see if it matches the provided blindedId
|
||
let contactsThatApprovedMeCursor: RecordCursor<Contact> = try Contact
|
||
.filter(Contact.Columns.didApproveMe == true)
|
||
.fetchCursor(db)
|
||
|
||
while let contact: Contact = try contactsThatApprovedMeCursor.next() {
|
||
guard
|
||
dependencies[singleton: .crypto].verify(
|
||
.sessionId(
|
||
contact.id,
|
||
matchesBlindedId: blindedId,
|
||
serverPublicKey: openGroupPublicKey
|
||
)
|
||
)
|
||
else { continue }
|
||
|
||
// We found a match so update the lookup and leave the loop
|
||
lookup = try lookup
|
||
.with(sessionId: contact.id)
|
||
.upserted(db)
|
||
db.addContactEvent(
|
||
id: lookup.blindedId,
|
||
change: .unblinded(blindedId: lookup.blindedId, unblindedId: contact.id)
|
||
)
|
||
|
||
// There is an edge-case where the contact might not have their 'isApproved' flag set to true
|
||
// but if we have a `BlindedIdLookup` for them and are performing the lookup from the outbox
|
||
// then that means we sent them a message request and the 'isApproved' flag should be true
|
||
if isCheckingForOutbox && !contact.isApproved {
|
||
try Contact
|
||
.filter(id: contact.id)
|
||
.updateAllAndConfig(
|
||
db,
|
||
Contact.Columns.isApproved.set(to: true),
|
||
using: dependencies
|
||
)
|
||
|
||
db.addContactEvent(id: contact.id, change: .isApproved(true))
|
||
}
|
||
|
||
break
|
||
}
|
||
|
||
// Finish if we have a result
|
||
guard lookup.sessionId == nil else { return lookup }
|
||
|
||
// Lastly loop through existing id lookups (in case the user is looking at a different SOGS but once had
|
||
// a thread with this contact in a different SOGS and had cached the lookup) - we really should never hit
|
||
// this case since the contact approval status is sync'ed (the only situation I can think of is a config
|
||
// message hasn't been handled correctly?)
|
||
let blindedIdLookupCursor: RecordCursor<BlindedIdLookup> = try BlindedIdLookup
|
||
.filter(BlindedIdLookup.Columns.sessionId != nil)
|
||
.filter(BlindedIdLookup.Columns.openGroupServer != openGroupServer.lowercased())
|
||
.fetchCursor(db)
|
||
|
||
while let otherLookup: BlindedIdLookup = try blindedIdLookupCursor.next() {
|
||
guard
|
||
let sessionId: String = otherLookup.sessionId,
|
||
dependencies[singleton: .crypto].verify(
|
||
.sessionId(
|
||
sessionId,
|
||
matchesBlindedId: blindedId,
|
||
serverPublicKey: openGroupPublicKey
|
||
)
|
||
)
|
||
else { continue }
|
||
|
||
// We found a match so update the lookup and leave the loop
|
||
lookup = try lookup
|
||
.with(sessionId: sessionId)
|
||
.upserted(db)
|
||
db.addContactEvent(
|
||
id: lookup.blindedId,
|
||
change: .unblinded(blindedId: lookup.blindedId, unblindedId: sessionId)
|
||
)
|
||
break
|
||
}
|
||
|
||
// Want to save the lookup even if it doesn't have a sessionId so it can be used when handling
|
||
// MessageRequestResponse messages
|
||
return try lookup
|
||
.upserted(db)
|
||
}
|
||
}
|