╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─╮ │src/ui.zig│ ╰──────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1275 try out.writer.print("{s}/repo/roles", .{try repoUrlPrefix(arena, r.name.slice())}); │ │1276 if (r.user.len != 0) try out.writer.print("/" ++ user_seg ++ "{s}", .{r.user.slice()});│ │1277 if (r.start != 0) try out.writer.print("/" ++ start_seg ++ "{d}", .{r.start}); │ │1278 + if (r.confirm) |action| try out.writer.print("/{s}", .{@tagName(action)}); │ │1279 break :blk out.written(); │ │1280 }, │ │1281 .repo_user => |u| try userTabUrl(arena, try repoUrlPrefix(arena, u.name.slice()), u.tab), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1314 fn userTabUrl(arena: *std.heap.ArenaAllocator, prefix: []const u8, tab: UserTab) ![]const u8 { │ │1315 return switch (tab.view) { │ │1316 .settings => try arena.allocator().print("{s}/user/settings", .{prefix}), │ │1303 - .keys => try arena.allocator().print("{s}/user/keys", .{prefix}), │ │1317 + .keys => blk: { │ │1318 + var out: std.Io.Writer.Allocating = .init(arena.allocator()); │ │1319 + try out.writer.print("{s}/user/keys", .{prefix}); │ │1320 + if (tab.key.len != 0) try out.writer.print("/" ++ key_seg ++ "{s}", .{try urlEncodeRef(arena.allocator(), tab.key.slice())});│ │1321 + if (tab.remove_key) try out.writer.writeAll("/" ++ remove_key_word); │ │1322 + break :blk out.written(); │ │1323 + }, │ │1324 .notifications => if (tab.start == 0) │ │1325 try arena.allocator().print("{s}/user/notifications", .{prefix}) │ │1326 else │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1525 } │ │1526 │ │1527 // the tail after a page's "user": none or "notifications" with an optional │ │1508 - // "start:<n>", "settings", "keys", or "new" │ │1528 + // "start:<n>", "settings", "keys" with an optional "key:<fingerprint>" and │ │1529 + // removal confirmation, or "new" │ │1530 fn userTail(segments: *std.mem.SplitIterator(u8, .scalar)) ?union(enum) { new, tab: UserTab } {│ │1531 const word = segments.next() orelse return .{ .tab = .{} }; │ │1532 if (std.mem.eql(u8, word, "new")) return if (segments.next() == null) .new else null; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1533 return switch (std.meta.stringToEnum(UserView, word) orelse return null) { │ │1513 - inline .settings, .keys => |view| if (segments.next() == null) .{ .tab = .{ .view = view } } else null, │ │1534 + .settings => if (segments.next() == null) .{ .tab = .{ .view = .settings } } else null, │ │1535 + .keys => .{ .tab = keysTab(segments) orelse return null }, │ │1536 .notifications => .{ .tab = .{ .start = listStart(segments) orelse return null } }, │ │1537 }; │ │1538 + } │ │1539 + │ │1540 + fn keysTab(segments: *std.mem.SplitIterator(u8, .scalar)) ?UserTab { │ │1541 + var params = Params{}; │ │1542 + const word = params.scanTail(segments) catch return null; │ │1543 + if (!params.only(&.{.key})) return null; │ │1544 + var tab: UserTab = .{ .view = .keys }; │ │1545 + if (params.values.get(.key)) |encoded| { │ │1546 + var buf: [3 * evt.User.fingerprint_len]u8 = undefined; │ │1547 + if (encoded.len > buf.len) return null; │ │1548 + @memcpy(buf[0..encoded.len], encoded); │ │1549 + tab.key = Array(evt.User.fingerprint_len).from(std.Uri.percentDecodeInPlace(buf[0..encoded.len])) orelse return null;│ │1550 + } │ │1551 + if (word) |value| { │ │1552 + if (!std.mem.eql(u8, value, remove_key_word) or tab.key.len == 0) return null; │ │1553 + tab.remove_key = true; │ │1554 + } │ │1555 + return tab; │ │1556 } │ │1557 │ │1558 fn listStart(segments: *std.mem.SplitIterator(u8, .scalar)) ?usize { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1589 } │ │1590 if (std.mem.eql(u8, word, "roles")) { │ │1591 _ = segments.next(); │ │1552 - const roles_params = listParams(&segments, &.{ .user, .start }) orelse return null; │ │1553 - var route = repoRolesRoute(pair, roles_params.start() orelse return null) orelse return null; │ │1554 - if (roles_params.values.get(.user)) |user| route.repo_roles.user = Array(evt.User.name_max_len).from(user) orelse return null;│ │1592 + const confirm_word = params.scanTail(&segments) catch return null; │ │1593 + if (!params.only(&.{ .user, .start })) return null; │ │1594 + var route = repoRolesRoute(pair, params.start() orelse return null) orelse return null; │ │1595 + if (params.values.get(.user)) |user| route.repo_roles.user = Array(evt.User.name_max_len).from(user) orelse return null; │ │1596 + if (confirm_word) |value| { │ │1597 + // only taking a role away asks first, and it names whose │ │1598 + const action = std.meta.stringToEnum(Repo.Roles.Action, value) orelse return null; │ │1599 + if ((action != .revoke and action != .demote) or route.repo_roles.user.len == 0) return null; │ │1600 + route.repo_roles.confirm = action; │ │1601 + } │ │1602 return route; │ │1603 } │ │1604 return if (isNewTail(&segments)) .{ .repo_repo_new = Array(repo_route_max_len).from(pair) orelse return null } else null; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1855 if (std.meta.activeTag(a) != std.meta.activeTag(b)) return false; │ │1856 return switch (a) { │ │1857 .home_users => |a_u| a_u.start == b.home_users.start and std.mem.eql(u8, a_u.search.slice(), b.home_users.search.slice()), │ │1811 - .home_user => |a_t| std.meta.eql(a_t, b.home_user), │ │1858 + .home_user => |a_t| a_t.eql(b.home_user), │ │1859 .user_repos => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user_repos.name.slice()) and a_u.start == b.user_repos.start and std.mem.eql(u8, a_u.search.slice(), b.user_repos.search.slice()),│ │1860 .user_forks => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user_forks.name.slice()) and a_u.start == b.user_forks.start, │ │1861 .user_repo_new => |a_name| std.mem.eql(u8, a_name.slice(), b.user_repo_new.slice()), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1862 .user_user_new => |a_name| std.mem.eql(u8, a_name.slice(), b.user_user_new.slice()), │ │1816 - .user_user => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user_user.name.slice()) and std.meta.eql(a_u.tab, b.user_user.tab),│ │1863 + .user_user => |a_u| std.mem.eql(u8, a_u.name.slice(), b.user_user.name.slice()) and a_u.tab.eql(b.user_user.tab), │ │1864 .repo_files => |a_f| std.mem.eql(u8, a_f.name.slice(), b.repo_files.name.slice()) and │ │1865 a_f.ref_kind == b.repo_files.ref_kind and │ │1866 std.mem.eql(u8, a_f.ref_value.slice(), b.repo_files.ref_value.slice()) and │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1916 .repo_settings => |a_name| std.mem.eql(u8, a_name.slice(), b.repo_settings.slice()), │ │1917 .repo_roles => |a_r| std.mem.eql(u8, a_r.name.slice(), b.repo_roles.name.slice()) and │ │1918 a_r.start == b.repo_roles.start and │ │1872 - std.mem.eql(u8, a_r.user.slice(), b.repo_roles.user.slice()), │ │1873 - .repo_user => |a_u| std.mem.eql(u8, a_u.name.slice(), b.repo_user.name.slice()) and std.meta.eql(a_u.tab, b.repo_user.tab), │ │1919 + std.mem.eql(u8, a_r.user.slice(), b.repo_roles.user.slice()) and │ │1920 + a_r.confirm == b.repo_roles.confirm, │ │1921 + .repo_user => |a_u| std.mem.eql(u8, a_u.name.slice(), b.repo_user.name.slice()) and a_u.tab.eql(b.repo_user.tab), │ │1922 .fork_patch => |a_f| std.mem.eql(u8, a_f.name.slice(), b.fork_patch.name.slice()) and std.mem.eql(u8, a_f.id.slice(), b.fork_patch.id.slice()),│ │1923 .fork_diff => |a_d| std.mem.eql(u8, a_d.fork.name.slice(), b.fork_diff.fork.name.slice()) and │ │1924 std.mem.eql(u8, a_d.fork.id.slice(), b.fork_diff.fork.id.slice()) and │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1939 .fork_user_new => |a_f| std.mem.eql(u8, a_f.name.slice(), b.fork_user_new.name.slice()) and std.mem.eql(u8, a_f.id.slice(), b.fork_user_new.id.slice()),│ │1940 .fork_user => |a_u| std.mem.eql(u8, a_u.fork.name.slice(), b.fork_user.fork.name.slice()) and │ │1941 std.mem.eql(u8, a_u.fork.id.slice(), b.fork_user.fork.id.slice()) and │ │1894 - std.meta.eql(a_u.tab, b.fork_user.tab), │ │1942 + a_u.tab.eql(b.fork_user.tab), │ │1943 else => true, │ │1944 }; │ │1945 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────╮ │212 .repo_events => |*e| e.selected.slice(), │ │213 else => "", │ │214 }; │ │215 - const roles_start: usize = switch (route) { │ │216 - .repo_roles => |r| r.start, │ │217 - else => 0, │ │215 + const roles_route: ui.RoutablePage.RepoRolesRoute = switch (route) {│ │216 + .repo_roles => |r| r, │ │217 + else => .{ .name = .{} }, │ │218 }; │ │219 // the thread the route shows, whose notif viewing it reads │ │220 const viewed_thread: []const u8 = switch (route) { │ ╰─────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯