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