| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780 | // @flow
import type { Dispatch } from 'redux';
import {
    createStartMutedConfigurationEvent,
    sendAnalytics
} from '../../analytics';
import { getName } from '../../app';
import { endpointMessageReceived } from '../../subtitles';
import { JITSI_CONNECTION_CONFERENCE_KEY } from '../connection';
import { JitsiConferenceEvents } from '../lib-jitsi-meet';
import { setAudioMuted, setVideoMuted } from '../media';
import {
    dominantSpeakerChanged,
    getLocalParticipant,
    getNormalizedDisplayName,
    participantConnectionStatusChanged,
    participantKicked,
    participantMutedUs,
    participantPresenceChanged,
    participantRoleChanged,
    participantUpdated
} from '../participants';
import { getLocalTracks, trackAdded, trackRemoved } from '../tracks';
import {
    getBackendSafeRoomName,
    getJitsiMeetGlobalNS
} from '../util';
import {
    AUTH_STATUS_CHANGED,
    CONFERENCE_FAILED,
    CONFERENCE_JOINED,
    CONFERENCE_LEFT,
    CONFERENCE_SUBJECT_CHANGED,
    CONFERENCE_TIMESTAMP_CHANGED,
    CONFERENCE_WILL_JOIN,
    CONFERENCE_WILL_LEAVE,
    DATA_CHANNEL_OPENED,
    KICKED_OUT,
    LOCK_STATE_CHANGED,
    P2P_STATUS_CHANGED,
    SEND_TONES,
    SET_DESKTOP_SHARING_ENABLED,
    SET_FOLLOW_ME,
    SET_MAX_RECEIVER_VIDEO_QUALITY,
    SET_PASSWORD,
    SET_PASSWORD_FAILED,
    SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
    SET_ROOM,
    SET_PENDING_SUBJECT_CHANGE,
    SET_START_MUTED_POLICY
} from './actionTypes';
import {
    AVATAR_ID_COMMAND,
    AVATAR_URL_COMMAND,
    EMAIL_COMMAND,
    JITSI_CONFERENCE_URL_KEY
} from './constants';
import {
    _addLocalTracksToConference,
    commonUserJoinedHandling,
    commonUserLeftHandling,
    getCurrentConference,
    sendLocalParticipant
} from './functions';
import logger from './logger';
declare var APP: Object;
/**
 * Adds conference (event) listeners.
 *
 * @param {JitsiConference} conference - The JitsiConference instance.
 * @param {Dispatch} dispatch - The Redux dispatch function.
 * @private
 * @returns {void}
 */
function _addConferenceListeners(conference, dispatch) {
    // A simple logger for conference errors received through
    // the listener. These errors are not handled now, but logged.
    conference.on(JitsiConferenceEvents.CONFERENCE_ERROR,
        error => logger.error('Conference error.', error));
    // Dispatches into features/base/conference follow:
    conference.on(
        JitsiConferenceEvents.CONFERENCE_FAILED,
        (...args) => dispatch(conferenceFailed(conference, ...args)));
    conference.on(
        JitsiConferenceEvents.CONFERENCE_JOINED,
        (...args) => dispatch(conferenceJoined(conference, ...args)));
    conference.on(
        JitsiConferenceEvents.CONFERENCE_LEFT,
        (...args) => {
            dispatch(conferenceTimestampChanged(0));
            dispatch(conferenceLeft(conference, ...args));
        });
    conference.on(JitsiConferenceEvents.SUBJECT_CHANGED,
        (...args) => dispatch(conferenceSubjectChanged(...args)));
    conference.on(JitsiConferenceEvents.CONFERENCE_CREATED_TIMESTAMP,
        (...args) => dispatch(conferenceTimestampChanged(...args)));
    conference.on(
        JitsiConferenceEvents.KICKED,
        (...args) => dispatch(kickedOut(conference, ...args)));
    conference.on(
        JitsiConferenceEvents.PARTICIPANT_KICKED,
        (kicker, kicked) => dispatch(participantKicked(kicker, kicked)));
    conference.on(
        JitsiConferenceEvents.LOCK_STATE_CHANGED,
        (...args) => dispatch(lockStateChanged(conference, ...args)));
    // Dispatches into features/base/media follow:
    conference.on(
        JitsiConferenceEvents.STARTED_MUTED,
        () => {
            const audioMuted = Boolean(conference.startAudioMuted);
            const videoMuted = Boolean(conference.startVideoMuted);
            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));
        });
    // Dispatches into features/base/tracks follow:
    conference.on(
        JitsiConferenceEvents.TRACK_ADDED,
        t => t && !t.isLocal() && dispatch(trackAdded(t)));
    conference.on(
        JitsiConferenceEvents.TRACK_REMOVED,
        t => t && !t.isLocal() && dispatch(trackRemoved(t)));
    conference.on(
        JitsiConferenceEvents.TRACK_MUTE_CHANGED,
        (_, participantThatMutedUs) => {
            if (participantThatMutedUs) {
                dispatch(participantMutedUs(participantThatMutedUs));
            }
        });
    // Dispatches into features/base/participants follow:
    conference.on(
        JitsiConferenceEvents.DISPLAY_NAME_CHANGED,
        (id, displayName) => dispatch(participantUpdated({
            conference,
            id,
            name: getNormalizedDisplayName(displayName)
        })));
    conference.on(
        JitsiConferenceEvents.DOMINANT_SPEAKER_CHANGED,
        id => dispatch(dominantSpeakerChanged(id, conference)));
    conference.on(
        JitsiConferenceEvents.ENDPOINT_MESSAGE_RECEIVED,
        (...args) => dispatch(endpointMessageReceived(...args)));
    conference.on(
        JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
        (...args) => dispatch(participantConnectionStatusChanged(...args)));
    conference.on(
        JitsiConferenceEvents.USER_JOINED,
        (id, user) => commonUserJoinedHandling({ dispatch }, conference, user));
    conference.on(
        JitsiConferenceEvents.USER_LEFT,
        (id, user) => commonUserLeftHandling({ dispatch }, conference, user));
    conference.on(
        JitsiConferenceEvents.USER_ROLE_CHANGED,
        (...args) => dispatch(participantRoleChanged(...args)));
    conference.on(
        JitsiConferenceEvents.USER_STATUS_CHANGED,
        (...args) => dispatch(participantPresenceChanged(...args)));
    conference.on(
        JitsiConferenceEvents.BOT_TYPE_CHANGED,
        (id, botType) => dispatch(participantUpdated({
            conference,
            id,
            botType
        })));
    conference.addCommandListener(
        AVATAR_ID_COMMAND,
        (data, id) => dispatch(participantUpdated({
            conference,
            id,
            avatarID: data.value
        })));
    conference.addCommandListener(
        AVATAR_URL_COMMAND,
        (data, id) => dispatch(participantUpdated({
            conference,
            id,
            avatarURL: data.value
        })));
    conference.addCommandListener(
        EMAIL_COMMAND,
        (data, id) => dispatch(participantUpdated({
            conference,
            id,
            email: data.value
        })));
}
/**
 * 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.
 * @returns {{
 *     type: CONFERENCE_FAILED,
 *     conference: JitsiConference,
 *     error: Error
 * }}
 * @public
 */
export function conferenceFailed(conference: Object, error: string) {
    return {
        type: CONFERENCE_FAILED,
        conference,
        // Make the error resemble an Error instance (to the extent that
        // jitsi-meet needs it).
        error: {
            name: error,
            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: Object) {
    return {
        type: CONFERENCE_JOINED,
        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: Object) {
    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
    };
}
/**
 * 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}
 */
function _conferenceWillJoin(conference: Object) {
    return (dispatch: Dispatch<any>, getState: Function) => {
        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: Object) {
    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: Object) {
    return {
        type: CONFERENCE_WILL_LEAVE,
        conference
    };
}
/**
 * Initializes a new conference.
 *
 * @returns {Function}
 */
export function createConference() {
    return (dispatch: Function, getState: Function) => {
        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!');
        }
        const config = state['features/base/config'];
        const { email, name: nick } = getLocalParticipant(state);
        const conference
            = connection.initJitsiConference(
                getBackendSafeRoomName(room), {
                    ...config,
                    applicationName: getName(),
                    getWiFiStatsMethod: getJitsiMeetGlobalNS().getWiFiStats,
                    confID: `${locationURL.host}${locationURL.pathname}`,
                    statisticsDisplayName: config.enableDisplayNameInStats ? nick : undefined,
                    statisticsId: config.enableEmailInStats ? email : undefined
                });
        connection[JITSI_CONNECTION_CONFERENCE_KEY] = conference;
        conference[JITSI_CONFERENCE_URL_KEY] = locationURL;
        dispatch(_conferenceWillJoin(conference));
        _addConferenceListeners(conference, dispatch);
        sendLocalParticipant(state, conference);
        conference.join(password);
    };
}
/**
 * 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: Function, getState: Function) => {
        const { authRequired, password }
            = getState()['features/base/conference'];
        authRequired && dispatch(_conferenceWillJoin(authRequired));
        authRequired && authRequired.join(password);
    };
}
/**
 * Signals the data channel with the bridge has successfully opened.
 *
 * @returns {{
 *     type: DATA_CHANNEL_OPENED
 * }}
 */
export function dataChannelOpened() {
    return {
        type: DATA_CHANNEL_OPENED
    };
}
/**
 * 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
    };
}
/**
 * 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
    };
}
/**
 * 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
    };
}
/**
 * Sets the flag for indicating if desktop sharing is enabled.
 *
 * @param {boolean} desktopSharingEnabled - True if desktop sharing is enabled.
 * @returns {{
 *     type: SET_DESKTOP_SHARING_ENABLED,
 *     desktopSharingEnabled: boolean
 * }}
 */
export function setDesktopSharingEnabled(desktopSharingEnabled: boolean) {
    return {
        type: SET_DESKTOP_SHARING_ENABLED,
        desktopSharingEnabled
    };
}
/**
 * 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
    };
}
/**
 * Sets the max frame height that should be received from remote videos.
 *
 * @param {number} maxReceiverVideoQuality - The max video frame height to
 * receive.
 * @returns {{
 *     type: SET_MAX_RECEIVER_VIDEO_QUALITY,
 *     maxReceiverVideoQuality: number
 * }}
 */
export function setMaxReceiverVideoQuality(maxReceiverVideoQuality: number) {
    return {
        type: SET_MAX_RECEIVER_VIDEO_QUALITY,
        maxReceiverVideoQuality
    };
}
/**
 * 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: Object,
        method: Function,
        password: string) {
    return (dispatch: Dispatch<any>, getState: Function): ?Promise<void> => {
        switch (method) {
        case conference.join: {
            let state = getState()['features/base/conference'];
            // Make sure that the action will set a password for a conference
            // that the application wants joined.
            if (state.passwordRequired === 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
                        && !state.passwordRequired
                        // 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 => dispatch({
                            type: SET_PASSWORD_FAILED,
                            error
                        }))
                );
            }
            return Promise.reject();
        }
        }
    };
}
/**
 * Sets the max frame height the user prefers to receive from remote participant
 * videos.
 *
 * @param {number} preferredReceiverVideoQuality - The max video resolution to
 * receive.
 * @returns {{
 *     type: SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
 *     preferredReceiverVideoQuality: number
 * }}
 */
export function setPreferredReceiverVideoQuality(
        preferredReceiverVideoQuality: number) {
    return {
        type: SET_PREFERRED_RECEIVER_VIDEO_QUALITY,
        preferredReceiverVideoQuality
    };
}
/**
 * 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: Dispatch<any>, getState: Function) => {
        const conference = getCurrentConference(getState());
        conference && conference.setStartMutedPolicy({
            audio: startAudioMuted,
            video: startVideoMuted
        });
        return dispatch(
            onStartMutedPolicyChanged(startAudioMuted, startVideoMuted));
    };
}
/**
 * Changing conference subject.
 *
 * @param {string} subject - The new subject.
 * @returns {void}
 */
export function setSubject(subject: string = '') {
    return (dispatch: Dispatch<any>, getState: Function) => {
        const { conference } = getState()['features/base/conference'];
        if (conference) {
            conference.setSubject(subject);
        } else {
            dispatch({
                type: SET_PENDING_SUBJECT_CHANGE,
                subject
            });
        }
    };
}
 |