Fix: Usar URL interna para debug webhook receiver (bypass authentik)
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
This commit is contained in:
@@ -27,10 +27,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-sm text-[var(--wa-text-muted)] truncate">{{ chat.lastMessage }}</p>
|
||||
<div class="flex items-center gap-1 text-sm text-[var(--wa-text-muted)] truncate">
|
||||
<UIcon v-if="messageTypeIcon" :name="messageTypeIcon" class="w-4 h-4 flex-shrink-0" />
|
||||
<span class="truncate">{{ lastMessagePreview }}</span>
|
||||
</div>
|
||||
<span
|
||||
v-if="chat.unreadCount > 0"
|
||||
class="bg-[var(--wa-green-light)] text-white text-xs rounded-full px-2 py-0.5"
|
||||
class="bg-[var(--wa-green-light)] text-white text-xs rounded-full px-2 py-0.5 flex-shrink-0"
|
||||
>
|
||||
{{ chat.unreadCount }}
|
||||
</span>
|
||||
@@ -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<Props>()
|
||||
const props = defineProps<Props>()
|
||||
defineEmits<{
|
||||
click: []
|
||||
}>()
|
||||
|
||||
const showDebug = ref(false)
|
||||
|
||||
// Message type to icon mapping
|
||||
const messageTypeIcons: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
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)
|
||||
|
||||
@@ -154,14 +154,22 @@
|
||||
>
|
||||
<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>
|
||||
<div class="relative">
|
||||
<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.stop="showReactionPicker = !showReactionPicker"
|
||||
>
|
||||
<UIcon name="i-lucide-smile-plus" class="w-4 h-4" />
|
||||
</button>
|
||||
<ReactionPicker
|
||||
:visible="showReactionPicker"
|
||||
position="bottom"
|
||||
@select="handleReaction"
|
||||
@close="showReactionPicker = false"
|
||||
/>
|
||||
</div>
|
||||
<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)]'"
|
||||
@@ -206,6 +214,7 @@ import MessageDocument from './content/MessageDocument.vue'
|
||||
import MessageSticker from './content/MessageSticker.vue'
|
||||
import MessageContact from './content/MessageContact.vue'
|
||||
import MessageLocation from './content/MessageLocation.vue'
|
||||
import ReactionPicker from './ReactionPicker.vue'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@@ -217,13 +226,20 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
isGroup: false
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
reply: [message: Message]
|
||||
react: [message: Message]
|
||||
react: [message: Message, emoji: string]
|
||||
scrollToMessage: [id: string]
|
||||
}>()
|
||||
|
||||
const showDebug = ref(false)
|
||||
const showReactionPicker = ref(false)
|
||||
|
||||
// Handle reaction selection
|
||||
const handleReaction = (emoji: string) => {
|
||||
emit('react', props.message, emoji)
|
||||
showReactionPicker.value = false
|
||||
}
|
||||
|
||||
// Computed properties
|
||||
const senderName = computed(() => {
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
:maxrows="5"
|
||||
class="bg-[var(--wa-bg-light)]"
|
||||
@keydown.enter.exact.prevent="handleSend"
|
||||
@input="emit('typing')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
125
app/components/messages/ReactionPicker.vue
Normal file
125
app/components/messages/ReactionPicker.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible"
|
||||
class="absolute z-50 bg-[var(--wa-bg-dark)] rounded-full shadow-lg border border-[var(--wa-border)] p-1 flex items-center gap-1"
|
||||
:class="position === 'top' ? 'bottom-full mb-2' : 'top-full mt-2'"
|
||||
@click.stop
|
||||
>
|
||||
<button
|
||||
v-for="emoji in quickReactions"
|
||||
:key="emoji"
|
||||
class="w-8 h-8 flex items-center justify-center text-lg hover:bg-[var(--wa-bg-light)] rounded-full transition-transform hover:scale-125"
|
||||
@click="selectReaction(emoji)"
|
||||
>
|
||||
{{ emoji }}
|
||||
</button>
|
||||
<button
|
||||
class="w-8 h-8 flex items-center justify-center text-lg hover:bg-[var(--wa-bg-light)] rounded-full"
|
||||
@click="showFullPicker = true"
|
||||
title="Más emojis"
|
||||
>
|
||||
<UIcon name="i-lucide-plus" class="w-4 h-4 text-[var(--wa-text-muted)]" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Full emoji picker modal -->
|
||||
<UModal v-model="showFullPicker">
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-semibold mb-4 text-[var(--wa-text)]">Elegir reacción</h3>
|
||||
|
||||
<div class="grid grid-cols-8 gap-2 max-h-64 overflow-y-auto">
|
||||
<button
|
||||
v-for="emoji in allEmojis"
|
||||
:key="emoji"
|
||||
class="w-10 h-10 flex items-center justify-center text-2xl hover:bg-[var(--wa-bg-light)] rounded transition-transform hover:scale-110"
|
||||
@click="selectReaction(emoji); showFullPicker = false"
|
||||
>
|
||||
{{ emoji }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<UButton variant="ghost" @click="showFullPicker = false">
|
||||
Cancelar
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</UModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
visible: boolean
|
||||
position?: 'top' | 'bottom'
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
position: 'top'
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
select: [emoji: string]
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const showFullPicker = ref(false)
|
||||
|
||||
// Quick reactions (WhatsApp style)
|
||||
const quickReactions = ['👍', '❤️', '😂', '😮', '😢', '🙏']
|
||||
|
||||
// Extended emoji list
|
||||
const allEmojis = [
|
||||
// Smileys
|
||||
'😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂',
|
||||
'🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩',
|
||||
'😘', '😗', '😚', '😙', '🥲', '😋', '😛', '😜',
|
||||
'🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐',
|
||||
'🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬',
|
||||
'😮💨', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷',
|
||||
// Gestures
|
||||
'👍', '👎', '👌', '🤌', '🤏', '✌️', '🤞', '🤟',
|
||||
'🤘', '🤙', '👈', '👉', '👆', '👇', '☝️', '👋',
|
||||
'🤚', '🖐️', '✋', '🖖', '👏', '🙌', '👐', '🤲',
|
||||
'🤝', '🙏', '✍️', '💪', '🦾', '🦿', '🦵', '🦶',
|
||||
// Hearts
|
||||
'❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍',
|
||||
'🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖',
|
||||
'💘', '💝', '💟', '♥️', '💌', '💋', '👄', '👅',
|
||||
// Objects
|
||||
'🎉', '🎊', '🎈', '🎁', '🏆', '🥇', '🥈', '🥉',
|
||||
'⚽', '🏀', '🏈', '⚾', '🎾', '🏐', '🎱', '🎮',
|
||||
'🎵', '🎶', '🎤', '🎧', '🎸', '🎹', '🎺', '🎻',
|
||||
'🍕', '🍔', '🍟', '🌭', '🍿', '🧁', '🍰', '🎂',
|
||||
'☕', '🍵', '🍺', '🍻', '🥂', '🍷', '🥃', '🍸',
|
||||
// Nature
|
||||
'🌸', '🌺', '🌹', '🌷', '🌻', '🌼', '💐', '🌿',
|
||||
'☀️', '🌙', '⭐', '🌟', '✨', '💫', '🔥', '💧',
|
||||
'🌈', '☁️', '⛈️', '❄️', '☃️', '⚡', '🌊', '🌍',
|
||||
// Animals
|
||||
'🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼',
|
||||
'🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🐔',
|
||||
'🦄', '🐝', '🦋', '🐌', '🐞', '🐜', '🦗', '🕷️'
|
||||
]
|
||||
|
||||
const selectReaction = (emoji: string) => {
|
||||
emit('select', emoji)
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// Close on click outside
|
||||
onMounted(() => {
|
||||
const handleClickOutside = () => {
|
||||
emit('close')
|
||||
}
|
||||
|
||||
// Delay to avoid immediate close
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
}, 100)
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user