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;