<commit_before>// @flow

import { Alert, NativeModules, Platform } from 'react-native';
import uuid from 'uuid';

import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
import { appNavigate } from '../../app/actions';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { SET_AUDIO_ONLY } from '../../base/audio-only';
import {
    CONFERENCE_FAILED,
    CONFERENCE_JOINED,
    CONFERENCE_LEFT,
    CONFERENCE_WILL_JOIN,
    CONFERENCE_WILL_LEAVE,
    getConferenceName,
    getCurrentConference
} from '../../base/conference';
import { getInviteURL } from '../../base/connection';
import {
    MEDIA_TYPE,
    isVideoMutedByAudioOnly,
    setAudioMuted
} from '../../base/media';
import { MiddlewareRegistry } from '../../base/redux';
import {
    TRACK_ADDED,
    TRACK_REMOVED,
    TRACK_UPDATED,
    isLocalTrackMuted
} from '../../base/tracks';

import CallKit from './CallKit';
import ConnectionService from './ConnectionService';
import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
import { isCallIntegrationEnabled } from './functions';

const { AudioMode } = NativeModules;
const CallIntegration = CallKit || ConnectionService;

/**
 * Middleware that captures system actions and hooks up CallKit.
 *
 * @param {Store} store - The redux store.
 * @returns {Function}
 */
CallIntegration && MiddlewareRegistry.register(store => next => action => {
    switch (action.type) {
    case _SET_CALL_INTEGRATION_SUBSCRIPTIONS:
        return _setCallKitSubscriptions(store, next, action);

    case APP_WILL_MOUNT:
        return _appWillMount(store, next, action);

    case APP_WILL_UNMOUNT:
        store.dispatch({
            type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
            subscriptions: undefined
        });
        break;

    case CONFERENCE_FAILED:
        return _conferenceFailed(store, next, action);

    case CONFERENCE_JOINED:
        return _conferenceJoined(store, next, action);

    // If a conference is being left in a graceful manner then
    // the CONFERENCE_WILL_LEAVE fires as soon as the conference starts
    // disconnecting. We need to destroy the call on the native side as soon
    // as possible, because the disconnection process is asynchronous and
    // Android not always supports two simultaneous calls at the same time
    // (even though it should according to the spec).
    case CONFERENCE_LEFT:
    case CONFERENCE_WILL_LEAVE:
        return _conferenceLeft(store, next, action);

    case CONFERENCE_WILL_JOIN:
        return _conferenceWillJoin(store, next, action);

    case SET_AUDIO_ONLY:
        return _setAudioOnly(store, next, action);

    case TRACK_ADDED:
    case TRACK_REMOVED:
    case TRACK_UPDATED:
        return _syncTrackState(store, next, action);
    }

    return next(action);
});

/**
 * Notifies the feature callkit that the action {@link APP_WILL_MOUNT} is being
 * dispatched within a specific redux {@code 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 APP_WILL_MOUNT} which is
 * being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _appWillMount({ dispatch, getState }, next, action) {
    const result = next(action);

    const context = {
        dispatch,
        getState
    };

    const delegate = {
        _onPerformSetMutedCallAction,
        _onPerformEndCallAction
    };

    const subscriptions
        = CallIntegration.registerSubscriptions(context, delegate);

    subscriptions && dispatch({
        type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
        subscriptions
    });

    return result;
}

/**
 * Notifies the feature callkit that the action {@link CONFERENCE_FAILED} is
 * being dispatched within a specific redux {@code 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 CONFERENCE_FAILED} which is
 * being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _conferenceFailed({ getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    // XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
    // prevented the user from joining a specific conference but the app may be
    // able to eventually join the conference.
    if (!action.error.recoverable) {
        const { callUUID } = action.conference;

        if (callUUID) {
            delete action.conference.callUUID;
            CallIntegration.reportCallFailed(callUUID);
        }
    }

    return result;
}

/**
 * Notifies the feature callkit that the action {@link CONFERENCE_JOINED} is
 * being dispatched within a specific redux {@code 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 CONFERENCE_JOINED} which is
 * being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _conferenceJoined({ getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    const { callUUID } = action.conference;

    if (callUUID) {
        CallIntegration.reportConnectedOutgoingCall(callUUID)
            .then(() => {
                // iOS 13 doesn't like the mute state to be false before the call is started
                // so we update it here in case the user selected startWithAudioMuted.
                if (Platform.OS === 'ios') {
                    _updateCallIntegrationMuted(action.conference, getState());
                }
            })
            .catch(() => {
                // Currently errors here are only emitted by Android.
                //
                // Some Samsung devices will fail to fully engage ConnectionService if no SIM card
                // was ever installed on the device. We could check for it, but it would require
                // the CALL_PHONE permission, which is not something we want to do, so fallback to
                // not using ConnectionService.
                _handleConnectionServiceFailure(getState());
            });
    }

    return result;
}

/**
 * Notifies the feature callkit that the action {@link CONFERENCE_LEFT} is being
 * dispatched within a specific redux {@code 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 CONFERENCE_LEFT} which is
 * being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _conferenceLeft({ getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    const { callUUID } = action.conference;

    if (callUUID) {
        delete action.conference.callUUID;
        CallIntegration.endCall(callUUID);
    }

    return result;
}

/**
 * Notifies the feature callkit that the action {@link CONFERENCE_WILL_JOIN} is
 * being dispatched within a specific redux {@code 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 CONFERENCE_WILL_JOIN} which
 * is being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _conferenceWillJoin({ dispatch, getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    const { conference } = action;
    const state = getState();
    const { callHandle, callUUID } = state['features/base/config'];
    const url = getInviteURL(state);
    const handle = callHandle || url.toString();
    const hasVideo = !isVideoMutedByAudioOnly(state);

    // When assigning the call UUID, do so in upper case, since iOS will return
    // it upper cased.
    conference.callUUID = (callUUID || uuid.v4()).toUpperCase();

    CallIntegration.startCall(conference.callUUID, handle, hasVideo)
        .then(() => {
            const displayName = getConferenceName(state);

            CallIntegration.updateCall(
                conference.callUUID,
                {
                    displayName,
                    hasVideo
                });

            // iOS 13 doesn't like the mute state to be false before the call is started
            // so delay it until the conference was joined.
            if (Platform.OS !== 'ios') {
                _updateCallIntegrationMuted(conference, state);
            }
        })
        .catch(error => {
            // Currently this error codes are emitted only by Android.
            //
            if (error.code === 'CREATE_OUTGOING_CALL_FAILED') {
                // We're not tracking the call anymore - it doesn't exist on
                // the native side.
                delete conference.callUUID;
                dispatch(appNavigate(undefined));
                Alert.alert(
                    'Call aborted',
                    'There\'s already another call in progress.'
                        + ' Please end it first and try again.',
                    [
                        { text: 'OK' }
                    ],
                    { cancelable: false });
            } else {
                // Some devices fail because the CALL_PHONE permission is not granted, which is
                // nonsense, because it's not needed for self-managed connections.
                // Some other devices fail because ConnectionService is not supported.
                // Be that as it may, fallback to non-ConnectionService audio device handling.

                _handleConnectionServiceFailure(state);
            }
        });

    return result;
}

/**
 * Handles a ConnectionService fatal error by falling back to non-ConnectionService device management.
 *
 * @param {Object} state - Redux store.
 * @returns {void}
 */
function _handleConnectionServiceFailure(state: Object) {
    const conference = getCurrentConference(state);

    if (conference) {
        // We're not tracking the call anymore.
        delete conference.callUUID;

        // ConnectionService has fatally failed. Alas, this also means audio device management would be broken, so
        // fallback to not using ConnectionService.
        // NOTE: We are not storing this in Settings, in case it's a transient issue, as far fetched as
        // that may be.
        if (AudioMode.setUseConnectionService) {
            AudioMode.setUseConnectionService(false);

            const hasVideo = !isVideoMutedByAudioOnly(state);

            // Set the desired audio mode, since we just reset the whole thing.
            AudioMode.setMode(hasVideo ? AudioMode.VIDEO_CALL : AudioMode.AUDIO_CALL);
        }
    }
}

/**
 * Handles CallKit's event {@code performEndCallAction}.
 *
 * @param {Object} event - The details of the CallKit event
 * {@code performEndCallAction}.
 * @returns {void}
 */
function _onPerformEndCallAction({ callUUID }) {
    const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
    const conference = getCurrentConference(getState);

    if (conference && conference.callUUID === callUUID) {
        // We arrive here when a call is ended by the system, for example, when
        // another incoming call is received and the user selects "End &
        // Accept".
        delete conference.callUUID;
        dispatch(appNavigate(undefined));
    }
}

/**
 * Handles CallKit's event {@code performSetMutedCallAction}.
 *
 * @param {Object} event - The details of the CallKit event
 * {@code performSetMutedCallAction}.
 * @returns {void}
 */
function _onPerformSetMutedCallAction({ callUUID, muted }) {
    const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
    const conference = getCurrentConference(getState);

    if (conference && conference.callUUID === callUUID) {
        muted = Boolean(muted); // eslint-disable-line no-param-reassign
        sendAnalytics(
            createTrackMutedEvent('audio', 'call-integration', muted));
        dispatch(setAudioMuted(muted, /* ensureTrack */ true));
    }
}

/**
 * Update CallKit with the audio only state of the conference. When a conference
 * is in audio only mode we will tell CallKit the call has no video. This
 * affects how the call is saved in the recent calls list.
 *
 * XXX: Note that here we are taking the `audioOnly` value straight from the
 * action, instead of examining the state. This is intentional, as setting the
 * audio only involves multiple actions which will be reflected in the state
 * later, but we are just interested in knowing if the mode is going to be
 * set or not.
 *
 * @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 which is being dispatched in the
 * specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _setAudioOnly({ getState }, next, action) {
    const result = next(action);
    const state = getState();

    if (!isCallIntegrationEnabled(state)) {
        return result;
    }

    const conference = getCurrentConference(state);

    if (conference && conference.callUUID) {
        CallIntegration.updateCall(
            conference.callUUID,
            { hasVideo: !action.audioOnly });
    }

    return result;
}

/**
 * Notifies the feature callkit that the action
 * {@link _SET_CALL_INTEGRATION_SUBSCRIPTIONS} is being dispatched within
 * a specific redux {@code 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 _SET_CALL_INTEGRATION_SUBSCRIPTIONS} which is being dispatched in
 * the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _setCallKitSubscriptions({ getState }, next, action) {
    const { subscriptions } = getState()['features/call-integration'];

    if (subscriptions) {
        for (const subscription of subscriptions) {
            subscription.remove();
        }
    }

    return next(action);
}

/**
 * Synchronize the muted state of tracks with CallKit.
 *
 * @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 which is being dispatched in the
 * specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _syncTrackState({ getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    const { jitsiTrack } = action.track;
    const state = getState();
    const conference = getCurrentConference(state);

    if (jitsiTrack.isLocal() && conference && conference.callUUID) {
        switch (jitsiTrack.getType()) {
        case 'audio': {
            _updateCallIntegrationMuted(conference, state);
            break;
        }
        case 'video': {
            CallIntegration.updateCall(
                conference.callUUID,
                { hasVideo: !isVideoMutedByAudioOnly(state) });
            break;
        }

        }
    }

    return result;
}

/**
 * Update the muted state in the native side.
 *
 * @param {Object} conference - The current active conference.
 * @param {Object} state - The redux store state.
 * @private
 * @returns {void}
 */
function _updateCallIntegrationMuted(conference, state) {
    const muted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);

    CallIntegration.setMuted(conference.callUUID, muted);
}
<commit_msg>rn: don't start native call integration multiple times<commit_after>// @flow

import { Alert, NativeModules, Platform } from 'react-native';
import uuid from 'uuid';

import { createTrackMutedEvent, sendAnalytics } from '../../analytics';
import { appNavigate } from '../../app/actions';
import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../../base/app';
import { SET_AUDIO_ONLY } from '../../base/audio-only';
import {
    CONFERENCE_FAILED,
    CONFERENCE_JOINED,
    CONFERENCE_LEFT,
    CONFERENCE_WILL_JOIN,
    CONFERENCE_WILL_LEAVE,
    getConferenceName,
    getCurrentConference
} from '../../base/conference';
import { getInviteURL } from '../../base/connection';
import {
    MEDIA_TYPE,
    isVideoMutedByAudioOnly,
    setAudioMuted
} from '../../base/media';
import { MiddlewareRegistry } from '../../base/redux';
import {
    TRACK_ADDED,
    TRACK_REMOVED,
    TRACK_UPDATED,
    isLocalTrackMuted
} from '../../base/tracks';

import CallKit from './CallKit';
import ConnectionService from './ConnectionService';
import { _SET_CALL_INTEGRATION_SUBSCRIPTIONS } from './actionTypes';
import { isCallIntegrationEnabled } from './functions';

const { AudioMode } = NativeModules;
const CallIntegration = CallKit || ConnectionService;

/**
 * Middleware that captures system actions and hooks up CallKit.
 *
 * @param {Store} store - The redux store.
 * @returns {Function}
 */
CallIntegration && MiddlewareRegistry.register(store => next => action => {
    switch (action.type) {
    case _SET_CALL_INTEGRATION_SUBSCRIPTIONS:
        return _setCallKitSubscriptions(store, next, action);

    case APP_WILL_MOUNT:
        return _appWillMount(store, next, action);

    case APP_WILL_UNMOUNT:
        store.dispatch({
            type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
            subscriptions: undefined
        });
        break;

    case CONFERENCE_FAILED:
        return _conferenceFailed(store, next, action);

    case CONFERENCE_JOINED:
        return _conferenceJoined(store, next, action);

    // If a conference is being left in a graceful manner then
    // the CONFERENCE_WILL_LEAVE fires as soon as the conference starts
    // disconnecting. We need to destroy the call on the native side as soon
    // as possible, because the disconnection process is asynchronous and
    // Android not always supports two simultaneous calls at the same time
    // (even though it should according to the spec).
    case CONFERENCE_LEFT:
    case CONFERENCE_WILL_LEAVE:
        return _conferenceLeft(store, next, action);

    case CONFERENCE_WILL_JOIN:
        return _conferenceWillJoin(store, next, action);

    case SET_AUDIO_ONLY:
        return _setAudioOnly(store, next, action);

    case TRACK_ADDED:
    case TRACK_REMOVED:
    case TRACK_UPDATED:
        return _syncTrackState(store, next, action);
    }

    return next(action);
});

/**
 * Notifies the feature callkit that the action {@link APP_WILL_MOUNT} is being
 * dispatched within a specific redux {@code 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 APP_WILL_MOUNT} which is
 * being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _appWillMount({ dispatch, getState }, next, action) {
    const result = next(action);

    const context = {
        dispatch,
        getState
    };

    const delegate = {
        _onPerformSetMutedCallAction,
        _onPerformEndCallAction
    };

    const subscriptions
        = CallIntegration.registerSubscriptions(context, delegate);

    subscriptions && dispatch({
        type: _SET_CALL_INTEGRATION_SUBSCRIPTIONS,
        subscriptions
    });

    return result;
}

/**
 * Notifies the feature callkit that the action {@link CONFERENCE_FAILED} is
 * being dispatched within a specific redux {@code 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 CONFERENCE_FAILED} which is
 * being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _conferenceFailed({ getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    // XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
    // prevented the user from joining a specific conference but the app may be
    // able to eventually join the conference.
    if (!action.error.recoverable) {
        const { callUUID } = action.conference;

        if (callUUID) {
            delete action.conference.callUUID;
            CallIntegration.reportCallFailed(callUUID);
        }
    }

    return result;
}

/**
 * Notifies the feature callkit that the action {@link CONFERENCE_JOINED} is
 * being dispatched within a specific redux {@code 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 CONFERENCE_JOINED} which is
 * being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _conferenceJoined({ getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    const { callUUID } = action.conference;

    if (callUUID) {
        CallIntegration.reportConnectedOutgoingCall(callUUID)
            .then(() => {
                // iOS 13 doesn't like the mute state to be false before the call is started
                // so we update it here in case the user selected startWithAudioMuted.
                if (Platform.OS === 'ios') {
                    _updateCallIntegrationMuted(action.conference, getState());
                }
            })
            .catch(() => {
                // Currently errors here are only emitted by Android.
                //
                // Some Samsung devices will fail to fully engage ConnectionService if no SIM card
                // was ever installed on the device. We could check for it, but it would require
                // the CALL_PHONE permission, which is not something we want to do, so fallback to
                // not using ConnectionService.
                _handleConnectionServiceFailure(getState());
            });
    }

    return result;
}

/**
 * Notifies the feature callkit that the action {@link CONFERENCE_LEFT} is being
 * dispatched within a specific redux {@code 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 CONFERENCE_LEFT} which is
 * being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _conferenceLeft({ getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    const { callUUID } = action.conference;

    if (callUUID) {
        delete action.conference.callUUID;
        CallIntegration.endCall(callUUID);
    }

    return result;
}

/**
 * Notifies the feature callkit that the action {@link CONFERENCE_WILL_JOIN} is
 * being dispatched within a specific redux {@code 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 CONFERENCE_WILL_JOIN} which
 * is being dispatched in the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _conferenceWillJoin({ dispatch, getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    const { conference } = action;
    const state = getState();
    const { callHandle, callUUID } = state['features/base/config'];
    const url = getInviteURL(state);
    const handle = callHandle || url.toString();
    const hasVideo = !isVideoMutedByAudioOnly(state);

    // If we already have a callUUID set, don't start a new call.
    if (conference.callUUID) {
        return result;
    }

    // When assigning the call UUID, do so in upper case, since iOS will return
    // it upper cased.
    conference.callUUID = (callUUID || uuid.v4()).toUpperCase();

    CallIntegration.startCall(conference.callUUID, handle, hasVideo)
        .then(() => {
            const displayName = getConferenceName(state);

            CallIntegration.updateCall(
                conference.callUUID,
                {
                    displayName,
                    hasVideo
                });

            // iOS 13 doesn't like the mute state to be false before the call is started
            // so delay it until the conference was joined.
            if (Platform.OS !== 'ios') {
                _updateCallIntegrationMuted(conference, state);
            }
        })
        .catch(error => {
            // Currently this error codes are emitted only by Android.
            //
            if (error.code === 'CREATE_OUTGOING_CALL_FAILED') {
                // We're not tracking the call anymore - it doesn't exist on
                // the native side.
                delete conference.callUUID;
                dispatch(appNavigate(undefined));
                Alert.alert(
                    'Call aborted',
                    'There\'s already another call in progress.'
                        + ' Please end it first and try again.',
                    [
                        { text: 'OK' }
                    ],
                    { cancelable: false });
            } else {
                // Some devices fail because the CALL_PHONE permission is not granted, which is
                // nonsense, because it's not needed for self-managed connections.
                // Some other devices fail because ConnectionService is not supported.
                // Be that as it may, fallback to non-ConnectionService audio device handling.

                _handleConnectionServiceFailure(state);
            }
        });

    return result;
}

/**
 * Handles a ConnectionService fatal error by falling back to non-ConnectionService device management.
 *
 * @param {Object} state - Redux store.
 * @returns {void}
 */
function _handleConnectionServiceFailure(state: Object) {
    const conference = getCurrentConference(state);

    if (conference) {
        // We're not tracking the call anymore.
        delete conference.callUUID;

        // ConnectionService has fatally failed. Alas, this also means audio device management would be broken, so
        // fallback to not using ConnectionService.
        // NOTE: We are not storing this in Settings, in case it's a transient issue, as far fetched as
        // that may be.
        if (AudioMode.setUseConnectionService) {
            AudioMode.setUseConnectionService(false);

            const hasVideo = !isVideoMutedByAudioOnly(state);

            // Set the desired audio mode, since we just reset the whole thing.
            AudioMode.setMode(hasVideo ? AudioMode.VIDEO_CALL : AudioMode.AUDIO_CALL);
        }
    }
}

/**
 * Handles CallKit's event {@code performEndCallAction}.
 *
 * @param {Object} event - The details of the CallKit event
 * {@code performEndCallAction}.
 * @returns {void}
 */
function _onPerformEndCallAction({ callUUID }) {
    const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
    const conference = getCurrentConference(getState);

    if (conference && conference.callUUID === callUUID) {
        // We arrive here when a call is ended by the system, for example, when
        // another incoming call is received and the user selects "End &
        // Accept".
        delete conference.callUUID;
        dispatch(appNavigate(undefined));
    }
}

/**
 * Handles CallKit's event {@code performSetMutedCallAction}.
 *
 * @param {Object} event - The details of the CallKit event
 * {@code performSetMutedCallAction}.
 * @returns {void}
 */
function _onPerformSetMutedCallAction({ callUUID, muted }) {
    const { dispatch, getState } = this; // eslint-disable-line no-invalid-this
    const conference = getCurrentConference(getState);

    if (conference && conference.callUUID === callUUID) {
        muted = Boolean(muted); // eslint-disable-line no-param-reassign
        sendAnalytics(
            createTrackMutedEvent('audio', 'call-integration', muted));
        dispatch(setAudioMuted(muted, /* ensureTrack */ true));
    }
}

/**
 * Update CallKit with the audio only state of the conference. When a conference
 * is in audio only mode we will tell CallKit the call has no video. This
 * affects how the call is saved in the recent calls list.
 *
 * XXX: Note that here we are taking the `audioOnly` value straight from the
 * action, instead of examining the state. This is intentional, as setting the
 * audio only involves multiple actions which will be reflected in the state
 * later, but we are just interested in knowing if the mode is going to be
 * set or not.
 *
 * @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 which is being dispatched in the
 * specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _setAudioOnly({ getState }, next, action) {
    const result = next(action);
    const state = getState();

    if (!isCallIntegrationEnabled(state)) {
        return result;
    }

    const conference = getCurrentConference(state);

    if (conference && conference.callUUID) {
        CallIntegration.updateCall(
            conference.callUUID,
            { hasVideo: !action.audioOnly });
    }

    return result;
}

/**
 * Notifies the feature callkit that the action
 * {@link _SET_CALL_INTEGRATION_SUBSCRIPTIONS} is being dispatched within
 * a specific redux {@code 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 _SET_CALL_INTEGRATION_SUBSCRIPTIONS} which is being dispatched in
 * the specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _setCallKitSubscriptions({ getState }, next, action) {
    const { subscriptions } = getState()['features/call-integration'];

    if (subscriptions) {
        for (const subscription of subscriptions) {
            subscription.remove();
        }
    }

    return next(action);
}

/**
 * Synchronize the muted state of tracks with CallKit.
 *
 * @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 which is being dispatched in the
 * specified {@code store}.
 * @private
 * @returns {*} The value returned by {@code next(action)}.
 */
function _syncTrackState({ getState }, next, action) {
    const result = next(action);

    if (!isCallIntegrationEnabled(getState)) {
        return result;
    }

    const { jitsiTrack } = action.track;
    const state = getState();
    const conference = getCurrentConference(state);

    if (jitsiTrack.isLocal() && conference && conference.callUUID) {
        switch (jitsiTrack.getType()) {
        case 'audio': {
            _updateCallIntegrationMuted(conference, state);
            break;
        }
        case 'video': {
            CallIntegration.updateCall(
                conference.callUUID,
                { hasVideo: !isVideoMutedByAudioOnly(state) });
            break;
        }

        }
    }

    return result;
}

/**
 * Update the muted state in the native side.
 *
 * @param {Object} conference - The current active conference.
 * @param {Object} state - The redux store state.
 * @private
 * @returns {void}
 */
function _updateCallIntegrationMuted(conference, state) {
    const muted = isLocalTrackMuted(state['features/base/tracks'], MEDIA_TYPE.AUDIO);

    CallIntegration.setMuted(conference.callUUID, muted);
}
<|endoftext|>


<commit_before>/* global $, APP, interfaceConfig */

import React from 'react';
import { connect } from 'react-redux';

import { Watermarks } from '../../base/react';

import { AbstractWelcomePage, mapStateToProps } from './AbstractWelcomePage';

/* eslint-disable require-jsdoc */

/**
 * The Web container rendering the welcome page.
 *
 * @extends AbstractWelcomePage
 */
class WelcomePage extends AbstractWelcomePage {

/* eslint-enable require-jsdoc */

    /**
     * Initializes a new WelcomePage instance.
     *
     * @param {Object} props - The read-only properties with which the new
     * instance is to be initialized.
     */
    constructor(props) {
        super(props);

        this.state = {
            ...this.state,

            enableWelcomePage: true,
            generateRoomnames:
                interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE
        };

        // Bind event handlers so they are only bound once for every instance.
        this._onDisableWelcomeChange = this._onDisableWelcomeChange.bind(this);
        this._onKeyDown = this._onKeyDown.bind(this);
        this._onRoomChange = this._onRoomChange.bind(this);
    }

    /**
     * This method is executed when comonent is mounted.
     *
     * @inheritdoc
     * @returns {void}
     */
    componentDidMount() {
        if (this.state.generateRoomnames) {
            this._updateRoomname();
        }

        // XXX Temporary solution until we add React translation.
        APP.translation.translateElement($('#welcome_page'));
    }

    /**
     * Implements React's {@link Component#render()}.
     *
     * @inheritdoc
     * @returns {ReactElement|null}
     */
    render() {
        return (

                {
                    this._renderHeader()
                }
                {
                    this._renderMain()
                }

        );
    }

    /**
     * Returns the domain name.
     *
     * @private
     * @returns {string} Domain name.
     */
    _getDomain() {
        return `${window.location.protocol}//${window.location.host}/`;
    }

    /**
     * Handles <tt>change</tt> event of the checkbox which allows specifying
     * whether the WelcomePage is disabled.
     *
     * @param {Event} event - The (HTML) Event which details the change such as
     * the EventTarget.
     * @returns {void}
     */
    _onDisableWelcomeChange(event) {
        this.setState({
            enableWelcomePage: !event.target.value
        }, () => {
            APP.settings.setWelcomePageEnabled(this.state.enableWelcomePage);
        });
    }

    /**
     * Handles 'keydown' event to initiate joining the room when the
     * 'Enter/Return' button is pressed.
     *
     * @param {Event} event - Key down event object.
     * @private
     * @returns {void}
     */
    _onKeyDown(event) {
        if (event.keyCode === /* Enter */ 13) {
            this._onJoin();
        }
    }

    /**
     * Overrides the super to account for the differences in the argument types
     * provided by HTML and React Native text inputs.
     *
     * @inheritdoc
     * @override
     * @param {Event} event - The (HTML) Event which details the change such as
     * the EventTarget.
     * @protected
     */
    _onRoomChange(event) {
        super._onRoomChange(event.target.value);
    }

    /**
     * Renders a feature with a specific index.
     *
     * @param {number} index - The index of the feature to render.
     * @private
     * @returns {ReactElement}
     */
    _renderFeature(index) {
        return (

                <div
                    className = 'feature_icon'
                    data-i18n = { `welcomepage.feature${index}.title` } />
                <div
                    className = 'feature_description'
                    data-i18n = { `welcomepage.feature${index}.content` }
                    data-i18n-options = { JSON.stringify({
                        postProcess: 'resolveAppName'
                    }) } />

        );
    }

    /**
     * Renders a row of features.
     *
     * @param {number} beginIndex - The inclusive feature index to begin the row
     * with.
     * @param {number} endIndex - The exclusive feature index to end the row
     * with.
     * @private
     * @returns {ReactElement}
     */
    _renderFeatureRow(beginIndex, endIndex) {
        const features = [];

        for (let index = beginIndex; index < endIndex; ++index) {
            features.push(this._renderFeature(index));
        }

        return (

                {
                    features
                }

        );
    }

/* eslint-disable require-jsdoc */

    /**
     * Renders the header part of this WelcomePage.
     *
     * @private
     * @returns {ReactElement|null}
     */
    _renderHeader() {

/* eslint-enable require-jsdoc */

        return (

                <Watermarks />

                            {
                                this._getDomain()
                            }

                            <input
                                autoFocus = { true }
                                className = 'enter-room__field'
                                data-room-name
                                    = { this.state.generatedRoomname }
                                id = 'enter_room_field'
                                onChange = { this._onRoomChange }
                                onKeyDown = { this._onKeyDown }
                                placeholder = { this.state.roomPlaceholder }
                                type = 'text'
                                value = { this.state.room } />

                            { /* eslint-disable react/jsx-handler-names */ }

                            { /* eslint-enable react/jsx-handler-names */ }

                            <button
                                className = 'enter-room__button'
                                data-i18n = 'welcomepage.go'
                                id = 'enter_room_button'
                                onClick = { this._onJoin }
                                type = 'button' />

                <input
                    checked = { !this.state.enableWelcomePage }
                    id = 'disable_welcome'
                    name = 'checkbox'
                    onChange = { this._onDisableWelcomeChange }
                    type = 'checkbox' />

        );
    }

    /**
     * Renders the main part of this WelcomePage.
     *
     * @private
     * @returns {ReactElement|null}
     */
    _renderMain() {
        return (

                    {
                        this._renderFeatureRow(1, 5)
                    }
                    {
                        this._renderFeatureRow(5, 9)
                    }

        );
    }
}

export default connect(mapStateToProps)(WelcomePage);
<commit_msg>Fix the checkbox to disable the Welcome page<commit_after>/* global $, APP, interfaceConfig */

import React from 'react';
import { connect } from 'react-redux';

import { Watermarks } from '../../base/react';

import { AbstractWelcomePage, mapStateToProps } from './AbstractWelcomePage';

/* eslint-disable require-jsdoc */

/**
 * The Web container rendering the welcome page.
 *
 * @extends AbstractWelcomePage
 */
class WelcomePage extends AbstractWelcomePage {

/* eslint-enable require-jsdoc */

    /**
     * Initializes a new WelcomePage instance.
     *
     * @param {Object} props - The read-only properties with which the new
     * instance is to be initialized.
     */
    constructor(props) {
        super(props);

        this.state = {
            ...this.state,

            enableWelcomePage: true,
            generateRoomnames:
                interfaceConfig.GENERATE_ROOMNAMES_ON_WELCOME_PAGE
        };

        // Bind event handlers so they are only bound once for every instance.
        this._onDisableWelcomeChange = this._onDisableWelcomeChange.bind(this);
        this._onKeyDown = this._onKeyDown.bind(this);
        this._onRoomChange = this._onRoomChange.bind(this);
    }

    /**
     * This method is executed when comonent is mounted.
     *
     * @inheritdoc
     * @returns {void}
     */
    componentDidMount() {
        if (this.state.generateRoomnames) {
            this._updateRoomname();
        }

        // XXX Temporary solution until we add React translation.
        APP.translation.translateElement($('#welcome_page'));
    }

    /**
     * Implements React's {@link Component#render()}.
     *
     * @inheritdoc
     * @returns {ReactElement|null}
     */
    render() {
        return (

                {
                    this._renderHeader()
                }
                {
                    this._renderMain()
                }

        );
    }

    /**
     * Returns the domain name.
     *
     * @private
     * @returns {string} Domain name.
     */
    _getDomain() {
        return `${window.location.protocol}//${window.location.host}/`;
    }

    /**
     * Handles <tt>change</tt> event of the checkbox which allows specifying
     * whether the WelcomePage is disabled.
     *
     * @param {Event} event - The (HTML) Event which details the change such as
     * the EventTarget.
     * @returns {void}
     */
    _onDisableWelcomeChange(event) {
        this.setState({
            enableWelcomePage: !event.target.checked
        }, () => {
            APP.settings.setWelcomePageEnabled(this.state.enableWelcomePage);
        });
    }

    /**
     * Handles 'keydown' event to initiate joining the room when the
     * 'Enter/Return' button is pressed.
     *
     * @param {Event} event - Key down event object.
     * @private
     * @returns {void}
     */
    _onKeyDown(event) {
        if (event.keyCode === /* Enter */ 13) {
            this._onJoin();
        }
    }

    /**
     * Overrides the super to account for the differences in the argument types
     * provided by HTML and React Native text inputs.
     *
     * @inheritdoc
     * @override
     * @param {Event} event - The (HTML) Event which details the change such as
     * the EventTarget.
     * @protected
     */
    _onRoomChange(event) {
        super._onRoomChange(event.target.value);
    }

    /**
     * Renders a feature with a specific index.
     *
     * @param {number} index - The index of the feature to render.
     * @private
     * @returns {ReactElement}
     */
    _renderFeature(index) {
        return (

                <div
                    className = 'feature_icon'
                    data-i18n = { `welcomepage.feature${index}.title` } />
                <div
                    className = 'feature_description'
                    data-i18n = { `welcomepage.feature${index}.content` }
                    data-i18n-options = { JSON.stringify({
                        postProcess: 'resolveAppName'
                    }) } />

        );
    }

    /**
     * Renders a row of features.
     *
     * @param {number} beginIndex - The inclusive feature index to begin the row
     * with.
     * @param {number} endIndex - The exclusive feature index to end the row
     * with.
     * @private
     * @returns {ReactElement}
     */
    _renderFeatureRow(beginIndex, endIndex) {
        const features = [];

        for (let index = beginIndex; index < endIndex; ++index) {
            features.push(this._renderFeature(index));
        }

        return (

                {
                    features
                }

        );
    }

/* eslint-disable require-jsdoc */

    /**
     * Renders the header part of this WelcomePage.
     *
     * @private
     * @returns {ReactElement|null}
     */
    _renderHeader() {

/* eslint-enable require-jsdoc */

        return (

                <Watermarks />

                            {
                                this._getDomain()
                            }

                            <input
                                autoFocus = { true }
                                className = 'enter-room__field'
                                data-room-name
                                    = { this.state.generatedRoomname }
                                id = 'enter_room_field'
                                onChange = { this._onRoomChange }
                                onKeyDown = { this._onKeyDown }
                                placeholder = { this.state.roomPlaceholder }
                                type = 'text'
                                value = { this.state.room } />

                            { /* eslint-disable react/jsx-handler-names */ }

                            { /* eslint-enable react/jsx-handler-names */ }

                            <button
                                className = 'enter-room__button'
                                data-i18n = 'welcomepage.go'
                                id = 'enter_room_button'
                                onClick = { this._onJoin }
                                type = 'button' />

                <input
                    checked = { !this.state.enableWelcomePage }
                    id = 'disable_welcome'
                    name = 'checkbox'
                    onChange = { this._onDisableWelcomeChange }
                    type = 'checkbox' />

        );
    }

    /**
     * Renders the main part of this WelcomePage.
     *
     * @private
     * @returns {ReactElement|null}
     */
    _renderMain() {
        return (

                    {
                        this._renderFeatureRow(1, 5)
                    }
                    {
                        this._renderFeatureRow(5, 9)
                    }

        );
    }
}

export default connect(mapStateToProps)(WelcomePage);
<|endoftext|>


<commit_before>import React from 'react'
import PropTypes from 'prop-types'
import { injectIntl } from 'react-intl'
import styled from 'styled-components'
import { Flex } from 'rebass'
import { Button } from 'components/UI'
import LayoutCards from 'components/Icon/LayoutCards'
import LayoutList from 'components/Icon/LayoutList'
import { CHANNEL_LIST_VIEW_MODE_SUMMARY, CHANNEL_LIST_VIEW_MODE_CARD } from './constants'
import messages from './messages'

const StyledButton = styled(Button)`
  color: ${props => (props.active ? props.theme.colors.lightningOrange : null)};
  &:hover {
    color: ${props => props.theme.colors.lightningOrange};
  }
`
StyledButton.propTypes = {
  active: PropTypes.bool,
}

const CardButton = injectIntl(({ active, onClick, intl, ...rest }) => (
  <StyledButton
    active={active}
    className="hint--bottom-left"
    data-hint={intl.formatMessage({ ...messages.view_mode_card })}
    onClick={onClick}
    size="small"
    variant="secondary"
    {...rest}
  >
    <LayoutCards height="16px" width="16px" />
  </StyledButton>
))

CardButton.propTypes = {
  active: PropTypes.bool,
  onClick: PropTypes.func.isRequired,
}

const ListButton = injectIntl(({ active, onClick, intl, ...rest }) => (
  <StyledButton
    active={active}
    alignSelf="center"
    className="hint--bottom-left"
    data-hint={intl.formatMessage({ ...messages.view_mode_list })}
    onClick={onClick}
    size="small"
    variant="secondary"
    {...rest}
  >
    <LayoutList height="16px" width="16px" />
  </StyledButton>
))

ListButton.propTypes = {
  active: PropTypes.bool,
  onClick: PropTypes.func.isRequired,
}

const ChannelsViewSwitcher = ({ channelViewMode, setChannelViewMode, ...rest }) => (
  <Flex alignItems="center" as="section" {...rest}>
    <CardButton
      active={channelViewMode === CHANNEL_LIST_VIEW_MODE_CARD}
      onClick={() => setChannelViewMode(CHANNEL_LIST_VIEW_MODE_CARD)}
      px={2}
      py={1}
    />
    <ListButton
      active={channelViewMode === CHANNEL_LIST_VIEW_MODE_SUMMARY}
      onClick={() => setChannelViewMode(CHANNEL_LIST_VIEW_MODE_SUMMARY)}
      px={2}
      py={1}
    />
  </Flex>
)

ChannelsViewSwitcher.propTypes = {
  channelViewMode: PropTypes.string.isRequired,
  setChannelViewMode: PropTypes.func.isRequired,
}

export default ChannelsViewSwitcher
<commit_msg>fix(ui): convert channel view mode buttons to a switch<commit_after>import React from 'react'
import PropTypes from 'prop-types'
import { injectIntl } from 'react-intl'
import { SwitchButton } from 'components/UI'
import LayoutCards from 'components/Icon/LayoutCards'
import LayoutList from 'components/Icon/LayoutList'
import { CHANNEL_LIST_VIEW_MODE_SUMMARY, CHANNEL_LIST_VIEW_MODE_CARD } from './constants'
import messages from './messages'

const ChannelsViewSwitcher = injectIntl(
  ({ channelViewMode, setChannelViewMode, intl, ...rest }) => {
    const isCardView = channelViewMode === CHANNEL_LIST_VIEW_MODE_CARD
    //
    const onClick = () => {
      if (isCardView) {
        setChannelViewMode(CHANNEL_LIST_VIEW_MODE_SUMMARY)
      } else {
        setChannelViewMode(CHANNEL_LIST_VIEW_MODE_CARD)
      }
    }
    return (
      <SwitchButton
        {...rest}
        data-hint={intl.formatMessage({
          ...(isCardView ? messages.view_mode_list : messages.view_mode_card),
        })}
        Icon1={LayoutList}
        Icon2={LayoutCards}
        isSwitched={isCardView}
        onClick={onClick}
      />
    )
  }
)

ChannelsViewSwitcher.propTypes = {
  channelViewMode: PropTypes.string.isRequired,
  setChannelViewMode: PropTypes.func.isRequired,
}

export default ChannelsViewSwitcher
<|endoftext|>


<commit_before>const env = require('../config/env');

const { createFilePath } = require('gatsby-source-filesystem');

const { dasherize } = require('../utils/slugs');
const { blockNameify } = require('../utils/block-nameify');
const {
  createChallengePages,
  createBlockIntroPages,
  createSuperBlockIntroPages
} = require('./utils/gatsby');

const createByIdentityMap = {
  blockIntroMarkdown: createBlockIntroPages,
  superBlockIntroMarkdown: createSuperBlockIntroPages
};

exports.onCreateNode = function onCreateNode({ node, actions, getNode }) {
  const { createNodeField } = actions;
  if (node.internal.type === 'ChallengeNode') {
    const { tests = [], block, title, superBlock } = node;
    const slug = `/learn/${dasherize(superBlock)}/${dasherize(
      block
    )}/${dasherize(title)}`;
    createNodeField({ node, name: 'slug', value: slug });
    createNodeField({ node, name: 'blockName', value: blockNameify(block) });
    createNodeField({ node, name: 'tests', value: tests });
  }

  if (node.internal.type === 'MarkdownRemark') {
    const slug = createFilePath({ node, getNode });
    if (!slug.includes('LICENSE')) {
      const {
        frontmatter: { component = '' }
      } = node;
      createNodeField({ node, name: 'slug', value: slug });
      createNodeField({ node, name: 'component', value: component });
    }
  }
};

exports.createPages = function createPages({ graphql, actions, reporter }) {
  if (!env.algoliaAPIKey || !env.algoliaAppId) {
    if (process.env.FREECODECAMP_NODE_ENV === 'production') {
      throw new Error(
        'Algolia App id and API key are required to start the client!'
      );
    } else {
      reporter.info(
        'Algolia keys missing or invalid. Required for search to yield results.'
      );
    }
  }

  if (!env.stripePublicKey) {
    if (process.env.FREECODECAMP_NODE_ENV === 'production') {
      throw new Error('Stripe public key is required to start the client!');
    } else {
      reporter.info(
        'Stripe public key missing or invalid. Required for donations.'
      );
    }
  }
  const { createPage } = actions;

  return new Promise((resolve, reject) => {
    // Query for all markdown 'nodes' and for the slug we previously created.
    resolve(
      graphql(`
        {
          allChallengeNode(
            sort: { fields: [superOrder, order, challengeOrder] }
          ) {
            edges {
              node {
                block
                challengeType
                fields {
                  slug
                }
                id
                order
                required {
                  link
                  src
                }
                challengeOrder
                superBlock
                superOrder
                template
              }
            }
          }
          allMarkdownRemark {
            edges {
              node {
                fields {
                  slug
                  nodeIdentity
                  component
                }
                frontmatter {
                  block
                  superBlock
                  title
                }
                htmlAst
                id
                excerpt
              }
            }
          }
        }
      `).then(result => {
        if (result.errors) {
          console.log(result.errors);
          return reject(result.errors);
        }

        // Create challenge pages.
        result.data.allChallengeNode.edges.forEach(
          createChallengePages(createPage)
        );

        // Create intro pages
        result.data.allMarkdownRemark.edges.forEach(edge => {
          const {
            node: { frontmatter, fields }
          } = edge;

          if (!fields) {
            return null;
          }
          const { slug, nodeIdentity } = fields;
          if (slug.includes('LICENCE')) {
            return null;
          }
          try {
            const pageBuilder = createByIdentityMap[nodeIdentity](createPage);
            return pageBuilder(edge);
          } catch (e) {
            console.log(`
            ident: ${nodeIdentity} does not belong to a function

            ${frontmatter ? JSON.stringify(edge.node) : 'no frontmatter'}

            `);
          }
          return null;
        });

        return null;
      })
    );
  });
};

const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

exports.onCreateWebpackConfig = ({ stage, plugins, actions }) => {
  const newPlugins = [
    plugins.define({
      HOME_PATH: JSON.stringify(
        process.env.HOME_PATH || 'http://localhost:3000'
      ),
      STRIPE_PUBLIC_KEY: JSON.stringify(process.env.STRIPE_PUBLIC_KEY || ''),
      PAYPAL_SUPPORTERS: JSON.stringify(process.env.PAYPAL_SUPPORTERS || 404)
    })
  ];
  // The monaco editor relies on some browser only globals so should not be
  // involved in SSR. Also, if the plugin is used during the 'build-html' stage
  // it overwrites the minfied files with ordinary ones.
  if (stage !== 'build-html') {
    newPlugins.push(new MonacoWebpackPlugin());
  }
  actions.setWebpackConfig({
    node: {
      fs: 'empty'
    },
    plugins: newPlugins
  });
};

exports.onCreateBabelConfig = ({ actions }) => {
  actions.setBabelPlugin({
    name: '@babel/plugin-proposal-function-bind'
  });
  actions.setBabelPlugin({
    name: '@babel/plugin-proposal-export-default-from'
  });
  actions.setBabelPlugin({
    name: 'babel-plugin-transform-imports',
    options: {
      '@freecodecamp/react-bootstrap': {
        transform: '@freecodecamp/react-bootstrap/lib/${member}',
        preventFullImport: true
      },
      lodash: {
        transform: 'lodash/${member}',
        preventFullImport: true
      }
    }
  });
};

exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions;
  // Only update the `/challenges` page.
  if (page.path.match(/^\/challenges/)) {
    // page.matchPath is a special key that's used for matching pages
    // with corresponding routes only on the client.
    page.matchPath = '/challenges/*';
    // Update the page.
    createPage(page);
  }
};

// TODO: this broke the React challenges, not sure why, but I'll investigate
// further and reimplement if it's possible and necessary (Oliver)
// Typically the schema can be inferred, but not when some challenges are
// skipped (at time of writing the Chinese only has responsive web design), so
// this makes the missing fields explicit.
// exports.createSchemaCustomization = ({ actions }) => {
//   const { createTypes } = actions;
//   const typeDefs = `
//     type ChallengeNode implements Node {
//       question: Question
//       videoId: String
//       required: ExternalFile
//       files: ChallengeFile
//     }
//     type Question {
//       text: String
//       answers: [String]
//       solution: Int
//     }
//     type ChallengeFile {
//       indexhtml: FileContents
//       indexjs: FileContents
//       indexjsx: FileContents
//     }
//     type ExternalFile {
//       link: String
//       src: String
//     }
//     type FileContents {
//       key: String
//       ext: String
//       name: String
//       contents: String
//       head: String
//       tail: String
//     }
//   `;
//   createTypes(typeDefs);
// };
<commit_msg>fix(client): set custom schema<commit_after>const env = require('../config/env');

const { createFilePath } = require('gatsby-source-filesystem');

const { dasherize } = require('../utils/slugs');
const { blockNameify } = require('../utils/block-nameify');
const {
  createChallengePages,
  createBlockIntroPages,
  createSuperBlockIntroPages
} = require('./utils/gatsby');

const createByIdentityMap = {
  blockIntroMarkdown: createBlockIntroPages,
  superBlockIntroMarkdown: createSuperBlockIntroPages
};

exports.onCreateNode = function onCreateNode({ node, actions, getNode }) {
  const { createNodeField } = actions;
  if (node.internal.type === 'ChallengeNode') {
    const { tests = [], block, title, superBlock } = node;
    const slug = `/learn/${dasherize(superBlock)}/${dasherize(
      block
    )}/${dasherize(title)}`;
    createNodeField({ node, name: 'slug', value: slug });
    createNodeField({ node, name: 'blockName', value: blockNameify(block) });
    createNodeField({ node, name: 'tests', value: tests });
  }

  if (node.internal.type === 'MarkdownRemark') {
    const slug = createFilePath({ node, getNode });
    if (!slug.includes('LICENSE')) {
      const {
        frontmatter: { component = '' }
      } = node;
      createNodeField({ node, name: 'slug', value: slug });
      createNodeField({ node, name: 'component', value: component });
    }
  }
};

exports.createPages = function createPages({ graphql, actions, reporter }) {
  if (!env.algoliaAPIKey || !env.algoliaAppId) {
    if (process.env.FREECODECAMP_NODE_ENV === 'production') {
      throw new Error(
        'Algolia App id and API key are required to start the client!'
      );
    } else {
      reporter.info(
        'Algolia keys missing or invalid. Required for search to yield results.'
      );
    }
  }

  if (!env.stripePublicKey) {
    if (process.env.FREECODECAMP_NODE_ENV === 'production') {
      throw new Error('Stripe public key is required to start the client!');
    } else {
      reporter.info(
        'Stripe public key missing or invalid. Required for donations.'
      );
    }
  }
  const { createPage } = actions;

  return new Promise((resolve, reject) => {
    // Query for all markdown 'nodes' and for the slug we previously created.
    resolve(
      graphql(`
        {
          allChallengeNode(
            sort: { fields: [superOrder, order, challengeOrder] }
          ) {
            edges {
              node {
                block
                challengeType
                fields {
                  slug
                }
                id
                order
                required {
                  link
                  src
                }
                challengeOrder
                superBlock
                superOrder
                template
              }
            }
          }
          allMarkdownRemark {
            edges {
              node {
                fields {
                  slug
                  nodeIdentity
                  component
                }
                frontmatter {
                  block
                  superBlock
                  title
                }
                htmlAst
                id
                excerpt
              }
            }
          }
        }
      `).then(result => {
        if (result.errors) {
          console.log(result.errors);
          return reject(result.errors);
        }

        // Create challenge pages.
        result.data.allChallengeNode.edges.forEach(
          createChallengePages(createPage)
        );

        // Create intro pages
        result.data.allMarkdownRemark.edges.forEach(edge => {
          const {
            node: { frontmatter, fields }
          } = edge;

          if (!fields) {
            return null;
          }
          const { slug, nodeIdentity } = fields;
          if (slug.includes('LICENCE')) {
            return null;
          }
          try {
            const pageBuilder = createByIdentityMap[nodeIdentity](createPage);
            return pageBuilder(edge);
          } catch (e) {
            console.log(`
            ident: ${nodeIdentity} does not belong to a function

            ${frontmatter ? JSON.stringify(edge.node) : 'no frontmatter'}

            `);
          }
          return null;
        });

        return null;
      })
    );
  });
};

const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');

exports.onCreateWebpackConfig = ({ stage, plugins, actions }) => {
  const newPlugins = [
    plugins.define({
      HOME_PATH: JSON.stringify(
        process.env.HOME_PATH || 'http://localhost:3000'
      ),
      STRIPE_PUBLIC_KEY: JSON.stringify(process.env.STRIPE_PUBLIC_KEY || ''),
      PAYPAL_SUPPORTERS: JSON.stringify(process.env.PAYPAL_SUPPORTERS || 404)
    })
  ];
  // The monaco editor relies on some browser only globals so should not be
  // involved in SSR. Also, if the plugin is used during the 'build-html' stage
  // it overwrites the minfied files with ordinary ones.
  if (stage !== 'build-html') {
    newPlugins.push(new MonacoWebpackPlugin());
  }
  actions.setWebpackConfig({
    node: {
      fs: 'empty'
    },
    plugins: newPlugins
  });
};

exports.onCreateBabelConfig = ({ actions }) => {
  actions.setBabelPlugin({
    name: '@babel/plugin-proposal-function-bind'
  });
  actions.setBabelPlugin({
    name: '@babel/plugin-proposal-export-default-from'
  });
  actions.setBabelPlugin({
    name: 'babel-plugin-transform-imports',
    options: {
      '@freecodecamp/react-bootstrap': {
        transform: '@freecodecamp/react-bootstrap/lib/${member}',
        preventFullImport: true
      },
      lodash: {
        transform: 'lodash/${member}',
        preventFullImport: true
      }
    }
  });
};

exports.onCreatePage = async ({ page, actions }) => {
  const { createPage } = actions;
  // Only update the `/challenges` page.
  if (page.path.match(/^\/challenges/)) {
    // page.matchPath is a special key that's used for matching pages
    // with corresponding routes only on the client.
    page.matchPath = '/challenges/*';
    // Update the page.
    createPage(page);
  }
};

exports.createSchemaCustomization = ({ actions }) => {
  const { createTypes } = actions;
  const typeDefs = `
    type ChallengeNode implements Node {
      files: ChallengeFile
    }
    type ChallengeFile {
      indexcss: FileContents
      indexhtml: FileContents
      indexjs: FileContents
      indexjsx: FileContents
    }
    type FileContents {
      key: String
      ext: String
      name: String
      contents: String
      head: String
      tail: String
      editableRegionBoundaries: [Int]
    }
  `;
  createTypes(typeDefs);
};

// TODO: this broke the React challenges, not sure why, but I'll investigate
// further and reimplement if it's possible and necessary (Oliver)
// I'm still not sure why, but the above schema seems to work.
// Typically the schema can be inferred, but not when some challenges are
// skipped (at time of writing the Chinese only has responsive web design), so
// this makes the missing fields explicit.
// exports.createSchemaCustomization = ({ actions }) => {
//   const { createTypes } = actions;
//   const typeDefs = `
//     type ChallengeNode implements Node {
//       question: Question
//       videoId: String
//       required: ExternalFile
//       files: ChallengeFile
//     }
//     type Question {
//       text: String
//       answers: [String]
//       solution: Int
//     }
//     type ChallengeFile {
//       indexhtml: FileContents
//       indexjs: FileContents
//       indexjsx: FileContents
//     }
//     type ExternalFile {
//       link: String
//       src: String
//     }
//     type FileContents {
//       key: String
//       ext: String
//       name: String
//       contents: String
//       head: String
//       tail: String
//     }
//   `;
//   createTypes(typeDefs);
// };
<|endoftext|>


<commit_before>goog.provide('ojster');

goog.require('goog.string');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');

/**
 * @constructor
 */
ojster.StringWriter = function () {
  this.buff = [];
};

ojster.StringWriter.prototype.write = function () {
  this.buff.push.apply(this.buff, arguments);
};

/**
 * @return {string}
 */
ojster.StringWriter.prototype.done = function () {
  return this.buff.join('');
};

ojster.DefaultWriterClass = ojster.StringWriter;

/**
 * @param {Object=} opt_data
 * @param {Object=} opt_ctx
 * @param {Object=} opt_writer
 * @constructor
 */
ojster.Template = function (opt_data, opt_ctx, opt_writer) {
  /** @type {Object} */
  this.data = opt_data || null;
  /** @type {Object} */
  this.ctx = opt_ctx || null;
  /** @type {Object} */
  this.writer = opt_writer || null;
  /** @type {!Object} */
  this.vars = {};
  /** @type {?string} */
  this.baseCssName = null;

  this.init();
};

ojster.Template.prototype.init = function () {
};

/**
 * @param {function (ojster.Template)} props
 */
ojster.Template.prototype.setup = function (setupFunc) {
  setupFunc.call(this);
  return this;
};

/**
 * @return {?string}
 */
ojster.Template.prototype.getBaseCssName = function (setupFunc) {
  return this.baseCssName;
};

/**
 * @param {?string} baseCssName
 */
ojster.Template.prototype.setBaseCssName = function (baseCssName) {
  this.baseCssName = baseCssName;
};

/**
 * @param {?string} str
 * @return {?string}
 */
ojster.Template.prototype.escape = function (str) {
  return ojster.escape(str);
};

/**
 * @return {string}
 */
ojster.Template.prototype.render = function () {
  // ensure we have a writer
  if (this.writer == null) {
    this.writer = new ojster.DefaultWriterClass();
  }

  // render
  this.renderBlockMain();
  return this.writer.done();
};

/**
 * @param {ojster.Template} template
 */
ojster.Template.prototype.renderTo = function (template) {
  this.writer = template.writer;
  this.renderBlockMain();
};

ojster.Template.prototype.renderBlockMain = function () {
  throw new Error('Not implemented');
};

// functions

/**
 * @param {?string} str
 * @param {boolean=} opt_isLikelyToContainHtmlChars Don't perform a check to see
 *     if the character needs replacing - use this option if you expect each of
 *     the characters to appear often. Leave false if you expect few html
 *     characters to occur in your strings, such as if you are escaping HTML.
 * @return {?string}
 */
ojster.escape = function (str, opt_isLikelyToContainHtmlChars) {
  if (str != null) {
    str = goog.string.htmlEscape(str, opt_isLikelyToContainHtmlChars);
  }
  return str;
};

/**
 * @param {Element} element
 * @param {ojster.Template} template
 * @return {Element}
 */
ojster.fillElement = function (element, template) {
  element.innerHTML = template.render();
  return element;
};

/**
 * @param {ojster.Template} template
 * @param {goog.dom.DomHelper=} opt_domHelper
 * @return {Element}
 */
ojster.createElement = function (template, opt_domHelper) {
  /** @type {goog.dom.DomHelper} */
  var dom = opt_domHelper || goog.dom.getDomHelper();
  /** @type {Element} */
  var wrapper = dom.createElement(goog.dom.TagName.DIV);
  wrapper.innerHTML = template.render();

  if (wrapper.childNodes.length == 1) {
    /** @type {Element} */
    var firstChild = wrapper.firstChild;

    if (firstChild.nodeType == goog.dom.NodeType.ELEMENT) {
      return firstChild;
    }
  }

  return wrapper;
};

/**
 * @param {ojster.Template} template
 * @param {goog.dom.DomHelper=} opt_domHelper
 * @return {Node}
 */
ojster.createFragment = function (template, opt_domHelper) {
  var dom = opt_domHelper || goog.dom.getDomHelper();
  return dom.htmlToDocumentFragment(template.render());
};
<commit_msg>goog client updated<commit_after>goog.provide('ojster');

goog.require('goog.string');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');

/**
 * @constructor
 */
ojster.StringWriter = function () {
  this.buff = [];
};

ojster.StringWriter.prototype.write = function () {
  this.buff.push.apply(this.buff, arguments);
};

/**
 * @return {string}
 */
ojster.StringWriter.prototype.done = function () {
  return this.buff.join('');
};

/**
 * @param {Object=} opt_data
 * @param {Object=} opt_ctx
 * @param {ojster.StringWriter=} opt_writer
 * @constructor
 */
ojster.Template = function (opt_data, opt_ctx, opt_writer) {
  /** @type {Object} */
  this.data = opt_data || null;
  /** @type {Object} */
  this.ctx = opt_ctx || null;
  /** @type {Object} */
  this.writer = opt_writer || null;
  /** @type {!Object} */
  this.vars = {};
  /** @type {?string} */
  this.baseCssName = null;

  this.init();
};

ojster.Template.prototype.init = function () {
};

/**
 * @param {function (ojster.Template)} setupFunc
 */
ojster.Template.prototype.setup = function (setupFunc) {
  setupFunc.call(this);
  return this;
};

/**
 * @return {?string}
 */
ojster.Template.prototype.getBaseCssName = function (setupFunc) {
  return this.baseCssName;
};

/**
 * @param {?string} baseCssName
 */
ojster.Template.prototype.setBaseCssName = function (baseCssName) {
  this.baseCssName = baseCssName;
};

/**
 * @param {?string} str
 * @return {?string}
 */
ojster.Template.prototype.escape = function (str) {
  return ojster.escape(str);
};

/**
 * @return {ojster.StringWriter}
 */
Template.prototype.createWriter = function () {
  return new StringWriter();
};

/**
 * @return {string}
 */
ojster.Template.prototype.render = function () {
  // ensure we have a writer
  if (this.writer == null) {
    this.writer = this.createWriter();
  }

  // render
  this.renderBlockMain();
  return this.writer.done();
};

/**
 * @param {ojster.Template} template
 */
ojster.Template.prototype.renderTo = function (template) {
  this.writer = template.writer;
  this.renderBlockMain();
};

ojster.Template.prototype.renderBlockMain = function () {
  throw new Error('Not implemented');
};

// functions

/**
 * @param {?string} str
 * @param {boolean=} opt_isLikelyToContainHtmlChars Don't perform a check to see
 *     if the character needs replacing - use this option if you expect each of
 *     the characters to appear often. Leave false if you expect few html
 *     characters to occur in your strings, such as if you are escaping HTML.
 * @return {?string}
 */
ojster.escape = function (str, opt_isLikelyToContainHtmlChars) {
  if (str != null) {
    str = goog.string.htmlEscape(str, opt_isLikelyToContainHtmlChars);
  }
  return str;
};

/**
 * @param {Element} element
 * @param {ojster.Template} template
 * @return {Element}
 */
ojster.fillElement = function (element, template) {
  element.innerHTML = template.render();
  return element;
};

/**
 * @param {ojster.Template} template
 * @param {goog.dom.DomHelper=} opt_domHelper
 * @return {Element}
 */
ojster.createElement = function (template, opt_domHelper) {
  /** @type {goog.dom.DomHelper} */
  var dom = opt_domHelper || goog.dom.getDomHelper();
  /** @type {Element} */
  var wrapper = dom.createElement(goog.dom.TagName.DIV);
  wrapper.innerHTML = template.render();

  if (wrapper.childNodes.length == 1) {
    /** @type {Element} */
    var firstChild = wrapper.firstChild;

    if (firstChild.nodeType == goog.dom.NodeType.ELEMENT) {
      return firstChild;
    }
  }

  return wrapper;
};

/**
 * @param {ojster.Template} template
 * @param {goog.dom.DomHelper=} opt_domHelper
 * @return {Node}
 */
ojster.createFragment = function (template, opt_domHelper) {
  var dom = opt_domHelper || goog.dom.getDomHelper();
  return dom.htmlToDocumentFragment(template.render());
};
<|endoftext|>