feat: [US-046] - Presence indicators for active collaborators
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
7278b7527f
commit
f92fc1ad01
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user