/* eslint-disable lines-around-comment */ import { createStartMutedConfigurationEvent } from '../../analytics/AnalyticsEvents'; import { sendAnalytics } from '../../analytics/functions'; import { appNavigate } from '../../app/actions'; import { IReduxState, IStore } from '../../app/types'; import { endpointMessageReceived } from '../../subtitles/actions.any'; import { getReplaceParticipant } from '../config/functions'; import { disconnect } from '../connection/actions'; import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection/constants'; import { JitsiConferenceEvents, JitsiE2ePingEvents } from '../lib-jitsi-meet'; import { setAudioMuted, setAudioUnmutePermissions, setVideoMuted, setVideoUnmutePermissions } from '../media/actions'; import { MEDIA_TYPE } from '../media/constants'; import { dominantSpeakerChanged, participantConnectionStatusChanged, participantKicked, participantMutedUs, participantPresenceChanged, participantRoleChanged, participantUpdated } from '../participants/actions'; import { getNormalizedDisplayName } from '../participants/functions'; import { toState } from '../redux/functions'; import { destroyLocalTracks, replaceLocalTrack, trackAdded, trackRemoved } from '../tracks/actions.any'; import { getLocalTracks } from '../tracks/functions'; import { getBackendSafeRoomName } from '../util/uri'; import { AUTH_STATUS_CHANGED, CONFERENCE_FAILED, CONFERENCE_JOINED, CONFERENCE_JOIN_IN_PROGRESS, CONFERENCE_LEFT, CONFERENCE_LOCAL_SUBJECT_CHANGED, CONFERENCE_SUBJECT_CHANGED, CONFERENCE_TIMESTAMP_CHANGED, CONFERENCE_UNIQUE_ID_SET, CONFERENCE_WILL_JOIN, CONFERENCE_WILL_LEAVE, DATA_CHANNEL_OPENED, E2E_RTT_CHANGED, KICKED_OUT, LOCK_STATE_CHANGED, NON_PARTICIPANT_MESSAGE_RECEIVED, P2P_STATUS_CHANGED, SEND_TONES, SET_FOLLOW_ME, SET_OBFUSCATED_ROOM, SET_PASSWORD, SET_PASSWORD_FAILED, SET_PENDING_SUBJECT_CHANGE, SET_ROOM, SET_START_MUTED_POLICY, SET_START_REACTIONS_MUTED } from './actionTypes'; import { AVATAR_URL_COMMAND, EMAIL_COMMAND, JITSI_CONFERENCE_URL_KEY } from './constants'; import { _addLocalTracksToConference, commonUserJoinedHandling, commonUserLeftHandling, getConferenceOptions, getConferenceState, getCurrentConference, sendLocalParticipant } from './functions'; import logger from './logger'; import { IJitsiConference } from './reducer'; /** * Adds conference (event) listeners. * * @param {JitsiConference} conference - The JitsiConference instance. * @param {Dispatch} dispatch - The Redux dispatch function. * @param {Object} state - The Redux state. * @private * @returns {void} */ function _addConferenceListeners(conference: IJitsiConference, dispatch: IStore['dispatch'], state: IReduxState) { // A simple logger for conference errors received through // the listener. These errors are not handled now, but logged. conference.on(JitsiConferenceEvents.CONFERENCE_ERROR, (error: Error) => logger.error('Conference error.', error)); // Dispatches into features/base/conference follow: conference.on( JitsiConferenceEvents.AUTH_STATUS_CHANGED, (authEnabled: boolean, authLogin: string) => dispatch(authStatusChanged(authEnabled, authLogin))); conference.on( JitsiConferenceEvents.CONFERENCE_FAILED, // @ts-ignore (...args: any[]) => dispatch(conferenceFailed(conference, ...args))); conference.on( JitsiConferenceEvents.CONFERENCE_JOINED, // @ts-ignore (...args: any[]) => dispatch(conferenceJoined(conference, ...args))); conference.on( JitsiConferenceEvents.CONFERENCE_UNIQUE_ID_SET, // @ts-ignore (...args: any[]) => dispatch(conferenceUniqueIdSet(conference, ...args))); conference.on( JitsiConferenceEvents.CONFERENCE_JOIN_IN_PROGRESS, // @ts-ignore (...args: any[]) => dispatch(conferenceJoinInProgress(conference, ...args))); conference.on( JitsiConferenceEvents.CONFERENCE_LEFT, (...args: any[]) => { dispatch(conferenceTimestampChanged(0)); // @ts-ignore dispatch(conferenceLeft(conference, ...args)); }); conference.on(JitsiConferenceEvents.SUBJECT_CHANGED, // @ts-ignore (...args: any[]) => dispatch(conferenceSubjectChanged(...args))); conference.on(JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP, // @ts-ignore (...args: any[]) => dispatch(conferenceTimestampChanged(...args))); conference.on( JitsiConferenceEvents.KICKED, // @ts-ignore (...args: any[]) => dispatch(kickedOut(conference, ...args))); conference.on( JitsiConferenceEvents.PARTICIPANT_KICKED, (kicker: any, kicked: any) => dispatch(participantKicked(kicker, kicked))); conference.on( JitsiConferenceEvents.LOCK_STATE_CHANGED, // @ts-ignore (...args: any[]) => dispatch(lockStateChanged(conference, ...args))); // Dispatches into features/base/media follow: conference.on( JitsiConferenceEvents.STARTED_MUTED, () => { const audioMuted = Boolean(conference.isStartAudioMuted()); const videoMuted = Boolean(conference.isStartVideoMuted()); const localTracks = getLocalTracks(state['features/base/tracks']); sendAnalytics(createStartMutedConfigurationEvent('remote', audioMuted, videoMuted)); logger.log(`Start muted: ${audioMuted ? 'audio, ' : ''}${videoMuted ? 'video' : ''}`); // XXX Jicofo tells lib-jitsi-meet to start with audio and/or video // muted i.e. Jicofo expresses an intent. Lib-jitsi-meet has turned // Jicofo's intent into reality by actually muting the respective // tracks. The reality is expressed in base/tracks already so what // is left is to express Jicofo's intent in base/media. // TODO Maybe the app needs to learn about Jicofo's intent and // transfer that intent to lib-jitsi-meet instead of lib-jitsi-meet // acting on Jicofo's intent without the app's knowledge. dispatch(setAudioMuted(audioMuted)); dispatch(setVideoMuted(videoMuted)); // Remove the tracks from peerconnection as well. for (const track of localTracks) { const trackType = track.jitsiTrack.getType(); // Do not remove the audio track on RN. Starting with iOS 15 it will fail to unmute otherwise. if ((audioMuted && trackType === MEDIA_TYPE.AUDIO && navigator.product !== 'ReactNative') || (videoMuted && trackType === MEDIA_TYPE.VIDEO)) { dispatch(replaceLocalTrack(track.jitsiTrack, null, conference)); } } }); conference.on( JitsiConferenceEvents.AUDIO_UNMUTE_PERMISSIONS_CHANGED, (disableAudioMuteChange: boolean) => { dispatch(setAudioUnmutePermissions(disableAudioMuteChange)); }); conference.on( JitsiConferenceEvents.VIDEO_UNMUTE_PERMISSIONS_CHANGED, (disableVideoMuteChange: boolean) => { dispatch(setVideoUnmutePermissions(disableVideoMuteChange)); }); // Dispatches into features/base/tracks follow: conference.on( JitsiConferenceEvents.TRACK_ADDED, (t: any) => t && !t.isLocal() && dispatch(trackAdded(t))); conference.on( JitsiConferenceEvents.TRACK_REMOVED, (t: any) => t && !t.isLocal() && dispatch(trackRemoved(t))); conference.on( JitsiConferenceEvents.TRACK_MUTE_CHANGED, (track: any, participantThatMutedUs: any) => { if (participantThatMutedUs) { dispatch(participantMutedUs(participantThatMutedUs, track)); } }); conference.on(JitsiConferenceEvents.TRACK_UNMUTE_REJECTED, (track: any) => dispatch(destroyLocalTracks(track))); // Dispatches into features/base/participants follow: conference.on( JitsiConferenceEvents.DISPLAY_NAME_CHANGED, (id: string, displayName: string) => dispatch(participantUpdated({ conference, id, name: getNormalizedDisplayName(displayName) }))); conference.on( JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED, (dominant: string, previous: string[], silence: boolean | string) => { dispatch(dominantSpeakerChanged(dominant, previous, Boolean(silence), conference)); }); conference.on( JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED, // @ts-ignore (...args: any[]) => dispatch(endpointMessageReceived(...args))); conference.on( JitsiConferenceEvents.NON_PARTICIPANT_MESSAGE_RECEIVED, // @ts-ignore (...args: any[]) => dispatch(nonParticipantMessageReceived(...args))); conference.on( JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED, // @ts-ignore (...args: any[]) => dispatch(participantConnectionStatusChanged(...args))); conference.on( JitsiConferenceEvents.USER_JOINED, (_id: string, user: any) => commonUserJoinedHandling({ dispatch }, conference, user)); conference.on( JitsiConferenceEvents.USER_LEFT, (_id: string, user: any) => commonUserLeftHandling({ dispatch }, conference, user)); conference.on( JitsiConferenceEvents.USER_ROLE_CHANGED, // @ts-ignore (...args: any[]) => dispatch(participantRoleChanged(...args))); conference.on( JitsiConferenceEvents.USER_STATUS_CHANGED, // @ts-ignore (...args: any[]) => dispatch(participantPresenceChanged(...args))); conference.on( JitsiE2ePingEvents.E2E_RTT_CHANGED, // @ts-ignore (...args: any[]) => dispatch(e2eRttChanged(...args))); conference.on( JitsiConferenceEvents.BOT_TYPE_CHANGED, (id: string, botType: string) => dispatch(participantUpdated({ conference, id, botType }))); conference.addCommandListener( AVATAR_URL_COMMAND, (data: { value: string; }, id: string) => dispatch(participantUpdated({ conference, id, avatarURL: data.value }))); conference.addCommandListener( EMAIL_COMMAND, (data: { value: string; }, id: string) => dispatch(participantUpdated({ conference, id, email: data.value }))); } /** * Create an action for when the end-to-end RTT against a specific remote participant has changed. * * @param {Object} participant - The participant against which the rtt is measured. * @param {number} rtt - The rtt. * @returns {{ * type: E2E_RTT_CHANGED, * e2eRtt: { * participant: Object, * rtt: number * } * }} */ export function e2eRttChanged(participant: Object, rtt: number) { return { type: E2E_RTT_CHANGED, e2eRtt: { rtt, participant } }; } /** * Updates the current known state of server-side authentication. * * @param {boolean} authEnabled - Whether or not server authentication is * enabled. * @param {string} authLogin - The current name of the logged in user, if any. * @returns {{ * type: AUTH_STATUS_CHANGED, * authEnabled: boolean, * authLogin: string * }} */ export function authStatusChanged(authEnabled: boolean, authLogin: string) { return { type: AUTH_STATUS_CHANGED, authEnabled, authLogin }; } /** * Signals that a specific conference has failed. * * @param {JitsiConference} conference - The JitsiConference that has failed. * @param {string} error - The error describing/detailing the cause of the * failure. * @param {any} params - Rest of the params that we receive together with the event. * @returns {{ * type: CONFERENCE_FAILED, * conference: JitsiConference, * error: Error * }} * @public */ export function conferenceFailed(conference: IJitsiConference, error: string, ...params: any) { return { type: CONFERENCE_FAILED, conference, // Make the error resemble an Error instance (to the extent that // jitsi-meet needs it). error: { name: error, params, recoverable: undefined } }; } /** * Signals that a specific conference has been joined. * * @param {JitsiConference} conference - The JitsiConference instance which was * joined by the local participant. * @returns {{ * type: CONFERENCE_JOINED, * conference: JitsiConference * }} */ export function conferenceJoined(conference: IJitsiConference) { return { type: CONFERENCE_JOINED, conference }; } /** * Signals that a specific conference join is in progress. * * @param {JitsiConference} conference - The JitsiConference instance for which join by the local participant * is in progress. * @returns {{ * type: CONFERENCE_JOIN_IN_PROGRESS, * conference: JitsiConference * }} */ export function conferenceJoinInProgress(conference: IJitsiConference) { return { type: CONFERENCE_JOIN_IN_PROGRESS, conference }; } /** * Signals that a specific conference has been left. * * @param {JitsiConference} conference - The JitsiConference instance which was * left by the local participant. * @returns {{ * type: CONFERENCE_LEFT, * conference: JitsiConference * }} */ export function conferenceLeft(conference: IJitsiConference) { return { type: CONFERENCE_LEFT, conference }; } /** * Signals that the conference subject has been changed. * * @param {string} subject - The new subject. * @returns {{ * type: CONFERENCE_SUBJECT_CHANGED, * subject: string * }} */ export function conferenceSubjectChanged(subject: string) { return { type: CONFERENCE_SUBJECT_CHANGED, subject }; } /** * Signals that the conference timestamp has been changed. * * @param {number} conferenceTimestamp - The UTC timestamp. * @returns {{ * type: CONFERENCE_TIMESTAMP_CHANGED, * conferenceTimestamp * }} */ export function conferenceTimestampChanged(conferenceTimestamp: number) { return { type: CONFERENCE_TIMESTAMP_CHANGED, conferenceTimestamp }; } /** * Signals that the unique identifier for conference has been set. * * @param {JitsiConference} conference - The JitsiConference instance, where the uuid has been set. * @returns {{ * type: CONFERENCE_UNIQUE_ID_SET, * conference: JitsiConference, * }} */ export function conferenceUniqueIdSet(conference: IJitsiConference) { return { type: CONFERENCE_UNIQUE_ID_SET, conference }; } /** * Adds any existing local tracks to a specific conference before the conference * is joined. Then signals the intention of the application to have the local * participant join the specified conference. * * @param {JitsiConference} conference - The {@code JitsiConference} instance * the local participant will (try to) join. * @returns {Function} */ export function _conferenceWillJoin(conference: IJitsiConference) { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const localTracks = getLocalTracks(getState()['features/base/tracks']) .map(t => t.jitsiTrack); if (localTracks.length) { _addLocalTracksToConference(conference, localTracks); } dispatch(conferenceWillJoin(conference)); }; } /** * Signals the intention of the application to have the local participant * join the specified conference. * * @param {JitsiConference} conference - The {@code JitsiConference} instance * the local participant will (try to) join. * @returns {{ * type: CONFERENCE_WILL_JOIN, * conference: JitsiConference * }} */ export function conferenceWillJoin(conference: IJitsiConference) { return { type: CONFERENCE_WILL_JOIN, conference }; } /** * Signals the intention of the application to have the local participant leave * a specific conference. Similar in fashion to CONFERENCE_LEFT. Contrary to it * though, it's not guaranteed because CONFERENCE_LEFT may be triggered by * lib-jitsi-meet and not the application. * * @param {JitsiConference} conference - The JitsiConference instance which will * be left by the local participant. * @returns {{ * type: CONFERENCE_LEFT, * conference: JitsiConference * }} */ export function conferenceWillLeave(conference: IJitsiConference) { return { type: CONFERENCE_WILL_LEAVE, conference }; } /** * Initializes a new conference. * * @param {string} overrideRoom - Override the room to join, instead of taking it * from Redux. * @returns {Function} */ export function createConference(overrideRoom?: string) { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const state = getState(); const { connection, locationURL } = state['features/base/connection']; if (!connection) { throw new Error('Cannot create a conference without a connection!'); } const { password, room } = state['features/base/conference']; if (!room) { throw new Error('Cannot join a conference without a room name!'); } // XXX: revisit this. // Hide the custom domain in the room name. const tmp: any = overrideRoom || room; let _room: any = getBackendSafeRoomName(tmp); if (tmp.domain) { // eslint-disable-next-line no-new-wrappers _room = new String(tmp); // $FlowExpectedError _room.domain = tmp.domain; } const conference = connection.initJitsiConference(_room, getConferenceOptions(state)); // @ts-ignore connection[JITSI_CONNECTION_CONFERENCE_KEY] = conference; conference[JITSI_CONFERENCE_URL_KEY] = locationURL; dispatch(_conferenceWillJoin(conference)); _addConferenceListeners(conference, dispatch, state); sendLocalParticipant(state, conference); const replaceParticipant = getReplaceParticipant(state); conference.join(password, replaceParticipant); }; } /** * Will try to join the conference again in case it failed earlier with * {@link JitsiConferenceErrors.AUTHENTICATION_REQUIRED}. It means that Jicofo * did not allow to create new room from anonymous domain, but it can be tried * again later in case authenticated user created it in the meantime. * * @returns {Function} */ export function checkIfCanJoin() { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const { authRequired, password } = getState()['features/base/conference']; const replaceParticipant = getReplaceParticipant(getState()); // @ts-ignore authRequired && dispatch(_conferenceWillJoin(authRequired)); authRequired?.join(password, replaceParticipant); }; } /** * Signals the data channel with the bridge has successfully opened. * * @returns {{ * type: DATA_CHANNEL_OPENED * }} */ export function dataChannelOpened() { return { type: DATA_CHANNEL_OPENED }; } /** * Action to end a conference for all participants. * * @returns {Function} */ export function endConference() { return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const { conference } = getConferenceState(toState(getState)); conference?.end(); }; } /** * Signals that we've been kicked out of the conference. * * @param {JitsiConference} conference - The {@link JitsiConference} instance * for which the event is being signaled. * @param {JitsiParticipant} participant - The {@link JitsiParticipant} * instance which initiated the kick event. * @returns {{ * type: KICKED_OUT, * conference: JitsiConference, * participant: JitsiParticipant * }} */ export function kickedOut(conference: Object, participant: Object) { return { type: KICKED_OUT, conference, participant }; } /** * Action to leave a conference. * * @returns {Function} */ export function leaveConference() { return async (dispatch: IStore['dispatch']) => { // FIXME: these should be unified. if (navigator.product === 'ReactNative') { dispatch(appNavigate(undefined)); } else { dispatch(disconnect(true)); } }; } /** * Signals that the lock state of a specific JitsiConference changed. * * @param {JitsiConference} conference - The JitsiConference which had its lock * state changed. * @param {boolean} locked - If the specified conference became locked, true; * otherwise, false. * @returns {{ * type: LOCK_STATE_CHANGED, * conference: JitsiConference, * locked: boolean * }} */ export function lockStateChanged(conference: Object, locked: boolean) { return { type: LOCK_STATE_CHANGED, conference, locked }; } /** * Signals that a non participant endpoint message has been received. * * @param {string} id - The resource id of the sender. * @param {Object} json - The json carried by the endpoint message. * @returns {{ * type: NON_PARTICIPANT_MESSAGE_RECEIVED, * id: Object, * json: Object * }} */ export function nonParticipantMessageReceived(id: String, json: Object) { return { type: NON_PARTICIPANT_MESSAGE_RECEIVED, id, json }; } /** * Updates the known state of start muted policies. * * @param {boolean} audioMuted - Whether or not members will join the conference * as audio muted. * @param {boolean} videoMuted - Whether or not members will join the conference * as video muted. * @returns {{ * type: SET_START_MUTED_POLICY, * startAudioMutedPolicy: boolean, * startVideoMutedPolicy: boolean * }} */ export function onStartMutedPolicyChanged( audioMuted: boolean, videoMuted: boolean) { return { type: SET_START_MUTED_POLICY, startAudioMutedPolicy: audioMuted, startVideoMutedPolicy: videoMuted }; } /** * Sets whether or not peer2peer is currently enabled. * * @param {boolean} p2p - Whether or not peer2peer is currently active. * @returns {{ * type: P2P_STATUS_CHANGED, * p2p: boolean * }} */ export function p2pStatusChanged(p2p: boolean) { return { type: P2P_STATUS_CHANGED, p2p }; } /** * Signals to play touch tones. * * @param {string} tones - The tones to play. * @param {number} [duration] - How long to play each tone. * @param {number} [pause] - How long to pause between each tone. * @returns {{ * type: SEND_TONES, * tones: string, * duration: number, * pause: number * }} */ export function sendTones(tones: string, duration: number, pause: number) { return { type: SEND_TONES, tones, duration, pause }; } /** * Enables or disables the Follow Me feature. * * @param {boolean} enabled - Whether or not Follow Me should be enabled. * @returns {{ * type: SET_FOLLOW_ME, * enabled: boolean * }} */ export function setFollowMe(enabled: boolean) { return { type: SET_FOLLOW_ME, enabled }; } /** * Enables or disables the Mute reaction sounds feature. * * @param {boolean} muted - Whether or not reaction sounds should be muted for all participants. * @param {boolean} updateBackend - Whether or not the moderator should notify all participants for the new setting. * @returns {{ * type: SET_START_REACTIONS_MUTED, * muted: boolean * }} */ export function setStartReactionsMuted(muted: boolean, updateBackend = false) { return { type: SET_START_REACTIONS_MUTED, muted, updateBackend }; } /** * Sets the password to join or lock a specific JitsiConference. * * @param {JitsiConference} conference - The JitsiConference which requires a * password to join or is to be locked with the specified password. * @param {Function} method - The JitsiConference method of password protection * such as join or lock. * @param {string} password - The password with which the specified conference * is to be joined or locked. * @returns {Function} */ export function setPassword( conference: IJitsiConference | undefined, method: Function | undefined, password: string) { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { if (!conference) { return; } switch (method) { case conference.join: { let state = getState()['features/base/conference']; dispatch({ type: SET_PASSWORD, conference, method, password }); // Join the conference with the newly-set password. // Make sure that the action did set the password. state = getState()['features/base/conference']; if (state.password === password // Make sure that the application still wants the // conference joined. && !state.conference) { method.call(conference, password); } break; } case conference.lock: { const state = getState()['features/base/conference']; if (state.conference === conference) { return ( method.call(conference, password) .then(() => dispatch({ type: SET_PASSWORD, conference, method, password })) .catch((error: Error) => dispatch({ type: SET_PASSWORD_FAILED, error })) ); } return Promise.reject(); } } }; } /** * Sets the obfuscated room name of the conference to be joined. * * @param {(string)} obfuscatedRoom - Obfuscated room name. * @param {(string)} obfuscatedRoomSource - The room name that was obfuscated. * @returns {{ * type: SET_OBFUSCATED_ROOM, * room: string * }} */ export function setObfuscatedRoom(obfuscatedRoom: string, obfuscatedRoomSource: string) { return { type: SET_OBFUSCATED_ROOM, obfuscatedRoom, obfuscatedRoomSource }; } /** * Sets (the name of) the room of the conference to be joined. * * @param {(string|undefined)} room - The name of the room of the conference to * be joined. * @returns {{ * type: SET_ROOM, * room: string * }} */ export function setRoom(room?: string) { return { type: SET_ROOM, room }; } /** * Sets whether or not members should join audio and/or video muted. * * @param {boolean} startAudioMuted - Whether or not members will join the * conference as audio muted. * @param {boolean} startVideoMuted - Whether or not members will join the * conference as video muted. * @returns {Function} */ export function setStartMutedPolicy( startAudioMuted: boolean, startVideoMuted: boolean) { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const conference = getCurrentConference(getState()); conference?.setStartMutedPolicy({ audio: startAudioMuted, video: startVideoMuted }); return dispatch( onStartMutedPolicyChanged(startAudioMuted, startVideoMuted)); }; } /** * Sets the conference subject. * * @param {string} subject - The new subject. * @returns {void} */ export function setSubject(subject: string) { return (dispatch: IStore['dispatch'], getState: IStore['getState']) => { const { conference } = getState()['features/base/conference']; if (conference) { conference.setSubject(subject || ''); } else { dispatch({ type: SET_PENDING_SUBJECT_CHANGE, subject }); } }; } /** * Sets the conference local subject. * * @param {string} localSubject - The new local subject. * @returns {{ * type: CONFERENCE_LOCAL_SUBJECT_CHANGED, * localSubject: string * }} */ export function setLocalSubject(localSubject: string) { return { type: CONFERENCE_LOCAL_SUBJECT_CHANGED, localSubject }; }