KI-AGENT: Live-Sync und Nachrichtenaktionen im Chat ergänzen
This commit is contained in:
@@ -20,6 +20,7 @@ const matrixMessagesViewport = ref(null)
|
||||
const matrixAttachmentInput = ref(null)
|
||||
const matrixAttachmentObjectUrls = ref({})
|
||||
const matrixReplyTarget = ref(null)
|
||||
const matrixEditingMessage = ref(null)
|
||||
const matrixDragActive = ref(false)
|
||||
const memberInviteOpen = ref(false)
|
||||
const memberInviteUserId = ref("")
|
||||
@@ -62,13 +63,14 @@ const matrixAttachmentUploading = ref(false)
|
||||
const matrixAttachmentUploadCount = ref(0)
|
||||
const matrixAutoRefreshActive = ref(false)
|
||||
const lastUpdated = ref(null)
|
||||
let matrixRefreshInterval = null
|
||||
let matrixCallDurationInterval = null
|
||||
let matrixMessagesRequestActive = false
|
||||
let matrixMembersRequestActive = false
|
||||
let matrixLiveKitRoom = null
|
||||
let matrixLiveSyncRunId = 0
|
||||
const matrixCallVideoElements = new Map()
|
||||
const matrixAttachmentPreviewRequests = new Set()
|
||||
const matrixSyncSince = ref("")
|
||||
|
||||
const canUseMatrixChat = computed(() =>
|
||||
Boolean(status.value?.reachable && status.value?.provisioningConfigured)
|
||||
@@ -248,6 +250,7 @@ const setActiveRoom = async (room) => {
|
||||
matrixSearchResults.value = []
|
||||
memberInviteOpen.value = false
|
||||
await loadRoomChat({ includeMembers: true })
|
||||
restartMatrixLiveSync()
|
||||
}
|
||||
|
||||
const mergeRoomIntoLists = (room) => {
|
||||
@@ -286,6 +289,7 @@ const provisionRoomFromList = async (room) => {
|
||||
})
|
||||
|
||||
await loadRoomChat({ includeMembers: true })
|
||||
restartMatrixLiveSync()
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Chatraum konnte nicht erstellt werden",
|
||||
@@ -311,9 +315,11 @@ const mergeMatrixMessages = (incomingMessages) => {
|
||||
}
|
||||
|
||||
for (const message of incomingMessages || []) {
|
||||
const member = matrixMembers.value.find((item) => item.matrixUserId === message.sender)
|
||||
byId.set(message.id, {
|
||||
...byId.get(message.id),
|
||||
...message,
|
||||
senderDisplayName: member?.displayName || message.senderDisplayName,
|
||||
pending: false,
|
||||
failed: false
|
||||
})
|
||||
@@ -324,6 +330,64 @@ const mergeMatrixMessages = (incomingMessages) => {
|
||||
loadAttachmentPreviews()
|
||||
}
|
||||
|
||||
const applyMatrixReplacements = (replacements) => {
|
||||
if (!replacements?.length) return
|
||||
|
||||
const replacementByTarget = new Map()
|
||||
for (const replacement of replacements) {
|
||||
if (!replacement.targetEventId) continue
|
||||
const previous = replacementByTarget.get(replacement.targetEventId)
|
||||
if (!previous || (replacement.timestamp || 0) >= (previous.timestamp || 0)) {
|
||||
replacementByTarget.set(replacement.targetEventId, replacement)
|
||||
}
|
||||
}
|
||||
|
||||
matrixMessages.value = matrixMessages.value.map((message) => {
|
||||
const replacement = replacementByTarget.get(message.id)
|
||||
if (!replacement) return message
|
||||
|
||||
return {
|
||||
...message,
|
||||
body: replacement.body,
|
||||
edited: true,
|
||||
timestamp: replacement.timestamp || message.timestamp
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const applyMatrixReactions = (reactions) => {
|
||||
if (!reactions?.length) return
|
||||
|
||||
matrixMessages.value = matrixMessages.value.map((message) => {
|
||||
const messageReactions = reactions.filter((reaction) => reaction.targetEventId === message.id)
|
||||
if (!messageReactions.length) return message
|
||||
|
||||
let nextReactions = [...(message.reactions || [])]
|
||||
for (const reaction of messageReactions) {
|
||||
const current = nextReactions.find((item) => item.key === reaction.key)
|
||||
nextReactions = current
|
||||
? nextReactions.map((item) => item.key === reaction.key ? {
|
||||
...item,
|
||||
count: item.count + 1,
|
||||
own: item.own || reaction.own
|
||||
} : item)
|
||||
: [...nextReactions, { key: reaction.key, count: 1, own: reaction.own }]
|
||||
}
|
||||
|
||||
return {
|
||||
...message,
|
||||
reactions: nextReactions
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const applyMatrixRedactions = (redactions) => {
|
||||
const redactedIds = new Set((redactions || []).map((redaction) => redaction.targetEventId).filter(Boolean))
|
||||
if (!redactedIds.size) return
|
||||
|
||||
matrixMessages.value = matrixMessages.value.filter((message) => !redactedIds.has(message.id))
|
||||
}
|
||||
|
||||
const findMessage = (messageId) =>
|
||||
matrixMessages.value.find((message) => message.id === messageId)
|
||||
|
||||
@@ -333,6 +397,7 @@ const replyPreview = (message) => {
|
||||
}
|
||||
|
||||
const setReplyTarget = (message) => {
|
||||
matrixEditingMessage.value = null
|
||||
matrixReplyTarget.value = {
|
||||
id: message.id,
|
||||
senderDisplayName: message.own ? "Du" : message.senderDisplayName || message.sender,
|
||||
@@ -344,6 +409,22 @@ const clearReplyTarget = () => {
|
||||
matrixReplyTarget.value = null
|
||||
}
|
||||
|
||||
const beginEditMessage = (message) => {
|
||||
if (!message?.id || !message.own || message.attachment) return
|
||||
|
||||
matrixEditingMessage.value = {
|
||||
id: message.id,
|
||||
body: message.body || ""
|
||||
}
|
||||
matrixMessageDraft.value = message.body || ""
|
||||
clearReplyTarget()
|
||||
}
|
||||
|
||||
const cancelEditMessage = () => {
|
||||
matrixEditingMessage.value = null
|
||||
matrixMessageDraft.value = ""
|
||||
}
|
||||
|
||||
const scrollMessagesToBottom = async () => {
|
||||
await nextTick()
|
||||
if (!matrixMessagesViewport.value) return
|
||||
@@ -485,6 +566,7 @@ const createRoom = async () => {
|
||||
})
|
||||
|
||||
await loadRoomChat({ includeMembers: true })
|
||||
restartMatrixLiveSync()
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Chatraum konnte nicht erstellt werden",
|
||||
@@ -963,6 +1045,29 @@ const sendMatrixMessage = async () => {
|
||||
const text = matrixMessageDraft.value.trim()
|
||||
if (!text || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||
|
||||
if (matrixEditingMessage.value?.id) {
|
||||
const editingId = matrixEditingMessage.value.id
|
||||
matrixMessageSending.value = true
|
||||
try {
|
||||
const replacement = await $api(`${activeRoomEndpoint.value}/messages/${encodeURIComponent(editingId)}`, {
|
||||
method: "PUT",
|
||||
body: { text }
|
||||
})
|
||||
|
||||
applyMatrixReplacements([replacement])
|
||||
matrixMessageDraft.value = ""
|
||||
matrixEditingMessage.value = null
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Nachricht konnte nicht bearbeitet werden",
|
||||
color: "error"
|
||||
})
|
||||
} finally {
|
||||
matrixMessageSending.value = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const optimisticId = `pending-${Date.now()}`
|
||||
const optimisticMessage = {
|
||||
id: optimisticId,
|
||||
@@ -1150,26 +1255,92 @@ const reactToMatrixMessage = async (message, key) => {
|
||||
}
|
||||
}
|
||||
|
||||
const deleteMatrixMessage = async (message) => {
|
||||
if (!message?.id || !message.own || !canUseMatrixChat.value || !activeRoom.value?.exists) return
|
||||
|
||||
try {
|
||||
await $api(`${activeRoomEndpoint.value}/messages/${encodeURIComponent(message.id)}`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
matrixMessages.value = matrixMessages.value.filter((item) => item.id !== message.id)
|
||||
if (matrixEditingMessage.value?.id === message.id) {
|
||||
cancelEditMessage()
|
||||
}
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
title: "Nachricht konnte nicht gelöscht werden",
|
||||
color: "error"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const applyMatrixSync = async (syncResult) => {
|
||||
if (syncResult.nextBatch) {
|
||||
matrixSyncSince.value = syncResult.nextBatch
|
||||
}
|
||||
|
||||
mergeMatrixMessages(syncResult.messages || [])
|
||||
applyMatrixReplacements(syncResult.replacements)
|
||||
applyMatrixReactions(syncResult.reactions)
|
||||
applyMatrixRedactions(syncResult.redactions)
|
||||
|
||||
if ((syncResult.messages || []).length) {
|
||||
await markActiveRoomRead()
|
||||
await scrollMessagesToBottom()
|
||||
}
|
||||
|
||||
if (syncResult.membersChanged) {
|
||||
await loadRoomMembers({ silent: true })
|
||||
}
|
||||
}
|
||||
|
||||
const runMatrixLiveSync = async (runId) => {
|
||||
while (matrixLiveSyncRunId === runId) {
|
||||
if (!canUseMatrixChat.value || !activeRoom.value?.exists || document.hidden) {
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 3000))
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams()
|
||||
if (matrixSyncSince.value) {
|
||||
params.set("since", matrixSyncSince.value)
|
||||
} else {
|
||||
params.set("initial", "1")
|
||||
}
|
||||
|
||||
const syncResult = await $api(`${activeRoomEndpoint.value}/sync?${params.toString()}`)
|
||||
if (matrixLiveSyncRunId !== runId) return
|
||||
|
||||
await applyMatrixSync(syncResult)
|
||||
await loadUnreadCounts()
|
||||
lastUpdated.value = new Date()
|
||||
} catch (error) {
|
||||
if (matrixLiveSyncRunId !== runId) return
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 5000))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const startMatrixAutoRefresh = () => {
|
||||
if (matrixRefreshInterval) return
|
||||
if (matrixAutoRefreshActive.value) return
|
||||
|
||||
matrixAutoRefreshActive.value = true
|
||||
matrixRefreshInterval = window.setInterval(() => {
|
||||
if (!document.hidden && canUseMatrixChat.value && activeRoom.value?.exists) {
|
||||
loadRoomChat({ silent: true })
|
||||
loadUnreadCounts()
|
||||
}
|
||||
}, 15000)
|
||||
matrixLiveSyncRunId += 1
|
||||
runMatrixLiveSync(matrixLiveSyncRunId)
|
||||
}
|
||||
|
||||
const stopMatrixAutoRefresh = () => {
|
||||
if (!matrixRefreshInterval) return
|
||||
|
||||
window.clearInterval(matrixRefreshInterval)
|
||||
matrixRefreshInterval = null
|
||||
matrixLiveSyncRunId += 1
|
||||
matrixAutoRefreshActive.value = false
|
||||
}
|
||||
|
||||
const restartMatrixLiveSync = () => {
|
||||
matrixSyncSince.value = ""
|
||||
stopMatrixAutoRefresh()
|
||||
startMatrixAutoRefresh()
|
||||
}
|
||||
|
||||
const formatMessageTime = (timestamp) => {
|
||||
if (!timestamp) return ""
|
||||
|
||||
@@ -1749,11 +1920,47 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
Antworten
|
||||
</button>
|
||||
<button
|
||||
v-if="message.own && !message.attachment && !message.pending"
|
||||
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="beginEditMessage(message)"
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
v-if="message.own && !message.pending"
|
||||
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="deleteMatrixMessage(message)"
|
||||
>
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="matrixEditingMessage"
|
||||
class="flex shrink-0 items-center gap-3 border-t border-default bg-default px-3 py-2 text-sm"
|
||||
>
|
||||
<UIcon name="i-heroicons-pencil-square" class="size-4 text-muted" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-xs text-muted">Nachricht bearbeiten</p>
|
||||
<p class="truncate text-highlighted">{{ matrixEditingMessage.body }}</p>
|
||||
</div>
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="cancelEditMessage"
|
||||
/>
|
||||
</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"
|
||||
@@ -1795,7 +2002,7 @@ onBeforeUnmount(() => {
|
||||
<UInput
|
||||
v-model="matrixMessageDraft"
|
||||
class="min-w-0 flex-1"
|
||||
:placeholder="matrixAttachmentUploading ? `${matrixAttachmentUploadCount} Anhang/Anhänge werden hochgeladen` : 'Nachricht schreiben'"
|
||||
:placeholder="matrixEditingMessage ? 'Änderung schreiben' : (matrixAttachmentUploading ? `${matrixAttachmentUploadCount} Anhang/Anhänge werden hochgeladen` : 'Nachricht schreiben')"
|
||||
:disabled="!canSendChatInput"
|
||||
@keydown.enter.exact.prevent="sendMatrixMessage"
|
||||
/>
|
||||
@@ -1805,7 +2012,7 @@ onBeforeUnmount(() => {
|
||||
:loading="matrixMessageSending"
|
||||
:disabled="!matrixMessageDraft.trim() || !canSendChatInput"
|
||||
>
|
||||
Senden
|
||||
{{ matrixEditingMessage ? "Speichern" : "Senden" }}
|
||||
</UButton>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user