Feature: Agregar botón para crear webhook de debug automáticamente
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
- Agregar botón "Crear Webhook de Debug" en WebhookReceiverSection - Detectar si ya existe un webhook apuntando al receptor de debug - Permitir eliminar el webhook de debug - Incluir todos los eventos disponibles al crear el webhook - También incluye mejoras previas de manejo de media y mensajes
This commit is contained in:
@@ -1,33 +1,136 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col"
|
||||
class="flex flex-col group/message"
|
||||
:class="message.fromMe ? 'items-end' : 'items-start'"
|
||||
>
|
||||
<div
|
||||
class="max-w-[70%] rounded-lg px-3 py-2"
|
||||
:class="message.fromMe ? 'bubble-out' : 'bubble-in'"
|
||||
<!-- Sender name for groups -->
|
||||
<span
|
||||
v-if="!message.fromMe && isGroup && senderName"
|
||||
class="text-xs font-medium mb-1 ml-1"
|
||||
:style="{ color: senderColor }"
|
||||
>
|
||||
<!-- Message content -->
|
||||
<p class="text-[var(--wa-text)] whitespace-pre-wrap break-words">{{ message.content }}</p>
|
||||
{{ senderName }}
|
||||
</span>
|
||||
|
||||
<!-- Image -->
|
||||
<img
|
||||
v-if="message.mediaUrl && message.type === 'image'"
|
||||
:src="message.mediaUrl"
|
||||
class="rounded-lg max-w-full mt-2"
|
||||
/>
|
||||
<div
|
||||
class="relative max-w-[70%] rounded-lg overflow-hidden"
|
||||
:class="bubbleClass"
|
||||
>
|
||||
<!-- Quoted message -->
|
||||
<div v-if="message.quoted" class="px-2 pt-2">
|
||||
<MessageQuoted
|
||||
:quoted="message.quoted"
|
||||
@click="$emit('scrollToMessage', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Caption for media -->
|
||||
<p
|
||||
v-if="message.caption"
|
||||
class="text-[var(--wa-text)] mt-2"
|
||||
<!-- Message content based on type -->
|
||||
<div :class="contentPadding">
|
||||
<!-- Text message -->
|
||||
<p
|
||||
v-if="message.type === 'text' && message.content"
|
||||
class="whitespace-pre-wrap break-words"
|
||||
:class="textClass"
|
||||
>
|
||||
{{ message.content }}
|
||||
</p>
|
||||
|
||||
<!-- Image -->
|
||||
<MessageImage
|
||||
v-else-if="message.type === 'image' && message.media"
|
||||
:media="message.media"
|
||||
:instance-id="instanceId"
|
||||
:message-id="message.messageId"
|
||||
/>
|
||||
|
||||
<!-- Video -->
|
||||
<MessageVideo
|
||||
v-else-if="message.type === 'video' && message.media"
|
||||
:media="message.media"
|
||||
:instance-id="instanceId"
|
||||
:message-id="message.messageId"
|
||||
/>
|
||||
|
||||
<!-- Audio -->
|
||||
<MessageAudio
|
||||
v-else-if="message.type === 'audio' && message.media"
|
||||
:media="message.media"
|
||||
:instance-id="instanceId"
|
||||
:message-id="message.messageId"
|
||||
:from-me="message.fromMe"
|
||||
/>
|
||||
|
||||
<!-- Document -->
|
||||
<MessageDocument
|
||||
v-else-if="message.type === 'document' && message.media"
|
||||
:media="message.media"
|
||||
:instance-id="instanceId"
|
||||
:message-id="message.messageId"
|
||||
:from-me="message.fromMe"
|
||||
/>
|
||||
|
||||
<!-- Sticker -->
|
||||
<MessageSticker
|
||||
v-else-if="message.type === 'sticker' && message.media"
|
||||
:media="message.media"
|
||||
:instance-id="instanceId"
|
||||
:message-id="message.messageId"
|
||||
/>
|
||||
|
||||
<!-- Contact -->
|
||||
<MessageContact
|
||||
v-else-if="message.type === 'contact' && message.contact"
|
||||
:contact="message.contact"
|
||||
:from-me="message.fromMe"
|
||||
/>
|
||||
|
||||
<!-- Location -->
|
||||
<MessageLocation
|
||||
v-else-if="message.type === 'location' && message.location"
|
||||
:location="message.location"
|
||||
:from-me="message.fromMe"
|
||||
/>
|
||||
|
||||
<!-- Unknown/unsupported type -->
|
||||
<div
|
||||
v-else-if="message.type === 'unknown' || !message.content"
|
||||
class="flex items-center gap-2 text-sm"
|
||||
:class="message.fromMe ? 'text-white/70' : 'text-[var(--wa-text-muted)]'"
|
||||
>
|
||||
<UIcon name="i-lucide-help-circle" class="w-4 h-4" />
|
||||
<span>Mensaje no soportado</span>
|
||||
</div>
|
||||
|
||||
<!-- Caption for media -->
|
||||
<p
|
||||
v-if="message.caption"
|
||||
class="mt-2 whitespace-pre-wrap break-words"
|
||||
:class="textClass"
|
||||
>
|
||||
{{ message.caption }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Reactions -->
|
||||
<div
|
||||
v-if="message.reactions && message.reactions.length > 0"
|
||||
class="flex flex-wrap gap-1 px-2 pb-1"
|
||||
>
|
||||
{{ message.caption }}
|
||||
</p>
|
||||
<span
|
||||
v-for="(reaction, i) in groupedReactions"
|
||||
:key="i"
|
||||
class="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded-full text-xs"
|
||||
:class="message.fromMe ? 'bg-white/20' : 'bg-[var(--wa-bg-light)]'"
|
||||
:title="reaction.reactors.join(', ')"
|
||||
>
|
||||
<span>{{ reaction.emoji }}</span>
|
||||
<span v-if="reaction.count > 1" class="text-[10px] opacity-70">{{ reaction.count }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="flex items-center justify-end gap-1 mt-1">
|
||||
<span class="text-xs text-[var(--wa-text-muted)]">
|
||||
<!-- Footer with time and status -->
|
||||
<div class="flex items-center justify-end gap-1 px-2 pb-1.5" :class="{ 'pt-1': !message.content && !message.caption }">
|
||||
<span class="text-[11px]" :class="message.fromMe ? 'text-white/60' : 'text-[var(--wa-text-muted)]'">
|
||||
{{ formatTime(message.timestamp) }}
|
||||
</span>
|
||||
<UIcon
|
||||
@@ -36,13 +139,36 @@
|
||||
class="w-4 h-4"
|
||||
:class="statusColor"
|
||||
/>
|
||||
<!-- Debug button -->
|
||||
</div>
|
||||
|
||||
<!-- Action buttons on hover -->
|
||||
<div
|
||||
class="absolute top-1 opacity-0 group-hover/message:opacity-100 transition-opacity flex gap-0.5"
|
||||
:class="message.fromMe ? 'left-1' : 'right-1'"
|
||||
>
|
||||
<button
|
||||
@click="showDebug = !showDebug"
|
||||
class="ml-1 text-xs text-[var(--wa-text-muted)] hover:text-[var(--wa-blue)] opacity-50 hover:opacity-100"
|
||||
title="Debug info"
|
||||
class="p-1 rounded-full hover:bg-black/10"
|
||||
:class="message.fromMe ? 'text-white/60 hover:text-white' : 'text-[var(--wa-text-muted)] hover:text-[var(--wa-text)]'"
|
||||
title="Responder"
|
||||
@click="$emit('reply', message)"
|
||||
>
|
||||
<UIcon name="i-lucide-bug" class="w-3 h-3" />
|
||||
<UIcon name="i-lucide-reply" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1 rounded-full hover:bg-black/10"
|
||||
:class="message.fromMe ? 'text-white/60 hover:text-white' : 'text-[var(--wa-text-muted)] hover:text-[var(--wa-text)]'"
|
||||
title="Reaccionar"
|
||||
@click="$emit('react', message)"
|
||||
>
|
||||
<UIcon name="i-lucide-smile-plus" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1 rounded-full hover:bg-black/10"
|
||||
:class="message.fromMe ? 'text-white/60 hover:text-white' : 'text-[var(--wa-text-muted)] hover:text-[var(--wa-text)]'"
|
||||
title="Debug"
|
||||
@click="showDebug = !showDebug"
|
||||
>
|
||||
<UIcon name="i-lucide-bug" class="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,9 +181,9 @@
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<span class="text-gray-400">Message:</span>
|
||||
<button
|
||||
@click="copyToClipboard(JSON.stringify(message, null, 2))"
|
||||
class="px-2 py-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
||||
title="Copiar al portapapeles"
|
||||
@click="copyToClipboard(JSON.stringify(message, null, 2))"
|
||||
>
|
||||
<UIcon name="i-lucide-copy" class="w-3 h-3" />
|
||||
</button>
|
||||
@@ -68,39 +194,86 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Message {
|
||||
id: string
|
||||
content: string
|
||||
type: 'text' | 'image' | 'video' | 'document' | 'audio'
|
||||
mediaUrl?: string
|
||||
caption?: string
|
||||
fromMe: boolean
|
||||
timestamp: Date
|
||||
status: 'pending' | 'sent' | 'delivered' | 'read' | 'failed'
|
||||
}
|
||||
import type { Message, ReactionInfo } from '~/types/message'
|
||||
import { stringToColor } from '~/types/message'
|
||||
|
||||
// Sub-components
|
||||
import MessageQuoted from './content/MessageQuoted.vue'
|
||||
import MessageImage from './content/MessageImage.vue'
|
||||
import MessageVideo from './content/MessageVideo.vue'
|
||||
import MessageAudio from './content/MessageAudio.vue'
|
||||
import MessageDocument from './content/MessageDocument.vue'
|
||||
import MessageSticker from './content/MessageSticker.vue'
|
||||
import MessageContact from './content/MessageContact.vue'
|
||||
import MessageLocation from './content/MessageLocation.vue'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
instanceId: string
|
||||
isGroup?: boolean
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
isGroup: false
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
reply: [message: Message]
|
||||
react: [message: Message]
|
||||
scrollToMessage: [id: string]
|
||||
}>()
|
||||
|
||||
const showDebug = ref(false)
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
}
|
||||
}
|
||||
// Computed properties
|
||||
const senderName = computed(() => {
|
||||
if (props.message.fromMe) return null
|
||||
return props.message.pushName || props.message.participant?.split('@')[0] || null
|
||||
})
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return new Date(date).toLocaleTimeString('es-AR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
const senderColor = computed(() => {
|
||||
return stringToColor(props.message.participant || props.message.fromJid)
|
||||
})
|
||||
|
||||
const bubbleClass = computed(() => {
|
||||
const base = props.message.fromMe ? 'bubble-out' : 'bubble-in'
|
||||
|
||||
// Stickers don't have background
|
||||
if (props.message.type === 'sticker') {
|
||||
return 'bg-transparent'
|
||||
}
|
||||
|
||||
return base
|
||||
})
|
||||
|
||||
const contentPadding = computed(() => {
|
||||
// Media messages have less padding
|
||||
if (['image', 'video', 'sticker'].includes(props.message.type)) {
|
||||
return 'p-1'
|
||||
}
|
||||
return 'px-3 py-2'
|
||||
})
|
||||
|
||||
const textClass = computed(() => {
|
||||
return props.message.fromMe ? 'text-white' : 'text-[var(--wa-text)]'
|
||||
})
|
||||
|
||||
// Group reactions by emoji
|
||||
const groupedReactions = computed(() => {
|
||||
if (!props.message.reactions) return []
|
||||
|
||||
const groups: Record<string, { emoji: string; count: number; reactors: string[] }> = {}
|
||||
|
||||
for (const reaction of props.message.reactions) {
|
||||
if (!groups[reaction.emoji]) {
|
||||
groups[reaction.emoji] = { emoji: reaction.emoji, count: 0, reactors: [] }
|
||||
}
|
||||
groups[reaction.emoji].count++
|
||||
groups[reaction.emoji].reactors.push(reaction.reactorName || reaction.reactorJid)
|
||||
}
|
||||
|
||||
return Object.values(groups)
|
||||
})
|
||||
|
||||
const statusIcon = computed(() => {
|
||||
const icons: Record<string, string> = {
|
||||
@@ -116,6 +289,34 @@ const statusIcon = computed(() => {
|
||||
const statusColor = computed(() => {
|
||||
if (props.message.status === 'read') return 'text-[var(--wa-blue)]'
|
||||
if (props.message.status === 'failed') return 'text-red-500'
|
||||
return 'text-[var(--wa-text-muted)]'
|
||||
return props.message.fromMe ? 'text-white/60' : 'text-[var(--wa-text-muted)]'
|
||||
})
|
||||
|
||||
// Methods
|
||||
const formatTime = (date: Date) => {
|
||||
return new Date(date).toLocaleTimeString('es-AR', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bubble-in {
|
||||
background-color: var(--wa-surface);
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
|
||||
.bubble-out {
|
||||
background-color: var(--wa-green-dark);
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user