feat(communication): add chat reactions emojis and images

This commit is contained in:
2026-10-03 17:56:07 +02:00
parent 59538f46b7
commit 29a9ec2242
25 changed files with 737 additions and 57 deletions

View File

@@ -7,7 +7,7 @@ const router = useRouter()
const baseRooms = ref([]), projectRooms = ref([]), directRooms = ref([]), collaborationRooms = ref([])
const invitations = ref([]), invitationBusy = ref(''), collaborationError = ref(''), listError = ref('')
const unreadRooms = ref({}), messages = ref([]), members = ref([]), draft = ref('')
const unreadRooms = ref({}), messages = ref([]), members = ref([]), draft = ref(''), draftImage = ref(null)
const requestedRoom = () => typeof route.query.room === 'string' ? route.query.room : 'allgemein'
const activeRoomKey = ref(requestedRoom())
const loading = ref(true), roomLoading = ref(false), sending = ref(false), creating = ref(false)
@@ -49,7 +49,7 @@ const activeRoom = computed(() => visibleRooms.value.find(room => room.key === a
const isCollaboration = computed(() => activeRoom.value?.source === 'collaboration')
const isInternal = computed(() => activeRoom.value?.source === 'internal')
function clearMessages() { messages.value = []; members.value = []; draft.value = ''; syncAfterId = 0; roomLoading.value = false; sending.value = false }
function clearMessages() { messages.value = []; members.value = []; draft.value = ''; draftImage.value = null; syncAfterId = 0; roomLoading.value = false; sending.value = false }
function mergeMessages(incoming) {
const byId = new Map(messages.value.map(message => [message.id, message]))
for (const message of incoming || []) byId.set(message.id, message)
@@ -208,13 +208,13 @@ async function respondToInvitation(id, accept) {
finally { if (currentScope(scope)) invitationBusy.value = '' }
}
async function sendMessage() {
const text = draft.value.trim(), request = selection()
if (!text || sending.value || !isInternal.value || !activeRoom.value?.exists) return
const text = draft.value.trim(), image = draftImage.value, request = selection()
if ((!text && !image) || sending.value || !isInternal.value || !activeRoom.value?.exists) return
sending.value = true; draft.value = ''
try {
const message = await $api(nativeEndpoint(request.key, '/messages'), { method: 'POST', body: { text } })
const message = await $api(nativeEndpoint(request.key, '/messages'), { method: 'POST', body: { text, ...(image ? { image } : {}) } })
if (!currentSelection(request)) return
mergeMessages([message]); await markRead(request); await scrollToBottom(request)
draftImage.value = null; mergeMessages([message]); await markRead(request); await scrollToBottom(request)
} catch (error) {
if (currentSelection(request)) { draft.value = text; toast.add({ title: errorMessage(error), color: 'error' }) }
} finally { if (currentSelection(request)) sending.value = false }
@@ -371,21 +371,22 @@ onBeforeUnmount(() => {
<p class="font-medium">Noch keine Nachrichten</p>
<p class="text-sm">Beginne die Unterhaltung.</p>
</div>
<div v-else class="space-y-4">
<ChatReactions v-else :key="`${session}:${activeRoomKey}`" v-slot="{ reactions, react, busy }" :api="$api" :endpoint="nativeEndpoint(activeRoomKey)" :messages="messages" @error="error => toast.add({ title: errorMessage(error), color: 'error' })">
<div class="space-y-4">
<div v-for="message in messages" :key="message.id" class="flex" :class="message.own ? 'justify-end' : 'justify-start'">
<div class="max-w-[80%] rounded-2xl px-4 py-2" :class="message.own ? 'bg-primary-600 text-white' : 'bg-gray-100 text-gray-900'">
<div v-if="!message.own" class="mb-1 text-xs font-semibold text-primary-600">{{ message.senderDisplayName }}</div>
<p class="whitespace-pre-wrap break-words text-sm">{{ message.body }}</p>
<ChatImage v-if="message.image" :api="$api" :endpoint="nativeEndpoint(activeRoomKey)" :message="message" />
<p v-if="message.body" class="whitespace-pre-wrap break-words text-sm">{{ message.body }}</p>
<div class="mt-1 text-right text-[11px]" :class="message.own ? 'text-primary-100' : 'text-gray-400'">{{ formatMessageTime(message.timestamp) }}</div>
<ChatReactionBar :reactions="reactions[message.id] || []" :busy="busy" @react="react(message.id, $event)" />
</div>
</div>
</div>
</ChatReactions>
</div>
<form class="flex items-end gap-3 border-t border-gray-200 p-4" @submit.prevent="sendMessage">
<UTextarea v-model="draft" class="flex-1" autoresize :rows="1" :maxrows="6" placeholder="Nachricht schreiben …" :disabled="!activeRoom?.exists" @keydown.enter.exact.prevent="sendMessage" />
<UButton type="submit" icon="i-heroicons-paper-airplane" :loading="sending" :disabled="!draft.trim() || !activeRoom?.exists">Senden</UButton>
</form>
<ChatComposer :key="`${session}:${activeRoomKey}`" v-model="draft" v-model:image="draftImage" :busy="sending" :disabled="!activeRoom?.exists || roomLoading" @send="sendMessage" />
</main>
<aside v-if="!isCollaboration" class="hidden min-h-0 overflow-y-auto border-l border-gray-200 p-4 lg:block">