Run SwiftFormat.

This commit is contained in:
Doug
2026-05-27 14:38:50 +01:00
committed by Doug
parent 5138e05e47
commit fa67713726
573 changed files with 2426 additions and 2426 deletions
@@ -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.")
+1 -1
View File
@@ -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]),
+1 -1
View File
@@ -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))
+4 -4
View File
@@ -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)"
+4 -4
View File
@@ -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:
+14 -14
View File
@@ -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
+2 -2
View File
@@ -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()
}
+2 -2
View File
@@ -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
}
+14 -14
View File
@@ -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
+12 -12
View File
@@ -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 = "#"
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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