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,45 +1,301 @@
|
||||
<template>
|
||||
<div class="flex items-end gap-2">
|
||||
<!-- Attachment button -->
|
||||
<UButton
|
||||
variant="ghost"
|
||||
icon="i-lucide-paperclip"
|
||||
class="text-[var(--wa-text-muted)]"
|
||||
<div class="space-y-2">
|
||||
<!-- Reply preview -->
|
||||
<div
|
||||
v-if="replyingTo"
|
||||
class="flex items-center gap-2 p-2 rounded-lg bg-[var(--wa-bg-light)]"
|
||||
>
|
||||
<div class="w-1 h-8 rounded-full bg-[var(--wa-green-light)]" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-medium text-[var(--wa-green-light)]">
|
||||
{{ replyingTo.fromMe ? 'Tú' : (replyingTo.pushName || 'Mensaje') }}
|
||||
</p>
|
||||
<p class="text-sm text-[var(--wa-text-muted)] truncate">
|
||||
{{ replyingTo.content || getTypePlaceholder(replyingTo.type) }}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="p-1 rounded-full hover:bg-[var(--wa-border)]"
|
||||
@click="$emit('cancelReply')"
|
||||
>
|
||||
<UIcon name="i-lucide-x" class="w-4 h-4 text-[var(--wa-text-muted)]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Media preview -->
|
||||
<MediaPreview
|
||||
v-if="selectedFiles.length > 0"
|
||||
:files="selectedFiles"
|
||||
:caption="caption"
|
||||
@remove="removeFile"
|
||||
@update:caption="caption = $event"
|
||||
/>
|
||||
|
||||
<!-- Text input -->
|
||||
<div class="flex-1">
|
||||
<UTextarea
|
||||
v-model="message"
|
||||
placeholder="Escribe un mensaje..."
|
||||
:rows="1"
|
||||
autoresize
|
||||
:maxrows="5"
|
||||
class="bg-[var(--wa-bg-light)]"
|
||||
@keydown.enter.exact.prevent="handleSend"
|
||||
<!-- Recording indicator -->
|
||||
<div
|
||||
v-if="isRecording"
|
||||
class="flex items-center gap-3 p-3 rounded-lg bg-red-900/30"
|
||||
>
|
||||
<div class="w-3 h-3 rounded-full bg-red-500 animate-pulse" />
|
||||
<span class="text-red-400 font-medium">{{ formatDuration(recordingDuration) }}</span>
|
||||
<div class="flex-1" />
|
||||
<button
|
||||
class="p-2 rounded-full bg-red-500/20 text-red-400 hover:bg-red-500/30"
|
||||
@click="cancelRecording"
|
||||
title="Cancelar"
|
||||
>
|
||||
<UIcon name="i-lucide-trash-2" class="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
class="p-2 rounded-full bg-green-500 text-white hover:bg-green-600"
|
||||
@click="sendVoiceMessage"
|
||||
title="Enviar"
|
||||
>
|
||||
<UIcon name="i-lucide-send" class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Main input area -->
|
||||
<div
|
||||
v-else
|
||||
class="flex items-end gap-2"
|
||||
:class="{ 'ring-2 ring-[var(--wa-green-light)] rounded-lg': isDragging }"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave="isDragging = false"
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<!-- Attachment button -->
|
||||
<UDropdown :items="attachmentMenuItems" :popper="{ placement: 'top-start' }">
|
||||
<UButton
|
||||
variant="ghost"
|
||||
icon="i-lucide-paperclip"
|
||||
class="text-[var(--wa-text-muted)]"
|
||||
/>
|
||||
</UDropdown>
|
||||
|
||||
<!-- Hidden file inputs -->
|
||||
<input
|
||||
ref="imageInput"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
hidden
|
||||
@change="handleFileSelect($event, 'image')"
|
||||
/>
|
||||
<input
|
||||
ref="videoInput"
|
||||
type="file"
|
||||
accept="video/*"
|
||||
multiple
|
||||
hidden
|
||||
@change="handleFileSelect($event, 'video')"
|
||||
/>
|
||||
<input
|
||||
ref="audioInput"
|
||||
type="file"
|
||||
accept="audio/*"
|
||||
multiple
|
||||
hidden
|
||||
@change="handleFileSelect($event, 'audio')"
|
||||
/>
|
||||
<input
|
||||
ref="documentInput"
|
||||
type="file"
|
||||
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.zip,.rar"
|
||||
multiple
|
||||
hidden
|
||||
@change="handleFileSelect($event, 'document')"
|
||||
/>
|
||||
|
||||
<!-- Text input -->
|
||||
<div class="flex-1">
|
||||
<UTextarea
|
||||
v-model="message"
|
||||
placeholder="Escribe un mensaje..."
|
||||
:rows="1"
|
||||
autoresize
|
||||
:maxrows="5"
|
||||
class="bg-[var(--wa-bg-light)]"
|
||||
@keydown.enter.exact.prevent="handleSend"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Send / Mic button -->
|
||||
<UButton
|
||||
v-if="hasContent"
|
||||
icon="i-lucide-send"
|
||||
@click="handleSend"
|
||||
/>
|
||||
<UButton
|
||||
v-else
|
||||
icon="i-lucide-mic"
|
||||
:disabled="!audioRecorderSupported"
|
||||
@click="startRecording"
|
||||
:title="audioRecorderSupported ? 'Grabar nota de voz' : 'Grabación no soportada'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Send button -->
|
||||
<UButton
|
||||
:icon="message.trim() ? 'i-lucide-send' : 'i-lucide-mic'"
|
||||
:disabled="!message.trim()"
|
||||
@click="handleSend"
|
||||
/>
|
||||
<!-- Drag hint -->
|
||||
<div
|
||||
v-if="isDragging"
|
||||
class="absolute inset-0 flex items-center justify-center bg-black/50 rounded-lg pointer-events-none z-10"
|
||||
>
|
||||
<div class="text-center">
|
||||
<UIcon name="i-lucide-upload" class="w-12 h-12 text-white mb-2" />
|
||||
<p class="text-white font-medium">Suelta los archivos aquí</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Message, MessageType } from '~/types/message'
|
||||
import { getMessageTypePlaceholder } from '~/types/message'
|
||||
import { useAudioRecorder } from '~/composables/useAudioRecorder'
|
||||
|
||||
interface Props {
|
||||
replyingTo?: Message | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
replyingTo: null
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
send: [content: string]
|
||||
send: [content: string, files: File[], caption: string, quotedId?: string]
|
||||
sendVoice: [audioFile: File]
|
||||
cancelReply: []
|
||||
}>()
|
||||
|
||||
// Refs for file inputs
|
||||
const imageInput = ref<HTMLInputElement | null>(null)
|
||||
const videoInput = ref<HTMLInputElement | null>(null)
|
||||
const audioInput = ref<HTMLInputElement | null>(null)
|
||||
const documentInput = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// State
|
||||
const message = ref('')
|
||||
const selectedFiles = ref<File[]>([])
|
||||
const caption = ref('')
|
||||
const isDragging = ref(false)
|
||||
|
||||
// Audio recorder
|
||||
const {
|
||||
isRecording,
|
||||
duration: recordingDuration,
|
||||
isSupported: audioRecorderSupported,
|
||||
startRecording: startAudioRecording,
|
||||
stopRecording,
|
||||
cancelRecording: cancelAudioRecording,
|
||||
getAudioFile,
|
||||
clearAudio
|
||||
} = useAudioRecorder()
|
||||
|
||||
// Computed
|
||||
const hasContent = computed(() => {
|
||||
return message.value.trim().length > 0 || selectedFiles.value.length > 0
|
||||
})
|
||||
|
||||
// Attachment menu
|
||||
const attachmentMenuItems = [
|
||||
[{
|
||||
label: 'Imagen',
|
||||
icon: 'i-lucide-image',
|
||||
click: () => imageInput.value?.click()
|
||||
}],
|
||||
[{
|
||||
label: 'Video',
|
||||
icon: 'i-lucide-video',
|
||||
click: () => videoInput.value?.click()
|
||||
}],
|
||||
[{
|
||||
label: 'Audio',
|
||||
icon: 'i-lucide-music',
|
||||
click: () => audioInput.value?.click()
|
||||
}],
|
||||
[{
|
||||
label: 'Documento',
|
||||
icon: 'i-lucide-file',
|
||||
click: () => documentInput.value?.click()
|
||||
}]
|
||||
]
|
||||
|
||||
// Methods
|
||||
const handleFileSelect = (event: Event, type: string) => {
|
||||
const input = event.target as HTMLInputElement
|
||||
if (input.files) {
|
||||
addFiles(Array.from(input.files))
|
||||
input.value = '' // Reset input
|
||||
}
|
||||
}
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
isDragging.value = false
|
||||
if (event.dataTransfer?.files) {
|
||||
addFiles(Array.from(event.dataTransfer.files))
|
||||
}
|
||||
}
|
||||
|
||||
const addFiles = (files: File[]) => {
|
||||
// Limit to 10 files
|
||||
const remaining = 10 - selectedFiles.value.length
|
||||
const toAdd = files.slice(0, remaining)
|
||||
selectedFiles.value.push(...toAdd)
|
||||
}
|
||||
|
||||
const removeFile = (index: number) => {
|
||||
selectedFiles.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (!message.value.trim()) return
|
||||
if (!hasContent.value) return
|
||||
|
||||
emit('send', message.value)
|
||||
emit(
|
||||
'send',
|
||||
message.value.trim(),
|
||||
[...selectedFiles.value],
|
||||
caption.value,
|
||||
props.replyingTo?.messageId
|
||||
)
|
||||
|
||||
// Clear state
|
||||
message.value = ''
|
||||
selectedFiles.value = []
|
||||
caption.value = ''
|
||||
}
|
||||
|
||||
const startRecording = async () => {
|
||||
const success = await startAudioRecording()
|
||||
if (!success) {
|
||||
// Show error toast
|
||||
console.error('Failed to start recording')
|
||||
}
|
||||
}
|
||||
|
||||
const cancelRecording = () => {
|
||||
cancelAudioRecording()
|
||||
}
|
||||
|
||||
const sendVoiceMessage = () => {
|
||||
stopRecording()
|
||||
|
||||
// Wait a bit for the blob to be ready
|
||||
setTimeout(() => {
|
||||
const audioFile = getAudioFile()
|
||||
if (audioFile) {
|
||||
emit('sendVoice', audioFile)
|
||||
clearAudio()
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const formatDuration = (seconds: number): string => {
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = seconds % 60
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const getTypePlaceholder = (type: MessageType): string => {
|
||||
return getMessageTypePlaceholder(type)
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user