diff options
-rw-r--r-- | lib/Service/BreakoutRoomService.php | 4 | ||||
-rw-r--r-- | lib/Signaling/BackendNotifier.php | 27 | ||||
-rw-r--r-- | lib/Signaling/Listener.php | 85 | ||||
-rw-r--r-- | lib/Signaling/Manager.php | 3 | ||||
-rw-r--r-- | src/App.vue | 54 | ||||
-rw-r--r-- | src/components/LeftSidebar/LeftSidebar.vue | 12 | ||||
-rw-r--r-- | src/mixins/isInCall.js | 5 | ||||
-rw-r--r-- | src/store/callViewStore.js | 9 | ||||
-rw-r--r-- | src/store/conversationsStore.js | 11 | ||||
-rw-r--r-- | src/utils/signaling.js | 34 | ||||
-rw-r--r-- | tests/php/Signaling/BackendNotifierTest.php | 289 |
11 files changed, 522 insertions, 11 deletions
diff --git a/lib/Service/BreakoutRoomService.php b/lib/Service/BreakoutRoomService.php index 9872ecdd4..e2180124a 100644 --- a/lib/Service/BreakoutRoomService.php +++ b/lib/Service/BreakoutRoomService.php @@ -432,6 +432,8 @@ class BreakoutRoomService { throw new \InvalidArgumentException('mode'); } + $this->roomService->setBreakoutRoomStatus($parent, BreakoutRoom::STATUS_STOPPED); + $breakoutRooms = $this->manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $parent->getToken()); foreach ($breakoutRooms as $breakoutRoom) { $this->roomService->setLobby($breakoutRoom, Webinary::LOBBY_NON_MODERATORS, null); @@ -441,8 +443,6 @@ class BreakoutRoomService { } } - $this->roomService->setBreakoutRoomStatus($parent, BreakoutRoom::STATUS_STOPPED); - return $breakoutRooms; } diff --git a/lib/Signaling/BackendNotifier.php b/lib/Signaling/BackendNotifier.php index 139107a9d..577cc7645 100644 --- a/lib/Signaling/BackendNotifier.php +++ b/lib/Signaling/BackendNotifier.php @@ -281,6 +281,33 @@ class BackendNotifier { } /** + * The given participants should switch to the given room. + * + * @param Room $room + * @param string $switchToRoomToken + * @param string[] $sessionIds + * @throws \Exception + */ + public function switchToRoom(Room $room, string $switchToRoomToken, array $sessionIds): void { + $start = microtime(true); + $this->backendRequest($room, [ + 'type' => 'switchto', + 'switchto' => [ + 'roomid' => $switchToRoomToken, + 'sessions' => $sessionIds, + ], + ]); + $duration = microtime(true) - $start; + $this->logger->debug('Switch to room: {token} {roomid} {sessions} ({duration})', [ + 'token' => $room->getToken(), + 'roomid' => $switchToRoomToken, + 'sessions' => print_r($sessionIds, true), + 'duration' => sprintf('%.2f', $duration), + 'app' => 'spreed-hpb', + ]); + } + + /** * The participant list of the given room has been modified. * * @param Room $room diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index 7087d49d9..a129f3b49 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -37,6 +37,8 @@ use OCA\Talk\Events\RemoveParticipantEvent; use OCA\Talk\Events\RemoveUserEvent; use OCA\Talk\Events\RoomEvent; use OCA\Talk\GuestManager; +use OCA\Talk\Manager; +use OCA\Talk\Model\BreakoutRoom; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; @@ -98,6 +100,7 @@ class Listener { $dispatcher->addListener(Room::EVENT_AFTER_END_CALL_FOR_EVERYONE, [self::class, 'sendEndCallForEveryone']); $dispatcher->addListener(Room::EVENT_AFTER_GUESTS_CLEAN, [self::class, 'notifyParticipantsAfterGuestClean']); $dispatcher->addListener(Room::EVENT_AFTER_SET_CALL_RECORDING, [self::class, 'sendSignalingMessageWhenToggleRecording']); + $dispatcher->addListener(Room::EVENT_AFTER_SET_BREAKOUT_ROOM_STATUS, [self::class, 'notifyParticipantsAfterSetBreakoutRoomStatus']); $dispatcher->addListener(GuestManager::EVENT_AFTER_NAME_UPDATE, [self::class, 'notifyParticipantsAfterNameUpdated']); $dispatcher->addListener(ChatManager::EVENT_AFTER_MESSAGE_SEND, [self::class, 'notifyUsersViaExternalSignalingToRefreshTheChat']); $dispatcher->addListener(ChatManager::EVENT_AFTER_SYSTEM_MESSAGE_SEND, [self::class, 'notifyUsersViaExternalSignalingToRefreshTheChat']); @@ -324,6 +327,88 @@ class Listener { $notifier->participantsModified($event->getRoom(), $sessionIds); } + public static function notifyParticipantsAfterSetBreakoutRoomStatus(RoomEvent $event): void { + if (self::isUsingInternalSignaling()) { + return; + } + + $room = $event->getRoom(); + if ($room->getBreakoutRoomStatus() === BreakoutRoom::STATUS_STARTED) { + self::notifyParticipantsAfterBreakoutRoomStarted($room); + } else { + self::notifyParticipantsAfterBreakoutRoomStopped($room); + } + } + + private static function notifyParticipantsAfterBreakoutRoomStarted(Room $room): void { + $manager = Server::get(Manager::class); + $breakoutRooms = $manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $room->getToken()); + + $switchToData = []; + + $participantService = Server::get(ParticipantService::class); + $parentRoomParticipants = $participantService->getSessionsAndParticipantsForRoom($room); + + $notifier = Server::get(BackendNotifier::class); + + foreach ($breakoutRooms as $breakoutRoom) { + $sessionIds = []; + + $breakoutRoomParticipants = $participantService->getParticipantsForRoom($breakoutRoom); + foreach ($breakoutRoomParticipants as $breakoutRoomParticipant) { + foreach (self::getSessionIdsForNonModeratorsMatchingParticipant($breakoutRoomParticipant, $parentRoomParticipants) as $sessionId) { + $sessionIds[] = $sessionId; + } + } + + if (!empty($sessionIds)) { + $notifier->switchToRoom($room, $breakoutRoom->getToken(), $sessionIds); + } + } + } + + private static function getSessionIdsForNonModeratorsMatchingParticipant(Participant $targetParticipant, array $participants) { + $sessionIds = []; + + foreach ($participants as $participant) { + if ($participant->getAttendee()->getActorType() === $targetParticipant->getAttendee()->getActorType() && + $participant->getAttendee()->getActorId() === $targetParticipant->getAttendee()->getActorId() && + !$participant->hasModeratorPermissions()) { + $session = $participant->getSession(); + if ($session) { + $sessionIds[] = $session->getSessionId(); + } + } + } + + return $sessionIds; + } + + private static function notifyParticipantsAfterBreakoutRoomStopped(Room $room): void { + $manager = Server::get(Manager::class); + $breakoutRooms = $manager->getMultipleRoomsByObject(BreakoutRoom::PARENT_OBJECT_TYPE, $room->getToken()); + + $participantService = Server::get(ParticipantService::class); + + $notifier = Server::get(BackendNotifier::class); + + foreach ($breakoutRooms as $breakoutRoom) { + $sessionIds = []; + + $participants = $participantService->getSessionsAndParticipantsForRoom($breakoutRoom); + foreach ($participants as $participant) { + $session = $participant->getSession(); + if ($session) { + $sessionIds[] = $session->getSessionId(); + } + } + + if (!empty($sessionIds)) { + $notifier->switchToRoom($breakoutRoom, $room->getToken(), $sessionIds); + } + } + } + public static function notifyParticipantsAfterNameUpdated(ModifyParticipantEvent $event): void { if (self::isUsingInternalSignaling()) { return; diff --git a/lib/Signaling/Manager.php b/lib/Signaling/Manager.php index 87f472900..8fb6667a1 100644 --- a/lib/Signaling/Manager.php +++ b/lib/Signaling/Manager.php @@ -55,7 +55,8 @@ class Manager { $features = array_map('trim', $features); return in_array('audio-video-permissions', $features, true) && in_array('incall-all', $features, true) - && in_array('hello-v2', $features, true); + && in_array('hello-v2', $features, true) + && in_array('switchto', $features, true); } public function getSignalingServerLinkForConversation(?Room $room): string { diff --git a/src/App.vue b/src/App.vue index b06b40ee7..22f689664 100644 --- a/src/App.vue +++ b/src/App.vue @@ -65,7 +65,7 @@ import UploadEditor from './components/UploadEditor.vue' import SettingsDialog from './components/SettingsDialog/SettingsDialog.vue' import ConversationSettingsDialog from './components/ConversationSettings/ConversationSettingsDialog.vue' import '@nextcloud/dialogs/styles/toast.scss' -import { CONVERSATION } from './constants.js' +import { CONVERSATION, PARTICIPANT } from './constants.js' import DeviceChecker from './components/DeviceChecker/DeviceChecker.vue' import isMobile from '@nextcloud/vue/dist/Mixins/isMobile.js' @@ -263,6 +263,58 @@ export default { } }) + EventBus.$on('switch-to-conversation', (params) => { + if (this.isInCall) { + this.$store.dispatch('setForceCallView', true) + + const enableAudio = !localStorage.getItem('audioDisabled_' + this.token) + const enableVideo = !localStorage.getItem('videoDisabled_' + this.token) + const enableVirtualBackground = !!localStorage.getItem('virtualBackgroundEnabled_' + this.token) + + EventBus.$once('joined-conversation', async ({ token }) => { + if (params.token !== token) { + return + } + + if (enableAudio) { + localStorage.removeItem('audioDisabled_' + token) + } else { + localStorage.setItem('audioDisabled_' + token, 'true') + } + if (enableVideo) { + localStorage.removeItem('videoDisabled_' + token) + } else { + localStorage.setItem('videoDisabled_' + token, 'true') + } + if (enableVirtualBackground) { + localStorage.setItem('virtualBackgroundEnabled_' + token, 'true') + } else { + localStorage.removeItem('virtualBackgroundEnabled_' + token) + } + + const conversation = this.$store.getters.conversation(token) + + 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) { + flags |= PARTICIPANT.CALL_FLAG.WITH_VIDEO + } + + await this.$store.dispatch('joinCall', { + token: params.token, + participantIdentifier: this.$store.getters.getParticipantIdentifier(), + flags, + }) + + this.$store.dispatch('setForceCallView', false) + }) + } + + this.$router.push({ name: 'conversation', params: { token: params.token, skipLeaveWarning: true } }) + }) + EventBus.$on('conversations-received', (params) => { if (this.$route.name === 'conversation' && !this.$store.getters.conversation(this.token)) { diff --git a/src/components/LeftSidebar/LeftSidebar.vue b/src/components/LeftSidebar/LeftSidebar.vue index c526991ac..03e07561f 100644 --- a/src/components/LeftSidebar/LeftSidebar.vue +++ b/src/components/LeftSidebar/LeftSidebar.vue @@ -260,14 +260,14 @@ export default { } }, 30000) - EventBus.$on('should-refresh-conversations', this.debounceFetchConversations) + EventBus.$on('should-refresh-conversations', this.handleShouldRefreshConversations) EventBus.$once('conversations-received', this.handleUnreadMention) this.mountArrowNavigation() }, beforeDestroy() { - EventBus.$off('should-refresh-conversations', this.debounceFetchConversations) + EventBus.$off('should-refresh-conversations', this.handleShouldRefreshConversations) EventBus.$off('conversations-received', this.handleUnreadMention) this.cancelSearchPossibleConversations() @@ -439,6 +439,14 @@ export default { return conversation2.lastActivity - conversation1.lastActivity }, + async handleShouldRefreshConversations(token, properties) { + if (token && properties) { + await this.$store.dispatch('setConversationProperties', { token, properties }) + } + + this.debounceFetchConversations() + }, + debounceFetchConversations: debounce(function() { if (!this.isFetchingConversations) { this.fetchConversations() diff --git a/src/mixins/isInCall.js b/src/mixins/isInCall.js index a5cfa0a0c..d3c6924f0 100644 --- a/src/mixins/isInCall.js +++ b/src/mixins/isInCall.js @@ -35,8 +35,9 @@ export default { computed: { isInCall() { - return this.sessionStorageJoinedConversation === this.$store.getters.getToken() - && this.$store.getters.isInCall(this.$store.getters.getToken()) + return this.$store.getters.forceCallView + || (this.sessionStorageJoinedConversation === this.$store.getters.getToken() + && this.$store.getters.isInCall(this.$store.getters.getToken())) }, }, diff --git a/src/store/callViewStore.js b/src/store/callViewStore.js index 3fcbec3c3..b5abbc21b 100644 --- a/src/store/callViewStore.js +++ b/src/store/callViewStore.js @@ -27,6 +27,7 @@ import { } from '../constants.js' const state = { + forceCallView: false, isGrid: false, isStripeOpen: true, lastIsGrid: null, @@ -39,6 +40,7 @@ const state = { } const getters = { + forceCallView: (state) => state.forceCallView, isGrid: (state) => state.isGrid, isStripeOpen: (state) => state.isStripeOpen, lastIsGrid: (state) => state.lastIsGrid, @@ -65,6 +67,9 @@ const getters = { const mutations = { + setForceCallView(state, value) { + state.forceCallView = value + }, isGrid(state, value) { state.isGrid = value }, @@ -108,6 +113,10 @@ const mutations = { } const actions = { + setForceCallView(context, value) { + context.commit('setForceCallView', value) + }, + selectedVideoPeerId(context, value) { context.commit('selectedVideoPeerId', value) }, diff --git a/src/store/conversationsStore.js b/src/store/conversationsStore.js index fc3a1a68d..b5dbd637f 100644 --- a/src/store/conversationsStore.js +++ b/src/store/conversationsStore.js @@ -407,6 +407,17 @@ const actions = { commit('addConversation', conversation) }, + async setConversationProperties({ commit, getters }, { token, properties }) { + let conversation = Object.assign({}, getters.conversations[token]) + if (!conversation) { + return + } + + conversation = Object.assign(conversation, properties) + + commit('addConversation', conversation) + }, + async markConversationRead({ commit, getters }, token) { const conversation = Object.assign({}, getters.conversations[token]) if (!conversation) { diff --git a/src/utils/signaling.js b/src/utils/signaling.js index ed09af429..463e3f188 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -1026,7 +1026,7 @@ Signaling.Standalone.prototype.helloResponseReceived = function(data) { } } - if (!this.hasFeature('audio-video-permissions') || !this.hasFeature('incall-all')) { + if (!this.hasFeature('audio-video-permissions') || !this.hasFeature('incall-all') || !this.hasFeature('switchto')) { showError( t('spreed', 'The configured signaling server needs to be updated to be compatible with this version of Talk. Please contact your administrator.'), { @@ -1275,6 +1275,11 @@ Signaling.Standalone.prototype.processRoomEvent = function(data) { this._trigger('participantListChanged') } break + case 'switchto': + EventBus.$emit('switch-to-conversation', { + token: data.event.switchto.roomid, + }) + break case 'message': this.processRoomMessageEvent(data.event.message.data) break @@ -1306,6 +1311,33 @@ Signaling.Standalone.prototype.processRoomListEvent = function(data) { // Participant list in another room changed, we don't really care } break + } else { + // Some keys do not exactly match those in the room data, so they + // are normalized before emitting the event. + const properties = data.event.update.properties + const normalizedProperties = {} + + Object.keys(properties).forEach(key => { + if (key === 'active-since') { + return + } + + let normalizedKey = key + if (key === 'lobby-state') { + normalizedKey = 'lobbyState' + } else if (key === 'lobby-timer') { + normalizedKey = 'lobbyTimer' + } else if (key === 'read-only') { + normalizedKey = 'readOnly' + } else if (key === 'sip-enabled') { + normalizedKey = 'sipEnabled' + } + + normalizedProperties[normalizedKey] = properties[key] + }) + + EventBus.$emit('should-refresh-conversations', data.event.update.roomid, normalizedProperties) + break } // eslint-disable-next-line no-fallthrough case 'disinvite': diff --git a/tests/php/Signaling/BackendNotifierTest.php b/tests/php/Signaling/BackendNotifierTest.php index 56eeb8203..5fbe80491 100644 --- a/tests/php/Signaling/BackendNotifierTest.php +++ b/tests/php/Signaling/BackendNotifierTest.php @@ -23,15 +23,18 @@ namespace OCA\Talk\Tests\php\Signaling; use OCA\Talk\AppInfo\Application; +use OCA\Talk\Chat\ChatManager; use OCA\Talk\Chat\CommentsManager; use OCA\Talk\Config; use OCA\Talk\Events\SignalingRoomPropertiesEvent; use OCA\Talk\Manager; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\AttendeeMapper; +use OCA\Talk\Model\BreakoutRoom; use OCA\Talk\Model\SessionMapper; use OCA\Talk\Participant; use OCA\Talk\Room; +use OCA\Talk\Service\BreakoutRoomService; use OCA\Talk\Service\ParticipantService; use OCA\Talk\Service\RoomService; use OCA\Talk\Signaling\BackendNotifier; @@ -47,6 +50,7 @@ use OCP\IL10N; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; +use OCP\Notification\IManager as INotificationManager; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; use OCP\Share\IManager; @@ -91,6 +95,7 @@ class BackendNotifierTest extends TestCase { private ?Manager $manager = null; private ?RoomService $roomService = null; + private ?BreakoutRoomService $breakoutRoomService = null; private ?string $userId = null; priva |