From d466a7396fad19190f6ee841dad5c369e779b331 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Thu, 26 Mar 2026 19:35:03 +0100 Subject: [PATCH 1/5] fix: extract joinCall logic from CallButton component Signed-off-by: Maksim Sukharev --- src/components/TopBar/CallButton.vue | 67 ++--------- src/composables/useJoinCall.ts | 124 +++++++++++++++++++++ src/types/vendor/@nextcloud/event-bus.d.ts | 8 ++ 3 files changed, 140 insertions(+), 59 deletions(-) create mode 100644 src/composables/useJoinCall.ts diff --git a/src/components/TopBar/CallButton.vue b/src/components/TopBar/CallButton.vue index 03100b0fc57..5edc646dab2 100644 --- a/src/components/TopBar/CallButton.vue +++ b/src/components/TopBar/CallButton.vue @@ -115,6 +115,7 @@ import IconPhoneOffOutline from 'vue-material-design-icons/PhoneOffOutline.vue' import IconPhoneOutline from 'vue-material-design-icons/PhoneOutline.vue' import { useGetToken } from '../../composables/useGetToken.ts' import { useIsInCall } from '../../composables/useIsInCall.js' +import { useJoinCall } from '../../composables/useJoinCall.ts' import { ATTENDEE, CALL, CONVERSATION, PARTICIPANT } from '../../constants.ts' import { callSIPDialOut } from '../../services/callsService.ts' import { getTalkConfig, hasTalkFeature } from '../../services/CapabilitiesManager.ts' @@ -204,6 +205,7 @@ export default { }, setup() { + const { joinCall } = useJoinCall() return { actorStore: useActorStore(), tokenStore: useTokenStore(), @@ -215,6 +217,7 @@ export default { settingsStore: useSettingsStore(), soundsStore: useSoundsStore(), isMobile: useIsMobile(), + joinCall, } }, @@ -379,52 +382,15 @@ export default { methods: { t, - isParticipantTypeModerator(participantType) { - return [PARTICIPANT.TYPE.OWNER, PARTICIPANT.TYPE.MODERATOR, PARTICIPANT.TYPE.GUEST_MODERATOR].includes(participantType) - }, - /** - * Starts or joins a call - */ - async joinCall() { - let flags = PARTICIPANT.CALL_FLAG.IN_CALL - if (this.conversation.permissions & PARTICIPANT.PERMISSIONS.PUBLISH_AUDIO) { - flags |= PARTICIPANT.CALL_FLAG.WITH_AUDIO - } - if (this.conversation.permissions & PARTICIPANT.PERMISSIONS.PUBLISH_VIDEO && !this.isPhoneRoom) { - flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO - } - - console.info('Joining call') + async handleJoinCall() { this.loading = true - // Close navigation - emit('toggle-navigation', { - open: false, - }) - await this.$store.dispatch('joinCall', { - token: this.token, - participantIdentifier: this.actorStore.participantIdentifier, - flags, + await this.joinCall(this.token, { silent: this.hasCall ? true : this.silentCall, recordingConsent: this.recordingConsentGiven, + shouldStartRecording: this.isRecordingFromStart, }) this.loading = false - - if (this.isRecordingFromStart) { - this.$store.dispatch('startCallRecording', { - token: this.token, - callRecording: CALL.RECORDING.VIDEO, - }) - } - - if (this.isPhoneRoom) { - const attendeeId = this.$store.getters.participantsList(this.token) - .find((participant) => participant.actorType === ATTENDEE.ACTOR_TYPE.PHONES) - ?.attendeeId - if (attendeeId) { - this.dialOutPhoneNumber(attendeeId) - } - } }, async leaveCall(endMeetingForAll = false) { @@ -463,16 +429,14 @@ export default { this.soundsStore.initAudioObjects() if (this.isMediaSettings || this.isPhoneRoom) { - emit('talk:media-settings:hide') - this.joinCall() + this.handleJoinCall() return } if (this.showRecordingWarning || this.showMediaSettings) { emit('talk:media-settings:show') } else { - emit('talk:media-settings:hide') - this.joinCall() + this.handleJoinCall() } }, @@ -481,21 +445,6 @@ export default { token: this.breakoutRoomsStore.getParentRoomToken(this.token), }) }, - - async dialOutPhoneNumber(attendeeId) { - try { - await callSIPDialOut(this.token, attendeeId) - } catch (error) { - if (error?.response?.data?.ocs?.data?.message) { - showError(t('spreed', 'Phone number could not be called: {error}', { - error: error?.response?.data?.ocs?.data?.message, - })) - } else { - console.error(error) - showError(t('spreed', 'Phone number could not be called')) - } - } - }, }, } diff --git a/src/composables/useJoinCall.ts b/src/composables/useJoinCall.ts new file mode 100644 index 00000000000..da802e77594 --- /dev/null +++ b/src/composables/useJoinCall.ts @@ -0,0 +1,124 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { Conversation, Participant } from '../types/index.ts' + +import { showError } from '@nextcloud/dialogs' +import { emit } from '@nextcloud/event-bus' +import { t } from '@nextcloud/l10n' +import { useStore } from 'vuex' +import { ATTENDEE, CALL, CONVERSATION, PARTICIPANT } from '../constants.ts' +import { callSIPDialOut } from '../services/callsService.ts' +import { getTalkConfig } from '../services/CapabilitiesManager.ts' +import { useActorStore } from '../stores/actor.ts' +import { isAxiosErrorResponse } from '../types/guards.ts' + +/** + * Handler function to join a call and manage side effects + */ +export function useJoinCall() { + const actorStore = useActorStore() + const vuexStore = useStore() + + /** + * Returns whether the conversation is a phone room (with a single SIP phone participant) + * + * @param conversation - conversation object + * @param conversation.objectId - conversation objectId + * @param conversation.objectType - conversation objectType + */ + function isConversationPhoneRoom({ objectId, objectType }: Conversation) { + return objectId === CONVERSATION.OBJECT_ID.PHONE_OUTGOING + && [ + CONVERSATION.OBJECT_TYPE.PHONE_LEGACY, + CONVERSATION.OBJECT_TYPE.PHONE_PERSISTENT, + CONVERSATION.OBJECT_TYPE.PHONE_TEMPORARY, + ].includes(objectType) + } + + /** + * Tries to call the given SIP phone participant + * + * @param token - conversation token of where to join + * @param attendeeId - id of the phone participant + */ + async function dialOutPhoneNumber(token: string, attendeeId: number) { + try { + await callSIPDialOut(token, attendeeId) + } catch (exception) { + if (isAxiosErrorResponse<{ message: string }>(exception) && exception.response?.data?.ocs?.data?.message) { + showError(t('spreed', 'Phone number could not be called: {error}', { + error: exception.response.data.ocs.data.message, + })) + } else { + console.error(exception) + showError(t('spreed', 'Phone number could not be called')) + } + } + } + + /** + * Starts or joins a call + * + * @param token - conversation token of where to join + * @param options - joining options + * @param options.silent - whether to join the call silently (no notifications) + * @param options.recordingConsent - whether to join the call with recording consent + * @param options.shouldStartRecording - whether to start the recording together with the call (requires recording backend) + */ + async function joinCall(token: string, { + silent = false, + recordingConsent = false, + shouldStartRecording = false, + } = {}) { + const conversation = vuexStore.getters.conversation(token) + const isPhoneRoom = isConversationPhoneRoom(conversation) + + // Define flags to join with (just call / with audio / with video) + let flags = PARTICIPANT.CALL_FLAG.IN_CALL + if (conversation.permissions & PARTICIPANT.PERMISSIONS.PUBLISH_AUDIO) { + flags |= PARTICIPANT.CALL_FLAG.WITH_AUDIO + } + if (conversation.permissions & PARTICIPANT.PERMISSIONS.PUBLISH_VIDEO && !isPhoneRoom) { + flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO + } + + // Close MediaSettings + emit('talk:media-settings:hide') + // Close navigation when joining the call + emit('toggle-navigation', { open: false }) + + console.debug('Joining call') + await vuexStore.dispatch('joinCall', { + token, + participantIdentifier: actorStore.participantIdentifier, + flags, + silent, + recordingConsent, + }) + + if (shouldStartRecording && getTalkConfig(token, 'call', 'recording')) { + // Do not wait for async operation + vuexStore.dispatch('startCallRecording', { + token, + callRecording: CALL.RECORDING.VIDEO, + }) + } + + if (isPhoneRoom) { + const attendeeId = vuexStore.getters.participantsList(token) + .find((participant: Participant) => participant.actorType === ATTENDEE.ACTOR_TYPE.PHONES) + ?.attendeeId + if (attendeeId) { + // Do not wait for async operation + dialOutPhoneNumber(token, attendeeId) + } + } + } + + return { + joinCall, + } +} diff --git a/src/types/vendor/@nextcloud/event-bus.d.ts b/src/types/vendor/@nextcloud/event-bus.d.ts index 65910f31064..43059e2565e 100644 --- a/src/types/vendor/@nextcloud/event-bus.d.ts +++ b/src/types/vendor/@nextcloud/event-bus.d.ts @@ -10,6 +10,14 @@ declare module '@nextcloud/event-bus' { 'user:info:changed': NextcloudUser 'notifications:action:execute': NotificationEvent 'notifications:notification:received': NotificationEvent + // LeftSidebar > NcAppNavigation + 'toggle-navigation': { open: boolean } + // MediaSettings + 'talk:media-settings:hide': void + 'talk:media-settings:show': void | 'video-verification' | 'device-check' | 'backgrounds' + // ConversationSettingsDialog + 'show-conversation-settings': { token: string } + 'hide-conversation-settings': void } } export {} From 8c977ee80a022adb90d44bece3c2833db933ed1f Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Thu, 26 Mar 2026 19:44:31 +0100 Subject: [PATCH 2/5] fix: reuse logic in MainView for #direct-call feature - add additional check for the need to show media settings - add additional check for the need to wait for joining conversation Signed-off-by: Maksim Sukharev --- src/composables/useJoinCall.ts | 5 +++ src/views/MainView.vue | 56 ++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/src/composables/useJoinCall.ts b/src/composables/useJoinCall.ts index da802e77594..31253915e3b 100644 --- a/src/composables/useJoinCall.ts +++ b/src/composables/useJoinCall.ts @@ -74,6 +74,11 @@ export function useJoinCall() { shouldStartRecording = false, } = {}) { const conversation = vuexStore.getters.conversation(token) + if (!actorStore.participantIdentifier.sessionId || conversation.attendeeId !== actorStore.participantIdentifier.attendeeId) { + console.error('Trying to join call without having joined the conversation') + return + } + const isPhoneRoom = isConversationPhoneRoom(conversation) // Define flags to join with (just call / with audio / with video) diff --git a/src/views/MainView.vue b/src/views/MainView.vue index 849846e293f..0c4e219bb10 100644 --- a/src/views/MainView.vue +++ b/src/views/MainView.vue @@ -4,7 +4,7 @@ -->