Spaces:
Sleeping
Sleeping
File size: 3,615 Bytes
43370b3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | import * as Y from 'yjs'
// Comment threads live inside the shared Yjs doc, so they sync in real time and persist
// with the deck. Each thread is anchored to a text range via Yjs *relative positions*,
// which survive concurrent edits.
export interface ReplySnap {
id: string
author: string
color: string
text: string
ts: number
}
export interface ThreadSnap {
id: string
from: number | null // absolute position in the current doc (null if the anchor was deleted)
to: number | null
quote: string
resolved: boolean
replies: ReplySnap[]
}
export interface CommentRange {
id: string
from: number | null
to: number | null
resolved: boolean
}
export interface ReplyInput {
author: string
color: string
text: string
}
const uid = () => Math.random().toString(36).slice(2) + Date.now().toString(36)
function commentsArray(doc: Y.Doc): Y.Array<Y.Map<unknown>> {
return doc.getArray<Y.Map<unknown>>('comments')
}
function makeReply(input: ReplyInput): Y.Map<unknown> {
const r = new Y.Map<unknown>()
r.set('id', uid())
r.set('author', input.author)
r.set('color', input.color)
r.set('text', input.text)
r.set('ts', Date.now())
return r
}
export function createThread(
doc: Y.Doc,
ytext: Y.Text,
from: number,
to: number,
quote: string,
reply: ReplyInput,
): void {
const anchor = Y.relativePositionToJSON(Y.createRelativePositionFromTypeIndex(ytext, from))
const anchorEnd = Y.relativePositionToJSON(Y.createRelativePositionFromTypeIndex(ytext, to))
doc.transact(() => {
const t = new Y.Map<unknown>()
t.set('id', uid())
t.set('anchor', JSON.stringify(anchor))
t.set('anchorEnd', JSON.stringify(anchorEnd))
t.set('quote', quote.slice(0, 240))
t.set('resolved', false)
const replies = new Y.Array<Y.Map<unknown>>()
replies.push([makeReply(reply)])
t.set('replies', replies)
commentsArray(doc).push([t])
})
}
export function addReply(doc: Y.Doc, threadId: string, reply: ReplyInput): void {
for (const t of commentsArray(doc)) {
if (t.get('id') === threadId) {
;(t.get('replies') as Y.Array<Y.Map<unknown>>).push([makeReply(reply)])
return
}
}
}
export function setResolved(doc: Y.Doc, threadId: string, resolved: boolean): void {
for (const t of commentsArray(doc)) {
if (t.get('id') === threadId) {
t.set('resolved', resolved)
return
}
}
}
export function deleteThread(doc: Y.Doc, threadId: string): void {
const arr = commentsArray(doc)
for (let i = 0; i < arr.length; i++) {
if (arr.get(i).get('id') === threadId) {
arr.delete(i, 1)
return
}
}
}
function absFromJSON(doc: Y.Doc, json: unknown): number | null {
try {
const rel = Y.createRelativePositionFromJSON(JSON.parse(json as string))
const abs = Y.createAbsolutePositionFromRelativePosition(rel, doc)
return abs ? abs.index : null
} catch {
return null
}
}
export function snapshotThreads(doc: Y.Doc, _ytext: Y.Text): ThreadSnap[] {
const out: ThreadSnap[] = []
for (const t of commentsArray(doc)) {
const replies = (t.get('replies') as Y.Array<Y.Map<unknown>>).map((r) => ({
id: r.get('id') as string,
author: r.get('author') as string,
color: r.get('color') as string,
text: r.get('text') as string,
ts: r.get('ts') as number,
}))
out.push({
id: t.get('id') as string,
from: absFromJSON(doc, t.get('anchor')),
to: absFromJSON(doc, t.get('anchorEnd')),
quote: t.get('quote') as string,
resolved: Boolean(t.get('resolved')),
replies,
})
}
return out
}
|