╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────────────╮ │src/ui/UserSettings.zig│ ╰───────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │216 │ │217 evt.updateUser(io, allocator, users_dir, admin_repo, &user_id, email, current_password, new_password, new_password_again) catch |err| switch (err) {│ │218 // the account was removed under them │ │259 - error.NotFound => return self.logOut(), │ │219 + error.NotFound => return ui.UserNotifications.logOut(self.session), │ │220 else => { │ │221 const failure = ui.Session.FormFeedback.UserFailure.fromError(err) orelse return err; │ │222 const aa = self.session.arena.allocator(); │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────╮ │229 self.session.haxy_moment = try evt.currentMoment(evt.admin_repo_opts, admin_repo);│ │230 try self.session.loadUser(); │ │231 try self.session.navigate(self.session.data.current_page); │ │272 - } │ │273 - │ │274 - fn headerActive(self: *View) bool { │ │275 - return self.box.getFocus().child_id == self.box.children.keys()[header_index]; │ │232 } │ │233 │ │234 fn formScroll(self: *View) *wgt.Scroll(ui.Widget) { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────╮ │251 return self.box.getFocus(); │ │252 } │ │253 │ │298 - // up leaves the view from the logout button │ │299 - pub fn atTop(self: *View) bool { │ │300 - return self.headerActive(); │ │254 + // up leaves the view from the first control │ │255 + pub fn atTop(self: *View, root_focus: *Focus) bool { │ │256 + return root_focus.grandchild_id == self.control_ids[0];│ │257 } │ │258 }; │ │259 │ ╰────────────────────────────────────────────────────────────────────╯ ╭ changed ────────╮ │src/ui/widget.zig│ ╰─────────────────╯ ╭─────────────────────────────────────────────────────────────────────────╮ │76 new_user: ui.NewUser.View, │ │77 user_login: ui.UserLogin.View, │ │78 user_settings: ui.UserSettings.View, │ │79 + user_notifications: ui.UserNotifications.View, │ │80 footer: Footer, │ │81 │ │82 pub fn deinit(self: *Widget, allocator: std.mem.Allocator) void {│ ╰─────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │118 // whether moving up should return to the page header │ │119 pub fn atTop(self: *Widget, root_focus: *Focus) bool { │ │120 return switch (self.*) { │ │120 - inline .home_about, .home_users, .user_repos, .flow_box_scroll, .new_repo, .new_user, .user_login, .user_settings, .quit, .repo_files, .repo_commits, .diff_view, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_settings, .repo_settings_form, .repo_roles => |*view| view.atTop(), │ │121 - inline .repo_patch_detail => |*view| view.atTop(root_focus), │ │121 + inline .home_about, .home_users, .user_repos, .flow_box_scroll, .new_repo, .new_user, .user_login, .user_notifications, .quit, .repo_files, .repo_commits, .diff_view, .repo_refs, .repo_issues, .repo_patches, .repo_discussions, .repo_events, .repo_undo, .repo_settings, .repo_settings_form, .repo_roles => |*view| view.atTop(),│ │122 + inline .repo_patch_detail, .user_settings => |*view| view.atTop(root_focus), │ │123 else => false, │ │124 }; │ │125 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │383 │ │384 // one flow item: its display text plus an optional link and bottom label. a │ │385 // non-empty `link` sets the item's focus kind such as `.{ .custom = "a:/user/foo" }`, │ │385 - // which the web renderer turns into an anchor. │ │386 - pub const Item = struct { text: []const u8, link: []const u8 = "", bottom_label: []const u8 = "" }; │ │386 + // which the web renderer turns into an anchor. `bold` covers the text and the label. │ │387 + pub const Item = struct { text: []const u8, link: []const u8 = "", bottom_label: []const u8 = "", bold: bool = false };│ │388 │ │389 // the text box copies the item text; links and bottom labels are copied into │ │390 // the arena, so the caller's slices needn't outlive this call. │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │401 self.focus.child_id = null; │ │402 │ │403 for (items) |item| { │ │403 - var text_box = try wgt.TextBox.init(allocator, item.text, .{ .border = .hidden, .round_corners = true, .wrap_kind = .word, .bottom_label = .{ .text = try aa.dupe(u8, item.bottom_label) } }); │ │404 + const style: Grid.Style = .{ .bold = item.bold }; │ │405 + var text_box = try wgt.TextBox.init(allocator, item.text, .{ .border = .hidden, .round_corners = true, .wrap_kind = .word, .bottom_label = .{ .text = try aa.dupe(u8, item.bottom_label), .style = style }, .style = style });│ │406 errdefer text_box.deinit(allocator); │ │407 text_box.getFocus().mode = .all; │ │408 │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭──────────────────────────────────────────────╮ │2 const builtin = @import("builtin"); │ │3 const pch = @import("../../patch.zig"); │ │4 const evt = @import("../../event.zig"); │ │5 +const notify = @import("../../notify.zig");│ │6 const ui = @import("../../ui.zig"); │ │7 const widget = @import("../widget.zig"); │ │8 const xit = @import("xit"); │ ╰──────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │932 try addToolButton(allocator, row, label, "", action); │ │933 } │ │934 │ │934 - if (supports_conflicts and entryConflicted(entry)) { │ │935 + const conflicted = supports_conflicts and entryConflicted(entry); │ │936 + if (conflicted) { │ │937 const route = resolveRoute(self.data.identity, entry.id, "") orelse return error.RouteTooLong; │ │938 try addToolButton(allocator, row, "resolve conflict", "", try pa.print("a:{s}", .{try route.toUrl(self.session.page_arena)})); │ │937 - } else { │ │938 - if (has_status) { │ │939 - if (statusChange(entryStatus(entry))) |change| { │ │940 - const route = ui.RoutablePage.repoThreadCommentsRoute(kind, self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │941 - row.getFocus().kind = .{ .custom = try pa.print("form:{s}/{s}", .{ try route.toUrl(self.session.page_arena), change.action }) };│ │942 - try addToolButton(allocator, row, change.action, "", "submit"); │ │943 - } │ │939 + } else if (has_status) { │ │940 + if (statusChange(entryStatus(entry))) |change| { │ │941 + const route = ui.RoutablePage.repoThreadCommentsRoute(kind, self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │942 + row.getFocus().kind = .{ .custom = try pa.print("form:{s}/{s}", .{ try route.toUrl(self.session.page_arena), change.action }) }; │ │943 + try addToolButton(allocator, row, change.action, "", "submit"); │ │944 } │ │945 + } │ │946 │ │947 + if (entry.notify_action) |mode| { │ │948 + const route = ui.RoutablePage.repoThreadCommentsRoute(kind, self.data.identity, entry.id, 0) orelse return error.RouteTooLong; │ │949 + const action = try pa.print("{s}/{s}", .{ try route.toUrl(self.session.page_arena), @tagName(mode) }); │ │950 + // a status change already made the row a form, which the button's own action overrides │ │951 + if (row.getFocus().kind != .custom) row.getFocus().kind = .{ .custom = try pa.print("form:{s}", .{action}) }; │ │952 + try addToolButton(allocator, row, @tagName(mode), "", try pa.print("{s}{s}", .{ ui.submit_action_prefix, action })); │ │953 + } │ │954 + │ │955 + if (!conflicted) { │ │956 const route = ui.RoutablePage.repoThreadEditRoute(kind, self.data.identity, entry.id) orelse return error.RouteTooLong; │ │957 try addToolButton(allocator, row, "edit", "", try pa.print("a:{s}", .{try route.toUrl(self.session.page_arena)})); │ │958 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1204 const row = self.toolRow(); │ │1205 const child_id = row.getFocus().child_id orelse return; │ │1206 const cur = row.children.getIndex(child_id) orelse return; │ │1197 - const on_primary = switch (row.children.values()[cur].widget.getFocus().kind) { │ │1198 - .custom => |action| std.mem.eql(u8, action, "submit"), │ │1199 - else => false, │ │1207 + const action: []const u8 = switch (row.children.values()[cur].widget.getFocus().kind) {│ │1208 + .custom => |custom| custom, │ │1209 + else => "", │ │1210 }; │ │1211 + const on_primary = std.mem.eql(u8, action, "submit"); │ │1212 + // the notify button is the row's only one with its own action │ │1213 + const on_notify = std.mem.startsWith(u8, action, ui.submit_action_prefix); │ │1214 switch (key) { │ │1215 .arrow_left => { │ │1216 var previous = cur; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯