mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Swift 6 migration for NSE (#5608)
* swift 6 support for NSE and concurrency safe implementations for it * enforcing required Sendable conformance for swift 6 migration in NSE * use Mutex to access the string builder cache * use an async stream to update timeline items diffs * fix for Sendable appHooks
This commit is contained in:
@@ -7,66 +7,107 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Synchronization
|
||||
|
||||
class AppHooks: AppHooksProtocol {
|
||||
final class AppHooks: AppHooksProtocol {
|
||||
#if IS_MAIN_APP
|
||||
func configure(with userSession: UserSessionProtocol?) async {
|
||||
await roomScreenHook.configure(with: userSession)
|
||||
}
|
||||
|
||||
private(set) var appSettingsHook: AppSettingsHookProtocol = DefaultAppSettingsHook()
|
||||
private let _appSettingsHook: Mutex<AppSettingsHookProtocol> = Mutex(DefaultAppSettingsHook())
|
||||
var appSettingsHook: AppSettingsHookProtocol {
|
||||
_appSettingsHook.withLock { $0 }
|
||||
}
|
||||
|
||||
func registerAppSettingsHook(_ hook: AppSettingsHookProtocol) {
|
||||
appSettingsHook = hook
|
||||
_appSettingsHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
private(set) var compoundHook: CompoundHookProtocol = DefaultCompoundHook()
|
||||
private let _compoundHook: Mutex<CompoundHookProtocol> = Mutex(DefaultCompoundHook())
|
||||
var compoundHook: CompoundHookProtocol {
|
||||
_compoundHook.withLock { $0 }
|
||||
}
|
||||
|
||||
func registerCompoundHook(_ hook: CompoundHookProtocol) {
|
||||
compoundHook = hook
|
||||
_compoundHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
private let _bugReportHook: Mutex<BugReportHookProtocol> = Mutex(DefaultBugReportHook())
|
||||
var bugReportHook: BugReportHookProtocol {
|
||||
_bugReportHook.withLock { $0 }
|
||||
}
|
||||
|
||||
private(set) var bugReportHook: BugReportHookProtocol = DefaultBugReportHook()
|
||||
func registerBugReportHook(_ hook: BugReportHookProtocol) {
|
||||
bugReportHook = hook
|
||||
_bugReportHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
private let _certificateValidatorHook: Mutex<CertificateValidatorHookProtocol> = Mutex(DefaultCertificateValidator())
|
||||
var certificateValidatorHook: CertificateValidatorHookProtocol {
|
||||
_certificateValidatorHook.withLock { $0 }
|
||||
}
|
||||
|
||||
private(set) var certificateValidatorHook: CertificateValidatorHookProtocol = DefaultCertificateValidator()
|
||||
func registerCertificateValidatorHook(_ hook: CertificateValidatorHookProtocol) {
|
||||
certificateValidatorHook = hook
|
||||
_certificateValidatorHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
private let _oAuthPresenterHook: Mutex<OAuthPresenterHookProtocol> = Mutex(DefaultOAuthPresenterHook())
|
||||
var oAuthPresenterHook: OAuthPresenterHookProtocol {
|
||||
_oAuthPresenterHook.withLock { $0 }
|
||||
}
|
||||
|
||||
private(set) var oAuthPresenterHook: OAuthPresenterHookProtocol = DefaultOAuthPresenterHook()
|
||||
func registerOAuthPresenterHook(_ hook: OAuthPresenterHookProtocol) {
|
||||
oAuthPresenterHook = hook
|
||||
_oAuthPresenterHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
private let _roomScreenHook: Mutex<RoomScreenHookProtocol> = Mutex(DefaultRoomScreenHook())
|
||||
var roomScreenHook: RoomScreenHookProtocol {
|
||||
_roomScreenHook.withLock { $0 }
|
||||
}
|
||||
|
||||
private(set) var roomScreenHook: RoomScreenHookProtocol = DefaultRoomScreenHook()
|
||||
func registerRoomScreenHook(_ hook: RoomScreenHookProtocol) {
|
||||
roomScreenHook = hook
|
||||
_roomScreenHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
private let _developerOptionsScreenHook: Mutex<DeveloperOptionsScreenHookProtocol> = Mutex(DefaultDeveloperOptionsScreenHook())
|
||||
var developerOptionsScreenHook: DeveloperOptionsScreenHookProtocol {
|
||||
_developerOptionsScreenHook.withLock { $0 }
|
||||
}
|
||||
|
||||
private(set) var developerOptionsScreenHook: DeveloperOptionsScreenHookProtocol = DefaultDeveloperOptionsScreenHook()
|
||||
func registerDeveloperOptionsScreenHook(_ hook: DeveloperOptionsScreenHookProtocol) {
|
||||
developerOptionsScreenHook = hook
|
||||
_developerOptionsScreenHook.withLock { $0 = hook }
|
||||
}
|
||||
#endif
|
||||
|
||||
private(set) var tracingHook: TracingHookProtocol = DefaultTracingHook()
|
||||
private let _tracingHook: Mutex<TracingHookProtocol> = Mutex(DefaultTracingHook())
|
||||
var tracingHook: TracingHookProtocol {
|
||||
_tracingHook.withLock { $0 }
|
||||
}
|
||||
|
||||
func registerTracingHook(_ hook: TracingHookProtocol) {
|
||||
tracingHook = hook
|
||||
_tracingHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
private let _clientBuilderHook: Mutex<ClientBuilderHookProtocol> = Mutex(DefaultClientBuilderHook())
|
||||
var clientBuilderHook: ClientBuilderHookProtocol {
|
||||
_clientBuilderHook.withLock { $0 }
|
||||
}
|
||||
|
||||
private(set) var clientBuilderHook: ClientBuilderHookProtocol = DefaultClientBuilderHook()
|
||||
func registerClientBuilderHook(_ hook: ClientBuilderHookProtocol) {
|
||||
clientBuilderHook = hook
|
||||
_clientBuilderHook.withLock { $0 = hook }
|
||||
}
|
||||
|
||||
private let _remoteSettingsHook: Mutex<RemoteSettingsHookProtocol> = Mutex(DefaultRemoteSettingsHook())
|
||||
var remoteSettingsHook: RemoteSettingsHookProtocol {
|
||||
_remoteSettingsHook.withLock { $0 }
|
||||
}
|
||||
|
||||
private(set) var remoteSettingsHook: RemoteSettingsHookProtocol = DefaultRemoteSettingsHook()
|
||||
func registerRemoteSettingsHook(_ hook: RemoteSettingsHookProtocol) {
|
||||
remoteSettingsHook = hook
|
||||
_remoteSettingsHook.withLock { $0 = hook }
|
||||
}
|
||||
}
|
||||
|
||||
protocol AppHooksProtocol {
|
||||
protocol AppHooksProtocol: Sendable {
|
||||
func setUp()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
protocol ClientBuilderHookProtocol {
|
||||
protocol ClientBuilderHookProtocol: Sendable {
|
||||
func configure(_ builder: ClientBuilder) -> ClientBuilder
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ enum RemoteSettingsError: Error {
|
||||
case elementProRequired(serverName: String)
|
||||
}
|
||||
|
||||
protocol RemoteSettingsHookProtocol {
|
||||
protocol RemoteSettingsHookProtocol: Sendable {
|
||||
#if IS_MAIN_APP
|
||||
@MainActor func initializeCache(using client: ClientProtocol, applyingTo appSettings: CommonSettingsProtocol) async -> Result<Void, RemoteSettingsError>
|
||||
func updateCache(using client: ClientProtocol) async
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import MatrixRustSDK
|
||||
|
||||
protocol TracingHookProtocol {
|
||||
protocol TracingHookProtocol: Sendable {
|
||||
func update(_ configuration: TracingConfiguration, with rageshakeURL: RemotePreference<RageshakeConfiguration>)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Common settings between app and NSE
|
||||
protocol CommonSettingsProtocol: AnyObject {
|
||||
protocol CommonSettingsProtocol: AnyObject, Sendable {
|
||||
var lastNotificationBootTime: TimeInterval? { get set }
|
||||
var selectedNotificationTone: NotificationTone? { get set }
|
||||
|
||||
@@ -34,7 +34,9 @@ enum AppBuildType {
|
||||
}
|
||||
|
||||
/// Store Element specific app settings.
|
||||
final class AppSettings {
|
||||
///
|
||||
/// State is persisted in `UserDefaults`, which is thread-safe per Apple's documentation, hence `@unchecked`.
|
||||
final class AppSettings: @unchecked Sendable {
|
||||
private enum UserDefaultsKeys: String {
|
||||
case lastVersionLaunched
|
||||
case seenInvites
|
||||
@@ -92,10 +94,10 @@ final class AppSettings {
|
||||
case developerOptionsEnabled
|
||||
}
|
||||
|
||||
private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||
private nonisolated(unsafe) static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
|
||||
|
||||
/// UserDefaults to be used on reads and writes.
|
||||
private static var store: UserDefaults! = UserDefaults(suiteName: suiteName)
|
||||
/// UserDefaults to be used on reads and writes. `UserDefaults` is thread-safe per Apple's documentation.
|
||||
private nonisolated(unsafe) static var store: UserDefaults! = UserDefaults(suiteName: suiteName)
|
||||
|
||||
static var appBuildType: AppBuildType {
|
||||
#if DEBUG
|
||||
|
||||
@@ -10,7 +10,9 @@ import Combine
|
||||
|
||||
/// A wrapper of CurrentValueSubject.
|
||||
/// The purpose of this type is to remove the possibility to send new values on the underlying subject.
|
||||
struct CurrentValuePublisher<Output, Failure: Error>: Publisher {
|
||||
///
|
||||
/// `CurrentValueSubject` is documented as thread-safe but is not formally `Sendable`, hence `@unchecked`.
|
||||
struct CurrentValuePublisher<Output, Failure: Error>: Publisher, @unchecked Sendable {
|
||||
private let subject: CurrentValueSubject<Output, Failure>
|
||||
|
||||
init(_ subject: CurrentValueSubject<Output, Failure>) {
|
||||
|
||||
@@ -15,9 +15,9 @@ enum ExpiringTaskRunnerError: Error {
|
||||
actor ExpiringTaskRunner<T: Sendable> {
|
||||
private var continuation: CheckedContinuation<T, Error>?
|
||||
|
||||
private var task: () async throws -> T
|
||||
|
||||
init(_ task: @escaping () async throws -> T) {
|
||||
private var task: @Sendable () async throws -> T
|
||||
|
||||
init(_ task: @escaping @Sendable () async throws -> T) {
|
||||
self.task = task
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Synchronization
|
||||
|
||||
public extension Bundle {
|
||||
/// The top-level bundle that contains the entire app.
|
||||
@@ -24,12 +25,12 @@ public extension Bundle {
|
||||
|
||||
// MARK: - Localisation
|
||||
|
||||
/// Overrides `Bundle.app.preferredLocalizations` for testing translations.
|
||||
static var overrideLocalizations: [String]?
|
||||
|
||||
private static let cacheDispatchQueue = DispatchQueue(label: "io.element.elementx.localization_bundle_cache")
|
||||
private static var cachedBundles = [String: 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.
|
||||
@@ -37,32 +38,25 @@ 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) {
|
||||
cacheDispatchQueue.sync {
|
||||
cachedBundles[key] = value
|
||||
}
|
||||
cachedBundles.withLock { $0[key] = value }
|
||||
}
|
||||
|
||||
|
||||
private static func cachedValue(forKey key: String) -> Bundle? {
|
||||
var result: Bundle?
|
||||
cacheDispatchQueue.sync {
|
||||
result = cachedBundles[key]
|
||||
}
|
||||
|
||||
return result
|
||||
cachedBundles.withLock { $0[key] }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import Compound
|
||||
import LRUCache
|
||||
import MatrixRustSDK
|
||||
import SwiftSoup
|
||||
import Synchronization
|
||||
import UIKit
|
||||
|
||||
protocol MentionBuilderProtocol {
|
||||
@@ -42,11 +43,10 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
private let mentionBuilder: MentionBuilderProtocol
|
||||
|
||||
private static let attributeMSC4286 = "msc4286-external-payment-details"
|
||||
private static let cacheDispatchQueue = DispatchQueue(label: "io.element.elementx.attributed_string_builder_v2_cache")
|
||||
private static var caches: [String: LRUCache<String, AttributedString>] = [:]
|
||||
private static let caches = Mutex<[String: LRUCache<String, AttributedString>]>([:])
|
||||
|
||||
static func invalidateCaches() {
|
||||
caches.removeAll()
|
||||
caches.withLock { $0.removeAll() }
|
||||
}
|
||||
|
||||
init(cacheKey: String = defaultKey, mentionBuilder: MentionBuilderProtocol) {
|
||||
@@ -279,22 +279,17 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
||||
}
|
||||
|
||||
private static func cacheValue(_ value: AttributedString?, forKey key: String, cacheKey: String) {
|
||||
cacheDispatchQueue.sync {
|
||||
caches.withLock { caches in
|
||||
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? {
|
||||
var result: AttributedString?
|
||||
cacheDispatchQueue.sync {
|
||||
result = caches[cacheKey]?.value(forKey: key)
|
||||
}
|
||||
|
||||
return result
|
||||
caches.withLock { $0[cacheKey]?.value(forKey: key) }
|
||||
}
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
|
||||
@@ -22,7 +22,7 @@ enum Tracing {
|
||||
/// Set this to temporarily override the directory from which logs will be collected.
|
||||
/// This basically only affects ``logFiles``, and doesn't inform the SDK to write
|
||||
/// the logs to a different directory, which should be done before setting this.
|
||||
static var logsDirectoryOverride: URL?
|
||||
nonisolated(unsafe) static var logsDirectoryOverride: URL?
|
||||
static var legacyLogsDirectory: URL {
|
||||
.appGroupContainerDirectory
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ import MatrixRustSDK
|
||||
///
|
||||
/// To use this you'll need to add a conformance to the required listener
|
||||
/// protocol with a specialisation for the type it listens for.
|
||||
final class SDKListener<T> {
|
||||
private let onUpdateClosure: (T) -> Void
|
||||
|
||||
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 (T) -> Void) {
|
||||
init(_ onUpdateClosure: @escaping @Sendable (T) -> Void) {
|
||||
self.onUpdateClosure = onUpdateClosure
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ enum MediaLoaderError: Error {
|
||||
}
|
||||
|
||||
// sourcery: AutoMockable
|
||||
protocol MediaLoaderProtocol {
|
||||
protocol MediaLoaderProtocol: Sendable {
|
||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
|
||||
|
||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) async throws -> Data
|
||||
|
||||
@@ -159,7 +159,7 @@ final class NotificationManager: NSObject, NotificationManagerProtocol {
|
||||
}
|
||||
|
||||
private func removeReceivedWhileOfflineNotification() {
|
||||
notificationCenter.removeDeliveredNotifications(withIdentifiers: [NotificationServiceExtension.receivedWhileOfflineNotificationID])
|
||||
notificationCenter.removeDeliveredNotifications(withIdentifiers: [NotificationServiceExtensionActor.receivedWhileOfflineNotificationID])
|
||||
}
|
||||
|
||||
private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool {
|
||||
|
||||
@@ -12,10 +12,14 @@ import MatrixRustSDK
|
||||
|
||||
class TimelineItemProvider: TimelineItemProviderProtocol {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private let serialDispatchQueue: DispatchQueue
|
||||
|
||||
|
||||
private var roomTimelineObservationToken: TaskHandle?
|
||||
|
||||
/// Bridge from the SDK's synchronous callback into Swift Concurrency. Yielding is safe from any
|
||||
/// thread; a single long-lived `for await` consumer (set up in `init`) applies the diffs on the
|
||||
/// main actor in FIFO order, guaranteeing one in-flight update at a time.
|
||||
private let diffContinuation: AsyncStream<[TimelineDiff]>.Continuation
|
||||
|
||||
private let paginationStateSubject = CurrentValueSubject<TimelinePaginationState, Never>(.initial)
|
||||
var paginationState: TimelinePaginationState {
|
||||
paginationStateSubject.value
|
||||
@@ -33,35 +37,44 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
|
||||
.combineLatest(paginationStateSubject)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
let kind: TimelineKind
|
||||
|
||||
|
||||
private let membershipChangeSubject = PassthroughSubject<Void, Never>()
|
||||
var membershipChangePublisher: AnyPublisher<Void, Never> {
|
||||
membershipChangeSubject
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
diffContinuation.finish()
|
||||
roomTimelineObservationToken?.cancel()
|
||||
}
|
||||
|
||||
init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher<TimelinePaginationState, Never>) {
|
||||
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.timeline_item_provider", qos: .utility)
|
||||
itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([])
|
||||
self.kind = kind
|
||||
|
||||
|
||||
let (diffStream, diffContinuation) = AsyncStream<[TimelineDiff]>.makeStream()
|
||||
self.diffContinuation = diffContinuation
|
||||
|
||||
paginationStatePublisher
|
||||
.sink { [weak self] in
|
||||
self?.paginationStateSubject.send($0)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
// `for await` guarantees FIFO ordering and that only one diff is being applied at a time.
|
||||
// The stream also allows to handle the sendable items in the listener.
|
||||
Task { [weak self] in
|
||||
for await diffs in diffStream {
|
||||
self?.updateItemsWithDiffs(diffs)
|
||||
}
|
||||
}
|
||||
|
||||
Task {
|
||||
roomTimelineObservationToken = await timeline.addListener(listener: SDKListener { [weak self] timelineDiffs in
|
||||
self?.serialDispatchQueue.sync {
|
||||
self?.updateItemsWithDiffs(timelineDiffs)
|
||||
}
|
||||
roomTimelineObservationToken = await timeline.addListener(listener: SDKListener { diffs in
|
||||
diffContinuation.yield(diffs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user