╭ changed ──────────────────╮ │src/ui/Repo/Discussions.zig│ ╰───────────────────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭─────────────────────────────────────────────────╮ │ 5 const xit = @import("xit"); │ │ 6 const rp = xit.repo; │ │ 7 const hash = xit.hash; │ │ 8 -const xitui = xit.xitui; │ │ 9 -const wgt = xitui.widget; │ │10 -const layout = xitui.layout; │ │11 -const Key = xitui.input.Key; │ │12 -const Grid = xitui.grid.Grid; │ │13 -const Focus = xitui.focus.Focus; │ │14 -const inp = @import("../input.zig"); │ │ 8 const Comment = @import("Comment.zig"); │ │ 9 const Attachment = @import("Attachment.zig");│ │10 const Threads = @import("Threads.zig"); │ ╰─────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │147 │ │148 var root_key: ?[]const u8 = null; │ │149 if (rooted) { │ │157 - if (empty.selected_id.len != evt.event_id_size * 2) return error.NotFound; │ │158 - var id: [evt.event_id_size]u8 = undefined; │ │159 - _ = std.fmt.hexToBytes(&id, empty.selected_id) catch return error.NotFound; │ │150 + const id = evt.parseEventId(empty.selected_id) catch return error.NotFound; │ │151 const record_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &id)) orelse return error.NotFound; │ │152 const record = try evt.read(evt.Discussion.Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(record_cursor));│ │153 if (record.removed) return error.NotFound; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │258 } │ │259 pub const View = Threads.View(.discuss, Self); │ │260 │ │270 -// tabs switching between the discussions page's views. │ │271 -pub const Header = struct { │ │272 - box: wgt.Box(ui.Widget), │ │273 - tab_ids: std.AutoArrayHashMapUnmanaged(usize, void), │ │274 - │ │275 - pub fn init(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { │ │276 - var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); │ │277 - errdefer box.deinit(allocator); │ │278 - │ │279 - var tab_ids: std.AutoArrayHashMapUnmanaged(usize, void) = .empty; │ │280 - errdefer tab_ids.deinit(allocator); │ │261 +pub const Header = Threads.Header; │ │262 │ │282 - const aa = session.page_arena.allocator(); │ │263 +// tabs switching between the discussions page's views │ │264 +pub fn initHeader(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { │ │265 + var header = try Header.init(allocator); │ │266 + errdefer header.deinit(allocator); │ │267 + const aa = session.page_arena.allocator(); │ │268 │ │284 - // recent discussions │ │285 - { │ │286 - const route = ui.RoutablePage.repoDiscussionsRoute(data.identity, data.tag, "") orelse return error.RouteTooLong; │ │287 - const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); │ │288 - var label_buf: [64]u8 = undefined; │ │289 - const label = try std.fmt.bufPrint(&label_buf, "recent ({d})", .{data.recent.count}); │ │290 - try addTab(allocator, &box, &tab_ids, label, link); │ │291 - } │ │292 - │ │293 - // tags tab, labeled with the active tag filter │ │294 - { │ │295 - const tags_route = ui.RoutablePage.repoThreadTagsRoute(.discuss, data.identity, data.tag) orelse return error.RouteTooLong; │ │296 - const tags_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try tags_route.toUrl(session.page_arena)}); │ │297 - const label = if (data.tag.len == 0) "tags" else blk: { │ │298 - const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); │ │299 - break :blk try std.fmt.allocPrint(aa, "tags ({s})", .{decoded}); │ │300 - }; │ │301 - try addTab(allocator, &box, &tab_ids, label, tags_link); │ │302 - } │ │303 - │ │304 - // new-discussion tab; an edit or comment url shows its tab in this place │ │305 - { │ │306 - const route = switch (data.view) { │ │307 - .edit => ui.RoutablePage.repoThreadEditRoute(.discuss, data.identity, data.selected_id) orelse return error.RouteTooLong, │ │308 - .new_comment => ui.RoutablePage.repoThreadCommentNewRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, │ │309 - .edit_comment => ui.RoutablePage.repoThreadCommentEditRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong,│ │310 - .remove => ui.RoutablePage.repoThreadRemoveRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, │ │311 - else => ui.RoutablePage.repoThreadNewRoute(.discuss, data.identity) orelse return error.RouteTooLong, │ │312 - }; │ │313 - const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); │ │314 - const label: []const u8 = switch (data.view) { │ │315 - .edit => "edit", │ │316 - .new_comment => "reply", │ │317 - .edit_comment => "edit", │ │318 - .remove => "remove", │ │319 - else => "new", │ │320 - }; │ │321 - try addTab(allocator, &box, &tab_ids, label, link); │ │322 - } │ │323 - │ │324 - var self = Header{ .box = box, .tab_ids = tab_ids }; │ │325 - // the tab matching the page's view is selected initially. │ │326 - self.getFocus().child_id = self.tab_ids.keys()[View.viewIndex(data.view)]; │ │327 - return self; │ │269 + // recent discussions │ │270 + { │ │271 + const route = ui.RoutablePage.repoDiscussionsRoute(data.identity, data.tag, "") orelse return error.RouteTooLong; │ │272 + const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); │ │273 + var label_buf: [64]u8 = undefined; │ │274 + const label = try std.fmt.bufPrint(&label_buf, "recent ({d})", .{data.recent.count}); │ │275 + try header.addTab(allocator, label, link, 0); │ │276 } │ │277 │ │330 - fn addTab(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), tab_ids: *std.AutoArrayHashMapUnmanaged(usize, void), label: []const u8, link: []const u8) !void { │ │331 - var text_box = try wgt.TextBox(ui.Widget).init(allocator, label, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │332 - errdefer text_box.deinit(allocator); │ │333 - text_box.getFocus().focusable = true; │ │334 - text_box.getFocus().kind = .{ .custom = link }; │ │335 - try tab_ids.put(allocator, text_box.getFocus().id, {}); │ │336 - try box.children.put(allocator, text_box.getFocus().id, .{ │ │337 - .widget = .{ .text_box = text_box }, │ │338 - .rect = null, │ │339 - .min_size = .{ .width = label.len + 2, .height = null }, │ │340 - }); │ │341 - } │ │342 - │ │343 - pub fn deinit(self: *Header, allocator: std.mem.Allocator) void { │ │344 - self.box.deinit(allocator); │ │345 - self.tab_ids.deinit(allocator); │ │346 - } │ │347 - │ │348 - pub fn build(self: *Header, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { │ │349 - self.clearGrid(); │ │350 - // only the selected tab shows its border │ │351 - for (self.box.children.keys(), self.box.children.values()) |id, *child| { │ │352 - switch (child.widget) { │ │353 - .text_box => |*tb| tb.options.border_style = if (self.getFocus().child_id == id) .single else .hidden, │ │354 - else => {}, │ │355 - } │ │356 - } │ │357 - try self.box.build(allocator, constraint, root_focus); │ │358 - } │ │359 - │ │360 - pub fn input(self: *Header, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { │ │361 - _ = allocator; │ │362 - const current_tab = self.currentTabIndex() orelse return; │ │363 - if (inp.moveTab(key, current_tab, self.tab_ids.count())) |new_tab| { │ │364 - root_focus.setFocus(self.tab_ids.keys()[new_tab]); │ │365 - } │ │366 - } │ │367 - │ │368 - pub fn clearGrid(self: *Header) void { │ │369 - self.box.clearGrid(); │ │370 - } │ │371 - │ │372 - pub fn getGrid(self: Header) ?Grid { │ │373 - return self.box.getGrid(); │ │278 + // tags tab, labeled with the active tag filter │ │279 + { │ │280 + const tags_route = ui.RoutablePage.repoThreadTagsRoute(.discuss, data.identity, data.tag) orelse return error.RouteTooLong; │ │281 + const tags_link = try std.fmt.allocPrint(aa, "ai:{s}", .{try tags_route.toUrl(session.page_arena)}); │ │282 + const label = if (data.tag.len == 0) "tags" else blk: { │ │283 + const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); │ │284 + break :blk try std.fmt.allocPrint(aa, "tags ({s})", .{decoded}); │ │285 + }; │ │286 + try header.addTab(allocator, label, tags_link, View.viewIndex(.tags)); │ │287 } │ │288 │ │376 - pub fn getFocus(self: *Header) *Focus { │ │377 - return self.box.getFocus(); │ │378 - } │ │379 - │ │380 - pub fn getSelectedIndex(self: Header) ?usize { │ │381 - return self.currentTabIndex(); │ │289 + // new-discussion tab; an edit or comment url shows its tab in this place │ │290 + { │ │291 + const route = switch (data.view) { │ │292 + .edit => ui.RoutablePage.repoThreadEditRoute(.discuss, data.identity, data.selected_id) orelse return error.RouteTooLong, │ │293 + .new_comment => ui.RoutablePage.repoThreadCommentNewRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, │ │294 + .edit_comment => ui.RoutablePage.repoThreadCommentEditRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, │ │295 + .remove => ui.RoutablePage.repoThreadRemoveRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, │ │296 + else => ui.RoutablePage.repoThreadNewRoute(.discuss, data.identity) orelse return error.RouteTooLong, │ │297 + }; │ │298 + const link = try std.fmt.allocPrint(aa, "ai:{s}", .{try route.toUrl(session.page_arena)}); │ │299 + const label: []const u8 = switch (data.view) { │ │300 + .edit => "edit", │ │301 + .new_comment => "reply", │ │302 + .edit_comment => "edit", │ │303 + .remove => "remove", │ │304 + else => "new", │ │305 + }; │ │306 + try header.addTab(allocator, label, link, View.viewIndex(.new)); │ │307 } │ │308 │ │384 - fn currentTabIndex(self: Header) ?usize { │ │385 - const child_id = self.box.focus.child_id orelse return null; │ │386 - return self.tab_ids.getIndex(child_id); │ │387 - } │ │388 -}; │ │309 + header.select(View.viewIndex(data.view)); │ │310 + return header; │ │311 +} │ │312 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯