diff --git a/ElementX/Sources/AccessibilityTests/AccessibilityTestsAppCoordinator.swift b/ElementX/Sources/AccessibilityTests/AccessibilityTestsAppCoordinator.swift index 07cd18c9f..b4c67fd91 100644 --- a/ElementX/Sources/AccessibilityTests/AccessibilityTestsAppCoordinator.swift +++ b/ElementX/Sources/AccessibilityTests/AccessibilityTestsAppCoordinator.swift @@ -41,7 +41,7 @@ class AccessibilityTestsAppCoordinator: AppCoordinatorProtocol { MXLog.configure(currentTarget: "accessibility-tests") let appSettings = AppSettings.volatile() - + guard let name = ProcessInfo.accessibilityViewID, let previewType = TestablePreviewsDictionary.dictionary[name] else { fatalError("Unable to launch with unknown screen.") diff --git a/ElementX/Sources/AppHooks/AppHooks.swift b/ElementX/Sources/AppHooks/AppHooks.swift index ad735c92a..b72828743 100644 --- a/ElementX/Sources/AppHooks/AppHooks.swift +++ b/ElementX/Sources/AppHooks/AppHooks.swift @@ -28,7 +28,7 @@ final class AppHooks: AppHooksProtocol { var compoundHook: CompoundHookProtocol { _compoundHook.withLock { $0 } } - + func registerCompoundHook(_ hook: CompoundHookProtocol) { _compoundHook.withLock { $0 = hook } } diff --git a/ElementX/Sources/Application/AppCoordinator.swift b/ElementX/Sources/Application/AppCoordinator.swift index c4cdf9ac8..c3856555d 100644 --- a/ElementX/Sources/Application/AppCoordinator.swift +++ b/ElementX/Sources/Application/AppCoordinator.swift @@ -29,7 +29,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg private let appHooks: AppHooks private let bugReportService: BugReportServiceProtocol private let elementCallService: ElementCallServiceProtocol - + /// Common background task to continue long-running tasks in the background. private var backgroundTask: UIBackgroundTaskIdentifier? @@ -60,13 +60,13 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg let windowManager: SecureWindowManagerProtocol let notificationManager: NotificationManagerProtocol - + private let appRouteURLParser: AppRouteURLParser private var storedAppRoute: AppRoute? @Consumable private var storedInlineReply: (roomID: String, message: String)? @Consumable private var storedRoomsToAwait: Set? - + init(appDelegate: AppDelegate) { let appHooks = AppHooks() appHooks.setUp() @@ -115,9 +115,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg navigationRootCoordinator = NavigationRootCoordinator() stateMachine = AppCoordinatorStateMachine() - + navigationRootCoordinator.setRootCoordinator(SplashScreenCoordinator()) - + let keychainController = KeychainController(service: .sessions, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) userSessionStore = UserSessionStore(keychainController: keychainController, @@ -160,9 +160,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg wipeUserData(includingSettings: true) } appSettings.lastVersionLaunched = currentVersion.description - + setupStateMachine() - + observeApplicationState() observeAppLockChanges() @@ -213,13 +213,13 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg stateMachine.processEvent(.startWithExistingSession) } - + func toPresentable() -> AnyView { AnyView(navigationRootCoordinator.toPresentable() .environment(\.analyticsService, analyticsService) .onReceive(appSettings.$appAppearance) { [weak self] appAppearance in guard let self else { return } - + windowManager.windows.forEach { window in // Unfortunately .preferredColorScheme doesn't propagate properly throughout the app when changed window.overrideUserInterfaceStyle = appAppearance.interfaceStyle @@ -267,7 +267,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg storedAppRoute = appRoute } } - + func handleDeepLink(_ url: URL, isExternalURL: Bool, windowType: SecondaryWindowType?) -> Bool { // Parse into an AppRoute to redirect these in a type safe way. @@ -382,7 +382,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg func unregisterForRemoteNotifications() { UIApplication.shared.unregisterForRemoteNotifications() } - + func shouldDisplayInAppNotification(content: UNNotificationContent) -> Bool { guard let roomID = content.roomID else { return true @@ -542,7 +542,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } } } - + /// Clears the keychain, app support directory etc ready for a fresh use. /// - Parameter includingSettings: Whether to additionally wipe the user's app settings too. private func wipeUserData(includingSettings: Bool = false) { @@ -608,7 +608,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg presentSplashScreen() case (.restoringSession, .createdUserSession, .signedIn): setupUserSession(isNewLogin: false) - + case (.signingOut, .signOut, .signingOut): // We can ignore signOut when already in the process of signing out, // such as the SDK sending an authError due to token invalidation. @@ -696,7 +696,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg await processInlineReply(roomID: storedInlineReply.roomID, replyText: storedInlineReply.message) } } - + private func startAuthenticationSoftLogout() { guard let userSession else { fatalError("User session not setup") @@ -799,7 +799,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg await runPostSessionSetupTasks() } } - + private func logout(isSoft: Bool) { guard let userSession else { fatalError("User session not setup") @@ -841,7 +841,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg analyticsService.resetConsentState() stateMachine.processEvent(.completedSigningOut) - + hideLoadingIndicator() } } @@ -852,7 +852,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg userSession = nil userSessionFlowCoordinator = nil - + notificationManager.setUserSession(nil) } @@ -882,10 +882,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg elementCallService.setClientProxy(userSession.clientProxy) } - + private func configureNotificationManager() { notificationManager.setUserSession(userSession) - + appDelegateObserver = appDelegate.callbacks .receive(on: DispatchQueue.main) .sink { [weak self] callback in @@ -940,7 +940,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg stopSync(isBackgroundTask: false) userSessionFlowCoordinator?.stop() - + // Allow for everything to deallocate properly Task { await userSession.clientProxy.clearCaches() @@ -959,7 +959,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg #else options.enabled = appSettings.analyticsConsentState == .optedIn #endif - + options.dsn = bugReportSentryURL.absoluteString // Matches android, at least for now. @@ -997,7 +997,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg options.sampleRate = 1.0 options.tracesSampleRate = 1.0 options.configureProfiling = { $0.sessionSampleRate = 1.0 } - + // This callback is only executed once during the entire run of the program to avoid // multiple callbacks if there are multiple crash events to send (see method documentation) options.onLastRunStatusDetermined = { status, event in @@ -1056,9 +1056,9 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg private func showLoginErrorToast() { userIndicatorController.submitIndicator(UserIndicator(title: "Failed logging in")) } - + // MARK: - Application State - + private func stopSync(isBackgroundTask: Bool, completion: (() -> Void)? = nil) { if isBackgroundTask, UIApplication.shared.applicationState == .active { // Attempt to stop the background task sync loop cleanly, only if the app not already running @@ -1070,7 +1070,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg clientProxyObserver = nil } } - + private func startSync() { guard let userSession else { return } @@ -1103,7 +1103,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg } } } - + private func observeApplicationState() { NotificationCenter.default.addObserver(self, selector: #selector(applicationWillResignActive), @@ -1123,7 +1123,7 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg selector: #selector(applicationDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil) - + NotificationCenter.default.addObserver(self, selector: #selector(applicationWillTerminate), name: UIApplication.willTerminateNotification, @@ -1133,18 +1133,18 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg name: UIContentSizeCategory.didChangeNotification, object: nil) } - + @objc private func didChangeContentSizeCategory() { AttributedStringBuilder.invalidateCaches() } - + @objc private func applicationWillTerminate() { MXLog.info("Application will terminate") stopSync(isBackgroundTask: false) } - + @objc private func applicationDidEnterBackground() { MXLog.info("Application did enter background") diff --git a/ElementX/Sources/Application/AppCoordinatorStateMachine.swift b/ElementX/Sources/Application/AppCoordinatorStateMachine.swift index 833301e97..c4b6e1508 100644 --- a/ElementX/Sources/Application/AppCoordinatorStateMachine.swift +++ b/ElementX/Sources/Application/AppCoordinatorStateMachine.swift @@ -20,14 +20,14 @@ class AppCoordinatorStateMachine { case softLogout /// Opening an existing session. case restoringSession - + /// User session started case signedIn - + /// Processing a sign out request case signingOut(isSoft: Bool, disableAppLock: Bool) } - + /// Events that can be triggered on the AppCoordinator state machine enum Event: EventType { /// Start the `AppCoordinator` by showing authentication. @@ -41,7 +41,7 @@ class AppCoordinatorStateMachine { /// A session has been created. case createdUserSession - + /// Request sign out. case signOut(isSoft: Bool, disableAppLock: Bool) /// Request the soft logout screen. @@ -63,7 +63,7 @@ class AppCoordinatorStateMachine { stateMachine = StateMachine(state: .initial) configure() } - + private func configure() { stateMachine.addRoutes(event: .startWithAuthentication, transitions: [.initial => .signedOut]) stateMachine.addRoutes(event: .createdUserSession, transitions: [.signedOut => .signedIn, @@ -71,13 +71,13 @@ class AppCoordinatorStateMachine { stateMachine.addRoutes(event: .startWithExistingSession, transitions: [.initial => .restoringSession]) stateMachine.addRoutes(event: .createdUserSession, transitions: [.restoringSession => .signedIn]) stateMachine.addRoutes(event: .failedRestoringSession, transitions: [.restoringSession => .signedOut]) - + stateMachine.addRoutes(event: .completedSigningOut, transitions: [.signingOut(isSoft: false, disableAppLock: false) => .signedOut, .signingOut(isSoft: false, disableAppLock: true) => .signedOut]) stateMachine.addRoutes(event: .showSoftLogout, transitions: [.signingOut(isSoft: true, disableAppLock: false) => .softLogout]) stateMachine.addRoutes(event: .clearCache, transitions: [.signedIn => .initial]) - + // Transitions with associated values need to be handled through `addRouteMapping` stateMachine.addRouteMapping { event, fromState, _ in switch (fromState, event) { @@ -87,7 +87,7 @@ class AppCoordinatorStateMachine { return nil } } - + addTransitionHandler { context in if let event = context.event { MXLog.info("Transitioning from `\(context.fromState)` to `\(context.toState)` with event `\(event)`") diff --git a/ElementX/Sources/Application/AppDelegate.swift b/ElementX/Sources/Application/AppDelegate.swift index 51d6f92b3..90e24c8a8 100644 --- a/ElementX/Sources/Application/AppDelegate.swift +++ b/ElementX/Sources/Application/AppDelegate.swift @@ -24,7 +24,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate { configuration.delegateClass = SceneDelegate.self return configuration } - + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { NSTextAttachment.registerViewProviderClass(PillAttachmentViewProvider.self, forFileType: InfoPlistReader.main.pillsUTType) return true diff --git a/ElementX/Sources/Application/AppMediator.swift b/ElementX/Sources/Application/AppMediator.swift index b8016fda4..df67f0622 100644 --- a/ElementX/Sources/Application/AppMediator.swift +++ b/ElementX/Sources/Application/AppMediator.swift @@ -17,12 +17,12 @@ class AppMediator: AppMediatorProtocol { self.windowManager = windowManager self.networkMonitor = networkMonitor } - + /// UIApplication.State won't update if we store this e.g. in the constructor private var application: UIApplication { UIApplication.shared } - + var appState: UIApplication.State { switch application.applicationState { case .active: @@ -39,7 +39,7 @@ class AppMediator: AppMediatorProtocol { func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier { application.beginBackgroundTask(expirationHandler: handler) } - + func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) { application.endBackgroundTask(identifier) } diff --git a/ElementX/Sources/Application/AppMediatorProtocol.swift b/ElementX/Sources/Application/AppMediatorProtocol.swift index 2a73e5ce1..4267a73e7 100644 --- a/ElementX/Sources/Application/AppMediatorProtocol.swift +++ b/ElementX/Sources/Application/AppMediatorProtocol.swift @@ -18,7 +18,7 @@ protocol AppMediatorProtocol { var appState: UIApplication.State { get } func beginBackgroundTask(expirationHandler handler: (() -> Void)?) -> UIBackgroundTaskIdentifier - + func endBackgroundTask(_ identifier: UIBackgroundTaskIdentifier) func open(_ url: URL) diff --git a/ElementX/Sources/Application/Application.swift b/ElementX/Sources/Application/Application.swift index 878664f35..1bc86d567 100644 --- a/ElementX/Sources/Application/Application.swift +++ b/ElementX/Sources/Application/Application.swift @@ -16,7 +16,7 @@ struct Application: App { @Environment(\.dismissWindow) private var dismissWindow private var appCoordinator: AppCoordinatorProtocol! - + init() { if ProcessInfo.isRunningUITests { appCoordinator = UITestsAppCoordinator(appDelegate: appDelegate) @@ -30,7 +30,7 @@ struct Application: App { SceneDelegate.windowManager = appCoordinator.windowManager } - + var body: some Scene { WindowGroup(id: SceneDelegate.mainSceneID) { appCoordinator.toPresentable() @@ -114,7 +114,7 @@ struct Application: App { openURLInSystemBrowser(url) } } - + /// Hide the status bar so it doesn't interfere with the screenshot tests private var shouldHideStatusBar: Bool { ProcessInfo.isRunningUITests diff --git a/ElementX/Sources/Application/CoordinatorProtocol.swift b/ElementX/Sources/Application/CoordinatorProtocol.swift index d31e2e1b7..f6c0390b8 100644 --- a/ElementX/Sources/Application/CoordinatorProtocol.swift +++ b/ElementX/Sources/Application/CoordinatorProtocol.swift @@ -17,9 +17,9 @@ protocol CoordinatorProtocol: AnyObject { extension CoordinatorProtocol { func start() { } - + func stop() { } - + func toPresentable() -> AnyView { AnyView(Text("View not configured")) } diff --git a/ElementX/Sources/Application/FlowCoordinatorProtocol.swift b/ElementX/Sources/Application/FlowCoordinatorProtocol.swift index 1f656a50d..818d79172 100644 --- a/ElementX/Sources/Application/FlowCoordinatorProtocol.swift +++ b/ElementX/Sources/Application/FlowCoordinatorProtocol.swift @@ -44,7 +44,7 @@ struct CommonFlowParameters { var windowManager: WindowManagerProtocol { appMediator.windowManager } - + var ongoingCallRoomIDPublisher: CurrentValuePublisher { elementCallService.ongoingCallRoomIDPublisher } diff --git a/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift b/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift index e4cf8be6d..5e2bc8f6e 100644 --- a/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift +++ b/ElementX/Sources/Application/Navigation/NavigationCoordinators.swift @@ -13,7 +13,7 @@ import SwiftUI /// into a single navigation stack on compact layouts @Observable class NavigationSplitCoordinator: CoordinatorProtocol, CustomStringConvertible { fileprivate let placeholderModule: NavigationModule - + fileprivate var sidebarModule: NavigationModule? { didSet { if let oldValue { @@ -105,7 +105,7 @@ import SwiftUI var compactLayoutRootCoordinator: (any CoordinatorProtocol)? { compactLayoutRootModule?.coordinator } - + var compactLayoutStackModules: [NavigationModule] { get { getCompactStackModules() @@ -114,7 +114,7 @@ import SwiftUI setCompactStackModules(newValue) } } - + private func getCompactStackModules() -> [NavigationModule] { // Start building the new compact layout navigation stack var stackModules: [NavigationModule] = [] @@ -123,23 +123,23 @@ import SwiftUI if let sidebarNavigationStackCoordinator = sidebarModule?.coordinator as? NavigationStackCoordinator { stackModules.append(contentsOf: sidebarNavigationStackCoordinator.stackModules) } - + // If the detail is a stackCoordinator then push its root and children to the compact layout stack if let detailNavigationStackCoordinator = detailModule?.coordinator as? NavigationStackCoordinator { if let detailRootCoordinator = detailNavigationStackCoordinator.rootModule { stackModules.append(detailRootCoordinator) } - + stackModules.append(contentsOf: detailNavigationStackCoordinator.stackModules) } else if let detailModule { // Otherwise just push it entirely stackModules.append(detailModule) } return stackModules } - + private func setCompactStackModules(_ modules: [NavigationModule]) { guard compactLayoutStackModules != modules else { return } - + let diffs = modules.difference(from: compactLayoutStackModules) diffs.forEach { change in switch change { @@ -150,7 +150,7 @@ import SwiftUI } } } - + var compactLayoutStackCoordinators: [any CoordinatorProtocol] { compactLayoutStackModules.compactMap(\.coordinator) } @@ -175,10 +175,10 @@ import SwiftUI if sidebarModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { sidebarModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } @@ -198,10 +198,10 @@ import SwiftUI if detailModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { detailModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } @@ -221,10 +221,10 @@ import SwiftUI if sheetModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } @@ -244,15 +244,15 @@ import SwiftUI if fullScreenCoverModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { fullScreenCoverModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } } - + // MARK: - CoordinatorProtocol func toPresentable() -> AnyView { @@ -264,7 +264,7 @@ import SwiftUI } // MARK: - CustomStringConvertible - + var description: String { switch (sidebarModule?.coordinator, detailModule?.coordinator) { case (.some(let sidebarCoordinator), .some(let detailCoordinator)): @@ -533,10 +533,10 @@ private struct NavigationSplitCoordinatorView: View { } popToRoot(animated: false) - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { rootModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } @@ -550,7 +550,7 @@ private struct NavigationSplitCoordinatorView: View { func push(_ coordinator: any CoordinatorProtocol, animated: Bool = true, dismissalCallback: (() -> Void)? = nil) { var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { stackModules.append(NavigationModule(coordinator, dismissalCallback: dismissalCallback)) } @@ -562,10 +562,10 @@ private struct NavigationSplitCoordinatorView: View { guard !stackModules.isEmpty else { return } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { stackModules.removeAll() } @@ -576,7 +576,7 @@ private struct NavigationSplitCoordinatorView: View { func pop(animated: Bool = true) { var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { _ = stackModules.popLast() } @@ -617,15 +617,15 @@ private struct NavigationSplitCoordinatorView: View { if sheetModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } } - + // periphery:ignore - might be useful to have /// Present a fullscreen cover on top of the stack. If this NavigationStackCoordinator is embedded within a NavigationSplitCoordinator /// then the presentation will be proxied to the split @@ -647,10 +647,10 @@ private struct NavigationSplitCoordinatorView: View { if fullScreenCoverModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { fullScreenCoverModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } diff --git a/ElementX/Sources/Application/Navigation/NavigationRootCoordinator.swift b/ElementX/Sources/Application/Navigation/NavigationRootCoordinator.swift index cdafb4a27..f8b9ed67d 100644 --- a/ElementX/Sources/Application/Navigation/NavigationRootCoordinator.swift +++ b/ElementX/Sources/Application/Navigation/NavigationRootCoordinator.swift @@ -77,15 +77,15 @@ import SwiftUI if sheetModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } } - + // MARK: - CoordinatorProtocol func toPresentable() -> AnyView { diff --git a/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift b/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift index c44a23262..ff7935add 100644 --- a/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift +++ b/ElementX/Sources/Application/Navigation/NavigationTabCoordinator.swift @@ -57,7 +57,7 @@ import SwiftUI var id: ObjectIdentifier { module.id } - + @MainActor var coordinator: CoordinatorProtocol? { module.coordinator } @@ -137,10 +137,10 @@ import SwiftUI if sheetModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { sheetModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } @@ -183,10 +183,10 @@ import SwiftUI if fullScreenCoverModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { fullScreenCoverModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) } @@ -234,10 +234,10 @@ import SwiftUI if overlayModule?.coordinator === coordinator { fatalError("Cannot use the same coordinator more than once") } - + var transaction = Transaction() transaction.disablesAnimations = !animated - + withTransaction(transaction) { overlayPresentationMode = presentationMode overlayModule = NavigationModule(coordinator, dismissalCallback: dismissalCallback) diff --git a/ElementX/Sources/Application/Settings/AppAppearance.swift b/ElementX/Sources/Application/Settings/AppAppearance.swift index 59cea462c..dab31796f 100644 --- a/ElementX/Sources/Application/Settings/AppAppearance.swift +++ b/ElementX/Sources/Application/Settings/AppAppearance.swift @@ -13,7 +13,7 @@ enum AppAppearance: CaseIterable, Codable { case system case dark case light - + var interfaceStyle: UIUserInterfaceStyle { switch self { case .light: diff --git a/ElementX/Sources/Application/Settings/AppSettings.swift b/ElementX/Sources/Application/Settings/AppSettings.swift index b8154807b..bb4aa0e8f 100644 --- a/ElementX/Sources/Application/Settings/AppSettings.swift +++ b/ElementX/Sources/Application/Settings/AppSettings.swift @@ -17,7 +17,7 @@ import SwiftUI protocol CommonSettingsProtocol: AnyObject, Sendable { var lastNotificationBootTime: TimeInterval? { get set } var selectedNotificationTone: NotificationTone? { get set } - + var logLevel: LogLevel { get } var traceLogPacks: Set { get } var bugReportRageshakeURL: RemotePreference { get } @@ -95,7 +95,7 @@ final class AppSettings: @unchecked Sendable { } static let suiteName: String = InfoPlistReader.main.appGroupIdentifier - + /// UserDefaults to be used on reads and writes. private let store: UserDefaultsProtocol @@ -174,7 +174,7 @@ final class AppSettings: @unchecked Sendable { /// deleted between runs so should clear data in the shared container and keychain. @UserPreference var lastVersionLaunched: String? - + /// The Set of room identifiers of invites that the user already saw in the invites list. /// This Set is being used to implement badges for unread invites. @UserPreference @@ -196,7 +196,7 @@ final class AppSettings: @unchecked Sendable { /// The task identifier used for background app refresh. Also used in main target's the Info.plist let backgroundAppRefreshTaskIdentifier = "io.element.elementx.background.refresh" - + /// A URL where users can go read more about the app. private(set) var websiteURL: URL = "https://element.io" /// A URL that contains the app's logo that may be used when showing content in a web view. @@ -217,7 +217,7 @@ final class AppSettings: @unchecked Sendable { private(set) var identityPinningViolationDetailsURL: URL = "https://element.io/help#encryption18" /// A URL describing how history sharing works private(set) var historySharingDetailsURL: URL = "https://element.io/en/help#e2ee-history-sharing" - + /// Any domains that Element web may be hosted on - used for handling links. private(set) var elementWebHosts = ["app.element.io", "staging.element.io", "develop.element.io"] /// The domain that account provisioning links will be hosted on - used for handling the links. @@ -279,13 +279,13 @@ final class AppSettings: @unchecked Sendable { @UserPreference var enableNotifications: Bool - + @UserPreference var enableInAppNotifications: Bool @UserPreference var hideQuietNotificationAlerts: Bool - + /// Tag describing which set of device specific rules a pusher executes. @UserPreference var pusherProfileTag: String? @@ -293,13 +293,13 @@ final class AppSettings: @unchecked Sendable { /// The device's last boot time as recorded by the NSE. @UserPreference var lastNotificationBootTime: TimeInterval? - + /// The sound played when delivering noisy notifications. If nil, use the ElementX default @UserPreference var selectedNotificationTone: NotificationTone? - + // MARK: - Logging - + @UserPreference var logLevel: LogLevel @@ -369,14 +369,14 @@ final class AppSettings: @unchecked Sendable { @UserPreference var optimizeMediaUploads: Bool - + @UserPreference var voiceMessagePlaybackSpeed: AudioPlaybackSpeed - + /// Whether or not to show a warning on the media caption composer so the user knows /// that captions might not be visible to users who are using other Matrix clients. let shouldShowMediaCaptionWarning = true - + // MARK: - Element Call #if IS_MAIN_APP @@ -434,13 +434,13 @@ final class AppSettings: @unchecked Sendable { @UserPreference var focusEventOnNotificationTap: Bool - + @UserPreference var linkPreviewsEnabled: Bool @UserPreference var jumpToReadMarkerEnabled: Bool - + @UserPreference var linkNewDeviceEnabled: Bool @@ -452,11 +452,11 @@ final class AppSettings: @unchecked Sendable { @UserPreference var developerOptionsEnabled: Bool - + init(store: UserDefaultsProtocol) { // UserDefaults to be used on reads and writes. self.store = store - + _lastVersionLaunched = UserPreference(key: .lastVersionLaunched, storage: store) _seenInvites = UserPreference(key: .seenInvites, defaultValue: [], storage: store) _hasSeenNewSoundBanner = UserPreference(key: .hasSeenNewSoundBanner, defaultValue: true, storage: store) diff --git a/ElementX/Sources/Application/Settings/RemotePreference.swift b/ElementX/Sources/Application/Settings/RemotePreference.swift index 0b14b5c85..eea8ac740 100644 --- a/ElementX/Sources/Application/Settings/RemotePreference.swift +++ b/ElementX/Sources/Application/Settings/RemotePreference.swift @@ -19,7 +19,7 @@ class RemotePreference { var publisher: CurrentValuePublisher { subject.asCurrentValuePublisher() } - + var isRemotelyConfigured: Bool { subject.value != defaultValue } diff --git a/ElementX/Sources/Application/Settings/UserPreference.swift b/ElementX/Sources/Application/Settings/UserPreference.swift index 51a065b7d..488a111e6 100644 --- a/ElementX/Sources/Application/Settings/UserPreference.swift +++ b/ElementX/Sources/Application/Settings/UserPreference.swift @@ -27,7 +27,7 @@ final class UserPreference { private var remoteKey: String { "\(Self.remotePrefix)\(key)" } - + private var keyedStorage: any KeyedStorage private let defaultValue: T private let subject: PassthroughSubject = .init() @@ -84,7 +84,7 @@ final class UserPreference { } } } - + var projectedValue: AnyPublisher { subject .prepend(wrappedValue) @@ -97,7 +97,7 @@ final class UserPreference { extension UserPreference { convenience init(key: any PreferenceKeyable, defaultValue: T, storage backingStorage: UserDefaultsProtocol, mode: Mode = .localOverRemote) { let storage: any KeyedStorage = UserDefaultsStorage(userDefaults: backingStorage) - + self.init(key: key, defaultValue: defaultValue, keyedStorage: storage, mode: mode) } diff --git a/ElementX/Sources/Application/Settings/VolatileUserDefaults.swift b/ElementX/Sources/Application/Settings/VolatileUserDefaults.swift index 73adbe42c..8e0224d09 100644 --- a/ElementX/Sources/Application/Settings/VolatileUserDefaults.swift +++ b/ElementX/Sources/Application/Settings/VolatileUserDefaults.swift @@ -9,36 +9,36 @@ import Foundation public final class VolatileUserDefaults: UserDefaultsProtocol, Sendable { private let lock = NSLock() - + private nonisolated(unsafe) var _storage: [String: Any] public private(set) var storage: [String: Any] { get { lock.withLock { _storage } } set { lock.withLock { _storage = newValue } } } - + public init(initialValues: [String: Any] = [:]) { lock.lock() defer { lock.unlock() } - + _storage = initialValues } - + public func data(forKey key: String) -> Data? { storage[key] as? Data } - + public func object(forKey key: String) -> Any? { storage[key] } - + public func removeObject(forKey key: String) { storage.removeValue(forKey: key) } - + public func set(_ value: Any?, forKey key: String) { storage[key] = value } - + public func reset() { storage = [:] } diff --git a/ElementX/Sources/Application/Windowing/WindowManager.swift b/ElementX/Sources/Application/Windowing/WindowManager.swift index 08dc3728e..90a463ded 100644 --- a/ElementX/Sources/Application/Windowing/WindowManager.swift +++ b/ElementX/Sources/Application/Windowing/WindowManager.swift @@ -30,7 +30,7 @@ class WindowManager: SecureWindowManagerProtocol { } } } - + var windows: [UIWindow] { [mainWindow, overlayWindow, globalSearchWindow, alternateWindow] } diff --git a/ElementX/Sources/FlowCoordinators/AppLockFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/AppLockFlowCoordinator.swift index a869804ac..9a1dd93fe 100644 --- a/ElementX/Sources/FlowCoordinators/AppLockFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/AppLockFlowCoordinator.swift @@ -49,7 +49,7 @@ class AppLockFlowCoordinator: CoordinatorProtocol { /// The user failed to unlock the app (or forgot their PIN) and is being logged out. case loggingOut } - + /// Events that can be triggered on the flow state machine enum Event: EventType { /// Starts the flow while the app is launching in the background. @@ -145,7 +145,7 @@ class AppLockFlowCoordinator: CoordinatorProtocol { switch (fromState, event) { case (.initial, .start): return .backgrounded - + case (.unlocked, .willResignActive): return .appObscured case (.appObscured, .didBecomeActive): @@ -169,14 +169,14 @@ class AppLockFlowCoordinator: CoordinatorProtocol { return .unlocked case (.attemptingPINUnlock, .forceLogout): return .loggingOut - + // Transition to a valid state when enabling the service for the first time. case (.initial, .serviceEnabled): return .unlocked // Transition to a valid state once the service is disabled following a forced logout. case (.loggingOut, .serviceDisabled): return .unlocked - + default: return fromState } diff --git a/ElementX/Sources/FlowCoordinators/AppLockSetupFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/AppLockSetupFlowCoordinator.swift index 85d577cdd..bc5a76b8c 100644 --- a/ElementX/Sources/FlowCoordinators/AppLockSetupFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/AppLockSetupFlowCoordinator.swift @@ -49,7 +49,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol { /// The user is being signed out. This is a final state. case loggingOut } - + /// Events that can be triggered on the flow state machine enum Event: EventType { /// Start the flow. @@ -170,7 +170,7 @@ class AppLockSetupFlowCoordinator: FlowCoordinatorProtocol { fatalError("Unexpected transition from `\(context.fromState)` to `\(context.toState)` with event `\(String(describing: context.event))`.") } } - + private func showCreatePIN() { // Despite appLockService.isMandatory existing, we don't use that here, // to allow for cancellation when changing the PIN code within settings. diff --git a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift index 8818d39a7..5a935d54d 100644 --- a/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/AuthenticationFlowCoordinator.swift @@ -304,7 +304,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { showOAuthAuthentication(oAuthData: oAuthData, presentationAnchor: window) case .loginDirectlyWithPassword(let loginHint): stateMachine.tryEvent(.continueWithPassword, userInfo: loginHint) - + case .reportProblem: stateMachine.tryEvent(.reportProblem) case .developerOptions: @@ -451,7 +451,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { coordinator.actions .sink { [weak self] action in guard let self else { return } - + switch action { case .signedIn(let userSession): stateMachine.tryEvent(.signedIn, userInfo: userSession) @@ -509,7 +509,7 @@ class AuthenticationFlowCoordinator: FlowCoordinatorProtocol { } // MARK: - Completion - + private func userHasSignedIn(userSession: UserSessionProtocol) { delegate?.authenticationFlowCoordinator(didLoginWithSession: userSession) } diff --git a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift index 59a98d85d..1612f88dc 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinator.swift @@ -49,7 +49,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { private var cancellables = Set() private let sidebarNavigationStackCoordinator: NavigationStackCoordinator - + private let selectedRoomSubject = CurrentValueSubject(nil) private let actionsSubject: PassthroughSubject = .init() @@ -76,7 +76,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { } func stop() { } - + func isDisplayingRoomScreen(withRoomID roomID: String) -> Bool { stateMachine.isDisplayingRoomScreen(withRoomID: roomID) } @@ -94,7 +94,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { func clearRoute(animated: Bool) { roomFlowCoordinator?.clearRoute(animated: animated) } - + // MARK: - Private func asyncHandleAppRoute(_ appRoute: AppRoute, animated: Bool) async { @@ -204,7 +204,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { handleSelectRoomTransition(roomID: roomID, via: via, entryPoint: entryPoint, detailState: detailState, animated: animated) case(.roomList, .deselectRoom, .roomList): dismissRoomFlow(animated: animated) - + case(.roomList, .startSpaceFlow, .roomList): guard let spaceRoomListProxy = userInfo?.spaceRoomListProxy else { fatalError("A space room list proxy is required.") } startSpaceFlow(spaceRoomListProxy: spaceRoomListProxy, animated: animated) @@ -247,7 +247,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { presentRoomDirectorySearch() case (.roomDirectorySearchScreen, .dismissedRoomDirectorySearchScreen, .roomList): dismissRoomDirectorySearch() - + case (_, .showUserProfileScreen(let userID), .userProfileScreen): presentUserProfileScreen(userID: userID, animated: animated) case (.userProfileScreen, .dismissedUserProfileScreen, .roomList): @@ -572,7 +572,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { case .thread(let rootEventID, let focusEventID): coordinator.handleAppRoute(.thread(roomID: roomID, threadRootEventID: rootEventID, focusEventID: focusEventID), animated: animated) } - + Task { let _ = await userSession.clientProxy.trackRecentlyVisitedRoom(roomID) @@ -637,7 +637,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol { switch action { case .finished(let result): navigationSplitCoordinator.setSheetCoordinator(nil) - + switch result { case .room(let roomID): stateMachine.processEvent(.selectRoom(roomID: roomID, via: [], entryPoint: .room)) diff --git a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift index 66f3da203..fbe8f2456 100644 --- a/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/ChatsTabFlowCoordinatorStateMachine.swift @@ -15,15 +15,15 @@ class ChatsTabFlowCoordinatorStateMachine { case room(roomID: String) case space } - + /// States the AppCoordinator can find itself in enum State: StateType { /// The initial state, used before the coordinator starts case initial - + /// Showing the home screen. The `roomListSelectedRoomID` represents the timeline shown on the detail panel (if any) case roomList(detailState: DetailState?) - + /// Showing the feedback screen. case feedbackScreen(detailState: DetailState?) @@ -78,7 +78,7 @@ class ChatsTabFlowCoordinatorStateMachine { let animated: Bool var spaceRoomListProxy: SpaceRoomListProxyProtocol? } - + /// Events that can be triggered on the AppCoordinator state machine enum Event: EventType { /// Start the user session flows. @@ -152,10 +152,10 @@ class ChatsTabFlowCoordinatorStateMachine { stateMachine = StateMachine(state: .initial) configure() } - + private func configure() { stateMachine.addRoutes(event: .start, transitions: [.initial => .roomList(detailState: nil)]) - + stateMachine.addRouteMapping { event, fromState, _ in switch (fromState, event) { case (.roomList, .selectRoom(let roomID, _, _)): @@ -163,7 +163,7 @@ class ChatsTabFlowCoordinatorStateMachine { case (.roomList(let detailState), .deselectRoom): // Ignore the flow's dismissal if it has already been replaced with a space. return detailState == .space ? nil : .roomList(detailState: nil) - + case (.roomList, .startSpaceFlow): return .roomList(detailState: .space) case (.roomList, .finishedSpaceFlow): @@ -193,7 +193,7 @@ class ChatsTabFlowCoordinatorStateMachine { return .roomDirectorySearchScreen(detailState: detailState) case (.roomDirectorySearchScreen(let detailState), .dismissedRoomDirectorySearchScreen): return .roomList(detailState: detailState) - + case (_, .showUserProfileScreen): return .userProfileScreen case (.userProfileScreen, .dismissedUserProfileScreen): diff --git a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift index 69b060965..0ffc288a8 100644 --- a/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/OnboardingFlowCoordinator.swift @@ -113,7 +113,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { } actionsSubject.send(.requestPresentation(animated: !isNewLogin)) - + stateMachine.tryEvent(.next) } @@ -203,7 +203,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { case (.notificationPermissions, _, _, _, _): return .finished - + default: return nil } @@ -381,7 +381,7 @@ class OnboardingFlowCoordinator: FlowCoordinatorProtocol { appLockFlowCoordinator = coordinator coordinator.start() } - + private func presentAnalyticsPromptScreen() { let coordinator = AnalyticsPromptScreenCoordinator(analytics: analyticsService, termsURL: appSettings.analyticsTermsURL) diff --git a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift index ef0808b9d..985aa3700 100644 --- a/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/PinnedEventsTimelineFlowCoordinator.swift @@ -156,7 +156,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol { .store(in: &cancellables) stackCoordinator.setRootCoordinator(coordinator) - + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) } } diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift index e495f2ae0..a568860bd 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinator.swift @@ -119,7 +119,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { setupStateMachine() } - + // MARK: - FlowCoordinatorProtocol func start(animated: Bool) { @@ -357,7 +357,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { MXLog.warning("Found an existing proxy, returning.") return } - + await roomProxy.subscribeForUpdates() // Make sure not to set this until after the subscription has succeeded, otherwise the @@ -399,15 +399,15 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } case (_, .dismissFlow, .complete): dismissFlow(animated: animated) - + case (.room, .presentPinnedEventsTimeline, .pinnedEventsTimeline): startPinnedEventsTimelineFlow() // Thread List - + case (.room, .presentThreadList, .threadList): Task { await self.presentThreadList(animated: animated) } - + // Thread case (_, .presentThread(let threadRootEventID, let focusEventID), .thread): @@ -443,13 +443,13 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (_, .presentMessageForwarding(let forwardingItem), .messageForwarding): presentMessageForwarding(with: forwardingItem) - + case (_, .presentMapNavigator(let mode), .mapNavigator): guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else { fatalError("Missing required TimelineController") } presentMapNavigator(interactionMode: mode, timelineController: timelineController, animated: animated) - + case (_, .presentPollForm(let mode), .pollForm): guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else { fatalError("Missing required TimelineController") @@ -460,7 +460,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { presentResolveSendFailure(failure: failure, sendHandle: sendHandle) // Room Details - + case (.initial, .presentRoomDetails, .roomDetails(let isRoot)), (.room, .presentRoomDetails, .roomDetails(let isRoot)), (.roomDetails, .presentRoomDetails, .roomDetails(let isRoot)): @@ -482,7 +482,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { presentRolesAndPermissionsScreen() case (.rolesAndPermissions, .dismissRolesAndPermissionsScreen, .roomDetails): rolesAndPermissionsFlowCoordinator = nil - + case (.roomDetails, .presentMediaEventsTimeline, .mediaEventsTimeline): Task { await self.startMediaEventsTimelineFlow() } @@ -519,7 +519,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { startMembersFlow(entryPoint: entryPoint, animated: animated) case (.membersFlow, .stopMembersFlow, _): membersFlowCoordinator = nil - + case (_, .startChildFlow(let roomID, let via, let entryPoint), .presentingChild): startChildFlow(for: roomID, via: via, entryPoint: entryPoint) case (.presentingChild, .dismissChildFlow, _): @@ -530,7 +530,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (.notificationSettings, .presentGlobalNotificationSettingsScreen, .globalNotificationSettings): presentGlobalNotificationSettingsScreen() - + case (.pollsHistory, .presentPollForm(let mode), .pollsHistoryForm): guard let timelineController = (context.userInfo as? EventUserInfo)?.timelineController else { fatalError("Missing required TimelineController") @@ -549,7 +549,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case (_, .presentTransferOwnershipScreen, .transferOwnershipScreen): presentTransferOwnershipScreen() - + default: break } @@ -642,7 +642,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { self?.stateMachine.tryEvent(.dismissFlow) } } - + switch presentationAction { case .share(.mediaFiles(_, let mediaFiles)): stateMachine.tryEvent(.presentMediaUploadPreview(mediaURLs: mediaFiles.map(\.url), caption: nil), @@ -846,7 +846,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } .store(in: &cancellables) - + navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in guard let self else { return } stateMachine.tryEvent(.dismissThread) @@ -1098,16 +1098,16 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { userInfo: EventUserInfo(animated: animated, timelineController: timelineController)) } } - + stackCoordinator.setRootCoordinator(mediaPickerCoordinator) - + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in if case .mediaUploadPicker = self?.stateMachine.state { self?.stateMachine.tryEvent(.dismissMediaUploadPicker) } } } - + private func presentMediaUploadPreviewScreen(for mediaURLs: [URL], caption: NSAttributedString?, timelineController: TimelineControllerProtocol, @@ -1119,7 +1119,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } else { nil } - + let parameters = MediaUploadPreviewScreenCoordinatorParameters(mediaURLs: mediaURLs, caption: caption, title: title, @@ -1129,7 +1129,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { timelineController: timelineController, clientProxy: userSession.clientProxy, userIndicatorController: flowParameters.userIndicatorController) - + let mediaUploadPreviewScreenCoordinator = MediaUploadPreviewScreenCoordinator(parameters: parameters) mediaUploadPreviewScreenCoordinator.actions @@ -1142,7 +1142,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } .store(in: &cancellables) - + stackCoordinator.setRootCoordinator(mediaUploadPreviewScreenCoordinator) navigationStackCoordinator.setSheetCoordinator(stackCoordinator, animated: animated) { [weak self] in @@ -1174,12 +1174,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { self?.stateMachine.tryEvent(.dismissEmojiPicker) } } - + private func presentMapNavigator(interactionMode: LocationSharingInteractionMode, timelineController: TimelineControllerProtocol, animated: Bool) { let stackCoordinator = NavigationStackCoordinator() - + let params = LocationSharingScreenCoordinatorParameters(interactionMode: interactionMode, mapURLBuilder: flowParameters.appSettings.mapTilerConfiguration, roomProxy: roomProxy, @@ -1214,7 +1214,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { analytics: flowParameters.analytics, userIndicatorController: flowParameters.userIndicatorController)) stackCoordinator.setRootCoordinator(coordinator) - + coordinator.actions .sink { [weak self] action in guard let self else { return } @@ -1225,7 +1225,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { } } .store(in: &cancellables) - + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in self?.stateMachine.tryEvent(.dismissPollForm) } @@ -1237,7 +1237,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { let timelineItemFactory = RoomTimelineItemFactory(userID: userID, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID)) - + let timelineController = flowParameters.timelineControllerFactory.buildTimelineController(roomProxy: roomProxy, initialFocussedEventID: nil, timelineItemFactory: timelineItemFactory, @@ -1291,7 +1291,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { .store(in: &cancellables) stackCoordinator.setRootCoordinator(coordinator) - + navigationStackCoordinator.setSheetCoordinator(stackCoordinator) { [weak self] in self?.stateMachine.tryEvent(.dismissMessageForwarding) } @@ -1341,12 +1341,12 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { private func presentInviteUsersScreen(flow: InviteUsersFlow = .existingRoom) { let stackCoordinator = NavigationStackCoordinator() - + let roomType: InviteUsersScreenRoomType = switch flow { case .existingRoom: .existingRoom(roomProxy: roomProxy) case .newRoom(let mandatoryInvitee): .draft(mandatoryInvitees: [mandatoryInvitee]) } - + let inviteParameters = InviteUsersScreenCoordinatorParameters(userSession: userSession, roomType: roomType, isSkippable: false, @@ -1617,7 +1617,7 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol { case .viewInRoomTimeline(let itemID): guard let eventID = itemID.eventID else { MXLog.error("Unable to present room timeline for event \(itemID)") - + return } stateMachine.tryEvent(.presentRoom(presentationAction: .eventFocus(.init(eventID: eventID, shouldSetPin: false))), diff --git a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift index e9d17eb48..94bb357e5 100644 --- a/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift +++ b/ElementX/Sources/FlowCoordinators/RoomFlowCoordinatorStateMachine.swift @@ -12,11 +12,11 @@ import SwiftState extension RoomFlowCoordinator { struct HashableRoomMemberWrapper: Hashable { let value: RoomMemberProxyProtocol - + static func == (lhs: HashableRoomMemberWrapper, rhs: HashableRoomMemberWrapper) -> Bool { lhs.value.userID == rhs.value.userID } - + func hash(into hasher: inout Hasher) { hasher.combine(value.userID) } @@ -54,7 +54,7 @@ extension RoomFlowCoordinator { } } } - + indirect enum State: StateType { case initial case joinRoomScreen @@ -103,7 +103,7 @@ extension RoomFlowCoordinator { var spaceRoomListProxy: SpaceRoomListProxyProtocol? var authorizedSpacesSelection: AuthorizedSpacesSelection? } - + enum Event: EventType { case presentJoinRoomScreen(via: [String]) case dismissJoinRoomScreen @@ -114,7 +114,7 @@ extension RoomFlowCoordinator { case presentThread(threadRootEventID: String, focusEventID: String?) case dismissThread - + case presentThreadList case dismissThreadList @@ -138,7 +138,7 @@ extension RoomFlowCoordinator { case presentInviteUsersScreen(flow: InviteUsersFlow) case dismissInviteUsersScreen - + case presentMediaUploadPicker(mode: MediaPickerScreenMode, caption: NSAttributedString) case dismissMediaUploadPicker @@ -147,13 +147,13 @@ extension RoomFlowCoordinator { case presentEmojiPicker(itemID: TimelineItemIdentifier, selectedEmojis: Set) case dismissEmojiPicker - + case presentMapNavigator(interactionMode: LocationSharingInteractionMode) case dismissMapNavigator case presentMessageForwarding(forwardingItem: MessageForwardingItem) case dismissMessageForwarding - + case presentPollForm(mode: PollFormMode) case dismissPollForm @@ -212,21 +212,21 @@ extension RoomFlowCoordinator { case (.room, .presentMediaUploadPicker(let mode, let caption)): return .mediaUploadPicker(mode: mode, caption: caption, previousState: fromState) - + case (.room, .presentMediaUploadPreview(let mediaURLs, let caption)): return .mediaUploadPreview(mediaURLs: mediaURLs, caption: caption, previousState: fromState) case (.room, .presentEmojiPicker(let itemID, let selectedEmoji)): return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState) - + case (.room, .presentMessageForwarding(let forwardingItem)): return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState) case (.mediaEventsTimeline, .presentMessageForwarding(forwardingItem: let forwardingItem)): return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState) - + case (.room, .presentMapNavigator): return .mapNavigator(previousState: fromState) - + case (.room, .presentPollForm): return .pollForm(previousState: fromState) @@ -247,7 +247,7 @@ extension RoomFlowCoordinator { return .room case (.threadList, .presentThread(let threadRootEventID, _)): return .thread(threadRootEventID: threadRootEventID, previousState: fromState) - + // Thread case (.room, .presentThread(let threadRootEventID, _)): return .thread(threadRootEventID: threadRootEventID, previousState: fromState) @@ -261,19 +261,19 @@ extension RoomFlowCoordinator { case (.thread, .presentMediaUploadPicker(let mode, let caption)): return .mediaUploadPicker(mode: mode, caption: caption, previousState: fromState) - + case (.thread, .presentMediaUploadPreview(let mediaURLs, let caption)): return .mediaUploadPreview(mediaURLs: mediaURLs, caption: caption, previousState: fromState) case (.thread, .presentEmojiPicker(let itemID, let selectedEmoji)): return .emojiPicker(itemID: itemID, selectedEmojis: selectedEmoji, previousState: fromState) - + case (.thread, .presentMessageForwarding(let forwardingItem)): return .messageForwarding(forwardingItem: forwardingItem, previousState: fromState) - + case (.thread, .presentMapNavigator): return .mapNavigator(previousState: fromState) - + case (.thread, .presentPollForm): return .pollForm(previousState: fromState) @@ -372,7 +372,7 @@ extension RoomFlowCoordinator { return .membersFlow(previousState: fromState) case (.membersFlow(let previousState), .stopMembersFlow): return previousState - + case (_, .startChildFlow(let roomID, _, _)): return .presentingChild(childRoomID: roomID, previousState: fromState) case (.presentingChild(_, let previousState), .dismissChildFlow): @@ -413,7 +413,7 @@ extension RoomFlowCoordinator { return .transferOwnershipScreen(previousState: fromState) case (.transferOwnershipScreen(let previousState), .dismissedTransferOwnershipScreen): return previousState - + default: return nil } diff --git a/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift index 8c38630c9..b7f292b6c 100644 --- a/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/RoomMembersFlowCoordinator.swift @@ -42,7 +42,7 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol { case start case presentRoomMembersList - + case presentRoomMemberDetails(userID: String) case dismissedRoomMemberDetails @@ -246,7 +246,7 @@ final class RoomMembersFlowCoordinator: FlowCoordinatorProtocol { } } .store(in: &cancellables) - + navigationStackCoordinator.push(coordinator, animated: animated) { [weak self] in guard let self else { return } if case let .roomMemberDetails(_, previousState) = stateMachine.state, diff --git a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift index 5fea12afb..71b615ad7 100644 --- a/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SettingsFlowCoordinator.swift @@ -238,7 +238,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol { userIndicatorController: flowParameters.userIndicatorController)) navigationStackCoordinator.push(coordinator) } - + private func presentNotificationSettings() { let notificationParameters = NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator, userSession: flowParameters.userSession, diff --git a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift index 3610a4088..3fbfcc7d3 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceFlowCoordinator.swift @@ -203,22 +203,22 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { return .space case (.initial, .startUnjoined): return .joinSpace - + case (.joinSpace, .joinedSpace): return .space case (.space, .leftSpace): return .leftSpace - + case (.space, .addRooms): return .addingRooms case (.addingRooms, .dismissedAddRooms): return .space - + case (.space, .presentTransferOwnership): return .transferOwnership case (.transferOwnership, .dismissedTransferOwnership): return .space - + case (.space, .startChildFlow): guard let childEntryPoint = userInfo as? SpaceFlowCoordinatorEntryPoint else { fatalError("An entry point must be provided.") } return .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: fromState) @@ -227,34 +227,34 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { return .presentingChild(childSpaceID: childEntryPoint.spaceID, previousState: previousState) case (.presentingChild(_, let previousState), .stopChildFlow): return previousState - + case (.space, .startRoomFlow): return .roomFlow(previousState: fromState) case (.roomFlow, .startRoomFlow): return fromState // Ignore tapping on multiple rooms at the same time case (.roomFlow(let previousState), .stopRoomFlow): return previousState - + case (.space, .startMembersFlow): return .membersFlow case (.membersFlow, .stopMembersFlow): return .space - + case (.space, .startSettingsFlow): return .settingsFlow case (.settingsFlow, .stopSettingsFlow): return .space - + case (.space, .startRolesAndPermissionsFlow): return .rolesAndPermissionsFlow case (.rolesAndPermissionsFlow, .stopRolesAndPermissionsFlow): return .space - + case (.space, .startCreateChildRoomFlow): return .createChildRoomFlow case (.createChildRoomFlow, .stopCreateChildRoomFlow): return .space - + default: return nil } @@ -268,23 +268,23 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { presentSpace() case (.initial, .startUnjoined, .joinSpace): presentJoinSpaceScreen() - + case (.joinSpace, .joinedSpace, .space): presentSpaceAfterJoining() case (.space, .leftSpace, .leftSpace): clearRoute(animated: true) - + case (.space, .addRooms, .addingRooms): presentSpaceAddRoomsScreen() case (.addingRooms, .dismissedAddRooms, .space): break - + case (.space, .presentTransferOwnership, .transferOwnership): guard let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } presentTransferOwnershipScreen(roomProxy: roomProxy) case (.transferOwnership, .dismissedTransferOwnership, .space): break - + case (.space, .startChildFlow, .presentingChild), (.roomFlow, .startChildFlow, .presentingChild): guard let entryPoint = context.userInfo as? SpaceFlowCoordinatorEntryPoint else { return } @@ -292,7 +292,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case (.presentingChild, .stopChildFlow, _): childSpaceFlowCoordinator = nil selectedSpaceRoomSubject.send(nil) - + case (.space, .startRoomFlow(let roomID), .roomFlow): startRoomFlow(roomID: roomID) case (.roomFlow, .startRoomFlow, .roomFlow): @@ -300,7 +300,7 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { case (.roomFlow, .stopRoomFlow, _): roomFlowCoordinator = nil selectedSpaceRoomSubject.send(nil) - + case (.space, .startMembersFlow, .membersFlow): guard let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { fatalError("The room proxy must always be provided") @@ -308,25 +308,25 @@ class SpaceFlowCoordinator: FlowCoordinatorProtocol { startMembersFlow(roomProxy: roomProxy) case (.membersFlow, .stopMembersFlow, .space): membersFlowCoordinator = nil - + case (.space, .startSettingsFlow, .settingsFlow): guard let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } startSettingsFlow(roomProxy: roomProxy) case (.settingsFlow, .stopSettingsFlow, .space): settingsFlowCoordinator = nil - + case (.space, .startRolesAndPermissionsFlow, .rolesAndPermissionsFlow): guard let roomProxy = context.userInfo as? JoinedRoomProxyProtocol else { return } startRolesAndPermissionsFlow(roomProxy: roomProxy) case (.rolesAndPermissionsFlow, .stopRolesAndPermissionsFlow, .space): rolesAndPermissionsFlowCoordinator = nil - + case (.space, .startCreateChildRoomFlow, .createChildRoomFlow): guard let space = context.userInfo as? SpaceServiceRoom else { fatalError("The space is missing") } startCreateChildFlow(space: space) case (.createChildRoomFlow, .stopCreateChildRoomFlow, .space): createChildRoomFlowCoordinator = nil - + default: break } diff --git a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift index 29cd5888a..6d4c6622c 100644 --- a/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpaceSettingsFlowCoordinator.swift @@ -55,7 +55,7 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { case presentManageAuthorizedSpacesScreen case dismissedManageAuthorizedSpacesScreen - + case presentTransferOwnership case dismissedTransferOwnership @@ -84,7 +84,7 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(roomProxy: JoinedRoomProxyProtocol, navigationStackCoordinator: NavigationStackCoordinator, flowParameters: CommonFlowParameters) { @@ -140,7 +140,7 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { return .manageAuthorizedSpacesScreen case (.manageAuthorizedSpacesScreen, .dismissedManageAuthorizedSpacesScreen): return .securityAndPrivacy - + case (.spaceSettings, .presentTransferOwnership): return .transferOwnership case (.transferOwnership, .dismissedTransferOwnership): @@ -191,12 +191,12 @@ final class SpaceSettingsFlowCoordinator: FlowCoordinatorProtocol { presentManageAuthorizedSpacesScreen(selection: selection) case (.manageAuthorizedSpacesScreen, .dismissedManageAuthorizedSpacesScreen, .securityAndPrivacy): break - + case (.spaceSettings, .presentTransferOwnership, .transferOwnership): presentTransferOwnershipScreen() case (.transferOwnership, .dismissedTransferOwnership, .spaceSettings): break - + case (.spaceSettings, .startMembersListFlow, .membersFlow): startMembersListFlow() case (.membersFlow, .stopMembersListFlow, .spaceSettings): diff --git a/ElementX/Sources/FlowCoordinators/SpacesTabFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/SpacesTabFlowCoordinator.swift index 8bd8bf785..418e925ec 100644 --- a/ElementX/Sources/FlowCoordinators/SpacesTabFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/SpacesTabFlowCoordinator.swift @@ -100,7 +100,7 @@ class SpacesTabFlowCoordinator: FlowCoordinatorProtocol { stateMachine.addRoutes(event: .start, transitions: [.initial => .spacesScreen(selectedSpaceID: nil)]) { [weak self] _ in self?.presentSpacesScreen() } - + stateMachine.addRouteMapping { event, fromState, userInfo in guard event == .selectSpace, case .spacesScreen = fromState else { return nil } guard let spaceRoomListProxy = userInfo as? SpaceRoomListProxyProtocol else { fatalError("A space proxy must be provided.") } diff --git a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift index 58d2b406d..f6555662b 100644 --- a/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift +++ b/ElementX/Sources/FlowCoordinators/UserSessionFlowCoordinator.swift @@ -478,7 +478,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol { navigationTabCoordinator.setOverlayCoordinator(nil) } - + // MARK: - Logout private func runLogoutFlow() async { diff --git a/ElementX/Sources/Mocks/AggregatedReactionMock.swift b/ElementX/Sources/Mocks/AggregatedReactionMock.swift index cdd598806..4fa0ecca3 100644 --- a/ElementX/Sources/Mocks/AggregatedReactionMock.swift +++ b/ElementX/Sources/Mocks/AggregatedReactionMock.swift @@ -26,7 +26,7 @@ extension AggregatedReaction { private static var alice: String { RoomMemberProxyMock.mockAlice.userID } - + static var mockReactions: [AggregatedReaction] { [ mockReaction(key: "😅", senderIDs: [alice]), diff --git a/ElementX/Sources/Mocks/ClientProxyMock.swift b/ElementX/Sources/Mocks/ClientProxyMock.swift index f29a12e97..f6668ae52 100644 --- a/ElementX/Sources/Mocks/ClientProxyMock.swift +++ b/ElementX/Sources/Mocks/ClientProxyMock.swift @@ -159,7 +159,7 @@ extension ClientProxyMock { underlyingTimelineMediaVisibilityPublisher = CurrentValueSubject(configuration.timelineMediaVisibility).asCurrentValuePublisher() underlyingHideInviteAvatarsPublisher = CurrentValueSubject(configuration.hideInviteAvatars).asCurrentValuePublisher() - + liveLocationOwnInfoUpdatesPublisher = PassthroughSubject().eraseToAnyPublisher() underlyingMaxMediaUploadSize = .success(configuration.maxMediaUploadSize) diff --git a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift index a971036be..50f221a9d 100644 --- a/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift +++ b/ElementX/Sources/Mocks/JoinedRoomProxyMock.swift @@ -53,21 +53,21 @@ extension JoinedRoomProxyMock { @MainActor convenience init(_ configuration: JoinedRoomProxyMockConfiguration) { self.init() - + id = configuration.id - + timeline = TimelineProxyMock(.init(isAutoUpdating: configuration.shouldUseAutoUpdatingTimeline, timelineStartReached: configuration.timelineStartReached)) pinnedEventsTimelineReturnValue = .failure(.failedCreatingPinnedTimeline) - + ownUserID = configuration.ownUserID membersPublisher = CurrentValueSubject(configuration.members).asCurrentValuePublisher() knockRequestsStatePublisher = CurrentValueSubject(configuration.knockRequestsState).asCurrentValuePublisher() typingMembersPublisher = CurrentValueSubject([]).asCurrentValuePublisher() identityStatusChangesPublisher = CurrentValueSubject([]).asCurrentValuePublisher() - + makeLiveLocationServiceReturnValue = RoomLiveLocationServiceMock(.init()) updateMembersClosure = { } @@ -82,7 +82,7 @@ extension JoinedRoomProxyMock { ignoreDeviceTrustAndResendDevicesSendHandleReturnValue = .success(()) withdrawVerificationAndResendUserIDsSendHandleReturnValue = .success(()) - + flagAsUnreadReturnValue = .success(()) markAsReadReceiptTypeReturnValue = .success(()) flagAsFavouriteReturnValue = .success(()) diff --git a/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift b/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift index 985b4edeb..db996c5e1 100644 --- a/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift +++ b/ElementX/Sources/Mocks/NotificationSettingsProxyMock.swift @@ -51,7 +51,7 @@ extension NotificationSettingsProxyMock { self.callbacks.send(.settingsDidChange) } } - + restoreDefaultNotificationModeRoomIdClosure = { [weak self] _ in guard let self else { return } self.getNotificationSettingsRoomIdIsEncryptedIsOneToOneReturnValue = RoomNotificationSettingsProxyMock(with: .init(mode: configuration.defaultRoomMode, isDefault: true)) diff --git a/ElementX/Sources/Mocks/PollMock.swift b/ElementX/Sources/Mocks/PollMock.swift index f2eca4ea7..d52ea0a3d 100644 --- a/ElementX/Sources/Mocks/PollMock.swift +++ b/ElementX/Sources/Mocks/PollMock.swift @@ -23,7 +23,7 @@ extension Poll { endDate: ended ? Date() : nil, createdByAccountOwner: createdByAccountOwner) } - + static func disclosed(createdByAccountOwner: Bool = false) -> Self { mock(question: "What country do you like most?", pollKind: .disclosed, @@ -32,7 +32,7 @@ extension Poll { .mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)], createdByAccountOwner: createdByAccountOwner) } - + static func undisclosed(createdByAccountOwner: Bool = false) -> Self { mock(question: "What country do you like most?", pollKind: .undisclosed, @@ -41,7 +41,7 @@ extension Poll { .mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)], createdByAccountOwner: createdByAccountOwner) } - + static var endedDisclosed: Self { mock(question: "What country do you like most?", pollKind: .disclosed, @@ -50,7 +50,7 @@ extension Poll { .mock(text: "USA 🇺🇸", votes: 2, allVotes: 10)], ended: true) } - + static var endedUndisclosed: Self { mock(question: "What country do you like most?", pollKind: .undisclosed, diff --git a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift index f803e28a5..66e26f1df 100644 --- a/ElementX/Sources/Mocks/RoomMemberProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomMemberProxyMock.swift @@ -37,7 +37,7 @@ extension RoomMemberProxyMock { powerLevel = configuration.powerLevel } - + /// Mocks static var mockMe: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@me:matrix.org", @@ -73,19 +73,19 @@ extension RoomMemberProxyMock { displayName: "Alice", membership: .invite)) } - + static var mockBob: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@bob:matrix.org", displayName: "Bob", membership: .join)) } - + static var mockCharlie: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@charlie:matrix.org", displayName: "Charlie", membership: .join)) } - + static var mockDan: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@dan:matrix.org", displayName: "Dan", @@ -110,7 +110,7 @@ extension RoomMemberProxyMock { membership: .invite, isIgnored: true)) } - + static var mockIgnored: RoomMemberProxyMock { RoomMemberProxyMock(with: .init(userID: "@ignored:matrix.org", displayName: "Ignored", diff --git a/ElementX/Sources/Mocks/RoomNotificationSettingsProxyMock.swift b/ElementX/Sources/Mocks/RoomNotificationSettingsProxyMock.swift index f07092f82..2feeae18a 100644 --- a/ElementX/Sources/Mocks/RoomNotificationSettingsProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomNotificationSettingsProxyMock.swift @@ -17,7 +17,7 @@ struct RoomNotificationSettingsProxyMockConfiguration { extension RoomNotificationSettingsProxyMock { convenience init(with configuration: RoomNotificationSettingsProxyMockConfiguration) { self.init() - + isDefault = configuration.isDefault mode = configuration.mode } diff --git a/ElementX/Sources/Mocks/RoomPowerLevelsProxyMock.swift b/ElementX/Sources/Mocks/RoomPowerLevelsProxyMock.swift index d96f652fb..04da567fa 100644 --- a/ElementX/Sources/Mocks/RoomPowerLevelsProxyMock.swift +++ b/ElementX/Sources/Mocks/RoomPowerLevelsProxyMock.swift @@ -27,7 +27,7 @@ extension RoomPowerLevelsProxyMock { self.init() underlyingValues = RoomPowerLevelsValues.mock - + canOwnUserSendMessageReturnValue = configuration.canUserSendMessage canOwnUserSendStateEventReturnValue = configuration.canUserSendState canOwnUserInviteReturnValue = configuration.canUserInvite diff --git a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift index 969ed2de9..02d4ae95e 100644 --- a/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift +++ b/ElementX/Sources/Mocks/RoomSummaryProviderMock.swift @@ -289,7 +289,7 @@ extension Array where Element == RoomSummary { static let mockRoomsWithNotificationsState: [Element] = { var result: [Element] = [] - + // Iterate over settings modes for mode in RoomNotificationModeProxy.allCases { // Iterate over unread messages states diff --git a/ElementX/Sources/Mocks/SessionVerificationControllerProxyMock.swift b/ElementX/Sources/Mocks/SessionVerificationControllerProxyMock.swift index e34bd6599..8581a7bd3 100644 --- a/ElementX/Sources/Mocks/SessionVerificationControllerProxyMock.swift +++ b/ElementX/Sources/Mocks/SessionVerificationControllerProxyMock.swift @@ -16,7 +16,7 @@ extension SessionVerificationControllerProxyMock { SessionVerificationEmoji(symbol: "🎂", description: "Cake"), SessionVerificationEmoji(symbol: "🏁", description: "Flag"), SessionVerificationEmoji(symbol: "🌏", description: "Globe")] - + static func configureMock(actions: PassthroughSubject = .init(), isVerified: Bool = false, otherDeviceStartsSasVerification: Bool = false, @@ -25,7 +25,7 @@ extension SessionVerificationControllerProxyMock { mock.underlyingActions = actions mock.acknowledgeVerificationRequestDetailsReturnValue = .success(()) - + mock.requestDeviceVerificationClosure = { [weak mock] in Task.detached { guard let mock else { return } @@ -41,26 +41,26 @@ extension SessionVerificationControllerProxyMock { mock.actions.send(.receivedVerificationData(emojis)) } } - + return .success(()) } - + mock.startSasVerificationClosure = { [weak mock] in Task.detached { guard let mock else { return } try await Task.sleep(for: requestDelay) mock.actions.send(.startedSasVerification) - + Task.detached { try await Task.sleep(for: requestDelay) mock.actions.send(.receivedVerificationData(emojis)) } } - + return .success(()) } - + mock.approveVerificationClosure = { [weak mock] in Task.detached { guard let mock else { return } @@ -68,10 +68,10 @@ extension SessionVerificationControllerProxyMock { try await Task.sleep(for: requestDelay) mock.actions.send(.finished) } - + return .success(()) } - + mock.declineVerificationClosure = { [weak mock] in Task.detached { guard let mock else { return } @@ -79,7 +79,7 @@ extension SessionVerificationControllerProxyMock { try await Task.sleep(for: requestDelay) mock.actions.send(.cancelled) } - + return .success(()) } @@ -90,7 +90,7 @@ extension SessionVerificationControllerProxyMock { try await Task.sleep(for: requestDelay) mock.actions.send(.cancelled) } - + return .success(()) } diff --git a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift index d9f89fc64..7ec55ceb7 100644 --- a/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift +++ b/ElementX/Sources/Mocks/SpaceServiceProxyMock.swift @@ -61,7 +61,7 @@ extension SpaceServiceProxyMock.Configuration { partialResult.append(SpaceServiceFilter(room: spaceRoom, level: 0, descendants: .init())) partialResult.append(SpaceServiceFilter(room: spaceRoom, level: 1, descendants: .init())) } - + let spaceRoomLists = [SpaceServiceRoom].mockJoinedSpaces.map { ($0.id, SpaceRoomListProxyMock(.init(spaceServiceRoom: $0, initialSpaceRooms: .mockSpaceList))) } diff --git a/ElementX/Sources/Mocks/UserProfile+Mock.swift b/ElementX/Sources/Mocks/UserProfile+Mock.swift index 27a8b8ecf..b2650e980 100644 --- a/ElementX/Sources/Mocks/UserProfile+Mock.swift +++ b/ElementX/Sources/Mocks/UserProfile+Mock.swift @@ -13,15 +13,15 @@ extension UserProfileProxy { static var mockAlice: UserProfileProxy { .init(userID: "@alice:matrix.org", displayName: "Alice", avatarURL: "mxc://matrix.org/UcCimidcvpFvWkPzvjXMQPHA") } - + static var mockBob: UserProfileProxy { .init(userID: "@bob:matrix.org", displayName: "Bob", avatarURL: nil) } - + static var mockBobby: UserProfileProxy { .init(userID: "@bobby:matrix.org", displayName: "Bobby", avatarURL: nil) } - + static var mockCharlie: UserProfileProxy { .init(userID: "@charlie:matrix.org", displayName: "Charlie", avatarURL: nil) } diff --git a/ElementX/Sources/Other/AccessibilityIdentifiers.swift b/ElementX/Sources/Other/AccessibilityIdentifiers.swift index 2e2dd6649..69159ab48 100644 --- a/ElementX/Sources/Other/AccessibilityIdentifiers.swift +++ b/ElementX/Sources/Other/AccessibilityIdentifiers.swift @@ -75,13 +75,13 @@ enum A11yIdentifiers { let changePIN = "app_lock_setup_settings-change_pin" let removePIN = "app_lock_setup_settings-remove_pin" } - + struct AnalyticsPromptScreen { let title = "analytics_prompt-title" let enable = "analytics_prompt-enable" let notNow = "analytics_prompt-not_now" } - + struct BugReportScreen { let report = "bug_report-report" let sendLogs = "bug_report-send_logs" @@ -164,9 +164,9 @@ enum A11yIdentifiers { let messageComposer = "room-message_composer" let sendButton = "room-send_button" - + let composerToolbar = ComposerToolbar() - + struct ComposerToolbar { let bold = "composer_toolbar-bold" let italic = "composer_toolbar-italic" @@ -292,13 +292,13 @@ enum A11yIdentifiers { let mediaPicker = "create_room-media_picker" let filePicker = "create_room-file_picker" } - + struct PollFormScreen { let addOption = "poll_form-add_option" let pollKind = "poll_form-kind" let question = "poll_form-question" let submit = "poll_form-submit" - + private let optionPrefix = "poll_form-option" func optionID(_ index: Int) -> String { "\(optionPrefix)-\(index)" diff --git a/ElementX/Sources/Other/Avatars.swift b/ElementX/Sources/Other/Avatars.swift index 9c0eb7154..b5e05a021 100644 --- a/ElementX/Sources/Other/Avatars.swift +++ b/ElementX/Sources/Other/Avatars.swift @@ -15,7 +15,7 @@ enum Avatars { case room(on: RoomAvatarSizeOnScreen) /// custom case custom(CGFloat) - + /// Value in UIKit points var value: CGFloat { switch self { @@ -27,7 +27,7 @@ enum Avatars { return val } } - + /// Value in pixels by using the scale of the main screen var scaledValue: CGFloat { value * UIScreen.main.scale @@ -87,7 +87,7 @@ enum UserAvatarSizeOnScreen { case threadSummary case map case classicAppAccount - + var value: CGFloat { switch self { case .readReceipt: @@ -143,7 +143,7 @@ enum RoomAvatarSizeOnScreen { case spaceAddRoomsSelected case completionSuggestions case createRoomSelectSpace - + var value: CGFloat { switch self { case .notificationSettings: diff --git a/ElementX/Sources/Other/BlurHashDecode.swift b/ElementX/Sources/Other/BlurHashDecode.swift index 7aa6e169c..134b70312 100644 --- a/ElementX/Sources/Other/BlurHashDecode.swift +++ b/ElementX/Sources/Other/BlurHashDecode.swift @@ -27,16 +27,16 @@ import UIKit public extension UIImage { convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { guard blurHash.count >= 6 else { return nil } - + let sizeFlag = String(blurHash[0]).decode83() let numY = (sizeFlag / 9) + 1 let numX = (sizeFlag % 9) + 1 - + let quantisedMaximumValue = String(blurHash[1]).decode83() let maximumValue = Float(quantisedMaximumValue + 1) / 166 - + guard blurHash.count == 4 + 2 * numX * numY else { return nil } - + let colours: [(Float, Float, Float)] = (0.. (Float, Float, Float let quantR = value / (19 * 19) let quantG = (value / 19) % 19 let quantB = value % 19 - + return (signPow((Float(quantR) - 9) / 9, 2) * maximumValue, signPow((Float(quantG) - 9) / 9, 2) * maximumValue, signPow((Float(quantB) - 9) / 9, 2) * maximumValue) @@ -149,7 +149,7 @@ private extension String { subscript(offset: Int) -> Character { self[index(startIndex, offsetBy: offset)] } - + subscript(bounds: CountableRange) -> Substring { let start = index(startIndex, offsetBy: bounds.lowerBound) let end = index(startIndex, offsetBy: bounds.upperBound) diff --git a/ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleReactionLayout.swift b/ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleReactionLayout.swift index 6a2bf4d00..e63dd1f05 100644 --- a/ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleReactionLayout.swift +++ b/ElementX/Sources/Other/CollapsibleFlowLayout/CollapsibleReactionLayout.swift @@ -59,7 +59,7 @@ struct CollapsibleReactionLayout: Layout { } return } - + // Calculate the layout of the rows with the reactions button and add more button let reactionsAndAddMore = calculateRows(proposal: ProposedViewSize(bounds.size), subviews: Array(subviewsByType.reactions + [subviewsByType.addMoreButton])) // If we have extended beyond the defined number of rows we are showing the expand/collapse ui diff --git a/ElementX/Sources/Other/Consumable.swift b/ElementX/Sources/Other/Consumable.swift index 66ad25725..571b93ac3 100644 --- a/ElementX/Sources/Other/Consumable.swift +++ b/ElementX/Sources/Other/Consumable.swift @@ -20,9 +20,9 @@ import Foundation value = newValue } } - + private var value: Value? - + init(value: Value? = nil) { self.value = value } diff --git a/ElementX/Sources/Other/ExpiringTaskRunner.swift b/ElementX/Sources/Other/ExpiringTaskRunner.swift index ecb470571..d0540f40c 100644 --- a/ElementX/Sources/Other/ExpiringTaskRunner.swift +++ b/ElementX/Sources/Other/ExpiringTaskRunner.swift @@ -16,7 +16,7 @@ actor ExpiringTaskRunner { private var continuation: CheckedContinuation? private var task: @Sendable () async throws -> T - + init(_ task: @escaping @Sendable () async throws -> T) { self.task = task } diff --git a/ElementX/Sources/Other/Extensions/AVMetadataMachineReadableCodeObject.swift b/ElementX/Sources/Other/Extensions/AVMetadataMachineReadableCodeObject.swift index 3a48d9f62..4e199dcad 100644 --- a/ElementX/Sources/Other/Extensions/AVMetadataMachineReadableCodeObject.swift +++ b/ElementX/Sources/Other/Extensions/AVMetadataMachineReadableCodeObject.swift @@ -81,7 +81,7 @@ private extension [Bit] { return value } - + mutating func takeBits16(_ count: Int) throws(BitError) -> UInt16 { guard count <= 16 else { throw .moreThan16BitsTaken } @@ -92,7 +92,7 @@ private extension [Bit] { return value } - + mutating func takeUInt8() throws(BitError) -> UInt8 { try takeBits(8) } diff --git a/ElementX/Sources/Other/Extensions/Alert.swift b/ElementX/Sources/Other/Extensions/Alert.swift index f175163ce..94e6355e7 100644 --- a/ElementX/Sources/Other/Extensions/Alert.swift +++ b/ElementX/Sources/Other/Extensions/Alert.swift @@ -23,7 +23,7 @@ extension View { }) return alert(item.wrappedValue?.title ?? "", isPresented: binding, presenting: item.wrappedValue, actions: actions, message: message) } - + // periphery: ignore - not used yet but might be useful func alert(item: Binding, @ViewBuilder actions: (Item) -> Actions) -> some View { let binding = Binding(get: { @@ -51,7 +51,7 @@ struct AlertInfo: Identifiable, AlertProtocol { var role: ButtonRole? let action: (() -> Void)? } - + struct AlertTextField: Identifiable { let id = UUID() let placeholder: String @@ -59,7 +59,7 @@ struct AlertInfo: Identifiable, AlertProtocol { let autoCapitalization: TextInputAutocapitalization let autoCorrectionDisabled: Bool } - + /// An identifier that can be used to distinguish one error from another. let id: T /// The alert's title. @@ -86,7 +86,7 @@ extension AlertInfo { title = L10n.commonError message = L10n.errorUnknown } - + // periphery: ignore - might be useful in the future /// Initialises the type with the title from an `Error`'s localised description along with the default Ok button. /// @@ -109,7 +109,7 @@ extension View { } } } - + if let textFields = item.textFields { VStack(spacing: 24) { ForEach(textFields) { textField in @@ -119,12 +119,12 @@ extension View { } } } - + Button(item.primaryButton.title, role: item.primaryButton.role) { item.primaryButton.action?() } .accessibilityIdentifier(A11yIdentifiers.alertInfo.primaryButton) - + if let secondaryButton = item.secondaryButton { Button(secondaryButton.title, role: secondaryButton.role) { secondaryButton.action?() diff --git a/ElementX/Sources/Other/Extensions/AttributedString.swift b/ElementX/Sources/Other/Extensions/AttributedString.swift index 9e087cd84..508123ec3 100644 --- a/ElementX/Sources/Other/Extensions/AttributedString.swift +++ b/ElementX/Sources/Other/Extensions/AttributedString.swift @@ -36,7 +36,7 @@ extension AttributedString { case (false, false): .plainText } - + components.append(AttributedStringBuilderComponent(id: String(attributedString.characters), attributedString: attributedString, type: componentType)) diff --git a/ElementX/Sources/Other/Extensions/Bundle.swift b/ElementX/Sources/Other/Extensions/Bundle.swift index 40c055e9f..c74a16998 100644 --- a/ElementX/Sources/Other/Extensions/Bundle.swift +++ b/ElementX/Sources/Other/Extensions/Bundle.swift @@ -28,9 +28,9 @@ public extension Bundle { /// Overrides `Bundle.app.preferredLocalizations` for testing translations since this is /// only for testing, and is changed at runtime only in tests, it's fine to keep as `nonisolated(unsafe)` nonisolated(unsafe) static var overrideLocalizations: [String]? - + private static let cachedBundles = Mutex<[String: Bundle]>([:]) - + /// Get an lproj language bundle from the receiver bundle. /// - Parameter language: The language to try to load. /// - Returns: The lproj bundle if found otherwise nil. @@ -38,24 +38,24 @@ public extension Bundle { if let bundle = cachedValue(forKey: language) { return bundle } - + guard let lprojURL = Bundle.app.url(forResource: language, withExtension: "lproj") else { return nil } - + let bundle = Bundle(url: lprojURL) - + cacheValue(bundle, forKey: language) - + return bundle } - + // MARK: - Private - + private static func cacheValue(_ value: Bundle?, forKey key: String) { cachedBundles.withLock { $0[key] = value } } - + private static func cachedValue(forKey key: String) -> Bundle? { cachedBundles.withLock { $0[key] } } diff --git a/ElementX/Sources/Other/Extensions/ClientBuilder.swift b/ElementX/Sources/Other/Extensions/ClientBuilder.swift index fc6fa9aaa..6710a444e 100644 --- a/ElementX/Sources/Other/Extensions/ClientBuilder.swift +++ b/ElementX/Sources/Other/Extensions/ClientBuilder.swift @@ -43,7 +43,7 @@ extension ClientBuilder { .enableShareHistoryOnInvite(enableShareHistoryOnInvite: true) .autoEnableBackups(autoEnableBackups: true) } - + // Set recipient strategy and trust requirement even if `setupEncryption` is false to ensure messages // from insecure devices aren't displayed in push notifications. // See https://github.com/element-hq/element-x-ios/issues/4702. diff --git a/ElementX/Sources/Other/Extensions/ConfirmationDialog.swift b/ElementX/Sources/Other/Extensions/ConfirmationDialog.swift index ccdc8bfb6..0a68653d4 100644 --- a/ElementX/Sources/Other/Extensions/ConfirmationDialog.swift +++ b/ElementX/Sources/Other/Extensions/ConfirmationDialog.swift @@ -25,7 +25,7 @@ extension View { }) return confirmationDialog(item.wrappedValue?.title ?? "", isPresented: binding, titleVisibility: titleVisibility, presenting: item.wrappedValue, actions: actions) } - + // periphery: ignore - not used yet but might be useful func confirmationDialog(item: Binding, titleVisibility: Visibility = .automatic, diff --git a/ElementX/Sources/Other/Extensions/Encodable.swift b/ElementX/Sources/Other/Extensions/Encodable.swift index 0c3b3257a..ded705635 100644 --- a/ElementX/Sources/Other/Extensions/Encodable.swift +++ b/ElementX/Sources/Other/Extensions/Encodable.swift @@ -18,7 +18,7 @@ extension Encodable { } return json } - + func toJsonString(_ encoder: JSONEncoder = JSONEncoder()) throws -> String? { try toJsonDictionary(encoder).jsonString } diff --git a/ElementX/Sources/Other/Extensions/FileManager.swift b/ElementX/Sources/Other/Extensions/FileManager.swift index 5ef807fe4..221024dbf 100644 --- a/ElementX/Sources/Other/Extensions/FileManager.swift +++ b/ElementX/Sources/Other/Extensions/FileManager.swift @@ -20,7 +20,7 @@ extension FileManager { } return isDirectory.boolValue } - + func createDirectoryIfNeeded(at url: URL, withIntermediateDirectories: Bool = true) throws { guard !directoryExists(at: url) else { return @@ -36,7 +36,7 @@ extension FileManager { return newURL } - + @discardableResult func writeDataToTemporaryDirectory(data: Data, fileName: String, withinAppGroupContainer: Bool = false) throws -> URL { let baseURL: URL = withinAppGroupContainer ? .appGroupTemporaryDirectory : .temporaryDirectory @@ -59,7 +59,7 @@ extension FileManager { return size } - + func sizeForDirectory(at url: URL) throws -> UInt { guard let enumerator = enumerator(at: url, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey]) else { throw FileManagerError.invalidFileSize diff --git a/ElementX/Sources/Other/Extensions/ImageCache.swift b/ElementX/Sources/Other/Extensions/ImageCache.swift index 46176b4d9..b481c24ce 100644 --- a/ElementX/Sources/Other/Extensions/ImageCache.swift +++ b/ElementX/Sources/Other/Extensions/ImageCache.swift @@ -16,7 +16,7 @@ extension ImageCache { result.diskStorage.config.sizeLimit = 1 return result } - + static var onlyOnDisk: ImageCache { let result = ImageCache.default result.memoryStorage.config.totalCostLimit = 1 diff --git a/ElementX/Sources/Other/Extensions/NSItemProvider.swift b/ElementX/Sources/Other/Extensions/NSItemProvider.swift index 1c53dc496..ad81fe010 100644 --- a/ElementX/Sources/Other/Extensions/NSItemProvider.swift +++ b/ElementX/Sources/Other/Extensions/NSItemProvider.swift @@ -133,7 +133,7 @@ extension NSItemProvider { guard registeredContentTypes.contains(where: { $0.conforms(to: .fileURL) }) else { return nil } - + for type in registeredContentTypes { if let fileExtension = type.preferredFilenameExtension { return .init(type: type, fileExtension: fileExtension) diff --git a/ElementX/Sources/Other/Extensions/ProcessInfo.swift b/ElementX/Sources/Other/Extensions/ProcessInfo.swift index bdedc2a20..f566e9de4 100644 --- a/ElementX/Sources/Other/Extensions/ProcessInfo.swift +++ b/ElementX/Sources/Other/Extensions/ProcessInfo.swift @@ -17,7 +17,7 @@ extension ProcessInfo { false #endif } - + /// Flag indicating whether the app is running the UI tests. static var isRunningUITests: Bool { #if DEBUG @@ -42,7 +42,7 @@ extension ProcessInfo { false #endif } - + /// Flag indicating whether the app is running the UI tests or unit tests. static var isRunningTests: Bool { isRunningUITests || isRunningUnitTests || isRunningIntegrationTests || isRunningAccessibilityTests diff --git a/ElementX/Sources/Other/Extensions/Snapshotting.swift b/ElementX/Sources/Other/Extensions/Snapshotting.swift index 8f2707ebd..6919fef01 100644 --- a/ElementX/Sources/Other/Extensions/Snapshotting.swift +++ b/ElementX/Sources/Other/Extensions/Snapshotting.swift @@ -11,7 +11,7 @@ import SwiftUI struct SnapshotPrecisionPreferenceKey: PreferenceKey { static var defaultValue: Float = 1.0 - + static func reduce(value: inout Float, nextValue: () -> Float) { value = nextValue() } @@ -19,7 +19,7 @@ struct SnapshotPrecisionPreferenceKey: PreferenceKey { struct SnapshotPerceptualPrecisionPreferenceKey: PreferenceKey { static var defaultValue: Float = 0.98 - + static func reduce(value: inout Float, nextValue: () -> Float) { value = nextValue() } @@ -27,7 +27,7 @@ struct SnapshotPerceptualPrecisionPreferenceKey: PreferenceKey { struct SnapshotFulfillmentPreferenceKey: PreferenceKey { static var defaultValue: Wrapper? - + static func reduce(value: inout Wrapper?, nextValue: () -> Wrapper?) { value = nextValue() } diff --git a/ElementX/Sources/Other/Extensions/Task.swift b/ElementX/Sources/Other/Extensions/Task.swift index e05fc9d5b..1f2f6324c 100644 --- a/ElementX/Sources/Other/Extensions/Task.swift +++ b/ElementX/Sources/Other/Extensions/Task.swift @@ -29,7 +29,7 @@ public extension Task where Success == Never, Failure == Never { } } } - + /// Dispatches the given throwing closure onto the given queue, wrapped within /// a continuation to make it non-blocking and awaitable. /// @@ -59,7 +59,7 @@ extension Task { func store(in cancellables: inout Set) { asCancellable().store(in: &cancellables) } - + func asCancellable() -> AnyCancellable { AnyCancellable(cancel) } diff --git a/ElementX/Sources/Other/Extensions/UIDevice.swift b/ElementX/Sources/Other/Extensions/UIDevice.swift index 012d5d8eb..520db21b6 100644 --- a/ElementX/Sources/Other/Extensions/UIDevice.swift +++ b/ElementX/Sources/Other/Extensions/UIDevice.swift @@ -13,7 +13,7 @@ extension UIDevice { var isPhone: Bool { userInterfaceIdiom == .phone } - + var initialDeviceName: String { L10n.loginInitialDeviceNameIos(InfoPlistReader.main.bundleDisplayName) } diff --git a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift index 33ae8edc5..86779b772 100644 --- a/ElementX/Sources/Other/Extensions/UNNotificationContent.swift +++ b/ElementX/Sources/Other/Extensions/UNNotificationContent.swift @@ -13,7 +13,7 @@ extension UNNotificationContent { @objc var receiverID: String? { userInfo[NotificationConstants.UserInfoKey.receiverIdentifier] as? String } - + @objc var roomID: String? { userInfo[NotificationConstants.UserInfoKey.roomIdentifier] as? String } @@ -21,7 +21,7 @@ extension UNNotificationContent { @objc var eventID: String? { userInfo[NotificationConstants.UserInfoKey.eventIdentifier] as? String } - + @objc var pusherNotificationClientIdentifier: String? { userInfo[NotificationConstants.UserInfoKey.pusherNotificationClientIdentifier] as? String } diff --git a/ElementX/Sources/Other/Extensions/URL.swift b/ElementX/Sources/Other/Extensions/URL.swift index 8b1c39392..9f69abc25 100644 --- a/ElementX/Sources/Other/Extensions/URL.swift +++ b/ElementX/Sources/Other/Extensions/URL.swift @@ -31,13 +31,13 @@ extension URL { .appending(component: "Logs", directoryHint: .isDirectory) .appending(component: InfoPlistReader.main.baseBundleIdentifier, directoryHint: .isDirectory) } - + /// The base directory where all session data is stored. static var sessionsBaseDirectory: URL { let applicationSupportSessionsURL = applicationSupportBaseDirectory.appending(component: "Sessions", directoryHint: .isDirectory) try? FileManager.default.createDirectoryIfNeeded(at: applicationSupportSessionsURL) - + return applicationSupportSessionsURL } @@ -47,7 +47,7 @@ extension URL { .appending(component: "Library", directoryHint: .isDirectory) .appending(component: "Application Support", directoryHint: .isDirectory) .appending(component: InfoPlistReader.main.baseBundleIdentifier, directoryHint: .isDirectory) - + try? FileManager.default.createDirectoryIfNeeded(at: url) do { @@ -57,7 +57,7 @@ extension URL { } catch { MXLog.error("Failed excluding Application Support from backups") } - + return url } @@ -68,12 +68,12 @@ extension URL { .appending(component: "Caches", directoryHint: .isDirectory) .appending(component: InfoPlistReader.main.baseBundleIdentifier, directoryHint: .isDirectory) .appending(component: "Sessions", directoryHint: .isDirectory) - + try? FileManager.default.createDirectoryIfNeeded(at: url) // Caches are excluded from backups automatically. // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html - + return url } @@ -84,12 +84,12 @@ extension URL { static var appGroupTemporaryDirectory: URL { let url = appGroupContainerDirectory .appending(component: "tmp", directoryHint: .isDirectory) - + try? FileManager.default.createDirectoryIfNeeded(at: url) // Temporary files are excluded from backups automatically. // https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html - + return url } @@ -116,7 +116,7 @@ extension URL { MXLog.info("Found \(String(describing: proxies?.count)) proxies, using the first one.") MXLog.info("Proxy type is \(proxyType)") - + guard let host = firstProxy[kCFProxyHostNameKey] as? String else { MXLog.error("Found proxy with invalid host name") return nil @@ -138,23 +138,23 @@ extension URL { static var mockMXCAudio: URL { "mxc://matrix.org/1234567890AuDiO" } - + static var mockMXCFile: URL { "mxc://matrix.org/1234567890FiLe" } - + static var mockMXCImage: URL { "mxc://matrix.org/1234567890ImAgE" } - + static var mockMXCVideo: URL { "mxc://matrix.org/1234567890ViDeO" } - + static var mockMXCAvatar: URL { "mxc://matrix.org/1234567890AvAtAr" } - + static var mockMXCUserAvatar: URL { "mxc://matrix.org/1234567890AvAtArUsEr" } diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift index fbc8bcd76..c7d37ba90 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilder.swift @@ -44,7 +44,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { private static let attributeMSC4286 = "msc4286-external-payment-details" private static let caches = Mutex<[String: LRUCache]>([:]) - + static func invalidateCaches() { caches.withLock { $0.removeAll() } } @@ -53,7 +53,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { self.cacheKey = cacheKey self.mentionBuilder = mentionBuilder } - + func fromPlain(_ string: String?) -> AttributedString? { guard let string else { return nil @@ -62,7 +62,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { if let cached = Self.cachedValue(forKey: string, cacheKey: cacheKey) { return cached } - + let mutableAttributedString = NSMutableAttributedString(string: string) addLinksAndMentions(mutableAttributedString) addMatrixEntityPermalinkAttributesTo(mutableAttributedString) @@ -72,7 +72,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return result } - + /// Do not use the default HTML renderer of NSAttributedString because this method /// runs on the UI thread which we want to avoid because renderHTMLString is called /// most of the time from a background thread. @@ -84,7 +84,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { if let cached = Self.cachedValue(forKey: originalHTMLString, cacheKey: cacheKey) { return cached } - + let htmlString = originalHTMLString.replacingHtmlBreaksOccurrences() let doc = try? SwiftSoup.parseBodyFragment(htmlString) @@ -105,7 +105,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { return result } - + // MARK: - Private // swiftlint:disable:next function_body_length cyclomatic_complexity @@ -133,7 +133,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { if (node.previousSibling() as? Element)?.tagName() == "br" { text.trimPrefix(" ") } - + result.append(NSAttributedString(string: text)) continue } @@ -155,7 +155,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { content = attributedString(element: childElement, documentBody: documentBody, preserveFormatting: preserveFormatting, listTag: listTag, listIndex: &childIndex, indentLevel: indentLevel) content.append(NSAttributedString(string: "\n")) content.setFontPreservingSymbolicTraits(UIFont.boldSystemFont(ofSize: size)) - + case "p", "div": content = attributedString(element: childElement, documentBody: documentBody, preserveFormatting: preserveFormatting, listTag: listTag, listIndex: &childIndex, indentLevel: indentLevel) content.append(NSAttributedString(string: "\n")) @@ -245,7 +245,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { if indentLevel > 0 || !element.ownText().isEmpty { content.insert(NSAttributedString("\n"), at: 0) } - + case "li": var bullet = String(repeating: " ", count: indentLevel) if listTag == "ol" { @@ -283,11 +283,11 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { if caches[cacheKey] == nil { caches[cacheKey] = LRUCache(countLimit: 1000) } - + caches[cacheKey]?.setValue(value, forKey: key) } } - + private static func cachedValue(forKey key: String, cacheKey: String) -> AttributedString? { caches.withLock { $0[cacheKey]?.value(forKey: key) } } @@ -305,7 +305,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { } let identifier = String(string[matchRange]) - + return TextParsingMatch(type: .userID(identifier: identifier), range: match.range) } @@ -371,7 +371,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { if hasLink { return } - + switch match.type { case .atRoom: attributedString.addAttribute(.MatrixAllUsersMention, value: true, range: match.range) @@ -420,7 +420,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol { } } } - + private func detectPhishingAttempts(_ attributedString: NSMutableAttributedString) { attributedString.enumerateAttribute(.link, in: .init(location: 0, length: attributedString.length), options: []) { value, range, _ in guard value != nil, let internalURL = value as? URL else { diff --git a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift index 6e51689cc..8b3e996f7 100644 --- a/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift +++ b/ElementX/Sources/Other/HTMLParsing/AttributedStringBuilderProtocol.swift @@ -14,7 +14,7 @@ struct AttributedStringBuilderComponent: Hashable, Identifiable { case blockquote case codeBlock } - + /// Identifier for the `Identifiable` conformance, allows edits to the `FormattedBodyText` to animate seamlessly let id: String let attributedString: AttributedString diff --git a/ElementX/Sources/Other/HTMLParsing/PhishingDetector.swift b/ElementX/Sources/Other/HTMLParsing/PhishingDetector.swift index 89a673908..165573665 100644 --- a/ElementX/Sources/Other/HTMLParsing/PhishingDetector.swift +++ b/ElementX/Sources/Other/HTMLParsing/PhishingDetector.swift @@ -15,7 +15,7 @@ enum PhishingDetector { let disambiguatedDisplayString = displayString.replacingOccurrences(of: "﹒", with: ".") let linkMatch = MatrixEntityRegex.linkRegex.firstMatch(in: disambiguatedDisplayString) let linkMatchLength = linkMatch?.range.length ?? 0 - + // We check if we the link string contains a matrix user ID. if let match = MatrixEntityRegex.userIdentifierRegex.firstMatch(in: disambiguatedDisplayString), // If there is a bigger permalink including it we leave it handled by the link branch diff --git a/ElementX/Sources/Other/InfoPlistReader.swift b/ElementX/Sources/Other/InfoPlistReader.swift index 3c646fac7..b2f75d00a 100644 --- a/ElementX/Sources/Other/InfoPlistReader.swift +++ b/ElementX/Sources/Other/InfoPlistReader.swift @@ -33,26 +33,26 @@ struct InfoPlistReader { private enum Values { static let mentionPills = "Mention Pills" } - + /// Info.plist reader on the bundle object that contains the current executable. static let main = InfoPlistReader(bundle: .main) - + /// Info.plist reader on the bundle object that contains the main app executable. static let app = InfoPlistReader(bundle: .app) - + private let bundle: Bundle - + /// Initializer /// - Parameter bundle: bundle to read values from init(bundle: Bundle) { self.bundle = bundle } - + /// App group identifier set in Info.plist of the target var appGroupIdentifier: String { infoPlistValue(forKey: Keys.appGroupIdentifier) } - + /// Base bundle identifier set in Info.plist of the target var baseBundleIdentifier: String { infoPlistValue(forKey: Keys.baseBundleIdentifier) @@ -62,27 +62,27 @@ struct InfoPlistReader { var keychainAccessGroupIdentifier: String { infoPlistValue(forKey: Keys.keychainAccessGroupIdentifier) } - + /// Bundle executable of the target var bundleExecutable: String { infoPlistValue(forKey: kCFBundleExecutableKey as String) } - + /// Bundle identifier of the target var bundleIdentifier: String { infoPlistValue(forKey: kCFBundleIdentifierKey as String) } - + /// Bundle short version string of the target var bundleShortVersionString: String { infoPlistValue(forKey: Keys.bundleShortVersion) } - + /// Bundle version of the target var bundleVersion: String { infoPlistValue(forKey: kCFBundleVersionKey as String) } - + /// Bundle display name of the target var bundleDisplayName: String { infoPlistValue(forKey: Keys.bundleDisplayName) @@ -104,7 +104,7 @@ struct InfoPlistReader { } // MARK: - Mention Pills - + /// Mention Pills UTType var pillsUTType: String { let exportedTypes: [[String: Any]] = infoPlistValue(forKey: Keys.utExportedTypeDeclarationsKey) diff --git a/ElementX/Sources/Other/Logging/ExtensionLogger.swift b/ElementX/Sources/Other/Logging/ExtensionLogger.swift index 258914ff5..f7707eb10 100644 --- a/ElementX/Sources/Other/Logging/ExtensionLogger.swift +++ b/ElementX/Sources/Other/Logging/ExtensionLogger.swift @@ -20,7 +20,7 @@ enum ExtensionLogger { formatter.minimumFractionDigits = 2 return formatter } - + private static var formattedMemoryAvailable: String { let freeBytes = os_proc_available_memory() let freeMB = Double(freeBytes) / 1024 / 1024 @@ -29,7 +29,7 @@ enum ExtensionLogger { } return "\(formattedStr) MB" } - + /// Details: https://developer.apple.com/forums/thread/105088 /// - Returns: Current memory footprint private static var memoryFootprint: Float? { @@ -50,10 +50,10 @@ enum ExtensionLogger { guard kr == KERN_SUCCESS, count >= TASK_VM_INFO_REV1_COUNT else { return nil } - + return Float(info.phys_footprint) } - + /// Formatted memory footprint for debugging purposes /// - Returns: Memory footprint in MBs as a readable string static var formattedMemoryFootprint: String { @@ -64,7 +64,7 @@ enum ExtensionLogger { } return "\(formattedStr) MB" } - + static func logMemory(with tag: String) { MXLog.info("\(tag) Memory: footprint: \(formattedMemoryFootprint) - available: \(formattedMemoryAvailable)") } diff --git a/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift b/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift index 1431635c4..8ee685804 100644 --- a/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift +++ b/ElementX/Sources/Other/MapLibre/MapLibreMapView.swift @@ -22,7 +22,7 @@ struct MapLibreMapView: UIViewRepresentable { /// Map annotations let annotations: [LocationAnnotation] - + init(zoomLevel: Double, initialZoomLevel: Double, mapCenter: CLLocationCoordinate2D, annotations: [LocationAnnotation] = []) { self.zoomLevel = zoomLevel self.initialZoomLevel = initialZoomLevel @@ -36,7 +36,7 @@ struct MapLibreMapView: UIViewRepresentable { @Environment(\.colorScheme) private var colorScheme let mapURLBuilder: MapTilerURLBuilderProtocol - + let options: Options let mediaProvider: MediaProviderProtocol? @@ -93,7 +93,7 @@ struct MapLibreMapView: UIViewRepresentable { } // MARK: - Private - + private func setupMap(mapView: MLNMapView, with options: Options) { mapView.addAnnotations(options.annotations) mapView.zoomLevel = options.annotations.isEmpty ? options.initialZoomLevel : options.zoomLevel @@ -174,16 +174,16 @@ struct MapLibreMapView: UIViewRepresentable { extension MapLibreMapView { class Coordinator: NSObject, MLNMapViewDelegate { // MARK: - Properties - + var mapLibreView: MapLibreMapView private var previousUserLocation: MLNUserLocation? /// Tracks the last center coordinate reported by the map (or set programmatically), /// so that `updateUIView` can tell apart external binding changes from internal ones. var lastReportedCenter: CLLocationCoordinate2D? - + // MARK: - Setup - + init(_ mapLibreView: MapLibreMapView) { self.mapLibreView = mapLibreView } @@ -212,7 +212,7 @@ extension MapLibreMapView { mapView.setCenter(userLocation.coordinate, zoomLevel: self.mapLibreView.options.zoomLevel, animated: true) } } - + previousUserLocation = userLocation updateGeolocationUncertainty(location: userLocation) } @@ -238,7 +238,7 @@ extension MapLibreMapView { mapLibreView.mapCenterCoordinate = center } } - + func mapView(_ mapView: MLNMapView, shouldChangeFrom oldCamera: MLNMapCamera, to newCamera: MLNMapCamera, reason: MLNCameraChangeReason) -> Bool { // we send the userDidPan event only for the reasons that actually will change the map center, and not zoom only / rotations only events. switch reason { @@ -259,21 +259,21 @@ extension MapLibreMapView { } return true } - + // MARK: Callout - + func mapView(_ mapView: MLNMapView, annotationCanShowCallout annotation: MLNAnnotation) -> Bool { false } - + // MARK: Private - + private func updateGeolocationUncertainty(location: MLNUserLocation) { guard let clLocation = location.location, clLocation.horizontalAccuracy >= 0 else { mapLibreView.geolocationUncertainty = nil return } - + mapLibreView.geolocationUncertainty = clLocation.horizontalAccuracy } } diff --git a/ElementX/Sources/Other/MapLibre/MapLibreStaticMapView.swift b/ElementX/Sources/Other/MapLibre/MapLibreStaticMapView.swift index 87af02a16..0ddca93a8 100644 --- a/ElementX/Sources/Other/MapLibre/MapLibreStaticMapView.swift +++ b/ElementX/Sources/Other/MapLibre/MapLibreStaticMapView.swift @@ -17,7 +17,7 @@ struct MapLibreStaticMapView: View { private let mapTilerAttributionPlacement: MapTilerAttributionPlacement private let mapSize: CGSize private let pinAnnotationView: PinAnnotation - + @Environment(\.colorScheme) private var colorScheme @State private var fetchAttempt = 0 @@ -79,13 +79,13 @@ struct MapLibreStaticMapView: View { } } } - + private var placeholderImage: some View { Image(asset: Asset.Images.mapBlurred) .resizable() .scaledToFill() } - + private var errorView: some View { Button { fetchAttempt += 1 diff --git a/ElementX/Sources/Other/Pills/MentionBuilder.swift b/ElementX/Sources/Other/Pills/MentionBuilder.swift index e4fcb0370..83f841744 100644 --- a/ElementX/Sources/Other/Pills/MentionBuilder.swift +++ b/ElementX/Sources/Other/Pills/MentionBuilder.swift @@ -49,7 +49,7 @@ struct MentionBuilder: MentionBuilderProtocol { func handleAllUsersMention(for attributedString: NSMutableAttributedString, in range: NSRange) { let attributesToRestore = getAttributesToRestore(for: attributedString, in: range) - + let attachmentData = PillTextAttachmentData(type: .allUsers, font: attributesToRestore.font) guard let attachment = PillTextAttachment(attachmentData: attachmentData) else { return @@ -80,7 +80,7 @@ struct MentionBuilder: MentionBuilderProtocol { .font: attributesToRestore.font, .foregroundColor: attributesToRestore.foregroundColor] attachmentAttributes.addBlockquoteIfNeeded(attributesToRestore.blockquote) - + setPillAttachment(attachment: attachment, attributedString: attributedString, in: range, diff --git a/ElementX/Sources/Other/Pills/MessageText.swift b/ElementX/Sources/Other/Pills/MessageText.swift index 3cf861984..2d1603c9c 100644 --- a/ElementX/Sources/Other/Pills/MessageText.swift +++ b/ElementX/Sources/Other/Pills/MessageText.swift @@ -93,7 +93,7 @@ struct MessageText: UIViewRepresentable { guard let baseText = try? NSAttributedString(attributedString, including: \.elementX) else { return nil } - + let combined = NSMutableAttributedString(attributedString: baseText) if trailingReservedSize.width > 0 { let attachment = TransparentTextAttachment() @@ -101,7 +101,7 @@ struct MessageText: UIViewRepresentable { attachment.bounds = CGRect(origin: .zero, size: CGSize(width: trailingReservedSize.width, height: max(trailingReservedSize.height, 1))) - + // Inherit the font from the preceding character so the appended runs // (newline and attachment) carry the same line metrics as the surrounding // text — otherwise a small default font on those characters can shrink the @@ -109,7 +109,7 @@ struct MessageText: UIViewRepresentable { let trailingFont = baseText.length > 0 ? baseText.attribute(.font, at: baseText.length - 1, effectiveRange: nil) : nil - + // When the last paragraph's natural text direction doesn't match the layout // direction, the inline attachment would land on the wrong side and overlap the // overlaid timestamp. Force it onto its own line so the bubble just grows taller. @@ -122,7 +122,7 @@ struct MessageText: UIViewRepresentable { } combined.append(newlineString) } - + let attachmentString = NSMutableAttributedString(attachment: attachment) if let trailingFont { attachmentString.addAttribute(.font, @@ -131,19 +131,19 @@ struct MessageText: UIViewRepresentable { } combined.append(attachmentString) } - + return combined } - + private func lastParagraphDirectionMatchesLayout(in attributedText: NSAttributedString) -> Bool { let string = attributedText.string as NSString guard string.length > 0 else { return true } - + // Isolate the last paragraph because its direction is what decides // which side the trailing attachment ends up on. let lastParagraphRange = string.paragraphRange(for: NSRange(location: string.length - 1, length: 0)) let lastParagraph = string.substring(with: lastParagraphRange) - + let textIsRTL = lastParagraph.firstStrongCharacterIsRTL let layoutIsRTL = layoutDirection == .rightToLeft return textIsRTL == layoutIsRTL diff --git a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift index 5b72d031d..62a29558a 100644 --- a/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift +++ b/ElementX/Sources/Other/Pills/PillAttachmentViewProvider.swift @@ -25,7 +25,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo override init(textAttachment: NSTextAttachment, parentView: UIView?, textLayoutManager: NSTextLayoutManager?, location: NSTextLocation) { super.init(textAttachment: textAttachment, parentView: parentView, textLayoutManager: textLayoutManager, location: location) - + // Keep a reference to the parent text view for size adjustments and pills flushing. delegate = parentView?.superview as? PillAttachmentViewProviderDelegate tracksTextAttachmentViewBounds = true @@ -34,7 +34,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo @MainActor override func loadView() { super.loadView() - + guard let textAttachment = textAttachment as? PillTextAttachment, let pillData = textAttachment.pillData else { MXLog.failure("Attachment is missing data or not of expected class") @@ -82,7 +82,7 @@ final class PillAttachmentViewProvider: NSTextAttachmentViewProvider, NSSecureCo final class ComposerMentionDisplayHelper: MentionDisplayHelper { weak var timelineContext: TimelineViewModel.Context? - + init(timelineContext: TimelineViewModel.Context) { self.timelineContext = timelineContext } diff --git a/ElementX/Sources/Other/Pills/PillUtilities.swift b/ElementX/Sources/Other/Pills/PillUtilities.swift index 5d06c2ae0..84d02ce91 100644 --- a/ElementX/Sources/Other/Pills/PillUtilities.swift +++ b/ElementX/Sources/Other/Pills/PillUtilities.swift @@ -10,7 +10,7 @@ import Foundation enum PillUtilities { static let atRoom = "@room" - + /// Used by the WYSIWYG as the urlString value to identify @room mentions static let composerAtRoomURLString = "#" diff --git a/ElementX/Sources/Other/Pills/PillView.swift b/ElementX/Sources/Other/Pills/PillView.swift index 881ace1dd..ef4f511bf 100644 --- a/ElementX/Sources/Other/Pills/PillView.swift +++ b/ElementX/Sources/Other/Pills/PillView.swift @@ -21,7 +21,7 @@ struct PillView: View { var backgroundColor: Color { context.viewState.isOwnMention ? .compound.bgBadgeAccent : .compound.bgBadgePrimary } - + var body: some View { mainContent .onChange(of: context.viewState.displayText) { diff --git a/ElementX/Sources/Other/Progress/ProgressMaskModifier.swift b/ElementX/Sources/Other/Progress/ProgressMaskModifier.swift index 20ad02c85..cc008b35e 100644 --- a/ElementX/Sources/Other/Progress/ProgressMaskModifier.swift +++ b/ElementX/Sources/Other/Progress/ProgressMaskModifier.swift @@ -22,13 +22,13 @@ private struct ProgressMaskModifier: ViewModifier { private let progress: CGFloat private let trackColor: Color private let backgroundTrackColor: Color - + init(progress: CGFloat, trackColor: Color, backgroundTrackColor: Color) { self.progress = progress self.trackColor = trackColor self.backgroundTrackColor = backgroundTrackColor } - + func body(content: Content) -> some View { GeometryReader { geometry in ZStack(alignment: .leading) { diff --git a/ElementX/Sources/Other/SDKListener.swift b/ElementX/Sources/Other/SDKListener.swift index c8284af74..762eaf763 100644 --- a/ElementX/Sources/Other/SDKListener.swift +++ b/ElementX/Sources/Other/SDKListener.swift @@ -15,7 +15,7 @@ import MatrixRustSDK /// protocol with a specialisation for the type it listens for. final class SDKListener: Sendable { private let onUpdateClosure: @Sendable (T) -> Void - + /// Creates a new listener. /// - Parameter onUpdateClosure: A closure that will be called whenever a new value is available. init(_ onUpdateClosure: @escaping @Sendable (T) -> Void) { diff --git a/ElementX/Sources/Other/ScrollViewAdapter.swift b/ElementX/Sources/Other/ScrollViewAdapter.swift index 1b27c7223..ff1598376 100644 --- a/ElementX/Sources/Other/ScrollViewAdapter.swift +++ b/ElementX/Sources/Other/ScrollViewAdapter.swift @@ -16,9 +16,9 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate { scrollView?.delegate = self } } - + var shouldScrollToTopClosure: ((UIScrollView) -> Bool)? - + private let didScrollSubject = PassthroughSubject() var didScroll: AnyPublisher { didScrollSubject.eraseToAnyPublisher() @@ -54,7 +54,7 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate { func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { updateDidScroll(scrollView) } - + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { updateDidScroll(scrollView) } @@ -62,7 +62,7 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate { func scrollViewDidScrollToTop(_ scrollView: UIScrollView) { updateDidScroll(scrollView) } - + func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { guard let shouldScrollToTopClosure else { // Default behaviour diff --git a/ElementX/Sources/Other/ShareToMapsAppActivity.swift b/ElementX/Sources/Other/ShareToMapsAppActivity.swift index 31c7a290f..6629dc262 100644 --- a/ElementX/Sources/Other/ShareToMapsAppActivity.swift +++ b/ElementX/Sources/Other/ShareToMapsAppActivity.swift @@ -15,34 +15,34 @@ final class ShareToMapsAppActivity: UIActivity { case google case osm } - + private let type: MapsAppType private let location: CLLocationCoordinate2D private let senderName: String? - + init(type: MapsAppType, location: CLLocationCoordinate2D, senderName: String?) { self.type = type self.location = location self.senderName = senderName super.init() } - + override private init() { fatalError() } - + override var activityTitle: String? { type.activityTitle } - + override var activityType: UIActivity.ActivityType { .shareToMapsApp } - + override func canPerform(withActivityItems activityItems: [Any]) -> Bool { true } - + override func prepare(withActivityItems activityItems: [Any]) { UIApplication.shared.open(type.activityURL(for: location, senderName: senderName), options: [:]) { [weak self] result in self?.activityDidFinish(result) @@ -76,7 +76,7 @@ extension ShareToMapsAppActivity.MapsAppType { return url } } - + var activityTitle: String { switch self { case .apple: diff --git a/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift b/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift index fd47373b7..a74e47ff7 100644 --- a/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift +++ b/ElementX/Sources/Other/SwiftUI/Animation/ElementAnimations.swift @@ -12,21 +12,21 @@ import SwiftUI public extension Animation { /// Animation to be used to disable animations. static let noAnimation: Animation = .linear(duration: 0) - + /// `noAnimation` if running tests, otherwise `default` animation if `UIAccessibility.isReduceMotionEnabled` is false static var elementDefault: Animation { let animation: Animation = ProcessInfo.isRunningTests ? .noAnimation : .default return animation.disabledIfReduceMotionEnabled() } - + /// `noAnimation` if running tests, otherwise `self` if `UIAccessibility.isReduceMotionEnabled` is false func disabledDuringTests() -> Self { let animation: Animation = ProcessInfo.isRunningTests ? .noAnimation : self return animation.disabledIfReduceMotionEnabled() } - + // MARK: - Private - + private func disabledIfReduceMotionEnabled() -> Self { UIAccessibility.isReduceMotionEnabled ? .noAnimation : self } diff --git a/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift b/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift index beed971c0..fe79f8e3b 100644 --- a/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift +++ b/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModel.swift @@ -22,28 +22,28 @@ class StateStoreViewModel { /// /// Left as public for `ViewModel` implementations convenience. var cancellables = Set() - + /// Constrained interface for passing to Views. var context: Context - + var state: State { get { context.viewState } set { context.viewState = newValue } } - + init(initialViewState: State, mediaProvider: MediaProviderProtocol? = nil) { context = Context(initialViewState: initialViewState, mediaProvider: mediaProvider) context.viewModel = self } - + /// Override to handles incoming `ViewAction`s from the `ViewModel`. /// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation. func process(viewAction: ViewAction) { // Default implementation, -no-op } - + // MARK: - Context - + /// A constrained and concise interface for interacting with the ViewModel. /// /// This class is closely bound to`StateStoreViewModel`. It provides the exact interface the view should need to interact @@ -60,26 +60,26 @@ class StateStoreViewModel { @MainActor final class Context: ObservableObject { fileprivate weak var viewModel: StateStoreViewModel? - + /// Get-able/Observable `Published` property for the `ViewState` @Published fileprivate(set) var viewState: State - + /// An optional image loading service so that views can manage themselves /// Intentionally non-generic so that it doesn't grow uncontrollably let mediaProvider: MediaProviderProtocol? - + /// Set-able/Bindable access to the bindable state. subscript(dynamicMember keyPath: WritableKeyPath) -> T { get { viewState.bindings[keyPath: keyPath] } set { viewState.bindings[keyPath: keyPath] = newValue } } - + /// Send a `ViewAction` to the `ViewModel` for processing. /// - Parameter viewAction: The `ViewAction` to send to the `ViewModel`. func send(viewAction: ViewAction) { viewModel?.process(viewAction: viewAction) } - + fileprivate init(initialViewState: State, mediaProvider: MediaProviderProtocol?) { self.viewState = initialViewState self.mediaProvider = mediaProvider diff --git a/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModelV2.swift b/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModelV2.swift index 10841a947..13cbbcf82 100644 --- a/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModelV2.swift +++ b/ElementX/Sources/Other/SwiftUI/ViewModel/StateStoreViewModelV2.swift @@ -24,26 +24,26 @@ class StateStoreViewModelV2 { /// /// Left as public for `ViewModel` implementations convenience. var cancellables = Set() - + /// Constrained interface for passing to Views. var context: Context - + var state: State { get { context.viewState } set { context.viewState = newValue } } - + init(initialViewState: State, mediaProvider: MediaProviderProtocol? = nil) { context = Context(initialViewState: initialViewState, mediaProvider: mediaProvider) context.viewModel = self } - + /// Override to handles incoming `ViewAction`s from the `ViewModel`. /// - Parameter viewAction: The `ViewAction` to be processed in `ViewModel` implementation. func process(viewAction: ViewAction) { // Default implementation, -no-op } - + // MARK: - Context /// A constrained and concise interface for interacting with the ViewModel. @@ -62,26 +62,26 @@ class StateStoreViewModelV2 { @MainActor @Observable final class Context { fileprivate weak var viewModel: StateStoreViewModelV2? - + /// Get-able property for the `ViewState` fileprivate(set) var viewState: State - + /// An optional image loading service so that views can manage themselves /// Intentionally non-generic so that it doesn't grow uncontrollably let mediaProvider: MediaProviderProtocol? - + /// Set-able access to the bindable state. subscript(dynamicMember keyPath: WritableKeyPath) -> T { get { viewState.bindings[keyPath: keyPath] } set { viewState.bindings[keyPath: keyPath] = newValue } } - + /// Send a `ViewAction` to the `ViewModel` for processing. /// - Parameter viewAction: The `ViewAction` to send to the `ViewModel`. func send(viewAction: ViewAction) { viewModel?.process(viewAction: viewAction) } - + fileprivate init(initialViewState: State, mediaProvider: MediaProviderProtocol?) { self.viewState = initialViewState self.mediaProvider = mediaProvider diff --git a/ElementX/Sources/Other/SwiftUI/Views/AppActivityView.swift b/ElementX/Sources/Other/SwiftUI/Views/AppActivityView.swift index 807ae4149..475a5a322 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/AppActivityView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/AppActivityView.swift @@ -11,13 +11,13 @@ import SwiftUI struct AppActivityView: UIViewControllerRepresentable { typealias UIViewControllerType = UIActivityViewController typealias CompletionType = (Result<(activity: UIActivity.ActivityType, items: [Any]?), Error>) -> Void - + private let activityItems: [Any] private let applicationActivities: [UIActivity]? private var excludedActivityTypes: [UIActivity.ActivityType] private var onCancel: (() -> Void)? private var onComplete: CompletionType? - + init(activityItems: [Any], applicationActivities: [UIActivity]? = nil, excludedActivityTypes: [UIActivity.ActivityType] = [], @@ -29,11 +29,11 @@ struct AppActivityView: UIViewControllerRepresentable { self.onCancel = onCancel self.onComplete = onComplete } - + func makeUIViewController(context: Context) -> UIViewControllerType { let viewController = UIViewControllerType(activityItems: activityItems, applicationActivities: applicationActivities) viewController.excludedActivityTypes = excludedActivityTypes - + viewController.completionWithItemsHandler = { activity, completed, items, error in if let error { onComplete?(.failure(error)) @@ -45,12 +45,12 @@ struct AppActivityView: UIViewControllerRepresentable { assertionFailure() } } - + return viewController } - + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { } - + static func dismantleUIViewController(_ uiViewController: UIViewControllerType, coordinator: Coordinator) { uiViewController.completionWithItemsHandler = nil } diff --git a/ElementX/Sources/Other/SwiftUI/Views/CopyTextButton.swift b/ElementX/Sources/Other/SwiftUI/Views/CopyTextButton.swift index 3b491649b..7607755fe 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/CopyTextButton.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/CopyTextButton.swift @@ -11,7 +11,7 @@ import SwiftUI /// A button that contains text that is copied on tap struct CopyTextButton: View { let content: String - + var body: some View { Button { UIPasteboard.general.string = content diff --git a/ElementX/Sources/Other/SwiftUI/Views/JoinCallButton.swift b/ElementX/Sources/Other/SwiftUI/Views/JoinCallButton.swift index ab63c6fb3..fcd97dd81 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/JoinCallButton.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/JoinCallButton.swift @@ -12,7 +12,7 @@ import SwiftUI struct JoinCallButton: View { let isVoiceCall: Bool let action: () -> Void - + var body: some View { if #available(iOS 26, *) { glassButton diff --git a/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift b/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift index c02bca337..b1aecbfcb 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/LoadableImage.swift @@ -358,7 +358,7 @@ struct LoadableImage_Previews: PreviewProvider, TestablePreview { mediaProvider: loadingMediaProvider, placeholder: placeholder) .layout(title: "Loading (avatar)") - + LoadableImage(url: "mxc://wherever/345", mediaType: .timelineItem(uniqueID: .init("id")), blurhash: "KbLM^j]q$jT|EfR-3rtjXk", @@ -388,7 +388,7 @@ struct LoadableImage_Previews: PreviewProvider, TestablePreview { static func placeholder() -> some View { Color.compound._bgBubbleIncoming } - + static func transformer(_ view: AnyView) -> some View { view.overlay { Image(systemSymbol: .playCircleFill) diff --git a/ElementX/Sources/Other/SwiftUI/Views/PlaceholderAvatarImage.swift b/ElementX/Sources/Other/SwiftUI/Views/PlaceholderAvatarImage.swift index 76f0d2595..4f2ded9e3 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/PlaceholderAvatarImage.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/PlaceholderAvatarImage.swift @@ -11,7 +11,7 @@ import SwiftUI struct PlaceholderAvatarImage: View { @Environment(\.redactionReasons) private var redactionReasons - + private let textForImage: String private let contentID: String @@ -32,18 +32,18 @@ struct PlaceholderAvatarImage: View { } .aspectRatio(1, contentMode: .fill) } - + init(name: String?, contentID: String) { let baseName = name ?? contentID.trimmingCharacters(in: .punctuationCharacters) textForImage = baseName.first?.uppercased() ?? "" self.contentID = contentID } - + private var backgroundColor: Color { if redactionReasons.contains(.placeholder) { return Color(.systemGray4) // matches the default text redaction } - + return avatarColor?.background ?? .compound.iconPrimary } diff --git a/ElementX/Sources/Other/SwiftUI/Views/RoundedCornerShape.swift b/ElementX/Sources/Other/SwiftUI/Views/RoundedCornerShape.swift index a5d4ed4b9..7ec3d5e08 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/RoundedCornerShape.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/RoundedCornerShape.swift @@ -11,42 +11,42 @@ import SwiftUI struct RoundedCornerShape: Shape { let radius: CGFloat let corners: UIRectCorner - + func path(in rect: CGRect) -> Path { var path = Path() - + let width = rect.size.width let height = rect.size.height - + var topLeft: CGFloat = corners.contains(.topLeft) ? radius : 0.0 var topRight: CGFloat = corners.contains(.topRight) ? radius : 0.0 var bottomLeft: CGFloat = corners.contains(.bottomLeft) ? radius : 0.0 var bottomRight: CGFloat = corners.contains(.bottomRight) ? radius : 0.0 - + // Make sure we do not exceed the size of the rectangle topRight = min(min(topRight, height / 2), width / 2) topLeft = min(min(topLeft, height / 2), width / 2) bottomLeft = min(min(bottomLeft, height / 2), width / 2) bottomRight = min(min(bottomRight, height / 2), width / 2) - + path.move(to: CGPoint(x: width / 2.0, y: 0)) path.addLine(to: CGPoint(x: width - topRight, y: 0)) path.addArc(center: CGPoint(x: width - topRight, y: topRight), radius: topRight, startAngle: Angle(degrees: -90), endAngle: Angle(degrees: 0), clockwise: false) - + path.addLine(to: CGPoint(x: width, y: height - bottomRight)) path.addArc(center: CGPoint(x: width - bottomRight, y: height - bottomRight), radius: bottomRight, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 90), clockwise: false) - + path.addLine(to: CGPoint(x: bottomLeft, y: height)) path.addArc(center: CGPoint(x: bottomLeft, y: height - bottomLeft), radius: bottomLeft, startAngle: Angle(degrees: 90), endAngle: Angle(degrees: 180), clockwise: false) - + path.addLine(to: CGPoint(x: 0, y: topLeft)) path.addArc(center: CGPoint(x: topLeft, y: topLeft), radius: topLeft, startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 270), clockwise: false) path.closeSubpath() - + return path } } diff --git a/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift b/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift index 7727bdc63..a36678b7e 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/StackedAvatarsView.swift @@ -48,7 +48,7 @@ struct StackedAvatarsView_Previews: PreviewProvider, TestablePreview { .init(url: nil, name: "Charlie", contentID: "@charlie:matrix.org"), .init(url: nil, name: "Dan", contentID: "@charlie:matrix.org") ] - + static var previews: some View { VStack(spacing: 10) { StackedAvatarsView(overlap: 16, diff --git a/ElementX/Sources/Other/SwiftUI/Views/TimelineScrollButton.swift b/ElementX/Sources/Other/SwiftUI/Views/TimelineScrollButton.swift index 49546f667..dfef66de5 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/TimelineScrollButton.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/TimelineScrollButton.swift @@ -16,29 +16,29 @@ struct TimelineScrollButton: View { case down case up } - + let direction: Direction let isHidden: Bool let showsBadge: Bool let onLongPress: (() -> Void)? let callback: () -> Void - + @ScaledMetric(relativeTo: .compound.bodyLG) private var iconSize: CGFloat = 24 @ScaledMetric(relativeTo: .compound.bodyLG) private var badgeDotSize: CGFloat = 12 @GestureState private var isPressing = false /// Set when a long-press is recognized so the trailing touch-up doesn't also fire `callback()`. @State private var consumedByLongPress = false - + /// Padding around the chevron, balanced so the total circle stays ~36pt at default text size. private let iconPadding: CGFloat = 6 - + /// Vertical offset that half-centers the badge dot on the circle's edge. Magnitude is half /// the dot's diameter so half sits inside the circle, half outside. Sign is applied at the /// call site based on direction. private var badgeOffsetMagnitude: CGFloat { badgeDotSize / 2 } - + init(direction: Direction = .down, isHidden: Bool = false, showsBadge: Bool = false, @@ -50,7 +50,7 @@ struct TimelineScrollButton: View { self.onLongPress = onLongPress self.callback = callback } - + var body: some View { if let onLongPress { buttonContent @@ -76,7 +76,7 @@ struct TimelineScrollButton: View { buttonContent } } - + private var buttonContent: some View { Button { if consumedByLongPress { @@ -99,7 +99,7 @@ struct TimelineScrollButton: View { .accessibilityLabel(direction == .down ? L10n.a11yJumpToBottom : L10n.a11yJumpToUnread) .animation(.elementDefault, value: isHidden) } - + @ViewBuilder private var icon: some View { let chevron = CompoundIcon(direction == .down ? \.chevronDown : \.chevronUp, @@ -117,7 +117,7 @@ struct TimelineScrollButton: View { .overlay(Circle().stroke(Color.compound.borderInteractiveSecondary, lineWidth: 1)) } } - + private var badge: some View { Circle() .fill(Color.compound.iconAccentPrimary) diff --git a/ElementX/Sources/Other/SwiftUI/Views/ToolbarButton.swift b/ElementX/Sources/Other/SwiftUI/Views/ToolbarButton.swift index 04f34be93..07ec2beff 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/ToolbarButton.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/ToolbarButton.swift @@ -15,7 +15,7 @@ struct ToolbarButton: View { static let done = Role.confirm(title: L10n.actionDone) static let save = Role.confirm(title: L10n.actionSave) static let close = Role.cancel(title: L10n.actionClose) - + case cancel(title: String) case confirm(title: String) case destructive(title: String) diff --git a/ElementX/Sources/Other/SwiftUI/Views/TopBannerModifier.swift b/ElementX/Sources/Other/SwiftUI/Views/TopBannerModifier.swift index 4f0bbd178..ac795fd44 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/TopBannerModifier.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/TopBannerModifier.swift @@ -46,7 +46,7 @@ extension View { func topBanner(_ banner: some View, isVisible: Bool, footer: some View = EmptyView()) -> some View { topBanners([TopBannerLayer(banner, isVisible: isVisible)], footer: footer) } - + /// Overlays the given Z-axis banner slots at the top edge of this view. /// Later items in the array are overlayed on top of earlier ones. Within /// each slot, visible vertical banners are stacked in a VStack and slide diff --git a/ElementX/Sources/Other/SwiftUI/Views/VisualListItem.swift b/ElementX/Sources/Other/SwiftUI/Views/VisualListItem.swift index d7b6d9be9..342a78ab8 100644 --- a/ElementX/Sources/Other/SwiftUI/Views/VisualListItem.swift +++ b/ElementX/Sources/Other/SwiftUI/Views/VisualListItem.swift @@ -11,7 +11,7 @@ import SwiftUI /// Represents the position of a checkmark item in a list. enum ListPosition { case top, middle, bottom - + /// The corners that should be rounded for this position. var roundedCorners: UIRectCorner { switch self { @@ -57,11 +57,11 @@ private struct VisualListItemLabelStyle: LabelStyle { struct VisualListItem_Previews: PreviewProvider, TestablePreview { static let strings = AnalyticsPromptScreenStrings(termsURL: AppSettings.volatile().analyticsTermsURL) - + static var testImage1: some View { Image(systemName: "circle") } - + static var testImage2: some View { Image(systemName: "square") } diff --git a/ElementX/Sources/Other/UserAgentBuilder.swift b/ElementX/Sources/Other/UserAgentBuilder.swift index 69d45aaa1..52e7a6476 100644 --- a/ElementX/Sources/Other/UserAgentBuilder.swift +++ b/ElementX/Sources/Other/UserAgentBuilder.swift @@ -21,7 +21,7 @@ enum UserAgentBuilder { static func makeUserAgent() -> String? { let clientName = InfoPlistReader.app.bundleDisplayName let clientVersion = InfoPlistReader.app.bundleShortVersionString - + #if os(iOS) let scale = UIScreen.main.scale return if ProcessInfo.processInfo.isiOSAppOnMac { diff --git a/ElementX/Sources/Other/UserIndicator/UserIndicator.swift b/ElementX/Sources/Other/UserIndicator/UserIndicator.swift index 754f4ff89..cc9105603 100644 --- a/ElementX/Sources/Other/UserIndicator/UserIndicator.swift +++ b/ElementX/Sources/Other/UserIndicator/UserIndicator.swift @@ -17,7 +17,7 @@ enum UserIndicatorType: Equatable { static var toast: Self { .toast(progress: .none) } - + static var modal: Self { .modal(progress: .indeterminate, interactiveDismissDisabled: false, allowsInteraction: false) } diff --git a/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift b/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift index e11cf3d9a..93df1249a 100644 --- a/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift +++ b/ElementX/Sources/Other/UserIndicator/UserIndicatorController.swift @@ -54,7 +54,7 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol guard delayedIndicators.contains(indicator.id) else { return } - + enqueue(indicator: indicator) } } else { @@ -85,7 +85,7 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol } // MARK: - Private - + private func enqueue(indicator: UserIndicator) { retractIndicatorWithId(indicator.id) indicatorQueue.append(indicator) diff --git a/ElementX/Sources/Other/UserIndicator/UserIndicatorModalView.swift b/ElementX/Sources/Other/UserIndicator/UserIndicatorModalView.swift index 17240d053..ac6409b0c 100644 --- a/ElementX/Sources/Other/UserIndicator/UserIndicatorModalView.swift +++ b/ElementX/Sources/Other/UserIndicator/UserIndicatorModalView.swift @@ -13,7 +13,7 @@ import SwiftUI struct UserIndicatorModalView: View { let indicator: UserIndicator @State private var progressFraction = 0.0 - + var body: some View { ZStack { VStack(spacing: 12.0) { @@ -22,7 +22,7 @@ struct UserIndicatorModalView: View { } else if case .published = indicator.progress { ProgressView(value: progressFraction) } - + HStack(spacing: 8) { if let icon = indicator.icon { CompoundIcon(icon, size: iconSize, relativeTo: titleFont) diff --git a/ElementX/Sources/Other/VoiceMessage/EstimatedWaveformView.swift b/ElementX/Sources/Other/VoiceMessage/EstimatedWaveformView.swift index ccab61890..b85851105 100644 --- a/ElementX/Sources/Other/VoiceMessage/EstimatedWaveformView.swift +++ b/ElementX/Sources/Other/VoiceMessage/EstimatedWaveformView.swift @@ -20,7 +20,7 @@ extension EstimatedWaveform { guard maxSamplesCount > 0 else { return [] } - + // Filter the data to keep only the expected number of samples let result: [UInt16] if data.count > maxSamplesCount { @@ -32,7 +32,7 @@ extension EstimatedWaveform { } else { result = data } - + // Normalize the sample in the allowed range return result.map { Float($0) / Float(Self.dataRange.upperBound) } } diff --git a/ElementX/Sources/Other/VoiceMessage/PlaybackSpeedButton.swift b/ElementX/Sources/Other/VoiceMessage/PlaybackSpeedButton.swift index 7eb65738c..69a59852f 100644 --- a/ElementX/Sources/Other/VoiceMessage/PlaybackSpeedButton.swift +++ b/ElementX/Sources/Other/VoiceMessage/PlaybackSpeedButton.swift @@ -10,14 +10,14 @@ import SwiftUI struct PlaybackSpeedButton: View { let speed: AudioPlaybackSpeed let onTap: () -> Void - + var body: some View { Button(action: onTap) { ZStack { Text(speed.placeholder) .font(.compound.bodyXSSemibold) .hidden() - + Text(speed.label) .font(.compound.bodyXSSemibold) .foregroundColor(.compound.iconSecondary) diff --git a/ElementX/Sources/Other/VoiceMessage/VoiceMessageButton.swift b/ElementX/Sources/Other/VoiceMessage/VoiceMessageButton.swift index 4bfc45fce..b84a9a7c7 100644 --- a/ElementX/Sources/Other/VoiceMessage/VoiceMessageButton.swift +++ b/ElementX/Sources/Other/VoiceMessage/VoiceMessageButton.swift @@ -44,7 +44,7 @@ struct VoiceMessageButton: View { self.state = state self.action = action } - + var body: some View { Button(action: action) { buttonLabel @@ -58,7 +58,7 @@ struct VoiceMessageButton: View { .disabled(state == .loading) .accessibilityLabel(accessibilityLabel) } - + @ViewBuilder private var buttonLabel: some View { switch state { @@ -87,7 +87,7 @@ private struct VoiceMessageButtonStyle: ButtonStyle { @Environment(\.isEnabled) var isEnabled: Bool let color: Color - + func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundColor(isEnabled ? color.opacity(configuration.isPressed ? 0.6 : 1) : .compound.iconDisabled) diff --git a/ElementX/Sources/Other/VoiceMessage/WaveformCursorView.swift b/ElementX/Sources/Other/VoiceMessage/WaveformCursorView.swift index 4731d12cd..bdc35793a 100644 --- a/ElementX/Sources/Other/VoiceMessage/WaveformCursorView.swift +++ b/ElementX/Sources/Other/VoiceMessage/WaveformCursorView.swift @@ -11,7 +11,7 @@ import SwiftUI struct WaveformCursorView: View { var color: Color = .compound.iconAccentTertiary - + var body: some View { RoundedRectangle(cornerRadius: 1) .fill(color) diff --git a/ElementX/Sources/Other/VoiceMessage/WaveformInteractionModifier.swift b/ElementX/Sources/Other/VoiceMessage/WaveformInteractionModifier.swift index d8c346f36..8886023cc 100644 --- a/ElementX/Sources/Other/VoiceMessage/WaveformInteractionModifier.swift +++ b/ElementX/Sources/Other/VoiceMessage/WaveformInteractionModifier.swift @@ -19,11 +19,11 @@ private struct WaveformInteractionModifier: ViewModifier { let progress: Double let showCursor: Bool let onSeek: (Double) -> Void - + @ScaledMetric private var cursorVisibleWidth = 2.0 @ScaledMetric private var cursorVisibleHeight = 24.0 private let cursorInteractiveSize: CGFloat = 50 - + func body(content: Content) -> some View { GeometryReader { geometry in content @@ -52,6 +52,6 @@ private struct WaveformInteractionModifier: ViewModifier { .coordinateSpace(name: Self.namespaceName) .animation(nil, value: progress) } - + private static let namespaceName = "voice-message-waveform" } diff --git a/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenCoordinator.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenCoordinator.swift index 58fa39a84..9041e1942 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenCoordinator.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenCoordinator.swift @@ -48,7 +48,7 @@ final class AppLockScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(AppLockScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenModels.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenModels.swift index 497713567..17b8fc16b 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenModels.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenModels.swift @@ -30,12 +30,12 @@ struct AppLockScreenViewState: BindableState { var numberOfDigitsEntered: Int { bindings.pinCode.count } - + /// Whether the subtitle is in a warning state or not. var isSubtitleWarning: Bool { numberOfPINAttempts > 0 } - + /// The string shown in the screen's subtitle. var subtitle: String { if !isSubtitleWarning { diff --git a/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenViewModel.swift b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenViewModel.swift index e86bbb34e..68e595926 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenViewModel.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockScreen/AppLockScreenViewModel.swift @@ -18,7 +18,7 @@ class AppLockScreenViewModel: AppLockScreenViewModelType, AppLockScreenViewModel var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(appLockService: AppLockServiceProtocol) { self.appLockService = appLockService diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenCoordinator.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenCoordinator.swift index 380d0303e..d9395202e 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenCoordinator.swift @@ -42,7 +42,7 @@ final class AppLockSetupBiometricsScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(AppLockSetupBiometricsScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenModels.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenModels.swift index 08af4e403..ecd9885c4 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenModels.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenModels.swift @@ -20,11 +20,11 @@ struct AppLockSetupBiometricsScreenViewState: BindableState { var icon: SFSymbol { biometryType.systemSymbol } - + var title: String { L10n.screenAppLockSetupBiometricUnlockAllowTitle(biometryType.localizedString) } - + var subtitle: String { L10n.screenAppLockSetupBiometricUnlockSubtitle(biometryType.localizedString) } diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenViewModel.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenViewModel.swift index da7c1175d..f3c6e1c70 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenViewModel.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupBiometricsScreen/AppLockSetupBiometricsScreenViewModel.swift @@ -18,7 +18,7 @@ class AppLockSetupBiometricsScreenViewModel: AppLockSetupBiometricsScreenViewMod var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(appLockService: AppLockServiceProtocol) { self.appLockService = appLockService super.init(initialViewState: AppLockSetupBiometricsScreenViewState(biometryType: appLockService.biometryType)) diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenCoordinator.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenCoordinator.swift index 897ebcbbd..f80282aa9 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenCoordinator.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenCoordinator.swift @@ -60,7 +60,7 @@ final class AppLockSetupPINScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(AppLockSetupPINScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift index e7d038156..4a812fd86 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenModels.swift @@ -53,7 +53,7 @@ struct AppLockSetupPINScreenViewState: BindableState { var isSubtitleWarning: Bool { mode == .unlock && numberOfUnlockAttempts > 0 } - + var subtitle: String { guard mode == .unlock else { return L10n.screenAppLockSetupPinContext(InfoPlistReader.main.bundleDisplayName) } if !isSubtitleWarning { diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift index 53d355c39..62536a5a7 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupPINScreen/AppLockSetupPINScreenViewModel.swift @@ -21,7 +21,7 @@ class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLoc var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(initialMode: AppLockSetupPINScreenMode, isMandatory: Bool, appLockService: AppLockServiceProtocol) { self.appLockService = appLockService @@ -103,7 +103,7 @@ class AppLockSetupPINScreenViewModel: AppLockSetupPINScreenViewModelType, AppLoc return } } - + actionsSubject.send(.complete) } diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/AppLockSetupSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/AppLockSetupSettingsScreenCoordinator.swift index 13c9bec8a..a124c190f 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/AppLockSetupSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/AppLockSetupSettingsScreenCoordinator.swift @@ -45,7 +45,7 @@ final class AppLockSetupSettingsScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(AppLockSetupSettingsScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/AppLockSetupSettingsScreenModels.swift b/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/AppLockSetupSettingsScreenModels.swift index edb2fc270..fba2cdc80 100644 --- a/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/AppLockSetupSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/AppLock/AppLockSetupSettingsScreen/AppLockSetupSettingsScreenModels.swift @@ -25,7 +25,7 @@ struct AppLockSetupSettingsScreenViewState: BindableState { var supportsBiometrics: Bool { biometryType != .none } - + var enableBiometricsTitle: String { L10n.screenAppLockSetupBiometricUnlockAllowTitle(biometryType.localizedString) } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift index 5979887df..4667afc72 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenCoordinator.swift @@ -30,11 +30,11 @@ enum LoginScreenCoordinatorAction { final class LoginScreenCoordinator: CoordinatorProtocol { private let parameters: LoginScreenCoordinatorParameters private var viewModel: LoginScreenViewModelProtocol - + private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService } - + private let actionsSubject: PassthroughSubject = .init() private var cancellables = Set() @@ -55,7 +55,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol { } // MARK: - Public - + func start() { viewModel.actions .sink { [weak self] action in @@ -70,7 +70,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func stop() { viewModel.stopLoading() } diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift index ab608a2a2..e785349b8 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/LoginScreenViewModel.swift @@ -21,7 +21,7 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(authenticationService: AuthenticationServiceProtocol, loginHint: String?, userIndicatorController: UserIndicatorControllerProtocol, @@ -48,7 +48,7 @@ class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtoc .weakAssign(to: \.state.homeserver, on: self) .store(in: &cancellables) } - + override func process(viewAction: LoginScreenViewAction) { switch viewAction { case .parseUsername: diff --git a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift index 25da7739d..f3793c866 100644 --- a/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift +++ b/ElementX/Sources/Screens/Authentication/LoginScreen/View/LoginScreen.swift @@ -92,7 +92,7 @@ struct LoginScreen: View { .onSubmit(submit) Spacer().frame(height: 32) - + Button(action: submit) { Text(L10n.actionContinue) } diff --git a/ElementX/Sources/Screens/Authentication/OAuthAuthenticationPresenter.swift b/ElementX/Sources/Screens/Authentication/OAuthAuthenticationPresenter.swift index 4d4108a84..b9039db00 100644 --- a/ElementX/Sources/Screens/Authentication/OAuthAuthenticationPresenter.swift +++ b/ElementX/Sources/Screens/Authentication/OAuthAuthenticationPresenter.swift @@ -139,7 +139,7 @@ class OAuthAuthenticationPresenter: NSObject { private var loadingIndicatorID: String { "\(Self.self)-Loading" } - + private var failureIndicatorID: String { "\(Self.self)-Failure" } diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift index 5c4c71c31..8ddd70042 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenCoordinator.swift @@ -60,7 +60,7 @@ final class ServerConfirmationScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(ServerConfirmationScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift index 0af2c6bef..dc71e4df3 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenModels.swift @@ -52,7 +52,7 @@ struct ServerConfirmationScreenViewState: BindableState { /// The message shown beneath the title. var message: String? { guard case let .confirmation(homeserverAddress) = mode else { return nil } - + return switch authenticationFlow { case .login: if homeserverAddress == "matrix.org" { diff --git a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift index 396b9a27e..b49300d45 100644 --- a/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerConfirmationScreen/ServerConfirmationScreenViewModel.swift @@ -22,7 +22,7 @@ class ServerConfirmationScreenViewModel: ServerConfirmationScreenViewModelType, var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(authenticationService: AuthenticationServiceProtocol, mode: ServerConfirmationScreenMode, authenticationFlow: AuthenticationFlow, diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift index e5baec76e..7864a0268 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenCoordinator.swift @@ -30,7 +30,7 @@ final class ServerSelectionScreenCoordinator: CoordinatorProtocol { private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService } - + private let actionsSubject: PassthroughSubject = .init() private var cancellables = Set() diff --git a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift index d35155b25..550fe4a6d 100644 --- a/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/ServerSelectionScreen/ServerSelectionScreenViewModel.swift @@ -22,7 +22,7 @@ class ServerSelectionScreenViewModel: ServerSelectionScreenViewModelType, Server var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(authenticationService: AuthenticationServiceProtocol, authenticationFlow: AuthenticationFlow, appSettings: AppSettings, diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/MockSoftLogoutScreenState.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/MockSoftLogoutScreenState.swift index 73e8ef4c4..b6ded78ca 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/MockSoftLogoutScreenState.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/MockSoftLogoutScreenState.swift @@ -20,7 +20,7 @@ enum MockSoftLogoutScreenState: String, CaseIterable { case oAuth case unsupported case keyBackupNeeded - + /// Generate the view struct for the screen state. @MainActor var viewModel: SoftLogoutScreenViewModel { let credentials = SoftLogoutScreenCredentials(userID: "@mock:matrix.org", diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift index f26e3e243..962a1a224 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenCoordinator.swift @@ -46,7 +46,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { private var authenticationService: AuthenticationServiceProtocol { parameters.authenticationService } - + private var oAuthPresenter: OAuthAuthenticationPresenter? var actions: AnyPublisher { @@ -69,7 +69,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { .sink { [weak self] action in guard let self else { return } MXLog.info("Did complete with result: \(action).") - + switch action { case .login(let password): login(withPassword: password) @@ -117,18 +117,18 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { @MainActor private func stopLoading() { parameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) } - + /// Shows the forgot password screen. @MainActor private func showForgotPasswordScreen() { viewModel.displayError(.alert("Not implemented.")) } - + /// Login with the supplied username and password. @MainActor private func login(withPassword password: String) { let username = parameters.credentials.userID - + startLoading() - + Task { switch await authenticationService.login(username: username, password: password, @@ -143,7 +143,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { } } } - + private func continueWithOAuth(presentationAnchor: UIWindow?) { guard let presentationAnchor else { return } @@ -174,7 +174,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol { } } } - + /// Processes an error to either update the flow or display it to the user. private func handleError(_ error: AuthenticationServiceError) { switch error { diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenModels.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenModels.swift index 598fce80b..d2cd1712b 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenModels.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenModels.swift @@ -43,16 +43,16 @@ enum SoftLogoutScreenViewModelAction: CustomStringConvertible { struct SoftLogoutScreenViewState: BindableState { /// Soft logout credentials var credentials: SoftLogoutScreenCredentials - + /// Data about the selected homeserver. var homeserver: LoginHomeserver - + /// Flag indicating soft logged out user needs backup for some keys var keyBackupNeeded: Bool - + /// View state that can be bound to from SwiftUI. var bindings: SoftLogoutScreenBindings - + /// The types of login supported by the homeserver. var loginMode: LoginMode { homeserver.loginMode @@ -60,12 +60,12 @@ struct SoftLogoutScreenViewState: BindableState { /// The presentation anchor used for OAuth authentication. var window: UIWindow? - + /// Whether to show recover encryption keys message var showRecoverEncryptionKeysMessage: Bool { keyBackupNeeded } - + /// `true` when valid credentials have been entered and a homeserver has been loaded. var canSubmit: Bool { !bindings.password.isEmpty diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModel.swift index 402061977..58b0fba34 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/SoftLogoutScreenViewModel.swift @@ -17,7 +17,7 @@ class SoftLogoutScreenViewModel: SoftLogoutScreenViewModelType, SoftLogoutScreen var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(credentials: SoftLogoutScreenCredentials, homeserver: LoginHomeserver, keyBackupNeeded: Bool, @@ -45,7 +45,7 @@ class SoftLogoutScreenViewModel: SoftLogoutScreenViewModelType, SoftLogoutScreen Task { state.window = window } } } - + @MainActor func displayError(_ type: SoftLogoutScreenErrorType) { switch type { case .alert(let message): diff --git a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/View/SoftLogoutScreen.swift b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/View/SoftLogoutScreen.swift index 2a9e9045c..37de2db17 100644 --- a/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/View/SoftLogoutScreen.swift +++ b/ElementX/Sources/Screens/Authentication/SoftLogoutScreen/View/SoftLogoutScreen.swift @@ -10,10 +10,10 @@ import SwiftUI struct SoftLogoutScreen: View { @State private var showingClearDataConfirmation = false - + /// The focus state of the password text field. @FocusState private var isPasswordFocused: Bool - + @Bindable var context: SoftLogoutScreenViewModel.Context var body: some View { @@ -22,7 +22,7 @@ struct SoftLogoutScreen: View { header .padding(.top, UIConstants.titleTopPaddingToNavigationBar) .padding(.bottom, 36) - + switch context.viewState.loginMode { case .password: loginForm @@ -31,7 +31,7 @@ struct SoftLogoutScreen: View { default: loginUnavailableText } - + clearDataForm .padding(.top, 16) } @@ -45,7 +45,7 @@ struct SoftLogoutScreen: View { context.send(viewAction: .updateWindow(window)) } } - + /// The title, message and icon at the top of the screen. var header: some View { VStack(alignment: .leading, spacing: 16) { @@ -54,13 +54,13 @@ struct SoftLogoutScreen: View { .multilineTextAlignment(.leading) .foregroundColor(.compound.textPrimary) .accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.title) - + Text(UntranslatedL10n.softLogoutSigninNotice(context.viewState.credentials.homeserverName, context.viewState.credentials.userDisplayName, context.viewState.credentials.userID)) .font(.compound.bodyLG) .multilineTextAlignment(.leading) .foregroundColor(.compound.textPrimary) .accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.message) - + if context.viewState.showRecoverEncryptionKeysMessage { Text(UntranslatedL10n.softLogoutSigninE2eWarningNotice) .font(.compound.bodyLG) @@ -69,7 +69,7 @@ struct SoftLogoutScreen: View { } } } - + /// The form with text fields for username and password, along with a submit button. var loginForm: some View { VStack(spacing: 14) { @@ -80,7 +80,7 @@ struct SoftLogoutScreen: View { .submitLabel(.done) .onSubmit(submit) .accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.password) - + Button { context.send(viewAction: .forgotPassword) } label: { Text(L10n.actionForgotPassword) .font(.compound.bodyLG) @@ -88,7 +88,7 @@ struct SoftLogoutScreen: View { .frame(maxWidth: .infinity, alignment: .trailing) .padding(.bottom, 8) .accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.forgotPassword) - + Button(action: submit) { Text(L10n.actionNext) } @@ -97,7 +97,7 @@ struct SoftLogoutScreen: View { .accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.next) } } - + /// The OAuth button that can be used for login. var oAuthButton: some View { Button { context.send(viewAction: .continueWithOAuth) } label: { @@ -105,7 +105,7 @@ struct SoftLogoutScreen: View { } .buttonStyle(.compound(.primary)) } - + /// Text shown if neither password or OAuth login is supported. var loginUnavailableText: some View { Text(L10n.screenLoginErrorUnsupportedAuthentication) @@ -114,7 +114,7 @@ struct SoftLogoutScreen: View { .foregroundColor(.compound.textPrimary) .frame(maxWidth: .infinity) } - + /// The text field and submit button where the user enters an email address. var clearDataForm: some View { VStack(alignment: .leading, spacing: 12) { @@ -123,14 +123,14 @@ struct SoftLogoutScreen: View { .multilineTextAlignment(.leading) .foregroundColor(.compound.textPrimary) .accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.clearDataTitle) - + Text(UntranslatedL10n.softLogoutClearDataNotice) .font(.compound.bodyLG) .multilineTextAlignment(.leading) .foregroundColor(.compound.textPrimary) .accessibilityIdentifier(A11yIdentifiers.softLogoutScreen.clearDataMessage) .padding(.bottom, 12) - + Button(role: .destructive, action: clearData) { Text(UntranslatedL10n.softLogoutClearDataSubmit) } @@ -146,13 +146,13 @@ struct SoftLogoutScreen: View { } } } - + /// Sends the `login` view action so long as a valid email address has been input. func submit() { guard context.viewState.canSubmit else { return } context.send(viewAction: .login) } - + /// Sends the `clearAllData` view action. func clearData() { context.send(viewAction: .clearAllData) @@ -167,7 +167,7 @@ struct SoftLogoutScreen_Previews: PreviewProvider, TestablePreview { screen(for: state.viewModel) } } - + static func screen(for viewModel: SoftLogoutScreenViewModel) -> some View { ElementNavigationStack { SoftLogoutScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenCoordinator.swift b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenCoordinator.swift index bbdbfcbc7..86123c966 100644 --- a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenCoordinator.swift @@ -64,12 +64,12 @@ final class AuthenticationStartScreenCoordinator: CoordinatorProtocol { actionsSubject.send(.login) case .register: actionsSubject.send(.register) - + case .loginDirectlyWithOAuth(let data, let window): actionsSubject.send(.loginDirectlyWithOAuth(data: data, window: window)) case .loginDirectlyWithPassword(let loginHint): actionsSubject.send(.loginDirectlyWithPassword(loginHint: loginHint)) - + case .reportProblem: actionsSubject.send(.reportProblem) case .developerOptions: diff --git a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift index 3e16fd313..e824c0df9 100644 --- a/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift +++ b/ElementX/Sources/Screens/Authentication/StartScreen/AuthenticationStartScreenViewModel.swift @@ -89,14 +89,14 @@ class AuthenticationStartScreenViewModel: AuthenticationStartScreenViewModelType } case .developerOptions: actionsSubject.send(.developerOptions) - + case .loginWithQR: actionsSubject.send(.loginWithQR) case .login: Task { await login() } case .register: actionsSubject.send(.register) - + case .continueWithClassic(let account): Task { await login(classicAppAccount: account) } case .otherOptions(let account): diff --git a/ElementX/Sources/Screens/Authentication/StartScreen/View/AuthenticationStartLogo.swift b/ElementX/Sources/Screens/Authentication/StartScreen/View/AuthenticationStartLogo.swift index a05100cee..418889a34 100644 --- a/ElementX/Sources/Screens/Authentication/StartScreen/View/AuthenticationStartLogo.swift +++ b/ElementX/Sources/Screens/Authentication/StartScreen/View/AuthenticationStartLogo.swift @@ -65,12 +65,12 @@ private struct AuthenticationBrandLogoModifier: ViewModifier { private var isLight: Bool { colorScheme == .light } - + /// Extra padding needed to avoid cropping the shadows. private var extra: CGFloat { 64 * scale } - + /// The shape that the logo is composed on top of. private var outerShape: RoundedRectangle { RoundedRectangle(cornerRadius: 44 * scale) diff --git a/ElementX/Sources/Screens/Authentication/UIConstants.swift b/ElementX/Sources/Screens/Authentication/UIConstants.swift index 9387fb8d0..3082bce0f 100644 --- a/ElementX/Sources/Screens/Authentication/UIConstants.swift +++ b/ElementX/Sources/Screens/Authentication/UIConstants.swift @@ -21,7 +21,7 @@ enum UIConstants { /// The padding used to the top of the view for breaker screens that don't have a navigation bar. static let startScreenBreakerScreenTopPadding: CGFloat = 80 - + /// The height to use for top/bottom spacers to pad the views to fit the `maxContentHeight`. static func spacerHeight(in geometry: GeometryProxy) -> CGFloat { max(0, (geometry.size.height - maxContentHeight) / 2) diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift index d256f0063..1d2bd2792 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenCoordinator.swift @@ -27,7 +27,7 @@ final class BlockedUsersScreenCoordinator: CoordinatorProtocol { func stop() { viewModel.stop() } - + func toPresentable() -> AnyView { AnyView(BlockedUsersScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift index 2d120eff6..81d858a2b 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift @@ -15,7 +15,7 @@ class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsers let hideProfiles: Bool let clientProxy: ClientProxyProtocol let userIndicatorController: UserIndicatorControllerProtocol - + init(hideProfiles: Bool, userSession: UserSessionProtocol, userIndicatorController: UserIndicatorControllerProtocol) { diff --git a/ElementX/Sources/Screens/BugReportScreen/BugReportScreenCoordinator.swift b/ElementX/Sources/Screens/BugReportScreen/BugReportScreenCoordinator.swift index 4d36d91de..69b41cd1d 100644 --- a/ElementX/Sources/Screens/BugReportScreen/BugReportScreenCoordinator.swift +++ b/ElementX/Sources/Screens/BugReportScreen/BugReportScreenCoordinator.swift @@ -69,7 +69,7 @@ final class BugReportScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func stop() { stopLoading() } diff --git a/ElementX/Sources/Screens/BugReportScreen/BugReportScreenViewModel.swift b/ElementX/Sources/Screens/BugReportScreen/BugReportScreenViewModel.swift index be0bbc52c..5721ec4b0 100644 --- a/ElementX/Sources/Screens/BugReportScreen/BugReportScreenViewModel.swift +++ b/ElementX/Sources/Screens/BugReportScreen/BugReportScreenViewModel.swift @@ -20,7 +20,7 @@ class BugReportScreenViewModel: BugReportScreenViewModelType, BugReportScreenVie private let actionsSubject: PassthroughSubject = .init() // periphery:ignore - when set to nil this is automatically cancelled @CancellableTask private var uploadTask: Task? - + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } @@ -79,7 +79,7 @@ class BugReportScreenViewModel: BugReportScreenViewModelType, BugReportScreenVie } return true } - + private func submitBugReport() async { let progressSubject = CurrentValueSubject(0.0) diff --git a/ElementX/Sources/Screens/BugReportScreen/View/BugReportScreen.swift b/ElementX/Sources/Screens/BugReportScreen/View/BugReportScreen.swift index 1a51144d1..79ba5012d 100644 --- a/ElementX/Sources/Screens/BugReportScreen/View/BugReportScreen.swift +++ b/ElementX/Sources/Screens/BugReportScreen/View/BugReportScreen.swift @@ -18,7 +18,7 @@ struct BugReportScreen: View { var canSendLogFiles: Bool { context.viewState.canSendLogFiles } - + var photosPickerTitle: String { context.viewState.screenshot == nil ? L10n.screenBugReportAttachScreenshot : L10n.screenBugReportEditScreenshot } @@ -82,7 +82,7 @@ struct BugReportScreen: View { } } } - + private var canContactSection: some View { Section { ListRow(label: .plain(title: L10n.screenBugReportContactMeTitle), @@ -93,7 +93,7 @@ struct BugReportScreen: View { .compoundListSectionFooter() } } - + private var attachScreenshotSection: some View { Section { ListRow(kind: .custom { diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift b/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift index ae24b3e7d..d81522216 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenCoordinator.swift @@ -70,7 +70,7 @@ final class CallScreenCoordinator: CoordinatorProtocol { func stop() { viewModel.stop() } - + func toPresentable() -> AnyView { AnyView(CallScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift index 63ce49819..78bb60e0e 100644 --- a/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift +++ b/ElementX/Sources/Screens/CallScreen/CallScreenViewModel.swift @@ -29,7 +29,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol @CancellableTask private var timeoutTask: Task? - + /// Designated initialiser /// - Parameters: /// - elementCallService: service responsible for setting up CallKit @@ -140,7 +140,7 @@ class CallScreenViewModel: CallScreenViewModelType, CallScreenViewModelProtocol } // MARK: - Private - + private func handleWidgetAction(message: String) async { if timeoutTask != nil, let decodedMessage = try? DecodedWidgetMessage.decode(message: message), diff --git a/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift b/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift index 951e7466b..735bd20de 100644 --- a/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift +++ b/ElementX/Sources/Screens/CallScreen/View/CallScreen.swift @@ -196,7 +196,7 @@ private struct CallView: UIViewRepresentable { guard let body = message.body as? [String: String], let level = body["level"], let logMessage = body["message"] else { return } - + switch level { case "log", "debug": MXLog.debug("[ElementCall]: \(logMessage)") @@ -363,7 +363,7 @@ struct CallScreen_Previews: PreviewProvider { widgetDriver.startBaseURLClientIDColorSchemeVoiceOnlyRageshakeURLAnalyticsConfigurationReturnValue = .success(URL.userDirectory) roomProxy.elementCallWidgetDriverDeviceIDReturnValue = widgetDriver - + return CallScreenViewModel(elementCallService: ElementCallServiceMock(.init()), configuration: .init(roomProxy: roomProxy, clientProxy: clientProxy, diff --git a/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFilterCell.swift b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFilterCell.swift index 686def107..87a846cbf 100644 --- a/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFilterCell.swift +++ b/ElementX/Sources/Screens/ChatsSpaceFiltersScreen/View/ChatsSpaceFilterCell.swift @@ -14,12 +14,12 @@ struct ChatsSpaceFilterCell: View { let filter: SpaceServiceFilter let mediaProvider: MediaProviderProtocol! - + private let verticalInsets = 12.0 private let horizontalInsets = 16.0 let action: (SpaceServiceFilter) -> Void - + var body: some View { Button { action(filter) diff --git a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenCoordinator.swift b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenCoordinator.swift index f43c6f247..05eb72dae 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenCoordinator.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenCoordinator.swift @@ -51,7 +51,7 @@ final class PollFormScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(PollFormScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift index adfdc2eb1..3347c9ddf 100644 --- a/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift +++ b/ElementX/Sources/Screens/CreatePollScreen/PollFormScreenModels.swift @@ -65,7 +65,7 @@ struct PollFormScreenViewState: BindableState { case .edit(_, let poll): initialBindings = .init(poll: poll) } - + return bindings != initialBindings } } @@ -79,16 +79,16 @@ struct PollFormScreenViewStateBindings: Equatable { var question = "" var options: [Option] = [.init(), .init()] var isUndisclosed = false - + struct Option: Identifiable, Equatable { let id = UUID() var text = "" } - + var hasValidContent: Bool { !question.isEmpty && options.count >= 2 && options.allSatisfy { !$0.text.isEmpty } } - + var alertInfo: AlertInfo? static func == (lhs: PollFormScreenViewStateBindings, rhs: PollFormScreenViewStateBindings) -> Bool { diff --git a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenCoordinator.swift b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenCoordinator.swift index 3248aee39..11552a256 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenCoordinator.swift @@ -62,7 +62,7 @@ final class CreateRoomScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(CreateRoomScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift index 1a34c4f0a..6ea8962f5 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenModels.swift @@ -51,7 +51,7 @@ struct CreateRoomScreenViewState: BindableState { var canCreateRoom: Bool { !roomName.isEmpty && aliasErrors.isEmpty } - + var aliasErrors: Set = [] var aliasErrorDescription: String? { if aliasErrors.contains(.alreadyExists) { @@ -62,7 +62,7 @@ struct CreateRoomScreenViewState: BindableState { nil } } - + var availableAccessTypes: [CreateRoomScreenAccessType] { var availableAccessTypes: [CreateRoomScreenAccessType] = [] if isSpace { diff --git a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift index c944927a9..96ca8cd53 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/CreateRoomScreenViewModel.swift @@ -58,7 +58,7 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen let bindings = CreateRoomScreenViewStateBindings(roomTopic: "", selectedAccessType: selectedAccessType, selectedSpace: selectedSpace) - + super.init(initialViewState: CreateRoomScreenViewState(isSpace: isSpace, shouldShowCancelButton: shouldShowCancelButton, roomName: "", @@ -137,7 +137,7 @@ class CreateRoomScreenViewModel: CreateRoomScreenViewModelType, CreateRoomScreen } // MARK: - Private - + private func setupBindings() { // Reset the state related to public rooms if the user choses the room to be empty context.$viewState diff --git a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift index 1baa346ca..1a13bb848 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomScreen.swift @@ -12,7 +12,7 @@ import SwiftUI struct CreateRoomScreen: View { @ObservedObject var context: CreateRoomScreenViewModel.Context @FocusState private var focus: Focus? - + private enum Focus { case name case topic @@ -380,7 +380,7 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { viewModel.context.selectedAccessType = .askToJoinWithSpaceMembers return viewModel }() - + static var previews: some View { ElementNavigationStack { CreateRoomScreen(context: viewModel.context) @@ -455,7 +455,7 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview { let spaces = [SpaceServiceRoom].mockJoinedSpaces2 clientProxy.spaceService = SpaceServiceProxyMock(.init(editableSpaces: spaces)) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) - + return CreateRoomScreenViewModel(isSpace: isSpace, spaceSelectionMode: selectionMode, shouldShowCancelButton: isSpace, diff --git a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomSpaceSelectionSheet.swift b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomSpaceSelectionSheet.swift index 2cebea7eb..38058e99e 100644 --- a/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomSpaceSelectionSheet.swift +++ b/ElementX/Sources/Screens/CreateRoomScreen/View/CreateRoomSpaceSelectionSheet.swift @@ -66,9 +66,9 @@ struct CreateRoomSpaceSelectionSheet_Previews: PreviewProvider, TestablePreview let clientProxy = ClientProxyMock(.init(userID: "@userid:example.com")) clientProxy.spaceService = SpaceServiceProxyMock(.init(editableSpaces: .mockJoinedSpaces2)) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) - + let appSettings = AppSettings.volatile() - + return CreateRoomScreenViewModel(isSpace: false, spaceSelectionMode: .editableSpacesList(preSelectedSpace: nil), shouldShowCancelButton: false, diff --git a/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenCoordinator.swift b/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenCoordinator.swift index 9c48823d0..f32cf8446 100644 --- a/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenCoordinator.swift +++ b/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenCoordinator.swift @@ -27,7 +27,7 @@ final class DeclineAndBlockScreenCoordinator: CoordinatorProtocol { private let viewModel: DeclineAndBlockScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -54,7 +54,7 @@ final class DeclineAndBlockScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(DeclineAndBlockScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenViewModel.swift b/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenViewModel.swift index b1a91c7d3..0f98864af 100644 --- a/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenViewModel.swift +++ b/ElementX/Sources/Screens/DeclineAndBlockScreen/DeclineAndBlockScreenViewModel.swift @@ -21,7 +21,7 @@ class DeclineAndBlockScreenViewModel: DeclineAndBlockScreenViewModelType, Declin var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(userID: String, roomID: String, clientProxy: ClientProxyProtocol, diff --git a/ElementX/Sources/Screens/EditRoomAddressScreen/EditRoomAddressScreenCoordinator.swift b/ElementX/Sources/Screens/EditRoomAddressScreen/EditRoomAddressScreenCoordinator.swift index 097f02525..61b7dbe05 100644 --- a/ElementX/Sources/Screens/EditRoomAddressScreen/EditRoomAddressScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EditRoomAddressScreen/EditRoomAddressScreenCoordinator.swift @@ -25,7 +25,7 @@ final class EditRoomAddressScreenCoordinator: CoordinatorProtocol { private let viewModel: EditRoomAddressScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -49,7 +49,7 @@ final class EditRoomAddressScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(EditRoomAddressScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/EditRoomAddressScreen/EditRoomAddressScreenViewModel.swift b/ElementX/Sources/Screens/EditRoomAddressScreen/EditRoomAddressScreenViewModel.swift index 8fe85869e..1764b3475 100644 --- a/ElementX/Sources/Screens/EditRoomAddressScreen/EditRoomAddressScreenViewModel.swift +++ b/ElementX/Sources/Screens/EditRoomAddressScreen/EditRoomAddressScreenViewModel.swift @@ -23,7 +23,7 @@ class EditRoomAddressScreenViewModel: EditRoomAddressScreenViewModelType, EditRo } @CancellableTask private var checkAliasAvailabilityTask: Task? - + init(initialViewState: EditRoomAddressScreenViewState? = nil, roomProxy: JoinedRoomProxyProtocol, clientProxy: ClientProxyProtocol, @@ -169,7 +169,7 @@ class EditRoomAddressScreenViewModel: EditRoomAddressScreenViewModelType, EditRo if savedCanonicalAlias == nil || savedCanonicalAlias?.hasSuffix(state.serverName) == true { var newAlternativeAliases = roomProxy.infoPublisher.value.alternativeAliases newAlternativeAliases.removeAll { $0 == savedAliasFromHomeserver } - + if case .failure = await roomProxy.updateCanonicalAlias(desiredCanonicalAlias, altAliases: newAlternativeAliases) { userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown)) return diff --git a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift index 515b33846..4e35010dd 100644 --- a/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift +++ b/ElementX/Sources/Screens/EmojiPickerScreen/EmojiPickerScreenViewModel.swift @@ -47,7 +47,7 @@ class EmojiPickerScreenViewModel: EmojiPickerScreenViewModelType, EmojiPickerScr } // MARK: - Private - + private func loadEmojis() { Task(priority: .userInitiated) { [weak self] in guard let self else { return } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift index 6ede5cac4..af72ac438 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenCoordinator.swift @@ -22,7 +22,7 @@ final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol { private let viewModel: EncryptionResetPasswordScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -46,7 +46,7 @@ final class EncryptionResetPasswordScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(EncryptionResetPasswordScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift index bc779899a..eed472f0e 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetPasswordScreen/EncryptionResetPasswordScreenViewModel.swift @@ -18,7 +18,7 @@ class EncryptionResetPasswordScreenViewModel: EncryptionResetPasswordScreenViewM var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(passwordPublisher: PassthroughSubject) { self.passwordPublisher = passwordPublisher diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift index 872487172..43348c89d 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenCoordinator.swift @@ -26,7 +26,7 @@ final class EncryptionResetScreenCoordinator: CoordinatorProtocol { private let viewModel: EncryptionResetScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -60,7 +60,7 @@ final class EncryptionResetScreenCoordinator: CoordinatorProtocol { func stop() { viewModel.stop() } - + func toPresentable() -> AnyView { AnyView(EncryptionResetScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift index 4d44f458e..ec401e32d 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenModels.swift @@ -35,7 +35,7 @@ struct EncryptionResetScreenViewState: BindableState { AttributedString(L10n.screenCreateNewRecoveryKeyListItem5) ] } - + var bindings: EncryptionResetScreenViewStateBindings } diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift index 4a142f2a1..f3b9d6bfc 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/EncryptionResetScreenViewModel.swift @@ -23,7 +23,7 @@ class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, Encryp private var identityResetHandle: IdentityResetHandle? private var passwordCancellable: AnyCancellable? - + init(clientProxy: ClientProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { self.clientProxy = clientProxy self.userIndicatorController = userIndicatorController diff --git a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift index be0075262..67788b0b0 100644 --- a/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift +++ b/ElementX/Sources/Screens/EncryptionReset/EncryptionResetScreen/View/EncryptionResetScreen.swift @@ -69,7 +69,7 @@ struct EncryptionResetScreen: View { .frame(maxWidth: .infinity) .environment(\.backgroundStyle, AnyShapeStyle(.compound.bgSubtleSecondary)) } - + private func checkMarkItem(title: String, position: ListPosition, positive: Bool) -> some View { VisualListItem(title: title, position: position) { CompoundIcon(positive ? \.check : \.info) diff --git a/ElementX/Sources/Screens/FilePreviewScreen/InteractiveQuickLook.swift b/ElementX/Sources/Screens/FilePreviewScreen/InteractiveQuickLook.swift index ffa2725b6..e416c1c0e 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/InteractiveQuickLook.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/InteractiveQuickLook.swift @@ -43,14 +43,14 @@ private struct MediaPreviewViewController: UIViewControllerRepresentable { let allowEditing: Bool let dismissalPublisher: PassthroughSubject let onDismiss: () -> Void - + func makeUIViewController(context: Context) -> PreviewHostingController { PreviewHostingController(previewItem: previewItem, allowEditing: allowEditing, dismissalPublisher: dismissalPublisher, onDismiss: onDismiss) } - + func updateUIViewController(_ uiViewController: PreviewHostingController, context: Context) { } /// A view controller that hosts the QuickLook preview. @@ -66,7 +66,7 @@ private struct MediaPreviewViewController: UIViewControllerRepresentable { private var dismissalObserver: AnyCancellable? var previewController: QLPreviewController? - + init(previewItem: MediaPreviewItem, allowEditing: Bool, dismissalPublisher: PassthroughSubject, @@ -74,7 +74,7 @@ private struct MediaPreviewViewController: UIViewControllerRepresentable { self.previewItem = previewItem self.allowEditing = allowEditing self.onDismiss = onDismiss - + super.init(nibName: nil, bundle: nil) // The QLPreviewController will not automatically dismiss itself when the underlying view is removed @@ -128,7 +128,7 @@ private struct MediaPreviewViewController: UIViewControllerRepresentable { func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 } - + func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { previewItem } @@ -156,9 +156,9 @@ class MediaPreviewItem: NSObject, QLPreviewItem { var previewItemURL: URL? { file.url } - + let previewItemTitle: String? - + init(file: MediaFileHandleProxy, title: String?) { self.file = file previewItemTitle = title diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift index 083894d77..78b55b9bb 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewDataSource.swift @@ -117,7 +117,7 @@ class TimelineMediaPreviewDataSource: NSObject, QLPreviewControllerDataSource { var firstPreviewItemIndex: Int { backwardPadding } - + var lastPreviewItemIndex: Int { backwardPadding + previewItems.count - 1 } @@ -276,11 +276,11 @@ enum TimelineMediaPreviewItem: Equatable { var caption: String? { timelineItem.mediaCaption } - + var formattedCaption: AttributedString? { timelineItem.formattedMediaCaption } - + var contentType: String? { switch timelineItem { case let audioItem as AudioRoomTimelineItem: diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModels.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModels.swift index 1b3ac160e..158b92330 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModels.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModels.swift @@ -59,7 +59,7 @@ struct TimelineMediaPreviewViewState: BindableState { var currentItem: TimelineMediaPreviewItem { dataSource.currentItem } - + /// All of the available actions for the current item. var currentItemActions: TimelineItemMenuActions? diff --git a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModifier.swift b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModifier.swift index 1c732d084..ad547b0a9 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModifier.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/TimelineMediaPreviewModifier.swift @@ -41,13 +41,13 @@ private struct MediaPreviewViewController: UIViewControllerRepresentable { let viewModel: TimelineMediaPreviewViewModel let dismissalPublisher: PassthroughSubject let onDismiss: () -> Void - + func makeUIViewController(context: Context) -> PreviewHostingController { PreviewHostingController(viewModel: viewModel, dismissalPublisher: dismissalPublisher, onDismiss: onDismiss) } - + func updateUIViewController(_ uiViewController: PreviewHostingController, context: Context) { } /// A view controller that hosts the QuickLook preview. @@ -69,7 +69,7 @@ private struct MediaPreviewViewController: UIViewControllerRepresentable { onDismiss: @escaping () -> Void) { self.onDismiss = onDismiss previewController = TimelineMediaPreviewController(context: viewModel.context) - + super.init(nibName: nil, bundle: nil) // The QLPreviewController will not automatically dismiss itself when the underlying view is removed diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewController.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewController.swift index 5b4d726bf..6c90b09cf 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewController.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewController.swift @@ -27,7 +27,7 @@ class TimelineMediaPreviewController: QLPreviewController { private var navigationBar: UINavigationBar? { view.subviews.first?.subviews.first { $0 is UINavigationBar } as? UINavigationBar } - + private var bottomBarItemsContainer: UIView? { if #available(iOS 26, *) { view.subviews.first?.subviews.last?.subviews.first @@ -35,11 +35,11 @@ class TimelineMediaPreviewController: QLPreviewController { view.subviews.first?.subviews.last { $0 is UIToolbar } } } - + private var pageScrollView: UIScrollView? { view.firstScrollView() } - + private var captionView: UIView { captionHostingController.view } @@ -316,7 +316,7 @@ private struct CaptionView: View { private var currentItem: TimelineMediaPreviewItem { context.viewState.currentItem } - + var body: some View { if case let .media(mediaItem) = currentItem, mediaItem.hasCaption { CaptionScrollView(mediaItem: mediaItem) @@ -361,7 +361,7 @@ private struct CaptionScrollView: View { .ignoresSafeArea() } } - + @ViewBuilder private var captionContent: some View { if let formattedCaption = mediaItem.formattedCaption { diff --git a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift index 840b53c6f..b52a9213e 100644 --- a/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift +++ b/ElementX/Sources/Screens/FilePreviewScreen/View/TimelineMediaPreviewRedactConfirmationView.swift @@ -15,7 +15,7 @@ struct TimelineMediaPreviewRedactConfirmationView: View { let item: TimelineMediaPreviewItem.Media @ObservedObject var context: TimelineMediaPreviewViewModel.Context var preferredColorScheme: ColorScheme? = .dark - + @State private var sheetHeight: CGFloat = .zero private let topPadding: CGFloat = 19 @@ -74,12 +74,12 @@ struct TimelineMediaPreviewRedactConfirmationView: View { .clipShape(RoundedRectangle(cornerRadius: 8)) .accessibilityHidden(true) } - + VStack(alignment: .leading, spacing: 4) { Text(item.filename ?? "") .font(.compound.bodyMD) .foregroundStyle(.compound.textPrimary) - + if let contentType = item.contentType { Group { if let fileSize = item.fileSize { diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift index 30c66b9ee..c78269b6a 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/GlobalSearchScreenViewModel.swift @@ -18,7 +18,7 @@ class GlobalSearchScreenViewModel: GlobalSearchScreenViewModelType, GlobalSearch var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(roomSummaryProvider: RoomSummaryProviderProtocol, mediaProvider: MediaProviderProtocol) { self.roomSummaryProvider = roomSummaryProvider diff --git a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift index 97ae1cd58..be40fc3a7 100644 --- a/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift +++ b/ElementX/Sources/Screens/GlobalSearchScreen/View/GlobalSearchScreen.swift @@ -134,7 +134,7 @@ private struct GlobalSearchTextFieldRepresentable: UIViewRepresentable { @Binding var text: String let keyPressHandler: (UIKeyboardHIDUsage) -> Bool let endEditingHandler: () -> Void - + func makeUIView(context: Context) -> UITextField { let textField = GlobalSearchTextField(keyPressHandler: keyPressHandler) textField.delegate = context.coordinator @@ -142,15 +142,15 @@ private struct GlobalSearchTextFieldRepresentable: UIViewRepresentable { textField.placeholder = placeholder return textField } - + func updateUIView(_ uiView: UITextField, context: Context) { uiView.text = text } - + func makeCoordinator() -> Coordinator { Coordinator(text: $text, endEditingHandler: endEditingHandler) } - + class Coordinator: NSObject, UITextFieldDelegate { var text: Binding let endEditingHandler: () -> Void @@ -188,7 +188,7 @@ private class GlobalSearchTextField: UITextField { self.keyPressHandler = keyPressHandler super.init(frame: .zero) } - + override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { guard let key = presses.first?.key else { super.pressesBegan(presses, with: event) diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift index 612dc615d..24936d38b 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenModels.swift @@ -98,12 +98,12 @@ struct HomeScreenViewState: BindableState { var shouldShowNewSoundBanner = false var requiresExtraAccountSetup = false - + var rooms: [HomeScreenRoom] = [] var roomListMode: HomeScreenRoomListMode = .skeletons var hasPendingInvitations = false - + var selectedRoomID: String? var hideInviteAvatars = false @@ -111,7 +111,7 @@ struct HomeScreenViewState: BindableState { var roomListActivityVisibility: RoomListActivityVisibility = .current var reportRoomEnabled = false - + var shouldShowSpaceFilters = false var selectedSpaceFilter: SpaceServiceFilter? @@ -122,7 +122,7 @@ struct HomeScreenViewState: BindableState { return rooms } - + var bindings: HomeScreenViewStateBindings var placeholderRooms: [HomeScreenRoom] { @@ -175,7 +175,7 @@ struct HomeScreenRoom: Identifiable, Equatable { } static let placeholderLastMessage = AttributedString("Hidden last message") - + /// The list item identifier is it's room identifier. let id: String @@ -217,7 +217,7 @@ struct HomeScreenRoom: Identifiable, Equatable { let lastMessageState: LastMessageState? let avatar: RoomAvatar - + let canonicalAlias: String? let isTombstoned: Bool diff --git a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift index 285719608..beccd67f9 100644 --- a/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift +++ b/ElementX/Sources/Screens/HomeScreen/HomeScreenViewModel.swift @@ -354,7 +354,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol } } } - + private func updateRooms() { guard let roomSummaryProvider else { MXLog.error("Room summary provider unavailable") diff --git a/ElementX/Sources/Screens/HomeScreen/View/Filters/RoomListFilterView.swift b/ElementX/Sources/Screens/HomeScreen/View/Filters/RoomListFilterView.swift index 5dcf7312d..7dde3c150 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/Filters/RoomListFilterView.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/Filters/RoomListFilterView.swift @@ -11,7 +11,7 @@ import SwiftUI struct RoomListFilterView: View { let filter: RoomListFilter @Binding var isActive: Bool - + var body: some View { Toggle(isOn: $isActive) { Text(filter.localizedName) diff --git a/ElementX/Sources/Screens/HomeScreen/View/Filters/RoomListFiltersEmptyStateView.swift b/ElementX/Sources/Screens/HomeScreen/View/Filters/RoomListFiltersEmptyStateView.swift index c0bde9483..38ba08b01 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/Filters/RoomListFiltersEmptyStateView.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/Filters/RoomListFiltersEmptyStateView.swift @@ -37,7 +37,7 @@ struct RoomListFiltersEmptyStateView: View { } return L10n.screenRoomlistFilterMixedEmptyStateSubtitle } - + var body: some View { VStack(spacing: 24) { Text(emptyStateTitle) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift index 24cd17169..135b2e184 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreen.swift @@ -49,7 +49,7 @@ struct HomeScreen: View { L10n.screenRoomlistMainSpaceTitle } } - + @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .navigationBarLeading) { @@ -70,7 +70,7 @@ struct HomeScreen: View { if #available(iOS 26, *) { ToolbarSpacer(.fixed, placement: .primaryAction) } - + ToolbarItem(placement: .primaryAction) { SpaceFiltersButton(selected: context.viewState.selectedSpaceFilter != nil) { context.send(viewAction: .spaceFilters) @@ -221,7 +221,7 @@ struct HomeScreen_Previews: PreviewProvider, TestablePreview { roomSummaryProvider: RoomSummaryProviderMock(.init(state: roomSummaryProviderState)))) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) - + return HomeScreenViewModel(userSession: userSession, selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: .volatile(), diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenEmptyStateView.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenEmptyStateView.swift index d4148d087..ce7baaa49 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenEmptyStateView.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenEmptyStateView.swift @@ -79,7 +79,7 @@ struct HomeScreenEmptyStateLayout: Layout { // Place the main view in the center if there is space, otherwise add it to the stack. guard let mainView else { return } - + let mainViewSize = mainView.sizeThatFits(proposal) if (y + mainViewSize.height / 2) < bounds.height / 2 { let center = CGPoint(x: bounds.midX, y: bounds.midY) diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift index 31ed08642..e53d0aa6a 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenInviteCell.swift @@ -134,7 +134,7 @@ struct HomeScreenInviteCell: View { .fill(Color.compound.borderDisabled) .frame(height: 1 / UIScreen.main.scale) } - + private var title: String { room.name } @@ -199,7 +199,7 @@ struct HomeScreenInviteCell_Previews: PreviewProvider, TestablePreview { let clientProxy = ClientProxyMock(.init()) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) - + return HomeScreenViewModel(userSession: userSession, selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: .volatile(), diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift index b1cf99382..1dcc7933b 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenKnockedCell.swift @@ -46,7 +46,7 @@ struct HomeScreenKnockedCell: View { } // MARK: - Private - + private var mainContent: some View { VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) { @@ -86,7 +86,7 @@ struct HomeScreenKnockedCell: View { .fill(Color.compound.borderDisabled) .frame(height: 1 / UIScreen.main.scale) } - + private var title: String { room.name } @@ -131,7 +131,7 @@ struct HomeScreenKnockedCell_Previews: PreviewProvider, TestablePreview { let clientProxy = ClientProxyMock(.init()) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) - + return HomeScreenViewModel(userSession: userSession, selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: .volatile(), diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift index f6d86a97c..7e36a8967 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRecoveryKeyConfirmationBanner.swift @@ -21,21 +21,21 @@ struct HomeScreenRecoveryKeyConfirmationBanner: View { case .recoveryOutOfSync: L10n.confirmRecoveryKeyBannerTitle } } - + var message: String { switch state { case .setUpRecovery: L10n.bannerSetUpRecoveryContent case .recoveryOutOfSync: L10n.confirmRecoveryKeyBannerMessage } } - + var actionTitle: String { switch state { case .setUpRecovery: L10n.bannerSetUpRecoverySubmit case .recoveryOutOfSync: L10n.confirmRecoveryKeyBannerPrimaryButtonTitle } } - + var primaryAction: HomeScreenViewAction { switch state { case .setUpRecovery: .setupRecovery @@ -119,7 +119,7 @@ struct HomeScreenRecoveryKeyConfirmationBanner_Previews: PreviewProvider, Testab roomSummaryProvider: RoomSummaryProviderMock(.init(state: .loading)))) let userSession = UserSessionMock(.init(clientProxy: clientProxy)) - + return HomeScreenViewModel(userSession: userSession, selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: .volatile(), diff --git a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift index c0e1fdb72..82f006b97 100644 --- a/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift +++ b/ElementX/Sources/Screens/HomeScreen/View/HomeScreenRoomCell.swift @@ -140,7 +140,7 @@ struct HomeScreenRoomCell: View { CompoundIcon(\.videoCallSolid, size: .xSmall, relativeTo: .compound.bodySM) .accessibilityLabel(L10n.a11yNotificationsOngoingCall) } - + if room.badges.isMuteShown { CompoundIcon(\.notificationsOffSolid, size: .custom(15), relativeTo: .compound.bodyMD) .accessibilityLabel(L10n.a11yNotificationsMuted) @@ -159,7 +159,7 @@ struct HomeScreenRoomCell: View { .foregroundColor(room.isHighlighted ? .compound.iconAccentTertiary : .compound.iconQuaternary) } } - + private var mentionIcon: some View { CompoundIcon(\.mention, size: .custom(15), relativeTo: .compound.bodyMD) .accessibilityLabel(L10n.a11yNotificationsNewMentions) @@ -252,7 +252,7 @@ struct HomeScreenRoomCell_Previews: PreviewProvider, TestablePreview { static func makeViewModel(roomSummaryProvider: RoomSummaryProviderProtocol) -> HomeScreenViewModel { let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "John Doe", roomSummaryProvider: roomSummaryProvider)))) - + return HomeScreenViewModel(userSession: userSession, selectedRoomPublisher: CurrentValueSubject(nil).asCurrentValuePublisher(), appSettings: .volatile(), diff --git a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift index 4222de8dd..c2e9fcbc3 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/InviteUsersScreenViewModel.swift @@ -37,9 +37,9 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr self.userDiscoveryService = userDiscoveryService self.userIndicatorController = userIndicatorController self.appSettings = appSettings - + let mandatoryInvitees: [UserProfileProxy] = if case .draft(let invitees) = roomType { invitees } else { [] } - + super.init(initialViewState: InviteUsersScreenViewState(selectedUsers: mandatoryInvitees, mandatoryInvitees: mandatoryInvitees, isSkippable: isSkippable), @@ -92,12 +92,12 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr toggleUser(user) } } - + // MARK: - Private private func toggleUser(_ user: UserProfileProxy) { guard !state.isInviteeMandatory(user) else { return } - + if state.selectedUsers.contains(user) { state.selectedUsers.removeAll { $0.userID == user.userID } } else { @@ -119,10 +119,10 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr private func createDraftRoom(mandatoryUserIDs: [String]) { showLoadingIndicator(title: L10n.commonCreatingRoom) - + Task { defer { hideLoadingIndicator() } - + switch await clientProxy.createRoom(name: nil, topic: nil, accessType: .private, @@ -244,7 +244,7 @@ class InviteUsersScreenViewModel: InviteUsersScreenViewModelType, InviteUsersScr } } } - + private var searchQuery: String { context.searchQuery } diff --git a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift index 5ab860a3a..8e46a6582 100644 --- a/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift +++ b/ElementX/Sources/Screens/InviteUsersScreen/View/InviteUsersScreen.swift @@ -105,7 +105,7 @@ struct InviteUsersScreen: View { } @ScaledMetric private var selectedUserCellWidth: CGFloat = 80 - + private var selectedUsersSection: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { @@ -204,10 +204,10 @@ struct InviteUsersScreen_Previews: PreviewProvider, TestablePreview { isSkippable: Bool = true) -> InviteUsersScreenViewModel { let clientProxy = ClientProxyMock(.init()) clientProxy.recentConversationCounterpartsReturnValue = [.mockAlice, .mockBob, .mockCharlie, .mockDan, .mockVerbose] - + let userDiscoveryService = UserDiscoveryServiceMock() userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice]) - + let viewModel = InviteUsersScreenViewModel(userSession: UserSessionMock(.init(clientProxy: clientProxy)), roomType: roomType ?? .existingRoom(roomProxy: JoinedRoomProxyMock(.init(members: []))), isSkippable: isSkippable, diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift index af6015ec4..321dd0e59 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenCoordinator.swift @@ -45,7 +45,7 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol { private let viewModel: JoinRoomScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -78,7 +78,7 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol { func stop() { viewModel.stop() } - + func toPresentable() -> AnyView { AnyView(JoinRoomScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift index 2d24a81e2..1ee0ba951 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenModels.swift @@ -60,7 +60,7 @@ struct JoinRoomScreenViewState: BindableState { var mode: JoinRoomScreenMode = .loading var hideInviteAvatars = false - + var bindings = JoinRoomScreenViewStateBindings() var shouldHideAvatars: Bool { diff --git a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift index b4c337a14..34df80faf 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/JoinRoomScreenViewModel.swift @@ -27,7 +27,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(source: JoinRoomScreenSource, appSettings: AppSettings, userSession: UserSessionProtocol, @@ -377,7 +377,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo } } } - + private func showDeclineInviteConfirmationAlert() { guard let roomDetails = state.roomDetails else { userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown)) diff --git a/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift b/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift index 8c1b920c6..a6d8e8229 100644 --- a/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift +++ b/ElementX/Sources/Screens/JoinRoomScreen/View/JoinRoomScreen.swift @@ -26,7 +26,7 @@ struct JoinRoomScreen: View { } return context.viewState.mode == .knocked ? 151 : 32 } - + var body: some View { FullscreenDialog(topPadding: topPadding) { if context.viewState.mode == .loading { @@ -130,7 +130,7 @@ struct JoinRoomScreen: View { } } } - + private var knockMessage: some View { VStack(alignment: .leading, spacing: 12) { HStack(spacing: 0) { @@ -404,7 +404,7 @@ struct JoinRoomScreenPreviewWrapper: Identifiable { self.customPreviewName = customPreviewName let appSettings = AppSettings.volatile() - + let clientProxy = ClientProxyMock(.init(hideInviteAvatars: hideInviteAvatars)) clientProxy.canJoinRoomWithReturnValue = canJoinRoom diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenCoordinator.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenCoordinator.swift index cf354f3d5..bcd9e6ec7 100644 --- a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenCoordinator.swift @@ -23,7 +23,7 @@ final class KnockRequestsListScreenCoordinator: CoordinatorProtocol { private let viewModel: KnockRequestsListScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -36,7 +36,7 @@ final class KnockRequestsListScreenCoordinator: CoordinatorProtocol { } func start() { } - + func toPresentable() -> AnyView { AnyView(KnockRequestsListScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift index b0a626444..7eb30e644 100644 --- a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenModels.swift @@ -28,7 +28,7 @@ struct KnockRequestsListScreenViewState: BindableState { false } } - + // If you are in this view one of these must have been true so by default we assume all of them to be true var canAccept = true var canDecline = true diff --git a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift index b051cf652..b6fc728d1 100644 --- a/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift +++ b/ElementX/Sources/Screens/KnockRequestsListScreen/KnockRequestsListScreenViewModel.swift @@ -19,7 +19,7 @@ class KnockRequestsListScreenViewModel: KnockRequestsListScreenViewModelType, Kn var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(roomProxy: JoinedRoomProxyProtocol, mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol) { diff --git a/ElementX/Sources/Screens/LabsScreen/LabsScreenCoordinator.swift b/ElementX/Sources/Screens/LabsScreen/LabsScreenCoordinator.swift index aa96a3556..9b473d115 100644 --- a/ElementX/Sources/Screens/LabsScreen/LabsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/LabsScreen/LabsScreenCoordinator.swift @@ -44,7 +44,7 @@ final class LabsScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(LabsScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/LabsScreen/LabsScreenModels.swift b/ElementX/Sources/Screens/LabsScreen/LabsScreenModels.swift index 50ea5382f..70e30d8be 100644 --- a/ElementX/Sources/Screens/LabsScreen/LabsScreenModels.swift +++ b/ElementX/Sources/Screens/LabsScreen/LabsScreenModels.swift @@ -24,11 +24,11 @@ struct LabsScreenViewState: BindableState { @dynamicMemberLookup struct LabsScreenViewStateBindings { private let labsOptions: LabsOptionsProtocol - + init(labsOptions: LabsOptionsProtocol) { self.labsOptions = labsOptions } - + subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Setting { get { labsOptions[keyPath: keyPath] } set { labsOptions[keyPath: keyPath] = newValue } diff --git a/ElementX/Sources/Screens/LabsScreen/LabsScreenViewModel.swift b/ElementX/Sources/Screens/LabsScreen/LabsScreenViewModel.swift index 04238267b..4666c9483 100644 --- a/ElementX/Sources/Screens/LabsScreen/LabsScreenViewModel.swift +++ b/ElementX/Sources/Screens/LabsScreen/LabsScreenViewModel.swift @@ -15,7 +15,7 @@ class LabsScreenViewModel: LabsScreenViewModelType, LabsScreenViewModelProtocol var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(labsOptions: LabsOptionsProtocol) { let bindings = LabsScreenViewStateBindings(labsOptions: labsOptions) let state = LabsScreenViewState(bindings: bindings) diff --git a/ElementX/Sources/Screens/LabsScreen/View/LabsScreen.swift b/ElementX/Sources/Screens/LabsScreen/View/LabsScreen.swift index 27f0d55e8..fbb0dd5b9 100644 --- a/ElementX/Sources/Screens/LabsScreen/View/LabsScreen.swift +++ b/ElementX/Sources/Screens/LabsScreen/View/LabsScreen.swift @@ -65,7 +65,7 @@ struct LabsScreen: View { struct LabsScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = LabsScreenViewModel(labsOptions: AppSettings.volatile()) - + static var previews: some View { ElementNavigationStack { LabsScreen(context: viewModel.context) diff --git a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenCoordinator.swift b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenCoordinator.swift index 4fd8ea758..6961979d7 100644 --- a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenCoordinator.swift @@ -24,7 +24,7 @@ final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol { private let orientationManager: OrientationManagerProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -58,7 +58,7 @@ final class LinkNewDeviceScreenCoordinator: CoordinatorProtocol { func stop() { orientationManager.lockOrientation(.all) } - + func toPresentable() -> AnyView { AnyView(LinkNewDeviceScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenViewModel.swift b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenViewModel.swift index b9b14097d..e893aad1c 100644 --- a/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenViewModel.swift +++ b/ElementX/Sources/Screens/LinkNewDeviceScreen/LinkNewDeviceScreenViewModel.swift @@ -18,7 +18,7 @@ class LinkNewDeviceScreenViewModel: LinkNewDeviceScreenViewModelType, LinkNewDev var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(clientProxy: ClientProxyProtocol, initialState: LinkNewDeviceScreenViewState? = nil) { self.clientProxy = clientProxy diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift index 3a4199c30..1eeb0a4c3 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenModels.swift @@ -28,12 +28,12 @@ enum LocationSharingInteractionMode: Hashable { case picker(shouldShowLiveLocationOption: Bool) case viewStatic(StaticLocationData) case viewLive(sender: TimelineItemSender?, initialLiveLocationShare: LiveLocationShare?) - + var isPicker: Bool { if case .picker = self { return true } return false } - + var shouldShowLiveLocationOption: Bool { if case .picker(let shouldShowLiveLocationOption) = self { return shouldShowLiveLocationOption @@ -73,7 +73,7 @@ struct LocationSharingScreenViewState: BindableState { case .viewStatic, .viewLive: .show } } - + let interactionMode: LocationSharingInteractionMode let mapURLBuilder: MapTilerURLBuilderProtocol let ownUserID: String @@ -112,7 +112,7 @@ struct LocationSharingScreenViewState: BindableState { } var bindings = LocationSharingScreenBindings(showsUserLocationMode: .hide) - + /// Indicates whether the user is sharing his current location var isSharingUserLocation: Bool { bindings.isLocationAuthorized == true && bindings.showsUserLocationMode == .showAndFollow @@ -148,7 +148,7 @@ struct LocationSharingScreenViewState: BindableState { !bindings.hasLoadedUserLocation && bindings.isLocationAuthorized == true } } - + var zoomLevel: Double { 15.0 } @@ -194,7 +194,7 @@ struct LocationSharingScreenBindings { /// Information describing the currently displayed alert. var alertInfo: AlertInfo? - + var sharedAnnotation: LocationAnnotation? } diff --git a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift index 4c9257d89..86bb0300a 100644 --- a/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift +++ b/ElementX/Sources/Screens/LocationSharing/LocationSharingScreenViewModel.swift @@ -46,7 +46,7 @@ class LocationSharingScreenViewModel: LocationSharingScreenViewModelType, Locati self.analytics = analytics self.userIndicatorController = userIndicatorController self.notificationCenter = notificationCenter - + super.init(initialViewState: .init(interactionMode: interactionMode, mapURLBuilder: mapURLBuilder, ownUserID: roomProxy.ownUserID), @@ -338,7 +338,7 @@ extension LocationSharingScreenViewModel { case viewLive case viewLiveEmpty } - + static func mock(type: MockType, senderID: String = "@dan:matrix.org") -> LocationSharingScreenViewModel { let interactionMode: LocationSharingInteractionMode = switch type { @@ -388,7 +388,7 @@ extension LocationSharingScreenViewModel { let liveLocationServiceMock = RoomLiveLocationServiceMock(.init(shares: liveLocationShares)) let roomProxy = JoinedRoomProxyMock(.init(members: .allMembers, ownUserID: RoomMemberProxyMock.mockMe.userID)) roomProxy.makeLiveLocationServiceReturnValue = liveLocationServiceMock - + return LocationSharingScreenViewModel(interactionMode: interactionMode, mapURLBuilder: AppSettings.volatile().mapTilerConfiguration, roomProxy: roomProxy, diff --git a/ElementX/Sources/Screens/LocationSharing/View/LocationPickerSheet.swift b/ElementX/Sources/Screens/LocationSharing/View/LocationPickerSheet.swift index bbf5e7b8d..3f1c7a5e3 100644 --- a/ElementX/Sources/Screens/LocationSharing/View/LocationPickerSheet.swift +++ b/ElementX/Sources/Screens/LocationSharing/View/LocationPickerSheet.swift @@ -11,14 +11,14 @@ import SwiftUI struct LocationPickerSheet: View { @Bindable var context: LocationSharingScreenViewModel.Context @State private var height: CGFloat = .zero - + /// Fixes an iOS 26 sheet issue /// if the content doesn't meet a certain size /// additional insets are added. private var additionalHeight: CGFloat { context.viewState.interactionMode.shouldShowLiveLocationOption ? 0 : 28 } - + var body: some View { VStack(spacing: 0) { Text(L10n.screenSharingLocationOptionSheetTitle) diff --git a/ElementX/Sources/Screens/LocationSharing/View/LocationShareSheet.swift b/ElementX/Sources/Screens/LocationSharing/View/LocationShareSheet.swift index 3fe57d53f..5ae7b2f15 100644 --- a/ElementX/Sources/Screens/LocationSharing/View/LocationShareSheet.swift +++ b/ElementX/Sources/Screens/LocationSharing/View/LocationShareSheet.swift @@ -44,7 +44,7 @@ struct LocationShareSheet_Previews: PreviewProvider { static let annotation = LocationAnnotation(id: profile.userID, coordinate: .init(latitude: 51.509865, longitude: -0.118092), kind: .liveUser(profile)) - + static var previews: some View { LocationShareSheet(annotation: annotation) .previewDisplayName("Live location share sheet") diff --git a/ElementX/Sources/Screens/LocationSharing/View/LocationSharingScreen.swift b/ElementX/Sources/Screens/LocationSharing/View/LocationSharingScreen.swift index c9a7a7b5e..0f34293b9 100644 --- a/ElementX/Sources/Screens/LocationSharing/View/LocationSharingScreen.swift +++ b/ElementX/Sources/Screens/LocationSharing/View/LocationSharingScreen.swift @@ -126,26 +126,26 @@ struct LocationSharingScreen: View { struct LocationSharingScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = LocationSharingScreenViewModel.mock(type: .staticSenderLocation) - + static let withoutLiveSharingViewModel = LocationSharingScreenViewModel.mock(type: .pickerWithoutLiveLocationOption) - + static let pinViewModel = LocationSharingScreenViewModel.mock(type: .staticPinLocation) - + static let pickerViewModel = LocationSharingScreenViewModel.mock(type: .picker) - + static let liveLocationViewModel = LocationSharingScreenViewModel.mock(type: .viewLive) - + static var previews: some View { ElementNavigationStack { LocationSharingScreen(context: pickerViewModel.context) } .previewDisplayName("Picker") - + ElementNavigationStack { LocationSharingScreen(context: withoutLiveSharingViewModel.context) } .previewDisplayName("Picker without live location sharing") - + ElementNavigationStack { LocationSharingScreen(context: viewModel.context) } diff --git a/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenCoordinator.swift b/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenCoordinator.swift index 6608dbbe3..da115d133 100644 --- a/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenCoordinator.swift +++ b/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenCoordinator.swift @@ -38,7 +38,7 @@ final class LogViewerScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(LogViewerScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenViewModel.swift b/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenViewModel.swift index 434a5e839..2a5eb8f1b 100644 --- a/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenViewModel.swift +++ b/ElementX/Sources/Screens/LogViewerScreen/LogViewerScreenViewModel.swift @@ -17,7 +17,7 @@ class LogViewerScreenViewModel: LogViewerScreenViewModelType, LogViewerScreenVie var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init() { super.init(initialViewState: LogViewerScreenViewState(urls: Tracing.logFiles)) } diff --git a/ElementX/Sources/Screens/LogViewerScreen/View/LogViewerScreen.swift b/ElementX/Sources/Screens/LogViewerScreen/View/LogViewerScreen.swift index 38bbbbee5..476c67aaf 100644 --- a/ElementX/Sources/Screens/LogViewerScreen/View/LogViewerScreen.swift +++ b/ElementX/Sources/Screens/LogViewerScreen/View/LogViewerScreen.swift @@ -20,7 +20,7 @@ struct LogViewerScreen: View { private struct PreviewView: UIViewControllerRepresentable { let urls: [URL] - + func makeUIViewController(context: Context) -> UIViewController { let previewController = QLPreviewController() previewController.dataSource = context.coordinator @@ -28,16 +28,16 @@ private struct PreviewView: UIViewControllerRepresentable { return UINavigationController(rootViewController: previewController) } - + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { } - + func makeCoordinator() -> Coordinator { Coordinator(view: self) } class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate { let view: PreviewView - + init(view: PreviewView) { self.view = view } @@ -47,7 +47,7 @@ private struct PreviewView: UIViewControllerRepresentable { func numberOfPreviewItems(in controller: QLPreviewController) -> Int { view.urls.count } - + func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { let url = view.urls[index] @@ -65,7 +65,7 @@ private struct PreviewView: UIViewControllerRepresentable { private class PreviewItem: NSObject, QLPreviewItem { var previewItemURL: URL? var previewItemTitle: String? - + init(previewItemURL: URL?, previewItemTitle: String?) { self.previewItemURL = previewItemURL self.previewItemTitle = previewItemTitle diff --git a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenCoordinator.swift b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenCoordinator.swift index 48b2f423c..e4ac410f8 100644 --- a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenCoordinator.swift @@ -23,7 +23,7 @@ final class ManageAuthorizedSpacesScreenCoordinator: CoordinatorProtocol { private let viewModel: ManageAuthorizedSpacesScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -46,7 +46,7 @@ final class ManageAuthorizedSpacesScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(ManageAuthorizedSpacesScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenViewModel.swift b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenViewModel.swift index 2a8bc81e9..79a1e40c1 100644 --- a/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenViewModel.swift +++ b/ElementX/Sources/Screens/ManageAuthorizedSpacesScreen/ManageAuthorizedSpacesScreenViewModel.swift @@ -15,7 +15,7 @@ class ManageAuthorizedSpacesScreenViewModel: ManageAuthorizedSpacesScreenViewMod var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(authorizedSpacesSelection: AuthorizedSpacesSelection, mediaProvider: MediaProviderProtocol) { super.init(initialViewState: ManageAuthorizedSpacesScreenViewState(authorizedSpacesSelection: authorizedSpacesSelection), diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift index 1f6387d37..5d4b169ca 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/MediaEventsTimelineScreenViewModel.swift @@ -54,7 +54,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType } super.init(initialViewState: .init(activeTimelineContext: activeTimelineContext, bindings: .init(screenMode: initialScreenMode)), mediaProvider: mediaProvider) - + mediaTimelineViewModel.context.$viewState.sink { [weak self] timelineViewState in guard let self, state.bindings.screenMode == .media else { return @@ -201,7 +201,7 @@ class MediaEventsTimelineScreenViewModel: MediaEventsTimelineScreenViewModelType items: currentItems) newGroups.append(group) } - + state.groups = newGroups state.isBackPaginating = timelineViewState.timelineState.paginationState.backward == .paginating diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift index 12b35e528..4bdbf3e62 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/MediaEventsTimelineScreen.swift @@ -309,7 +309,7 @@ struct MediaEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { } else { MockTimelineController.mediaGallery } - + let appSettings = AppSettings.volatile() return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Preview room")), timelineController: timelineController, diff --git a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/ImageMediaEventsTimelineView.swift b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/ImageMediaEventsTimelineView.swift index ee6cabd16..a8ce4b6e1 100644 --- a/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/ImageMediaEventsTimelineView.swift +++ b/ElementX/Sources/Screens/MediaEventsTimelineScreen/View/TimelineViews/ImageMediaEventsTimelineView.swift @@ -46,7 +46,7 @@ struct ImageMediaEventsTimelineView: View { .mediaGalleryTimelineAspectRatio(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo) } } - + private var placeholder: some View { Rectangle() .foregroundColor(.compound.bgSubtleSecondary) diff --git a/ElementX/Sources/Screens/MediaPickerScreen/CameraPicker.swift b/ElementX/Sources/Screens/MediaPickerScreen/CameraPicker.swift index 392641566..d14e6d208 100644 --- a/ElementX/Sources/Screens/MediaPickerScreen/CameraPicker.swift +++ b/ElementX/Sources/Screens/MediaPickerScreen/CameraPicker.swift @@ -29,7 +29,7 @@ struct CameraPicker: UIViewControllerRepresentable { self.userIndicatorController = userIndicatorController self.callback = callback } - + func makeUIViewController(context: Context) -> UIImagePickerController { let imagePicker = UIImagePickerController() imagePicker.sourceType = .camera diff --git a/ElementX/Sources/Screens/MediaPickerScreen/DocumentPicker.swift b/ElementX/Sources/Screens/MediaPickerScreen/DocumentPicker.swift index 6af8caf93..28287d8f1 100644 --- a/ElementX/Sources/Screens/MediaPickerScreen/DocumentPicker.swift +++ b/ElementX/Sources/Screens/MediaPickerScreen/DocumentPicker.swift @@ -30,7 +30,7 @@ struct DocumentPicker: UIViewControllerRepresentable { self.userIndicatorController = userIndicatorController self.callback = callback } - + func makeUIViewController(context: Context) -> UIDocumentPickerViewController { let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: contentTypes) documentPicker.delegate = context.coordinator diff --git a/ElementX/Sources/Screens/MediaPickerScreen/PhotoLibraryPicker.swift b/ElementX/Sources/Screens/MediaPickerScreen/PhotoLibraryPicker.swift index 9698072c8..9f3931bb3 100644 --- a/ElementX/Sources/Screens/MediaPickerScreen/PhotoLibraryPicker.swift +++ b/ElementX/Sources/Screens/MediaPickerScreen/PhotoLibraryPicker.swift @@ -32,7 +32,7 @@ struct PhotoLibraryPicker: UIViewControllerRepresentable { self.userIndicatorController = userIndicatorController self.callback = callback } - + func makeUIViewController(context: Context) -> PHPickerViewController { var configuration = PHPickerConfiguration(photoLibrary: .shared()) configuration.selection = .ordered diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenModels.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenModels.swift index 92e5ba9c3..d46c408ca 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenModels.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenModels.swift @@ -21,7 +21,7 @@ struct MediaUploadPreviewScreenViewState: BindableState { var shouldDisableInteraction = false var mediaEditVersion = 0 - + var bindings: MediaUploadPreviewScreenBindings } diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift index e0ae5f39a..ee3925712 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/MediaUploadPreviewScreenViewModel.swift @@ -28,7 +28,7 @@ class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(mediaURLs: [URL], caption: NSAttributedString?, title: String?, diff --git a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift index 1d03b13c8..d29846473 100644 --- a/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift +++ b/ElementX/Sources/Screens/MediaUploadPreviewScreen/View/MediaUploadPreviewScreen.swift @@ -26,7 +26,7 @@ struct MediaUploadPreviewScreen: View { private var title: String { ProcessInfo.processInfo.isiOSAppOnMac ? context.viewState.title ?? "" : "" } - + private var colorSchemeOverride: ColorScheme { ProcessInfo.processInfo.isiOSAppOnMac ? colorScheme : .dark } @@ -189,7 +189,7 @@ struct MediaUploadPreviewScreen: View { return type.conforms(to: .image) } - + private func handleKeyPress(_ key: UIKeyboardHIDUsage) { switch key { case .keyboardReturnOrEnter: @@ -217,7 +217,7 @@ private struct PreviewView: UIViewControllerRepresentable { let title: String? let mediaEditVersion: Int @Binding var currentIndex: Int - + func makeUIViewController(context: Context) -> UIViewController { let previewController = PreviewViewController(currentIndex: $currentIndex) previewController.dataSource = context.coordinator @@ -229,7 +229,7 @@ private struct PreviewView: UIViewControllerRepresentable { return UINavigationController(rootViewController: previewController) } } - + func updateUIViewController(_ uiViewController: UIViewController, context: Context) { guard context.coordinator.mediaEditVersion != mediaEditVersion else { return @@ -241,7 +241,7 @@ private struct PreviewView: UIViewControllerRepresentable { ?? uiViewController as? QLPreviewController previewController?.reloadData() } - + func makeCoordinator() -> Coordinator { Coordinator(view: self) } @@ -249,7 +249,7 @@ private struct PreviewView: UIViewControllerRepresentable { class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate { let view: PreviewView var mediaEditVersion: Int - + init(view: PreviewView) { self.view = view mediaEditVersion = view.mediaEditVersion @@ -260,7 +260,7 @@ private struct PreviewView: UIViewControllerRepresentable { func numberOfPreviewItems(in controller: QLPreviewController) -> Int { view.mediaURLs.count } - + func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { PreviewItem(previewItemURL: view.mediaURLs[index], previewItemTitle: view.title) } @@ -276,7 +276,7 @@ private struct PreviewView: UIViewControllerRepresentable { private class PreviewItem: NSObject, QLPreviewItem { var previewItemURL: URL? var previewItemTitle: String? - + init(previewItemURL: URL?, previewItemTitle: String?) { self.previewItemURL = previewItemURL self.previewItemTitle = previewItemTitle @@ -300,7 +300,7 @@ private class PreviewViewController: QLPreviewController { } .store(in: &cancellables) } - + @available(*, unavailable) required init?(coder: NSCoder) { fatalError() @@ -311,7 +311,7 @@ private class PreviewViewController: QLPreviewController { // Remove top file details bar navigationController?.navigationBar.isHidden = true - + // Hide toolbar share button toolbarItems?.first?.isHidden = true } @@ -323,7 +323,7 @@ private struct ImageEditorView: UIViewControllerRepresentable { let imageURL: URL var onCrop: (UIImage) -> Void var onCancel: () -> Void - + func makeUIViewController(context: Context) -> CropViewController { let image = UIImage(contentsOfFile: imageURL.path) ?? UIImage() @@ -331,29 +331,29 @@ private struct ImageEditorView: UIViewControllerRepresentable { cropViewController.delegate = context.coordinator return cropViewController } - + func updateUIViewController(_ uiViewController: CropViewController, context: Context) { } - + func makeCoordinator() -> Coordinator { Coordinator(onCrop: onCrop, onCancel: onCancel) } - + class Coordinator: NSObject, CropViewControllerDelegate { var onCrop: (UIImage) -> Void var onCancel: () -> Void - + init(onCrop: @escaping (UIImage) -> Void, onCancel: @escaping () -> Void) { self.onCrop = onCrop self.onCancel = onCancel } - + func cropViewControllerDidCrop(_ cropViewController: CropViewController, cropped: UIImage, transformation: Transformation, cropInfo: CropInfo) { onCrop(cropped) } - + func cropViewControllerDidCancel(_ cropViewController: CropViewController, original: UIImage) { onCancel() } diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift index 31d200305..6c4ee7982 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenCoordinator.swift @@ -48,7 +48,7 @@ final class MessageForwardingScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(MessageForwardingScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift index d4ef32ee4..455f38d84 100644 --- a/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift +++ b/ElementX/Sources/Screens/MessageForwardingScreen/MessageForwardingScreenViewModel.swift @@ -22,7 +22,7 @@ class MessageForwardingScreenViewModel: MessageForwardingScreenViewModelType, Me var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(forwardingItem: MessageForwardingItem, userSession: UserSessionProtocol, roomSummaryProvider: RoomSummaryProviderProtocol, diff --git a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift index 8bb308ff3..2f283c74e 100644 --- a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift +++ b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/AnalyticsPromptScreenViewModel.swift @@ -23,7 +23,7 @@ class AnalyticsPromptScreenViewModel: AnalyticsPromptScreenViewModelType, Analyt let promptStrings = AnalyticsPromptScreenStrings(termsURL: termsURL) super.init(initialViewState: AnalyticsPromptScreenViewState(strings: promptStrings)) } - + // MARK: - Public override func process(viewAction: AnalyticsPromptScreenViewAction) { diff --git a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift index 6e45b3d84..cb1e18131 100644 --- a/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/AnalyticsPromptScreen/View/AnalyticsPromptScreen.swift @@ -63,7 +63,7 @@ struct AnalyticsPromptScreen: View { .frame(maxWidth: .infinity) .environment(\.backgroundStyle, AnyShapeStyle(.compound.bgSubtleSecondary)) } - + private func checkMarkItem(title: String, position: ListPosition) -> some View { VisualListItem(title: title, position: position) { CompoundIcon(\.checkCircle, size: .small, relativeTo: .body) diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/IdentityConfirmationScreenCoordinator.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/IdentityConfirmationScreenCoordinator.swift index 011df2b8d..8402fe73a 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/IdentityConfirmationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/IdentityConfirmationScreenCoordinator.swift @@ -29,7 +29,7 @@ final class IdentityConfirmationScreenCoordinator: CoordinatorProtocol { private let viewModel: IdentityConfirmationScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -63,7 +63,7 @@ final class IdentityConfirmationScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(IdentityConfirmationScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift index d4636d0c2..62bc65b93 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmationScreen/View/IdentityConfirmationScreen.swift @@ -46,7 +46,7 @@ struct IdentityConfirmationScreen: View { .multilineTextAlignment(.center) .foregroundColor(.compound.textPrimary) .padding(.bottom, 8) - + Text(L10n.screenIdentityConfirmationSubtitle) .font(.compound.bodyMD) .multilineTextAlignment(.center) diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/IdentityConfirmedScreenCoordinator.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/IdentityConfirmedScreenCoordinator.swift index f0e2e0907..ca20087fd 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/IdentityConfirmedScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/IdentityConfirmedScreenCoordinator.swift @@ -20,7 +20,7 @@ final class IdentityConfirmedScreenCoordinator: CoordinatorProtocol { private let viewModel: IdentityConfirmedScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -44,7 +44,7 @@ final class IdentityConfirmedScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(IdentityConfirmedScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/IdentityConfirmedScreenViewModel.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/IdentityConfirmedScreenViewModel.swift index aca3300a1..aa4a71456 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/IdentityConfirmedScreenViewModel.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/IdentityConfirmedScreenViewModel.swift @@ -16,7 +16,7 @@ class IdentityConfirmedScreenViewModel: IdentityConfirmedScreenViewModelType, Id var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init() { super.init(initialViewState: .init()) } diff --git a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift index a039d50e3..a64d4d35f 100644 --- a/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/IdentityConfirmedScreen/View/IdentityConfirmedScreen.swift @@ -37,7 +37,7 @@ struct IdentityConfirmedScreen: View { .multilineTextAlignment(.center) .foregroundColor(.compound.textPrimary) .padding(.bottom, 8) - + Text(L10n.screenIdentityConfirmedSubtitle) .font(.compound.bodyMD) .multilineTextAlignment(.center) diff --git a/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/NotificationPermissionsScreenViewModel.swift b/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/NotificationPermissionsScreenViewModel.swift index d4862f60d..35f1d44f7 100644 --- a/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/NotificationPermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/NotificationPermissionsScreenViewModel.swift @@ -24,7 +24,7 @@ class NotificationPermissionsScreenViewModel: NotificationPermissionsScreenViewM super.init(initialViewState: .init()) } - + // MARK: - Public override func process(viewAction: NotificationPermissionsScreenViewAction) { diff --git a/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/View/NotificationPermissionsScreen.swift b/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/View/NotificationPermissionsScreen.swift index 3da63e25c..074e00b40 100644 --- a/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/View/NotificationPermissionsScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/NotificationPermissionsScreen/View/NotificationPermissionsScreen.swift @@ -49,7 +49,7 @@ struct NotificationPermissionsScreen: View { .accessibilityHidden(true) } } - + private var buttons: some View { VStack(spacing: 16) { Button(L10n.actionOk) { context.send(viewAction: .enable) } diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift index 3d91783b5..c0e15f945 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenModels.swift @@ -88,7 +88,7 @@ struct SessionVerificationScreenViewState: BindableState { return L10n.commonVerificationFailed } } - + var message: String { switch verificationState { case .initial, .acceptingVerificationRequest, .requestingVerification, @@ -127,7 +127,7 @@ struct SessionVerificationScreenViewState: BindableState { return L10n.screenSessionVerificationFailedSubtitle } } - + var isWaiting: Bool { switch verificationState { case .acceptingVerificationRequest, .requestingVerification, .verificationRequestAccepted, diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenStateMachine.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenStateMachine.swift index b4d7ea55d..39adb7239 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenStateMachine.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenStateMachine.swift @@ -71,7 +71,7 @@ class SessionVerificationScreenStateMachine { var state: State { stateMachine.state } - + init(state: State) { stateMachine = StateMachine(state: state) configure() diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenViewModel.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenViewModel.swift index 6740eea8d..c438b6f84 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenViewModel.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/SessionVerificationScreenViewModel.swift @@ -16,13 +16,13 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess private let flow: SessionVerificationScreenFlow private var stateMachine: SessionVerificationScreenStateMachine - + private var actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(sessionVerificationControllerProxy: SessionVerificationControllerProxyProtocol, flow: SessionVerificationScreenFlow, appSettings: AppSettings, @@ -115,7 +115,7 @@ class SessionVerificationScreenViewModel: SessionVerificationViewModelType, Sess private func setupStateMachine() { stateMachine.addTransitionHandler { [weak self] context in guard let self else { return } - + state.verificationState = context.toState switch (context.fromState, context.event, context.toState) { diff --git a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift index ac6db74ef..40457dcc5 100644 --- a/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift +++ b/ElementX/Sources/Screens/Onboarding/SessionVerificationScreen/View/SessionVerificationScreen.swift @@ -70,7 +70,7 @@ struct SessionVerificationScreen: View { accessibilityFocus = .title } .accessibilityFocused($accessibilityFocus, equals: .title) - + Text(context.viewState.message) .font(.compound.bodyMD) .multilineTextAlignment(.center) @@ -153,7 +153,7 @@ struct SessionVerificationScreen: View { } .buttonStyle(.compound(.primary)) } - + case .showingChallenge: VStack(spacing: 16) { Button(L10n.screenSessionVerificationTheyMatch) { diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift index 8095b887f..2651f245c 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/PinnedEventsTimelineScreenCoordinator.swift @@ -37,7 +37,7 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { private let timelineViewModel: TimelineViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -108,7 +108,7 @@ final class PinnedEventsTimelineScreenCoordinator: CoordinatorProtocol { func stop() { viewModel.stop() } - + func toPresentable() -> AnyView { AnyView(PinnedEventsTimelineScreen(context: viewModel.context, timelineContext: timelineViewModel.context)) } diff --git a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift index ed80c9b72..40b244cfd 100644 --- a/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift +++ b/ElementX/Sources/Screens/PinnedEventsTimelineScreen/View/PinnedEventsTimelineScreen.swift @@ -89,7 +89,7 @@ struct PinnedEventsTimelineScreen_Previews: PreviewProvider, TestablePreview { linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) }() - + static var previews: some View { ElementNavigationStack { PinnedEventsTimelineScreen(context: viewModel.context, timelineContext: emptyTimelineViewModel.context) diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenCoordinator.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenCoordinator.swift index 46108f5a7..05f3933fd 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenCoordinator.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenCoordinator.swift @@ -46,7 +46,7 @@ final class QRCodeLoginScreenCoordinator: CoordinatorProtocol { private let orientationManager: OrientationManagerProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -88,7 +88,7 @@ final class QRCodeLoginScreenCoordinator: CoordinatorProtocol { func stop() { orientationManager.lockOrientation(.all) } - + func toPresentable() -> AnyView { AnyView(QRCodeLoginScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenModels.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenModels.swift index a2f1d7164..90d512b39 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenModels.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenModels.swift @@ -68,7 +68,7 @@ struct QRCodeLoginScreenViewState: BindableState { } } } - + var shouldDisplayBackButton: Bool { switch mode { case .login: diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift index c6136019a..ece163c83 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/QRCodeLoginScreenViewModel.swift @@ -194,7 +194,7 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr private func listenToDisplayQRProgress(progressPublisher: LinkNewDeviceService.LinkMobileProgressPublisher) { state.bindings.qrResult = nil - + MXLog.info("Link showing QR code.") currentTask = progressPublisher @@ -249,12 +249,12 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr default: false } } - + guard case .qrReady(let image) = try await (qrReadyProgress, minimumDelay).0 else { state.state = .error(.unknown) return } - + state.state = .displayQR(.active(image)) } catch let error as QRCodeLoginError { handleError(error) @@ -337,7 +337,7 @@ class QRCodeLoginScreenViewModel: QRCodeLoginScreenViewModelType, QRCodeLoginScr state.state = .error(.unknown) } } - + /// Only for mocking initial states fileprivate init(state: QRCodeLoginState, mode: QRCodeLoginScreenMode, canSignInManually: Bool, checkCodeInput: String) { appMediator = AppMediatorMock(.init()) diff --git a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeScannerView.swift b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeScannerView.swift index 2ed95615f..364863293 100644 --- a/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeScannerView.swift +++ b/ElementX/Sources/Screens/QRCodeLoginScreen/View/QRCodeScannerView.swift @@ -19,7 +19,7 @@ struct QRCodeScannerView: UIViewControllerRepresentable { controller.delegate = context.coordinator return controller } - + func updateUIViewController(_ uiViewController: QRScannerController, context: Context) { if isScanning { uiViewController.startScan() @@ -34,11 +34,11 @@ struct QRCodeScannerView: UIViewControllerRepresentable { final class Coordinator: NSObject, AVCaptureMetadataOutputObjectsDelegate { @Binding var scanResult: Data? - + init(_ scanResult: Binding) { _scanResult = scanResult } - + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { // Check if the metadataObjects array is not nil and it contains at least one object. guard let metadataObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject else { @@ -60,41 +60,41 @@ struct QRCodeScannerView: UIViewControllerRepresentable { final class QRScannerController: UIViewController { private var captureSession = AVCaptureSession() private var videoPreviewLayer: AVCaptureVideoPreviewLayer? - + weak var delegate: AVCaptureMetadataOutputObjectsDelegate? override func viewDidLoad() { super.viewDidLoad() - + // Get the back-facing camera for capturing videos guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { MXLog.error("Failed to get the camera device") return } - + let videoInput: AVCaptureDeviceInput - + do { // Get an instance of the AVCaptureDeviceInput class using the previous device object. videoInput = try AVCaptureDeviceInput(device: captureDevice) - + } catch { // If any error occurs, simply print it out and don't continue any more. MXLog.error("ACaptureDeviceInput error: \(error)") return } - + // Set the input device on the capture session. captureSession.addInput(videoInput) - + // Initialize a AVCaptureMetadataOutput object and set it as the output device to the capture session. let captureMetadataOutput = AVCaptureMetadataOutput() captureSession.addOutput(captureMetadataOutput) - + // Set delegate and use the default dispatch queue to execute the call back captureMetadataOutput.setMetadataObjectsDelegate(delegate, queue: DispatchQueue.main) captureMetadataOutput.metadataObjectTypes = [.qr] - + // Initialize the video preview layer and add it as a sublayer to the viewPreview view's layer. let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession) videoPreviewLayer = previewLayer diff --git a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift index fca2c7146..086f51f2c 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenCoordinator.swift @@ -40,7 +40,7 @@ final class ReportContentScreenCoordinator: CoordinatorProtocol { roomProxy: parameters.roomProxy, clientProxy: parameters.clientProxy) } - + // MARK: - Public func start() { @@ -63,26 +63,26 @@ final class ReportContentScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func stop() { stopLoading() } - + func toPresentable() -> AnyView { AnyView(ReportContentScreen(context: viewModel.context)) } - + // MARK: - Private - + private static let loadingIndicatorIdentifier = "\(ReportContentScreenCoordinator.self)-Loading" - + private func startLoading() { parameters.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, type: .modal, title: L10n.commonSending, persistent: true)) } - + private func stopLoading() { parameters.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier) } diff --git a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift index ac06832b1..7ee50fab0 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/ReportContentScreenViewModel.swift @@ -21,7 +21,7 @@ class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportCont var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(eventID: String, senderID: String, roomProxy: JoinedRoomProxyProtocol, clientProxy: ClientProxyProtocol) { self.eventID = eventID self.senderID = senderID @@ -30,7 +30,7 @@ class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportCont super.init(initialViewState: ReportContentScreenViewState(bindings: ReportContentScreenViewStateBindings(reasonText: "", ignoreUser: false))) } - + // MARK: - Public override func process(viewAction: ReportContentScreenViewAction) { @@ -41,9 +41,9 @@ class ReportContentScreenViewModel: ReportContentScreenViewModelType, ReportCont Task { await submitReport() } } } - + // MARK: Private - + private func submitReport() async { actionsSubject.send(.submitStarted) diff --git a/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift b/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift index 625c05613..2bbb394f5 100644 --- a/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift +++ b/ElementX/Sources/Screens/ReportContentScreen/View/ReportContentScreen.swift @@ -11,7 +11,7 @@ import SwiftUI struct ReportContentScreen: View { @Bindable var context: ReportContentScreenViewModel.Context - + var body: some View { Form { reasonSection @@ -25,7 +25,7 @@ struct ReportContentScreen: View { .toolbar { toolbar } .interactiveDismissDisabled() } - + private var reasonSection: some View { Section { ListRow(label: .plain(title: L10n.screenReportContentHint), @@ -47,7 +47,7 @@ struct ReportContentScreen: View { .compoundListSectionFooter() } } - + @ToolbarContentBuilder private var toolbar: some ToolbarContent { ToolbarItem(placement: .cancellationAction) { @@ -55,7 +55,7 @@ struct ReportContentScreen: View { context.send(viewAction: .cancel) } } - + ToolbarItem(placement: .confirmationAction) { Button(L10n.actionSend) { context.send(viewAction: .submit) diff --git a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenCoordinator.swift b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenCoordinator.swift index 9b49fb207..29c9479f7 100644 --- a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenCoordinator.swift @@ -25,7 +25,7 @@ final class ReportRoomScreenCoordinator: CoordinatorProtocol { private let viewModel: ReportRoomScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -48,7 +48,7 @@ final class ReportRoomScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(ReportRoomScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift index 948d7c511..78b94080b 100644 --- a/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/ReportRoomScreen/ReportRoomScreenViewModel.swift @@ -19,7 +19,7 @@ class ReportRoomScreenViewModel: ReportRoomScreenViewModelType, ReportRoomScreen var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(roomProxy: JoinedRoomProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController @@ -36,7 +36,7 @@ class ReportRoomScreenViewModel: ReportRoomScreenViewModelType, ReportRoomScreen actionsSubject.send(.dismiss(shouldLeaveRoom: false)) } } - + private func report() async { showLoadingIndicator() let result = await roomProxy.reportRoom(reason: state.bindings.reason) diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift index 30470df7e..b5a765333 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenCoordinator.swift @@ -25,7 +25,7 @@ final class ResolveVerifiedUserSendFailureScreenCoordinator: CoordinatorProtocol private let viewModel: ResolveVerifiedUserSendFailureScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -52,7 +52,7 @@ final class ResolveVerifiedUserSendFailureScreenCoordinator: CoordinatorProtocol } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(ResolveVerifiedUserSendFailureScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift index 35a55cae5..058a46bbd 100644 --- a/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift +++ b/ElementX/Sources/Screens/ResolveVerifiedUserSendFailureScreen/ResolveVerifiedUserSendFailureScreenViewModel.swift @@ -24,7 +24,7 @@ class ResolveVerifiedUserSendFailureScreenViewModel: ResolveVerifiedUserSendFail var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(failure: TimelineItemSendFailure.VerifiedUser, sendHandle: SendHandleProxy, roomProxy: JoinedRoomProxyProtocol, diff --git a/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenCoordinator.swift index 259022bb7..b7c7002a7 100644 --- a/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomChangePermissionsScreen/RoomChangePermissionsScreenCoordinator.swift @@ -50,7 +50,7 @@ final class RoomChangePermissionsScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(RoomChangePermissionsScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift index 4af27b12b..aaf15b511 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenCoordinator.swift @@ -26,7 +26,7 @@ final class RoomChangeRolesScreenCoordinator: CoordinatorProtocol { private let viewModel: RoomChangeRolesScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -54,7 +54,7 @@ final class RoomChangeRolesScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(RoomChangeRolesScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift index bb2029b4c..dbb8283ba 100644 --- a/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomChangeRolesScreen/RoomChangeRolesScreenViewModel.swift @@ -20,14 +20,14 @@ class RoomChangeRolesScreenViewModel: RoomChangeRolesScreenViewModelType, RoomCh var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(mode: RoomRole, roomProxy: JoinedRoomProxyProtocol, mediaProvider: MediaProviderProtocol, userIndicatorController: UserIndicatorControllerProtocol, analytics: AnalyticsServiceProtocol) { guard mode != .user || mode != .creator else { fatalError("Invalid screen configuration: \(mode)") } - + self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController self.analytics = analytics diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift index f67987410..a92a2dfc8 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/RoomDetailsEditScreenModels.swift @@ -27,7 +27,7 @@ struct RoomDetailsEditScreenViewState: BindableState { var canEditTopic = false var avatarURL: URL? var localMedia: MediaInfo? - + var bindings: RoomDetailsEditScreenViewStateBindings var nameDidChange: Bool { @@ -51,7 +51,7 @@ struct RoomDetailsEditScreenViewState: BindableState { var avatarDidChange: Bool { localMedia != nil || avatarURL != initialAvatarURL } - + var canSave: Bool { !bindings.name.isEmpty && (avatarDidChange || nameDidChange || topicDidChange) } diff --git a/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift b/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift index 364c3591b..16113df22 100644 --- a/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsEditScreen/View/RoomDetailsEditScreen.swift @@ -52,7 +52,7 @@ struct RoomDetailsEditScreen: View { .disabled(!context.viewState.canSave) } } - + private var avatar: some View { Button { context.send(viewAction: .presentMediaSource) @@ -74,7 +74,7 @@ struct RoomDetailsEditScreen: View { .frame(maxWidth: .infinity, alignment: .center) .listRowBackground(Color.clear) } - + private var nameSection: some View { Section { if context.viewState.canEditName { diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift index 20ead18e0..a65e4384e 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenCoordinator.swift @@ -44,11 +44,11 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol { private let actionsSubject: PassthroughSubject = .init() private var cancellables = Set() - + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(parameters: RoomDetailsScreenCoordinatorParameters) { isSpace = parameters.roomProxy.infoPublisher.value.isSpace viewModel = RoomDetailsScreenViewModel(roomProxy: parameters.roomProxy, diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift index 520b66e39..76f512619 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenModels.swift @@ -45,7 +45,7 @@ struct RoomDetailsScreenViewState: BindableState { var isEncrypted: Bool var isDirect: Bool var permalink: URL? - + var topic: AttributedString? var topicSummary: AttributedString? @@ -67,9 +67,9 @@ struct RoomDetailsScreenViewState: BindableState { var isKnockableRoom = false var knockRequestsCount = 0 - + var reportRoomEnabled = false - + var canSeeKnockingRequests: Bool { dmRecipientInfo == nil && isKnockableRoom && (canInviteUsers || canKickUsers || canBanUsers) } @@ -85,9 +85,9 @@ struct RoomDetailsScreenViewState: BindableState { var hasTopicSection: Bool { topic != nil || canEditRoomTopic } - + var bindings: RoomDetailsScreenViewStateBindings - + var dmRecipientInfo: DMRecipientInfo? var accountOwner: RoomMemberDetails? @@ -135,31 +135,31 @@ struct RoomDetailsScreenViewStateBindings { case ignore case unignore } - + let action: Action let cancelTitle = L10n.actionCancel - + var title: String { switch action { case .ignore: return L10n.screenDmDetailsBlockUser case .unignore: return L10n.screenDmDetailsUnblockUser } } - + var confirmationTitle: String { switch action { case .ignore: return L10n.screenDmDetailsBlockAlertAction case .unignore: return L10n.screenDmDetailsUnblockAlertAction } } - + var description: String { switch action { case .ignore: return L10n.screenDmDetailsBlockAlertDescription case .unignore: return L10n.screenDmDetailsUnblockAlertDescription } } - + var viewAction: RoomDetailsScreenViewAction { switch action { case .ignore: return .ignoreConfirmed @@ -169,7 +169,7 @@ struct RoomDetailsScreenViewStateBindings { } var isFavourite = false - + /// Information describing the currently displayed alert. var alertInfo: AlertInfo? var leaveRoomAlertItem: LeaveRoomAlertItem? @@ -188,7 +188,7 @@ struct LeaveRoomAlertItem: AlertProtocol { case `public` case `private` } - + let roomID: String let isDM: Bool let state: RoomState @@ -198,7 +198,7 @@ struct LeaveRoomAlertItem: AlertProtocol { var title: String { isDM ? L10n.actionLeaveConversation : L10n.actionLeaveRoom } - + var subtitle: String { switch state { case .empty: return L10n.leaveRoomAlertEmptySubtitle diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift index be18cf34b..1030b9733 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/RoomDetailsScreenViewModel.swift @@ -19,7 +19,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr private let notificationSettingsProxy: NotificationSettingsProxyProtocol private let attributedStringBuilder: AttributedStringBuilderProtocol private let appSettings: AppSettings - + private var pinnedEventsTimelineItemProvider: TimelineItemProviderProtocol? { didSet { guard let pinnedEventsTimelineItemProvider else { @@ -90,7 +90,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr } updateRoomInfo(roomProxy.infoPublisher.value) - + setupRoomSubscription() Task { await fetchMembersIfNeeded() } @@ -421,9 +421,9 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr analyticsService.trackInteraction(name: .MobileRoomFavouriteToggle) } } - + private static let leaveRoomLoadingID = "LeaveRoomLoading" - + private func leaveRoom() async { userIndicatorController.submitIndicator(UserIndicator(id: Self.leaveRoomLoadingID, type: .modal, title: L10n.commonLeavingRoom, persistent: true)) let result = await roomProxy.leaveRoom() @@ -435,7 +435,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr actionsSubject.send(.leftRoom) } } - + private func ignore() async { guard let dmUserID = state.dmRecipientInfo?.member.id else { MXLog.error("Attempting to ignore a nil DM Recipient") @@ -456,7 +456,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr state.bindings.alertInfo = .init(id: .unknown) } } - + private func unignore() async { guard let dmUserID = state.dmRecipientInfo?.member.id else { MXLog.error("Attempting to unignore a nil DM Recipient") diff --git a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift index 08c27d7ac..58ad9f113 100644 --- a/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomDetailsScreen/View/RoomDetailsScreen.swift @@ -17,7 +17,7 @@ struct RoomDetailsScreen: View { var body: some View { Form { roomHeaderSection - + topicSection aboutSection @@ -32,9 +32,9 @@ struct RoomDetailsScreen: View { if context.viewState.dmRecipientInfo == nil { peopleSection } - + securitySection - + if let recipient = context.viewState.dmRecipientInfo?.member { ignoreUserSection(user: recipient) } @@ -147,7 +147,7 @@ struct RoomDetailsScreen: View { } } } - + private var aboutSection: some View { Section { ListRow(label: .default(title: L10n.screenMediaBrowserTitle, icon: \.image), @@ -245,7 +245,7 @@ struct RoomDetailsScreen: View { }) .accessibilityIdentifier(A11yIdentifiers.roomDetailsScreen.people) } - + if context.viewState.canSeeKnockingRequests { ListRow(label: .default(title: L10n.screenRoomDetailsRequestsToJoinTitle, icon: \.askToJoin), details: context.viewState.knockRequestsCount > 0 ? .counter(context.viewState.knockRequestsCount) : nil, @@ -320,7 +320,7 @@ struct RoomDetailsScreen: View { .disabled(context.viewState.isProcessingIgnoreRequest) } } - + @ViewBuilder private func leaveRoomAlertActions(_ item: LeaveRoomAlertItem) -> some View { Button(item.cancelTitle, role: .cancel) { } @@ -328,11 +328,11 @@ struct RoomDetailsScreen: View { context.send(viewAction: .confirmLeave) } } - + private func leaveRoomAlertMessage(_ item: LeaveRoomAlertItem) -> some View { Text(item.subtitle) } - + @ViewBuilder private func blockUserAlertActions(_ item: RoomDetailsScreenViewStateBindings.IgnoreUserAlertItem) -> some View { Button(item.cancelTitle, role: .cancel) { } @@ -341,7 +341,7 @@ struct RoomDetailsScreen: View { context.send(viewAction: item.viewAction) } } - + private func blockUserAlertMessage(_ item: RoomDetailsScreenViewStateBindings.IgnoreUserAlertItem) -> some View { Text(item.description) } @@ -417,7 +417,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { notificationSettingsProxyMockConfiguration.roomMode.isDefault = false let notificationSettingsProxy = NotificationSettingsProxyMock(with: notificationSettingsProxyMockConfiguration) - + return .init(roomProxy: roomProxy, userSession: UserSessionMock(.init()), analyticsService: AnalyticsServiceMock(.init()), @@ -445,7 +445,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { joinRule: .knock)) let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) - + return .init(roomProxy: roomProxy, userSession: UserSessionMock(.init()), analyticsService: AnalyticsServiceMock(.init()), @@ -483,7 +483,7 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview { } let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) - + return .init(roomProxy: roomProxy, userSession: UserSessionMock(.init(clientProxy: clientProxyMock)), analyticsService: AnalyticsServiceMock(.init()), diff --git a/ElementX/Sources/Screens/RoomDirectorySearchScreen/RoomDirectorySearchScreenCoordinator.swift b/ElementX/Sources/Screens/RoomDirectorySearchScreen/RoomDirectorySearchScreenCoordinator.swift index 53ea0092d..1c0dfcdc9 100644 --- a/ElementX/Sources/Screens/RoomDirectorySearchScreen/RoomDirectorySearchScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomDirectorySearchScreen/RoomDirectorySearchScreenCoordinator.swift @@ -49,7 +49,7 @@ final class RoomDirectorySearchScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(RoomDirectorySearchScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/RoomDirectorySearchScreen/RoomDirectorySearchScreenViewModel.swift b/ElementX/Sources/Screens/RoomDirectorySearchScreen/RoomDirectorySearchScreenViewModel.swift index 8ce7d8383..e1456347c 100644 --- a/ElementX/Sources/Screens/RoomDirectorySearchScreen/RoomDirectorySearchScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomDirectorySearchScreen/RoomDirectorySearchScreenViewModel.swift @@ -19,7 +19,7 @@ class RoomDirectorySearchScreenViewModel: RoomDirectorySearchScreenViewModelType var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(userSession: UserSessionProtocol, userIndicatorController: UserIndicatorControllerProtocol) { roomDirectorySearchProxy = userSession.clientProxy.roomDirectorySearchProxy() diff --git a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift index 0e2cd8e1a..73a4066cb 100644 --- a/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift +++ b/ElementX/Sources/Screens/RoomDirectorySearchScreen/View/RoomDirectoryCell.swift @@ -13,7 +13,7 @@ struct RoomDirectorySearchCell: View { let result: RoomDirectorySearchResult let mediaProvider: MediaProviderProtocol? let joinAction: () -> Void - + private var description: String? { if let topic = result.topic { return topic @@ -27,7 +27,7 @@ struct RoomDirectorySearchCell: View { return result.id } - + var body: some View { ListRow(label: .avatar(title: result.name ?? result.alias ?? result.id, description: description, diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift index a9d6285ad..68f5767a1 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenCoordinator.swift @@ -27,14 +27,14 @@ enum RoomMemberDetailsScreenCoordinatorAction { final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol { private var viewModel: RoomMemberDetailsScreenViewModelProtocol - + private let actionsSubject: PassthroughSubject = .init() private var cancellables = Set() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(parameters: RoomMemberDetailsScreenCoordinatorParameters) { viewModel = RoomMemberDetailsScreenViewModel(userID: parameters.userID, roomProxy: parameters.roomProxy, @@ -65,7 +65,7 @@ final class RoomMemberDetailsScreenCoordinator: CoordinatorProtocol { func stop() { viewModel.stop() } - + func toPresentable() -> AnyView { AnyView(RoomMemberDetailsScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift index 0ea65db72..6a7d9db81 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenModels.swift @@ -22,7 +22,7 @@ struct RoomMemberDetailsScreenViewState: BindableState { var isOwnMemberDetails = false var isProcessingIgnoreRequest = false var dmRoomID: String? - + var bindings: RoomMemberDetailsScreenViewStateBindings var showVerifiedBadge: Bool { @@ -44,31 +44,31 @@ struct RoomMemberDetailsScreenViewStateBindings { case ignore case unignore } - + let action: Action let cancelTitle = L10n.actionCancel - + var title: String { switch action { case .ignore: return L10n.screenRoomMemberDetailsBlockUser case .unignore: return L10n.screenRoomMemberDetailsUnblockUser } } - + var confirmationTitle: String { switch action { case .ignore: return L10n.screenRoomMemberDetailsBlockAlertAction case .unignore: return L10n.screenRoomMemberDetailsUnblockAlertAction } } - + var description: String { switch action { case .ignore: return L10n.screenRoomMemberDetailsBlockAlertDescription case .unignore: return L10n.screenRoomMemberDetailsUnblockAlertDescription } } - + var viewAction: RoomMemberDetailsScreenViewAction { switch action { case .ignore: return .ignoreConfirmed diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift index 8760ffb61..9b4ef47e5 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/RoomMemberDetailsScreenViewModel.swift @@ -92,7 +92,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro Task { await userSession.clientProxy.withdrawUserIdentityVerification(state.userID) } } } - + // MARK: - Private private func loadMember() async { @@ -141,7 +141,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro state.bindings.alertInfo = .init(id: .unknown) } } - + @MainActor private func unignoreUser() async { guard let roomMemberProxy else { @@ -162,7 +162,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro state.bindings.alertInfo = .init(id: .unknown) } } - + private func updateMembers() { Task.detached { await self.roomProxy.updateMembers() @@ -177,7 +177,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro let loadingIndicatorIdentifier = "roomMemberAvatarLoadingIndicator" userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true)) defer { userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier) } - + // We don't actually know the mime type here, assume it's an image. if let mediaSource = try? MediaSourceProxy(url: url, mimeType: "image/jpeg"), case let .success(file) = await userSession.mediaProvider.loadFileFromSource(mediaSource) { @@ -221,7 +221,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro private func createDirectChat() async { guard let roomMemberProxy else { fatalError() } - + let loadingIndicatorIdentifier = "createDirectChatLoadingIndicator" userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), @@ -252,7 +252,7 @@ class RoomMemberDetailsScreenViewModel: RoomMemberDetailsScreenViewModelType, Ro private var loadingIndicatorIdentifier: String { "\(Self.self)-Loading" } - + private var statusIndicatorIdentifier: String { "\(Self.self)-Status" } diff --git a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift index 8f40a133a..a3fe7024d 100644 --- a/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift +++ b/ElementX/Sources/Screens/RoomMemberDetailsScreen/View/RoomMemberDetailsScreen.swift @@ -156,7 +156,7 @@ struct RoomMemberDetailsScreen: View { } } } - + @ViewBuilder private func blockUserAlertActions(_ item: RoomMemberDetailsScreenViewStateBindings.IgnoreUserAlertItem) -> some View { Button(item.cancelTitle, role: .cancel) { } @@ -165,7 +165,7 @@ struct RoomMemberDetailsScreen: View { context.send(viewAction: item.viewAction) } } - + private func blockUserAlertMessage(_ item: RoomMemberDetailsScreenViewStateBindings.IgnoreUserAlertItem) -> some View { Text(item.description) } @@ -192,19 +192,19 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview { state.verificationState == .verificationViolation }) .previewDisplayName("Verification Violation User") - + RoomMemberDetailsScreen(context: otherUserViewModel.context) .snapshotPreferences(expect: otherUserViewModel.context.$viewState.map { state in state.memberDetails?.role == .user && state.dmRoomID != nil }) .previewDisplayName("Other User") - + RoomMemberDetailsScreen(context: accountOwnerViewModel.context) .snapshotPreferences(expect: accountOwnerViewModel.context.$viewState.map { state in state.isOwnMemberDetails == true }) .previewDisplayName("Account Owner") - + RoomMemberDetailsScreen(context: ignoredUserViewModel.context) .snapshotPreferences(expect: ignoredUserViewModel.context.$viewState.map { state in state.memberDetails?.isIgnored ?? false && state.dmRoomID != nil @@ -235,7 +235,7 @@ struct RoomMemberDetailsScreen_Previews: PreviewProvider, TestablePreview { if member.userID != RoomMemberProxyMock.mockMe.userID { clientProxyMock.directRoomForUserIDReturnValue = .success("roomID") } - + return RoomMemberDetailsScreenViewModel(userID: member.userID, roomProxy: roomProxyMock, userSession: UserSessionMock(.init(clientProxy: clientProxyMock)), diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift index 8860516f5..529d6fced 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenCoordinator.swift @@ -56,7 +56,7 @@ final class RoomMembersListScreenCoordinator: CoordinatorProtocol { func stop() { viewModel.stop() } - + func toPresentable() -> AnyView { AnyView(RoomMembersListScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift index c249721c4..270b71ab3 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenModels.swift @@ -94,7 +94,7 @@ struct RoomMembersListScreenViewStateBindings { var mode: RoomMembersListScreenMode = .members /// A sheet model for the selected member to kick, ban, promote etc. var manageMemeberViewModel: ManageRoomMemberSheetViewModel? - + /// Information describing the currently displayed alert. var alertInfo: AlertInfo? } diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift index 0317d2799..7e92d7182 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/RoomMembersListScreenViewModel.swift @@ -25,7 +25,7 @@ class RoomMembersListScreenViewModel: RoomMembersListScreenViewModelType, RoomMe var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(initialMode: RoomMembersListScreenMode = .members, userSession: UserSessionProtocol, roomProxy: JoinedRoomProxyProtocol, diff --git a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift index 3e2dea229..0dbf02037 100644 --- a/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift +++ b/ElementX/Sources/Screens/RoomMemberListScreen/View/RoomMembersListScreenMemberCell.swift @@ -13,7 +13,7 @@ struct RoomMembersListScreenMemberCell: View { let listEntry: RoomMemberListScreenEntry let isLast: Bool let context: RoomMembersListScreenViewModel.Context - + var body: some View { Button { context.send(viewAction: .selectMember(listEntry.member)) diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift index 7feb78448..c350dc45d 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenCoordinator.swift @@ -26,11 +26,11 @@ final class RoomNotificationSettingsScreenCoordinator: CoordinatorProtocol { private let actionsSubject: PassthroughSubject = .init() private var cancellables = Set() - + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(parameters: RoomNotificationSettingsScreenCoordinatorParameters) { self.parameters = parameters viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: parameters.notificationSettingsProxy, diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift index 25aa4c1c7..128182bbf 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenModels.swift @@ -100,7 +100,7 @@ struct RoomNotificationSettingsScreenStrings { self.customSettingFootnote = customSettingFootnote } - + func string(for mode: RoomNotificationModeProxy) -> String { switch mode { case .allMessages: diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift index a4f7f1ba1..064e42948 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/RoomNotificationSettingsScreenViewModel.swift @@ -22,7 +22,7 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(notificationSettingsProxy: NotificationSettingsProxyProtocol, roomProxy: JoinedRoomProxyProtocol, displayAsUserDefinedRoomSettings: Bool) { let bindings = RoomNotificationSettingsScreenViewStateBindings() self.notificationSettingsProxy = notificationSettingsProxy @@ -37,7 +37,7 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie setupNotificationSettingsSubscription() fetchNotificationSettings() } - + // MARK: - Public override func process(viewAction: RoomNotificationSettingsScreenViewAction) { @@ -54,7 +54,7 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie } // MARK: - Private - + private func setupNotificationSettingsSubscription() { notificationSettingsProxy.callbacks .receive(on: DispatchQueue.main) @@ -152,7 +152,7 @@ class RoomNotificationSettingsScreenViewModel: RoomNotificationSettingsScreenVie state.bindings.alertInfo = AlertInfo(id: type, title: L10n.commonError, message: L10n.screenRoomNotificationSettingsErrorSettingMode) - + case .restoreDefaultFailed: state.bindings.alertInfo = AlertInfo(id: type, title: L10n.commonError, diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift index 7df3461ba..a5c18cc7a 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsScreen.swift @@ -15,7 +15,7 @@ struct RoomNotificationSettingsScreen: View { var body: some View { Form { allowCustomSettingSection - + if !context.allowCustomSetting { defaultSettingSection } else { @@ -29,7 +29,7 @@ struct RoomNotificationSettingsScreen: View { } // MARK: - Private - + private var allowCustomSettingSection: some View { Section { ListRow(label: .plain(title: L10n.screenRoomNotificationSettingsAllowCustom), @@ -74,17 +74,17 @@ struct RoomNotificationSettingsScreen: View { struct RoomNotificationSettingsScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = { let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init(defaultRoomMode: .mentionsAndKeywordsOnly, roomMode: .mentionsAndKeywordsOnly)) - + let roomProxy = JoinedRoomProxyMock(.init(name: "Room", isEncrypted: true)) return RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, roomProxy: roomProxy, displayAsUserDefinedRoomSettings: false) }() - + static let viewModelCustom = { let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init(defaultRoomMode: .allMessages, roomMode: .mentionsAndKeywordsOnly)) - + let roomProxy = JoinedRoomProxyMock(.init(name: "Room", isEncrypted: true)) return RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, diff --git a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsUserDefinedScreen.swift b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsUserDefinedScreen.swift index dc59544ef..e50575da5 100644 --- a/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsUserDefinedScreen.swift +++ b/ElementX/Sources/Screens/RoomNotificationSettingsScreen/View/RoomNotificationSettingsUserDefinedScreen.swift @@ -43,7 +43,7 @@ struct RoomNotificationSettingsUserDefinedScreen: View { struct RoomNotificationSettingsUserDefinedScreen_Previews: PreviewProvider, TestablePreview { static let viewModel = { let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init(defaultRoomMode: .mentionsAndKeywordsOnly, roomMode: .mentionsAndKeywordsOnly)) - + let roomProxy = JoinedRoomProxyMock(.init(name: "Room", isEncrypted: true)) return RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, @@ -53,14 +53,14 @@ struct RoomNotificationSettingsUserDefinedScreen_Previews: PreviewProvider, Test static let viewModelUnencrypted = { let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init(defaultRoomMode: .mentionsAndKeywordsOnly, roomMode: .mentionsAndKeywordsOnly)) - + let roomProxy = JoinedRoomProxyMock(.init(name: "Room", isEncrypted: false)) return RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxy, roomProxy: roomProxy, displayAsUserDefinedRoomSettings: true) }() - + static var previews: some View { RoomNotificationSettingsUserDefinedScreen(context: viewModel.context) .previewDisplayName("Encrypted") diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenCoordinator.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenCoordinator.swift index 1dfc3a819..7fbc13b56 100644 --- a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenCoordinator.swift @@ -44,7 +44,7 @@ final class RoomPollsHistoryScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(RoomPollsHistoryScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift index 57849f358..5d903bf79 100644 --- a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenModels.swift @@ -33,7 +33,7 @@ struct RoomPollsHistoryScreenViewState: BindableState { struct RoomPollsHistoryScreenViewStateBindings { /// Polls list filter var filter: RoomPollsHistoryFilter - + /// Information describing the currently displayed alert. var alertInfo: AlertInfo? } diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift index 8b02fe1ef..509723f48 100644 --- a/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/RoomPollsHistoryScreenViewModel.swift @@ -30,7 +30,7 @@ class RoomPollsHistoryScreenViewModel: RoomPollsHistoryScreenViewModelType, Room var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(pollInteractionHandler: PollInteractionHandlerProtocol, timelineController: TimelineControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol) { @@ -103,7 +103,7 @@ class RoomPollsHistoryScreenViewModel: RoomPollsHistoryScreenViewModelType, Room private func displayError(message: String) { state.bindings.alertInfo = .init(id: .alert, title: message) } - + // MARK: - Poll Interaction Handler private func endPoll(pollStartID: String) { @@ -162,7 +162,7 @@ class RoomPollsHistoryScreenViewModel: RoomPollsHistoryScreenViewModelType, Room guard paginateBackwardsTask == nil else { return } - + paginateBackwardsTask = Task { [weak self] in guard let self else { return diff --git a/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift b/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift index 8c80fc1b3..9cbc6a9e1 100644 --- a/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift +++ b/ElementX/Sources/Screens/RoomPollsHistoryScreen/View/RoomPollsHistoryScreen.swift @@ -122,7 +122,7 @@ struct RoomPollsHistoryScreen_Previews: PreviewProvider, TestablePreview { timelineController: timelineController, userIndicatorController: UserIndicatorControllerMock()) }() - + static let viewModel: RoomPollsHistoryScreenViewModel = { let timelineController = MockTimelineController() @@ -131,25 +131,25 @@ struct RoomPollsHistoryScreen_Previews: PreviewProvider, TestablePreview { PollRoomTimelineItem.mock(poll: .emptyDisclosed, isEditable: true)] timelineController.timelineItems = polls - + for i in 0.. AnyView { AnyView(RoomRolesAndPermissionsScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenModels.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenModels.swift index 4d21d2a28..5b25fe867 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenModels.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenModels.swift @@ -27,7 +27,7 @@ struct RoomRolesAndPermissionsScreenViewState: BindableState { var moderatorCount: Int? /// The permissions of the room when loaded. var permissions: RoomPermissions? - + var bindings = RoomRolesAndPermissionsScreenViewStateBindings() } diff --git a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift index 833aee96e..716b50990 100644 --- a/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomRolesAndPermissionsScreen/RoomRolesAndPermissionsScreenViewModel.swift @@ -21,7 +21,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(initialPermissions: RoomPermissions? = nil, roomProxy: JoinedRoomProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol, analytics: AnalyticsServiceProtocol) { self.roomProxy = roomProxy self.userIndicatorController = userIndicatorController @@ -83,7 +83,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM secondaryButton: .init(title: L10n.actionCancel, role: .cancel) { }) } } - + // MARK: - Members private func updateMembers(_ members: [RoomMemberProxyProtocol]) { @@ -148,7 +148,7 @@ class RoomRolesAndPermissionsScreenViewModel: RoomRolesAndPermissionsScreenViewM hideSavingIndicator() } - + // MARK: Loading indicator private static let savingIndicatorID = "RolesAndPermissionsSaving" diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift index a98bfcbeb..bb20b60b4 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/CompletionSuggestionService.swift @@ -126,11 +126,11 @@ final class CompletionSuggestionService: CompletionSuggestionServiceProtocol { && selectedRange.location <= upperBound && selectedRange.length <= upperBound - lowerBound } - + guard let match else { return nil } - + var suggestionText = String(text[match.range]) let firstChar = suggestionText.removeFirst() diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift index 48bc22c81..fcccee9ac 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarModels.swift @@ -27,9 +27,9 @@ enum ComposerToolbarViewModelAction { case sendMessage(plain: String, html: String?, mode: ComposerMode, intentionalMentions: IntentionalMentions) case editLastMessage case attach(ComposerAttachmentType) - + case handlePasteOrDrop(providers: [NSItemProvider]) - + case composerModeChanged(mode: ComposerMode) case composerFocusedChanged(isFocused: Bool) @@ -84,7 +84,7 @@ struct ComposerToolbarViewState: BindableState { var keyCommands: [WysiwygKeyCommand] = [] var bindings: ComposerToolbarViewStateBindings - + var isUploading: Bool { switch composerMode { case .previewVoiceMessage(_, _, let isUploading): @@ -93,7 +93,7 @@ struct ComposerToolbarViewState: BindableState { return false } } - + var showSendButton: Bool { switch composerMode { case .recordVoiceMessage: @@ -221,7 +221,7 @@ extension FormatItem { return \.link } } - + var accessibilityIdentifier: String { switch type { case .bold: @@ -250,7 +250,7 @@ extension FormatItem { return A11yIdentifiers.roomScreen.composerToolbar.link } } - + var accessibilityLabel: String { let localizedAction = switch type { case .bold: @@ -336,7 +336,7 @@ enum ComposerMode: Equatable { case edit(originalEventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID, type: EditType) case recordVoiceMessage(state: AudioRecorderState) case previewVoiceMessage(state: AudioPlayerState, waveform: WaveformSource, isUploading: Bool) - + var isEdit: Bool { switch self { case .edit: @@ -345,7 +345,7 @@ enum ComposerMode: Equatable { return false } } - + var isTextEditingEnabled: Bool { switch self { case .default, .reply, .edit: diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift index ac75d5e14..5d1ee8ce2 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModel.swift @@ -28,23 +28,23 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool private let attributedStringBuilder: AttributedStringBuilderProtocol private var hasAppeard = false - + private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + private struct WysiwygLinkData { let action: LinkAction let range: NSRange var url: String var text: String } - + private var currentLinkData: WysiwygLinkData? private var replyLoadingTask: Task? - + init(initialText: String? = nil, roomProxy: JoinedRoomProxyProtocol, wysiwygViewModel: WysiwygComposerViewModel, @@ -84,7 +84,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool .removeDuplicates() .weakAssign(to: \.state.isRoomEncrypted, on: self) .store(in: &cancellables) - + context.$viewState .map(\.composerMode) .removeDuplicates() @@ -93,13 +93,13 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool self?.actionsSubject.send(.composerModeChanged(mode: $0)) } .store(in: &cancellables) - + context.$viewState .map(\.bindings.composerFocused) .removeDuplicates() .sink { [weak self] in self?.actionsSubject.send(.composerFocusedChanged(isFocused: $0)) } .store(in: &cancellables) - + wysiwygViewModel.$isContentEmpty .removeDuplicates() .sink { [weak self] isEmpty in @@ -122,7 +122,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool self?.actionsSubject.send(.contentChanged(isEmpty: plainComposerText.string.isEmpty)) } .store(in: &cancellables) - + wysiwygViewModel.$actionStates .map { actions in FormatType @@ -149,13 +149,13 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool focusComposerIfHardwareKeyboardConnected() let identityStatusChangesPublisher = roomProxy.identityStatusChangesPublisher.receive(on: DispatchQueue.main) - + Task { [weak self] in for await changes in identityStatusChangesPublisher.values { guard !Task.isCancelled else { return } - + await self?.processIdentityStatusChanges(changes) } } @@ -176,7 +176,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool func stop() { saveDraft() } - + override func process(viewAction: ComposerToolbarViewAction) { switch viewAction { case .composerAppeared: @@ -252,7 +252,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } } } - + func process(timelineAction: TimelineComposerAction) { switch timelineAction { case .setMode(mode: let mode): @@ -398,7 +398,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool private var plainComposerContent: PlainComposerContent { let attributedString = NSMutableAttributedString(attributedString: context.plainComposerText) - + var shouldMakeAnotherPass = false var userIDs = Set() var containsAtRoom = false @@ -558,24 +558,24 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool MXLog.error("Failed retrieving room member for identity status change: \(change)") continue } - + identityPinningViolations[change.userId] = member default: // clear identityPinningViolations[change.userId] = nil } } - + state.canSend = identityPinningViolations.isEmpty } - + private func set(mode: ComposerMode) { if state.composerMode.isLoadingReply, state.composerMode.replyEventID != mode.replyEventID { replyLoadingTask?.cancel() } guard mode != state.composerMode else { return } - + state.composerMode = mode switch mode { case .default: @@ -589,14 +589,14 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool state.bindings.composerFocused = true } } - + private func set(text: String) { if context.composerFormattingEnabled { wysiwygViewModel.textView.flushPills() wysiwygViewModel.setHtmlContent(text) } else { let attributedString = NSMutableAttributedString(string: text) - + parseUserMentionsMarkdown(text) { range, url in // Call your handleUserMention function here attributedString.addAttribute(.link, value: url, range: range) @@ -649,19 +649,19 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool range: wysiwygViewModel.attributedContent.selection, url: linkAction.url ?? "", text: "") - + let urlBinding: Binding = .init { [weak self] in self?.currentLinkData?.url ?? "" } set: { [weak self] value in self?.currentLinkData?.url = value } - + let textBinding: Binding = .init { [weak self] in self?.currentLinkData?.text ?? "" } set: { [weak self] value in self?.currentLinkData?.text = value } - + switch linkAction { case .createWithText: state.bindings.alertInfo = makeCreateWithTextAlertInfo(urlBinding: urlBinding, textBinding: textBinding) @@ -673,7 +673,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool break } } - + private func makeCreateWithTextAlertInfo(urlBinding: Binding, textBinding: Binding) -> AlertInfo { AlertInfo(id: UUID(), title: L10n.richTextEditorCreateLink, @@ -683,7 +683,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool secondaryButton: AlertInfo.AlertButton(title: L10n.actionSave) { self.restoreComposerSelectedRange() self.createLinkWithText() - + }, textFields: [AlertInfo.AlertTextField(placeholder: L10n.commonText, text: textBinding, @@ -694,7 +694,7 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool autoCapitalization: .never, autoCorrectionDisabled: true)]) } - + private func makeSetUrlAlertInfo(urlBinding: Binding, isEdit: Bool) -> AlertInfo { AlertInfo(id: UUID(), title: isEdit ? L10n.richTextEditorEditLink : L10n.richTextEditorCreateLink, @@ -704,14 +704,14 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool secondaryButton: AlertInfo.AlertButton(title: L10n.actionSave) { self.restoreComposerSelectedRange() self.setLink() - + }, textFields: [AlertInfo.AlertTextField(placeholder: L10n.richTextEditorUrlPlaceholder, text: urlBinding, autoCapitalization: .never, autoCorrectionDisabled: true)]) } - + private func makeEditChoiceAlertInfo(urlBinding: Binding) -> AlertInfo { AlertInfo(id: UUID(), title: L10n.richTextEditorEditLink, @@ -726,23 +726,23 @@ final class ComposerToolbarViewModel: ComposerToolbarViewModelType, ComposerTool } }]) } - + private func restoreComposerSelectedRange() { guard let currentLinkData else { return } wysiwygViewModel.select(range: currentLinkData.range) } - + private func setLink() { guard let currentLinkData else { return } wysiwygViewModel.applyLinkOperation(.setLink(urlString: currentLinkData.url)) } - + private func createLinkWithText() { guard let currentLinkData else { return } wysiwygViewModel.applyLinkOperation(.createLink(urlString: currentLinkData.url, text: currentLinkData.text)) } - + private func removeLinks() { wysiwygViewModel.applyLinkOperation(.removeLinks) } @@ -819,7 +819,7 @@ extension ComposerToolbarViewModel { if !canSend { roomProxy.identityStatusChangesPublisher = .init([.init(userId: RoomMemberProxyMock.mockAlice.userID, changedTo: .verificationViolation)]) } - + let wysiwygViewModel = WysiwygComposerViewModel() let viewModel = ComposerToolbarViewModel(roomProxy: roomProxy, wysiwygViewModel: wysiwygViewModel, diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift index 2225e9e43..5d503c0fb 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/ComposerToolbarViewModelProtocol.swift @@ -17,6 +17,6 @@ protocol ComposerToolbarViewModelProtocol { func start() func stop() - + func process(timelineAction: TimelineComposerAction) } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift index 90d5e77c7..ea170fc16 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/CompletionSuggestionView.swift @@ -23,7 +23,7 @@ struct CompletionSuggestionView: View { /// To make the scrolling more apparent we show a factional amount static let maxVisibleRows: CGFloat = 4.5 } - + // MARK: Public @State private var prototypeListItemFrame: CGRect = .zero @@ -47,7 +47,7 @@ struct CompletionSuggestionView: View { .padding(.bottom, Constants.listItemPadding) } } - + private func list() -> some View { List(items) { item in Button { @@ -66,18 +66,18 @@ struct CompletionSuggestionView: View { private func contentHeightForRowCount(_ count: CGFloat) -> CGFloat { (prototypeListItemFrame.height + Constants.listItemPadding * 2 + Constants.listItemSpacing) * count - Constants.listItemSpacing / 2 + Constants.topPadding - Constants.listItemPadding } - + private struct ListItemPaddingModifier: ViewModifier { private let isFirst: Bool - + init(isFirst: Bool) { self.isFirst = isFirst } - + func body(content: Content) -> some View { let topPadding: CGFloat = isFirst ? Constants.topPadding : Constants.listItemPadding let bottomPadding: CGFloat = Constants.listItemPadding - + return content .padding(.top, topPadding) .padding(.bottom, bottomPadding) diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift index 0da4f1262..d7e8a116a 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/ComposerToolbar.swift @@ -236,7 +236,7 @@ struct ComposerToolbar: View { // sending e.g. accepting current autocorrection. // Fixes https://github.com/element-hq/element-x-ios/issues/3216 context.presendCallback?() - + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { context.send(viewAction: .sendMessage) } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/FormattingToolbar.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/FormattingToolbar.swift index 15390bf77..dea0a6800 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/FormattingToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/FormattingToolbar.swift @@ -14,7 +14,7 @@ struct FormattingToolbar: View { var formatItems: [FormatItem] /// The action when an item is selected var formatAction: (FormatType) -> Void - + var body: some View { ScrollView(.horizontal) { HStack(spacing: 5) { @@ -50,7 +50,7 @@ private extension FormatItem { return .compound.iconDisabled } } - + var backgroundColor: Color { switch state { case .reversed: diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift index 939056539..90c049526 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposer.swift @@ -78,7 +78,7 @@ struct MessageComposer: View { pasteHandler: pasteAction) } } - + private var composerHeight: CGFloat { let baseHeight = isExpanded ? ComposerConstant.maxHeight : ComposerConstant.minHeight return (baseHeight - composerTranslation).clamped(to: ComposerConstant.allowedHeightRange) @@ -95,7 +95,7 @@ struct MessageComposer: View { EmptyView() } } - + private var resizeGrabber: some View { Capsule() .foregroundStyle(.tertiary) @@ -103,7 +103,7 @@ struct MessageComposer: View { .padding(.vertical, 8) .frame(maxWidth: .infinity) } - + private var dragGesture: some Gesture { DragGesture() .onChanged { value in diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift index 91de26ddb..e70ed505d 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/MessageComposerTextField.swift @@ -14,11 +14,11 @@ struct MessageComposerTextField: View { @Binding var text: NSAttributedString @Binding var presendCallback: (() -> Void)? @Binding var selectedRange: NSRange - + let maxHeight: CGFloat let keyHandler: GenericKeyHandler let pasteHandler: PasteHandler - + var body: some View { UITextViewWrapper(text: $text, presendCallback: $presendCallback, @@ -30,7 +30,7 @@ struct MessageComposerTextField: View { .background(placeholderView, alignment: .topLeading) .background { keyboardShortcuts } } - + @ViewBuilder private var placeholderView: some View { if text.string.isEmpty { @@ -54,18 +54,18 @@ struct MessageComposerTextField: View { private struct UITextViewWrapper: UIViewRepresentable { @Environment(\.timelineContext) private var timelineContext - + @Binding var text: NSAttributedString @Binding var presendCallback: (() -> Void)? @Binding var selectedRange: NSRange - + let maxHeight: CGFloat - + let keyHandler: GenericKeyHandler let pasteHandler: PasteHandler - + private let font = UIFont.preferredFont(forTextStyle: .body) - + func makeUIView(context: UIViewRepresentableContext) -> UITextView { // Need to use TextKit 1 for mentions let textView = ElementTextView(timelineContext: timelineContext, @@ -89,21 +89,21 @@ private struct UITextViewWrapper: UIViewRepresentable { if ProcessInfo.processInfo.isiOSAppOnMac { textView.autocorrectionType = .no } - + textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) return textView } - + func sizeThatFits(_ proposal: ProposedViewSize, uiView: UITextView, context: Context) -> CGSize? { // Note: Coalescing a width of zero here returns a size for the view with 1 line of text visible. let newSize = uiView.sizeThatFits(CGSize(width: proposal.width ?? .zero, height: maxHeight)) let width = proposal.width ?? newSize.width let height = min(maxHeight, newSize.height) - + return CGSize(width: width, height: height) } - + func updateUIView(_ textView: UITextView, context: UIViewRepresentableContext) { // Prevent the textView from inheriting attributes from mention pills textView.typingAttributes = [.font: font, @@ -149,7 +149,7 @@ private struct UITextViewWrapper: UIViewRepresentable { } } } - + func makeCoordinator() -> Coordinator { Coordinator(text: $text, selectedRange: $selectedRange, @@ -157,16 +157,16 @@ private struct UITextViewWrapper: UIViewRepresentable { keyHandler: keyHandler, pasteHandler: pasteHandler) } - + final class Coordinator: NSObject, UITextViewDelegate, ElementTextViewDelegate { private var text: Binding private var selectedRange: Binding - + private let maxHeight: CGFloat - + private let keyHandler: GenericKeyHandler private let pasteHandler: PasteHandler - + init(text: Binding, selectedRange: Binding, maxHeight: CGFloat, @@ -178,11 +178,11 @@ private struct UITextViewWrapper: UIViewRepresentable { self.keyHandler = keyHandler self.pasteHandler = pasteHandler } - + func textViewDidChange(_ textView: UITextView) { text.wrappedValue = textView.attributedText } - + func textViewDidReceiveKeyPress(_ textView: UITextView, key: UIKeyboardHIDUsage) { keyHandler(key) } @@ -190,7 +190,7 @@ private struct UITextViewWrapper: UIViewRepresentable { func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView) { textView.insertText("\n") } - + func textView(_ textView: UITextView, didReceivePasteWith providers: [NSItemProvider]) { pasteHandler(providers) } @@ -242,7 +242,7 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate { [UIKeyCommand(input: "\r", modifierFlags: .shift, action: #selector(shiftEnterKeyPressed)), UIKeyCommand(input: "\r", modifierFlags: [], action: #selector(enterKeyPressed))] } - + @objc func shiftEnterKeyPressed(sender: UIKeyCommand) { elementDelegate?.textViewDidReceiveShiftEnterKeyPress(self) } @@ -269,21 +269,21 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate { super.pressesBegan(presses, with: event) } - + // Pasting support - + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { if super.canPerformAction(action, withSender: sender) { return true } - + guard action == #selector(paste(_:)) else { return false } - + return UIPasteboard.general.itemProviders.filter { !$0.isSupportedForPasteOrDrop }.isEmpty } - + override func paste(_ sender: Any?) { let providers = UIPasteboard.general.itemProviders @@ -312,7 +312,7 @@ private class ElementTextView: UITextView, PillAttachmentViewProviderDelegate { func registerPillView(_ pillView: UIView) { pillViews.add(pillView) } - + func flushPills() { for view in pillViews.allObjects { view.alpha = 0.0 @@ -341,15 +341,15 @@ struct MessageComposerTextField_Previews: PreviewProvider, TestablePreview { PreviewWrapper(text: "A really long message that will wrap to multiple lines on a phone in portrait.") } } - + struct PreviewWrapper: View { @State var text: NSAttributedString - + init(text: String) { _text = .init(initialValue: .init(string: text, attributes: [.font: UIFont.preferredFont(forTextStyle: .body), .foregroundColor: UIColor.compound.textPrimary])) } - + var body: some View { MessageComposerTextField(placeholder: "Placeholder", text: $text, diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift index bb4966de3..f981971a1 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/RoomAttachmentPicker.swift @@ -79,10 +79,10 @@ struct RoomAttachmentPicker: View { struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview { static let viewModel = makeViewModel() - + static func makeViewModel() -> ComposerToolbarViewModel { let appSettings = AppSettings.volatile() - + return ComposerToolbarViewModel(roomProxy: JoinedRoomProxyMock(.init()), wysiwygViewModel: WysiwygComposerViewModel(), completionSuggestionService: CompletionSuggestionServiceMock(configuration: .init()), @@ -92,7 +92,7 @@ struct RoomAttachmentPicker_Previews: PreviewProvider, TestablePreview { analyticsService: AnalyticsServiceMock(.init()), composerDraftService: ComposerDraftServiceMock(.init())) } - + static var previews: some View { RoomAttachmentPicker(context: viewModel.context) } diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessagePreviewComposer.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessagePreviewComposer.swift index 6c5ec3bd3..18ac0fd1c 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessagePreviewComposer.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessagePreviewComposer.swift @@ -18,12 +18,12 @@ struct VoiceMessagePreviewComposer: View { @ScaledMetric private var waveformLineWidth = 2.0 @ScaledMetric private var waveformLinePadding = 2.0 @GestureState var isDragging = false - + let onPlay: () -> Void let onPause: () -> Void let onSeek: (Double) -> Void let onScrubbing: (Bool) -> Void - + var timeLabelContent: String { // Display the duration if progress is 0.0 let percent = playerState.progress > 0.0 ? playerState.progress : 1.0 @@ -43,7 +43,7 @@ struct VoiceMessagePreviewComposer: View { .foregroundColor(.compound.textSecondary) .monospacedDigit() .fixedSize(horizontal: true, vertical: true) - + waveformView .waveformInteraction(isDragging: $isDragging, progress: playerState.progress, @@ -77,7 +77,7 @@ struct VoiceMessagePreviewComposer: View { .progressMask(progress: playerState.progress) } } - + private func onPlayPause() { if playerState.playbackState == .playing { onPause() diff --git a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessageRecordingView.swift b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessageRecordingView.swift index 546d28eda..1ec2aff18 100644 --- a/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessageRecordingView.swift +++ b/ElementX/Sources/Screens/RoomScreen/ComposerToolbar/View/VoiceMessageRecordingView.swift @@ -17,13 +17,13 @@ struct VoiceMessageRecordingView: View { @ScaledMetric private var waveformLineWidth = 2.0 @ScaledMetric private var waveformLinePadding = 2.0 @ScaledMetric private var recordingIndicatorSize = 8 - + private static let elapsedTimeFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "mm:ss" return dateFormatter }() - + private var timeLabelContent: String { Self.elapsedTimeFormatter.string(from: Date(timeIntervalSinceReferenceDate: recorderState.duration)) } @@ -37,7 +37,7 @@ struct VoiceMessageRecordingView: View { HStack(spacing: 8) { VoiceMessageRecordingBadge() .frame(width: recordingIndicatorSize, height: recordingIndicatorSize) - + Text(timeLabelContent) .lineLimit(1) .font(.compound.bodySMSemibold) @@ -55,7 +55,7 @@ struct VoiceMessageRecordingView: View { private struct VoiceMessageRecordingBadge: View { @State private var opacity: CGFloat = 0 - + var body: some View { Circle() .foregroundColor(.red) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift index 8e6a3d555..06eea22ee 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenCoordinator.swift @@ -57,9 +57,9 @@ final class RoomScreenCoordinator: CoordinatorProtocol { private var roomViewModel: RoomScreenViewModelProtocol private var timelineViewModel: TimelineViewModelProtocol private var composerViewModel: ComposerToolbarViewModelProtocol - + private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -115,7 +115,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { timelineViewModel.actions .sink { [weak self] action in guard let self else { return } - + switch action { case .displayEmojiPicker(let itemID, let selectedEmojis): actionsSubject.send(.presentEmojiPicker(itemID: itemID, selectedEmojis: selectedEmojis)) @@ -167,11 +167,11 @@ final class RoomScreenCoordinator: CoordinatorProtocol { } } .store(in: &cancellables) - + composerViewModel.actions .sink { [weak self] action in guard let self else { return } - + timelineViewModel.process(composerAction: action) } .store(in: &cancellables) @@ -240,7 +240,7 @@ final class RoomScreenCoordinator: CoordinatorProtocol { func toPresentable() -> AnyView { let composerToolbar = ComposerToolbar(context: composerViewModel.context) - + return AnyView(RoomScreen(context: roomViewModel.context, timelineContext: timelineViewModel.context, composerToolbar: composerToolbar)) diff --git a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift index 728b02757..a798bd300 100644 --- a/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomScreen/RoomScreenViewModel.swift @@ -66,7 +66,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol self.initialSelectedPinnedEventID = initialSelectedPinnedEventID pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID) - + let viewState = RoomScreenViewState(roomTitle: roomProxy.infoPublisher.value.displayName ?? roomProxy.id, roomAvatar: roomProxy.infoPublisher.value.avatar, hasOngoingCall: roomProxy.infoPublisher.value.hasRoomCall, @@ -83,7 +83,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol await updateVerificationBadge() } } - + override func process(viewAction: RoomScreenViewAction) { switch viewAction { case .tappedPinnedEventsBanner: @@ -174,7 +174,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol state.isSharingLiveLocation = sessionsByRoomID.keys.contains(roomProxy.id) } .store(in: &cancellables) - + roomProxy.infoPublisher .receive(on: DispatchQueue.main) .sink { [weak self] roomInfo in @@ -251,7 +251,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol MXLog.error("Failed retrieving room member for identity status change: \(change)") continue } - + identityVerificationViolations[change.userId] = member default: identityVerificationViolations[change.userId] = nil @@ -303,9 +303,9 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol defer { hideLoadingIndicator() } - + showLoadingIndicator() - + if case .failure = await clientProxy.withdrawUserIdentityVerification(userID) { state.bindings.alertInfo = .init(id: .unknown, title: L10n.commonError) } @@ -352,7 +352,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol default: state.isKnockableRoom = false } - + if let powerLevels = roomInfo.powerLevels { state.canSendMessage = powerLevels.canOwnUser(sendMessage: .roomMessage) state.canJoinCall = powerLevels.canOwnUserJoinCall() @@ -379,7 +379,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol } } } - + private func acceptKnock(eventID: String) async { guard case let .loaded(requests) = roomProxy.knockRequestsStatePublisher.value, let request = requests.first(where: { $0.eventID == eventID }) else { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomCallControlsToolbar.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomCallControlsToolbar.swift index a2fea76fd..a037722fe 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomCallControlsToolbar.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomCallControlsToolbar.swift @@ -33,7 +33,7 @@ struct RoomCallControlsToolbar: ToolbarContent { } label: { Label(L10n.a11yStartVoiceCall, icon: \.voiceCallSolid) } - + Button { onCallTap(false) } label: { diff --git a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift index 5240c970a..d1442f689 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/RoomScreen.swift @@ -16,16 +16,16 @@ struct RoomScreen: View { @ObservedObject private var timelineContext: TimelineViewModelType.Context let composerToolbar: ComposerToolbar @Environment(\.accessibilityVoiceOverEnabled) private var isVoiceOverEnabled - + enum MarkAsReadSource { case up case down } - + /// Which scroll button (if any) currently has the "Mark as read" pill displayed alongside it. /// Set when the user long-presses one of the scroll buttons; the pill anchors to that button. @State private var markAsReadSource: MarkAsReadSource? - + init(context: RoomScreenViewModelType.Context, timelineContext: TimelineViewModelType.Context, composerToolbar: ComposerToolbar) { @@ -33,7 +33,7 @@ struct RoomScreen: View { self.timelineContext = timelineContext self.composerToolbar = composerToolbar } - + var body: some View { TimelineView(timelineContext: timelineContext) .overlay { @@ -183,7 +183,7 @@ struct RoomScreen: View { } } } - + private var markAsReadPill: some View { Button { timelineContext.send(viewAction: .markAllAsRead) @@ -192,7 +192,7 @@ struct RoomScreen: View { markAsReadPillLabel } } - + @ViewBuilder private var markAsReadPillLabel: some View { // Font scales with Dynamic Type via the Compound token; padding is a fixed point value @@ -212,15 +212,15 @@ struct RoomScreen: View { label.background(.regularMaterial, in: Capsule()) } } - + private func revealMarkAsReadPill(source: MarkAsReadSource) { markAsReadSource = source } - + private func dismissMarkAsReadPill() { markAsReadSource = nil } - + /// Whether the scroll button that the pill is anchored to is still being rendered. /// Used to dismiss an orphaned pill when the source button gets hidden — without /// this, the pill can render alongside an invisible button. Returns `true` when no @@ -232,13 +232,13 @@ struct RoomScreen: View { case .none: true } } - + /// Hide the new-messages dot when the jump-to-read-marker feature is disabled. private var scrollToBottomShowsBadge: Bool { timelineContext.viewState.jumpToReadMarkerEnabled && timelineContext.viewState.bindings.hasNewMessagesAtBottom } - + @ViewBuilder private var composer: some View { if context.viewState.hasSuccessor { @@ -328,7 +328,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { static let readOnlyViewModels = makeViewModels(canSendMessage: false) static let tombstonedViewModels = makeViewModels(hasSuccessor: true) static let composerViewModel = ComposerToolbarViewModel.mock() - + static var previews: some View { ElementNavigationStack { RoomScreen(context: viewModels.room.context, @@ -361,7 +361,7 @@ struct RoomScreen_Previews: PreviewProvider, TestablePreview { successor: hasSuccessor ? .init(roomId: UUID().uuidString, reason: nil) : nil, powerLevelsConfiguration: .init(canUserSendMessage: canSendMessage))) let roomViewModel = RoomScreenViewModel.mock(roomProxyMock: roomProxyMock) - + let appSettings = AppSettings.volatile() let timelineViewModel = TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockTimelineController(), diff --git a/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift b/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift index af554a237..86738764e 100644 --- a/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift +++ b/ElementX/Sources/Screens/RoomScreen/View/SwipeRightAction.swift @@ -225,7 +225,7 @@ private struct PanGestureRepresentable: UIGestureRecognizerRepresentable { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { guard let panRecognizer = gestureRecognizer as? UIPanGestureRecognizer else { return false } - + let velocity = panRecognizer.velocity(in: gestureRecognizer.view) return abs(velocity.y) < abs(velocity.x) } diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift index c92868518..2de33c291 100644 --- a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenCoordinator.swift @@ -48,7 +48,7 @@ final class RoomSelectionScreenCoordinator: CoordinatorProtocol { func stop() { viewModel.stop() } - + func toPresentable() -> AnyView { AnyView(RoomSelectionScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift index 4ca54cdc6..a9144567a 100644 --- a/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomSelectionScreen/RoomSelectionScreenViewModel.swift @@ -20,7 +20,7 @@ class RoomSelectionScreenViewModel: RoomSelectionScreenViewModelType, RoomSelect var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(userSession: UserSessionProtocol, roomSummaryProvider: RoomSummaryProviderProtocol) { clientProxy = userSession.clientProxy diff --git a/ElementX/Sources/Screens/RoomThreadListScreen/RoomThreadListScreenCoordinator.swift b/ElementX/Sources/Screens/RoomThreadListScreen/RoomThreadListScreenCoordinator.swift index c99f69f6f..79b4715cf 100644 --- a/ElementX/Sources/Screens/RoomThreadListScreen/RoomThreadListScreenCoordinator.swift +++ b/ElementX/Sources/Screens/RoomThreadListScreen/RoomThreadListScreenCoordinator.swift @@ -22,7 +22,7 @@ final class RoomThreadListScreenCoordinator: CoordinatorProtocol { private let viewModel: RoomThreadListScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -39,7 +39,7 @@ final class RoomThreadListScreenCoordinator: CoordinatorProtocol { viewModel.actionsPublisher.sink { [weak self] action in guard let self else { return } MXLog.info("Coordinator: received view model action: \(action)") - + switch action { case .presentThread(let threadRootEventID): actionsSubject.send(.presentThread(threadRootEventID: threadRootEventID)) @@ -47,7 +47,7 @@ final class RoomThreadListScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(RoomThreadListScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/RoomThreadListScreen/RoomThreadListScreenViewModel.swift b/ElementX/Sources/Screens/RoomThreadListScreen/RoomThreadListScreenViewModel.swift index 64a7556ad..835c2b59f 100644 --- a/ElementX/Sources/Screens/RoomThreadListScreen/RoomThreadListScreenViewModel.swift +++ b/ElementX/Sources/Screens/RoomThreadListScreen/RoomThreadListScreenViewModel.swift @@ -19,7 +19,7 @@ class RoomThreadListScreenViewModel: RoomThreadListScreenViewModelType, RoomThre var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(threadListServiceProxy: RoomThreadListServiceProxyProtocol, mediaProvider: MediaProviderProtocol) { self.threadListServiceProxy = threadListServiceProxy diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenCoordinator.swift index fc7d43485..634f3cf8a 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenCoordinator.swift @@ -44,7 +44,7 @@ final class SecureBackupKeyBackupScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(SecureBackupKeyBackupScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift index 99969bb01..90a36016f 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/SecureBackupKeyBackupScreenViewModel.swift @@ -19,7 +19,7 @@ class SecureBackupKeyBackupScreenViewModel: SecureBackupKeyBackupScreenViewModel var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(secureBackupController: SecureBackupControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol?) { self.secureBackupController = secureBackupController self.userIndicatorController = userIndicatorController @@ -63,7 +63,7 @@ class SecureBackupKeyBackupScreenViewModel: SecureBackupKeyBackupScreenViewModel } // MARK: - Private - + private func disableBackup() { Task { switch await secureBackupController.disable() { diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift index 3ddae2023..94b417cc5 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupKeyBackupScreen/View/SecureBackupKeyBackupScreen.swift @@ -39,7 +39,7 @@ struct SecureBackupKeyBackupScreen: View { disableBackupSection } } - + private var disableBackupSection: some View { VStack(spacing: 24) { VStack(spacing: 16) { diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenCoordinator.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenCoordinator.swift index e6a4c6b09..f554afa55 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenCoordinator.swift @@ -51,7 +51,7 @@ final class SecureBackupLogoutConfirmationScreenCoordinator: CoordinatorProtocol } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(SecureBackupLogoutConfirmationScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift index 8b7186a4f..985915fe7 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/SecureBackupLogoutConfirmationScreenViewModel.swift @@ -27,7 +27,7 @@ class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmat var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(secureBackupController: SecureBackupControllerProtocol, homeserverReachabilityPublisher: CurrentValuePublisher) { self.secureBackupController = secureBackupController self.homeserverReachabilityPublisher = homeserverReachabilityPublisher diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift index 680d482cc..394112f6d 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupLogoutConfirmationScreen/View/SecureBackupLogoutConfirmationScreen.swift @@ -28,7 +28,7 @@ struct SecureBackupLogoutConfirmationScreen: View { .backgroundStyle(.compound.bgCanvasDefault) .alert(item: $context.alertInfo) } - + @ViewBuilder private var content: some View { Text(title) diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift index a6012318f..6d1b0d47e 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupRecoveryKeyScreen/SecureBackupRecoveryKeyScreenViewModel.swift @@ -19,7 +19,7 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(secureBackupController: SecureBackupControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol, isModallyPresented: Bool) { diff --git a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift index 24c98fdc0..6baba5dd9 100644 --- a/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecureBackup/SecureBackupScreen/SecureBackupScreenViewModel.swift @@ -19,7 +19,7 @@ class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackup var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(secureBackupController: SecureBackupControllerProtocol, userIndicatorController: UserIndicatorControllerProtocol, chatBackupDetailsURL: URL) { diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenCoordinator.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenCoordinator.swift index 9d2e1cd27..22b886fbc 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenCoordinator.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenCoordinator.swift @@ -26,7 +26,7 @@ final class SecurityAndPrivacyScreenCoordinator: CoordinatorProtocol { private let viewModel: SecurityAndPrivacyScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -55,7 +55,7 @@ final class SecurityAndPrivacyScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(SecurityAndPrivacyScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift index 2d8aebd56..2656a01f4 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenModels.swift @@ -152,7 +152,7 @@ struct SecurityAndPrivacyScreenViewState: BindableState { fatalError("Not reachable") } } - + init(serverName: String, accessType: SecurityAndPrivacyRoomAccessType, isEncryptionEnabled: Bool, diff --git a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift index 02dce5074..dd6f27f06 100644 --- a/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift +++ b/ElementX/Sources/Screens/SecurityAndPrivacyScreen/SecurityAndPrivacyScreenViewModel.swift @@ -279,7 +279,7 @@ class SecurityAndPrivacyScreenViewModel: SecurityAndPrivacyScreenViewModelType, displayManageAuthorizedSpacesScreen(isAskToJoin: true) } } - + private func displayManageAuthorizedSpacesScreen(isAskToJoin: Bool) { let joinedSpaces = state.selectableJoinedSpaces let unknownSpaceIDs = state.currentSettings.accessType.spaceIDs.filter { id in diff --git a/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenCoordinator.swift index 822961070..0f13fe139 100644 --- a/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenCoordinator.swift @@ -25,7 +25,7 @@ final class AdvancedSettingsScreenCoordinator: CoordinatorProtocol { clientProxy: parameters.clientProxy, userIndicatorController: parameters.userIndicatorController) } - + func toPresentable() -> AnyView { AnyView(AdvancedSettingsScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenModels.swift index 5dc19d436..48b455549 100644 --- a/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/AdvancedSettingsScreen/AdvancedSettingsScreenModels.swift @@ -38,11 +38,11 @@ struct AdvancedSettingsScreenViewState: BindableState { @dynamicMemberLookup struct AdvancedSettingsScreenViewStateBindings { private let advancedSettings: AdvancedSettingsProtocol - + init(advancedSettings: AdvancedSettingsProtocol) { self.advancedSettings = advancedSettings } - + subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Setting { get { advancedSettings[keyPath: keyPath] } set { advancedSettings[keyPath: keyPath] = newValue } diff --git a/ElementX/Sources/Screens/Settings/DeactivateAccountScreen/DeactivateAccountScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/DeactivateAccountScreen/DeactivateAccountScreenCoordinator.swift index 15087e183..2ea55babd 100644 --- a/ElementX/Sources/Screens/Settings/DeactivateAccountScreen/DeactivateAccountScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/DeactivateAccountScreen/DeactivateAccountScreenCoordinator.swift @@ -23,7 +23,7 @@ final class DeactivateAccountScreenCoordinator: CoordinatorProtocol { private let viewModel: DeactivateAccountScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -48,7 +48,7 @@ final class DeactivateAccountScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(DeactivateAccountScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/Settings/DeactivateAccountScreen/DeactivateAccountScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeactivateAccountScreen/DeactivateAccountScreenViewModel.swift index 77690fca7..e301f8b9a 100644 --- a/ElementX/Sources/Screens/Settings/DeactivateAccountScreen/DeactivateAccountScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeactivateAccountScreen/DeactivateAccountScreenViewModel.swift @@ -19,7 +19,7 @@ class DeactivateAccountScreenViewModel: DeactivateAccountScreenViewModelType, De var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(clientProxy: ClientProxyProtocol, userIndicatorController: UserIndicatorControllerProtocol) { self.clientProxy = clientProxy self.userIndicatorController = userIndicatorController @@ -49,7 +49,7 @@ class DeactivateAccountScreenViewModel: DeactivateAccountScreenViewModelType, De }, secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) } - + func deactivateAccount() async { userIndicatorController.submitIndicator(UserIndicator(id: deactivatingIndicatorID, type: .modal(progress: .indeterminate, interactiveDismissDisabled: true, allowsInteraction: false), diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift index 9f0f2372f..31d4535dc 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenModels.swift @@ -35,11 +35,11 @@ struct DeveloperOptionsScreenViewState: BindableState { @dynamicMemberLookup struct DeveloperOptionsScreenViewStateBindings { private let developerOptions: DeveloperOptionsProtocol - + init(developerOptions: DeveloperOptionsProtocol) { self.developerOptions = developerOptions } - + subscript(dynamicMember keyPath: ReferenceWritableKeyPath) -> Setting { get { developerOptions[keyPath: keyPath] } set { developerOptions[keyPath: keyPath] = newValue } @@ -68,11 +68,11 @@ protocol DeveloperOptionsProtocol: AnyObject { var knockingEnabled: Bool { get set } var linkPreviewsEnabled: Bool { get set } - + var jumpToReadMarkerEnabled: Bool { get set } var linkNewDeviceEnabled: Bool { get set } - + var roomThreadListEnabled: Bool { get set } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift index 0c096148f..8b5aff060 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/DeveloperOptionsScreenViewModel.swift @@ -48,7 +48,7 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve if let logsSize = try? FileManager.default.sizeForDirectory(at: .appGroupLogsDirectory) { components.append(.init(name: "Log Files", size: formatter.format(Int64(logsSize)))) } - + state.storeSizes = components } } diff --git a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift index 958e56359..c9a55adc2 100644 --- a/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift +++ b/ElementX/Sources/Screens/Settings/DeveloperOptionsScreen/View/DeveloperOptionsScreen.swift @@ -87,12 +87,12 @@ struct DeveloperOptionsScreen: View { Text("Can leak the device IP address when loading link metadata.") .foregroundStyle(.compound.textCriticalPrimary) } - + Toggle(isOn: $context.jumpToReadMarkerEnabled) { Text("Jump to unread") Text("Adds a button to jump to the read marker, plus a presence dot on the scroll-to-bottom button when new messages arrive while scrolled away.") } - + Toggle(isOn: $context.knockingEnabled) { Text("Knocking") Text("Ask to join rooms") @@ -109,7 +109,7 @@ struct DeveloperOptionsScreen: View { } footer: { Text("This setting controls how end-to-end encryption (E2EE) keys are exchanged. Enabling it will prevent the inclusion of devices that have not been explicitly verified by their owners.") } - + Section("Element Call remote URL override") { TextField("Leave empty to use EC locally", text: $elementCallURLOverrideString) .autocorrectionDisabled(true) @@ -182,7 +182,7 @@ struct DeveloperOptionsScreen: View { .navigationBarTitleDisplayMode(.inline) .toolbar { toolbar } } - + @ViewBuilder private var effectsView: some View { if showConfetti { @@ -192,7 +192,7 @@ struct DeveloperOptionsScreen: View { .task { await removeConfettiAfterDelay() } } } - + private func removeConfettiAfterDelay() async { try? await Task.sleep(for: .seconds(4)) showConfetti = false diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift index 400d80d3d..5934ce779 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenCoordinator.swift @@ -39,7 +39,7 @@ final class NotificationSettingsEditScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(NotificationSettingsEditScreen(context: viewModel.context)) } @@ -48,7 +48,7 @@ final class NotificationSettingsEditScreenCoordinator: CoordinatorProtocol { private func presentRoomNotificationSettings(roomID: String) async { guard case let .joined(roomProxy) = await parameters.userSession.clientProxy.roomForIdentifier(roomID) else { return } - + let roomNotificationSettingsParameters = RoomNotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: parameters.navigationStackCoordinator, notificationSettingsProxy: parameters.userSession.clientProxy.notificationSettings, roomProxy: roomProxy, diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift index 7eb5b3b61..078825d97 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/NotificationSettingsEditScreenModels.swift @@ -25,7 +25,7 @@ struct NotificationSettingsEditScreenViewState: BindableState { var pendingMode: NotificationSettingsEditScreenDefaultMode? var roomsWithUserDefinedMode: [NotificationSettingsEditScreenRoom] = [] var canPushEncryptedEvents = false - + func isSelected(mode: NotificationSettingsEditScreenDefaultMode) -> Bool { pendingMode == nil && defaultMode == mode } @@ -101,7 +101,7 @@ struct NotificationSettingsEditScreenRoom: Identifiable, Equatable { let roomId: String? var name = "" - + var avatar: RoomAvatar var notificationMode: RoomNotificationModeProxy? diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift index 7bf0527b5..d437321c4 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreen.swift @@ -27,7 +27,7 @@ struct NotificationSettingsEditScreen: View { } // MARK: - Private - + private var notificationModeSection: some View { Section { ForEach(context.viewState.availableDefaultModes, id: \.self) { mode in diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift index 39f9fb70d..d8b0dfb1b 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsEditScreen/View/NotificationSettingsEditScreenRoomCell.swift @@ -50,7 +50,7 @@ struct NotificationSettingsEditScreenRoomCell: View { struct NotificationSettingsEditScreenRoomCell_Previews: PreviewProvider, TestablePreview { static var previews: some View { let summaryProvider = RoomSummaryProviderMock(.init(state: .loaded(.mockRooms))) - + let notificationSettingsProxy = NotificationSettingsProxyMock(with: .init()) notificationSettingsProxy.getRoomsWithUserDefinedRulesReturnValue = [] diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift index 345836a24..a01a20495 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenCoordinator.swift @@ -38,7 +38,7 @@ final class NotificationSettingsScreenCoordinator: CoordinatorProtocol { init(parameters: NotificationSettingsScreenCoordinatorParameters) { self.parameters = parameters - + viewModel = NotificationSettingsScreenViewModel(appSettings: parameters.appSettings, userNotificationCenter: parameters.userNotificationCenter, notificationToneManager: NotificationToneManager(appSettings: parameters.appSettings), diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift index 823e843f7..5811543c3 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenModels.swift @@ -26,7 +26,7 @@ struct NotificationSettingsScreenViewState: BindableState { var showSystemNotificationsAlert: Bool { bindings.enableNotifications && isUserPermissionGranted == false } - + var settings: NotificationSettingsScreenSettings? var applyingChange = false var selectedAlertTone: NotificationTone diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift index 2589ae600..cd54de643 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/NotificationSettingsScreenViewModel.swift @@ -21,11 +21,11 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy @CancellableTask private var fetchSettingsTask: Task? private let notificationTonePreviewer: AudioPlayerProtocol private let notificationToneManager: NotificationToneManagerProtocol - + var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(appSettings: AppSettings, userNotificationCenter: UserNotificationCenterProtocol, notificationToneManager: NotificationToneManagerProtocol, @@ -44,7 +44,7 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy isModallyPresented: isModallyPresented, selectedAlertTone: appSettings.selectedNotificationTone ?? NotificationToneManager.defaultElementXMessageTone, availableCustomTones: notificationToneManager.customTones())) - + // Listen for changes to AppSettings. appSettings.$enableNotifications .weakAssign(to: \.state.bindings.enableNotifications, on: self) @@ -75,7 +75,7 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy func fetchInitialContent() { fetchSettings() } - + // MARK: - Public override func process(viewAction: NotificationSettingsScreenViewAction) { @@ -117,11 +117,11 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy } // MARK: - Private - + func readSystemAuthorizationStatus() async { state.isUserPermissionGranted = await userNotificationCenter.authorizationStatus() == .authorized } - + func toggleNotifications() { appSettings.enableNotifications.toggle() } @@ -161,11 +161,11 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy // Group chats var groupChatsMode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: false) let encryptedGroupChatsMode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: false) - + // Direct chats var directChatsMode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: false, isOneToOne: true) let encryptedDirectChatsMode = await notificationSettingsProxy.getDefaultRoomNotificationMode(isEncrypted: true, isOneToOne: true) - + // Old clients were having specific settings for encrypted and unencrypted rooms, // so it's possible for `group chats` and `direct chats` settings to be inconsistent (e.g. encrypted `direct chats` can have a different mode that unencrypted `direct chats`) if groupChatsMode != encryptedGroupChatsMode { @@ -183,16 +183,16 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy let roomMentionsEnabled = try? await notificationSettingsProxy.isRoomMentionEnabled() let callEnabled = try? await notificationSettingsProxy.isCallEnabled() let invitationsEnabled = try? await notificationSettingsProxy.isInviteForMeEnabled() - + guard !Task.isCancelled else { return } - + let notificationSettings = NotificationSettingsScreenSettings(groupChatsMode: groupChatsMode, directChatsMode: directChatsMode, roomMentionsEnabled: roomMentionsEnabled, callsEnabled: callEnabled, invitationsEnabled: invitationsEnabled, inconsistentSettings: inconsistentSettings) - + state.settings = notificationSettings state.bindings.roomMentionsEnabled = notificationSettings.roomMentionsEnabled ?? false state.bindings.callsEnabled = notificationSettings.callsEnabled ?? false @@ -234,7 +234,7 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy } state.applyingChange = false } - + func enableCalls(_ enable: Bool) async { guard let notificationSettings = state.settings else { return } do { @@ -260,7 +260,7 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy } state.applyingChange = false } - + /// Imports the audio file at the given sandboxed URL into the tone library, refreshing the available custom tones on success. private func addCustomAlertTone(from urlResult: Result) async { do { @@ -290,13 +290,13 @@ class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelTy MXLog.error("Error setting selected alert tone to designated location in filesystem: \(error)") } } - + /// Deletes the given tones from the library. If the active tone is deleted, the selection resets to the default. private func deleteAlertTones(_ tones: [NotificationTone]) { for tone in tones { do { try notificationToneManager.deleteCustomTone(tone) - + if tone == state.selectedAlertTone { appSettings.selectedNotificationTone = nil } diff --git a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift index b53203aa1..11df431f6 100644 --- a/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift +++ b/ElementX/Sources/Screens/Settings/NotificationSettingsScreen/View/NotificationSettingsScreen.swift @@ -38,7 +38,7 @@ struct NotificationSettingsScreen: View { if context.viewState.settings?.invitationsEnabled != nil { additionalSettingsSection } - + soundSelectionSection } } @@ -126,7 +126,7 @@ struct NotificationSettingsScreen: View { .compoundListSectionHeader() } } - + private var mentionsSection: some View { Section { ListRow(label: .plain(title: L10n.screenNotificationSettingsRoomMentionLabel), @@ -171,7 +171,7 @@ struct NotificationSettingsScreen: View { .compoundListSectionHeader() } } - + @ViewBuilder private var soundSelectionSection: some View { Section { @@ -185,12 +185,12 @@ struct NotificationSettingsScreen: View { Text(L10n.screenNotificationSettingsSoundSectionTitle) .compoundListSectionHeader() } - + Section(isExpanded: $context.shouldShowAlertSounds) { presetSoundSelectionSection } header: { } } - + private var presetSoundSelectionSection: some View { ForEach(NotificationToneManager.allDefaultAlerts, id: \.filename) { alertTone in ListRow(label: .plain(title: alertTone.label), @@ -199,7 +199,7 @@ struct NotificationSettingsScreen: View { }) } } - + @ViewBuilder private var customSoundSelectionSection: some View { ForEach(context.viewState.availableCustomTones, id: \.filename) { alertTone in @@ -212,10 +212,10 @@ struct NotificationSettingsScreen: View { let tones = indices.map { context.viewState.availableCustomTones[$0] } - + context.send(viewAction: .deleteCustomAlertTones(tones)) } - + ListRow(label: .plain(title: L10n.screenNotificationSettingsSoundCustomSoundButtonTitle), kind: .button { context.shouldShowCustomAlertTonePicker = true @@ -231,7 +231,7 @@ struct NotificationSettingsScreen: View { context.send(viewAction: .addedCustomAlertTone($0)) } } - + private var configurationMismatchSection: some View { Section { ListRow(kind: .custom { @@ -278,9 +278,9 @@ struct NotificationSettingsScreen_Previews: PreviewProvider, TestablePreview { } notificationSettingsProxy.isRoomMentionEnabledReturnValue = true notificationSettingsProxy.isCallEnabledReturnValue = false - + let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "John Doe")))) - + var viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, userNotificationCenter: notificationCenter, notificationToneManager: NotificationToneManagerMock(.init()), @@ -310,7 +310,7 @@ struct NotificationSettingsScreen_Previews: PreviewProvider, TestablePreview { notificationSettingsProxy.isCallEnabledReturnValue = false let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "John Doe")))) - + var viewModel = NotificationSettingsScreenViewModel(appSettings: appSettings, userNotificationCenter: notificationCenter, notificationToneManager: NotificationToneManagerMock(.init()), @@ -320,7 +320,7 @@ struct NotificationSettingsScreen_Previews: PreviewProvider, TestablePreview { viewModel.fetchInitialContent() return viewModel }() - + static var previews: some View { NotificationSettingsScreen(context: viewModel.context) .snapshotPreferences(expect: viewModel.context.observe(\.viewState.settings).map { $0 != nil }) diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift index 5ec6ccb81..66a569d06 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/UserDetailsEditScreenModels.swift @@ -33,7 +33,7 @@ struct UserDetailsEditScreenViewState: BindableState { var nameDidChange: Bool { bindings.name != currentDisplayName } - + var avatarDidChange: Bool { localMedia != nil || selectedAvatarURL != currentAvatarURL } diff --git a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift index 3e09bfd06..6b8e6d1fb 100644 --- a/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift +++ b/ElementX/Sources/Screens/Settings/UserDetailsEditScreen/View/UserDetailsEditScreen.swift @@ -12,7 +12,7 @@ import SwiftUI struct UserDetailsEditScreen: View { @Bindable var context: UserDetailsEditScreenViewModel.Context @FocusState private var focus: Bool - + var body: some View { Form { Section { @@ -55,7 +55,7 @@ struct UserDetailsEditScreen: View { .disabled(!context.viewState.canSave) } } - + private var avatar: some View { Button { context.send(viewAction: .presentMediaSource) @@ -76,7 +76,7 @@ struct UserDetailsEditScreen: View { .frame(maxWidth: .infinity, alignment: .center) .listRowBackground(Color.clear) } - + private var nameSection: some View { Section { ListRow(label: .plain(title: L10n.screenEditProfileDisplayNamePlaceholder), diff --git a/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift b/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift index dcb1cfb9a..57f38e237 100644 --- a/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift +++ b/ElementX/Sources/Screens/Spaces/Common/SpaceRoomCell.swift @@ -28,7 +28,7 @@ struct SpaceRoomCell: View { private var isEditModeActive: Bool { editMode?.wrappedValue ?? .inactive != .inactive } - + private var isHighlighted: Bool { isSelected && !isEditModeActive } diff --git a/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift index 560447aee..381752d41 100644 --- a/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/LeaveSpace/LeaveSpaceViewModel.swift @@ -15,10 +15,10 @@ class LeaveSpaceViewModel: LeaveSpaceViewModelType { var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + private let userIndicatorController: UserIndicatorControllerProtocol private let mediaProvider: MediaProviderProtocol - + init(spaceName: String, canEditRolesAndPermissions: Bool, leaveHandle: LeaveSpaceHandleProxy, userIndicatorController: UserIndicatorControllerProtocol, mediaProvider: MediaProviderProtocol) { self.userIndicatorController = userIndicatorController self.mediaProvider = mediaProvider @@ -61,7 +61,7 @@ class LeaveSpaceViewModel: LeaveSpaceViewModelType { private static var leavingIndicatorID: String { "\(Self.self)-Leaving" } - + private static var failureIndicatorID: String { "\(Self.self)-Failure" } diff --git a/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenCoordinator.swift index 48b55b540..ec2adec5f 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenCoordinator.swift @@ -44,7 +44,7 @@ final class SpaceAddRoomsScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(SpaceAddRoomsScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenViewModel.swift index fb1537b47..1d03056ce 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceAddRoomsScreen/SpaceAddRoomsScreenViewModel.swift @@ -25,7 +25,7 @@ class SpaceAddRoomsScreenViewModel: SpaceAddRoomsScreenViewModelType, SpaceAddRo var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(spaceRoomListProxy: SpaceRoomListProxyProtocol, userSession: UserSessionProtocol, roomSummaryProvider: RoomSummaryProviderProtocol, @@ -167,7 +167,7 @@ class SpaceAddRoomsScreenViewModel: SpaceAddRoomsScreenViewModelType, SpaceAddRo private var savingIndicatorID: String { "\(Self.self)-Saving" } - + private var failureIndicatorID: String { "\(Self.self)-Failure" } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift index bb3f7dea1..13557069d 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenCoordinator.swift @@ -37,7 +37,7 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { private let viewModel: SpaceScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -87,7 +87,7 @@ final class SpaceScreenCoordinator: CoordinatorProtocol { func stop() { viewModel.stop() } - + func toPresentable() -> AnyView { AnyView(SpaceScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift index 60e7a9404..ecd97644c 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceScreen/SpaceScreenViewModel.swift @@ -22,7 +22,7 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(spaceRoomListProxy: SpaceRoomListProxyProtocol, spaceServiceProxy: SpaceServiceProxyProtocol, selectedSpaceRoomPublisher: CurrentValuePublisher, @@ -269,13 +269,13 @@ class SpaceScreenViewModel: SpaceScreenViewModelType, SpaceScreenViewModelProtoc state.bindings.leaveSpaceViewModel = leaveSpaceViewModel } - + // MARK: - Indicators private static var removingIndicatorID: String { "\(Self.self)-Removing" } - + private static var failureIndicatorID: String { "\(Self.self)-Failure" } diff --git a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift index 339ac6cad..0cf2dfb01 100644 --- a/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift +++ b/ElementX/Sources/Screens/Spaces/SpaceSettingsScreen/View/SpaceSettingsScreen.swift @@ -138,7 +138,7 @@ struct SpaceSettingsScreen_Previews: PreviewProvider, TestablePreview { attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), appSettings: .volatile()) } - + static var previews: some View { ElementNavigationStack { SpaceSettingsScreen(context: ownerViewModel.context) diff --git a/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenCoordinator.swift b/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenCoordinator.swift index fc2de168c..88abd654c 100644 --- a/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenCoordinator.swift +++ b/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenCoordinator.swift @@ -27,7 +27,7 @@ final class SpacesScreenCoordinator: CoordinatorProtocol { private let viewModel: SpacesScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -58,7 +58,7 @@ final class SpacesScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(SpacesScreen(context: viewModel.context)) } diff --git a/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenViewModel.swift b/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenViewModel.swift index cd68afd5e..63853465e 100644 --- a/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenViewModel.swift +++ b/ElementX/Sources/Screens/Spaces/SpacesScreen/SpacesScreenViewModel.swift @@ -20,7 +20,7 @@ class SpacesScreenViewModel: SpacesScreenViewModelType, SpacesScreenViewModelPro var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init(userSession: UserSessionProtocol, selectedSpacePublisher: CurrentValuePublisher, appSettings: AppSettings, diff --git a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift index b4d2afa6e..0607f6c33 100644 --- a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift +++ b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenCoordinator.swift @@ -28,7 +28,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { private let parameters: StartChatScreenCoordinatorParameters private var viewModel: StartChatScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -60,7 +60,7 @@ final class StartChatScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + // MARK: - Public func toPresentable() -> AnyView { diff --git a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenModels.swift b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenModels.swift index 93529d916..4819ea66b 100644 --- a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenModels.swift +++ b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenModels.swift @@ -24,7 +24,7 @@ struct StartChatScreenViewState: BindableState { let userID: String var bindings = StartChatScreenViewStateBindings() var usersSection: UserDiscoverySection = .init(type: .suggestions, users: []) - + var isSearching: Bool { !bindings.searchQuery.isEmpty } diff --git a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenViewModel.swift b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenViewModel.swift index 92714fdf6..42f698292 100644 --- a/ElementX/Sources/Screens/StartChatScreen/StartChatScreenViewModel.swift +++ b/ElementX/Sources/Screens/StartChatScreen/StartChatScreenViewModel.swift @@ -151,7 +151,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie internalRoomAddressState = .addressNotFound return } - + guard !Task.isCancelled else { return } @@ -185,7 +185,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie } } } - + private func createDirectRoom(user: UserProfileProxy) async { defer { hideLoadingIndicator() @@ -227,7 +227,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie state.joinByAddressState = internalRoomAddressState } } - + // MARK: Loading indicator private static let loadingIndicatorIdentifier = "\(StartChatScreenViewModel.self)-Loading" diff --git a/ElementX/Sources/Screens/StartChatScreen/View/JoinRoomByAddressView.swift b/ElementX/Sources/Screens/StartChatScreen/View/JoinRoomByAddressView.swift index 49450fdf9..84df6fec4 100644 --- a/ElementX/Sources/Screens/StartChatScreen/View/JoinRoomByAddressView.swift +++ b/ElementX/Sources/Screens/StartChatScreen/View/JoinRoomByAddressView.swift @@ -82,7 +82,7 @@ struct JoinRoomByAddressView_Previews: PreviewProvider, TestablePreview { let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com")))) let userDiscoveryService = UserDiscoveryServiceMock() userDiscoveryService.searchProfilesWithReturnValue = .success([.mockAlice]) - + return StartChatScreenViewModel(userSession: userSession, analytics: AnalyticsServiceMock(.init()), userIndicatorController: UserIndicatorControllerMock(), diff --git a/ElementX/Sources/Screens/StartChatScreen/View/StartChatScreen.swift b/ElementX/Sources/Screens/StartChatScreen/View/StartChatScreen.swift index 6d9f151aa..36b1de380 100644 --- a/ElementX/Sources/Screens/StartChatScreen/View/StartChatScreen.swift +++ b/ElementX/Sources/Screens/StartChatScreen/View/StartChatScreen.swift @@ -44,9 +44,9 @@ struct StartChatScreen: View { JoinRoomByAddressView(context: context) } } - + // MARK: - Private - + /// The content shown in the form when the search query is empty. @ViewBuilder private var mainContent: some View { diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift index ce5570e0d..2ac6b7420 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/ThreadTimelineScreenCoordinator.swift @@ -49,7 +49,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol { private var composerViewModel: ComposerToolbarViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -153,7 +153,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol { composerViewModel.actions .sink { [weak self] action in guard let self else { return } - + timelineViewModel.process(composerAction: action) } .store(in: &cancellables) @@ -167,7 +167,7 @@ final class ThreadTimelineScreenCoordinator: CoordinatorProtocol { composerViewModel.stop() viewModel.stop() } - + func toPresentable() -> AnyView { let composerToolbar = ComposerToolbar(context: composerViewModel.context) diff --git a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift index 09fcb0e98..5421f66ce 100644 --- a/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift +++ b/ElementX/Sources/Screens/ThreadTimelineScreen/View/ThreadTimelineScreen.swift @@ -21,7 +21,7 @@ struct ThreadTimelineScreen: View { self.timelineContext = timelineContext self.composerToolbar = composerToolbar } - + var body: some View { TimelineView(timelineContext: timelineContext) .background(.compound.bgCanvasDefault) diff --git a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift index 705f9d9a2..dc8daab29 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift @@ -98,12 +98,12 @@ class TimelineInteractionHandler { // Don't show a menu for non-event based items. return } - + actionsSubject.send(.composer(action: .removeFocus)) actionsSubject.send(.showActionMenu(.init(item: eventTimelineItem))) } } - + // swiftlint:disable:next cyclomatic_complexity func handleTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) { guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), @@ -250,7 +250,7 @@ class TimelineInteractionHandler { } // MARK: Polls - + func sendPollResponse(pollStartID: String, optionID: String) { Task { let sendPollResponseResult = await pollInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID) @@ -369,7 +369,7 @@ class TimelineInteractionHandler { isReply: false, messageType: .VoiceMessage, startsThread: nil) - + actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState, waveform: .url(recordingURL), isUploading: true)))) await voiceMessageRecorder.stopPlayback() @@ -417,13 +417,13 @@ class TimelineInteractionHandler { } // MARK: Audio Playback - + func changePlaybackSpeed(for itemID: TimelineItemIdentifier) { let nextSpeed = appSettings.voiceMessagePlaybackSpeed.next appSettings.voiceMessagePlaybackSpeed = nextSpeed audioPlayerState(for: itemID)?.setPlaybackSpeed(nextSpeed) } - + func playPauseAudio(for itemID: TimelineItemIdentifier) async { MXLog.info("Toggle play/pause audio for itemID \(itemID)") guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else { @@ -440,12 +440,12 @@ class TimelineInteractionHandler { } let audioPlayer = mediaPlayerProvider.player - + // Stop any recording in progress if voiceMessageRecorder.isRecording { await voiceMessageRecorder.stopRecording() } - + guard let audioPlayerState = audioPlayerState(for: itemID) else { fatalError("Audio player state not found for \(itemID)") } @@ -454,16 +454,16 @@ class TimelineInteractionHandler { if !audioPlayerState.isAttached { audioPlayerState.attachAudioPlayer(audioPlayer) } - + // Detach all other states await mediaPlayerProvider.detachAllStates(except: audioPlayerState) - + guard audioPlayer.sourceURL == source.url, audioPlayer.state != .error else { // Load content do { MXLog.info("Loading voice message audio content from source for itemID \(itemID)") let url = try await userSession.voiceMessageMediaManager.loadVoiceMessageFromSource(source, body: nil) - + // Make sure that the player is still attached, as it may have been detached while waiting for the voice message to be loaded. if audioPlayerState.isAttached { audioPlayer.load(sourceURL: source.url, playbackURL: url, autoplay: true) @@ -482,7 +482,7 @@ class TimelineInteractionHandler { audioPlayer.play() } } - + func seekAudio(for itemID: TimelineItemIdentifier, progress: Double) async { guard let playerState = mediaPlayerProvider.playerState(for: .timelineItemIdentifier(itemID)) else { return diff --git a/ElementX/Sources/Screens/Timeline/TimelineModels.swift b/ElementX/Sources/Screens/Timeline/TimelineModels.swift index acb332ef0..2670ef668 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineModels.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineModels.swift @@ -108,7 +108,7 @@ struct TimelineViewState: BindableState { var showReadReceipts = false var isDM = false var timelineState: TimelineState // check the doc before changing this - + var ownUserID: String var canCurrentUserSendMessage = false var canCurrentUserRedactOthers = false @@ -125,7 +125,7 @@ struct TimelineViewState: BindableState { var jumpToReadMarkerEnabled: Bool let hasPredecessor: Bool - + /// The `pinnedEventIDs` are used only to determine if an item is already pinned or not. /// It's updated from the room info, so it's faster than using the timeline var pinnedEventIDs: Set = [] @@ -147,24 +147,24 @@ struct TimelineViewState: BindableState { var linkMetadataProvider: LinkMetadataProviderProtocol? var mapTilerConfiguration: MapTilerConfiguration - + var stoppedLiveLocationIDs: Set = [] - + var bindings: TimelineViewStateBindings } struct TimelineViewStateBindings { var isScrolledToBottom = true - + /// Whether the read marker (NEW banner) is currently visible in the timeline viewport. /// Used to hide the jump-to-unread button once the user has scrolled to the read marker. var isReadMarkerVisible = false - + /// Whether new messages have arrived while the user is scrolled away from the bottom of a /// live timeline. Drives the presence dot on the scroll-to-bottom button and resets when the /// user returns to the bottom. var hasNewMessagesAtBottom = false - + /// The timestamp of the topmost visible item, used to drive the floating date badge while scrolling. var floatingDate: Date? @@ -183,7 +183,7 @@ struct TimelineViewStateBindings { var readReceiptsSummaryInfo: ReadReceiptSummaryInfo? var manageMemberViewModel: ManageRoomMemberSheetViewModel? - + var showTranslation = false var textToBeTranslated: String? } @@ -248,7 +248,7 @@ struct TimelineState { /// The event has already been shown. case hasAppeared } - + /// The ID of the event. let eventID: String /// How the event should be shown, or whether it has already appeared. @@ -262,26 +262,26 @@ struct TimelineState { var scrollToBottomPublisher = PassthroughSubject() var scrollToFirstItemForDatePublisher = PassthroughSubject() var scrollToReadMarkerPublisher = PassthroughSubject() - + var itemsDictionary = OrderedDictionary() - + var uniqueIDs: [TimelineItemIdentifier.UniqueID] { itemsDictionary.keys.elements } - + var itemViewStates: [RoomTimelineItemViewState] { itemsDictionary.values.elements } - + /// The unique ID of the read marker (NEW banner) in the timeline, if present. /// Recomputed by ``recomputeReadMarkerUniqueID()`` whenever ``itemsDictionary`` changes. private(set) var readMarkerUniqueID: TimelineItemIdentifier.UniqueID? - + /// The user's `m.fully_read` event ID, pushed from `RoomInfo`. Used as a fallback /// signal when ``readMarkerUniqueID`` is nil because the marker event isn't paginated /// into the loaded timeline window. var fullyReadEventID: String? - + /// Recomputes ``readMarkerUniqueID`` from ``itemsDictionary``. Call after assigning a new /// value to ``itemsDictionary``. mutating func recomputeReadMarkerUniqueID() { @@ -290,7 +290,7 @@ struct TimelineState { return false }?.key } - + func hasLoadedItem(with eventID: String) -> Bool { itemViewStates.contains { $0.identifier.eventID == eventID } } @@ -306,13 +306,13 @@ extension TimelineViewState { var isAtBottomAndLive: Bool { bindings.isScrolledToBottom && timelineState.isLive } - + /// Whether the scroll-to-bottom button should be shown: the user is scrolled away from the /// bottom of a live timeline. var shouldShowScrollToBottomButton: Bool { !isAtBottomAndLive } - + /// Whether the jump-to-read-marker button should be shown. /// /// Primary path: the SDK has materialised a virtual `ReadMarker` item in the @@ -334,7 +334,7 @@ extension TimelineViewState { return !timelineState.itemsDictionary.isEmpty && !timelineState.hasLoadedItem(with: fullyReadEventID) } - + /// The string shown as the message preview. /// /// This converts the formatted body to a plain string to remove formatting diff --git a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift index dc891ac6b..cbe3ee14c 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineTableViewController.swift @@ -148,14 +148,14 @@ class TimelineTableViewController: UIViewController { @Binding private var isReadMarkerVisible: Bool @Binding private var hasNewMessagesAtBottom: Bool @Binding private var floatingDate: Date? - + /// The unique ID of the read marker (NEW banner) currently in the timeline, if any. /// Updated by `TimelineViewRepresentable.updateUIViewController` whenever it changes. var readMarkerUniqueID: TimelineItemIdentifier.UniqueID? - + /// A work item used to auto-hide the floating date badge after scrolling stops. private var floatingDateHideWorkItem: DispatchWorkItem? - + private var timelineItemsIDs: [TimelineItemIdentifier.UniqueID] { timelineItemsDictionary.keys.elements.reversed() } @@ -163,7 +163,7 @@ class TimelineTableViewController: UIViewController { /// The table's diffable data source. private var dataSource: UITableViewDiffableDataSource? private var cancellables = Set() - + /// A publisher used to throttle back pagination requests. /// /// Our view actions get wrapped in a `Task` so it is possible that a second call in @@ -196,7 +196,7 @@ class TimelineTableViewController: UIViewController { _isReadMarkerVisible = isReadMarkerVisible _hasNewMessagesAtBottom = hasNewMessagesAtBottom _floatingDate = floatingDate - + super.init(nibName: nil, bundle: nil) tableView.register(TimelineItemCell.self, forCellReuseIdentifier: TimelineItemCell.reuseIdentifier) @@ -226,7 +226,7 @@ class TimelineTableViewController: UIViewController { self?.scrollToFirstItemForCurrentDate() } .store(in: &cancellables) - + scrollToReadMarkerPublisher .sink { [weak self] uniqueID in self?.scrollToItem(uniqueID: uniqueID, animated: true) @@ -362,7 +362,7 @@ class TimelineTableViewController: UIViewController { /// the scroll position will be updated to maintain the position of the last visible item. private func applySnapshot() { guard let dataSource else { return } - + var snapshot = NSDiffableDataSourceSnapshot() // We don't want to display the typing notification in this timeline @@ -400,13 +400,13 @@ class TimelineTableViewController: UIViewController { if isSwitchingTimelines { coordinator.send(viewAction: .hasSwitchedTimeline) } - + // Re-evaluate after the snapshot has been applied so the new layout is reflected. DispatchQueue.main.async { [weak self] in self?.updateReadMarkerVisibility() } } - + /// Scrolls to the newest item in the timeline. private func scrollToNewestItem(animated: Bool) { guard !timelineItemsIDs.isEmpty else { @@ -415,7 +415,7 @@ class TimelineTableViewController: UIViewController { tableView.scrollToRow(at: IndexPath(item: 0, section: 0), at: .top, animated: animated) scrollDirectionPublisher.send(.bottom) } - + /// Scrolls to the oldest item in the timeline. private func scrollToOldestItem(animated: Bool) { guard !timelineItemsIDs.isEmpty else { @@ -434,7 +434,7 @@ class TimelineTableViewController: UIViewController { tableView.scrollToRow(at: indexPath, at: .middle, animated: animated) } } - + /// Scrolls to the item with the corresponding event ID if loaded in the timeline. private func scrollToItem(eventID: String, animated: Bool) { DispatchQueue.main.async { [weak self] in // Fixes #2805 @@ -488,12 +488,12 @@ class TimelineTableViewController: UIViewController { } return readMarkerIndexPath <= maxVisibleIndexPath }() - + if isReadMarkerVisible != isVisible { isReadMarkerVisible = isVisible } } - + private func sendLastVisibleItemReadReceipt() { // Find the last visible timeline item and send a read receipt for it guard let visibleIndexPaths = tableView.indexPathsForVisibleRows else { @@ -520,9 +520,9 @@ extension TimelineTableViewController: UITableViewDelegate { // Dispatch to fix runtime warning about making changes during a view update. DispatchQueue.main.async { [weak self] in guard let self else { return } - + let isScrolledToBottom = scrollView.contentOffset.y <= 0 - + // Only update the binding on changes to avoid needlessly recomputing the hierarchy when scrolling. if self.isScrolledToBottom != isScrolledToBottom { self.isScrolledToBottom = isScrolledToBottom @@ -530,14 +530,14 @@ extension TimelineTableViewController: UITableViewDelegate { self.hasNewMessagesAtBottom = false } } - + if !isScrolledToBottom { updateFloatingDate() } - + updateReadMarkerVisibility() } - + // We never want the table view to be fully at the bottom to allow the status bar tap to work properly if scrollView.contentOffset.y == 0 { scrollView.contentOffset.y = -1 diff --git a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift index 8de3011e3..29757f654 100644 --- a/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift +++ b/ElementX/Sources/Screens/Timeline/TimelineViewModel.swift @@ -21,7 +21,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { static let focusTimelineToastIndicatorID = "RoomScreenFocusTimelineToastIndicator" static let toastErrorID = "RoomScreenToastError" } - + private let roomProxy: JoinedRoomProxyProtocol private let timelineController: TimelineControllerProtocol private let userSession: UserSessionProtocol @@ -46,7 +46,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { private var paginateBackwardsTask: Task? private var paginateForwardsTask: Task? - + init(roomProxy: JoinedRoomProxyProtocol, focussedEventID: String? = nil, timelineController: TimelineControllerProtocol, @@ -144,7 +144,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { updateRoomInfo(roomProxy.infoPublisher.value) updateMembers(roomProxy.membersPublisher.value) - + // Note: beware if we get to e.g. restore a reply / edit, // maybe we are tracking a non-needed first initial state trackComposerMode(.default) @@ -233,7 +233,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { actionsSubject.send(.displayRoom(roomID: predecessorID, via: Array(serverNames))) } } - + func process(composerAction: ComposerToolbarViewModelAction) { switch composerAction { case .sendMessage(let message, let html, let mode, let intentionalMentions): @@ -451,7 +451,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { .receive(on: DispatchQueue.main) .sink { [weak self] callback in guard let self else { return } - + switch callback { case .updatedTimelineItems(let updatedItems, let isSwitchingTimelines): buildTimelineViews(timelineItems: updatedItems, isSwitchingTimelines: isSwitchingTimelines) @@ -477,7 +477,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } } .store(in: &cancellables) - + roomProxy.infoPublisher .receive(on: DispatchQueue.main) .sink { [weak self] roomInfo in @@ -540,7 +540,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } .store(in: &cancellables) } - + func viewInRoomTimeline(eventID: String) async { switch await roomProxy.loadOrFetchEventDetails(for: eventID) { case .success(let event): @@ -567,7 +567,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { appSettings.$threadsEnabled .weakAssign(to: \.state.areThreadsEnabled, on: self) .store(in: &cancellables) - + appSettings.$jumpToReadMarkerEnabled .weakAssign(to: \.state.jumpToReadMarkerEnabled, on: self) .store(in: &cancellables) @@ -592,39 +592,39 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { .weakAssign(to: \.state.hideTimelineMedia, on: self) .store(in: &cancellables) } - + private func setupDirectRoomSubscriptionsIfNeeded() { guard roomProxy.infoPublisher.value.isDirect else { return } - + let shouldShowInviteAlert = composerFocusedSubject .removeDuplicates() .map { [weak self] isFocused in guard let self else { return false } - + return isFocused && self.roomProxy.infoPublisher.value.isUserAloneInDirectRoom } // We want to show the alert just once, so we are taking the first "true" emitted .first { $0 } - + shouldShowInviteAlert .sink { [weak self] _ in self?.displayAlert(.inviteAgain) } .store(in: &cancellables) } - + private func paginateBackwards() { guard paginateBackwardsTask == nil else { return } - + paginateBackwardsTask = Task { [weak self] in guard let self else { return } - + switch await timelineController.paginateBackwards(requestSize: Constants.paginationEventLimit) { case .failure: displayErrorToast(L10n.errorFailedLoadingMessages) @@ -639,12 +639,12 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { guard paginateForwardsTask == nil else { return } - + paginateForwardsTask = Task { [weak self] in guard let self else { return } - + switch await timelineController.paginateForwards(requestSize: Constants.paginationEventLimit) { case .failure: displayErrorToast(L10n.errorFailedLoadingMessages) @@ -667,20 +667,20 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { focusLive() } } - + private func scrollToReadMarker() async { // Primary: SDK has materialised the virtual ReadMarker. Smooth in-window scroll. if let readMarkerID = state.timelineState.readMarkerUniqueID { state.timelineState.scrollToReadMarkerPublisher.send(readMarkerID) return } - + // Fallback: mirror the same-room permalink flow exactly // (RoomFlowCoordinator.handleChildEventRoute lines 223-258): pre-fetch the // event first to prime the SDK's event cache, then focus on it via the // same focusOnEvent path permalinks use. guard let fullyReadEventID = state.timelineState.fullyReadEventID else { return } - + switch await roomProxy.loadOrFetchEventDetails(for: fullyReadEventID) { case .success: await focusOnEvent(eventID: fullyReadEventID) @@ -688,13 +688,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { displayErrorToast(L10n.errorMessageNotFound) } } - + private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async { guard appMediator.appState == .active else { return } - + await timelineController.sendReadReceipt(for: lastVisibleItemID) } - + private func handleMediaTapped(with itemID: TimelineItemIdentifier) async { state.showLoading = true let action = await timelineInteractionHandler.processItemTap(itemID) @@ -749,7 +749,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } return nil } - + private func handleJoinCommand(message: String) async { guard let alias = String(message.dropFirst(SlashCommand.join.rawValue.count)) .components(separatedBy: .whitespacesAndNewlines) @@ -799,7 +799,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { scrollToBottom() } - + private func trackComposerMode(_ mode: ComposerMode) { var isEdit = false var isReply = false @@ -846,7 +846,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { mediaPlayerProvider.unregister(audioPlayerState: playerState) } } - + let itemsGroupedByTimelineDisplayStyle = timelineItems.chunked { current, next in canGroupItem(timelineItem: current, with: next) } @@ -881,13 +881,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { if isSwitchingTimelines { state.timelineState.isSwitchingTimelines = true } - + updateHasNewMessagesAtBottom(with: timelineItemsDictionary) - + state.timelineState.itemsDictionary = timelineItemsDictionary state.timelineState.recomputeReadMarkerUniqueID() } - + private func updateViewState(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewState { if let timelineItemViewState = state.timelineState.itemsDictionary[item.id.uniqueID] { timelineItemViewState.groupStyle = groupStyle @@ -897,7 +897,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { return RoomTimelineItemViewState(item: item, groupStyle: groupStyle) } } - + private func canGroupItem(timelineItem: RoomTimelineItemProtocol, with otherTimelineItem: RoomTimelineItemProtocol) -> Bool { if timelineItem is CollapsibleTimelineItem || otherTimelineItem is CollapsibleTimelineItem { return false @@ -917,7 +917,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { && eventTimelineItem.properties.reactions.isEmpty // Reactions break the grouping. && otherEventTimelineItem.timestamp.timeIntervalSince(eventTimelineItem.timestamp) < 5 * 60 // As does the passage of time. } - + /// Sets `hasNewMessagesAtBottom` to `true` when newer items arrive while the user is scrolled /// up in a live timeline. Skips initial load and timeline switches. private func updateHasNewMessagesAtBottom(with newTimelineItems: OrderedDictionary) { @@ -928,33 +928,33 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { !state.bindings.hasNewMessagesAtBottom else { return } - + let oldDictionary = state.timelineState.itemsDictionary guard !oldDictionary.isEmpty, !newTimelineItems.isEmpty, oldDictionary.keys.last != newTimelineItems.keys.last else { return } - + state.bindings.hasNewMessagesAtBottom = true } - + // MARK: - Direct chats logics - + private let inviteLoadingIndicatorID = UUID().uuidString - + private func inviteOtherDMUserBack() { guard roomProxy.infoPublisher.value.isUserAloneInDirectRoom else { displayAlert(.unknown) return } - + Task { userIndicatorController.submitIndicator(.init(id: inviteLoadingIndicatorID, type: .toast, title: L10n.commonLoading)) defer { userIndicatorController.retractIndicatorWithId(inviteLoadingIndicatorID) } - + guard let members = await roomProxy.members(), members.count == 2, @@ -963,7 +963,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { displayAlert(.unknown) return } - + switch await roomProxy.invite(userID: otherPerson.userID) { case .success: break @@ -974,7 +974,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } // MARK: - Reactions - + private func displayReactionSummary(for itemID: TimelineItemIdentifier, selectedKey: String) { guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { @@ -985,7 +985,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { } // MARK: - Read Receipts - + private func displayReadReceipts(for itemID: TimelineItemIdentifier) { guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID), let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else { @@ -994,7 +994,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol { state.bindings.readReceiptsSummaryInfo = .init(orderedReceipts: eventTimelineItem.properties.orderedReadReceipts, id: eventTimelineItem.id) } - + // MARK: - Message forwarding private func forwardMessage(itemID: TimelineItemIdentifier) async { @@ -1120,9 +1120,9 @@ extension TimelineViewModel { clientProxyMock.roomSummaryForAliasReturnValue = .mock(id: "!room:matrix.org", name: "Room") clientProxyMock.roomSummaryForIdentifierReturnValue = .mock(id: "!room:matrix.org", name: "Room", canonicalAlias: "#room:matrix.org") let roomProxy = JoinedRoomProxyMock(.init(name: "Preview room", predecessor: hasPredecessor ? .init(roomId: UUID().uuidString) : nil)) - + let appSettings = AppSettings.volatile() - + return TimelineViewModel(roomProxy: roomProxy, focussedEventID: nil, timelineController: timelineController ?? MockTimelineController(timelineKind: timelineKind), diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift index ee1440d6a..30f826d92 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenu.swift @@ -36,14 +36,14 @@ struct TimelineItemMenu: View { if !actions.reactions.isEmpty { reactionsSection .padding(.bottom, 8.0) - + Divider() .background(Color.compound.bgSubtlePrimary) } - + if !actions.actions.isEmpty { viewsForActions(actions.actions) - + Divider() .background(Color.compound.bgSubtlePrimary) } @@ -278,7 +278,7 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview { static let (mediaItem, mediaItemActions) = makeActions(itemType: .outgoingMedia) static let (mediaItemWithCaption, mediaItemWithCaptionActions) = makeActions(itemType: .outgoingMediaWithCaption) - + static var previews: some View { TimelineItemMenu(item: item, actions: actions) .environmentObject(viewModel.context) diff --git a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift index 9f6cfec6b..c53bab449 100644 --- a/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift +++ b/ElementX/Sources/Screens/Timeline/View/ItemMenu/TimelineItemMenuActionProvider.swift @@ -28,12 +28,12 @@ struct TimelineItemMenuActionProvider { // Don't show a context menu for non-event based items. return nil } - + if timelineItem is StateRoomTimelineItem { // Don't show a context menu for state events. return nil } - + if let encryptedItem = timelineItem as? EncryptedRoomTimelineItem { return makeEncryptedItemActions(encryptedItem) } @@ -48,7 +48,7 @@ struct TimelineItemMenuActionProvider { if canRedactItem(item), let poll = item.pollIfAvailable, !poll.hasEnded, let eventID = item.id.eventID { actions.append(.endPoll(pollStartID: eventID)) } - + if item.canBeRepliedTo, canCurrentUserSendMessage { if let messageItem = item as? EventBasedMessageTimelineItemProtocol { // If threads are enabled we will have the dedicated `replyInThread` action @@ -88,7 +88,7 @@ struct TimelineItemMenuActionProvider { if canCurrentUserPin, let eventID = item.id.eventID { actions.append(pinnedEventIDs.contains(eventID) ? .unpin : .pin) } - + if item.isCopyable { actions.append(.copy) @@ -141,17 +141,17 @@ struct TimelineItemMenuActionProvider { } let isReactable = timelineKind == .live || timelineKind == .detached || timelineKind.isThread ? item.isReactable : false - + return .init(isReactable: isReactable, actions: actions, secondaryActions: secondaryActions, emojiProvider: emojiProvider) } private func makeEncryptedItemActions(_ encryptedItem: EncryptedRoomTimelineItem) -> TimelineItemMenuActions? { var actions: [TimelineItemMenuAction] = [.copyPermalink] - + if isViewSourceEnabled { actions.append(.viewSource) } - + return .init(isReactable: false, actions: actions, secondaryActions: [], diff --git a/ElementX/Sources/Screens/Timeline/View/Polls/PollOptionView.swift b/ElementX/Sources/Screens/Timeline/View/Polls/PollOptionView.swift index a3b7e2453..1a47b2f4e 100644 --- a/ElementX/Sources/Screens/Timeline/View/Polls/PollOptionView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Polls/PollOptionView.swift @@ -15,14 +15,14 @@ struct PollOptionView: View { let pollOption: Poll.Option let showVotes: Bool let isFinalResult: Bool - + var body: some View { HStack(alignment: .firstTextBaseline, spacing: 12) { Image(systemSymbol: pollOption.isSelected ? .checkmarkCircleFill : .circle) .font(.compound.bodyLG) .foregroundColor(pollOption.isSelected && isEnabled ? .compound.iconPrimary : .compound.iconTertiary) .accessibilityAddTraits(pollOption.isSelected ? .isSelected : []) - + VStack(spacing: 10) { HStack(alignment: .lastTextBaseline, spacing: 8) { Text(pollOption.text) @@ -30,7 +30,7 @@ struct PollOptionView: View { .multilineTextAlignment(.leading) .foregroundColor(.compound.textPrimary) .frame(maxWidth: .infinity, alignment: .leading) - + if showVotes { if isFinalWinningOption { HStack(spacing: 4) { @@ -49,14 +49,14 @@ struct PollOptionView: View { } } } - + PollProgressView(progress: progress) } } } - + // MARK: - Private - + private var progress: Double { switch (showVotes, pollOption.allVotes, pollOption.isSelected) { case (true, let allVotes, _) where allVotes > 0: @@ -67,7 +67,7 @@ struct PollOptionView: View { return 0 } } - + private var isFinalWinningOption: Bool { pollOption.isWinning && isFinalResult } @@ -75,13 +75,13 @@ struct PollOptionView: View { private struct PollProgressView: View { let progress: Double - + var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { Capsule() .foregroundColor(.compound._bgEmptyItemAlpha) - + Capsule() .frame(maxWidth: progress * geometry.size.width) } @@ -102,7 +102,7 @@ struct PollOptionView_Previews: PreviewProvider, TestablePreview { isWinning: false), showVotes: false, isFinalResult: false) - + PollOptionView(pollOption: .init(id: "2", text: "Chinese 🇨🇳", votes: 9, @@ -111,7 +111,7 @@ struct PollOptionView_Previews: PreviewProvider, TestablePreview { isWinning: true), showVotes: true, isFinalResult: false) - + PollOptionView(pollOption: .init(id: "2", text: "Chinese 🇨🇳", votes: 9, diff --git a/ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift b/ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift index 98c766b7e..cad35b27e 100644 --- a/ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Polls/PollView.swift @@ -59,7 +59,7 @@ struct PollView: View { .frame(maxWidth: 450) } } - + // MARK: - Private private var senderString: String { @@ -76,14 +76,14 @@ struct PollView: View { .accessibilityElement(children: .combine) } } - + private var questionView: some View { HStack(alignment: .top, spacing: 12) { CompoundIcon(poll.hasEnded ? \.pollsEnd : \.polls, size: .custom(22), relativeTo: .compound.bodyLGSemibold) .accessibilityLabel(poll.hasEnded ? L10n.a11yPollEnd : L10n.a11yPoll) - + Text(poll.question) .multilineTextAlignment(.leading) .font(.compound.bodyLGSemibold) @@ -91,7 +91,7 @@ struct PollView: View { .accessibilityElement(children: .combine) .accessibilityAddTraits(.isHeader) } - + private var optionsView: some View { ForEach(poll.options, id: \.id) { option in pollOption(option: option) @@ -116,7 +116,7 @@ struct PollView: View { } .disabled(poll.hasEnded) } - + @ViewBuilder private var summaryView: some View { if let summaryText = poll.summaryText { @@ -127,7 +127,7 @@ struct PollView: View { .frame(maxWidth: .infinity, alignment: showVotes ? .trailing : .leading) } } - + @ViewBuilder private var toolbarView: some View { if !poll.hasEnded, poll.createdByAccountOwner { @@ -157,7 +157,7 @@ struct PollView: View { actionHandler(.end) } } - + private func progressBarColor(for option: Poll.Option) -> Color { if poll.hasEnded { return option.isWinning ? .compound.textActionAccent : .compound.textDisabled @@ -165,7 +165,7 @@ struct PollView: View { return .compound.textPrimary } } - + private var showVotes: Bool { poll.hasEnded || poll.kind == .disclosed } @@ -178,7 +178,7 @@ private extension Poll { L10n.commonPollTotalVotes($0.allVotes) } } - + switch kind { case .disclosed: return options.first.map { @@ -195,19 +195,19 @@ struct PollView_Previews: PreviewProvider, TestablePreview { PollView(poll: .disclosed(), state: .full(isEditable: false), sender: .test) { _ in } .padding() .previewDisplayName("Disclosed") - + PollView(poll: .undisclosed(), state: .full(isEditable: false), sender: .test) { _ in } .padding() .previewDisplayName("Undisclosed") - + PollView(poll: .endedDisclosed, state: .full(isEditable: false), sender: .test) { _ in } .padding() .previewDisplayName("Ended, Disclosed") - + PollView(poll: .endedUndisclosed, state: .full(isEditable: false), sender: .test) { _ in } .padding() .previewDisplayName("Ended, Undisclosed") - + PollView(poll: .disclosed(createdByAccountOwner: true), state: .full(isEditable: true), sender: .test) { _ in } .padding() .previewDisplayName("Creator, disclosed") diff --git a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift index 1d614282e..970aa9116 100644 --- a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift +++ b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptCell.swift @@ -23,7 +23,7 @@ struct ReadReceiptCell: View { } return readReceipt.userID } - + var body: some View { HStack(spacing: 12) { LoadableAvatarImage(url: memberState?.avatarURL, diff --git a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift index 63b9964fb..4a27d2ced 100644 --- a/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift +++ b/ElementX/Sources/Screens/Timeline/View/ReadReceipts/ReadReceiptsSummaryView.swift @@ -44,9 +44,9 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview { .mockDan ] let roomProxyMock = JoinedRoomProxyMock(.init(name: "Room", members: members)) - + let appSettings = AppSettings.volatile() - + return TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockTimelineController(), userSession: UserSessionMock(.init()), diff --git a/ElementX/Sources/Screens/Timeline/View/Style/LongPressWithFeedback.swift b/ElementX/Sources/Screens/Timeline/View/Style/LongPressWithFeedback.swift index a8b3b29cd..2c421e8b2 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/LongPressWithFeedback.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/LongPressWithFeedback.swift @@ -144,7 +144,7 @@ private struct LongPressGestureRepresentable: UIGestureRecognizerRepresentable { func handleUIGestureRecognizerAction(_ recognizer: UILongPressGestureRecognizer, context: Context) { handle(recognizer) } - + class Coordinator: NSObject, UIGestureRecognizerDelegate { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineBubbleLayout.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineBubbleLayout.swift index b63a24cb3..55f6900e4 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineBubbleLayout.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineBubbleLayout.swift @@ -69,7 +69,7 @@ struct TimelineBubbleLayout: Layout { // Calculate the natural size using the regular text and non-greedy quote bubbles. let layoutSubviews = subviews.filter { $0[Size.self].shouldLayout } - + let subviewSizes = layoutSubviews.map { size(for: $0, subviews: subviews, proposedSize: proposal, cache: &cache) } let maxWidth = subviewSizes.map(\.width).reduce(0, max) @@ -88,7 +88,7 @@ struct TimelineBubbleLayout: Layout { // Place the regular text and greedy components using the calculated width. let visibleSubviews = subviews.filter { $0[Size.self].shouldRender } - + let subviewSizes = visibleSubviews.map { size(for: $0, subviews: subviews, proposedSize: ProposedViewSize(width: maxWidth, height: proposal.height), cache: &cache) } var y = bounds.minY diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift index a3f0830b9..cc8c8c455 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineItemBubbledStylerView.swift @@ -17,15 +17,15 @@ struct TimelineItemBubbledStylerView: View { let timelineItem: EventBasedTimelineItemProtocol let adjustedDeliveryStatus: TimelineItemDeliveryStatus? @ViewBuilder let content: () -> Content - + private var isDM: Bool { context.viewState.isDM } - + private var isFocussed: Bool { focussedEventID != nil && timelineItem.id.eventID == focussedEventID } - + private var isPinned: Bool { guard context.viewState.timelineKind != .pinned, let eventID = timelineItem.id.eventID else { @@ -51,17 +51,17 @@ struct TimelineItemBubbledStylerView: View { header .zIndex(1) } - + VStack(alignment: alignment, spacing: 0) { HStack(spacing: 0) { if timelineItem.isOutgoing { Spacer() } - + messageBubbleWithReactions } .padding(timelineItem.isOutgoing ? .leading : .trailing, 48) // Additional padding to differentiate alignment. - + HStack(spacing: 0) { if !timelineItem.isOutgoing { Spacer() @@ -246,12 +246,12 @@ private extension EventBasedTimelineItemProtocol { return defaultColor } } - + /// The insets for the full bubble content. /// Padding affecting just the "send info" should be added inside `TimelineItemSendInfoView` var bubbleInsets: EdgeInsets { let defaultInsets: EdgeInsets = .init(around: 8) - + switch self { case is StickerRoomTimelineItem: return .zero @@ -286,7 +286,7 @@ private extension EdgeInsets { init(around: CGFloat) { self.init(top: around, leading: around, bottom: around, trailing: around) } - + static var zero: Self = .init(around: 0) } @@ -370,7 +370,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) }() - + static var previews: some View { mockTimeline .previewDisplayName("Mock Timeline") @@ -465,7 +465,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview .environmentObject(viewModelWithPins.context) .environment(\.timelineContext, viewModel.context) } - + static var pinned: some View { ScrollView { MockTimelineContent(isPinned: true) @@ -646,7 +646,7 @@ private struct MockTimelineContent: View { isThreaded: isThreaded, threadSummary: threadSummary)), groupStyle: .single)) - + AudioRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(), timestamp: .mock, isOutgoing: true, diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift index 2315409d5..53d008f65 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyle.swift @@ -14,7 +14,7 @@ enum TimelineGroupStyle: Hashable { case first case middle case last - + var shouldShowSenderDetails: Bool { switch self { case .single, .first: diff --git a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift index afb79af42..e2d16acee 100644 --- a/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift +++ b/ElementX/Sources/Screens/Timeline/View/Style/TimelineStyler.swift @@ -23,7 +23,7 @@ struct TimelineStyler: View { self.content = content _adjustedDeliveryStatus = State(initialValue: timelineItem.properties.deliveryStatus) } - + var body: some View { mainContent .onChange(of: timelineItem.properties.deliveryStatus) { _, newStatus in @@ -56,7 +56,7 @@ struct TimelineStyler: View { struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock - + static let base = TextRoomTimelineItem(id: .randomEvent, timestamp: .mock, isOutgoing: true, @@ -64,19 +64,19 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { canBeRepliedTo: true, sender: .test, content: .init(body: "Test")) - + static let sentNonLast: TextRoomTimelineItem = { var result = base result.properties.deliveryStatus = .sent return result }() - + static let sendingNonLast: TextRoomTimelineItem = { var result = base result.properties.deliveryStatus = .sending return result }() - + static let sendingLast: TextRoomTimelineItem = { let id = viewModel.state.timelineState.uniqueIDs.last ?? .init(UUID().uuidString) var result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventID(UUID().uuidString)), @@ -89,13 +89,13 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { result.properties.deliveryStatus = .sending return result }() - + static let failed: TextRoomTimelineItem = { var result = base result.properties.deliveryStatus = .sendingFailed(.unknown) return result }() - + static let sentLast: TextRoomTimelineItem = { let id = viewModel.state.timelineState.uniqueIDs.last ?? .init(UUID().uuidString) return TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventID(UUID().uuidString)), @@ -106,21 +106,21 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { sender: .test, content: .init(body: "Test")) }() - + static let ltrString = TextRoomTimelineItem(id: .randomEvent, timestamp: .mock, isOutgoing: true, isEditable: false, canBeRepliedTo: true, sender: .test, content: .init(body: "house!")) - + static let rtlString = TextRoomTimelineItem(id: .randomEvent, timestamp: .mock, isOutgoing: true, isEditable: false, canBeRepliedTo: true, sender: .test, content: .init(body: "באמת!")) - + static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .randomEvent, timestamp: .mock, isOutgoing: true, @@ -128,7 +128,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { canBeRepliedTo: true, sender: .test, content: .init(body: "house! -- באמת! -- house!")) - + static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .randomEvent, timestamp: .mock, isOutgoing: true, @@ -136,7 +136,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { canBeRepliedTo: true, sender: .test, content: .init(body: "באמת! -- house! -- באמת!")) - + static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .randomEvent, timestamp: .mock, isOutgoing: true, @@ -144,7 +144,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { canBeRepliedTo: true, sender: .test, content: .init(body: "house! -- באמת!")) - + static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .randomEvent, timestamp: .mock, isOutgoing: true, @@ -161,7 +161,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { shouldBoost: true, sender: .test, content: .init(body: "😮")) - + static let endingWithBlockquote: TextRoomTimelineItem = { let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder()) let attributedString = builder.fromHTML("

Some text before

A quoted line at the end
") @@ -173,7 +173,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { sender: .test, content: .init(body: "", formattedBody: attributedString)) }() - + static let endingWithCodeblock: TextRoomTimelineItem = { let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder()) let attributedString = builder.fromHTML("

Some text before

let x = 42
") @@ -185,7 +185,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { sender: .test, content: .init(body: "", formattedBody: attributedString)) }() - + static var testView: some View { VStack(spacing: 0) { TextRoomTimelineView(timelineItem: base) @@ -198,7 +198,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { TextRoomTimelineView(timelineItem: endingWithCodeblock) } } - + static var languagesTestView: some View { VStack(spacing: 0) { TextRoomTimelineView(timelineItem: ltrString) @@ -210,7 +210,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { TextRoomTimelineView(timelineItem: bigEmoji) } } - + static var previews: some View { testView .environmentObject(viewModel.context) @@ -219,7 +219,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview { languagesTestView .environmentObject(viewModel.context) .previewDisplayName("Bubbles LTR with different layout languages") - + languagesTestView .environmentObject(viewModel.context) .environment(\.layoutDirection, .rightToLeft) diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineDeliveryStatusView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineDeliveryStatusView.swift index 6a93bf2ea..b97d94ae5 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineDeliveryStatusView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineDeliveryStatusView.swift @@ -14,9 +14,9 @@ struct TimelineDeliveryStatusView: View { case sending case sent } - + let deliveryStatus: Status - + private var icon: CompoundIcon { switch deliveryStatus { case .sending: diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift index 4529b30dd..3c6ca945c 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineItemStatusView.swift @@ -13,15 +13,15 @@ struct TimelineItemStatusView: View { let timelineItem: EventBasedTimelineItemProtocol let adjustedDeliveryStatus: TimelineItemDeliveryStatus? @EnvironmentObject private var context: TimelineViewModel.Context - + private var isLastOutgoingMessage: Bool { timelineItem.isOutgoing && context.viewState.timelineState.uniqueIDs.last == timelineItem.id.uniqueID } - + var body: some View { mainContent } - + @ViewBuilder private var mainContent: some View { if context.viewState.timelineKind == .pinned { @@ -33,7 +33,7 @@ struct TimelineItemStatusView: View { deliveryStatusBadge } } - + @ViewBuilder var deliveryStatusBadge: some View { switch adjustedDeliveryStatus { @@ -49,7 +49,7 @@ struct TimelineItemStatusView: View { EmptyView() } } - + var readReceipts: some View { TimelineReadReceiptsView(timelineItem: timelineItem) .environmentObject(context) diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift index f8baf5de9..0a7ca42b0 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReactionsView.swift @@ -13,7 +13,7 @@ import SwiftUI struct TimelineReactionsView: View { private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) @Environment(\.layoutDirection) private var layoutDirection: LayoutDirection - + let context: TimelineViewModel.Context let itemID: TimelineItemIdentifier let reactions: [AggregatedReaction] diff --git a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift index 3a8dc4827..6b53e1a10 100644 --- a/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift +++ b/ElementX/Sources/Screens/Timeline/View/Supplementary/TimelineReadReceiptsView.swift @@ -20,7 +20,7 @@ struct TimelineReadReceiptsView: View { contentID: receipt.userID) } } - + var body: some View { HStack(spacing: 2) { StackedAvatarsView(overlap: 6, @@ -78,10 +78,10 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { .mockDan, .mockMe ] - + static let viewModel = { let appSettings = AppSettings.volatile() - + return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Test", members: members)), timelineController: MockTimelineController(), userSession: UserSessionMock(.init()), @@ -94,7 +94,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) }() - + static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")] static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"), ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before")] @@ -105,7 +105,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before"), ReadReceipt(userID: RoomMemberProxyMock.mockCharlie.userID, formattedTimestamp: "Way before"), ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")] - + static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem { TextRoomTimelineItem(id: .randomEvent, timestamp: .mock, @@ -115,7 +115,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview { sender: .init(id: UUID().uuidString), content: .init(body: "Test"), properties: .init(orderedReadReceipts: receipts)) } - + static var previews: some View { VStack(spacing: 8) { TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: singleReceipt)) diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift index f2b478522..eb86afb08 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/CallNotificationRoomTimelineView.swift @@ -20,14 +20,14 @@ struct CallNotificationRoomTimelineView: View { CompoundIcon(iconKeyPath, size: .medium, relativeTo: .compound.headingMDBold) .foregroundStyle(.compound.iconSecondary) .accessibilityHidden(true) - + Text(tileTitle) .font(.compound.bodyMD) .foregroundColor(.compound.textSecondary) .labelStyle(.custom(spacing: 4)) - + Spacer() - + Text(timelineItem.timestamp.formattedTime()) .font(.compound.bodyXS) .foregroundColor(.compound.textSecondary) @@ -39,7 +39,7 @@ struct CallNotificationRoomTimelineView: View { } // MARK: - Private - + private var tileTitle: String { if timelineItem.isDM { // As per design only have declined variants in DM @@ -54,7 +54,7 @@ struct CallNotificationRoomTimelineView: View { L10n.commonCallStarted } } - + private var iconKeyPath: KeyPath { if timelineItem.isDM, timelineItem.isDeclined || timelineItem.isDeclinedByMe { // As per design only have declined variants in DM @@ -82,9 +82,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi isDeclinedByMe: false, isDeclined: false, isVoiceCall: false)) - + Divider() - + CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: .mock, isEditable: false, @@ -93,9 +93,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi isDeclinedByMe: false, isDeclined: false, isVoiceCall: true)) - + Divider() - + CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: .mock, isEditable: false, @@ -104,9 +104,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi isDeclinedByMe: true, isDeclined: true, isVoiceCall: false)) - + Divider() - + CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: .mock, isEditable: false, @@ -115,9 +115,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi isDeclinedByMe: false, isDeclined: true, isVoiceCall: true)) - + Divider() - + CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: .mock, isEditable: false, @@ -126,9 +126,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi isDeclinedByMe: true, isDeclined: true, isVoiceCall: true)) - + Divider() - + CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: .mock, isEditable: false, @@ -137,9 +137,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi isDeclinedByMe: false, isDeclined: true, isVoiceCall: false)) - + Divider() - + CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: .mock, isEditable: false, @@ -148,7 +148,7 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi isDeclinedByMe: true, isDeclined: true, isVoiceCall: false)) - + Divider() } .padding() diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift index 9fffd9035..0d59afeb1 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/FormattedBodyText.swift @@ -73,7 +73,7 @@ struct FormattedBodyText: View { type: .plainText)) } let lastPlainTextIndex = components.lastIndex { $0.type == .plainText } - + return TimelineBubbleLayout(spacing: 8) { // The `ForEach` needs to iterate over the id of the element to allow // SwiftUI animations to work properly ater any edit. @@ -98,7 +98,7 @@ struct FormattedBodyText: View { .timelineBubbleLayoutSize(.natural) } } - + // Make a second iteration through the components adding naturally sized versions of the // block quotes and code blocks which are used for layout calculations but won't be rendered. ForEach(components) { component in diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift index 11526621a..daf1881b6 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/HighlightedTimelineItemModifier.swift @@ -91,7 +91,7 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { static let composerViewModel = ComposerToolbarViewModel.mock() static let timelineViewModel = { let appSettings = AppSettings.volatile() - + return TimelineViewModel(roomProxy: roomProxyMock, focussedEventID: focussedEventID, timelineController: MockTimelineController(), @@ -105,7 +105,7 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider { linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) }() - + static var previews: some View { ElementNavigationStack { RoomScreen(context: roomViewModel.context, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift index 23a508bc4..b9cd961a8 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ImageRoomTimelineView.swift @@ -65,7 +65,7 @@ struct ImageRoomTimelineView: View { .timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo) } } - + private var placeholder: some View { Rectangle() .foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming) diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift index 83ff4be04..34af44082 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/LocationRoomTimelineView.swift @@ -23,7 +23,7 @@ struct LocationRoomTimelineView: View { } } } - + @ViewBuilder private var mainContent: some View { if let geoURI = timelineItem.content.geoURI { @@ -41,16 +41,16 @@ struct LocationRoomTimelineView: View { trailingReservedSize: timelineItem.trailingReservedSize) } } - + // MARK: - Private - + private let mapAspectRatio: Double = 3 / 2 private let mapMaxHeight: Double = 300 } struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock - + static var previews: some View { PreviewScrollView { VStack(spacing: 8) { @@ -62,7 +62,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview { .previewLayout(.sizeThatFits) .previewDisplayName("Bubbles") } - + @ViewBuilder static var states: some View { LocationRoomTimelineView(timelineItem: .init(id: .randomEvent, @@ -72,7 +72,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview { canBeRepliedTo: true, sender: .init(id: "Bob"), content: .init(body: "Fallback geo uri description"))) - + LocationRoomTimelineView(timelineItem: .init(id: .randomEvent, timestamp: .mock, isOutgoing: false, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift index 503217353..a417e1641 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/PollRoomTimelineView.swift @@ -54,24 +54,24 @@ struct PollRoomTimelineView: View { struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock static let pinnedEventsTimelineViewModel = TimelineViewModel.mock(timelineKind: .pinned) - + static var previews: some View { PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false)) .environmentObject(viewModel.context) .previewDisplayName("Disclosed, Bubble") - + PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed(), isOutgoing: false)) .environmentObject(viewModel.context) .previewDisplayName("Undisclosed, Bubble") - + PollRoomTimelineView(timelineItem: .mock(poll: .endedDisclosed)) .environmentObject(viewModel.context) .previewDisplayName("Ended, Disclosed, Bubble") - + PollRoomTimelineView(timelineItem: .mock(poll: .endedUndisclosed)) .environmentObject(viewModel.context) .previewDisplayName("Ended, Undisclosed, Bubble") - + PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(createdByAccountOwner: true))) .environmentObject(viewModel.context) .previewDisplayName("Creator, disclosed, Bubble") diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift index 4032b6c6d..3c628708c 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/ReadMarkerRoomTimelineView.swift @@ -29,9 +29,9 @@ struct ReadMarkerRoomTimelineView: View { struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview { static let viewModel = TimelineViewModel.mock - + static let item = ReadMarkerRoomTimelineItem(id: .randomVirtual) - + static var previews: some View { VStack(alignment: .leading, spacing: 0) { RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init("Separator")), timestamp: .mock)), groupStyle: .single)) @@ -42,9 +42,9 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview { canBeRepliedTo: true, sender: .init(id: "1", displayName: "Bob"), content: .init(body: "This is another message"))), groupStyle: .single)) - + ReadMarkerRoomTimelineView(timelineItem: item) - + RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init("Separator")), timestamp: .mock)), groupStyle: .single)) RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent, timestamp: .mock, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextBasedRoomTimelineViewProtocol.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextBasedRoomTimelineViewProtocol.swift index c97862dc1..db5d921cb 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextBasedRoomTimelineViewProtocol.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/TextBasedRoomTimelineViewProtocol.swift @@ -9,6 +9,6 @@ // periphery: ignore - markdown protocol protocol TextBasedRoomTimelineViewProtocol { associatedtype TimelineItemType: TextBasedRoomTimelineItem - + var timelineItem: TimelineItemType { get } } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VoiceMessageRoomTimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VoiceMessageRoomTimelineView.swift index 62aae91f0..bfe5326b4 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VoiceMessageRoomTimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineItemViews/VoiceMessageRoomTimelineView.swift @@ -37,11 +37,11 @@ struct VoiceMessageRoomTimelineContent: View { onPlaybackSpeedChange: onPlaybackSpeedChange) .fixedSize(horizontal: false, vertical: true) } - + private func onPlaybackPlayPause() { context?.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id))) } - + private func onPlaybackSpeedChange() { context?.send(viewAction: .handleAudioPlayerAction(.changePlaybackSpeed(itemID: timelineItem.id))) } diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift index 6d46c195a..ec4c9ab50 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineSenderAvatarView.swift @@ -11,9 +11,9 @@ import SwiftUI struct TimelineSenderAvatarView: View { @Environment(\.timelineContext) private var context - + let timelineItem: EventBasedTimelineItemProtocol - + var body: some View { LoadableAvatarImage(url: timelineItem.sender.avatarURL, name: timelineItem.sender.displayName, diff --git a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift index 3aae2bef3..88bfc6fd3 100644 --- a/ElementX/Sources/Screens/Timeline/View/TimelineView.swift +++ b/ElementX/Sources/Screens/Timeline/View/TimelineView.swift @@ -79,7 +79,7 @@ struct TimelineView: View { /// A table view wrapper that displays the timeline of a room. struct TimelineViewRepresentable: UIViewControllerRepresentable { @EnvironmentObject private var viewModelContext: TimelineViewModel.Context - + func makeUIViewController(context: Context) -> TimelineTableViewController { TimelineTableViewController(coordinator: context.coordinator, isScrolledToBottom: $viewModelContext.isScrolledToBottom, @@ -133,7 +133,7 @@ struct TimelineViewRepresentable: UIViewControllerRepresentable { if tableViewController.readMarkerUniqueID != context.viewState.timelineState.readMarkerUniqueID { tableViewController.readMarkerUniqueID = context.viewState.timelineState.readMarkerUniqueID } - + if tableViewController.typingMembers.members != context.viewState.typingMembers { tableViewController.setTypingMembers(context.viewState.typingMembers) } @@ -154,7 +154,7 @@ struct TimelineView_Previews: PreviewProvider { // Not testable as this preview static let composerViewModel = ComposerToolbarViewModel.mock() static let timelineViewModel = { let appSettings = AppSettings.volatile() - + return TimelineViewModel(roomProxy: roomProxyMock, timelineController: MockTimelineController(), userSession: UserSessionMock(.init()), @@ -167,7 +167,7 @@ struct TimelineView_Previews: PreviewProvider { // Not testable as this preview linkMetadataProvider: LinkMetadataProvider(), timelineControllerFactory: TimelineControllerFactoryMock(.init())) }() - + static var previews: some View { ElementNavigationStack { RoomScreen(context: roomViewModel.context, diff --git a/ElementX/Sources/Screens/Timeline/View/VoiceMessageRoomPlaybackView.swift b/ElementX/Sources/Screens/Timeline/View/VoiceMessageRoomPlaybackView.swift index 64ea90c69..fcd6aef26 100644 --- a/ElementX/Sources/Screens/Timeline/View/VoiceMessageRoomPlaybackView.swift +++ b/ElementX/Sources/Screens/Timeline/View/VoiceMessageRoomPlaybackView.swift @@ -15,12 +15,12 @@ struct VoiceMessageRoomPlaybackView: View { @ScaledMetric private var waveformLineWidth = 2.0 @ScaledMetric private var waveformLinePadding = 2.0 @GestureState var isDragging = false - + let onPlayPause: () -> Void let onSeek: (Double) -> Void let onScrubbing: (Bool) -> Void let onPlaybackSpeedChange: () -> Void - + var body: some View { HStack(spacing: 8) { VoiceMessageButton(state: .init(playerState.playerButtonPlaybackState), @@ -36,7 +36,7 @@ struct VoiceMessageRoomPlaybackView: View { .monospacedDigit() .fixedSize(horizontal: true, vertical: true) } - + waveformView .waveformInteraction(isDragging: $isDragging, progress: playerState.progress, @@ -62,7 +62,7 @@ struct VoiceMessageRoomPlaybackView: View { DateFormatter.elapsedTimeFormatter.string(from: duration) } } - + private var timeLabelContent: String { // Display the duration if progress is 0.0 let percent = playerState.progress > 0.0 ? playerState.progress : 1.0 @@ -82,7 +82,7 @@ struct VoiceMessageRoomPlaybackView: View { L10n.a11yVoiceMessage(durationString) } } - + @ViewBuilder private var waveformView: some View { if let url = playerState.fileURL { @@ -96,7 +96,7 @@ struct VoiceMessageRoomPlaybackView: View { estimatedWaveformView } } - + private var estimatedWaveformView: some View { EstimatedWaveformView(lineWidth: waveformLineWidth, linePadding: waveformLinePadding, @@ -111,7 +111,7 @@ private extension DateFormatter { dateFormatter.dateFormat = "m:ss" return dateFormatter }() - + static let longElapsedTimeFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "mm:ss" diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift index a1b8b7c69..2c5657b9b 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenModels.swift @@ -24,7 +24,7 @@ struct UserProfileScreenViewState: BindableState { var isVerified: Bool? var permalink: URL? var dmRoomID: String? - + var bindings: UserProfileScreenViewStateBindings var showVerifiedBadge: Bool { diff --git a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift index f7d7fb733..9197f5db9 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/UserProfileScreenViewModel.swift @@ -71,7 +71,7 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr actionsSubject.send(.dismiss) } } - + // MARK: - Private private func loadProfile() async { @@ -169,7 +169,7 @@ class UserProfileScreenViewModel: UserProfileScreenViewModelType, UserProfileScr private var loadingIndicatorIdentifier: String { "\(Self.self)-Loading" } - + private var statusIndicatorIdentifier: String { "\(Self.self)-Status" } diff --git a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift index 14ec7e88e..4097ea052 100644 --- a/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift +++ b/ElementX/Sources/Screens/UserProfileScreen/View/UserProfileScreen.swift @@ -91,7 +91,7 @@ struct UserProfileScreen: View { } .padding(.top, 32) } - + @ToolbarContentBuilder private var toolbar: some ToolbarContent { if context.viewState.isPresentedModally { @@ -138,11 +138,11 @@ struct UserProfileScreen_Previews: PreviewProvider, TestablePreview { return .success(identity) } - + if userID != RoomMemberProxyMock.mockMe.userID { clientProxyMock.directRoomForUserIDReturnValue = .success("roomID") } - + return UserProfileScreenViewModel(userID: userID, isPresentedModally: false, userSession: UserSessionMock(.init(clientProxy: clientProxyMock)), diff --git a/ElementX/Sources/Services/AlertTones/NotificationTone.swift b/ElementX/Sources/Services/AlertTones/NotificationTone.swift index fa064fb5d..39bcc0322 100644 --- a/ElementX/Sources/Services/AlertTones/NotificationTone.swift +++ b/ElementX/Sources/Services/AlertTones/NotificationTone.swift @@ -13,16 +13,16 @@ struct NotificationTone: Hashable, Comparable, Codable { var label: String { labelOverride ?? URL(filePath: "/\(relativePath.last, default: "")").deletingPathExtension().lastPathComponent } - + private let labelOverride: String? let storageLocationRoot: StorageLocation let relativePath: [String] - + /// The audio filename including its extension. var filename: String { relativePath.last ?? "" } - + /// - Parameters: /// - labelOverride: Optional custom display name. If `nil`, the filename stem is used. /// - storageLocationRoot: Where the file lives (system, bundle, or library). @@ -32,7 +32,7 @@ struct NotificationTone: Hashable, Comparable, Codable { self.storageLocationRoot = storageLocationRoot self.relativePath = relativePath } - + /// Indicates which storage root backs the tone's audio file. enum StorageLocation: Codable, Hashable { /// The device's system sounds directory. @@ -42,22 +42,22 @@ struct NotificationTone: Hashable, Comparable, Codable { /// The app's Library directory (user-imported tones). case appLibrary } - + /// Creates a tone backed by a file in the system sounds directory. static func createSystemSound(label: String?, filename: String, systemSoundsSubdirectory: [String] = []) -> NotificationTone { NotificationTone(labelOverride: label, storageLocationRoot: .system, relativePath: systemSoundsSubdirectory + [filename]) } - + /// Creates a tone backed by a file in the app bundle. static func createBundledSound(label: String?, filename: String) -> NotificationTone { NotificationTone(labelOverride: label, storageLocationRoot: .appBundle, relativePath: [filename]) } - + /// Creates a tone backed by a user-imported file in the app library. static func createCustomUserSound(filename: String) -> NotificationTone { NotificationTone(labelOverride: nil, storageLocationRoot: .appLibrary, relativePath: [filename]) } - + static func < (lhs: NotificationTone, rhs: NotificationTone) -> Bool { lhs.label < rhs.label } diff --git a/ElementX/Sources/Services/AlertTones/NotificationToneManager.swift b/ElementX/Sources/Services/AlertTones/NotificationToneManager.swift index 82931ab01..f3aaf62b0 100644 --- a/ElementX/Sources/Services/AlertTones/NotificationToneManager.swift +++ b/ElementX/Sources/Services/AlertTones/NotificationToneManager.swift @@ -170,10 +170,10 @@ struct NotificationToneManager: NotificationToneManagerProtocol { try FileManager.default.moveItem(at: tempURL, to: destURL) MXLog.info("Converted \(sourceURL.path(percentEncoded: false)) to caf") } - + /// File URL of the active tone copied/linked for use by the system. private static let selectedToneLocation = NotificationToneManager.libraryLocation.deletingLastPathComponent().appending(component: selectedToneFilename) - + /// Pre-defined iOS system tones available for selection, sorted by name. private static let defaultSystemAlerts: [NotificationTone] = [ .createSystemSound(label: L10n.screenNotificationSettingsSoundSystemTriTone, @@ -261,7 +261,7 @@ struct NotificationToneManager: NotificationToneManagerProtocol { .createBundledSound(label: L10n.screenNotificationSettingsSoundElementFade, filename: "sound_01.caf") ].sorted() - + private static let systemLocation = { let systemRoot: URL if let simulatorRoot = ProcessInfo.processInfo.environment["SIMULATOR_ROOT"] { @@ -271,7 +271,7 @@ struct NotificationToneManager: NotificationToneManagerProtocol { } return systemRoot.appending(components: "System", "Library", "Audio", "UISounds", directoryHint: .isDirectory) }() - + private static let bundledLocation: URL = { guard let url = Bundle.app.resourceURL else { fatalError("The app is seriously corrupt if resourceURL is missing.") diff --git a/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift b/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift index fb060730b..8914c37a5 100644 --- a/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift +++ b/ElementX/Sources/Services/Analytics/AnalyticsClientProtocol.swift @@ -15,7 +15,7 @@ protocol AnalyticsClientProtocol { /// Starts the analytics client reporting data. func start(analyticsConfiguration: AnalyticsConfiguration) - + /// Reset all stored properties and any event queues on the client. Note that /// the client will remain active, but in a fresh unidentified state. func reset() diff --git a/ElementX/Sources/Services/Analytics/AnalyticsService.swift b/ElementX/Sources/Services/Analytics/AnalyticsService.swift index bb55ea75d..dd11bd405 100644 --- a/ElementX/Sources/Services/Analytics/AnalyticsService.swift +++ b/ElementX/Sources/Services/Analytics/AnalyticsService.swift @@ -42,7 +42,7 @@ class AnalyticsService: AnalyticsServiceProtocol { // The order is important here. PostHog ignores the reset if stopped. reset() client.stop() - + MXLog.info("Stopped.") } @@ -50,7 +50,7 @@ class AnalyticsService: AnalyticsServiceProtocol { guard isEnabled, !client.isRunning, let configuration = appSettings.analyticsConfiguration else { return } client.start(analyticsConfiguration: configuration) - + // Sanity check in case something went wrong. guard client.isRunning else { return } @@ -139,17 +139,17 @@ extension AnalyticsService { } capture(event: AnalyticsEvent.JoinedRoom(isDM: isDM, isSpace: isSpace, roomSize: roomSize, trigger: nil)) } - + func trackPollCreated(isUndisclosed: Bool, numberOfAnswers: Int) { capture(event: AnalyticsEvent.PollCreation(action: .Create, isUndisclosed: isUndisclosed, numberOfAnswers: numberOfAnswers)) } - + func trackPollVote() { capture(event: AnalyticsEvent.PollVote(doNotUse: nil)) } - + func trackPollEnd() { capture(event: AnalyticsEvent.PollEnd(doNotUse: nil)) } @@ -185,7 +185,7 @@ extension AnalyticsService { case .settingUp: return } - + let event = AnalyticsEvent.CryptoSessionStateChange(recoveryState: analyticsRecoveryState, verificationState: analyticsVerificationState) client.capture(event) } diff --git a/ElementX/Sources/Services/Analytics/AnalyticsServiceProtocol.swift b/ElementX/Sources/Services/Analytics/AnalyticsServiceProtocol.swift index 9bd8e0026..28d1049cb 100644 --- a/ElementX/Sources/Services/Analytics/AnalyticsServiceProtocol.swift +++ b/ElementX/Sources/Services/Analytics/AnalyticsServiceProtocol.swift @@ -27,7 +27,7 @@ protocol AnalyticsServiceProtocol: AnyObject { /// Whether analytics tracking is currently enabled. var isEnabled: Bool { get } - + /// Opts in to analytics tracking. func optIn() @@ -45,7 +45,7 @@ protocol AnalyticsServiceProtocol: AnyObject { /// Resets the consent state for analytics. func resetConsentState() - + /// Track the presentation of a screen. /// - Parameter screen: The screen that was shown. /// - Parameter milliseconds: An optional value representing how long the screen was shown for in milliseconds. @@ -121,11 +121,11 @@ extension AnalyticsServiceProtocol { func track(screen: AnalyticsEvent.MobileScreen.ScreenName) { track(screen: screen, duration: nil) } - + func trackInteraction(name: AnalyticsEvent.Interaction.Name) { trackInteraction(index: nil, name: name) } - + @_disfavoredOverload // make sure this doesn't cause infinite recursion func trackError(context: String?, domain: AnalyticsEvent.Error.Domain, @@ -146,7 +146,7 @@ extension AnalyticsServiceProtocol { userTrustsOwnIdentity: userTrustsOwnIdentity, wasVisibleToUser: wasVisibleToUser) } - + func trackComposer(inThread: Bool, isEditing: Bool, isReply: Bool, diff --git a/ElementX/Sources/Services/Audio/Player/AudioPlayer.swift b/ElementX/Sources/Services/Audio/Player/AudioPlayer.swift index 5620b2105..22b1591eb 100644 --- a/ElementX/Sources/Services/Audio/Player/AudioPlayer.swift +++ b/ElementX/Sources/Services/Audio/Player/AudioPlayer.swift @@ -49,7 +49,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol { private(set) var playbackURL: URL? private(set) var playbackSpeed: Float = 1.0 - + private var deinitInProgress = false var duration: TimeInterval { @@ -129,14 +129,14 @@ class AudioPlayer: NSObject, AudioPlayerProtocol { let time = progress * duration await internalAudioPlayer.seek(to: CMTime(seconds: time, preferredTimescale: 60)) } - + func setPlaybackSpeed(_ speed: Float) { playbackSpeed = speed if state == .playing { internalAudioPlayer?.rate = speed } } - + // MARK: - Private private func setupAudioSession() { @@ -178,7 +178,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol { playerItem = nil removeObservers() } - + private func addObservers() { guard let internalAudioPlayer, let playerItem else { return @@ -197,7 +197,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol { break } } - + rateObserver = internalAudioPlayer.observe(\.rate, options: [.old, .new]) { [weak self] _, _ in guard let self else { return } @@ -211,7 +211,7 @@ class AudioPlayer: NSObject, AudioPlayerProtocol { setInternalState(.playing) } } - + NotificationCenter.default.publisher(for: Notification.Name.AVPlayerItemDidPlayToEndTime) .sink { [weak self] _ in guard let self else { return } diff --git a/ElementX/Sources/Services/Audio/Player/AudioPlayerState.swift b/ElementX/Sources/Services/Audio/Player/AudioPlayerState.swift index 317d67809..b2a4e66b6 100644 --- a/ElementX/Sources/Services/Audio/Player/AudioPlayerState.swift +++ b/ElementX/Sources/Services/Audio/Player/AudioPlayerState.swift @@ -50,7 +50,7 @@ class AudioPlayerState: ObservableObject, Identifiable { var showProgressIndicator: Bool { progress > 0 } - + var isAttached: Bool { audioPlayer != nil } @@ -124,7 +124,7 @@ class AudioPlayerState: ObservableObject, Identifiable { playbackSpeed = speed audioPlayer?.setPlaybackSpeed(speed.rawValue) } - + // MARK: - Private private func subscribeToAudioPlayer(audioPlayer: AudioPlayerProtocol) { @@ -246,7 +246,7 @@ class AudioPlayerState: ObservableObject, Identifiable { } audioPlayer.pause() - + return MPRemoteCommandHandlerStatus.success } diff --git a/ElementX/Sources/Services/Audio/Recorder/AudioRecorder.swift b/ElementX/Sources/Services/Audio/Recorder/AudioRecorder.swift index 7120fe81c..377c6f36c 100644 --- a/ElementX/Sources/Services/Audio/Recorder/AudioRecorder.swift +++ b/ElementX/Sources/Services/Audio/Recorder/AudioRecorder.swift @@ -37,7 +37,7 @@ class AudioRecorder: AudioRecorderProtocol { private let maximumRecordingTime: TimeInterval = 1800 // 30 minutes private let silenceThreshold: Float = -50.0 private var meterLevel: Float = 0 - + private(set) var audioFileURL: URL? var currentTime: TimeInterval = .zero var isRecording: Bool { @@ -94,7 +94,7 @@ class AudioRecorder: AudioRecorderProtocol { } } } - + func averagePower() -> Float { meterLevel } @@ -161,7 +161,7 @@ class AudioRecorder: AudioRecorderProtocol { // Initialize a new audio engine let audioEngine = AVAudioEngine() self.audioEngine = audioEngine - + let inputNode = audioEngine.inputNode let inputFormat = inputNode.outputFormat(forBus: 0) let hardwareSampleRate = audioEngine.inputNode.outputFormat(forBus: 0).sampleRate @@ -204,12 +204,12 @@ class AudioRecorder: AudioRecorderProtocol { } else { audioConverter = nil } - + // Install tap to process audio buffers coming from the mixer mixer.installTap(onBus: 0, bufferSize: 1024, format: mixerFormat) { [weak self] buffer, _ in self?.processAudioBuffer(buffer) } - + do { try audioEngine.start() completion(.success(())) @@ -304,7 +304,7 @@ class AudioRecorder: AudioRecorderProtocol { // Write the buffer into the audio file do { try audioFile.write(from: inputBuffer) - + // Compute the sample value for the waveform updateMeterLevel(inputBuffer) @@ -366,10 +366,10 @@ class AudioRecorder: AudioRecorderProtocol { setInternalState(.suspended) case .ended: MXLog.info("Interruption ended: \(notification)") - + guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return } let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue) - + if options.contains(.shouldResume) { do { try audioEngine?.start() @@ -413,7 +413,7 @@ class AudioRecorder: AudioRecorderProtocol { actionsSubject.send(.didStopRecording) case .error(let error): cleanupAudioEngine() - + actionsSubject.send(.didFailWithError(error: error)) } } diff --git a/ElementX/Sources/Services/Authentication/AuthenticationService.swift b/ElementX/Sources/Services/Authentication/AuthenticationService.swift index 96b5f8498..06d8dbb80 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationService.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationService.swift @@ -25,7 +25,7 @@ class AuthenticationService: AuthenticationServiceProtocol { var homeserver: CurrentValuePublisher { homeserverSubject.asCurrentValuePublisher() } - + private(set) var flow: AuthenticationFlow let classicAppAccount: ClassicAppAccount? @@ -195,7 +195,7 @@ class AuthenticationService: AuthenticationServiceProtocol { progressSubject.send(completion: .failure(.qrCodeError(.deviceNotSignedIn))) return progressSubject.asCurrentValuePublisher() } - + // n.b. We deliberatley don't check whether the received server is in our appSettings.accountProviders let listener = SDKListener { progress in @@ -382,7 +382,7 @@ extension AuthenticationService { static var mock: AuthenticationService { mock(classicAppManager: nil) } - + static func mock(classicAppManager: ClassicAppManagerProtocol?) -> AuthenticationService { AuthenticationService(userSessionStore: UserSessionStoreMock(.init()), encryptionKeyProvider: EncryptionKeyProvider(), diff --git a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift index ed4cb8dab..53f629f8f 100644 --- a/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift +++ b/ElementX/Sources/Services/Authentication/AuthenticationServiceProtocol.swift @@ -43,7 +43,7 @@ protocol AuthenticationServiceProtocol: QRCodeLoginServiceProtocol { var homeserver: CurrentValuePublisher { get } /// The type of flow the service is currently configured with. var flow: AuthenticationFlow { get } - + /// Sets up the service for login on the specified homeserver address. func configure(for homeserverAddress: String, flow: AuthenticationFlow) async -> Result /// Performs login using OAuth for the current homeserver. diff --git a/ElementX/Sources/Services/BugReport/BugReportService.swift b/ElementX/Sources/Services/BugReport/BugReportService.swift index 883b94981..d2410ba9e 100644 --- a/ElementX/Sources/Services/BugReport/BugReportService.swift +++ b/ElementX/Sources/Services/BugReport/BugReportService.swift @@ -25,7 +25,7 @@ class BugReportService: NSObject, BugReportServiceProtocol { var isEnabled: Bool { rageshakeURL != .disabled } - + var lastCrashEventID: String? init(rageshakeURLPublisher: CurrentValuePublisher, @@ -45,7 +45,7 @@ class BugReportService: NSObject, BugReportServiceProtocol { .weakAssign(to: \.rageshakeURL, on: self) .store(in: &cancellables) } - + // MARK: - BugReportServiceProtocol var crashedLastRun: Bool { @@ -110,7 +110,7 @@ class BugReportService: NSObject, BugReportServiceProtocol { for url in bugReport.files { params.append(MultipartFormData(key: "file", type: .file(url: url))) } - + let boundary = "Boundary-\(UUID().uuidString)" var body = Data() for param in params { @@ -122,13 +122,13 @@ class BugReportService: NSObject, BugReportServiceProtocol { } } body.appendString(string: "--\(boundary)--\r\n") - + var request = URLRequest(url: rageshakeURL) request.addValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") - + request.httpMethod = "POST" request.httpBody = body as Data - + progressSubject .receive(on: DispatchQueue.main) .weakAssign(to: \.value, on: progressListener) @@ -164,9 +164,9 @@ class BugReportService: NSObject, BugReportServiceProtocol { return .failure(.uploadFailure(error)) } } - + // MARK: - Private - + private var defaultParams: [MultipartFormData] { let (localTime, utcTime) = localAndUTCTime(for: Date()) let version = "\(InfoPlistReader.main.bundleShortVersionString) (\(InfoPlistReader.main.bundleVersion))" @@ -185,7 +185,7 @@ class BugReportService: NSObject, BugReportServiceProtocol { MultipartFormData(key: "base_bundle_identifier", type: .text(value: InfoPlistReader.main.baseBundleIdentifier)) ] } - + private func localAndUTCTime(for date: Date) -> (String, String) { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" @@ -194,7 +194,7 @@ class BugReportService: NSObject, BugReportServiceProtocol { let utcTime = dateFormatter.string(from: date) return (localTime, utcTime) } - + private var os: String { if ProcessInfo.processInfo.isiOSAppOnMac { // The other APIs report macOS's equivalent iOS version, so lets use the right one to get the macOS version. @@ -203,7 +203,7 @@ class BugReportService: NSObject, BugReportServiceProtocol { "\(UIDevice.current.systemName) \(UIDevice.current.systemVersion)" } } - + private func zipFiles(_ logFiles: [URL]) async -> Logs { MXLog.info("zipFiles") @@ -219,7 +219,7 @@ class BugReportService: NSObject, BugReportServiceProtocol { } MXLog.info("zipFiles: originalSize: \(compressedLogs.originalSize), zippedSize: \(compressedLogs.zippedSize)") - + return compressedLogs } @@ -263,7 +263,7 @@ private extension Data { append(data) } } - + mutating func appendParam(_ param: MultipartFormData, boundary: String) throws { appendString(string: "--\(boundary)\r\n") appendString(string: "Content-Disposition:form-data; name=\"\(param.key)\"") diff --git a/ElementX/Sources/Services/Client/ClientProxy.swift b/ElementX/Sources/Services/Client/ClientProxy.swift index 611ffd70d..fe2afd0d6 100644 --- a/ElementX/Sources/Services/Client/ClientProxy.swift +++ b/ElementX/Sources/Services/Client/ClientProxy.swift @@ -27,7 +27,7 @@ class ClientProxy: ClientProxyProtocol { private var roomListStateUpdateTaskHandle: TaskHandle? // periphery: ignore - only for retain private var roomListStateLoadingStateUpdateTaskHandle: TaskHandle? - + private var syncService: SyncService // periphery: ignore - only for retain private var syncServiceStateUpdateTaskHandle: TaskHandle? @@ -46,7 +46,7 @@ class ClientProxy: ClientProxyProtocol { // periphery:ignore - required for instance retention in the rust codebase private var mediaPreviewConfigListenerTaskHandle: TaskHandle? - + // periphery:ignore - required for instance retention in the rust codebase private var liveLocationOwnInfoUpdatesListenerTaskHandle: TaskHandle? @@ -60,7 +60,7 @@ class ClientProxy: ClientProxyProtocol { private(set) var staticRoomSummaryProvider: StaticRoomSummaryProviderProtocol let notificationSettings: NotificationSettingsProxyProtocol - + let secureBackupController: SecureBackupControllerProtocol private(set) var sessionVerificationController: SessionVerificationControllerProxyProtocol? @@ -128,7 +128,7 @@ class ClientProxy: ClientProxyProtocol { users: [:], events: [:]) } - + private var loadCachedAvatarURLTask: Task? private let userAvatarURLSubject = CurrentValueSubject(nil) var userAvatarURLPublisher: CurrentValuePublisher { @@ -191,12 +191,12 @@ class ClientProxy: ClientProxyProtocol { } var roomsToAwait: Set = [] - + private let liveLocationOwnInfoUpdatesSubject = PassthroughSubject() var liveLocationOwnInfoUpdatesPublisher: AnyPublisher { liveLocationOwnInfoUpdatesSubject.eraseToAnyPublisher() } - + private let sendQueueStatusSubject = CurrentValueSubject(false) init(client: ClientProtocol, @@ -240,7 +240,7 @@ class ClientProxy: ClientProxyProtocol { syncServiceStateUpdateTaskHandle = createSyncServiceStateObserver(syncService) roomListStateUpdateTaskHandle = createRoomListServiceObserver(roomListService) roomListStateLoadingStateUpdateTaskHandle = createRoomListLoadingStateUpdateObserver(roomListService) - + delegateHandle = try client.setDelegate(delegate: ClientDelegateWrapper { [weak self] isSoftLogout in self?.hasEncounteredAuthError = true self?.actionsSubject.send(.receivedAuthError(isSoftLogout: isSoftLogout)) @@ -278,7 +278,7 @@ class ClientProxy: ClientProxyProtocol { Task { mediaPreviewConfigListenerTaskHandle = await createMediaPreviewConfigObserver() } - + liveLocationOwnInfoUpdatesListenerTaskHandle = createLiveLocationOwnInfoUpdatesObserver() } @@ -290,7 +290,7 @@ class ClientProxy: ClientProxyProtocol { return "Unknown user identifier" } } - + var deviceID: String? { do { return try client.deviceId() @@ -299,7 +299,7 @@ class ClientProxy: ClientProxyProtocol { return nil } } - + var homeserver: String { client.homeserver() } @@ -360,7 +360,7 @@ class ClientProxy: ClientProxyProtocol { } } } - + private(set) lazy var pusherNotificationClientIdentifier: String? = { // NOTE: The result is stored as part of the restoration token. Any changes // here would require a migration to correctly match incoming notifications. @@ -378,7 +378,7 @@ class ClientProxy: ClientProxyProtocol { return .failure(.sdkError(error)) } } - + func hasDevicesToVerifyAgainst() async -> Result { do { let result = try await client.encryption().hasDevicesToVerifyAgainst() @@ -388,7 +388,7 @@ class ClientProxy: ClientProxyProtocol { return .failure(.sdkError(error)) } } - + func startSync() { guard !hasEncounteredAuthError else { MXLog.warning("Ignoring request, this client has an unknown token.") @@ -566,7 +566,7 @@ class ClientProxy: ClientProxyProtocol { func joinRoom(_ roomID: String, via: [String]) async -> Result { do { let _ = try await client.joinRoomByIdOrAlias(roomIdOrAlias: roomID, serverNames: via) - + await waitForRoomToSync(roomID: roomID, timeout: .seconds(30)) return .success(()) @@ -652,7 +652,7 @@ class ClientProxy: ClientProxyProtocol { return .failure(ClientProxyError.sdkError(error)) } } - + func roomForIdentifier(_ identifier: String) async -> RoomProxyType? { let shouldAwait = roomsToAwait.remove(identifier) != nil @@ -706,7 +706,7 @@ class ClientProxy: ClientProxyProtocol { return .failure(.sdkError(error)) } } - + func loadUserDisplayName() async -> Result { do { let displayName = try await client.displayName() @@ -730,7 +730,7 @@ class ClientProxy: ClientProxyProtocol { return .failure(.sdkError(error)) } } - + func loadUserAvatarURL() async -> Result { do { let urlString = try await client.avatarUrl() @@ -748,7 +748,7 @@ class ClientProxy: ClientProxyProtocol { MXLog.error("Failed uploading, invalid media: \(media)") return .failure(.invalidMedia) } - + do { let data = try Data(contentsOf: imageURL) try await client.uploadAvatar(mimeType: mimeType, data: data) @@ -875,7 +875,7 @@ class ClientProxy: ClientProxyProtocol { return .failure(.sdkError(error)) } } - + func markAllRoomsAsRead() async -> Result { do { return try await .success(client.markAllRoomsAsRead()) @@ -903,7 +903,7 @@ class ClientProxy: ClientProxyProtocol { return .failure(.sdkError(error)) } } - + // MARK: Ignored users func ignoreUser(_ userID: String) async -> Result { @@ -1123,7 +1123,7 @@ class ClientProxy: ClientProxyProtocol { MXLog.error("Failed retrieving session verification controller proxy with error: \(error)") } } - + private func loadUserAvatarURLFromCache() { loadCachedAvatarURLTask = Task { do { @@ -1172,7 +1172,7 @@ class ClientProxy: ClientProxyProtocol { return nil } } - + private func createLiveLocationOwnInfoUpdatesObserver() -> TaskHandle? { do { return try client.subscribeToOwnBeaconInfoUpdates(listener: SDKListener { [weak self] update in @@ -1187,7 +1187,7 @@ class ClientProxy: ClientProxyProtocol { return nil } } - + private func createRoomListServiceObserver(_ roomListService: RoomListService) -> TaskHandle { roomListService.state(listener: SDKListener { [weak self] state in guard let self else { return } @@ -1272,7 +1272,7 @@ class ClientProxy: ClientProxyProtocol { MXLog.info("Wait for \(roomID) failed: \(error)") } } - + private func updateIgnoredUsers() { Task { do { @@ -1355,7 +1355,7 @@ private final class ClientDelegateWrapper: ClientDelegate { } // MARK: - ClientDelegate - + func didReceiveAuthError(isSoftLogout: Bool) { MXLog.error("Received authentication error, softlogout=\(isSoftLogout)") authErrorCallback(isSoftLogout) diff --git a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift index 9cec00177..06fd40f79 100644 --- a/ElementX/Sources/Services/Client/ClientProxyProtocol.swift +++ b/ElementX/Sources/Services/Client/ClientProxyProtocol.swift @@ -111,9 +111,9 @@ protocol ClientProxyProtocol: AnyObject { var homeserverReachabilityPublisher: CurrentValuePublisher { get } var userID: String { get } - + var deviceID: String? { get } - + var homeserver: String { get } var canDeactivateAccount: Bool { get } @@ -121,9 +121,9 @@ protocol ClientProxyProtocol: AnyObject { var userIDServerName: String? { get } var userDisplayNamePublisher: CurrentValuePublisher { get } - + var userAvatarURLPublisher: CurrentValuePublisher { get } - + /// We delay fetching this until after the first sync. Nil until then var ignoredUsersPublisher: CurrentValuePublisher<[String]?, Never> { get } @@ -170,13 +170,13 @@ protocol ClientProxyProtocol: AnyObject { func hasDevicesToVerifyAgainst() async -> Result func startSync() - + func stopSync() func stopSync(completion: (() -> Void)?) // Hopefully this will become async once we get SE-0371. func expireSyncSessions() async - + func accountURL(action: AccountManagementAction) async -> URL? func directRoomForUserID(_ userID: String) -> Result @@ -217,7 +217,7 @@ protocol ClientProxyProtocol: AnyObject { @discardableResult func loadUserDisplayName() async -> Result func setUserDisplayName(_ name: String) async -> Result - + @discardableResult func loadUserAvatarURL() async -> Result func setUserAvatar(media: MediaInfo) async -> Result @@ -229,7 +229,7 @@ protocol ClientProxyProtocol: AnyObject { func deactivateAccount(password: String?, eraseData: Bool) async -> Result func logout() async - + func setPusher(with configuration: PusherConfiguration) async throws func searchUsers(searchTerm: String, limit: UInt) async -> Result @@ -245,13 +245,13 @@ protocol ClientProxyProtocol: AnyObject { @discardableResult func clearCaches() async -> Result @discardableResult func optimizeStores() async -> Result - + @discardableResult func markAllRoomsAsRead() async -> Result func storeSizes() async -> Result func fetchMediaPreviewConfiguration() async -> Result - + // MARK: - Ignored users func ignoreUser(_ userID: String) async -> Result @@ -277,10 +277,10 @@ protocol ClientProxyProtocol: AnyObject { func userIdentity(for userID: String, fallBackToServer: Bool) async -> Result // MARK: - Live Location - + /// Publishes updates about the current user's own live location beacon info state changes (start/stop) as echoed by the server. var liveLocationOwnInfoUpdatesPublisher: AnyPublisher { get } - + // MARK: - Moderation & Safety func setTimelineMediaVisibility(_ value: TimelineMediaVisibility) async -> Result diff --git a/ElementX/Sources/Services/ElementCall/ElementCallService.swift b/ElementX/Sources/Services/ElementCall/ElementCallService.swift index 7e01ac4f0..381986809 100644 --- a/ElementX/Sources/Services/ElementCall/ElementCallService.swift +++ b/ElementX/Sources/Services/ElementCall/ElementCallService.swift @@ -154,7 +154,7 @@ class ElementCallService: NSObject, ElementCallServiceProtocol, PKPushRegistryDe } } } - + // MARK: - PKPushRegistryDelegate func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { } diff --git a/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift b/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift index 76681d64d..236ab5944 100644 --- a/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift +++ b/ElementX/Sources/Services/Emojis/EmojiProviderProtocol.swift @@ -12,7 +12,7 @@ struct EmojiItem: Equatable, Identifiable { var id: String { label } - + let label: String let unicode: String let keywords: [String] diff --git a/ElementX/Sources/Services/Keychain/KeychainController.swift b/ElementX/Sources/Services/Keychain/KeychainController.swift index 13818c449..32f805402 100644 --- a/ElementX/Sources/Services/Keychain/KeychainController.swift +++ b/ElementX/Sources/Services/Keychain/KeychainController.swift @@ -13,7 +13,7 @@ import MatrixRustSDK enum KeychainControllerService: String { case sessions case tests - + var restorationTokenID: String { InfoPlistReader.main.baseBundleIdentifier + "." + rawValue } @@ -33,14 +33,14 @@ final class KeychainController: KeychainControllerProtocol { case appLockPINCode case appLockBiometricState } - + init(service: KeychainControllerService, accessGroup: String) { restorationTokenKeychain = Keychain(service: service.restorationTokenID, accessGroup: accessGroup) mainKeychain = Keychain(service: service.mainID, accessGroup: accessGroup) } // MARK: - Restoration Tokens - + func setRestorationToken(_ restorationToken: RestorationToken, forUsername username: String) { do { let tokenData = try JSONEncoder().encode(restorationToken) @@ -49,7 +49,7 @@ final class KeychainController: KeychainControllerProtocol { MXLog.error("Failed storing user restore token with error: \(error)") } } - + func restorationTokenForUsername(_ username: String) -> RestorationToken? { do { guard let tokenData = try restorationTokenKeychain.getData(username) else { @@ -66,17 +66,17 @@ final class KeychainController: KeychainControllerProtocol { return nil } } - + func restorationTokens() -> [KeychainCredentials] { restorationTokenKeychain.allKeys().compactMap { username in guard let restorationToken = restorationTokenForUsername(username) else { return nil } - + return KeychainCredentials(userID: username, restorationToken: restorationToken) } } - + func removeRestorationTokenForUsername(_ username: String) { MXLog.warning("Removing restoration token for user: \(username).") @@ -86,7 +86,7 @@ final class KeychainController: KeychainControllerProtocol { MXLog.error("Failed removing restore token with error: \(error)") } } - + func removeAllRestorationTokens() { MXLog.warning("Removing all user restoration tokens.") diff --git a/ElementX/Sources/Services/Keychain/KeychainControllerMock.swift b/ElementX/Sources/Services/Keychain/KeychainControllerMock.swift index d38deb3d3..9db5b1d4a 100644 --- a/ElementX/Sources/Services/Keychain/KeychainControllerMock.swift +++ b/ElementX/Sources/Services/Keychain/KeychainControllerMock.swift @@ -13,7 +13,7 @@ extension KeychainControllerMock { func retrieveSessionFromKeychain(userId: String) throws -> Session { fatalError("Not implemented") } - + func saveSessionInKeychain(session: Session) { fatalError("Not implemented") } diff --git a/ElementX/Sources/Services/Location/LiveLocationManager.swift b/ElementX/Sources/Services/Location/LiveLocationManager.swift index c58b22812..a394765df 100644 --- a/ElementX/Sources/Services/Location/LiveLocationManager.swift +++ b/ElementX/Sources/Services/Location/LiveLocationManager.swift @@ -198,7 +198,7 @@ class LiveLocationManager: NSObject, LiveLocationManagerProtocol, CLLocationMana .sink { [weak self] sessions in guard let self else { return } syncActiveRoomProxies(with: sessions) - + if sessions.isEmpty { self.stopUpdatingLocation() } else { diff --git a/ElementX/Sources/Services/Location/LiveLocationManagerProtocol.swift b/ElementX/Sources/Services/Location/LiveLocationManagerProtocol.swift index c65245625..9fbd04903 100644 --- a/ElementX/Sources/Services/Location/LiveLocationManagerProtocol.swift +++ b/ElementX/Sources/Services/Location/LiveLocationManagerProtocol.swift @@ -20,7 +20,7 @@ protocol LiveLocationManagerProtocol: AnyObject { var hasDisplayedLiveLocationDisclaimer: Bool { get set } /// Publishes the current location authorization status. var authorizationStatus: CurrentValuePublisher { get } - + /// Requests "Always" location authorization from the user if the system allows it. /// /// - Returns: `true` if the request was forwarded to the system and a prompt will be shown; diff --git a/ElementX/Sources/Services/Location/RoomLiveLocationService.swift b/ElementX/Sources/Services/Location/RoomLiveLocationService.swift index 151d24b27..b4a3b7c87 100644 --- a/ElementX/Sources/Services/Location/RoomLiveLocationService.swift +++ b/ElementX/Sources/Services/Location/RoomLiveLocationService.swift @@ -19,7 +19,7 @@ final class RoomLiveLocationService: RoomLiveLocationServiceProtocol { var liveLocationsPublisher: CurrentValuePublisher<[LiveLocationShare], Never> { liveLocationsSubject.asCurrentValuePublisher() } - + init(liveLocationsObserver: LiveLocationsObserver) { self.liveLocationsObserver = liveLocationsObserver observationToken = liveLocationsObserver diff --git a/ElementX/Sources/Services/Media/MediaUploadingPreprocessor.swift b/ElementX/Sources/Services/Media/MediaUploadingPreprocessor.swift index 3f67cdd37..28ca06ed4 100644 --- a/ElementX/Sources/Services/Media/MediaUploadingPreprocessor.swift +++ b/ElementX/Sources/Services/Media/MediaUploadingPreprocessor.swift @@ -358,7 +358,7 @@ struct MediaUploadingPreprocessor { return .init(url: thumbnailURL, height: thumbnail.size.height, width: thumbnail.size.width, mimeType: "image/jpeg", blurhash: blurhash) } - + private func resizeImage(at url: URL, maxPixelSize: CGFloat, destination: URL, type: UTType) throws(MediaUploadingPreprocessorError) { guard let imageSource = CGImageSourceCreateWithURL(url as NSURL, nil) else { throw .failedResizingImage @@ -440,7 +440,7 @@ struct MediaUploadingPreprocessor { private func convertVideoToMP4(_ url: URL, targetFileSize: UInt) async throws(MediaUploadingPreprocessorError) -> VideoProcessingInfo { let asset = AVURLAsset(url: url) let presetName = appSettings.optimizeMediaUploads ? AVAssetExportPreset1280x720 : AVAssetExportPreset1920x1080 - + guard let exportSession = AVAssetExportSession(asset: asset, presetName: presetName) else { throw .failedConvertingVideo } @@ -524,11 +524,11 @@ private extension AVAssetTrack { guard mediaType == .video else { return naturalSize } - + // The naturalSize does not take the preferredTransform into consideration resulting // in portrait videos reporting inverted values. let transform = try await load(.preferredTransform) - + switch (transform.a, transform.b, transform.c, transform.d) { case (0, 1, -1, 0), (0, -1, 1, 0): return CGSize(width: naturalSize.height, height: naturalSize.width) diff --git a/ElementX/Sources/Services/Media/Provider/MediaLoader.swift b/ElementX/Sources/Services/Media/Provider/MediaLoader.swift index d1754945c..b28c1cbf2 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaLoader.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaLoader.swift @@ -29,7 +29,7 @@ actor MediaLoader: MediaLoaderProtocol { // don't have an accompanying ClientMock to own it. private weak var client: ClientProtocol? private var ongoingRequests = [MediaSourceProxy: MediaRequest]() - + init(client: ClientProtocol) { self.client = client } diff --git a/ElementX/Sources/Services/Media/Provider/MediaLoaderProtocol.swift b/ElementX/Sources/Services/Media/Provider/MediaLoaderProtocol.swift index 2763228b0..ec1dea1ff 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaLoaderProtocol.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaLoaderProtocol.swift @@ -18,7 +18,7 @@ enum MediaLoaderError: Error { // sourcery: AutoMockable protocol MediaLoaderProtocol: Sendable { func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data - + func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data func loadMediaFileForSource(_ source: MediaSourceProxy, filename: String?) async throws -> MediaFileHandleProxy diff --git a/ElementX/Sources/Services/Media/Provider/MediaProvider.swift b/ElementX/Sources/Services/Media/Provider/MediaProvider.swift index dad17ee15..af78fe0d7 100644 --- a/ElementX/Sources/Services/Media/Provider/MediaProvider.swift +++ b/ElementX/Sources/Services/Media/Provider/MediaProvider.swift @@ -44,7 +44,7 @@ struct MediaProvider: MediaProviderProtocol { let image = cacheResult.image { return .success(image) } - + do { let imageData: Data if let size { @@ -52,14 +52,14 @@ struct MediaProvider: MediaProviderProtocol { } else { imageData = try await mediaLoader.loadMediaContentForSource(source) } - + guard let image = UIImage(data: imageData) else { MXLog.error("Invalid image data") return .failure(.invalidImageData) } - + try await imageCache.store(image, forKey: cacheKey) - + return .success(image) } catch { MXLog.error("Failed retrieving image with error: \(error)") diff --git a/ElementX/Sources/Services/Notification/Manager/APNSPayload.swift b/ElementX/Sources/Services/Notification/Manager/APNSPayload.swift index 31d8eba1f..50e135fd2 100644 --- a/ElementX/Sources/Services/Notification/Manager/APNSPayload.swift +++ b/ElementX/Sources/Services/Notification/Manager/APNSPayload.swift @@ -11,7 +11,7 @@ import Foundation struct APSAlert: Encodable { let locKey: String let locArgs: [String] - + enum CodingKeys: String, CodingKey { case locKey = "loc-key" case locArgs = "loc-args" @@ -21,7 +21,7 @@ struct APSAlert: Encodable { struct APSInfo: Encodable { let mutableContent: Int let alert: APSAlert - + enum CodingKeys: String, CodingKey { case mutableContent = "mutable-content" case alert @@ -31,7 +31,7 @@ struct APSInfo: Encodable { struct APNSPayload: Encodable { let aps: APSInfo let pusherNotificationClientIdentifier: String? - + enum CodingKeys: String, CodingKey { case aps case pusherNotificationClientIdentifier = "pusher_notification_client_identifier" diff --git a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift index bb8626408..9c674fbec 100644 --- a/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift +++ b/ElementX/Sources/Services/Notification/Manager/NotificationManager.swift @@ -26,9 +26,9 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { self.appSettings = appSettings super.init() } - + // MARK: NotificationManagerProtocol - + weak var delegate: NotificationManagerDelegate? func start() { @@ -80,14 +80,14 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { } } } - + func register(with deviceToken: Data) async -> Bool { guard let userSession else { return false } return await setPusher(with: deviceToken, clientProxy: userSession.clientProxy) } - + func setUserSession(_ userSession: UserSessionProtocol?) { self.userSession = userSession @@ -106,11 +106,11 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { MXLog.info("Notification sound enabled: \(settings.soundSetting == .enabled)") } } - + func registrationFailed(with error: Error) { MXLog.error("Device token registration failed with error: \(error)") } - + func showLocalNotification(with title: String, subtitle: String?) async { let content = UNMutableNotificationContent() content.title = title @@ -150,7 +150,7 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { let lastMessageDate = roomsToLastMessageDates[roomID] else { return false } - + return notification.date <= lastMessageDate } .map(\.request.identifier) @@ -161,14 +161,14 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { private func removeReceivedWhileOfflineNotification() { notificationCenter.removeDeliveredNotifications(withIdentifiers: [NotificationServiceExtensionActor.receivedWhileOfflineNotificationID]) } - + private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool { do { let defaultPayload = APNSPayload(aps: APSInfo(mutableContent: 1, alert: APSAlert(locKey: "Notification", locArgs: [])), pusherNotificationClientIdentifier: clientProxy.pusherNotificationClientIdentifier) - + let configuration = try await PusherConfiguration(identifiers: .init(pushkey: deviceToken.base64EncodedString(), appId: appSettings.pusherAppID), kind: .http(data: .init(url: appSettings.pushGatewayNotifyEndpoint.absoluteString, @@ -186,7 +186,7 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { return false } } - + private func pusherProfileTag() -> String { if let currentTag = appSettings.pusherProfileTag { return currentTag @@ -196,7 +196,7 @@ final class NotificationManager: NSObject, NotificationManagerProtocol { let offset = Int.random(in: 0.. Bool func registrationFailed(with error: Error) diff --git a/ElementX/Sources/Services/Notification/NotificationConstants.swift b/ElementX/Sources/Services/Notification/NotificationConstants.swift index 6615e9a37..d35fb41be 100644 --- a/ElementX/Sources/Services/Notification/NotificationConstants.swift +++ b/ElementX/Sources/Services/Notification/NotificationConstants.swift @@ -17,12 +17,12 @@ enum NotificationConstants { static let pusherNotificationClientIdentifier = "pusher_notification_client_identifier" static let receiverIdentifier = "receiver_id" } - + enum Category { static let message = "message" static let invite = "invite" } - + enum Action { static let inlineReply = "inline-reply" } diff --git a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift index 3741f58ec..4d436d660 100644 --- a/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift +++ b/ElementX/Sources/Services/NotificationSettings/NotificationSettingsProxy.swift @@ -30,7 +30,7 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol { private(set) var notificationSettings: MatrixRustSDK.NotificationSettingsProtocol let callbacks = PassthroughSubject() - + init(notificationSettings: MatrixRustSDK.NotificationSettingsProtocol) { self.notificationSettings = notificationSettings notificationSettings.setDelegate(delegate: WeakNotificationSettingsProxy(proxy: self)) @@ -55,7 +55,7 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol { let roomNotificationMode = await notificationSettings.getDefaultRoomNotificationMode(isEncrypted: isEncrypted, isOneToOne: isOneToOne) return RoomNotificationModeProxy.from(roomNotificationMode: roomNotificationMode) } - + func setDefaultRoomNotificationMode(isEncrypted: Bool, isOneToOne: Bool, mode: RoomNotificationModeProxy) async throws { do { try await notificationSettings.setDefaultRoomNotificationMode(isEncrypted: isEncrypted, isOneToOne: isOneToOne, mode: mode.roomNotificationMode) @@ -65,7 +65,7 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol { MXLog.warning("Unable to find the rule: \(ruleId)") return } - + await updatedSettings() } @@ -73,7 +73,7 @@ final class NotificationSettingsProxy: NotificationSettingsProxyProtocol { try await notificationSettings.restoreDefaultRoomNotificationMode(roomId: roomId) await updatedSettings() } - + func unmuteRoom(roomId: String, isEncrypted: Bool, isOneToOne: Bool) async throws { try await notificationSettings.unmuteRoom(roomId: roomId, isEncrypted: isEncrypted, isOneToOne: isOneToOne) await updatedSettings() diff --git a/ElementX/Sources/Services/Room/BannedRoomProxy.swift b/ElementX/Sources/Services/Room/BannedRoomProxy.swift index 29d867de7..8aa536441 100644 --- a/ElementX/Sources/Services/Room/BannedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/BannedRoomProxy.swift @@ -16,7 +16,7 @@ class BannedRoomProxy: BannedRoomProxyProtocol { lazy var ownUserID = room.ownUserId() let info: BaseRoomInfoProxyProtocol - + init(room: Room) async throws { self.room = room diff --git a/ElementX/Sources/Services/Room/InvitedRoomProxy.swift b/ElementX/Sources/Services/Room/InvitedRoomProxy.swift index 4278ef1e8..a5254d9b7 100644 --- a/ElementX/Sources/Services/Room/InvitedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/InvitedRoomProxy.swift @@ -18,7 +18,7 @@ class InvitedRoomProxy: InvitedRoomProxyProtocol { let info: BaseRoomInfoProxyProtocol let inviter: RoomMemberProxyProtocol? - + init(room: Room) async throws { self.room = room diff --git a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift index 1c88ec87c..700fd3e90 100644 --- a/ElementX/Sources/Services/Room/JoinedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/JoinedRoomProxy.swift @@ -55,7 +55,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { var infoPublisher: CurrentValuePublisher { infoSubject.asCurrentValuePublisher() } - + private let membersSubject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { membersSubject.asCurrentValuePublisher() @@ -119,7 +119,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { } subscribedForUpdates = true - + do { try await roomListService.subscribeToRooms(roomIds: [id]) } catch { @@ -353,7 +353,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { MXLog.error("Failed updating members using sync API: \(error)") } } - + func getMember(userID: String) async -> Result { if let member = membersPublisher.value.filter({ $0.userID == userID }).first { return .success(member) @@ -396,7 +396,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { return .failure(.sdkError(error)) } } - + func setTopic(_ topic: String) async -> Result { do { return try await .success(room.setTopic(topic: topic)) @@ -420,7 +420,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { MXLog.error("Failed uploading avatar, invalid media: \(media)") return .failure(.invalidMedia) } - + do { let data = try Data(contentsOf: imageURL) return try await .success(room.uploadAvatar(mimeType: mimeType, data: data, mediaInfo: nil)) @@ -790,7 +790,7 @@ class JoinedRoomProxy: JoinedRoomProxyProtocol { return .failure(.sdkError(error)) } } - + // MARK: - Private private func subscribeToTypingNotifications() { diff --git a/ElementX/Sources/Services/Room/KnockedRoomProxy.swift b/ElementX/Sources/Services/Room/KnockedRoomProxy.swift index 54ae93cc1..1f45a4b96 100644 --- a/ElementX/Sources/Services/Room/KnockedRoomProxy.swift +++ b/ElementX/Sources/Services/Room/KnockedRoomProxy.swift @@ -16,7 +16,7 @@ class KnockedRoomProxy: KnockedRoomProxyProtocol { lazy var ownUserID: String = room.ownUserId() let info: BaseRoomInfoProxyProtocol - + init(room: Room) async throws { self.room = room diff --git a/ElementX/Sources/Services/Room/RoomInfoProxy.swift b/ElementX/Sources/Services/Room/RoomInfoProxy.swift index e8d14cac0..d87da7a6e 100644 --- a/ElementX/Sources/Services/Room/RoomInfoProxy.swift +++ b/ElementX/Sources/Services/Room/RoomInfoProxy.swift @@ -15,28 +15,28 @@ struct RoomInfoProxy: RoomInfoProxyProtocol { var id: String { roomInfo.id } - + var creators: [String] { roomInfo.creators ?? [] } - + var displayName: String? { roomInfo.displayName } - + var rawName: String? { roomInfo.rawName } - + var topic: String? { roomInfo.topic } - + /// The room's avatar URL. Use this for editing and favour ``avatar`` for display. var avatarURL: URL? { roomInfo.avatarUrl.flatMap(URL.init) } - + /// Here we're assuming unknown rooms are unencrypted. /// Fortunately https://github.com/matrix-org/matrix-rust-sdk/pull/4778 makes that very much of an edge case and we /// also automatically start a `latestEncryptionState` fetch if needed. @@ -53,7 +53,7 @@ struct RoomInfoProxy: RoomInfoProxyProtocol { var isDM: Bool { roomInfo.isDm } - + var isSpace: Bool { roomInfo.isSpace } @@ -61,59 +61,59 @@ struct RoomInfoProxy: RoomInfoProxyProtocol { var successor: SuccessorRoom? { roomInfo.successorRoom } - + var isFavourite: Bool { roomInfo.isFavourite } - + var canonicalAlias: String? { roomInfo.canonicalAlias } - + var alternativeAliases: [String] { roomInfo.alternativeAliases } - + var membership: Membership { roomInfo.membership } - + var inviter: RoomMemberProxyProtocol? { roomInfo.inviter.map(RoomMemberProxy.init) } - + var heroes: [RoomHero] { roomInfo.heroes } - + var activeMembersCount: Int { Int(roomInfo.activeMembersCount) } - + var invitedMembersCount: Int { Int(roomInfo.invitedMembersCount) } - + var joinedMembersCount: Int { Int(roomInfo.joinedMembersCount) } - + var highlightCount: Int { Int(roomInfo.highlightCount) } - + var notificationCount: Int { Int(roomInfo.notificationCount) } - + var cachedUserDefinedNotificationMode: RoomNotificationMode? { roomInfo.cachedUserDefinedNotificationMode } - + var hasRoomCall: Bool { roomInfo.hasRoomCall } - + var activeRoomCallIntent: CallIntent? { switch roomInfo.activeRoomCallConsensusIntent { case .full(let intent): @@ -128,35 +128,35 @@ struct RoomInfoProxy: RoomInfoProxyProtocol { var activeRoomCallParticipants: [String] { roomInfo.activeRoomCallParticipants } - + var isMarkedUnread: Bool { roomInfo.isMarkedUnread } - + var unreadMessagesCount: UInt { UInt(roomInfo.numUnreadMessages) } - + var unreadNotificationsCount: UInt { UInt(roomInfo.numUnreadNotifications) } - + var unreadMentionsCount: UInt { UInt(roomInfo.numUnreadMentions) } - + var fullyReadEventID: String? { roomInfo.fullyReadEventId } - + var pinnedEventIDs: Set { Set(roomInfo.pinnedEventIds) } - + var joinRule: JoinRule? { roomInfo.joinRule.map(JoinRule.init) } - + var historyVisibility: RoomHistoryVisibility { roomInfo.historyVisibility } @@ -174,39 +174,39 @@ struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol { var id: String { roomPreviewInfo.roomId } - + var displayName: String? { roomPreviewInfo.name } - + var heroes: [RoomHero] { roomPreviewInfo.heroes ?? [] } - + var topic: String? { roomPreviewInfo.topic } - + var canonicalAlias: String? { roomPreviewInfo.canonicalAlias } - + var avatarURL: URL? { roomPreviewInfo.avatarUrl.flatMap(URL.init) } - + var isDirect: Bool { roomPreviewInfo.isDirect ?? false } - + var isSpace: Bool { roomPreviewInfo.roomType == .space } - + var activeMembersCount: Int { Int(roomPreviewInfo.numActiveMembers ?? roomPreviewInfo.numJoinedMembers) } - + var joinedMembersCount: Int { Int(roomPreviewInfo.numJoinedMembers) } @@ -214,7 +214,7 @@ struct RoomPreviewInfoProxy: BaseRoomInfoProxyProtocol { var joinRule: JoinRule? { roomPreviewInfo.joinRule.map(JoinRule.init) } - + var membership: Membership? { roomPreviewInfo.membership } diff --git a/ElementX/Sources/Services/Room/RoomInfoProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomInfoProxyProtocol.swift index d487896d3..32201488d 100644 --- a/ElementX/Sources/Services/Room/RoomInfoProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomInfoProxyProtocol.swift @@ -33,7 +33,7 @@ protocol RoomInfoProxyProtocol: BaseRoomInfoProxyProtocol { var topic: String? { get } /// The room's avatar URL. Use this for editing and favour ``avatar`` for display. var avatarURL: URL? { get } - + var isEncrypted: Bool { get } var isDirect: Bool { get } var isDM: Bool { get } diff --git a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift index 8dc81c440..4afb7799f 100644 --- a/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift +++ b/ElementX/Sources/Services/Room/RoomMember/RoomMemberDetails.swift @@ -19,7 +19,7 @@ struct RoomMemberDetails: Identifiable, Hashable { var isIgnored: Bool var isBanned: Bool var isActive: Bool - + let role: RoomRole let powerLevel: RoomPowerLevel diff --git a/ElementX/Sources/Services/Room/RoomPermissions.swift b/ElementX/Sources/Services/Room/RoomPermissions.swift index 825ca0b7e..6bc6dd435 100644 --- a/ElementX/Sources/Services/Room/RoomPermissions.swift +++ b/ElementX/Sources/Services/Room/RoomPermissions.swift @@ -32,7 +32,7 @@ struct RoomPermissionsSetting: Identifiable { value = newValue.powerLevelValue } } - + /// The `RoomPermissions` property that this setting is for. let keyPath: KeyPath /// The `RoomPowerLevelChanges` properties that this setting is saved into. diff --git a/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift index d55b6107e..4a9d32623 100644 --- a/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomPowerLevelProxyProtocol.swift @@ -12,7 +12,7 @@ import MatrixRustSDK protocol RoomPowerLevelsProxyProtocol { var values: RoomPowerLevelsValues { get } var userPowerLevels: [String: Int64] { get } - + func canOwnUser(sendMessage messageType: MessageLikeEventType) -> Bool func canOwnUser(sendStateEvent event: StateEventType) -> Bool func canOwnUserInvite() -> Bool diff --git a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift index 47b010ff6..59126316c 100644 --- a/ElementX/Sources/Services/Room/RoomProxyProtocol.swift +++ b/ElementX/Sources/Services/Room/RoomProxyProtocol.swift @@ -76,7 +76,7 @@ struct RTCDeclinedEvent { // sourcery: AutoMockable protocol JoinedRoomProxyProtocol: RoomProxyProtocol { var infoPublisher: CurrentValuePublisher { get } - + var membersPublisher: CurrentValuePublisher<[RoomMemberProxyProtocol], Never> { get } var typingMembersPublisher: CurrentValuePublisher<[String], Never> { get } @@ -116,11 +116,11 @@ protocol JoinedRoomProxyProtocol: RoomProxyProtocol { func reportContent(_ eventID: String, reason: String?) async -> Result func reportRoom(reason: String) async -> Result - + func leaveRoom() async -> Result func updateMembers() async - + func getMember(userID: String) async -> Result func invite(userID: String) async -> Result @@ -222,7 +222,7 @@ extension JoinedRoomProxyProtocol { isDirect: infoPublisher.value.isDirect, historySharingState: historySharingState) } - + func members() async -> [RoomMemberProxyProtocol]? { await updateMembers() return membersPublisher.value diff --git a/ElementX/Sources/Services/Room/RoomRole.swift b/ElementX/Sources/Services/Room/RoomRole.swift index a32132c83..d2c4b439f 100644 --- a/ElementX/Sources/Services/Room/RoomRole.swift +++ b/ElementX/Sources/Services/Room/RoomRole.swift @@ -49,7 +49,7 @@ extension RoomRole { self = .user } } - + var isAdminOrHigher: Bool { switch self { case .administrator, .creator, .owner: diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift index 71cd2f89f..8ae35985e 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomMessageEventStringBuilder.swift @@ -70,7 +70,7 @@ struct RoomMessageEventStringBuilder { case .other(_, let body): message = AttributedString(body) } - + if style == .senderPrefixed { return prefix(message, with: isOutgoing ? L10n.commonYou : senderDisplayName) } else { @@ -90,7 +90,7 @@ struct RoomMessageEventStringBuilder { return message } } - + private func buildMessage(for style: Style, caption: String?, type: String) -> AttributedString { guard let caption else { return AttributedString(type) diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift index 508493e1b..52e08b662 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummary.swift @@ -31,7 +31,7 @@ struct RoomSummary { } enum LastMessageState { case sending, failed } - + let room: Room let id: String @@ -66,15 +66,15 @@ struct RoomSummary { var hasUnreadMessages: Bool { unreadMessagesCount > 0 } - + var hasUnreadMentions: Bool { unreadMentionsCount > 0 } - + var hasUnreadNotifications: Bool { unreadNotificationsCount > 0 } - + var isMuted: Bool { notificationMode == .mute } diff --git a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift index 0a9491573..2ab3adfb4 100644 --- a/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift +++ b/ElementX/Sources/Services/Room/RoomSummary/RoomSummaryProvider.swift @@ -17,7 +17,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { private let shouldUpdateVisibleRange: Bool private let notificationSettings: NotificationSettingsProxyProtocol private let appSettings: AppSettings - + private let roomListPageSize: UInt32 private let serialDispatchQueue: DispatchQueue @@ -40,7 +40,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { var roomListPublisher: CurrentValuePublisher<[RoomSummary], Never> { roomListSubject.asCurrentValuePublisher() } - + var statePublisher: CurrentValuePublisher { stateSubject.asCurrentValuePublisher() } @@ -233,7 +233,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol { return updatedItems } - + private func fetchRoomDetails(from room: Room) -> (roomInfo: RoomInfo?, latestEvent: LatestEventValue?) { class FetchResult { var roomInfo: RoomInfo? diff --git a/ElementX/Sources/Services/Room/RoomThreadListServiceProxy.swift b/ElementX/Sources/Services/Room/RoomThreadListServiceProxy.swift index e42ef7097..0e31dd4b4 100644 --- a/ElementX/Sources/Services/Room/RoomThreadListServiceProxy.swift +++ b/ElementX/Sources/Services/Room/RoomThreadListServiceProxy.swift @@ -76,7 +76,7 @@ class RoomThreadListProxy: RoomThreadListServiceProxyProtocol { let sender = TimelineItemSender(senderID: threadListItemEvent.sender, senderProfile: threadListItemEvent.senderProfile) let timestamp = Date(timeIntervalSince1970: TimeInterval(threadListItemEvent.timestamp / 1000)) - + let message: AttributedString? = if let content = threadListItemEvent.content { eventStringBuilder.buildAttributedString(for: content, sender: sender, diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift index a51e0345c..0dcab45b2 100644 --- a/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift @@ -154,7 +154,7 @@ class SecureBackupController: SecureBackupControllerProtocol { return .failure(.failedConfirmingRecoveryKey) } } - + func waitForKeyBackupUpload(uploadStateSubject: CurrentValueSubject) async -> Result { do { MXLog.info("Waiting for backup upload steady state") diff --git a/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift b/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift index 86afd113e..1e0aa725d 100644 --- a/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift +++ b/ElementX/Sources/Services/SecureBackup/SecureBackupControllerProtocol.swift @@ -42,7 +42,7 @@ enum SecureBackupControllerError: Error { case failedGeneratingRecoveryKey case failedConfirmingRecoveryKey - + case failedUploadingForBackup } diff --git a/ElementX/Sources/Services/Session/UserSession.swift b/ElementX/Sources/Services/Session/UserSession.swift index dbfdfcd81..158fc6336 100644 --- a/ElementX/Sources/Services/Session/UserSession.swift +++ b/ElementX/Sources/Services/Session/UserSession.swift @@ -13,7 +13,7 @@ class UserSession: UserSessionProtocol { private var cancellables = Set() private var authErrorCancellable: AnyCancellable? - + let clientProxy: ClientProxyProtocol let mediaProvider: MediaProviderProtocol let voiceMessageMediaManager: VoiceMessageMediaManagerProtocol diff --git a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift index 83eed825a..f3ed06711 100644 --- a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift +++ b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxy.swift @@ -95,7 +95,7 @@ class SessionVerificationControllerProxy: SessionVerificationControllerProxyProt return .failure(.failedAcceptingVerificationRequest) } } - + func requestDeviceVerification() async -> Result { MXLog.info("Requesting device verification") diff --git a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift index 2cbd4c7f3..f277bb3ca 100644 --- a/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift +++ b/ElementX/Sources/Services/SessionVerification/SessionVerificationControllerProxyProtocol.swift @@ -54,7 +54,7 @@ protocol SessionVerificationControllerProxyProtocol { func acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) async -> Result func acceptVerificationRequest() async -> Result - + func requestDeviceVerification() async -> Result func requestUserVerification(_ userID: String) async -> Result diff --git a/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift b/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift index 1553253c1..929657815 100644 --- a/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift +++ b/ElementX/Sources/Services/Spaces/LeaveSpaceHandleProxy.swift @@ -32,7 +32,7 @@ final class LeaveSpaceHandleProxy { true } } - + var selectedCount: Int { rooms.count { $0.isSelected } } diff --git a/ElementX/Sources/Services/Spaces/SpaceRoomListProxy.swift b/ElementX/Sources/Services/Spaces/SpaceRoomListProxy.swift index 5346e8b31..16f11d58e 100644 --- a/ElementX/Sources/Services/Spaces/SpaceRoomListProxy.swift +++ b/ElementX/Sources/Services/Spaces/SpaceRoomListProxy.swift @@ -33,17 +33,17 @@ class SpaceRoomListProxy: SpaceRoomListProxyProtocol { init(_ spaceRoomList: SpaceRoomListProtocol) async throws { guard let spaceRoom = spaceRoomList.space() else { throw SpaceRoomListProxyError.missingSpace } - + self.spaceRoomList = spaceRoomList spaceServiceRoomSubject = .init(SpaceServiceRoom(spaceRoom: spaceRoom)) - + let paginationStateSubject = CurrentValueSubject(spaceRoomList.paginationState()) paginationStatePublisher = paginationStateSubject.asCurrentValuePublisher() - + paginationStateHandle = spaceRoomList.subscribeToPaginationStateUpdates(listener: SDKListener { paginationState in paginationStateSubject.send(paginationState) }) - + spaceRoomsHandle = await spaceRoomList.subscribeToRoomUpdate(listener: SDKListener { [weak self] updates in self?.handleUpdates(updates) }) diff --git a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift index 2bee3cb2b..9328a60dc 100644 --- a/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift +++ b/ElementX/Sources/Services/Timeline/Fixtures/RoomTimelineItemFixtures.swift @@ -100,7 +100,7 @@ enum RoomTimelineItemFixtures { TextRoomTimelineItem(text: "How are you?", senderDisplayName: "Alice")] } - + /// A small chunk of events, containing 2 text items. static var smallChunkWithReadReceipts: [RoomTimelineItemProtocol] { [TextRoomTimelineItem(text: "Hey there 👋", @@ -219,17 +219,17 @@ enum RoomTimelineItemFixtures { TextRoomTimelineItem(text: "Pork buffalo mollit culpa strip steak in leberkas flank cow.", senderDisplayName: "Alice")] } - + static var disclosedPolls: [RoomTimelineItemProtocol] { [PollRoomTimelineItem.mock(poll: .disclosed(), isOutgoing: false), PollRoomTimelineItem.mock(poll: .endedDisclosed)] } - + static var undisclosedPolls: [RoomTimelineItemProtocol] { [PollRoomTimelineItem.mock(poll: .undisclosed(), isOutgoing: false), PollRoomTimelineItem.mock(poll: .endedUndisclosed)] } - + static var outgoingPolls: [RoomTimelineItemProtocol] { [PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: true), isOutgoing: true)] } diff --git a/ElementX/Sources/Services/Timeline/GeoURI.swift b/ElementX/Sources/Services/Timeline/GeoURI.swift index 85a0684b0..4e6034107 100644 --- a/ElementX/Sources/Services/Timeline/GeoURI.swift +++ b/ElementX/Sources/Services/Timeline/GeoURI.swift @@ -12,7 +12,7 @@ import Foundation /// A structure that parses a geo URI (i.e. geo:53.99803101552848,-8.25347900390625;u=10) and constructs their constituent parts. struct GeoURI: Hashable { // MARK: - Properties - + let latitude: Double let longitude: Double let uncertainty: Double? @@ -20,9 +20,9 @@ struct GeoURI: Hashable { var bodyMessage: String { "Location was shared at \(string)" } - + // MARK: - Setup - + init?(string: String) { guard let parsedURI = Self.parseGeoURI(from: string) else { MXLog.warning("\(Self.self) failed to parse the string: \(string)") @@ -30,13 +30,13 @@ struct GeoURI: Hashable { } self = parsedURI } - + init(latitude: Double, longitude: Double, uncertainty: Double? = nil) { self.latitude = latitude self.longitude = longitude self.uncertainty = uncertainty } - + var string: String { if let uncertainty { return "geo:\(string(for: latitude)),\(string(for: longitude));u=\(string(for: uncertainty))" @@ -44,9 +44,9 @@ struct GeoURI: Hashable { return "geo:\(string(for: latitude)),\(string(for: longitude))" } } - + // MARK: - Private - + /// Parse a geo URI string like "geo:53.99803101552848,-8.25347900390625;u=10" private static func parseGeoURI(from string: String) -> GeoURI? { guard @@ -56,11 +56,11 @@ struct GeoURI: Hashable { else { return nil } - + let uncertainty = matchOutput.uncertainty.flatMap(Double.init) return .init(latitude: latitude, longitude: longitude, uncertainty: uncertainty) } - + private func string(for number: Double) -> String { NumberFormatter.decimal.string(from: .init(floatLiteral: number)) ?? "\(number)" } diff --git a/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift index b85f5cba9..4d398fea4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/MockTimelineController.swift @@ -27,7 +27,7 @@ class MockTimelineController: TimelineControllerProtocol { var roomID: String { roomProxy?.id ?? "MockRoomIdentifier" } - + var timelineKind: TimelineKind let callbacks = PassthroughSubject() @@ -43,7 +43,7 @@ class MockTimelineController: TimelineControllerProtocol { callbacks.send(.updatedTimelineItems(timelineItems: timelineItems, isSwitchingTimelines: false)) } } - + var timelineItemsTimestamp: [TimelineItemIdentifier: Date] = [:] private var client: UITestsSignalling.Client? @@ -120,7 +120,7 @@ class MockTimelineController: TimelineControllerProtocol { func processItemAppearance(_ itemID: TimelineItemIdentifier) async { } func processItemDisappearance(_ itemID: TimelineItemIdentifier) async { } - + func toggleReaction(_ reaction: String, to eventID: TimelineItemIdentifier.EventOrTransactionID) async { if let timelineProxy { _ = await timelineProxy.toggleReaction(reaction, to: eventID) @@ -173,7 +173,7 @@ class MockTimelineController: TimelineControllerProtocol { func sendHandle(for itemID: TimelineItemIdentifier) -> SendHandleProxy? { nil } - + func eventTimestamp(for itemID: TimelineItemIdentifier) -> Date? { timelineItemsTimestamp[itemID] ?? .now } @@ -328,7 +328,7 @@ class MockTimelineController: TimelineControllerProtocol { } return .success(()) } - + // MARK: - UI Test signalling /// The cancellable used for UI Tests signalling. diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift index 106f7fadd..91e783b23 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineController.swift @@ -35,7 +35,7 @@ class TimelineController: TimelineControllerProtocol { callbacks.send(.paginationState(paginationState)) } } - + var roomID: String { roomProxy.id } @@ -128,7 +128,7 @@ class TimelineController: TimelineControllerProtocol { guard let eventID = itemID.eventID else { return } - + _ = await activeTimeline.sendReadReceipt(for: eventID, type: receiptType) } @@ -571,7 +571,7 @@ class TimelineController: TimelineControllerProtocol { guard let eventID = timelineItem.id.eventID else { return } - + switch timelineItem.properties.replyDetails { case .notLoaded: activeTimeline.fetchDetails(for: eventID) diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactory.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactory.swift index f2181ea59..6b07d1d2f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerFactory.swift @@ -11,7 +11,7 @@ import MatrixRustSDK struct TimelineControllerFactory: TimelineControllerFactoryProtocol { let appSettings: AppSettings - + func buildTimelineController(roomProxy: JoinedRoomProxyProtocol, initialFocussedEventID: String?, timelineItemFactory: RoomTimelineItemFactoryProtocol, diff --git a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift index 0ff70ad0f..786d0c53a 100644 --- a/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineController/TimelineControllerProtocol.swift @@ -80,7 +80,7 @@ protocol TimelineControllerProtocol: Sendable { func removeCaption(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID) async func toggleReaction(_ reaction: String, to eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID) async - + func redact(_ eventOrTransactionID: TimelineItemIdentifier.EventOrTransactionID) async func pin(eventID: String) async diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift b/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift index 8205cebc1..725e8447c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProvider.swift @@ -12,58 +12,58 @@ import MatrixRustSDK class TimelineItemProvider: TimelineItemProviderProtocol { private var cancellables = Set() - + private var roomTimelineObservationToken: TaskHandle? - + /// Bridge from the SDK's synchronous callback into Swift Concurrency. Yielding is safe from any /// thread; a single long-lived `for await` consumer (set up in `init`) applies the diffs on the /// main actor in FIFO order, guaranteeing one in-flight update at a time. private let diffContinuation: AsyncStream<[TimelineDiff]>.Continuation - + private let paginationStateSubject = CurrentValueSubject(.initial) var paginationState: TimelinePaginationState { paginationStateSubject.value } - + private let itemProxiesSubject: CurrentValueSubject<[TimelineItemProxy], Never> private(set) var itemProxies: [TimelineItemProxy] = [] { didSet { itemProxiesSubject.send(itemProxies) } } - + var updatePublisher: AnyPublisher<([TimelineItemProxy], TimelinePaginationState), Never> { itemProxiesSubject .combineLatest(paginationStateSubject) .eraseToAnyPublisher() } - + let kind: TimelineKind - + private let membershipChangeSubject = PassthroughSubject() var membershipChangePublisher: AnyPublisher { membershipChangeSubject .eraseToAnyPublisher() } - + deinit { diffContinuation.finish() roomTimelineObservationToken?.cancel() } - + init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher) { itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([]) self.kind = kind - + let (diffStream, diffContinuation) = AsyncStream<[TimelineDiff]>.makeStream() self.diffContinuation = diffContinuation - + paginationStatePublisher .sink { [weak self] in self?.paginationStateSubject.send($0) } .store(in: &cancellables) - + // `for await` guarantees FIFO ordering and that only one diff is being applied at a time. // The stream also allows to handle the sendable items in the listener. Task { [weak self] in @@ -71,7 +71,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol { self?.updateItemsWithDiffs(diffs) } } - + Task { roomTimelineObservationToken = await timeline.addListener(listener: SDKListener { diffs in diffContinuation.yield(diffs) @@ -128,11 +128,11 @@ class TimelineItemProvider: TimelineItemProviderProtocol { changes.append(.insert(offset: Int(index), element: itemProxy, associatedWith: nil)) case .popBack: guard let itemProxy = itemProxies.last else { fatalError() } - + changes.append(.remove(offset: itemProxies.count - 1, element: itemProxy, associatedWith: nil)) case .popFront: guard let itemProxy = itemProxies.first else { fatalError() } - + changes.append(.remove(offset: 0, element: itemProxy, associatedWith: nil)) case .pushBack(let item): let itemProxy = TimelineItemProxy(item: item) @@ -154,7 +154,7 @@ class TimelineItemProvider: TimelineItemProviderProtocol { for (index, itemProxy) in itemProxies.enumerated() { changes.append(.remove(offset: index, element: itemProxy, associatedWith: nil)) } - + for (index, timelineItem) in items.enumerated() { changes.append(.insert(offset: index, element: TimelineItemProxy(item: timelineItem), associatedWith: nil)) } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift index 4c4fc33b5..82fa09d1f 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemProxy.swift @@ -101,11 +101,11 @@ class EventTimelineItemProxy { }() lazy var canBeRepliedTo = item.canBeRepliedTo - + lazy var content = item.content - + lazy var isOwn = item.isOwn - + lazy var isEditable = item.isEditable lazy var sender = TimelineItemSender(senderID: item.sender, senderProfile: item.senderProfile) @@ -192,11 +192,11 @@ struct SendHandleProxy: Hashable { } // MARK: - Hashable - + static func == (lhs: SendHandleProxy, rhs: SendHandleProxy) -> Bool { lhs.itemID == rhs.itemID } - + func hash(into hasher: inout Hasher) { hasher.combine(itemID) } diff --git a/ElementX/Sources/Services/Timeline/TimelineItemSender.swift b/ElementX/Sources/Services/Timeline/TimelineItemSender.swift index f3208744f..6eb11f1f4 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItemSender.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItemSender.swift @@ -38,7 +38,7 @@ struct TimelineItemSender: Identifiable, Hashable { avatarURL: nil) } } - + var disambiguatedDisplayName: String? { guard let displayName else { return nil diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift index 960eafbde..49f79fb2c 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/EventBasedTimelineItemProtocol.swift @@ -26,11 +26,11 @@ extension EventBasedTimelineItemProtocol { var description: String { "\(String(describing: Self.self)): id: \(id), timestamp: \(timestamp), isOutgoing: \(isOutgoing), properties: \(properties)" } - + var isForwardable: Bool { isRemoteMessage && !(self is PollRoomTimelineItem) } - + var isRemoteMessage: Bool { id.eventID != nil } @@ -50,11 +50,11 @@ extension EventBasedTimelineItemProtocol { var hasFailedToSend: Bool { properties.deliveryStatus?.isSendingFailed == true } - + var hasFailedDecryption: Bool { self is EncryptedRoomTimelineItem } - + var timelineMenuDescription: String { switch self { case is VoiceMessageRoomTimelineItem: @@ -63,7 +63,7 @@ extension EventBasedTimelineItemProtocol { return body.trimmingCharacters(in: .whitespacesAndNewlines) } } - + /// The inline area to reserve at the trailing edge of the message text so the /// bubble's natural size accommodates the timestamp (which is overlaid on the /// bubble's bottom-trailing corner). Text wraps around this area: if it fits on @@ -86,7 +86,7 @@ extension EventBasedTimelineItemProtocol { width += 4 return CGSize(width: ceil(width), height: ceil(height)) } - + /// contains the timestamp and an optional edited localised prefix /// example: (edited) 12:17 PM var localizedSendInfo: String { @@ -96,12 +96,12 @@ extension EventBasedTimelineItemProtocol { } return start + timestamp.formattedTime() } - + var isCopyable: Bool { guard let messageBasedItem = self as? EventBasedMessageTimelineItemProtocol else { return false } - + switch messageBasedItem.contentType { case .audio, .file, .image, .video, .location, .voice: return false diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift index f26857185..4a99b68ba 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/AudioRoomTimelineItem.swift @@ -19,7 +19,7 @@ struct AudioRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { let sender: TimelineItemSender let content: AudioRoomTimelineItemContent - + var properties = RoomTimelineItemProperties() var body: String { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LiveLocationRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LiveLocationRoomTimelineItem.swift index 2fd0b7b3f..f138f4c2b 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LiveLocationRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LiveLocationRoomTimelineItem.swift @@ -14,7 +14,7 @@ struct LiveLocationRoomTimelineItem: EventBasedTimelineItemProtocol, Equatable { var body: String { "" } - + let timestamp: Date let isOutgoing: Bool let isEditable: Bool diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift index 504dbb563..d02da8e99 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItem.swift @@ -16,15 +16,15 @@ struct LocationRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatabl let canBeRepliedTo: Bool let sender: TimelineItemSender - + let content: LocationRoomTimelineItemContent var properties = RoomTimelineItemProperties() - + var body: String { content.body } - + var contentType: EventBasedMessageTimelineItemContentType { .location(content) } diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItemContent.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItemContent.swift index 69127e717..fc6d94ef5 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItemContent.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/LocationRoomTimelineItemContent.swift @@ -12,7 +12,7 @@ struct LocationRoomTimelineItemContent: Hashable { let body: String let geoURI: GeoURI? let kind: StaticLocationKind - + init(body: String, geoURI: GeoURI? = nil, kind: StaticLocationKind = .sender) { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift index b59f9a409..e2c1c9f84 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VideoRoomTimelineItem.swift @@ -18,7 +18,7 @@ struct VideoRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equatable { var shouldBoost = false let sender: TimelineItemSender - + let content: VideoRoomTimelineItemContent var properties = RoomTimelineItemProperties() diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift index 98cb12ab3..bdb0cbbdc 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Messages/VoiceMessageRoomTimelineItem.swift @@ -18,7 +18,7 @@ struct VoiceMessageRoomTimelineItem: EventBasedMessageTimelineItemProtocol, Equa let sender: TimelineItemSender let content: AudioRoomTimelineItemContent - + var properties = RoomTimelineItemProperties() var body: String { diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift index 3ddd2b705..0c1a22986 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/Items/Other/PollRoomTimelineItem.swift @@ -29,11 +29,11 @@ struct Poll: Hashable { let endDate: Date? /// Whether the poll has been created by the account owner let createdByAccountOwner: Bool - + var hasEnded: Bool { endDate != nil } - + enum Kind: Hashable { case disclosed case undisclosed @@ -42,7 +42,7 @@ struct Poll: Hashable { var hasMaxSelections: Bool { options.filter(\.isSelected).count == maxSelections } - + struct Option: Hashable { let id: String let text: String diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift index c0eca6a4f..84694db09 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemFactory.swift @@ -384,9 +384,9 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { let allVotes = votes.reduce(0) { count, pair in count + pair.value.count } - + let maxOptionVotes = votes.map(\.value.count).max() - + let options = answers.map { answer in let optionVotesCount = votes[answer.id]?.count @@ -404,7 +404,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { case .undisclosed: .undisclosed } - + let poll = Poll(question: question, kind: pollKind, maxSelections: Int(maxSelections), @@ -412,7 +412,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { votes: votes, endDate: endTime.map { Date(timeIntervalSince1970: TimeInterval($0 / 1000)) }, createdByAccountOwner: eventItemProxy.sender.id == userID) - + return PollRoomTimelineItem(id: eventItemProxy.id, poll: poll, body: poll.question, @@ -538,7 +538,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { if let audioWaveform = messageContent.audio?.waveform { waveform = EstimatedWaveform(data: audioWaveform) } - + return AudioRoomTimelineItemContent(filename: messageContent.filename, caption: messageContent.caption, formattedCaption: formattedCaption, @@ -602,13 +602,13 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { blurhash: messageContent.info?.blurhash, contentType: UTType(mimeType: messageContent.info?.mimetype, fallbackFilename: messageContent.filename)) } - + private func buildLocationTimelineItemContent(_ locationContent: LocationContent) -> LocationRoomTimelineItemContent { LocationRoomTimelineItemContent(body: locationContent.body, geoURI: .init(string: locationContent.geoUri), kind: .init(from: locationContent.asset)) } - + private func buildFileTimelineItemContent(_ messageContent: FileMessageContent) -> FileRoomTimelineItemContent { let htmlCaption = messageContent.formattedCaption?.format == .html ? messageContent.formattedCaption?.body : nil let formattedCaption = htmlCaption != nil ? attributedStringBuilder.fromHTML(htmlCaption) : attributedStringBuilder.fromPlain(messageContent.caption) @@ -636,7 +636,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { let name = senderDisplayName ?? senderID let htmlBody = messageContent.formatted?.format == .html ? messageContent.formatted?.body : nil - + var formattedBody: AttributedString? if let htmlBody { formattedBody = buildEmoteFormattedBodyFromHTML(html: htmlBody, name: name) @@ -686,7 +686,7 @@ struct RoomTimelineItemFactory: RoomTimelineItemFactoryProtocol { return a.count > b.count } } - + private func buildOrderedReadReceipts(_ receipts: [String: Receipt]) -> [ReadReceipt] { receipts .sorted { firstElement, secondElement in diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift index 72250e989..7740e88ae 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemView.swift @@ -25,7 +25,7 @@ struct RoomTimelineItemView: View { context?.send(viewAction: .itemDisappeared(itemID: viewState.identifier)) } } - + @ViewBuilder private var timelineView: some View { switch viewState.type { case .text(let item): diff --git a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift index b0709f6fd..ebb358a2e 100644 --- a/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift +++ b/ElementX/Sources/Services/Timeline/TimelineItems/RoomTimelineItemViewState.swift @@ -12,17 +12,17 @@ import MatrixRustSDK final class RoomTimelineItemViewState: Identifiable, Equatable, ObservableObject { @Published var type: RoomTimelineItemType @Published var groupStyle: TimelineGroupStyle - + /// Contains all the identification info of the item, `uniqueID`, `eventID` and `transactionID` var identifier: TimelineItemIdentifier { type.id } - + init(type: RoomTimelineItemType, groupStyle: TimelineGroupStyle) { self.type = type self.groupStyle = groupStyle } - + convenience init(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) { self.init(type: .init(item: item), groupStyle: groupStyle) } @@ -70,7 +70,7 @@ enum RoomTimelineItemType: Equatable { case callInvite(CallInviteRoomTimelineItem) case callNotification(CallNotificationRoomTimelineItem) case liveLocation(LiveLocationRoomTimelineItem) - + init(item: RoomTimelineItemProtocol) { switch item { case let item as TextRoomTimelineItem: @@ -123,7 +123,7 @@ enum RoomTimelineItemType: Equatable { fatalError("Unknown timeline item") } } - + var id: TimelineItemIdentifier { switch self { case .text(let item as RoomTimelineItemProtocol), diff --git a/ElementX/Sources/Services/Timeline/TimelineProxy.swift b/ElementX/Sources/Services/Timeline/TimelineProxy.swift index 9ece45dc5..f893bcad6 100644 --- a/ElementX/Sources/Services/Timeline/TimelineProxy.swift +++ b/ElementX/Sources/Services/Timeline/TimelineProxy.swift @@ -24,7 +24,7 @@ final class TimelineProxy: TimelineProxyProtocol { private let forwardPaginationStateSubject = CurrentValueSubject(.endReached) private let kind: TimelineKind - + private var innerTimelineItemProvider: TimelineItemProviderProtocol! var timelineItemProvider: TimelineItemProviderProtocol { innerTimelineItemProvider @@ -146,7 +146,7 @@ final class TimelineProxy: TimelineProxyProtocol { case .forwards: try await timeline.paginateForwards(numEvents: requestSize) } MXLog.info("Finished paginating \(direction.rawValue)") - + subject.send(timelineEndReached ? .endReached : .idle) return .success(()) } catch { @@ -170,7 +170,7 @@ final class TimelineProxy: TimelineProxyProtocol { try await timeline.edit(eventOrTransactionId: eventOrTransactionID.rustValue, newContent: newContent) MXLog.info("Finished editing timeline item: \(eventOrTransactionID)") - + return .success(()) } catch { MXLog.error("Failed editing timeline item: \(eventOrTransactionID) with error: \(error)") @@ -412,10 +412,10 @@ final class TimelineProxy: TimelineProxyProtocol { } else { MXLog.error("Failed sending message with error: \(error)") } - + return .failure(.sdkError(error)) } - + return .success(()) } @@ -473,7 +473,7 @@ final class TimelineProxy: TimelineProxyProtocol { } // MARK: - Polls - + func createPoll(question: String, answers: [String], pollKind: Poll.Kind) async -> Result { MXLog.info("Creating poll") @@ -528,7 +528,7 @@ final class TimelineProxy: TimelineProxyProtocol { return .failure(.sdkError(error)) } } - + func sendPollResponse(pollStartID: String, answers: [String]) async -> Result { MXLog.info("Sending response for poll with eventID: \(pollStartID)") @@ -543,7 +543,7 @@ final class TimelineProxy: TimelineProxyProtocol { return .failure(.sdkError(error)) } } - + func buildMessageContentFor(_ message: String, html: String?, intentionalMentions: Mentions) -> RoomMessageEventContentWithoutRelation { diff --git a/ElementX/Sources/Services/UserSession/SessionDirectories.swift b/ElementX/Sources/Services/UserSession/SessionDirectories.swift index 12bbece08..f96ddfacb 100644 --- a/ElementX/Sources/Services/UserSession/SessionDirectories.swift +++ b/ElementX/Sources/Services/UserSession/SessionDirectories.swift @@ -15,7 +15,7 @@ struct SessionDirectories: Hashable, Codable { var dataPath: String { dataDirectory.path(percentEncoded: false) } - + var cachePath: String { cacheDirectory.path(percentEncoded: false) } diff --git a/ElementX/Sources/Services/UserSession/UserSessionStore.swift b/ElementX/Sources/Services/UserSession/UserSessionStore.swift index dd8ceb4e0..2b53332e3 100644 --- a/ElementX/Sources/Services/UserSession/UserSessionStore.swift +++ b/ElementX/Sources/Services/UserSession/UserSessionStore.swift @@ -20,7 +20,7 @@ class UserSessionStore: UserSessionStoreProtocol { var hasSessions: Bool { !keychainController.restorationTokens().isEmpty } - + /// All the user IDs managed by the store. var userIDs: [String] { keychainController.restorationTokens().map(\.userID) @@ -100,7 +100,7 @@ class UserSessionStore: UserSessionStoreProtocol { credentials.restorationToken.sessionDirectories.delete() } } - + // MARK: - Private private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) async -> UserSessionProtocol { diff --git a/ElementX/Sources/Services/Users/UserDiscoveryService.swift b/ElementX/Sources/Services/Users/UserDiscoveryService.swift index ede565516..c936bd6fa 100644 --- a/ElementX/Sources/Services/Users/UserDiscoveryService.swift +++ b/ElementX/Sources/Services/Users/UserDiscoveryService.swift @@ -14,10 +14,10 @@ final class UserDiscoveryService: UserDiscoveryServiceProtocol { init(clientProxy: ClientProxyProtocol) { self.clientProxy = clientProxy } - + func searchProfiles(with searchQuery: String) async -> Result<[UserProfileProxy], UserDiscoveryErrorType> { async let queriedProfile = profileIfPossible(with: searchQuery) - + do { async let searchedUsers = clientProxy.searchUsers(searchTerm: searchQuery, limit: 10).get() let users = try await merge(queriedProfile: queriedProfile, searchResults: searchedUsers) @@ -31,18 +31,18 @@ final class UserDiscoveryService: UserDiscoveryServiceProtocol { } } } - + private func merge(queriedProfile: UserProfileProxy?, searchResults: SearchUsersResultsProxy) -> [UserProfileProxy] { let searchResults = searchResults.results guard let queriedProfile else { return searchResults } - + let filteredSearchResult = searchResults.filter { $0.userID != queriedProfile.userID } - + return [queriedProfile] + filteredSearchResult } @@ -56,7 +56,7 @@ final class UserDiscoveryService: UserDiscoveryServiceProtocol { // fallback to a "local profile" if the profile api fails return getProfileResult ?? .init(userID: searchQuery) } - + private func filterAccountOwner(_ profiles: [UserProfileProxy]) -> [UserProfileProxy] { let accountOwnerID = clientProxy.userID return profiles.filter { $0.userID != accountOwnerID } diff --git a/ElementX/Sources/Services/VoiceMessage/VoiceMessageCache.swift b/ElementX/Sources/Services/VoiceMessage/VoiceMessageCache.swift index 0253f470f..ab79322d4 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageCache.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageCache.swift @@ -38,7 +38,7 @@ class VoiceMessageCache: VoiceMessageCacheProtocol { } return .success(url) } - + func clearCache() { if FileManager.default.fileExists(atPath: temporaryFilesFolderURL.path) { do { diff --git a/ElementX/Sources/Services/VoiceMessage/VoiceMessageCacheProtocol.swift b/ElementX/Sources/Services/VoiceMessage/VoiceMessageCacheProtocol.swift index b6b00f342..586af014a 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageCacheProtocol.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageCacheProtocol.swift @@ -29,7 +29,7 @@ protocol VoiceMessageCacheProtocol { /// - move: wheter to move or copy the source file /// - Returns: the cached URL func cache(mediaSource: MediaSourceProxy, using fileURL: URL, move: Bool) -> Result - + /// Clears the cache func clearCache() } diff --git a/ElementX/Sources/Services/VoiceMessage/VoiceMessageMediaManager.swift b/ElementX/Sources/Services/VoiceMessage/VoiceMessageMediaManager.swift index 785168d86..607283133 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageMediaManager.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageMediaManager.swift @@ -36,7 +36,7 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol { self.audioConverter = audioConverter self.processingQueue = processingQueue } - + deinit { voiceMessageCache.clearCache() } @@ -50,7 +50,7 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol { if let fileURL = voiceMessageCache.fileURL(for: source) { return fileURL } - + // Otherwise, load the file from source guard case .success(let fileHandle) = await mediaProvider.loadFileFromSource(source, filename: body) else { throw MediaProviderError.failedRetrievingFile @@ -61,14 +61,14 @@ class VoiceMessageMediaManager: VoiceMessageMediaManagerProtocol { if let fileURL = voiceMessageCache.fileURL(for: source) { return fileURL } - + // Convert from ogg guard let url = fileHandle.url else { throw VoiceMessageMediaManagerError.missingURL } let convertedFileURL = URL.temporaryDirectory.appendingPathComponent(url.deletingPathExtension().lastPathComponent).appendingPathExtension(AudioConverterPreferredFileExtension.mpeg4aac.rawValue) try audioConverter.convertToMPEG4AAC(sourceURL: url, destinationURL: convertedFileURL) - + // Cache the file and return the url let result = voiceMessageCache.cache(mediaSource: source, using: convertedFileURL, move: true) switch result { diff --git a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift index f83ac2c03..7c95affa1 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorder.swift @@ -20,7 +20,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { var actions: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + private let mp4accMimeType = "audio/m4a" var isRecording: Bool { @@ -36,11 +36,11 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { } private var recordingCancelled = false - + private(set) var previewAudioPlayerState: AudioPlayerState? private(set) var previewAudioPlayer: AudioPlayerProtocol? private var cancellables = Set() - + init(audioRecorder: AudioRecorderProtocol = AudioRecorder(), mediaPlayerProvider: MediaPlayerProviderProtocol, voiceMessageCache: VoiceMessageCacheProtocol = VoiceMessageCache()) { @@ -86,7 +86,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { previewAudioPlayer?.reset() previewAudioPlayerState = nil } - + // MARK: - Preview func startPlayback() async -> Result { @@ -158,13 +158,13 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { // delete the temporary file try? FileManager.default.removeItem(at: oggFile) } - + do { try audioConverter.convertToOpusOgg(sourceURL: url, destinationURL: oggFile) } catch { return .failure(.failedSendingVoiceMessage) } - + // send it let size: UInt64 do { @@ -189,7 +189,7 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { return .success(()) } - + // MARK: - Private private func addObservers() { @@ -237,10 +237,10 @@ class VoiceMessageRecorder: VoiceMessageRecorderProtocol { guard audioRecorder.audioFileURL != nil, audioRecorder.currentTime > 0 else { return .failure(.previewNotAvailable) } - + // Build the preview audio player state previewAudioPlayerState = await AudioPlayerState(id: .recorderPreview, title: L10n.commonVoiceMessage, duration: recordingDuration, waveform: EstimatedWaveform(data: [])) - + // Build the preview audio player let audioPlayer = await mediaPlayerProvider.player previewAudioPlayer = audioPlayer diff --git a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorderProtocol.swift b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorderProtocol.swift index 2772a20ce..2b91da7ed 100644 --- a/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorderProtocol.swift +++ b/ElementX/Sources/Services/VoiceMessage/VoiceMessageRecorderProtocol.swift @@ -27,9 +27,9 @@ protocol VoiceMessageRecorderProtocol { var previewAudioPlayerState: AudioPlayerState? { get } var isRecording: Bool { get } var recordingURL: URL? { get } - + var actions: AnyPublisher { get } - + func startRecording() async func stopRecording() async func cancelRecording() async diff --git a/ElementX/Sources/UITests/UITestsAppCoordinator.swift b/ElementX/Sources/UITests/UITestsAppCoordinator.swift index f8dc3209d..bacc2cab1 100644 --- a/ElementX/Sources/UITests/UITestsAppCoordinator.swift +++ b/ElementX/Sources/UITests/UITestsAppCoordinator.swift @@ -439,7 +439,7 @@ class MockScreen: Identifiable { composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) - + navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomLayoutMiddle: @@ -529,7 +529,7 @@ class MockScreen: Identifiable { return navigationStackCoordinator case .roomWithDisclosedPolls: let navigationStackCoordinator = NavigationStackCoordinator() - + let timelineController = MockTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.disclosedPolls timelineController.incomingItems = [] @@ -548,12 +548,12 @@ class MockScreen: Identifiable { composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) - + navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomWithUndisclosedPolls: let navigationStackCoordinator = NavigationStackCoordinator() - + let timelineController = MockTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.undisclosedPolls timelineController.incomingItems = [] @@ -572,12 +572,12 @@ class MockScreen: Identifiable { composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) - + navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .roomWithOutgoingPolls: let navigationStackCoordinator = NavigationStackCoordinator() - + let timelineController = MockTimelineController() timelineController.timelineItems = RoomTimelineItemFixtures.outgoingPolls timelineController.incomingItems = [] @@ -596,7 +596,7 @@ class MockScreen: Identifiable { composerDraftService: ComposerDraftServiceMock(.init()), timelineControllerFactory: TimelineControllerFactoryMock(.init()), userIndicatorController: UserIndicatorControllerMock()) let coordinator = RoomScreenCoordinator(parameters: parameters) - + navigationStackCoordinator.setRootCoordinator(coordinator) return navigationStackCoordinator case .sessionVerification: @@ -632,7 +632,7 @@ class MockScreen: Identifiable { let appMediator = AppMediatorMock(.init()) appMediator.underlyingWindowManager = windowManager - + let flowCoordinator = UserSessionFlowCoordinator(isNewLogin: false, navigationRootCoordinator: navigationRootCoordinator, appLockService: AppLockService(keychainController: KeychainControllerMock(), @@ -650,7 +650,7 @@ class MockScreen: Identifiable { userIndicatorController: UserIndicatorControllerMock(), notificationManager: NotificationManagerMock(), stateMachineFactory: StateMachineFactory())) - + flowCoordinator.start() retainedState.append(flowCoordinator) @@ -761,7 +761,7 @@ class MockScreen: Identifiable { userIndicatorController: userIndicatorController, navigationStackCoordinator: navigationStackCoordinator, windowManger: windowManager)) - + retainedState.append(coordinator) coordinator.start() @@ -844,7 +844,7 @@ class MockScreen: Identifiable { userIndicatorController: UserIndicatorControllerMock(), notificationManager: NotificationManagerMock(), stateMachineFactory: StateMachineFactory())) - + flowCoordinator.start() retainedState.append(flowCoordinator) diff --git a/ElementX/Sources/UITests/UITestsSignalling.swift b/ElementX/Sources/UITests/UITestsSignalling.swift index e3cac5c44..fb35a533b 100644 --- a/ElementX/Sources/UITests/UITestsSignalling.swift +++ b/ElementX/Sources/UITests/UITestsSignalling.swift @@ -115,12 +115,12 @@ enum UITestsSignalling { await _ = signals.values.first { $0 == .ready } NSLog("UITestsSignalling: Connected to app.") } - + /// Stops listening for signals. func stop() throws { try fileWatcher.stop() } - + /// Sends a signal. func send(_ signal: UITestsSignal) throws { guard isConnected else { throw UITestsSignalError.notConnected } diff --git a/Enterprise b/Enterprise index 8bbd9a530..fb9a31514 160000 --- a/Enterprise +++ b/Enterprise @@ -1 +1 @@ -Subproject commit 8bbd9a5301609b3b4448c7027434330b6c516c76 +Subproject commit fb9a31514968c444c87ab4801f9aa69d25f1d269 diff --git a/IntegrationTests/Sources/Common.swift b/IntegrationTests/Sources/Common.swift index 5f417a5e7..f7ea5e9b6 100644 --- a/IntegrationTests/Sources/Common.swift +++ b/IntegrationTests/Sources/Common.swift @@ -74,7 +74,7 @@ extension XCUIApplication { let webAuthenticationView = XCUIApplication(bundleIdentifier: "com.apple.SafariViewService") XCTAssertTrue(webAuthenticationView.waitForExistence(timeout: 10.0)) webAuthenticationView.tap(.top) // Tap the web view to properly focus the app again. - + // The user may already be authenticated on MAS. Sign them out. // The button label varies by MAS version: "Sign out" or "Use another account". let webLogoutPredicate = NSPredicate(format: "label == 'Sign out' OR label == 'Use another account'") @@ -113,7 +113,7 @@ extension XCUIApplication { // Wait for login to finish currentTestCase.expectation(for: doesNotExistPredicate, evaluatedWith: webUsernameTextField) currentTestCase.waitForExpectations(timeout: 300.0) - + // Wait for the home screen to become visible. let profileButton = buttons[A11yIdentifiers.homeScreen.userAvatar] // Timeouts are huge because we're waiting for the server. @@ -126,7 +126,7 @@ extension XCUIApplication { swipeDown() let profileButton = buttons[A11yIdentifiers.homeScreen.userAvatar] - + // `Failed to scroll to visible (by AX action) Button` https://stackoverflow.com/a/33534187/730924 profileButton.tap(.center) diff --git a/IntegrationTests/Sources/UserFlowTests.swift b/IntegrationTests/Sources/UserFlowTests.swift index 239864066..9a9308f9f 100644 --- a/IntegrationTests/Sources/UserFlowTests.swift +++ b/IntegrationTests/Sources/UserFlowTests.swift @@ -43,7 +43,7 @@ class UserFlowTests: XCTestCase { firstRoom.tap(.center) sendMessages() - + checkTimelineItemActionMenu() // Go back to the room list diff --git a/NSE/Sources/BootDetectionManager.swift b/NSE/Sources/BootDetectionManager.swift index 7bb91a6db..b307db8d4 100644 --- a/NSE/Sources/BootDetectionManager.swift +++ b/NSE/Sources/BootDetectionManager.swift @@ -17,11 +17,11 @@ enum BootDetectionManager { guard let dummyData = dummyString.data(using: .utf8) else { return true } - + do { // add a unique filename let url = containerURL.appendingPathComponent(ProcessInfo.processInfo.globallyUniqueString) - + try dummyData.write(to: url, options: .completeFileProtectionUntilFirstUserAuthentication) let readData = try Data(contentsOf: url) let readString = String(data: readData, encoding: .utf8) diff --git a/NSE/Sources/CommonSettings+NotificationName.swift b/NSE/Sources/CommonSettings+NotificationName.swift index 4a90e58ae..6cb2b529d 100644 --- a/NSE/Sources/CommonSettings+NotificationName.swift +++ b/NSE/Sources/CommonSettings+NotificationName.swift @@ -17,7 +17,7 @@ extension CommonSettingsProtocol { return UNNotificationSoundName(NotificationToneManager.selectedToneFilename) } } - + /// A `UNNotificationSound` built from `notificationSoundName`, ready to attach to a notification content object. var notificationSound: UNNotificationSound { UNNotificationSound(named: notificationSoundName) diff --git a/NSE/Sources/NSEUserSession.swift b/NSE/Sources/NSEUserSession.swift index 97a728ed9..4510131b2 100644 --- a/NSE/Sources/NSEUserSession.swift +++ b/NSE/Sources/NSEUserSession.swift @@ -55,7 +55,7 @@ final class NSEUserSession: NSEUserSessionProtocol { var threadsEnabled: Bool { appSettings.threadsEnabled } - + init(credentials: KeychainCredentials, roomID: String, clientSessionDelegate: ClientSessionDelegate, @@ -95,7 +95,7 @@ final class NSEUserSession: NSEUserSessionProtocol { func notificationItemProxy(roomID: String, eventID: String) async -> NotificationItemProxyProtocol? { do { let notificationStatus = try await notificationClient.getNotification(roomId: roomID, eventId: eventID) - + switch notificationStatus { case .event(let notification): return NotificationItemProxy(notificationItem: notification, @@ -134,7 +134,7 @@ final class NSEUserSession: NSEUserSessionProtocol { private final class ClientDelegateWrapper: ClientDelegate { // MARK: - ClientDelegate - + func didReceiveAuthError(isSoftLogout: Bool) { MXLog.error("Received authentication error, the NSE can't handle this.") } diff --git a/NSE/Sources/NotificationContentBuilder.swift b/NSE/Sources/NotificationContentBuilder.swift index 43e089966..3b752f330 100644 --- a/NSE/Sources/NotificationContentBuilder.swift +++ b/NSE/Sources/NotificationContentBuilder.swift @@ -85,7 +85,7 @@ struct NotificationContentBuilder { } } } - + // MARK: - Private private func processEmpty(_ notificationContent: inout UNMutableNotificationContent) { @@ -93,7 +93,7 @@ struct NotificationContentBuilder { notificationContent.body = L10n.notification notificationContent.categoryIdentifier = NotificationConstants.Category.message } - + private func processInvited(notificationContent: inout UNMutableNotificationContent, notificationItem: NotificationItemProxyProtocol, mediaProvider: MediaProviderProtocol) async { @@ -166,7 +166,7 @@ struct NotificationContentBuilder { } } } - + private func processRoomMessage(notificationContent: inout UNMutableNotificationContent, notificationItem: NotificationItemProxyProtocol, messageType: MessageType, @@ -228,7 +228,7 @@ struct NotificationContentBuilder { MXLog.error("Couldn't load the file for media attachment: \(error)") } } - + private func addCommunicationContext(notificationContent: inout UNMutableNotificationContent, senderID: String, senderAvatarDisplayName: String, @@ -247,7 +247,7 @@ struct NotificationContentBuilder { MXLog.error("Couldn't add sender icon: \(error)") } } - + if let fetchedImage { image = fetchedImage } else if let data = await Self.getPlaceholderAvatarImageData(name: icon.groupInfo?.avatarDisplayName ?? senderAvatarDisplayName, @@ -256,7 +256,7 @@ struct NotificationContentBuilder { } else { image = INImage(named: "") } - + let senderHandle = INPersonHandle(value: senderID, type: .unknown) let sender = INPerson(personHandle: senderHandle, nameComponents: nil, @@ -264,7 +264,7 @@ struct NotificationContentBuilder { image: !icon.shouldDisplayAsGroup ? image : nil, contactIdentifier: nil, customIdentifier: nil) - + // These are required to show the group name as subtitle var speakableGroupName: INSpeakableString? var recipients: [INPerson]? @@ -274,7 +274,7 @@ struct NotificationContentBuilder { speakableGroupName = INSpeakableString(spokenPhrase: groupInfo.displayName) recipients = [sender, me] } - + let intent = INSendMessageIntent(recipients: recipients, outgoingMessageType: .outgoingMessageText, content: nil, @@ -286,14 +286,14 @@ struct NotificationContentBuilder { if speakableGroupName != nil { intent.setImage(image, forParameterNamed: \.speakableGroupName) } - + // Use the intent to initialize the interaction. let interaction = INInteraction(intent: intent, response: nil) - + // Interaction direction is incoming because the user is // receiving this message. interaction.direction = .incoming - + // Donate the interaction before updating notification content. if !ProcessInfo.isRunningTests { try? await interaction.donate() @@ -307,7 +307,7 @@ struct NotificationContentBuilder { notificationContent = content } } - + @MainActor static func getPlaceholderAvatarImageData(name: String, id: String) async -> Data? { // The version value is used in case the design of the placeholder is updated to force a replacement diff --git a/NSE/Sources/NotificationHandler.swift b/NSE/Sources/NotificationHandler.swift index ebaaa92ac..b2a19afd5 100644 --- a/NSE/Sources/NotificationHandler.swift +++ b/NSE/Sources/NotificationHandler.swift @@ -19,7 +19,7 @@ class NotificationHandler { private let tag: String private let notificationContentBuilder: NotificationContentBuilder - + init(userSession: NSEUserSession, settings: CommonSettingsProtocol, contentHandler: @escaping (UNNotificationContent) -> Void, @@ -33,7 +33,7 @@ class NotificationHandler { let eventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), style: .plain) - + notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: eventStringBuilder, notificationSoundName: settings.notificationSoundName, userSession: userSession) @@ -82,7 +82,7 @@ class NotificationHandler { MXLog.info("\(tag) Delivering notification") contentHandler(notificationContent) } - + private func discardNotification() { MXLog.info("\(tag) Discarding notification") @@ -199,11 +199,11 @@ class NotificationHandler { } } } - + try? await expiringTask.run(timeout: Duration.seconds(5)) // Wait 5 seconds or just use whatever is available // Release the SDK subscription explicitly once we no longer need updates. observationToken.withLock { $0 = nil } - + guard room.hasActiveRoomCall() else { MXLog.info("The room no longer has an ongoing call, handling as push notification") return .shouldDisplay diff --git a/NSE/Sources/NotificationItemProxy/NotificationItemProxy.swift b/NSE/Sources/NotificationItemProxy/NotificationItemProxy.swift index 390bfbd33..365c91dad 100644 --- a/NSE/Sources/NotificationItemProxy/NotificationItemProxy.swift +++ b/NSE/Sources/NotificationItemProxy/NotificationItemProxy.swift @@ -15,15 +15,15 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { let eventID: String let receiverID: String let roomID: String - + var event: NotificationEvent? { notificationItem.event } - + var senderDisplayName: String? { notificationItem.senderInfo.displayName } - + var senderID: String { switch notificationItem.event { case .timeline(let event): @@ -32,7 +32,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { return senderID } } - + var roomDisplayName: String { notificationItem.roomInfo.displayName } @@ -40,7 +40,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { var isRoomSpace: Bool { notificationItem.roomInfo.isSpace } - + var isRoomDirect: Bool { notificationItem.roomInfo.isDirect } @@ -57,11 +57,11 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { false } } - + var roomJoinedMembers: Int { Int(notificationItem.roomInfo.joinedMembersCount) } - + var isNoisy: Bool { notificationItem.isNoisy ?? false } @@ -69,7 +69,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { var hasMention: Bool { notificationItem.hasMention ?? false } - + var senderAvatarMediaSource: MediaSourceProxy? { if let senderAvatarURLString = notificationItem.senderInfo.avatarUrl, let senderAvatarURL = URL(string: senderAvatarURLString) { @@ -77,7 +77,7 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { } return nil } - + var roomAvatarMediaSource: MediaSourceProxy? { if let roomAvatarURLString = notificationItem.roomInfo.avatarUrl, let roomAvatarURL = URL(string: roomAvatarURLString) { @@ -93,19 +93,19 @@ struct NotificationItemProxy: NotificationItemProxyProtocol { struct EmptyNotificationItemProxy: NotificationItemProxyProtocol { let eventID: String - + var event: NotificationEvent? { nil } - + let roomID: String - + let receiverID: String - + var senderID: String { "" } - + var senderDisplayName: String? { nil } @@ -113,11 +113,11 @@ struct EmptyNotificationItemProxy: NotificationItemProxyProtocol { var roomDisplayName: String { "" } - + var isNoisy: Bool { false } - + var isRoomSpace: Bool { false } @@ -133,15 +133,15 @@ struct EmptyNotificationItemProxy: NotificationItemProxyProtocol { var isRoomPrivate: Bool { false } - + var senderAvatarMediaSource: MediaSourceProxy? { nil } - + var roomAvatarMediaSource: MediaSourceProxy? { nil } - + var roomJoinedMembers: Int { 0 } diff --git a/NSE/Sources/NotificationItemProxy/NotificationItemProxyProtocol.swift b/NSE/Sources/NotificationItemProxy/NotificationItemProxyProtocol.swift index f8b918a0e..a618c7b22 100644 --- a/NSE/Sources/NotificationItemProxy/NotificationItemProxyProtocol.swift +++ b/NSE/Sources/NotificationItemProxy/NotificationItemProxyProtocol.swift @@ -15,31 +15,31 @@ protocol NotificationItemProxyProtocol { var event: NotificationEvent? { get } var senderID: String { get } - + var roomID: String { get } - + var receiverID: String { get } - + var senderDisplayName: String? { get } - + var senderAvatarMediaSource: MediaSourceProxy? { get } - + var roomDisplayName: String { get } - + var roomAvatarMediaSource: MediaSourceProxy? { get } - + var roomJoinedMembers: Int { get } var isRoomSpace: Bool { get } - + var isRoomDirect: Bool { get } var isRoomPrivate: Bool { get } var isDM: Bool { get } - + var isNoisy: Bool { get } - + var hasMention: Bool { get } var threadRootEventID: String? { get } diff --git a/NSE/Sources/NotificationServiceExtension.swift b/NSE/Sources/NotificationServiceExtension.swift index 33bb84933..e2f71410f 100644 --- a/NSE/Sources/NotificationServiceExtension.swift +++ b/NSE/Sources/NotificationServiceExtension.swift @@ -53,11 +53,11 @@ final class NotificationServiceExtension: UNNotificationServiceExtension { actor NotificationServiceExtensionActor { static let receivedWhileOfflineNotificationID = "io.element.elementx.receivedWhileOfflineNotification" - + /// Process-wide target configuration, `Mutex`-protected so it can be read/written from any /// thread (in particular, from the actor's synchronous `init` without spawning a Task). private static let targetConfiguration = Mutex(nil) - + @MainActor private static var hasHandledFirstNotificationSinceBoot = false @@ -72,7 +72,7 @@ actor NotificationServiceExtensionActor { private nonisolated(unsafe) var notificationHandler: NotificationHandler? private let keychainController = KeychainController(service: .sessions, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) - + deinit { ExtensionLogger.logMemory(with: tag) MXLog.info("\(tag) deinit") @@ -86,7 +86,7 @@ actor NotificationServiceExtensionActor { appHooks = AppHooks() appHooks.setUp() - + // If the device is still locked then we can't write to the app group container and // the target configuration will fail. We could call exit(0) here, however with the // notification filtering entitlement that results in the notification being discarded @@ -260,13 +260,13 @@ actor NotificationServiceExtensionActor { content.body = L10n.notificationReceivedWhileOfflineIos content.badge = originalRequest.content.unreadCount as NSNumber? content.sound = settings.notificationSound - + let request = UNNotificationRequest(identifier: Self.receivedWhileOfflineNotificationID, content: content, trigger: nil) UNUserNotificationCenter.current().add(request) } // MARK: - Logging - + private nonisolated var tag: String { "[NSE][\(Unmanaged.passUnretained(self).toOpaque())][\(Unmanaged.passUnretained(Thread.current).toOpaque())][\(ProcessInfo.processInfo.processIdentifier)]" } diff --git a/PreviewTests/Sources/PreviewTests.swift b/PreviewTests/Sources/PreviewTests.swift index d2007caed..375a9e753 100644 --- a/PreviewTests/Sources/PreviewTests.swift +++ b/PreviewTests/Sources/PreviewTests.swift @@ -27,12 +27,12 @@ struct PreviewTests { private let snapshotDevices: [SnapshotDevice] = [.init(name: "iPhone", device: "iPhone 17"), .init(name: "iPad", device: "iPad")] private var recordMode: SnapshotTestingConfiguration.Record = .missing - + init() { if ProcessInfo().environment["RECORD_FAILURES"].map(Bool.init) == true { recordMode = .failed } - + checkEnvironments() UIView.setAnimationsEnabled(false) } @@ -45,7 +45,7 @@ struct PreviewTests { fatalError("\(deviceModel ?? "Unknown") is the wrong one. Switch to using \(simulatorDevice) for these tests.") } } - + let osVersion = ProcessInfo().operatingSystemVersion guard osVersion.majorVersion == requiredOSVersion.major, osVersion.minorVersion == requiredOSVersion.minor else { fatalError("Switch to iOS \(requiredOSVersion) for these tests.") @@ -56,7 +56,7 @@ struct PreviewTests { } // MARK: - Snapshots - + func assertSnapshots(matching preview: _Preview, step: Int, testName: String = #function, @@ -124,15 +124,15 @@ struct PreviewTests { } return languageCode + "-" + regionCode } - + private var languageCode: String { Locale.current.language.languageCode?.identifier ?? "" } - + private var regionCode: String { Locale.current.language.region?.identifier ?? "" } - + private func assertSnapshots(matching view: AnyView, name: String?, isScreen: Bool, @@ -193,7 +193,7 @@ private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { layout: SwiftUISnapshotLayout = .sizeThatFits, traits: UITraitCollection = .init()) -> Snapshotting { let config: ViewImageConfig - + switch layout { #if os(iOS) || os(tvOS) case let .device(config: deviceConfig): @@ -207,21 +207,21 @@ private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { // Make sure to use the workaround safe area insets. config = .init(safeArea: .one, size: size, traits: traits) } - + return SimplySnapshotting(pathExtension: "png", diffing: .prefireImage(preferences: preferences, scale: traits.displayScale)) .asyncPullback { view in var config = config - + let controller: UIViewController - + if config.size != nil { controller = UIHostingController(rootView: view) } else { let hostingController = UIHostingController(rootView: view) - + let maxSize = CGSize.zero config.size = hostingController.sizeThatFits(in: maxSize) - + controller = hostingController } diff --git a/ShareExtension/Sources/ShareExtensionViewController.swift b/ShareExtension/Sources/ShareExtensionViewController.swift index 286912161..741983c4b 100644 --- a/ShareExtension/Sources/ShareExtensionViewController.swift +++ b/ShareExtension/Sources/ShareExtensionViewController.swift @@ -18,7 +18,7 @@ class ShareExtensionViewController: UIViewController { } return AppSettings(store: userDefaults) }() - + private var appHooks: AppHooks! private let keychainController = KeychainController(service: .sessions, diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift index 2ac4e3469..42a038365 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenCoordinator.swift @@ -23,7 +23,7 @@ final class TemplateScreenCoordinator: CoordinatorProtocol { private let viewModel: TemplateScreenViewModelProtocol private var cancellables = Set() - + private let actionsSubject: PassthroughSubject = .init() var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() @@ -47,7 +47,7 @@ final class TemplateScreenCoordinator: CoordinatorProtocol { } .store(in: &cancellables) } - + func toPresentable() -> AnyView { AnyView(TemplateScreen(context: viewModel.context)) } diff --git a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModel.swift b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModel.swift index d25009c01..c33f8efc5 100644 --- a/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModel.swift +++ b/Tools/Scripts/Templates/SimpleScreenExample/ElementX/TemplateScreenViewModel.swift @@ -15,7 +15,7 @@ class TemplateScreenViewModel: TemplateScreenViewModelType, TemplateScreenViewMo var actionsPublisher: AnyPublisher { actionsSubject.eraseToAnyPublisher() } - + init() { super.init(initialViewState: TemplateScreenViewState(title: "Template title", placeholder: "Enter something here", diff --git a/Tools/Sources/Commands/AppIconBanner.swift b/Tools/Sources/Commands/AppIconBanner.swift index edca7f1e9..bd9666d6a 100644 --- a/Tools/Sources/Commands/AppIconBanner.swift +++ b/Tools/Sources/Commands/AppIconBanner.swift @@ -6,10 +6,10 @@ struct AppIconBanner: AsyncParsableCommand { @Argument(help: "Path to the input image.") var path: String - + @Option(help: "Text for the banner.") var bannerText: String - + @MainActor func run() async throws { let currentDirectoryURL = URL(filePath: FileManager.default.currentDirectoryPath) diff --git a/Tools/Sources/Commands/BuildSDK.swift b/Tools/Sources/Commands/BuildSDK.swift index b089cd0a7..6e565e61a 100644 --- a/Tools/Sources/Commands/BuildSDK.swift +++ b/Tools/Sources/Commands/BuildSDK.swift @@ -33,7 +33,7 @@ struct BuildSDK: AsyncParsableCommand { @Option(help: "The profile to use when building the SDK. Omit this option to build in debug mode.") var profile: Profile = .reldbg - + enum Error: LocalizedError { case rustupOutputFailure case missingRustTargets([String]) @@ -133,7 +133,7 @@ struct BuildSDK: AsyncParsableCommand { let updatedYAMLString = try Yams.serialize(node: projectConfig) try updatedYAMLString.write(to: yamlURL, atomically: true, encoding: .utf8) } - + func generateSDKMocks() async throws { var command = GenerateSDKMocks() command.version = "local" diff --git a/Tools/Sources/Commands/BumpCalendarVersion.swift b/Tools/Sources/Commands/BumpCalendarVersion.swift index 13a422d41..a834647df 100644 --- a/Tools/Sources/Commands/BumpCalendarVersion.swift +++ b/Tools/Sources/Commands/BumpCalendarVersion.swift @@ -5,7 +5,7 @@ import Foundation struct BumpCalendarVersion: ParsableCommand { static let configuration = CommandConfiguration(abstract: "A tool that bumps the CalVer every month (if needed), setting the patch back to 0.", discussion: "The tool assumes the release will be published in 6-days so bumps early.") - + func run() throws { try updateProjectYAML() try Zsh.run(command: "xcodegen") diff --git a/Tools/Sources/Commands/CI/AccessibilityTests.swift b/Tools/Sources/Commands/CI/AccessibilityTests.swift index 5f545d3ef..70dbb2bf0 100644 --- a/Tools/Sources/Commands/CI/AccessibilityTests.swift +++ b/Tools/Sources/Commands/CI/AccessibilityTests.swift @@ -4,7 +4,7 @@ import Foundation struct AccessibilityTests: AsyncParsableCommand { static let configuration = CommandConfiguration(commandName: "accessibility-tests", abstract: "Runs the accessibility test CI workflow.") - + @Option(help: "Device name for tests.") var device = "iPhone 17" @@ -25,15 +25,15 @@ struct AccessibilityTests: AsyncParsableCommand { testsFailed = true logger.error("\n❌ Accessibility tests failed.\n") } - + // Zip results (best-effort, useful for CI artifact uploads) await CI.zipResults(bundles: ["AccessibilityTests.xcresult"], outputName: "AccessibilityTests.xcresult.zip") - + if testsFailed { throw ExitCode.failure } - + logger.info("\n✅ Accessibility tests passed.\n") } } diff --git a/Tools/Sources/Commands/CI/CI.swift b/Tools/Sources/Commands/CI/CI.swift index 2a4ac9bb4..d2539d422 100644 --- a/Tools/Sources/Commands/CI/CI.swift +++ b/Tools/Sources/Commands/CI/CI.swift @@ -152,7 +152,7 @@ struct CI: ParsableCommand { else { throw ValidationError("GITHUB_TOKEN environment variable is not set.") } - + let repoURL = try await CI.gitRepositoryURL() if let tagName { diff --git a/Tools/Sources/Commands/CI/ConfigureNightly.swift b/Tools/Sources/Commands/CI/ConfigureNightly.swift index 7e856f763..042c810ac 100644 --- a/Tools/Sources/Commands/CI/ConfigureNightly.swift +++ b/Tools/Sources/Commands/CI/ConfigureNightly.swift @@ -5,24 +5,24 @@ import Yams struct ConfigureNightly: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Configures the project for a Nightly build.", discussion: "Adds the Nightly variant to project.yml, updates secrets, runs xcodegen, and generates the app icon banner.") - + @Option(help: "The build number to display on the app icon banner.") var buildNumber: String - + func run() async throws { guard !buildNumber.isEmpty else { throw ValidationError("Invalid build number.") } - + try addNightlyVariant() try await CI.run(.name("swift"), ["run", "pipeline", "update-foss-secrets"]) try await CI.run(.name("xcodegen")) - + let releaseVersion = try CI.readMarketingVersion() try await generateAppIconBanner(version: releaseVersion, buildNumber: buildNumber) } - + /// Adds the Nightly variant include path to `project.yml` if it isn't already present. private func addNightlyVariant() throws { let projectURL = URL.projectDirectory.appending(component: "project.yml") @@ -35,11 +35,11 @@ struct ConfigureNightly: AsyncParsableCommand { if projectConfig["include"]?.sequence?.contains(where: { $0.mapping?["path"] == "Variants/Nightly/nightly.yml" }) == false { projectConfig["include"]?.sequence?.append(["path": "Variants/Nightly/nightly.yml"]) } - + let updatedYAMLString = try Yams.serialize(node: projectConfig) try updatedYAMLString.write(to: projectURL, atomically: true, encoding: .utf8) } - + /// Generates the app icon banner with version and build number. private func generateAppIconBanner(version: String, buildNumber: String) async throws { let bannerText = "\(version) (\(buildNumber))" diff --git a/Tools/Sources/Commands/CI/IntegrationTests.swift b/Tools/Sources/Commands/CI/IntegrationTests.swift index 26c80e322..727d67ca4 100644 --- a/Tools/Sources/Commands/CI/IntegrationTests.swift +++ b/Tools/Sources/Commands/CI/IntegrationTests.swift @@ -8,7 +8,7 @@ struct IntegrationTests: AsyncParsableCommand { Deletes old log files, runs integration tests, validates that logs are set to the trace level and don't contain private messages, then collects results. """) - + @Option(help: "Device name for tests.") var device = "iPhone 17" diff --git a/Tools/Sources/Commands/CI/PreviewTests.swift b/Tools/Sources/Commands/CI/PreviewTests.swift index dcb288ffd..8364a0ed7 100644 --- a/Tools/Sources/Commands/CI/PreviewTests.swift +++ b/Tools/Sources/Commands/CI/PreviewTests.swift @@ -4,21 +4,21 @@ import Foundation struct PreviewTests: AsyncParsableCommand { static let configuration = CommandConfiguration(commandName: "preview-tests", abstract: "Runs the preview test CI workflow, with optional snapshot recording.") - + @Flag(help: "Re-record snapshots for tests that fail or are missing a reference image.") var record = false - + private static let scheme = "PreviewTests" private static let device = "iPhone SE (3rd generation)" private static let osVersion = "26.4.1" private static let simulatorType = "com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation" private static let testPlanPath = "PreviewTests/SupportingFiles/PreviewTests.xctestplan" - + func run() async throws { if record { try setRecordFailures(enabled: true) } - + var testsFailed = false do { logger.info("\n🧪 Running preview tests…\n") @@ -47,41 +47,41 @@ struct PreviewTests: AsyncParsableCommand { testsFailed = true } } - + // Collect coverage and test results regardless of test outcome (best-effort). await CI.collectCoverage(resultBundle: "\(Self.scheme).xcresult", outputName: "preview-cobertura.xml") await CI.collectTestResults(resultBundle: "\(Self.scheme).xcresult", outputName: "preview-junit.xml") - + if testsFailed { throw ExitCode.failure } - + if !record { logger.info("\n✅ Preview tests passed.\n") } } - + // MARK: - Test Plan - + /// Enables or disables the `RECORD_FAILURES` environment variable entry in the test plan. private func setRecordFailures(enabled: Bool) throws { let url = URL.projectDirectory.appendingPathComponent(Self.testPlanPath) let data = try Data(contentsOf: url) - + guard var plan = try JSONSerialization.jsonObject(with: data) as? [String: Any], var defaultOptions = plan["defaultOptions"] as? [String: Any], var envVars = defaultOptions["environmentVariableEntries"] as? [[String: Any]] else { throw ValidationError("Could not parse test plan at \(Self.testPlanPath).") } - + for index in envVars.indices where envVars[index]["key"] as? String == "RECORD_FAILURES" { envVars[index]["enabled"] = enabled break } - + defaultOptions["environmentVariableEntries"] = envVars plan["defaultOptions"] = defaultOptions - + let jsonData = try JSONSerialization.data(withJSONObject: plan, options: [.prettyPrinted, .sortedKeys]) try jsonData.write(to: url) } diff --git a/Tools/Sources/Commands/CI/ReleaseToGithub.swift b/Tools/Sources/Commands/CI/ReleaseToGithub.swift index b1018d5c4..54a0dbade 100644 --- a/Tools/Sources/Commands/CI/ReleaseToGithub.swift +++ b/Tools/Sources/Commands/CI/ReleaseToGithub.swift @@ -5,14 +5,14 @@ import Yams struct ReleaseToGitHub: AsyncParsableCommand { static let configuration = CommandConfiguration(commandName: "release-to-github", abstract: "Creates a GitHub release and updates CHANGES.md with generated release notes.") - + enum ReleaseError: LocalizedError { case missingGitHubToken case failedToCreateRelease(String) case failedToParseResponse case missingReleaseNotes case failedToReadVersion - + var errorDescription: String? { switch self { case .missingGitHubToken: @@ -28,45 +28,45 @@ struct ReleaseToGitHub: AsyncParsableCommand { } } } - + func run() async throws { let currentVersion = try CI.readMarketingVersion() logger.info("Creating GitHub release for version \(currentVersion)…") - + let releaseBody = try await createGitHubRelease(version: currentVersion) - + try updateChangelog(version: currentVersion, generatedNotes: releaseBody) - + let changesFilePath = URL.projectDirectory.appendingPathComponent("CHANGES.md").path try await CI.run(.name("git"), ["add", changesFilePath]) - + logger.info("Successfully created GitHub release \(currentVersion) and updated CHANGES.md.") let targetFilePath = "project.yml" let xcodeProjPath = "ElementX.xcodeproj" - + guard let newVersion = bumpPatchVersion(currentVersion) else { throw ValidationError("Invalid version format: \(currentVersion)") } - + // Bump the patch version using sed (preserves file formatting) try await CI.run(.name("sed"), ["-i", "", "s/MARKETING_VERSION: \(currentVersion)/MARKETING_VERSION: \(newVersion)/g", targetFilePath]) logger.info("Version updated from \(currentVersion) to \(newVersion)") - + try await CI.run(.name("xcodegen")) - + try await CI.gitConfigureGlobals() - + try await CI.run(.name("git"), ["add", targetFilePath, xcodeProjPath]) try await CI.run(.name("git"), ["commit", "-m", "Prepare next release"]) try await CI.gitPush() - + try await rebaseMainOntoCurrentBranch() } - + // MARK: - Private - + private func createGitHubRelease(version: String) async throws -> String { guard let apiToken = ProcessInfo.processInfo.environment["GITHUB_TOKEN"], !apiToken.isEmpty else { @@ -79,23 +79,23 @@ struct ReleaseToGitHub: AsyncParsableCommand { request.setValue("Bearer \(apiToken)", forHTTPHeaderField: "Authorization") request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept") request.setValue("application/json", forHTTPHeaderField: "Content-Type") - + let body: [String: Any] = ["tag_name": "release/\(version)", "name": version, "generate_release_notes": true] request.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, response) = try await URLSession.shared.data(for: request) - + guard let httpResponse = response as? HTTPURLResponse else { throw ReleaseError.failedToParseResponse } - + guard (200...299).contains(httpResponse.statusCode) else { let errorBody = String(data: data, encoding: .utf8) ?? "Unknown error" throw ReleaseError.failedToCreateRelease("HTTP \(httpResponse.statusCode): \(errorBody)") } - + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let releaseBody = json["body"] as? String else { throw ReleaseError.failedToParseResponse @@ -103,25 +103,25 @@ struct ReleaseToGitHub: AsyncParsableCommand { return releaseBody } - + private func updateChangelog(version: String, generatedNotes: String) throws { let changesURL = URL.projectDirectory.appending(component: "CHANGES.md") - + // Clean up the generated notes: remove HTML comments and adjust header levels let cleanedNotes = generatedNotes .replacingOccurrences(of: "", with: "", options: .regularExpression) .replacingOccurrences(of: "### ", with: "\n") .replacingOccurrences(of: "## ", with: "### ") - + guard !cleanedNotes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { throw ReleaseError.missingReleaseNotes } - + let releaseDate = Date().formatted(.iso8601.year().month().day()) - + let existingContent = try String(contentsOf: changesURL) let newContent = "## Changes in \(version) (\(releaseDate))\(cleanedNotes)\n\n\(existingContent)" - + try newContent.write(to: changesURL, atomically: true, encoding: .utf8) logger.info("Updated CHANGES.md with release notes.") } @@ -131,7 +131,7 @@ struct ReleaseToGitHub: AsyncParsableCommand { guard let match = version.firstMatch(of: regex), var patch = Int(match.3) else { return nil } - + let year = String(match.1) let month = String(match.2) patch = patch + 1 @@ -146,14 +146,14 @@ struct ReleaseToGitHub: AsyncParsableCommand { } logger.info("Current branch: \(currentBranch)") - + try await CI.run(.name("git"), ["reset", "--hard"]) try await CI.run(.name("git"), ["checkout", "main"]) try await CI.run(.name("git"), ["pull", "origin", "main"]) try await CI.run(.name("git"), ["rebase", currentBranch]) - + try await CI.gitPush() - + logger.info("Successfully rebased main onto \(currentBranch)") } } diff --git a/Tools/Sources/Commands/CI/RunTests.swift b/Tools/Sources/Commands/CI/RunTests.swift index 8676b9301..79f814d07 100644 --- a/Tools/Sources/Commands/CI/RunTests.swift +++ b/Tools/Sources/Commands/CI/RunTests.swift @@ -143,9 +143,9 @@ struct RunTests: AsyncParsableCommand { if let testName { command += " -only-testing:\(scheme)/\(testName)" } - + command += " | \(formatter)" - + try await CI.run(.path("/bin/zsh"), ["-cu", command]) } } diff --git a/Tools/Sources/Commands/CI/TagNightly.swift b/Tools/Sources/Commands/CI/TagNightly.swift index 16cb8ba36..d890db9fe 100644 --- a/Tools/Sources/Commands/CI/TagNightly.swift +++ b/Tools/Sources/Commands/CI/TagNightly.swift @@ -4,22 +4,22 @@ import Yams struct TagNightly: AsyncParsableCommand { static let configuration = CommandConfiguration(abstract: "Tags the current commit as a nightly build and pushes the tag.") - + @Option(help: "The build number to include in the tag.") var buildNumber: String - + func run() async throws { guard !buildNumber.isEmpty else { throw ValidationError("Invalid build number.") } try await CI.gitConfigureGlobals() - + let currentVersion = try CI.readMarketingVersion() let tagName = "nightly/\(currentVersion).\(buildNumber)" try await CI.gitPush(tagName: tagName) - + logger.info("\n🚀 Successfully tagged nightly: \(tagName)\n") } } diff --git a/Tools/Sources/Commands/CI/UITests.swift b/Tools/Sources/Commands/CI/UITests.swift index cf7359b1b..512acfcd7 100644 --- a/Tools/Sources/Commands/CI/UITests.swift +++ b/Tools/Sources/Commands/CI/UITests.swift @@ -10,35 +10,35 @@ struct UITests: AsyncParsableCommand { swift run tools ci ui-tests --device-type iPad swift run tools ci ui-tests --device-type iPhone --test-name "ClassName/testName" """) - + enum DeviceType: String, CaseIterable, ExpressibleByArgument { case iPhone case iPad } - + @Option(help: "The device type to test (iPhone or iPad).") var deviceType: DeviceType - + @Option(help: "iOS version for the simulator.") var osVersion = "26.4.1" - + @Option(help: "Run only a specific test (format: 'ClassName/testName').") var testName: String? - + private var simulatorName: String { switch deviceType { case .iPhone: "iPhone-\(osVersion)" case .iPad: "iPad-\(osVersion)" } } - + private var simulatorType: String { switch deviceType { case .iPhone: "com.apple.CoreSimulator.SimDeviceType.iPhone-17" case .iPad: "com.apple.CoreSimulator.SimDeviceType.iPad-A16" } } - + /// We used to run these simultaneously on iPhone and iPad but it is *really* slow on GitHub runners. /// Presumably because launching 2 simulators uses more memory than the runner has available. func run() async throws { @@ -49,11 +49,11 @@ struct UITests: AsyncParsableCommand { "--create-simulator-name", simulatorName, "--create-simulator-type", simulatorType ] - + if let testName { args += ["--test-name", testName] } - + var testsFailed = false do { print("\n🧪 Running UI tests (\(deviceType.rawValue))…\n") @@ -68,11 +68,11 @@ struct UITests: AsyncParsableCommand { await CI.collectCoverage(resultBundle: "UITests.xcresult", outputName: "ui-cobertura.xml") await CI.collectTestResults(resultBundle: "UITests.xcresult", outputName: "ui-junit.xml") - + if testsFailed { throw ExitCode.failure } - + print("\n✅ UI tests (\(deviceType.rawValue)) passed.\n") } } diff --git a/Tools/Sources/Commands/CI/UnitTests.swift b/Tools/Sources/Commands/CI/UnitTests.swift index 677257bde..1ab7e3ac2 100644 --- a/Tools/Sources/Commands/CI/UnitTests.swift +++ b/Tools/Sources/Commands/CI/UnitTests.swift @@ -4,7 +4,7 @@ import Foundation struct UnitTests: AsyncParsableCommand { static let configuration = CommandConfiguration(commandName: "unit-tests", abstract: "Runs the unit test CI workflow: lint, unit tests, preview tests, and result collection.") - + @Flag(help: "Skip preview tests") var skipPreviews = false diff --git a/Tools/Sources/Commands/CI/UploadDSYMs.swift b/Tools/Sources/Commands/CI/UploadDSYMs.swift index 294520a96..4a7fded2d 100644 --- a/Tools/Sources/Commands/CI/UploadDSYMs.swift +++ b/Tools/Sources/Commands/CI/UploadDSYMs.swift @@ -5,28 +5,28 @@ struct UploadDSYMs: AsyncParsableCommand { static let configuration = CommandConfiguration(commandName: "upload-dsyms", abstract: "Uploads dSYMs to Sentry using sentry-cli.", discussion: "Requires the SENTRY_AUTH_TOKEN environment variable to be set.") - + @Option(help: "The path to the dSYMs directory or file to upload.") var dsymPath: String - + @Option(help: "The Sentry organization slug.") var orgSlug = "element" - + @Option(help: "The Sentry project slug.") var projectSlug = "element-x-ios" - + @Option(help: "The Sentry server URL.") var url = "https://sentry.tools.element.io/" - + @Option(help: "The maximum number of upload attempts.") var maxRetries = 5 - + func run() async throws { guard let authToken = ProcessInfo.processInfo.environment["SENTRY_AUTH_TOKEN"], !authToken.isEmpty else { throw ValidationError("SENTRY_AUTH_TOKEN environment variable is not set.") } - + let command = """ sentry-cli --url "\(url)" dif upload \ --auth-token "\(authToken)" \ @@ -35,9 +35,9 @@ struct UploadDSYMs: AsyncParsableCommand { --log-level debug \ "\(dsymPath)" """ - + var lastError: Swift.Error? - + for attempt in 1...maxRetries { do { logger.info("\n📡 Uploading dSYMs to Sentry (attempt \(attempt)/\(maxRetries))…\n") @@ -49,7 +49,7 @@ struct UploadDSYMs: AsyncParsableCommand { logger.error("\n❌ Sentry upload attempt \(attempt) failed: \(error.localizedDescription)\n") } } - + if let lastError { throw lastError } diff --git a/Tools/Sources/Commands/DownloadStrings.swift b/Tools/Sources/Commands/DownloadStrings.swift index c7b3007ba..d58cdd459 100644 --- a/Tools/Sources/Commands/DownloadStrings.swift +++ b/Tools/Sources/Commands/DownloadStrings.swift @@ -4,16 +4,16 @@ import Foundation struct DownloadStrings: ParsableCommand { static let configuration = CommandConfiguration(abstract: "A tool to download localizable strings from localazy") - + @Flag(help: "Use to download translation keys for all languages") var allLanguages = false - + func run() throws { try localazyDownload() try sortStringsFiles() try swiftgen() } - + private func localazyDownload() throws { let arguments = allLanguages ? " all" : "" try Zsh.run(command: "localazy download\(arguments)") @@ -75,7 +75,7 @@ struct DownloadStrings: ParsableCommand { guard let closeQuote = line[afterOpen...].firstIndex(of: "\"") else { return nil } return String(line[afterOpen.. String { let fileURLString = String(format: fileURLFormat, version) diff --git a/Tools/Sources/Commands/Locheck.swift b/Tools/Sources/Commands/Locheck.swift index f8cb30b90..dec636e3e 100644 --- a/Tools/Sources/Commands/Locheck.swift +++ b/Tools/Sources/Commands/Locheck.swift @@ -6,7 +6,7 @@ struct Locheck: ParsableCommand { enum LocheckError: LocalizedError { case missingMint case outputError - + var errorDescription: String? { switch self { case .missingMint: @@ -16,28 +16,28 @@ struct Locheck: ParsableCommand { } } } - + static let configuration = CommandConfiguration(abstract: "A tool that verifies bad strings contained in localization files") - + private var stringsDirectoryURL: URL { .projectDirectory.appendingPathComponent("ElementX/Resources/Localizations") } - + func run() throws { try checkMint() try checkStrings() } - + func checkStrings() throws { guard let output = try Zsh.run(command: "mint run locheck discoverlproj --ignore-missing --ignore lproj_file_missing_from_translation --treat-warnings-as-errors \(stringsDirectoryURL.path)") else { throw LocheckError.missingMint } print(output) } - + private func checkMint() throws { let result = try Zsh.run(command: "which mint") - + if result?.contains("not found") == true { throw LocheckError.missingMint } diff --git a/Tools/Sources/Commands/OutdatedPackages.swift b/Tools/Sources/Commands/OutdatedPackages.swift index e2962aba4..e796bbede 100644 --- a/Tools/Sources/Commands/OutdatedPackages.swift +++ b/Tools/Sources/Commands/OutdatedPackages.swift @@ -4,21 +4,21 @@ import Foundation struct OutdatedPackages: ParsableCommand { static let configuration = CommandConfiguration(abstract: "A tool to check outdated package dependencies. Please make sure you have already run setup-project before using this tool.") - + private var projectSwiftPMDirectoryURL: URL { .projectDirectory.appendingPathComponent("ElementX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm") } - + func run() throws { try checkToolsDependencies() try checkProjectDependencies() } - + func checkToolsDependencies() throws { guard let output = try Zsh.run(command: "swift outdated"), !output.isEmpty else { return } print("outdated tools Swift packages:\n\(output)") } - + func checkProjectDependencies() throws { guard let output = try Zsh.run(command: "swift outdated", directory: projectSwiftPMDirectoryURL), !output.isEmpty else { return } print("outdated project Swift packages:\n\(output)") diff --git a/Tools/Sources/Commands/SetupProject.swift b/Tools/Sources/Commands/SetupProject.swift index 8379025ac..2ef37e6b6 100644 --- a/Tools/Sources/Commands/SetupProject.swift +++ b/Tools/Sources/Commands/SetupProject.swift @@ -4,29 +4,29 @@ import Foundation struct SetupProject: ParsableCommand { static let configuration = CommandConfiguration(abstract: "A tool to setup the required components to efficiently run and contribute to Element X iOS") - + func run() throws { try setupGitHooks() try brewInstall() try mintPackagesInstall() try xcodegen() } - + func setupGitHooks() throws { try Zsh.run(command: "git config core.hooksPath .githooks") } - + func brewInstall() throws { try Zsh.run(command: "brew install xcodegen swiftgen git-lfs sourcery mint pkl kiliankoe/formulae/swift-outdated localazy/tools/localazy peripheryapp/periphery/periphery") - // Install swiftformat from develop (for now), making sure to avoid conflicts with an existing versioned installation. + // Install swiftformat from develop (for now), making sure to avoid conflicts with an existing versioned installation. try Zsh.run(command: "source ci_scripts/ci_common.sh && install_swiftformat_head") } - + func mintPackagesInstall() throws { try Zsh.run(command: "mint install Asana/locheck") } - + func xcodegen() throws { try Zsh.run(command: "xcodegen") } diff --git a/Tools/Sources/Commands/UnusedStrings.swift b/Tools/Sources/Commands/UnusedStrings.swift index 9418800f6..6593710b1 100644 --- a/Tools/Sources/Commands/UnusedStrings.swift +++ b/Tools/Sources/Commands/UnusedStrings.swift @@ -7,11 +7,11 @@ struct UnusedStrings: ParsableCommand { @Flag(help: "Save the results to disk instead of printing them.") var saveToFile = false - + func run() throws { try peripheryScan() } - + func peripheryScan() throws { print("Analysing project, this may take a while…") diff --git a/Tools/Sources/Extensions/URL.swift b/Tools/Sources/Extensions/URL.swift index 94e57b0e7..681871d9e 100644 --- a/Tools/Sources/Extensions/URL.swift +++ b/Tools/Sources/Extensions/URL.swift @@ -5,11 +5,11 @@ extension URL { static var projectDirectory: URL { URL(fileURLWithPath: FileManager.default.currentDirectoryPath) } - + static var parentDirectory: URL { .projectDirectory.deletingLastPathComponent() } - + static var sdkDirectory: URL { .parentDirectory.appendingPathComponent("matrix-rust-sdk") } diff --git a/UITests/Sources/Application.swift b/UITests/Sources/Application.swift index a96cfe3be..d98e828ce 100644 --- a/UITests/Sources/Application.swift +++ b/UITests/Sources/Application.swift @@ -102,11 +102,11 @@ extension XCUIApplication { } return languageCode + "-" + regionCode } - + private var languageCode: String { Locale.current.language.languageCode?.identifier ?? "" } - + private var regionCode: String { Locale.current.language.region?.identifier ?? "" } diff --git a/UITests/Sources/AuthenticationFlowCoordinatorTests.swift b/UITests/Sources/AuthenticationFlowCoordinatorTests.swift index 501d8d1e3..1171359e3 100644 --- a/UITests/Sources/AuthenticationFlowCoordinatorTests.swift +++ b/UITests/Sources/AuthenticationFlowCoordinatorTests.swift @@ -39,7 +39,7 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase { // Login Screen: Enter valid credentials app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice\n", app: app) app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("12345678", app: app) - + try await app.assertScreenshot() // Login Screen: Tap next @@ -72,7 +72,7 @@ class AuthenticationFlowCoordinatorUITests: XCTestCase { // Login Screen: Enter invalid credentials app.textFields[A11yIdentifiers.loginScreen.emailUsername].clearAndTypeText("alice", app: app) app.secureTextFields[A11yIdentifiers.loginScreen.password].clearAndTypeText("87654321", app: app) - + // Login Screen: Tap continue XCTAssertTrue(continueButton.isEnabled) continueButton.tap() diff --git a/UITests/Sources/PollFormScreenTests.swift b/UITests/Sources/PollFormScreenTests.swift index 42361bc0c..579d2d7af 100644 --- a/UITests/Sources/PollFormScreenTests.swift +++ b/UITests/Sources/PollFormScreenTests.swift @@ -14,35 +14,35 @@ class PollFormScreenUITests: XCTestCase { let app = Application.launch(.createPoll) try await app.assertScreenshot() } - + func testFilledPoll() async throws { let app = Application.launch(.createPoll) let questionTextField = app.textFields[A11yIdentifiers.pollFormScreen.question] questionTextField.tap(.center) questionTextField.typeText("Do you like polls?") - + let option1TextField = app.textFields[A11yIdentifiers.pollFormScreen.optionID(0)] option1TextField.tap(.center) option1TextField.typeText("Yes") - + let option2TextField = app.textFields[A11yIdentifiers.pollFormScreen.optionID(1)] option2TextField.tap(.center) option2TextField.typeText("No") // Dismiss the keyboard app.swipeDown() - + let createButton = app.buttons[A11yIdentifiers.pollFormScreen.submit] XCTAssertTrue(createButton.isEnabled) - + try await app.assertScreenshot() } - + func testMaxOptions() async throws { let app = Application.launch(.createPoll) let createButton = app.buttons[A11yIdentifiers.pollFormScreen.submit] let addOption = app.buttons[A11yIdentifiers.pollFormScreen.addOption] - + for _ in 1...8 { // Use the frame as a fallback to fix the button being obscured by the home indicator. if !addOption.isHittable || addOption.frame.maxY > (app.frame.maxY - 20) { @@ -57,7 +57,7 @@ class PollFormScreenUITests: XCTestCase { XCTAssertFalse(addOption.exists) XCTAssertFalse(createButton.isEnabled) - + try await app.assertScreenshot() } } diff --git a/UITests/Sources/RoomScreenTests.swift b/UITests/Sources/RoomScreenTests.swift index 057e20c4c..04a0a1ca8 100644 --- a/UITests/Sources/RoomScreenTests.swift +++ b/UITests/Sources/RoomScreenTests.swift @@ -12,10 +12,10 @@ import XCTest class RoomScreenUITests: XCTestCase { func testPlainNoAvatar() async throws { let app = Application.launch(.roomPlainNoAvatar) - + XCTAssert(app.buttons[A11yIdentifiers.roomScreen.name].exists) XCTAssert(app.buttons[A11yIdentifiers.roomScreen.avatar].exists) - + try await app.assertScreenshot() } @@ -37,7 +37,7 @@ class RoomScreenUITests: XCTestCase { // When a back pagination occurs and an incoming message arrives. try await performOperation(.incomingMessage, using: client) try await performOperation(.paginate, using: client) - + // Then the 4 visible messages should stay aligned to the bottom. try await app.assertScreenshot() } @@ -52,7 +52,7 @@ class RoomScreenUITests: XCTestCase { // When a large back pagination occurs. try await performOperation(.paginate, using: client) - + // The bottom of the timeline should remain visible with more items added above. try await app.assertScreenshot() } @@ -73,11 +73,11 @@ class RoomScreenUITests: XCTestCase { // When a back pagination occurs. try await performOperation(.paginate, using: client) - + // Then the bottom of the timeline should remain unchanged (with new items having been added above). try await app.assertScreenshot() } - + func testTimelineLayoutAtBottom() async throws { let client = try UITestsSignalling.Client(mode: .tests) @@ -126,32 +126,32 @@ class RoomScreenUITests: XCTestCase { // Then the item should also be highlighted and scrolled to in the same state as before. try await app.assertScreenshot() } - + func testTimelineReadReceipts() async throws { let app = Application.launch(.roomSmallTimelineWithReadReceipts) - + // The messages should be bottom aligned. try await app.assertScreenshot() } - + func testTimelineDisclosedPolls() async throws { let app = Application.launch(.roomWithDisclosedPolls) - + try await app.assertScreenshot() } - + func testTimelineUndisclosedPolls() async throws { let app = Application.launch(.roomWithUndisclosedPolls) - + try await app.assertScreenshot() } - + func testTimelineOutgoingPolls() async throws { let app = Application.launch(.roomWithOutgoingPolls) - + try await app.assertScreenshot() } - + // MARK: - Helper Methods private func performOperation(_ operation: UITestsSignal.Timeline, using client: UITestsSignalling.Client) async throws { diff --git a/UITests/Sources/ServerSelectionTests.swift b/UITests/Sources/ServerSelectionTests.swift index 0dcf04193..472da0ff6 100644 --- a/UITests/Sources/ServerSelectionTests.swift +++ b/UITests/Sources/ServerSelectionTests.swift @@ -17,7 +17,7 @@ class ServerSelectionUITests: XCTestCase { // Then it should be configured for matrix.org try await app.assertScreenshot() } - + func testEmptyAddress() async throws { // Given the initial server selection screen as a modal. let app = Application.launch(.serverSelection) @@ -29,7 +29,7 @@ class ServerSelectionUITests: XCTestCase { // Then the screen should not allow the user to continue. try await app.assertScreenshot() } - + func testInvalidAddress() async throws { // Given the initial server selection screen as a modal. let app = Application.launch(.serverSelection) diff --git a/UITests/Sources/UserSessionScreenTests.swift b/UITests/Sources/UserSessionScreenTests.swift index 2248457f4..fac3d3909 100644 --- a/UITests/Sources/UserSessionScreenTests.swift +++ b/UITests/Sources/UserSessionScreenTests.swift @@ -39,7 +39,7 @@ class UserSessionScreenTests: XCTestCase { app.swipeDown() // Make sure the header shows a large title try await app.assertScreenshot(step: Step.homeScreen) - + app.buttons[A11yIdentifiers.homeScreen.roomName(firstRoomName)].tap() XCTAssert(app.buttons[firstRoomName].waitForExistence(timeout: 5.0)) try await Task.sleep(for: .seconds(1)) @@ -53,7 +53,7 @@ class UserSessionScreenTests: XCTestCase { let latestMessage = app.textViews.matching(NSPredicate(format: "value CONTAINS 'sugar honey ice'")).firstMatch XCTAssert(latestMessage.waitForExistence(timeout: 5.0)) - + // Tapping on the mesage text view or the cell doesn't work. // Use the sender static text instead. let latestEventSender = app.staticTexts["@test.matrix.org"] @@ -72,29 +72,29 @@ class UserSessionScreenTests: XCTestCase { let latestEditedMessage = app.textViews.matching(NSPredicate(format: "value CONTAINS 'sugar honey ice & tea'")).firstMatch XCTAssert(latestEditedMessage.waitForExistence(timeout: 5.0)) - + app.buttons[A11yIdentifiers.roomScreen.composerToolbar.openComposeOptions].tap(.center) try await app.assertScreenshot(step: Step.composerAttachments) } - + func testUserSessionReply() async throws { let app = Application.launch(.userSessionScreenReply, disableTimelineAccessibility: false) app.buttons[A11yIdentifiers.homeScreen.roomName(firstRoomName)].tap() XCTAssert(app.buttons[firstRoomName].waitForExistence(timeout: 5.0)) try await Task.sleep(for: .seconds(1)) - + let cell = app.cells.element(boundBy: 1) // Skip the typing indicator cell cell.swipeRight(velocity: .slow) // The iOS 26 simulator doesn't like a fast swipe. - + try await app.assertScreenshot() } - + func testElementCall() { let app = Application.launch(.userSessionScreen) - + app.buttons[A11yIdentifiers.homeScreen.roomName(firstRoomName)].tap() XCTAssert(app.buttons[firstRoomName].waitForExistence(timeout: 5.0)) - + app.buttons[A11yIdentifiers.roomScreen.joinCall].tap() let textField = app.textFields["Display name"] @@ -106,7 +106,7 @@ class UserSessionScreenTests: XCTestCase { func testRoomDetails() { let app = Application.launch(.userSessionScreen) - + app.buttons[A11yIdentifiers.homeScreen.roomName(firstRoomName)].tap() XCTAssert(app.buttons[firstRoomName].waitForExistence(timeout: 5.0)) @@ -124,7 +124,7 @@ class UserSessionScreenTests: XCTestCase { attempts += 1 } } - + // Open the room members list. app.buttons[A11yIdentifiers.roomDetailsScreen.people].tap() @@ -150,7 +150,7 @@ class UserSessionScreenTests: XCTestCase { func testPhotoSharing() { let app = Application.launch(.userSessionScreen) - + app.buttons[A11yIdentifiers.homeScreen.roomName(firstRoomName)].tap() XCTAssert(app.buttons[firstRoomName].waitForExistence(timeout: 5.0)) @@ -165,7 +165,7 @@ class UserSessionScreenTests: XCTestCase { func testDocumentSharing() { let app = Application.launch(.userSessionScreen) - + app.buttons[A11yIdentifiers.homeScreen.roomName(firstRoomName)].tap() XCTAssert(app.buttons[firstRoomName].waitForExistence(timeout: 5.0)) @@ -175,7 +175,7 @@ class UserSessionScreenTests: XCTestCase { func testLocationSharing() { let app = Application.launch(.userSessionScreen) - + app.buttons[A11yIdentifiers.homeScreen.roomName(firstRoomName)].tap() XCTAssert(app.buttons[firstRoomName].waitForExistence(timeout: 5.0)) diff --git a/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift b/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift index 9572ce333..1bde76ed3 100644 --- a/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/AnalyticsSettingsScreenViewModelTests.swift @@ -13,7 +13,7 @@ import Testing final class AnalyticsSettingsScreenViewModelTests { private let appSettings: AppSettings private let analytics: AnalyticsService - + private var viewModel: AnalyticsSettingsScreenViewModelProtocol! private var context: AnalyticsSettingsScreenViewModelType.Context! @@ -22,7 +22,7 @@ final class AnalyticsSettingsScreenViewModelTests { let client = AnalyticsClientMock() client.isRunning = false analytics = AnalyticsService(client: client, appSettings: appSettings) - + viewModel = AnalyticsSettingsScreenViewModel(appSettings: appSettings, analytics: analytics) context = viewModel.context @@ -32,7 +32,7 @@ final class AnalyticsSettingsScreenViewModelTests { func initialState() { #expect(!context.enableAnalytics) } - + @Test func optIn() { appSettings.analyticsConsentState = .optedOut diff --git a/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift index 169ae4f82..8a7bcd730 100644 --- a/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/AppLock/AppLockSettingsScreenViewModelTests.swift @@ -24,7 +24,7 @@ struct AppLockSetupSettingsScreenViewModelTests { appLockService = AppLockService(keychainController: keychainController, appSettings: .volatile()) viewModel = AppLockSetupSettingsScreenViewModel(appLockService: AppLockServiceMock.mock()) } - + @Test func disablingShowsAlert() { // Given a fresh screen with the PIN code enabled. diff --git a/UnitTests/Sources/AppLock/AppLockSetupBiometricsScreenViewModelTests.swift b/UnitTests/Sources/AppLock/AppLockSetupBiometricsScreenViewModelTests.swift index 1898b2f31..649277ca6 100644 --- a/UnitTests/Sources/AppLock/AppLockSetupBiometricsScreenViewModelTests.swift +++ b/UnitTests/Sources/AppLock/AppLockSetupBiometricsScreenViewModelTests.swift @@ -36,7 +36,7 @@ final class AppLockSetupBiometricsScreenViewModelTests { // Then the service should now have biometric unlock enabled. #expect(appLockService.enableBiometricUnlockCallsCount == 1) } - + @Test func skip() async throws { // When skipping biometrics. diff --git a/UnitTests/Sources/AudioPlayerStateTests.swift b/UnitTests/Sources/AudioPlayerStateTests.swift index 6db739593..7fc75a2fd 100644 --- a/UnitTests/Sources/AudioPlayerStateTests.swift +++ b/UnitTests/Sources/AudioPlayerStateTests.swift @@ -277,22 +277,22 @@ struct AudioPlayerStateTests { @Test func setPlaybackSpeed() { audioPlayerState.attachAudioPlayer(audioPlayerMock) - + #expect(audioPlayerState.playbackSpeed == .default) - + audioPlayerState.setPlaybackSpeed(.fast) #expect(audioPlayerState.playbackSpeed == .fast) #expect(audioPlayerMock.setPlaybackSpeedReceivedSpeed == 1.5) - + audioPlayerState.setPlaybackSpeed(.fastest) #expect(audioPlayerState.playbackSpeed == .fastest) #expect(audioPlayerMock.setPlaybackSpeedReceivedSpeed == 2.0) - + audioPlayerState.setPlaybackSpeed(.slow) #expect(audioPlayerState.playbackSpeed == .slow) #expect(audioPlayerMock.setPlaybackSpeedReceivedSpeed == 0.5) } - + @Test func audioPlayerActionsDidFailed() async throws { audioPlayerState.attachAudioPlayer(audioPlayerMock) diff --git a/UnitTests/Sources/ComposerToolbarViewModelTests.swift b/UnitTests/Sources/ComposerToolbarViewModelTests.swift index cde58f20b..b8961ec62 100644 --- a/UnitTests/Sources/ComposerToolbarViewModelTests.swift +++ b/UnitTests/Sources/ComposerToolbarViewModelTests.swift @@ -19,7 +19,7 @@ final class ComposerToolbarViewModelTests { private var viewModel: ComposerToolbarViewModel! private var completionSuggestionServiceMock: CompletionSuggestionServiceMock! private var draftServiceMock: ComposerDraftServiceMock! - + init() { setUpViewModel() } diff --git a/UnitTests/Sources/CreateRoomViewModelTests.swift b/UnitTests/Sources/CreateRoomViewModelTests.swift index 226fc693e..5d42e9d97 100644 --- a/UnitTests/Sources/CreateRoomViewModelTests.swift +++ b/UnitTests/Sources/CreateRoomViewModelTests.swift @@ -16,19 +16,19 @@ final class CreateRoomScreenViewModelTests { var clientProxy: ClientProxyMock! var spaceService: SpaceServiceProxyMock! var userSession: UserSessionMock! - + private let appSettings: AppSettings - + private let usersSubject = CurrentValueSubject<[UserProfileProxy], Never>([]) var context: CreateRoomScreenViewModel.Context { viewModel.context } - + init() { appSettings = AppSettings.volatile() } - + deinit { viewModel = nil clientProxy = nil diff --git a/UnitTests/Sources/EditRoomAddressScreenViewModelTests.swift b/UnitTests/Sources/EditRoomAddressScreenViewModelTests.swift index 612aa0be1..088d10bf9 100644 --- a/UnitTests/Sources/EditRoomAddressScreenViewModelTests.swift +++ b/UnitTests/Sources/EditRoomAddressScreenViewModelTests.swift @@ -121,20 +121,20 @@ struct EditRoomAddressScreenViewModelTests { confirm() return .success(true) } - + roomProxy.updateCanonicalAliasAltAliasesClosure = { roomAlias, altAliases in #expect(altAliases == []) #expect(roomAlias == "#room-name:matrix.org") confirm() return .success(()) } - + roomProxy.removeRoomAliasFromRoomDirectoryClosure = { roomAlias in #expect(roomAlias == "#old-room-name:matrix.org") confirm() return .success(true) } - + context.send(viewAction: .save) } diff --git a/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift b/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift index 0d677280e..7c018564e 100644 --- a/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift +++ b/UnitTests/Sources/EmojiPickerScreenViewModelTests.swift @@ -17,7 +17,7 @@ struct EmojiPickerScreenViewModelTests { var context: EmojiPickerScreenViewModel.Context { viewModel.context } - + @Test mutating func toggleReaction() async throws { setupViewModel() diff --git a/UnitTests/Sources/EmojiProviderTests.swift b/UnitTests/Sources/EmojiProviderTests.swift index c0d129231..782837205 100644 --- a/UnitTests/Sources/EmojiProviderTests.swift +++ b/UnitTests/Sources/EmojiProviderTests.swift @@ -24,7 +24,7 @@ struct EmojiProviderTests { let categories = await emojiProvider.categories() #expect(emojiLoaderMock.categories == categories) } - + @Test @MainActor func emojisLoadedAndSearchStringEmptyAllCategoriesReturned() async { let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"]) @@ -38,7 +38,7 @@ struct EmojiProviderTests { let categories = await emojiProvider.categories(searchString: "") #expect(emojiLoaderMock.categories == categories) } - + @Test @MainActor func emojisLoadedSecondTimeCachedValuesAreUsed() async { let item = EmojiItem(label: "test", unicode: "test", keywords: ["1", "2"], shortcodes: ["1", "2"]) diff --git a/UnitTests/Sources/InviteUsersViewModelTests.swift b/UnitTests/Sources/InviteUsersViewModelTests.swift index d724fa31b..f8590d476 100644 --- a/UnitTests/Sources/InviteUsersViewModelTests.swift +++ b/UnitTests/Sources/InviteUsersViewModelTests.swift @@ -108,7 +108,7 @@ final class InviteUsersScreenViewModelTests { context.send(viewAction: .toggleUser(.mockAlice)) try await deferredState.fulfill() - + context.send(viewAction: .proceed) #expect(context.presentConfirmationDialog) diff --git a/UnitTests/Sources/LiveLocationManagerTests.swift b/UnitTests/Sources/LiveLocationManagerTests.swift index 548d2c05a..6c1fd09f9 100644 --- a/UnitTests/Sources/LiveLocationManagerTests.swift +++ b/UnitTests/Sources/LiveLocationManagerTests.swift @@ -179,24 +179,24 @@ final class LiveLocationManagerTests { } // MARK: - Beacon info updates - + @Test func beaconInfoUpdateFromAnotherDeviceRemovesActiveSession() async throws { setUp() let roomProxy = makeRoomProxy(roomID: "!room:matrix.org") clientProxy.roomForIdentifierClosure = { _ in .joined(roomProxy) } - + try await manager.startLiveLocation(roomID: "!room:matrix.org", duration: .seconds(300)).get() try await simulateBeaconEcho(roomID: "!room:matrix.org", eventID: "$event:matrix.org") #expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] != nil) - + let deferred = deferFulfillment(appSettings.$liveLocationSharingSessionsByRoomID) { $0["!room:matrix.org"] == nil } beaconInfoSubject.send(LiveLocationOwnInfoUpdate(roomID: "!room:matrix.org", eventID: "$external_event:matrix.org", isLive: true)) try await deferred.fulfill() - + #expect(appSettings.liveLocationSharingSessionsByRoomID["!room:matrix.org"] == nil) } - + // MARK: - Reduced accuracy @Test diff --git a/UnitTests/Sources/LocationSharingScreenViewModelTests.swift b/UnitTests/Sources/LocationSharingScreenViewModelTests.swift index a2339e29f..cf1325dbc 100644 --- a/UnitTests/Sources/LocationSharingScreenViewModelTests.swift +++ b/UnitTests/Sources/LocationSharingScreenViewModelTests.swift @@ -404,7 +404,7 @@ struct LocationSharingScreenViewModelTests { analytics: AnalyticsServiceMock(.init()), userIndicatorController: UserIndicatorControllerMock(), mediaProvider: MediaProviderMock(.init())) - + // Initially no annotations and no map center since sender and share are both nil. #expect(context.viewState.annotations.isEmpty) #expect(context.mapCenterLocation == nil) @@ -441,7 +441,7 @@ struct LocationSharingScreenViewModelTests { mediaProvider: MediaProviderMock(.init())) viewModel.state.bindings.isLocationAuthorized = true } - + private mutating func setupViewModel(liveLocationManagerMock: LiveLocationManagerMock, members: [RoomMemberProxyMock] = .allMembersAsAdmin) { let appSettings = AppSettings.volatile() diff --git a/UnitTests/Sources/LoginScreenViewModelTests.swift b/UnitTests/Sources/LoginScreenViewModelTests.swift index 4c32a9dbb..0d1d03335 100644 --- a/UnitTests/Sources/LoginScreenViewModelTests.swift +++ b/UnitTests/Sources/LoginScreenViewModelTests.swift @@ -153,7 +153,7 @@ struct LoginScreenViewModelTests { #expect(context.viewState.canSubmit, "The form should be ready to submit.") } - + @Test mutating func oAuthServer() async throws { // Given the screen configured for matrix.org @@ -166,7 +166,7 @@ struct LoginScreenViewModelTests { context.username = "@bob:company.com" context.send(viewAction: .parseUsername) try await deferred.fulfill() - + // Then the view state should be updated with the homeserver and show the OAuth button. #expect(context.viewState.loginMode.supportsOAuthFlow, "The OAuth button should be shown.") @@ -186,7 +186,7 @@ struct LoginScreenViewModelTests { context.username = "@bob:server.net" context.send(viewAction: .parseUsername) try await deferred.fulfill() - + // Then the view state should be updated to show an alert. #expect(context.alertInfo?.id == .unknown, "An alert should be shown to the user.") @@ -206,7 +206,7 @@ struct LoginScreenViewModelTests { context.username = "@bob:secure.gov" context.send(viewAction: .parseUsername) try await deferred.fulfill() - + // Then the view state should be updated to show an alert. #expect(context.alertInfo?.id == .elementProAlert, "An alert should be shown to the user.") diff --git a/UnitTests/Sources/MediaProvider/MediaProviderTests.swift b/UnitTests/Sources/MediaProvider/MediaProviderTests.swift index 64aa21b2a..744102eec 100644 --- a/UnitTests/Sources/MediaProvider/MediaProviderTests.swift +++ b/UnitTests/Sources/MediaProvider/MediaProviderTests.swift @@ -139,7 +139,7 @@ struct MediaProviderTests { let url = URL.mockMXCImage let key = "\(url.absoluteString){\(avatarSize.scaledValue),\(avatarSize.scaledValue)}" let expectedImage = try loadTestImage() - + mediaLoader.loadMediaThumbnailForSourceWidthHeightReturnValue = expectedImage.pngData() _ = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: url, mimeType: "image/jpeg"), @@ -151,7 +151,7 @@ struct MediaProviderTests { @Test func whenLoadImageFromSourceAndImageNotCachedAndRetrieveImageFailsAndNoAvatarSize_imageContentIsLoaded() async throws { let expectedImage = try loadTestImage() - + mediaLoader.loadMediaContentForSourceReturnValue = expectedImage.pngData() let result = try await mediaProvider.loadImageFromSource(MediaSourceProxy(url: .mockMXCImage, mimeType: "image/jpeg"), diff --git a/UnitTests/Sources/MediaProvider/MockImageCache.swift b/UnitTests/Sources/MediaProvider/MockImageCache.swift index 7b339fca7..425a25fdc 100644 --- a/UnitTests/Sources/MediaProvider/MockImageCache.swift +++ b/UnitTests/Sources/MediaProvider/MockImageCache.swift @@ -13,7 +13,7 @@ class MockImageCache: ImageCache, @unchecked Sendable { var retrievedImagesInMemory = [String: UIImage]() var retrievedImages = [String: UIImage]() var storedImages = [String: UIImage]() - + override func retrieveImageInMemoryCache(forKey key: String, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { retrievedImagesInMemory[key] } diff --git a/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift b/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift index 6e64472e5..871ea0516 100644 --- a/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift +++ b/UnitTests/Sources/MediaUploadPreviewScreenViewModelTests.swift @@ -16,7 +16,7 @@ final class MediaUploadPreviewScreenViewModelTests { var clientProxy: ClientProxyMock! var userIndicatorController: UserIndicatorControllerMock private let appSettings: AppSettings - + var viewModel: MediaUploadPreviewScreenViewModel! var context: MediaUploadPreviewScreenViewModel.Context { viewModel.context @@ -30,7 +30,7 @@ final class MediaUploadPreviewScreenViewModelTests { init() { appSettings = AppSettings.volatile() appSettings.optimizeMediaUploads = false - + userIndicatorController = UserIndicatorControllerMock() } @@ -286,7 +286,7 @@ final class MediaUploadPreviewScreenViewModelTests { if let maxUploadSizeResult { clientProxy.underlyingMaxMediaUploadSize = maxUploadSizeResult } - + viewModel = MediaUploadPreviewScreenViewModel(mediaURLs: urls, caption: nil, title: "Some File", diff --git a/UnitTests/Sources/NSItemProviderTests.swift b/UnitTests/Sources/NSItemProviderTests.swift index d9d776ce3..217c4393e 100644 --- a/UnitTests/Sources/NSItemProviderTests.swift +++ b/UnitTests/Sources/NSItemProviderTests.swift @@ -11,7 +11,7 @@ import Testing struct NSItemProviderTests { // MARK: - hasPathExtension - + @Test func hasPathExtensionRecognisesRegisteredExtensions() { #expect(("IMG_1234.jpg" as NSString).hasPathExtension) @@ -19,7 +19,7 @@ struct NSItemProviderTests { #expect(("clip.heic" as NSString).hasPathExtension) #expect(("doc.pdf" as NSString).hasPathExtension) } - + @Test func hasPathExtensionRejectsDateLikeTrailingSegments() { // iOS screenshot drags expose a `suggestedName` like @@ -30,38 +30,38 @@ struct NSItemProviderTests { #expect(!("report 2025.10" as NSString).hasPathExtension) #expect(!("v1.2.3" as NSString).hasPathExtension) } - + @Test func hasPathExtensionRejectsAbsentExtension() { #expect(!("plainname" as NSString).hasPathExtension) #expect(!("" as NSString).hasPathExtension) #expect(!("trailing dot." as NSString).hasPathExtension) } - + // MARK: - storeData() - + @Test func storeDataAppendsExtensionWhenSuggestedNameLooksLikeAScreenshot() async throws { let imageURL = try #require(Bundle(for: BundleAnchor.self).url(forResource: "test_image.png", withExtension: nil), "Failed retrieving fixture") let provider = try #require(NSItemProvider(contentsOf: imageURL)) provider.suggestedName = "Screenshot 2026-05-22 at 14.49.40" - + let storedURL = try #require(await provider.storeData()) - + #expect(storedURL.pathExtension == "png") #expect(storedURL.deletingPathExtension().lastPathComponent == "Screenshot 2026-05-22 at 14.49.40") } - + @Test func storeDataPreservesSuggestedNameWithRealExtension() async throws { let imageURL = try #require(Bundle(for: BundleAnchor.self).url(forResource: "test_image.png", withExtension: nil), "Failed retrieving fixture") let provider = try #require(NSItemProvider(contentsOf: imageURL)) provider.suggestedName = "IMG_1234.png" - + let storedURL = try #require(await provider.storeData()) - + #expect(storedURL.lastPathComponent == "IMG_1234.png") } } diff --git a/UnitTests/Sources/NavigationRootCoordinatorTests.swift b/UnitTests/Sources/NavigationRootCoordinatorTests.swift index ea7bafe03..756ed7459 100644 --- a/UnitTests/Sources/NavigationRootCoordinatorTests.swift +++ b/UnitTests/Sources/NavigationRootCoordinatorTests.swift @@ -24,7 +24,7 @@ struct NavigationRootCoordinatorTests { let firstRootCoordinator = SomeTestCoordinator() navigationRootCoordinator.setRootCoordinator(firstRootCoordinator) - + assertCoordinatorsEqual(firstRootCoordinator, navigationRootCoordinator.rootCoordinator) let secondRootCoordinator = SomeTestCoordinator() diff --git a/UnitTests/Sources/NavigationSplitCoordinatorTests.swift b/UnitTests/Sources/NavigationSplitCoordinatorTests.swift index f487d6987..8f4d971cc 100644 --- a/UnitTests/Sources/NavigationSplitCoordinatorTests.swift +++ b/UnitTests/Sources/NavigationSplitCoordinatorTests.swift @@ -191,7 +191,7 @@ struct NavigationSplitCoordinatorTests { func splitTracksEmbeddedStackRootChanges() async { let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator) sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator()) - + navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator) assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator) @@ -210,7 +210,7 @@ struct NavigationSplitCoordinatorTests { func splitTracksEmbeddedStackChanges() async { let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator) sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator()) - + navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator) assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator) @@ -233,7 +233,7 @@ struct NavigationSplitCoordinatorTests { let sidebarNavigationStackCoordinator = NavigationStackCoordinator(navigationSplitCoordinator: navigationSplitCoordinator) sidebarNavigationStackCoordinator.setRootCoordinator(SomeTestCoordinator()) sidebarNavigationStackCoordinator.push(SomeTestCoordinator()) - + navigationSplitCoordinator.setSidebarCoordinator(sidebarNavigationStackCoordinator) assertCoordinatorsEqual(sidebarNavigationStackCoordinator.rootCoordinator, navigationSplitCoordinator.compactLayoutRootCoordinator) @@ -299,20 +299,20 @@ struct NavigationSplitCoordinatorTests { } } } - + @Test mutating func setRootDetailToNilAfterPoppingToRoot() async { navigationSplitCoordinator = NavigationSplitCoordinator(placeholderCoordinator: SomeTestCoordinator()) let sidebarCoordinator = NavigationStackCoordinator() sidebarCoordinator.setRootCoordinator(SomeTestCoordinator()) - + let detailCoordinator = NavigationStackCoordinator() detailCoordinator.setRootCoordinator(SomeTestCoordinator()) detailCoordinator.push(SomeTestCoordinator()) - + navigationSplitCoordinator.setSidebarCoordinator(sidebarCoordinator) navigationSplitCoordinator.setDetailCoordinator(detailCoordinator) - + await waitForConfirmation("Details coordinator should be nil, and the compact layout revert to the sidebar root", timeout: .seconds(1)) { [navigationSplitCoordinator] confirm in DispatchQueue.main.async { diff --git a/UnitTests/Sources/NavigationStackCoordinatorTests.swift b/UnitTests/Sources/NavigationStackCoordinatorTests.swift index 441afa253..e75882dcc 100644 --- a/UnitTests/Sources/NavigationStackCoordinatorTests.swift +++ b/UnitTests/Sources/NavigationStackCoordinatorTests.swift @@ -24,7 +24,7 @@ struct NavigationStackCoordinatorTests { let rootCoordinator = SomeTestCoordinator() navigationStackCoordinator.setRootCoordinator(rootCoordinator) - + assertCoordinatorsEqual(rootCoordinator, navigationStackCoordinator.rootCoordinator) } diff --git a/UnitTests/Sources/NotificationContentBuilderTests.swift b/UnitTests/Sources/NotificationContentBuilderTests.swift index deebe1f1d..816600651 100644 --- a/UnitTests/Sources/NotificationContentBuilderTests.swift +++ b/UnitTests/Sources/NotificationContentBuilderTests.swift @@ -139,7 +139,7 @@ struct NotificationContentBuilderTests { #expect(notificationContent.threadIdentifier == "bob:matrix.org!test:matrix.orgthread") #expect(notificationContent.attachments == []) } - + @Test mutating func roomMessageNotification() async { let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org", @@ -193,7 +193,7 @@ struct NotificationContentBuilderTests { #expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.org") #expect(notificationContent.attachments == []) } - + @Test mutating func roomMessageNotificationWithThread() async { let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org", @@ -220,7 +220,7 @@ struct NotificationContentBuilderTests { #expect(notificationContent.threadIdentifier == "bob:matrix.org!testroom:matrix.orgthread123") #expect(notificationContent.attachments == []) } - + @Test mutating func roomMessageNotificationWithThreadAndMention() async { let notificationItem = NotificationItemProxyMock(.init(roomID: "!testroom:matrix.org", diff --git a/UnitTests/Sources/NotificationManager/MockCoder.swift b/UnitTests/Sources/NotificationManager/MockCoder.swift index c75855be9..6ce84eba6 100644 --- a/UnitTests/Sources/NotificationManager/MockCoder.swift +++ b/UnitTests/Sources/NotificationManager/MockCoder.swift @@ -16,7 +16,7 @@ final class MockCoder: NSKeyedArchiver { override func decodeObject(forKey _: String) -> Any { "" } - + override func decodeInt64(forKey key: String) -> Int64 { 0 } diff --git a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift index 6f8662b99..fe844c42e 100644 --- a/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift +++ b/UnitTests/Sources/NotificationManager/NotificationManagerTests.swift @@ -23,7 +23,7 @@ final class NotificationManagerTests { private var notificationTappedDelegateCalled = false private var registerForRemoteNotificationsDelegateCalled: (() -> Void)? private let appSettings: AppSettings - + init() { appSettings = AppSettings.volatile() notificationCenter = UserNotificationCenterMock() @@ -53,7 +53,7 @@ final class NotificationManagerTests { let success = await notificationManager.register(with: Data()) #expect(success) } - + @Test func whenRegisteredAndPusherThrowsError_completionFalseIsCalled() async { enum TestError: Error { @@ -64,7 +64,7 @@ final class NotificationManagerTests { let success = await notificationManager.register(with: Data()) #expect(!success) } - + @Test func whenRegistered_pusherIsCalledWithCorrectValues() async throws { let pushkeyData = Data("1234".utf8) @@ -93,21 +93,21 @@ final class NotificationManagerTests { pusherNotificationClientIdentifier: nil) #expect(try data.defaultPayload == (defaultPayload.toJsonString())) } - + @Test func whenRegisteredAndPusherTagNotSetInSettings_tagGeneratedAndSavedInSettings() async { appSettings.pusherProfileTag = nil _ = await notificationManager.register(with: Data()) #expect(appSettings.pusherProfileTag != nil) } - + @Test func whenRegisteredAndPusherTagIsSetInSettings_tagNotGenerated() async { appSettings.pusherProfileTag = "12345" _ = await notificationManager.register(with: Data()) #expect(appSettings.pusherProfileTag == "12345") } - + @Test func whenShowLocalNotification_notificationRequestGetsAdded() async throws { await notificationManager.showLocalNotification(with: "Title", subtitle: "Subtitle") @@ -132,13 +132,13 @@ final class NotificationManagerTests { options: []) #expect(notificationCenter.setNotificationCategoriesReceivedCategories == [messageCategory, inviteCategory]) } - + @Test func whenStart_delegateIsSet() throws { let delegate = try #require(notificationCenter.delegate) #expect(delegate.isEqual(notificationManager)) } - + @Test func whenStart_requestAuthorizationCalledWithCorrectParams() async { await waitForConfirmation("requestAuthorization should be called", timeout: .seconds(10)) { confirm in @@ -150,7 +150,7 @@ final class NotificationManagerTests { } #expect(notificationCenter.requestAuthorizationOptionsReceivedOptions == [.alert, .sound, .badge]) } - + @Test func whenStartAndAuthorizationGranted_delegateCalled() async { authorizationStatusWasGranted = false @@ -191,7 +191,7 @@ final class NotificationManagerTests { #expect(authorizationStatusWasGranted) } - + @Test func whenWillPresentNotificationsDelegateNotSet_CorrectPresentationOptionsReturned() async throws { let archiver = MockCoder(requiringSecureCoding: false) @@ -199,27 +199,27 @@ final class NotificationManagerTests { let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification) #expect(options == [.badge, .sound, .list, .banner]) } - + @Test func whenWillPresentNotificationsDelegateSetAndNotificationsShoudNotBeDisplayed_CorrectPresentationOptionsReturned() async throws { shouldDisplayInAppNotificationReturnValue = false notificationManager.delegate = self - + let notification = try UNNotification.with(userInfo: [AnyHashable: Any]()) let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification) #expect(options == []) } - + @Test func whenWillPresentNotificationsDelegateSetAndNotificationsShoudBeDisplayed_CorrectPresentationOptionsReturned() async throws { shouldDisplayInAppNotificationReturnValue = true notificationManager.delegate = self - + let notification = try UNNotification.with(userInfo: [AnyHashable: Any]()) let options = await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), willPresent: notification) #expect(options == [.badge, .sound, .list, .banner]) } - + @Test func whenNotificationCenterReceivedResponseInLineReply_delegateIsCalled() async throws { handleInlineReplyDelegateCalled = false @@ -228,7 +228,7 @@ final class NotificationManagerTests { await notificationManager.userNotificationCenter(UNUserNotificationCenter.current(), didReceive: response) #expect(handleInlineReplyDelegateCalled) } - + @Test func whenNotificationCenterReceivedResponseWithActionIdentifier_delegateIsCalled() async throws { notificationTappedDelegateCalled = false diff --git a/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift index 55bbb648a..f1ab5893c 100644 --- a/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsEditScreenViewModelTests.swift @@ -40,7 +40,7 @@ struct NotificationSettingsEditScreenViewModelTests { } } viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession) - + let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil } viewModel.fetchInitialContent() @@ -78,7 +78,7 @@ struct NotificationSettingsEditScreenViewModelTests { true } viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession) - + let deferred = deferFulfillment(viewModel.context.observe(\.viewState.defaultMode)) { $0 != nil } viewModel.fetchInitialContent() @@ -114,7 +114,7 @@ struct NotificationSettingsEditScreenViewModelTests { var deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.pendingMode), transitionValues: [nil, .allMessages, nil]) - + context.send(viewAction: .setMode(.allMessages)) try await deferredViewState.fulfill() @@ -135,11 +135,11 @@ struct NotificationSettingsEditScreenViewModelTests { transitionValues: [.allMessages]) try await deferredViewState.fulfill() - + #expect(context.viewState.defaultMode == .allMessages) #expect(context.viewState.bindings.alertInfo == nil) } - + @Test mutating func setModeMentions() async throws { viewModel = NotificationSettingsEditScreenViewModel(chatType: .groupChat, userSession: userSession) @@ -152,7 +152,7 @@ struct NotificationSettingsEditScreenViewModelTests { var deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.pendingMode), transitionValues: [nil, .mentionsAndKeywordsOnly, nil]) - + context.send(viewAction: .setMode(.mentionsAndKeywordsOnly)) try await deferredViewState.fulfill() @@ -173,11 +173,11 @@ struct NotificationSettingsEditScreenViewModelTests { transitionValues: [.mentionsAndKeywordsOnly]) try await deferredViewState.fulfill() - + #expect(context.viewState.defaultMode == .mentionsAndKeywordsOnly) #expect(context.viewState.bindings.alertInfo == nil) } - + @Test mutating func setModeDirectChats() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly @@ -196,7 +196,7 @@ struct NotificationSettingsEditScreenViewModelTests { context.send(viewAction: .setMode(.allMessages)) try await deferredViewState.fulfill() - + // `setDefaultRoomNotificationModeIsEncryptedIsOneToOneMode` must have been called twice (for encrypted and unencrypted direct chats) let invocations = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations #expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 2) @@ -209,7 +209,7 @@ struct NotificationSettingsEditScreenViewModelTests { #expect(invocations[1].isOneToOne == true) #expect(invocations[1].mode == .allMessages) } - + @Test mutating func setModeFailure() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneReturnValue = .mentionsAndKeywordsOnly @@ -224,14 +224,14 @@ struct NotificationSettingsEditScreenViewModelTests { let deferredViewState = deferFulfillment(viewModel.context.observe(\.viewState.pendingMode), transitionValues: [nil, .allMessages, nil]) - + context.send(viewAction: .setMode(.allMessages)) try await deferredViewState.fulfill() #expect(context.viewState.bindings.alertInfo != nil) } - + @Test mutating func selectRoom() async throws { let roomID = "!roomidentifier:matrix.org" diff --git a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift index 01972db12..51e85387a 100644 --- a/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/NotificationSettingsScreenViewModelTests.swift @@ -19,7 +19,7 @@ struct NotificationSettingsScreenViewModelTests { private var userNotificationCenter: UserNotificationCenterMock private var notificationSettingsProxy: NotificationSettingsProxyMock private var toneManager: NotificationToneManagerMock - + init() throws { userNotificationCenter = UserNotificationCenterMock() userNotificationCenter.authorizationStatusReturnValue = .authorized @@ -39,21 +39,21 @@ struct NotificationSettingsScreenViewModelTests { isModallyPresented: false) context = viewModel.context } - + @Test func enableNotifications() { appSettings.enableNotifications = false context.send(viewAction: .changedEnableNotifications) #expect(appSettings.enableNotifications) } - + @Test func disableNotifications() { appSettings.enableNotifications = true context.send(viewAction: .changedEnableNotifications) #expect(!appSettings.enableNotifications) } - + @Test func fetchSettings() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in @@ -70,17 +70,17 @@ struct NotificationSettingsScreenViewModelTests { notificationSettingsProxy.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + #expect(notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneCallsCount == 4) #expect(notificationSettingsProxy.isRoomMentionEnabledCalled) #expect(notificationSettingsProxy.isCallEnabledCalled) - + #expect(context.viewState.settings?.groupChatsMode == .mentionsAndKeywordsOnly) #expect(context.viewState.settings?.directChatsMode == .allMessages) #expect(context.viewState.settings?.inconsistentSettings == []) #expect(context.viewState.bindings.alertInfo == nil) } - + @Test func inconsistentGroupChatsSettings() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in @@ -93,17 +93,17 @@ struct NotificationSettingsScreenViewModelTests { return .allMessages } } - + let deferred = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil } notificationSettingsProxy.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + #expect(context.viewState.settings?.groupChatsMode == .allMessages) #expect(context.viewState.settings?.inconsistentSettings == [.init(chatType: .groupChat, isEncrypted: false)]) } - + @Test func inconsistentDirectChatsSettings() async throws { notificationSettingsProxy.getDefaultRoomNotificationModeIsEncryptedIsOneToOneClosure = { isEncrypted, isOneToOne in @@ -116,17 +116,17 @@ struct NotificationSettingsScreenViewModelTests { return .allMessages } } - + let deferred = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil } - + notificationSettingsProxy.callbacks.send(.settingsDidChange) - + try await deferred.fulfill() - + #expect(context.viewState.settings?.directChatsMode == .allMessages) #expect(context.viewState.settings?.inconsistentSettings == [.init(chatType: .oneToOneChat, isEncrypted: false)]) } - + @Test func fixInconsistentSettings() async throws { // Initialize with a configuration mismatch where encrypted one-to-one chats is `.allMessages` and unencrypted one-to-one chats is `.mentionsAndKeywordsOnly` @@ -140,13 +140,13 @@ struct NotificationSettingsScreenViewModelTests { return .allMessages } } - + let deferredSettings = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil } notificationSettingsProxy.callbacks.send(.settingsDidChange) try await deferredSettings.fulfill() - + #expect(context.viewState.settings?.directChatsMode == .allMessages) #expect(context.viewState.settings?.inconsistentSettings == [.init(chatType: .oneToOneChat, isEncrypted: false)]) @@ -156,7 +156,7 @@ struct NotificationSettingsScreenViewModelTests { context.send(viewAction: .fixConfigurationMismatchTapped) try await deferredMismatch.fulfill() - + // Ensure we only fix the invalid setting: unencrypted one-to-one chats should be set to `.allMessages` (to match encrypted one-to-one chats) #expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 1) let callArguments = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedArguments @@ -164,7 +164,7 @@ struct NotificationSettingsScreenViewModelTests { #expect(callArguments?.isOneToOne == true) #expect(callArguments?.mode == .allMessages) } - + @Test func fixAllInconsistentSettings() async throws { // Initialize with a configuration mismatch where @@ -178,16 +178,16 @@ struct NotificationSettingsScreenViewModelTests { return .mentionsAndKeywordsOnly } } - + let deferredSettings = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil } - + notificationSettingsProxy.callbacks.send(.settingsDidChange) - + try await deferredSettings.fulfill() - + #expect(context.viewState.settings?.directChatsMode == .allMessages) #expect(context.viewState.settings?.inconsistentSettings == [.init(chatType: .groupChat, isEncrypted: false), .init(chatType: .oneToOneChat, isEncrypted: false)]) - + var deferredMismatch = deferFulfillment(viewModel.context.observe(\.viewState.fixingConfigurationMismatch)) { $0 } context.send(viewAction: .fixConfigurationMismatchTapped) @@ -197,7 +197,7 @@ struct NotificationSettingsScreenViewModelTests { deferredMismatch = deferFulfillment(viewModel.context.observe(\.viewState.fixingConfigurationMismatch)) { !$0 } try await deferredMismatch.fulfill() - + // All problems should be fixed #expect(notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeCallsCount == 2) let callArguments = notificationSettingsProxy.setDefaultRoomNotificationModeIsEncryptedIsOneToOneModeReceivedInvocations @@ -210,7 +210,7 @@ struct NotificationSettingsScreenViewModelTests { #expect(callArguments[1].isOneToOne == true) #expect(callArguments[1].mode == .allMessages) } - + @Test func toggleRoomMentionOff() async throws { notificationSettingsProxy.isRoomMentionEnabledReturnValue = true @@ -220,7 +220,7 @@ struct NotificationSettingsScreenViewModelTests { notificationSettingsProxy.callbacks.send(.settingsDidChange) try await deferredState.fulfill() - + context.roomMentionsEnabled = false let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in @@ -230,11 +230,11 @@ struct NotificationSettingsScreenViewModelTests { context.send(viewAction: .roomMentionChanged) try await deferred.fulfill() - + #expect(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled) #expect(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled == false) } - + @Test func toggleRoomMentionOn() async throws { notificationSettingsProxy.isRoomMentionEnabledReturnValue = false @@ -243,7 +243,7 @@ struct NotificationSettingsScreenViewModelTests { viewModel.fetchInitialContent() try await deferredInitialFetch.fulfill() - + context.roomMentionsEnabled = true let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in @@ -253,11 +253,11 @@ struct NotificationSettingsScreenViewModelTests { context.send(viewAction: .roomMentionChanged) try await deferred.fulfill() - + #expect(notificationSettingsProxy.setRoomMentionEnabledEnabledCalled) #expect(notificationSettingsProxy.setRoomMentionEnabledEnabledReceivedEnabled == true) } - + @Test func toggleRoomMentionFailure() async throws { notificationSettingsProxy.setRoomMentionEnabledEnabledThrowableError = NotificationSettingsError.Generic(msg: "error") @@ -268,7 +268,7 @@ struct NotificationSettingsScreenViewModelTests { viewModel.fetchInitialContent() try await deferredInitialFetch.fulfill() - + context.roomMentionsEnabled = true var deferred = deferFulfillment(context.observe(\.viewState.applyingChange)) { $0 } @@ -280,10 +280,10 @@ struct NotificationSettingsScreenViewModelTests { deferred = deferFulfillment(context.observe(\.viewState.applyingChange)) { !$0 } try await deferred.fulfill() - + #expect(context.alertInfo != nil) } - + @Test func toggleCallsOff() async throws { notificationSettingsProxy.isCallEnabledReturnValue = true @@ -293,7 +293,7 @@ struct NotificationSettingsScreenViewModelTests { viewModel.fetchInitialContent() try await deferredInitialFetch.fulfill() - + context.callsEnabled = false let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in callback == .settingsDidChange @@ -302,21 +302,21 @@ struct NotificationSettingsScreenViewModelTests { context.send(viewAction: .callsChanged) try await deferred.fulfill() - + #expect(notificationSettingsProxy.setCallEnabledEnabledCalled) #expect(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled == false) } - + @Test func toggleCallsOn() async throws { notificationSettingsProxy.isCallEnabledReturnValue = false - + let deferredInitialFetch = deferFulfillment(viewModel.context.observe(\.viewState.settings)) { $0 != nil } viewModel.fetchInitialContent() try await deferredInitialFetch.fulfill() - + context.callsEnabled = true let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in @@ -326,11 +326,11 @@ struct NotificationSettingsScreenViewModelTests { context.send(viewAction: .callsChanged) try await deferred.fulfill() - + #expect(notificationSettingsProxy.setCallEnabledEnabledCalled) #expect(notificationSettingsProxy.setCallEnabledEnabledReceivedEnabled == true) } - + @Test func toggleCallsFailure() async throws { notificationSettingsProxy.setCallEnabledEnabledThrowableError = NotificationSettingsError.Generic(msg: "error") @@ -341,7 +341,7 @@ struct NotificationSettingsScreenViewModelTests { viewModel.fetchInitialContent() try await deferredInitialFetch.fulfill() - + context.callsEnabled = true var deferred = deferFulfillment(context.observe(\.viewState.applyingChange)) { $0 } @@ -366,7 +366,7 @@ struct NotificationSettingsScreenViewModelTests { viewModel.fetchInitialContent() try await deferredInitialFetch.fulfill() - + context.invitationsEnabled = false let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in callback == .settingsDidChange @@ -375,11 +375,11 @@ struct NotificationSettingsScreenViewModelTests { context.send(viewAction: .invitationsChanged) try await deferred.fulfill() - + #expect(notificationSettingsProxy.setInviteForMeEnabledEnabledCalled) #expect(notificationSettingsProxy.setInviteForMeEnabledEnabledReceivedEnabled == false) } - + @Test func toggleInvitationsOn() async throws { notificationSettingsProxy.isInviteForMeEnabledReturnValue = false @@ -389,7 +389,7 @@ struct NotificationSettingsScreenViewModelTests { viewModel.fetchInitialContent() try await deferredInitialFetch.fulfill() - + context.invitationsEnabled = true let deferred = deferFulfillment(notificationSettingsProxy.callbacks) { callback in @@ -399,13 +399,13 @@ struct NotificationSettingsScreenViewModelTests { context.send(viewAction: .invitationsChanged) try await deferred.fulfill() - + #expect(notificationSettingsProxy.setInviteForMeEnabledEnabledCalled) #expect(notificationSettingsProxy.setInviteForMeEnabledEnabledReceivedEnabled == true) } - + // MARK: - Alert Tones - + @Test func selectingAlertTonePersistsSelection() { // Given a tone and a manager that persists the selection to app settings @@ -414,28 +414,28 @@ struct NotificationSettingsScreenViewModelTests { appSettings.selectedNotificationTone = selectedTone return NotificationToneManager.libraryLocation } - + // When the user selects that tone context.send(viewAction: .selectAlertTone(tone)) - + // Then it is reflected in app settings #expect(appSettings.selectedNotificationTone == tone) } - + @Test func deletingActiveToneResetsSelection() { // Given the active tone is a custom tone let customTone = NotificationTone.createCustomUserSound(filename: "custom.caf") appSettings.selectedNotificationTone = customTone toneManager.customTonesReturnValue = [customTone] - + // When that tone is deleted context.send(viewAction: .deleteCustomAlertTones([customTone])) - + // Then the selection is cleared, falling back to the default #expect(appSettings.selectedNotificationTone == nil) } - + @Test func deletingNonActiveTonePreservesSelection() { // Given a custom tone is active and a different custom tone also exists @@ -443,14 +443,14 @@ struct NotificationSettingsScreenViewModelTests { let otherTone = NotificationTone.createCustomUserSound(filename: "other.caf") appSettings.selectedNotificationTone = activeTone toneManager.customTonesReturnValue = [activeTone, otherTone] - + // When the non-active tone is deleted context.send(viewAction: .deleteCustomAlertTones([otherTone])) - + // Then the active selection is unchanged #expect(appSettings.selectedNotificationTone == activeTone) } - + @Test func deletingToneUpdatesCustomList() { // Given a manager that initially returns one custom tone @@ -463,15 +463,15 @@ struct NotificationSettingsScreenViewModelTests { notificationSettingsProxy: notificationSettingsProxy, userIndicatorController: UserIndicatorControllerMock(), isModallyPresented: false) - + // When the tone is deleted and the manager reports no remaining tones remainingTones = [] localVM.context.send(viewAction: .deleteCustomAlertTones([customTone])) - + // Then the available list is empty #expect(localVM.context.viewState.availableCustomTones.isEmpty) } - + @Test func deleteCustomToneFailurePreservesSelection() { // Given the active tone is a custom tone and deletion will fail @@ -479,14 +479,14 @@ struct NotificationSettingsScreenViewModelTests { appSettings.selectedNotificationTone = customTone toneManager.deleteCustomToneThrowableError = NSError(domain: "test", code: 1) toneManager.customTonesReturnValue = [customTone] - + // When deletion is attempted context.send(viewAction: .deleteCustomAlertTones([customTone])) - + // Then the selection is unchanged #expect(appSettings.selectedNotificationTone == customTone) } - + @Test func importingCustomToneRefreshesAvailableList() async throws { // Given a tone file ready to import @@ -494,16 +494,16 @@ struct NotificationSettingsScreenViewModelTests { let location = NotificationToneManager.libraryLocation.appending(component: importedTone.filename) toneManager.addNewToneToLibraryFromReturnValue = location toneManager.customTonesReturnValue = [importedTone] - + // When the import result is received let deferred = deferFulfillment(context.observe(\.viewState.availableCustomTones)) { !$0.isEmpty } context.send(viewAction: .addedCustomAlertTone(.success(location))) - + // Then the available list is updated to include the new tone try await deferred.fulfill() #expect(context.viewState.availableCustomTones == [importedTone]) } - + @Test func toggleInvitesFailure() async throws { notificationSettingsProxy.setInviteForMeEnabledEnabledThrowableError = NotificationSettingsError.Generic(msg: "error") @@ -514,7 +514,7 @@ struct NotificationSettingsScreenViewModelTests { viewModel.fetchInitialContent() try await deferredInitialFetch.fulfill() - + context.invitationsEnabled = true var deferred = deferFulfillment(context.observe(\.viewState.applyingChange)) { $0 } diff --git a/UnitTests/Sources/NotificationToneManagerTests.swift b/UnitTests/Sources/NotificationToneManagerTests.swift index 677423833..9706cfc91 100644 --- a/UnitTests/Sources/NotificationToneManagerTests.swift +++ b/UnitTests/Sources/NotificationToneManagerTests.swift @@ -16,18 +16,18 @@ import Testing /// relying on that behavior. struct NotificationToneManagerTests { private let manager: NotificationToneManager - + init() { manager = NotificationToneManager(appSettings: .volatile()) } - + // MARK: - Deletion - + @Test func deletingSystemToneThrowsNotACustomTone() { // Given a system tone (lives outside the library directory) let tone = NotificationTone.createSystemSound(label: nil, filename: "alarm.caf") - + // When deletion is attempted // Then it throws because only library tones may be deleted let error = #expect(throws: NotificationToneManager.ManagerError.self) { @@ -36,12 +36,12 @@ struct NotificationToneManagerTests { #expect(error == .notACustomTone) } - + @Test func deletingBundledToneThrowsNotACustomTone() { // Given a bundled app tone (lives outside the library directory) let tone = NotificationTone.createBundledSound(label: nil, filename: "message.caf") - + // When deletion is attempted // Then it throws because only library tones may be deleted let error = #expect(throws: NotificationToneManager.ManagerError.self) { @@ -50,9 +50,9 @@ struct NotificationToneManagerTests { #expect(error == .notACustomTone) } - + // MARK: - Custom Tone Listing - + @Test func customTonesFiltersToCAFOnly() throws { // Given both CAF and non-CAF files written to the library directory @@ -64,17 +64,17 @@ struct NotificationToneManagerTests { } try Data().write(to: cafURL) try Data().write(to: mp3URL) - + // When fetching the list of custom tones let tones = manager.customTones() - + // Then only the CAF file is included #expect(tones.contains { $0.filename == cafURL.lastPathComponent }) #expect(!tones.contains { $0.filename == mp3URL.lastPathComponent }) } - + // MARK: - Import - + @Test func addingDuplicateToneThrowsFileAlreadyExists() async throws { // Given a CAF file that has already been imported into the library @@ -88,7 +88,7 @@ struct NotificationToneManagerTests { } try Data().write(to: sourceURL) try await manager.addNewToneToLibrary(from: sourceURL) - + // When importing the same file a second time // Then it throws ConversionError (specifically fileAlreadyExists) await #expect(throws: NotificationToneManager.ManagerError.self) { diff --git a/UnitTests/Sources/PillContextTests.swift b/UnitTests/Sources/PillContextTests.swift index 8d595c6cc..4e3a18dc9 100644 --- a/UnitTests/Sources/PillContextTests.swift +++ b/UnitTests/Sources/PillContextTests.swift @@ -17,7 +17,7 @@ struct PillContextTests { func user() async { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let id = "@test:matrix.org" let proxyMock = JoinedRoomProxyMock(.init(name: "Test")) let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) @@ -51,7 +51,7 @@ struct PillContextTests { func ownUser() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let id = "@test:matrix.org" let proxyMock = JoinedRoomProxyMock(.init(name: "Test", ownUserID: id)) let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([]) @@ -76,7 +76,7 @@ struct PillContextTests { func allUsers() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let avatarURL = URL(string: "https://matrix.jpg") let id = "test_room" let displayName = "Test" @@ -104,7 +104,7 @@ struct PillContextTests { func roomIDMention() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let proxyMock = JoinedRoomProxyMock(.init()) let mockController = MockTimelineController() let clientMock = ClientProxyMock(.init()) @@ -132,7 +132,7 @@ struct PillContextTests { func roomIDMentionMissingRoom() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let proxyMock = JoinedRoomProxyMock(.init()) let mockController = MockTimelineController() mockController.roomProxy = proxyMock @@ -158,7 +158,7 @@ struct PillContextTests { func roomAliasMention() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let proxyMock = JoinedRoomProxyMock(.init()) let mockController = MockTimelineController() mockController.roomProxy = proxyMock @@ -188,7 +188,7 @@ struct PillContextTests { func roomAliasMentionMissingRoom() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let proxyMock = JoinedRoomProxyMock(.init()) let mockController = MockTimelineController() mockController.roomProxy = proxyMock @@ -214,7 +214,7 @@ struct PillContextTests { func eventOnRoomIDMention() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let proxyMock = JoinedRoomProxyMock(.init()) let mockController = MockTimelineController() mockController.roomProxy = proxyMock @@ -242,7 +242,7 @@ struct PillContextTests { func eventOnRoomIDMentionMissingRoom() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let proxyMock = JoinedRoomProxyMock(.init()) let mockController = MockTimelineController() mockController.roomProxy = proxyMock @@ -268,7 +268,7 @@ struct PillContextTests { func eventOnRoomAliasMention() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let proxyMock = JoinedRoomProxyMock(.init()) let mockController = MockTimelineController() mockController.roomProxy = proxyMock @@ -298,7 +298,7 @@ struct PillContextTests { func eventOnRoomAliasMentionMissingRoom() { let appSettings = AppSettings.volatile() let userIndicatorController = UserIndicatorControllerMock() - + let proxyMock = JoinedRoomProxyMock(.init()) let mockController = MockTimelineController() mockController.roomProxy = proxyMock diff --git a/UnitTests/Sources/PollFormScreenViewModelTests.swift b/UnitTests/Sources/PollFormScreenViewModelTests.swift index f82fd4fab..f9f0f2844 100644 --- a/UnitTests/Sources/PollFormScreenViewModelTests.swift +++ b/UnitTests/Sources/PollFormScreenViewModelTests.swift @@ -17,7 +17,7 @@ struct PollFormScreenViewModelTests { private var context: PollFormScreenViewModelType.Context { viewModel.context } - + @Test mutating func newPollInitialState() async throws { setupViewModel() @@ -89,7 +89,7 @@ struct PollFormScreenViewModelTests { context.send(viewAction: .cancel) #expect(context.alertInfo != nil) } - + @Test mutating func newPollSubmit() async throws { setupViewModel() @@ -97,7 +97,7 @@ struct PollFormScreenViewModelTests { context.options[0].text = "bla1" context.options[1].text = "bla2" #expect(!context.viewState.isSubmitButtonDisabled) - + let deferred = deferFulfillment(viewModel.actions) { $0 == .close } try await confirmation { confirmation in @@ -115,7 +115,7 @@ struct PollFormScreenViewModelTests { try await deferred.fulfill() } } - + @Test mutating func editPollSubmit() async throws { setupViewModel(mode: .edit(eventID: "foo", poll: .emptyDisclosed)) @@ -123,7 +123,7 @@ struct PollFormScreenViewModelTests { context.question = "What is your favorite country?" context.options.append(.init(text: "France 🇫🇷")) #expect(!context.viewState.isSubmitButtonDisabled) - + let deferred = deferFulfillment(viewModel.actions) { $0 == .close } try await confirmation { confirmation in @@ -152,7 +152,7 @@ struct PollFormScreenViewModelTests { context.question = "What is your favorite country?" context.options.append(.init(text: "France 🇫🇷")) #expect(!context.viewState.isSubmitButtonDisabled) - + let deferredFailure = deferFailure(viewModel.actions, timeout: .seconds(1)) { $0 == .close } context.send(viewAction: .delete) diff --git a/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift b/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift index b385b8f34..c72acf42a 100644 --- a/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift +++ b/UnitTests/Sources/QRCodeLoginScreenViewModelTests.swift @@ -107,7 +107,7 @@ struct QRCodeLoginScreenViewModelTests { } context.qrResult = .init() try await deferred.fulfill() - + deferred = deferFulfillment(context.$viewState) { state in state.state == .displayCode(.deviceCode("01")) } @@ -195,7 +195,7 @@ struct QRCodeLoginScreenViewModelTests { let deferredFailure = deferFailure(context.$viewState, timeout: .seconds(1)) { $0.state != currentState } linkMobileProgressSubject.send(.syncingSecrets) try await deferredFailure.fulfill() - + deferredAction = deferFulfillment(viewModel.actionsPublisher) { action in guard case .linkedDevice = action else { return false } return true diff --git a/UnitTests/Sources/ReportContentScreenViewModelTests.swift b/UnitTests/Sources/ReportContentScreenViewModelTests.swift index 89fc4a883..fa2630027 100644 --- a/UnitTests/Sources/ReportContentScreenViewModelTests.swift +++ b/UnitTests/Sources/ReportContentScreenViewModelTests.swift @@ -36,7 +36,7 @@ struct ReportContentScreenViewModelTests { } try await deferred.fulfill() - + // Then the content should be reported, but the user should not be included. #expect(roomProxy.reportContentReasonCallsCount == 1, "The content should always be reported.") #expect(roomProxy.reportContentReasonReceivedArguments?.eventID == eventID, "The event ID should match the content being reported.") @@ -59,7 +59,7 @@ struct ReportContentScreenViewModelTests { // When reporting the content and also ignoring the user. viewModel.state.bindings.reasonText = reportReason viewModel.state.bindings.ignoreUser = true - + viewModel.context.send(viewAction: .submit) let deferred = deferFulfillment(viewModel.actions) { action in diff --git a/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift b/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift index dd93ab1da..287bbe362 100644 --- a/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomChangePermissionsScreenViewModelTests.swift @@ -17,7 +17,7 @@ struct RoomChangePermissionsScreenViewModelTests { var context: RoomChangePermissionsScreenViewModelType.Context { viewModel.context } - + @Test mutating func changeSetting() throws { setup(isSpace: false) diff --git a/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift b/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift index 83afcc144..b12788c49 100644 --- a/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomDetailsScreenViewModelTests.swift @@ -21,7 +21,7 @@ struct RoomDetailsScreenViewModelTests { var context: RoomDetailsScreenViewModelType.Context { viewModel.context } - + var cancellables = Set() init() { @@ -174,7 +174,7 @@ struct RoomDetailsScreenViewModelTests { try await deferredRecipient.fulfill() #expect(context.viewState.dmRecipientInfo?.member == RoomMemberDetails(withProxy: recipient)) - + let deferredProcessing = deferFulfillment(viewModel.context.observe(\.viewState.isProcessingIgnoreRequest), transitionValues: [false, true, false]) @@ -240,7 +240,7 @@ struct RoomDetailsScreenViewModelTests { transitionValues: [false, true, false]) context.send(viewAction: .unignoreConfirmed) - + try await deferredProcessing.fulfill() #expect(context.viewState.dmRecipientInfo?.member.isIgnored == false) @@ -271,7 +271,7 @@ struct RoomDetailsScreenViewModelTests { transitionValues: [false, true, false]) context.send(viewAction: .unignoreConfirmed) - + try await deferredProcessing.fulfill() #expect(context.viewState.dmRecipientInfo?.member.isIgnored == true) diff --git a/UnitTests/Sources/RoomFlowCoordinatorTests.swift b/UnitTests/Sources/RoomFlowCoordinatorTests.swift index c4177382e..ff3394e1b 100644 --- a/UnitTests/Sources/RoomFlowCoordinatorTests.swift +++ b/UnitTests/Sources/RoomFlowCoordinatorTests.swift @@ -18,13 +18,13 @@ final class RoomFlowCoordinatorTests { var roomFlowCoordinator: RoomFlowCoordinator! var navigationStackCoordinator: NavigationStackCoordinator! var cancellables = Set() - + private let appSettings: AppSettings - + init() { appSettings = AppSettings.volatile() } - + @Test func roomPresentation() async throws { setupRoomFlowCoordinator() diff --git a/UnitTests/Sources/RoomListFiltersStateTests.swift b/UnitTests/Sources/RoomListFiltersStateTests.swift index 092b1b29e..f1b32e4ba 100644 --- a/UnitTests/Sources/RoomListFiltersStateTests.swift +++ b/UnitTests/Sources/RoomListFiltersStateTests.swift @@ -66,11 +66,11 @@ final class RoomListFiltersStateTests { state.activateFilter(.people) #expect(state.activeFilters == [.people]) #expect(state.availableFilters == [.unreads, .favourites]) - + state.activateFilter(.unreads) #expect(state.activeFilters == [.people, .unreads]) #expect(state.availableFilters == [.favourites]) - + state.activateFilter(.favourites) #expect(state.activeFilters == [.people, .unreads, .favourites]) #expect(state.availableFilters == []) @@ -86,7 +86,7 @@ final class RoomListFiltersStateTests { state.activateFilter(.favourites) #expect(state.activeFilters == [.favourites]) #expect(state.availableFilters == [.unreads, .people, .rooms]) - + state.deactivateFilter(.favourites) #expect(state.activeFilters == []) #expect(state.availableFilters == allCasesWithoutLowPriority) @@ -94,7 +94,7 @@ final class RoomListFiltersStateTests { state.activateFilter(.rooms) #expect(state.activeFilters == [.rooms]) #expect(state.availableFilters == [.unreads, .favourites]) - + state.activateFilter(.unreads) #expect(state.activeFilters == [.rooms, .unreads]) #expect(state.availableFilters == [.favourites]) diff --git a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift index 86bed04fa..f4266fadf 100644 --- a/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift +++ b/UnitTests/Sources/RoomMemberDetailsViewModelTests.swift @@ -18,7 +18,7 @@ struct RoomMemberDetailsViewModelTests { var context: RoomMemberDetailsScreenViewModelType.Context { viewModel.context } - + @Test mutating func initialState() async throws { setup(roomMemberProxyMock: .mockAlice) @@ -30,7 +30,7 @@ struct RoomMemberDetailsViewModelTests { #expect(context.ignoreUserAlert == nil) #expect(context.alertInfo == nil) } - + @Test mutating func ignoreSuccess() async throws { setup(roomMemberProxyMock: .mockAlice) @@ -53,7 +53,7 @@ struct RoomMemberDetailsViewModelTests { try await Task.sleep(for: .milliseconds(100)) #expect(roomProxyMock.updateMembersCalled) } - + @Test mutating func ignoreFailure() async throws { let clientProxy = ClientProxyMock(.init()) @@ -79,7 +79,7 @@ struct RoomMemberDetailsViewModelTests { try await Task.sleep(for: .milliseconds(100)) #expect(!roomProxyMock.updateMembersCalled) } - + @Test mutating func unignoreSuccess() async throws { setup(roomMemberProxyMock: .mockIgnored) @@ -102,7 +102,7 @@ struct RoomMemberDetailsViewModelTests { try await Task.sleep(for: .milliseconds(100)) #expect(roomProxyMock.updateMembersCalled) } - + @Test mutating func unignoreFailure() async throws { let clientProxy = ClientProxyMock(.init()) @@ -128,7 +128,7 @@ struct RoomMemberDetailsViewModelTests { try await Task.sleep(for: .milliseconds(100)) #expect(!roomProxyMock.updateMembersCalled) } - + @Test mutating func initialStateAccountOwner() async throws { setup(roomMemberProxyMock: .mockMe) @@ -140,7 +140,7 @@ struct RoomMemberDetailsViewModelTests { #expect(context.ignoreUserAlert == nil) #expect(context.alertInfo == nil) } - + @Test mutating func initialStateIgnoredUser() async throws { setup(roomMemberProxyMock: .mockIgnored) @@ -154,7 +154,7 @@ struct RoomMemberDetailsViewModelTests { } // MARK: - History Sharing - + @Test mutating func inviteConfirmationFetchesIdentity() async throws { let clientProxy = ClientProxyMock(.init()) @@ -196,9 +196,9 @@ struct RoomMemberDetailsViewModelTests { #expect(clientProxy.userIdentityForFallBackToServerCalled) } - + // MARK: - Helpers - + private mutating func setup(roomMemberProxyMock: RoomMemberProxyMock, clientProxy: ClientProxyMock? = nil) { self.roomMemberProxyMock = roomMemberProxyMock roomProxyMock = JoinedRoomProxyMock(.init(name: "")) diff --git a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift index 108516fd2..fc48ce15d 100644 --- a/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomMembersListScreenViewModelTests.swift @@ -17,29 +17,29 @@ struct RoomMembersListScreenViewModelTests { var context: RoomMembersListScreenViewModel.Context { viewModel.context } - + @Test mutating func joinedMembers() async throws { setup(members: [.mockAlice, .mockBob]) - + let deferred = deferFulfillment(context.$viewState) { state in state.visibleJoinedMembers.count == 2 } try await deferred.fulfill() - + #expect(viewModel.state.joinedMembersCount == 2) #expect(viewModel.state.visibleJoinedMembers.count == 2) } - + @Test mutating func sortingMembers() async throws { setup(members: [.mockModerator, .mockDan, .mockAlice, .mockAdmin]) - + let deferred = deferFulfillment(context.$viewState) { state in state.visibleJoinedMembers.count == 4 } try await deferred.fulfill() - + let sortedMembers: [RoomMemberListScreenEntry] = [ .init(member: .init(withProxy: RoomMemberProxyMock.mockAdmin), verificationState: .notVerified), @@ -50,121 +50,121 @@ struct RoomMembersListScreenViewModelTests { .init(member: .init(withProxy: RoomMemberProxyMock.mockDan), verificationState: .notVerified) ] - + #expect(viewModel.state.visibleJoinedMembers == sortedMembers) } - + @Test mutating func search() async throws { setup(members: [.mockAlice, .mockBob]) - + let deferred = deferFulfillment(context.$viewState) { state in state.visibleJoinedMembers.count == 1 } context.searchQuery = "alice" try await deferred.fulfill() - + #expect(viewModel.state.joinedMembersCount == 2) #expect(viewModel.state.visibleJoinedMembers.count == 1) } - + @Test mutating func emptySearch() async throws { setup(members: [.mockAlice, .mockBob]) - + context.searchQuery = "WWW" let deferred = deferFulfillment(context.$viewState) { state in state.joinedMembersCount == 2 } try await deferred.fulfill() - + #expect(viewModel.state.joinedMembersCount == 2) #expect(viewModel.state.visibleJoinedMembers.count == 0) } - + @Test mutating func joinedAndInvitedMembers() async throws { setup(members: [.mockInvited, .mockBob]) - + let deferred = deferFulfillment(context.$viewState) { state in state.visibleInvitedMembers.count == 1 } try await deferred.fulfill() - + #expect(viewModel.state.joinedMembersCount == 1) #expect(viewModel.state.visibleInvitedMembers.count == 1) #expect(viewModel.state.visibleJoinedMembers.count == 1) } - + @Test mutating func invitedMembers() async throws { setup(members: [.mockInvited]) - + let deferred = deferFulfillment(context.$viewState) { state in state.visibleInvitedMembers.count == 1 } try await deferred.fulfill() - + #expect(viewModel.state.joinedMembersCount == 0) #expect(viewModel.state.visibleInvitedMembers.count == 1) #expect(viewModel.state.visibleJoinedMembers.count == 0) } - + @Test mutating func searchInvitedMembers() async throws { setup(members: [.mockInvited]) - + context.searchQuery = "invited" let deferred = deferFulfillment(context.$viewState) { state in state.visibleInvitedMembers.count == 1 } try await deferred.fulfill() - + #expect(viewModel.state.joinedMembersCount == 0) #expect(viewModel.state.visibleInvitedMembers.count == 1) #expect(viewModel.state.visibleJoinedMembers.count == 0) } - + @Test mutating func selectUserAsUser() async throws { setup(members: .allMembers) - + var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty } try await deferred.fulfill() - + deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil } guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else { Issue.record("Expected to find a regular user.") return } - + context.send(viewAction: .selectMember(user)) try await deferred.fulfill() - + #expect(context.manageMemeberViewModel != nil) #expect(context.manageMemeberViewModel?.state.memberDetails.id == user.id) #expect(context.manageMemeberViewModel?.state.permissions.canKick == false) #expect(context.manageMemeberViewModel?.state.permissions.canBan == false) } - + @Test mutating func selectUserAsAdmin() async throws { setup(members: .allMembersAsAdmin) - + var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } try await deferred.fulfill() - + #expect(context.manageMemeberViewModel == nil) - + deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil } guard let user = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .user && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else { Issue.record("Expected to find a regular user.") return } - + context.send(viewAction: .selectMember(user)) try await deferred.fulfill() - + #expect(context.manageMemeberViewModel?.state.memberDetails.id == user.id) #expect(context.manageMemeberViewModel?.state.permissions.canKick == true) #expect(context.manageMemeberViewModel?.state.permissions.canBan == true) @@ -172,25 +172,25 @@ struct RoomMembersListScreenViewModelTests { #expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == false) #expect(context.manageMemeberViewModel?.state.isMemberBanned == false) } - + @Test mutating func selectModeratorAsAdmin() async throws { setup(members: .allMembersAsAdmin) - + var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } try await deferred.fulfill() - + #expect(context.manageMemeberViewModel == nil) - + deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil } guard let moderator = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role == .moderator })?.member else { Issue.record("Expected to find a moderator.") return } - + context.send(viewAction: .selectMember(moderator)) try await deferred.fulfill() - + #expect(context.manageMemeberViewModel?.state.memberDetails.id == moderator.id) #expect(context.manageMemeberViewModel?.state.permissions.canKick == true) #expect(context.manageMemeberViewModel?.state.permissions.canBan == true) @@ -198,23 +198,23 @@ struct RoomMembersListScreenViewModelTests { #expect(context.manageMemeberViewModel?.state.isKickDisabled == false) #expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == false) } - + @Test mutating func selectAdminAsAdmin() async throws { setup(members: .allMembersAsAdmin) - + var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } try await deferred.fulfill() - + deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil } guard let admin = viewModel.state.visibleJoinedMembers.first(where: { $0.member.role.isAdminOrHigher && $0.member.id != RoomMemberProxyMock.mockMe.userID })?.member else { Issue.record("Expected to find another admin.") return } - + context.send(viewAction: .selectMember(admin)) try await deferred.fulfill() - + #expect(context.manageMemeberViewModel?.state.memberDetails.id == admin.id) #expect(context.manageMemeberViewModel?.state.permissions.canKick == true) #expect(context.manageMemeberViewModel?.state.permissions.canBan == true) @@ -222,44 +222,44 @@ struct RoomMembersListScreenViewModelTests { #expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == true) #expect(context.manageMemeberViewModel?.state.isMemberBanned == false) } - + @Test mutating func selectOwnMemberAsAdmin() async throws { setup(members: .allMembersAsAdmin) - + let deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty } try await deferred.fulfill() - + let memberDetailsAction = deferFulfillment(viewModel.actions) { $0.isSelectMember } guard let ownMember = viewModel.state.visibleJoinedMembers.first(where: { $0.member.id == RoomMemberProxyMock.mockMe.userID })?.member else { Issue.record("Expected to find own user admin.") return } - + context.send(viewAction: .selectMember(ownMember)) try await memberDetailsAction.fulfill() - + #expect(context.manageMemeberViewModel == nil) } - + @Test mutating func selectBannedMember() async throws { setup(members: .allMembersAsAdmin + RoomMemberProxyMock.mockBanned) - + var deferred = deferFulfillment(context.$viewState) { !$0.visibleInvitedMembers.isEmpty && $0.canKickUsers && $0.canBanUsers } try await deferred.fulfill() - + #expect(context.alertInfo == nil) - + deferred = deferFulfillment(context.$viewState) { $0.bindings.manageMemeberViewModel != nil } guard let bannedMember = viewModel.state.visibleBannedMembers.first?.member else { Issue.record("Expected to find a banned user.") return } - + context.send(viewAction: .selectMember(bannedMember)) try await deferred.fulfill() - + #expect(context.manageMemeberViewModel?.state.memberDetails.id == bannedMember.id) #expect(context.manageMemeberViewModel?.state.permissions.canKick == true) #expect(context.manageMemeberViewModel?.state.permissions.canBan == true) @@ -267,32 +267,32 @@ struct RoomMembersListScreenViewModelTests { #expect(context.manageMemeberViewModel?.state.isBanUnbanDisabled == false) #expect(context.manageMemeberViewModel?.state.isMemberBanned == true) } - + @Test mutating func switchesToMembersModeWhenThereAreNoBannedMembers() async throws { roomProxy = JoinedRoomProxyMock(.init(name: "test")) - + let subject = CurrentValueSubject<[RoomMemberProxyProtocol], Never>([RoomMemberProxyMock].allMembersAsAdmin + RoomMemberProxyMock.mockBanned) roomProxy.membersPublisher = subject.asCurrentValuePublisher() - + viewModel = RoomMembersListScreenViewModel(userSession: UserSessionMock(.init()), roomProxy: roomProxy, userIndicatorController: UserIndicatorControllerMock(), analytics: AnalyticsServiceMock(.init())) - + let context = viewModel.context - + var deferred = deferFulfillment(context.$viewState) { $0.visibleBannedMembers.count == 4 && $0.bindings.mode == .banned } context.mode = .banned try await deferred.fulfill() - + deferred = deferFulfillment(context.$viewState) { $0.visibleBannedMembers.count == 0 && $0.bindings.mode == .members } subject.value = [RoomMemberProxyMock].allMembersAsAdmin try await deferred.fulfill() } - + // MARK: - Helpers - + private mutating func setup(members: [RoomMemberProxyMock]) { roomProxy = JoinedRoomProxyMock(.init(name: "test", members: members)) viewModel = RoomMembersListScreenViewModel(userSession: UserSessionMock(.init()), diff --git a/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift b/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift index 0264756cb..3cd6501f0 100644 --- a/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomNotificationSettingsScreenViewModelTests.swift @@ -16,7 +16,7 @@ struct RoomNotificationSettingsScreenViewModelTests { var roomProxyMock: JoinedRoomProxyMock! var notificationSettingsProxyMock: NotificationSettingsProxyMock! var cancellables = Set() - + init() { roomProxyMock = JoinedRoomProxyMock(.init(name: "Test")) notificationSettingsProxyMock = NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()) @@ -32,14 +32,14 @@ struct RoomNotificationSettingsScreenViewModelTests { let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, roomProxy: roomProxyMock, displayAsUserDefinedRoomSettings: false) - + let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in state.notificationSettingsState.isLoaded } notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + #expect(!viewModel.context.allowCustomSetting) #expect(viewModel.context.viewState.shouldDisplayMentionsOnlyDisclaimer) #expect(viewModel.context.viewState.description(mode: .mentionsAndKeywordsOnly) != nil) @@ -55,14 +55,14 @@ struct RoomNotificationSettingsScreenViewModelTests { let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, roomProxy: roomProxyMock, displayAsUserDefinedRoomSettings: false) - + let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in state.notificationSettingsState.isLoaded } notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + #expect(!viewModel.context.allowCustomSetting) #expect(!viewModel.context.viewState.shouldDisplayMentionsOnlyDisclaimer) #expect(viewModel.context.viewState.description(mode: .mentionsAndKeywordsOnly) == nil) @@ -78,14 +78,14 @@ struct RoomNotificationSettingsScreenViewModelTests { let viewModel = RoomNotificationSettingsScreenViewModel(notificationSettingsProxy: notificationSettingsProxyMock, roomProxy: roomProxyMock, displayAsUserDefinedRoomSettings: false) - + let deferred = deferFulfillment(viewModel.context.observe(\.viewState)) { state in state.notificationSettingsState.isLoaded } notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + #expect(!viewModel.context.allowCustomSetting) #expect(!viewModel.context.viewState.shouldDisplayMentionsOnlyDisclaimer) #expect(viewModel.context.viewState.description(mode: .mentionsAndKeywordsOnly) == nil) @@ -103,7 +103,7 @@ struct RoomNotificationSettingsScreenViewModelTests { notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + #expect(viewModel.context.allowCustomSetting) } @@ -119,7 +119,7 @@ struct RoomNotificationSettingsScreenViewModelTests { notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + let expectedAlertInfo = AlertInfo(id: RoomNotificationSettingsScreenErrorType.loadingSettingsFailed, title: L10n.commonError, message: L10n.screenRoomNotificationSettingsErrorLoadingSettings) @@ -140,7 +140,7 @@ struct RoomNotificationSettingsScreenViewModelTests { notificationSettingsProxyMock.callbacks.send(.settingsDidChange) try await deferred.fulfill() - + let deferredIsRestoringDefaultSettings = deferFulfillment(viewModel.context.observe(\.viewState.isRestoringDefaultSetting), transitionValues: [false, true, false]) @@ -296,7 +296,7 @@ struct RoomNotificationSettingsScreenViewModelTests { viewModel.context.send(viewAction: .deleteCustomSettingTapped) try await deferredViewState.fulfill() - + // an alert is expected #expect(viewModel.context.alertInfo?.id == .restoreDefaultFailed) // the `dismiss` action must not have been sent diff --git a/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift b/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift index 6dc5a3f2d..b72264b61 100644 --- a/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomPollsHistoryScreenViewModelTests.swift @@ -23,7 +23,7 @@ struct RoomPollsHistoryScreenViewModelTests { timelineController: timelineController, userIndicatorController: UserIndicatorControllerMock()) } - + @Test func backPaginate() async throws { timelineController.backPaginationResponses = [ @@ -32,7 +32,7 @@ struct RoomPollsHistoryScreenViewModelTests { PollRoomTimelineItem.mock(poll: .disclosed(createdByAccountOwner: false)), PollRoomTimelineItem.mock(poll: .endedDisclosed)] ] - + let deferredViewState = deferFulfillment(viewModel.context.$viewState, keyPath: \.isBackPaginating, transitionValues: [false, true, false]) viewModel.context.send(viewAction: .loadMore) @@ -52,7 +52,7 @@ struct RoomPollsHistoryScreenViewModelTests { PollRoomTimelineItem.mock(poll: .endedDisclosed)], [] ] - + let deferredViewState = deferFulfillment(viewModel.context.$viewState, keyPath: \.isBackPaginating, transitionValues: [false, true, false]) viewModel.context.send(viewAction: .loadMore) @@ -91,7 +91,7 @@ struct RoomPollsHistoryScreenViewModelTests { PollRoomTimelineItem.mock(poll: .endedDisclosed)], [] ] - + let deferredViewState = deferFulfillment(viewModel.context.$viewState) { value in !value.pollTimelineItems.isEmpty } @@ -110,25 +110,25 @@ struct RoomPollsHistoryScreenViewModelTests { @Test func endPoll() async throws { let deferred = deferFulfillment(interactionHandler.publisher.delay(for: 0.1, scheduler: DispatchQueue.main)) { _ in true } - + interactionHandler.endPollPollStartIDReturnValue = .success(()) viewModel.context.send(viewAction: .end(pollStartID: "somePollID")) - + try await deferred.fulfill() #expect(interactionHandler.endPollPollStartIDCalled) #expect(interactionHandler.endPollPollStartIDReceivedPollStartID == "somePollID") } - + @Test func endPollFailure() async throws { let deferred = deferFulfillment(viewModel.context.$viewState) { value in value.bindings.alertInfo != nil } - + interactionHandler.endPollPollStartIDReturnValue = .failure(SDKError.generic) viewModel.context.send(viewAction: .end(pollStartID: "somePollID")) - + try await deferred.fulfill() #expect(interactionHandler.endPollPollStartIDCalled) @@ -138,26 +138,26 @@ struct RoomPollsHistoryScreenViewModelTests { @Test func sendPollResponse() async throws { let deferred = deferFulfillment(interactionHandler.publisher.delay(for: 0.1, scheduler: DispatchQueue.main)) { _ in true } - + interactionHandler.sendPollResponsePollStartIDOptionIDReturnValue = .success(()) viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", optionID: "someOptionID")) - + try await deferred.fulfill() #expect(interactionHandler.sendPollResponsePollStartIDOptionIDCalled) #expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].pollStartID == "somePollID") #expect(interactionHandler.sendPollResponsePollStartIDOptionIDReceivedInvocations[0].optionID == "someOptionID") } - + @Test func sendPollResponseFailure() async throws { let deferred = deferFulfillment(viewModel.context.$viewState) { value in value.bindings.alertInfo != nil } - + interactionHandler.sendPollResponsePollStartIDOptionIDReturnValue = .failure(SDKError.generic) viewModel.context.send(viewAction: .sendPollResponse(pollStartID: "somePollID", optionID: "someOptionID")) - + try await deferred.fulfill() #expect(interactionHandler.sendPollResponsePollStartIDOptionIDCalled) diff --git a/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift b/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift index f1ed812f8..aae37d7fe 100644 --- a/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomRolesAndPermissionsScreenViewModelTests.swift @@ -13,23 +13,23 @@ import Testing struct RoomRolesAndPermissionsScreenViewModelTests { var viewModel: RoomRolesAndPermissionsScreenViewModelProtocol! var roomProxy: JoinedRoomProxyMock! - + var context: RoomRolesAndPermissionsScreenViewModelType.Context { viewModel.context } - + @Test mutating func emptyCounters() { setup(members: .allMembers) - + #expect(context.viewState.administratorCount == 0) #expect(context.viewState.moderatorCount == 0) } - + @Test mutating func filledCounters() { setup(members: .allMembersAsAdmin) - + #expect(context.viewState.administratorCount == 2) #expect(context.viewState.moderatorCount == 1) } @@ -37,10 +37,10 @@ struct RoomRolesAndPermissionsScreenViewModelTests { @Test mutating func resetPermissions() async throws { setup(members: .allMembersAsAdmin) - + context.send(viewAction: .reset) #expect(context.alertInfo != nil) - + context.alertInfo?.primaryButton.action?() try await Task.sleep(for: .milliseconds(100)) @@ -51,14 +51,14 @@ struct RoomRolesAndPermissionsScreenViewModelTests { @Test mutating func demoteToModerator() async throws { setup(members: .allMembersAsAdmin) - + context.send(viewAction: .editOwnUserRole) #expect(context.alertInfo != nil) - + context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("moderator") }?.action?() - + try await Task.sleep(for: .milliseconds(100)) - + #expect(roomProxy.updatePowerLevelsForUsersCalled) #expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel == RoomRole.moderator.powerLevelValue) } @@ -66,20 +66,20 @@ struct RoomRolesAndPermissionsScreenViewModelTests { @Test mutating func demoteToMember() async throws { setup(members: .allMembersAsAdmin) - + context.send(viewAction: .editOwnUserRole) #expect(context.alertInfo != nil) - + context.alertInfo?.verticalButtons?.first { $0.title.localizedStandardContains("member") }?.action?() - + try await Task.sleep(for: .milliseconds(100)) - + #expect(roomProxy.updatePowerLevelsForUsersCalled) #expect(roomProxy.updatePowerLevelsForUsersReceivedUpdates?.first?.powerLevel == RoomRole.user.powerLevelValue) } - + // MARK: - Helpers - + private mutating func setup(members: [RoomMemberProxyMock]) { roomProxy = JoinedRoomProxyMock(.init(members: members)) viewModel = RoomRolesAndPermissionsScreenViewModel(roomProxy: roomProxy, diff --git a/UnitTests/Sources/RoomScreenViewModelTests.swift b/UnitTests/Sources/RoomScreenViewModelTests.swift index 420480c03..f3e0b513b 100644 --- a/UnitTests/Sources/RoomScreenViewModelTests.swift +++ b/UnitTests/Sources/RoomScreenViewModelTests.swift @@ -56,7 +56,7 @@ final class RoomScreenViewModelTests { try await deferred.fulfill() #expect(viewModel.context.viewState.pinnedEventsBannerState.isLoading) #expect(!viewModel.context.viewState.shouldShowPinnedEventsBanner) - + // check if if after the pinned event ids are set the banner is still in a loading state, but is both loading and showing with a counter deferred = deferFulfillment(viewModel.context.$viewState) { viewState in viewState.pinnedEventsBannerState.count == 2 @@ -98,7 +98,7 @@ final class RoomScreenViewModelTests { #expect(!viewModel.context.viewState.pinnedEventsBannerState.isLoading) #expect(viewModel.context.viewState.shouldShowPinnedEventsBanner) #expect(viewModel.context.viewState.pinnedEventsBannerState.selectedPinnedIndex == 1) - + // check how the scrolling changes the banner visibility viewModel.timelineHasScrolled(direction: .top) #expect(!viewModel.context.viewState.shouldShowPinnedEventsBanner) @@ -254,7 +254,7 @@ final class RoomScreenViewModelTests { #expect(viewModel.state.roomAvatar == .room(id: "TestID", name: "StartingName", avatarURL: nil)) #expect(!viewModel.state.canJoinCall) #expect(!viewModel.state.hasOngoingCall) - + let deferred = deferFulfillment(viewModel.context.$viewState) { viewState in viewState.roomTitle == "NewName" && viewState.roomAvatar == .room(id: "TestID", name: "NewName", avatarURL: .mockMXCAvatar) && @@ -358,7 +358,7 @@ final class RoomScreenViewModelTests { state.unseenKnockRequests == [.init(displayName: "Alice", avatarURL: nil, userID: "@alice:matrix.org", reason: "Hello World!", eventID: "1")] } try await deferred.fulfill() - + let deferredAction = deferFulfillment(viewModel.actions) { $0 == .displayKnockRequests } viewModel.context.send(viewAction: .viewKnockRequests) try await deferredAction.fulfill() @@ -450,7 +450,7 @@ final class RoomScreenViewModelTests { var configuration = JoinedRoomProxyMockConfiguration(isEncrypted: false, historyVisibility: .joined) let infoSubject = CurrentValueSubject(RoomInfoProxyMock(configuration)) let roomProxyMock = JoinedRoomProxyMock(configuration) - + // setup the room proxy actions publisher roomProxyMock.underlyingInfoPublisher = infoSubject.asCurrentValuePublisher() let viewModel = RoomScreenViewModel(userSession: UserSessionMock(.init()), diff --git a/UnitTests/Sources/RoomSummaryProviderTests.swift b/UnitTests/Sources/RoomSummaryProviderTests.swift index 4c47d3c36..8248da883 100644 --- a/UnitTests/Sources/RoomSummaryProviderTests.swift +++ b/UnitTests/Sources/RoomSummaryProviderTests.swift @@ -16,72 +16,72 @@ final class RoomSummaryProviderTests { private let baseFilters: [RoomListEntriesDynamicFilterKind] = [.any(filters: [.all(filters: [.nonSpace, .nonLeft]), .all(filters: [.space, .invite])]), .deduplicateVersions] - + var appSettings: AppSettings! var roomList: RoomListSDKMock! var dynamicEntriesController: RoomListDynamicEntriesControllerSDKMock! var roomSummaryProvider: RoomSummaryProvider! - + @Test func defaultRustFilters() async { // Given a new room provider. setup() await Task.yield() - + // Then it should have the default Rust filters enabled. #expect(dynamicEntriesController.setFilterKindCallsCount == 1) #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters)) - + // When setting one our user filters. roomSummaryProvider.setFilter(.all(filters: [.favourites])) await Task.yield() - + // Then that filter should be added to the default Rust filters. #expect(dynamicEntriesController.setFilterKindCallsCount == 2) #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.favourite, .joined])] + baseFilters)) } - + @Test func lowPriorityRustFilters() async { // Given a new room provider with the low priority filter enabled. setup(isLowPriorityFilterEnabled: true) await Task.yield() - + // Then the default Rust filters should include the non-low priority filter, // so that low priority rooms are hidden from the top of the room list. #expect(dynamicEntriesController.setFilterKindCallsCount == 1) #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters + [.nonLowPriority])) - + // When setting the low priority filter. roomSummaryProvider.setFilter(.all(filters: [.lowPriority])) await Task.yield() - + // Then the non-low priority filter should be replaced with the low priority filter. #expect(dynamicEntriesController.setFilterKindCallsCount == 2) #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.lowPriority, .joined])] + baseFilters)) - + // When setting another one of our filters. roomSummaryProvider.setFilter(.all(filters: [.rooms])) await Task.yield() - + // Then the filter should be combined with the non-low priority filter. #expect(dynamicEntriesController.setFilterKindCallsCount == 3) #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.category(expect: .group), .joined])] + baseFilters + [.nonLowPriority])) } - + @Test func roomIdentifierFilters() async { setup() await Task.yield() - + // Then it should have the default Rust filters enabled. #expect(dynamicEntriesController.setFilterKindCallsCount == 1) #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: baseFilters)) - + // When setting one our user filters. roomSummaryProvider.setFilter(.rooms(roomsIDs: ["SomeRoom"], filters: [.favourites])) await Task.yield() - + // Then that filter should be added to the default Rust filters. #expect(dynamicEntriesController.setFilterKindCallsCount == 2) #expect(dynamicEntriesController.setFilterKindReceivedInvocations.last == .all(filters: [.all(filters: [.favourite, .joined])] + baseFilters + [.identifiers(identifiers: ["SomeRoom"])])) @@ -92,20 +92,20 @@ final class RoomSummaryProviderTests { private func setup(isLowPriorityFilterEnabled: Bool = false) { appSettings = AppSettings.volatile() appSettings.lowPriorityFilterEnabled = isLowPriorityFilterEnabled - + let stateEventStringBuilder = RoomStateEventStringBuilder(userID: "@me:matrix.org") let attributedStringBuilder = AttributedStringBuilder(mentionBuilder: MentionBuilder()) let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: stateEventStringBuilder, messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: attributedStringBuilder, style: .senderPrefixed), shouldPrefixSenderName: true) - + roomSummaryProvider = RoomSummaryProvider(roomListService: RoomListServiceSDKMock(), eventStringBuilder: eventStringBuilder, name: "Test", notificationSettings: NotificationSettingsProxyMock(with: .init()), appSettings: appSettings) - + dynamicEntriesController = RoomListDynamicEntriesControllerSDKMock() dynamicEntriesController.setFilterKindReturnValue = true let dynamicAdaptersResult = RoomListEntriesWithDynamicAdaptersResultSDKMock() diff --git a/UnitTests/Sources/RoomTests.swift b/UnitTests/Sources/RoomTests.swift index 92dc7fb49..1cad582c2 100644 --- a/UnitTests/Sources/RoomTests.swift +++ b/UnitTests/Sources/RoomTests.swift @@ -20,7 +20,7 @@ struct RoomTests { var callIntent = await room.joinCallIntent() #expect(callIntent == .startCall) - + room.isDirectReturnValue = true callIntent = await room.joinCallIntent() #expect(callIntent == .startCallDm) diff --git a/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift b/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift index 225a82634..4ef4c8421 100644 --- a/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift +++ b/UnitTests/Sources/SecurityAndPrivacyScreenViewModelTests.swift @@ -20,7 +20,7 @@ final class SecurityAndPrivacyScreenViewModelTests { var context: SecurityAndPrivacyScreenViewModelType.Context { viewModel.context } - + deinit { viewModel = nil roomProxy = nil @@ -157,7 +157,7 @@ final class SecurityAndPrivacyScreenViewModelTests { #expect(context.desiredSettings.accessType == .spaceMembers(spaceIDs: [spaces[0].id])) #expect(context.viewState.shouldShowAccessSectionFooter) #expect(!context.viewState.isSaveDisabled) - + await waitForConfirmation("Join rule has updated") { confirm in roomProxy.updateJoinRuleClosure = { value in #expect(value == .restricted(rules: [.roomMembership(roomID: spaces[0].id)])) @@ -202,7 +202,7 @@ final class SecurityAndPrivacyScreenViewModelTests { #expect(context.desiredSettings.accessType == .askToJoinWithSpaceMembers(spaceIDs: [spaces[0].id])) #expect(context.viewState.shouldShowAccessSectionFooter) #expect(!context.viewState.isSaveDisabled) - + await waitForConfirmation("Join rule has updated") { confirm in roomProxy.updateJoinRuleClosure = { value in #expect(value == .knockRestricted(rules: [.roomMembership(roomID: spaces[0].id)])) @@ -249,7 +249,7 @@ final class SecurityAndPrivacyScreenViewModelTests { #expect(context.desiredSettings.accessType == .spaceMembers(spaceIDs: [spaces[0].id, "unknownSpaceID"])) #expect(context.viewState.shouldShowAccessSectionFooter) #expect(!context.viewState.isSaveDisabled) - + await waitForConfirmation("Join rule has updated") { confirm in roomProxy.updateJoinRuleClosure = { value in #expect(value == .restricted(rules: [.roomMembership(roomID: spaces[0].id), .roomMembership(roomID: "unknownSpaceID")])) diff --git a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift index fc8fd4f05..8b629e688 100644 --- a/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift +++ b/UnitTests/Sources/ServerConfirmationScreenViewModelTests.swift @@ -16,14 +16,14 @@ final class ServerConfirmationScreenViewModelTests { var clientFactory: AuthenticationClientFactoryMock! var client: ClientSDKMock! var service: AuthenticationServiceProtocol! - + var viewModel: ServerConfirmationScreenViewModel! var context: ServerConfirmationScreenViewModel.Context { viewModel.context } - + private let appSettings: AppSettings - + init() { appSettings = AppSettings.volatile() } diff --git a/UnitTests/Sources/SessionDirectoriesTests.swift b/UnitTests/Sources/SessionDirectoriesTests.swift index d98304935..afa3f9652 100644 --- a/UnitTests/Sources/SessionDirectoriesTests.swift +++ b/UnitTests/Sources/SessionDirectoriesTests.swift @@ -100,11 +100,11 @@ private extension SessionDirectories { var mockStateStorePath: String { dataDirectory.appending(component: "matrix-sdk-state.sqlite3").path(percentEncoded: false) } - + var mockCryptoStorePath: String { dataDirectory.appending(component: "matrix-sdk-crypto.sqlite3").path(percentEncoded: false) } - + var mockEventCachePath: String { cacheDirectory.appending(component: "matrix-sdk-event-cache.sqlite3").path(percentEncoded: false) } diff --git a/UnitTests/Sources/SessionVerificationViewModelTests.swift b/UnitTests/Sources/SessionVerificationViewModelTests.swift index 12cd118b4..da78303a9 100644 --- a/UnitTests/Sources/SessionVerificationViewModelTests.swift +++ b/UnitTests/Sources/SessionVerificationViewModelTests.swift @@ -25,7 +25,7 @@ struct SessionVerificationViewModelTests { mediaProvider: MediaProviderMock(.init())) context = viewModel.context } - + @Test func requestVerification() async throws { #expect(context.viewState.verificationState == .initial) @@ -58,7 +58,7 @@ struct SessionVerificationViewModelTests { context.send(viewAction: .restart) #expect(context.viewState.verificationState == .initial) - + #expect(sessionVerificationController.requestDeviceVerificationCallsCount == 1) #expect(sessionVerificationController.cancelVerificationCallsCount == 1) } diff --git a/UnitTests/Sources/SettingsScreenViewModelTests.swift b/UnitTests/Sources/SettingsScreenViewModelTests.swift index 793c6049f..166845c83 100644 --- a/UnitTests/Sources/SettingsScreenViewModelTests.swift +++ b/UnitTests/Sources/SettingsScreenViewModelTests.swift @@ -24,14 +24,14 @@ struct SettingsScreenViewModelTests { isInSecondaryWindow: false) context = viewModel.context } - + @Test func logout() async throws { let deferred = deferFulfillment(viewModel.actions) { $0 == .logout } context.send(viewAction: .logout) try await deferred.fulfill() } - + @Test func reportBug() async throws { let deferred = deferFulfillment(viewModel.actions) { $0 == .reportBug } diff --git a/UnitTests/Sources/SoftLogoutScreenViewModelTests.swift b/UnitTests/Sources/SoftLogoutScreenViewModelTests.swift index 634fbe869..48088a5ef 100644 --- a/UnitTests/Sources/SoftLogoutScreenViewModelTests.swift +++ b/UnitTests/Sources/SoftLogoutScreenViewModelTests.swift @@ -37,13 +37,13 @@ struct SoftLogoutScreenViewModelTests { keyBackupNeeded: true, password: "12345678") let context = viewModel.context - + // Given a view model where the user hasn't yet sent the verification email. #expect(context.viewState.canSubmit, "The view model should start with a valid password.") #expect(context.viewState.loginMode == .password, "The view model should show login form for the given homeserver.") #expect(context.viewState.showRecoverEncryptionKeysMessage, "The view model should show recover encryption keys message.") } - + @Test func initialStateForOAuth() { let viewModel = SoftLogoutScreenViewModel(credentials: credentials, @@ -64,7 +64,7 @@ struct SoftLogoutScreenViewModelTests { homeserver: .mockUnsupported, keyBackupNeeded: false) let context = viewModel.context - + // Given a view model where the user hasn't yet sent the verification email. #expect(context.password.isEmpty, "The view model should start with an empty password.") #expect(!context.viewState.canSubmit, "The view model should start with an invalid password.") diff --git a/UnitTests/Sources/SpaceScreenViewModelTests.swift b/UnitTests/Sources/SpaceScreenViewModelTests.swift index 18dd9d709..c7f6d10ce 100644 --- a/UnitTests/Sources/SpaceScreenViewModelTests.swift +++ b/UnitTests/Sources/SpaceScreenViewModelTests.swift @@ -26,7 +26,7 @@ struct SpaceScreenViewModelTests { var context: SpaceScreenViewModelType.Context { viewModel.context } - + @Test mutating func initialState() { setupViewModel() diff --git a/UnitTests/Sources/StringTests.swift b/UnitTests/Sources/StringTests.swift index 9e5008457..f2d4181a5 100644 --- a/UnitTests/Sources/StringTests.swift +++ b/UnitTests/Sources/StringTests.swift @@ -19,7 +19,7 @@ struct StringTests { func spaceIsAscii() { #expect("".isASCII) } - + @Test func johnnyIsAscii() { #expect("johnny".isASCII) @@ -54,7 +54,7 @@ struct StringTests { #expect("Heartbreak Hotel 🏩".asciified() == "Heartbreak Hotel", "The emoji should be stripped.") #expect("1️⃣2️⃣3️⃣".asciified() == "123", "The emoji should be converted to ASCII.") } - + @Test func ellipsizeWorks() { #expect("ellipsize".ellipsize(length: 5) == "ellip…") diff --git a/UnitTests/Sources/TimelineItemFactoryTests.swift b/UnitTests/Sources/TimelineItemFactoryTests.swift index a50496980..ec9b24f61 100644 --- a/UnitTests/Sources/TimelineItemFactoryTests.swift +++ b/UnitTests/Sources/TimelineItemFactoryTests.swift @@ -16,7 +16,7 @@ struct TimelineItemFactoryTests { func callInvite() throws { let ownUserID = "@alice:matrix.org" let senderUserID = "@bob:matrix.org" - + let factory = RoomTimelineItemFactory(userID: ownUserID, attributedStringBuilder: AttributedStringBuilder(mentionBuilder: MentionBuilder()), stateEventStringBuilder: RoomStateEventStringBuilder(userID: ownUserID)) @@ -24,7 +24,7 @@ struct TimelineItemFactoryTests { let eventTimelineItem = EventTimelineItem.mockCallInvite(sender: senderUserID) let eventTimelineItemProxy = EventTimelineItemProxy(item: eventTimelineItem, uniqueID: .init("0")) - + let item = try #require(factory.buildTimelineItem(for: eventTimelineItemProxy, isDM: false) as? CallInviteRoomTimelineItem, "Incorrect item type") diff --git a/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift b/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift index 56ccbefc1..faeca611c 100644 --- a/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift +++ b/UnitTests/Sources/TimelineMediaPreviewViewModelTests.swift @@ -19,7 +19,7 @@ struct TimelineMediaPreviewViewModelTests { var context: TimelineMediaPreviewViewModel.Context { viewModel.context } - + var mediaProvider: MediaProviderMock! var photoLibraryManager: PhotoLibraryManagerMock! var timelineController: MockTimelineController! diff --git a/UnitTests/Sources/TimelineStateTests.swift b/UnitTests/Sources/TimelineStateTests.swift index 01e23ec41..67de38c27 100644 --- a/UnitTests/Sources/TimelineStateTests.swift +++ b/UnitTests/Sources/TimelineStateTests.swift @@ -12,7 +12,7 @@ import Testing struct TimelineStateTests { // MARK: - Read marker - + @Test("Read marker unique ID is captured when present in the timeline") func readMarkerUniqueIDPresent() { var state = TimelineState() @@ -20,22 +20,22 @@ struct TimelineStateTests { state.itemsDictionary[.init("read-marker")] = makeReadMarkerViewState(uniqueID: "read-marker") state.itemsDictionary[.init("msg2")] = makeTextViewState(uniqueID: "msg2") state.recomputeReadMarkerUniqueID() - + #expect(state.readMarkerUniqueID == .init("read-marker")) } - + @Test("Read marker unique ID is nil when no marker exists") func readMarkerUniqueIDAbsent() { var state = TimelineState() state.itemsDictionary[.init("msg1")] = makeTextViewState(uniqueID: "msg1") state.itemsDictionary[.init("msg2")] = makeTextViewState(uniqueID: "msg2") state.recomputeReadMarkerUniqueID() - + #expect(state.readMarkerUniqueID == nil) } - + // MARK: - Helpers - + private func makeTextViewState(uniqueID: String) -> RoomTimelineItemViewState { let item = TextRoomTimelineItem(id: .virtual(uniqueID: .init(uniqueID)), timestamp: .mock, @@ -46,7 +46,7 @@ struct TimelineStateTests { content: .init(body: "Test message")) return RoomTimelineItemViewState(type: .text(item), groupStyle: .single) } - + private func makeReadMarkerViewState(uniqueID: String) -> RoomTimelineItemViewState { let item = ReadMarkerRoomTimelineItem(id: .virtual(uniqueID: .init(uniqueID))) return RoomTimelineItemViewState(type: .readMarker(item), groupStyle: .single) diff --git a/UnitTests/Sources/TimelineViewModelTests.swift b/UnitTests/Sources/TimelineViewModelTests.swift index f97ecf208..d7f4e1067 100644 --- a/UnitTests/Sources/TimelineViewModelTests.swift +++ b/UnitTests/Sources/TimelineViewModelTests.swift @@ -15,13 +15,13 @@ import Testing @MainActor final class TimelineViewModelTests { var cancellables = Set() - + init() async throws { cancellables.removeAll() } - + // MARK: - Message Grouping - + @Test func messageGrouping() { // Given 3 messages from Bob. @@ -305,9 +305,9 @@ final class TimelineViewModelTests { timelineController.timelineItems = items timelineController.roomProxy = roomProxy - + let appSettings = AppSettings.volatile() - + let viewModel = TimelineViewModel(roomProxy: roomProxy, timelineController: timelineController, userSession: UserSessionMock(.init()), @@ -333,7 +333,7 @@ final class TimelineViewModelTests { let id = message.id let appSettings = AppSettings.volatile() - + // When showing them in a timeline. let timelineController = MockTimelineController() timelineController.timelineItems = [message] @@ -360,7 +360,7 @@ final class TimelineViewModelTests { @Test func showManageUserAsAdmin() async throws { let appSettings = AppSettings.volatile() - + let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice], @@ -399,7 +399,7 @@ final class TimelineViewModelTests { @Test func showDetailsForAnAdmin() async throws { let appSettings = AppSettings.volatile() - + let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockAlice], @@ -438,7 +438,7 @@ final class TimelineViewModelTests { @Test func showDetailsForABannedUser() async throws { let appSettings = AppSettings.volatile() - + let viewModel = TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "", members: [RoomMemberProxyMock.mockAdmin, RoomMemberProxyMock.mockBanned[0]], @@ -480,7 +480,7 @@ final class TimelineViewModelTests { @Test func pinnedEvents() async throws { let appSettings = AppSettings.volatile() - + var configuration = JoinedRoomProxyMockConfiguration(name: "", pinnedEventIDs: .init(["test1"])) let roomProxyMock = JoinedRoomProxyMock(configuration) @@ -511,7 +511,7 @@ final class TimelineViewModelTests { @Test func canUserPinEvents() async throws { let appSettings = AppSettings.volatile() - + let configuration = JoinedRoomProxyMockConfiguration(name: "", powerLevelsConfiguration: .init(canUserPin: true)) let roomProxyMock = JoinedRoomProxyMock(configuration) @@ -588,7 +588,7 @@ final class TimelineViewModelTests { focussedEventID: String? = nil, timelineController: TimelineControllerProtocol) -> TimelineViewModel { let appSettings = AppSettings.volatile() - + return TimelineViewModel(roomProxy: roomProxy ?? JoinedRoomProxyMock(.init(name: "")), focussedEventID: focussedEventID, timelineController: timelineController, diff --git a/UnitTests/Sources/UserDetailsEditScreenViewModelTests.swift b/UnitTests/Sources/UserDetailsEditScreenViewModelTests.swift index 6b6558e3d..dcce979d3 100644 --- a/UnitTests/Sources/UserDetailsEditScreenViewModelTests.swift +++ b/UnitTests/Sources/UserDetailsEditScreenViewModelTests.swift @@ -13,7 +13,7 @@ import Testing @MainActor struct UserDetailsEditScreenViewModelTests { private var viewModel: UserDetailsEditScreenViewModel! - + private var context: UserDetailsEditScreenViewModelType.Context { viewModel.context } diff --git a/UnitTests/Sources/UserDiscoveryService/UserDiscoveryServiceTest.swift b/UnitTests/Sources/UserDiscoveryService/UserDiscoveryServiceTest.swift index 1d257e05c..f3acd2115 100644 --- a/UnitTests/Sources/UserDiscoveryService/UserDiscoveryServiceTest.swift +++ b/UnitTests/Sources/UserDiscoveryService/UserDiscoveryServiceTest.swift @@ -48,7 +48,7 @@ struct UserDiscoveryServiceTest { assertSearchResults(results, toBe: 3) #expect(!clientProxy.profileForCalled) } - + @Test func getProfileIsNotCalledForAccountOwnerID() async { clientProxy.searchUsersSearchTermLimitReturnValue = .success(.init(results: searchResults, limited: true)) diff --git a/UnitTests/Sources/UserPreferenceTests.swift b/UnitTests/Sources/UserPreferenceTests.swift index 03ce0660f..d4b0e86e1 100644 --- a/UnitTests/Sources/UserPreferenceTests.swift +++ b/UnitTests/Sources/UserPreferenceTests.swift @@ -127,7 +127,7 @@ struct UserPreferenceTests { let value = TestPreferences(testDefaults) value.codable = nil - + #expect(value.codable == nil) #expect(testDefaults.data(forKey: TestsKey.key3.rawValue) == nil) } diff --git a/UnitTests/Sources/UserProfileScreenViewModelTests.swift b/UnitTests/Sources/UserProfileScreenViewModelTests.swift index c0e7466cf..935488bf7 100644 --- a/UnitTests/Sources/UserProfileScreenViewModelTests.swift +++ b/UnitTests/Sources/UserProfileScreenViewModelTests.swift @@ -14,7 +14,7 @@ struct UserProfileScreenViewModelTests { @Test func initialState() async throws { let userIndicatorController = UserIndicatorControllerMock() - + let profile = UserProfileProxy(userID: "@alice:matrix.org", displayName: "Alice", avatarURL: .mockMXCAvatar) let clientProxy = ClientProxyMock(.init()) clientProxy.profileForReturnValue = .success(profile) @@ -38,7 +38,7 @@ struct UserProfileScreenViewModelTests { @Test func initialStateAccountOwner() async throws { let userIndicatorController = UserIndicatorControllerMock() - + let profile = UserProfileProxy(userID: RoomMemberProxyMock.mockMe.userID, displayName: "Me", avatarURL: .mockMXCAvatar) let clientProxy = ClientProxyMock(.init()) clientProxy.profileForReturnValue = .success(profile) @@ -62,7 +62,7 @@ struct UserProfileScreenViewModelTests { @Test func startingDmWithUnknownUserFetchesIdentity() async throws { let userIndicatorController = UserIndicatorControllerMock() - + let profile = UserProfileProxy.mockAlice let clientProxy = ClientProxyMock(.init()) @@ -82,7 +82,7 @@ struct UserProfileScreenViewModelTests { try await waitForMemberToLoad.fulfill() let deferred = deferFulfillment(context.observe(\.viewState.bindings).compactMap(\.inviteConfirmationUser), timeout: .seconds(5)) { $0.isUnknown } - + context.send(viewAction: .openDirectChat) try await deferred.fulfill() } diff --git a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift index 95c0a357d..f3c54691f 100644 --- a/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift +++ b/UnitTests/Sources/UserSessionFlowCoordinatorTests.swift @@ -51,7 +51,7 @@ struct UserSessionFlowCoordinatorTests { userIndicatorController = UserIndicatorControllerMock() let appSettings = AppSettings.volatile() - + let flowParameters = CommonFlowParameters(userSession: UserSessionMock(.init(clientProxy: clientProxy)), bugReportService: BugReportServiceMock(.init()), elementCallService: ElementCallServiceMock(.init()), diff --git a/UnitTests/Sources/VoiceMessageMediaManagerTests.swift b/UnitTests/Sources/VoiceMessageMediaManagerTests.swift index 1bd6cab36..93370ddd2 100644 --- a/UnitTests/Sources/VoiceMessageMediaManagerTests.swift +++ b/UnitTests/Sources/VoiceMessageMediaManagerTests.swift @@ -107,7 +107,7 @@ struct VoiceMessageMediaManagerTests { let loadedFile = URL("/some/url/loaded_file") // URL representing the final cached file let cachedConvertedFileURL = URL("/some/url/cached_converted_file") - + // Check if the file is not already present in cache voiceMessageCache.fileURLForReturnValue = nil let mediaSource = try MediaSourceProxy(url: someURL, mimeType: audioOGGMimeType) @@ -129,7 +129,7 @@ struct VoiceMessageMediaManagerTests { // The returned URL must point to the cached converted file #expect(url == cachedConvertedFileURL) } - + @Test mutating func loadVoiceMessageFromSourceMultipleCalls() async throws { // URL representing the file loaded by the media provider @@ -149,7 +149,7 @@ struct VoiceMessageMediaManagerTests { let audioConverter = AudioConverterMock() mediaProvider.loadFileFromSourceFilenameReturnValue = .success(MediaFileHandleProxy.unmanaged(url: loadedFile)) - + voiceMessageMediaManager = VoiceMessageMediaManager(mediaProvider: mediaProvider, voiceMessageCache: voiceMessageCache, audioConverter: audioConverter) @@ -159,10 +159,10 @@ struct VoiceMessageMediaManagerTests { let url = try await voiceMessageMediaManager.loadVoiceMessageFromSource(mediaSource, body: nil) #expect(url == cachedConvertedFileURL) } - + // The file must have been converted only once #expect(audioConverter.convertToMPEG4AACSourceURLDestinationURLCallsCount == 1) - + // The converted file must have been cached only once #expect(voiceMessageCache.cacheMediaSourceUsingMoveCallsCount == 1) } diff --git a/UnitTests/Sources/VoiceMessageRecorderTests.swift b/UnitTests/Sources/VoiceMessageRecorderTests.swift index acbb9dce5..921393fe8 100644 --- a/UnitTests/Sources/VoiceMessageRecorderTests.swift +++ b/UnitTests/Sources/VoiceMessageRecorderTests.swift @@ -20,17 +20,17 @@ struct VoiceMessageRecorderTests { private var audioRecorderActions: AnyPublisher { audioRecorderActionsSubject.eraseToAnyPublisher() } - + private var mediaPlayerProvider: MediaPlayerProviderMock! private var audioConverter: AudioConverterMock! private var voiceMessageCache: VoiceMessageCacheMock! - + private var audioPlayer: AudioPlayerMock! private var audioPlayerActionsSubject: PassthroughSubject = .init() private var audioPlayerActions: AnyPublisher { audioPlayerActionsSubject.eraseToAnyPublisher() } - + private let recordingURL = URL("/some/url") init() async throws { @@ -58,7 +58,7 @@ struct VoiceMessageRecorderTests { private func setRecordingComplete() async throws { audioRecorder.audioFileURL = recordingURL audioRecorder.currentTime = 5 - + let deferred = deferFulfillment(voiceMessageRecorder.actions) { action in switch action { case .didStopRecording(_, let url) where url == recordingURL: @@ -104,14 +104,14 @@ struct VoiceMessageRecorderTests { // The recording audio file must have been deleted #expect(audioRecorder.deleteRecordingCalled) } - + @Test func deleteRecording() async { await voiceMessageRecorder.deleteRecording() // The recording audio file must have been deleted #expect(audioRecorder.deleteRecordingCalled) } - + @Test func startPlaybackNoPreview() async { guard case .failure(.previewNotAvailable) = await voiceMessageRecorder.startPlayback() else { @@ -139,10 +139,10 @@ struct VoiceMessageRecorderTests { @Test func pausePlayback() async throws { try await setRecordingComplete() - + _ = await voiceMessageRecorder.startPlayback() #expect(voiceMessageRecorder.previewAudioPlayerState?.isAttached == true) - + voiceMessageRecorder.pausePlayback() #expect(audioPlayer.pauseCalled) } @@ -151,7 +151,7 @@ struct VoiceMessageRecorderTests { func resumePlayback() async throws { try await setRecordingComplete() audioPlayer.playbackURL = recordingURL - + guard case .success = await voiceMessageRecorder.startPlayback() else { Issue.record("Playback should start") return @@ -161,11 +161,11 @@ struct VoiceMessageRecorderTests { #expect(!audioPlayer.loadSourceURLPlaybackURLAutoplayCalled) #expect(audioPlayer.playCalled) } - + @Test func stopPlayback() async throws { try await setRecordingComplete() - + _ = await voiceMessageRecorder.startPlayback() #expect(voiceMessageRecorder.previewAudioPlayerState?.isAttached == true) @@ -177,10 +177,10 @@ struct VoiceMessageRecorderTests { @Test func seekPlayback() async throws { try await setRecordingComplete() - + _ = await voiceMessageRecorder.startPlayback() #expect(voiceMessageRecorder.previewAudioPlayerState?.isAttached == true) - + await voiceMessageRecorder.seekPlayback(to: 0.4) #expect(audioPlayer.seekToReceivedProgress == 0.4) } @@ -358,7 +358,7 @@ struct VoiceMessageRecorderTests { func audioRecorderActionHandling_didStopRecording() async throws { audioRecorder.audioFileURL = recordingURL audioRecorder.currentTime = 5 - + let deferred = deferFulfillment(voiceMessageRecorder.actions) { action in switch action { case .didStopRecording(_, let url) where url == recordingURL: diff --git a/compound-ios/Inspector/Sources/CompoundInspectorApp.swift b/compound-ios/Inspector/Sources/CompoundInspectorApp.swift index 081f3d231..a6ef944ec 100644 --- a/compound-ios/Inspector/Sources/CompoundInspectorApp.swift +++ b/compound-ios/Inspector/Sources/CompoundInspectorApp.swift @@ -18,7 +18,7 @@ struct CompoundInspectorApp: App { private var isDark: Bool { colorScheme == .dark } - + private var preferredColorScheme: ColorScheme? { ProcessInfo.processInfo.isMacCatalystApp ? colorScheme : nil } @@ -81,7 +81,7 @@ struct CompoundInspectorApp: App { Image(systemName: "textformat.size") } #endif - + Button(action: HyperionManager.sharedInstance().togglePluginDrawer) { Image(systemName: "ruler") } diff --git a/compound-ios/Inspector/Sources/Screen.swift b/compound-ios/Inspector/Sources/Screen.swift index 582a9ae9c..b235fa6b5 100644 --- a/compound-ios/Inspector/Sources/Screen.swift +++ b/compound-ios/Inspector/Sources/Screen.swift @@ -51,7 +51,7 @@ enum Screen: Hashable, View { case .gradients: GradientsScreen() case .fonts: FontsScreen() case .icons: IconsScreen() - + case .list: ListScreen() case .sizes: EmptyView() case .labels: EmptyView() @@ -59,7 +59,7 @@ enum Screen: Hashable, View { case .textFields: EmptyView() case .bigIcon: BigIconScreen() case .titleAndIcon: TitleAndIconScreen() - + case .actionSheet: ActionSheetScreen() case .alert: AlertScreen() case .contextMenu: ContextMenuScreen() diff --git a/compound-ios/Sources/Compound/BaseStyles/CompoundButtonStyle.swift b/compound-ios/Sources/Compound/BaseStyles/CompoundButtonStyle.swift index 680cb3645..5b41a1da6 100644 --- a/compound-ios/Sources/Compound/BaseStyles/CompoundButtonStyle.swift +++ b/compound-ios/Sources/Compound/BaseStyles/CompoundButtonStyle.swift @@ -69,7 +69,7 @@ public struct CompoundButtonStyle: ButtonStyle { case .toolbarIcon: 3 } } - + private var verticalPadding: CGFloat { if kind == .textLink { return 0 @@ -103,7 +103,7 @@ public struct CompoundButtonStyle: ButtonStyle { private var isUnderlined: Bool { kind == .textLink && accessibilityShowButtonShapes } - + public func makeBody(configuration: Self.Configuration) -> some View { configuration.label .font(font) @@ -147,7 +147,7 @@ public struct CompoundButtonStyle: ButtonStyle { EmptyView() } } - + private var contentShape: AnyShape { switch kind { case .super, .primary, .secondary, .tertiary: @@ -156,7 +156,7 @@ public struct CompoundButtonStyle: ButtonStyle { return AnyShape(Rectangle()) } } - + private func fillColor(configuration: Self.Configuration) -> Color { guard isEnabled else { return .compound.bgActionPrimaryDisabled } if configuration.role == .destructive { diff --git a/compound-ios/Sources/Compound/Colors/CompoundColors.swift b/compound-ios/Sources/Compound/Colors/CompoundColors.swift index 0a63ac12d..70289cf14 100644 --- a/compound-ios/Sources/Compound/Colors/CompoundColors.swift +++ b/compound-ios/Sources/Compound/Colors/CompoundColors.swift @@ -61,7 +61,7 @@ public class CompoundColors { } // MARK: - Decorative Colors - + // Used to determine the background and text colors of avatars, usernames etc. let decorativeColors: [DecorativeColor] diff --git a/compound-ios/Sources/Compound/Colors/CompoundGradients.swift b/compound-ios/Sources/Compound/Colors/CompoundGradients.swift index 3902159c1..7aa6e37de 100644 --- a/compound-ios/Sources/Compound/Colors/CompoundGradients.swift +++ b/compound-ios/Sources/Compound/Colors/CompoundGradients.swift @@ -25,7 +25,7 @@ public struct CompoundGradients { .compound.gradientActionStop3, .compound.gradientActionStop4]) } - + public var subtle: Gradient { .init(colors: [.compound.gradientSubtleStop1, .compound.gradientSubtleStop2, @@ -34,7 +34,7 @@ public struct CompoundGradients { .compound.gradientSubtleStop5, .compound.gradientSubtleStop6]) } - + public var info: Gradient { .init(colors: [.compound.gradientInfoStop1, .compound.gradientInfoStop2]) } diff --git a/compound-ios/Sources/Compound/Colors/CompoundUIColors.swift b/compound-ios/Sources/Compound/Colors/CompoundUIColors.swift index 88879ad5c..f886a6ae9 100644 --- a/compound-ios/Sources/Compound/Colors/CompoundUIColors.swift +++ b/compound-ios/Sources/Compound/Colors/CompoundUIColors.swift @@ -49,7 +49,7 @@ public final nonisolated class CompoundUIColors { public var _bgCodeBlock: UIColor { CompoundUIColors.coreTokens.gray100 } - + /// This token is a placeholder and hasn't been finalised. @available(iOS, deprecated: 100000.0, message: "This token should be generated by now.") public let _bgSubtleSecondaryAlpha = coreTokens.alphaGray300 diff --git a/compound-ios/Sources/Compound/List/ListInlinePicker.swift b/compound-ios/Sources/Compound/List/ListInlinePicker.swift index 67c0fe2c0..ac9acae72 100644 --- a/compound-ios/Sources/Compound/List/ListInlinePicker.swift +++ b/compound-ios/Sources/Compound/List/ListInlinePicker.swift @@ -21,7 +21,7 @@ struct ListInlinePicker: View { kind: .selection(isSelected: !isWaiting ? selection == item.tag : false) { var transaction = Transaction() transaction.disablesAnimations = true - + withTransaction(transaction) { selection = item.tag } diff --git a/compound-ios/Sources/Compound/List/ListRow.swift b/compound-ios/Sources/Compound/List/ListRow.swift index f30c51c0b..d8ebde161 100644 --- a/compound-ios/Sources/Compound/List/ListRow.swift +++ b/compound-ios/Sources/Compound/List/ListRow.swift @@ -145,7 +145,7 @@ public struct ListRow: View { guard isEnabled else { return .compound.textDisabled } return role == .destructive ? .compound.textCriticalPrimary : .compound.textPrimary } - + var titleLineLimit: Int? { layout == .avatar ? 1 : lineLimit } @@ -90,7 +90,7 @@ public struct ListRowLabel: View { var descriptionColor: Color { isEnabled ? .compound.textSecondary : .compound.textDisabled } - + var descriptionLineLimit: Int? { guard layout == .avatar else { return lineLimit } return role != .error ? 1 : lineLimit diff --git a/compound-ios/Sources/Compound/List/ListRowTrailingSection.swift b/compound-ios/Sources/Compound/List/ListRowTrailingSection.swift index fe4f1e5f0..44f7fd0ec 100644 --- a/compound-ios/Sources/Compound/List/ListRowTrailingSection.swift +++ b/compound-ios/Sources/Compound/List/ListRowTrailingSection.swift @@ -30,7 +30,7 @@ private struct ListRowDetailsLabelStyle: LabelStyle { /// This consists of both the `ListRowDetails` and the `ListRowAccessory`. public struct ListRowTrailingSection: View { @Environment(\.isEnabled) private var isEnabled - + private var title: String? private var icon: Icon? private var counter: Int? diff --git a/compound-ios/Sources/Compound/Previews/Snapshotting.swift b/compound-ios/Sources/Compound/Previews/Snapshotting.swift index eb468af54..df02ae2d3 100644 --- a/compound-ios/Sources/Compound/Previews/Snapshotting.swift +++ b/compound-ios/Sources/Compound/Previews/Snapshotting.swift @@ -11,7 +11,7 @@ import SwiftUI struct SnapshotPrecisionPreferenceKey: PreferenceKey { static var defaultValue: Float = 1.0 - + static func reduce(value: inout Float, nextValue: () -> Float) { value = nextValue() } @@ -19,7 +19,7 @@ struct SnapshotPrecisionPreferenceKey: PreferenceKey { struct SnapshotPerceptualPrecisionPreferenceKey: PreferenceKey { static var defaultValue: Float = 0.98 - + static func reduce(value: inout Float, nextValue: () -> Float) { value = nextValue() } diff --git a/compound-ios/Sources/Compound/Text Field Styles/SearchFieldStyle.swift b/compound-ios/Sources/Compound/Text Field Styles/SearchFieldStyle.swift index 410d84d72..e21fff677 100644 --- a/compound-ios/Sources/Compound/Text Field Styles/SearchFieldStyle.swift +++ b/compound-ios/Sources/Compound/Text Field Styles/SearchFieldStyle.swift @@ -77,7 +77,7 @@ struct SearchStyle_Previews: PreviewProvider, TestablePreview { Text("Settings") .compoundListSectionHeader() } - + Section { ListRow(label: .plain(title: "Some setting"), kind: .toggle(.constant(true))) diff --git a/compound-ios/Tests/CompoundTests/PreviewTests.swift b/compound-ios/Tests/CompoundTests/PreviewTests.swift index be3dbc54b..5d92f15c0 100644 --- a/compound-ios/Tests/CompoundTests/PreviewTests.swift +++ b/compound-ios/Tests/CompoundTests/PreviewTests.swift @@ -26,12 +26,12 @@ struct PreviewTests { private let snapshotDevices: [SnapshotDevice] = [.init(name: "iPhone", device: "iPhone 17"), .init(name: "iPad", device: "iPad")] private var recordMode: SnapshotTestingConfiguration.Record = .missing - + init() { if ProcessInfo().environment["RECORD_FAILURES"].map(Bool.init) == true { recordMode = .failed } - + checkEnvironments() UIView.setAnimationsEnabled(false) } @@ -44,7 +44,7 @@ struct PreviewTests { fatalError("\(deviceModel ?? "Unknown") is the wrong one. Switch to using \(simulatorDevice) for these tests.") } } - + let osVersion = ProcessInfo().operatingSystemVersion guard osVersion.majorVersion == requiredOSVersion.major, osVersion.minorVersion == requiredOSVersion.minor else { fatalError("Switch to iOS \(requiredOSVersion) for these tests.") @@ -55,7 +55,7 @@ struct PreviewTests { } // MARK: - Snapshots - + func assertSnapshots(matching preview: _Preview, testName: String = #function, step: Int) async throws { let preferences = SnapshotPreferences() @@ -107,15 +107,15 @@ struct PreviewTests { } return languageCode + "-" + regionCode } - + private var languageCode: String { Locale.current.language.languageCode?.identifier ?? "" } - + private var regionCode: String { Locale.current.language.region?.identifier ?? "" } - + private func assertSnapshots(matching view: AnyView, name: String?, isScreen: Bool, @@ -177,7 +177,7 @@ private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { layout: SwiftUISnapshotLayout = .sizeThatFits, traits: UITraitCollection = .init()) -> Snapshotting { let config: ViewImageConfig - + switch layout { #if os(iOS) || os(tvOS) case let .device(config: deviceConfig): @@ -191,21 +191,21 @@ private extension Snapshotting where Value: SwiftUI.View, Format == UIImage { // Make sure to use the workaround safe area insets. config = .init(safeArea: .one, size: size, traits: traits) } - + return SimplySnapshotting(pathExtension: "png", diffing: .prefireImage(preferences: preferences, scale: traits.displayScale)) .asyncPullback { @MainActor view in var config = config - + let controller: UIViewController - + if config.size != nil { controller = UIHostingController(rootView: view) } else { let hostingController = UIHostingController(rootView: view) - + let maxSize = CGSize.zero config.size = hostingController.sizeThatFits(in: maxSize) - + controller = hostingController }