/** * Composable for managing presence (typing indicators, online status) */ export type PresenceState = 'available' | 'unavailable' | 'composing' | 'recording' | 'paused' interface PresenceInfo { presence: PresenceState | null lastSeen: Date | null } interface PresenceStore { [jid: string]: PresenceInfo } export function usePresence(instanceId: Ref) { const { on } = useRealtime() // Store of presence states by JID const presences = ref({}) // Debounce timer for composing presence let composingTimeout: NodeJS.Timeout | null = null // Subscribe to presence updates for a contact const subscribeToPresence = async (jid: string): Promise => { if (!instanceId.value) return try { await $fetch(`/api/presence/${instanceId.value}/subscribe`, { method: 'POST', body: { jid } }) } catch (error) { console.error('[usePresence] Error subscribing:', error) } } // Send own presence update const sendPresence = async (jid: string, presence: PresenceState): Promise => { if (!instanceId.value) return try { await $fetch(`/api/presence/${instanceId.value}/send`, { method: 'POST', body: { jid, presence } }) } catch (error) { console.error('[usePresence] Error sending presence:', error) } } // Send composing presence with automatic paused after timeout const sendTyping = async (jid: string): Promise => { if (!instanceId.value) return // Clear previous timeout if (composingTimeout) { clearTimeout(composingTimeout) } // Send composing await sendPresence(jid, 'composing') // Auto-pause after 5 seconds of no typing composingTimeout = setTimeout(async () => { await sendPresence(jid, 'paused') }, 5000) } // Send recording presence const sendRecording = async (jid: string): Promise => { await sendPresence(jid, 'recording') } // Stop typing/recording indicator const stopIndicator = async (jid: string): Promise => { if (composingTimeout) { clearTimeout(composingTimeout) composingTimeout = null } await sendPresence(jid, 'paused') } // Get presence for a JID const getPresence = (jid: string): PresenceInfo => { return presences.value[jid] || { presence: null, lastSeen: null } } // Get presence text for display const getPresenceText = (jid: string): string | null => { const presence = getPresence(jid) if (!presence.presence) return null switch (presence.presence) { case 'composing': return 'escribiendo...' case 'recording': return 'grabando audio...' case 'available': return 'en línea' case 'unavailable': if (presence.lastSeen) { return `última vez ${formatLastSeen(presence.lastSeen)}` } return null case 'paused': return null default: return null } } // Format last seen date const formatLastSeen = (date: Date): string => { const now = new Date() const diff = now.getTime() - date.getTime() const minutes = Math.floor(diff / 60000) if (minutes < 1) return 'hace un momento' if (minutes < 60) return `hace ${minutes} min` const hours = Math.floor(minutes / 60) if (hours < 24) return `hace ${hours}h` const days = Math.floor(hours / 24) if (days === 1) return 'ayer' if (days < 7) return `hace ${days} días` return date.toLocaleDateString('es', { day: 'numeric', month: 'short' }) } // Listen for presence updates from SSE onMounted(() => { on('presence.update', (data: any) => { if (data.instanceId !== instanceId.value) return // Update presence store for (const [participantJid, presence] of Object.entries(data.presences)) { const presenceData = presence as { lastKnownPresence: string; lastSeen?: number } presences.value[participantJid] = { presence: presenceData.lastKnownPresence as PresenceState, lastSeen: presenceData.lastSeen ? new Date(presenceData.lastSeen * 1000) : null } } }) }) // Cleanup on unmount onUnmounted(() => { if (composingTimeout) { clearTimeout(composingTimeout) } }) return { presences: readonly(presences), subscribeToPresence, sendPresence, sendTyping, sendRecording, stopIndicator, getPresence, getPresenceText } }