╭ 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 Key = xitui.input.Key; 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.Public, 16 start: usize, // the window start this page was built with, mirrored into the url 17 next_start: ?usize, // the `start` 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 start: 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.Public) = .empty; 30 31 // the active users ordered by creation (newest first); absent until the first user 32 // exists. keyed by orderKey ([created-order][event-id]); the trailing bytes 33 // of each key are the user event id. 34 const user_id_set_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, evt.User.active_id_set_key)) orelse 35 return .{ .users = &.{}, .start = start, .next_start = null }; 36 const user_id_set = try DB.SortedSet(.read_only).init(user_id_set_cursor); 37 const count = try user_id_set.count(); 38 39 const event_id_to_user_cursor = try haxy_moment.getCursor(hash.hashInt(hash_kind, "event-id->user")) orelse 40 return .{ .users = &.{}, .start = start, .next_start = null }; 41 const event_id_to_user = try DB.HashMap(.read_only).init(event_id_to_user_cursor); 42 43 // read the window [start, start+page_size) with one seek to the start rank, 44 // then a sequential walk 45 const end = @min(start +| page_size, count); 46 var iter = try user_id_set.iteratorFromIndex(start); 47 var i = start; 48 while (i < end) : (i += 1) { 49 var id_cursor = (try iter.next()) orelse break; 50 const id_kv = try id_cursor.readKeyValuePair(); 51 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 52 _ = try id_kv.key_cursor.readBytes(&order_key); 53 const event_id = order_key[@sizeOf(u64)..]; 54 const user_cursor = try event_id_to_user.getCursor(hash.hashInt(hash_kind, event_id)) orelse continue; 55 const user_map = try DB.HashMap(.read_only).init(user_cursor); 56 const user_event = try evt.read(evt.User.Record, DB, hash_kind, arena, user_map); 57 try users.append(arena.allocator(), evt.project(evt.User.Public, user_event.event)); 58 } 59 60 return .{ 61 .users = users.items, 62 .start = start, 63 .next_start = if (end < count) end else null, 64 }; 65 } 66 67 pub const View = struct { 68 list: ui.widget.FlowBox.Scroll, 69 data: *const Self, 70 71 pub fn init(allocator: std.mem.Allocator, data: *const Self, web_native: bool) !View { 72 var self = blk: { 73 var list = try ui.widget.FlowBox.Scroll.init(allocator, .{ .cell_height = 1 }, web_native); 74 errdefer list.deinit(allocator); 75 76 break :blk View{ 77 .list = list, 78 .data = data, 79 }; 80 }; 81 errdefer self.deinit(allocator); 82 83 var arena = std.heap.ArenaAllocator.init(allocator); 84 defer arena.deinit(); 85 const aa = arena.allocator(); 86 87 // a leading "previous" row off the first window, one row per user, then 88 // a trailing "next" row when more remain. each window row navigates to the 89 // adjacent window (full reload on web, Nav rebuild on the TUI). 90 var items: std.ArrayList(ui.widget.FlowBox.Item) = .empty; 91 if (data.start > 0) 92 try items.append(aa, .{ .text = "← previous", .link = try std.fmt.allocPrint(aa, "a:/users/start:{d}", .{data.start -| page_size}) }); 93 for (data.users) |user| 94 // clicking a user opens their page; the "a:" prefix makes the web 95 // renderer emit an <a href="/user/foo"> anchor. 96 try items.append(aa, .{ 97 .text = try aa.dupe(u8, user.name), 98 .link = try std.fmt.allocPrint(aa, "a:/user/{s}", .{user.name}), 99 }); 100 if (data.next_start) |next_start| 101 try items.append(aa, .{ .text = "next →", .link = try std.fmt.allocPrint(aa, "a:/users/start:{d}", .{next_start}) }); 102 try self.list.setItems(allocator, items.items); 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: 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