diff --git a/docs/jitsi.md b/docs/jitsi.md new file mode 100644 index 0000000000..779ef79d3a --- /dev/null +++ b/docs/jitsi.md @@ -0,0 +1,31 @@ +# Jitsi Wrapper + +**Note**: These are developer docs. Please consult your client's documentation for +instructions on setting up Jitsi. + +The react-sdk wraps all Jitsi call widgets in a local wrapper called `jitsi.html` +which takes several parameters: + +*Query string*: +* `widgetId`: The ID of the widget. This is needed for communication back to the + react-sdk. +* `parentUrl`: The URL of the parent window. This is also needed for + communication back to the react-sdk. + +*Hash/fragment (formatted as a query string)*: +* `conferenceDomain`: The domain to connect Jitsi Meet to. +* `conferenceId`: The room or conference ID to connect Jitsi Meet to. +* `isAudioOnly`: Boolean for whether this is a voice-only conference. May not + be present, should default to `false`. +* `displayName`: The display name of the user viewing the widget. May not + be present or could be null. +* `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May + not be present or could be null. +* `userId`: The MXID of the user viewing the widget. May not be present or could + be null. + +The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently +being served. For example, `https://riot.im/develop/jitsi.html` or `vector://webapp/jitsi.html`. + +The `jitsi.html` wrapper can use the react-sdk's `WidgetApi` to communicate, making +it easier to actually implement the feature. diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index b3f6a12103..981cf06c69 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -25,9 +25,6 @@ limitations under the License. h3 { font-size: 18px; font-weight: 600; - // break-word, with fallback to break-all, which is wider supported - word-break: break-all; - word-break: break-word; &.mx_RoomPreviewBar_spinnerTitle { display: flex; @@ -36,6 +33,13 @@ limitations under the License. } } + h3, + .mx_RoomPreviewBar_message p { + // break-word, with fallback to break-all, which is wider supported + word-break: break-all; + word-break: break-word; + } + .mx_Spinner { width: auto; height: auto; diff --git a/src/CallHandler.js b/src/CallHandler.js index 2988e90f40..7ec4e7d8bb 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -64,7 +64,6 @@ import SdkConfig from './SdkConfig'; import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; -import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore, { SettingLevel } from './settings/SettingsStore'; global.mxCalls = { @@ -395,32 +394,6 @@ function _onAction(payload) { } async function _startCallApp(roomId, type) { - // check for a working integration manager. Technically we could put - // the state event in anyway, but the resulting widget would then not - // work for us. Better that the user knows before everyone else in the - // room sees it. - const managers = IntegrationManagers.sharedInstance(); - let haveScalar = false; - if (managers.hasManager()) { - try { - const scalarClient = managers.getPrimaryManager().getScalarClient(); - await scalarClient.connect(); - haveScalar = scalarClient.hasCredentials(); - } catch (e) { - // ignore - } - } - - if (!haveScalar) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - - Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, { - title: _t('Could not connect to the integration server'), - description: _t('A conference call could not be started because the integrations server is not available'), - }); - return; - } - dis.dispatch({ action: 'appsDrawer', show: true, @@ -460,27 +433,16 @@ async function _startCallApp(roomId, type) { // 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 jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain']; - let widgetUrl; - if (SdkConfig.get().integrations_jitsi_widget_url) { - // Try this config key. This probably isn't ideal as a way of discovering this - // URL, but this will at least allow the integration manager to not be hardcoded. - widgetUrl = SdkConfig.get().integrations_jitsi_widget_url + '?' + queryString; - } else { - const apiUrl = IntegrationManagers.sharedInstance().getPrimaryManager().apiUrl; - widgetUrl = apiUrl + '/widgets/jitsi.html?' + queryString; - } + const widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); - const widgetData = { widgetSessionId }; + const widgetData = { + widgetSessionId, // TODO: Remove this eventually + conferenceId: confId, + isAudioOnly: type === 'voice', + domain: jitsiDomain, + }; const widgetId = ( 'jitsi_' + diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 8177a6c5b8..34f3402334 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -26,6 +26,13 @@ export const DEFAULTS: ConfigOptions = { integrations_rest_url: "https://scalar.vector.im/api", // Where to send bug reports. If not specified, bugs cannot be sent. bug_report_endpoint_url: null, + // Jitsi conference options + jitsi: { + // Default conference domain + preferredDomain: "jitsi.riot.im", + // Default Jitsi Meet API location + externalApiUrl: "https://jitsi.riot.im/libs/external_api.min.js", + }, }; export default class SdkConfig { diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index cadc511fc3..0d10dd6d8f 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -555,10 +555,6 @@ export default createReactClass({ GROUP_JOINPOLICY_INVITE, }, }); - dis.dispatch({ - action: 'panel_disable', - sideDisabled: true, - }); }, _onShareClick: function() { diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 3e59112a63..b2f4493d5b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1494,6 +1494,16 @@ export default createReactClass({ } }); + cli.on("crypto.keySignatureUploadFailure", (failures, source, continuation) => { + const KeySignatureUploadFailedDialog = + sdk.getComponent('views.dialogs.KeySignatureUploadFailedDialog'); + Modal.createTrackedDialog( + 'Failed to upload key signatures', + 'Failed to upload key signatures', + KeySignatureUploadFailedDialog, + { failures, source, continuation }); + }); + cli.on("crypto.verification.request", request => { const isFlagOn = SettingsStore.isFeatureEnabled("feature_cross_signing"); diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js index d27a66165e..f0d5443cac 100644 --- a/src/components/views/dialogs/InviteDialog.js +++ b/src/components/views/dialogs/InviteDialog.js @@ -35,6 +35,7 @@ import createRoom, {canEncryptToAllUsers} from "../../../createRoom"; import {inviteMultipleToRoom} from "../../../RoomInvite"; import SettingsStore from '../../../settings/SettingsStore'; import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore"; +import {Key} from "../../../Keyboard"; export const KIND_DM = "dm"; export const KIND_INVITE = "invite"; @@ -125,7 +126,7 @@ class ThreepidMember extends Member { class DMUserTile extends React.PureComponent { static propTypes = { member: PropTypes.object.isRequired, // Should be a Member (see interface above) - onRemove: PropTypes.func.isRequired, // takes 1 argument, the member being removed + onRemove: PropTypes.func, // takes 1 argument, the member being removed }; _onRemove = (e) => { @@ -156,18 +157,25 @@ class DMUserTile extends React.PureComponent { width={avatarSize} height={avatarSize} />; - return ( - - - {avatar} - {this.props.member.name} - + let closeButton; + if (this.props.onRemove) { + closeButton = ( {_t('Remove')} + ); + } + + return ( + + + {avatar} + {this.props.member.name} + + { closeButton } ); } @@ -640,11 +648,14 @@ export default class InviteDialog extends React.PureComponent { }); }; - _cancel = () => { - // We do not want the user to close the dialog while an action is in progress - if (this.state.busy) return; - - this.props.onFinished(); + _onKeyDown = (e) => { + // when the field is empty and the user hits backspace remove the right-most target + if (!e.target.value && !this.state.busy && this.state.targets.length > 0 && e.key === Key.BACKSPACE && + !e.ctrlKey && !e.shiftKey && !e.metaKey + ) { + e.preventDefault(); + this._removeMember(this.state.targets[this.state.targets.length - 1]); + } }; _updateFilter = (e) => { @@ -889,7 +900,7 @@ export default class InviteDialog extends React.PureComponent { _onManageSettingsClick = (e) => { e.preventDefault(); dis.dispatch({ action: 'view_user_settings' }); - this._cancel(); + this.props.onFinished(); }; _renderSection(kind: "recents"|"suggestions") { @@ -984,17 +995,18 @@ export default class InviteDialog extends React.PureComponent { _renderEditor() { const targets = this.state.targets.map(t => ( - + )); const input = (