summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/Service/BreakoutRoomService.php4
-rw-r--r--lib/Signaling/BackendNotifier.php27
-rw-r--r--lib/Signaling/Listener.php85
-rw-r--r--lib/Signaling/Manager.php3
-rw-r--r--src/App.vue54
-rw-r--r--src/components/LeftSidebar/LeftSidebar.vue12
-rw-r--r--src/mixins/isInCall.js5
-rw-r--r--src/store/callViewStore.js9
-rw-r--r--src/store/conversationsStore.js11
-rw-r--r--src/utils/signaling.js34
-rw-r--r--tests/php/Signaling/BackendNotifierTest.php289
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