~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/knotless

git that lives in your Atmosphere account

Forked from chadtmiller.com/knotless

0
0
113 lines · 4.2 KB Raw History
import SwiftUI
import UserNotifications

/// Where a notification that was tapped leads, read from what the server put in
/// it: as the inbox's row for it does, its thread, else its repository, else who
/// did it. One that names nothing opens the inbox.
struct PushTarget: Equatable, Sendable {
    var repo: RepoRef?
    var subject: String?
    var author: String?

    init(_ info: [AnyHashable: Any]) {
        if let repo = info["repo"] as? [String: String], let did = repo["did"], let handle = repo["handle"], let name = repo["name"] {
            self.repo = RepoRef(did: did, handle: handle, name: name)
        }
        subject = info["subject"] as? String
        author = info["author"] as? String
    }

    var route: Route? {
        if let repo, let subject, kind(ofURI: subject) != nil { return .thread(repo, uri: subject) }
        if let repo { return .repo(repo) }
        if let author { return .profile(author) }
        return nil
    }
}

/// This device's standing with the push service, and a notification just tapped.
@MainActor @Observable final class Pushes {
    static let shared = Pushes()

    var opened: PushTarget?
    private var token: String?

    /// Debug builds are signed for Apple's sandbox, and those from an archive for
    /// production, each with tokens only that service knows.
    private static var environment: String {
        #if DEBUG
        "sandbox"
        #else
        "production"
        #endif
    }

    /// Asks once to send notifications, then asks Apple for this device's token,
    /// which it gives at each launch and may change.
    func start() async {
        guard SessionStore.token != nil else { return }
        let center = UNUserNotificationCenter.current()
        guard (try? await center.requestAuthorization(options: [.alert, .badge, .sound])) == true else { return }
        UIApplication.shared.registerForRemoteNotifications()
    }

    /// Apple gave a token: the server tells this device of the account's
    /// notifications from now on.
    func registered(_ token: String) async {
        self.token = token
        try? await Client.shared.send(Writes.registerPush(token: token, environment: Self.environment))
    }

    /// Signing out: this device no longer hears of the account.
    func stop() async {
        guard let token else { return }
        self.token = nil
        try? await Client.shared.send(Writes.unregisterPush(token: token))
        clearBadge()
    }

    /// Forgets this device's token, for an account the server has already forgotten.
    func forget() {
        token = nil
        clearBadge()
    }

    func clearBadge() {
        UNUserNotificationCenter.current().setBadgeCount(0)
    }
}

final class PushDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        return true
    }

    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let token = deviceToken.map { String(format: "%02x", $0) }.joined()
        Task { await Pushes.shared.registered(token) }
    }

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("no push token: \(error.localizedDescription)")
    }

    /// Shown while the app is open too.
    nonisolated func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        willPresent notification: UNNotification,
        withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        completionHandler([.banner, .list, .badge, .sound])
    }

    nonisolated func userNotificationCenter(
        _ center: UNUserNotificationCenter,
        didReceive response: UNNotificationResponse,
        withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        let target = PushTarget(response.notification.request.content.userInfo)
        Task { @MainActor in Pushes.shared.opened = target }
        completionHandler()
    }
}