╭ changed ───╮ │src/fork.zig│ ╰────────────╯ ╭────────────────────────────────────────────────────────────────╮ │429 } │ │430 } │ │431 │ │432 -// delete first so a failed tombstone can be retried safely│ │432 +// tombstone first so a missing fork never remains visible │ │433 pub fn remove( │ │434 io: std.Io, │ │435 allocator: std.mem.Allocator, │ ╰────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │445 const moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo); │ │446 const record = (try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &arena, &fork_id)) orelse return error.InvalidPatchDraft;│ │447 if (!std.mem.eql(u8, record.event.user_id, user_id)) return error.InvalidPatchDraft; │ │448 + if (!record.removed) try evt.remove(.admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, &fork_id, .fork, author); │ │449 │ │450 const path = try forkPath(allocator, repo_root_path, id); │ │451 defer allocator.free(path); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │452 try std.Io.Dir.cwd().deleteTree(io, path); │ │452 - if (!record.removed) try evt.remove(.admin, .xit, evt.admin_repo_opts, io, allocator, admin_repo, &fork_id, .fork, author);│ │453 } │ │454 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────────────╮ │src/ui/Repo/Patches.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │208 .author = author, │ │209 .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │210 }); │ │211 +} │ │212 + │ │213 +pub fn removeDraft(session: *ui.Session, allocator: std.mem.Allocator, id: *const [evt.event_id_size]u8) !void {│ │214 + const io = session.io orelse return error.NotFound; │ │215 + const repos_dir = session.repos_dir orelse return error.NotFound; │ │216 + const admin_repo = session.admin_repo orelse return error.NotFound; │ │217 + const user_id_slice = session.data.user_id orelse return error.NotFound; │ │218 + if (user_id_slice.len != evt.event_id_size) return error.NotFound; │ │219 + const author = (try session.eventAuthor()) orelse return error.NotFound; │ │220 + │ │221 + var user_id: [evt.event_id_size]u8 = undefined; │ │222 + @memcpy(&user_id, user_id_slice); │ │223 + const id_hex = std.fmt.bytesToHex(id.*, .lower); │ │224 + try fork.remove(io, allocator, repos_dir, admin_repo, &id_hex, &user_id, author); │ │225 } │ │226 │ │227 // `status`'s windowed listing. │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │330 } │ │331 }; │ │332 empty.drafts = drafts_window; │ │319 - if (draft_selected) empty.view = .drafts; │ │333 + if (draft_selected and view != .remove) empty.view = .drafts; │ │334 │ │335 // an explicitly named posted patch that doesn't exist is a bad url │ │336 // (NotFound -> 404); drafts, tags, and bare routes can use the empty fallback.│ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1278 const route = listRoute(self.data.identity, @enumFromInt(0), "", entry.id) orelse return error.RouteTooLong; │ │1279 row.getFocus().kind = .{ .custom = try std.fmt.allocPrint(pa, "form:{s}/post", .{try route.toUrl(self.session.page_arena)}) };│ │1280 try addToolButton(allocator, row, "post", "submit"); │ │1281 + } │ │1282 + if (self.session.data.user_id != null) { │ │1283 + const route = ui.RoutablePage.repoThreadRemoveRoute(kind, self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │1284 + try addToolButton(allocator, row, "✕", try std.fmt.allocPrint(pa, "a:{s}", .{try route.toUrl(self.session.page_arena)})); │ │1285 } │ │1286 if (row.children.count() > first_in_row_index) │ │1287 row.getFocus().child_id = row.children.keys()[first_in_row_index]; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2337 │ │2338 fn removeEvent(self: *This, allocator: std.mem.Allocator, event_kind: evt.EventKind, id: [evt.event_id_size]u8) !void {│ │2339 if (comptime wasm) return; │ │2340 + │ │2341 + if (comptime supports_drafts) { │ │2342 + const selected = self.data.selectedThread() orelse return; │ │2343 + if (event_kind == kind and entryDraft(selected.*)) { │ │2344 + try Data.removeDraft(self.session, allocator, &id); │ │2345 + const route = draftsRoute(self.data.identity) orelse return; │ │2346 + try self.session.navigate(route); │ │2347 + return; │ │2348 + } │ │2349 + } │ │2350 + │ │2351 const io = self.session.io orelse return; │ │2352 const src = self.data.repo_source orelse return; │ │2353 const author = (try self.session.eventAuthor()) orelse return; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1117 const request_repo = (try requestRepoSource(io, allocator, host, parts.repo_base)) orelse return respondRemoveNotFound(request); │ │1118 defer request_repo.deinit(allocator); │ │1119 const source = request_repo.source; │ │1120 + │ │1121 + if (parts.kind == .patch) switch (host) { │ │1122 + .remote => |remote| { │ │1123 + const token = getCookieValue(request, cookie_name) orelse return respondLoginRequired(request); │ │1124 + var user_id: [evt.event_id_size]u8 = undefined; │ │1125 + if (!remote.session_store.lookup(token, &user_id)) return respondLoginRequired(request); │ │1126 + │ │1127 + var admin_repo = try rp.Repo(.xit, evt.admin_repo_opts).open(io, allocator, .{ .path = remote.admin_repo_path }); │ │1128 + defer admin_repo.deinit(io, allocator); │ │1129 + const moment = try evt.currentMoment(evt.admin_repo_opts, &admin_repo); │ │1130 + const record = try evt.Fork.readById(evt.AdminDB, evt.admin_repo_opts.hash, moment, &author_arena, &parts.id); │ │1131 + if (record) |fork_record| { │ │1132 + const repo_id = evt.parseEventId(std.fs.path.basename(source.path)) catch return respondRemoveNotFound(request); │ │1133 + if (!fork_record.removed and fork_record.event.stage == .draft and std.mem.eql(u8, fork_record.event.repo_id, &repo_id)) { │ │1134 + const id = std.fmt.bytesToHex(parts.id, .lower); │ │1135 + const repos_dir = try std.fs.path.join(allocator, &.{ std.fs.path.dirname(remote.admin_repo_path) orelse ".", "repos" });│ │1136 + defer allocator.free(repos_dir); │ │1137 + try fork.remove(io, allocator, repos_dir, &admin_repo, &id, &user_id, author); │ │1138 + │ │1139 + const location = try std.fmt.allocPrint(allocator, "{s}/patches/drafts", .{parts.repo_base}); │ │1140 + defer allocator.free(location); │ │1141 + try request.respond("", .{ │ │1142 + .status = .see_other, │ │1143 + .keep_alive = false, │ │1144 + .extra_headers = &.{.{ .name = "location", .value = location }}, │ │1145 + }); │ │1146 + return; │ │1147 + } │ │1148 + } │ │1149 + }, │ │1150 + .local => {}, │ │1151 + }; │ │1152 + │ │1153 switch (source.repo_kind) { │ │1154 inline else => |repo_kind| { │ │1155 var any_repo = try rp.AnyRepo(repo_kind, .{}).open(io, allocator, source.localInitOpts()); │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯