From f0a2eb843cd0eca5eccc68808130007aa1dc6b34 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Tue, 23 Sep 2025 10:35:16 +0800 Subject: [PATCH 01/27] fix user cursor should not over the panel --- .../workflow/collaboration/components/user-cursors.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/workflow/collaboration/components/user-cursors.tsx b/web/app/components/workflow/collaboration/components/user-cursors.tsx index bcc94960e3..fff4d5bb8c 100644 --- a/web/app/components/workflow/collaboration/components/user-cursors.tsx +++ b/web/app/components/workflow/collaboration/components/user-cursors.tsx @@ -37,7 +37,7 @@ const UserCursors: FC = ({ return (
Date: Tue, 23 Sep 2025 10:46:18 +0800 Subject: [PATCH 02/27] fix avatar inset --- web/app/components/workflow/comment/comment-icon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/app/components/workflow/comment/comment-icon.tsx b/web/app/components/workflow/comment/comment-icon.tsx index 0bced3a6e1..a9834d60a5 100644 --- a/web/app/components/workflow/comment/comment-icon.tsx +++ b/web/app/components/workflow/comment/comment-icon.tsx @@ -48,7 +48,7 @@ export const CommentIcon: FC = memo(({ comment, onClick }) => className={'relative h-10 overflow-hidden rounded-br-full rounded-tl-full rounded-tr-full'} style={{ width: dynamicWidth }} > -
+
Date: Tue, 23 Sep 2025 11:09:02 +0800 Subject: [PATCH 03/27] fix avatar background color --- web/app/components/workflow/comment/thread.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/web/app/components/workflow/comment/thread.tsx b/web/app/components/workflow/comment/thread.tsx index c17a0f18fc..f8292c9abb 100644 --- a/web/app/components/workflow/comment/thread.tsx +++ b/web/app/components/workflow/comment/thread.tsx @@ -11,6 +11,7 @@ import { useFormatTimeFromNow } from '@/app/components/workflow/hooks' import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/service/workflow-comment' import { useAppContext } from '@/context/app-context' import { MentionInput } from './mention-input' +import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color' type CommentThreadProps = { comment: WorkflowCommentDetail @@ -28,12 +29,17 @@ type CommentThreadProps = { } const ThreadMessage: FC<{ + authorId: string authorName: string avatarUrl?: string | null createdAt: number content: string -}> = ({ authorName, avatarUrl, createdAt, content }) => { +}> = ({ authorId, authorName, avatarUrl, createdAt, content }) => { const { formatTimeFromNow } = useFormatTimeFromNow() + const { userProfile } = useAppContext() + const currentUserId = userProfile?.id + const isCurrentUser = authorId === currentUserId + const userColor = isCurrentUser ? undefined : getUserColor(authorId) return (
@@ -43,6 +49,7 @@ const ThreadMessage: FC<{ avatar={avatarUrl || null} size={24} className={cn('h-8 w-8 rounded-full')} + backgroundColor={userColor} />
@@ -183,6 +190,7 @@ export const CommentThread: FC = memo(({
= memo(({
) : ( Date: Tue, 23 Sep 2025 11:24:17 +0800 Subject: [PATCH 04/27] fix mentioned names color --- .../components/workflow/comment/thread.tsx | 90 ++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/web/app/components/workflow/comment/thread.tsx b/web/app/components/workflow/comment/thread.tsx index f8292c9abb..ad5d2955a8 100644 --- a/web/app/components/workflow/comment/thread.tsx +++ b/web/app/components/workflow/comment/thread.tsx @@ -1,6 +1,6 @@ 'use client' -import type { FC } from 'react' +import type { FC, ReactNode } from 'react' import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { useReactFlow, useViewport } from 'reactflow' import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react' @@ -34,13 +34,77 @@ const ThreadMessage: FC<{ avatarUrl?: string | null createdAt: number content: string -}> = ({ authorId, authorName, avatarUrl, createdAt, content }) => { + mentionedNames?: string[] +}> = ({ authorId, authorName, avatarUrl, createdAt, content, mentionedNames }) => { const { formatTimeFromNow } = useFormatTimeFromNow() const { userProfile } = useAppContext() const currentUserId = userProfile?.id const isCurrentUser = authorId === currentUserId const userColor = isCurrentUser ? undefined : getUserColor(authorId) + const highlightedContent = useMemo(() => { + if (!content) + return '' + + const normalizedNames = Array.from(new Set((mentionedNames || []) + .map(name => name.trim()) + .filter(Boolean))) + + if (normalizedNames.length === 0) + return content + + const segments: ReactNode[] = [] + let hasMention = false + let cursor = 0 + + while (cursor < content.length) { + let nextMatchStart = -1 + let matchedName = '' + + for (const name of normalizedNames) { + const searchStart = content.indexOf(`@${name}`, cursor) + if (searchStart === -1) + continue + + const previousChar = searchStart > 0 ? content[searchStart - 1] : '' + if (searchStart > 0 && !/\s/.test(previousChar)) + continue + + if ( + nextMatchStart === -1 + || searchStart < nextMatchStart + || (searchStart === nextMatchStart && name.length > matchedName.length) + ) { + nextMatchStart = searchStart + matchedName = name + } + } + + if (nextMatchStart === -1) + break + + if (nextMatchStart > cursor) + segments.push({content.slice(cursor, nextMatchStart)}) + + const mentionEnd = nextMatchStart + matchedName.length + 1 + segments.push( + + {content.slice(nextMatchStart, mentionEnd)} + , + ) + hasMention = true + cursor = mentionEnd + } + + if (!hasMention) + return content + + if (cursor < content.length) + segments.push({content.slice(cursor)}) + + return segments + }, [content, mentionedNames]) + return (
@@ -58,7 +122,7 @@ const ThreadMessage: FC<{ {formatTimeFromNow(createdAt * 1000)}
- {content} + {highlightedContent}
@@ -127,6 +191,24 @@ export const CommentThread: FC = memo(({ }, [editingReply, onReplyEdit]) const replies = comment.replies || [] + const mentionsByTarget = useMemo(() => { + const map = new Map() + for (const mention of comment.mentions || []) { + const name = mention.mentioned_user_account?.name?.trim() + if (!name) + continue + const key = mention.reply_id ?? 'root' + const existing = map.get(key) + if (existing) { + if (!existing.includes(name)) + existing.push(name) + } + else { + map.set(key, [name]) + } + } + return map + }, [comment.mentions]) return (
= memo(({ avatarUrl={comment.created_by_account?.avatar_url || null} createdAt={comment.created_at} content={comment.content} + mentionedNames={mentionsByTarget.get('root')} /> {replies.length > 0 && (
@@ -261,6 +344,7 @@ export const CommentThread: FC = memo(({ avatarUrl={reply.created_by_account?.avatar_url || null} createdAt={reply.created_at} content={reply.content} + mentionedNames={mentionsByTarget.get(reply.id)} /> )}
From 0f3f8bc0d915e7b39cecfe16be488ac5a110432e Mon Sep 17 00:00:00 2001 From: hjlarry Date: Tue, 23 Sep 2025 11:38:38 +0800 Subject: [PATCH 05/27] make mention input can display name different color --- .../workflow/comment/mention-input.tsx | 85 ++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/web/app/components/workflow/comment/mention-input.tsx b/web/app/components/workflow/comment/mention-input.tsx index 3510ac7aa9..beee0c010c 100644 --- a/web/app/components/workflow/comment/mention-input.tsx +++ b/web/app/components/workflow/comment/mention-input.tsx @@ -1,6 +1,6 @@ 'use client' -import type { FC } from 'react' +import type { FC, ReactNode } from 'react' import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { useParams } from 'next/navigation' @@ -47,6 +47,76 @@ export const MentionInput: FC = memo(({ const [selectedMentionIndex, setSelectedMentionIndex] = useState(0) const [mentionedUserIds, setMentionedUserIds] = useState([]) + const mentionNameList = useMemo(() => { + const names = mentionUsers + .map(user => user.name?.trim()) + .filter((name): name is string => Boolean(name)) + + const uniqueNames = Array.from(new Set(names)) + uniqueNames.sort((a, b) => b.length - a.length) + return uniqueNames + }, [mentionUsers]) + + const highlightedValue = useMemo(() => { + if (!value) + return '' + + if (mentionNameList.length === 0) + return value + + const segments: ReactNode[] = [] + let cursor = 0 + let hasMention = false + + while (cursor < value.length) { + let nextMatchStart = -1 + let matchedName = '' + + for (const name of mentionNameList) { + const searchStart = value.indexOf(`@${name}`, cursor) + if (searchStart === -1) + continue + + const previousChar = searchStart > 0 ? value[searchStart - 1] : '' + if (searchStart > 0 && !/\s/.test(previousChar)) + continue + + if ( + nextMatchStart === -1 + || searchStart < nextMatchStart + || (searchStart === nextMatchStart && name.length > matchedName.length) + ) { + nextMatchStart = searchStart + matchedName = name + } + } + + if (nextMatchStart === -1) + break + + if (nextMatchStart > cursor) + segments.push({value.slice(cursor, nextMatchStart)}) + + const mentionEnd = nextMatchStart + matchedName.length + 1 + segments.push( + + {value.slice(nextMatchStart, mentionEnd)} + , + ) + + hasMention = true + cursor = mentionEnd + } + + if (!hasMention) + return value + + if (cursor < value.length) + segments.push({value.slice(cursor)}) + + return segments + }, [value, mentionNameList]) + const loadMentionableUsers = useCallback(async () => { if (!appId) return try { @@ -220,10 +290,21 @@ export const MentionInput: FC = memo(({ return ( <>
+
+ {highlightedValue} + {'​'} +