mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user