╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Issues.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────╮ │43 session: *ui.Session, │ │44 event_id: *const [evt.event_id_size]u8,│ │45 identity: []const u8, │ │46 + tag: []const u8, │ │47 selected_id: []const u8, │ │48 ) !Self { │ │49 const aa = arena.allocator(); │ ╰───────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │50 const empty: Self = .{ │ │51 .identity = try aa.dupe(u8, identity), │ │52 + .tag = try aa.dupe(u8, tag), │ │53 .selected_id = try aa.dupe(u8, selected_id),│ │54 .issues = &.{}, │ │55 .prev_id = null, │ ╰────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │76 }; │ │77 } │ │78 │ │75 -// read one window of the repo's issues, ordered by creation time (oldest │ │76 -// first), starting at the issue empty.selected_id names ("" = the beginning).│ │79 +// read one window of the repo's issues (filtered to empty.tag when set), │ │80 +// ordered by creation time (oldest first), starting at the issue │ │81 +// empty.selected_id names ("" = the beginning). │ │82 fn collect( │ │83 comptime repo_opts: rp.RepoOpts(.xit), │ │84 arena: *std.heap.ArenaAllocator, │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │87 ) !Self { │ │88 const aa = arena.allocator(); │ │89 const DB = rp.Repo(.xit, repo_opts).DB; │ │85 - // an explicitly named issue that doesn't exist is a bad url (NotFound ->│ │86 - // 404); the bare route falls through to an empty listing. │ │90 const rooted = empty.selected_id.len != 0; │ │91 + const tagged = empty.tag.len != 0; │ │92 + // an explicitly named issue or tag that doesn't exist is a bad url │ │93 + // (NotFound -> 404); the bare route falls through to an empty listing. │ │94 + const strict = rooted or tagged; │ │95 │ │96 // a repo with no consumed events has no moment yet. │ │97 const haxy_moment = evt.currentMoment(repo_opts, repo) catch { │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────╮ │ 91 - if (rooted) return error.NotFound;│ │ 98 + if (strict) return error.NotFound;│ │ 99 return empty; │ │100 }; │ │101 │ ╰───────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 95 - const issue_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "issue-id-set")) orelse { │ │ 96 - if (rooted) return error.NotFound; │ │ 97 - return empty; │ │102 + // the ordered set to window: the tag's issue set when filtered, else the │ │103 + // full issue set. │ │104 + const issue_id_set_cursor = blk: { │ │105 + if (tagged) { │ │106 + const tag_to_issues_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "tag->issue-id-set")) orelse return error.NotFound;│ │107 + const tag_to_issues = try DB.SortedMap(.read_only).init(tag_to_issues_cursor); │ │108 + const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, empty.tag)); │ │109 + break :blk (try tag_to_issues.getCursor(decoded)) orelse return error.NotFound; │ │110 + } │ │111 + break :blk (try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "issue-id-set"))) orelse { │ │112 + if (strict) return error.NotFound; │ │113 + return empty; │ │114 + }; │ │115 }; │ │116 const issue_id_set = try DB.SortedSet(.read_only).init(issue_id_set_cursor); │ │117 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │118 const event_id_to_issue_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, "event-id->issue")) orelse {│ │102 - if (rooted) return error.NotFound; │ │119 + if (strict) return error.NotFound; │ │120 return empty; │ │121 }; │ │122 const event_id_to_issue = try DB.HashMap(.read_only).init(event_id_to_issue_cursor); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │136 const issue_event = try evt.read(evt.Issue, DB, repo_opts.hash, arena, issue_map);│ │137 const order_key = evt.orderKey(issue_event.created_ts, &id_bytes); │ │138 │ │139 + // the named issue must be in the windowed set (a tag url can name an │ │140 + // issue that doesn't carry the tag). │ │141 + if (!try issue_id_set.contains(&order_key)) return error.NotFound; │ │142 + │ │143 // the previous window starts page_size ranks back. │ │144 const rank = try issue_id_set.rank(&order_key); │ │145 if (rank > 0) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────╮ │184 │ │185 return .{ │ │186 .identity = empty.identity, │ │187 + .tag = empty.tag, │ │188 .selected_id = empty.selected_id,│ │189 .issues = issues.items, │ │190 .prev_id = prev_id, │ ╰──────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │217 var list_box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); │ │218 errdefer list_box.deinit(allocator); │ │219 if (data.prev_id) |prev| │ │198 - try addRow(allocator, &list_box, "← previous", try issuesLink(session.page_arena, data.identity, prev)); │ │220 + try addRow(allocator, &list_box, "← previous", try issuesLink(session.page_arena, data.identity, data.tag, prev)); │ │221 for (data.issues) |entry| │ │222 try addRow(allocator, &list_box, entry.issue.title, try issueRowLink(session.page_arena, data.identity, entry.id));│ │223 if (data.next_id) |next| │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯