Skip to content

Commit 936d2d3

Browse files
Merge pull request #550 from simstudioai/fix/user-presence
fix(userPresence): show avatars by user ID not socket conn id
2 parents c3fe758 + fb40d25 commit 936d2d3

4 files changed

Lines changed: 85 additions & 14 deletions

File tree

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-presence.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
import { useMemo } from 'react'
44
import { useSocket } from '@/contexts/socket-context'
55

6+
// Socket presence user from server
7+
interface SocketPresenceUser {
8+
socketId: string
9+
userId: string
10+
userName: string
11+
cursor?: { x: number; y: number }
12+
selection?: { type: 'block' | 'edge' | 'none'; id?: string }
13+
}
14+
15+
// UI presence user for components
616
type PresenceUser = {
717
connectionId: string | number
818
name?: string
@@ -24,10 +34,17 @@ export function usePresence(): UsePresenceReturn {
2434
const { presenceUsers, isConnected } = useSocket()
2535

2636
const users = useMemo(() => {
27-
return presenceUsers.map((user, index) => ({
28-
// Use socketId directly as connectionId to ensure uniqueness
29-
// If no socketId, use a unique fallback based on userId + index
30-
connectionId: user.socketId || `fallback-${user.userId}-${index}`,
37+
// Deduplicate by userId - only show one presence per unique user
38+
const uniqueUsers = new Map<string, SocketPresenceUser>()
39+
40+
presenceUsers.forEach((user) => {
41+
// Keep the most recent presence for each user (last one wins)
42+
uniqueUsers.set(user.userId, user)
43+
})
44+
45+
return Array.from(uniqueUsers.values()).map((user) => ({
46+
// Use userId as connectionId since we've deduplicated
47+
connectionId: user.userId,
3148
name: user.userName,
3249
color: undefined, // Let the avatar component generate colors
3350
info: user.selection?.type ? `Editing ${user.selection.type}` : undefined,

apps/sim/contexts/socket-context.tsx

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,21 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
101101

102102
// Initialize socket when user is available
103103
useEffect(() => {
104-
if (!user?.id || socket) return
104+
if (!user?.id) return
105+
106+
// Prevent duplicate connections - disconnect existing socket first
107+
if (socket) {
108+
logger.info('Disconnecting existing socket before creating new one')
109+
socket.disconnect()
110+
setSocket(null)
111+
setIsConnected(false)
112+
}
113+
114+
// Prevent multiple simultaneous initialization attempts
115+
if (isConnecting) {
116+
logger.info('Socket initialization already in progress, skipping')
117+
return
118+
}
105119

106120
logger.info('Initializing socket connection for user:', user.id)
107121
setIsConnecting(true)
@@ -282,8 +296,16 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
282296
// Start the socket initialization
283297
initializeSocket()
284298

285-
// Cleanup on unmount
299+
// Cleanup on unmount or user change
286300
return () => {
301+
if (socket) {
302+
logger.info('Cleaning up socket connection')
303+
socket.disconnect()
304+
setSocket(null)
305+
setIsConnected(false)
306+
setIsConnecting(false)
307+
}
308+
287309
positionUpdateTimeouts.current.forEach((timeoutId) => {
288310
clearTimeout(timeoutId)
289311
})
@@ -295,15 +317,30 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
295317
// Join workflow room
296318
const joinWorkflow = useCallback(
297319
(workflowId: string) => {
298-
if (socket && user?.id) {
299-
logger.info(`Joining workflow: ${workflowId}`)
300-
socket.emit('join-workflow', {
301-
workflowId, // Server gets user info from authenticated session
302-
})
303-
setCurrentWorkflowId(workflowId)
320+
if (!socket || !user?.id) {
321+
logger.warn('Cannot join workflow: socket or user not available')
322+
return
304323
}
324+
325+
// Prevent duplicate joins to the same workflow
326+
if (currentWorkflowId === workflowId) {
327+
logger.info(`Already in workflow ${workflowId}, skipping join`)
328+
return
329+
}
330+
331+
// Leave current workflow first if we're in one
332+
if (currentWorkflowId) {
333+
logger.info(`Leaving current workflow ${currentWorkflowId} before joining ${workflowId}`)
334+
socket.emit('leave-workflow')
335+
}
336+
337+
logger.info(`Joining workflow: ${workflowId}`)
338+
socket.emit('join-workflow', {
339+
workflowId, // Server gets user info from authenticated session
340+
})
341+
setCurrentWorkflowId(workflowId)
305342
},
306-
[socket, user]
343+
[socket, user, currentWorkflowId]
307344
)
308345

309346
// Leave current workflow room

apps/sim/socket-server/handlers/workflow.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,9 @@ export function setupWorkflowHandlers(
9797
// Broadcast updated presence list to all users in the room
9898
roomManager.broadcastPresenceUpdate(workflowId)
9999

100+
const uniqueUserCount = roomManager.getUniqueUserCount(workflowId)
100101
logger.info(
101-
`User ${userId} (${userName}) joined workflow ${workflowId}. Room now has ${room.activeConnections} users.`
102+
`User ${userId} (${userName}) joined workflow ${workflowId}. Room now has ${uniqueUserCount} unique users (${room.activeConnections} connections).`
102103
)
103104
} catch (error) {
104105
logger.error('Error joining workflow:', error)

apps/sim/socket-server/rooms/manager.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,20 @@ export class RoomManager {
180180
this.io.to(workflowId).emit('presence-update', roomPresence)
181181
}
182182
}
183+
184+
/**
185+
* Get the number of unique users in a workflow room
186+
* (not the number of socket connections)
187+
*/
188+
getUniqueUserCount(workflowId: string): number {
189+
const room = this.workflowRooms.get(workflowId)
190+
if (!room) return 0
191+
192+
const uniqueUsers = new Set<string>()
193+
room.users.forEach((presence) => {
194+
uniqueUsers.add(presence.userId)
195+
})
196+
197+
return uniqueUsers.size
198+
}
183199
}

0 commit comments

Comments
 (0)