eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1import Foundation
2
3// 🤖 Resolves a tapped notification's thread id to the space that owns it.
4// The tap route used to hang everything on one source — the SharedThreadKeys
5// entry the ThreadStore folds park — and bailed SILENTLY when it was missing
6// or its origin no longer matched a saved instance (stale park, re-added
7// space, wiped keychain): the app cold-launched to the chat list and the tap
8// just… did nothing (the 2026-07-28 report). Resolution now falls back down
9// the same ladder the data actually lives in, and the caller can always tell
10// why a tap still couldn't route.
11public enum NotificationRoute {
12 /// Where a resolution came from — the caller logs it so a stranded tap in
13 /// the field names its failing rung instead of vanishing.
14 public enum Resolution {
15 case stamped(SavedInstance) // the NSE wrote the owning origin onto the banner
16 case parkedKey(SavedInstance) // SharedThreadKeys entry matched
17 case snapshot(SavedInstance) // found in a space's thread snapshot
18 case onlySpace(SavedInstance) // one signed-in space — must be it
19 case lastActive(SavedInstance) // best guess: the space this device was last in
20 case unresolved
21 }
22
23 /// The production ladder. `origin` is the NSE's delivery-time stamp (the
24 /// banner remembers which space it came from even if the park is gone by
25 /// tap time); `lastActiveID` is the device's last-opened space, the
26 /// no-evidence fallback. `entryOrigin` and `snapshotHasThread` are seams
27 /// so the ladder's order and edge cases stay unit-testable without a
28 /// keychain (ThreadStoreTests pattern).
29 public static func resolve(threadID: String,
30 origin: String? = nil,
31 lastActiveID: String? = nil,
32 instances: [SavedInstance],
33 entryOrigin: (String) -> String? = {
34 SharedThreadKeys.lookup(threadID: $0)?.origin
35 },
36 snapshotHasThread: (String, String) -> Bool = { origin, thread in
37 ThreadListSnapshot.load(origin: origin)?
38 .rows.contains { $0.id == thread } ?? false
39 }) -> Resolution {
40 // The stamp is delivery-time truth: the NSE resolved this thread's
41 // space when it decrypted the banner, so trust it above the (evictable)
42 // park it was read from.
43 if let origin, let inst = instances.first(where: { $0.origin == origin }) {
44 return .stamped(inst)
45 }
46 if let origin = entryOrigin(threadID),
47 let inst = instances.first(where: { $0.origin == origin }) {
48 return .parkedKey(inst)
49 }
50 // Stale or missing park: each space's last folded thread list also
51 // knows its thread ids (and survives the park being wiped).
52 if let inst = instances.first(where: { snapshotHasThread($0.origin, threadID) }) {
53 return .snapshot(inst)
54 }
55 // One signed-in space — the thread can only be there. A genuinely new
56 // thread resolves inside the chat screen's own hydrate + refresh once
57 // pushed; that beats dropping the tap on the floor.
58 let active = instances.filter { !$0.isArchived }
59 if active.count == 1 { return .onlySpace(active[0]) }
60 if instances.count == 1 { return .onlySpace(instances[0]) }
61 // Multi-space with no record anywhere (an undecryptable push for a
62 // brand-new thread): the space this device was last in is the best
63 // prior — and it's where the launch lands anyway, so a right guess
64 // (the common case) opens the chat in place, and a wrong one costs a
65 // "Chat not found" with a working back. Either beats dropping the
66 // tap on the chat list.
67 if let lastActiveID, let inst = instances.first(where: { $0.id == lastActiveID }) {
68 return .lastActive(inst)
69 }
70 return .unresolved
71 }
72}
73