session-ios/Session/Onboarding/Onboarding.swift

243 lines
11 KiB
Swift
Raw Normal View History

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
2021-02-23 03:53:30 +01:00
import Sodium
import GRDB
import SessionUtilitiesKit
import SessionMessagingKit
2021-02-23 03:53:30 +01:00
enum Onboarding {
private static let profileNameRetrievalIdentifier: Atomic<UUID?> = Atomic(nil)
private static let profileNameRetrievalPublisher: Atomic<AnyPublisher<String?, Error>?> = Atomic(nil)
public static var profileNamePublisher: AnyPublisher<String?, Error> {
guard let existingPublisher: AnyPublisher<String?, Error> = profileNameRetrievalPublisher.wrappedValue else {
return profileNameRetrievalPublisher.mutate { value in
let requestId: UUID = UUID()
let result: AnyPublisher<String?, Error> = createProfileNameRetrievalPublisher(requestId)
value = result
profileNameRetrievalIdentifier.mutate { $0 = requestId }
return result
}
}
return existingPublisher
}
private static func createProfileNameRetrievalPublisher(_ requestId: UUID) -> AnyPublisher<String?, Error> {
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
guard SessionUtil.userConfigsEnabled else {
return Just(nil)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
let userPublicKey: String = getUserHexEncodedPublicKey()
return SnodeAPI.getSwarm(for: userPublicKey)
.tryFlatMapWithRandomSnode { snode -> AnyPublisher<Void, Error> in
CurrentUserPoller
.poll(
namespaces: [.configUserProfile],
from: snode,
for: userPublicKey,
// Note: These values mean the received messages will be
// processed immediately rather than async as part of a Job
calledFromBackgroundPoller: true,
isBackgroundPollValid: { true }
)
.tryFlatMap { receivedMessageTypes -> AnyPublisher<Void, Error> in
// FIXME: Remove this entire 'tryFlatMap' once the updated user config has been released for long enough
guard
receivedMessageTypes.isEmpty,
requestId == profileNameRetrievalIdentifier.wrappedValue
else {
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
SNLog("Onboarding failed to retrieve user config, checking for legacy config")
return CurrentUserPoller
.poll(
namespaces: [.default],
from: snode,
for: userPublicKey,
// Note: These values mean the received messages will be
// processed immediately rather than async as part of a Job
calledFromBackgroundPoller: true,
isBackgroundPollValid: { true }
)
.tryMap { receivedMessageTypes -> Void in
guard
let message: ConfigurationMessage = receivedMessageTypes
.last(where: { $0 is ConfigurationMessage })
.asType(ConfigurationMessage.self),
let displayName: String = message.displayName,
requestId == profileNameRetrievalIdentifier.wrappedValue
else { return () }
// Handle user profile changes
Storage.shared.write { db in
try ProfileManager.updateProfileIfNeeded(
db,
publicKey: userPublicKey,
name: displayName,
avatarUpdate: {
guard
let profilePictureUrl: String = message.profilePictureUrl,
let profileKey: Data = message.profileKey
else { return .none }
return .updateTo(
url: profilePictureUrl,
key: profileKey,
fileName: nil
)
}(),
sentTimestamp: TimeInterval((message.sentTimestamp ?? 0) / 1000),
calledFromConfigHandling: false
)
}
return ()
}
.eraseToAnyPublisher()
}
}
.map { _ -> String? in
guard requestId == profileNameRetrievalIdentifier.wrappedValue else {
return nil
}
return Storage.shared.read { db in
try Profile
.filter(id: userPublicKey)
.select(.name)
.asRequest(of: String.self)
.fetchOne(db)
}
}
.shareReplay(1)
.eraseToAnyPublisher()
}
2021-02-23 03:53:30 +01:00
enum Flow {
case register, recover, link
/// If the user returns to an earlier screen during Onboarding we might need to clear out a partially created
/// account (eg. returning from the PN setting screen to the seed entry screen when linking a device)
func unregister() {
// Clear the in-memory state from SessionUtil
SessionUtil.clearMemoryState()
// Clear any data which gets set during Onboarding
Storage.shared.write { db in
db[.hasViewedSeed] = false
try SessionThread.deleteAll(db)
try Profile.deleteAll(db)
try Contact.deleteAll(db)
try Identity.deleteAll(db)
try ConfigDump.deleteAll(db)
try SnodeReceivedMessageInfo.deleteAll(db)
}
// Clear the profile name retrieve publisher
profileNameRetrievalIdentifier.mutate { $0 = nil }
profileNameRetrievalPublisher.mutate { $0 = nil }
UserDefaults.standard[.hasSyncedInitialConfiguration] = false
}
func preregister(with seed: Data, ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair) {
2021-07-22 07:23:35 +02:00
let x25519PublicKey = x25519KeyPair.hexEncodedPublicKey
// Create the initial shared util state (won't have been created on
// launch due to lack of ed25519 key)
SessionUtil.loadState(
userPublicKey: x25519PublicKey,
ed25519SecretKey: ed25519KeyPair.secretKey
)
// Store the user identity information
Storage.shared.write { db in
try Identity.store(
db,
seed: seed,
ed25519KeyPair: ed25519KeyPair,
x25519KeyPair: x25519KeyPair
)
// No need to show the seed again if the user is restoring or linking
db[.hasViewedSeed] = (self == .recover || self == .link)
// Create a contact for the current user and set their approval/trusted statuses so
// they don't get weird behaviours
try Contact
.fetchOrCreate(db, id: x25519PublicKey)
.save(db)
try Contact
.filter(id: x25519PublicKey)
.updateAllAndConfig(
db,
Contact.Columns.isTrusted.set(to: true), // Always trust the current user
Contact.Columns.isApproved.set(to: true),
Contact.Columns.didApproveMe.set(to: true)
)
/// Create the 'Note to Self' thread (not visible by default)
///
/// **Note:** We need to explicitly `updateAllAndConfig` the `shouldBeVisible` value to `false`
/// otherwise it won't actually get synced correctly
try SessionThread
.fetchOrCreate(db, id: x25519PublicKey, variant: .contact, shouldBeVisible: false)
try SessionThread
.filter(id: x25519PublicKey)
.updateAllAndConfig(
db,
SessionThread.Columns.shouldBeVisible.set(to: false)
)
2021-07-22 07:23:35 +02:00
}
// Set hasSyncedInitialConfiguration to true so that when we hit the
// home screen a configuration sync is triggered (yes, the logic is a
// bit weird). This is needed so that if the user registers and
// immediately links a device, there'll be a configuration in their swarm.
UserDefaults.standard[.hasSyncedInitialConfiguration] = (self == .register)
// Only continue if this isn't a new account
guard self != .register else { return }
// Fetch the
Fixed a number of issues found during internal testing Added copy for an unrecoverable startup case Added some additional logs to better debug ValueObservation query errors Increased the pageSize to 20 on iPad devices (to prevent it immediately loading a second page) Cleaned up a bunch of threading logic (try to avoid overriding subscribe/receive threads specified at subscription) Consolidated the 'sendMessage' and 'sendAttachments' functions Updated the various frameworks to use 'DAWRF with DSYM' to allow for better debugging during debug mode (at the cost of a longer build time) Updated the logic to optimistically insert messages when sending to avoid any database write delays Updated the logic to avoid sending notifications for messages which are already marked as read by the config Fixed an issue where multiple paths could incorrectly get built at the same time in some cases Fixed an issue where other job queues could be started before the blockingQueue finishes Fixed a potential bug with the snode version comparison (was just a string comparison which would fail when getting to double-digit values) Fixed a bug where you couldn't remove the last reaction on a message Fixed the broken media message zoom animations Fixed a bug where the last message read in a conversation wouldn't be correctly detected as already read Fixed a bug where the QuoteView had no line limits (resulting in the '@You' mention background highlight being incorrectly positioned in the quote preview) Fixed a bug where a large number of configSyncJobs could be scheduled (only one would run at a time but this could result in performance impacts)
2023-06-23 09:54:29 +02:00
Onboarding.profileNamePublisher
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete()
}
func completeRegistration() {
// Set the `lastNameUpdate` to the current date, so that we don't overwrite
// what the user set in the display name step with whatever we find in their
// swarm (otherwise the user could enter a display name and have it immediately
// overwritten due to the config request running slow)
Storage.shared.write { db in
try Profile
.filter(id: getUserHexEncodedPublicKey(db))
.updateAllAndConfig(
db,
Profile.Columns.lastNameUpdate.set(to: Date().timeIntervalSince1970)
)
}
// Notify the app that registration is complete
Identity.didRegister()
// Now that we have registered get the Snode pool and sync push tokens
GetSnodePoolJob.run()
SyncPushTokensJob.run(uploadOnlyIfStale: false)
2021-02-23 03:53:30 +01:00
}
}
}