mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-03 16:10:20 +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:
@@ -906,7 +906,6 @@
|
|||||||
94D9172C4B91FC6971C668CE /* LeaveSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE4B7AD74E402BBC46A801A /* LeaveSpaceView.swift */; };
|
94D9172C4B91FC6971C668CE /* LeaveSpaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AE4B7AD74E402BBC46A801A /* LeaveSpaceView.swift */; };
|
||||||
94E15D018D70563FA4AB4E5A /* ComposerToolbarModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D3A7375AB22721C436EB056 /* ComposerToolbarModels.swift */; };
|
94E15D018D70563FA4AB4E5A /* ComposerToolbarModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D3A7375AB22721C436EB056 /* ComposerToolbarModels.swift */; };
|
||||||
94EB81606839C175CA6C2ADB /* RoomScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97A4E73EA97CA08D2BB9806 /* RoomScreenTests.swift */; };
|
94EB81606839C175CA6C2ADB /* RoomScreenTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D97A4E73EA97CA08D2BB9806 /* RoomScreenTests.swift */; };
|
||||||
94F0B78928E952689ACDB271 /* NetworkMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D67E616BCA82D8A1258D488 /* NetworkMonitor.swift */; };
|
|
||||||
95690DDD9D547D3D842ACBE3 /* AnalyticsSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */; };
|
95690DDD9D547D3D842ACBE3 /* AnalyticsSettingsScreenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BD371B60E07A5324B9507EF /* AnalyticsSettingsScreenCoordinator.swift */; };
|
||||||
9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */; };
|
9586E90A447C4896C0CA3A8E /* TimelineItemReplyDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE89A8BD65CCE3FCC925CA14 /* TimelineItemReplyDetails.swift */; };
|
||||||
95B690EA99A3385D4B8A593D /* NotificationToneManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B94A6A57FE7E71F7605070AA /* NotificationToneManager.swift */; };
|
95B690EA99A3385D4B8A593D /* NotificationToneManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B94A6A57FE7E71F7605070AA /* NotificationToneManager.swift */; };
|
||||||
@@ -1448,7 +1447,6 @@
|
|||||||
F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.swift */; };
|
F18CA61A58C77C84F551B8E7 /* GeneratedMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 57916A1578D8043BB0795441 /* GeneratedMocks.swift */; };
|
||||||
F1C68F64FC8A66B6B9510BF7 /* ManageRoomMemberSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */; };
|
F1C68F64FC8A66B6B9510BF7 /* ManageRoomMemberSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1B30CD19ED6243FEDFBA8400 /* ManageRoomMemberSheetView.swift */; };
|
||||||
F252C0EA49088801F4CA6006 /* landscape_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */; };
|
F252C0EA49088801F4CA6006 /* landscape_test_image.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 96CE9D6642DD487D8CC90C9C /* landscape_test_image.jpg */; };
|
||||||
F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71D52BAA5BADB06E5E8C295D /* Assets.swift */; };
|
|
||||||
F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; };
|
F255083E18CDBFDF7E640FB1 /* Avatars.swift in Sources */ = {isa = PBXBuildFile; fileRef = C142248014E08E885E323E56 /* Avatars.swift */; };
|
||||||
F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; };
|
F2D5C0E1351DA7BD16867629 /* TimelineStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CD4823EAB4B4E8BAB4F6B8C /* TimelineStyle.swift */; };
|
||||||
F2E580C0FBFBEFFE9D69893B /* RoomPreviewProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739077686814E4EA339B1C83 /* RoomPreviewProxyProtocol.swift */; };
|
F2E580C0FBFBEFFE9D69893B /* RoomPreviewProxyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 739077686814E4EA339B1C83 /* RoomPreviewProxyProtocol.swift */; };
|
||||||
@@ -7796,7 +7794,6 @@
|
|||||||
68C3AF257678F6E7BB238C3F /* AppAppearance.swift in Sources */,
|
68C3AF257678F6E7BB238C3F /* AppAppearance.swift in Sources */,
|
||||||
A2172B5A26976F9174228B8A /* AppHooks.swift in Sources */,
|
A2172B5A26976F9174228B8A /* AppHooks.swift in Sources */,
|
||||||
E1DE8A5422643F759385290F /* AppSettings.swift in Sources */,
|
E1DE8A5422643F759385290F /* AppSettings.swift in Sources */,
|
||||||
F253AAB4C8F06208173C9C4A /* Assets.swift in Sources */,
|
|
||||||
484202C5D50983442D24D061 /* AttributedString.swift in Sources */,
|
484202C5D50983442D24D061 /* AttributedString.swift in Sources */,
|
||||||
CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */,
|
CDCA8A559E098503DDE29477 /* AttributedStringBuilder.swift in Sources */,
|
||||||
BA43D782BE85C7F5F20C624A /* AttributedStringBuilderProtocol.swift in Sources */,
|
BA43D782BE85C7F5F20C624A /* AttributedStringBuilderProtocol.swift in Sources */,
|
||||||
@@ -7835,7 +7832,6 @@
|
|||||||
E2DB696117BAEABAD5718023 /* MediaSourceProxy.swift in Sources */,
|
E2DB696117BAEABAD5718023 /* MediaSourceProxy.swift in Sources */,
|
||||||
4CB30D9BD72CA3FEEFAA0793 /* NSEUserSession.swift in Sources */,
|
4CB30D9BD72CA3FEEFAA0793 /* NSEUserSession.swift in Sources */,
|
||||||
1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */,
|
1D5DC685CED904386C89B7DA /* NSRegularExpresion.swift in Sources */,
|
||||||
94F0B78928E952689ACDB271 /* NetworkMonitor.swift in Sources */,
|
|
||||||
4DEEFB73181C3B023DB42686 /* NetworkMonitorProtocol.swift in Sources */,
|
4DEEFB73181C3B023DB42686 /* NetworkMonitorProtocol.swift in Sources */,
|
||||||
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */,
|
5C02841B2A86327B2C377682 /* NotificationConstants.swift in Sources */,
|
||||||
2689D22EF1D10D22B0A4DAEA /* NotificationContentBuilder.swift in Sources */,
|
2689D22EF1D10D22B0A4DAEA /* NotificationContentBuilder.swift in Sources */,
|
||||||
@@ -9545,7 +9541,9 @@
|
|||||||
PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
|
PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
|
||||||
PRODUCT_NAME = NSE;
|
PRODUCT_NAME = NSE;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "GeneratedInterface-Swift.h";
|
SWIFT_OBJC_INTERFACE_HEADER_NAME = "GeneratedInterface-Swift.h";
|
||||||
|
SWIFT_VERSION = 6.2;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -9926,7 +9924,9 @@
|
|||||||
PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
|
PRODUCT_DISPLAY_NAME = "$(APP_DISPLAY_NAME)";
|
||||||
PRODUCT_NAME = NSE;
|
PRODUCT_NAME = NSE;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
SWIFT_OBJC_INTERFACE_HEADER_NAME = "GeneratedInterface-Swift.h";
|
SWIFT_OBJC_INTERFACE_HEADER_NAME = "GeneratedInterface-Swift.h";
|
||||||
|
SWIFT_VERSION = 6.2;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
|
|||||||
@@ -7,66 +7,107 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Synchronization
|
||||||
|
|
||||||
class AppHooks: AppHooksProtocol {
|
final class AppHooks: AppHooksProtocol {
|
||||||
#if IS_MAIN_APP
|
#if IS_MAIN_APP
|
||||||
func configure(with userSession: UserSessionProtocol?) async {
|
func configure(with userSession: UserSessionProtocol?) async {
|
||||||
await roomScreenHook.configure(with: userSession)
|
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) {
|
func registerAppSettingsHook(_ hook: AppSettingsHookProtocol) {
|
||||||
appSettingsHook = hook
|
_appSettingsHook.withLock { $0 = hook }
|
||||||
|
}
|
||||||
|
|
||||||
|
private let _compoundHook: Mutex<CompoundHookProtocol> = Mutex(DefaultCompoundHook())
|
||||||
|
var compoundHook: CompoundHookProtocol {
|
||||||
|
_compoundHook.withLock { $0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
private(set) var compoundHook: CompoundHookProtocol = DefaultCompoundHook()
|
|
||||||
func registerCompoundHook(_ hook: CompoundHookProtocol) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
func registerDeveloperOptionsScreenHook(_ hook: DeveloperOptionsScreenHookProtocol) {
|
||||||
developerOptionsScreenHook = hook
|
_developerOptionsScreenHook.withLock { $0 = hook }
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private(set) var tracingHook: TracingHookProtocol = DefaultTracingHook()
|
private let _tracingHook: Mutex<TracingHookProtocol> = Mutex(DefaultTracingHook())
|
||||||
|
var tracingHook: TracingHookProtocol {
|
||||||
|
_tracingHook.withLock { $0 }
|
||||||
|
}
|
||||||
|
|
||||||
func registerTracingHook(_ hook: TracingHookProtocol) {
|
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) {
|
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) {
|
func registerRemoteSettingsHook(_ hook: RemoteSettingsHookProtocol) {
|
||||||
remoteSettingsHook = hook
|
_remoteSettingsHook.withLock { $0 = hook }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol AppHooksProtocol {
|
protocol AppHooksProtocol: Sendable {
|
||||||
func setUp()
|
func setUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
|
||||||
protocol ClientBuilderHookProtocol {
|
protocol ClientBuilderHookProtocol: Sendable {
|
||||||
func configure(_ builder: ClientBuilder) -> ClientBuilder
|
func configure(_ builder: ClientBuilder) -> ClientBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ enum RemoteSettingsError: Error {
|
|||||||
case elementProRequired(serverName: String)
|
case elementProRequired(serverName: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
protocol RemoteSettingsHookProtocol {
|
protocol RemoteSettingsHookProtocol: Sendable {
|
||||||
#if IS_MAIN_APP
|
#if IS_MAIN_APP
|
||||||
@MainActor func initializeCache(using client: ClientProtocol, applyingTo appSettings: CommonSettingsProtocol) async -> Result<Void, RemoteSettingsError>
|
@MainActor func initializeCache(using client: ClientProtocol, applyingTo appSettings: CommonSettingsProtocol) async -> Result<Void, RemoteSettingsError>
|
||||||
func updateCache(using client: ClientProtocol) async
|
func updateCache(using client: ClientProtocol) async
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
|
||||||
protocol TracingHookProtocol {
|
protocol TracingHookProtocol: Sendable {
|
||||||
func update(_ configuration: TracingConfiguration, with rageshakeURL: RemotePreference<RageshakeConfiguration>)
|
func update(_ configuration: TracingConfiguration, with rageshakeURL: RemotePreference<RageshakeConfiguration>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import Foundation
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
/// Common settings between app and NSE
|
/// Common settings between app and NSE
|
||||||
protocol CommonSettingsProtocol: AnyObject {
|
protocol CommonSettingsProtocol: AnyObject, Sendable {
|
||||||
var lastNotificationBootTime: TimeInterval? { get set }
|
var lastNotificationBootTime: TimeInterval? { get set }
|
||||||
var selectedNotificationTone: NotificationTone? { get set }
|
var selectedNotificationTone: NotificationTone? { get set }
|
||||||
|
|
||||||
@@ -34,7 +34,9 @@ enum AppBuildType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Store Element specific app settings.
|
/// 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 {
|
private enum UserDefaultsKeys: String {
|
||||||
case lastVersionLaunched
|
case lastVersionLaunched
|
||||||
case seenInvites
|
case seenInvites
|
||||||
@@ -92,10 +94,10 @@ final class AppSettings {
|
|||||||
case developerOptionsEnabled
|
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.
|
/// UserDefaults to be used on reads and writes. `UserDefaults` is thread-safe per Apple's documentation.
|
||||||
private static var store: UserDefaults! = UserDefaults(suiteName: suiteName)
|
private nonisolated(unsafe) static var store: UserDefaults! = UserDefaults(suiteName: suiteName)
|
||||||
|
|
||||||
static var appBuildType: AppBuildType {
|
static var appBuildType: AppBuildType {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import Combine
|
|||||||
|
|
||||||
/// A wrapper of CurrentValueSubject.
|
/// A wrapper of CurrentValueSubject.
|
||||||
/// The purpose of this type is to remove the possibility to send new values on the underlying subject.
|
/// 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>
|
private let subject: CurrentValueSubject<Output, Failure>
|
||||||
|
|
||||||
init(_ subject: CurrentValueSubject<Output, Failure>) {
|
init(_ subject: CurrentValueSubject<Output, Failure>) {
|
||||||
|
|||||||
@@ -15,9 +15,9 @@ enum ExpiringTaskRunnerError: Error {
|
|||||||
actor ExpiringTaskRunner<T: Sendable> {
|
actor ExpiringTaskRunner<T: Sendable> {
|
||||||
private var continuation: CheckedContinuation<T, Error>?
|
private var continuation: CheckedContinuation<T, Error>?
|
||||||
|
|
||||||
private var task: () async throws -> T
|
private var task: @Sendable () async throws -> T
|
||||||
|
|
||||||
init(_ task: @escaping () async throws -> T) {
|
init(_ task: @escaping @Sendable () async throws -> T) {
|
||||||
self.task = task
|
self.task = task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import Synchronization
|
||||||
|
|
||||||
public extension Bundle {
|
public extension Bundle {
|
||||||
/// The top-level bundle that contains the entire app.
|
/// The top-level bundle that contains the entire app.
|
||||||
@@ -24,11 +25,11 @@ public extension Bundle {
|
|||||||
|
|
||||||
// MARK: - Localisation
|
// MARK: - Localisation
|
||||||
|
|
||||||
/// Overrides `Bundle.app.preferredLocalizations` for testing translations.
|
/// Overrides `Bundle.app.preferredLocalizations` for testing translations since this is
|
||||||
static var overrideLocalizations: [String]?
|
/// 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 cacheDispatchQueue = DispatchQueue(label: "io.element.elementx.localization_bundle_cache")
|
private static let cachedBundles = Mutex<[String: Bundle]>([:])
|
||||||
private static var cachedBundles = [String: Bundle]()
|
|
||||||
|
|
||||||
/// Get an lproj language bundle from the receiver bundle.
|
/// Get an lproj language bundle from the receiver bundle.
|
||||||
/// - Parameter language: The language to try to load.
|
/// - Parameter language: The language to try to load.
|
||||||
@@ -52,17 +53,10 @@ public extension Bundle {
|
|||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private static func cacheValue(_ value: Bundle?, forKey key: String) {
|
private static func cacheValue(_ value: Bundle?, forKey key: String) {
|
||||||
cacheDispatchQueue.sync {
|
cachedBundles.withLock { $0[key] = value }
|
||||||
cachedBundles[key] = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func cachedValue(forKey key: String) -> Bundle? {
|
private static func cachedValue(forKey key: String) -> Bundle? {
|
||||||
var result: Bundle?
|
cachedBundles.withLock { $0[key] }
|
||||||
cacheDispatchQueue.sync {
|
|
||||||
result = cachedBundles[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Compound
|
|||||||
import LRUCache
|
import LRUCache
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
import SwiftSoup
|
import SwiftSoup
|
||||||
|
import Synchronization
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol MentionBuilderProtocol {
|
protocol MentionBuilderProtocol {
|
||||||
@@ -42,11 +43,10 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
private let mentionBuilder: MentionBuilderProtocol
|
private let mentionBuilder: MentionBuilderProtocol
|
||||||
|
|
||||||
private static let attributeMSC4286 = "msc4286-external-payment-details"
|
private static let attributeMSC4286 = "msc4286-external-payment-details"
|
||||||
private static let cacheDispatchQueue = DispatchQueue(label: "io.element.elementx.attributed_string_builder_v2_cache")
|
private static let caches = Mutex<[String: LRUCache<String, AttributedString>]>([:])
|
||||||
private static var caches: [String: LRUCache<String, AttributedString>] = [:]
|
|
||||||
|
|
||||||
static func invalidateCaches() {
|
static func invalidateCaches() {
|
||||||
caches.removeAll()
|
caches.withLock { $0.removeAll() }
|
||||||
}
|
}
|
||||||
|
|
||||||
init(cacheKey: String = defaultKey, mentionBuilder: MentionBuilderProtocol) {
|
init(cacheKey: String = defaultKey, mentionBuilder: MentionBuilderProtocol) {
|
||||||
@@ -279,7 +279,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func cacheValue(_ value: AttributedString?, forKey key: String, cacheKey: String) {
|
private static func cacheValue(_ value: AttributedString?, forKey key: String, cacheKey: String) {
|
||||||
cacheDispatchQueue.sync {
|
caches.withLock { caches in
|
||||||
if caches[cacheKey] == nil {
|
if caches[cacheKey] == nil {
|
||||||
caches[cacheKey] = LRUCache<String, AttributedString>(countLimit: 1000)
|
caches[cacheKey] = LRUCache<String, AttributedString>(countLimit: 1000)
|
||||||
}
|
}
|
||||||
@@ -289,12 +289,7 @@ struct AttributedStringBuilder: AttributedStringBuilderProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func cachedValue(forKey key: String, cacheKey: String) -> AttributedString? {
|
private static func cachedValue(forKey key: String, cacheKey: String) -> AttributedString? {
|
||||||
var result: AttributedString?
|
caches.withLock { $0[cacheKey]?.value(forKey: key) }
|
||||||
cacheDispatchQueue.sync {
|
|
||||||
result = caches[cacheKey]?.value(forKey: key)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next cyclomatic_complexity
|
// swiftlint:disable:next cyclomatic_complexity
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ enum Tracing {
|
|||||||
/// Set this to temporarily override the directory from which logs will be collected.
|
/// 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
|
/// 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.
|
/// 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 {
|
static var legacyLogsDirectory: URL {
|
||||||
.appGroupContainerDirectory
|
.appGroupContainerDirectory
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ import MatrixRustSDK
|
|||||||
///
|
///
|
||||||
/// To use this you'll need to add a conformance to the required listener
|
/// To use this you'll need to add a conformance to the required listener
|
||||||
/// protocol with a specialisation for the type it listens for.
|
/// protocol with a specialisation for the type it listens for.
|
||||||
final class SDKListener<T> {
|
final class SDKListener<T>: Sendable {
|
||||||
private let onUpdateClosure: (T) -> Void
|
private let onUpdateClosure: @Sendable (T) -> Void
|
||||||
|
|
||||||
/// Creates a new listener.
|
/// Creates a new listener.
|
||||||
/// - Parameter onUpdateClosure: A closure that will be called whenever a new value is available.
|
/// - 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
|
self.onUpdateClosure = onUpdateClosure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ enum MediaLoaderError: Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sourcery: AutoMockable
|
// sourcery: AutoMockable
|
||||||
protocol MediaLoaderProtocol {
|
protocol MediaLoaderProtocol: Sendable {
|
||||||
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
|
func loadMediaContentForSource(_ source: MediaSourceProxy) async throws -> Data
|
||||||
|
|
||||||
func loadMediaThumbnailForSource(_ source: MediaSourceProxy, width: UInt, height: UInt) 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() {
|
private func removeReceivedWhileOfflineNotification() {
|
||||||
notificationCenter.removeDeliveredNotifications(withIdentifiers: [NotificationServiceExtension.receivedWhileOfflineNotificationID])
|
notificationCenter.removeDeliveredNotifications(withIdentifiers: [NotificationServiceExtensionActor.receivedWhileOfflineNotificationID])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool {
|
private func setPusher(with deviceToken: Data, clientProxy: ClientProxyProtocol) async -> Bool {
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ import MatrixRustSDK
|
|||||||
|
|
||||||
class TimelineItemProvider: TimelineItemProviderProtocol {
|
class TimelineItemProvider: TimelineItemProviderProtocol {
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
private let serialDispatchQueue: DispatchQueue
|
|
||||||
|
|
||||||
private var roomTimelineObservationToken: TaskHandle?
|
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)
|
private let paginationStateSubject = CurrentValueSubject<TimelinePaginationState, Never>(.initial)
|
||||||
var paginationState: TimelinePaginationState {
|
var paginationState: TimelinePaginationState {
|
||||||
paginationStateSubject.value
|
paginationStateSubject.value
|
||||||
@@ -43,25 +47,34 @@ class TimelineItemProvider: TimelineItemProviderProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
|
diffContinuation.finish()
|
||||||
roomTimelineObservationToken?.cancel()
|
roomTimelineObservationToken?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher<TimelinePaginationState, Never>) {
|
init(timeline: Timeline, kind: TimelineKind, paginationStatePublisher: AnyPublisher<TimelinePaginationState, Never>) {
|
||||||
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.timeline_item_provider", qos: .utility)
|
|
||||||
itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([])
|
itemProxiesSubject = CurrentValueSubject<[TimelineItemProxy], Never>([])
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
|
|
||||||
|
let (diffStream, diffContinuation) = AsyncStream<[TimelineDiff]>.makeStream()
|
||||||
|
self.diffContinuation = diffContinuation
|
||||||
|
|
||||||
paginationStatePublisher
|
paginationStatePublisher
|
||||||
.sink { [weak self] in
|
.sink { [weak self] in
|
||||||
self?.paginationStateSubject.send($0)
|
self?.paginationStateSubject.send($0)
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
Task {
|
// `for await` guarantees FIFO ordering and that only one diff is being applied at a time.
|
||||||
roomTimelineObservationToken = await timeline.addListener(listener: SDKListener { [weak self] timelineDiffs in
|
// The stream also allows to handle the sendable items in the listener.
|
||||||
self?.serialDispatchQueue.sync {
|
Task { [weak self] in
|
||||||
self?.updateItemsWithDiffs(timelineDiffs)
|
for await diffs in diffStream {
|
||||||
|
self?.updateItemsWithDiffs(diffs)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
roomTimelineObservationToken = await timeline.addListener(listener: SDKListener { diffs in
|
||||||
|
diffContinuation.yield(diffs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
Submodule Enterprise updated: 553ca9a91c...5480892abb
@@ -250,7 +250,7 @@ struct NotificationContentBuilder {
|
|||||||
|
|
||||||
if let fetchedImage {
|
if let fetchedImage {
|
||||||
image = fetchedImage
|
image = fetchedImage
|
||||||
} else if let data = await getPlaceholderAvatarImageData(name: icon.groupInfo?.avatarDisplayName ?? senderAvatarDisplayName,
|
} else if let data = await Self.getPlaceholderAvatarImageData(name: icon.groupInfo?.avatarDisplayName ?? senderAvatarDisplayName,
|
||||||
id: icon.groupInfo?.id ?? senderID) {
|
id: icon.groupInfo?.id ?? senderID) {
|
||||||
image = INImage(imageData: data)
|
image = INImage(imageData: data)
|
||||||
} else {
|
} else {
|
||||||
@@ -309,7 +309,7 @@ struct NotificationContentBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func getPlaceholderAvatarImageData(name: String, id: String) async -> Data? {
|
static func getPlaceholderAvatarImageData(name: String, id: String) async -> Data? {
|
||||||
// The version value is used in case the design of the placeholder is updated to force a replacement
|
// The version value is used in case the design of the placeholder is updated to force a replacement
|
||||||
let prefix = "notification_placeholderV9"
|
let prefix = "notification_placeholderV9"
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import CallKit
|
import CallKit
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
import Synchronization
|
||||||
import UserNotifications
|
import UserNotifications
|
||||||
|
|
||||||
class NotificationHandler {
|
class NotificationHandler {
|
||||||
@@ -19,9 +20,6 @@ class NotificationHandler {
|
|||||||
|
|
||||||
private let notificationContentBuilder: NotificationContentBuilder
|
private let notificationContentBuilder: NotificationContentBuilder
|
||||||
|
|
||||||
// periphery:ignore - required for instance retention in the rust codebase
|
|
||||||
private var roomInfoObservationToken: TaskHandle?
|
|
||||||
|
|
||||||
init(userSession: NSEUserSession,
|
init(userSession: NSEUserSession,
|
||||||
settings: CommonSettingsProtocol,
|
settings: CommonSettingsProtocol,
|
||||||
contentHandler: @escaping (UNNotificationContent) -> Void,
|
contentHandler: @escaping (UNNotificationContent) -> Void,
|
||||||
@@ -184,9 +182,13 @@ class NotificationHandler {
|
|||||||
// Check to see if a call is still ongoing
|
// Check to see if a call is still ongoing
|
||||||
if let room = userSession.roomForIdentifier(roomID) { // Try to get call details from the room info
|
if let room = userSession.roomForIdentifier(roomID) { // Try to get call details from the room info
|
||||||
if !room.hasActiveRoomCall() { // If I don't have an active call wait a bit and make sure
|
if !room.hasActiveRoomCall() { // If I don't have an active call wait a bit and make sure
|
||||||
|
// Local `Mutex` holder for the SDK subscription token — keeps it alive across the
|
||||||
|
// await without needing `[weak self]`, which would force this class to be `Sendable`.
|
||||||
|
let observationToken = Mutex<TaskHandle?>(nil)
|
||||||
let expiringTask = ExpiringTaskRunner {
|
let expiringTask = ExpiringTaskRunner {
|
||||||
await withCheckedContinuation { [weak self] continuation in
|
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
||||||
self?.roomInfoObservationToken = room.subscribeToRoomInfoUpdates(listener: SDKListener { info in
|
observationToken.withLock { state in
|
||||||
|
state = room.subscribeToRoomInfoUpdates(listener: SDKListener { info in
|
||||||
if info.hasRoomCall {
|
if info.hasRoomCall {
|
||||||
MXLog.info("Received room info update and the room has an active call now.")
|
MXLog.info("Received room info update and the room has an active call now.")
|
||||||
continuation.resume()
|
continuation.resume()
|
||||||
@@ -196,8 +198,11 @@ class NotificationHandler {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try? await expiringTask.run(timeout: .seconds(5)) // Wait 5 seconds or just use whatever is available
|
try? await expiringTask.run(timeout: Duration.seconds(5)) // Wait 5 seconds or just use whatever is available
|
||||||
|
// Release the SDK subscription explicitly once we no longer need updates.
|
||||||
|
observationToken.withLock { $0 = nil }
|
||||||
|
|
||||||
guard room.hasActiveRoomCall() else {
|
guard room.hasActiveRoomCall() else {
|
||||||
MXLog.info("The room no longer has an ongoing call, handling as push notification")
|
MXLog.info("The room no longer has an ongoing call, handling as push notification")
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
|
|
||||||
import Combine
|
import Combine
|
||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
import UserNotifications
|
import Synchronization
|
||||||
|
@preconcurrency import UserNotifications
|
||||||
|
|
||||||
// The lifecycle of the NSE looks something like the following:
|
// The lifecycle of the NSE looks something like the following:
|
||||||
// 1) App receives notification
|
// 1) App receives notification
|
||||||
@@ -29,31 +30,55 @@ import UserNotifications
|
|||||||
// called on the same instance of `NotificationService` as a previous
|
// called on the same instance of `NotificationService` as a previous
|
||||||
// notification.
|
// notification.
|
||||||
|
|
||||||
class NotificationServiceExtension: UNNotificationServiceExtension {
|
final class NotificationServiceExtension: UNNotificationServiceExtension {
|
||||||
|
let actor: NotificationServiceExtensionActor
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
actor = NotificationServiceExtensionActor()
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This wouldn't be allowed normally because we added `@Sendable` to the contentHandler which is not by default, however there's nothing we can do about it
|
||||||
|
/// since the `UNNotificationServiceExtension` does not support swift 6 concurency yet, so we need to use the `@preconcurrency import` to suppress
|
||||||
|
/// this error, and then we handle all the concurrency logic inside the actor that the NSE delegates all the logic to.
|
||||||
|
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping @Sendable (UNNotificationContent) -> Void) {
|
||||||
|
let actor = actor
|
||||||
|
Task { await actor.handle(request, withContentHandler: contentHandler) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override func serviceExtensionTimeWillExpire() {
|
||||||
|
actor.serviceExtensionTimeWillExpire()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actor NotificationServiceExtensionActor {
|
||||||
static let receivedWhileOfflineNotificationID = "io.element.elementx.receivedWhileOfflineNotification"
|
static let receivedWhileOfflineNotificationID = "io.element.elementx.receivedWhileOfflineNotification"
|
||||||
|
|
||||||
private static var targetConfiguration: Target.ConfigurationResult?
|
/// Process-wide target configuration, `Mutex`-protected so it can be read/written from any
|
||||||
|
/// thread (in particular, from the actor's synchronous `init` without spawning a Task).
|
||||||
|
private static let targetConfiguration = Mutex<Target.ConfigurationResult?>(nil)
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private static var hasHandledFirstNotificationSinceBoot = false
|
private static var hasHandledFirstNotificationSinceBoot = false
|
||||||
|
|
||||||
private static let firstNotificationThreshold: TimeInterval = 15 * 60
|
private static let firstNotificationThreshold: TimeInterval = 15 * 60
|
||||||
|
|
||||||
private let settings: CommonSettingsProtocol = AppSettings()
|
private let settings: CommonSettingsProtocol = AppSettings()
|
||||||
private let appHooks: AppHooks
|
private let appHooks: AppHooks
|
||||||
|
|
||||||
private var notificationHandler: NotificationHandler?
|
/// This is nonisolated just because the expire function needs to be served ASAP so we should not call it from a Task, however this is
|
||||||
|
/// always written and read in the actor isolated context, and only read nonisolated in `serviceExtensionTimeWillExpire` function
|
||||||
|
/// which is an acceptable compromise since it's a failure state, and the race condition will not make it worse and present a partial notification regardless.
|
||||||
|
private nonisolated(unsafe) var notificationHandler: NotificationHandler?
|
||||||
private let keychainController = KeychainController(service: .sessions,
|
private let keychainController = KeychainController(service: .sessions,
|
||||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||||
|
|
||||||
private var cancellables: Set<AnyCancellable> = []
|
|
||||||
|
|
||||||
/// We can make the whole NSE a MainActor after https://github.com/swiftlang/swift-evolution/blob/main/proposals/0371-isolated-synchronous-deinit.md
|
|
||||||
/// otherwise we wouldn't be able to log the tag in the deinit.
|
|
||||||
deinit {
|
deinit {
|
||||||
ExtensionLogger.logMemory(with: tag)
|
ExtensionLogger.logMemory(with: tag)
|
||||||
MXLog.info("\(tag) deinit")
|
MXLog.info("\(tag) deinit")
|
||||||
}
|
}
|
||||||
|
|
||||||
override init() {
|
init() {
|
||||||
appHooks = AppHooks()
|
appHooks = AppHooks()
|
||||||
appHooks.setUp()
|
appHooks.setUp()
|
||||||
|
|
||||||
@@ -61,42 +86,40 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
// the target configuration will fail. We could call exit(0) here, however with the
|
// the target configuration will fail. We could call exit(0) here, however with the
|
||||||
// notification filtering entitlement that results in the notification being discarded
|
// notification filtering entitlement that results in the notification being discarded
|
||||||
// so we need to wait for the delegate method to be called and bail out there instead.
|
// so we need to wait for the delegate method to be called and bail out there instead.
|
||||||
if !BootDetectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory),
|
// The `Mutex` guarantees `Target.nse.configure(...)` runs at most once per process even
|
||||||
Self.targetConfiguration == nil {
|
// under concurrent NSE instance creation.
|
||||||
Self.targetConfiguration = Target.nse.configure(logLevel: settings.logLevel,
|
if !BootDetectionManager.isDeviceLockedAfterReboot(containerURL: URL.appGroupContainerDirectory) {
|
||||||
|
Self.targetConfiguration.withLock { state in
|
||||||
|
guard state == nil else { return }
|
||||||
|
state = Target.nse.configure(logLevel: settings.logLevel,
|
||||||
traceLogPacks: settings.traceLogPacks,
|
traceLogPacks: settings.traceLogPacks,
|
||||||
sentryURL: nil,
|
sentryURL: nil,
|
||||||
rageshakeURL: settings.bugReportRageshakeURL,
|
rageshakeURL: settings.bugReportRageshakeURL,
|
||||||
appHooks: appHooks)
|
appHooks: appHooks)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
super.init()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
func handle(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) async {
|
||||||
Task { await handle(request, withContentHandler: contentHandler) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) async {
|
|
||||||
// If we skipped configuring the target it means we can't write to the app group, so we're unlikely to
|
// If we skipped configuring the target it means we can't write to the app group, so we're unlikely to
|
||||||
// be able to create a session (and even if we could, we would be missing the lightweightTokioRuntime).
|
// be able to create a session (and even if we could, we would be missing the lightweightTokioRuntime).
|
||||||
// Additionally, APNs servers only store the most recent notification when the device is powered off.
|
// Additionally, APNs servers only store the most recent notification when the device is powered off.
|
||||||
// So lets a) skip processing the notification and b) deliver a special "offline" notification as a workaround.
|
// So lets a) skip processing the notification and b) deliver a special "offline" notification as a workaround.
|
||||||
guard Self.targetConfiguration != nil else {
|
guard Self.targetConfiguration.withLock({ $0 != nil }) else {
|
||||||
// MXLog isn't configured:
|
// MXLog isn't configured:
|
||||||
// swiftlint:disable:next print_deprecation
|
// swiftlint:disable:next print_deprecation
|
||||||
print("Device is locked after reboot.")
|
print("Device is locked after reboot.")
|
||||||
|
|
||||||
if Self.hasHandledFirstNotificationSinceBoot {
|
if await Self.hasHandledFirstNotificationSinceBoot {
|
||||||
return contentHandler(request.content)
|
return contentHandler(request.content)
|
||||||
} else {
|
} else {
|
||||||
Self.hasHandledFirstNotificationSinceBoot = true
|
await MainActor.run { Self.hasHandledFirstNotificationSinceBoot = true }
|
||||||
deliverReceivedWhileOfflineNotification(for: request)
|
deliverReceivedWhileOfflineNotification(for: request)
|
||||||
return contentHandler(.init())
|
return contentHandler(.init())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard !shouldDeliverReceivedWhileOfflineNotification() else {
|
guard await !shouldDeliverReceivedWhileOfflineNotification() else {
|
||||||
// Don't log until the app hooks have been run:
|
// Don't log until the app hooks have been run:
|
||||||
// swiftlint:disable:next print_deprecation
|
// swiftlint:disable:next print_deprecation
|
||||||
print("Device is unlocked but may have missed notifications while offline.")
|
print("Device is unlocked but may have missed notifications while offline.")
|
||||||
@@ -167,7 +190,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func serviceExtensionTimeWillExpire() {
|
nonisolated func serviceExtensionTimeWillExpire() {
|
||||||
notificationHandler?.handleTimeExpiration()
|
notificationHandler?.handleTimeExpiration()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,13 +203,13 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
///
|
///
|
||||||
/// Note that this only handles the first-boot case. When the SDK is able to compute the unread count, we should start to use the NSE,
|
/// Note that this only handles the first-boot case. When the SDK is able to compute the unread count, we should start to use the NSE,
|
||||||
/// remote-notifications (content-available) and background app refreshes to fetch and deliver our notifications as a more robust solution.
|
/// remote-notifications (content-available) and background app refreshes to fetch and deliver our notifications as a more robust solution.
|
||||||
private func shouldDeliverReceivedWhileOfflineNotification() -> Bool {
|
private func shouldDeliverReceivedWhileOfflineNotification() async -> Bool {
|
||||||
if Self.hasHandledFirstNotificationSinceBoot {
|
if await Self.hasHandledFirstNotificationSinceBoot {
|
||||||
// If we've already handled the first notification in this process there's no need to continue.
|
// If we've already handled the first notification in this process there's no need to continue.
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
Self.hasHandledFirstNotificationSinceBoot = true
|
await MainActor.run { Self.hasHandledFirstNotificationSinceBoot = true }
|
||||||
|
|
||||||
guard let currentBootTime = BootDetectionManager.systemBootTime() else {
|
guard let currentBootTime = BootDetectionManager.systemBootTime() else {
|
||||||
// There's not much we can do if the boot time is unknown, so don't show the offline notification.
|
// There's not much we can do if the boot time is unknown, so don't show the offline notification.
|
||||||
@@ -239,7 +262,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
|
|
||||||
// MARK: - Logging
|
// MARK: - Logging
|
||||||
|
|
||||||
private var tag: String {
|
private nonisolated var tag: String {
|
||||||
"[NSE][\(Unmanaged.passUnretained(self).toOpaque())][\(Unmanaged.passUnretained(Thread.current).toOpaque())][\(ProcessInfo.processInfo.processIdentifier)]"
|
"[NSE][\(Unmanaged.passUnretained(self).toOpaque())][\(Unmanaged.passUnretained(Thread.current).toOpaque())][\(ProcessInfo.processInfo.processIdentifier)]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ targets:
|
|||||||
CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION)
|
CURRENT_PROJECT_VERSION: $(CURRENT_PROJECT_VERSION)
|
||||||
DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM)
|
DEVELOPMENT_TEAM: $(DEVELOPMENT_TEAM)
|
||||||
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
|
SWIFT_OBJC_INTERFACE_HEADER_NAME: GeneratedInterface-Swift.h
|
||||||
|
SWIFT_VERSION: 6.2
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY: YES
|
||||||
OTHER_SWIFT_FLAGS:
|
OTHER_SWIFT_FLAGS:
|
||||||
- "-DIS_NSE"
|
- "-DIS_NSE"
|
||||||
|
|
||||||
@@ -87,7 +89,6 @@ targets:
|
|||||||
- path: ../../Secrets/Secrets.swift
|
- path: ../../Secrets/Secrets.swift
|
||||||
- path: ../../ElementX/Sources/Application/Settings
|
- path: ../../ElementX/Sources/Application/Settings
|
||||||
- path: ../../ElementX/Sources/Application/TargetConfiguration.swift
|
- path: ../../ElementX/Sources/Application/TargetConfiguration.swift
|
||||||
- path: ../../ElementX/Sources/Generated/Assets.swift
|
|
||||||
- path: ../../ElementX/Sources/Generated/Strings.swift
|
- path: ../../ElementX/Sources/Generated/Strings.swift
|
||||||
- path: ../../ElementX/Sources/Other/Avatars.swift
|
- path: ../../ElementX/Sources/Other/Avatars.swift
|
||||||
- path: ../../ElementX/Sources/Other/CurrentValuePublisher.swift
|
- path: ../../ElementX/Sources/Other/CurrentValuePublisher.swift
|
||||||
@@ -111,7 +112,7 @@ targets:
|
|||||||
- path: ../../ElementX/Sources/Other/InfoPlistReader.swift
|
- path: ../../ElementX/Sources/Other/InfoPlistReader.swift
|
||||||
- path: ../../ElementX/Sources/Other/Logging
|
- path: ../../ElementX/Sources/Other/Logging
|
||||||
- path: ../../ElementX/Sources/Other/MatrixEntityRegex.swift
|
- path: ../../ElementX/Sources/Other/MatrixEntityRegex.swift
|
||||||
- path: ../../ElementX/Sources/Other/NetworkMonitor
|
- path: ../../ElementX/Sources/Other/NetworkMonitor/NetworkMonitorProtocol.swift
|
||||||
- path: ../../ElementX/Sources/Other/Pills/PillUtilities.swift
|
- path: ../../ElementX/Sources/Other/Pills/PillUtilities.swift
|
||||||
- path: ../../ElementX/Sources/Other/Pills/PlainMentionBuilder.swift
|
- path: ../../ElementX/Sources/Other/Pills/PlainMentionBuilder.swift
|
||||||
- path: ../../ElementX/Sources/Other/SDKListener.swift
|
- path: ../../ElementX/Sources/Other/SDKListener.swift
|
||||||
|
|||||||
Reference in New Issue
Block a user