╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1232 }, │ │1233 .arrow_up => self.exit = .header, │ │1234 .arrow_down => _ = self.moveVertical(root_focus, true), │ │1222 - .enter => if (on_primary) try self.primaryAction(allocator), │ │1223 - .mouse => |mouse| if (on_primary and inp.leftClickOn(root_focus, child_id, mouse)) try self.primaryAction(allocator),│ │1235 + .enter => if (on_primary) { │ │1236 + try self.primaryAction(allocator); │ │1237 + } else if (on_notify) try self.changeNotify(allocator), │ │1238 + .mouse => |mouse| if (inp.leftClickOn(root_focus, child_id, mouse)) { │ │1239 + if (on_primary) { │ │1240 + try self.primaryAction(allocator); │ │1241 + } else if (on_notify) try self.changeNotify(allocator); │ │1242 + }, │ │1243 else => {}, │ │1244 } │ │1245 } │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1680 }, │ │1681 } │ │1682 }, │ │1683 + } │ │1684 + try self.session.navigate(ui.RoutablePage.repoThreadCommentsRoute(kind, self.data.identity, entry.id, 0) orelse return error.RouteTooLong);│ │1685 + } │ │1686 + │ │1687 + // subscribe to the thread or mute it, as its button says │ │1688 + fn changeNotify(self: *This, allocator: std.mem.Allocator) !void { │ │1689 + if (comptime wasm) return; │ │1690 + const entry = self.entry orelse return; │ │1691 + const mode = entry.notify_action orelse return; │ │1692 + const io = self.session.io orelse return; │ │1693 + const users_dir = self.session.users_dir orelse return; │ │1694 + const source = self.data.repo_source orelse return; │ │1695 + const actor = (try self.session.authorize(self.data.identity, .read, null)) orelse return; │ │1696 + // a server actor always has both ids │ │1697 + const user_id = actor.user_id orelse unreachable; │ │1698 + const location: evt.RepoLocation = .{ │ │1699 + .owner_id = actor.repo_user_id orelse unreachable, │ │1700 + .repo_id = try evt.parseEventId(std.Io.Dir.path.basename(source.path)), │ │1701 + }; │ │1702 + const thread_id = try evt.parseEventId(entry.id); │ │1703 + switch (mode) { │ │1704 + .subscribe => try notify.subscribe(io, allocator, users_dir, &user_id, location, &thread_id, actor.author), │ │1705 + .mute => try notify.mute(io, allocator, users_dir, &user_id, location, &thread_id, actor.author), │ │1706 } │ │1707 try self.session.navigate(ui.RoutablePage.repoThreadCommentsRoute(kind, self.data.identity, entry.id, 0) orelse return error.RouteTooLong);│ │1708 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ─────────────────────╮ │src/ui/UserNotifications.zig│ ╰────────────────────────────╯ ╭ changed ──╮ │src/web.zig│ ╰───────────╯ ╭───────────────────────────────────────────╮ │ 8 const ui = @import("./ui.zig"); │ │ 9 const fork = @import("./fork.zig"); │ │10 const pch = @import("./patch.zig"); │ │11 +const notify = @import("./notify.zig");│ │12 const xitui = xit.xitui; │ │13 const wgt = xitui.widget; │ │14 const Focus = xitui.focus.Focus; │ ╰───────────────────────────────────────────╯ ╭────────────────────────────────────────────╮ │44 server: struct { │ │45 admin_repo_path: []const u8, │ │46 users_dir: []const u8, │ │47 + dirty_repos: *notify.DirtyRepos,│ │48 session_store: SessionStore, │ │49 git_http_port: ?u16, │ │50 git_ssh_port: ?u16, │ ╰────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │55 pub fn event(self: Host) evt.Host { │ │56 return switch (self) { │ │57 .local => .local, │ │56 - .server => |server| .{ .server = .{ .users_dir = server.users_dir } }, │ │58 + .server => |server| .{ .server = .{ .users_dir = server.users_dir, .dirty_repos = server.dirty_repos, .admin_repo_path = server.admin_repo_path } },│ │59 }; │ │60 } │ │61 }; │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │159 switch (host) { │ │160 .server => |server| { │ │161 // "repo/new" and "user/new" come before "new", which would claim them │ │160 - const PostRoute = enum { login, logout, @"repo/new", @"user/new", user, settings, grant, revoke, promote, demote, new, edit, remove, open, close, resolve, publish, merge, squash, attach, assign, unassign, undo, clear }; │ │162 + const PostRoute = enum { login, logout, @"repo/new", @"user/new", user, settings, grant, revoke, promote, demote, new, edit, remove, open, close, resolve, publish, merge, squash, attach, assign, unassign, subscribe, mute, undo, clear };│ │163 inline for (@typeInfo(PostRoute).@"enum".field_names) |name| { │ │164 const suffix = "/" ++ name; │ │165 if (std.mem.endsWith(u8, path, suffix)) { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │166 const base = path[0 .. path.len - suffix.len]; │ │167 return switch (@field(PostRoute, name)) { │ │168 .login => handleLogin(io, request, allocator, base, server.admin_repo_path, server.session_store), │ │167 - .logout => handleLogout(request, base, server.session_store), │ │169 + .logout => handleLogout(request, allocator, base, server.session_store), │ │170 .@"repo/new" => handleRepoNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store),│ │171 .@"user/new" => handleUserNew(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store),│ │170 - .user => handleUserSettings(io, request, allocator, path, server.admin_repo_path, server.users_dir, server.session_store), │ │172 + .user => handleUserSettings(io, request, allocator, base, server.admin_repo_path, server.users_dir, server.session_store), │ │173 .settings => handleRepoSettings(io, request, allocator, path, host), │ │174 .grant => handleRoles(io, request, allocator, base, host, .grant), │ │175 .revoke => handleRoles(io, request, allocator, base, host, .revoke), │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │189 .attach => handleAttach(io, request, allocator, base, host), │ │190 .assign => handleAssign(io, request, allocator, base, host), │ │191 .unassign => handleUnassign(io, request, allocator, base, host), │ │192 + .subscribe => handleNotify(io, request, allocator, base, host, server.users_dir, .subscribe),│ │193 + .mute => handleNotify(io, request, allocator, base, host, server.users_dir, .mute), │ │194 }; │ │195 } │ │196 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │394 const haxy_moment = try evt.currentMoment(evt.admin_repo_opts, &repo); │ │395 const result = try evt.User.verifyCredentials(evt.AdminDB, evt.admin_repo_opts.hash, haxy_moment, &arena, username, password);│ │396 │ │393 - // on success, return to the page the login came from (base, or "/" at the │ │394 - // root); on failure, stay on its user tab to surface the error. │ │395 - const success_location: []const u8 = if (base.len == 0) "/" else base; │ │396 - const failure_location = try arena.allocator().print("{s}/user", .{base}); │ │397 + // on success, return to the page whose user tab the login came from; on │ │398 + // failure, stay on that tab to surface the error. │ │399 + const success_location = try userTabPageRoot(&arena, base); │ │400 + const failure_location = base; │ │401 │ │402 switch (result) { │ │403 .success => |user_id| { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯