mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 12:28:29 +00:00
Refactor the NSE to create a new UserSession every time a payload comes in and pass the room down to the SDK so it can optimize how many rooms it loads from the store and how much memory it's using.
- this will undo creating at most one user session pe process as that shoulnd't be a problem anymore - it also renames some methods and improves the logging
This commit is contained in:
@@ -28,25 +28,24 @@ import UserNotifications
|
|||||||
// but it will always be called on different threads. It may or may not be
|
// but it will always be called on different threads. It may or may not be
|
||||||
// called on the same instance of `NotificationService` as a previous
|
// called on the same instance of `NotificationService` as a previous
|
||||||
// notification.
|
// notification.
|
||||||
//
|
|
||||||
// We keep a global `environment` singleton to ensure that our app context,
|
|
||||||
// database, logging, etc. are only ever setup once per *process*
|
|
||||||
|
|
||||||
private let settings: CommonSettingsProtocol = AppSettings()
|
private let settings: CommonSettingsProtocol = AppSettings()
|
||||||
private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), destination: .notification),
|
|
||||||
settings: settings)
|
|
||||||
private let keychainController = KeychainController(service: .sessions,
|
private let keychainController = KeychainController(service: .sessions,
|
||||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||||
|
|
||||||
|
private let eventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()),
|
||||||
|
destination: .notification)
|
||||||
|
|
||||||
|
private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: eventStringBuilder,
|
||||||
|
settings: settings)
|
||||||
|
|
||||||
class NotificationServiceExtension: UNNotificationServiceExtension {
|
class NotificationServiceExtension: UNNotificationServiceExtension {
|
||||||
private var handler: ((UNNotificationContent) -> Void)?
|
private var handler: ((UNNotificationContent) -> Void)?
|
||||||
private var modifiedContent: UNMutableNotificationContent?
|
private var modifiedContent: UNMutableNotificationContent?
|
||||||
|
|
||||||
private let appHooks = AppHooks()
|
private let appHooks = AppHooks()
|
||||||
|
|
||||||
// Used to create one single UserSession across process/instances/runs
|
|
||||||
private static let serialQueue = DispatchQueue(label: "io.element.elementx.nse")
|
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
cleanUp()
|
cleanUp()
|
||||||
ExtensionLogger.logMemory(with: tag)
|
ExtensionLogger.logMemory(with: tag)
|
||||||
@@ -74,63 +73,49 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent
|
modifiedContent = request.content.mutableCopy() as? UNMutableNotificationContent
|
||||||
|
|
||||||
MXLog.info("\(tag) #########################################")
|
MXLog.info("\(tag) #########################################")
|
||||||
|
|
||||||
ExtensionLogger.logMemory(with: tag)
|
ExtensionLogger.logMemory(with: tag)
|
||||||
MXLog.info("\(tag) Payload came: \(request.content.userInfo)")
|
MXLog.info("\(tag) Received payload: \(request.content.userInfo)")
|
||||||
|
|
||||||
Self.serialQueue.sync {
|
|
||||||
// If the session directories have changed, the user has logged out and back in (even if they entered the same user ID).
|
|
||||||
// We can't do this comparison with the access token of the existing session here due to token refresh when using OIDC.
|
|
||||||
if Self.userSession == nil || Self.userSession?.sessionDirectories != credentials.restorationToken.sessionDirectories {
|
|
||||||
// This function might be run concurrently and from different processes
|
|
||||||
// It's imperative that we create **at most** one UserSession/Client per process
|
|
||||||
Task.synchronous { [appHooks] in
|
|
||||||
do {
|
|
||||||
Self.userSession = try await NSEUserSession(credentials: credentials,
|
|
||||||
clientSessionDelegate: keychainController,
|
|
||||||
appHooks: appHooks,
|
|
||||||
appSettings: settings)
|
|
||||||
} catch {
|
|
||||||
MXLog.error("Failed creating user session with error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if Self.userSession == nil {
|
|
||||||
return discard(unreadCount: request.unreadCount)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
await run(with: credentials,
|
do {
|
||||||
roomID: roomID,
|
let userSession = try await NSEUserSession(credentials: credentials,
|
||||||
eventID: eventID,
|
roomID: roomID,
|
||||||
unreadCount: request.unreadCount)
|
clientSessionDelegate: keychainController,
|
||||||
|
appHooks: appHooks,
|
||||||
|
appSettings: settings)
|
||||||
|
|
||||||
|
ExtensionLogger.logMemory(with: tag)
|
||||||
|
MXLog.info("\(tag) Configured user session")
|
||||||
|
|
||||||
|
await processEvent(eventID,
|
||||||
|
roomID: roomID,
|
||||||
|
unreadCount: request.unreadCount,
|
||||||
|
userSession: userSession)
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed creating user session with error: \(error)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func serviceExtensionTimeWillExpire() {
|
override func serviceExtensionTimeWillExpire() {
|
||||||
// Called just before the extension will be terminated by the system.
|
// Called just before the extension will be terminated by the system.
|
||||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||||
MXLog.warning("\(tag) serviceExtensionTimeWillExpire")
|
MXLog.warning("\(tag) Extension time will expire")
|
||||||
notify(unreadCount: nil)
|
notify(unreadCount: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func run(with credentials: KeychainCredentials,
|
private func processEvent(_ eventID: String,
|
||||||
roomID: String,
|
roomID: String,
|
||||||
eventID: String,
|
unreadCount: Int?,
|
||||||
unreadCount: Int?) async {
|
userSession: NSEUserSession) async {
|
||||||
MXLog.info("\(tag) run with roomId: \(roomID), eventId: \(eventID)")
|
MXLog.info("\(tag) Processing event: \(eventID) in room: \(roomID)")
|
||||||
|
|
||||||
guard let userSession = Self.userSession else {
|
|
||||||
MXLog.error("Invalid NSE User Session, discarding.")
|
|
||||||
return discard(unreadCount: unreadCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
guard let itemProxy = await userSession.notificationItemProxy(roomID: roomID, eventID: eventID) else {
|
guard let itemProxy = await userSession.notificationItemProxy(roomID: roomID, eventID: eventID) else {
|
||||||
MXLog.info("\(tag) no notification for the event, discard")
|
MXLog.error("\(tag) Failed retrieving notification item")
|
||||||
return discard(unreadCount: unreadCount)
|
return discard(unreadCount: unreadCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,40 +126,35 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// After the first processing, update the modified content
|
|
||||||
modifiedContent = try await notificationContentBuilder.content(for: itemProxy, mediaProvider: nil)
|
modifiedContent = try await notificationContentBuilder.content(for: itemProxy, mediaProvider: nil)
|
||||||
|
|
||||||
guard itemProxy.hasMedia else {
|
guard itemProxy.hasMedia else {
|
||||||
MXLog.info("\(tag) no media needed")
|
MXLog.info("\(tag) Notification item doesn't contain media")
|
||||||
|
|
||||||
// We've processed the item and no media operations needed, so no need to go further
|
|
||||||
return notify(unreadCount: unreadCount)
|
return notify(unreadCount: unreadCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
MXLog.info("\(tag) process with media")
|
MXLog.info("\(tag) Processing media")
|
||||||
|
|
||||||
// There is some media to load, process it again
|
|
||||||
if let latestContent = try? await notificationContentBuilder.content(for: itemProxy, mediaProvider: userSession.mediaProvider) {
|
if let latestContent = try? await notificationContentBuilder.content(for: itemProxy, mediaProvider: userSession.mediaProvider) {
|
||||||
// Processing finished, hopefully with some media
|
|
||||||
modifiedContent = latestContent
|
modifiedContent = latestContent
|
||||||
|
} else {
|
||||||
|
MXLog.error("\(tag) Failed processing notification media")
|
||||||
}
|
}
|
||||||
// We still notify, but without the media attachment if it fails to load
|
|
||||||
|
|
||||||
return notify(unreadCount: unreadCount)
|
return notify(unreadCount: unreadCount)
|
||||||
} catch {
|
} catch {
|
||||||
MXLog.error("NSE run error: \(error)")
|
MXLog.error("Failed processing with error: \(error)")
|
||||||
return discard(unreadCount: unreadCount)
|
return discard(unreadCount: unreadCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func notify(unreadCount: Int?) {
|
private func notify(unreadCount: Int?) {
|
||||||
MXLog.info("\(tag) notify")
|
|
||||||
|
|
||||||
guard let modifiedContent else {
|
guard let modifiedContent else {
|
||||||
MXLog.info("\(tag) notify: no modified content")
|
MXLog.error("\(tag) Notification modified content invalid")
|
||||||
return discard(unreadCount: unreadCount)
|
return discard(unreadCount: unreadCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MXLog.info("\(tag) Displaying notification")
|
||||||
|
|
||||||
if let unreadCount {
|
if let unreadCount {
|
||||||
modifiedContent.badge = NSNumber(value: unreadCount)
|
modifiedContent.badge = NSNumber(value: unreadCount)
|
||||||
}
|
}
|
||||||
@@ -184,7 +164,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func discard(unreadCount: Int?) {
|
private func discard(unreadCount: Int?) {
|
||||||
MXLog.info("\(tag) discard")
|
MXLog.info("\(tag) Discarding notification")
|
||||||
|
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
@@ -226,7 +206,7 @@ class NotificationServiceExtension: UNNotificationServiceExtension {
|
|||||||
}
|
}
|
||||||
case .roomRedaction(let redactedEventID, _):
|
case .roomRedaction(let redactedEventID, _):
|
||||||
guard let redactedEventID else {
|
guard let redactedEventID else {
|
||||||
MXLog.error("Unable to handle redact notification due to missing event ID.")
|
MXLog.error("Unable to handle redact notification due to missing event ID")
|
||||||
return .processedShouldDiscard
|
return .processedShouldDiscard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,11 @@ final class NSEUserSession {
|
|||||||
networkMonitor: nil)
|
networkMonitor: nil)
|
||||||
private let delegateHandle: TaskHandle?
|
private let delegateHandle: TaskHandle?
|
||||||
|
|
||||||
init(credentials: KeychainCredentials, clientSessionDelegate: ClientSessionDelegate, appHooks: AppHooks, appSettings: CommonSettingsProtocol) async throws {
|
init(credentials: KeychainCredentials,
|
||||||
|
roomID: String,
|
||||||
|
clientSessionDelegate: ClientSessionDelegate,
|
||||||
|
appHooks: AppHooks,
|
||||||
|
appSettings: CommonSettingsProtocol) async throws {
|
||||||
sessionDirectories = credentials.restorationToken.sessionDirectories
|
sessionDirectories = credentials.restorationToken.sessionDirectories
|
||||||
|
|
||||||
userID = credentials.userID
|
userID = credentials.userID
|
||||||
@@ -34,8 +38,7 @@ final class NSEUserSession {
|
|||||||
slidingSync: .restored,
|
slidingSync: .restored,
|
||||||
sessionDelegate: clientSessionDelegate,
|
sessionDelegate: clientSessionDelegate,
|
||||||
appHooks: appHooks,
|
appHooks: appHooks,
|
||||||
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode,
|
enableOnlySignedDeviceIsolationMode: appSettings.enableOnlySignedDeviceIsolationMode)
|
||||||
eventCacheEnabled: appSettings.eventCacheEnabled)
|
|
||||||
.sessionPaths(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
|
.sessionPaths(dataPath: credentials.restorationToken.sessionDirectories.dataPath,
|
||||||
cachePath: credentials.restorationToken.sessionDirectories.cachePath)
|
cachePath: credentials.restorationToken.sessionDirectories.cachePath)
|
||||||
.username(username: credentials.userID)
|
.username(username: credentials.userID)
|
||||||
@@ -44,7 +47,9 @@ final class NSEUserSession {
|
|||||||
|
|
||||||
baseClient = try await clientBuilder.build()
|
baseClient = try await clientBuilder.build()
|
||||||
delegateHandle = baseClient.setDelegate(delegate: ClientDelegateWrapper())
|
delegateHandle = baseClient.setDelegate(delegate: ClientDelegateWrapper())
|
||||||
try await baseClient.restoreSession(session: credentials.restorationToken.session)
|
|
||||||
|
try await baseClient.restoreSessionWith(session: credentials.restorationToken.session,
|
||||||
|
roomLoadSettings: .one(roomId: roomID))
|
||||||
|
|
||||||
notificationClient = try await baseClient.notificationClient(processSetup: .multipleProcesses)
|
notificationClient = try await baseClient.notificationClient(processSetup: .multipleProcesses)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user