KI-AGENT: Chat Anhänge und Nachrichteninteraktionen abrunden
This commit is contained in:
@@ -18,6 +18,8 @@ const matrixMessageDraft = ref("")
|
||||
const matrixMessagesViewport = ref(null)
|
||||
const matrixAttachmentInput = ref(null)
|
||||
const matrixAttachmentObjectUrls = ref({})
|
||||
const matrixReplyTarget = ref(null)
|
||||
const matrixDragActive = ref(false)
|
||||
const roomCreateOpen = ref(false)
|
||||
const collapsedRoomGroups = ref({})
|
||||
const matrixCallOpen = ref(false)
|
||||
@@ -49,6 +51,7 @@ const matrixMessagesLoading = ref(false)
|
||||
const matrixMembersLoading = ref(false)
|
||||
const matrixMessageSending = ref(false)
|
||||
const matrixAttachmentUploading = ref(false)
|
||||
const matrixAttachmentUploadCount = ref(0)
|
||||
const matrixAutoRefreshActive = ref(false)
|
||||
const lastUpdated = ref(null)
|
||||
let matrixRefreshInterval = null
|
||||
@@ -86,6 +89,10 @@ const canStartMatrixCall = computed(() =>
|
||||
Boolean(canUseMatrixChat.value && activeRoom.value?.exists)
|
||||
)
|
||||
|
||||
const canSendChatInput = computed(() =>
|
||||
Boolean(canUseMatrixChat.value && activeRoom.value?.exists && !matrixMessageSending.value && !matrixAttachmentUploading.value)
|
||||
)
|
||||
|
||||
const matrixCallTitle = computed(() =>
|
||||
matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz"
|
||||
)
|
||||
@@ -291,6 +298,26 @@ const mergeMatrixMessages = (incomingMessages) => {
|
||||
loadAttachmentPreviews()
|
||||
}
|
||||
|
||||
const findMessage = (messageId) =>
|
||||
matrixMessages.value.find((message) => message.id === messageId)
|
||||
|
||||
const replyPreview = (message) => {
|
||||
if (!message) return ""
|
||||
return message.body || message.attachment?.fileName || "Nachricht"
|
||||
}
|
||||
|
||||
const setReplyTarget = (message) => {
|
||||
matrixReplyTarget.value = {
|
||||
id: message.id,
|
||||
senderDisplayName: message.own ? "Du" : message.senderDisplayName || message.sender,
|
||||
body: replyPreview(message)
|
||||
}
|
||||
}
|
||||
|
||||
const clearReplyTarget = () => {
|
||||
matrixReplyTarget.value = null
|
||||
}
|
||||
|
||||
const scrollMessagesToBottom = async () => {
|
||||
await nextTick()
|
||||
if (!matrixMessagesViewport.value) return
|
||||
@@ -313,8 +340,12 @@ const markActiveRoomRead = async () => {
|
||||
if (!canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||
|
||||
try {
|
||||
const latestMessage = matrixMessages.value.at(-1)
|
||||
await $api(`${activeRoomEndpoint.value}/read`, {
|
||||
method: "POST"
|
||||
method: "POST",
|
||||
body: {
|
||||
eventId: latestMessage?.id
|
||||
}
|
||||
})
|
||||
unreadRooms.value = {
|
||||
...unreadRooms.value,
|
||||
@@ -825,6 +856,7 @@ const sendMatrixMessage = async () => {
|
||||
sender: identity.value?.matrixUserId || "Du",
|
||||
senderDisplayName: identity.value?.displayName || "Du",
|
||||
body: text,
|
||||
replyToEventId: matrixReplyTarget.value?.id || null,
|
||||
timestamp: Date.now(),
|
||||
own: true,
|
||||
pending: true,
|
||||
@@ -842,12 +874,16 @@ const sendMatrixMessage = async () => {
|
||||
try {
|
||||
const message = await $api(`${activeRoomEndpoint.value}/messages`, {
|
||||
method: "POST",
|
||||
body: { text }
|
||||
body: {
|
||||
text,
|
||||
replyToEventId: matrixReplyTarget.value?.id
|
||||
}
|
||||
})
|
||||
|
||||
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||
item.id === optimisticId ? message : item
|
||||
)
|
||||
clearReplyTarget()
|
||||
loadAttachmentPreviews()
|
||||
} catch (error) {
|
||||
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||
@@ -867,12 +903,18 @@ const openAttachmentPicker = () => {
|
||||
matrixAttachmentInput.value?.click()
|
||||
}
|
||||
|
||||
const uploadMatrixAttachment = async (event) => {
|
||||
const file = event.target.files?.[0]
|
||||
event.target.value = ""
|
||||
|
||||
const uploadSingleMatrixAttachment = async (file) => {
|
||||
if (!file || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||
|
||||
if (file.size > 25 * 1024 * 1024) {
|
||||
toast.add({
|
||||
title: "Anhang ist zu groß",
|
||||
description: "Maximal erlaubt sind 25 MB.",
|
||||
color: "warning"
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const optimisticId = `attachment-${Date.now()}`
|
||||
const optimisticMessage = {
|
||||
id: optimisticId,
|
||||
@@ -900,7 +942,6 @@ const uploadMatrixAttachment = async (event) => {
|
||||
const formData = new FormData()
|
||||
formData.append("file", file)
|
||||
|
||||
matrixAttachmentUploading.value = true
|
||||
try {
|
||||
const message = await $api(`${activeRoomEndpoint.value}/attachments`, {
|
||||
method: "POST",
|
||||
@@ -918,8 +959,81 @@ const uploadMatrixAttachment = async (event) => {
|
||||
title: "Anhang konnte nicht gesendet werden",
|
||||
color: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const uploadMatrixFiles = async (files) => {
|
||||
const fileList = Array.from(files || [])
|
||||
if (!fileList.length || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||
|
||||
matrixAttachmentUploading.value = true
|
||||
matrixAttachmentUploadCount.value = fileList.length
|
||||
|
||||
try {
|
||||
for (const file of fileList) {
|
||||
await uploadSingleMatrixAttachment(file)
|
||||
}
|
||||
} finally {
|
||||
matrixAttachmentUploading.value = false
|
||||
matrixAttachmentUploadCount.value = 0
|
||||
matrixDragActive.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const uploadMatrixAttachment = async (event) => {
|
||||
const files = event.target.files
|
||||
event.target.value = ""
|
||||
await uploadMatrixFiles(files)
|
||||
}
|
||||
|
||||
const handleAttachmentDrop = async (event) => {
|
||||
matrixDragActive.value = false
|
||||
await uploadMatrixFiles(event.dataTransfer?.files)
|
||||
}
|
||||
|
||||
const downloadMatrixAttachment = async (attachment) => {
|
||||
if (!attachment?.url) return
|
||||
|
||||
try {
|
||||
const blob = await $api(matrixMediaProxyUrl(attachment), {
|
||||
responseType: "blob"
|
||||
})
|
||||
const objectUrl = URL.createObjectURL(blob)
|
||||
const link = document.createElement("a")
|
||||
link.href = objectUrl
|
||||
link.download = attachment.fileName || "Anhang"
|
||||
link.click()
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Anhang konnte nicht geladen werden",
|
||||
color: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const reactToMatrixMessage = async (message, key) => {
|
||||
if (!message?.id || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||
|
||||
try {
|
||||
await $api(`${activeRoomEndpoint.value}/messages/${encodeURIComponent(message.id)}/reactions`, {
|
||||
method: "POST",
|
||||
body: { key }
|
||||
})
|
||||
const existing = message.reactions || []
|
||||
const current = existing.find((reaction) => reaction.key === key)
|
||||
const reactions = current
|
||||
? existing.map((reaction) => reaction.key === key ? { ...reaction, count: reaction.count + 1, own: true } : reaction)
|
||||
: [...existing, { key, count: 1, own: true }]
|
||||
|
||||
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||
item.id === message.id ? { ...item, reactions } : item
|
||||
)
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Reaktion konnte nicht gesendet werden",
|
||||
color: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1400,8 +1514,19 @@ onBeforeUnmount(() => {
|
||||
|
||||
<div
|
||||
ref="matrixMessagesViewport"
|
||||
class="min-h-0 flex-1 space-y-3 overflow-y-auto p-4 sm:p-5"
|
||||
class="relative min-h-0 flex-1 space-y-3 overflow-y-auto p-4 sm:p-5"
|
||||
:class="matrixDragActive ? 'ring-2 ring-inset ring-primary' : ''"
|
||||
@dragenter.prevent="matrixDragActive = true"
|
||||
@dragover.prevent="matrixDragActive = true"
|
||||
@dragleave.prevent="matrixDragActive = false"
|
||||
@drop.prevent="handleAttachmentDrop"
|
||||
>
|
||||
<div
|
||||
v-if="matrixDragActive"
|
||||
class="pointer-events-none absolute inset-4 z-10 flex items-center justify-center rounded-lg border-2 border-dashed border-primary bg-primary/10 text-sm font-medium text-primary"
|
||||
>
|
||||
Dateien hier ablegen
|
||||
</div>
|
||||
<div
|
||||
v-if="!matrixMessages.length && !matrixMessagesLoading"
|
||||
class="flex h-full min-h-64 items-center justify-center text-sm text-muted"
|
||||
@@ -1430,7 +1555,17 @@ onBeforeUnmount(() => {
|
||||
<span>{{ formatMessageTime(message.timestamp) }}</span>
|
||||
<span v-if="message.pending">wird gesendet</span>
|
||||
<span v-if="message.failed">nicht gesendet</span>
|
||||
<span v-if="message.edited">bearbeitet</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="message.replyToEventId"
|
||||
type="button"
|
||||
class="mb-2 block w-full rounded border px-2 py-1 text-left text-xs opacity-80"
|
||||
:class="message.own ? 'border-white/20' : 'border-default'"
|
||||
>
|
||||
Antwort auf {{ findMessage(message.replyToEventId)?.senderDisplayName || "Nachricht" }}:
|
||||
{{ replyPreview(findMessage(message.replyToEventId)) }}
|
||||
</button>
|
||||
<p class="whitespace-pre-wrap break-words text-sm">
|
||||
{{ message.body }}
|
||||
</p>
|
||||
@@ -1445,12 +1580,11 @@ onBeforeUnmount(() => {
|
||||
:alt="message.attachment.fileName || message.body"
|
||||
class="max-h-72 w-full object-contain"
|
||||
>
|
||||
<a
|
||||
<button
|
||||
v-if="message.attachment.url"
|
||||
:href="matrixMediaProxyUrl(message.attachment)"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="flex items-center gap-2 px-3 py-2 text-sm"
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm"
|
||||
@click="downloadMatrixAttachment(message.attachment)"
|
||||
>
|
||||
<UIcon name="i-heroicons-paper-clip" class="size-4 shrink-0" />
|
||||
<span class="min-w-0 flex-1 truncate">
|
||||
@@ -1459,7 +1593,7 @@ onBeforeUnmount(() => {
|
||||
<span class="shrink-0 text-xs opacity-70">
|
||||
{{ formatAttachmentSize(message.attachment.size) }}
|
||||
</span>
|
||||
</a>
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center gap-2 px-3 py-2 text-sm"
|
||||
@@ -1473,10 +1607,58 @@ onBeforeUnmount(() => {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-wrap items-center gap-1">
|
||||
<button
|
||||
v-for="reaction in message.reactions || []"
|
||||
:key="reaction.key"
|
||||
type="button"
|
||||
class="rounded-full px-2 py-0.5 text-xs"
|
||||
:class="reaction.own ? 'bg-white/20' : 'bg-muted'"
|
||||
@click="reactToMatrixMessage(message, reaction.key)"
|
||||
>
|
||||
{{ reaction.key }} {{ reaction.count }}
|
||||
</button>
|
||||
<button
|
||||
v-for="reactionKey in ['👍', '✅', '👀']"
|
||||
:key="reactionKey"
|
||||
type="button"
|
||||
class="rounded-full px-2 py-0.5 text-xs opacity-70 hover:opacity-100"
|
||||
:class="message.own ? 'bg-white/10' : 'bg-muted'"
|
||||
@click="reactToMatrixMessage(message, reactionKey)"
|
||||
>
|
||||
{{ reactionKey }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full px-2 py-0.5 text-xs opacity-70 hover:opacity-100"
|
||||
:class="message.own ? 'bg-white/10' : 'bg-muted'"
|
||||
@click="setReplyTarget(message)"
|
||||
>
|
||||
Antworten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="matrixReplyTarget"
|
||||
class="flex shrink-0 items-center gap-3 border-t border-default bg-default px-3 py-2 text-sm"
|
||||
>
|
||||
<UIcon name="i-heroicons-arrow-uturn-left" class="size-4 text-muted" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-xs text-muted">Antwort an {{ matrixReplyTarget.senderDisplayName }}</p>
|
||||
<p class="truncate text-highlighted">{{ matrixReplyTarget.body }}</p>
|
||||
</div>
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="clearReplyTarget"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form
|
||||
class="flex shrink-0 gap-2 border-t border-default bg-default p-3"
|
||||
@submit.prevent="sendMatrixMessage"
|
||||
@@ -1484,6 +1666,7 @@ onBeforeUnmount(() => {
|
||||
<input
|
||||
ref="matrixAttachmentInput"
|
||||
type="file"
|
||||
multiple
|
||||
class="hidden"
|
||||
@change="uploadMatrixAttachment"
|
||||
>
|
||||
@@ -1499,15 +1682,15 @@ onBeforeUnmount(() => {
|
||||
<UInput
|
||||
v-model="matrixMessageDraft"
|
||||
class="min-w-0 flex-1"
|
||||
placeholder="Nachricht schreiben"
|
||||
:disabled="matrixMessageSending || matrixAttachmentUploading || !canUseMatrixChat || !activeRoom?.exists"
|
||||
:placeholder="matrixAttachmentUploading ? `${matrixAttachmentUploadCount} Anhang/Anhänge werden hochgeladen` : 'Nachricht schreiben'"
|
||||
:disabled="!canSendChatInput"
|
||||
@keydown.enter.exact.prevent="sendMatrixMessage"
|
||||
/>
|
||||
<UButton
|
||||
type="submit"
|
||||
icon="i-heroicons-paper-airplane"
|
||||
:loading="matrixMessageSending"
|
||||
:disabled="!matrixMessageDraft.trim() || matrixAttachmentUploading || !canUseMatrixChat || !activeRoom?.exists"
|
||||
:disabled="!matrixMessageDraft.trim() || !canSendChatInput"
|
||||
>
|
||||
Senden
|
||||
</UButton>
|
||||
|
||||
Reference in New Issue
Block a user