Unificar endpoint de envío y agregar soporte para stickers
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 2m28s

- Consolidar send.post.ts y send-media.post.ts en un único endpoint /send
- Agregar servicio sticker-processor.ts para convertir imágenes a WebP 512x512
- Agregar toggle Imagen/Sticker en MediaPreview para enviar imágenes como stickers
- Actualizar MessageInput y página de mensajes para usar endpoint unificado
- Instalar dependencia sharp para procesamiento de imágenes
This commit is contained in:
2025-12-04 09:33:03 -06:00
parent 09d3c5398a
commit ec40cd6826
8 changed files with 458 additions and 291 deletions

View File

@@ -7,15 +7,27 @@
:key="index"
class="relative group"
>
<!-- Image preview -->
<!-- Image preview with sticker toggle -->
<div
v-if="isImage(file)"
class="relative"
>
<img
:src="getPreviewUrl(file)"
class="h-20 w-20 object-cover rounded-lg"
class="h-20 w-20 object-cover rounded-lg transition-all"
:class="{ 'ring-2 ring-[var(--wa-green)]': stickerModes[index] }"
/>
<!-- Sticker toggle button -->
<button
class="absolute bottom-0 left-0 right-0 py-0.5 text-[10px] font-medium transition-colors"
:class="stickerModes[index]
? 'bg-[var(--wa-green)] text-white rounded-b-lg'
: 'bg-black/60 text-white/80 rounded-b-lg hover:bg-black/80'"
@click="toggleStickerMode(index)"
>
{{ stickerModes[index] ? 'Sticker' : 'Imagen' }}
</button>
<!-- Remove button -->
<button
class="absolute -top-2 -right-2 w-5 h-5 rounded-full bg-red-500 text-white flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
@click="$emit('remove', index)"
@@ -81,16 +93,21 @@
</div>
</div>
<!-- Caption input -->
<div v-if="showCaption && files.length > 0">
<!-- Caption input (hide if all files are stickers) -->
<div v-if="showCaption && files.length > 0 && !allStickers">
<UInput
:model-value="caption"
placeholder="Agregar descripción..."
placeholder="Agregar descripcion..."
size="sm"
class="bg-[var(--wa-bg-light)]"
@update:model-value="$emit('update:caption', $event)"
/>
</div>
<!-- Info text for stickers -->
<p v-if="hasStickers" class="text-xs text-[var(--wa-text-muted)]">
Los stickers se convertiran a formato 512x512 WebP
</p>
</div>
</template>
@@ -101,16 +118,45 @@ interface Props {
showCaption?: boolean
}
withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<Props>(), {
caption: '',
showCaption: true
})
defineEmits<{
const emit = defineEmits<{
remove: [index: number]
'update:caption': [value: string]
'update:stickerModes': [modes: boolean[]]
}>()
// Track which images should be sent as stickers
const stickerModes = ref<boolean[]>([])
// Initialize sticker modes when files change
watch(() => props.files, (files) => {
// Preserve existing modes and add false for new files
const newModes = files.map((_, i) => stickerModes.value[i] ?? false)
stickerModes.value = newModes
emit('update:stickerModes', [...newModes])
}, { immediate: true })
const toggleStickerMode = (index: number) => {
stickerModes.value[index] = !stickerModes.value[index]
emit('update:stickerModes', [...stickerModes.value])
}
// Check if all files are stickers (images marked as sticker)
const allStickers = computed(() => {
return props.files.length > 0 && props.files.every((f, i) =>
isImage(f) && stickerModes.value[i]
)
})
// Check if any file is marked as sticker
const hasStickers = computed(() => {
return props.files.some((f, i) => isImage(f) && stickerModes.value[i])
})
// Preview URL cache
const previewUrls = new Map<File, string>()

View File

@@ -29,6 +29,7 @@
:caption="caption"
@remove="removeFile"
@update:caption="caption = $event"
@update:stickerModes="stickerModes = $event"
/>
<!-- Recording indicator -->
@@ -163,7 +164,7 @@ const props = withDefaults(defineProps<Props>(), {
})
const emit = defineEmits<{
send: [content: string, files: File[], caption: string, quotedId?: string]
send: [content: string, files: File[], caption: string, quotedId?: string, stickerModes?: boolean[]]
sendVoice: [audioFile: File]
cancelReply: []
typing: []
@@ -181,6 +182,7 @@ const message = ref('')
const selectedFiles = ref<File[]>([])
const caption = ref('')
const isDragging = ref(false)
const stickerModes = ref<boolean[]>([])
// Audio recorder
const {
@@ -258,13 +260,15 @@ const handleSend = () => {
message.value.trim(),
[...selectedFiles.value],
caption.value,
props.replyingTo?.messageId
props.replyingTo?.messageId,
[...stickerModes.value]
)
// Clear state
message.value = ''
selectedFiles.value = []
caption.value = ''
stickerModes.value = []
}
const startRecording = async () => {

View File

@@ -496,14 +496,15 @@ const copyToClipboard = async (text: string) => {
// Reply state (will be used for quote functionality)
const replyingTo = ref<any>(null)
const handleSendMessage = async (content: string, files: File[], caption: string, quotedId?: string) => {
const handleSendMessage = async (content: string, files: File[], caption: string, quotedId?: string, stickerModes?: boolean[]) => {
if (!selectedInstance.value?.value || !selectedChat.value) return
try {
const instanceId = selectedInstance.value.value
const chatId = selectedChat.value.id
const endpoint = `/api/messages/${instanceId}/${chatId}/send`
// If we have files, send as media
// If we have files, send as media (with optional sticker flags)
if (files.length > 0) {
const formData = new FormData()
@@ -522,13 +523,18 @@ const handleSendMessage = async (content: string, files: File[], caption: string
formData.append('quotedMessageId', quotedId || replyingTo.value.messageId)
}
await $fetch(`/api/messages/${instanceId}/${chatId}/send-media`, {
// Add sticker modes if any images should be sent as stickers
if (stickerModes && stickerModes.length > 0) {
formData.append('asSticker', JSON.stringify(stickerModes))
}
await $fetch(endpoint, {
method: 'POST',
body: formData
})
} else if (content) {
// Send text message
await $fetch(`/api/messages/${instanceId}/${chatId}/send`, {
await $fetch(endpoint, {
method: 'POST',
body: {
content,
@@ -559,7 +565,7 @@ const handleSendVoice = async (audioFile: File) => {
formData.append('files', audioFile)
formData.append('isPtt', 'true') // Mark as push-to-talk (voice note)
await $fetch(`/api/messages/${instanceId}/${chatId}/send-media`, {
await $fetch(`/api/messages/${instanceId}/${chatId}/send`, {
method: 'POST',
body: formData
})