diff --git a/modules/API/API.js b/modules/API/API.js index 720ff4cd3e..28adf820bf 100644 --- a/modules/API/API.js +++ b/modules/API/API.js @@ -55,7 +55,7 @@ import { removeBreakoutRoom, sendParticipantToRoom } from '../../react/features/breakout-rooms/actions'; -import { getBreakoutRooms } from '../../react/features/breakout-rooms/functions'; +import { getBreakoutRooms, getRoomsInfo } from '../../react/features/breakout-rooms/functions'; import { sendMessage, setPrivateMessageRecipient, @@ -846,6 +846,10 @@ function initCommands() { callback(getBreakoutRooms(APP.store.getState())); break; } + case 'rooms-info': { + callback(getRoomsInfo(APP.store.getState())); + break; + } default: return false; } diff --git a/modules/API/external/external_api.js b/modules/API/external/external_api.js index a4e092274c..3c2661e5fa 100644 --- a/modules/API/external/external_api.js +++ b/modules/API/external/external_api.js @@ -619,6 +619,9 @@ export default class JitsiMeetExternalAPI extends EventEmitter { case 'video-quality-changed': this._videoQuality = data.videoQuality; break; + case 'breakout-rooms-updated': + this.updateNumberOfParticipants(data.rooms); + break; case 'local-storage-changed': jitsiLocalStorage.setItem('jitsiLocalStorage', data.localStorageContent); @@ -638,6 +641,40 @@ export default class JitsiMeetExternalAPI extends EventEmitter { }); } + /** + * Update number of participants based on all rooms. + * + * @param {Object} rooms - Rooms available rooms in the conference. + * @returns {void} + */ + updateNumberOfParticipants(rooms) { + if (!rooms || !Object.keys(rooms).length) { + return; + } + + const allParticipants = Object.keys(rooms).reduce((prev, roomItemKey) => { + if (rooms[roomItemKey]?.participants) { + return Object.keys(rooms[roomItemKey].participants).length + prev; + } + + return prev; + }, 0); + + this._numberOfParticipants = allParticipants; + } + + + /** + * Returns the rooms info in the conference. + * + * @returns {Object} Rooms info. + */ + async getRoomsInfo() { + return this._transport.sendRequest({ + name: 'rooms-info' + }); + } + /** * Adds event listener to Meet Jitsi. * @@ -1101,7 +1138,7 @@ export default class JitsiMeetExternalAPI extends EventEmitter { } /** - * Returns the number of participants in the conference. The local + * Returns the number of participants in the conference from all rooms. The local * participant is included. * * @returns {int} The number of participants in the conference. diff --git a/react/features/base/conference/functions.js b/react/features/base/conference/functions.js index 805e84b7c3..ccdccc6738 100644 --- a/react/features/base/conference/functions.js +++ b/react/features/base/conference/functions.js @@ -132,7 +132,7 @@ export function commonUserLeftHandling( } else { const isReplaced = user.isReplaced && user.isReplaced(); - dispatch(participantLeft(id, conference, isReplaced)); + dispatch(participantLeft(id, conference, { isReplaced })); } } diff --git a/react/features/base/participants/actions.ts b/react/features/base/participants/actions.ts index 93b99839d8..affdda5188 100644 --- a/react/features/base/participants/actions.ts +++ b/react/features/base/participants/actions.ts @@ -381,8 +381,13 @@ export function hiddenParticipantLeft(id: string) { * with the participant identified by the specified {@code id}. Only the local * participant is allowed to not specify an associated {@code JitsiConference} * instance. - * @param {boolean} isReplaced - Whether the participant is to be replaced in the meeting. - * @param {boolean} isVirtualScreenshareParticipant - Whether the participant is a virtual screen share participant. + * @param {Object} participantLeftProps - Other participant properties. + * @typedef {Object} participantLeftProps + * @param {boolean} participantLeftProps.isReplaced - Whether the participant is to be replaced in the meeting. + * @param {boolean} participantLeftProps.isVirtualScreenshareParticipant - Whether the participant is a + * virtual screen share participant. + * @param {boolean} participantLeftProps.isFakeParticipant - Whether the participant is a fake participant. + * * @returns {{ * type: PARTICIPANT_LEFT, * participant: { @@ -391,15 +396,15 @@ export function hiddenParticipantLeft(id: string) { * } * }} */ -export function participantLeft(id: string, conference: any, - isReplaced?: boolean, isVirtualScreenshareParticipant?: boolean) { +export function participantLeft(id: string, conference: any, participantLeftProps: any = {}) { return { type: PARTICIPANT_LEFT, participant: { conference, id, - isReplaced, - isVirtualScreenshareParticipant + isReplaced: participantLeftProps.isReplaced, + isVirtualScreenshareParticipant: participantLeftProps.isVirtualScreenshareParticipant, + isFakeParticipant: participantLeftProps.isFakeParticipant } }; } diff --git a/react/features/base/participants/middleware.js b/react/features/base/participants/middleware.js index dfede63178..88d5c35051 100644 --- a/react/features/base/participants/middleware.js +++ b/react/features/base/participants/middleware.js @@ -369,7 +369,9 @@ StateListenerRegistry.register( batch(() => { for (const [ id, p ] of getRemoteParticipants(getState())) { (!conference || p.conference !== conference) - && dispatch(participantLeft(id, p.conference, p.isReplaced)); + && dispatch(participantLeft(id, p.conference, { + isReplaced: p.isReplaced + })); } }); }); diff --git a/react/features/base/participants/subscriber.js b/react/features/base/participants/subscriber.js index 400a67fb8a..68f21fc82c 100644 --- a/react/features/base/participants/subscriber.js +++ b/react/features/base/participants/subscriber.js @@ -56,7 +56,10 @@ function _updateScreenshareParticipants({ getState, dispatch }) { } if (localScreenShare && !newLocalSceenshareSourceName) { - dispatch(participantLeft(localScreenShare.id, conference, undefined, true)); + dispatch(participantLeft(localScreenShare.id, conference, { + isReplaced: undefined, + isVirtualScreenshareParticipant: true + })); } } @@ -64,7 +67,10 @@ function _updateScreenshareParticipants({ getState, dispatch }) { const addedScreenshareSourceNames = _.difference(currentScreenshareSourceNames, previousScreenshareSourceNames); if (removedScreenshareSourceNames.length) { - removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, undefined, true))); + removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, { + isReplaced: undefined, + isVirtualScreenshareParticipant: true + }))); } if (addedScreenshareSourceNames.length) { diff --git a/react/features/breakout-rooms/functions.js b/react/features/breakout-rooms/functions.js index 90232260dd..057ffec18d 100644 --- a/react/features/breakout-rooms/functions.js +++ b/react/features/breakout-rooms/functions.js @@ -3,7 +3,7 @@ import _ from 'lodash'; import { getCurrentConference } from '../base/conference'; -import { getParticipantCount, isLocalParticipantModerator } from '../base/participants'; +import { getParticipantById, getParticipantCount, isLocalParticipantModerator } from '../base/participants'; import { toState } from '../base/redux'; import { FEATURE_KEY } from './constants'; @@ -30,6 +30,69 @@ export const getMainRoom = (stateful: Function | Object) => { return _.find(rooms, (room: Object) => room.isMainRoom); }; +export const getRoomsInfo = (stateful: Function | Object) => { + const breakoutRooms = getBreakoutRooms(stateful); + const conference = getCurrentConference(stateful); + + const initialRoomsInfo = { + rooms: [] + }; + + // only main roomn + if (!breakoutRooms || Object.keys(breakoutRooms).length === 0) { + return { + ...initialRoomsInfo, + rooms: [ { + isMainRoom: true, + id: conference?.room?.roomjid, + jid: conference?.room?.myroomjid, + participants: conference && conference.participants && Object.keys(conference.participants).length + ? Object.keys(conference.participants).map(participantId => { + const participantItem = conference?.participants[participantId]; + const storeParticipant = getParticipantById(stateful, participantItem._id); + + return { + jid: participantItem._jid, + role: participantItem._role, + displayName: participantItem._displayName, + avatarUrl: storeParticipant?.loadableAvatarUrl, + id: participantItem._id + }; + }) : [] + } ] + }; + } + + return { + ...initialRoomsInfo, + rooms: Object.keys(breakoutRooms).map(breakoutRoomKey => { + const breakoutRoomItem = breakoutRooms[breakoutRoomKey]; + + return { + isMainRoom: Boolean(breakoutRoomItem.isMainRoom), + id: breakoutRoomItem.id, + jid: breakoutRoomItem.jid, + participants: breakoutRoomItem.participants && Object.keys(breakoutRoomItem.participants).length + ? Object.keys(breakoutRoomItem.participants).map(participantLongId => { + const participantItem = breakoutRoomItem.participants[participantLongId]; + const ids = participantLongId.split('/'); + const storeParticipant = getParticipantById(stateful, + ids.length > 1 ? ids[1] : participantItem.jid); + + return { + jid: participantItem?.jid, + role: participantItem?.role, + displayName: participantItem?.displayName, + avatarUrl: storeParticipant?.loadableAvatarUrl, + id: storeParticipant ? storeParticipant.id + : participantLongId + }; + }) : [] + }; + }) + }; +}; + /** * Returns the room by Jid. * diff --git a/react/features/external-api/middleware.js b/react/features/external-api/middleware.js index 770a1534a9..3c4388ef6b 100644 --- a/react/features/external-api/middleware.js +++ b/react/features/external-api/middleware.js @@ -150,19 +150,32 @@ MiddlewareRegistry.register(store => next => action => { { id: action.kicker }); break; - case PARTICIPANT_LEFT: + case PARTICIPANT_LEFT: { + const { participant } = action; + const { isFakeParticipant, isVirtualScreenshareParticipant } = participant; + + // Skip sending participant left event for fake or virtual screenshare participants. + if (isFakeParticipant || isVirtualScreenshareParticipant) { + break; + } + APP.API.notifyUserLeft(action.participant.id); break; - + } case PARTICIPANT_JOINED: { const state = store.getState(); const { defaultRemoteDisplayName } = state['features/base/config']; const { participant } = action; - const { id, local, name } = participant; + const { id, isFakeParticipant, isVirtualScreenshareParticipant, local, name } = participant; // The version of external api outside of middleware did not emit // the local participant being created. if (!local) { + // Skip sending participant joined event for fake or virtual screenshare participants. + if (isFakeParticipant || isVirtualScreenshareParticipant) { + break; + } + APP.API.notifyUserJoined(id, { displayName: name, formattedDisplayName: appendSuffix( diff --git a/react/features/shared-video/middleware.any.js b/react/features/shared-video/middleware.any.js index ddd241665e..aac256e404 100644 --- a/react/features/shared-video/middleware.any.js +++ b/react/features/shared-video/middleware.any.js @@ -10,7 +10,8 @@ import { getLocalParticipant, participantJoined, participantLeft, - pinParticipant + pinParticipant, + getParticipantById } from '../base/participants'; import { MiddlewareRegistry } from '../base/redux'; @@ -51,7 +52,12 @@ MiddlewareRegistry.register(store => next => action => { if (isSharingStatus(sharedVideoStatus)) { handleSharingVideoStatus(store, value, attributes, conference); } else if (sharedVideoStatus === 'stop') { - dispatch(participantLeft(value, conference)); + const videoParticipant = getParticipantById(state, value); + + dispatch(participantLeft(value, conference, { + isFakeParticipant: videoParticipant?.isFakeParticipant + })); + if (localParticipantId !== from) { dispatch(resetSharedVideoStatus()); }