diff --git a/lang/main.json b/lang/main.json index f8424a5c06584..712dbd7d6e789 100644 --- a/lang/main.json +++ b/lang/main.json @@ -440,6 +440,19 @@ "streamIdHelp": "Where do I find this?", "unavailableTitle": "Live Streaming unavailable" }, + "videoSIPGW": + { + "busy": "We're working on freeing resources. Please try again in a few minutes.", + "busyTitle": "The Room service is currently busy", + "errorInvite": "Conference not established yet. Please try again later.", + "errorInviteTitle": "Error inviting room", + "errorAlreadyInvited": "__displayName__ already invited", + "errorInviteFailedTitle": "Inviting __displayName__ failed", + "errorInviteFailed": "We're working on resolving the issue. Please try again later.", + "pending": "__displayName__ has been invited", + "serviceName": "Room service", + "unavailableTitle": "Room service unavailable" + }, "speakerStats": { "hours": "__count__h", diff --git a/react/features/base/lib-jitsi-meet/index.js b/react/features/base/lib-jitsi-meet/index.js index a5c6322490781..f0c71bea1df4f 100644 --- a/react/features/base/lib-jitsi-meet/index.js +++ b/react/features/base/lib-jitsi-meet/index.js @@ -16,6 +16,7 @@ export const JitsiMediaDevicesEvents = JitsiMeetJS.events.mediaDevices; export const JitsiParticipantConnectionStatus = JitsiMeetJS.constants.participantConnectionStatus; export const JitsiRecordingStatus = JitsiMeetJS.constants.recordingStatus; +export const JitsiSIPVideoGWStatus = JitsiMeetJS.constants.sipVideoGW; export const JitsiTrackErrors = JitsiMeetJS.errors.track; export const JitsiTrackEvents = JitsiMeetJS.events.track; diff --git a/react/features/invite/components/AddPeopleDialog.web.js b/react/features/invite/components/AddPeopleDialog.web.js index 1769560e5b657..c8e681a0a5338 100644 --- a/react/features/invite/components/AddPeopleDialog.web.js +++ b/react/features/invite/components/AddPeopleDialog.web.js @@ -12,7 +12,8 @@ import { Dialog, hideDialog } from '../../base/dialog'; import { translate } from '../../base/i18n'; import { MultiSelectAutocomplete } from '../../base/react'; -import { invitePeople, inviteRooms, searchPeople } from '../functions'; +import { invitePeople, searchPeople } from '../functions'; +import { inviteRooms } from '../../videosipgw'; declare var interfaceConfig: Object; @@ -62,6 +63,11 @@ class AddPeopleDialog extends Component<*, *> { */ hideDialog: PropTypes.func, + /** + * Used to invite video rooms. + */ + inviteRooms: PropTypes.func, + /** * Invoked to obtain translated strings. */ @@ -215,10 +221,10 @@ class AddPeopleDialog extends Component<*, *> { }); this.props._conference - && inviteRooms( - this.props._conference, - this.state.inviteItems.filter( - i => i.type === 'videosipgw')); + && this.props.inviteRooms( + this.props._conference, + this.state.inviteItems.filter( + i => i.type === 'videosipgw')); invitePeople( this.props._inviteServiceUrl, @@ -356,5 +362,7 @@ function _mapStateToProps(state) { }; } -export default translate(connect(_mapStateToProps, { hideDialog })( +export default translate(connect(_mapStateToProps, { + hideDialog, + inviteRooms })( AddPeopleDialog)); diff --git a/react/features/invite/functions.js b/react/features/invite/functions.js index da0db79899631..306221cf5d31d 100644 --- a/react/features/invite/functions.js +++ b/react/features/invite/functions.js @@ -48,33 +48,6 @@ export function invitePeople( // eslint-disable-line max-params }); } -/** - * Invites room participants to the conference through the SIP Jibri service. - * - * @param {JitsiMeetConference} conference - The conference to which the rooms - * will be invited to. - * @param {Immutable.List} rooms - The list of the "videosipgw" type items to - * invite. - * @returns {void} - */ -export function inviteRooms( - conference: { createVideoSIPGWSession: Function }, - rooms: Object) { - for (const room of rooms) { - const { id: sipAddress, name: displayName } = room; - - if (sipAddress && displayName) { - const newSession - = conference.createVideoSIPGWSession(sipAddress, displayName); - - newSession.start(); - } else { - console.error( - `No display name or sip number for ${JSON.stringify(room)}`); - } - } -} - /** * Indicates if an invite option is enabled in the configuration. * diff --git a/react/features/videosipgw/actionTypes.js b/react/features/videosipgw/actionTypes.js new file mode 100644 index 0000000000000..60b1d62ef81ae --- /dev/null +++ b/react/features/videosipgw/actionTypes.js @@ -0,0 +1,23 @@ +/** + * The type of (redux) action which signals that sip GW service change its + * availability status. + * + * { + * type: SIP_GW_AVAILABILITY_CHANGED, + * status: string + * } + */ +export const SIP_GW_AVAILABILITY_CHANGED + = Symbol('SIP_GW_AVAILABILITY_CHANGED'); + +/** + * The type of the action which signals to invite room participants to the + * conference through the SIP Jibri service. + * + * { + * type: SIP_GW_INVITE_ROOMS, + * conference: JitsiConference, + * rooms: {Immutable.List} + * } + */ +export const SIP_GW_INVITE_ROOMS = Symbol('SIP_GW_INVITE_ROOMS'); diff --git a/react/features/videosipgw/actions.js b/react/features/videosipgw/actions.js new file mode 100644 index 0000000000000..0dd841265154d --- /dev/null +++ b/react/features/videosipgw/actions.js @@ -0,0 +1,22 @@ +/* @flow */ + +import { SIP_GW_INVITE_ROOMS } from './actionTypes'; + +/** + * Invites room participants to the conference through the SIP Jibri service. + * + * @param {JitsiMeetConference} conference - The conference to which the rooms + * will be invited to. + * @param {Immutable.List} rooms - The list of the "videosipgw" type items to + * invite. + * @returns {void} + */ +export function inviteRooms( + conference: Object, + rooms: Object) { + return { + type: SIP_GW_INVITE_ROOMS, + conference, + rooms + }; +} diff --git a/react/features/videosipgw/index.js b/react/features/videosipgw/index.js new file mode 100644 index 0000000000000..dbf9c8839e123 --- /dev/null +++ b/react/features/videosipgw/index.js @@ -0,0 +1,4 @@ +export * from './actions'; + +import './middleware'; +import './reducer'; diff --git a/react/features/videosipgw/middleware.js b/react/features/videosipgw/middleware.js new file mode 100644 index 0000000000000..3bd0d32fcc110 --- /dev/null +++ b/react/features/videosipgw/middleware.js @@ -0,0 +1,182 @@ +/* @flow */ + +import Logger from 'jitsi-meet-logger'; +import { CONFERENCE_WILL_JOIN } from '../base/conference'; +import { + SIP_GW_AVAILABILITY_CHANGED, + SIP_GW_INVITE_ROOMS +} from './actionTypes'; +import { + JitsiConferenceEvents, + JitsiSIPVideoGWStatus +} from '../base/lib-jitsi-meet'; +import { MiddlewareRegistry } from '../base/redux'; +import { + Notification, + showErrorNotification, + showNotification, + showWarningNotification +} from '../notifications'; + +const logger = Logger.getLogger(__filename); + +/** + * Middleware that captures conference video sip gw events and stores + * the global sip gw availability in redux or show appropriate notification + * for sip gw sessions. + * Captures invitation actions that create sip gw sessions or display + * appropriate error/warning notifications. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { + const result = next(action); + + switch (action.type) { + case CONFERENCE_WILL_JOIN: { + const conference = getState()['features/base/conference'].joining; + + conference.on( + JitsiConferenceEvents.VIDEO_SIP_GW_AVAILABILITY_CHANGED, + (...args) => dispatch(_availabilityChanged(...args))); + conference.on( + JitsiConferenceEvents.VIDEO_SIP_GW_SESSION_STATE_CHANGED, + event => { + const toDispatch = _sessionStateChanged(event); + + // sessionStateChanged can decide there is nothing to dispatch + if (toDispatch) { + dispatch(toDispatch); + } + }); + + break; + } + case SIP_GW_INVITE_ROOMS: { + const { status } = getState()['features/videosipgw']; + + if (status === JitsiSIPVideoGWStatus.STATUS_UNDEFINED) { + dispatch(showErrorNotification({ + descriptionKey: 'recording.unavailable', + descriptionArguments: { + serviceName: '$t(videoSIPGW.serviceName)' + }, + titleKey: 'videoSIPGW.unavailableTitle' + })); + + return; + } else if (status === JitsiSIPVideoGWStatus.STATUS_BUSY) { + dispatch(showWarningNotification({ + descriptionKey: 'videoSIPGW.busy', + titleKey: 'videoSIPGW.busyTitle' + })); + + return; + } else if (status !== JitsiSIPVideoGWStatus.STATUS_AVAILABLE) { + logger.error(`Unknown sip videogw status ${status}`); + + return; + } + + for (const room of action.rooms) { + const { id: sipAddress, name: displayName } = room; + + if (sipAddress && displayName) { + const newSession = action.conference + .createVideoSIPGWSession(sipAddress, displayName); + + if (newSession instanceof Error) { + const e = newSession; + + if (e) { + switch (e.message) { + case JitsiSIPVideoGWStatus.ERROR_NO_CONNECTION: { + dispatch(showErrorNotification({ + descriptionKey: 'videoSIPGW.errorInvite', + titleKey: 'videoSIPGW.errorInviteTitle' + })); + + return; + } + case JitsiSIPVideoGWStatus.ERROR_SESSION_EXISTS: { + dispatch(showWarningNotification({ + titleKey: 'videoSIPGW.errorAlreadyInvited', + titleArguments: { displayName } + })); + + return; + } + } + } + logger.error( + 'Unknown error trying to create sip videogw session', + e); + + return; + } + + newSession.start(); + } else { + logger.error(`No display name or sip number for ${ + JSON.stringify(room)}`); + } + } + } + } + + return result; +}); + +/** + * Signals that sip gw availability had changed. + * + * @param {string} status - The new status of the service. + * @returns {{ + * type: SIP_GW_AVAILABILITY_CHANGED, + * status: string + * }} + * @private + */ +function _availabilityChanged(status: string) { + return { + type: SIP_GW_AVAILABILITY_CHANGED, + status + }; +} + +/** + * Signals that a session we created has a change in its status. + * + * @param {string} event - The event describing the session state change. + * @returns {{ + * type: SHOW_NOTIFICATION + * }}|null + * @private + */ +function _sessionStateChanged( + event: Object) { + switch (event.newState) { + case JitsiSIPVideoGWStatus.STATE_PENDING: { + return showNotification( + Notification, { + titleKey: 'videoSIPGW.pending', + titleArguments: { + displayName: event.displayName + } + }, 2000); + } + case JitsiSIPVideoGWStatus.STATE_FAILED: { + return showErrorNotification({ + titleKey: 'videoSIPGW.errorInviteFailedTitle', + titleArguments: { + displayName: event.displayName + }, + descriptionKey: 'videoSIPGW.errorInviteFailed' + }); + } + } + + // nothing to show + return null; +} diff --git a/react/features/videosipgw/reducer.js b/react/features/videosipgw/reducer.js new file mode 100644 index 0000000000000..9f3606fd16b79 --- /dev/null +++ b/react/features/videosipgw/reducer.js @@ -0,0 +1,17 @@ +import { ReducerRegistry } from '../base/redux'; + +import { SIP_GW_AVAILABILITY_CHANGED } from './actionTypes'; + +ReducerRegistry.register( + 'features/videosipgw', (state = [], action) => { + switch (action.type) { + case SIP_GW_AVAILABILITY_CHANGED: { + return { + ...state, + status: action.status + }; + } + } + + return state; + });