╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Discussions.zig ──────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
178 const tag_sets = try DB.SortedMap(.read_only).init(tags_cursor); 179 var tag_iter = try tag_sets.iterator(); 180 while (try tag_iter.next()) |entry_cursor| { 181 if (tag_names.items.len == max_tags) break; 182 var entry = entry_cursor; 183 const pair = try entry.readKeyValuePair(); 184 try tag_names.append(aa, try pair.key_cursor.readBytesAlloc(aa, null)); 185 } 186 } 187 188 return .{ 189 .identity = empty.identity, 190 .tag = empty.tag, 191 .selected_id = empty.selected_id, 192 .comment_id = empty.comment_id, 193 .comments_start = comments_start, 194 .comment_page = comment_page, 195 .all = loaded_window, 196 .view = empty.view, 197 .description_page = empty.description_page, 198 .tags = tag_names.items, 199 }; 200 } 201 202 fn loadWindow( 203 comptime hash_kind: hash.HashKind, 204 arena: *std.heap.ArenaAllocator, 205 admin_moment: ?evt.AdminDB.HashMap(.read_only), 206 haxy_moment: evt.EventDB(hash_kind).HashMap(.read_only), 207 records: evt.EventDB(hash_kind).HashMap(.read_only), 208 set_maybe: ?evt.EventDB(hash_kind).SortedSet(.read_only), 209 root_key: ?[]const u8, 210 selected_id: []const u8, 211 comments_start: usize, 212 ) !Window { 213 const set = set_maybe orelse return .empty; 214 const DB = evt.EventDB(hash_kind); 215 const aa = arena.allocator(); 216 217 var prev_id: ?[]const u8 = null; 218 var iter = if (root_key) |key| blk: { 219 const rank = try set.rank(key); 220 if (rank > 0 and rank <= page_size) { 221 prev_id = ""; 222 } else if (rank > page_size) { 223 const pair = try set.getIndexKeyValuePair(@intCast(rank - page_size)) orelse return error.NotFound; 224 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 225 _ = try pair.key_cursor.readBytes(&order_key); 226 const hex = std.fmt.bytesToHex(order_key[@sizeOf(u64)..].*, .lower); 227 prev_id = try aa.dupe(u8, &hex); 228 } 229 break :blk try set.iteratorFrom(key); 230 } else try set.iteratorFromIndex(0); 231 232 var discussions: std.ArrayList(DiscussionWithId) = .empty; 233 var next_id: ?[]const u8 = null; 234 while (try iter.next()) |cursor| { 235 const id = try evt.readOrderKeyId(DB, cursor); 236 const id_hex = std.fmt.bytesToHex(id, .lower); 237 if (discussions.items.len == page_size) { 238 next_id = try aa.dupe(u8, &id_hex); 239 break; 240 } 241 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, &id)) orelse continue; 242 const record = try evt.read(evt.Discussion.Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); 243 try discussions.append(aa, .{ 244 .id = try aa.dupe(u8, &id_hex), 245 .record = record, 246 .author = try ui.Author.initFromEmail(admin_moment, arena, record.author_email), 247 .comments = try Comment.loadWindow( 248 hash_kind, 249 arena, 250 admin_moment, 251 haxy_moment, 252 evt.Comment.thread_id_to_comment_id_set_key, 253 &id_hex, 254 if (std.mem.eql(u8, &id_hex, selected_id)) comments_start else 0, 255 ), 256 .attachments = try Attachment.load(hash_kind, arena, haxy_moment, &id_hex), 257 }); 258 } 259 260 return .{ 261 .items = discussions.items, 262 .prev_id = prev_id, 263 .next_id = next_id, 264 .count = @intCast(try set.count()), 265 }; 266 } 267 pub const View = Threads.View(.discussion, Self); 268 269 // tabs switching between the discussions page's views. 270 pub const Header = struct { 271 box: wgt.Box(ui.Widget), 272 tab_ids: std.AutoArrayHashMapUnmanaged(usize, void), 273 274 pub fn init(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { 275 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 276 errdefer box.deinit(allocator); 277 278 var tab_ids: std.AutoArrayHashMapUnmanaged(usize, void) = .empty; 279 errdefer tab_ids.deinit(allocator); 280 281 const aa = session.page_arena.allocator(); 282 283 // all discussions 284 { 285 const route = ui.RoutablePage.repoDiscussionsRoute(data.identity, data.tag, "") orelse return error.RouteTooLong; 286 const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); 287 const label = try std.fmt.allocPrint(aa, "all ({d})", .{data.all.count}); 288 try addTab(allocator, &box, &tab_ids, label, link); 289 } 290 291 // tags tab, labeled with the active tag filter 292 { 293 const tags_route = ui.RoutablePage.repoDiscussionsTagsRoute(data.identity, data.tag) orelse return error.RouteTooLong; 294 const tags_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try tags_route.toUrl(session.page_arena)}); 295 const label = if (data.tag.len == 0) "tags" else blk: { 296 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); 297 break :blk try std.fmt.allocPrint(aa, "tags ({s})", .{decoded}); 298 }; 299 try addTab(allocator, &box, &tab_ids, label, tags_link); 300 } 301 302 // new-discussion tab; an edit or comment url shows its tab in this place 303 { 304 const route = switch (data.view) { 305 .edit => ui.RoutablePage.repoDiscussionsEditRoute(data.identity, data.selected_id) orelse return error.RouteTooLong, 306 .new_comment => ui.RoutablePage.repoDiscussionCommentNewRoute(data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 307 .edit_comment => ui.RoutablePage.repoDiscussionCommentEditRoute(data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 308 else => ui.RoutablePage.repoDiscussionsNewRoute(data.identity) orelse return error.RouteTooLong, 309 }; 310 const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); 311 const label: []const u8 = switch (data.view) { 312 .edit => "edit", 313 .new_comment => "reply", 314 .edit_comment => "edit", 315 else => "new", 316 }; 317 try addTab(allocator, &box, &tab_ids, label, link); 318 } 319 320 var self = Header{ .box = box, .tab_ids = tab_ids }; 321 // the tab matching the page's view is selected initially. 322 self.getFocus().child_id = self.tab_ids.keys()[View.viewIndex(data.view)]; 323 return self; 324 } 325 326 fn addTab(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), tab_ids: *std.AutoArrayHashMapUnmanaged(usize, void), label: []const u8, link: []const u8) !void { 327 var text_box = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 328 errdefer text_box.deinit(allocator); 329 text_box.getFocus().focusable = true; 330 text_box.getFocus().kind = .{ .custom = link }; 331 try tab_ids.put(allocator, text_box.getFocus().id, {}); 332 try box.children.put(allocator, text_box.getFocus().id, .{ 333 .widget = .{ .text_box = text_box }, 334 .rect = null, 335 .min_size = .{ .width = label.len + 2, .height = null }, 336 }); 337 } 338 339 pub fn deinit(self: *Header, allocator: std.mem.Allocator) void { 340 self.box.deinit(allocator); 341 self.tab_ids.deinit(allocator); 342 } 343 344 pub fn build(self: *Header, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 345 self.clearGrid(); 346 // only the selected tab shows its border 347 for (self.box.children.keys(), self.box.children.values()) |id, *child| { 348 switch (child.widget) { 349 .text_box => |*tb| tb.options.border_style = if (self.getFocus().child_id == id) .single else .hidden, 350 else => {}, 351 } 352 } 353 try self.box.build(allocator, constraint, root_focus); 354 } 355 356 pub fn input(self: *Header, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 357 _ = allocator; 358 const current_tab = self.currentTabIndex() orelse return; 359 if (inp.moveTab(key, current_tab, self.tab_ids.count())) |new_tab| { 360 root_focus.setFocus(self.tab_ids.keys()[new_tab]); 361 } 362 } 363 364 pub fn clearGrid(self: *Header) void { 365 self.box.clearGrid(); 366 } 367 368 pub fn getGrid(self: Header) ?Grid { 369 return self.box.getGrid(); 370 } 371 372 pub fn getFocus(self: *Header) *Focus { 373 return self.box.getFocus(); 374 } 375 376 pub fn getSelectedIndex(self: Header) ?usize { 377 return self.currentTabIndex(); 378 } 379 380 fn currentTabIndex(self: Header) ?usize { 381 const child_id = self.box.focus.child_id orelse return null; 382 return self.tab_ids.getIndex(child_id); 383 } 384 }; 385