KI-AGENT: Matrix-Raum-API verallgemeinern

This commit is contained in:
2026-05-18 18:12:26 +02:00
parent 8824b1c9c8
commit f33ccf730a
3 changed files with 286 additions and 118 deletions

View File

@@ -35,6 +35,12 @@ type MatrixUserSession = {
validUntilMs: number validUntilMs: number
} }
type MatrixTenantRoomOptions = {
key?: string
name?: string
topic?: string
}
type MatrixCachedValue<T = any> = { type MatrixCachedValue<T = any> = {
exists: true exists: true
cachedUntil: number cachedUntil: number
@@ -48,6 +54,13 @@ const matrixTenantSpaceCache = new Map<string, MatrixCachedValue>()
const matrixTenantRoomCache = new Map<string, MatrixCachedValue>() const matrixTenantRoomCache = new Map<string, MatrixCachedValue>()
let matrixServiceSessionCache: MatrixUserSession | null = null let matrixServiceSessionCache: MatrixUserSession | null = null
const defaultTenantRooms: Required<Pick<MatrixTenantRoomOptions, "key" | "name">>[] = [
{
key: "allgemein",
name: "Allgemeiner Chat",
},
]
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 ""
@@ -173,6 +186,19 @@ export function matrixService(server: FastifyInstance) {
roomKey: string roomKey: string
) => `#${tenantRoomAliasLocalpart(tenant, roomKey)}:${serverName()}` ) => `#${tenantRoomAliasLocalpart(tenant, roomKey)}:${serverName()}`
const normalizeTenantRoomOptions = (options: MatrixTenantRoomOptions = {}) => {
const fallbackName = options.key || options.name || "Allgemeiner Chat"
const key = normalizeMatrixAliasSeed(options.key || fallbackName)
const name = (options.name || fallbackName).trim() || "Allgemeiner Chat"
const topic = options.topic?.trim()
return {
key,
name,
topic,
}
}
const buildSharedSecretMac = ( const buildSharedSecretMac = (
nonce: string, nonce: string,
username: string, username: string,
@@ -637,16 +663,13 @@ export function matrixService(server: FastifyInstance) {
const provisionTenantRoom = async ( const provisionTenantRoom = async (
userId: string, userId: string,
tenantId: number | null, tenantId: number | null,
options: { options: MatrixTenantRoomOptions = {}
key?: string
name?: string
topic?: string
} = {}
) => { ) => {
const tenant = await getCurrentTenant(tenantId) const tenant = await getCurrentTenant(tenantId)
const key = normalizeMatrixAliasSeed(options.key || options.name || "allgemein") const normalizedOptions = normalizeTenantRoomOptions(options)
const name = (options.name || "Allgemeiner Chat").trim() || "Allgemeiner Chat" const key = normalizedOptions.key
const topic = (options.topic || `Allgemeiner Kommunikationsraum für ${tenant.name}`).trim() const name = normalizedOptions.name
const topic = (normalizedOptions.topic || `Allgemeiner Kommunikationsraum für ${tenant.name}`).trim()
const cacheKey = `${tenant.id}:${key}` const cacheKey = `${tenant.id}:${key}`
const cachedRoom = matrixTenantRoomCache.get(cacheKey) const cachedRoom = matrixTenantRoomCache.get(cacheKey)
@@ -780,11 +803,39 @@ export function matrixService(server: FastifyInstance) {
return session return session
} }
const getGeneralRoomMessages = async (userId: string, tenantId: number | null) => { const listTenantRooms = async (tenantId: number | null) => {
const room = await provisionTenantRoom(userId, tenantId, { const tenant = await getCurrentTenant(tenantId)
key: "allgemein", const rooms = new Map<string, any>()
name: "Allgemeiner Chat",
}) for (const room of defaultTenantRooms) {
rooms.set(room.key, await getTenantRoomStatus(tenant.id, room.key, room.name))
}
for (const [cacheKey, cachedRoom] of matrixTenantRoomCache.entries()) {
const [cachedTenantId, roomKey] = cacheKey.split(":")
if (cachedTenantId !== String(tenant.id) || !cachedRoom.value) continue
rooms.set(roomKey, {
...cachedRoom.value,
exists: true,
})
}
return {
tenantId: tenant.id,
tenantName: tenant.name,
rooms: Array.from(rooms.values()).sort((a, b) =>
String(a.name || a.key).localeCompare(String(b.name || b.key), "de")
),
}
}
const getTenantRoomMessages = async (
userId: string,
tenantId: number | null,
options: MatrixTenantRoomOptions = {}
) => {
const room = await provisionTenantRoom(userId, tenantId, options)
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, { const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
roomId: room.roomId, roomId: room.roomId,
@@ -807,6 +858,8 @@ export function matrixService(server: FastifyInstance) {
return { return {
roomId: room.roomId, roomId: room.roomId,
alias: room.alias, alias: room.alias,
key: room.key,
name: room.name,
matrixUserId: session.matrixUserId, matrixUserId: session.matrixUserId,
messages: response.chunk messages: response.chunk
.filter((event) => event.type === "m.room.message" && event.content?.msgtype === "m.text") .filter((event) => event.type === "m.room.message" && event.content?.msgtype === "m.text")
@@ -822,11 +875,12 @@ export function matrixService(server: FastifyInstance) {
} }
} }
const getGeneralRoomMembers = async (userId: string, tenantId: number | null) => { const getTenantRoomMembers = async (
const room = await provisionTenantRoom(userId, tenantId, { userId: string,
key: "allgemein", tenantId: number | null,
name: "Allgemeiner Chat", options: MatrixTenantRoomOptions = {}
}) ) => {
const room = await provisionTenantRoom(userId, tenantId, options)
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, { const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
roomId: room.roomId, roomId: room.roomId,
@@ -840,6 +894,8 @@ export function matrixService(server: FastifyInstance) {
return { return {
roomId: room.roomId, roomId: room.roomId,
alias: room.alias, alias: room.alias,
key: room.key,
name: room.name,
members: Object.entries(members.joined).map(([matrixUserId, member]) => ({ members: Object.entries(members.joined).map(([matrixUserId, member]) => ({
matrixUserId, matrixUserId,
displayName: member.display_name || matrixUserId, displayName: member.display_name || matrixUserId,
@@ -849,9 +905,10 @@ export function matrixService(server: FastifyInstance) {
} }
} }
const sendGeneralRoomMessage = async ( const sendTenantRoomMessage = async (
userId: string, userId: string,
tenantId: number | null, tenantId: number | null,
options: MatrixTenantRoomOptions = {},
text: string text: string
) => { ) => {
const message = text.trim() const message = text.trim()
@@ -863,10 +920,7 @@ export function matrixService(server: FastifyInstance) {
) )
} }
const room = await provisionTenantRoom(userId, tenantId, { const room = await provisionTenantRoom(userId, tenantId, options)
key: "allgemein",
name: "Allgemeiner Chat",
})
const session = await ensureCurrentUserJoinedRoom(userId, tenantId, { const session = await ensureCurrentUserJoinedRoom(userId, tenantId, {
roomId: room.roomId, roomId: room.roomId,
@@ -896,9 +950,33 @@ export function matrixService(server: FastifyInstance) {
own: true, own: true,
roomId: room.roomId, roomId: room.roomId,
alias: room.alias, alias: room.alias,
key: room.key,
} }
} }
const getGeneralRoomMessages = (userId: string, tenantId: number | null) =>
getTenantRoomMessages(userId, tenantId, {
key: "allgemein",
name: "Allgemeiner Chat",
})
const getGeneralRoomMembers = (userId: string, tenantId: number | null) =>
getTenantRoomMembers(userId, tenantId, {
key: "allgemein",
name: "Allgemeiner Chat",
})
const sendGeneralRoomMessage = (userId: string, tenantId: number | null, text: string) =>
sendTenantRoomMessage(
userId,
tenantId,
{
key: "allgemein",
name: "Allgemeiner Chat",
},
text
)
return { return {
getStatus, getStatus,
matrixUserIdForUser, matrixUserIdForUser,
@@ -906,9 +984,13 @@ export function matrixService(server: FastifyInstance) {
provisionCurrentUser, provisionCurrentUser,
getTenantSpaceStatus, getTenantSpaceStatus,
provisionCurrentTenantSpace, provisionCurrentTenantSpace,
listTenantRooms,
getTenantRoomStatus, getTenantRoomStatus,
provisionTenantRoom, provisionTenantRoom,
createAccessTokenForUser, createAccessTokenForUser,
getTenantRoomMessages,
getTenantRoomMembers,
sendTenantRoomMessage,
getGeneralRoomMessages, getGeneralRoomMessages,
getGeneralRoomMembers, getGeneralRoomMembers,
sendGeneralRoomMessage, sendGeneralRoomMessage,

View File

@@ -3,6 +3,23 @@ import { matrixService } from "../modules/matrix.service"
export default async function communicationRoutes(server: FastifyInstance) { export default async function communicationRoutes(server: FastifyInstance) {
const matrix = matrixService(server) const matrix = matrixService(server)
const handleMatrixError = (req: any, reply: any, err: any, fallbackMessage: string) => {
req.log.error(err)
return reply
.code(err.statusCode || 500)
.send({ error: err.message || fallbackMessage })
}
const roomOptionsFromRequest = (req: any) => {
const params = req.params as { roomKey?: string }
const body = (req.body || {}) as { key?: string, name?: string, topic?: string }
return {
key: params.roomKey || body.key,
name: body.name,
topic: body.topic,
}
}
server.get("/communication/matrix/status", async () => { server.get("/communication/matrix/status", async () => {
return matrix.getStatus() return matrix.getStatus()
@@ -21,10 +38,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
try { try {
return await matrix.provisionCurrentUser(req.user.user_id, req.user.tenant_id) return await matrix.provisionCurrentUser(req.user.user_id, req.user.tenant_id)
} catch (err: any) { } catch (err: any) {
req.log.error(err) return handleMatrixError(req, reply, err, "Matrix provisioning failed")
return reply
.code(err.statusCode || 500)
.send({ error: err.message || "Matrix provisioning failed" })
} }
}) })
@@ -32,10 +46,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
try { try {
return await matrix.getTenantSpaceStatus(req.user.tenant_id) return await matrix.getTenantSpaceStatus(req.user.tenant_id)
} catch (err: any) { } catch (err: any) {
req.log.error(err) return handleMatrixError(req, reply, err, "Matrix tenant space status failed")
return reply
.code(err.statusCode || 500)
.send({ error: err.message || "Matrix tenant space status failed" })
} }
}) })
@@ -43,10 +54,27 @@ export default async function communicationRoutes(server: FastifyInstance) {
try { try {
return await matrix.provisionCurrentTenantSpace(req.user.user_id, req.user.tenant_id) return await matrix.provisionCurrentTenantSpace(req.user.user_id, req.user.tenant_id)
} catch (err: any) { } catch (err: any) {
req.log.error(err) return handleMatrixError(req, reply, err, "Matrix tenant space provisioning failed")
return reply }
.code(err.statusCode || 500) })
.send({ error: err.message || "Matrix tenant space provisioning failed" })
server.get("/communication/matrix/rooms", async (req, reply) => {
try {
return await matrix.listTenantRooms(req.user.tenant_id)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix rooms failed")
}
})
server.post("/communication/matrix/rooms", async (req, reply) => {
try {
return await matrix.provisionTenantRoom(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req)
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix room provisioning failed")
} }
}) })
@@ -54,10 +82,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
try { try {
return await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat") return await matrix.getTenantRoomStatus(req.user.tenant_id, "allgemein", "Allgemeiner Chat")
} catch (err: any) { } catch (err: any) {
req.log.error(err) return handleMatrixError(req, reply, err, "Matrix room status failed")
return reply
.code(err.statusCode || 500)
.send({ error: err.message || "Matrix room status failed" })
} }
}) })
@@ -68,10 +93,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
name: "Allgemeiner Chat", name: "Allgemeiner Chat",
}) })
} catch (err: any) { } catch (err: any) {
req.log.error(err) return handleMatrixError(req, reply, err, "Matrix room provisioning failed")
return reply
.code(err.statusCode || 500)
.send({ error: err.message || "Matrix room provisioning failed" })
} }
}) })
@@ -79,10 +101,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
try { try {
return await matrix.getGeneralRoomMessages(req.user.user_id, req.user.tenant_id) return await matrix.getGeneralRoomMessages(req.user.user_id, req.user.tenant_id)
} catch (err: any) { } catch (err: any) {
req.log.error(err) return handleMatrixError(req, reply, err, "Matrix messages failed")
return reply
.code(err.statusCode || 500)
.send({ error: err.message || "Matrix messages failed" })
} }
}) })
@@ -90,10 +109,7 @@ export default async function communicationRoutes(server: FastifyInstance) {
try { try {
return await matrix.getGeneralRoomMembers(req.user.user_id, req.user.tenant_id) return await matrix.getGeneralRoomMembers(req.user.user_id, req.user.tenant_id)
} catch (err: any) { } catch (err: any) {
req.log.error(err) return handleMatrixError(req, reply, err, "Matrix members failed")
return reply
.code(err.statusCode || 500)
.send({ error: err.message || "Matrix members failed" })
} }
}) })
@@ -102,10 +118,66 @@ export default async function communicationRoutes(server: FastifyInstance) {
const body = req.body as { text?: string } const body = req.body as { text?: string }
return await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "") return await matrix.sendGeneralRoomMessage(req.user.user_id, req.user.tenant_id, body.text || "")
} catch (err: any) { } catch (err: any) {
req.log.error(err) return handleMatrixError(req, reply, err, "Matrix message send failed")
return reply }
.code(err.statusCode || 500) })
.send({ error: err.message || "Matrix message send failed" })
server.get("/communication/matrix/rooms/:roomKey", async (req, reply) => {
try {
const params = req.params as { roomKey: string }
return await matrix.getTenantRoomStatus(req.user.tenant_id, params.roomKey, params.roomKey)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix room status failed")
}
})
server.post("/communication/matrix/rooms/:roomKey/provision", async (req, reply) => {
try {
return await matrix.provisionTenantRoom(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req)
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix room provisioning failed")
}
})
server.get("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => {
try {
return await matrix.getTenantRoomMessages(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req)
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix messages failed")
}
})
server.get("/communication/matrix/rooms/:roomKey/members", async (req, reply) => {
try {
return await matrix.getTenantRoomMembers(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req)
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix members failed")
}
})
server.post("/communication/matrix/rooms/:roomKey/messages", async (req, reply) => {
try {
const body = req.body as { text?: string }
return await matrix.sendTenantRoomMessage(
req.user.user_id,
req.user.tenant_id,
roomOptionsFromRequest(req),
body.text || ""
)
} catch (err: any) {
return handleMatrixError(req, reply, err, "Matrix message send failed")
} }
}) })
} }

View File

@@ -4,7 +4,8 @@ const { $api } = useNuxtApp()
const status = ref(null) const status = ref(null)
const identity = ref(null) const identity = ref(null)
const generalRoom = ref(null) const matrixRooms = ref([])
const activeRoomKey = ref("allgemein")
const matrixMessages = ref([]) const matrixMessages = ref([])
const matrixMembers = ref([]) const matrixMembers = ref([])
const matrixMessageDraft = ref("") const matrixMessageDraft = ref("")
@@ -24,17 +25,26 @@ const canUseMatrixChat = computed(() =>
Boolean(status.value?.reachable && status.value?.provisioningConfigured) Boolean(status.value?.reachable && status.value?.provisioningConfigured)
) )
const activeRoom = computed(() => ({ const activeRoom = computed(() =>
key: "general", matrixRooms.value.find((room) => room.key === activeRoomKey.value) || {
name: generalRoom.value?.name || "Allgemeiner Chat", key: activeRoomKey.value,
description: generalRoom.value?.alias || "Mandantenweiter Austausch", name: activeRoomKey.value,
exists: Boolean(generalRoom.value?.exists), description: "Mandantenweiter Austausch",
roomId: generalRoom.value?.roomId || "", exists: false,
unread: 0 roomId: "",
})) unread: 0
}
)
const activeRoomEndpoint = computed(() =>
`/api/communication/matrix/rooms/${encodeURIComponent(activeRoomKey.value)}`
)
const rooms = computed(() => [ const rooms = computed(() => [
activeRoom.value, ...matrixRooms.value.map((room) => ({
...room,
description: room.alias || room.roomId || "Mandantenweiter Austausch"
})),
{ {
key: "projects", key: "projects",
name: "Projekt-Chats", name: "Projekt-Chats",
@@ -51,6 +61,15 @@ const rooms = computed(() => [
} }
]) ])
const setActiveRoom = async (room) => {
if (room.disabled || room.key === activeRoomKey.value) return
activeRoomKey.value = room.key
matrixMessages.value = []
matrixMembers.value = []
await loadRoomChat({ includeMembers: true })
}
const mergeMatrixMessages = (incomingMessages) => { const mergeMatrixMessages = (incomingMessages) => {
const byId = new Map() const byId = new Map()
@@ -81,19 +100,19 @@ const scrollMessagesToBottom = async () => {
const loadChatInfo = async () => { const loadChatInfo = async () => {
loading.value = true loading.value = true
try { try {
const [statusRes, identityRes, roomRes] = await Promise.all([ const [statusRes, identityRes, roomsRes] = await Promise.all([
$api("/api/communication/matrix/status"), $api("/api/communication/matrix/status"),
$api("/api/communication/matrix/me"), $api("/api/communication/matrix/me"),
$api("/api/communication/matrix/rooms/general") $api("/api/communication/matrix/rooms")
]) ])
status.value = statusRes status.value = statusRes
identity.value = identityRes identity.value = identityRes
generalRoom.value = roomRes matrixRooms.value = roomsRes.rooms || []
lastUpdated.value = new Date() lastUpdated.value = new Date()
if (roomRes?.exists && canUseMatrixChat.value) { if (activeRoom.value?.exists && canUseMatrixChat.value) {
await loadGeneralChat({ silent: true, includeMembers: true }) await loadRoomChat({ silent: true, includeMembers: true })
} }
} catch (error) { } catch (error) {
toast.add({ toast.add({
@@ -105,33 +124,27 @@ const loadChatInfo = async () => {
} }
} }
const provisionGeneralRoom = async () => { const provisionActiveRoom = async () => {
roomProvisioning.value = true roomProvisioning.value = true
try { try {
const res = await $api("/api/communication/matrix/rooms/general/provision", { const res = await $api(`${activeRoomEndpoint.value}/provision`, {
method: "POST" method: "POST"
}) })
generalRoom.value = { const roomWasKnown = matrixRooms.value.some((room) => room.key === res.key)
tenantId: res.tenantId, matrixRooms.value = roomWasKnown
tenantName: res.tenantName, ? matrixRooms.value.map((room) => room.key === res.key ? { ...room, ...res, exists: true } : room)
key: res.key, : [...matrixRooms.value, { ...res, exists: true }]
name: res.name,
alias: res.alias,
exists: true,
roomId: res.roomId,
servers: res.servers || []
}
toast.add({ toast.add({
title: res.alreadyExisted ? "Allgemeiner Chat ist bereit" : "Allgemeiner Chat erstellt", title: res.alreadyExisted ? "Chatraum ist bereit" : "Chatraum erstellt",
color: "success" color: "success"
}) })
await loadGeneralChat({ includeMembers: true }) await loadRoomChat({ includeMembers: true })
} catch (error) { } catch (error) {
toast.add({ toast.add({
title: "Allgemeiner Chat konnte nicht erstellt werden", title: "Chatraum konnte nicht erstellt werden",
color: "error" color: "error"
}) })
} finally { } finally {
@@ -139,22 +152,22 @@ const provisionGeneralRoom = async () => {
} }
} }
const loadGeneralMessages = async ({ silent = false } = {}) => { const loadRoomMessages = async ({ silent = false } = {}) => {
if (!canUseMatrixChat.value || !generalRoom.value?.exists) return if (!canUseMatrixChat.value || !activeRoom.value?.exists) return
if (matrixMessagesRequestActive) return if (matrixMessagesRequestActive) return
matrixMessagesRequestActive = true matrixMessagesRequestActive = true
if (!silent) matrixMessagesLoading.value = true if (!silent) matrixMessagesLoading.value = true
try { try {
const res = await $api("/api/communication/matrix/rooms/general/messages") const res = await $api(`${activeRoomEndpoint.value}/messages`)
mergeMatrixMessages(res.messages || []) mergeMatrixMessages(res.messages || [])
generalRoom.value = { matrixRooms.value = matrixRooms.value.map((room) => room.key === activeRoomKey.value ? {
...generalRoom.value, ...room,
alias: res.alias || generalRoom.value?.alias, alias: res.alias || room.alias,
exists: true, exists: true,
roomId: res.roomId || generalRoom.value?.roomId roomId: res.roomId || room.roomId
} } : room)
await scrollMessagesToBottom() await scrollMessagesToBottom()
} catch (error) { } catch (error) {
if (!silent) { if (!silent) {
@@ -169,15 +182,15 @@ const loadGeneralMessages = async ({ silent = false } = {}) => {
} }
} }
const loadGeneralMembers = async ({ silent = false } = {}) => { const loadRoomMembers = async ({ silent = false } = {}) => {
if (!canUseMatrixChat.value || !generalRoom.value?.exists) return if (!canUseMatrixChat.value || !activeRoom.value?.exists) return
if (matrixMembersRequestActive) return if (matrixMembersRequestActive) return
matrixMembersRequestActive = true matrixMembersRequestActive = true
if (!silent) matrixMembersLoading.value = true if (!silent) matrixMembersLoading.value = true
try { try {
const res = await $api("/api/communication/matrix/rooms/general/members") const res = await $api(`${activeRoomEndpoint.value}/members`)
matrixMembers.value = res.members || [] matrixMembers.value = res.members || []
} catch (error) { } catch (error) {
if (!silent) { if (!silent) {
@@ -192,17 +205,17 @@ const loadGeneralMembers = async ({ silent = false } = {}) => {
} }
} }
const loadGeneralChat = async ({ silent = false, includeMembers = false } = {}) => { const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => {
await loadGeneralMessages({ silent }) await loadRoomMessages({ silent })
if (includeMembers) { if (includeMembers) {
await loadGeneralMembers({ silent }) await loadRoomMembers({ silent })
} }
} }
const sendMatrixMessage = async () => { const sendMatrixMessage = async () => {
const text = matrixMessageDraft.value.trim() const text = matrixMessageDraft.value.trim()
if (!text || !canUseMatrixChat.value || !generalRoom.value?.exists) return if (!text || !canUseMatrixChat.value || !activeRoom.value?.exists) return
const optimisticId = `pending-${Date.now()}` const optimisticId = `pending-${Date.now()}`
const optimisticMessage = { const optimisticMessage = {
@@ -225,7 +238,7 @@ const sendMatrixMessage = async () => {
matrixMessageSending.value = true matrixMessageSending.value = true
try { try {
const message = await $api("/api/communication/matrix/rooms/general/messages", { const message = await $api(`${activeRoomEndpoint.value}/messages`, {
method: "POST", method: "POST",
body: { text } body: { text }
}) })
@@ -251,8 +264,8 @@ const startMatrixAutoRefresh = () => {
matrixAutoRefreshActive.value = true matrixAutoRefreshActive.value = true
matrixRefreshInterval = window.setInterval(() => { matrixRefreshInterval = window.setInterval(() => {
if (!document.hidden && canUseMatrixChat.value && generalRoom.value?.exists) { if (!document.hidden && canUseMatrixChat.value && activeRoom.value?.exists) {
loadGeneralChat({ silent: true }) loadRoomChat({ silent: true })
} }
}, 15000) }, 15000)
} }
@@ -315,7 +328,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
variant="ghost" variant="ghost"
:loading="loading || matrixMessagesLoading" :loading="loading || matrixMessagesLoading"
:disabled="!canUseMatrixChat" :disabled="!canUseMatrixChat"
@click="loadGeneralChat({ includeMembers: true })" @click="loadRoomChat({ includeMembers: true })"
/> />
</div> </div>
</div> </div>
@@ -328,6 +341,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
class="mb-1 flex w-full items-center gap-3 rounded-md px-3 py-2 text-left transition" class="mb-1 flex w-full items-center gap-3 rounded-md px-3 py-2 text-left transition"
:class="room.disabled ? 'cursor-not-allowed opacity-50' : 'bg-muted text-highlighted'" :class="room.disabled ? 'cursor-not-allowed opacity-50' : 'bg-muted text-highlighted'"
:disabled="room.disabled" :disabled="room.disabled"
@click="setActiveRoom(room)"
> >
<span class="flex size-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary"> <span class="flex size-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<UIcon name="i-heroicons-chat-bubble-left-right" class="size-5" /> <UIcon name="i-heroicons-chat-bubble-left-right" class="size-5" />
@@ -368,7 +382,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
{{ activeRoom.name }} {{ activeRoom.name }}
</h2> </h2>
<UBadge <UBadge
v-if="matrixAutoRefreshActive && generalRoom?.exists" v-if="matrixAutoRefreshActive && activeRoom?.exists"
color="success" color="success"
variant="soft" variant="soft"
size="xs" size="xs"
@@ -395,8 +409,8 @@ onBeforeUnmount(stopMatrixAutoRefresh)
color="neutral" color="neutral"
variant="outline" variant="outline"
:loading="matrixMessagesLoading" :loading="matrixMessagesLoading"
:disabled="!canUseMatrixChat || !generalRoom?.exists" :disabled="!canUseMatrixChat || !activeRoom?.exists"
@click="loadGeneralChat({ includeMembers: true })" @click="loadRoomChat({ includeMembers: true })"
> >
Laden Laden
</UButton> </UButton>
@@ -417,7 +431,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
</div> </div>
<div <div
v-else-if="status && !generalRoom?.exists" v-else-if="status && !activeRoom?.exists"
class="border-b border-default bg-default px-4 py-3" class="border-b border-default bg-default px-4 py-3"
> >
<UAlert <UAlert
@@ -431,7 +445,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
<UButton <UButton
icon="i-heroicons-plus" icon="i-heroicons-plus"
:loading="roomProvisioning" :loading="roomProvisioning"
@click="provisionGeneralRoom" @click="provisionActiveRoom"
> >
Chat erstellen Chat erstellen
</UButton> </UButton>
@@ -487,14 +501,14 @@ onBeforeUnmount(stopMatrixAutoRefresh)
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 || !canUseMatrixChat || !generalRoom?.exists" :disabled="matrixMessageSending || !canUseMatrixChat || !activeRoom?.exists"
@keydown.enter.exact.prevent="sendMatrixMessage" @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() || !canUseMatrixChat || !generalRoom?.exists" :disabled="!matrixMessageDraft.trim() || !canUseMatrixChat || !activeRoom?.exists"
> >
Senden Senden
</UButton> </UButton>
@@ -507,7 +521,7 @@ onBeforeUnmount(stopMatrixAutoRefresh)
Raumdetails Raumdetails
</h3> </h3>
<p class="mt-1 break-all font-mono text-xs text-muted"> <p class="mt-1 break-all font-mono text-xs text-muted">
{{ generalRoom?.roomId || generalRoom?.alias || "Noch kein Matrix-Raum" }} {{ activeRoom?.roomId || activeRoom?.alias || "Noch kein Matrix-Raum" }}
</p> </p>
</div> </div>
@@ -523,26 +537,26 @@ onBeforeUnmount(stopMatrixAutoRefresh)
variant="ghost" variant="ghost"
size="xs" size="xs"
:loading="matrixMembersLoading" :loading="matrixMembersLoading"
:disabled="!canUseMatrixChat || !generalRoom?.exists" :disabled="!canUseMatrixChat || !activeRoom?.exists"
@click="loadGeneralMembers" @click="loadRoomMembers"
/> />
</div> </div>
<div class="space-y-2"> <div class="space-y-2">
<div <div
v-for="member in matrixMembers" v-for="member in matrixMembers"
:key="member.userId" :key="member.matrixUserId"
class="flex items-center gap-2" class="flex items-center gap-2"
> >
<span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-medium text-highlighted"> <span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-medium text-highlighted">
{{ (member.displayName || member.userId || "?").slice(0, 1).toUpperCase() }} {{ (member.displayName || member.matrixUserId || "?").slice(0, 1).toUpperCase() }}
</span> </span>
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-sm font-medium text-highlighted"> <p class="truncate text-sm font-medium text-highlighted">
{{ member.displayName || member.userId }} {{ member.displayName || member.matrixUserId }}
</p> </p>
<p class="truncate text-xs text-muted"> <p class="truncate text-xs text-muted">
{{ member.userId }} {{ member.matrixUserId }}
</p> </p>
</div> </div>
</div> </div>