Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138import type { CommentRecord } from "$lib/api/comment";import type { CommitSummary } from "$lib/api/repo";import type { ReactionGroup } from "$lib/components/reaction/reactions";
export interface CommentView { uri: string; cid?: string; rkey: string; authorDid: string; authorHandle: string; createdAt: string; // the appview's comment header shows "Edited <time>" in place of the created one editedAt?: string; body: string; bodyHtml: string | null; // carried so an edit rewrites the record without dropping what it pinned embed?: CommentRecord["embed"]; reactions?: ReactionGroup[]; deleted?: boolean;}
export interface CommentThread { self: CommentView; replies: CommentView[];}
export interface ThreadInput { comment: CommentView; replyTo: string | null; // lets us reply to a parent even after it's deleted replyToCid?: string;}
const byCreatedAt = (a: CommentView, b: CommentView) => a.createdAt.localeCompare(b.createdAt);
// stand-in for a deleted parentexport function deletedComment(uri: string, createdAt: string, cid?: string): CommentView { return { uri, cid, rkey: uri.split("/").pop() ?? "", authorDid: "", authorHandle: "", createdAt, body: "", bodyHtml: null, deleted: true };}
export function buildCommentThreads(inputs: ThreadInput[]): CommentThread[] { const threads = new Map<string, CommentThread>();
for (const { comment, replyTo } of inputs) { if (replyTo === null) { threads.set(comment.uri, { self: comment, replies: [] }); } }
for (const { comment, replyTo, replyToCid } of inputs) { if (replyTo === null) continue; let parent = threads.get(replyTo); if (!parent) { parent = { self: deletedComment(replyTo, comment.createdAt), replies: [] }; threads.set(replyTo, parent); } if (parent.self.deleted && replyToCid && !parent.self.cid) { parent.self.cid = replyToCid; } parent.replies.push(comment); }
const list = [...threads.values()].sort((a, b) => byCreatedAt(a.self, b.self)); for (const thread of list) thread.replies.sort(byCreatedAt); return list;}
export interface CommentGroup { /** version this group follows; undefined for the comments that predate v0 */ version?: number; comments: CommentView[];}
/** * A comment belongs to the newest version created at or before it. There is no * "commented on version N" field on the record, and there will not be one -- * createdAt on both sides is the only ordering there is. * * `versions` must be in chronological order, as `pull.versions` is. */export function groupCommentsByVersion( comments: CommentView[], versions: { createdAt: string }[]): CommentGroup[] { // slot 0 holds anything older than v0 and renders without a header const groups: CommentGroup[] = [ { comments: [] }, ...versions.map((_, version) => ({ version, comments: [] })) ]; // parsed rather than string-compared: a boundary must not turn on whether the // two sides happen to share an offset or the same fractional precision const at = (createdAt: string) => Date.parse(createdAt); // both sides ascend, so one walk places every comment -- slot never goes back let slot = 0; for (const comment of [...comments].sort((a, b) => at(a.createdAt) - at(b.createdAt))) { while (slot < versions.length && at(versions[slot].createdAt) <= at(comment.createdAt)) slot++; groups[slot].comments.push(comment); } // v0 lands with the pull itself, so slot 0 is empty in practice -- drop it then return groups.filter((group) => group.version !== undefined || group.comments.length);}
export function countCommentsByCommit(comments: CommentView[]): Map<string, number> { const counts = new Map<string, number>(); for (const { embed } of comments) { if (!embed) continue; const oid = embed.commit.oid; counts.set(oid, (counts.get(oid) ?? 0) + 1); } return counts;}
export function filterCommentsByCommit( comments: CommentView[], commits: CommitSummary[], filter?: (commit: CommitSummary) => boolean): CommentView[] { if (!filter) return comments; return comments.filter((comment) => { // full oids on both sides, so equality is enough -- no prefix matching const oid = comment.embed?.commit.oid; if (!oid) return true; const commit = commits.find((c) => c.hash === oid); return !!commit && filter(commit); });}