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

244 lines
9.2 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import SessionUtilitiesKit
import SessionMessagingKit
import SessionUIKit
// MARK: - Singleton
public extension Singleton {
static let app: SingletonConfig<SessionAppType> = Dependencies.create(
identifier: "app",
createInstance: { dependencies in SessionApp(using: dependencies) }
)
}
// MARK: - SessionApp
public class SessionApp: SessionAppType {
private let dependencies: Dependencies
private var homeViewController: HomeVC?
@MainActor public var homePresentedViewController: UIViewController? {
homeViewController?.presentedViewController
}
static var versionInfo: String {
let buildNumber: String = (Bundle.main.infoDictionary?["CFBundleVersion"] as? String)
.map { " (\($0))" }
.defaulting(to: "")
let appVersion: String? = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String)
.map { "App: \($0)\(buildNumber)" }
let commitInfo: String? = (Bundle.main.infoDictionary?["GitCommitHash"] as? String).map { "Commit: \($0)" }
let versionInfo: [String] = [
"iOS \(UIDevice.current.systemVersion)",
appVersion,
"libSession: \(LibSession.version)",
commitInfo
].compactMap { $0 }
return versionInfo.joined(separator: ", ")
}
// MARK: - Initialization
init(using dependencies: Dependencies) {
self.dependencies = dependencies
}
// MARK: - Functions
public func setHomeViewController(_ homeViewController: HomeVC) {
self.homeViewController = homeViewController
}
public func showHomeView() {
guard Thread.isMainThread else {
return DispatchQueue.main.async {
self.showHomeView()
}
}
let homeViewController: HomeVC = HomeVC(using: dependencies)
let navController: UINavigationController = StyledNavigationController(rootViewController: homeViewController)
(UIApplication.shared.delegate as? AppDelegate)?.window?.rootViewController = navController
self.homeViewController = homeViewController
}
public func presentConversationCreatingIfNeeded(
for threadId: String,
variant: SessionThread.Variant,
action: ConversationViewModel.Action = .none,
dismissing presentingViewController: UIViewController?,
animated: Bool
) async {
guard let homeViewController: HomeVC = self.homeViewController else {
Log.error("[SessionApp] Unable to present conversation due to missing HomeVC.")
return
}
/// The thread should generally exist at the time of calling this method, but on the off chance it doesn't then we need to
/// `fetchOrCreate` it and should do it on a background thread just in case something is keeping the DBWrite thread
/// busy as in the past this could cause the app to hang
let threadExists: Bool? = try? await dependencies[singleton: .storage].readAsync { db in
SessionThread.filter(id: threadId).isNotEmpty(db)
}
if threadExists != true {
_ = try? await dependencies[singleton: .storage].writeAsync { [dependencies] db in
try SessionThread.upsert(
db,
id: threadId,
variant: variant,
values: SessionThread.TargetValues(
shouldBeVisible: .useLibSession,
isDraft: .useExistingOrSetTo(true)
),
using: dependencies
)
}
}
let maybeThreadInfo: ConversationInfoViewModel? = try? await ConversationViewModel.fetchConversationInfo(
threadId: threadId,
using: dependencies
)
guard let threadInfo: ConversationInfoViewModel = maybeThreadInfo else {
Log.error("Failed to present \(variant) conversation \(threadId) due to failure to fetch threadViewModel")
return
}
await MainActor.run { [weak self] in
self?.showConversation(
threadInfo: threadInfo,
action: action,
dismissing: presentingViewController,
homeViewController: homeViewController,
animated: animated
)
}
}
public func createNewConversation() {
guard let homeViewController: HomeVC = self.homeViewController else { return }
let viewController = SessionHostingViewController(
rootView: StartConversationScreen(using: dependencies),
customizedNavigationBackground: .backgroundSecondary
)
viewController.setNavBarTitle("conversationsStart".localized())
viewController.setUpDismissingButton(on: .right)
let navigationController = StyledNavigationController(rootViewController: viewController)
if UIDevice.current.isIPad {
navigationController.modalPresentationStyle = .fullScreen
}
navigationController.modalPresentationCapturesStatusBarAppearance = true
homeViewController.present(navigationController, animated: true, completion: nil)
}
public func resetData(onReset: (() -> ())) {
homeViewController = nil
dependencies.remove(cache: .general)
dependencies.remove(cache: .snodeAPI)
dependencies.remove(cache: .libSession)
dependencies.mutate(cache: .libSessionNetwork) {
$0.suspendNetworkAccess()
$0.clearSnodeCache()
$0.clearCallbacks()
}
dependencies[singleton: .storage].resetAllStorage()
dependencies[singleton: .extensionHelper].deleteCache()
dependencies[singleton: .displayPictureManager].resetStorage()
dependencies[singleton: .attachmentManager].resetStorage()
dependencies[singleton: .notificationsManager].clearAllNotifications()
try? dependencies[singleton: .keychain].removeAll()
UserDefaults.removeAll(using: dependencies)
onReset()
LibSession.clearLoggers()
Log.info("Data Reset Complete.")
Log.flush()
/// Wait for a small duration before killing the app (hoping to avoid a crash due to `libSession` shutting down connections
/// which result in spdlog trying to log and crashing)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
exit(0)
}
}
/// Show Session Network Page for this release. We'll be able to extend this fuction to show other screens that is new
/// or we want to promote in the future.
@MainActor public func showPromotedScreen() {
guard let homeViewController: HomeVC = self.homeViewController else { return }
let viewController: SessionHostingViewController = SessionHostingViewController(
rootView: SessionNetworkScreen(
viewModel: SessionNetworkScreenContent.ViewModel(dependencies: dependencies)
)
)
viewController.setNavBarTitle(Constants.network_name)
viewController.setUpDismissingButton(on: .left)
let navigationController = StyledNavigationController(rootViewController: viewController)
navigationController.modalPresentationStyle = .fullScreen
homeViewController.present(navigationController, animated: true, completion: nil)
}
// MARK: - Internal Functions
@MainActor private func showConversation(
threadInfo: ConversationInfoViewModel,
action: ConversationViewModel.Action,
dismissing presentingViewController: UIViewController?,
homeViewController: HomeVC,
animated: Bool
) {
presentingViewController?.dismiss(animated: true, completion: nil)
homeViewController.navigationController?.setViewControllers(
[
homeViewController,
(threadInfo.isMessageRequest && action != .compose ?
SessionTableViewController(viewModel: MessageRequestsViewModel(using: dependencies)) :
nil
),
ConversationVC(
threadInfo: threadInfo,
focusedInteractionInfo: nil,
using: dependencies
)
].compactMap { $0 },
animated: animated
)
}
}
// MARK: - SessionAppType
public protocol SessionAppType {
@MainActor var homePresentedViewController: UIViewController? { get }
func setHomeViewController(_ homeViewController: HomeVC)
@MainActor func showHomeView()
func presentConversationCreatingIfNeeded(
for threadId: String,
variant: SessionThread.Variant,
action: ConversationViewModel.Action,
dismissing presentingViewController: UIViewController?,
animated: Bool
) async
func createNewConversation()
func resetData(onReset: (() -> ()))
@MainActor func showPromotedScreen()
}
public extension SessionAppType {
func resetData() { resetData(onReset: {}) }
}