Improve open action handling for notifications
Push notifications were not opened reliably. To improve robustness, the following changes were introduced: 1. The notification opening logic was updated to become more similar to URL handling, in a way that uses better defined interfaces and functions that provide better result guarantees, by separating complex handling logic, and the side-effects/mutations that are made after computing the open action — instead of relying on a complex logic function that produces side-effects as a result, which obfuscates the actual behavior of the function. 2. The LoadableThreadView was expanded and renamed to LoadableNostrEventView, to reflect that it can also handle non-thread nostr events, such as DMs, which is a necessity for handling push notifications. 3. A new type of Notify object, the `QueueableNotify` was introduced, to address issues where the listener/handler is not instantiated at the time the app notifies that there is a push notification to be opened. This was implemented using async streams, which simplifies the usage of this down to a simple "for-in" loop. Closes: https://github.com/damus-io/damus/issues/2825 Changelog-Fixed: Fixed issue where some push notifications would not open in the app and leave users confused Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -7,19 +7,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LocalNotificationNotify: Notify {
|
||||
typealias Payload = LossyLocalNotification
|
||||
var payload: Payload
|
||||
}
|
||||
|
||||
extension NotifyHandler {
|
||||
static var local_notification: NotifyHandler<LocalNotificationNotify> {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension Notifications {
|
||||
static func local_notification(_ payload: LossyLocalNotification) -> Notifications<LocalNotificationNotify> {
|
||||
.init(.init(payload: payload))
|
||||
}
|
||||
extension QueueableNotify<LossyLocalNotification> {
|
||||
/// A shared singleton for opening local and push user notifications
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - The queue can only hold one element. This is done because if the user hypothetically opened 10 push notifications and there was a lag, we wouldn't want the app to suddenly open 10 different things.
|
||||
static let shared = QueueableNotify(maxQueueItems: 1)
|
||||
}
|
||||
|
||||
90
damus/Notify/QueueableNotify.swift
Normal file
90
damus/Notify/QueueableNotify.swift
Normal file
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// QueueableNotify.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-14.
|
||||
//
|
||||
|
||||
/// This notifies another object about some payload,
|
||||
/// with automatic "queueing" of messages if there are no listeners.
|
||||
///
|
||||
/// When used as a singleton, this can be used to easily send notifications to be handled at the app-level.
|
||||
///
|
||||
/// This serves the same purpose as `Notify`, except this implements the queueing of messages,
|
||||
/// which means that messages can be handled even if the listener is not instantiated yet.
|
||||
///
|
||||
/// **Example:** The app delegate can send some events that need handling from `ContentView` — but some can occur before `ContentView` is even instantiated.
|
||||
///
|
||||
///
|
||||
/// ## Usage notes
|
||||
///
|
||||
/// - This code was mainly written to have one listener at a time. Have more than one listener may be possible, but this class has not been tested/optimized for that purpose.
|
||||
///
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This makes heavy use of `AsyncStream` and continuations, because that allows complexities here to be handled elegantly with a simple "for-in" loop
|
||||
/// - Without this, it would take a couple of callbacks and manual handling of queued items to achieve the same effect
|
||||
/// - Modeled as an `actor` for extra thread-safety
|
||||
actor QueueableNotify<T: Sendable> {
|
||||
/// The continuation, which allows us to publish new items to the listener
|
||||
/// If `nil`, that means there is no listeners to the stream, which is used for determining whether to queue new incoming items.
|
||||
private var continuation: AsyncStream<T>.Continuation?
|
||||
/// Holds queue items
|
||||
private var queue: [T] = []
|
||||
/// The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
|
||||
var maxQueueItems: Int
|
||||
|
||||
/// Initializes the object
|
||||
/// - Parameter maxQueueItems: The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
|
||||
init(maxQueueItems: Int) {
|
||||
self.maxQueueItems = maxQueueItems
|
||||
}
|
||||
|
||||
/// The async stream, used for listening for notifications
|
||||
///
|
||||
/// This will first stream the queued "inbox" items that the listener may have missed, and then it will do a real-time stream of new items as they come in.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```swift
|
||||
/// for await notification in queueableNotify.stream {
|
||||
/// // Do something with the notification
|
||||
/// }
|
||||
/// ```
|
||||
var stream: AsyncStream<T> {
|
||||
return AsyncStream { continuation in
|
||||
// Stream queued "inbox" items that the listener may have missed
|
||||
for item in queue {
|
||||
continuation.yield(item)
|
||||
}
|
||||
|
||||
// Clean up if the stream closes
|
||||
continuation.onTermination = { continuation in
|
||||
Task { await self.cleanup() }
|
||||
}
|
||||
|
||||
// Point to this stream, so that it can receive new updates
|
||||
self.continuation = continuation
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up after a stream is closed by the listener
|
||||
private func cleanup() {
|
||||
self.continuation = nil // This will cause new items to be queued for when another listener is attached
|
||||
}
|
||||
|
||||
/// Adds a new notification item to be handled by a listener.
|
||||
///
|
||||
/// This will automatically stream the new item to the listener, or queue the item if no one is listening
|
||||
func add(item: T) {
|
||||
while queue.count >= maxQueueItems { queue.removeFirst() } // Ensures queue stays within the desired size
|
||||
guard let continuation else {
|
||||
// No one is listening, queue it (send it to an inbox for later handling)
|
||||
queue.append(item)
|
||||
return
|
||||
}
|
||||
// Send directly to the active listener stream
|
||||
continuation.yield(item)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user