feat: add threaded comments, activity feeds, and task dependencies

This commit is contained in:
2026-08-10 21:59:10 +00:00
parent 1059512888
commit a3e4d3c868
35 changed files with 10283 additions and 578 deletions
+1 -89
View File
@@ -7,6 +7,7 @@ import { useApiDomain } from "@/lib/stores/use-active-domain-store";
import { useRealtime } from "@/hooks/use-realtime";
import { useOpenCreateDialog } from "@/hooks/use-open-create-dialog";
import { Plus, Trash2, Search, Pin, FileText, Link as LinkIcon, History } from "lucide-react";
import { NoteEditor } from "@/components/entities/note-editor";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
@@ -15,95 +16,6 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
import type { Note, PaginatedResponse } from "@/lib/types";
import { format, parseISO } from "date-fns";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import Link from "@tiptap/extension-link";
import Placeholder from "@tiptap/extension-placeholder";
const AUTOSAVE_DEBOUNCE_MS = 800;
// TipTap-based note editor. Autosaves with a debounce (plus a save-on-blur and a
// flush-on-unmount safety net) and deliberately does NOT stop propagation of
// key/mouse events, so global shortcuts (command palette, etc.) keep working.
const NoteEditor = memo(function NoteEditor({ initialContent, onSave, placeholder = "Start writing..." }: { initialContent: string; onSave: (html: string) => void; placeholder?: string }) {
const latestHtmlRef = useRef(initialContent || "");
const dirtyRef = useRef(false);
const saveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const editor = useEditor(
{
extensions: [
StarterKit.configure({ link: false }),
Link.configure({ openOnClick: false }),
Placeholder.configure({ placeholder }),
],
content: initialContent || "",
editorProps: {
attributes: {
class: "focus:outline-none min-h-[300px] p-3",
},
},
},
[placeholder, initialContent]
);
useEffect(() => {
if (!editor) return;
const flushSave = () => {
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = null;
}
if (!dirtyRef.current) return;
dirtyRef.current = false;
onSave(latestHtmlRef.current);
};
const handleUpdate = () => {
latestHtmlRef.current = editor.getHTML();
dirtyRef.current = true;
if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
saveTimerRef.current = setTimeout(flushSave, AUTOSAVE_DEBOUNCE_MS);
};
editor.on("update", handleUpdate);
editor.on("blur", flushSave);
return () => {
editor.off("update", handleUpdate);
editor.off("blur", flushSave);
if (saveTimerRef.current) {
clearTimeout(saveTimerRef.current);
saveTimerRef.current = null;
}
// Flush any unsaved edits on unmount so switching notes doesn't drop typing.
if (dirtyRef.current) {
dirtyRef.current = false;
onSave(latestHtmlRef.current);
}
};
}, [editor, onSave]);
if (!editor) return null;
return (
<div className="note-editor relative min-h-[300px]">
{/* Placeholder needs its ::before styling; the @tailwindcss/typography plugin
is not installed, so this is scoped CSS for the empty-editor state. */}
<style>{`
.note-editor p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
color: hsl(var(--muted-foreground));
float: left;
height: 0;
pointer-events: none;
}
`}</style>
<EditorContent editor={editor} />
</div>
);
});
// Completely uncontrolled title input - uses ref to avoid any re-render
const NoteTitleInput = memo(function NoteTitleInput({ noteId, initialTitle }: { noteId: string; initialTitle: string }) {