╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Discussions.zig ──────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
105 admin_moment: ?evt.AdminDB.HashMap(.read_only), 106 identity: []const u8, 107 tag: []const u8, 108 selected_id: []const u8, 109 comment_id: []const u8, 110 comments_start: usize, 111 view: ui.RoutablePage.DiscussionsView, 112 ) !Self { 113 const empty = try emptyResult(arena.allocator(), identity, tag, selected_id, comment_id, comments_start, view); 114 const aa = arena.allocator(); 115 const DB = evt.EventDB(repo_opts.hash); 116 const rooted = empty.selected_id.len != 0; 117 const tagged = empty.tag.len != 0; 118 const strict = rooted or tagged; 119 120 const gpa = arena.child_allocator; 121 var event_db_maybe: ?evt.LocalEventDB(repo_opts.hash) = if (repo_kind == .git) try evt.LocalEventDB(repo_opts.hash).openReadOnly(io, gpa, repo.core.repo_dir) else null; 122 defer if (event_db_maybe) |*event_db| event_db.deinit(io, gpa); 123 const haxy_moment = (if (event_db_maybe) |*event_db| 124 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 125 else if (repo_kind == .git) 126 return empty 127 else 128 evt.currentMoment(repo_opts, repo)) catch { 129 if (strict) return error.NotFound; 130 return empty; 131 }; 132 133 const set_maybe: ?DB.SortedSet(.read_only) = if (tagged) blk: { 134 const tags_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.tag_to_id_set_key)) orelse return error.NotFound; 135 const tag_sets = try DB.SortedMap(.read_only).init(tags_cursor); 136 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, empty.tag)); 137 const cursor = try tag_sets.getCursor(decoded) orelse return error.NotFound; 138 break :blk try DB.SortedSet(.read_only).init(cursor); 139 } else if (try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.active_id_set_key))) |cursor| 140 try DB.SortedSet(.read_only).init(cursor) 141 else 142 null; 143 144 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.record_map_key)) orelse { 145 if (strict) return error.NotFound; 146 return empty; 147 }; 148 const records = try DB.HashMap(.read_only).init(records_cursor); 149 150 var root_key: ?[]const u8 = null; 151 if (rooted) { 152 const id = evt.parseEventId(empty.selected_id) catch return error.NotFound; 153 const record_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &id)) orelse return error.NotFound; 154 const record = try evt.read(evt.Discussion.Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(record_cursor)); 155 if (record.removed) return error.NotFound; 156 const activity_order = try evt.Discussion.activityOrder(DB, repo_opts.hash, haxy_moment, &id); 157 const order_key = try aa.dupe(u8, &evt.orderKeyDesc(activity_order, &id)); 158 const set = set_maybe orelse return error.NotFound; 159 if (!try set.contains(order_key)) return error.NotFound; 160 root_key = order_key; 161 } 162 163 const discussion_comments_start = if (empty.comment_id.len == 0) comments_start else 0; 164 const loaded_window = try loadWindow(repo_opts.hash, arena, admin_moment, haxy_moment, records, set_maybe, root_key, empty.selected_id, discussion_comments_start); 165 const comment_page = if (empty.comment_id.len == 0) 166 null 167 else 168 try Comment.init(repo_opts.hash, arena, admin_moment, haxy_moment, empty.selected_id, empty.comment_id, comments_start); 169 170 var tag_names: std.ArrayList([]const u8) = .empty; 171 if (try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.tag_to_id_set_key))) |tags_cursor| { 172 const tag_sets = try DB.SortedMap(.read_only).init(tags_cursor); 173 var tag_iter = try tag_sets.iterator(); 174 while (try tag_iter.next()) |entry_cursor| { 175 if (tag_names.items.len == max_tags) break; 176 var entry = entry_cursor; 177 const pair = try entry.readKeyValuePair(); 178 try tag_names.append(aa, try pair.key_cursor.readBytesAlloc(aa, null)); 179 } 180 } 181 182 return .{ 183 .identity = empty.identity, 184 .tag = empty.tag, 185 .selected_id = empty.selected_id, 186 .comment_id = empty.comment_id, 187 .comments_start = comments_start, 188 .comment_page = comment_page, 189 .recent = loaded_window, 190 .view = empty.view, 191 .description_page = empty.description_page, 192 .tags = tag_names.items, 193 }; 194 } 195 196 fn loadWindow( 197 comptime hash_kind: hash.HashKind, 198 arena: *std.heap.ArenaAllocator, 199 admin_moment: ?evt.AdminDB.HashMap(.read_only), 200 haxy_moment: evt.EventDB(hash_kind).HashMap(.read_only), 201 records: evt.EventDB(hash_kind).HashMap(.read_only), 202 set_maybe: ?evt.EventDB(hash_kind).SortedSet(.read_only), 203 root_key: ?[]const u8, 204 selected_id: []const u8, 205 comments_start: usize, 206 ) !Window { 207 const set = set_maybe orelse return .empty; 208 const DB = evt.EventDB(hash_kind); 209 const aa = arena.allocator(); 210 211 var prev_id: ?[]const u8 = null; 212 var iter = if (root_key) |key| blk: { 213 const rank = try set.rank(key); 214 if (rank > 0 and rank <= page_size) { 215 prev_id = ""; 216 } else if (rank > page_size) { 217 const pair = try set.getIndexKeyValuePair(@intCast(rank - page_size)) orelse return error.NotFound; 218 var order_key: [@sizeOf(u64) + evt.event_id_size]u8 = undefined; 219 _ = try pair.key_cursor.readBytes(&order_key); 220 const hex = std.fmt.bytesToHex(order_key[@sizeOf(u64)..].*, .lower); 221 prev_id = try aa.dupe(u8, &hex); 222 } 223 break :blk try set.iteratorFrom(key); 224 } else try set.iteratorFromIndex(0); 225 226 var discussions: std.ArrayList(DiscussionWithId) = .empty; 227 var next_id: ?[]const u8 = null; 228 while (try iter.next()) |cursor| { 229 const id = try evt.readOrderKeyId(DB, cursor); 230 const id_hex = std.fmt.bytesToHex(id, .lower); 231 if (discussions.items.len == page_size) { 232 next_id = try aa.dupe(u8, &id_hex); 233 break; 234 } 235 const record_cursor = try records.getCursor(hash.hashInt(hash_kind, &id)) orelse continue; 236 const record = try evt.read(evt.Discussion.Record, DB, hash_kind, arena, try DB.HashMap(.read_only).init(record_cursor)); 237 try discussions.append(aa, .{ 238 .id = try aa.dupe(u8, &id_hex), 239 .record = record, 240 .author = try ui.Author.initFromEmail(admin_moment, arena, record.author_email), 241 .comments = try Comment.loadWindow( 242 hash_kind, 243 arena, 244 admin_moment, 245 haxy_moment, 246 evt.Comment.thread_id_to_comment_id_set_key, 247 &id_hex, 248 if (std.mem.eql(u8, &id_hex, selected_id)) comments_start else 0, 249 ), 250 .attachments = try Attachment.load(hash_kind, arena, haxy_moment, &id_hex), 251 }); 252 } 253 254 return .{ 255 .items = discussions.items, 256 .prev_id = prev_id, 257 .next_id = next_id, 258 .count = @intCast(try set.count()), 259 }; 260 } 261 pub const View = thread.View(.discuss, Self); 262 pub const Detail = thread.Detail(.discuss, Self); 263 pub const detail_widget_name = "repo_discussion_detail"; 264 265 pub const Header = thread.Header; 266 267 // tabs switching between the discussions page's views 268 pub fn initHeader(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { 269 var header = try Header.init(allocator); 270 errdefer header.deinit(allocator); 271 const aa = session.page_arena.allocator(); 272 const selected_index = View.viewIndex(data.view); 273 const page_selected = std.meta.activeTag(session.data.current_page) == .repo_discussions; 274 275 // recent discussions 276 { 277 const route = ui.RoutablePage.repoDiscussionsRoute(data.identity, data.tag, "") orelse return error.RouteTooLong; 278 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == 0); 279 var label_buf: [64]u8 = undefined; 280 const label = try std.fmt.bufPrint(&label_buf, "recent ({d})", .{data.recent.count}); 281 try header.addTab(allocator, label, link, 0); 282 } 283 284 // tags tab, labeled with the active tag filter 285 { 286 const tags_route = ui.RoutablePage.repoThreadTagsRoute(.discuss, data.identity, data.tag) orelse return error.RouteTooLong; 287 const tags_link = try ui.inPageTabLink(session, tags_route, page_selected and selected_index == View.viewIndex(.tags)); 288 const label = if (data.tag.len == 0) "tags" else blk: { 289 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); 290 break :blk try std.fmt.allocPrint(aa, "tags ({s})", .{decoded}); 291 }; 292 try header.addTab(allocator, label, tags_link, View.viewIndex(.tags)); 293 } 294 295 // new-discussion tab; an edit or comment url shows its tab in this place 296 { 297 const route = switch (data.view) { 298 .edit => ui.RoutablePage.repoThreadEditRoute(.discuss, data.identity, data.selected_id) orelse return error.RouteTooLong, 299 .new_comment => ui.RoutablePage.repoThreadCommentNewRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 300 .edit_comment => ui.RoutablePage.repoThreadCommentEditRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 301 .remove => ui.RoutablePage.repoThreadRemoveRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 302 else => ui.RoutablePage.repoThreadNewRoute(.discuss, data.identity) orelse return error.RouteTooLong, 303 }; 304 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == View.viewIndex(.new)); 305 const label: []const u8 = switch (data.view) { 306 .edit => "edit", 307 .new_comment => "reply", 308 .edit_comment => "edit", 309 .remove => "remove", 310 else => "new", 311 }; 312 try header.addTab(allocator, label, link, View.viewIndex(.new)); 313 } 314 315 header.select(View.viewIndex(data.view)); 316 return header; 317 } 318