╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ─────╮ │src/notify.zig│ ╰──────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────╮ │266 thread_id: *const [evt.event_id_size]u8, │ │267 author: evt.CommitAuthor, │ │268 ) !void { │ │254 - try writeMode(io, allocator, users_dir, user_id, &location, thread_id, .mute, author); │ │269 + try writeMode(io, allocator, users_dir, user_id, &location, thread_id, .muted, author); │ │270 try dismiss(io, allocator, users_dir, user_id, &idOf(&location.repo_id, thread_id)); │ │256 - indexSubscriber(io, allocator, users_dir, location, thread_id, user_id, false); │ │271 } │ │272 │ │259 -// write the user's notify event for a thread │ │260 -fn writeMode( │ │273 +// write the user's notify event for a thread, then move them to its tier in the owner's index│ │274 +pub fn writeMode( │ │275 io: std.Io, │ │276 allocator: std.mem.Allocator, │ │277 users_dir: []const u8, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │281 mode: evt.Notify.Mode, │ │282 author: evt.CommitAuthor, │ │283 ) !void { │ │270 - var user_repo = (try evt.openUserRepo(io, allocator, users_dir, user_id)) orelse return error.NotFound; │ │271 - defer user_repo.deinit(io, allocator); │ │272 - try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &[_]evt.EventWithId{.{ │ │273 - .id = std.fmt.bytesToHex(evt.Notify.idOf(&location.repo_id, thread_id), .lower), │ │274 - .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │275 - .author = author, │ │276 - .event = .{ .notify = .{ │ │277 - .owner_id = std.fmt.bytesToHex(location.owner_id, .lower), │ │278 - .repo_id = std.fmt.bytesToHex(location.repo_id, .lower), │ │279 - .thread_id = std.fmt.bytesToHex(thread_id.*, .lower), │ │280 - .mode = mode, │ │281 - } }, │ │282 - }}); │ │284 + { │ │285 + var user_repo = (try evt.openUserRepo(io, allocator, users_dir, user_id)) orelse return error.NotFound; │ │286 + defer user_repo.deinit(io, allocator); │ │287 + try evt.consume(.{ .server = .{ .users_dir = users_dir } }, .user, .xit, evt.user_repo_opts, io, allocator, &user_repo, evt.events_ref, &[_]evt.EventWithId{.{│ │288 + .id = std.fmt.bytesToHex(evt.Notify.idOf(&location.repo_id, thread_id), .lower), │ │289 + .timestamp = @intCast(std.Io.Timestamp.now(io, .real).toSeconds()), │ │290 + .author = author, │ │291 + .event = .{ .notify = .{ │ │292 + .owner_id = std.fmt.bytesToHex(location.owner_id, .lower), │ │293 + .repo_id = std.fmt.bytesToHex(location.repo_id, .lower), │ │294 + .thread_id = std.fmt.bytesToHex(thread_id.*, .lower), │ │295 + .mode = mode, │ │296 + } }, │ │297 + }}); │ │298 + } │ │299 + indexMode(io, allocator, users_dir, location, thread_id, user_id, mode); │ │300 } │ │301 │ │285 -// add `user_id` to the thread's subscribers in its repo owner's index, or remove │ │286 -// it, logging rather than returning a failure │ │287 -fn indexSubscriber( │ │302 +// move `user_id` to `mode`'s tier on the thread in its repo owner's index, or │ │303 +// out of every tier for a mute, logging rather than returning a failure │ │304 +fn indexMode( │ │305 io: std.Io, │ │306 allocator: std.mem.Allocator, │ │307 users_dir: []const u8, │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────╮ │291 - location: evt.RepoLocation, │ │308 + location: *const evt.RepoLocation, │ │309 thread_id: *const [evt.event_id_size]u8,│ │310 user_id: *const [evt.event_id_size]u8, │ │294 - subscribed: bool, │ │311 + mode: evt.Notify.Mode, │ │312 ) void { │ │313 const Ctx = struct { │ │314 notif_id: [evt.event_id_size]u8, │ ╰─────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────╮ │315 user_id: *const [evt.event_id_size]u8, │ │299 - subscribed: bool, │ │316 + mode: evt.Notify.Mode, │ │317 │ │318 pub fn run(ctx: @This(), cursor: *UserDB.Cursor(.read_write)) !void {│ │319 const moment = try UserDB.HashMap(.read_write).init(cursor.*); │ ╰──────────────────────────────────────────────────────────────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │303 - const sets = try UserDB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(hash_kind, subscriber_map_key))); │ │304 - const subscribers = try UserDB.SortedSet(.read_write).init(try sets.putCursor(hash.hashInt(hash_kind, &ctx.notif_id))); │ │305 - if (ctx.subscribed) { │ │306 - try subscribers.put(ctx.user_id); │ │307 - } else if (!try subscribers.remove(ctx.user_id)) return error.CancelTransaction; │ │320 + inline for (comptime std.enums.values(evt.Notify.Mode)) |tier_mode| { │ │321 + if (comptime tierOf(tier_mode)) |tier| { │ │322 + const sets = try UserDB.HashMap(.read_write).init(try moment.putCursor(hash.hashInt(hash_kind, tier.map_key))); │ │323 + const users = try UserDB.SortedSet(.read_write).init(try sets.putCursor(hash.hashInt(hash_kind, &ctx.notif_id)));│ │324 + if (tier_mode == ctx.mode) try users.put(ctx.user_id) else _ = try users.remove(ctx.user_id); │ │325 + } │ │326 + } │ │327 } │ │328 }; │ │329 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────╮ │330 transact(io, allocator, users_dir, &location.owner_id, Ctx{ │ │331 .notif_id = idOf(&location.repo_id, thread_id), │ │332 .user_id = user_id, │ │314 - .subscribed = subscribed, │ │333 + .mode = mode, │ │334 }) catch |err| { │ │316 - std.log.warn("failed to index thread subscriber: {s}", .{@errorName(err)}); │ │335 + std.log.warn("failed to index thread notify mode: {s}", .{@errorName(err)});│ │336 }; │ │337 } │ │338 │ ╰─────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │320 -// subscribe the users `events` show taking part in their threads, the threads' │ │321 -// authors, the commenters and the assignees, unless they hold a record on the │ │322 -// thread. logs rather than returning a failure. │ │339 +// put the users `events` show taking part in their threads in a tier, the │ │340 +// threads' authors and the assignees as involved and the commenters as │ │341 +// commented. logs rather than returning a failure. │ │342 pub fn subscribeParticipants(io: std.Io, allocator: std.mem.Allocator, server: evt.Host.Server, repo_path: []const u8, events: []const evt.EventWithId) void {│ │343 const admin_repo_path = server.admin_repo_path orelse return; │ │344 const location = evt.parseRepoPath(server.users_dir, repo_path) orelse return; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │355 defer arena.deinit(); │ │356 │ │357 for (events) |event| { │ │339 - const thread_hex: [evt.event_id_size * 2]u8, const email: []const u8 = switch (event.event) { │ │340 - inline .discuss, .issue, .patch => |thread| if (thread != null) .{ event.id, event.author.email } else continue, │ │341 - .comment => |comment_maybe| if (comment_maybe) |comment| .{ comment.thread_id, event.author.email } else continue, │ │342 - .assign => |assign_maybe| if (assign_maybe) |assign| .{ assign.thread_id, assign.email } else continue, │ │358 + const thread_hex: [evt.event_id_size * 2]u8, const email: []const u8, const mode: evt.Notify.Mode = switch (event.event) { │ │359 + inline .discuss, .issue, .patch => |thread| if (thread != null) .{ event.id, event.author.email, .involved } else continue, │ │360 + .comment => |comment_maybe| if (comment_maybe) |comment| .{ comment.thread_id, event.author.email, .commented } else continue,│ │361 + .assign => |assign_maybe| if (assign_maybe) |assign| .{ assign.thread_id, assign.email, .involved } else continue, │ │362 else => continue, │ │363 }; │ │364 const user_id = (try evt.User.readIdByEmail(evt.AdminDB, evt.admin_repo_opts.hash, admin_moment, email)) orelse continue; │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │365 const thread_id = try evt.parseEventId(&thread_hex); │ │347 - // a mute or an earlier subscribe stands │ │366 + // a mute stands, and a record only ever moves up to involved │ │367 { │ │368 var user_repo = (try evt.openUserRepo(io, allocator, users_dir, &user_id)) orelse continue;│ │369 defer user_repo.deinit(io, allocator); │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │370 if (try evt.userMoment(&user_repo)) |moment| { │ │352 - if (try evt.Notify.readMode(UserDB, hash_kind, moment, &arena, &location.repo_id, &thread_id) != null) continue;│ │371 + if (try evt.Notify.readMode(UserDB, hash_kind, moment, &arena, &location.repo_id, &thread_id)) |existing| { │ │372 + if (mode != .involved or existing == .involved or existing == .muted) continue; │ │373 + } │ │374 } │ │375 } │ │376 + // a full commented tier takes nobody new, so a later comment checks again │ │377 + if (mode == .commented) { │ │378 + var owner_repo = (try evt.openUserRepo(io, allocator, users_dir, &location.owner_id)) orelse return error.NotFound; │ │379 + defer owner_repo.deinit(io, allocator); │ │380 + if (try tierFull(try owner_repo.core.latestMoment(), &idOf(&location.repo_id, &thread_id), .commented)) continue; │ │381 + } │ │382 // the write's own author, which for an assignment is the assigner │ │356 - try subscribe(io, allocator, users_dir, &user_id, location, &thread_id, event.author); │ │383 + try writeMode(io, allocator, users_dir, &user_id, &location, &thread_id, mode, event.author); │ │384 } │ │385 } │ │386 │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭──────╮ │next →│ ╰──────╯