╭ changed ──────╮ │src/ui/User.zig│ ╰───────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────────────────────────────────────╮ │14 pub const Settings = @import("./Settings.zig"); │ │15 pub const Auth = @import("./Auth.zig"); │ │16 pub const Quit = @import("./Quit.zig"); │ │17 + │ │18 +pub const page_size = 20; // how many repos one window of the repos tab shows│ │19 │ │20 header: Header, │ │21 user: evt.User.Safe, │ ╰─────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────╮ │22 repos: []const evt.Repo, │ │23 +repos_after: usize, // the repos window this page was built with, mirrored into the url│ │24 +repos_next_after: ?usize, // the `after` for the "next" row, or null on the last window│ │25 settings: Settings, │ │26 auth: Auth, │ │27 quit: Quit, │ ╰───────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────╮ │33 arena: *std.heap.ArenaAllocator, │ │34 haxy_moment: evt.AdminDB.HashMap(.read_only), │ │35 name: ui.RoutablePage.Array(evt.User.name_max_len),│ │36 + after: usize, │ │37 ) !Self { │ │38 const DB = evt.AdminDB; │ │39 const hash_kind = evt.admin_repo_opts.hash; │ ╰───────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │50 const user = (try evt.User.readById(DB, hash_kind, haxy_moment, arena, user_id)) orelse return error.NotFound; │ │51 │ │52 var repos: std.ArrayList(evt.Repo) = .empty; │ │53 + var repos_next_after: ?usize = null; │ │54 │ │49 - // the user-id->repos index maps each user to the set of repo event ids they │ │50 - // own; it only exists once a repo has been consumed, so a user with no repos │ │51 - // simply yields an empty list. │ │52 - if (try haxy_moment.getCursor(hash.hashInt(hash_kind, "user-id->repos"))) |user_id_to_repos_cursor| { │ │53 - const user_id_to_repos = try DB.HashMap(.read_only).init(user_id_to_repos_cursor); │ │54 - if (try user_id_to_repos.getCursor(hash.hashInt(hash_kind, user_id))) |user_repos_cursor| { │ │55 - const user_repos = try DB.CountedHashSet(.read_only).init(user_repos_cursor); │ │55 + // the user-id->repo-id-set index maps each user to a set of their repo event │ │56 + // ids ordered by creation time (oldest first); it only exists once a repo has │ │57 + // been consumed, so a user with no repos simply yields an empty list. │ │58 + if (try haxy_moment.getCursor(hash.hashInt(hash_kind, "user-id->repo-id-set"))) |user_id_to_repo_id_set_cursor| { │ │59 + const user_id_to_repo_id_set = try DB.HashMap(.read_only).init(user_id_to_repo_id_set_cursor); │ │60 + if (try user_id_to_repo_id_set.getCursor(hash.hashInt(hash_kind, user_id))) |user_repos_cursor| { │ │61 + const user_repos = try DB.SortedSet(.read_only).init(user_repos_cursor); │ │62 + const count = try user_repos.count(); │ │63 │ │64 const event_id_to_repo_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->repo")) orelse return error.NotFound;│ │65 const event_id_to_repo = try DB.HashMap(.read_only).init(event_id_to_repo_cursor); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │66 │ │60 - var repos_iter = try user_repos.iterator(); │ │61 - while (try repos_iter.next()) |kv_cursor| { │ │67 + // read the window [after, after+page_size) with one seek to the start │ │68 + // rank, then a sequential walk. │ │69 + const end = @min(after + page_size, count); │ │70 + var repos_iter = try user_repos.iteratorFromIndex(after); │ │71 + var i = after; │ │72 + while (i < end) : (i += 1) { │ │73 + var kv_cursor = (try repos_iter.next()) orelse break; │ │74 const kv = try kv_cursor.readKeyValuePair(); │ │63 - const repo_cursor = try event_id_to_repo.getCursor(kv.hash) orelse continue; │ │75 + // the set key is orderKey = [timestamp][event-id]; its trailing │ │76 + // bytes are the repo event id. │ │77 + var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; │ │78 + _ = try kv.key_cursor.readBytes(&order_key); │ │79 + const event_id = order_key[@sizeOf(u64)..]; │ │80 + const repo_cursor = try event_id_to_repo.getCursor(hash.hashInt(hash_kind, event_id)) orelse continue;│ │81 const repo_map = try DB.HashMap(.read_only).init(repo_cursor); │ │82 const repo_event = try evt.read(evt.Repo, DB, hash_kind, arena, repo_map); │ │83 try repos.append(arena.allocator(), repo_event); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────╮ │84 } │ │85 + repos_next_after = if (end < count) end else null;│ │86 } │ │87 } │ │88 │ ╰──────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────╮ │90 .header = try Header.init(arena, user.name),│ │91 .user = evt.User.Safe.init(user), │ │92 .repos = repos.items, │ │93 + .repos_after = after, │ │94 + .repos_next_after = repos_next_after, │ │95 .settings = Settings.init(), │ │96 .auth = Auth.init(), │ │97 .quit = Quit.init(), │ ╰────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │134 defer arena.deinit(); │ │135 const aa = arena.allocator(); │ │136 │ │137 + // a leading "previous" row off the first window, one row per repo, │ │138 + // then a trailing "next" row when more remain. each window row │ │139 + // navigates to the adjacent window of this user's repos. │ │140 var items: std.ArrayList(ui.FlowBox.Item) = .empty; │ │141 + if (data.repos_after > 0) │ │142 + try items.append(aa, .{ .text = "← previous", .link = try std.fmt.allocPrint(aa, "a:/user/{s}?after={d}", .{ data.user.name, data.repos_after -| page_size }) });│ │143 for (data.repos) |repo| │ │144 // clicking a repo opens its page; the "a:" prefix makes the web │ │145 // renderer emit an <a href="/repo/alice/foo"> anchor. │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 .text = try std.fmt.allocPrint(aa, "{s} - {s}", .{ repo.name, repo.description }), │ │148 .link = try std.fmt.allocPrint(aa, "a:/repo/{s}/{s}", .{ data.user.name, repo.name }), │ │149 }); │ │150 + if (data.repos_next_after) |next_after| │ │151 + try items.append(aa, .{ .text = "next →", .link = try std.fmt.allocPrint(aa, "a:/user/{s}?after={d}", .{ data.user.name, next_after }) });│ │152 try list.setItems(allocator, items.items); │ │153 │ │154 try stack.children.put(allocator, list.getFocus().id, .{ .flow_box_scroll = list }); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │203 // alone (nothing to mirror into the url). │ │204 .quit => {}, │ │205 // the repos list (the default tab) │ │179 - else => self.session.data.current_page = .{ .user = name }, │ │206 + else => self.session.data.current_page = .{ .user = .{ .name = name, .after = self.data.repos_after } },│ │207 } │ │208 } │ │209 try self.box.build(allocator, constraint, root_focus); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯