KI-AGENT: Matrix-Chat live aktualisieren
This commit is contained in:
@@ -22,6 +22,13 @@ type MatrixRoomEvent = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MatrixJoinedMembersResponse = {
|
||||||
|
joined: Record<string, {
|
||||||
|
display_name?: string
|
||||||
|
avatar_url?: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "")
|
const trimTrailingSlash = (value: string) => value.replace(/\/+$/, "")
|
||||||
const readLocalDevRegistrationSharedSecret = () => {
|
const readLocalDevRegistrationSharedSecret = () => {
|
||||||
if (process.env.NODE_ENV === "production") return ""
|
if (process.env.NODE_ENV === "production") return ""
|
||||||
@@ -687,6 +694,10 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/messages?dir=b&limit=50`,
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/messages?dir=b&limit=50`,
|
||||||
session.accessToken
|
session.accessToken
|
||||||
)
|
)
|
||||||
|
const members = await requestMatrixJson<MatrixJoinedMembersResponse>(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/joined_members`,
|
||||||
|
session.accessToken
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
roomId: room.roomId,
|
roomId: room.roomId,
|
||||||
@@ -697,6 +708,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
.map((event) => ({
|
.map((event) => ({
|
||||||
id: event.event_id,
|
id: event.event_id,
|
||||||
sender: event.sender,
|
sender: event.sender,
|
||||||
|
senderDisplayName: members.joined[event.sender]?.display_name || event.sender,
|
||||||
body: event.content?.body || "",
|
body: event.content?.body || "",
|
||||||
timestamp: event.origin_server_ts,
|
timestamp: event.origin_server_ts,
|
||||||
own: event.sender === session.matrixUserId,
|
own: event.sender === session.matrixUserId,
|
||||||
@@ -705,6 +717,33 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getGeneralRoomMembers = async (userId: string, tenantId: number | null) => {
|
||||||
|
const room = await provisionTenantRoom(userId, tenantId, {
|
||||||
|
key: "allgemein",
|
||||||
|
name: "Allgemeiner Chat",
|
||||||
|
})
|
||||||
|
|
||||||
|
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
})
|
||||||
|
const members = await requestMatrixJson<MatrixJoinedMembersResponse>(
|
||||||
|
`/_matrix/client/v3/rooms/${encodeURIComponent(room.roomId)}/joined_members`,
|
||||||
|
session.accessToken
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
roomId: room.roomId,
|
||||||
|
alias: room.alias,
|
||||||
|
members: Object.entries(members.joined).map(([matrixUserId, member]) => ({
|
||||||
|
matrixUserId,
|
||||||
|
displayName: member.display_name || matrixUserId,
|
||||||
|
avatarUrl: member.avatar_url || null,
|
||||||
|
own: matrixUserId === session.matrixUserId,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const sendGeneralRoomMessage = async (
|
const sendGeneralRoomMessage = async (
|
||||||
userId: string,
|
userId: string,
|
||||||
tenantId: number | null,
|
tenantId: number | null,
|
||||||
@@ -746,6 +785,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
return {
|
return {
|
||||||
id: response.event_id,
|
id: response.event_id,
|
||||||
sender: session.matrixUserId,
|
sender: session.matrixUserId,
|
||||||
|
senderDisplayName: await getCurrentUserDisplayName(userId, tenantId),
|
||||||
body: message,
|
body: message,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
own: true,
|
own: true,
|
||||||
@@ -765,6 +805,7 @@ export function matrixService(server: FastifyInstance) {
|
|||||||
provisionTenantRoom,
|
provisionTenantRoom,
|
||||||
createAccessTokenForUser,
|
createAccessTokenForUser,
|
||||||
getGeneralRoomMessages,
|
getGeneralRoomMessages,
|
||||||
|
getGeneralRoomMembers,
|
||||||
sendGeneralRoomMessage,
|
sendGeneralRoomMessage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,17 @@ export default async function communicationRoutes(server: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.get("/communication/matrix/rooms/general/members", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
return await matrix.getGeneralRoomMembers(req.user.user_id, req.user.tenant_id)
|
||||||
|
} catch (err: any) {
|
||||||
|
req.log.error(err)
|
||||||
|
return reply
|
||||||
|
.code(err.statusCode || 500)
|
||||||
|
.send({ error: err.message || "Matrix members failed" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
server.post("/communication/matrix/rooms/general/messages", async (req, reply) => {
|
server.post("/communication/matrix/rooms/general/messages", async (req, reply) => {
|
||||||
try {
|
try {
|
||||||
const body = req.body as { text?: string }
|
const body = req.body as { text?: string }
|
||||||
|
|||||||
@@ -11,14 +11,19 @@ const provisionResult = ref(null)
|
|||||||
const tenantSpaceProvisionResult = ref(null)
|
const tenantSpaceProvisionResult = ref(null)
|
||||||
const generalRoomProvisionResult = ref(null)
|
const generalRoomProvisionResult = ref(null)
|
||||||
const matrixMessages = ref([])
|
const matrixMessages = ref([])
|
||||||
|
const matrixMembers = ref([])
|
||||||
const matrixMessageDraft = ref("")
|
const matrixMessageDraft = ref("")
|
||||||
|
const matrixMessagesViewport = ref(null)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const provisioning = ref(false)
|
const provisioning = ref(false)
|
||||||
const tenantSpaceProvisioning = ref(false)
|
const tenantSpaceProvisioning = ref(false)
|
||||||
const generalRoomProvisioning = ref(false)
|
const generalRoomProvisioning = ref(false)
|
||||||
const matrixMessagesLoading = ref(false)
|
const matrixMessagesLoading = ref(false)
|
||||||
|
const matrixMembersLoading = ref(false)
|
||||||
const matrixMessageSending = ref(false)
|
const matrixMessageSending = ref(false)
|
||||||
|
const matrixAutoRefreshActive = ref(false)
|
||||||
const lastUpdated = ref(null)
|
const lastUpdated = ref(null)
|
||||||
|
let matrixRefreshInterval = null
|
||||||
|
|
||||||
const statusItems = computed(() => [
|
const statusItems = computed(() => [
|
||||||
{
|
{
|
||||||
@@ -72,6 +77,37 @@ const embeddedElementUrl = computed(() => {
|
|||||||
return `${elementBaseUrl.value}/#/room/${encodeURIComponent(activeMatrixTargetId.value)}`
|
return `${elementBaseUrl.value}/#/room/${encodeURIComponent(activeMatrixTargetId.value)}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canUseMatrixChat = computed(() =>
|
||||||
|
Boolean(status.value?.reachable && status.value?.provisioningConfigured)
|
||||||
|
)
|
||||||
|
|
||||||
|
const mergeMatrixMessages = (incomingMessages) => {
|
||||||
|
const byId = new Map()
|
||||||
|
|
||||||
|
for (const message of matrixMessages.value) {
|
||||||
|
byId.set(message.id, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const message of incomingMessages || []) {
|
||||||
|
byId.set(message.id, {
|
||||||
|
...byId.get(message.id),
|
||||||
|
...message,
|
||||||
|
pending: false,
|
||||||
|
failed: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixMessages.value = Array.from(byId.values())
|
||||||
|
.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollMessagesToBottom = async () => {
|
||||||
|
await nextTick()
|
||||||
|
if (!matrixMessagesViewport.value) return
|
||||||
|
|
||||||
|
matrixMessagesViewport.value.scrollTop = matrixMessagesViewport.value.scrollHeight
|
||||||
|
}
|
||||||
|
|
||||||
const loadMatrixInfo = async () => {
|
const loadMatrixInfo = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -88,8 +124,8 @@ const loadMatrixInfo = async () => {
|
|||||||
generalRoom.value = generalRoomRes
|
generalRoom.value = generalRoomRes
|
||||||
lastUpdated.value = new Date()
|
lastUpdated.value = new Date()
|
||||||
|
|
||||||
if (generalRoomRes?.exists) {
|
if (generalRoomRes?.exists && canUseMatrixChat.value) {
|
||||||
await loadGeneralMessages()
|
await loadGeneralChat({ silent: true })
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
toast.add({
|
||||||
@@ -189,7 +225,7 @@ const provisionGeneralRoom = async () => {
|
|||||||
color: "success"
|
color: "success"
|
||||||
})
|
})
|
||||||
|
|
||||||
await loadGeneralMessages()
|
await loadGeneralChat()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Allgemeiner Chat konnte nicht erstellt werden",
|
title: "Allgemeiner Chat konnte nicht erstellt werden",
|
||||||
@@ -200,31 +236,81 @@ const provisionGeneralRoom = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadGeneralMessages = async () => {
|
const loadGeneralMessages = async ({ silent = false } = {}) => {
|
||||||
matrixMessagesLoading.value = true
|
if (!canUseMatrixChat.value) return
|
||||||
|
if (!silent) matrixMessagesLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await $api("/api/communication/matrix/rooms/general/messages")
|
const res = await $api("/api/communication/matrix/rooms/general/messages")
|
||||||
matrixMessages.value = res.messages || []
|
mergeMatrixMessages(res.messages || [])
|
||||||
generalRoom.value = {
|
generalRoom.value = {
|
||||||
...generalRoom.value,
|
...generalRoom.value,
|
||||||
alias: res.alias || generalRoom.value?.alias,
|
alias: res.alias || generalRoom.value?.alias,
|
||||||
exists: true,
|
exists: true,
|
||||||
roomId: res.roomId || generalRoom.value?.roomId
|
roomId: res.roomId || generalRoom.value?.roomId
|
||||||
}
|
}
|
||||||
|
await scrollMessagesToBottom()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({
|
if (!silent) {
|
||||||
title: "Matrix-Nachrichten konnten nicht geladen werden",
|
toast.add({
|
||||||
color: "error"
|
title: "Matrix-Nachrichten konnten nicht geladen werden",
|
||||||
})
|
color: "error"
|
||||||
|
})
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
matrixMessagesLoading.value = false
|
if (!silent) matrixMessagesLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadGeneralMembers = async ({ silent = false } = {}) => {
|
||||||
|
if (!canUseMatrixChat.value) return
|
||||||
|
if (!silent) matrixMembersLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await $api("/api/communication/matrix/rooms/general/members")
|
||||||
|
matrixMembers.value = res.members || []
|
||||||
|
} catch (error) {
|
||||||
|
if (!silent) {
|
||||||
|
toast.add({
|
||||||
|
title: "Matrix-Teilnehmer konnten nicht geladen werden",
|
||||||
|
color: "error"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (!silent) matrixMembersLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadGeneralChat = async ({ silent = false } = {}) => {
|
||||||
|
await Promise.all([
|
||||||
|
loadGeneralMessages({ silent }),
|
||||||
|
loadGeneralMembers({ silent })
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
const sendMatrixMessage = async () => {
|
const sendMatrixMessage = async () => {
|
||||||
const text = matrixMessageDraft.value.trim()
|
const text = matrixMessageDraft.value.trim()
|
||||||
if (!text) return
|
if (!text) return
|
||||||
|
|
||||||
|
const optimisticId = `pending-${Date.now()}`
|
||||||
|
const optimisticMessage = {
|
||||||
|
id: optimisticId,
|
||||||
|
sender: identity.value?.matrixUserId || "Du",
|
||||||
|
senderDisplayName: identity.value?.displayName || "Du",
|
||||||
|
body: text,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
own: true,
|
||||||
|
pending: true,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
|
||||||
|
matrixMessages.value = [
|
||||||
|
...matrixMessages.value,
|
||||||
|
optimisticMessage
|
||||||
|
]
|
||||||
|
matrixMessageDraft.value = ""
|
||||||
|
await scrollMessagesToBottom()
|
||||||
|
|
||||||
matrixMessageSending.value = true
|
matrixMessageSending.value = true
|
||||||
try {
|
try {
|
||||||
const message = await $api("/api/communication/matrix/rooms/general/messages", {
|
const message = await $api("/api/communication/matrix/rooms/general/messages", {
|
||||||
@@ -232,12 +318,14 @@ const sendMatrixMessage = async () => {
|
|||||||
body: { text }
|
body: { text }
|
||||||
})
|
})
|
||||||
|
|
||||||
matrixMessages.value = [
|
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||||
...matrixMessages.value,
|
item.id === optimisticId ? message : item
|
||||||
message
|
)
|
||||||
]
|
await loadGeneralMembers({ silent: true })
|
||||||
matrixMessageDraft.value = ""
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
matrixMessages.value = matrixMessages.value.map((item) =>
|
||||||
|
item.id === optimisticId ? { ...item, pending: false, failed: true } : item
|
||||||
|
)
|
||||||
toast.add({
|
toast.add({
|
||||||
title: "Matrix-Nachricht konnte nicht gesendet werden",
|
title: "Matrix-Nachricht konnte nicht gesendet werden",
|
||||||
color: "error"
|
color: "error"
|
||||||
@@ -247,6 +335,25 @@ const sendMatrixMessage = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const startMatrixAutoRefresh = () => {
|
||||||
|
if (matrixRefreshInterval) return
|
||||||
|
|
||||||
|
matrixAutoRefreshActive.value = true
|
||||||
|
matrixRefreshInterval = window.setInterval(() => {
|
||||||
|
if (!document.hidden && canUseMatrixChat.value) {
|
||||||
|
loadGeneralChat({ silent: true })
|
||||||
|
}
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopMatrixAutoRefresh = () => {
|
||||||
|
if (!matrixRefreshInterval) return
|
||||||
|
|
||||||
|
window.clearInterval(matrixRefreshInterval)
|
||||||
|
matrixRefreshInterval = null
|
||||||
|
matrixAutoRefreshActive.value = false
|
||||||
|
}
|
||||||
|
|
||||||
const formatDateTime = (value) => {
|
const formatDateTime = (value) => {
|
||||||
if (!value) return "-"
|
if (!value) return "-"
|
||||||
|
|
||||||
@@ -265,7 +372,17 @@ const formatMessageTime = (timestamp) => {
|
|||||||
}).format(new Date(timestamp))
|
}).format(new Date(timestamp))
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(loadMatrixInfo)
|
watch(
|
||||||
|
() => matrixMessages.value.length,
|
||||||
|
() => scrollMessagesToBottom()
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadMatrixInfo()
|
||||||
|
startMatrixAutoRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(stopMatrixAutoRefresh)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -566,11 +683,22 @@ onMounted(loadMatrixInfo)
|
|||||||
<div class="flex min-w-0 items-center gap-2">
|
<div class="flex min-w-0 items-center gap-2">
|
||||||
<UIcon name="i-heroicons-chat-bubble-left-right" class="size-5 shrink-0 text-primary" />
|
<UIcon name="i-heroicons-chat-bubble-left-right" class="size-5 shrink-0 text-primary" />
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<h2 class="text-base font-semibold text-highlighted">
|
<div class="flex flex-wrap items-center gap-2">
|
||||||
Matrix-Kommunikation
|
<h2 class="text-base font-semibold text-highlighted">
|
||||||
</h2>
|
Matrix-Kommunikation
|
||||||
|
</h2>
|
||||||
|
<UBadge
|
||||||
|
v-if="matrixAutoRefreshActive"
|
||||||
|
color="success"
|
||||||
|
variant="soft"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Live
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
<p class="mt-1 truncate text-xs text-muted">
|
<p class="mt-1 truncate text-xs text-muted">
|
||||||
{{ generalRoom?.name || generalRoom?.alias || "Allgemeiner Chat" }}
|
{{ generalRoom?.name || generalRoom?.alias || "Allgemeiner Chat" }}
|
||||||
|
<span v-if="matrixMembers.length"> · {{ matrixMembers.length }} Teilnehmer</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -581,8 +709,8 @@ onMounted(loadMatrixInfo)
|
|||||||
color="neutral"
|
color="neutral"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:loading="matrixMessagesLoading"
|
:loading="matrixMessagesLoading"
|
||||||
:disabled="!status?.reachable || !status?.provisioningConfigured"
|
:disabled="!canUseMatrixChat"
|
||||||
@click="loadGeneralMessages"
|
@click="loadGeneralChat"
|
||||||
>
|
>
|
||||||
Nachrichten laden
|
Nachrichten laden
|
||||||
</UButton>
|
</UButton>
|
||||||
@@ -600,7 +728,10 @@ onMounted(loadMatrixInfo)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="flex h-[640px] min-h-[520px] flex-col bg-muted">
|
<div class="flex h-[640px] min-h-[520px] flex-col bg-muted">
|
||||||
<div class="flex-1 space-y-3 overflow-y-auto p-4 sm:p-5">
|
<div
|
||||||
|
ref="matrixMessagesViewport"
|
||||||
|
class="flex-1 space-y-3 overflow-y-auto p-4 sm:p-5"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-if="!matrixMessages.length && !matrixMessagesLoading"
|
v-if="!matrixMessages.length && !matrixMessagesLoading"
|
||||||
class="flex h-full min-h-64 items-center justify-center text-sm text-muted"
|
class="flex h-full min-h-64 items-center justify-center text-sm text-muted"
|
||||||
@@ -616,13 +747,19 @@ onMounted(loadMatrixInfo)
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="max-w-[78%] rounded-lg px-3 py-2 shadow-sm"
|
class="max-w-[78%] rounded-lg px-3 py-2 shadow-sm"
|
||||||
:class="message.own ? 'bg-primary text-inverted' : 'bg-default text-highlighted'"
|
:class="[
|
||||||
|
message.own ? 'bg-primary text-inverted' : 'bg-default text-highlighted',
|
||||||
|
message.pending ? 'opacity-70' : '',
|
||||||
|
message.failed ? 'bg-error text-inverted' : ''
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<div class="mb-1 flex items-center gap-2 text-[11px] opacity-75">
|
<div class="mb-1 flex items-center gap-2 text-[11px] opacity-75">
|
||||||
<span class="truncate font-medium">
|
<span class="truncate font-medium">
|
||||||
{{ message.own ? "Du" : message.sender }}
|
{{ message.own ? "Du" : message.senderDisplayName || message.sender }}
|
||||||
</span>
|
</span>
|
||||||
<span>{{ formatMessageTime(message.timestamp) }}</span>
|
<span>{{ formatMessageTime(message.timestamp) }}</span>
|
||||||
|
<span v-if="message.pending">wird gesendet</span>
|
||||||
|
<span v-if="message.failed">nicht gesendet</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="whitespace-pre-wrap break-words text-sm">
|
<p class="whitespace-pre-wrap break-words text-sm">
|
||||||
{{ message.body }}
|
{{ message.body }}
|
||||||
@@ -639,13 +776,14 @@ onMounted(loadMatrixInfo)
|
|||||||
v-model="matrixMessageDraft"
|
v-model="matrixMessageDraft"
|
||||||
class="min-w-0 flex-1"
|
class="min-w-0 flex-1"
|
||||||
placeholder="Nachricht schreiben"
|
placeholder="Nachricht schreiben"
|
||||||
:disabled="matrixMessageSending || !status?.reachable || !status?.provisioningConfigured"
|
:disabled="matrixMessageSending || !canUseMatrixChat"
|
||||||
|
@keydown.enter.exact.prevent="sendMatrixMessage"
|
||||||
/>
|
/>
|
||||||
<UButton
|
<UButton
|
||||||
type="submit"
|
type="submit"
|
||||||
icon="i-heroicons-paper-airplane"
|
icon="i-heroicons-paper-airplane"
|
||||||
:loading="matrixMessageSending"
|
:loading="matrixMessageSending"
|
||||||
:disabled="!matrixMessageDraft.trim() || !status?.reachable || !status?.provisioningConfigured"
|
:disabled="!matrixMessageDraft.trim() || !canUseMatrixChat"
|
||||||
>
|
>
|
||||||
Senden
|
Senden
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|||||||
Reference in New Issue
Block a user