╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────────────────╮ │src/ui/Repo/Attachment.zig│ ╰──────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────╮ │11 name: []const u8, │ │12 }; │ │13 │ │14 -// every attachment on a parent event, oldest first. tombstoned ones are│ │14 +// every attachment on a parent event, oldest first. removed ones are │ │15 // left out. │ │16 pub fn load( │ │17 comptime hash_kind: hash.HashKind, │ ╰────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │30 while (try iter.next()) |cursor_val| { │ │31 const id_bytes = try evt.readOrderKeyId(DB, cursor_val); │ │32 const record = (try evt.Attachment.readById(DB, hash_kind, haxy_moment, arena, &id_bytes)) orelse continue;│ │33 - if (record.deleted) continue; │ │33 + if (record.removed) continue; │ │34 try attachments.append(arena.allocator(), .{ │ │35 .id = std.fmt.bytesToHex(id_bytes, .lower), │ │36 .name = record.name, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────╮ │46 box: *wgt.Box(ui.Widget), │ │47 session: *ui.Session, │ │48 identity: []const u8, │ │49 + parent_url: []const u8, │ │50 attachments: []const WithId, │ │51 ) !void { │ │52 const pa = session.page_arena.allocator();│ ╰──────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │53 for (attachments) |entry| { │ │54 + var row = try wgt.Box(ui.Widget).init(allocator, .{ .border_style = null, .direction = .horiz });│ │55 + errdefer row.deinit(allocator); │ │56 + │ │57 var tb = try wgt.TextBox(ui.Widget).init(allocator, entry.name, .{ │ │58 .border_style = .single, │ │59 .rounded_corners = true, │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │ 63 errdefer tb.deinit(allocator); │ │ 64 tb.getFocus().focusable = true; │ │ 65 tb.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "{s}{s}", .{ ui.raw_link_prefix, try url(session.page_arena, identity, &entry.id) }) }; │ │ 62 - try box.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │ 66 + try row.children.put(allocator, tb.getFocus().id, .{ .widget = .{ .text_box = tb }, .rect = null, .min_size = null }); │ │ 67 + │ │ 68 + if (session.data.is_local or session.data.user_id != null) { │ │ 69 + row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/attachment:{s}/remove", .{ parent_url, &entry.id }) }; │ │ 70 + var remove = try wgt.TextBox(ui.Widget).init(allocator, "✕", .{ .border_style = .single, .rounded_corners = true, .wrap_kind = .none }); │ │ 71 + errdefer remove.deinit(allocator); │ │ 72 + remove.getFocus().focusable = true; │ │ 73 + remove.getFocus().kind = .{ .custom = "submit" }; │ │ 74 + try row.children.put(allocator, remove.getFocus().id, .{ .widget = .{ .text_box = remove }, .rect = null, .min_size = .{ .width = 3, .height = null } });│ │ 75 + } │ │ 76 + │ │ 77 + row.getFocus().child_id = row.children.keys()[0]; │ │ 78 + try box.children.put(allocator, row.getFocus().id, .{ .widget = .{ .box = row }, .rect = null, .min_size = null }); │ │ 79 } │ │ 80 +} │ │ 81 + │ │ 82 +// the attachment id carried by a removable row │ │ 83 +pub fn removeId(row: *wgt.Box(ui.Widget)) ?[evt.event_id_size]u8 { │ │ 84 + const prefix = "form:"; │ │ 85 + const infix = "/attachment:"; │ │ 86 + const suffix = "/remove"; │ │ 87 + const action = switch (row.getFocus().kind) { │ │ 88 + .custom => |custom| custom, │ │ 89 + else => return null, │ │ 90 + }; │ │ 91 + if (!std.mem.startsWith(u8, action, prefix) or !std.mem.endsWith(u8, action, suffix)) return null; │ │ 92 + const at = std.mem.lastIndexOf(u8, action, infix) orelse return null; │ │ 93 + const id = action[at + infix.len .. action.len - suffix.len]; │ │ 94 + if (id.len != evt.event_id_size * 2) return null; │ │ 95 + var bytes: [evt.event_id_size]u8 = undefined; │ │ 96 + _ = std.fmt.hexToBytes(&bytes, id) catch return null; │ │ 97 + return bytes; │ │ 98 } │ │ 99 │ │100 // the url an attachment's bytes are served from │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Comment.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────╮ │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; │ ╰─────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │149 pub fn init(allocator: std.mem.Allocator, session: *ui.Session, identity: []const u8, 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 }); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 │ │167 - var edit = try linkBox(allocator, session, "edit comment", ui.RoutablePage.repoCommentEditRoute(identity, &entry.comment.event.thread_id, &entry.id) orelse return error.RouteTooLong); │ │168 - errdefer edit.deinit(allocator); │ │169 - try bar.children.put(allocator, edit.getFocus().id, .{ .widget = .{ .text_box = edit }, .rect = null, .min_size = .{ .width = "edit comment".len + 2, .height = null } }); │ │170 + if (!entry.comment.removed) { │ │171 + var edit = try linkBox(allocator, session, "edit comment", ui.RoutablePage.repoCommentEditRoute(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", ui.RoutablePage.repoCommentsRoute(identity, &entry.comment.event.thread_id, &entry.id, 0) orelse return error.RouteTooLong); │ │177 errdefer permalink.deinit(allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │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 = ui.RoutablePage.repoCommentsRoute(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 │ │197 - const body_text = if (entry.comment.deleted) "(deleted)" else entry.comment.event.body; │ │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; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯