refactor(project-view-updates): optimize comment handling and rendering logic

- Introduced useMemo and useCallback hooks to enhance performance and prevent unnecessary re-renders.
- Refactored comment rendering logic into a separate function for better readability and maintainability.
- Updated mentionsOptions to utilize useMemo for efficient computation based on members.
- Improved comment change handling and member selection logic for a smoother user experience.
- Cleaned up code by removing redundant comments and optimizing dependencies in useEffect hooks.
This commit is contained in:
shancds
2025-06-19 10:40:40 +05:30
parent 4c4a860c76
commit 7b657120e9

View File

@@ -1,5 +1,5 @@
import { Button, ConfigProvider, Flex, Form, Mentions, Skeleton, Space, Tooltip, Typography } from 'antd'; import { Button, ConfigProvider, Flex, Form, Mentions, Skeleton, Space, Tooltip, Typography } from 'antd';
import { useEffect, useState, useCallback } from 'react'; import { useEffect, useState, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import DOMPurify from 'dompurify'; import DOMPurify from 'dompurify';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@@ -68,7 +68,7 @@ const ProjectViewUpdates = () => {
} }
}, [projectId]); }, [projectId]);
const handleAddComment = async () => { const handleAddComment = useCallback(async () => {
if (!projectId || characterLength === 0) return; if (!projectId || characterLength === 0) return;
try { try {
@@ -96,15 +96,13 @@ const ProjectViewUpdates = () => {
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
setCommentValue(''); setCommentValue('');
} }
}; }, [projectId, characterLength, commentValue, selectedMembers, getComments]);
useEffect(() => { useEffect(() => {
void getMembers(); void getMembers();
void getComments(); void getComments();
}, [getMembers, getComments,refreshTimestamp]); }, [getMembers, getComments, refreshTimestamp]);
const handleCancel = useCallback(() => { const handleCancel = useCallback(() => {
form.resetFields(['comment']); form.resetFields(['comment']);
@@ -113,14 +111,16 @@ const ProjectViewUpdates = () => {
setSelectedMembers([]); setSelectedMembers([]);
}, [form]); }, [form]);
const mentionsOptions = const mentionsOptions = useMemo(() =>
members?.map(member => ({ members?.map(member => ({
value: member.id, value: member.id,
label: member.name, label: member.name,
})) ?? []; })) ?? [], [members]
);
const memberSelectHandler = useCallback((member: IMentionMemberSelectOption) => { const memberSelectHandler = useCallback((member: IMentionMemberSelectOption) => {
if (!member?.value || !member?.label) return; if (!member?.value || !member?.label) return;
setSelectedMembers(prev => setSelectedMembers(prev =>
prev.some(mention => mention.id === member.value) prev.some(mention => mention.id === member.value)
? prev ? prev
@@ -131,13 +131,11 @@ const ProjectViewUpdates = () => {
const parts = prev.split('@'); const parts = prev.split('@');
const lastPart = parts[parts.length - 1]; const lastPart = parts[parts.length - 1];
const mentionText = member.label; const mentionText = member.label;
// Keep only the part before the @ and add the new mention
return prev.slice(0, prev.length - lastPart.length) + mentionText; return prev.slice(0, prev.length - lastPart.length) + mentionText;
}); });
}, []); }, []);
const handleCommentChange = useCallback((value: string) => { const handleCommentChange = useCallback((value: string) => {
// Only update the value without trying to replace mentions
setCommentValue(value); setCommentValue(value);
setCharacterLength(value.trim().length); setCharacterLength(value.trim().length);
}, []); }, []);
@@ -157,56 +155,69 @@ const ProjectViewUpdates = () => {
[getComments] [getComments]
); );
const configProviderTheme = useMemo(() => ({
components: {
Button: {
defaultColor: colors.lightGray,
defaultHoverColor: colors.darkGray,
},
},
}), []);
const renderComment = useCallback((comment: IProjectUpdateCommentViewModel) => {
const sanitizedContent = DOMPurify.sanitize(comment.content || '');
const timeDifference = calculateTimeDifference(comment.created_at || '');
const themeClass = theme === 'dark' ? 'dark' : 'light';
return (
<Flex key={comment.id} gap={8}>
<CustomAvatar avatarName={comment.created_by || ''} />
<Flex vertical flex={1}>
<Space>
<Typography.Text strong style={{ fontSize: 13, color: colors.lightGray }}>
{comment.created_by || ''}
</Typography.Text>
<Tooltip title={comment.created_at}>
<Typography.Text style={{ fontSize: 13, color: colors.deepLightGray }}>
{timeDifference}
</Typography.Text>
</Tooltip>
</Space>
<Typography.Paragraph
style={{ margin: '8px 0' }}
ellipsis={{ rows: 3, expandable: true }}
>
<div className={`mentions-${themeClass}`} dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
</Typography.Paragraph>
<ConfigProvider
wave={{ disabled: true }}
theme={configProviderTheme}
>
<Button
icon={<DeleteOutlined />}
shape="circle"
type="text"
size='small'
onClick={() => handleDeleteComment(comment.id)}
/>
</ConfigProvider>
</Flex>
</Flex>
);
}, [theme, configProviderTheme, handleDeleteComment]);
const commentsList = useMemo(() =>
comments.map(renderComment), [comments, renderComment]
);
return ( return (
<Flex gap={24} vertical> <Flex gap={24} vertical>
<Flex vertical gap={16}> <Flex vertical gap={16}>
{ {isLoadingComments ? (
isLoadingComments ? ( <Skeleton active />
<Skeleton active /> ) : (
): commentsList
comments.map(comment => ( )}
<Flex key={comment.id} gap={8}>
<CustomAvatar avatarName={comment.created_by || ''} />
<Flex vertical flex={1}>
<Space>
<Typography.Text strong style={{ fontSize: 13, color: colors.lightGray }}>
{comment.created_by || ''}
</Typography.Text>
<Tooltip title={comment.created_at}>
<Typography.Text style={{ fontSize: 13, color: colors.deepLightGray }}>
{calculateTimeDifference(comment.created_at || '')}
</Typography.Text>
</Tooltip>
</Space>
<Typography.Paragraph
style={{ margin: '8px 0' }}
ellipsis={{ rows: 3, expandable: true }}
>
<div className={`mentions-${theme === 'dark' ? 'dark' : 'light'}`} dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(comment.content || '') }} />
</Typography.Paragraph>
<ConfigProvider
wave={{ disabled: true }}
theme={{
components: {
Button: {
defaultColor: colors.lightGray,
defaultHoverColor: colors.darkGray,
},
},
}}
>
<Button
icon={<DeleteOutlined />}
shape="circle"
type="text"
size='small'
onClick={() => handleDeleteComment(comment.id)}
/>
</ConfigProvider>
</Flex>
</Flex>
))}
</Flex> </Flex>
<Form onFinish={handleAddComment}> <Form onFinish={handleAddComment}>
@@ -218,11 +229,16 @@ const ProjectViewUpdates = () => {
options={mentionsOptions} options={mentionsOptions}
autoSize autoSize
maxLength={MAX_COMMENT_LENGTH} maxLength={MAX_COMMENT_LENGTH}
onSelect={option => memberSelectHandler(option as IMentionMemberSelectOption)} onSelect={(option, prefix) => memberSelectHandler(option as IMentionMemberSelectOption)}
onClick={() => setIsCommentBoxExpand(true)} onClick={() => setIsCommentBoxExpand(true)}
onChange={handleCommentChange} onChange={handleCommentChange}
prefix="@" prefix="@"
split="" split=""
filterOption={(input, option) => {
if (!input) return true;
const optionLabel = (option as any)?.label || '';
return optionLabel.toLowerCase().includes(input.toLowerCase());
}}
style={{ style={{
minHeight: isCommentBoxExpand ? 180 : 60, minHeight: isCommentBoxExpand ? 180 : 60,
paddingBlockEnd: 24, paddingBlockEnd: 24,