Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send signaling messages when starting and stopping breakout rooms #8477

4 changes: 2 additions & 2 deletions lib/Service/BreakoutRoomService.php
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@ public function stopBreakoutRooms(Room $parent): array {
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);
Expand All @@ -441,8 +443,6 @@ public function stopBreakoutRooms(Room $parent): array {
}
}

$this->roomService->setBreakoutRoomStatus($parent, BreakoutRoom::STATUS_STOPPED);

return $breakoutRooms;
}

Expand Down
27 changes: 27 additions & 0 deletions lib/Signaling/BackendNotifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,33 @@ public function roomDeleted(Room $room, array $userIds): void {
]);
}

/**
* 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.
*
Expand Down
85 changes: 85 additions & 0 deletions lib/Signaling/Listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
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;
Expand Down Expand Up @@ -98,6 +100,7 @@ protected static function registerExternalSignaling(IEventDispatcher $dispatcher
$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']);
Expand Down Expand Up @@ -324,6 +327,88 @@ public static function notifyParticipantsAfterGuestClean(RoomEvent $event): void
$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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#8660 brings in a getSessionsAndParticipantsForRooms so we can get them all with one query instead of a query per room
Maybe something for a follow up

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;
Expand Down
3 changes: 2 additions & 1 deletion lib/Signaling/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ public function isCompatibleSignalingServer(IResponse $response): bool {
$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 {
Expand Down
54 changes: 53 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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)) {
Expand Down
12 changes: 10 additions & 2 deletions src/components/LeftSidebar/LeftSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down
5 changes: 3 additions & 2 deletions src/mixins/isInCall.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
},
},

Expand Down
9 changes: 9 additions & 0 deletions src/store/callViewStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from '../constants.js'

const state = {
forceCallView: false,
isGrid: false,
isStripeOpen: true,
lastIsGrid: null,
Expand All @@ -39,6 +40,7 @@ const state = {
}

const getters = {
forceCallView: (state) => state.forceCallView,
isGrid: (state) => state.isGrid,
isStripeOpen: (state) => state.isStripeOpen,
lastIsGrid: (state) => state.lastIsGrid,
Expand All @@ -65,6 +67,9 @@ const getters = {

const mutations = {

setForceCallView(state, value) {
state.forceCallView = value
},
isGrid(state, value) {
state.isGrid = value
},
Expand Down Expand Up @@ -108,6 +113,10 @@ const mutations = {
}

const actions = {
setForceCallView(context, value) {
context.commit('setForceCallView', value)
},

selectedVideoPeerId(context, value) {
context.commit('selectedVideoPeerId', value)
},
Expand Down
11 changes: 11 additions & 0 deletions src/store/conversationsStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
34 changes: 33 additions & 1 deletion src/utils/signaling.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.'),
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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':
Expand Down