╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Comment.zig ──────────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
122 } else null; 123 return .{ 124 .id = std.fmt.bytesToHex(id_bytes.*, .lower), 125 .comment = comment, 126 .author = try ui.Author.initFromEmail(admin_moment, arena, comment.author_email), 127 .parent_author = parent_author, 128 }; 129 } 130 131 fn idBytes(id: []const u8) ?[evt.event_id_size]u8 { 132 if (id.len != evt.event_id_size * 2) return null; 133 var bytes: [evt.event_id_size]u8 = undefined; 134 _ = std.fmt.hexToBytes(&bytes, id) catch return null; 135 if (!std.mem.eql(u8, id, &std.fmt.bytesToHex(bytes, .lower))) return null; 136 return bytes; 137 } 138 139 pub const Item = struct { 140 box: wgt.Box(ui.Widget), 141 id: [evt.event_id_size]u8, 142 remove_button_id: ?usize, 143 144 const metadata_index: usize = 0; 145 const body_index: usize = 1; 146 const gap_index: usize = 2; 147 const metadata_first_link_index: usize = 1; 148 149 pub fn init(allocator: std.mem.Allocator, session: *ui.Session, identity: []const u8, thread_kind: evt.EventKind, entry: CommentWithId) !Item { 150 var box = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .vert }); 151 errdefer box.deinit(allocator); 152 var remove_button_id: ?usize = null; 153 154 { 155 var bar = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 156 errdefer bar.deinit(allocator); 157 158 var spacer = try ui.Spacer.init(allocator); 159 errdefer spacer.deinit(allocator); 160 try bar.children.put(allocator, spacer.getFocus().id, .{ .widget = .{ .spacer = spacer }, .rect = null, .min_size = null }); 161 162 var author = try ui.authorBox(allocator, session.page_arena, entry.author); 163 errdefer author.deinit(allocator); 164 try bar.children.put(allocator, author.getFocus().id, .{ .widget = .{ .text_box = author }, .rect = null, .min_size = null, .shrink = true }); 165 166 var reply = try linkBox(allocator, session, "new reply", commentNewRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id) orelse return error.RouteTooLong); 167 errdefer reply.deinit(allocator); 168 try bar.children.put(allocator, reply.getFocus().id, .{ .widget = .{ .text_box = reply }, .rect = null, .min_size = .{ .width = "new reply".len + 2, .height = null } }); 169 170 if (!entry.comment.removed) { 171 var edit = try linkBox(allocator, session, "edit comment", commentEditRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id) orelse return error.RouteTooLong); 172 errdefer edit.deinit(allocator); 173 try bar.children.put(allocator, edit.getFocus().id, .{ .widget = .{ .text_box = edit }, .rect = null, .min_size = .{ .width = "edit comment".len + 2, .height = null } }); 174 } 175 176 var permalink = try linkBox(allocator, session, "permalink", commentsRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id, 0) orelse return error.RouteTooLong); 177 errdefer permalink.deinit(allocator); 178 try bar.children.put(allocator, permalink.getFocus().id, .{ .widget = .{ .text_box = permalink }, .rect = null, .min_size = .{ .width = "permalink".len + 2, .height = null } }); 179 180 if (!std.mem.eql(u8, &entry.comment.event.parent_id, &entry.comment.event.thread_id)) { 181 const parent_text = if (entry.parent_author) |parent_author| switch (parent_author) { 182 .unknown => "", 183 .email, .user_name => |text| text, 184 } else ""; 185 var parent = try wgt.TextBox(ui.Widget).init(allocator, parent_text, .{ 186 .border_style = .single, 187 .rounded_corners = true, 188 .wrap_kind = .none, 189 .label = " replying to ", 190 }); 191 errdefer parent.deinit(allocator); 192 parent.getFocus().focusable = true; 193 const route = commentsRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.comment.event.parent_id, 0) orelse return error.RouteTooLong; 194 parent.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), "a:{s}", .{try route.toUrl(session.page_arena)}) }; 195 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 } }); 196 } 197 198 if (!entry.comment.removed and (session.data.is_local or session.data.user_id != null)) { 199 const route = commentsRoute(thread_kind, identity, &entry.comment.event.thread_id, &entry.id, 0) orelse return error.RouteTooLong; 200 bar.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), "form:{s}/remove", .{try route.toUrl(session.page_arena)}) }; 201 var remove = try wgt.TextBox(ui.Widget).init(allocator, "✕", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 202 errdefer remove.deinit(allocator); 203 remove.getFocus().focusable = true; 204 remove.getFocus().kind = .{ .custom = "submit" }; 205 remove_button_id = remove.getFocus().id; 206 try bar.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } }); 207 } 208 209 bar.getFocus().child_id = bar.children.keys()[metadata_first_link_index]; 210 try box.children.put(allocator, bar.getFocus().id, .{ .widget = .{ .box = bar }, .rect = null, .min_size = null }); 211 } 212 213 const body_text = if (entry.comment.removed) "(removed)" else entry.comment.event.body; 214 var body_box = try wgt.TextBox(ui.Widget).init(allocator, body_text, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .word }); 215 errdefer body_box.deinit(allocator); 216 body_box.getFocus().focusable = true; 217 try box.children.put(allocator, body_box.getFocus().id, .{ .widget = .{ .text_box = body_box }, .rect = null, .min_size = null }); 218 219 var gap = try wgt.Text(ui.Widget).init(allocator, ""); 220 errdefer gap.deinit(allocator); 221 try box.children.put(allocator, gap.getFocus().id, .{ .widget = .{ .text = gap }, .rect = null, .min_size = null }); 222 223 box.getFocus().child_id = box.children.keys()[metadata_index]; 224 225 return .{ 226 .box = box, 227 .id = idBytes(&entry.id) orelse return error.InvalidEventId, 228 .remove_button_id = remove_button_id, 229 }; 230 } 231 232 fn metadata(self: *Item) *wgt.Box(ui.Widget) { 233 return &self.box.children.values()[metadata_index].widget.box; 234 } 235 236 fn body(self: *Item) *wgt.TextBox(ui.Widget) { 237 return &self.box.children.values()[body_index].widget.text_box; 238 } 239 240 pub fn bodyFocused(self: *Item) bool { 241 return self.box.getFocus().child_id == self.body().getFocus().id; 242 } 243 244 pub fn removeFocused(self: *Item) bool { 245 const id = self.remove_button_id orelse return false; 246 return self.metadata().getFocus().child_id == id; 247 } 248 249 pub fn focusMetadata(self: *Item, root_focus: *Focus) void { 250 const bar = self.metadata(); 251 root_focus.setFocus(bar.getFocus().child_id orelse bar.children.keys()[metadata_first_link_index]); 252 } 253 254 pub fn focusBody(self: *Item, root_focus: *Focus) void { 255 root_focus.setFocus(self.body().getFocus().id); 256 } 257 258 pub fn moveHorizontal(self: *Item, root_focus: *Focus, right: bool) bool { 259 if (self.bodyFocused()) return false; 260 const bar = self.metadata(); 261 const selected = bar.getFocus().child_id orelse return false; 262 const selected_index = bar.children.getIndex(selected) orelse return false; 263 const target = if (right) selected_index + 1 else selected_index -| 1; 264 if (target < metadata_first_link_index or target >= bar.children.count()) return false; 265 root_focus.setFocus(bar.children.keys()[target]); 266 return true; 267 } 268 269 pub fn rowRect(self: *Item, body_row: bool) ?layout.IRect { 270 if (!body_row) return self.box.children.values()[metadata_index].rect; 271 var rect = self.box.children.values()[body_index].rect orelse return null; 272 if (self.box.children.values()[gap_index].rect) |gap| rect.size.height += gap.size.height; 273 return rect; 274 } 275 276 pub fn deinit(self: *Item, allocator: std.mem.Allocator) void { 277 self.box.deinit(allocator); 278 } 279 280 pub fn build(self: *Item, allocator: std.mem.Allocator, constraint: layout.Constraint, root_focus: *Focus) !void { 281 try self.box.build(allocator, constraint, root_focus); 282 } 283 284 pub fn input(self: *Item, allocator: std.mem.Allocator, key: Key, root_focus: *Focus) !void { 285 try self.box.input(allocator, key, root_focus); 286 } 287 288 pub fn clearGrid(self: *Item) void { 289 self.box.clearGrid(); 290 } 291 292 pub fn getGrid(self: Item) ?Grid { 293 return self.box.getGrid(); 294 } 295 296 pub fn getFocus(self: *Item) *Focus { 297 return self.box.getFocus(); 298 } 299 }; 300 301 // append one comment's metadata bar and body. 302 pub fn appendComment(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, identity: []const u8, thread_kind: evt.EventKind, entry: CommentWithId) !void { 303 var item = try Item.init(allocator, session, identity, thread_kind, entry); 304 errdefer item.deinit(allocator); 305 try box.children.put(allocator, item.getFocus().id, .{ .widget = .{ .repo_comment = item }, .rect = null, .min_size = null }); 306 } 307 308 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 { 309 const text = try std.fmt.allocPrint(arena, "{d} {s}", .{ count, if (count == 1) singular else plural }); 310 var count_box = try wgt.TextBox(ui.Widget).init(allocator, text, .{ .border_style = .hidden, .wrap_kind = .none }); 311 errdefer count_box.deinit(allocator); 312 try box.children.put(allocator, count_box.getFocus().id, .{ .widget = .{ .text_box = count_box }, .rect = null, .min_size = null }); 313 } 314 315 pub fn appendWindowNav(allocator: std.mem.Allocator, box: *wgt.Box(ui.Widget), session: *ui.Session, identity: []const u8, thread_kind: evt.EventKind, thread_id: []const u8, selected_id: ?[]const u8, window: Window) !void { 316 const has_prev = window.start > 0; 317 const has_more = window.start < window.count and window.count - window.start > page_size; 318 if (!has_prev and !has_more) return; 319 var row = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz }); 320 errdefer row.deinit(allocator); 321 if (has_prev) { 322 const start = window.start -| page_size; 323 const route = commentsRoute(thread_kind, identity, thread_id, selected_id, start); 324 var previous = try linkBox(allocator, session, "← previous", route orelse return error.RouteTooLong); 325 errdefer previous.deinit(allocator); 326 try row.children.put(allocator, previous.getFocus().id, .{ .widget = .{ .text_box = previous }, .rect = null, .min_size = null }); 327 } 328 if (has_more) { 329 const start = window.start + page_size; 330 const route = commentsRoute(thread_kind, identity, thread_id, selected_id, start); 331 var next = try linkBox(allocator, session, "next →", route orelse return error.RouteTooLong); 332 errdefer next.deinit(allocator); 333 try row.children.put(allocator, next.getFocus().id, .{ .widget = .{ .text_box = next }, .rect = null, .min_size = null }); 334 } 335 row.getFocus().child_id = row.children.keys()[0]; 336 try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); 337 } 338 339 fn commentsRoute(kind: evt.EventKind, identity: []const u8, thread_id: []const u8, selected_id: ?[]const u8, start: usize) ?ui.RoutablePage { 340 return if (selected_id) |id| 341 ui.RoutablePage.repoThreadCommentRoute(kind, identity, thread_id, id, start) 342 else 343 ui.RoutablePage.repoThreadCommentsRoute(kind, identity, thread_id, start); 344 } 345 346 fn commentNewRoute(kind: evt.EventKind, identity: []const u8, thread_id: []const u8, parent_id: []const u8) ?ui.RoutablePage { 347 return ui.RoutablePage.repoThreadCommentNewRoute(kind, identity, thread_id, parent_id); 348 } 349 350 fn commentEditRoute(kind: evt.EventKind, identity: []const u8, thread_id: []const u8, comment_id: []const u8) ?ui.RoutablePage { 351 return ui.RoutablePage.repoThreadCommentEditRoute(kind, identity, thread_id, comment_id); 352 } 353 354 pub fn linkBox(allocator: std.mem.Allocator, session: *ui.Session, text: []const u8, route: ui.RoutablePage) !wgt.TextBox(ui.Widget) { 355 var box = try wgt.TextBox(ui.Widget).init(allocator, text, .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); 356 errdefer box.deinit(allocator); 357 box.getFocus().focusable = true; 358 box.getFocus().kind = .{ .custom = try std.fmt.allocPrint(session.page_arena.allocator(), "a:{s}", .{try route.toUrl(session.page_arena)}) }; 359 return box; 360 } 361