diff --git a/backend/src/modules/matrix.service.ts b/backend/src/modules/matrix.service.ts index 103046e..93a2d8b 100644 --- a/backend/src/modules/matrix.service.ts +++ b/backend/src/modules/matrix.service.ts @@ -35,6 +35,11 @@ type MatrixUserSession = { validUntilMs: number } +type MatrixLoginTokenResponse = { + login_token: string + expires_in_ms: number +} + type MatrixTenantRoomOptions = { key?: string name?: string @@ -1146,6 +1151,40 @@ export function matrixService(server: FastifyInstance) { } } + const createElementRoomSession = async ( + userId: string, + tenantId: number | null, + options: MatrixTenantRoomOptions = {} + ) => { + const room = await provisionTenantRoom(userId, tenantId, options) + const session = await ensureCurrentUserJoinedRoom(userId, tenantId, { + roomId: room.roomId, + alias: room.alias, + }) + + const token = await requestMatrixJson( + "/_matrix/client/v1/login/get_token", + session.accessToken, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + } + ) + + return { + roomId: room.roomId, + alias: room.alias, + key: room.key, + name: room.name, + matrixUserId: session.matrixUserId, + loginToken: token.login_token, + expiresInMs: token.expires_in_ms, + homeserverUrl: homeserverUrl(), + serverName: serverName(), + } + } + const listTenantCommunicationUsers = async (tenantId: number | null) => { const tenant = await getCurrentTenant(tenantId) const rows = await server.db @@ -1302,6 +1341,7 @@ export function matrixService(server: FastifyInstance) { getTenantRoomMessages, getTenantRoomMembers, sendTenantRoomMessage, + createElementRoomSession, syncTenantRoomMembers, getGeneralRoomMessages, getGeneralRoomMembers, diff --git a/backend/src/routes/communication.ts b/backend/src/routes/communication.ts index 768c28c..e878624 100644 --- a/backend/src/routes/communication.ts +++ b/backend/src/routes/communication.ts @@ -125,6 +125,17 @@ export default async function communicationRoutes(server: FastifyInstance) { } }) + server.post("/communication/matrix/rooms/general/session", async (req, reply) => { + try { + return await matrix.createElementRoomSession(req.user.user_id, req.user.tenant_id, { + key: "allgemein", + name: "Allgemeiner Chat", + }) + } catch (err: any) { + return handleMatrixError(req, reply, err, "Matrix session failed") + } + }) + server.post("/communication/matrix/rooms/general/members/sync", async (req, reply) => { try { return await matrix.syncTenantRoomMembers(req.user.user_id, req.user.tenant_id, { @@ -190,6 +201,18 @@ export default async function communicationRoutes(server: FastifyInstance) { } }) + server.post("/communication/matrix/rooms/:roomKey/session", async (req, reply) => { + try { + return await matrix.createElementRoomSession( + req.user.user_id, + req.user.tenant_id, + roomOptionsFromRequest(req) + ) + } catch (err: any) { + return handleMatrixError(req, reply, err, "Matrix session failed") + } + }) + server.post("/communication/matrix/rooms/:roomKey/members/sync", async (req, reply) => { try { return await matrix.syncTenantRoomMembers( diff --git a/frontend/pages/communication/chat.vue b/frontend/pages/communication/chat.vue index b7ad205..5c742f9 100644 --- a/frontend/pages/communication/chat.vue +++ b/frontend/pages/communication/chat.vue @@ -14,6 +14,8 @@ const matrixMessagesViewport = ref(null) const roomCreateOpen = ref(false) const matrixCallOpen = ref(false) const matrixCallMode = ref("video") +const matrixCallLoading = ref(false) +const matrixCallUrl = ref("") const roomCreateForm = ref({ name: "", key: "", @@ -73,6 +75,10 @@ const matrixCallTitle = computed(() => matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz" ) +const activeMatrixCallUrl = computed(() => + matrixCallUrl.value || activeRoomElementUrl.value +) + const roomCreateKeyPreview = computed(() => normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name) ) @@ -340,7 +346,18 @@ const syncRoomMembers = async () => { } } -const openMatrixCall = (mode = "video") => { +const buildElementRoomSessionUrl = (session) => { + const roomAddress = session.roomId || session.alias || activeRoomMatrixAddress.value + if (!matrixElementUrl.value || !roomAddress) return "" + + const params = new URLSearchParams({ + loginToken: session.loginToken + }) + + return `${matrixElementUrl.value}/?${params.toString()}#/room/${encodeURIComponent(roomAddress)}` +} + +const openMatrixCall = async (mode = "video") => { if (!canStartMatrixCall.value) { toast.add({ title: "Besprechung kann noch nicht gestartet werden", @@ -350,7 +367,24 @@ const openMatrixCall = (mode = "video") => { } matrixCallMode.value = mode + matrixCallLoading.value = true matrixCallOpen.value = true + + try { + const session = await $api(`${activeRoomEndpoint.value}/session`, { + method: "POST" + }) + matrixCallUrl.value = buildElementRoomSessionUrl(session) || activeRoomElementUrl.value + } catch (error) { + matrixCallUrl.value = activeRoomElementUrl.value + toast.add({ + title: "Matrix-Anmeldung konnte nicht vorbereitet werden", + description: "Der Raum wird ohne automatische Anmeldung geöffnet.", + color: "warning" + }) + } finally { + matrixCallLoading.value = false + } } const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => { @@ -619,6 +653,7 @@ onBeforeUnmount(stopMatrixAutoRefresh) color="neutral" variant="outline" aria-label="Audioanruf starten" + :loading="matrixCallLoading && matrixCallMode === 'audio'" :disabled="!canStartMatrixCall" @click="openMatrixCall('audio')" /> @@ -627,6 +662,7 @@ onBeforeUnmount(stopMatrixAutoRefresh) color="primary" variant="soft" aria-label="Videokonferenz starten" + :loading="matrixCallLoading && matrixCallMode === 'video'" :disabled="!canStartMatrixCall" @click="openMatrixCall('video')" /> @@ -893,6 +929,7 @@ onBeforeUnmount(stopMatrixAutoRefresh) color="neutral" variant="outline" block + :loading="matrixCallLoading && matrixCallMode === 'audio'" :disabled="!canStartMatrixCall" @click="openMatrixCall('audio')" > @@ -903,6 +940,7 @@ onBeforeUnmount(stopMatrixAutoRefresh) color="primary" variant="soft" block + :loading="matrixCallLoading && matrixCallMode === 'video'" :disabled="!canStartMatrixCall" @click="openMatrixCall('video')" > @@ -940,8 +978,8 @@ onBeforeUnmount(stopMatrixAutoRefresh)
+
+ Matrix wird geladen... +