diff --git a/lib/folk-comment-pin.ts b/lib/folk-comment-pin.ts index a56bcff..a172d60 100644 --- a/lib/folk-comment-pin.ts +++ b/lib/folk-comment-pin.ts @@ -17,6 +17,13 @@ interface SpaceMember { username: string; } +interface RNoteItem { + id: string; + title: string; + type: string; + updated_at: string; +} + export class CommentPinManager { #container: HTMLElement; #canvasContent: HTMLElement; @@ -32,6 +39,7 @@ export class CommentPinManager { #openPinId: string | null = null; #members: SpaceMember[] | null = null; #mentionDropdown: HTMLElement | null = null; + #notes: RNoteItem[] | null = null; constructor( container: HTMLElement, @@ -255,6 +263,30 @@ export class CommentPinManager { this.#renderAllPins(); } + linkNote(pinId: string, noteId: string, noteTitle: string) { + const newDoc = Automerge.change(this.#sync.doc, "Link note to pin", (doc) => { + if (doc.commentPins?.[pinId]) { + doc.commentPins[pinId].linkedNoteId = noteId; + doc.commentPins[pinId].linkedNoteTitle = noteTitle; + } + }); + this.#sync._applyDocChange(newDoc); + if (this.#openPinId === pinId) this.#openPinPopover(pinId, false); + this.#renderAllPins(); + } + + unlinkNote(pinId: string) { + const newDoc = Automerge.change(this.#sync.doc, "Unlink note from pin", (doc) => { + if (doc.commentPins?.[pinId]) { + delete (doc.commentPins[pinId] as any).linkedNoteId; + delete (doc.commentPins[pinId] as any).linkedNoteTitle; + } + }); + this.#sync._applyDocChange(newDoc); + if (this.#openPinId === pinId) this.#openPinPopover(pinId, false); + this.#renderAllPins(); + } + toggleShowResolved() { this.#showResolved = !this.#showResolved; this.#renderAllPins(); @@ -468,7 +500,20 @@ export class CommentPinManager { (!this.#sync.doc.shapes?.[pin.anchor.shapeId] || (this.#sync.doc.shapes[pin.anchor.shapeId] as any).deleted); - let html = ` + let html = ``; + + // Header + html += `
Pin #${pinIndex}
@@ -479,18 +524,31 @@ export class CommentPinManager {
- ${isOrphaned ? `
⚠ Attached shape was deleted
` : ""} `; + if (isOrphaned) { + html += `
⚠ Attached shape was deleted
`; + } + + // Linked rNote + if (pin.linkedNoteId) { + html += ` +
+ + 📝 ${this.#escapeHtml(pin.linkedNoteTitle || "Linked note")} + + +
+ `; + } + // Messages if (pin.messages.length > 0) { - html += `
`; + html += `
`; for (const msg of pin.messages) { const time = new Date(msg.createdAt).toLocaleString(undefined, { - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", + month: "short", day: "numeric", hour: "numeric", minute: "2-digit", }); html += `
@@ -505,11 +563,11 @@ export class CommentPinManager { html += `
`; } - // Input + // Input + link note row html += `
- @@ -518,6 +576,13 @@ export class CommentPinManager { padding: 6px 12px; cursor: pointer; font-size: 13px; font-weight: 600; ">Send
+ ${!pin.linkedNoteId ? ` +
+ + +
+ ` : ""}
`; @@ -534,6 +599,7 @@ export class CommentPinManager { else if (action === "unresolve") this.unresolvePin(pinId); else if (action === "delete") this.deletePin(pinId); else if (action === "remind") this.#createReminder(pinId); + else if (action === "unlink-note") this.unlinkNote(pinId); }); }); @@ -562,18 +628,38 @@ export class CommentPinManager { // @mention autocomplete input.addEventListener("input", () => this.#handleMentionInput(input)); + // Link note button + const linkBtn = this.#popover.querySelector('[data-action="link-note"]') as HTMLButtonElement | null; + const notePicker = this.#popover.querySelector(".cp-note-picker") as HTMLElement | null; + if (linkBtn && notePicker) { + linkBtn.addEventListener("click", async () => { + if (notePicker.style.display !== "none") { + notePicker.style.display = "none"; + return; + } + notePicker.innerHTML = `
Loading notes...
`; + notePicker.style.display = "block"; + const notes = await this.#fetchNotes(); + if (notes.length === 0) { + notePicker.innerHTML = `
No notes in this space
`; + return; + } + notePicker.innerHTML = ""; + for (const note of notes) { + const item = document.createElement("div"); + item.className = "cp-note-item"; + item.textContent = note.title || "Untitled"; + item.addEventListener("click", () => { + this.linkNote(pinId, note.id, note.title || "Untitled"); + }); + notePicker.appendChild(item); + } + }); + } + if (focusInput) { requestAnimationFrame(() => input.focus()); } - - // Style action buttons - const style = document.createElement("style"); - style.textContent = ` - .cp-action { background: none; border: 1px solid #444; border-radius: 4px; - padding: 2px 6px; cursor: pointer; color: #ccc; font-size: 13px; } - .cp-action:hover { background: #333; } - `; - this.#popover.prepend(style); } // ── @Mention Autocomplete ── @@ -594,6 +680,24 @@ export class CommentPinManager { } } + async #fetchNotes(): Promise { + if (this.#notes) return this.#notes; + try { + const sess = JSON.parse(localStorage.getItem("encryptid_session") || "{}"); + const res = await fetch(`/${this.#spaceSlug}/rnotes/api/notes?limit=50`, { + headers: sess?.accessToken ? { Authorization: `Bearer ${sess.accessToken}` } : {}, + }); + if (!res.ok) return []; + const data = await res.json(); + this.#notes = (data.notes || []).map((n: any) => ({ + id: n.id, title: n.title, type: n.type, updated_at: n.updated_at, + })); + return this.#notes!; + } catch { + return []; + } + } + async #handleMentionInput(input: HTMLInputElement) { const val = input.value; const cursorPos = input.selectionStart || 0; diff --git a/shared/comment-pin-types.ts b/shared/comment-pin-types.ts index 0d3158d..e1c2d39 100644 --- a/shared/comment-pin-types.ts +++ b/shared/comment-pin-types.ts @@ -27,4 +27,6 @@ export interface CommentPinData { createdAt: number; createdBy: string; // DID createdByName: string; + linkedNoteId?: string; // rNotes note ID + linkedNoteTitle?: string; // cached title for display }