All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m10s
- Usar spread operator en lugar de push() para que Vue detecte cambios - Agregar console.logs temporales para debug
317 lines
8.2 KiB
Vue
317 lines
8.2 KiB
Vue
<template>
|
|
<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"
|
|
@update:stickerModes="stickerModes = $event"
|
|
/>
|
|
|
|
<!-- 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 -->
|
|
<UDropdownMenu :items="attachmentMenuItems" :content="{ side: 'top', align: 'start' }">
|
|
<UButton
|
|
variant="ghost"
|
|
icon="i-lucide-paperclip"
|
|
class="text-[var(--wa-text-muted)]"
|
|
/>
|
|
</UDropdownMenu>
|
|
|
|
<!-- 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"
|
|
@input="emit('typing')"
|
|
/>
|
|
</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>
|
|
|
|
<!-- 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, files: File[], caption: string, quotedId?: string, stickerModes?: boolean[]]
|
|
sendVoice: [audioFile: File]
|
|
cancelReply: []
|
|
typing: []
|
|
recording: [isRecording: boolean]
|
|
}>()
|
|
|
|
// 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)
|
|
const stickerModes = ref<boolean[]>([])
|
|
|
|
// 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',
|
|
onSelect: () => imageInput.value?.click()
|
|
}],
|
|
[{
|
|
label: 'Video',
|
|
icon: 'i-lucide-video',
|
|
onSelect: () => videoInput.value?.click()
|
|
}],
|
|
[{
|
|
label: 'Audio',
|
|
icon: 'i-lucide-music',
|
|
onSelect: () => audioInput.value?.click()
|
|
}],
|
|
[{
|
|
label: 'Documento',
|
|
icon: 'i-lucide-file',
|
|
onSelect: () => documentInput.value?.click()
|
|
}]
|
|
]
|
|
|
|
// Methods
|
|
const handleFileSelect = (event: Event, type: string) => {
|
|
console.log('handleFileSelect called', type, event)
|
|
const input = event.target as HTMLInputElement
|
|
console.log('input.files:', input.files)
|
|
if (input.files) {
|
|
addFiles(Array.from(input.files))
|
|
input.value = '' // Reset input
|
|
}
|
|
}
|
|
|
|
const addFiles = (files: File[]) => {
|
|
console.log('addFiles called with:', files)
|
|
// Limit to 10 files
|
|
const remaining = 10 - selectedFiles.value.length
|
|
const toAdd = files.slice(0, remaining)
|
|
// Use spread operator to trigger reactivity
|
|
selectedFiles.value = [...selectedFiles.value, ...toAdd]
|
|
console.log('selectedFiles after adding:', selectedFiles.value)
|
|
}
|
|
|
|
const handleDrop = (event: DragEvent) => {
|
|
isDragging.value = false
|
|
if (event.dataTransfer?.files) {
|
|
addFiles(Array.from(event.dataTransfer.files))
|
|
}
|
|
}
|
|
|
|
const removeFile = (index: number) => {
|
|
selectedFiles.value.splice(index, 1)
|
|
}
|
|
|
|
const handleSend = () => {
|
|
if (!hasContent.value) return
|
|
|
|
emit(
|
|
'send',
|
|
message.value.trim(),
|
|
[...selectedFiles.value],
|
|
caption.value,
|
|
props.replyingTo?.messageId,
|
|
[...stickerModes.value]
|
|
)
|
|
|
|
// Clear state
|
|
message.value = ''
|
|
selectedFiles.value = []
|
|
caption.value = ''
|
|
stickerModes.value = []
|
|
}
|
|
|
|
const startRecording = async () => {
|
|
const success = await startAudioRecording()
|
|
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(() => {
|
|
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>
|