diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index 42818244b3..e5eb9d70c8 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -4,7 +4,6 @@ src/autocomplete/AutocompleteProvider.js src/autocomplete/Autocompleter.js src/autocomplete/EmojiProvider.js src/autocomplete/UserProvider.js -src/CallHandler.js src/component-index.js src/components/structures/BottomLeftMenu.js src/components/structures/CompatibilityPage.js diff --git a/src/CallHandler.js b/src/CallHandler.js index fd56d7f1b1..a65d82fe85 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 New Vector Ltd +Copyright 2017, 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -60,6 +60,7 @@ import { _t } from './languageHandler'; import Matrix from 'matrix-js-sdk'; import dis from './dispatcher'; import { showUnknownDeviceDialogForCalls } from './cryptodevices'; +import SettingsStore from "./settings/SettingsStore"; global.mxCalls = { //room_id: MatrixCall @@ -123,7 +124,7 @@ function _setCallListeners(call) { description: _t( "There are unknown devices in this room: "+ "if you proceed without verifying them, it will be "+ - "possible for someone to eavesdrop on your call." + "possible for someone to eavesdrop on your call.", ), button: _t('Review Devices'), onFinished: function(confirmed) { @@ -246,66 +247,58 @@ function _onAction(payload) { switch (payload.action) { case 'place_call': - if (module.exports.getAnyActiveCall()) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { - title: _t('Existing Call'), - description: _t('You are already in a call.'), - }); - return; // don't allow >1 call to be placed. - } + { + if (module.exports.getAnyActiveCall()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { + title: _t('Existing Call'), + description: _t('You are already in a call.'), + }); + return; // don't allow >1 call to be placed. + } - // if the runtime env doesn't do VoIP, whine. - if (!MatrixClientPeg.get().supportsVoip()) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { - title: _t('VoIP is unsupported'), - description: _t('You cannot place VoIP calls in this browser.'), - }); - return; - } + // if the runtime env doesn't do VoIP, whine. + if (!MatrixClientPeg.get().supportsVoip()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { + title: _t('VoIP is unsupported'), + description: _t('You cannot place VoIP calls in this browser.'), + }); + return; + } - var room = MatrixClientPeg.get().getRoom(payload.room_id); - if (!room) { - console.error("Room %s does not exist.", payload.room_id); - return; - } + const room = MatrixClientPeg.get().getRoom(payload.room_id); + if (!room) { + console.error("Room %s does not exist.", payload.room_id); + return; + } - var members = room.getJoinedMembers(); - if (members.length <= 1) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, { - description: _t('You cannot place a call with yourself.'), - }); - return; - } else if (members.length === 2) { - console.log("Place %s call in %s", payload.type, payload.room_id); - const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id); - placeCall(call); - } else { // > 2 - dis.dispatch({ - action: "place_conference_call", - room_id: payload.room_id, - type: payload.type, - remote_element: payload.remote_element, - local_element: payload.local_element, - }); + const members = room.getJoinedMembers(); + if (members.length <= 1) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, { + description: _t('You cannot place a call with yourself.'), + }); + return; + } else if (members.length === 2) { + console.log("Place %s call in %s", payload.type, payload.room_id); + const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id); + placeCall(call); + } else { // > 2 + dis.dispatch({ + action: "place_conference_call", + room_id: payload.room_id, + type: payload.type, + remote_element: payload.remote_element, + local_element: payload.local_element, + }); + } } break; case 'place_conference_call': console.log("Place conference call in %s", payload.room_id); - if (!ConferenceHandler) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, { - description: _t('Conference calls are not supported in this client'), - }); - } else if (!MatrixClientPeg.get().supportsVoip()) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { - title: _t('VoIP is unsupported'), - description: _t('You cannot place VoIP calls in this browser.'), - }); - } else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) { + + if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) { // Conference calls are implemented by sending the media to central // server which combines the audio from all the participants together // into a single stream. This is incompatible with end-to-end encryption @@ -316,47 +309,75 @@ function _onAction(payload) { Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, { description: _t('Conference calls are not supported in encrypted rooms'), }); + return; + } + + if (SettingsStore.isFeatureEnabled('feature_jitsi')) { + _startCallApp(payload.room_id, payload.type); } else { - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, { - title: _t('Warning!'), - description: _t('Conference calling is in development and may not be reliable.'), - onFinished: (confirm)=>{ - if (confirm) { - ConferenceHandler.createNewMatrixCall( - MatrixClientPeg.get(), payload.room_id, - ).done(function(call) { - placeCall(call); - }, function(err) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - console.error("Conference call failed: " + err); - Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, { - title: _t('Failed to set up conference call'), - description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''), + if (!ConferenceHandler) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, { + description: _t('Conference calls are not supported in this client'), + }); + } else if (!MatrixClientPeg.get().supportsVoip()) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, { + title: _t('VoIP is unsupported'), + description: _t('You cannot place VoIP calls in this browser.'), + }); + } else { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, { + title: _t('Warning!'), + description: _t('Conference calling is in development and may not be reliable.'), + onFinished: (confirm)=>{ + if (confirm) { + ConferenceHandler.createNewMatrixCall( + MatrixClientPeg.get(), payload.room_id, + ).done(function(call) { + placeCall(call); + }, function(err) { + const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); + console.error("Conference call failed: " + err); + Modal.createTrackedDialog( + 'Call Handler', + 'Failed to set up conference call', + ErrorDialog, + { + title: _t('Failed to set up conference call'), + description: ( + _t('Conference call failed.') + + ' ' + ((err && err.message) ? err.message : '') + ), + }, + ); }); - }); - } - }, - }); + } + }, + }); + } } break; case 'incoming_call': - if (module.exports.getAnyActiveCall()) { - // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup. - // we avoid rejecting with "busy" in case the user wants to answer it on a different device. - // in future we could signal a "local busy" as a warning to the caller. - // see https://github.com/vector-im/vector-web/issues/1964 - return; - } + { + if (module.exports.getAnyActiveCall()) { + // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup. + // we avoid rejecting with "busy" in case the user wants to answer it on a different device. + // in future we could signal a "local busy" as a warning to the caller. + // see https://github.com/vector-im/vector-web/issues/1964 + return; + } - // if the runtime env doesn't do VoIP, stop here. - if (!MatrixClientPeg.get().supportsVoip()) { - return; - } + // if the runtime env doesn't do VoIP, stop here. + if (!MatrixClientPeg.get().supportsVoip()) { + return; + } - var call = payload.call; - _setCallListeners(call); - _setCallState(call, call.roomId, "ringing"); + const call = payload.call; + _setCallListeners(call); + _setCallState(call, call.roomId, "ringing"); + } break; case 'hangup': if (!calls[payload.room_id]) { @@ -378,6 +399,71 @@ function _onAction(payload) { break; } } + +function _startCallApp(roomId, type) { + dis.dispatch({ + action: 'appsDrawer', + show: true, + }); + + const room = MatrixClientPeg.get().getRoom(roomId); + if (!room) { + console.error("Attempted to start conference call widget in unknown room: " + roomId); + return; + } + + const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets'); + const currentJitsiWidgets = appsStateEvents.filter((ev) => { + ev.getContent().type == 'jitsi'; + }); + if (currentJitsiWidgets.length > 0) { + console.warn( + "Refusing to start conference call widget in " + roomId + + " a conference call widget is already present", + ); + return; + } + + // This inherits its poor naming from the field of the same name that goes into + // the event. It's just a random string to make the Jitsi URLs unique. + const widgetSessionId = Math.random().toString(36).substring(2); + const confId = room.roomId.replace(/[^A-Za-z0-9]/g, '') + widgetSessionId; + // NB. we can't just encodeURICompoent all of these because the $ signs need to be there + // (but currently the only thing that needs encoding is the confId) + const queryString = [ + 'confId='+encodeURIComponent(confId), + 'isAudioConf='+(type === 'voice' ? 'true' : 'false'), + 'displayName=$matrix_display_name', + 'avatarUrl=$matrix_avatar_url', + 'email=$matrix_user_id', + ].join('&'); + const widgetUrl = ( + 'https://scalar.vector.im/api/widgets' + + '/jitsi.html?' + + queryString + ); + + const jitsiEvent = { + type: 'jitsi', + url: widgetUrl, + data: { + widgetSessionId: widgetSessionId, + }, + }; + const widgetId = ( + 'jitsi_' + + MatrixClientPeg.get().credentials.userId + + '_' + + Date.now() + ); + MatrixClientPeg.get().sendStateEvent( + roomId, + 'im.vector.modular.widgets', + jitsiEvent, + widgetId, + ).then(() => console.log('Sent jitsi widget state event'), (e) => console.error(e)); +} + // FIXME: Nasty way of making sure we only register // with the dispatcher once if (!global.mxCallHandler) { diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js index 8763ea3d7f..f0b7eaa1d7 100644 --- a/src/components/views/rooms/AppsDrawer.js +++ b/src/components/views/rooms/AppsDrawer.js @@ -94,15 +94,7 @@ module.exports = React.createClass({ const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer'; switch (action.action) { case 'appsDrawer': - // When opening the app drawer when there aren't any apps, - // auto-launch the integrations manager to skip the awkward - // click on "Add widget" if (action.show) { - const apps = this._getApps(); - if (apps.length === 0) { - this._launchManageIntegrations(); - } - localStorage.removeItem(hideWidgetKey); } else { // Store hidden state of widget diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js index 28a90b375a..bac996e65c 100644 --- a/src/components/views/rooms/MessageComposer.js +++ b/src/components/views/rooms/MessageComposer.js @@ -159,54 +159,20 @@ export default class MessageComposer extends React.Component { }); } - // _startCallApp(isAudioConf) { - // dis.dispatch({ - // action: 'appsDrawer', - // show: true, - // }); - - // const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', ''); - // let appsStateEvent = {}; - // if (appsStateEvents) { - // appsStateEvent = appsStateEvents.getContent(); - // } - // if (!appsStateEvent.videoConf) { - // appsStateEvent.videoConf = { - // type: 'jitsi', - // // FIXME -- This should not be localhost - // url: 'http://localhost:8000/jitsi.html', - // data: { - // confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(), - // isAudioConf: isAudioConf, - // }, - // }; - // MatrixClientPeg.get().sendStateEvent( - // this.props.room.roomId, - // 'im.vector.modular.widgets', - // appsStateEvent, - // '', - // ).then(() => console.log('Sent state'), (e) => console.error(e)); - // } - // } - onCallClick(ev) { - // NOTE -- Will be replaced by Jitsi code (currently commented) dis.dispatch({ action: 'place_call', type: ev.shiftKey ? "screensharing" : "video", room_id: this.props.room.roomId, }); - // this._startCallApp(false); } onVoiceCallClick(ev) { - // NOTE -- Will be replaced by Jitsi code (currently commented) dis.dispatch({ action: 'place_call', type: "voice", room_id: this.props.room.roomId, }); - // this._startCallApp(true); } onInputContentChanged(content: string, selection: {start: number, end: number}) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6d39586c0a..da47140c96 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -33,8 +33,8 @@ "VoIP is unsupported": "VoIP is unsupported", "You cannot place VoIP calls in this browser.": "You cannot place VoIP calls in this browser.", "You cannot place a call with yourself.": "You cannot place a call with yourself.", - "Conference calls are not supported in this client": "Conference calls are not supported in this client", "Conference calls are not supported in encrypted rooms": "Conference calls are not supported in encrypted rooms", + "Conference calls are not supported in this client": "Conference calls are not supported in this client", "Warning!": "Warning!", "Conference calling is in development and may not be reliable.": "Conference calling is in development and may not be reliable.", "Failed to set up conference call": "Failed to set up conference call", @@ -114,19 +114,36 @@ "Room %(roomId)s not visible": "Room %(roomId)s not visible", "Missing user_id in request": "Missing user_id in request", "Usage": "Usage", + "Searches DuckDuckGo for results": "Searches DuckDuckGo for results", "/ddg is not a command": "/ddg is not a command", "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", + "Changes your display nickname": "Changes your display nickname", + "Changes colour scheme of current room": "Changes colour scheme of current room", + "Sets the room topic": "Sets the room topic", + "Invites user with given id to current room": "Invites user with given id to current room", + "Joins room with given alias": "Joins room with given alias", + "Leave room": "Leave room", "Unrecognised room alias:": "Unrecognised room alias:", + "Kicks user with given id": "Kicks user with given id", + "Bans user with given id": "Bans user with given id", + "Unbans user with given id": "Unbans user with given id", + "Ignores a user, hiding their messages from you": "Ignores a user, hiding their messages from you", "Ignored user": "Ignored user", "You are now ignoring %(userId)s": "You are now ignoring %(userId)s", + "Stops ignoring a user, showing their messages going forward": "Stops ignoring a user, showing their messages going forward", "Unignored user": "Unignored user", "You are no longer ignoring %(userId)s": "You are no longer ignoring %(userId)s", + "Define the power level of a user": "Define the power level of a user", + "Deops user with given id": "Deops user with given id", + "Opens the Developer Tools dialog": "Opens the Developer Tools dialog", + "Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple", "Unknown (user, device) pair:": "Unknown (user, device) pair:", "Device already verified!": "Device already verified!", "WARNING: Device already verified, but keys do NOT MATCH!": "WARNING: Device already verified, but keys do NOT MATCH!", "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!": "WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device %(deviceId)s is \"%(fprint)s\" which does not match the provided key \"%(fingerprint)s\". This could mean your communications are being intercepted!", "Verified key": "Verified key", "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.", + "Displays action": "Displays action", "Unrecognised command:": "Unrecognised command:", "Reason": "Reason", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", @@ -187,6 +204,7 @@ "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", "Failed to join room": "Failed to join room", "Message Pinning": "Message Pinning", + "Jitsi Conference Calling": "Jitsi Conference Calling", "Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", "Hide removed messages": "Hide removed messages", @@ -497,7 +515,6 @@ "Muted Users": "Muted Users", "Banned users": "Banned users", "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", - "Leave room": "Leave room", "Favourite": "Favourite", "Tagged as: ": "Tagged as: ", "To link to a room it must have an address.": "To link to a room it must have an address.", @@ -748,8 +765,8 @@ "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", - "Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.", "You have entered an invalid address.": "You have entered an invalid address.", + "Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.", "Preparing to send logs": "Preparing to send logs", "Logs sent": "Logs sent", "Thank you!": "Thank you!", @@ -1142,22 +1159,6 @@ "You need to enter a user name.": "You need to enter a user name.", "An unknown error occurred.": "An unknown error occurred.", "I already have an account": "I already have an account", - "Displays action": "Displays action", - "Bans user with given id": "Bans user with given id", - "Unbans user with given id": "Unbans user with given id", - "Define the power level of a user": "Define the power level of a user", - "Deops user with given id": "Deops user with given id", - "Invites user with given id to current room": "Invites user with given id to current room", - "Joins room with given alias": "Joins room with given alias", - "Sets the room topic": "Sets the room topic", - "Kicks user with given id": "Kicks user with given id", - "Changes your display nickname": "Changes your display nickname", - "Searches DuckDuckGo for results": "Searches DuckDuckGo for results", - "Changes colour scheme of current room": "Changes colour scheme of current room", - "Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple", - "Ignores a user, hiding their messages from you": "Ignores a user, hiding their messages from you", - "Stops ignoring a user, showing their messages going forward": "Stops ignoring a user, showing their messages going forward", - "Opens the Developer Tools dialog": "Opens the Developer Tools dialog", "Commands": "Commands", "Results from DuckDuckGo": "Results from DuckDuckGo", "Emoji": "Emoji", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 40bb2dd57e..9f35b0bfec 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2018 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -82,6 +83,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_jitsi": { + isFeature: true, + displayName: _td("Jitsi Conference Calling"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "MessageComposerInput.dontSuggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Disable Emoji suggestions while typing'),