All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
167 lines
4.4 KiB
TypeScript
167 lines
4.4 KiB
TypeScript
/**
|
|
* 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<string | null>) {
|
|
const { on } = useRealtime()
|
|
|
|
// Store of presence states by JID
|
|
const presences = ref<PresenceStore>({})
|
|
|
|
// Debounce timer for composing presence
|
|
let composingTimeout: NodeJS.Timeout | null = null
|
|
|
|
// Subscribe to presence updates for a contact
|
|
const subscribeToPresence = async (jid: string): Promise<void> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
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<void> => {
|
|
await sendPresence(jid, 'recording')
|
|
}
|
|
|
|
// Stop typing/recording indicator
|
|
const stopIndicator = async (jid: string): Promise<void> => {
|
|
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
|
|
}
|
|
}
|