From a4ed1245959dbce8ed37d1cbec64de235996b88b Mon Sep 17 00:00:00 2001 From: dadukhankevin Date: Wed, 18 Feb 2026 22:50:05 -0600 Subject: [PATCH 1/2] fixed some merges --- .../src/CommentsView/CommentsView.tsx | 1006 ++++------------- 1 file changed, 237 insertions(+), 769 deletions(-) diff --git a/webviews/codex-webviews/src/CommentsView/CommentsView.tsx b/webviews/codex-webviews/src/CommentsView/CommentsView.tsx index af4ecb062..89a76faa3 100644 --- a/webviews/codex-webviews/src/CommentsView/CommentsView.tsx +++ b/webviews/codex-webviews/src/CommentsView/CommentsView.tsx @@ -12,7 +12,7 @@ import { Send, Hash, Clock, - MoreHorizontal, + Reply, ArrowDownUp, MapPin, } from "lucide-react"; @@ -80,7 +80,6 @@ const AuthorName = ({ username, size = "sm" }: { username: string; size?: "sm" | function App() { const [cellId, setCellId] = useState({ cellId: "", uri: "", globalReferences: [] }); - const [uri, setUri] = useState(); const [commentThreadArray, setCommentThread] = useState([]); const [messageText, setMessageText] = useState(""); const [selectedThread, setSelectedThread] = useState(null); @@ -95,6 +94,7 @@ function App() { const commentRefs = useRef>(new Map()); const MAX_MESSAGE_LENGTH = 8000; const REPLY_PREVIEW_MAX_WORDS = 12; + const [sortMode, setSortMode] = useState("location"); const [currentUser, setCurrentUser] = useState<{ username: string; email: string; @@ -125,17 +125,6 @@ function App() { autoResizeTextarea(); }, [messageText, autoResizeTextarea]); - const [expandedThreads, setExpandedThreads] = useState>(new Set()); - const [replyingTo, setReplyingTo] = useState<{ threadId: string; username?: string } | null>( - null - ); - const [editingTitle, setEditingTitle] = useState(null); - const [threadTitleEdit, setThreadTitleEdit] = useState(""); - - // Sort configuration - const [sortMode, setSortMode] = useState("location"); - - // Helper function to determine if thread is currently resolved based on latest event const isThreadResolved = useCallback((thread: NotebookCommentThread): boolean => { const resolvedEvents = thread.resolvedEvent || []; if (resolvedEvents.length === 0) return false; @@ -150,110 +139,77 @@ function App() { return latest.deleted || false; }, []); - // Create a map of Bible books for ordering + // Bible book map for canonical ordering (from Luke's branch) const bibleBookMap = useMemo(() => { const map = new Map(); - (bibleBooksData as any[]).forEach((book) => { - map.set(book.abbr, { - name: book.name, - abbr: book.abbr, - ord: book.ord, - testament: book.testament, - }); - // Also map by full name for flexibility - map.set(book.name, { - name: book.name, - abbr: book.abbr, - ord: book.ord, - testament: book.testament, - }); + (bibleBooksData as { name: string; abbr: string; ord: string; testament: string }[]).forEach((book) => { + map.set(book.abbr, book); + map.set(book.name, book); }); return map; }, []); - // Helper to determine if project uses Bible terminology based on data - //const isBibleProject = useMemo(() => { - // // Check if any thread has Bible-style references (e.g., "GEN 1:1") - // return commentThreadArray.some(thread => { - // const refs = thread.cellId.globalReferences || []; - // return refs.some(ref => /^[A-Z0-9]{3}\s+\d+:\d+/.test(ref)); - // }); - //}, [commentThreadArray]); - - // Get appropriate label for missing data const getMissingLabel = useCallback((type: "file" | "milestone" | "cell"): string => { - //if (isBibleProject) { - switch (type) { - case "file": return "No Book Name"; - case "milestone": return "No Chapter Number"; - case "cell": return "No Verse Number"; - } - //} else { - // switch (type) { - // case "file": return "No File Name"; - // case "milestone": return "No Milestone Value"; - // case "cell": return "No Cell Number"; - // } - //} - }, []);//[isBibleProject]); - - // Helper to get sort order from fileDisplayName (using canonical Bible book order) + switch (type) { + case "file": return "No Book Name"; + case "milestone": return "No Chapter Number"; + case "cell": return "No Verse Number"; + } + }, []); + const getFileSortOrder = useCallback((fileDisplayName: string | undefined): string => { if (!fileDisplayName) return "999"; - - // Try to look up in bible book map const bookInfo = bibleBookMap.get(fileDisplayName); - if (bookInfo) { - return bookInfo.ord; // "01", "02", etc. - } - - // For non-Bible books, return a high number so they sort after Bible books - return "999"; + return bookInfo ? bookInfo.ord : "999"; }, [bibleBookMap]); - // Sort threads based on current sort mode - const sortThreads = useCallback((threads: NotebookCommentThread[]): NotebookCommentThread[] => { - const getLatestTimestamp = (thread: NotebookCommentThread) => { - const timestamps = thread.comments.map((c) => c.timestamp); - return Math.max(...timestamps); + // Sort threads by latest activity, unresolved first (used for current cell section) + const sortByActivity = useCallback((threads: NotebookCommentThread[]): NotebookCommentThread[] => { + const byLatestActivity = (a: NotebookCommentThread, b: NotebookCommentThread) => { + const aTime = Math.max(...a.comments.map((c) => c.timestamp)); + const bTime = Math.max(...b.comments.map((c) => c.timestamp)); + return bTime - aTime; }; + const unresolved = threads.filter((t) => !isThreadResolved(t)); + const resolved = threads.filter((t) => isThreadResolved(t)); + return [...unresolved.sort(byLatestActivity), ...resolved.sort(byLatestActivity)]; + }, [isThreadResolved]); + + // Sort threads by sort mode (used for "all other threads" section) + const sortThreads = useCallback((threads: NotebookCommentThread[]): NotebookCommentThread[] => { + const getLatestTimestamp = (thread: NotebookCommentThread) => + Math.max(...thread.comments.map((c) => c.timestamp)); switch (sortMode) { case "time-increasing": return [...threads].sort((a, b) => getLatestTimestamp(a) - getLatestTimestamp(b)); - + case "time-decreasing": return [...threads].sort((a, b) => getLatestTimestamp(b) - getLatestTimestamp(a)); - + case "location": return [...threads].sort((a, b) => { const aFile = a.cellId.fileDisplayName || getMissingLabel("file"); const bFile = b.cellId.fileDisplayName || getMissingLabel("file"); - - // Get sort orders for canonical Bible book ordering const aOrder = getFileSortOrder(a.cellId.fileDisplayName); const bOrder = getFileSortOrder(b.cellId.fileDisplayName); - - // Sort by canonical order (Bible books first by ord, then non-Bible alphabetically) + const orderCompare = aOrder.localeCompare(bOrder); if (orderCompare !== 0) return orderCompare; - - // If same sort order, sort by file display name + const fileCompare = aFile.localeCompare(bFile); if (fileCompare !== 0) return fileCompare; - - // Then by milestone + const aMilestone = a.cellId.milestoneValue || getMissingLabel("milestone"); const bMilestone = b.cellId.milestoneValue || getMissingLabel("milestone"); const milestoneCompare = aMilestone.localeCompare(bMilestone); if (milestoneCompare !== 0) return milestoneCompare; - - // Then by line number + const aLine = a.cellId.cellLineNumber ?? Number.MAX_SAFE_INTEGER; const bLine = b.cellId.cellLineNumber ?? Number.MAX_SAFE_INTEGER; return aLine - bLine; }); - + default: return threads; } @@ -275,17 +231,11 @@ function App() { break; case "reload": if (message.data?.cellId) { - setCellId({ - cellId: message.data.cellId, + setCellId({ + cellId: message.data.cellId, uri: message.data.uri || "", - globalReferences: message.data.globalReferences || [] + globalReferences: message.data.globalReferences || [], }); - if (viewMode === "cell") { - setSearchQuery(message.data.cellId); - } - } - if (message.data?.uri) { - setUri(message.data.uri); } break; case "updateUserInfo": @@ -356,15 +306,43 @@ function App() { return words.slice(0, maxWords).join(" ") + "..."; }; - const getCellLabel = (cellIdState: CellIdGlobalState | string): string => { + /** + * Get display name for a cell using new display fields with fallback. + * Priority: + * 1. fileDisplayName · milestoneValue · cellLabel (if available) + * 2. globalReferences (for stored comments) + * 3. Shortened cellId + */ + const getCellDisplayName = (cellIdState: CellIdGlobalState | string): string => { if (typeof cellIdState === "string") { - return cellIdState.length > 20 ? cellIdState.slice(-12) : cellIdState; + const parts = cellIdState.split(":"); + const finalPart = parts[parts.length - 1] || cellIdState; + return cellIdState.length < 10 ? cellIdState : finalPart; + } + + const displayParts: string[] = []; + if (cellIdState.fileDisplayName) displayParts.push(cellIdState.fileDisplayName); + if (cellIdState.milestoneValue) displayParts.push(cellIdState.milestoneValue); + if (cellIdState.cellLabel) displayParts.push(cellIdState.cellLabel); + + if (displayParts.length > 0) { + return displayParts.join(" · "); } - if (cellIdState.globalReferences?.length > 0) { - return cellIdState.globalReferences[0]; + + if (cellIdState.globalReferences && cellIdState.globalReferences.length > 0) { + const formatted = cellIdState.globalReferences.map((ref) => { + const parts = ref.split(" "); + if (parts.length >= 2) { + const book = parts[0].charAt(0).toUpperCase() + parts[0].slice(1).toLowerCase(); + return `${book} ${parts.slice(1).join(" ")}`; + } + return ref; + }); + return formatted.join(", "); } + const id = cellIdState.cellId; - return id.length > 20 ? id.slice(-12) : id; + return id.length > 10 ? `...${id.slice(-8)}` : id || "Unknown cell"; }; const getThreadPreview = (thread: NotebookCommentThread): string => { @@ -378,33 +356,19 @@ function App() { return plainText.length > 50 ? plainText.slice(0, 47) + "..." : plainText || "Empty thread"; }; - // Sort function for threads - const byLatestActivity = (a: NotebookCommentThread, b: NotebookCommentThread) => { - const aTime = Math.max(...a.comments.map((c) => c.timestamp)); - const bTime = Math.max(...b.comments.map((c) => c.timestamp)); - return bTime - aTime; - }; - - // Sort threads: unresolved first, then resolved - const sortThreads = (threads: NotebookCommentThread[]) => { - const unresolved = threads.filter((t) => !isThreadResolved(t)); - const resolved = threads.filter((t) => isThreadResolved(t)); - return [...unresolved.sort(byLatestActivity), ...resolved.sort(byLatestActivity)]; - }; - - // Threads for current cell + // Threads for current cell (sorted by activity, unresolved first) const currentCellThreads = useMemo(() => { const nonDeleted = commentThreadArray.filter((t) => !isThreadDeleted(t)); const filtered = nonDeleted.filter((t) => cellId.cellId && t.cellId.cellId === cellId.cellId); - return sortThreads(filtered); - }, [commentThreadArray, cellId.cellId, isThreadDeleted, isThreadResolved]); + return sortByActivity(filtered); + }, [commentThreadArray, cellId.cellId, isThreadDeleted, sortByActivity]); - // All threads (excluding current cell to avoid duplicates) + // All threads excluding current cell (sorted by sort mode) const allOtherThreads = useMemo(() => { const nonDeleted = commentThreadArray.filter((t) => !isThreadDeleted(t)); const filtered = nonDeleted.filter((t) => !cellId.cellId || t.cellId.cellId !== cellId.cellId); return sortThreads(filtered); - }, [commentThreadArray, cellId.cellId, isThreadDeleted, isThreadResolved]); + }, [commentThreadArray, cellId.cellId, isThreadDeleted, sortThreads]); const currentThread = selectedThread ? commentThreadArray.find((t) => t.id === selectedThread) @@ -415,8 +379,6 @@ function App() { if (isThreadResolved(currentThread)) return; const timestamp = Date.now(); - - // Build message body with optional reply reference let body = messageText.trim(); if (replyingTo) { body = `@reply:${replyingTo.id}\n${body}`; @@ -482,170 +444,6 @@ function App() { }); }; -//Conflict: added by 593: - /** - * Get display name for a cell, using new display fields or calculating fallback - * Priority: - * 1. Use fileDisplayName + milestoneValue + cellLineNumber if available - * 2. Fall back to globalReferences if available - * 3. Fall back to shortened cellId - * - * Note: For stored comments, the display fields may not be present. - * The current cell selection will have them, but older saved comments won't. - * This is intentional - we want fresh data for the current cell, but we fall back - * to simpler display for historical comments to avoid expensive lookups. - */ - const getCellDisplayName = (cellIdState: CellIdGlobalState | string): string => { - // Handle legacy string format (shouldn't happen after migration, but just in case) - if (typeof cellIdState === 'string') { - const parts = cellIdState.split(":"); - const finalPart = parts[parts.length - 1] || cellIdState; - return cellIdState.length < 10 ? cellIdState : finalPart; - } - - // New format: CellIdGlobalState object - // Build display string from available fields: fileDisplayName · milestoneValue · cellLabel - const displayParts: string[] = []; - - if (cellIdState.fileDisplayName) { - displayParts.push(cellIdState.fileDisplayName); - } - if (cellIdState.milestoneValue) { - displayParts.push(cellIdState.milestoneValue); - } - if (cellIdState.cellLabel) { - displayParts.push(cellIdState.cellLabel); - } - - if (displayParts.length > 0) { - return displayParts.join(" · "); - } - - // Fallback: Use globalReferences if available (for stored comments) - if (cellIdState.globalReferences && cellIdState.globalReferences.length > 0) { - // For stored comments with globalReferences, show them nicely - // Extract just the reference part (e.g., "GEN 1:1" -> "Gen 1:1" or "NUM 1:7" -> "Num 1:7") - const formatted = cellIdState.globalReferences.map(ref => { - // Capitalize first letter, lowercase rest: "NUM 1:7" -> "Num 1:7" - const parts = ref.split(' '); - if (parts.length >= 2) { - const book = parts[0].charAt(0).toUpperCase() + parts[0].slice(1).toLowerCase(); - return `${book} ${parts.slice(1).join(' ')}`; - } - return ref; - }); - return formatted.join(", "); - } - - // Fallback: shortened cellId - const cellId = cellIdState.cellId; - if (cellId.length > 10) { - // Show last 8 characters for UUIDs - return `...${cellId.slice(-8)}`; - } - - return cellId || "Unknown cell"; - }; - - const filteredCommentThreads = useMemo(() => { - // First, get all non-deleted threads - const nonDeletedThreads = commentThreadArray.filter((thread) => !isThreadDeleted(thread)); - - // Then, apply additional filtering based on view mode, search, and resolved status - const filtered = nonDeletedThreads.filter((commentThread) => { - // Skip resolved threads if they're hidden - if (!showResolvedThreads && isThreadResolved(commentThread)) return false; - - // If in cell view mode, only show comments for the current cell - if (viewMode === "cell" && cellId.cellId) { - return commentThread.cellId.cellId === cellId.cellId; - } - - // If searching, filter by search query - if (searchQuery) { - return ( - commentThread.threadTitle?.toLowerCase().includes(searchQuery.toLowerCase()) || - commentThread.comments.some((comment) => - comment.body.toLowerCase().includes(searchQuery.toLowerCase()) - ) || - commentThread.cellId.cellId.toLowerCase().includes(searchQuery.toLowerCase()) - ); - } - - // In all view mode with no search, show all comments (except resolved ones if hidden) - return true; - }); - - // Apply sorting - return sortThreads(filtered); - }, [commentThreadArray, searchQuery, viewMode, cellId.cellId, showResolvedThreads, sortThreads, isThreadDeleted, isThreadResolved]); - - // Count of hidden resolved threads - const hiddenResolvedThreadsCount = useMemo(() => { - if (showResolvedThreads) return 0; - - const nonDeletedThreads = commentThreadArray.filter((thread) => !isThreadDeleted(thread)); - - return nonDeletedThreads.filter((thread) => { - const isResolved = isThreadResolved(thread); - const matchesCurrentCell = - viewMode !== "cell" || thread.cellId.cellId === cellId.cellId; - const matchesSearch = - !searchQuery || - thread.threadTitle?.toLowerCase().includes(searchQuery.toLowerCase()) || - thread.comments.some((comment) => - comment.body.toLowerCase().includes(searchQuery.toLowerCase()) - ) || - thread.cellId.cellId.toLowerCase().includes(searchQuery.toLowerCase()); - - return isResolved && matchesCurrentCell && matchesSearch; - }).length; - }, [commentThreadArray, viewMode, cellId.cellId, searchQuery, showResolvedThreads, isThreadDeleted, isThreadResolved]); - - // Whether a user can start a new top-level comment thread (requires auth and active cell) - const canStartNewComment = currentUser.isAuthenticated && Boolean(cellId.cellId); - - // Helper function to render comment body with blockquotes - const renderCommentBody = (body: string) => { - if (!body) return null; - - const lines = body.split("\n"); - const elements: JSX.Element[] = []; - let currentQuoteLines: string[] = []; - - const flushQuote = () => { - if (currentQuoteLines.length > 0) { - elements.push( -
- {currentQuoteLines.join("\n")} -
- ); - currentQuoteLines = []; - } - }; - - lines.forEach((line, index) => { - if (line.startsWith("> ")) { - currentQuoteLines.push(line.substring(2)); - } else { - flushQuote(); - if (line.trim() || index < lines.length - 1) { - elements.push( - - {line} - {index < lines.length - 1 &&
} -
- ); - } - } - }); - - flushQuote(); - return elements; - //Conflict: added by incoming const handleDeleteComment = (commentId: string, threadId: string) => { vscode.postMessage({ command: "deleteComment", args: { commentId, commentThreadId: threadId } }); }; @@ -653,7 +451,6 @@ function App() { const handleUndoDelete = (commentId: string, threadId: string) => { vscode.postMessage({ command: "undoCommentDeletion", args: { commentId, commentThreadId: threadId } }); }; - //Conflict: end of conflict // Render message content (without reply prefix) const renderMessageContent = (content: string) => { @@ -665,7 +462,7 @@ function App() { )); }; - // Render a thread item + // Render a thread list item (discord channel style) const renderThreadItem = (thread: NotebookCommentThread) => { const resolved = isThreadResolved(thread); const latestComment = thread.comments[thread.comments.length - 1]; @@ -701,32 +498,82 @@ function App() { ); }; - // Extract ThreadCard component to avoid duplication - const ThreadCard = ({ thread }: { thread: NotebookCommentThread }) => ( - - {/* Thread header */} - toggleCollapsed(thread.id)} - > -
-
- {collapsedThreads[thread.id] ? ( - - ) : ( - - )} -/* // Thread list view with collapsible sections + // Location-grouped thread list (from Luke's branch) for "All Other Threads" when sort=location + const LocationGroupedList = ({ threads }: { threads: NotebookCommentThread[] }) => { + const grouped = threads.reduce((acc, thread) => { + const file = thread.cellId.fileDisplayName || getMissingLabel("file"); + const milestone = thread.cellId.milestoneValue || getMissingLabel("milestone"); + if (!acc[file]) acc[file] = {}; + if (!acc[file][milestone]) acc[file][milestone] = []; + acc[file][milestone].push(thread); + return acc; + }, {} as Record>); + + return ( +
+ {Object.entries(grouped).map(([fileName, milestones]) => ( +
+
+ {fileName} +
+ {Object.entries(milestones).map(([milestoneName, threadsInMilestone]) => ( +
+
+ {milestoneName} +
+ {threadsInMilestone.map(renderThreadItem)} +
+ ))} +
+ ))} +
+ ); + }; + + // Thread list view with collapsible sections const ThreadList = () => (
+ {/* Sort controls */} +
+ + + + + + setSortMode("location")} + className={sortMode === "location" ? "bg-accent" : ""} + > + + Location in Project + {sortMode === "location" && } + + setSortMode("time-increasing")} + className={sortMode === "time-increasing" ? "bg-accent" : ""} + > + + Time Increasing + {sortMode === "time-increasing" && } + + setSortMode("time-decreasing")} + className={sortMode === "time-decreasing" ? "bg-accent" : ""} + > + + Time Decreasing + {sortMode === "time-decreasing" && } + + + +
+
{/* Current Cell Section */}
- {/* Section header */} - {/* Section content */} {currentSectionExpanded && (
{/* Inline new thread input */} @@ -781,7 +627,6 @@ function App() {
)} - {/* Thread list for current cell */} {currentCellThreads.length === 0 ? (
No threads on this cell yet @@ -793,9 +638,8 @@ function App() { )}
- {/* All Threads Section */} + {/* All Other Threads Section */}
- {/* Section header */} - {/* Section content */} {allSectionExpanded && (
{allOtherThreads.length === 0 ? (
No other threads
+ ) : sortMode === "location" ? ( + ) : ( allOtherThreads.map(renderThreadItem) )} @@ -826,7 +671,7 @@ function App() {
- ); */ + ); // Thread detail view (Discord chat style) const ThreadDetail = () => { @@ -841,11 +686,16 @@ function App() { - - {getThreadPreview(currentThread)} - +
+ + {getThreadPreview(currentThread)} + + + {getCellDisplayName(currentThread.cellId)} + +
{resolved && ( - + Resolved @@ -877,7 +727,9 @@ function App() {
{currentThread.comments.map((comment, idx) => { const time = formatTimestamp(comment.timestamp); - const showAuthor = idx === 0 || currentThread.comments[idx - 1].author.name !== comment.author.name; + const showAuthor = + idx === 0 || + currentThread.comments[idx - 1].author.name !== comment.author.name; const isOwn = comment.author.name === currentUser.username; const { replyToId, content } = parseReplyInfo(comment.body); const repliedComment = replyToId ? findCommentById(replyToId) : null; @@ -904,7 +756,10 @@ function App() { {repliedComment.author.name} - {truncateToWords(parseReplyInfo(repliedComment.body).content, REPLY_PREVIEW_MAX_WORDS)} + {truncateToWords( + parseReplyInfo(repliedComment.body).content, + REPLY_PREVIEW_MAX_WORDS + )}
)} @@ -914,22 +769,27 @@ function App() { - {time.display} + + {time.display} + {time.full}
)} +
{comment.deleted ? ( - Message deleted + + Message deleted + ) : ( renderMessageContent(content) )}
- {/* Actions */} + {/* Hover actions */} {!comment.deleted && !resolved && (
@@ -945,456 +805,51 @@ function App() { > -
+ + Reply + + {isOwn && ( + + + + + Delete + )}
-
- -
-
- - {getCellDisplayName(thread.cellId)} - - {thread.cellId.cellLineNumber != null && ( - - Cell {thread.cellId.cellLineNumber} - - )} -
- - - {thread.comments.length}{" "} - {thread.comments.length === 1 - ? "comment" - : "comments"} - -
- - - {/* Comments section */} - {!collapsedThreads[thread.id] && ( - -
- {/* Reply form at top */} - {currentUser.isAuthenticated && ( -
- - -
- {replyingTo?.threadId === thread.id && ( -
- - Replying to @ - {replyingTo.username} - -
- )} - -
-