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