╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭───────────╮ │← all files│ ╰───────────╯ ╭───────────────────────────────────────────╮ │1 const std = @import("std"); │ │2 const evt = @import("../event.zig"); │ │3 const pch = @import("../patch.zig"); │ │4 +const notify = @import("../notify.zig");│ │5 const ui = @import("../ui.zig"); │ │6 const xit = @import("xit"); │ │7 const rp = xit.repo; │ ╰───────────────────────────────────────────╯ ╭────────────────────────────────╮ │ 8 +const hash = xit.hash; │ │ 9 const xitui = xit.xitui; │ │10 const wgt = xitui.widget; │ │11 const layout = xitui.layout;│ ╰────────────────────────────────╯ ╭─────────────────────────────────────────────╮ │42 events: Events, │ │43 undo: ?Undo = null, │ │44 settings: ?Settings = null, │ │45 +user_notifications: ui.UserNotifications,│ │46 quit: Quit, │ │47 │ │48 const Self = @This(); │ ╰─────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────╮ │231 const roles_start: usize = switch (route) { │ │232 .repo_roles => |r| r.start, │ │233 else => 0, │ │234 + }; │ │235 + // the thread the route shows, whose notif viewing it reads │ │236 + const viewed_thread: []const u8 = switch (route) { │ │237 + .repo_issues => |*i| if (i.view == .conflicts) "" else i.selected.slice(), │ │238 + .repo_patches => |*p| if (p.view == .conflicts) "" else p.selected.slice(),│ │239 + .repo_discussions => |*t| t.selected.slice(), │ │240 + else => "", │ │241 }; │ │242 │ │243 // where the on-disk repo lives (null keeps the views' empty fallback), plus │ ╰────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │367 break :diff .{ .diff = try Diff.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, diff_route.repo_diff) }; │ │368 } else .{ .commits = try Commits.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, session.haxy_moment, handle, ref_or_oid, ref_value, commits_message, base_oid, commits_search, commits_from) }; │ │369 const target_branch = if (files_data.ref_or_oid == .branch) files_data.ref_or_oid_value else ""; │ │370 + var discussions_data = try Discussions.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, discussions_label, discussions_search, discussions_selected, discussions_comment, discussions_comments_start, discussions_view, tabViewer(handle, repo, .discuss)); │ │371 + var issues_data = try Issues.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, issues_label, issues_assignee, issues_search, issues_selected, issues_comment, issues_comments_start, issues_theirs, issues_view, tabViewer(handle, repo, .issue)); │ │372 + var patches_data = try Patches.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, session, repo_id_maybe, repo_identity.identity, target_branch, patches_label, patches_search, patches_selected, patches_comment, patches_comments_start, patches_theirs, patches_view, tabViewer(handle, repo, .patch));│ │373 + if (repo_kind == .xit and session.data.host_kind == .server) { │ │374 + // a server page always resolved its repo id │ │375 + const repo_id = repo_id_maybe orelse unreachable; │ │376 + readNotify(opened.self_repo_opts, session, arena, opened, repo.event.user_id[0..evt.event_id_size], &repo_id, viewed_thread, &discussions_data, &issues_data, &patches_data) catch |err| { │ │377 + std.log.warn("failed to read thread notifications: {s}", .{@errorName(err)}); │ │378 + }; │ │379 + } │ │380 break :blk .{ │ │381 files_data, │ │382 changes_data, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │383 try Refs.init(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, repo_identity.identity, refs_kind, refs_from, refs_search), │ │364 - try Discussions.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, discussions_label, discussions_search, discussions_selected, discussions_comment, discussions_comments_start, discussions_view, tabViewer(handle, repo, .discuss)), │ │365 - try Issues.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, issues_label, issues_assignee, issues_search, issues_selected, issues_comment, issues_comments_start, issues_theirs, issues_view, tabViewer(handle, repo, .issue)), │ │366 - try Patches.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, session, repo_id_maybe, repo_identity.identity, target_branch, patches_label, patches_search, patches_selected, patches_comment, patches_comments_start, patches_theirs, patches_view, tabViewer(handle, repo, .patch)),│ │384 + discussions_data, │ │385 + issues_data, │ │386 + patches_data, │ │387 try Events.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, repo.event, events_view, events_kind, events_selected, events_moment, session.local != null, session.data.sync_failure), │ │388 }; │ │389 }, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │424 .events = events, │ │425 .undo = undo_data, │ │426 .settings = settings, │ │427 + // after the shown thread's notif is marked read │ │428 + .user_notifications = try ui.UserNotifications.init(arena, session, route.userTab()),│ │429 .quit = Quit.init(), │ │430 }; │ │431 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │432 + │ │433 +// set the viewer's notify button on every listed thread and mark the viewed thread's notif read │ │434 +fn readNotify( │ │435 + comptime repo_opts: rp.RepoOpts(.xit), │ │436 + session: *ui.Session, │ │437 + arena: *std.heap.ArenaAllocator, │ │438 + repo: *rp.Repo(.xit, repo_opts), │ │439 + owner_id: *const [evt.event_id_size]u8, │ │440 + repo_id: *const [evt.event_id_size]u8, │ │441 + viewed_thread: []const u8, │ │442 + discussions: *Discussions, │ │443 + issues: *Issues, │ │444 + patches: *Patches, │ │445 +) !void { │ │446 + const user_id = session.userId() orelse return; │ │447 + const io = session.io orelse return error.NoMoment; │ │448 + const users_dir = session.users_dir orelse return error.NoMoment; │ │449 + const gpa = arena.child_allocator; │ │450 │ │451 + { │ │452 + // the owner's plain data, which holds the subscriber index │ │453 + var owner_repo = (try evt.openUserRepo(io, gpa, users_dir, owner_id)) orelse return error.NotFound; │ │454 + defer owner_repo.deinit(io, gpa); │ │455 + const owner_moment = try notify.latestMoment(&owner_repo); │ │456 + │ │457 + try setNotifyActions(arena, &discussions.recent, owner_moment, repo_id, &user_id); │ │458 + for ([_]*Issues.Window{ &issues.open, &issues.closed, &issues.conflicts }) |window| try setNotifyActions(arena, window, owner_moment, repo_id, &user_id); │ │459 + for ([_]*Patches.Window{ &patches.open, &patches.closed, &patches.merged, &patches.conflicts }) |window| try setNotifyActions(arena, window, owner_moment, repo_id, &user_id);│ │460 + } │ │461 + │ │462 + if (viewed_thread.len == 0) return; │ │463 + var user_repo = (try evt.openUserRepo(io, gpa, users_dir, &user_id)) orelse return; │ │464 + defer user_repo.deinit(io, gpa); │ │465 + const latest = (try notify.latestMoment(&user_repo)) orelse return; │ │466 + const notif_id = notify.idOf(repo_id, &try evt.parseEventId(viewed_thread)); │ │467 + const item = (try notify.readItem(latest, arena, &notif_id)) orelse return; │ │468 + if (!item.unread) return; │ │469 + // the page reads this moment or a newer one, so a child in it is shown │ │470 + const moment = try evt.currentMoment(repo_opts, repo); │ │471 + const kinds_cursor = try moment.getCursor(hash.hashInt(repo_opts.hash, evt.event_index_key)) orelse return error.CursorNotFound; │ │472 + const kinds = try evt.EventDB(repo_opts.hash).HashMap(.read_only).init(kinds_cursor); │ │473 + // a child the page can't show leaves it unread │ │474 + if (try evt.readEventKind(repo_opts.hash, kinds, &item.notif.last_child_id) == null) return; │ │475 + try notify.markRead(io, gpa, users_dir, &user_id, &notif_id, &item.notif.last_child_id); │ │476 +} │ │477 + │ │478 +// set the viewer's notify button on each of a window's threads │ │479 +fn setNotifyActions( │ │480 + arena: *std.heap.ArenaAllocator, │ │481 + window: anytype, │ │482 + owner_moment: ?evt.UserDB.HashMap(.read_only), │ │483 + repo_id: *const [evt.event_id_size]u8, │ │484 + user_id: *const [evt.event_id_size]u8, │ │485 +) !void { │ │486 + const items = try arena.allocator().dupe(std.meta.Elem(@TypeOf(window.items)), window.items); │ │487 + for (items) |*entry| { │ │488 + const thread_id = try evt.parseEventId(entry.id); │ │489 + // muting drops the user from the index, so membership alone decides │ │490 + const subscribed = if (owner_moment) |moment| try notify.isSubscribed(moment, &notify.idOf(repo_id, &thread_id), user_id) else false; │ │491 + entry.notify_action = if (subscribed) .mute else .subscribe; │ │492 + } │ │493 + window.items = items; │ │494 +} │ │495 + │ │496 // the viewer a tab's comment bar sees, which is none below the tab's role │ │497 fn tabViewer(handle: ui.RepoHandle, repo: evt.Repo.Record, kind: evt.EventKind) ?ui.Viewer { │ │498 const viewer = handle.viewer orelse return null; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │605 } │ │606 │ │607 if (session.data.host_kind == .server) { │ │523 - var user_view = try ui.UserSettings.initView(allocator, session); │ │608 + const route = ui.RoutablePage{ .repo_user = .{ .name = ui.RoutablePage.Array(ui.RoutablePage.repo_route_max_len).from(identity) orelse return error.RouteTooLong } };│ │609 + var user_view = try ui.UserSettings.initView(allocator, session, &data.user_notifications, route); │ │610 errdefer user_view.deinit(allocator); │ │611 try stack.children.put(allocator, user_view.getFocus().id, user_view); │ │612 } │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │682 .repo_events => |*v| if (v.focusHeader(root_focus)) return, │ │683 .repo_undo => |*v| if (v.focusHeader(root_focus)) return, │ │684 .repo_settings => |*v| if (v.focusHeader(root_focus)) return, │ │685 + .user_notifications => |*v| if (v.focusHeader(root_focus)) return,│ │686 else => {}, │ │687 }; │ │688 index = stack_index; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────╯