╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Discussions.zig ──────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭ find file ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ ╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╮ │ │ │http│ ssh │ │ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─╯
67 pub const Status = enum { recent }; 68 pub const ViewKind = ui.RoutablePage.DiscussionsView; 69 pub const thread_name = "discussion"; 70 pub const header_widget_name = "repo_discussions_header"; 71 72 pub fn listRoute(identity: []const u8, _: Status, selected: []const u8) ?ui.RoutablePage { 73 return ui.RoutablePage.repoDiscussionsRoute(identity, selected); 74 } 75 76 pub fn selectedThread(self: *const Self) ?*const DiscussionWithId { 77 if (self.selected_id.len == 0) return null; 78 for (self.recent.items) |*entry| { 79 if (std.mem.eql(u8, entry.id, self.selected_id)) return entry; 80 } 81 return null; 82 } 83 84 pub fn window(self: *const Self, _: Status) *const Window { 85 return &self.recent; 86 } 87 88 pub fn emptyResult( 89 aa: std.mem.Allocator, 90 identity: []const u8, 91 search: []const u8, 92 selected_id: []const u8, 93 comment_id: []const u8, 94 comments_start: usize, 95 view: ui.RoutablePage.DiscussionsView, 96 ) !Self { 97 return .{ 98 .identity = try aa.dupe(u8, identity), 99 .search = if (search.len == 0) null else std.Uri.percentDecodeInPlace(try aa.dupe(u8, search)), 100 .selected_id = try aa.dupe(u8, selected_id), 101 .comment_id = try aa.dupe(u8, comment_id), 102 .comments_start = comments_start, 103 .recent = .empty, 104 .view = if (view == .description) .recent else view, 105 .description_page = view == .description, 106 .labels = &.{}, 107 }; 108 } 109 110 pub fn init( 111 comptime repo_kind: rp.RepoKind, 112 comptime repo_opts: rp.RepoOpts(repo_kind), 113 arena: *std.heap.ArenaAllocator, 114 repo: *rp.Repo(repo_kind, repo_opts), 115 io: std.Io, 116 admin_moment: ?evt.AdminDB.HashMap(.read_only), 117 identity: []const u8, 118 search: []const u8, 119 selected_id: []const u8, 120 comment_id: []const u8, 121 comments_start: usize, 122 view: ui.RoutablePage.DiscussionsView, 123 viewer: ?ui.Viewer, 124 ) !Self { 125 var empty = try emptyResult(arena.allocator(), identity, search, selected_id, comment_id, comments_start, view); 126 // the early returns below still let the viewer write 127 empty.viewer = viewer; 128 const aa = arena.allocator(); 129 const DB = evt.EventDB(repo_opts.hash); 130 const rooted = empty.selected_id.len != 0; 131 132 const gpa = arena.child_allocator; 133 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; 134 defer if (event_db_maybe) |*event_db| event_db.deinit(io, gpa); 135 const haxy_moment = (if (event_db_maybe) |*event_db| 136 evt.currentMomentFromDb(repo_opts.hash, event_db.db) 137 else if (repo_kind == .git) 138 return empty 139 else 140 evt.currentMoment(repo_opts, repo)) catch { 141 if (rooted) return error.NotFound; 142 return empty; 143 }; 144 145 // the listing intersects every label's set, or is the active set without 146 // labels, since each label's set lies within it. discussions have no 147 // assignees, so an assignee term lists nothing. 148 const filters = try srch_thrd.parseFilters(aa, empty.search orelse ""); 149 const sets: []const DB.SortedSet(.read_only) = blk: { 150 if (filters.assignees.len != 0) break :blk &.{}; 151 var list: std.ArrayList(DB.SortedSet(.read_only)) = .empty; 152 if (filters.labels.len == 0) { 153 const active_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.active_id_set_key)) orelse break :blk &.{}; 154 try list.append(aa, try DB.SortedSet(.read_only).init(active_cursor)); 155 } else { 156 const labels_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.label_to_id_set_key)) orelse break :blk &.{}; 157 const label_sets = try DB.SortedMap(.read_only).init(labels_cursor); 158 for (filters.labels) |label| { 159 const cursor = try label_sets.getCursor(label) orelse break :blk &.{}; 160 try list.append(aa, try DB.SortedSet(.read_only).init(cursor)); 161 } 162 } 163 break :blk list.items; 164 }; 165 166 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.record_map_key)) orelse { 167 if (rooted) return error.NotFound; 168 return empty; 169 }; 170 const records = try DB.HashMap(.read_only).init(records_cursor); 171 172 var root_key: ?[]const u8 = null; 173 if (rooted) { 174 const id = evt.parseEventId(empty.selected_id) catch return error.NotFound; 175 const record_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &id)) orelse return error.NotFound; 176 const record = try evt.read(evt.Discussion.Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(record_cursor)); 177 if (record.removed) return error.NotFound; 178 const activity_order = try evt.Discussion.activityOrder(DB, repo_opts.hash, haxy_moment, &id); 179 const order_key = try aa.dupe(u8, &evt.orderKeyDesc(activity_order, &id)); 180 if (!try thread.inEverySet(DB, sets, order_key)) return error.NotFound; 181 root_key = order_key; 182 } 183 184 const discussion_comments_start = if (empty.comment_id.len == 0) comments_start else 0; 185 const loaded_window = try thread.loadWindow(Self, .discuss, repo_opts.hash, arena, admin_moment, haxy_moment, records, sets, root_key, null, empty.selected_id, discussion_comments_start, filters.words, viewer); 186 const comment_page = if (empty.comment_id.len == 0) 187 null 188 else 189 try Comment.init(repo_opts.hash, arena, admin_moment, haxy_moment, empty.selected_id, empty.comment_id, comments_start, viewer); 190 191 var label_names: std.ArrayList([]const u8) = .empty; 192 if (try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.label_to_id_set_key))) |labels_cursor| { 193 const label_sets = try DB.SortedMap(.read_only).init(labels_cursor); 194 var label_iter = try label_sets.iterator(); 195 while (try label_iter.next()) |entry_cursor| { 196 if (label_names.items.len == max_labels) break; 197 var entry = entry_cursor; 198 const pair = try entry.readKeyValuePair(); 199 try label_names.append(aa, try pair.key_cursor.readBytesAlloc(aa, null)); 200 } 201 } 202 203 return .{ 204 .identity = empty.identity, 205 .search = empty.search, 206 .selected_id = empty.selected_id, 207 .comment_id = empty.comment_id, 208 .comments_start = comments_start, 209 .comment_page = comment_page, 210 .viewer = viewer, 211 .recent = loaded_window, 212 .view = empty.view, 213 .description_page = empty.description_page, 214 .labels = label_names.items, 215 }; 216 } 217 218 pub const View = thread.View(.discuss, Self); 219 pub const Detail = thread.Detail(.discuss, Self); 220 pub const detail_widget_name = "repo_discussion_detail"; 221 222 pub const Header = thread.Header; 223 224 const recent_tab_label = "recent"; 225 const labels_tab_label = "labels"; 226 const edit_tab_label = "edit"; 227 const reply_tab_label = "reply"; 228 const remove_tab_label = "remove"; 229 const new_tab_label = "new"; 230 231 // tabs switching between the discussions page's views 232 pub fn initHeader(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { 233 var header = try Header.init(allocator, session, data.search, " search "); 234 errdefer header.deinit(allocator); 235 const selected_index = View.viewIndex(data.view); 236 const page_selected = std.meta.activeTag(session.data.current_page) == .repo_discussions; 237 238 // recent discussions 239 { 240 const route = try thread.searchRoute(ui.RoutablePage.repoDiscussionsRoute(data.identity, ""), data.search); 241 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == 0); 242 try header.addTab(allocator, recent_tab_label, try thread.countLabel(session, data.recent), link, 0); 243 } 244 245 // labels tab 246 { 247 const labels_route = try thread.searchRoute(ui.RoutablePage.repoThreadLabelsRoute(.discuss, data.identity), data.search); 248 const labels_link = try ui.inPageTabLink(session, labels_route, page_selected and selected_index == View.viewIndex(.labels)); 249 try header.addTab(allocator, labels_tab_label, "", labels_link, View.viewIndex(.labels)); 250 } 251 252 // new-discussion tab; an edit or comment url shows its tab in this place 253 { 254 const route = switch (data.view) { 255 .edit => ui.RoutablePage.repoThreadEditRoute(.discuss, data.identity, data.selected_id) orelse return error.RouteTooLong, 256 .new_comment => ui.RoutablePage.repoThreadCommentNewRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 257 .edit_comment => ui.RoutablePage.repoThreadCommentEditRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 258 .remove => ui.RoutablePage.repoThreadRemoveRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 259 else => ui.RoutablePage.repoThreadNewRoute(.discuss, data.identity) orelse return error.RouteTooLong, 260 }; 261 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == View.viewIndex(.new)); 262 const label: []const u8 = switch (data.view) { 263 .edit => edit_tab_label, 264 .new_comment => reply_tab_label, 265 .edit_comment => edit_tab_label, 266 .remove => remove_tab_label, 267 else => new_tab_label, 268 }; 269 try header.addTab(allocator, label, "", link, View.viewIndex(.new)); 270 } 271 272 header.select(View.viewIndex(data.view)); 273 return header; 274 } 275