fix(ios): preserve navigation for empty reply markers

Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-13 09:41:57 -07:00
parent e062110b25
commit 42816d4272
3 changed files with 32 additions and 10 deletions

View File

@@ -31,6 +31,19 @@ public struct BuzzPushNavigationTarget: Codable, Equatable, Sendable {
return value
}
static func threadRootID(in tags: [[String]]) -> String? {
// Inbox opens broadcast replies in the channel and other replies in their
// direct parent's thread, even when the NIP-10 root names an outer thread.
if tags.contains(where: { $0.count >= 2 && $0[0] == "broadcast" && $0[1] == "1" }) {
return nil
}
let parent = tags.last(where: { $0.count >= 4 && $0[0] == "e" && $0[3] == "reply" })?[1]
// Relay ingest ignores malformed reply IDs. An empty marker must not
// invalidate the otherwise usable channel/event navigation payload.
guard let parent, !parent.isEmpty else { return nil }
return parent
}
/// Decodes a target without trusting other fields from the APNs payload.
public static func decodeIfPresent(
from userInfo: [AnyHashable: Any]

View File

@@ -463,7 +463,7 @@ public final class BuzzPushNotificationResolver: BuzzPushNotificationResolving {
eventID: event.id,
communityID: community.id,
channelID: $0,
threadRootID: navigationThreadRootID(event)
threadRootID: BuzzPushNavigationTarget.threadRootID(in: event.tags)
)
},
senderPubkey: event.pubkey.lowercased(),
@@ -474,15 +474,6 @@ public final class BuzzPushNotificationResolver: BuzzPushNotificationResolving {
)
}
private static func navigationThreadRootID(_ event: VerifiedNostrEvent) -> String? {
// Inbox opens broadcast replies in the channel and other replies in their
// direct parent's thread, even when the NIP-10 root names an outer thread.
if event.tags.contains(where: { $0.count >= 2 && $0[0] == "broadcast" && $0[1] == "1" }) {
return nil
}
return event.tags.last(where: { $0.count >= 4 && $0[0] == "e" && $0[3] == "reply" })?[1]
}
private static func ephemeralProfile(
event: VerifiedNostrEvent,
communityID: String,

View File

@@ -103,3 +103,21 @@ import Testing
) == nil)
}
}
@Test func `Empty reply marker preserves channel navigation`() {
let thread = BuzzPushNavigationTarget.threadRootID(in: [["e", "", "", "reply"]])
#expect(thread == nil)
let target = BuzzPushNavigationTarget(
eventID: "message", communityID: "community", channelID: "channel", threadRootID: thread
)
#expect(BuzzPushNavigationTarget.decodeIfPresent(
from: [BuzzPushNavigationTarget.userInfoKey: target.userInfoValue]
) == target)
}
@Test func `Thread destination keeps direct parent and broadcast semantics`() {
let tags = [["e", "outer-root", "", "root"], ["e", "direct-parent", "", "reply"]]
#expect(BuzzPushNavigationTarget.threadRootID(in: tags) == "direct-parent")
#expect(BuzzPushNavigationTarget.threadRootID(in: tags + [["broadcast", "1"]]) == nil)
#expect(BuzzPushNavigationTarget.threadRootID(in: [["e", "outer-root", "", "root"]]) == nil)
}