From 8f44826e64aac1fa27561b691c8f06e5715e674c Mon Sep 17 00:00:00 2001 From: josedario87 Date: Tue, 2 Dec 2025 21:27:45 -0600 Subject: [PATCH] Fix: Usar URL interna para debug webhook receiver (bypass authentik) --- app/components/messages/ChatItem.vue | 55 +++++- app/components/messages/MessageBubble.vue | 36 ++-- app/components/messages/MessageInput.vue | 10 +- app/components/messages/ReactionPicker.vue | 125 +++++++++++++ app/composables/usePresence.ts | 166 ++++++++++++++++++ app/pages/messages/index.vue | 62 ++++++- server/api/events/stream.get.ts | 6 + server/api/messages/[instanceId]/chats.get.ts | 1 + .../api/messages/[instanceId]/react.post.ts | 62 +++++++ server/api/presence/[instanceId]/[jid].get.ts | 42 +++++ server/api/presence/[instanceId]/send.post.ts | 44 +++++ .../presence/[instanceId]/subscribe.post.ts | 33 ++++ server/api/webhooks/[id]/test.post.ts | 11 +- server/services/webhooks/dispatcher.ts | 10 +- 14 files changed, 642 insertions(+), 21 deletions(-) create mode 100644 app/components/messages/ReactionPicker.vue create mode 100644 app/composables/usePresence.ts create mode 100644 server/api/messages/[instanceId]/react.post.ts create mode 100644 server/api/presence/[instanceId]/[jid].get.ts create mode 100644 server/api/presence/[instanceId]/send.post.ts create mode 100644 server/api/presence/[instanceId]/subscribe.post.ts diff --git a/app/components/messages/ChatItem.vue b/app/components/messages/ChatItem.vue index 79f4574..0bdc273 100644 --- a/app/components/messages/ChatItem.vue +++ b/app/components/messages/ChatItem.vue @@ -27,10 +27,13 @@
- + @@ -66,7 +69,9 @@ interface Chat { profilePicture?: string lastMessage: string lastMessageAt: Date + lastMessageType?: string unreadCount: number + isGroup?: boolean } interface Props { @@ -74,13 +79,57 @@ interface Props { active?: boolean } -defineProps() +const props = defineProps() defineEmits<{ click: [] }>() const showDebug = ref(false) +// Message type to icon mapping +const messageTypeIcons: Record = { + image: 'i-lucide-image', + video: 'i-lucide-video', + audio: 'i-lucide-music', + document: 'i-lucide-file', + sticker: 'i-lucide-sticker', + contact: 'i-lucide-contact', + location: 'i-lucide-map-pin' +} + +// Message type to placeholder text +const messageTypePlaceholders: Record = { + image: 'Foto', + video: 'Video', + audio: 'Audio', + document: 'Documento', + sticker: 'Sticker', + contact: 'Contacto', + location: 'Ubicaciรณn' +} + +const messageTypeIcon = computed(() => { + const type = props.chat.lastMessageType + if (!type || type === 'text') return null + return messageTypeIcons[type] || null +}) + +const lastMessagePreview = computed(() => { + const type = props.chat.lastMessageType + + // If there's text content, show it + if (props.chat.lastMessage) { + return props.chat.lastMessage + } + + // Otherwise show placeholder based on type + if (type && messageTypePlaceholders[type]) { + return messageTypePlaceholders[type] + } + + return '' +}) + const copyToClipboard = async (text: string) => { try { await navigator.clipboard.writeText(text) diff --git a/app/components/messages/MessageBubble.vue b/app/components/messages/MessageBubble.vue index a1fce7e..239a4ed 100644 --- a/app/components/messages/MessageBubble.vue +++ b/app/components/messages/MessageBubble.vue @@ -154,14 +154,22 @@ > - +
+ + +
@@ -165,6 +166,8 @@ const emit = defineEmits<{ send: [content: string, files: File[], caption: string, quotedId?: string] sendVoice: [audioFile: File] cancelReply: [] + typing: [] + recording: [isRecording: boolean] }>() // Refs for file inputs @@ -266,18 +269,21 @@ const handleSend = () => { const startRecording = async () => { const success = await startAudioRecording() - if (!success) { - // Show error toast + if (success) { + emit('recording', true) + } else { console.error('Failed to start recording') } } const cancelRecording = () => { cancelAudioRecording() + emit('recording', false) } const sendVoiceMessage = () => { stopRecording() + emit('recording', false) // Wait a bit for the blob to be ready setTimeout(() => { diff --git a/app/components/messages/ReactionPicker.vue b/app/components/messages/ReactionPicker.vue new file mode 100644 index 0000000..b6939da --- /dev/null +++ b/app/components/messages/ReactionPicker.vue @@ -0,0 +1,125 @@ + + + diff --git a/app/composables/usePresence.ts b/app/composables/usePresence.ts new file mode 100644 index 0000000..1d8d2ae --- /dev/null +++ b/app/composables/usePresence.ts @@ -0,0 +1,166 @@ +/** + * 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 + } +} diff --git a/app/pages/messages/index.vue b/app/pages/messages/index.vue index c1daafb..26e922c 100644 --- a/app/pages/messages/index.vue +++ b/app/pages/messages/index.vue @@ -131,7 +131,10 @@
- + +