mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 20:39:24 +00:00
Run SwiftFormat.
This commit is contained in:
@@ -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.")
|
||||
|
||||
@@ -28,7 +28,7 @@ final class AppHooks: AppHooksProtocol {
|
||||
var compoundHook: CompoundHookProtocol {
|
||||
_compoundHook.withLock { $0 }
|
||||
}
|
||||
|
||||
|
||||
func registerCompoundHook(_ hook: CompoundHookProtocol) {
|
||||
_compoundHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
@@ -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<String>?
|
||||
|
||||
|
||||
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")
|
||||
|
||||
@@ -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)`")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,9 +17,9 @@ protocol CoordinatorProtocol: AnyObject {
|
||||
|
||||
extension CoordinatorProtocol {
|
||||
func start() { }
|
||||
|
||||
|
||||
func stop() { }
|
||||
|
||||
|
||||
func toPresentable() -> AnyView {
|
||||
AnyView(Text("View not configured"))
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ struct CommonFlowParameters {
|
||||
var windowManager: WindowManagerProtocol {
|
||||
appMediator.windowManager
|
||||
}
|
||||
|
||||
|
||||
var ongoingCallRoomIDPublisher: CurrentValuePublisher<String?, Never> {
|
||||
elementCallService.ongoingCallRoomIDPublisher
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -13,7 +13,7 @@ enum AppAppearance: CaseIterable, Codable {
|
||||
case system
|
||||
case dark
|
||||
case light
|
||||
|
||||
|
||||
var interfaceStyle: UIUserInterfaceStyle {
|
||||
switch self {
|
||||
case .light:
|
||||
|
||||
@@ -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<TraceLogPack> { get }
|
||||
var bugReportRageshakeURL: RemotePreference<RageshakeConfiguration> { 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)
|
||||
|
||||
@@ -19,7 +19,7 @@ class RemotePreference<T: Equatable> {
|
||||
var publisher: CurrentValuePublisher<T, Never> {
|
||||
subject.asCurrentValuePublisher()
|
||||
}
|
||||
|
||||
|
||||
var isRemotelyConfigured: Bool {
|
||||
subject.value != defaultValue
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ final class UserPreference<T: Codable> {
|
||||
private var remoteKey: String {
|
||||
"\(Self.remotePrefix)\(key)"
|
||||
}
|
||||
|
||||
|
||||
private var keyedStorage: any KeyedStorage<T>
|
||||
private let defaultValue: T
|
||||
private let subject: PassthroughSubject<T, Never> = .init()
|
||||
@@ -84,7 +84,7 @@ final class UserPreference<T: Codable> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var projectedValue: AnyPublisher<T, Never> {
|
||||
subject
|
||||
.prepend(wrappedValue)
|
||||
@@ -97,7 +97,7 @@ final class UserPreference<T: Codable> {
|
||||
extension UserPreference {
|
||||
convenience init(key: any PreferenceKeyable, defaultValue: T, storage backingStorage: UserDefaultsProtocol, mode: Mode = .localOverRemote) {
|
||||
let storage: any KeyedStorage<T> = UserDefaultsStorage(userDefaults: backingStorage)
|
||||
|
||||
|
||||
self.init(key: key, defaultValue: defaultValue, keyedStorage: storage, mode: mode)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [:]
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ class WindowManager: SecureWindowManagerProtocol {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var windows: [UIWindow] {
|
||||
[mainWindow, overlayWindow, globalSearchWindow, alternateWindow]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class ChatsTabFlowCoordinator: FlowCoordinatorProtocol {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private let sidebarNavigationStackCoordinator: NavigationStackCoordinator
|
||||
|
||||
|
||||
private let selectedRoomSubject = CurrentValueSubject<String?, Never>(nil)
|
||||
|
||||
private let actionsSubject: PassthroughSubject<ChatsTabFlowCoordinatorAction, Never> = .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))
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ class PinnedEventsTimelineFlowCoordinator: FlowCoordinatorProtocol {
|
||||
.store(in: &cancellables)
|
||||
|
||||
stackCoordinator.setRootCoordinator(coordinator)
|
||||
|
||||
|
||||
navigationStackCoordinator.setSheetCoordinator(stackCoordinator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))),
|
||||
|
||||
@@ -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<String>)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -238,7 +238,7 @@ class SettingsFlowCoordinator: FlowCoordinatorProtocol {
|
||||
userIndicatorController: flowParameters.userIndicatorController))
|
||||
navigationStackCoordinator.push(coordinator)
|
||||
}
|
||||
|
||||
|
||||
private func presentNotificationSettings() {
|
||||
let notificationParameters = NotificationSettingsScreenCoordinatorParameters(navigationStackCoordinator: navigationStackCoordinator,
|
||||
userSession: flowParameters.userSession,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<SpaceSettingsFlowCoordinatorAction, Never> {
|
||||
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):
|
||||
|
||||
@@ -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.") }
|
||||
|
||||
@@ -478,7 +478,7 @@ class UserSessionFlowCoordinator: FlowCoordinatorProtocol {
|
||||
|
||||
navigationTabCoordinator.setOverlayCoordinator(nil)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Logout
|
||||
|
||||
private func runLogoutFlow() async {
|
||||
|
||||
@@ -26,7 +26,7 @@ extension AggregatedReaction {
|
||||
private static var alice: String {
|
||||
RoomMemberProxyMock.mockAlice.userID
|
||||
}
|
||||
|
||||
|
||||
static var mockReactions: [AggregatedReaction] {
|
||||
[
|
||||
mockReaction(key: "😅", senderIDs: [alice]),
|
||||
|
||||
@@ -159,7 +159,7 @@ extension ClientProxyMock {
|
||||
|
||||
underlyingTimelineMediaVisibilityPublisher = CurrentValueSubject<TimelineMediaVisibility, Never>(configuration.timelineMediaVisibility).asCurrentValuePublisher()
|
||||
underlyingHideInviteAvatarsPublisher = CurrentValueSubject<Bool, Never>(configuration.hideInviteAvatars).asCurrentValuePublisher()
|
||||
|
||||
|
||||
liveLocationOwnInfoUpdatesPublisher = PassthroughSubject<LiveLocationOwnInfoUpdate, Never>().eraseToAnyPublisher()
|
||||
|
||||
underlyingMaxMediaUploadSize = .success(configuration.maxMediaUploadSize)
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -17,7 +17,7 @@ struct RoomNotificationSettingsProxyMockConfiguration {
|
||||
extension RoomNotificationSettingsProxyMock {
|
||||
convenience init(with configuration: RoomNotificationSettingsProxyMockConfiguration) {
|
||||
self.init()
|
||||
|
||||
|
||||
isDefault = configuration.isDefault
|
||||
mode = configuration.mode
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ extension RoomPowerLevelsProxyMock {
|
||||
self.init()
|
||||
|
||||
underlyingValues = RoomPowerLevelsValues.mock
|
||||
|
||||
|
||||
canOwnUserSendMessageReturnValue = configuration.canUserSendMessage
|
||||
canOwnUserSendStateEventReturnValue = configuration.canUserSendState
|
||||
canOwnUserInviteReturnValue = configuration.canUserInvite
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ extension SessionVerificationControllerProxyMock {
|
||||
SessionVerificationEmoji(symbol: "🎂", description: "Cake"),
|
||||
SessionVerificationEmoji(symbol: "🏁", description: "Flag"),
|
||||
SessionVerificationEmoji(symbol: "🌏", description: "Globe")]
|
||||
|
||||
|
||||
static func configureMock(actions: PassthroughSubject<SessionVerificationControllerProxyAction, Never> = .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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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..<numX * numY).map { i in
|
||||
if i == 0 {
|
||||
let value = String(blurHash[2..<6]).decode83()
|
||||
@@ -46,20 +46,20 @@ public extension UIImage {
|
||||
return decodeAC(value, maximumValue: maximumValue * punch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let width = Int(size.width)
|
||||
let height = Int(size.height)
|
||||
let bytesPerRow = width * 3
|
||||
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
|
||||
CFDataSetLength(data, bytesPerRow * height)
|
||||
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
|
||||
|
||||
|
||||
for y in 0..<height {
|
||||
for x in 0..<width {
|
||||
var r: Float = 0
|
||||
var g: Float = 0
|
||||
var b: Float = 0
|
||||
|
||||
|
||||
for j in 0..<numY {
|
||||
for i in 0..<numX {
|
||||
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
|
||||
@@ -69,23 +69,23 @@ public extension UIImage {
|
||||
b += colour.2 * basis
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let intR = UInt8(linearTosRGB(r))
|
||||
let intG = UInt8(linearTosRGB(g))
|
||||
let intB = UInt8(linearTosRGB(b))
|
||||
|
||||
|
||||
pixels[3 * x + 0 + y * bytesPerRow] = intR
|
||||
pixels[3 * x + 1 + y * bytesPerRow] = intG
|
||||
pixels[3 * x + 2 + y * bytesPerRow] = intB
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
|
||||
|
||||
|
||||
guard let provider = CGDataProvider(data: data) else { return nil }
|
||||
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
|
||||
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }
|
||||
|
||||
|
||||
self.init(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ private func decodeAC(_ value: Int, maximumValue: Float) -> (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<Int>) -> Substring {
|
||||
let start = index(startIndex, offsetBy: bounds.lowerBound)
|
||||
let end = index(startIndex, offsetBy: bounds.upperBound)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -20,9 +20,9 @@ import Foundation
|
||||
value = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var value: Value?
|
||||
|
||||
|
||||
init(value: Value? = nil) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ actor ExpiringTaskRunner<T: Sendable> {
|
||||
private var continuation: CheckedContinuation<T, Error>?
|
||||
|
||||
private var task: @Sendable () async throws -> T
|
||||
|
||||
|
||||
init(_ task: @escaping @Sendable () async throws -> T) {
|
||||
self.task = task
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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: AlertProtocol, Actions: View>(item: Binding<Item?>, @ViewBuilder actions: (Item) -> Actions) -> some View {
|
||||
let binding = Binding<Bool>(get: {
|
||||
@@ -51,7 +51,7 @@ struct AlertInfo<T: Hashable>: Identifiable, AlertProtocol {
|
||||
var role: ButtonRole?
|
||||
let action: (() -> Void)?
|
||||
}
|
||||
|
||||
|
||||
struct AlertTextField: Identifiable {
|
||||
let id = UUID()
|
||||
let placeholder: String
|
||||
@@ -59,7 +59,7 @@ struct AlertInfo<T: Hashable>: 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?()
|
||||
|
||||
@@ -36,7 +36,7 @@ extension AttributedString {
|
||||
case (false, false):
|
||||
.plainText
|
||||
}
|
||||
|
||||
|
||||
components.append(AttributedStringBuilderComponent(id: String(attributedString.characters),
|
||||
attributedString: attributedString,
|
||||
type: componentType))
|
||||
|
||||
@@ -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] }
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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: ConfirmationDialogProtocol, Actions: View, Message: View>(item: Binding<Item?>,
|
||||
titleVisibility: Visibility = .automatic,
|
||||
|
||||
@@ -18,7 +18,7 @@ extension Encodable {
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
|
||||
func toJsonString(_ encoder: JSONEncoder = JSONEncoder()) throws -> String? {
|
||||
try toJsonDictionary(encoder).jsonString
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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<AnyCancellable>) {
|
||||
asCancellable().store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
func asCancellable() -> AnyCancellable {
|
||||
AnyCancellable(cancel)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ extension UIDevice {
|
||||
var isPhone: Bool {
|
||||
userInterfaceIdiom == .phone
|
||||
}
|
||||
|
||||
|
||||
var initialDeviceName: String {
|
||||
L10n.loginInitialDeviceNameIos(InfoPlistReader.main.bundleDisplayName)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
|
||||
private static let attributeMSC4286 = "msc4286-external-payment-details"
|
||||
private static let caches = Mutex<[String: LRUCache<String, AttributedString>]>([:])
|
||||
|
||||
|
||||
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<String, AttributedString>(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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ struct MapLibreStaticMapView<PinAnnotation: View>: 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<PinAnnotation: View>: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var placeholderImage: some View {
|
||||
Image(asset: Asset.Images.mapBlurred)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
}
|
||||
|
||||
|
||||
private var errorView: some View {
|
||||
Button {
|
||||
fetchAttempt += 1
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 = "#"
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import MatrixRustSDK
|
||||
/// protocol with a specialisation for the type it listens for.
|
||||
final class SDKListener<T>: 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) {
|
||||
|
||||
@@ -16,9 +16,9 @@ class ScrollViewAdapter: NSObject, UIScrollViewDelegate {
|
||||
scrollView?.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var shouldScrollToTopClosure: ((UIScrollView) -> Bool)?
|
||||
|
||||
|
||||
private let didScrollSubject = PassthroughSubject<Void, Never>()
|
||||
var didScroll: AnyPublisher<Void, Never> {
|
||||
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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -22,28 +22,28 @@ class StateStoreViewModel<State: BindableState, ViewAction> {
|
||||
///
|
||||
/// Left as public for `ViewModel` implementations convenience.
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
/// 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<State: BindableState, ViewAction> {
|
||||
@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<T>(dynamicMember keyPath: WritableKeyPath<State.BindStateType, T>) -> 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
|
||||
|
||||
@@ -24,26 +24,26 @@ class StateStoreViewModelV2<State: BindableState, ViewAction> {
|
||||
///
|
||||
/// Left as public for `ViewModel` implementations convenience.
|
||||
var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
/// 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<State: BindableState, ViewAction> {
|
||||
@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<T>(dynamicMember keyPath: WritableKeyPath<State.BindStateType, T>) -> 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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -12,7 +12,7 @@ import SwiftUI
|
||||
struct JoinCallButton: View {
|
||||
let isVoiceCall: Bool
|
||||
let action: () -> Void
|
||||
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 26, *) {
|
||||
glassButton
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user