From 7ea6f836b177c1f2c40d8e4c969baefa38234847 Mon Sep 17 00:00:00 2001 From: waleed Date: Wed, 12 Nov 2025 09:28:22 -0800 Subject: [PATCH] fix(presence): fix additional avatars showing for presence --- .../control-bar/components/index.ts | 1 - .../components/user-avatar/user-avatar.tsx | 83 ------------ .../user-avatar-stack/user-avatar-stack.tsx | 106 ---------------- .../w/[workflowId]/hooks/use-presence.ts | 63 ---------- .../[workspaceId]/w/[workflowId]/workflow.tsx | 4 - .../workflow-item/avatars/avatars.tsx | 119 +++++++++++------- 6 files changed, 72 insertions(+), 304 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/index.ts index ae7a07da35..bee94a265e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/index.ts @@ -2,5 +2,4 @@ export { DeployModal } from './deploy-modal/deploy-modal' export { DeploymentControls } from './deployment-controls/deployment-controls' export { ExportControls } from './export-controls/export-controls' export { TemplateModal } from './template-modal/template-modal' -export { UserAvatarStack } from './user-avatar-stack/user-avatar-stack' export { WebhookSettings } from './webhook-settings/webhook-settings' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx deleted file mode 100644 index 6a6e3d628a..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar.tsx +++ /dev/null @@ -1,83 +0,0 @@ -'use client' - -import { type CSSProperties, useMemo } from 'react' -import Image from 'next/image' -import { Tooltip } from '@/components/emcn' -import { getPresenceColors } from '@/lib/collaboration/presence-colors' - -interface AvatarProps { - connectionId: string | number - name?: string - color?: string - avatarUrl?: string | null - tooltipContent?: React.ReactNode | null - size?: 'sm' | 'md' | 'lg' - index?: number // Position in stack for z-index -} - -export function UserAvatar({ - connectionId, - name, - color, - avatarUrl, - tooltipContent, - size = 'md', - index = 0, -}: AvatarProps) { - const { gradient } = useMemo(() => getPresenceColors(connectionId, color), [connectionId, color]) - - const sizeClass = { - sm: 'h-5 w-5 text-[10px]', - md: 'h-7 w-7 text-xs', - lg: 'h-9 w-9 text-sm', - }[size] - - const pixelSize = { - sm: 20, - md: 28, - lg: 36, - }[size] - - const initials = name ? name.charAt(0).toUpperCase() : '?' - const hasAvatar = Boolean(avatarUrl) - - const avatarElement = ( -
- {hasAvatar && avatarUrl ? ( - {name - ) : ( - initials - )} -
- ) - - if (tooltipContent) { - return ( - - {avatarElement} - - {tooltipContent} - - - ) - } - - return avatarElement -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx deleted file mode 100644 index ced2f666d6..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack.tsx +++ /dev/null @@ -1,106 +0,0 @@ -'use client' - -import { useMemo } from 'react' -import { cn } from '@/lib/utils' -import { UserAvatar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/components/user-avatar/user-avatar' -import { usePresence } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence' - -interface User { - connectionId: string | number - name?: string - color?: string - info?: string - avatarUrl?: string | null -} - -interface UserAvatarStackProps { - users?: User[] - maxVisible?: number - size?: 'sm' | 'md' | 'lg' - className?: string -} - -export function UserAvatarStack({ - users: propUsers, - maxVisible = 3, - size = 'md', - className = '', -}: UserAvatarStackProps) { - // Use presence data if no users are provided via props - const { users: presenceUsers } = usePresence() - const users = propUsers || presenceUsers - - // Get operation error state from collaborative workflow - // Memoize the processed users to avoid unnecessary re-renders - const { visibleUsers, overflowCount } = useMemo(() => { - if (users.length === 0) { - return { visibleUsers: [], overflowCount: 0 } - } - - const visible = users.slice(0, maxVisible) - const overflow = Math.max(0, users.length - maxVisible) - - return { - visibleUsers: visible, - overflowCount: overflow, - } - }, [users, maxVisible]) - - // Determine spacing based on size - const spacingClass = { - sm: '-space-x-1', - md: '-space-x-1.5', - lg: '-space-x-2', - }[size] - - const shouldShowAvatars = visibleUsers.length > 0 - - return ( -
- {shouldShowAvatars && ( -
- {visibleUsers.map((user, index) => ( - -
{user.name}
- {user.info && ( -
{user.info}
- )} -
- ) : null - } - /> - ))} - - {overflowCount > 0 && ( - -
- {overflowCount} more user{overflowCount > 1 ? 's' : ''} -
-
- {users.length} total online -
-
- } - /> - )} - - )} - - ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts deleted file mode 100644 index bf92b21ad7..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts +++ /dev/null @@ -1,63 +0,0 @@ -'use client' - -import { useMemo } from 'react' -import { useSession } from '@/lib/auth-client' -import { useSocket } from '@/contexts/socket-context' - -interface SocketPresenceUser { - socketId: string - userId: string - userName: string - avatarUrl?: string | null - cursor?: { x: number; y: number } | null - selection?: { type: 'block' | 'edge' | 'none'; id?: string } -} - -type PresenceUser = { - connectionId: string | number - name?: string - color?: string - info?: string - avatarUrl?: string | null -} - -interface UsePresenceReturn { - users: PresenceUser[] - currentUser: PresenceUser | null - isConnected: boolean -} - -/** - * Hook for managing user presence in collaborative workflows using Socket.IO - * Uses the existing Socket context to get real presence data - * Filters out the current user so only other collaborators are shown - */ -export function usePresence(): UsePresenceReturn { - const { presenceUsers, isConnected } = useSocket() - const { data: session } = useSession() - const currentUserId = session?.user?.id - - const users = useMemo(() => { - const uniqueUsers = new Map() - - presenceUsers.forEach((user) => { - uniqueUsers.set(user.userId, user) - }) - - return Array.from(uniqueUsers.values()) - .filter((user) => user.userId !== currentUserId) - .map((user) => ({ - connectionId: user.userId, - name: user.userName, - color: undefined, - info: user.selection?.type ? `Editing ${user.selection.type}` : undefined, - avatarUrl: user.avatarUrl, - })) - }, [presenceUsers, currentUserId]) - - return { - users, - currentUser: null, - isConnected, - } -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index a7495f5de1..7c477859a3 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -23,7 +23,6 @@ import { TrainingControls, } from '@/app/workspace/[workspaceId]/w/[workflowId]/components' import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat' -import { UserAvatarStack } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/components/user-avatar-stack/user-avatar-stack' import { Cursors } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/cursors/cursors' import { ErrorBoundary } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/error/index' import { NoteBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/note-block/note-block' @@ -2006,7 +2005,6 @@ const WorkflowContent = React.memo(() => {
-
@@ -2020,8 +2018,6 @@ const WorkflowContent = React.memo(() => { {/* Training Controls - for recording workflow edits */} - - void } +interface PresenceUser { + socketId: string + userId: string + userName?: string + avatarUrl?: string | null +} + +interface UserAvatarProps { + user: PresenceUser + index: number +} + +/** + * Individual user avatar with error handling for image loading. + * Falls back to colored circle with initials if image fails to load. + */ +function UserAvatar({ user, index }: UserAvatarProps) { + const [imageError, setImageError] = useState(false) + const color = getUserColor(user.userId) + const initials = user.userName ? user.userName.charAt(0).toUpperCase() : '?' + const hasAvatar = Boolean(user.avatarUrl) && !imageError + + // Reset error state when avatar URL changes + useEffect(() => { + setImageError(false) + }, [user.avatarUrl]) + + const avatarElement = ( +
+ {hasAvatar && user.avatarUrl ? ( + {user.userName setImageError(true)} + /> + ) : ( + initials + )} +
+ ) + + if (user.userName) { + return ( + + {avatarElement} + + {user.userName} + + + ) + } + + return avatarElement +} + /** * Displays user avatars for presence in a workflow item. - * Consolidated logic from user-avatar-stack and user-avatar components. * Only shows avatars for the currently active workflow. * * @param props - Component props @@ -69,51 +136,9 @@ export function Avatars({ workflowId, maxVisible = 3, onPresenceChange }: Avatar return (
- {visibleUsers.map((user, index) => { - const color = getUserColor(user.userId) - const initials = user.userName ? user.userName.charAt(0).toUpperCase() : '?' - const hasAvatar = Boolean(user.avatarUrl) - - const avatarElement = ( -
- {hasAvatar && user.avatarUrl ? ( - {user.userName - ) : ( - initials - )} -
- ) - - if (user.userName) { - return ( - - {avatarElement} - - {user.userName} - - - ) - } - - return avatarElement - })} + {visibleUsers.map((user, index) => ( + + ))} {overflowCount > 0 && (