diff --git a/worklenz-frontend/src/pages/projects/project-view-1/updates/project-view-updates.tsx b/worklenz-frontend/src/pages/projects/project-view-1/updates/project-view-updates.tsx index dbf66036..171440d2 100644 --- a/worklenz-frontend/src/pages/projects/project-view-1/updates/project-view-updates.tsx +++ b/worklenz-frontend/src/pages/projects/project-view-1/updates/project-view-updates.tsx @@ -1,4 +1,4 @@ -import { Button, ConfigProvider, Flex, Form, Mentions, Skeleton, Space, Tooltip, Typography } from 'antd'; +import { Button, ConfigProvider, Flex, Form, Mentions, Skeleton, Space, Tooltip, Typography, Dropdown, Menu, Popconfirm } from 'antd'; import { useEffect, useState, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import DOMPurify from 'dompurify'; @@ -10,7 +10,6 @@ import { IMentionMemberSelectOption, IMentionMemberViewModel, } from '@/types/project/projectComments.types'; -import { projectsApiService } from '@/api/projects/projects.api.service'; import { projectCommentsApiService } from '@/api/projects/comments/project-comments.api.service'; import { IProjectUpdateCommentViewModel } from '@/types/project/project.types'; import { calculateTimeDifference } from '@/utils/calculate-time-difference'; @@ -21,6 +20,19 @@ import { DeleteOutlined } from '@ant-design/icons'; const MAX_COMMENT_LENGTH = 2000; +// Compile RegExp once for linkify +const urlRegex = /((https?:\/\/|www\.)[\w\-._~:/?#[\]@!$&'()*+,;=%]+)/gi; + +function linkify(text: string): string { + return text.replace( + urlRegex, + url => { + const href = url.startsWith('http') ? url : `https://${url}`; + return `${url}`; + } + ); +} + const ProjectViewUpdates = () => { const { projectId } = useParams(); const [characterLength, setCharacterLength] = useState(0); @@ -88,7 +100,16 @@ const ProjectViewUpdates = () => { const res = await projectCommentsApiService.createProjectComment(body); if (res.done) { - await getComments(); + setComments(prev => [ + ...prev, + { + ...(res.body as IProjectUpdateCommentViewModel), + created_by: getUserSession()?.name || '', + created_at: new Date().toISOString(), + content: commentValue.trim(), + mentions: (res.body as IProjectUpdateCommentViewModel).mentions ?? [undefined, undefined], + } + ]); handleCancel(); } } catch (error) { @@ -155,6 +176,18 @@ const ProjectViewUpdates = () => { [getComments] ); + // Memoize link click handler for comment links + const handleCommentLinkClick = useCallback((e: React.MouseEvent) => { + const target = e.target as HTMLElement; + if (target.tagName === 'A') { + e.preventDefault(); + const href = (target as HTMLAnchorElement).getAttribute('href'); + if (href) { + window.open(href, '_blank', 'noopener,noreferrer'); + } + } + }, []); + const configProviderTheme = useMemo(() => ({ components: { Button: { @@ -164,47 +197,64 @@ const ProjectViewUpdates = () => { }, }), []); - const renderComment = useCallback((comment: IProjectUpdateCommentViewModel) => { - const sanitizedContent = DOMPurify.sanitize(comment.content || ''); - const timeDifference = calculateTimeDifference(comment.created_at || ''); - const themeClass = theme === 'dark' ? 'dark' : 'light'; - - return ( - - - - - - {comment.created_by || ''} - - - - {timeDifference} - - - - -
- - -