This commit is contained in:
248
react/features/base/participants/actionTypes.ts
Normal file
248
react/features/base/participants/actionTypes.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* Create an action to mark the participant as notified to speak next.
|
||||
*
|
||||
* {
|
||||
* type: NOTIFIED_TO_SPEAK
|
||||
* }
|
||||
*/
|
||||
export const NOTIFIED_TO_SPEAK = 'NOTIFIED_TO_SPEAK';
|
||||
|
||||
/**
|
||||
* Create an action for when dominant speaker changes.
|
||||
*
|
||||
* {
|
||||
* type: DOMINANT_SPEAKER_CHANGED,
|
||||
* participant: {
|
||||
* conference: JitsiConference,
|
||||
* id: string,
|
||||
* previousSpeakers: Array<string>,
|
||||
* silence: boolean
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const DOMINANT_SPEAKER_CHANGED = 'DOMINANT_SPEAKER_CHANGED';
|
||||
|
||||
/**
|
||||
* Create an action for granting moderator to a participant.
|
||||
*
|
||||
* {
|
||||
* type: GRANT_MODERATOR,
|
||||
* id: string
|
||||
* }
|
||||
*/
|
||||
export const GRANT_MODERATOR = 'GRANT_MODERATOR';
|
||||
|
||||
/**
|
||||
* Create an action for removing a participant from the conference.
|
||||
*
|
||||
* {
|
||||
* type: KICK_PARTICIPANT,
|
||||
* id: string
|
||||
* }
|
||||
*/
|
||||
export const KICK_PARTICIPANT = 'KICK_PARTICIPANT';
|
||||
|
||||
/**
|
||||
* Create an action for muting a remote participant.
|
||||
*
|
||||
* {
|
||||
* type: MUTE_REMOTE_PARTICIPANT,
|
||||
* id: string
|
||||
* }
|
||||
*/
|
||||
export const MUTE_REMOTE_PARTICIPANT = 'MUTE_REMOTE_PARTICIPANT';
|
||||
|
||||
/**
|
||||
* Action to signal that ID of participant has changed. This happens when
|
||||
* local participant joins a new conference or quits one.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_ID_CHANGED,
|
||||
* conference: JitsiConference
|
||||
* newValue: string,
|
||||
* oldValue: string
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_ID_CHANGED = 'PARTICIPANT_ID_CHANGED';
|
||||
|
||||
/**
|
||||
* Action to signal that participant role has changed. e.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_ROLE_CHANGED,
|
||||
* participant: {
|
||||
* id: string
|
||||
* }
|
||||
* role: string
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_ROLE_CHANGED = 'PARTICIPANT_ROLE_CHANGED';
|
||||
|
||||
/**
|
||||
* Action to signal that a participant has joined.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_JOINED,
|
||||
* participant: Participant
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_JOINED = 'PARTICIPANT_JOINED';
|
||||
|
||||
/**
|
||||
* Action to signal that a participant has been removed from a conference by
|
||||
* another participant.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_KICKED,
|
||||
* kicked: Object,
|
||||
* kicker: Object
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_KICKED = 'PARTICIPANT_KICKED';
|
||||
|
||||
/**
|
||||
* Action to handle case when participant lefts.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_LEFT,
|
||||
* participant: {
|
||||
* id: string
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_LEFT = 'PARTICIPANT_LEFT';
|
||||
|
||||
/**
|
||||
* Action to handle case when the remote participant mutes the local participant.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_MUTED_US,
|
||||
* participant: Participant,
|
||||
* track: JitsiLocalTrack
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_MUTED_US = 'PARTICIPANT_MUTED_US';
|
||||
|
||||
/**
|
||||
* Action to handle case when the sources attached to a participant are updated.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_SOURCES_UPDATED,
|
||||
* participant: {
|
||||
* id: string
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_SOURCES_UPDATED = 'PARTICIPANT_SOURCES_UPDATED';
|
||||
|
||||
/**
|
||||
* Action to handle case when info about participant changes.
|
||||
*
|
||||
* {
|
||||
* type: PARTICIPANT_UPDATED,
|
||||
* participant: Participant
|
||||
* }
|
||||
*/
|
||||
export const PARTICIPANT_UPDATED = 'PARTICIPANT_UPDATED';
|
||||
|
||||
/**
|
||||
* The type of the Redux action which pins a conference participant.
|
||||
*
|
||||
* {
|
||||
* type: PIN_PARTICIPANT,
|
||||
* participant: {
|
||||
* id: string
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const PIN_PARTICIPANT = 'PIN_PARTICIPANT';
|
||||
|
||||
/**
|
||||
* The type of Redux action which notifies the app that the loadable avatar URL has changed.
|
||||
*
|
||||
* {
|
||||
* type: SET_LOADABLE_AVATAR_URL,
|
||||
* participant: {
|
||||
* id: string,
|
||||
loadableAvatarUrl: string
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const SET_LOADABLE_AVATAR_URL = 'SET_LOADABLE_AVATAR_URL';
|
||||
|
||||
/**
|
||||
* The type of Redux action which notifies that the screenshare participant's display name has changed.
|
||||
*
|
||||
* {
|
||||
* type: SCREENSHARE_PARTICIPANT_NAME_CHANGED,
|
||||
* id: string,
|
||||
* name: string
|
||||
* }
|
||||
*/
|
||||
export const SCREENSHARE_PARTICIPANT_NAME_CHANGED = 'SCREENSHARE_PARTICIPANT_NAME_CHANGED';
|
||||
|
||||
/**
|
||||
* Raises hand for the local participant.
|
||||
* {
|
||||
* type: LOCAL_PARTICIPANT_RAISE_HAND
|
||||
* }
|
||||
*/
|
||||
export const LOCAL_PARTICIPANT_RAISE_HAND = 'LOCAL_PARTICIPANT_RAISE_HAND';
|
||||
|
||||
/**
|
||||
* Clear the raise hand queue.
|
||||
* {
|
||||
* type: RAISE_HAND_CLEAR
|
||||
* }
|
||||
*/
|
||||
export const RAISE_HAND_CLEAR = 'RAISE_HAND_CLEAR';
|
||||
|
||||
/**
|
||||
* Updates participant in raise hand queue.
|
||||
* {
|
||||
* type: RAISE_HAND_UPDATED,
|
||||
* participant: {
|
||||
* id: string,
|
||||
* raiseHand: boolean
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export const RAISE_HAND_UPDATED = 'RAISE_HAND_UPDATED';
|
||||
|
||||
/**
|
||||
* The type of Redux action which notifies that the local participant has changed the audio levels.
|
||||
* {
|
||||
* type: LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED
|
||||
* level: number
|
||||
* }
|
||||
*/
|
||||
export const LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED = 'LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED'
|
||||
|
||||
/**
|
||||
* The type of Redux action which overwrites the name of a participant.
|
||||
* {
|
||||
* type: OVERWRITE_PARTICIPANT_NAME,
|
||||
* id: string,
|
||||
* name: string
|
||||
* }
|
||||
*/
|
||||
export const OVERWRITE_PARTICIPANT_NAME = 'OVERWRITE_PARTICIPANT_NAME';
|
||||
|
||||
/**
|
||||
* The type of Redux action which overwrites the names of multiple participants.
|
||||
* {
|
||||
* type: OVERWRITE_PARTICIPANTS_NAMES,
|
||||
* participantsList: Array<Object>,
|
||||
* }
|
||||
*/
|
||||
export const OVERWRITE_PARTICIPANTS_NAMES = 'OVERWRITE_PARTICIPANTS_NAMES';
|
||||
|
||||
/**
|
||||
* Updates participants local recording status.
|
||||
* {
|
||||
* type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
|
||||
* recording: boolean,
|
||||
* onlySelf: boolean
|
||||
* }
|
||||
*/
|
||||
export const SET_LOCAL_PARTICIPANT_RECORDING_STATUS = 'SET_LOCAL_PARTICIPANT_RECORDING_STATUS';
|
||||
689
react/features/base/participants/actions.ts
Normal file
689
react/features/base/participants/actions.ts
Normal file
@@ -0,0 +1,689 @@
|
||||
import { IStore } from '../../app/types';
|
||||
import { showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../../notifications/constants';
|
||||
import { IJitsiConference } from '../conference/reducer';
|
||||
import { set } from '../redux/functions';
|
||||
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
GRANT_MODERATOR,
|
||||
KICK_PARTICIPANT,
|
||||
LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
|
||||
LOCAL_PARTICIPANT_RAISE_HAND,
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
OVERWRITE_PARTICIPANTS_NAMES,
|
||||
OVERWRITE_PARTICIPANT_NAME,
|
||||
PARTICIPANT_ID_CHANGED,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_KICKED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_MUTED_US,
|
||||
PARTICIPANT_ROLE_CHANGED,
|
||||
PARTICIPANT_SOURCES_UPDATED,
|
||||
PARTICIPANT_UPDATED,
|
||||
PIN_PARTICIPANT,
|
||||
RAISE_HAND_CLEAR,
|
||||
RAISE_HAND_UPDATED,
|
||||
SCREENSHARE_PARTICIPANT_NAME_CHANGED,
|
||||
SET_LOADABLE_AVATAR_URL,
|
||||
SET_LOCAL_PARTICIPANT_RECORDING_STATUS
|
||||
} from './actionTypes';
|
||||
import {
|
||||
DISCO_REMOTE_CONTROL_FEATURE
|
||||
} from './constants';
|
||||
import {
|
||||
getLocalParticipant,
|
||||
getNormalizedDisplayName,
|
||||
getParticipantById,
|
||||
getParticipantDisplayName,
|
||||
getVirtualScreenshareParticipantOwnerId
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { FakeParticipant, IJitsiParticipant, IParticipant } from './types';
|
||||
|
||||
/**
|
||||
* Create an action for when dominant speaker changes.
|
||||
*
|
||||
* @param {string} dominantSpeaker - Participant ID of the dominant speaker.
|
||||
* @param {Array<string>} previousSpeakers - Participant IDs of the previous speakers.
|
||||
* @param {boolean} silence - Whether the dominant speaker is silent or not.
|
||||
* @param {JitsiConference} conference - The {@code JitsiConference} associated
|
||||
* with the participant identified by the specified {@code id}. Only the local
|
||||
* participant is allowed to not specify an associated {@code JitsiConference}
|
||||
* instance.
|
||||
* @returns {{
|
||||
* type: DOMINANT_SPEAKER_CHANGED,
|
||||
* participant: {
|
||||
* conference: JitsiConference,
|
||||
* id: string,
|
||||
* previousSpeakers: Array<string>,
|
||||
* silence: boolean
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function dominantSpeakerChanged(
|
||||
dominantSpeaker: string, previousSpeakers: string[], silence: boolean, conference: IJitsiConference) {
|
||||
return {
|
||||
type: DOMINANT_SPEAKER_CHANGED,
|
||||
participant: {
|
||||
conference,
|
||||
id: dominantSpeaker,
|
||||
previousSpeakers,
|
||||
silence
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for granting moderator to a participant.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @returns {{
|
||||
* type: GRANT_MODERATOR,
|
||||
* id: string
|
||||
* }}
|
||||
*/
|
||||
export function grantModerator(id: string) {
|
||||
return {
|
||||
type: GRANT_MODERATOR,
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for removing a participant from the conference.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @returns {{
|
||||
* type: KICK_PARTICIPANT,
|
||||
* id: string
|
||||
* }}
|
||||
*/
|
||||
export function kickParticipant(id: string) {
|
||||
return {
|
||||
type: KICK_PARTICIPANT,
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that the ID of local participant has changed. It happens
|
||||
* when the local participant joins a new conference or leaves an existing
|
||||
* conference.
|
||||
*
|
||||
* @param {string} id - New ID for local participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function localParticipantIdChanged(id: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const participant = getLocalParticipant(getState);
|
||||
|
||||
if (participant) {
|
||||
return dispatch({
|
||||
type: PARTICIPANT_ID_CHANGED,
|
||||
|
||||
// XXX A participant is identified by an id-conference pair.
|
||||
// Only the local participant is with an undefined conference.
|
||||
conference: undefined,
|
||||
newValue: id,
|
||||
oldValue: participant.id
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a local participant has joined.
|
||||
*
|
||||
* @param {IParticipant} participant={} - Information about participant.
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_JOINED,
|
||||
* participant: IParticipant
|
||||
* }}
|
||||
*/
|
||||
export function localParticipantJoined(participant: IParticipant = { id: '' }) {
|
||||
return participantJoined(set(participant, 'local', true));
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to remove a local participant.
|
||||
*
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function localParticipantLeft() {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const participant = getLocalParticipant(getState);
|
||||
|
||||
if (participant) {
|
||||
return (
|
||||
dispatch(
|
||||
participantLeft(
|
||||
participant.id,
|
||||
|
||||
// XXX Only the local participant is allowed to leave
|
||||
// without stating the JitsiConference instance because
|
||||
// the local participant is uniquely identified by the
|
||||
// very fact that there is only one local participant
|
||||
// (and the fact that the local participant "joins" at
|
||||
// the beginning of the app and "leaves" at the end of
|
||||
// the app).
|
||||
undefined)));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal the role of the local participant has changed. It can happen
|
||||
* when the participant has joined a conference, even before a non-default local
|
||||
* id has been set, or after a moderator leaves.
|
||||
*
|
||||
* @param {string} role - The new role of the local participant.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function localParticipantRoleChanged(role: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const participant = getLocalParticipant(getState);
|
||||
|
||||
if (participant) {
|
||||
return dispatch(participantRoleChanged(participant.id, role));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action for muting another participant in the conference.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @param {MEDIA_TYPE} mediaType - The media to mute.
|
||||
* @returns {{
|
||||
* type: MUTE_REMOTE_PARTICIPANT,
|
||||
* id: string,
|
||||
* mediaType: MEDIA_TYPE
|
||||
* }}
|
||||
*/
|
||||
export function muteRemoteParticipant(id: string, mediaType: string) {
|
||||
return {
|
||||
type: MUTE_REMOTE_PARTICIPANT,
|
||||
id,
|
||||
mediaType
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a participant has joined.
|
||||
*
|
||||
* @param {IParticipant} participant - Information about participant.
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_JOINED,
|
||||
* participant: IParticipant
|
||||
* }}
|
||||
*/
|
||||
export function participantJoined(participant: IParticipant) {
|
||||
// Only the local participant is not identified with an id-conference pair.
|
||||
if (participant.local) {
|
||||
return {
|
||||
type: PARTICIPANT_JOINED,
|
||||
participant
|
||||
};
|
||||
}
|
||||
|
||||
// In other words, a remote participant is identified with an id-conference
|
||||
// pair.
|
||||
const { conference } = participant;
|
||||
|
||||
if (!conference) {
|
||||
throw Error(
|
||||
'A remote participant must be associated with a JitsiConference!');
|
||||
}
|
||||
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
// A remote participant is only expected to join in a joined or joining
|
||||
// conference. The following check is really necessary because a
|
||||
// JitsiConference may have moved into leaving but may still manage to
|
||||
// sneak a PARTICIPANT_JOINED in if its leave is delayed for any purpose
|
||||
// (which is not outrageous given that leaving involves network
|
||||
// requests.)
|
||||
const stateFeaturesBaseConference
|
||||
= getState()['features/base/conference'];
|
||||
|
||||
if (conference === stateFeaturesBaseConference.conference
|
||||
|| conference === stateFeaturesBaseConference.joining) {
|
||||
return dispatch({
|
||||
type: PARTICIPANT_JOINED,
|
||||
participant
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sources of a remote participant.
|
||||
*
|
||||
* @param {IJitsiParticipant} jitsiParticipant - The IJitsiParticipant instance.
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_SOURCES_UPDATED,
|
||||
* participant: IParticipant
|
||||
* }}
|
||||
*/
|
||||
export function participantSourcesUpdated(jitsiParticipant: IJitsiParticipant) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const id = jitsiParticipant.getId();
|
||||
const participant = getParticipantById(getState(), id);
|
||||
|
||||
if (participant?.local) {
|
||||
return;
|
||||
}
|
||||
const sources = jitsiParticipant.getSources();
|
||||
|
||||
if (!sources?.size) {
|
||||
return;
|
||||
}
|
||||
|
||||
return dispatch({
|
||||
type: PARTICIPANT_SOURCES_UPDATED,
|
||||
participant: {
|
||||
id,
|
||||
sources
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the features of a remote participant.
|
||||
*
|
||||
* @param {JitsiParticipant} jitsiParticipant - The ID of the participant.
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_UPDATED,
|
||||
* participant: IParticipant
|
||||
* }}
|
||||
*/
|
||||
export function updateRemoteParticipantFeatures(jitsiParticipant: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
if (!jitsiParticipant) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = jitsiParticipant.getId();
|
||||
|
||||
jitsiParticipant.getFeatures()
|
||||
.then((features: Map<string, string>) => {
|
||||
const supportsRemoteControl = features.has(DISCO_REMOTE_CONTROL_FEATURE);
|
||||
const participant = getParticipantById(getState(), id);
|
||||
|
||||
if (!participant || participant.local) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (participant?.supportsRemoteControl !== supportsRemoteControl) {
|
||||
return dispatch({
|
||||
type: PARTICIPANT_UPDATED,
|
||||
participant: {
|
||||
id,
|
||||
supportsRemoteControl
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error: any) => {
|
||||
logger.error(`Failed to get participant features for ${id}!`, error);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a participant has left.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @param {JitsiConference} conference - The {@code JitsiConference} associated
|
||||
* with the participant identified by the specified {@code id}. Only the local
|
||||
* participant is allowed to not specify an associated {@code JitsiConference}
|
||||
* instance.
|
||||
* @param {Object} participantLeftProps - Other participant properties.
|
||||
* @typedef {Object} participantLeftProps
|
||||
* @param {FakeParticipant|undefined} participantLeftProps.fakeParticipant - The type of fake participant.
|
||||
* @param {boolean} participantLeftProps.isReplaced - Whether the participant is to be replaced in the meeting.
|
||||
*
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_LEFT,
|
||||
* participant: {
|
||||
* conference: JitsiConference,
|
||||
* id: string
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function participantLeft(id: string, conference?: IJitsiConference, participantLeftProps: {
|
||||
fakeParticipant?: string; isReplaced?: boolean;
|
||||
} = {}) {
|
||||
return {
|
||||
type: PARTICIPANT_LEFT,
|
||||
participant: {
|
||||
conference,
|
||||
fakeParticipant: participantLeftProps.fakeParticipant,
|
||||
id,
|
||||
isReplaced: participantLeftProps.isReplaced
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a participant's presence status has changed.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @param {string} presence - Participant's new presence status.
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_UPDATED,
|
||||
* participant: {
|
||||
* id: string,
|
||||
* presence: string
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function participantPresenceChanged(id: string, presence: string) {
|
||||
return participantUpdated({
|
||||
id,
|
||||
presence
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a participant's role has changed.
|
||||
*
|
||||
* @param {string} id - Participant's ID.
|
||||
* @param {PARTICIPANT_ROLE} role - Participant's new role.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function participantRoleChanged(id: string, role: string) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const oldParticipantRole = getParticipantById(getState(), id)?.role;
|
||||
|
||||
dispatch(participantUpdated({
|
||||
id,
|
||||
role
|
||||
}));
|
||||
|
||||
if (oldParticipantRole !== role) {
|
||||
dispatch({
|
||||
type: PARTICIPANT_ROLE_CHANGED,
|
||||
participant: {
|
||||
id,
|
||||
role
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a participant's display name has changed.
|
||||
*
|
||||
* @param {string} id - Screenshare participant's ID.
|
||||
* @param {name} name - The new display name of the screenshare participant's owner.
|
||||
* @returns {{
|
||||
* type: SCREENSHARE_PARTICIPANT_NAME_CHANGED,
|
||||
* id: string,
|
||||
* name: string
|
||||
* }}
|
||||
*/
|
||||
export function screenshareParticipantDisplayNameChanged(id: string, name: string) {
|
||||
return {
|
||||
type: SCREENSHARE_PARTICIPANT_NAME_CHANGED,
|
||||
id,
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that some of participant properties has been changed.
|
||||
*
|
||||
* @param {IParticipant} participant={} - Information about participant. To
|
||||
* identify the participant the object should contain either property id with
|
||||
* value the id of the participant or property local with value true (if the
|
||||
* local participant hasn't joined the conference yet).
|
||||
* @returns {{
|
||||
* type: PARTICIPANT_UPDATED,
|
||||
* participant: IParticipant
|
||||
* }}
|
||||
*/
|
||||
export function participantUpdated(participant: IParticipant = { id: '' }) {
|
||||
const participantToUpdate = {
|
||||
...participant
|
||||
};
|
||||
|
||||
if (participant.name) {
|
||||
participantToUpdate.name = getNormalizedDisplayName(participant.name);
|
||||
}
|
||||
|
||||
return {
|
||||
type: PARTICIPANT_UPDATED,
|
||||
participant: participantToUpdate
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a participant has muted us.
|
||||
*
|
||||
* @param {JitsiParticipant} participant - Information about participant.
|
||||
* @param {JitsiLocalTrack} track - Information about the track that has been muted.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function participantMutedUs(participant: any, track: any) {
|
||||
return {
|
||||
type: PARTICIPANT_MUTED_US,
|
||||
participant,
|
||||
track
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to create a virtual screenshare participant.
|
||||
*
|
||||
* @param {(string)} sourceName - The source name of the JitsiTrack instance.
|
||||
* @param {(boolean)} local - Whether it's a local or remote participant.
|
||||
* @param {JitsiConference} conference - The conference instance for which the participant is to be created.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export function createVirtualScreenshareParticipant(sourceName: string, local: boolean, conference?: IJitsiConference) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(sourceName);
|
||||
const ownerName = getParticipantDisplayName(state, ownerId);
|
||||
|
||||
dispatch(participantJoined({
|
||||
conference,
|
||||
fakeParticipant: local ? FakeParticipant.LocalScreenShare : FakeParticipant.RemoteScreenShare,
|
||||
id: sourceName,
|
||||
name: ownerName
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to signal that a participant had been kicked.
|
||||
*
|
||||
* @param {JitsiParticipant} kicker - Information about participant performing the kick.
|
||||
* @param {JitsiParticipant} kicked - Information about participant that was kicked.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function participantKicked(kicker: any, kicked: any) {
|
||||
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
|
||||
const state = getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
const kickedId = kicked.getId();
|
||||
const kickerId = kicker?.getId();
|
||||
|
||||
dispatch({
|
||||
type: PARTICIPANT_KICKED,
|
||||
kicked: kickedId,
|
||||
kicker: kickerId
|
||||
});
|
||||
|
||||
if (kicked.isReplaced?.() || !kickerId || kickerId === localParticipant?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleArguments: {
|
||||
kicked:
|
||||
getParticipantDisplayName(state, kickedId),
|
||||
kicker:
|
||||
getParticipantDisplayName(state, kickerId)
|
||||
},
|
||||
titleKey: 'notify.kickParticipant'
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an action which pins a conference participant.
|
||||
*
|
||||
* @param {string|null} id - The ID of the conference participant to pin or null
|
||||
* if none of the conference's participants are to be pinned.
|
||||
* @returns {{
|
||||
* type: PIN_PARTICIPANT,
|
||||
* participant: {
|
||||
* id: string
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function pinParticipant(id?: string | null) {
|
||||
return {
|
||||
type: PIN_PARTICIPANT,
|
||||
participant: {
|
||||
id
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an action which notifies the app that the loadable URL of the avatar of a participant got updated.
|
||||
*
|
||||
* @param {string} participantId - The ID of the participant.
|
||||
* @param {string} url - The new URL.
|
||||
* @param {boolean} useCORS - Indicates whether we need to use CORS for this URL.
|
||||
* @returns {{
|
||||
* type: SET_LOADABLE_AVATAR_URL,
|
||||
* participant: {
|
||||
* id: string,
|
||||
* loadableAvatarUrl: string,
|
||||
* loadableAvatarUrlUseCORS: boolean
|
||||
* }
|
||||
* }}
|
||||
*/
|
||||
export function setLoadableAvatarUrl(participantId: string, url: string, useCORS: boolean) {
|
||||
return {
|
||||
type: SET_LOADABLE_AVATAR_URL,
|
||||
participant: {
|
||||
id: participantId,
|
||||
loadableAvatarUrl: url,
|
||||
loadableAvatarUrlUseCORS: useCORS
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise hand for the local participant.
|
||||
*
|
||||
* @param {boolean} enabled - Raise or lower hand.
|
||||
* @returns {{
|
||||
* type: LOCAL_PARTICIPANT_RAISE_HAND,
|
||||
* raisedHandTimestamp: number
|
||||
* }}
|
||||
*/
|
||||
export function raiseHand(enabled: boolean) {
|
||||
return {
|
||||
type: LOCAL_PARTICIPANT_RAISE_HAND,
|
||||
raisedHandTimestamp: enabled ? Date.now() : 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the raise hand queue.
|
||||
*
|
||||
* @returns {{
|
||||
* type: RAISE_HAND_CLEAR
|
||||
* }}
|
||||
*/
|
||||
export function raiseHandClear() {
|
||||
return {
|
||||
type: RAISE_HAND_CLEAR
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update raise hand queue of participants.
|
||||
*
|
||||
* @param {Object} participant - Participant that updated raised hand.
|
||||
* @returns {{
|
||||
* type: RAISE_HAND_UPDATED,
|
||||
* participant: Object
|
||||
* }}
|
||||
*/
|
||||
export function raiseHandUpdateQueue(participant: IParticipant) {
|
||||
return {
|
||||
type: RAISE_HAND_UPDATED,
|
||||
participant
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies if the local participant audio level has changed.
|
||||
*
|
||||
* @param {number} level - The audio level.
|
||||
* @returns {{
|
||||
* type: LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
|
||||
* level: number
|
||||
* }}
|
||||
*/
|
||||
export function localParticipantAudioLevelChanged(level: number) {
|
||||
return {
|
||||
type: LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
|
||||
level
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the name of the participant with the given id.
|
||||
*
|
||||
* @param {string} id - Participant id;.
|
||||
* @param {string} name - New participant name.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function overwriteParticipantName(id: string, name: string) {
|
||||
return {
|
||||
type: OVERWRITE_PARTICIPANT_NAME,
|
||||
id,
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrites the names of the given participants.
|
||||
*
|
||||
* @param {Array<Object>} participantList - The list of participants to overwrite.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function overwriteParticipantsNames(participantList: IParticipant[]) {
|
||||
return {
|
||||
type: OVERWRITE_PARTICIPANTS_NAMES,
|
||||
participantList
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Local video recording status for the local participant.
|
||||
*
|
||||
* @param {boolean} recording - If local recording is ongoing.
|
||||
* @param {boolean} onlySelf - If recording only local streams.
|
||||
* @returns {{
|
||||
* type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
|
||||
* recording: boolean
|
||||
* }}
|
||||
*/
|
||||
export function updateLocalRecordingStatus(recording: boolean, onlySelf?: boolean) {
|
||||
return {
|
||||
type: SET_LOCAL_PARTICIPANT_RECORDING_STATUS,
|
||||
recording,
|
||||
onlySelf
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
import React, { Component } from 'react';
|
||||
import { GestureResponderEvent, Text, TextStyle, View, ViewStyle } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import {
|
||||
isTrackStreamingStatusActive,
|
||||
isTrackStreamingStatusInactive
|
||||
} from '../../../connection-indicator/functions';
|
||||
import SharedVideo from '../../../shared-video/components/native/SharedVideo';
|
||||
import { isSharedVideoEnabled } from '../../../shared-video/functions';
|
||||
import { IStateful } from '../../app/types';
|
||||
import Avatar from '../../avatar/components/Avatar';
|
||||
import { translate } from '../../i18n/functions';
|
||||
import VideoTrack from '../../media/components/native/VideoTrack';
|
||||
import { shouldRenderVideoTrack } from '../../media/functions';
|
||||
import Container from '../../react/components/native/Container';
|
||||
import { toState } from '../../redux/functions';
|
||||
import { StyleType } from '../../styles/functions.any';
|
||||
import TestHint from '../../testing/components/TestHint';
|
||||
import { getVideoTrackByParticipant } from '../../tracks/functions';
|
||||
import { ITrack } from '../../tracks/types';
|
||||
import { getParticipantById, getParticipantDisplayName, isSharedVideoParticipant } from '../functions';
|
||||
|
||||
import styles from './styles';
|
||||
|
||||
/**
|
||||
* The type of the React {@link Component} props of {@link ParticipantView}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Whether the connection is inactive or not.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_isConnectionInactive: boolean;
|
||||
|
||||
/**
|
||||
* Whether the participant is a shared video participant.
|
||||
*/
|
||||
_isSharedVideoParticipant: boolean;
|
||||
|
||||
/**
|
||||
* The name of the participant which this component represents.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_participantName: string;
|
||||
|
||||
/**
|
||||
* True if the video should be rendered, false otherwise.
|
||||
*/
|
||||
_renderVideo: boolean;
|
||||
|
||||
/**
|
||||
* Whether the shared video is enabled or not.
|
||||
*/
|
||||
_sharedVideoEnabled: boolean;
|
||||
|
||||
/**
|
||||
* The video Track of the participant with {@link #participantId}.
|
||||
*/
|
||||
_videoTrack?: ITrack;
|
||||
|
||||
/**
|
||||
* The avatar size.
|
||||
*/
|
||||
avatarSize: number;
|
||||
|
||||
/**
|
||||
* Whether video should be disabled for his view.
|
||||
*/
|
||||
disableVideo?: boolean;
|
||||
|
||||
/**
|
||||
* Callback to invoke when the {@code ParticipantView} is clicked/pressed.
|
||||
*/
|
||||
onPress: (e?: GestureResponderEvent) => void;
|
||||
|
||||
/**
|
||||
* The ID of the participant (to be) depicted by {@link ParticipantView}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
participantId: string;
|
||||
|
||||
/**
|
||||
* The style, if any, to apply to {@link ParticipantView} in addition to its
|
||||
* default style.
|
||||
*/
|
||||
style: StyleType;
|
||||
|
||||
/**
|
||||
* The function to translate human-readable text.
|
||||
*/
|
||||
t: Function;
|
||||
|
||||
/**
|
||||
* The test hint id which can be used to locate the {@code ParticipantView}
|
||||
* on the jitsi-meet-torture side. If not provided, the
|
||||
* {@code participantId} with the following format will be used:
|
||||
* {@code `org.jitsi.meet.Participant#${participantId}`}.
|
||||
*/
|
||||
testHintId?: string;
|
||||
|
||||
/**
|
||||
* Indicates if the connectivity info label should be shown, if appropriate.
|
||||
* It will be shown in case the connection is interrupted.
|
||||
*/
|
||||
useConnectivityInfoLabel: boolean;
|
||||
|
||||
/**
|
||||
* The z-order of the {@link Video} of {@link ParticipantView} in the
|
||||
* stacking space of all {@code Video}s. For more details, refer to the
|
||||
* {@code zOrder} property of the {@code Video} class for React Native.
|
||||
*/
|
||||
zOrder: number;
|
||||
|
||||
/**
|
||||
* Indicates whether zooming (pinch to zoom and/or drag) is enabled.
|
||||
*/
|
||||
zoomEnabled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a React Component which depicts a specific participant's avatar
|
||||
* and video.
|
||||
*
|
||||
* @augments Component
|
||||
*/
|
||||
class ParticipantView extends Component<IProps> {
|
||||
|
||||
/**
|
||||
* Renders the inactive connection status label.
|
||||
*
|
||||
* @private
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
_renderInactiveConnectionInfo() {
|
||||
const {
|
||||
avatarSize,
|
||||
_participantName: displayName,
|
||||
t
|
||||
} = this.props;
|
||||
|
||||
// XXX Consider splitting this component into 2: one for the large view
|
||||
// and one for the thumbnail. Some of these don't apply to both.
|
||||
const containerStyle = {
|
||||
...styles.connectionInfoContainer,
|
||||
width: avatarSize * 1.5
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
pointerEvents = 'box-none'
|
||||
style = { containerStyle as ViewStyle }>
|
||||
<Text style = { styles.connectionInfoText as TextStyle }>
|
||||
{ t('connection.LOW_BANDWIDTH', { displayName }) }
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
* @returns {ReactElement}
|
||||
*/
|
||||
override render() {
|
||||
const {
|
||||
_isConnectionInactive,
|
||||
_isSharedVideoParticipant,
|
||||
_renderVideo: renderVideo,
|
||||
_sharedVideoEnabled,
|
||||
_videoTrack: videoTrack,
|
||||
disableVideo,
|
||||
onPress
|
||||
} = this.props;
|
||||
|
||||
const testHintId
|
||||
= this.props.testHintId
|
||||
? this.props.testHintId
|
||||
: `org.jitsi.meet.Participant#${this.props.participantId}`;
|
||||
|
||||
const renderSharedVideo = _isSharedVideoParticipant && !disableVideo && _sharedVideoEnabled;
|
||||
|
||||
return (
|
||||
<Container
|
||||
onClick = { renderVideo || renderSharedVideo ? undefined : onPress }
|
||||
style = {{
|
||||
...styles.participantView,
|
||||
...this.props.style
|
||||
}}
|
||||
touchFeedback = { false }>
|
||||
|
||||
<TestHint
|
||||
id = { testHintId }
|
||||
onPress = { renderSharedVideo ? undefined : onPress }
|
||||
value = '' />
|
||||
|
||||
{ renderSharedVideo && <SharedVideo /> }
|
||||
|
||||
{ renderVideo
|
||||
&& <VideoTrack
|
||||
onPress = { onPress }
|
||||
videoTrack = { videoTrack }
|
||||
waitForVideoStarted = { false }
|
||||
zOrder = { this.props.zOrder }
|
||||
zoomEnabled = { this.props.zoomEnabled } /> }
|
||||
|
||||
{ !renderSharedVideo && !renderVideo
|
||||
&& <View style = { styles.avatarContainer as ViewStyle }>
|
||||
<Avatar
|
||||
participantId = { this.props.participantId }
|
||||
size = { this.props.avatarSize } />
|
||||
</View> }
|
||||
|
||||
{ _isConnectionInactive && this.props.useConnectivityInfoLabel
|
||||
&& this._renderInactiveConnectionInfo() }
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps (parts of) the redux state to the associated {@link ParticipantView}'s
|
||||
* props.
|
||||
*
|
||||
* @param {Object} state - The redux state.
|
||||
* @param {Object} ownProps - The React {@code Component} props passed to the
|
||||
* associated (instance of) {@code ParticipantView}.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState, ownProps: any) {
|
||||
const { disableVideo, participantId } = ownProps;
|
||||
const participant = getParticipantById(state, participantId);
|
||||
const videoTrack = getVideoTrackByParticipant(state, participant);
|
||||
|
||||
return {
|
||||
_isConnectionInactive: isTrackStreamingStatusInactive(videoTrack),
|
||||
_isSharedVideoParticipant: isSharedVideoParticipant(participant),
|
||||
_participantName: getParticipantDisplayName(state, participantId),
|
||||
_renderVideo: shouldRenderParticipantVideo(state, participantId) && !disableVideo,
|
||||
_sharedVideoEnabled: isSharedVideoEnabled(state),
|
||||
_videoTrack: videoTrack
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the video of the participant should be rendered.
|
||||
*
|
||||
* @param {Object|Function} stateful - Object or function that can be resolved
|
||||
* to the Redux state.
|
||||
* @param {string} id - The ID of the participant.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function shouldRenderParticipantVideo(stateful: IStateful, id: string) {
|
||||
const state = toState(stateful);
|
||||
const participant = getParticipantById(state, id);
|
||||
|
||||
if (!participant) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* First check if we have an unmuted video track. */
|
||||
const videoTrack = getVideoTrackByParticipant(state, participant);
|
||||
|
||||
if (!videoTrack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!shouldRenderVideoTrack(videoTrack, /* waitForVideoStarted */ false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Then check if the participant connection or track streaming status is active. */
|
||||
if (!videoTrack.local && !isTrackStreamingStatusActive(videoTrack)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Then check if audio-only mode is not active. */
|
||||
const audioOnly = state['features/base/audio-only'].enabled;
|
||||
|
||||
if (!audioOnly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Last, check if the participant is sharing their screen and they are on stage. */
|
||||
const remoteScreenShares = state['features/video-layout'].remoteScreenShares || [];
|
||||
const largeVideoParticipantId = state['features/large-video'].participantId;
|
||||
const participantIsInLargeVideoWithScreen
|
||||
= participant.id === largeVideoParticipantId && remoteScreenShares.includes(participant.id);
|
||||
|
||||
return participantIsInLargeVideoWithScreen;
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(ParticipantView));
|
||||
46
react/features/base/participants/components/styles.ts
Normal file
46
react/features/base/participants/components/styles.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { BoxModel } from '../../styles/components/styles/BoxModel';
|
||||
import { ColorPalette } from '../../styles/components/styles/ColorPalette';
|
||||
|
||||
/**
|
||||
* The styles of the feature base/participants.
|
||||
*/
|
||||
export default {
|
||||
/**
|
||||
* Container for the avatar in the view.
|
||||
*/
|
||||
avatarContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the text rendered when there is a connectivity problem.
|
||||
*/
|
||||
connectionInfoText: {
|
||||
color: ColorPalette.white,
|
||||
fontSize: 12,
|
||||
marginVertical: BoxModel.margin,
|
||||
marginHorizontal: BoxModel.margin,
|
||||
textAlign: 'center'
|
||||
},
|
||||
|
||||
/**
|
||||
* Style for the container of the text rendered when there is a
|
||||
* connectivity problem.
|
||||
*/
|
||||
connectionInfoContainer: {
|
||||
alignSelf: 'center',
|
||||
backgroundColor: ColorPalette.darkGrey,
|
||||
borderRadius: 20,
|
||||
marginTop: BoxModel.margin
|
||||
},
|
||||
|
||||
/**
|
||||
* {@code ParticipantView} Style.
|
||||
*/
|
||||
participantView: {
|
||||
alignItems: 'stretch',
|
||||
flex: 1,
|
||||
justifyContent: 'center'
|
||||
}
|
||||
};
|
||||
82
react/features/base/participants/constants.ts
Normal file
82
react/features/base/participants/constants.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { IconPhoneRinging, IconWhiteboard } from '../icons/svg';
|
||||
|
||||
/**
|
||||
* The relative path to the default/stock avatar (image) file used on both
|
||||
* Web/React and mobile/React Native (for the purposes of consistency).
|
||||
*
|
||||
* XXX (1) Web/React utilizes relativity on the Jitsi Meet deployment.
|
||||
* (2) Mobile/React Native utilizes relativity on the local file system at build
|
||||
* time. Unfortunately, the packager of React Native cannot deal with the
|
||||
* {@code const} early enough for {@code require} to succeed at runtime.
|
||||
* Anyway, be sure to synchronize the relative path on Web and mobile for the
|
||||
* purposes of consistency.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const DEFAULT_AVATAR_RELATIVE_PATH = 'images/avatar.png';
|
||||
|
||||
/**
|
||||
* The value for the "var" attribute of feature tag in disco-info packets.
|
||||
*/
|
||||
export const DISCO_REMOTE_CONTROL_FEATURE = 'http://jitsi.org/meet/remotecontrol';
|
||||
|
||||
/**
|
||||
* Icon URL for jigasi participants.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const JIGASI_PARTICIPANT_ICON = IconPhoneRinging;
|
||||
|
||||
/**
|
||||
* The local participant might not have real ID until she joins a conference,
|
||||
* so use 'local' as her default ID.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const LOCAL_PARTICIPANT_DEFAULT_ID = 'local';
|
||||
|
||||
/**
|
||||
* Max length of the display names.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const MAX_DISPLAY_NAME_LENGTH = 50;
|
||||
|
||||
/**
|
||||
* The identifier of the sound to be played when new remote participant joins
|
||||
* the room.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const PARTICIPANT_JOINED_SOUND_ID = 'PARTICIPANT_JOINED_SOUND';
|
||||
|
||||
/**
|
||||
* The identifier of the sound to be played when remote participant leaves
|
||||
* the room.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const PARTICIPANT_LEFT_SOUND_ID = 'PARTICIPANT_LEFT_SOUND';
|
||||
|
||||
/**
|
||||
* The set of possible XMPP MUC roles for conference participants.
|
||||
*
|
||||
* @enum {string}
|
||||
*/
|
||||
export const PARTICIPANT_ROLE = {
|
||||
MODERATOR: 'moderator',
|
||||
NONE: 'none',
|
||||
PARTICIPANT: 'participant'
|
||||
};
|
||||
|
||||
/**
|
||||
* The audio level at which the hand will be lowered if raised.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
export const LOWER_HAND_AUDIO_LEVEL = 0.2;
|
||||
|
||||
/**
|
||||
* Icon URL for the whiteboard participant.
|
||||
*/
|
||||
export const WHITEBOARD_PARTICIPANT_ICON = IconWhiteboard;
|
||||
859
react/features/base/participants/functions.ts
Normal file
859
react/features/base/participants/functions.ts
Normal file
@@ -0,0 +1,859 @@
|
||||
// @ts-expect-error
|
||||
import { getGravatarURL } from '@jitsi/js-utils/avatar';
|
||||
|
||||
import { IReduxState, IStore } from '../../app/types';
|
||||
import { isVisitorChatParticipant } from '../../chat/functions';
|
||||
import { isStageFilmstripAvailable } from '../../filmstrip/functions';
|
||||
import { isAddPeopleEnabled, isDialOutEnabled } from '../../invite/functions';
|
||||
import { toggleShareDialog } from '../../share-room/actions';
|
||||
import { iAmVisitor } from '../../visitors/functions';
|
||||
import { IVisitorChatParticipant } from '../../visitors/types';
|
||||
import { IStateful } from '../app/types';
|
||||
import { GRAVATAR_BASE_URL } from '../avatar/constants';
|
||||
import { isCORSAvatarURL } from '../avatar/functions';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import { ADD_PEOPLE_ENABLED } from '../flags/constants';
|
||||
import { getFeatureFlag } from '../flags/functions';
|
||||
import i18next from '../i18n/i18next';
|
||||
import { MEDIA_TYPE, MediaType, VIDEO_TYPE } from '../media/constants';
|
||||
import { toState } from '../redux/functions';
|
||||
import { getScreenShareTrack, isLocalTrackMuted } from '../tracks/functions.any';
|
||||
|
||||
import {
|
||||
JIGASI_PARTICIPANT_ICON,
|
||||
MAX_DISPLAY_NAME_LENGTH,
|
||||
PARTICIPANT_ROLE,
|
||||
WHITEBOARD_PARTICIPANT_ICON
|
||||
} from './constants';
|
||||
import { preloadImage } from './preloadImage';
|
||||
import { FakeParticipant, IJitsiParticipant, IParticipant, ISourceInfo } from './types';
|
||||
|
||||
|
||||
/**
|
||||
* Temp structures for avatar urls to be checked/preloaded.
|
||||
*/
|
||||
const AVATAR_QUEUE: Object[] = [];
|
||||
const AVATAR_CHECKED_URLS = new Map();
|
||||
/* eslint-disable arrow-body-style */
|
||||
const AVATAR_CHECKER_FUNCTIONS = [
|
||||
(participant: IParticipant) => {
|
||||
return participant?.isJigasi ? JIGASI_PARTICIPANT_ICON : null;
|
||||
},
|
||||
(participant: IParticipant) => {
|
||||
return isWhiteboardParticipant(participant) ? WHITEBOARD_PARTICIPANT_ICON : null;
|
||||
},
|
||||
(participant: IParticipant) => {
|
||||
return participant?.avatarURL ? participant.avatarURL : null;
|
||||
},
|
||||
(participant: IParticipant, store: IStore) => {
|
||||
const config = store.getState()['features/base/config'];
|
||||
const isGravatarDisabled = config.gravatar?.disabled;
|
||||
|
||||
if (participant?.email && !isGravatarDisabled) {
|
||||
const gravatarBaseURL = config.gravatar?.baseUrl
|
||||
|| config.gravatarBaseURL
|
||||
|| GRAVATAR_BASE_URL;
|
||||
|
||||
return getGravatarURL(participant.email, gravatarBaseURL);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
];
|
||||
/* eslint-enable arrow-body-style */
|
||||
|
||||
/**
|
||||
* Returns the list of active speakers that should be moved to the top of the sorted list of participants so that the
|
||||
* dominant speaker is visible always on the vertical filmstrip in stage layout.
|
||||
*
|
||||
* @param {Function | Object} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getActiveSpeakersToBeDisplayed(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
dominantSpeaker,
|
||||
fakeParticipants,
|
||||
sortedRemoteVirtualScreenshareParticipants,
|
||||
speakersList
|
||||
} = state['features/base/participants'];
|
||||
const { visibleRemoteParticipants } = state['features/filmstrip'];
|
||||
let activeSpeakers = new Map(speakersList);
|
||||
|
||||
// Do not re-sort the active speakers if dominant speaker is currently visible.
|
||||
if (dominantSpeaker && visibleRemoteParticipants.has(dominantSpeaker)) {
|
||||
return activeSpeakers;
|
||||
}
|
||||
let availableSlotsForActiveSpeakers = visibleRemoteParticipants.size;
|
||||
|
||||
if (activeSpeakers.has(dominantSpeaker ?? '')) {
|
||||
activeSpeakers.delete(dominantSpeaker ?? '');
|
||||
}
|
||||
|
||||
// Add dominant speaker to the beginning of the list (not including self) since the active speaker list is always
|
||||
// alphabetically sorted.
|
||||
if (dominantSpeaker && dominantSpeaker !== getLocalParticipant(state)?.id) {
|
||||
const updatedSpeakers = Array.from(activeSpeakers);
|
||||
|
||||
updatedSpeakers.splice(0, 0, [ dominantSpeaker, getParticipantById(state, dominantSpeaker)?.name ?? '' ]);
|
||||
activeSpeakers = new Map(updatedSpeakers);
|
||||
}
|
||||
|
||||
// Remove screenshares from the count.
|
||||
if (sortedRemoteVirtualScreenshareParticipants) {
|
||||
availableSlotsForActiveSpeakers -= sortedRemoteVirtualScreenshareParticipants.size * 2;
|
||||
for (const screenshare of Array.from(sortedRemoteVirtualScreenshareParticipants.keys())) {
|
||||
const ownerId = getVirtualScreenshareParticipantOwnerId(screenshare as string);
|
||||
|
||||
activeSpeakers.delete(ownerId);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove fake participants from the count.
|
||||
if (fakeParticipants) {
|
||||
availableSlotsForActiveSpeakers -= fakeParticipants.size;
|
||||
}
|
||||
const truncatedSpeakersList = Array.from(activeSpeakers).slice(0, availableSlotsForActiveSpeakers);
|
||||
|
||||
truncatedSpeakersList.sort((a: any, b: any) => a[1].localeCompare(b[1]));
|
||||
|
||||
return new Map(truncatedSpeakersList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
* @param {Object} participant - The participant to resolve avatars for.
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getFirstLoadableAvatarUrl(participant: IParticipant, store: IStore) {
|
||||
const deferred: any = Promise.withResolvers();
|
||||
const fullPromise = deferred.promise
|
||||
.then(() => _getFirstLoadableAvatarUrl(participant, store))
|
||||
.then((result: any) => {
|
||||
|
||||
if (AVATAR_QUEUE.length) {
|
||||
const next: any = AVATAR_QUEUE.shift();
|
||||
|
||||
next.resolve();
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
if (AVATAR_QUEUE.length) {
|
||||
AVATAR_QUEUE.push(deferred);
|
||||
} else {
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
return fullPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns local participant from Redux state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {(IParticipant|undefined)}
|
||||
*/
|
||||
export function getLocalParticipant(stateful: IStateful) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
|
||||
return state.local;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns local screen share participant from Redux state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state features/base/participants.
|
||||
* @returns {(IParticipant|undefined)}
|
||||
*/
|
||||
export function getLocalScreenShareParticipant(stateful: IStateful) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
|
||||
return state.localScreenShare;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns screenshare participant.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state features/base/participants.
|
||||
* @param {string} id - The owner ID of the screenshare participant to retrieve.
|
||||
* @returns {(IParticipant|undefined)}
|
||||
*/
|
||||
export function getVirtualScreenshareParticipantByOwnerId(stateful: IStateful, id: string) {
|
||||
const state = toState(stateful);
|
||||
const track = getScreenShareTrack(state['features/base/tracks'], id);
|
||||
|
||||
return getParticipantById(stateful, track?.jitsiTrack.getSourceName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a display name so then no invalid values (padding, length...etc)
|
||||
* can be set.
|
||||
*
|
||||
* @param {string} name - The display name to set.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getNormalizedDisplayName(name: string) {
|
||||
if (!name?.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return name.trim().substring(0, MAX_DISPLAY_NAME_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns participant by ID from Redux state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @param {string} id - The ID of the participant to retrieve.
|
||||
* @private
|
||||
* @returns {(IParticipant|undefined)}
|
||||
*/
|
||||
export function getParticipantById(stateful: IStateful, id: string): IParticipant | undefined {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
const { local, localScreenShare, remote } = state;
|
||||
|
||||
return remote.get(id)
|
||||
|| (local?.id === id ? local : undefined)
|
||||
|| (localScreenShare?.id === id ? localScreenShare : undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the participant with the ID matching the passed ID or the local participant if the ID is
|
||||
* undefined.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @param {string|undefined} [participantID] - An optional partipantID argument.
|
||||
* @returns {IParticipant|undefined}
|
||||
*/
|
||||
export function getParticipantByIdOrUndefined(stateful: IStateful, participantID?: string) {
|
||||
return participantID ? getParticipantById(stateful, participantID) : getLocalParticipant(stateful);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a count of the known participants in the passed in redux state,
|
||||
* excluding any fake participants.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getParticipantCount(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const {
|
||||
local,
|
||||
remote,
|
||||
fakeParticipants,
|
||||
sortedRemoteVirtualScreenshareParticipants
|
||||
} = state['features/base/participants'];
|
||||
|
||||
return remote.size - fakeParticipants.size - sortedRemoteVirtualScreenshareParticipants.size + (local ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns participant ID of the owner of a virtual screenshare participant.
|
||||
*
|
||||
* @param {string} id - The ID of the virtual screenshare participant.
|
||||
* @private
|
||||
* @returns {(string|undefined)}
|
||||
*/
|
||||
export function getVirtualScreenshareParticipantOwnerId(id: string) {
|
||||
return id.split('-')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Map with fake participants.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {Map<string, IParticipant>} - The Map with fake participants.
|
||||
*/
|
||||
export function getFakeParticipants(stateful: IStateful) {
|
||||
return toState(stateful)['features/base/participants'].fakeParticipants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the fake participant is a local screenshare.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant entity.
|
||||
* @returns {boolean} - True if it's a local screenshare participant.
|
||||
*/
|
||||
export function isLocalScreenshareParticipant(participant?: IParticipant): boolean {
|
||||
return participant?.fakeParticipant === FakeParticipant.LocalScreenShare;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the fake participant is a remote screenshare.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant entity.
|
||||
* @returns {boolean} - True if it's a remote screenshare participant.
|
||||
*/
|
||||
export function isRemoteScreenshareParticipant(participant?: IParticipant): boolean {
|
||||
return participant?.fakeParticipant === FakeParticipant.RemoteScreenShare;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the fake participant is of local or virtual screenshare type.
|
||||
*
|
||||
* @param {IReduxState} state - The (whole) redux state, or redux's.
|
||||
* @param {string|undefined} participantId - The participant id.
|
||||
* @returns {boolean} - True if it's one of the two.
|
||||
*/
|
||||
export function isScreenShareParticipantById(state: IReduxState, participantId?: string): boolean {
|
||||
const participant = getParticipantByIdOrUndefined(state, participantId);
|
||||
|
||||
return isScreenShareParticipant(participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the fake participant is of local or virtual screenshare type.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant entity.
|
||||
* @returns {boolean} - True if it's one of the two.
|
||||
*/
|
||||
export function isScreenShareParticipant(participant?: IParticipant): boolean {
|
||||
return isLocalScreenshareParticipant(participant) || isRemoteScreenshareParticipant(participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the (fake) participant is a shared video.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant entity.
|
||||
* @returns {boolean} - True if it's a shared video participant.
|
||||
*/
|
||||
export function isSharedVideoParticipant(participant?: IParticipant): boolean {
|
||||
return participant?.fakeParticipant === FakeParticipant.SharedVideo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the fake participant is a whiteboard.
|
||||
*
|
||||
* @param {IParticipant|undefined} participant - The participant entity.
|
||||
* @returns {boolean} - True if it's a whiteboard participant.
|
||||
*/
|
||||
export function isWhiteboardParticipant(participant?: IParticipant): boolean {
|
||||
return participant?.fakeParticipant === FakeParticipant.Whiteboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a count of the known remote participants in the passed in redux state.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getRemoteParticipantCountWithFake(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const participantsState = state['features/base/participants'];
|
||||
|
||||
return participantsState.remote.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the muted state of the given media source for a given participant.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's.
|
||||
* @param {IParticipant} participant - The participant entity.
|
||||
* @param {MediaType} mediaType - The media type.
|
||||
* @returns {boolean} - True its muted, false otherwise.
|
||||
*/
|
||||
export function getMutedStateByParticipantAndMediaType(
|
||||
stateful: IStateful,
|
||||
participant: IParticipant,
|
||||
mediaType: MediaType): boolean {
|
||||
const type = mediaType === MEDIA_TYPE.SCREENSHARE ? 'video' : mediaType;
|
||||
|
||||
if (participant.local) {
|
||||
const state = toState(stateful);
|
||||
const tracks = state['features/base/tracks'];
|
||||
|
||||
return isLocalTrackMuted(tracks, mediaType);
|
||||
}
|
||||
|
||||
const sources = participant.sources?.get(type);
|
||||
|
||||
if (!sources) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mediaType === MEDIA_TYPE.AUDIO) {
|
||||
return Array.from(sources.values())[0].muted;
|
||||
}
|
||||
const videoType = mediaType === MEDIA_TYPE.VIDEO ? VIDEO_TYPE.CAMERA : VIDEO_TYPE.DESKTOP;
|
||||
const source = Array.from(sources.values()).find(src => src.videoType === videoType);
|
||||
|
||||
return source?.muted ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a count of the known participants in the passed in redux state,
|
||||
* including fake participants.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getParticipantCountWithFake(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { local, localScreenShare, remote } = state['features/base/participants'];
|
||||
|
||||
return remote.size + (local ? 1 : 0) + (localScreenShare ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a count of the known participants in the passed in redux state,
|
||||
* including fake participants. Subtract 1 when the local participant is a visitor as we do not show a local thumbnail.
|
||||
* The number used to display the participant count in the UI.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function getParticipantCountForDisplay(stateful: IStateful) {
|
||||
const _iAmVisitor = iAmVisitor(stateful);
|
||||
|
||||
return getParticipantCount(stateful) - (_iAmVisitor ? 1 : 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns participant's display name.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @param {string} id - The ID of the participant's display name to retrieve.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getParticipantDisplayName(stateful: IStateful, id: string): string {
|
||||
const state = toState(stateful);
|
||||
const participant = getParticipantById(state, id);
|
||||
const {
|
||||
defaultLocalDisplayName,
|
||||
defaultRemoteDisplayName
|
||||
} = state['features/base/config'];
|
||||
|
||||
if (participant) {
|
||||
if (isScreenShareParticipant(participant)) {
|
||||
return getScreenshareParticipantDisplayName(state, id);
|
||||
}
|
||||
|
||||
if (participant.name) {
|
||||
return participant.name;
|
||||
}
|
||||
|
||||
if (participant.local) {
|
||||
return defaultLocalDisplayName ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
return defaultRemoteDisplayName ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the source names of the screenshare sources in the conference based on the presence shared by the remote
|
||||
* endpoints. This should be only used for creating/removing virtual screenshare participant tiles when ssrc-rewriting
|
||||
* is enabled. Once the tile is created, the source-name gets added to the receiver constraints based on which the
|
||||
* JVB will add the source to the video sources map and signal it to the local endpoint. Only then, a remote track is
|
||||
* created/remapped and the tracks in redux will be updated. Once the track is updated in redux, the client will
|
||||
* will continue to use the other track based getter functions for other operations related to screenshare.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function getRemoteScreensharesBasedOnPresence(stateful: IStateful): string[] {
|
||||
const conference = getCurrentConference(stateful);
|
||||
|
||||
return conference?.getParticipants()?.reduce((screenshares: string[], participant: IJitsiParticipant) => {
|
||||
const sources: Map<string, Map<string, ISourceInfo>> = participant.getSources();
|
||||
const videoSources = sources.get(MEDIA_TYPE.VIDEO);
|
||||
const screenshareSources = Array.from(videoSources ?? new Map())
|
||||
.filter(source => source[1].videoType === VIDEO_TYPE.DESKTOP && !source[1].muted)
|
||||
.map(source => source[0]);
|
||||
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
screenshares = [ ...screenshares, ...screenshareSources ];
|
||||
|
||||
return screenshares;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns screenshare participant's display name.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @param {string} id - The ID of the screenshare participant's display name to retrieve.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getScreenshareParticipantDisplayName(stateful: IStateful, id: string) {
|
||||
const ownerDisplayName = getParticipantDisplayName(stateful, getVirtualScreenshareParticipantOwnerId(id));
|
||||
|
||||
return i18next.t('screenshareDisplayName', { name: ownerDisplayName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of IDs of the participants that are currently screensharing.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getScreenshareParticipantIds(stateful: IStateful): Array<string> {
|
||||
return toState(stateful)['features/base/tracks']
|
||||
.filter(track => track.videoType === VIDEO_TYPE.DESKTOP && !track.muted)
|
||||
.map(t => t.participantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of source names associated with a given remote participant and for the given media type.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @param {string} id - The id of the participant whose source names are to be retrieved.
|
||||
* @param {string} mediaType - The type of source, audio or video.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getSourceNamesByMediaTypeAndParticipant(
|
||||
stateful: IStateful,
|
||||
id: string,
|
||||
mediaType: string): Array<string> {
|
||||
const participant: IParticipant | undefined = getParticipantById(stateful, id);
|
||||
|
||||
if (!participant) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sources = participant.sources;
|
||||
|
||||
if (!sources) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(sources.get(mediaType) ?? new Map())
|
||||
.filter(source => source[1].videoType !== VIDEO_TYPE.DESKTOP || !source[1].muted)
|
||||
.map(s => s[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of source names associated with a given remote participant and for the given video type (only for
|
||||
* video sources).
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state.
|
||||
* @param {string} id - The id of the participant whose source names are to be retrieved.
|
||||
* @param {string} videoType - The type of video, camera or desktop.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getSourceNamesByVideoTypeAndParticipant(
|
||||
stateful: IStateful,
|
||||
id: string,
|
||||
videoType: string): Array<string> {
|
||||
const participant: IParticipant | undefined = getParticipantById(stateful, id);
|
||||
|
||||
if (!participant) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const sources = participant.sources;
|
||||
|
||||
if (!sources) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Array.from(sources.get(MEDIA_TYPE.VIDEO) ?? new Map())
|
||||
.filter(source => source[1].videoType === videoType && (videoType === VIDEO_TYPE.CAMERA || !source[1].muted))
|
||||
.map(s => s[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the presence status of a participant associated with the passed id.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state.
|
||||
* @param {string} id - The id of the participant.
|
||||
* @returns {string} - The presence status.
|
||||
*/
|
||||
export function getParticipantPresenceStatus(stateful: IStateful, id: string) {
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
const participantById = getParticipantById(stateful, id);
|
||||
|
||||
if (!participantById) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return participantById.presence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selectors for getting all remote participants.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {Map<string, Object>}
|
||||
*/
|
||||
export function getRemoteParticipants(stateful: IStateful): Map<string, IParticipant> {
|
||||
return toState(stateful)['features/base/participants'].remote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selectors for the getting the remote participants in the order that they are displayed in the filmstrip.
|
||||
*
|
||||
@param {(Function|Object)} stateful - The (whole) redux state, or redux's {@code getState} function to be used to
|
||||
* retrieve the state features/filmstrip.
|
||||
* @returns {Array<string>}
|
||||
*/
|
||||
export function getRemoteParticipantsSorted(stateful: IStateful) {
|
||||
return toState(stateful)['features/filmstrip'].remoteParticipants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the participant which has its pinned state set to truthy.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {(IParticipant|undefined)}
|
||||
*/
|
||||
export function getPinnedParticipant(stateful: IStateful) {
|
||||
const state = toState(stateful);
|
||||
const { pinnedParticipant } = state['features/base/participants'];
|
||||
const stageFilmstrip = isStageFilmstripAvailable(state);
|
||||
|
||||
if (stageFilmstrip) {
|
||||
const { activeParticipants } = state['features/filmstrip'];
|
||||
const id = activeParticipants.find(p => p.pinned)?.participantId;
|
||||
|
||||
return id ? getParticipantById(stateful, id) : undefined;
|
||||
}
|
||||
|
||||
if (!pinnedParticipant) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getParticipantById(stateful, pinnedParticipant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the participant is a moderator.
|
||||
*
|
||||
* @param {string} participant - Participant object.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isParticipantModerator(participant?: IParticipant) {
|
||||
return participant?.role === PARTICIPANT_ROLE.MODERATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the dominant speaker participant.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state or redux's
|
||||
* {@code getState} function to be used to retrieve the state features/base/participants.
|
||||
* @returns {IParticipant} - The participant from the redux store.
|
||||
*/
|
||||
export function getDominantSpeakerParticipant(stateful: IStateful) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
const { dominantSpeaker } = state;
|
||||
|
||||
if (!dominantSpeaker) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getParticipantById(stateful, dominantSpeaker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all of the meeting participants are moderators.
|
||||
*
|
||||
* @param {Object|Function} stateful -Object or function that can be resolved
|
||||
* to the Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isEveryoneModerator(stateful: IStateful) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
|
||||
return state.numberOfNonModeratorParticipants === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks a value and returns true if it's a preloaded icon object.
|
||||
*
|
||||
* @param {?string | ?Object} icon - The icon to check.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isIconUrl(icon?: string | Object) {
|
||||
return Boolean(icon) && (typeof icon === 'object' || typeof icon === 'function');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current local participant is a moderator in the
|
||||
* conference.
|
||||
*
|
||||
* @param {Object|Function} stateful - Object or function that can be resolved
|
||||
* to the Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isLocalParticipantModerator(stateful: IStateful) {
|
||||
const state = toState(stateful)['features/base/participants'];
|
||||
|
||||
const { local } = state;
|
||||
|
||||
if (!local) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isParticipantModerator(local);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the first loadable avatar URL for a participant.
|
||||
*
|
||||
* @param {Object} participant - The participant to resolve avatars for.
|
||||
* @param {Store} store - Redux store.
|
||||
* @returns {?string}
|
||||
*/
|
||||
async function _getFirstLoadableAvatarUrl(participant: IParticipant, store: IStore) {
|
||||
for (let i = 0; i < AVATAR_CHECKER_FUNCTIONS.length; i++) {
|
||||
const url = AVATAR_CHECKER_FUNCTIONS[i](participant, store);
|
||||
|
||||
if (url !== null) {
|
||||
if (AVATAR_CHECKED_URLS.has(url)) {
|
||||
const { isLoadable, isUsingCORS } = AVATAR_CHECKED_URLS.get(url) || {};
|
||||
|
||||
if (isLoadable) {
|
||||
return {
|
||||
isUsingCORS,
|
||||
src: url
|
||||
};
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const { corsAvatarURLs } = store.getState()['features/base/config'];
|
||||
const useCORS = isIconUrl(url) ? false : isCORSAvatarURL(url, corsAvatarURLs);
|
||||
const { isUsingCORS, src } = await preloadImage(url, useCORS);
|
||||
|
||||
AVATAR_CHECKED_URLS.set(src, {
|
||||
isLoadable: true,
|
||||
isUsingCORS
|
||||
});
|
||||
|
||||
return {
|
||||
isUsingCORS,
|
||||
src
|
||||
};
|
||||
} catch (e) {
|
||||
AVATAR_CHECKED_URLS.set(url, {
|
||||
isLoadable: false,
|
||||
isUsingCORS: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the participants queue with raised hands.
|
||||
*
|
||||
* @param {(Function|Object)} stateful - The (whole) redux state, or redux's
|
||||
* {@code getState} function to be used to retrieve the state
|
||||
* features/base/participants.
|
||||
* @returns {Array<Object>}
|
||||
*/
|
||||
export function getRaiseHandsQueue(stateful: IStateful): Array<{ id: string; raisedHandTimestamp: number; }> {
|
||||
const { raisedHandsQueue } = toState(stateful)['features/base/participants'];
|
||||
|
||||
return raisedHandsQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given participant has his hand raised or not.
|
||||
*
|
||||
* @param {Object} participant - The participant.
|
||||
* @returns {boolean} - Whether participant has raise hand or not.
|
||||
*/
|
||||
export function hasRaisedHand(participant?: IParticipant): boolean {
|
||||
return Boolean(participant?.raisedHandTimestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add people feature enabling/disabling.
|
||||
*
|
||||
* @param {Object|Function} stateful - Object or function that can be resolved
|
||||
* to the Redux state.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const addPeopleFeatureControl = (stateful: IStateful) => {
|
||||
const state = toState(stateful);
|
||||
|
||||
return getFeatureFlag(state, ADD_PEOPLE_ENABLED, true)
|
||||
&& (isAddPeopleEnabled(state) || isDialOutEnabled(state));
|
||||
};
|
||||
|
||||
/**
|
||||
* Controls share dialog visibility.
|
||||
*
|
||||
* @param {boolean} addPeopleFeatureEnabled - Checks if add people functionality is enabled.
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @returns {Function}
|
||||
*/
|
||||
export const setShareDialogVisiblity = (addPeopleFeatureEnabled: boolean, dispatch: IStore['dispatch']) => {
|
||||
if (addPeopleFeatureEnabled) {
|
||||
dispatch(toggleShareDialog(false));
|
||||
} else {
|
||||
dispatch(toggleShareDialog(true));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if private chat is enabled for the given participant.
|
||||
*
|
||||
* @param {IParticipant|IVisitorChatParticipant|undefined} participant - The participant to check.
|
||||
* @param {IReduxState} state - The Redux state.
|
||||
* @returns {boolean} - True if private chat is enabled, false otherwise.
|
||||
*/
|
||||
export function isPrivateChatEnabled(participant: IParticipant | IVisitorChatParticipant | undefined, state: IReduxState) {
|
||||
const { remoteVideoMenu = {} } = state['features/base/config'];
|
||||
const { disablePrivateChat } = remoteVideoMenu;
|
||||
|
||||
if ((!isVisitorChatParticipant(participant) && participant?.local) || disablePrivateChat === 'all') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (disablePrivateChat === 'disable-visitor-chat') {
|
||||
// Block if the participant we're trying to message is a visitor
|
||||
// OR if the local user is a visitor
|
||||
if (isVisitorChatParticipant(participant) || iAmVisitor(state)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true; // should allow private chat for other participants
|
||||
}
|
||||
|
||||
if (disablePrivateChat === 'allow-moderator-chat') {
|
||||
return isLocalParticipantModerator(state) || isParticipantModerator(participant);
|
||||
}
|
||||
|
||||
return !disablePrivateChat;
|
||||
}
|
||||
3
react/features/base/participants/logger.ts
Normal file
3
react/features/base/participants/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/participants');
|
||||
969
react/features/base/participants/middleware.ts
Normal file
969
react/features/base/participants/middleware.ts
Normal file
@@ -0,0 +1,969 @@
|
||||
import i18n from 'i18next';
|
||||
import { batch } from 'react-redux';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import {
|
||||
approveParticipant,
|
||||
approveParticipantAudio,
|
||||
approveParticipantDesktop,
|
||||
approveParticipantVideo
|
||||
} from '../../av-moderation/actions';
|
||||
import {
|
||||
AUDIO_RAISED_HAND_NOTIFICATION_ID,
|
||||
DESKTOP_RAISED_HAND_NOTIFICATION_ID,
|
||||
MEDIA_TYPE,
|
||||
VIDEO_RAISED_HAND_NOTIFICATION_ID
|
||||
} from '../../av-moderation/constants';
|
||||
import { isForceMuted } from '../../av-moderation/functions';
|
||||
import { UPDATE_BREAKOUT_ROOMS } from '../../breakout-rooms/actionTypes';
|
||||
import { getBreakoutRooms } from '../../breakout-rooms/functions';
|
||||
import { toggleE2EE } from '../../e2ee/actions';
|
||||
import { MAX_MODE } from '../../e2ee/constants';
|
||||
import { hideNotification, showNotification } from '../../notifications/actions';
|
||||
import {
|
||||
LOCAL_RECORDING_NOTIFICATION_ID,
|
||||
NOTIFICATION_TIMEOUT_TYPE,
|
||||
RAISE_HAND_NOTIFICATION_ID
|
||||
} from '../../notifications/constants';
|
||||
import { open as openParticipantsPane } from '../../participants-pane/actions';
|
||||
import { CALLING, INVITED } from '../../presence-status/constants';
|
||||
import { RAISE_HAND_SOUND_ID } from '../../reactions/constants';
|
||||
import { RECORDING_OFF_SOUND_ID, RECORDING_ON_SOUND_ID } from '../../recording/constants';
|
||||
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app/actionTypes';
|
||||
import { CONFERENCE_JOINED, CONFERENCE_WILL_JOIN } from '../conference/actionTypes';
|
||||
import { forEachConference, getCurrentConference } from '../conference/functions';
|
||||
import { IJitsiConference } from '../conference/reducer';
|
||||
import { SET_CONFIG } from '../config/actionTypes';
|
||||
import { getDisableRemoveRaisedHandOnFocus } from '../config/functions.any';
|
||||
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
|
||||
import { VIDEO_TYPE } from '../media/constants';
|
||||
import MiddlewareRegistry from '../redux/MiddlewareRegistry';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
import { playSound, registerSound, unregisterSound } from '../sounds/actions';
|
||||
import { isImageDataURL } from '../util/uri';
|
||||
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
GRANT_MODERATOR,
|
||||
KICK_PARTICIPANT,
|
||||
LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED,
|
||||
LOCAL_PARTICIPANT_RAISE_HAND,
|
||||
MUTE_REMOTE_PARTICIPANT,
|
||||
OVERWRITE_PARTICIPANTS_NAMES,
|
||||
OVERWRITE_PARTICIPANT_NAME,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_MUTED_US,
|
||||
PARTICIPANT_UPDATED,
|
||||
RAISE_HAND_UPDATED,
|
||||
SET_LOCAL_PARTICIPANT_RECORDING_STATUS
|
||||
} from './actionTypes';
|
||||
import {
|
||||
localParticipantIdChanged,
|
||||
localParticipantJoined,
|
||||
localParticipantLeft,
|
||||
overwriteParticipantName,
|
||||
participantLeft,
|
||||
participantUpdated,
|
||||
raiseHand,
|
||||
raiseHandUpdateQueue,
|
||||
setLoadableAvatarUrl
|
||||
} from './actions';
|
||||
import {
|
||||
LOCAL_PARTICIPANT_DEFAULT_ID,
|
||||
LOWER_HAND_AUDIO_LEVEL,
|
||||
PARTICIPANT_JOINED_SOUND_ID,
|
||||
PARTICIPANT_LEFT_SOUND_ID
|
||||
} from './constants';
|
||||
import {
|
||||
getDominantSpeakerParticipant,
|
||||
getFirstLoadableAvatarUrl,
|
||||
getLocalParticipant,
|
||||
getParticipantById,
|
||||
getParticipantCount,
|
||||
getParticipantDisplayName,
|
||||
getRaiseHandsQueue,
|
||||
getRemoteParticipants,
|
||||
hasRaisedHand,
|
||||
isLocalParticipantModerator,
|
||||
isScreenShareParticipant,
|
||||
isWhiteboardParticipant
|
||||
} from './functions';
|
||||
import logger from './logger';
|
||||
import { PARTICIPANT_JOINED_FILE, PARTICIPANT_LEFT_FILE } from './sounds';
|
||||
import { IJitsiParticipant } from './types';
|
||||
|
||||
import './subscriber';
|
||||
|
||||
/**
|
||||
* Middleware that captures CONFERENCE_JOINED and CONFERENCE_LEFT actions and
|
||||
* updates respectively ID of local participant.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @returns {Function}
|
||||
*/
|
||||
MiddlewareRegistry.register(store => next => action => {
|
||||
switch (action.type) {
|
||||
case APP_WILL_MOUNT:
|
||||
_registerSounds(store);
|
||||
|
||||
return _localParticipantJoined(store, next, action);
|
||||
|
||||
case APP_WILL_UNMOUNT:
|
||||
_unregisterSounds(store);
|
||||
|
||||
return _localParticipantLeft(store, next, action);
|
||||
|
||||
case CONFERENCE_WILL_JOIN:
|
||||
store.dispatch(localParticipantIdChanged(action.conference.myUserId()));
|
||||
break;
|
||||
|
||||
case DOMINANT_SPEAKER_CHANGED: {
|
||||
// Lower hand through xmpp when local participant becomes dominant speaker.
|
||||
const { id } = action.participant;
|
||||
const state = store.getState();
|
||||
const participant = getLocalParticipant(state);
|
||||
const dominantSpeaker = getDominantSpeakerParticipant(state);
|
||||
const isLocal = participant && participant.id === id;
|
||||
|
||||
if (isLocal && dominantSpeaker?.id !== id
|
||||
&& hasRaisedHand(participant)
|
||||
&& !getDisableRemoveRaisedHandOnFocus(state)) {
|
||||
store.dispatch(raiseHand(false));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case LOCAL_PARTICIPANT_AUDIO_LEVEL_CHANGED: {
|
||||
const state = store.getState();
|
||||
const participant = getDominantSpeakerParticipant(state);
|
||||
|
||||
if (
|
||||
participant?.local
|
||||
&& hasRaisedHand(participant)
|
||||
&& action.level > LOWER_HAND_AUDIO_LEVEL
|
||||
&& !getDisableRemoveRaisedHandOnFocus(state)
|
||||
) {
|
||||
store.dispatch(raiseHand(false));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case GRANT_MODERATOR: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
conference?.grantOwner(action.id);
|
||||
break;
|
||||
}
|
||||
|
||||
case KICK_PARTICIPANT: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
conference?.kickParticipant(action.id);
|
||||
break;
|
||||
}
|
||||
|
||||
case LOCAL_PARTICIPANT_RAISE_HAND: {
|
||||
const { raisedHandTimestamp } = action;
|
||||
const localId = getLocalParticipant(store.getState())?.id;
|
||||
|
||||
store.dispatch(participantUpdated({
|
||||
// XXX Only the local participant is allowed to update without
|
||||
// stating the JitsiConference instance (i.e. participant property
|
||||
// `conference` for a remote participant) because the local
|
||||
// participant is uniquely identified by the very fact that there is
|
||||
// only one local participant.
|
||||
|
||||
id: localId ?? '',
|
||||
local: true,
|
||||
raisedHandTimestamp
|
||||
}));
|
||||
|
||||
store.dispatch(raiseHandUpdateQueue({
|
||||
id: localId ?? '',
|
||||
raisedHandTimestamp
|
||||
}));
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRaiseHandUpdated(localId, raisedHandTimestamp);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case SET_CONFIG: {
|
||||
const result = next(action);
|
||||
|
||||
const state = store.getState();
|
||||
const { deploymentInfo } = state['features/base/config'];
|
||||
|
||||
// if there userRegion set let's use it for the local participant
|
||||
if (deploymentInfo?.userRegion) {
|
||||
const localId = getLocalParticipant(state)?.id;
|
||||
|
||||
if (localId) {
|
||||
store.dispatch(participantUpdated({
|
||||
id: localId,
|
||||
local: true,
|
||||
region: deploymentInfo.userRegion
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
case CONFERENCE_JOINED: {
|
||||
const result = next(action);
|
||||
|
||||
const state = store.getState();
|
||||
const { startSilent } = state['features/base/config'];
|
||||
|
||||
if (startSilent) {
|
||||
const localId = getLocalParticipant(store.getState())?.id;
|
||||
|
||||
if (localId) {
|
||||
store.dispatch(participantUpdated({
|
||||
id: localId,
|
||||
local: true,
|
||||
isSilent: startSilent
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
case SET_LOCAL_PARTICIPANT_RECORDING_STATUS: {
|
||||
const state = store.getState();
|
||||
const { recording, onlySelf } = action;
|
||||
const localId = getLocalParticipant(state)?.id;
|
||||
const { localRecording } = state['features/base/config'];
|
||||
|
||||
if (localRecording?.notifyAllParticipants && !onlySelf && localId) {
|
||||
store.dispatch(participantUpdated({
|
||||
// XXX Only the local participant is allowed to update without
|
||||
// stating the JitsiConference instance (i.e. participant property
|
||||
// `conference` for a remote participant) because the local
|
||||
// participant is uniquely identified by the very fact that there is
|
||||
// only one local participant.
|
||||
|
||||
id: localId,
|
||||
local: true,
|
||||
localRecording: recording
|
||||
}));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case MUTE_REMOTE_PARTICIPANT: {
|
||||
const { conference } = store.getState()['features/base/conference'];
|
||||
|
||||
conference?.muteParticipant(action.id, action.mediaType);
|
||||
break;
|
||||
}
|
||||
|
||||
case RAISE_HAND_UPDATED: {
|
||||
const { participant } = action;
|
||||
let queue = getRaiseHandsQueue(store.getState());
|
||||
|
||||
if (participant.raisedHandTimestamp) {
|
||||
queue = [ ...queue, { id: participant.id,
|
||||
raisedHandTimestamp: participant.raisedHandTimestamp } ];
|
||||
|
||||
// sort the queue before adding to store.
|
||||
queue = queue.sort(({ raisedHandTimestamp: a }, { raisedHandTimestamp: b }) => a - b);
|
||||
} else {
|
||||
// no need to sort on remove value.
|
||||
queue = queue.filter(({ id }) => id !== participant.id);
|
||||
}
|
||||
|
||||
action.queue = queue;
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_JOINED: {
|
||||
// Do not play sounds when a screenshare or whiteboard participant tile is created for screenshare.
|
||||
(!isScreenShareParticipant(action.participant)
|
||||
&& !isWhiteboardParticipant(action.participant)
|
||||
) && _maybePlaySounds(store, action);
|
||||
|
||||
return _participantJoinedOrUpdated(store, next, action);
|
||||
}
|
||||
|
||||
case PARTICIPANT_LEFT: {
|
||||
// Do not play sounds when a tile for screenshare or whiteboard is removed.
|
||||
(!isScreenShareParticipant(action.participant)
|
||||
&& !isWhiteboardParticipant(action.participant)
|
||||
) && _maybePlaySounds(store, action);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_MUTED_US: {
|
||||
const { dispatch, getState } = store;
|
||||
const { participant, track } = action;
|
||||
let titleKey;
|
||||
|
||||
if (track.isAudioTrack()) {
|
||||
titleKey = 'notify.mutedRemotelyTitle';
|
||||
} else if (track.isVideoTrack()) {
|
||||
if (track.getVideoType() === VIDEO_TYPE.DESKTOP) {
|
||||
titleKey = 'notify.desktopMutedRemotelyTitle';
|
||||
} else {
|
||||
titleKey = 'notify.videoMutedRemotelyTitle';
|
||||
}
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey,
|
||||
titleArguments: {
|
||||
participantDisplayName: getParticipantDisplayName(getState, participant.getId())
|
||||
}
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case PARTICIPANT_UPDATED:
|
||||
return _participantJoinedOrUpdated(store, next, action);
|
||||
|
||||
case OVERWRITE_PARTICIPANTS_NAMES: {
|
||||
const { participantList } = action;
|
||||
|
||||
if (!Array.isArray(participantList)) {
|
||||
logger.error('Overwrite names failed. Argument is not an array.');
|
||||
|
||||
return;
|
||||
}
|
||||
batch(() => {
|
||||
participantList.forEach(p => {
|
||||
store.dispatch(overwriteParticipantName(p.id, p.name));
|
||||
});
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case OVERWRITE_PARTICIPANT_NAME: {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const { id, name } = action;
|
||||
|
||||
let breakoutRoom = false, identifier = id;
|
||||
|
||||
if (id.indexOf('@') !== -1) {
|
||||
identifier = id.slice(id.indexOf('/') + 1);
|
||||
breakoutRoom = true;
|
||||
action.id = identifier;
|
||||
}
|
||||
|
||||
if (breakoutRoom) {
|
||||
const rooms = getBreakoutRooms(state);
|
||||
const roomCounter = state['features/breakout-rooms'].roomCounter;
|
||||
const newRooms: any = {};
|
||||
|
||||
Object.entries(rooms).forEach(([ key, r ]) => {
|
||||
const participants = r?.participants || {};
|
||||
const jid = Object.keys(participants).find(p =>
|
||||
p.slice(p.indexOf('/') + 1) === identifier);
|
||||
|
||||
if (jid) {
|
||||
newRooms[key] = {
|
||||
...r,
|
||||
participants: {
|
||||
...participants,
|
||||
[jid]: {
|
||||
...participants[jid],
|
||||
displayName: name
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
newRooms[key] = r;
|
||||
}
|
||||
});
|
||||
dispatch({
|
||||
type: UPDATE_BREAKOUT_ROOMS,
|
||||
rooms,
|
||||
roomCounter,
|
||||
updatedNames: true
|
||||
});
|
||||
} else {
|
||||
dispatch(participantUpdated({
|
||||
id: identifier,
|
||||
name
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
});
|
||||
|
||||
/**
|
||||
* Syncs the redux state features/base/participants up with the redux state
|
||||
* features/base/conference by ensuring that the former does not contain remote
|
||||
* participants no longer relevant to the latter. Introduced to address an issue
|
||||
* with multiplying thumbnails in the filmstrip.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => getCurrentConference(state),
|
||||
/* listener */ (conference, { dispatch, getState }) => {
|
||||
batch(() => {
|
||||
for (const [ id, p ] of getRemoteParticipants(getState())) {
|
||||
(!conference || p.conference !== conference)
|
||||
&& dispatch(participantLeft(id, p.conference, {
|
||||
isReplaced: p.isReplaced
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Reset the ID of the local participant to
|
||||
* {@link LOCAL_PARTICIPANT_DEFAULT_ID}. Such a reset is deemed possible only if
|
||||
* the local participant and, respectively, her ID is not involved in a
|
||||
* conference which is still of interest to the user and, consequently, the app.
|
||||
* For example, a conference which is in the process of leaving is no longer of
|
||||
* interest the user, is unrecoverable from the perspective of the user and,
|
||||
* consequently, the app.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/conference'],
|
||||
/* listener */ ({ leaving }, { dispatch, getState }) => {
|
||||
const state = getState();
|
||||
const localParticipant = getLocalParticipant(state);
|
||||
let id: string;
|
||||
|
||||
if (!localParticipant
|
||||
|| (id = localParticipant.id)
|
||||
=== LOCAL_PARTICIPANT_DEFAULT_ID) {
|
||||
// The ID of the local participant has been reset already.
|
||||
return;
|
||||
}
|
||||
|
||||
// The ID of the local may be reset only if it is not in use.
|
||||
const dispatchLocalParticipantIdChanged
|
||||
= forEachConference(
|
||||
state,
|
||||
conference =>
|
||||
conference === leaving || conference.myUserId() !== id);
|
||||
|
||||
dispatchLocalParticipantIdChanged
|
||||
&& dispatch(
|
||||
localParticipantIdChanged(LOCAL_PARTICIPANT_DEFAULT_ID));
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers listeners for participant change events.
|
||||
*/
|
||||
StateListenerRegistry.register(
|
||||
state => state['features/base/conference'].conference,
|
||||
(conference, store) => {
|
||||
if (conference) {
|
||||
const propertyHandlers: {
|
||||
[key: string]: Function;
|
||||
} = {
|
||||
'e2ee.enabled': (participant: IJitsiParticipant, value: string) =>
|
||||
_e2eeUpdated(store, conference, participant.getId(), value),
|
||||
'features_e2ee': (participant: IJitsiParticipant, value: boolean) =>
|
||||
getParticipantById(store.getState(), participant.getId())?.e2eeSupported !== value
|
||||
&& store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
e2eeSupported: value
|
||||
})),
|
||||
'features_jigasi': (participant: IJitsiParticipant, value: boolean) =>
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
isJigasi: value
|
||||
})),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
'features_screen-sharing': (participant: IJitsiParticipant, value: string) =>
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
features: { 'screen-sharing': true }
|
||||
})),
|
||||
'localRecording': (participant: IJitsiParticipant, value: string) =>
|
||||
_localRecordingUpdated(store, conference, participant.getId(), Boolean(value)),
|
||||
'raisedHand': (participant: IJitsiParticipant, value: string) =>
|
||||
_raiseHandUpdated(store, conference, participant.getId(), value),
|
||||
'region': (participant: IJitsiParticipant, value: string) =>
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
region: value
|
||||
})),
|
||||
'remoteControlSessionStatus': (participant: IJitsiParticipant, value: boolean) =>
|
||||
store.dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participant.getId(),
|
||||
remoteControlSessionStatus: value
|
||||
}))
|
||||
};
|
||||
|
||||
// update properties for the participants that are already in the conference
|
||||
conference.getParticipants().forEach((participant: any) => {
|
||||
Object.keys(propertyHandlers).forEach(propertyName => {
|
||||
const value = participant.getProperty(propertyName);
|
||||
|
||||
if (value !== undefined) {
|
||||
propertyHandlers[propertyName as keyof typeof propertyHandlers](participant, value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// We joined a conference
|
||||
conference.on(
|
||||
JitsiConferenceEvents.PARTICIPANT_PROPERTY_CHANGED,
|
||||
(participant: IJitsiParticipant, propertyName: string, oldValue: string, newValue: string) => {
|
||||
if (propertyHandlers.hasOwnProperty(propertyName)) {
|
||||
propertyHandlers[propertyName](participant, newValue);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const localParticipantId = getLocalParticipant(store.getState)?.id;
|
||||
|
||||
// We left the conference, the local participant must be updated.
|
||||
_e2eeUpdated(store, conference, localParticipantId ?? '', false);
|
||||
_raiseHandUpdated(store, conference, localParticipantId ?? '', 0);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Handles a E2EE enabled status update.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Object} conference - The conference for which we got an update.
|
||||
* @param {string} participantId - The ID of the participant from which we got an update.
|
||||
* @param {boolean} newValue - The new value of the E2EE enabled status.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _e2eeUpdated({ getState, dispatch }: IStore, conference: IJitsiConference,
|
||||
participantId: string, newValue: string | boolean) {
|
||||
const e2eeEnabled = newValue === 'true';
|
||||
const state = getState();
|
||||
const { e2ee = {} } = state['features/base/config'];
|
||||
|
||||
if (e2eeEnabled === getParticipantById(state, participantId)?.e2eeEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participantId,
|
||||
e2eeEnabled
|
||||
}));
|
||||
|
||||
if (e2ee.externallyManagedKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { maxMode } = getState()['features/e2ee'] || {};
|
||||
|
||||
if (maxMode !== MAX_MODE.THRESHOLD_EXCEEDED || !e2eeEnabled) {
|
||||
dispatch(toggleE2EE(e2eeEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the local participant and signals that it joined.
|
||||
*
|
||||
* @private
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Dispatch} next - The redux dispatch function to dispatch the
|
||||
* specified action to the specified store.
|
||||
* @param {Action} action - The redux action which is being dispatched
|
||||
* in the specified store.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _localParticipantJoined({ getState, dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
const settings = getState()['features/base/settings'];
|
||||
|
||||
dispatch(localParticipantJoined({
|
||||
avatarURL: settings.avatarURL,
|
||||
email: settings.email,
|
||||
name: settings.displayName,
|
||||
id: ''
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signals that the local participant has left.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} into the specified {@code store}.
|
||||
* @param {Action} action - The redux action which is being dispatched in the
|
||||
* specified {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _localParticipantLeft({ dispatch }: IStore, next: Function, action: AnyAction) {
|
||||
const result = next(action);
|
||||
|
||||
dispatch(localParticipantLeft());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Plays sounds when participants join/leave conference.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @param {Action} action - The redux action. Should be either
|
||||
* {@link PARTICIPANT_JOINED} or {@link PARTICIPANT_LEFT}.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _maybePlaySounds({ getState, dispatch }: IStore, action: AnyAction) {
|
||||
const state = getState();
|
||||
const { startAudioMuted } = state['features/base/config'];
|
||||
const { soundsParticipantJoined: joinSound, soundsParticipantLeft: leftSound } = state['features/base/settings'];
|
||||
|
||||
// We're not playing sounds for local participant
|
||||
// nor when the user is joining past the "startAudioMuted" limit.
|
||||
// The intention there was to not play user joined notification in big
|
||||
// conferences where 100th person is joining.
|
||||
if (!action.participant.local
|
||||
&& (!startAudioMuted
|
||||
|| getParticipantCount(state) < startAudioMuted)) {
|
||||
const { isReplacing, isReplaced } = action.participant;
|
||||
|
||||
if (action.type === PARTICIPANT_JOINED) {
|
||||
if (!joinSound) {
|
||||
return;
|
||||
}
|
||||
const { presence } = action.participant;
|
||||
|
||||
// The sounds for the poltergeist are handled by features/invite.
|
||||
if (presence !== INVITED && presence !== CALLING && !isReplacing) {
|
||||
dispatch(playSound(PARTICIPANT_JOINED_SOUND_ID));
|
||||
}
|
||||
} else if (action.type === PARTICIPANT_LEFT && !isReplaced && leftSound) {
|
||||
dispatch(playSound(PARTICIPANT_LEFT_SOUND_ID));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies the feature base/participants that the action
|
||||
* {@code PARTICIPANT_JOINED} or {@code PARTICIPANT_UPDATED} is being dispatched
|
||||
* within a specific redux store.
|
||||
*
|
||||
* @param {Store} store - The redux store in which the specified {@code action}
|
||||
* is being dispatched.
|
||||
* @param {Dispatch} next - The redux {@code dispatch} function to dispatch the
|
||||
* specified {@code action} in the specified {@code store}.
|
||||
* @param {Action} action - The redux action {@code PARTICIPANT_JOINED} or
|
||||
* {@code PARTICIPANT_UPDATED} which is being dispatched in the specified
|
||||
* {@code store}.
|
||||
* @private
|
||||
* @returns {Object} The value returned by {@code next(action)}.
|
||||
*/
|
||||
function _participantJoinedOrUpdated(store: IStore, next: Function, action: AnyAction) {
|
||||
const { dispatch, getState } = store;
|
||||
const { overwrittenNameList } = store.getState()['features/base/participants'];
|
||||
const { participant: {
|
||||
avatarURL,
|
||||
email,
|
||||
id,
|
||||
local,
|
||||
localRecording,
|
||||
name,
|
||||
raisedHandTimestamp
|
||||
} } = action;
|
||||
|
||||
// Send an external update of the local participant's raised hand state
|
||||
// if a new raised hand state is defined in the action.
|
||||
if (typeof raisedHandTimestamp !== 'undefined') {
|
||||
if (local) {
|
||||
const { conference } = getState()['features/base/conference'];
|
||||
const rHand = parseInt(raisedHandTimestamp, 10);
|
||||
|
||||
// Send raisedHand signalling only if there is a change
|
||||
if (conference && rHand !== getLocalParticipant(getState())?.raisedHandTimestamp) {
|
||||
conference.setLocalParticipantProperty('raisedHand', rHand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (overwrittenNameList[id]) {
|
||||
action.participant.name = overwrittenNameList[id];
|
||||
}
|
||||
|
||||
// Send an external update of the local participant's local recording state
|
||||
// if a new local recording state is defined in the action.
|
||||
if (typeof localRecording !== 'undefined') {
|
||||
if (local) {
|
||||
const conference = getCurrentConference(getState);
|
||||
|
||||
// Send localRecording signalling only if there is a change
|
||||
if (conference
|
||||
&& localRecording !== getLocalParticipant(getState())?.localRecording) {
|
||||
conference.setLocalParticipantProperty('localRecording', localRecording);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allow the redux update to go through and compare the old avatar
|
||||
// to the new avatar and emit out change events if necessary.
|
||||
const result = next(action);
|
||||
|
||||
// Only run this if the config is populated, otherwise we preload external resources
|
||||
// even if disableThirdPartyRequests is set to true in config
|
||||
if (getState()['features/base/config']?.hosts) {
|
||||
const { disableThirdPartyRequests } = getState()['features/base/config'];
|
||||
const participantId = !id && local ? getLocalParticipant(getState())?.id : id;
|
||||
|
||||
if (avatarURL || email || id || name) {
|
||||
if (!disableThirdPartyRequests) {
|
||||
const updatedParticipant = getParticipantById(getState(), participantId);
|
||||
|
||||
getFirstLoadableAvatarUrl(updatedParticipant ?? { id: '' }, store)
|
||||
.then((urlData?: { isUsingCORS: boolean; src: string; }) => {
|
||||
dispatch(setLoadableAvatarUrl(
|
||||
participantId, urlData?.src ?? '', Boolean(urlData?.isUsingCORS)));
|
||||
});
|
||||
} else if (isImageDataURL(avatarURL)) {
|
||||
dispatch(setLoadableAvatarUrl(participantId, avatarURL, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a local recording status update.
|
||||
*
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @param {Object} conference - The conference for which we got an update.
|
||||
* @param {string} participantId - The ID of the participant from which we got an update.
|
||||
* @param {boolean} newValue - The new value of the local recording status.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _localRecordingUpdated({ dispatch, getState }: IStore, conference: IJitsiConference,
|
||||
participantId: string, newValue: boolean) {
|
||||
const state = getState();
|
||||
const participant = getParticipantById(state, participantId);
|
||||
|
||||
if (participant?.localRecording === newValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participantId,
|
||||
localRecording: newValue
|
||||
}));
|
||||
const participantName = getParticipantDisplayName(state, participantId);
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.somebody',
|
||||
title: participantName,
|
||||
descriptionKey: newValue ? 'notify.localRecordingStarted' : 'notify.localRecordingStopped',
|
||||
uid: LOCAL_RECORDING_NOTIFICATION_ID
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
dispatch(playSound(newValue ? RECORDING_ON_SOUND_ID : RECORDING_OFF_SOUND_ID));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles a raise hand status update.
|
||||
*
|
||||
* @param {Function} dispatch - The Redux dispatch function.
|
||||
* @param {Object} conference - The conference for which we got an update.
|
||||
* @param {string} participantId - The ID of the participant from which we got an update.
|
||||
* @param {boolean} newValue - The new value of the raise hand status.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _raiseHandUpdated({ dispatch, getState }: IStore, conference: IJitsiConference,
|
||||
participantId: string, newValue: string | number) {
|
||||
let raisedHandTimestamp;
|
||||
|
||||
switch (newValue) {
|
||||
case undefined:
|
||||
case 'false':
|
||||
raisedHandTimestamp = 0;
|
||||
break;
|
||||
case 'true':
|
||||
raisedHandTimestamp = Date.now();
|
||||
break;
|
||||
default:
|
||||
raisedHandTimestamp = parseInt(`${newValue}`, 10);
|
||||
}
|
||||
const state = getState();
|
||||
|
||||
dispatch(participantUpdated({
|
||||
conference,
|
||||
id: participantId,
|
||||
raisedHandTimestamp
|
||||
}));
|
||||
|
||||
dispatch(raiseHandUpdateQueue({
|
||||
id: participantId,
|
||||
raisedHandTimestamp
|
||||
}));
|
||||
|
||||
if (typeof APP !== 'undefined') {
|
||||
APP.API.notifyRaiseHandUpdated(participantId, raisedHandTimestamp);
|
||||
}
|
||||
|
||||
if (!raisedHandTimestamp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Display notifications about raised hands.
|
||||
|
||||
const isModerator = isLocalParticipantModerator(state);
|
||||
const participant = getParticipantById(state, participantId);
|
||||
const participantName = getParticipantDisplayName(state, participantId);
|
||||
|
||||
let shouldDisplayAllowAudio = false;
|
||||
let shouldDisplayAllowVideo = false;
|
||||
let shouldDisplayAllowDesktop = false;
|
||||
|
||||
if (isModerator) {
|
||||
shouldDisplayAllowAudio = isForceMuted(participant, MEDIA_TYPE.AUDIO, state);
|
||||
shouldDisplayAllowVideo = isForceMuted(participant, MEDIA_TYPE.VIDEO, state);
|
||||
shouldDisplayAllowDesktop = isForceMuted(participant, MEDIA_TYPE.DESKTOP, state);
|
||||
}
|
||||
|
||||
if (shouldDisplayAllowAudio || shouldDisplayAllowVideo || shouldDisplayAllowDesktop) {
|
||||
const action: {
|
||||
customActionHandler: Array<() => void>;
|
||||
customActionNameKey: string[];
|
||||
} = {
|
||||
customActionHandler: [],
|
||||
customActionNameKey: [],
|
||||
};
|
||||
|
||||
// Always add a "allow all" at the end of the list.
|
||||
action.customActionNameKey.push('notify.allowAll');
|
||||
action.customActionHandler.push(() => {
|
||||
dispatch(approveParticipant(participantId));
|
||||
dispatch(hideNotification(AUDIO_RAISED_HAND_NOTIFICATION_ID));
|
||||
dispatch(hideNotification(DESKTOP_RAISED_HAND_NOTIFICATION_ID));
|
||||
dispatch(hideNotification(VIDEO_RAISED_HAND_NOTIFICATION_ID));
|
||||
});
|
||||
|
||||
if (shouldDisplayAllowAudio) {
|
||||
const customActionNameKey = action.customActionNameKey.slice();
|
||||
const customActionHandler = action.customActionHandler.slice();
|
||||
|
||||
customActionNameKey.unshift('notify.allowAudio');
|
||||
customActionHandler.unshift(() => {
|
||||
dispatch(approveParticipantAudio(participantId));
|
||||
dispatch(hideNotification(AUDIO_RAISED_HAND_NOTIFICATION_ID));
|
||||
});
|
||||
|
||||
dispatch(showNotification({
|
||||
title: participantName,
|
||||
descriptionKey: 'notify.raisedHand',
|
||||
uid: AUDIO_RAISED_HAND_NOTIFICATION_ID,
|
||||
customActionNameKey,
|
||||
customActionHandler,
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
|
||||
}
|
||||
if (shouldDisplayAllowVideo) {
|
||||
const customActionNameKey = action.customActionNameKey.slice();
|
||||
const customActionHandler = action.customActionHandler.slice();
|
||||
|
||||
customActionNameKey.unshift('notify.allowVideo');
|
||||
customActionHandler.unshift(() => {
|
||||
dispatch(approveParticipantVideo(participantId));
|
||||
dispatch(hideNotification(VIDEO_RAISED_HAND_NOTIFICATION_ID));
|
||||
});
|
||||
|
||||
dispatch(showNotification({
|
||||
title: participantName,
|
||||
descriptionKey: 'notify.raisedHand',
|
||||
uid: VIDEO_RAISED_HAND_NOTIFICATION_ID,
|
||||
customActionNameKey,
|
||||
customActionHandler,
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
|
||||
}
|
||||
if (shouldDisplayAllowDesktop) {
|
||||
const customActionNameKey = action.customActionNameKey.slice();
|
||||
const customActionHandler = action.customActionHandler.slice();
|
||||
|
||||
customActionNameKey.unshift('notify.allowDesktop');
|
||||
customActionHandler.unshift(() => {
|
||||
dispatch(approveParticipantDesktop(participantId));
|
||||
dispatch(hideNotification(DESKTOP_RAISED_HAND_NOTIFICATION_ID));
|
||||
});
|
||||
|
||||
dispatch(showNotification({
|
||||
title: participantName,
|
||||
descriptionKey: 'notify.raisedHand',
|
||||
uid: DESKTOP_RAISED_HAND_NOTIFICATION_ID,
|
||||
customActionNameKey,
|
||||
customActionHandler
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.EXTRA_LONG));
|
||||
}
|
||||
} else {
|
||||
let notificationTitle;
|
||||
const { raisedHandsQueue } = state['features/base/participants'];
|
||||
|
||||
if (raisedHandsQueue.length > 1) {
|
||||
notificationTitle = i18n.t('notify.raisedHands', {
|
||||
participantName,
|
||||
raisedHands: raisedHandsQueue.length - 1
|
||||
});
|
||||
} else {
|
||||
notificationTitle = participantName;
|
||||
}
|
||||
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.somebody',
|
||||
title: notificationTitle,
|
||||
descriptionKey: 'notify.raisedHand',
|
||||
concatText: true,
|
||||
uid: RAISE_HAND_NOTIFICATION_ID,
|
||||
customActionNameKey: [ 'notify.viewParticipants' ],
|
||||
customActionHandler: [ () => dispatch(openParticipantsPane()) ]
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
|
||||
}
|
||||
|
||||
dispatch(playSound(RAISE_HAND_SOUND_ID));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers sounds related with the participants feature.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _registerSounds({ dispatch }: IStore) {
|
||||
dispatch(
|
||||
registerSound(PARTICIPANT_JOINED_SOUND_ID, PARTICIPANT_JOINED_FILE));
|
||||
dispatch(registerSound(PARTICIPANT_LEFT_SOUND_ID, PARTICIPANT_LEFT_FILE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters sounds related with the participants feature.
|
||||
*
|
||||
* @param {Store} store - The redux store.
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
function _unregisterSounds({ dispatch }: IStore) {
|
||||
dispatch(unregisterSound(PARTICIPANT_JOINED_SOUND_ID));
|
||||
dispatch(unregisterSound(PARTICIPANT_LEFT_SOUND_ID));
|
||||
}
|
||||
26
react/features/base/participants/preloadImage.native.ts
Normal file
26
react/features/base/participants/preloadImage.native.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Image } from 'react-native';
|
||||
|
||||
import { isIconUrl } from './functions';
|
||||
|
||||
/**
|
||||
* Tries to preload an image.
|
||||
*
|
||||
* @param {string | Object} src - Source of the avatar.
|
||||
* @param {boolean} _isUsingCORS - Used on web.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function preloadImage(src: string | Object, _isUsingCORS: boolean): Promise<any> {
|
||||
if (isIconUrl(src)) {
|
||||
return Promise.resolve(src);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
Image.prefetch(src).then(
|
||||
() => resolve({
|
||||
src,
|
||||
isUsingCORS: false
|
||||
}),
|
||||
reject);
|
||||
});
|
||||
}
|
||||
46
react/features/base/participants/preloadImage.web.ts
Normal file
46
react/features/base/participants/preloadImage.web.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
import { isIconUrl } from './functions';
|
||||
|
||||
/**
|
||||
* Tries to preload an image.
|
||||
*
|
||||
* @param {string | Object} src - Source of the avatar.
|
||||
* @param {boolean} useCORS - Whether to use CORS or not.
|
||||
* @param {boolean} tryOnce - If true we try to load the image only using the specified CORS mode. Otherwise both modes
|
||||
* (CORS and no CORS) will be used to load the image if the first attempt fails.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function preloadImage(
|
||||
src: string,
|
||||
useCORS = false,
|
||||
tryOnce = false
|
||||
): Promise<{ isUsingCORS?: boolean; src: string | Object; }> {
|
||||
if (isIconUrl(src)) {
|
||||
return Promise.resolve({ src });
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = document.createElement('img');
|
||||
|
||||
if (useCORS) {
|
||||
image.setAttribute('crossOrigin', '');
|
||||
}
|
||||
image.onload = () => resolve({
|
||||
src,
|
||||
isUsingCORS: useCORS
|
||||
});
|
||||
image.onerror = error => {
|
||||
if (tryOnce) {
|
||||
reject(error);
|
||||
} else {
|
||||
preloadImage(src, !useCORS, true)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
}
|
||||
};
|
||||
|
||||
image.referrerPolicy = 'no-referrer';
|
||||
|
||||
image.src = src;
|
||||
});
|
||||
}
|
||||
694
react/features/base/participants/reducer.ts
Normal file
694
react/features/base/participants/reducer.ts
Normal file
@@ -0,0 +1,694 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { UPDATE_CONFERENCE_METADATA } from '../conference/actionTypes';
|
||||
import { MEDIA_TYPE } from '../media/constants';
|
||||
import ReducerRegistry from '../redux/ReducerRegistry';
|
||||
import { set } from '../redux/functions';
|
||||
|
||||
import {
|
||||
DOMINANT_SPEAKER_CHANGED,
|
||||
NOTIFIED_TO_SPEAK,
|
||||
OVERWRITE_PARTICIPANT_NAME,
|
||||
PARTICIPANT_ID_CHANGED,
|
||||
PARTICIPANT_JOINED,
|
||||
PARTICIPANT_LEFT,
|
||||
PARTICIPANT_SOURCES_UPDATED,
|
||||
PARTICIPANT_UPDATED,
|
||||
PIN_PARTICIPANT,
|
||||
RAISE_HAND_CLEAR,
|
||||
RAISE_HAND_UPDATED,
|
||||
SCREENSHARE_PARTICIPANT_NAME_CHANGED,
|
||||
SET_LOADABLE_AVATAR_URL
|
||||
} from './actionTypes';
|
||||
import { LOCAL_PARTICIPANT_DEFAULT_ID, PARTICIPANT_ROLE } from './constants';
|
||||
import {
|
||||
isLocalScreenshareParticipant,
|
||||
isParticipantModerator,
|
||||
isRemoteScreenshareParticipant,
|
||||
isScreenShareParticipant
|
||||
} from './functions';
|
||||
import { FakeParticipant, ILocalParticipant, IParticipant, ISourceInfo } from './types';
|
||||
|
||||
/**
|
||||
* Participant object.
|
||||
*
|
||||
* @typedef {Object} Participant
|
||||
* @property {string} id - Participant ID.
|
||||
* @property {string} name - Participant name.
|
||||
* @property {string} avatar - Path to participant avatar if any.
|
||||
* @property {string} role - Participant role.
|
||||
* @property {boolean} local - If true, participant is local.
|
||||
* @property {boolean} pinned - If true, participant is currently a
|
||||
* "PINNED_ENDPOINT".
|
||||
* @property {boolean} dominantSpeaker - If this participant is the dominant
|
||||
* speaker in the (associated) conference, {@code true}; otherwise,
|
||||
* {@code false}.
|
||||
* @property {string} email - Participant email.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The participant properties which cannot be updated through
|
||||
* {@link PARTICIPANT_UPDATED}. They either identify the participant or can only
|
||||
* be modified through property-dedicated actions.
|
||||
*
|
||||
* @type {string[]}
|
||||
*/
|
||||
const PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE = [
|
||||
|
||||
// The following properties identify the participant:
|
||||
'conference',
|
||||
'id',
|
||||
'local',
|
||||
|
||||
// The following properties can only be modified through property-dedicated
|
||||
// actions:
|
||||
'dominantSpeaker',
|
||||
'pinned'
|
||||
];
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
dominantSpeaker: undefined,
|
||||
fakeParticipants: new Map(),
|
||||
local: undefined,
|
||||
localScreenShare: undefined,
|
||||
numberOfNonModeratorParticipants: 0,
|
||||
numberOfParticipantsDisabledE2EE: 0,
|
||||
numberOfParticipantsNotSupportingE2EE: 0,
|
||||
overwrittenNameList: {},
|
||||
pinnedParticipant: undefined,
|
||||
raisedHandsQueue: [],
|
||||
remote: new Map(),
|
||||
remoteVideoSources: new Set<string>(),
|
||||
sortedRemoteVirtualScreenshareParticipants: new Map(),
|
||||
sortedRemoteParticipants: new Map(),
|
||||
speakersList: new Map()
|
||||
};
|
||||
|
||||
export interface IParticipantsState {
|
||||
dominantSpeaker?: string;
|
||||
fakeParticipants: Map<string, IParticipant>;
|
||||
local?: ILocalParticipant;
|
||||
localScreenShare?: IParticipant;
|
||||
numberOfNonModeratorParticipants: number;
|
||||
numberOfParticipantsDisabledE2EE: number;
|
||||
numberOfParticipantsNotSupportingE2EE: number;
|
||||
overwrittenNameList: { [id: string]: string; };
|
||||
pinnedParticipant?: string;
|
||||
raisedHandsQueue: Array<{ hasBeenNotified?: boolean; id: string; raisedHandTimestamp: number; }>;
|
||||
remote: Map<string, IParticipant>;
|
||||
remoteVideoSources: Set<string>;
|
||||
sortedRemoteParticipants: Map<string, string>;
|
||||
sortedRemoteVirtualScreenshareParticipants: Map<string, string>;
|
||||
speakersList: Map<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for actions which add, remove, or update the set of participants in
|
||||
* the conference.
|
||||
*
|
||||
* @param {IParticipant[]} state - List of participants to be modified.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @param {IParticipant} action.participant - Information about participant to be
|
||||
* added/removed/modified.
|
||||
* @returns {IParticipant[]}
|
||||
*/
|
||||
ReducerRegistry.register<IParticipantsState>('features/base/participants',
|
||||
(state = DEFAULT_STATE, action): IParticipantsState => {
|
||||
switch (action.type) {
|
||||
case NOTIFIED_TO_SPEAK: {
|
||||
return {
|
||||
...state,
|
||||
raisedHandsQueue: state.raisedHandsQueue.map((item, index) => {
|
||||
if (index === 0) {
|
||||
|
||||
return {
|
||||
...item,
|
||||
hasBeenNotified: true
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
case PARTICIPANT_ID_CHANGED: {
|
||||
const { local } = state;
|
||||
|
||||
if (local) {
|
||||
if (action.newValue === 'local' && state.raisedHandsQueue.find(pid => pid.id === local.id)) {
|
||||
state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== local.id);
|
||||
}
|
||||
state.local = {
|
||||
...local,
|
||||
id: action.newValue
|
||||
};
|
||||
|
||||
return {
|
||||
...state
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
case DOMINANT_SPEAKER_CHANGED: {
|
||||
const { participant } = action;
|
||||
const { id, previousSpeakers = [] } = participant;
|
||||
const { dominantSpeaker, local } = state;
|
||||
const newSpeakers = [ id, ...previousSpeakers ];
|
||||
const sortedSpeakersList: Array<Array<string>> = [];
|
||||
|
||||
for (const speaker of newSpeakers) {
|
||||
if (speaker !== local?.id) {
|
||||
const remoteParticipant = state.remote.get(speaker);
|
||||
|
||||
remoteParticipant
|
||||
&& sortedSpeakersList.push(
|
||||
[ speaker, _getDisplayName(state, remoteParticipant?.name) ]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the remote speaker list sorted alphabetically.
|
||||
sortedSpeakersList.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
// Only one dominant speaker is allowed.
|
||||
if (dominantSpeaker) {
|
||||
_updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
|
||||
}
|
||||
|
||||
if (_updateParticipantProperty(state, id, 'dominantSpeaker', true)) {
|
||||
return {
|
||||
...state,
|
||||
dominantSpeaker: id, // @ts-ignore
|
||||
speakersList: new Map(sortedSpeakersList)
|
||||
};
|
||||
}
|
||||
|
||||
delete state.dominantSpeaker;
|
||||
|
||||
return {
|
||||
...state
|
||||
};
|
||||
}
|
||||
case PIN_PARTICIPANT: {
|
||||
const { participant } = action;
|
||||
const { id } = participant;
|
||||
const { pinnedParticipant } = state;
|
||||
|
||||
// Only one pinned participant is allowed.
|
||||
if (pinnedParticipant) {
|
||||
_updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
|
||||
}
|
||||
|
||||
if (id && _updateParticipantProperty(state, id, 'pinned', true)) {
|
||||
return {
|
||||
...state,
|
||||
pinnedParticipant: id
|
||||
};
|
||||
}
|
||||
|
||||
delete state.pinnedParticipant;
|
||||
|
||||
return {
|
||||
...state
|
||||
};
|
||||
}
|
||||
case SET_LOADABLE_AVATAR_URL:
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { participant } = action;
|
||||
let { id } = participant;
|
||||
const { local } = participant;
|
||||
|
||||
if (!id && local) {
|
||||
id = LOCAL_PARTICIPANT_DEFAULT_ID;
|
||||
}
|
||||
|
||||
let newParticipant: IParticipant | null = null;
|
||||
const oldParticipant = local || state.local?.id === id ? state.local : state.remote.get(id);
|
||||
|
||||
if (state.remote.has(id)) {
|
||||
newParticipant = _participant(oldParticipant, action);
|
||||
state.remote.set(id, newParticipant);
|
||||
} else if (id === state.local?.id) {
|
||||
newParticipant = state.local = _participant(state.local, action);
|
||||
}
|
||||
|
||||
if (oldParticipant && newParticipant && !newParticipant.fakeParticipant) {
|
||||
const isModerator = isParticipantModerator(newParticipant);
|
||||
|
||||
if (isParticipantModerator(oldParticipant) !== isModerator) {
|
||||
state.numberOfNonModeratorParticipants += isModerator ? -1 : 1;
|
||||
}
|
||||
|
||||
const e2eeEnabled = Boolean(newParticipant.e2eeEnabled);
|
||||
const e2eeSupported = Boolean(newParticipant.e2eeSupported);
|
||||
|
||||
if (Boolean(oldParticipant.e2eeEnabled) !== e2eeEnabled) {
|
||||
state.numberOfParticipantsDisabledE2EE += e2eeEnabled ? -1 : 1;
|
||||
}
|
||||
if (!local && Boolean(oldParticipant.e2eeSupported) !== e2eeSupported) {
|
||||
state.numberOfParticipantsNotSupportingE2EE += e2eeSupported ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state
|
||||
};
|
||||
}
|
||||
case SCREENSHARE_PARTICIPANT_NAME_CHANGED: {
|
||||
const { id, name } = action;
|
||||
|
||||
if (state.sortedRemoteVirtualScreenshareParticipants.has(id)) {
|
||||
state.sortedRemoteVirtualScreenshareParticipants.delete(id);
|
||||
|
||||
const sortedRemoteVirtualScreenshareParticipants = [ ...state.sortedRemoteVirtualScreenshareParticipants ];
|
||||
|
||||
sortedRemoteVirtualScreenshareParticipants.push([ id, name ]);
|
||||
sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
|
||||
}
|
||||
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
case PARTICIPANT_JOINED: {
|
||||
const participant = _participantJoined(action);
|
||||
const {
|
||||
fakeParticipant,
|
||||
id,
|
||||
name,
|
||||
pinned,
|
||||
sources
|
||||
} = participant;
|
||||
const { pinnedParticipant, dominantSpeaker } = state;
|
||||
|
||||
if (pinned) {
|
||||
if (pinnedParticipant) {
|
||||
_updateParticipantProperty(state, pinnedParticipant, 'pinned', false);
|
||||
}
|
||||
|
||||
state.pinnedParticipant = id;
|
||||
}
|
||||
|
||||
if (participant.dominantSpeaker) {
|
||||
if (dominantSpeaker) {
|
||||
_updateParticipantProperty(state, dominantSpeaker, 'dominantSpeaker', false);
|
||||
}
|
||||
state.dominantSpeaker = id;
|
||||
}
|
||||
|
||||
if (!fakeParticipant) {
|
||||
const isModerator = isParticipantModerator(participant);
|
||||
|
||||
if (!isModerator) {
|
||||
state.numberOfNonModeratorParticipants += 1;
|
||||
}
|
||||
|
||||
const { e2eeEnabled, e2eeSupported } = participant as IParticipant;
|
||||
|
||||
if (!e2eeEnabled) {
|
||||
state.numberOfParticipantsDisabledE2EE += 1;
|
||||
}
|
||||
|
||||
if (!participant.local && !e2eeSupported) {
|
||||
state.numberOfParticipantsNotSupportingE2EE += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (participant.local) {
|
||||
return {
|
||||
...state,
|
||||
local: participant
|
||||
};
|
||||
}
|
||||
|
||||
if (isLocalScreenshareParticipant(participant)) {
|
||||
return {
|
||||
...state,
|
||||
localScreenShare: participant
|
||||
};
|
||||
}
|
||||
|
||||
state.remote.set(id, participant);
|
||||
|
||||
if (sources?.size) {
|
||||
const videoSources: Map<string, ISourceInfo> | undefined = sources.get(MEDIA_TYPE.VIDEO);
|
||||
|
||||
if (videoSources?.size) {
|
||||
const newRemoteVideoSources = new Set(state.remoteVideoSources);
|
||||
|
||||
for (const source of videoSources.keys()) {
|
||||
newRemoteVideoSources.add(source);
|
||||
}
|
||||
state.remoteVideoSources = newRemoteVideoSources;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the new participant.
|
||||
const displayName = _getDisplayName(state, name);
|
||||
const sortedRemoteParticipants = Array.from(state.sortedRemoteParticipants);
|
||||
|
||||
sortedRemoteParticipants.push([ id, displayName ]);
|
||||
sortedRemoteParticipants.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
// The sort order of participants is preserved since Map remembers the original insertion order of the keys.
|
||||
state.sortedRemoteParticipants = new Map(sortedRemoteParticipants);
|
||||
|
||||
if (isRemoteScreenshareParticipant(participant)) {
|
||||
const sortedRemoteVirtualScreenshareParticipants = [ ...state.sortedRemoteVirtualScreenshareParticipants ];
|
||||
|
||||
sortedRemoteVirtualScreenshareParticipants.push([ id, name ?? '' ]);
|
||||
sortedRemoteVirtualScreenshareParticipants.sort((a, b) => a[1].localeCompare(b[1]));
|
||||
|
||||
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
|
||||
}
|
||||
|
||||
// Exclude the screenshare participant from the fake participant count to avoid duplicates.
|
||||
if (fakeParticipant && !isScreenShareParticipant(participant)) {
|
||||
state.fakeParticipants.set(id, participant);
|
||||
}
|
||||
|
||||
return { ...state };
|
||||
|
||||
}
|
||||
case PARTICIPANT_LEFT: {
|
||||
// XXX A remote participant is uniquely identified by their id in a
|
||||
// specific JitsiConference instance. The local participant is uniquely
|
||||
// identified by the very fact that there is only one local participant
|
||||
// (and the fact that the local participant "joins" at the beginning of
|
||||
// the app and "leaves" at the end of the app).
|
||||
const { conference, id } = action.participant;
|
||||
const {
|
||||
fakeParticipants,
|
||||
sortedRemoteVirtualScreenshareParticipants,
|
||||
remote,
|
||||
local,
|
||||
localScreenShare,
|
||||
dominantSpeaker,
|
||||
pinnedParticipant
|
||||
} = state;
|
||||
let oldParticipant = remote.get(id);
|
||||
let isLocalScreenShare = false;
|
||||
|
||||
if (oldParticipant && oldParticipant.conference === conference) {
|
||||
remote.delete(id);
|
||||
} else if (local?.id === id) {
|
||||
oldParticipant = state.local;
|
||||
delete state.local;
|
||||
} else if (localScreenShare?.id === id) {
|
||||
isLocalScreenShare = true;
|
||||
oldParticipant = state.local;
|
||||
delete state.localScreenShare;
|
||||
} else {
|
||||
// no participant found
|
||||
return state;
|
||||
}
|
||||
|
||||
if (oldParticipant?.sources?.size) {
|
||||
const videoSources: Map<string, ISourceInfo> | undefined = oldParticipant.sources.get(MEDIA_TYPE.VIDEO);
|
||||
|
||||
if (videoSources?.size) {
|
||||
const newRemoteVideoSources = new Set(state.remoteVideoSources);
|
||||
|
||||
for (const source of videoSources.keys()) {
|
||||
newRemoteVideoSources.delete(source);
|
||||
}
|
||||
|
||||
state.remoteVideoSources = newRemoteVideoSources;
|
||||
}
|
||||
} else if (oldParticipant?.fakeParticipant === FakeParticipant.RemoteScreenShare) {
|
||||
const newRemoteVideoSources = new Set(state.remoteVideoSources);
|
||||
|
||||
if (newRemoteVideoSources.delete(id)) {
|
||||
state.remoteVideoSources = newRemoteVideoSources;
|
||||
}
|
||||
}
|
||||
|
||||
state.sortedRemoteParticipants.delete(id);
|
||||
state.raisedHandsQueue = state.raisedHandsQueue.filter(pid => pid.id !== id);
|
||||
|
||||
if (dominantSpeaker === id) {
|
||||
state.dominantSpeaker = undefined;
|
||||
}
|
||||
|
||||
// Remove the participant from the list of speakers.
|
||||
state.speakersList.has(id) && state.speakersList.delete(id);
|
||||
|
||||
if (pinnedParticipant === id) {
|
||||
state.pinnedParticipant = undefined;
|
||||
}
|
||||
|
||||
if (fakeParticipants.has(id)) {
|
||||
fakeParticipants.delete(id);
|
||||
}
|
||||
|
||||
if (sortedRemoteVirtualScreenshareParticipants.has(id)) {
|
||||
sortedRemoteVirtualScreenshareParticipants.delete(id);
|
||||
state.sortedRemoteVirtualScreenshareParticipants = new Map(sortedRemoteVirtualScreenshareParticipants);
|
||||
}
|
||||
|
||||
if (oldParticipant && !oldParticipant.fakeParticipant && !isLocalScreenShare) {
|
||||
const { e2eeEnabled, e2eeSupported } = oldParticipant;
|
||||
|
||||
if (!isParticipantModerator(oldParticipant)) {
|
||||
state.numberOfNonModeratorParticipants -= 1;
|
||||
}
|
||||
|
||||
if (!e2eeEnabled) {
|
||||
state.numberOfParticipantsDisabledE2EE -= 1;
|
||||
}
|
||||
|
||||
if (!oldParticipant.local && !e2eeSupported) {
|
||||
state.numberOfParticipantsNotSupportingE2EE -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...state };
|
||||
}
|
||||
case PARTICIPANT_SOURCES_UPDATED: {
|
||||
const { id, sources } = action.participant;
|
||||
const participant = state.remote.get(id);
|
||||
|
||||
if (participant) {
|
||||
participant.sources = sources;
|
||||
const videoSources: Map<string, ISourceInfo> = sources.get(MEDIA_TYPE.VIDEO);
|
||||
|
||||
if (videoSources?.size) {
|
||||
const newRemoteVideoSources = new Set(state.remoteVideoSources);
|
||||
|
||||
for (const source of videoSources.keys()) {
|
||||
newRemoteVideoSources.add(source);
|
||||
}
|
||||
state.remoteVideoSources = newRemoteVideoSources;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...state };
|
||||
}
|
||||
case RAISE_HAND_CLEAR: {
|
||||
return {
|
||||
...state,
|
||||
raisedHandsQueue: []
|
||||
};
|
||||
}
|
||||
case RAISE_HAND_UPDATED: {
|
||||
return {
|
||||
...state,
|
||||
raisedHandsQueue: action.queue
|
||||
};
|
||||
}
|
||||
case UPDATE_CONFERENCE_METADATA: {
|
||||
const { metadata } = action;
|
||||
|
||||
|
||||
if (metadata?.visitors?.promoted) {
|
||||
let participantProcessed = false;
|
||||
|
||||
Object.entries(metadata?.visitors?.promoted).forEach(([ key, _ ]) => {
|
||||
|
||||
const p = state.remote.get(key);
|
||||
|
||||
if (p && !p.isPromoted) {
|
||||
|
||||
state.remote.set(key, {
|
||||
...p,
|
||||
isPromoted: true
|
||||
});
|
||||
participantProcessed = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (participantProcessed) {
|
||||
return { ...state };
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case OVERWRITE_PARTICIPANT_NAME: {
|
||||
const { id, name } = action;
|
||||
|
||||
return {
|
||||
...state,
|
||||
overwrittenNameList: {
|
||||
...state.overwrittenNameList,
|
||||
[id]: name
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns the participant's display name, default string if display name is not set on the participant.
|
||||
*
|
||||
* @param {Object} state - The local participant redux state.
|
||||
* @param {string} name - The display name of the participant.
|
||||
* @returns {string}
|
||||
*/
|
||||
function _getDisplayName(state: Object, name?: string): string {
|
||||
// @ts-ignore
|
||||
const config = state['features/base/config'];
|
||||
|
||||
return name ?? (config?.defaultRemoteDisplayName || 'Fellow Jitster');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reducer function for a single participant.
|
||||
*
|
||||
* @param {IParticipant|undefined} state - Participant to be modified.
|
||||
* @param {Object} action - Action object.
|
||||
* @param {string} action.type - Type of action.
|
||||
* @param {IParticipant} action.participant - Information about participant to be
|
||||
* added/modified.
|
||||
* @param {JitsiConference} action.conference - Conference instance.
|
||||
* @private
|
||||
* @returns {IParticipant}
|
||||
*/
|
||||
function _participant(state: IParticipant | ILocalParticipant = { id: '' },
|
||||
action: AnyAction): IParticipant | ILocalParticipant {
|
||||
switch (action.type) {
|
||||
case SET_LOADABLE_AVATAR_URL:
|
||||
case PARTICIPANT_UPDATED: {
|
||||
const { participant } = action; // eslint-disable-line no-shadow
|
||||
|
||||
const newState = { ...state };
|
||||
|
||||
for (const key in participant) {
|
||||
if (participant.hasOwnProperty(key)
|
||||
&& PARTICIPANT_PROPS_TO_OMIT_WHEN_UPDATE.indexOf(key)
|
||||
=== -1) {
|
||||
// @ts-ignore
|
||||
newState[key] = participant[key];
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces a specific redux action of type {@link PARTICIPANT_JOINED} in the
|
||||
* feature base/participants.
|
||||
*
|
||||
* @param {Action} action - The redux action of type {@code PARTICIPANT_JOINED}
|
||||
* to reduce.
|
||||
* @private
|
||||
* @returns {Object} The new participant derived from the payload of the
|
||||
* specified {@code action} to be added into the redux state of the feature
|
||||
* base/participants after the reduction of the specified
|
||||
* {@code action}.
|
||||
*/
|
||||
function _participantJoined({ participant }: { participant: IParticipant; }) {
|
||||
const {
|
||||
avatarURL,
|
||||
botType,
|
||||
dominantSpeaker,
|
||||
email,
|
||||
fakeParticipant,
|
||||
isPromoted,
|
||||
isReplacing,
|
||||
loadableAvatarUrl,
|
||||
local,
|
||||
name,
|
||||
pinned,
|
||||
presence,
|
||||
role,
|
||||
sources
|
||||
} = participant;
|
||||
let { conference, id } = participant;
|
||||
|
||||
if (local) {
|
||||
// conference
|
||||
//
|
||||
// XXX The local participant is not identified in association with a
|
||||
// JitsiConference because it is identified by the very fact that it is
|
||||
// the local participant.
|
||||
conference = undefined;
|
||||
|
||||
// id
|
||||
id || (id = LOCAL_PARTICIPANT_DEFAULT_ID);
|
||||
}
|
||||
|
||||
return {
|
||||
avatarURL,
|
||||
botType,
|
||||
conference,
|
||||
dominantSpeaker: dominantSpeaker || false,
|
||||
email,
|
||||
fakeParticipant,
|
||||
id,
|
||||
isPromoted,
|
||||
isReplacing,
|
||||
loadableAvatarUrl,
|
||||
local: local || false,
|
||||
name,
|
||||
pinned: pinned || false,
|
||||
presence,
|
||||
role: role || PARTICIPANT_ROLE.NONE,
|
||||
sources
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a specific property for a participant.
|
||||
*
|
||||
* @param {State} state - The redux state.
|
||||
* @param {string} id - The ID of the participant.
|
||||
* @param {string} property - The property to update.
|
||||
* @param {*} value - The new value.
|
||||
* @returns {boolean} - True if a participant was updated and false otherwise.
|
||||
*/
|
||||
function _updateParticipantProperty(state: IParticipantsState, id: string, property: string, value: boolean) {
|
||||
const { remote, local, localScreenShare } = state;
|
||||
|
||||
if (remote.has(id)) {
|
||||
remote.set(id, set(remote.get(id) ?? {
|
||||
id: '',
|
||||
name: ''
|
||||
}, property as keyof IParticipant, value));
|
||||
|
||||
return true;
|
||||
} else if (local?.id === id || local?.id === 'local') {
|
||||
// The local participant's ID can chance from something to "local" when
|
||||
// not in a conference.
|
||||
state.local = set(local, property as keyof ILocalParticipant, value);
|
||||
|
||||
return true;
|
||||
|
||||
} else if (localScreenShare?.id === id) {
|
||||
state.localScreenShare = set(localScreenShare, property as keyof IParticipant, value);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
11
react/features/base/participants/sounds.ts
Normal file
11
react/features/base/participants/sounds.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* The name of the bundled sound file which will be played when new participant
|
||||
* joins the conference.
|
||||
*/
|
||||
export const PARTICIPANT_JOINED_FILE = 'joined.mp3';
|
||||
|
||||
/**
|
||||
* The name of the bundled sound file which will be played when any participant
|
||||
* leaves the conference.
|
||||
*/
|
||||
export const PARTICIPANT_LEFT_FILE = 'left.mp3';
|
||||
168
react/features/base/participants/subscriber.ts
Normal file
168
react/features/base/participants/subscriber.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
|
||||
import { difference } from 'lodash-es';
|
||||
import { batch } from 'react-redux';
|
||||
|
||||
import { IStore } from '../../app/types';
|
||||
import { hideNotification, showNotification } from '../../notifications/actions';
|
||||
import { NOTIFICATION_TIMEOUT_TYPE, RAISE_HAND_NOTIFICATION_ID } from '../../notifications/constants';
|
||||
import { getCurrentConference } from '../conference/functions';
|
||||
import {
|
||||
getDisableNextSpeakerNotification,
|
||||
getSsrcRewritingFeatureFlag,
|
||||
hasBeenNotified,
|
||||
isNextToSpeak } from '../config/functions.any';
|
||||
import { VIDEO_TYPE } from '../media/constants';
|
||||
import StateListenerRegistry from '../redux/StateListenerRegistry';
|
||||
|
||||
import { NOTIFIED_TO_SPEAK } from './actionTypes';
|
||||
import { createVirtualScreenshareParticipant, participantLeft } from './actions';
|
||||
import {
|
||||
getParticipantById,
|
||||
getRemoteScreensharesBasedOnPresence,
|
||||
getVirtualScreenshareParticipantOwnerId
|
||||
} from './functions';
|
||||
import { FakeParticipant } from './types';
|
||||
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/tracks'],
|
||||
/* listener */(tracks, store) => _updateScreenshareParticipants(store)
|
||||
);
|
||||
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/participants'].remoteVideoSources,
|
||||
/* listener */(remoteVideoSources, store) => getSsrcRewritingFeatureFlag(store.getState())
|
||||
&& _updateScreenshareParticipantsBasedOnPresence(store)
|
||||
);
|
||||
|
||||
StateListenerRegistry.register(
|
||||
/* selector */ state => state['features/base/participants'].raisedHandsQueue,
|
||||
/* listener */ (raisedHandsQueue, store) => {
|
||||
if (raisedHandsQueue.length
|
||||
&& isNextToSpeak(store.getState())
|
||||
&& !hasBeenNotified(store.getState())
|
||||
&& !getDisableNextSpeakerNotification(store.getState())
|
||||
&& !store.getState()['features/visitors'].iAmVisitor) { // visitors raise hand to be promoted
|
||||
_notifyNextSpeakerInRaisedHandQueue(store);
|
||||
}
|
||||
if (!raisedHandsQueue[0]) {
|
||||
store.dispatch(hideNotification(RAISE_HAND_NOTIFICATION_ID));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Compares the old and new screenshare lists provided and creates/removes the virtual screenshare participant
|
||||
* tiles accordingly.
|
||||
*
|
||||
* @param {Array<string>} oldScreenshareSourceNames - List of old screenshare source names.
|
||||
* @param {Array<string>} newScreenshareSourceNames - Current list of screenshare source names.
|
||||
* @param {Object} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _createOrRemoveVirtualParticipants(
|
||||
oldScreenshareSourceNames: string[],
|
||||
newScreenshareSourceNames: string[],
|
||||
store: IStore): void {
|
||||
const { dispatch, getState } = store;
|
||||
const conference = getCurrentConference(getState());
|
||||
const removedScreenshareSourceNames = difference(oldScreenshareSourceNames, newScreenshareSourceNames);
|
||||
const addedScreenshareSourceNames = difference(newScreenshareSourceNames, oldScreenshareSourceNames);
|
||||
|
||||
if (removedScreenshareSourceNames.length) {
|
||||
removedScreenshareSourceNames.forEach(id => dispatch(participantLeft(id, conference, {
|
||||
fakeParticipant: FakeParticipant.RemoteScreenShare
|
||||
})));
|
||||
}
|
||||
|
||||
if (addedScreenshareSourceNames.length) {
|
||||
addedScreenshareSourceNames.forEach(id => dispatch(
|
||||
createVirtualScreenshareParticipant(id, false, conference)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles creating and removing virtual screenshare participants.
|
||||
*
|
||||
* @param {*} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateScreenshareParticipants(store: IStore): void {
|
||||
const { dispatch, getState } = store;
|
||||
const state = getState();
|
||||
const conference = getCurrentConference(state);
|
||||
const tracks = state['features/base/tracks'];
|
||||
const { sortedRemoteVirtualScreenshareParticipants, localScreenShare } = state['features/base/participants'];
|
||||
const previousScreenshareSourceNames = [ ...sortedRemoteVirtualScreenshareParticipants.keys() ];
|
||||
|
||||
let newLocalSceenshareSourceName;
|
||||
|
||||
const currentScreenshareSourceNames = tracks.reduce((acc: string[], track) => {
|
||||
if (track.videoType === VIDEO_TYPE.DESKTOP && !track.jitsiTrack.isMuted()) {
|
||||
const sourceName: string = track.jitsiTrack.getSourceName();
|
||||
|
||||
// Ignore orphan tracks in ssrc-rewriting mode.
|
||||
if (!sourceName && getSsrcRewritingFeatureFlag(state)) {
|
||||
return acc;
|
||||
}
|
||||
if (track.local) {
|
||||
newLocalSceenshareSourceName = sourceName;
|
||||
} else if (getParticipantById(state, getVirtualScreenshareParticipantOwnerId(sourceName))) {
|
||||
acc.push(sourceName);
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (!localScreenShare && newLocalSceenshareSourceName) {
|
||||
dispatch(createVirtualScreenshareParticipant(newLocalSceenshareSourceName, true, conference));
|
||||
}
|
||||
|
||||
if (localScreenShare && !newLocalSceenshareSourceName) {
|
||||
dispatch(participantLeft(localScreenShare.id, conference, {
|
||||
fakeParticipant: FakeParticipant.LocalScreenShare
|
||||
}));
|
||||
}
|
||||
|
||||
if (getSsrcRewritingFeatureFlag(state)) {
|
||||
return;
|
||||
}
|
||||
|
||||
_createOrRemoveVirtualParticipants(previousScreenshareSourceNames, currentScreenshareSourceNames, store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the creation and removal of remote virtual screenshare participants when ssrc-rewriting is enabled.
|
||||
*
|
||||
* @param {Object} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _updateScreenshareParticipantsBasedOnPresence(store: IStore): void {
|
||||
const { getState } = store;
|
||||
const state = getState();
|
||||
const { sortedRemoteVirtualScreenshareParticipants } = state['features/base/participants'];
|
||||
const previousScreenshareSourceNames = [ ...sortedRemoteVirtualScreenshareParticipants.keys() ];
|
||||
const currentScreenshareSourceNames = getRemoteScreensharesBasedOnPresence(state);
|
||||
|
||||
_createOrRemoveVirtualParticipants(previousScreenshareSourceNames, currentScreenshareSourceNames, store);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles notifying the next speaker in the raised hand queue.
|
||||
*
|
||||
* @param {*} store - The redux store.
|
||||
* @returns {void}
|
||||
*/
|
||||
function _notifyNextSpeakerInRaisedHandQueue(store: IStore): void {
|
||||
const { dispatch } = store;
|
||||
|
||||
batch(() => {
|
||||
dispatch(showNotification({
|
||||
titleKey: 'notify.nextToSpeak',
|
||||
maxLines: 2
|
||||
}, NOTIFICATION_TIMEOUT_TYPE.MEDIUM));
|
||||
dispatch({
|
||||
type: NOTIFIED_TO_SPEAK
|
||||
});
|
||||
});
|
||||
}
|
||||
92
react/features/base/participants/types.ts
Normal file
92
react/features/base/participants/types.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { IJitsiConference } from '../conference/reducer';
|
||||
|
||||
export enum FakeParticipant {
|
||||
LocalScreenShare = 'LocalScreenShare',
|
||||
RemoteScreenShare = 'RemoteScreenShare',
|
||||
SharedVideo = 'SharedVideo',
|
||||
Whiteboard = 'Whiteboard'
|
||||
}
|
||||
|
||||
export interface IParticipant {
|
||||
avatarURL?: string;
|
||||
botType?: string;
|
||||
conference?: IJitsiConference;
|
||||
displayName?: string;
|
||||
dominantSpeaker?: boolean;
|
||||
e2eeEnabled?: boolean;
|
||||
e2eeSupported?: boolean;
|
||||
e2eeVerificationAvailable?: boolean;
|
||||
e2eeVerified?: boolean;
|
||||
email?: string;
|
||||
fakeParticipant?: FakeParticipant;
|
||||
features?: IParticipantFeatures;
|
||||
getId?: Function;
|
||||
id: string;
|
||||
isJigasi?: boolean;
|
||||
isPromoted?: boolean;
|
||||
isReplaced?: boolean;
|
||||
isReplacing?: number;
|
||||
isSilent?: boolean;
|
||||
jwtId?: string;
|
||||
loadableAvatarUrl?: string;
|
||||
loadableAvatarUrlUseCORS?: boolean;
|
||||
local?: boolean;
|
||||
localRecording?: boolean;
|
||||
name?: string;
|
||||
pinned?: boolean;
|
||||
presence?: string;
|
||||
raisedHandTimestamp?: number;
|
||||
region?: string;
|
||||
remoteControlSessionStatus?: boolean;
|
||||
role?: string;
|
||||
sources?: Map<string, Map<string, ISourceInfo>>;
|
||||
supportsRemoteControl?: boolean;
|
||||
}
|
||||
|
||||
export interface ILocalParticipant extends IParticipant {
|
||||
audioOutputDeviceId?: string;
|
||||
cameraDeviceId?: string;
|
||||
jwtId?: string;
|
||||
micDeviceId?: string;
|
||||
startWithAudioMuted?: boolean;
|
||||
startWithVideoMuted?: boolean;
|
||||
userSelectedMicDeviceId?: string;
|
||||
userSelectedMicDeviceLabel?: string;
|
||||
}
|
||||
|
||||
export interface IParticipantFeatures {
|
||||
'branding'?: boolean | string;
|
||||
'calendar'?: boolean | string;
|
||||
'create-polls'?: boolean | string;
|
||||
'file-upload'?: boolean | string;
|
||||
'flip'?: boolean | string;
|
||||
'inbound-call'?: boolean | string;
|
||||
'list-visitors'?: boolean | string;
|
||||
'livestreaming'?: boolean | string;
|
||||
'lobby'?: boolean | string;
|
||||
'moderation'?: boolean | string;
|
||||
'outbound-call'?: boolean | string;
|
||||
'recording'?: boolean | string;
|
||||
'room'?: boolean | string;
|
||||
'screen-sharing'?: boolean | string;
|
||||
'send-groupchat'?: boolean | string;
|
||||
'sip-inbound-call'?: boolean | string;
|
||||
'sip-outbound-call'?: boolean | string;
|
||||
'transcription'?: boolean | string;
|
||||
}
|
||||
|
||||
export interface ISourceInfo {
|
||||
muted: boolean;
|
||||
videoType: string;
|
||||
}
|
||||
|
||||
export interface IJitsiParticipant {
|
||||
getDisplayName: () => string;
|
||||
getId: () => string;
|
||||
getJid: () => string;
|
||||
getRole: () => string;
|
||||
getSources: () => Map<string, Map<string, ISourceInfo>>;
|
||||
isHidden: () => boolean;
|
||||
}
|
||||
|
||||
export type ParticipantFeaturesKey = keyof IParticipantFeatures;
|
||||
Reference in New Issue
Block a user