╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Comment.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
134 const parent_bytes = (try idBytes(&comment.event.parent_id)) orelse break :blk null; 135 const parent_cursor = try records.getCursor(hash.hashInt(hash_kind, &parent_bytes)) orelse break :blk null; 136 const parent_map = try DB.HashMap(.read_only).init(parent_cursor); 137 const parent = try evt.read(evt.Comment.Record, DB, hash_kind, arena, parent_map); 138 break :blk try ui.Author.initFromEmail(admin_moment, arena, parent.author_email); 139 } else null; 140 return .{ 141 .id = try arena.allocator().dupe(u8, id), 142 .comment = comment, 143 .author = try ui.Author.initFromEmail(admin_moment, arena, comment.author_email), 144 .parent_author = parent_author, 145 }; 146 } 147 148 fn idBytes(id: []const u8) !?[evt.event_id_size]u8 { 149 if (id.len != evt.event_id_size * 2) return null; 150 var bytes: [evt.event_id_size]u8 = undefined; 151 _ = std.fmt.hexToBytes(&bytes, id) catch return null; 152 if (!std.mem.eql(u8, id, &std.fmt.bytesToHex(bytes, .lower))) return null; 153 return bytes; 154 } 155 156 pub const Item = struct { 157 box: wgt.Box(ui.Widget), 158 159 const metadata_index: usize = 0; 160 const body_index: usize = 1; 161 162 pub fn init(allocator: std.mem.Allocator, session: *ui.Session, identity: []const u8, entry: CommentWithId) !Item { 163 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); 164 errdefer box.deinit(allocator); 165 166 { 167 var bar = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 168 errdefer bar.deinit(allocator); 169 170 var author = try ui.authorBox(allocator, session.page_arena, entry.author); 171 errdefer author.deinit(allocator); 172 try bar.children.put(allocator, author.getFocus().id, .{ .widget = .{ .text_box = author }, .rect = null, .min_size = null, .shrink = true }); 173 174 var reply = try linkBox(allocator, session, "new reply", ui.RoutablePage.repoCommentNewRoute(identity, &entry.comment.event.thread_id, entry.id) orelse return error.RouteTooLong); 175 errdefer reply.deinit(allocator); 176 try bar.children.put(allocator, reply.getFocus().id, .{ .widget = .{ .text_box = reply }, .rect = null, .min_size = .{ .width = "new reply".len + 2, .height = null } }); 177 178 var permalink = try linkBox(allocator, session, "permalink", ui.RoutablePage.repoCommentsRoute(identity, &entry.comment.event.thread_id, entry.id, 0) orelse return error.RouteTooLong); 179 errdefer permalink.deinit(allocator); 180 try bar.children.put(allocator, permalink.getFocus().id, .{ .widget = .{ .text_box = permalink }, .rect = null, .min_size = .{ .width = "permalink".len + 2, .height = null } }); 181 182 if (!std.mem.eql(u8, &entry.comment.event.parent_id, &entry.comment.event.thread_id)) { 183 const parent_text = if (entry.parent_author) |parent_author| switch (parent_author) { 184 .unknown => "", 185 .email, .user_name => |text| text, 186 } else ""; 187 var parent = try wgt.TextBox(ui.Widget).init(allocator, parent_text, .{ 188 .border_style = .single, 189 .rounded_corners = true, 190 .wrap_kind = .none, 191 .label = " replying to ", 192 }); 193 errdefer parent.deinit(allocator); 194 parent.getFocus().focusable = true; 195 const route = ui.RoutablePage.repoCommentsRoute(identity, &entry.comment.event.thread_id, &entry.comment.event.parent_id, 0) orelse return error.RouteTooLong; 196 parent.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), "a:{s}", .{try route.toUrl(session.page_arena)}) }; 197 try bar.children.put(allocator, parent.getFocus().id, .{ .widget = .{ .text_box = parent }, .rect = null, .min_size = .{ .width = @max(parent_text.len, " replying to ".len) + 2, .height = null } }); 198 } 199 200 bar.getFocus().child_id = bar.children.keys()[0]; 201 try box.children.put(allocator, bar.getFocus().id, .{ .widget = .{ .box = bar }, .rect = null, .min_size = null }); 202 } 203 204 const body_text = if (entry.comment.deleted) "(deleted)" else entry.comment.event.body; 205 var body_box = try wgt.TextBox(ui.Widget).init(allocator, body_text, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .word }); 206 errdefer body_box.deinit(allocator); 207 body_box.getFocus().focusable = true; 208 try box.children.put(allocator, body_box.getFocus().id, .{ .widget = .{ .text_box = body_box }, .rect = null, .min_size = null }); 209 box.getFocus().child_id = box.children.keys()[metadata_index]; 210 211 return .{ .box = box }; 212 } 213 214 fn metadata(self: *Item) *wgt.Box(ui.Widget) { 215 return &self.box.children.values()[metadata_index].widget.box; 216 } 217 218 fn body(self: *Item) *wgt.TextBox(ui.Widget) { 219 return &self.box.children.values()[body_index].widget.text_box; 220 } 221 222 pub fn bodyFocused(self: *Item) bool { 223 return self.box.getFocus().child_id == self.body().getFocus().id; 224 } 225 226 pub fn focusMetadata(self: *Item, root_focus: *Focus) void { 227 const bar = self.metadata(); 228 root_focus.setFocus(bar.getFocus().child_id orelse bar.children.keys()[0]); 229 } 230 231 pub fn focusBody(self: *Item, root_focus: *Focus) void { 232 root_focus.setFocus(self.body().getFocus().id); 233 } 234 235 pub fn moveHorizontal(self: *Item, root_focus: *Focus, right: bool) bool { 236 if (self.bodyFocused()) return false; 237 const bar = self.metadata(); 238 const selected = bar.getFocus().child_id orelse return false; 239 const selected_index = bar.children.getIndex(selected) orelse return false; 240 const target = if (right) selected_index + 1 else selected_index -| 1; 241 if (target == selected_index or target >= bar.children.count()) return false; 242 root_focus.setFocus(bar.children.keys()[target]); 243 return true; 244 } 245 246 pub fn rowRect(self: *Item, body_row: bool) ?layout.IRect { 247 return self.box.children.values()[if (body_row) body_index else metadata_index].rect; 248 } 249 250 pub fn deinit(self: *Item, allocator: std.mem.Allocator) void { 251 self.box.deinit(allocator); 252 } 253 254 pub fn build(self: *Item, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 255 try self.box.build(allocator, constraint, root_focus); 256 } 257 258 pub fn input(self: *Item, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 259 try self.box.input(allocator, key, root_focus); 260 } 261 262 pub fn clearGrid(self: *Item) void { 263 self.box.clearGrid(); 264 } 265 266 pub fn getGrid(self: Item) ?Grid { 267 return self.box.getGrid(); 268 } 269 270 pub fn getFocus(self: *Item) *Focus { 271 return self.box.getFocus(); 272 } 273 }; 274 275 // append one comment's metadata bar and body. 276 pub fn appendComment(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, identity: []const u8, entry: CommentWithId) !void { 277 var item = try Item.init(allocator, session, identity, entry); 278 errdefer item.deinit(allocator); 279 try box.children.put(allocator, item.getFocus().id, .{ .widget = .{ .repo_comment = item }, .rect = null, .min_size = null }); 280 } 281 282 pub fn appendCount(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), count: usize, singular: []const u8, plural: []const u8, arena: std.mem.Allocator) !void { 283 const text = try std.fmt.allocPrint(arena, "{d} {s}", .{ count, if (count == 1) singular else plural }); 284 var count_box = try wgt.TextBox(ui.Widget).init(allocator, text, .{ .border_style = .hidden, .wrap_kind = .none }); 285 errdefer count_box.deinit(allocator); 286 try box.children.put(allocator, count_box.getFocus().id, .{ .widget = .{ .text_box = count_box }, .rect = null, .min_size = null }); 287 } 288 289 pub fn appendWindowNav(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, identity: []const u8, thread_id: []const u8, selected_id: ?[]const u8, window: Window) !void { 290 if (!window.has_prev and !window.has_more) return; 291 var row = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 292 errdefer row.deinit(allocator); 293 if (window.has_prev) { 294 const start = window.start -| page_size; 295 const route = if (selected_id) |id| 296 ui.RoutablePage.repoCommentsRoute(identity, thread_id, id, start) 297 else 298 ui.RoutablePage.repoIssueCommentsRoute(identity, thread_id, start); 299 var previous = try linkBox(allocator, session, "← previous", route orelse return error.RouteTooLong); 300 errdefer previous.deinit(allocator); 301 try row.children.put(allocator, previous.getFocus().id, .{ .widget = .{ .text_box = previous }, .rect = null, .min_size = null }); 302 } 303 if (window.has_more) { 304 const start = window.start + page_size; 305 const route = if (selected_id) |id| 306 ui.RoutablePage.repoCommentsRoute(identity, thread_id, id, start) 307 else 308 ui.RoutablePage.repoIssueCommentsRoute(identity, thread_id, start); 309 var next = try linkBox(allocator, session, "next →", route orelse return error.RouteTooLong); 310 errdefer next.deinit(allocator); 311 try row.children.put(allocator, next.getFocus().id, .{ .widget = .{ .text_box = next }, .rect = null, .min_size = null }); 312 } 313 row.getFocus().child_id = row.children.keys()[0]; 314 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); 315 } 316 317 pub fn linkBox(allocator: std.mem.Allocator, session: *ui.Session, text: []const u8, route: ui.RoutablePage) !wgt.TextBox(ui.Widget) { 318 var box = try wgt.TextBox(ui.Widget).init(allocator, text, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 319 errdefer box.deinit(allocator); 320 box.getFocus().focusable = true; 321 box.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), "a:{s}", .{try route.toUrl(session.page_arena)}) }; 322 return box; 323 } 324