55
66<template >
77 <NcContent
8- :class =" { ' icon-loading' : loading , ' in-call' : isInCall } "
8+ :class =" { ' icon-loading' : loading , ' in-call' : isInCall , ' call-minimized ' : isCallMinimized } "
99 appName="talk">
10+ <MinimizedCallBar v-if =" isCallMinimized " class="app-call-bar" />
1011 <LeftSidebar v-if =" getUserId " ref="leftSidebar" />
1112 <NcAppContent >
1213 <router-view />
@@ -33,6 +34,7 @@ import { provide } from 'vue'
3334import { START_LOCATION } from ' vue-router'
3435import NcAppContent from ' @nextcloud/vue/components/NcAppContent'
3536import NcContent from ' @nextcloud/vue/components/NcContent'
37+ import MinimizedCallBar from ' ./components/CallView/MinimizedCallBar.vue'
3638import ConversationSettingsDialog from ' ./components/ConversationSettings/ConversationSettingsDialog.vue'
3739import LeftSidebar from ' ./components/LeftSidebar/LeftSidebar.vue'
3840import MediaSettings from ' ./components/MediaSettings/MediaSettings.vue'
@@ -50,7 +52,7 @@ import { useGetMessagesProvider } from './composables/useGetMessages.ts'
5052import { useGetToken } from ' ./composables/useGetToken.ts'
5153import { useHashCheck } from ' ./composables/useHashCheck.js'
5254import { useInterceptNotifications } from ' ./composables/useInterceptNotifications.ts'
53- import { useIsInCall } from ' ./composables/useIsInCall.js'
55+ import { useCallMinimized , useIsInCall } from ' ./composables/useIsInCall.js'
5456import { watchJoinedConversation } from ' ./composables/useJoinedConversation.ts'
5557import { useRecordingStatusSync } from ' ./composables/useRecordingStatusSync.ts'
5658import { useSessionIssueHandler } from ' ./composables/useSessionIssueHandler.ts'
@@ -88,6 +90,7 @@ export default {
8890 SettingsDialog,
8991 ConversationSettingsDialog,
9092 MediaSettings,
93+ MinimizedCallBar,
9194 PollManager,
9295 },
9396
@@ -107,6 +110,7 @@ export default {
107110 token: useGetToken (),
108111 tokenStore: useTokenStore (),
109112 isInCall: useIsInCall (),
113+ isCallMinimized: useCallMinimized (),
110114 isLeavingAfterSessionIssue: useSessionIssueHandler (),
111115 isMobile: useIsMobile (),
112116 isNextcloudTalkHashDirty: useHashCheck (),
@@ -322,15 +326,38 @@ export default {
322326 return
323327 }
324328
325- if (from .name === ' conversation' && from .params .token !== to .params .token ) {
329+ // Whether we are navigating away from the conversation that holds
330+ // the active call. In that case we keep the call (and its signaling
331+ // room) alive so it can be shown minimized, and open the target
332+ // conversation as chat-only (no signaling join for it).
333+ const keepCallAlive = from .name === ' conversation'
334+ && from .params .token !== to .params .token
335+ && this .callViewStore .activeCallToken === from .params .token
336+ && this .$store .getters .isInCall (from .params .token )
337+
338+ // Whether we are navigating back to the conversation that holds the
339+ // active (minimized) call. Its session was never left, so we must
340+ // NOT re-join it (that would trigger a "duplicate session" error).
341+ const returningToActiveCall = to .name === ' conversation'
342+ && from .params .token !== to .params .token
343+ && this .callViewStore .activeCallToken === to .params .token
344+ && this .$store .getters .isInCall (to .params .token )
345+
346+ if (from .name === ' conversation' && from .params .token !== to .params .token && ! keepCallAlive) {
326347 // Await to properly close session / leave call before joining another one
327348 await this .$store .dispatch (' leaveConversation' , { token: from .params .token })
328349 }
329350
330- /**
331- * This runs whenever the new route is a conversation.
332- */
333- if (to .name === ' conversation' && from .params .token !== to .params .token ) {
351+ if (returningToActiveCall) {
352+ // Returning to the active call's conversation: its session was
353+ // never left, so do not re-join. Restore the "joined" marker
354+ // (a chat-only visit to another conversation moved it away) so
355+ // the call controls are enabled again.
356+ this .tokenStore .updateLastJoinedConversationToken (to .params .token )
357+ } else if (to .name === ' conversation' && from .params .token !== to .params .token ) {
358+ /**
359+ * This runs whenever the new route is a conversation.
360+ */
334361 // Fetch conversation object, if it's not known yet to the client
335362 if (! this .$store .getters .conversation (to .params .token )) {
336363 const result = await this .fetchSingleConversation (to .params .token )
@@ -340,7 +367,13 @@ export default {
340367 return
341368 }
342369 }
343- this .$store .dispatch (' joinConversation' , { token: to .params .token })
370+ // While a call is kept alive in another conversation, open the
371+ // target as chat-only so we don't move the single signaling
372+ // connection out of the call's room. Chat works over REST/poll.
373+ // Awaited so the session is stable before next()/any follow-up
374+ // joinCall reads the actor session id (otherwise a concurrent
375+ // join can replace the session under a live call).
376+ await this .$store .dispatch (' joinConversation' , { token: to .params .token , chatOnly: keepCallAlive })
344377 }
345378
346379 next ()
@@ -374,6 +407,10 @@ export default {
374407 if (from .name === ' conversation' && to .name === ' conversation' && from .params .token === to .params .token ) {
375408 // Navigating within the same conversation
376409 beforeRouteChangeListener (to, from, next)
410+ } else if (this .canMinimizeCall (to, from)) {
411+ // Navigating to another conversation while in a call: keep the
412+ // call running and show it minimized instead of asking to leave.
413+ beforeRouteChangeListener (to, from, next)
377414 } else if (! this .warnLeaving || this .skipLeaveWarning || this .isVoiceRoom (from .params .token )) {
378415 // Safe to navigate
379416 // Note: voice rooms are intended to be left without confirmation.
@@ -423,6 +460,38 @@ export default {
423460 return Boolean (conversation? .attributes & CONVERSATION .ATTRIBUTE .VOICE_ROOM )
424461 },
425462
463+ /**
464+ * Whether navigating from `from` to `to` should keep the active call
465+ * running (minimized) instead of prompting to leave it. True both when
466+ * leaving the active call's conversation to browse another one, and when
467+ * returning to the active call's conversation.
468+ *
469+ * @param {object} to target route
470+ * @param {object} from current route
471+ * @return {boolean}
472+ */
473+ canMinimizeCall (to , from ) {
474+ const activeCallToken = this .callViewStore .activeCallToken
475+ if (! activeCallToken
476+ || from .name !== ' conversation'
477+ || to .name !== ' conversation'
478+ || from .params .token === to .params .token ) {
479+ return false
480+ }
481+ // Leaving the active call's conversation to browse another one.
482+ if (activeCallToken === from .params .token
483+ && this .$store .getters .isInCall (from .params .token )
484+ && ! this .isVoiceRoom (from .params .token )) {
485+ return true
486+ }
487+ // Returning to the active call's conversation from elsewhere.
488+ if (activeCallToken === to .params .token
489+ && this .$store .getters .isInCall (to .params .token )) {
490+ return true
491+ }
492+ return false
493+ },
494+
426495 preventUnload (event ) {
427496 if ((! this .warnLeaving && ! this .isSendingMessages ) || this .isVoiceRoom (this .token )) {
428497 return
@@ -655,12 +724,28 @@ body#body-public {
655724< style lang= " scss" scoped>
656725
657726.content {
727+ -- call- bar- height: var (-- default- clickable- area);
728+
658729 & .in - call {
659730 : deep (.app - content ) {
660731 background- color: transparent;
661732 }
662733 }
663734
735+ // Minimized call bar: a full-width banner pinned just below the app header,
736+ // pushing the navigation/content/sidebar columns down so it never overlaps
737+ // existing UI (matches the "return to call" banner in Teams/Meet/Zoom).
738+ & .call - minimized {
739+ padding- block- start: var (-- call- bar- height);
740+
741+ : deep (.app - call - bar ) {
742+ position: absolute;
743+ inset- block- start: 0 ;
744+ inset- inline: 0 ;
745+ z- index: 1 ;
746+ }
747+ }
748+
664749 // Fix fullscreen black bar on top
665750 & : fullscreen {
666751 padding- top: 0 ;
0 commit comments