import React, { useState, useMemo } from 'react'; import { FaComments, FaUser, FaTrash, FaClock, FaSpinner, FaSync, FaChevronDown, FaChevronUp, FaExclamationTriangle, FaCheckCircle, FaTimesCircle, FaInfoCircle, FaPaperclip, FaThumbsUp, FaEdit, } from 'react-icons/fa'; import { toast } from 'react-toastify'; import { useComments } from '../hooks/useComments'; import MentionInput from './MentionInput'; interface CommentSectionProps { referenceDoctype: string; referenceName: string | null; title?: string; pollInterval?: number; initialLimit?: number; collapsible?: boolean; startCollapsed?: boolean; } const commentTypeMeta: Record< string, { icon: React.ReactNode; color: string; label: string } > = { Comment: { icon: , color: 'text-blue-600 dark:text-blue-400', label: 'Comment', }, Info: { icon: , color: 'text-gray-500 dark:text-gray-400', label: 'Info', }, Edit: { icon: , color: 'text-orange-500 dark:text-orange-400', label: 'Edit', }, Attachment: { icon: , color: 'text-purple-500 dark:text-purple-400', label: 'Attachment', }, Like: { icon: , color: 'text-pink-500 dark:text-pink-400', label: 'Like', }, }; const timeAgo = (dateStr: string): string => { if (!dateStr) return ''; const date = new Date(dateStr); const now = new Date(); const diffMs = now.getTime() - date.getTime(); const mins = Math.floor(diffMs / 60000); const hrs = Math.floor(diffMs / 3600000); const days = Math.floor(diffMs / 86400000); if (mins < 1) return 'Just now'; if (mins < 60) return `${mins}m ago`; if (hrs < 24) return `${hrs}h ago`; if (days < 7) return `${days}d ago`; return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, }); }; const emailToName = (email: string): string => { if (!email) return 'Unknown'; const at = email.indexOf('@'); if (at === -1) return email; return email .substring(0, at) .replace(/[._-]/g, ' ') .replace(/\b\w/g, (c) => c.toUpperCase()); }; const CommentContent: React.FC<{ html: string }> = ({ html }) => { const cleaned = html .replace(/
/g, '') .replace(/<\/div>$/g, ''); return (
); }; const CommentSection: React.FC = ({ referenceDoctype, referenceName, title = 'Comments & Discussion', pollInterval = 30000, initialLimit = 5, collapsible = true, startCollapsed = false, }) => { const { comments, loading, posting, error, currentUser, refetch, postComment, deleteComment, mentionUsers, mentionLoading, searchMentionUsers, } = useComments({ referenceDoctype, referenceName, pollInterval, }); const [expanded, setExpanded] = useState(!startCollapsed); const [showAll, setShowAll] = useState(false); const [draftText, setDraftText] = useState(''); const [deletingId, setDeletingId] = useState(null); const userComments = useMemo( () => comments.filter((c) => c.comment_type === 'Comment'), [comments] ); const visibleComments = useMemo(() => { if (showAll) return comments; return comments.slice(-initialLimit); }, [comments, showAll, initialLimit]); const handlePost = async (html: string) => { try { await postComment(html); setDraftText(''); toast.success('Comment posted!', { position: 'top-right', autoClose: 2000, icon: , }); } catch (err: any) { toast.error(`Failed to post comment: ${err.message || 'Unknown error'}`, { position: 'top-right', autoClose: 5000, icon: , }); } }; const handleDelete = async (commentName: string) => { setDeletingId(commentName); try { await deleteComment(commentName); toast.success('Comment deleted', { position: 'top-right', autoClose: 2000, icon: , }); } catch (err: any) { toast.error(`Failed to delete: ${err.message || 'Unknown error'}`, { position: 'top-right', autoClose: 5000, icon: , }); } finally { setDeletingId(null); } }; if (!referenceName) { return (
Save the document first to enable comments.
); } return (
collapsible && setExpanded((v) => !v)} >

{title}

{userComments.length > 0 && ( {userComments.length} )}
{collapsible && ( {expanded ? : } )}
{expanded && (
{error && (
{error}
)} {loading && comments.length === 0 && (
Loading comments…
)} {comments.length > 0 && ( <> {!showAll && comments.length > initialLimit && ( )}
{visibleComments.map((comment) => { const meta = commentTypeMeta[comment.comment_type] || commentTypeMeta.Comment; const isOwn = comment.comment_email === currentUser || comment.owner === currentUser; const isDeleting = deletingId === comment.name; return (
{comment.comment_by || emailToName(comment.comment_email || comment.owner)} {comment.comment_type !== 'Comment' && ( {meta.icon} {meta.label} )}
{timeAgo(comment.creation)} {isOwn && comment.comment_type === 'Comment' && ( )}
); })}
{showAll && comments.length > initialLimit && ( )} )} {!loading && comments.length === 0 && (

No comments yet. Start the discussion!

)}
)}
); }; export default CommentSection;