╭─────────────╮ │scroll to top│ ╰─────────────╯ ╭ Discussions.zig ──────────────────────────────────────────────╮ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ╰───────────────────────────────────────────────────────────────╯
╭────╮ ╭ clone ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╮ │http│ ssh │ │ ╰────╯ ╰ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╯
143 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, empty.tag)); 144 const cursor = try tag_sets.getCursor(decoded) orelse return error.NotFound; 145 break :blk try DB.SortedSet(.read_only).init(cursor); 146 } else if (try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.active_id_set_key))) |cursor| 147 try DB.SortedSet(.read_only).init(cursor) 148 else 149 null; 150 151 const records_cursor = try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.record_map_key)) orelse { 152 if (strict) return error.NotFound; 153 return empty; 154 }; 155 const records = try DB.HashMap(.read_only).init(records_cursor); 156 157 var root_key: ?[]const u8 = null; 158 if (rooted) { 159 const id = evt.parseEventId(empty.selected_id) catch return error.NotFound; 160 const record_cursor = try records.getCursor(hash.hashInt(repo_opts.hash, &id)) orelse return error.NotFound; 161 const record = try evt.read(evt.Discussion.Record, DB, repo_opts.hash, arena, try DB.HashMap(.read_only).init(record_cursor)); 162 if (record.removed) return error.NotFound; 163 const activity_order = try evt.Discussion.activityOrder(DB, repo_opts.hash, haxy_moment, &id); 164 const order_key = try aa.dupe(u8, &evt.orderKeyDesc(activity_order, &id)); 165 const set = set_maybe orelse return error.NotFound; 166 if (!try set.contains(order_key)) return error.NotFound; 167 root_key = order_key; 168 } 169 170 const discussion_comments_start = if (empty.comment_id.len == 0) comments_start else 0; 171 const loaded_window = try thread.loadWindow(Self, .discuss, repo_opts.hash, arena, admin_moment, haxy_moment, records, set_maybe, root_key, null, empty.selected_id, discussion_comments_start, empty.search); 172 const comment_page = if (empty.comment_id.len == 0) 173 null 174 else 175 try Comment.init(repo_opts.hash, arena, admin_moment, haxy_moment, empty.selected_id, empty.comment_id, comments_start); 176 177 var tag_names: std.ArrayList([]const u8) = .empty; 178 if (try haxy_moment.getCursor(hash.hashInt(repo_opts.hash, evt.Discussion.tag_to_id_set_key))) |tags_cursor| { 179 const tag_sets = try DB.SortedMap(.read_only).init(tags_cursor); 180 var tag_iter = try tag_sets.iterator(); 181 while (try tag_iter.next()) |entry_cursor| { 182 if (tag_names.items.len == max_tags) break; 183 var entry = entry_cursor; 184 const pair = try entry.readKeyValuePair(); 185 try tag_names.append(aa, try pair.key_cursor.readBytesAlloc(aa, null)); 186 } 187 } 188 189 return .{ 190 .identity = empty.identity, 191 .tag = empty.tag, 192 .search = empty.search, 193 .selected_id = empty.selected_id, 194 .comment_id = empty.comment_id, 195 .comments_start = comments_start, 196 .comment_page = comment_page, 197 .recent = loaded_window, 198 .view = empty.view, 199 .description_page = empty.description_page, 200 .tags = tag_names.items, 201 }; 202 } 203 204 pub const View = thread.View(.discuss, Self); 205 pub const Detail = thread.Detail(.discuss, Self); 206 pub const detail_widget_name = "repo_discussion_detail"; 207 208 pub const Header = thread.Header; 209 210 const recent_tab_label = "recent"; 211 const tags_tab_label = "tags"; 212 const edit_tab_label = "edit"; 213 const reply_tab_label = "reply"; 214 const remove_tab_label = "remove"; 215 const new_tab_label = "new"; 216 217 // tabs switching between the discussions page's views 218 pub fn initHeader(allocator: std.mem.Allocator, session: *ui.Session, data: *const Self) !Header { 219 var header = try Header.init(allocator, session, data.search); 220 errdefer header.deinit(allocator); 221 const aa = session.page_arena.allocator(); 222 const selected_index = View.viewIndex(data.view); 223 const page_selected = std.meta.activeTag(session.data.current_page) == .repo_discussions; 224 225 // recent discussions 226 { 227 const route = try thread.searchRoute(ui.RoutablePage.repoDiscussionsRoute(data.identity, data.tag, ""), data.search); 228 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == 0); 229 var label_buf: [64]u8 = undefined; 230 const label = try thread.countLabel(&label_buf, recent_tab_label, data.recent); 231 try header.addTab(allocator, label, link, 0); 232 } 233 234 // tags tab, labeled with the active tag filter 235 { 236 const tags_route = try thread.searchRoute(ui.RoutablePage.repoThreadTagsRoute(.discuss, data.identity, data.tag), data.search); 237 const tags_link = try ui.inPageTabLink(session, tags_route, page_selected and selected_index == View.viewIndex(.tags)); 238 const label = if (data.tag.len == 0) tags_tab_label else blk: { 239 const decoded = std.Uri.percentDecodeInPlace(try aa.dupe(u8, data.tag)); 240 break :blk try std.fmt.allocPrint(aa, tags_tab_label ++ " ({s})", .{decoded}); 241 }; 242 try header.addTab(allocator, label, tags_link, View.viewIndex(.tags)); 243 } 244 245 // new-discussion tab; an edit or comment url shows its tab in this place 246 { 247 const route = switch (data.view) { 248 .edit => ui.RoutablePage.repoThreadEditRoute(.discuss, data.identity, data.selected_id) orelse return error.RouteTooLong, 249 .new_comment => ui.RoutablePage.repoThreadCommentNewRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 250 .edit_comment => ui.RoutablePage.repoThreadCommentEditRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 251 .remove => ui.RoutablePage.repoThreadRemoveRoute(.discuss, data.identity, data.selected_id, data.comment_id) orelse return error.RouteTooLong, 252 else => ui.RoutablePage.repoThreadNewRoute(.discuss, data.identity) orelse return error.RouteTooLong, 253 }; 254 const link = try ui.inPageTabLink(session, route, page_selected and selected_index == View.viewIndex(.new)); 255 const label: []const u8 = switch (data.view) { 256 .edit => edit_tab_label, 257 .new_comment => reply_tab_label, 258 .edit_comment => edit_tab_label, 259 .remove => remove_tab_label, 260 else => new_tab_label, 261 }; 262 try header.addTab(allocator, label, link, View.viewIndex(.new)); 263 } 264 265 header.select(View.viewIndex(data.view)); 266 return header; 267 } 268