Merge pull request #550 from simstudioai/fix/user-presence

fix(userPresence): show avatars by user ID not socket conn id
This commit is contained in:
Vikhyath Mondreti
2025-06-26 08:00:54 -07:00
committed by GitHub
4 changed files with 85 additions and 14 deletions

View File

@@ -3,6 +3,16 @@
import { useMemo } from 'react'
import { useSocket } from '@/contexts/socket-context'
// Socket presence user from server
interface SocketPresenceUser {
socketId: string
userId: string
userName: string
cursor?: { x: number; y: number }
selection?: { type: 'block' | 'edge' | 'none'; id?: string }
}
// UI presence user for components
type PresenceUser = {
connectionId: string | number
name?: string
@@ -24,10 +34,17 @@ export function usePresence(): UsePresenceReturn {
const { presenceUsers, isConnected } = useSocket()
const users = useMemo(() => {
return presenceUsers.map((user, index) => ({
// Use socketId directly as connectionId to ensure uniqueness
// If no socketId, use a unique fallback based on userId + index
connectionId: user.socketId || `fallback-${user.userId}-${index}`,
// Deduplicate by userId - only show one presence per unique user
const uniqueUsers = new Map<string, SocketPresenceUser>()
presenceUsers.forEach((user) => {
// Keep the most recent presence for each user (last one wins)
uniqueUsers.set(user.userId, user)
})
return Array.from(uniqueUsers.values()).map((user) => ({
// Use userId as connectionId since we've deduplicated
connectionId: user.userId,
name: user.userName,
color: undefined, // Let the avatar component generate colors
info: user.selection?.type ? `Editing ${user.selection.type}` : undefined,

View File

@@ -101,7 +101,21 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
// Initialize socket when user is available
useEffect(() => {
if (!user?.id || socket) return
if (!user?.id) return
// Prevent duplicate connections - disconnect existing socket first
if (socket) {
logger.info('Disconnecting existing socket before creating new one')
socket.disconnect()
setSocket(null)
setIsConnected(false)
}
// Prevent multiple simultaneous initialization attempts
if (isConnecting) {
logger.info('Socket initialization already in progress, skipping')
return
}
logger.info('Initializing socket connection for user:', user.id)
setIsConnecting(true)
@@ -282,8 +296,16 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
// Start the socket initialization
initializeSocket()
// Cleanup on unmount
// Cleanup on unmount or user change
return () => {
if (socket) {
logger.info('Cleaning up socket connection')
socket.disconnect()
setSocket(null)
setIsConnected(false)
setIsConnecting(false)
}
positionUpdateTimeouts.current.forEach((timeoutId) => {
clearTimeout(timeoutId)
})
@@ -295,15 +317,30 @@ export function SocketProvider({ children, user }: SocketProviderProps) {
// Join workflow room
const joinWorkflow = useCallback(
(workflowId: string) => {
if (socket && user?.id) {
logger.info(`Joining workflow: ${workflowId}`)
socket.emit('join-workflow', {
workflowId, // Server gets user info from authenticated session
})
setCurrentWorkflowId(workflowId)
if (!socket || !user?.id) {
logger.warn('Cannot join workflow: socket or user not available')
return
}
// Prevent duplicate joins to the same workflow
if (currentWorkflowId === workflowId) {
logger.info(`Already in workflow ${workflowId}, skipping join`)
return
}
// Leave current workflow first if we're in one
if (currentWorkflowId) {
logger.info(`Leaving current workflow ${currentWorkflowId} before joining ${workflowId}`)
socket.emit('leave-workflow')
}
logger.info(`Joining workflow: ${workflowId}`)
socket.emit('join-workflow', {
workflowId, // Server gets user info from authenticated session
})
setCurrentWorkflowId(workflowId)
},
[socket, user]
[socket, user, currentWorkflowId]
)
// Leave current workflow room

View File

@@ -97,8 +97,9 @@ export function setupWorkflowHandlers(
// Broadcast updated presence list to all users in the room
roomManager.broadcastPresenceUpdate(workflowId)
const uniqueUserCount = roomManager.getUniqueUserCount(workflowId)
logger.info(
`User ${userId} (${userName}) joined workflow ${workflowId}. Room now has ${room.activeConnections} users.`
`User ${userId} (${userName}) joined workflow ${workflowId}. Room now has ${uniqueUserCount} unique users (${room.activeConnections} connections).`
)
} catch (error) {
logger.error('Error joining workflow:', error)

View File

@@ -180,4 +180,20 @@ export class RoomManager {
this.io.to(workflowId).emit('presence-update', roomPresence)
}
}
/**
* Get the number of unique users in a workflow room
* (not the number of socket connections)
*/
getUniqueUserCount(workflowId: string): number {
const room = this.workflowRooms.get(workflowId)
if (!room) return 0
const uniqueUsers = new Set<string>()
room.users.forEach((presence) => {
uniqueUsers.add(presence.userId)
})
return uniqueUsers.size
}
}