johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
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()
}
}