╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ──────╮ │src/ui/Repo.zig│ ╰───────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │265 // a server shows events to whoever it shows undo to, so the route follows │ │266 if (route == .repo_events and session.data.host_kind != .local and !undo_allowed) return error.NotFound;│ │267 const undo_index = if (route == .repo_undo) route.repo_undo.index else null; │ │264 - var undo_data: ?Undo = null; │ │268 + // an unreadable repo still shows the tab, like every other tab │ │269 + var undo_data: ?Undo = if (undo_allowed) .{ .identity = repo_identity.identity } else null; │ │270 const files, const changes, const refs, var issues, var patches, var discussions, const events = blk: { │ │271 read: { │ │272 const io = session.io orelse break :read; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │283 try evt.consume(.local, .repo, repo_kind, opened.self_repo_opts, io, gpa, opened, evt.events_ref, &.{}); │ │284 try pch.refreshBranches(.local, repo_kind, opened.self_repo_opts, io, gpa, opened, null, null); │ │285 } │ │281 - if (repo_kind == .xit and undo_allowed) undo_data = try Undo.init(opened.self_repo_opts, arena, opened, repo_identity.identity, undo_index);│ │286 + // tabs switch in-page, so every tab's data is read here │ │287 + if (repo_kind == .xit and undo_allowed) { │ │288 + undo_data = Undo.init(opened.self_repo_opts, arena, opened, repo_identity.identity, undo_index) catch |err| switch (err) { │ │289 + error.OutOfMemory => return err, │ │290 + else => .{ .identity = repo_identity.identity, .failure = @errorName(err) }, │ │291 + }; │ │292 + } │ │293 const files_data = if (patchrev_id.len != 0) │ │294 try Files.initPatchRev(repo_kind, opened.self_repo_opts, arena, opened, io, gpa, location, patchrev_id, files_dir, files_line) │ │295 else │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │308 try Issues.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, issues_tag, issues_search, issues_selected, issues_comment, issues_comments_start, issues_theirs, issues_view), │ │309 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_tag, patches_search, patches_selected, patches_comment, patches_comments_start, patches_theirs, patches_view),│ │310 try Discussions.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, discussions_tag, discussions_search, discussions_selected, discussions_comment, discussions_comments_start, discussions_view), │ │300 - try Events.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, events_view, events_kind, events_selected, session.local != null, session.data.sync_failure), │ │311 + try Events.init(repo_kind, opened.self_repo_opts, arena, opened, io, session.haxy_moment, repo_identity.identity, events_view, events_kind, events_selected, events_moment, session.local != null, session.data.sync_failure), │ │312 }; │ │313 }, │ │314 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │326 try Events.empty(aa, repo_identity.identity, events_view, session.local != null, session.data.sync_failure),│ │327 }; │ │328 }; │ │329 + if (undo_data) |*undo| undo.can_undo = session.local != null or evt.Repo.roleOf(repo, session.userId()) == .owner; │ │330 if (route == .repo_undo and undo_data == null) return error.NotFound; │ │331 issues.repo_source = source; │ │332 patches.repo_source = source; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ changed ─────────────╮ │src/ui/Repo/Events.zig│ ╰──────────────────────╯ ╭───────────────────────────────────────────────────────────────────────╮ │43 active: Window, │ │44 removed: Window, │ │45 header: Header, │ │46 +// set when the list is limited to the events one transaction wrote│ │47 +moment: ?u64 = null, │ │48 │ │49 const Self = @This(); │ │50 │ ╰───────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────╮ │59 requested_view: ui.RoutablePage.EventsView,│ │60 selected_kind: ?evt.EventKind, │ │61 selected: []const u8, │ │62 + moment_index: ?u64, │ │63 local: bool, │ │64 failure: ?[]const u8, │ │65 ) !Self { │ ╰───────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ │66 const aa = arena.allocator(); │ │67 var result = try empty(aa, identity, requested_view, local, failure); │ │68 + result.moment = moment_index; │ │69 if (local) result.header = try Header.local(repo_kind, repo_opts, aa, repo, io, failure);│ │70 const require_selected_event = selected_kind != null or selected.len != 0; │ │71 if ((selected_kind == null) != (selected.len == 0)) return error.NotFound; │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │107 root_key = evt.orderKeyDesc(meta.updated_order, &id); │ │108 } │ │109 │ │110 + if (moment_index) |index| { │ │111 + try readMomentEvents(repo_kind, repo_opts, arena, &result, haxy_moment, kind_map, admin_moment, identity, index, selected);│ │112 + return result; │ │113 + } │ │114 + │ │115 for ([_]ui.RoutablePage.EventsView{ .active, .removed }) |view| { │ │116 const id_set_cursor = switch (view) { │ │117 .active => active_cursor, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │144 event_window.events = events.items; │ │145 } │ │146 return result; │ │147 +} │ │148 + │ │149 +// the moment's set is keyed on the event order, not the update order the │ │150 +// active and removed sets use, so it gets its own walk. one transaction's │ │151 +// events are few, so the whole set is scanned to count both views. │ │152 +fn readMomentEvents( │ │153 + comptime repo_kind: rp.RepoKind, │ │154 + comptime repo_opts: rp.RepoOpts(repo_kind), │ │155 + arena: *std.heap.ArenaAllocator, │ │156 + result: *Self, │ │157 + haxy_moment: evt.EventDB(repo_opts.hash).HashMap(.read_only), │ │158 + kind_map: evt.EventDB(repo_opts.hash).HashMap(.read_only), │ │159 + admin_moment: ?evt.AdminDB.HashMap(.read_only), │ │160 + identity: []const u8, │ │161 + moment_index: u64, │ │162 + selected: []const u8, │ │163 +) !void { │ │164 + const DB = evt.EventDB(repo_opts.hash); │ │165 + const aa = arena.allocator(); │ │166 + const ids = try evt.momentEventIds(DB, repo_opts.hash, haxy_moment, moment_index) orelse return error.NotFound; │ │167 + │ │168 + var events: [2]std.ArrayList(Item) = .{ .empty, .empty }; │ │169 + // the selected event's window starts at it, not at the top │ │170 + var awaiting_selected = selected.len != 0; │ │171 + │ │172 + var iter = try ids.iterator(); │ │173 + while (try iter.next()) |cursor_value| { │ │174 + const id = try evt.readOrderKeyId(DB, cursor_value); │ │175 + const kind = (try evt.readEventKind(repo_opts.hash, kind_map, &id)) orelse continue; │ │176 + const meta = try readMeta(repo_opts.hash, arena, haxy_moment, kind, &id); │ │177 + const view: ui.RoutablePage.EventsView = if (meta.removed) .removed else .active; │ │178 + const event_window = switch (view) { │ │179 + .active => &result.active, │ │180 + .removed => &result.removed, │ │181 + }; │ │182 + event_window.count += 1; │ │183 + if (awaiting_selected and view == result.view) { │ │184 + if (!std.mem.eql(u8, selected, &std.fmt.bytesToHex(id, .lower))) continue; │ │185 + awaiting_selected = false; │ │186 + } │ │187 + const window_events = &events[@intFromEnum(view)]; │ │188 + if (window_events.items.len == page_size) { │ │189 + if (event_window.next == null) event_window.next = .{ .id = try formatId(aa, &id), .kind = kind }; │ │190 + continue; │ │191 + } │ │192 + const item = (try readItem(repo_opts.hash, arena, haxy_moment, kind_map, admin_moment, identity, view == .active, kind, &id)) orelse continue;│ │193 + try window_events.append(aa, item); │ │194 + } │ │195 + if (awaiting_selected) return error.NotFound; │ │196 + result.active.events = events[@intFromEnum(ui.RoutablePage.EventsView.active)].items; │ │197 + result.removed.events = events[@intFromEnum(ui.RoutablePage.EventsView.removed)].items; │ │198 } │ │199 │ │200 pub fn empty(aa: std.mem.Allocator, identity: []const u8, view: ui.RoutablePage.EventsView, local: bool, failure: ?[]const u8) !Self { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │392 errdefer list_box.deinit(allocator); │ │393 const event_window = data.window(view); │ │394 for (event_window.events) |event| { │ │335 - const route = ui.RoutablePage.repoEventsRoute(data.identity, view, event.kind, event.id) orelse return error.RouteTooLong; │ │395 + const route = ui.RoutablePage.repoEventsRoute(data.identity, view, event.kind, event.id, data.moment) orelse return error.RouteTooLong;│ │396 const link = try std.fmt.allocPrint(session.page_arena.allocator(), "ai:{s}", .{try route.toUrl(session.page_arena)}); │ │397 try addRow(allocator, &list_box, @tagName(event.kind), link); │ │398 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯