╭ 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 users ordered by creation time (oldest first); absent until the first 32 // user exists. keyed by [timestamp][event-id], value = event-id. 33 const created_ts_to_user_id_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "created-ts->user-id")) orelse 34 return .{ .users = &.{}, .after = after, .next_after = null }; 35 const created_ts_to_user_id = try DB.SortedMap(.read_only).init(created_ts_to_user_id_cursor); 36 const count = try created_ts_to_user_id.count(); 37 38 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->user")) orelse 39 return .{ .users = &.{}, .after = after, .next_after = null }; 40 const event_id_to_user = try DB.HashMap(.read_only).init(event_id_to_user_cursor); 41 42 // read the window [after, after+page_size) with one seek to the start rank, 43 // then a sequential walk 44 const end = @min(after + page_size, count); 45 var iter = try created_ts_to_user_id.iteratorFromIndex(after); 46 var i = after; 47 while (i < end) : (i += 1) { 48 var id_cursor = (try iter.next()) orelse break; 49 const id_kv = try id_cursor.readKeyValuePair(); 50 var event_id: [evt.event_id_size]u8 = undefined; 51 _ = try id_kv.value_cursor.readBytes(&event_id); 52 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(hash_kind, &event_id)) orelse continue; 53 const user_map = try DB.HashMap(.read_only).init(user_cursor); 54 const user_event = try evt.read(evt.User, DB, hash_kind, arena, user_map); 55 try users.append(arena.allocator(), evt.User.Safe.init(user_event)); 56 } 57 58 return .{ 59 .users = users.items, 60 .after = after, 61 .next_after = if (end < count) end else null, 62 }; 63 } 64 65 pub const View = struct { 66 list: ui.FlowBox.Scroll, 67 data: *const Self, 68 69 pub fn init(allocator: std.mem.Allocator, data: *const Self, web_native: bool) !View { 70 var self = blk: { 71 var list = try ui.FlowBox.Scroll.init(allocator, .{ .cell_height = 1 }, web_native); 72 errdefer list.deinit(allocator); 73 74 break :blk View{ 75 .list = list, 76 .data = data, 77 }; 78 }; 79 errdefer self.deinit(allocator); 80 81 var arena = std.heap.ArenaAllocator.init(allocator); 82 defer arena.deinit(); 83 const aa = arena.allocator(); 84 85 // a leading "previous" row off the first window, one row per user, then 86 // a trailing "next" row when more remain. each window row navigates to the 87 // adjacent window (full reload on web, Nav rebuild on the TUI). 88 var items: std.ArrayList(ui.FlowBox.Item) = .empty; 89 if (data.after > 0) 90 try items.append(aa, .{ .text = "← previous", .link = try std.fmt.allocPrint(aa, "a:/users?after={d}", .{data.after -| page_size}) }); 91 for (data.users) |user| 92 // clicking a user opens their page; the "a:" prefix makes the web 93 // renderer emit an <a href="/user/foo"> anchor. 94 try items.append(aa, .{ 95 .text = try std.fmt.allocPrint(aa, "{s} ({s})", .{ user.name, user.display_name }), 96 .link = try std.fmt.allocPrint(aa, "a:/user/{s}", .{user.name}), 97 }); 98 if (data.next_after) |next_after| 99 try items.append(aa, .{ .text = "next →", .link = try std.fmt.allocPrint(aa, "a:/users?after={d}", .{next_after}) }); 100 try self.list.setItems(allocator, items.items); 101 102 return self; 103 } 104 105 pub fn deinit(self: *View, allocator: std.mem.Allocator) void { 106 self.list.deinit(allocator); 107 } 108 109 pub fn build(self: *View, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 110 try self.list.build(allocator, constraint, root_focus); 111 } 112 113 pub fn input(self: *View, allocator: std.mem.Allocator, key: inp.Key, root_focus: *Focus) !void { 114 try self.list.input(allocator, key, root_focus); 115 } 116 117 pub fn clearGrid(self: *View) void { 118 self.list.clearGrid(); 119 } 120 121 pub fn getGrid(self: View) ?Grid { 122 return self.list.getGrid(); 123 } 124 125 pub fn getFocus(self: *View) *Focus { 126 return self.list.getFocus(); 127 } 128 129 pub fn getSelectedIndex(self: View) ?usize { 130 return self.list.getSelectedIndex(); 131 } 132 }; 133