feat: [US-046] - Presence indicators for active collaborators

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gustavo Henrique Santos Souza de Miranda
2026-01-23 15:42:07 -03:00
co-authored by Claude Opus 4.5
parent 7278b7527f
commit f92fc1ad01
5 changed files with 118 additions and 8 deletions
+26 -3
View File
@@ -3,9 +3,14 @@ import type { RealtimeChannel } from '@supabase/supabase-js'
export type ConnectionState = 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
export type PresenceUser = {
userId: string
displayName: string
}
type RealtimeCallbacks = {
onConnectionStateChange: (state: ConnectionState) => void
onPresenceSync?: (presences: Record<string, unknown[]>) => void
onPresenceSync?: (users: PresenceUser[]) => void
}
const HEARTBEAT_INTERVAL_MS = 30_000
@@ -19,13 +24,15 @@ export class RealtimeConnection {
private reconnectAttempts = 0
private projectId: string
private userId: string
private displayName: string
private callbacks: RealtimeCallbacks
private isDestroyed = false
private supabase = createClient()
constructor(projectId: string, userId: string, callbacks: RealtimeCallbacks) {
constructor(projectId: string, userId: string, displayName: string, callbacks: RealtimeCallbacks) {
this.projectId = projectId
this.userId = userId
this.displayName = displayName
this.callbacks = callbacks
}
@@ -41,7 +48,18 @@ export class RealtimeConnection {
.on('presence', { event: 'sync' }, () => {
if (this.channel) {
const state = this.channel.presenceState()
this.callbacks.onPresenceSync?.(state)
const users: PresenceUser[] = []
for (const [key, presences] of Object.entries(state)) {
if (key === this.userId) continue // Exclude own presence
const presence = presences[0] as { userId?: string; displayName?: string } | undefined
if (presence?.userId) {
users.push({
userId: presence.userId,
displayName: presence.displayName || 'Anonymous',
})
}
}
this.callbacks.onPresenceSync?.(users)
}
})
.subscribe(async (status) => {
@@ -52,6 +70,11 @@ export class RealtimeConnection {
this.callbacks.onConnectionStateChange('connected')
this.startHeartbeat()
await this.createSession()
// Track presence with user info
await this.channel?.track({
userId: this.userId,
displayName: this.displayName,
})
} else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT') {
this.callbacks.onConnectionStateChange('reconnecting')
this.scheduleReconnect()