╭ Users.zig ────────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
1 const std = @import("std"); 2 const evt = @import("../../event.zig"); 3 const ui = @import("../../ui.zig"); 4 const xit = @import("xit"); 5 const hash = xit.hash; 6 const xitui = xit.xitui; 7 const wgt = xitui.widget; 8 const layout = xitui.layout; 9 const inp = xitui.input; 10 const Grid = xitui.grid.Grid; 11 const Focus = xitui.focus.Focus; 12 13 // how many users a page shows before the "load more" row appears. 14 pub const page_size = 20; 15 16 users: []const evt.User.Safe, 17 // the window start this page was built with, mirrored into the url. 18 after: usize, 19 // the `after` for the "load more" link, or null when this is the last page. 20 next_after: ?usize, 21 22 const Self = @This(); 23 24 pub fn init( 25 arena: *std.heap.ArenaAllocator, 26 haxy_moment: evt.AdminDB.HashMap(.read_only), 27 after: usize, 28 ) !Self { 29 const DB = evt.AdminDB; 30 const hash_kind = evt.admin_repo_opts.hash; 31 32 var users: std.ArrayList(evt.User.Safe) = .empty; 33 34 // the ordered user-list (oldest first); absent until the first user exists. 35 const user_list_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "user-list")) orelse 36 return .{ .users = &.{}, .after = after, .next_after = null }; 37 const user_list = try DB.ArrayList(.read_only).init(user_list_cursor); 38 const count = try user_list.count(); 39 40 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->user")) orelse 41 return .{ .users = &.{}, .after = after, .next_after = null }; 42 const event_id_to_user = try DB.HashMap(.read_only).init(event_id_to_user_cursor); 43 44 // read the window [after, after+page_size) by index — a direct O(log n) 45 // seek per entry, never scanning the whole table. ids whose user has been 46 // deleted (tombstones) are skipped. 47 const end = @min(after + page_size, count); 48 var i = after; 49 while (i < end) : (i += 1) { 50 const id_cursor = try user_list.getCursor(@intCast(i)) orelse continue; 51 var event_id: [evt.event_id_size]u8 = undefined; 52 _ = try id_cursor.readBytes(&event_id); 53 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(hash_kind, &event_id)) orelse continue; 54 const user_map = try DB.HashMap(.read_only).init(user_cursor); 55 const user_event = try evt.read(evt.User, DB, hash_kind, arena, user_map); 56 try users.append(arena.allocator(), evt.User.Safe.init(user_event)); 57 } 58 59 return .{ 60 .users = users.items, 61 .after = after, 62 .next_after = if (end < count) end else null, 63 }; 64 } 65 66 pub const View = struct { 67 list: ui.FlowBox.Scroll, 68 data: *const Self, 69 70 pub fn init(allocator: std.mem.Allocator, data: *const Self) !View { 71 var self = blk: { 72 var list = try ui.FlowBox.Scroll.init(allocator, .{ .cell_height = 1 }); 73 errdefer list.deinit(allocator); 74 75 break :blk View{ 76 .list = list, 77 .data = data, 78 }; 79 }; 80 errdefer self.deinit(allocator); 81 82 var arena = std.heap.ArenaAllocator.init(allocator); 83 defer arena.deinit(); 84 const aa = arena.allocator(); 85 86 // one row per user, plus a trailing "load more" row when more remain. 87 const extra: usize = if (data.next_after != null) 1 else 0; 88 const lines = try aa.alloc([]const u8, data.users.len + extra); 89 const links = try aa.alloc([]const u8, data.users.len + extra); 90 for (data.users, 0..) |user, i| { 91 lines[i] = try std.fmt.allocPrint(aa, "{s} ({s})", .{ user.name, user.display_name }); 92 // clicking a user opens their page; the "a:" prefix makes the web 93 // renderer emit an <a href="/user/foo"> anchor. 94 links[i] = try std.fmt.allocPrint(aa, "a:/user/{s}", .{user.name}); 95 } 96 if (data.next_after) |next_after| { 97 // the load-more row navigates to the next window (full reload on web, 98 // Nav rebuild on the TUI), exactly like the commits "load more". 99 lines[data.users.len] = "load more"; 100 links[data.users.len] = try std.fmt.allocPrint(aa, "a:/users?after={d}", .{next_after}); 101 } 102 try self.list.setItems(allocator, lines, links); 103 104 return self; 105 } 106 107 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 108 self.list.deinit(allocator); 109 } 110 111 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 112 try self.list.build(allocator, constraint, root_focus); 113 } 114 115 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 116 try self.list.input(allocator, key, root_focus); 117 } 118 119 pub fn clearGrid(self: *View) void { 120 self.list.clearGrid(); 121 } 122 123 pub fn getGrid(self: View) ?Grid { 124 return self.list.getGrid(); 125 } 126 127 pub fn getFocus(self: *View) *Focus { 128 return self.list.getFocus(); 129 } 130 131 pub fn getSelectedIndex(self: View) ?usize { 132 return self.list.getSelectedIndex(); 133 } 134 }; 135