diff --git a/frontend/pages/communication/chat.vue b/frontend/pages/communication/chat.vue index a50c63b..8c23b01 100644 --- a/frontend/pages/communication/chat.vue +++ b/frontend/pages/communication/chat.vue @@ -24,6 +24,10 @@ const matrixCallTiles = ref([]) const matrixCallMicEnabled = ref(true) const matrixCallCameraEnabled = ref(true) const matrixCallAudioContainer = ref(null) +const matrixCallScreenShareEnabled = ref(false) +const matrixCallStartedAt = ref(null) +const matrixCallDurationNow = ref(Date.now()) +const matrixCallAnnouncementSent = ref(false) const roomCreateForm = ref({ name: "", key: "", @@ -39,6 +43,7 @@ const matrixMessageSending = ref(false) const matrixAutoRefreshActive = ref(false) const lastUpdated = ref(null) let matrixRefreshInterval = null +let matrixCallDurationInterval = null let matrixMessagesRequestActive = false let matrixMembersRequestActive = false let matrixLiveKitRoom = null @@ -75,6 +80,29 @@ const matrixCallTitle = computed(() => matrixCallMode.value === "audio" ? "Audioanruf" : "Videokonferenz" ) +const matrixCallStateLabel = computed(() => { + const labels = { + connected: "Verbunden", + connecting: "Verbindet", + reconnecting: "Verbindung wird wiederhergestellt", + disconnected: "Getrennt" + } + + return labels[String(matrixCallState.value).toLowerCase()] || matrixCallState.value +}) + +const matrixCallParticipantCount = computed(() => matrixCallTiles.value.length) + +const matrixCallDurationLabel = computed(() => { + if (!matrixCallStartedAt.value) return "00:00" + + const durationSeconds = Math.max(0, Math.floor((matrixCallDurationNow.value - matrixCallStartedAt.value) / 1000)) + const minutes = String(Math.floor(durationSeconds / 60)).padStart(2, "0") + const seconds = String(durationSeconds % 60).padStart(2, "0") + + return `${minutes}:${seconds}` +}) + const roomCreateKeyPreview = computed(() => normalizeRoomKey(roomCreateForm.value.key || roomCreateForm.value.name) ) @@ -343,6 +371,11 @@ const syncRoomMembers = async () => { } const openMatrixCall = async (mode = "video") => { + if (matrixLiveKitRoom && matrixCallConnected.value) { + matrixCallOpen.value = true + return + } + if (!canStartMatrixCall.value) { toast.add({ title: "Besprechung kann noch nicht gestartet werden", @@ -355,6 +388,7 @@ const openMatrixCall = async (mode = "video") => { matrixCallLoading.value = true matrixCallError.value = "" matrixCallOpen.value = true + matrixCallAnnouncementSent.value = false try { await leaveMatrixCall() @@ -363,6 +397,7 @@ const openMatrixCall = async (mode = "video") => { }) matrixCallSession.value = session await connectMatrixCall(session, { video: mode === "video" }) + await announceMatrixCallStarted(mode) } catch (error) { matrixCallError.value = error?.data?.error || error?.message || "Verbindung fehlgeschlagen" toast.add({ @@ -377,12 +412,20 @@ const openMatrixCall = async (mode = "video") => { const getParticipantTile = (participant, local = false) => { const publications = participant.getTrackPublications?.() || [] - const videoPublication = publications.find((publication) => - publication.kind === Track.Kind.Video && publication.track + const screenSharePublication = publications.find((publication) => + publication.kind === Track.Kind.Video && + publication.source === Track.Source.ScreenShare && + publication.track + ) + const cameraPublication = publications.find((publication) => + publication.kind === Track.Kind.Video && + publication.source !== Track.Source.ScreenShare && + publication.track ) const audioPublication = publications.find((publication) => publication.kind === Track.Kind.Audio && publication.track ) + const videoPublication = screenSharePublication || cameraPublication return { id: local ? "local" : participant.sid || participant.identity, @@ -390,6 +433,7 @@ const getParticipantTile = (participant, local = false) => { name: local ? "Du" : participant.name || participant.identity, local, speaking: participant.isSpeaking, + screenSharing: Boolean(screenSharePublication?.track), videoTrack: videoPublication?.track || null, audioTrack: audioPublication?.track || null, cameraEnabled: participant.isCameraEnabled, @@ -428,11 +472,54 @@ const refreshMatrixCallTiles = async () => { ...Array.from(matrixLiveKitRoom.remoteParticipants.values()).map((participant) => getParticipantTile(participant) ) - ] + ].sort((a, b) => Number(b.screenSharing) - Number(a.screenSharing)) await attachMatrixCallMedia() } +const startMatrixCallDurationTimer = () => { + matrixCallStartedAt.value = Date.now() + matrixCallDurationNow.value = Date.now() + + if (matrixCallDurationInterval) { + window.clearInterval(matrixCallDurationInterval) + } + + matrixCallDurationInterval = window.setInterval(() => { + matrixCallDurationNow.value = Date.now() + }, 1000) +} + +const stopMatrixCallDurationTimer = () => { + if (matrixCallDurationInterval) { + window.clearInterval(matrixCallDurationInterval) + matrixCallDurationInterval = null + } + + matrixCallStartedAt.value = null + matrixCallDurationNow.value = Date.now() +} + +const announceMatrixCallStarted = async (mode) => { + if (matrixCallAnnouncementSent.value || !activeRoom.value?.exists) return + + matrixCallAnnouncementSent.value = true + + try { + const message = await $api(`${activeRoomEndpoint.value}/messages`, { + method: "POST", + body: { + text: `${mode === "audio" ? "Audioanruf" : "Videokonferenz"} gestartet.` + } + }) + + mergeMatrixMessages([message]) + await scrollMessagesToBottom() + } catch (error) { + matrixCallAnnouncementSent.value = false + } +} + const connectMatrixCall = async (session, { video = true } = {}) => { const room = new Room({ adaptiveStream: true, @@ -452,6 +539,7 @@ const connectMatrixCall = async (session, { video = true } = {}) => { RoomEvent.TrackMuted, RoomEvent.TrackUnmuted, RoomEvent.ActiveSpeakersChanged, + RoomEvent.LocalTrackSubscribed, ] for (const eventName of refreshEvents) { @@ -461,11 +549,16 @@ const connectMatrixCall = async (session, { video = true } = {}) => { room.on(RoomEvent.ConnectionStateChanged, (state) => { matrixCallState.value = state matrixCallConnected.value = state === ConnectionState.Connected + if (state === ConnectionState.Connected && !matrixCallStartedAt.value) { + startMatrixCallDurationTimer() + } }) room.on(RoomEvent.Disconnected, () => { matrixCallConnected.value = false matrixCallState.value = ConnectionState.Disconnected + matrixCallScreenShareEnabled.value = false + stopMatrixCallDurationTimer() }) await room.connect(session.liveKitUrl, session.liveKitToken) @@ -473,6 +566,10 @@ const connectMatrixCall = async (session, { video = true } = {}) => { await room.localParticipant.setCameraEnabled(video) matrixCallMicEnabled.value = true matrixCallCameraEnabled.value = video + matrixCallScreenShareEnabled.value = false + if (!matrixCallStartedAt.value) { + startMatrixCallDurationTimer() + } await refreshMatrixCallTiles() } @@ -487,6 +584,9 @@ const leaveMatrixCall = async () => { matrixCallTiles.value = [] matrixCallConnected.value = false matrixCallState.value = "disconnected" + matrixCallScreenShareEnabled.value = false + matrixCallAnnouncementSent.value = false + stopMatrixCallDurationTimer() } const closeMatrixCall = async () => { @@ -494,6 +594,10 @@ const closeMatrixCall = async () => { matrixCallOpen.value = false } +const minimizeMatrixCall = () => { + matrixCallOpen.value = false +} + const setMatrixCallVideoElement = (tileId, element) => { if (element) { matrixCallVideoElements.set(tileId, element) @@ -522,6 +626,22 @@ const toggleMatrixCallCamera = async () => { await refreshMatrixCallTiles() } +const toggleMatrixCallScreenShare = async () => { + if (!matrixLiveKitRoom) return + + const enabled = !matrixCallScreenShareEnabled.value + try { + await matrixLiveKitRoom.localParticipant.setScreenShareEnabled(enabled) + matrixCallScreenShareEnabled.value = enabled + await refreshMatrixCallTiles() + } catch (error) { + toast.add({ + title: "Bildschirmfreigabe konnte nicht geändert werden", + color: "error" + }) + } +} + const loadRoomChat = async ({ silent = false, includeMembers = false } = {}) => { await loadRoomMessages({ silent }) @@ -625,6 +745,7 @@ onMounted(async () => { onBeforeUnmount(() => { stopMatrixAutoRefresh() + stopMatrixCallDurationTimer() leaveMatrixCall() }) @@ -929,6 +1050,40 @@ onBeforeUnmount(() => { +
+ {{ matrixCallTitle }} läuft in {{ activeRoom.name }} +
++ {{ matrixCallParticipantCount }} Teilnehmer · {{ matrixCallDurationLabel }} · {{ matrixCallStateLabel }} +
+