mirror of
https://github.com/block/buzz.git
synced 2026-10-27 20:41:57 +00:00
fix(mobile): select the last valid push reply marker
Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
@@ -37,11 +37,15 @@ public struct BuzzPushNavigationTarget: Codable, Equatable, Sendable {
|
||||
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
|
||||
// Match relay ingest: the last valid 64-character ASCII-hex reply ID wins.
|
||||
// A later malformed marker must not hide an earlier valid parent.
|
||||
return tags.last(where: { tag in
|
||||
guard tag.count >= 4, tag[0] == "e", tag[3] == "reply" else { return false }
|
||||
let bytes = tag[1].utf8
|
||||
return bytes.count == 64 && bytes.allSatisfy {
|
||||
(48...57).contains($0) || (65...70).contains($0) || (97...102).contains($0)
|
||||
}
|
||||
})?[1]
|
||||
}
|
||||
|
||||
/// Decodes a target without trusting other fields from the APNs payload.
|
||||
|
||||
@@ -116,8 +116,24 @@ import Testing
|
||||
}
|
||||
|
||||
@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")
|
||||
let parent = String(repeating: "a", count: 64)
|
||||
let tags = [["e", "outer-root", "", "root"], ["e", parent, "", "reply"]]
|
||||
#expect(BuzzPushNavigationTarget.threadRootID(in: tags) == parent)
|
||||
#expect(BuzzPushNavigationTarget.threadRootID(in: tags + [["broadcast", "1"]]) == nil)
|
||||
#expect(BuzzPushNavigationTarget.threadRootID(in: [["e", "outer-root", "", "root"]]) == nil)
|
||||
}
|
||||
|
||||
@Test func `Last valid reply marker wins despite trailing malformed markers`() {
|
||||
let first = String(repeating: "a", count: 64)
|
||||
let last = String(repeating: "B", count: 64)
|
||||
let invalidIDs = ["", "bad", String(repeating: "a", count: 63),
|
||||
String(repeating: "a", count: 65), String(repeating: "g", count: 64),
|
||||
String(repeating: "é", count: 64)]
|
||||
for invalid in invalidIDs {
|
||||
let invalidTag = ["e", invalid, "", "reply"]
|
||||
#expect(BuzzPushNavigationTarget.threadRootID(in: [invalidTag]) == nil)
|
||||
#expect(BuzzPushNavigationTarget.threadRootID(in: [
|
||||
["e", first, "", "reply"], ["e", last, "", "reply"], invalidTag,
|
||||
]) == last)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,17 +163,20 @@ final class BuzzPushNotificationResolverTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testReplyNavigationUsesDirectParentAndKeepsExactEvent() throws {
|
||||
let parent = String(repeating: "a", count: 64)
|
||||
let reply = event(id: "reply-id", content: "Reply preview", tags: [
|
||||
["h", Self.channelID],
|
||||
["e", "outer-root", "", "root"],
|
||||
["e", "direct-parent", "", "reply"],
|
||||
["e", parent, "", "reply"],
|
||||
["e", "bad", "", "reply"],
|
||||
["e", "", "", "reply"],
|
||||
])
|
||||
let resolution = try XCTUnwrap(BuzzPushNotificationResolver.decodeResolution(
|
||||
events: [reply], community: community()
|
||||
)?.0)
|
||||
let target = try XCTUnwrap(resolution.navigationTarget)
|
||||
XCTAssertEqual(target.eventID, "reply-id")
|
||||
XCTAssertEqual(target.userInfoValue["thread_root_id"], "direct-parent")
|
||||
XCTAssertEqual(target.userInfoValue["thread_root_id"], parent)
|
||||
}
|
||||
|
||||
func testNavigationKeepsTopLevelAndBroadcastMessagesInChannel() throws {
|
||||
|
||||
Reference in New Issue
Block a user