Merge branches 'develop' and 't3chguy/shortcuts1' of github.com:matrix-org/matrix-react-sdk into t3chguy/shortcuts1
This commit is contained in:
commit
7d28137cfd
12 changed files with 404 additions and 73 deletions
31
docs/jitsi.md
Normal file
31
docs/jitsi.md
Normal file
|
@ -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.
|
|
@ -25,9 +25,6 @@ limitations under the License.
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
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 {
|
&.mx_RoomPreviewBar_spinnerTitle {
|
||||||
display: flex;
|
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 {
|
.mx_Spinner {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
|
@ -64,7 +64,6 @@ import SdkConfig from './SdkConfig';
|
||||||
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
|
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
|
||||||
import WidgetUtils from './utils/WidgetUtils';
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
|
||||||
import SettingsStore, { SettingLevel } from './settings/SettingsStore';
|
import SettingsStore, { SettingLevel } from './settings/SettingsStore';
|
||||||
|
|
||||||
global.mxCalls = {
|
global.mxCalls = {
|
||||||
|
@ -395,32 +394,6 @@ function _onAction(payload) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _startCallApp(roomId, type) {
|
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({
|
dis.dispatch({
|
||||||
action: 'appsDrawer',
|
action: 'appsDrawer',
|
||||||
show: true,
|
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.
|
// the event. It's just a random string to make the Jitsi URLs unique.
|
||||||
const widgetSessionId = Math.random().toString(36).substring(2);
|
const widgetSessionId = Math.random().toString(36).substring(2);
|
||||||
const confId = room.roomId.replace(/[^A-Za-z0-9]/g, '') + widgetSessionId;
|
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
|
const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain'];
|
||||||
// (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('&');
|
|
||||||
|
|
||||||
let widgetUrl;
|
const widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl();
|
||||||
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 widgetData = { widgetSessionId };
|
const widgetData = {
|
||||||
|
widgetSessionId, // TODO: Remove this eventually
|
||||||
|
conferenceId: confId,
|
||||||
|
isAudioOnly: type === 'voice',
|
||||||
|
domain: jitsiDomain,
|
||||||
|
};
|
||||||
|
|
||||||
const widgetId = (
|
const widgetId = (
|
||||||
'jitsi_' +
|
'jitsi_' +
|
||||||
|
|
|
@ -26,6 +26,13 @@ export const DEFAULTS: ConfigOptions = {
|
||||||
integrations_rest_url: "https://scalar.vector.im/api",
|
integrations_rest_url: "https://scalar.vector.im/api",
|
||||||
// Where to send bug reports. If not specified, bugs cannot be sent.
|
// Where to send bug reports. If not specified, bugs cannot be sent.
|
||||||
bug_report_endpoint_url: null,
|
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 {
|
export default class SdkConfig {
|
||||||
|
|
|
@ -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 => {
|
cli.on("crypto.verification.request", request => {
|
||||||
const isFlagOn = SettingsStore.isFeatureEnabled("feature_cross_signing");
|
const isFlagOn = SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
||||||
|
import {Key} from "../../../Keyboard";
|
||||||
|
|
||||||
export const KIND_DM = "dm";
|
export const KIND_DM = "dm";
|
||||||
export const KIND_INVITE = "invite";
|
export const KIND_INVITE = "invite";
|
||||||
|
@ -125,7 +126,7 @@ class ThreepidMember extends Member {
|
||||||
class DMUserTile extends React.PureComponent {
|
class DMUserTile extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
member: PropTypes.object.isRequired, // Should be a Member (see interface above)
|
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) => {
|
_onRemove = (e) => {
|
||||||
|
@ -156,18 +157,25 @@ class DMUserTile extends React.PureComponent {
|
||||||
width={avatarSize}
|
width={avatarSize}
|
||||||
height={avatarSize} />;
|
height={avatarSize} />;
|
||||||
|
|
||||||
return (
|
let closeButton;
|
||||||
<span className='mx_InviteDialog_userTile'>
|
if (this.props.onRemove) {
|
||||||
<span className='mx_InviteDialog_userTile_pill'>
|
closeButton = (
|
||||||
{avatar}
|
|
||||||
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
|
||||||
</span>
|
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className='mx_InviteDialog_userTile_remove'
|
className='mx_InviteDialog_userTile_remove'
|
||||||
onClick={this._onRemove}
|
onClick={this._onRemove}
|
||||||
>
|
>
|
||||||
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
<img src={require("../../../../res/img/icon-pill-remove.svg")} alt={_t('Remove')} width={8} height={8} />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className='mx_InviteDialog_userTile'>
|
||||||
|
<span className='mx_InviteDialog_userTile_pill'>
|
||||||
|
{avatar}
|
||||||
|
<span className='mx_InviteDialog_userTile_name'>{this.props.member.name}</span>
|
||||||
|
</span>
|
||||||
|
{ closeButton }
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -640,11 +648,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_cancel = () => {
|
_onKeyDown = (e) => {
|
||||||
// We do not want the user to close the dialog while an action is in progress
|
// when the field is empty and the user hits backspace remove the right-most target
|
||||||
if (this.state.busy) return;
|
if (!e.target.value && !this.state.busy && this.state.targets.length > 0 && e.key === Key.BACKSPACE &&
|
||||||
|
!e.ctrlKey && !e.shiftKey && !e.metaKey
|
||||||
this.props.onFinished();
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
this._removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateFilter = (e) => {
|
_updateFilter = (e) => {
|
||||||
|
@ -889,7 +900,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
_onManageSettingsClick = (e) => {
|
_onManageSettingsClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dis.dispatch({ action: 'view_user_settings' });
|
dis.dispatch({ action: 'view_user_settings' });
|
||||||
this._cancel();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderSection(kind: "recents"|"suggestions") {
|
_renderSection(kind: "recents"|"suggestions") {
|
||||||
|
@ -984,17 +995,18 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
|
|
||||||
_renderEditor() {
|
_renderEditor() {
|
||||||
const targets = this.state.targets.map(t => (
|
const targets = this.state.targets.map(t => (
|
||||||
<DMUserTile member={t} onRemove={this._removeMember} key={t.userId} />
|
<DMUserTile member={t} onRemove={!this.state.busy && this._removeMember} key={t.userId} />
|
||||||
));
|
));
|
||||||
const input = (
|
const input = (
|
||||||
<textarea
|
<textarea
|
||||||
key={"input"}
|
|
||||||
rows={1}
|
rows={1}
|
||||||
|
onKeyDown={this._onKeyDown}
|
||||||
onChange={this._updateFilter}
|
onChange={this._updateFilter}
|
||||||
value={this.state.filterText}
|
value={this.state.filterText}
|
||||||
ref={this._editorRef}
|
ref={this._editorRef}
|
||||||
onPaste={this._onPaste}
|
onPaste={this._onPaste}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
disabled={this.state.busy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
|
@ -1060,10 +1072,11 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
|
|
||||||
title = _t("Direct Messages");
|
title = _t("Direct Messages");
|
||||||
helpText = _t(
|
helpText = _t(
|
||||||
"If you can't find someone, ask them for their username, share your " +
|
"Start a conversation with someone using their name, username (like <userId/>) or email address.",
|
||||||
"username (%(userId)s) or <a>profile link</a>.",
|
{},
|
||||||
{userId},
|
{userId: () => {
|
||||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{sub}</a>},
|
return <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>;
|
||||||
|
}},
|
||||||
);
|
);
|
||||||
buttonText = _t("Go");
|
buttonText = _t("Go");
|
||||||
goButtonFn = this._startDm;
|
goButtonFn = this._startDm;
|
||||||
|
@ -1087,7 +1100,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className='mx_InviteDialog'
|
className='mx_InviteDialog'
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onFinished={this._cancel}
|
onFinished={this.props.onFinished}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
<div className='mx_InviteDialog_content'>
|
<div className='mx_InviteDialog_content'>
|
||||||
|
|
108
src/components/views/dialogs/KeySignatureUploadFailedDialog.js
Normal file
108
src/components/views/dialogs/KeySignatureUploadFailedDialog.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, {useState, useCallback, useRef} from 'react';
|
||||||
|
import * as sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
export default function KeySignatureUploadFailedDialog({
|
||||||
|
failures,
|
||||||
|
source,
|
||||||
|
continuation,
|
||||||
|
onFinished,
|
||||||
|
}) {
|
||||||
|
const RETRIES = 2;
|
||||||
|
const BaseDialog = sdk.getComponent('dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
const [retry, setRetry] = useState(RETRIES);
|
||||||
|
const [cancelled, setCancelled] = useState(false);
|
||||||
|
const [retrying, setRetrying] = useState(false);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
const onCancel = useRef(onFinished);
|
||||||
|
|
||||||
|
const causes = new Map([
|
||||||
|
["_afterCrossSigningLocalKeyChange", _t("a new master key signature")],
|
||||||
|
["checkOwnCrossSigningTrust", _t("a new cross-signing key signature")],
|
||||||
|
["setDeviceVerification", _t("a device cross-signing signature")],
|
||||||
|
]);
|
||||||
|
const defaultCause = _t("a key signature");
|
||||||
|
|
||||||
|
const onRetry = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
setRetrying(true);
|
||||||
|
const cancel = new Promise((resolve, reject) => {
|
||||||
|
onCancel.current = reject;
|
||||||
|
}).finally(() => {
|
||||||
|
setCancelled(true);
|
||||||
|
});
|
||||||
|
await Promise.race([
|
||||||
|
continuation(),
|
||||||
|
cancel,
|
||||||
|
]);
|
||||||
|
setSuccess(true);
|
||||||
|
} catch (e) {
|
||||||
|
setRetry(r => r-1);
|
||||||
|
} finally {
|
||||||
|
onCancel.current = onFinished;
|
||||||
|
setRetrying(false);
|
||||||
|
}
|
||||||
|
}, [continuation, onFinished]);
|
||||||
|
|
||||||
|
let body;
|
||||||
|
if (!success && !cancelled && continuation && retry > 0) {
|
||||||
|
const reason = causes.get(source) || defaultCause;
|
||||||
|
|
||||||
|
body = (<div>
|
||||||
|
<p>{_t("Riot encountered an error during upload of:")}</p>
|
||||||
|
<p>{reason}</p>
|
||||||
|
{retrying && <Spinner />}
|
||||||
|
<pre>{JSON.stringify(failures, null, 2)}</pre>
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton='Retry'
|
||||||
|
hasCancel={true}
|
||||||
|
onPrimaryButtonClick={onRetry}
|
||||||
|
onCancel={onCancel.current}
|
||||||
|
primaryDisabled={retrying}
|
||||||
|
/>
|
||||||
|
</div>);
|
||||||
|
} else {
|
||||||
|
body = (<div>
|
||||||
|
{success ?
|
||||||
|
<span>{_t("Upload completed")}</span> :
|
||||||
|
cancelled ?
|
||||||
|
<span>{_t("Cancelled signature upload")}</span> :
|
||||||
|
<span>{_t("Unabled to upload")}</span>}
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t("OK")}
|
||||||
|
hasCancel={false}
|
||||||
|
onPrimaryButtonClick={onFinished}
|
||||||
|
/>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
title={success ?
|
||||||
|
_t("Signature upload success") :
|
||||||
|
_t("Signature upload failed")}
|
||||||
|
fixedWidth={false}
|
||||||
|
onFinished={() => {}}
|
||||||
|
>
|
||||||
|
{body}
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
|
@ -520,7 +520,13 @@ export default class AppTile extends React.Component {
|
||||||
parsedWidgetUrl.query.react_perf = true;
|
parsedWidgetUrl.query.react_perf = true;
|
||||||
}
|
}
|
||||||
let safeWidgetUrl = '';
|
let safeWidgetUrl = '';
|
||||||
if (ALLOWED_APP_URL_SCHEMES.indexOf(parsedWidgetUrl.protocol) !== -1) {
|
if (ALLOWED_APP_URL_SCHEMES.includes(parsedWidgetUrl.protocol) || (
|
||||||
|
// Check if the widget URL is a Jitsi widget in Electron
|
||||||
|
parsedWidgetUrl.protocol === 'vector:'
|
||||||
|
&& parsedWidgetUrl.host === 'vector'
|
||||||
|
&& parsedWidgetUrl.pathname === '/webapp/jitsi.html'
|
||||||
|
&& this.props.type === 'jitsi'
|
||||||
|
)) {
|
||||||
safeWidgetUrl = url.format(parsedWidgetUrl);
|
safeWidgetUrl = url.format(parsedWidgetUrl);
|
||||||
}
|
}
|
||||||
return safeWidgetUrl;
|
return safeWidgetUrl;
|
||||||
|
|
|
@ -42,6 +42,8 @@ import {_t, _td} from '../../../languageHandler';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
|
|
||||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||||
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
|
||||||
|
@ -102,6 +104,12 @@ export default class SendMessageComposer extends React.Component {
|
||||||
this.model = null;
|
this.model = null;
|
||||||
this._editorRef = null;
|
this._editorRef = null;
|
||||||
this.currentlyComposedEditorState = null;
|
this.currentlyComposedEditorState = null;
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (cli.isCryptoEnabled() && cli.isRoomEncrypted(this.props.room.roomId)) {
|
||||||
|
this._prepareToEncrypt = new RateLimitedFunc(() => {
|
||||||
|
cli.prepareToEncrypt(this.props.room);
|
||||||
|
}, 60000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_setEditorRef = ref => {
|
_setEditorRef = ref => {
|
||||||
|
@ -121,6 +129,8 @@ export default class SendMessageComposer extends React.Component {
|
||||||
this.onVerticalArrow(event, true);
|
this.onVerticalArrow(event, true);
|
||||||
} else if (event.key === Key.ARROW_DOWN) {
|
} else if (event.key === Key.ARROW_DOWN) {
|
||||||
this.onVerticalArrow(event, false);
|
this.onVerticalArrow(event, false);
|
||||||
|
} else if (this._prepareToEncrypt) {
|
||||||
|
this._prepareToEncrypt();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,8 +45,6 @@
|
||||||
"VoIP is unsupported": "VoIP is unsupported",
|
"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 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.",
|
"You cannot place a call with yourself.": "You cannot place a call with yourself.",
|
||||||
"Could not connect to the integration server": "Could not connect to the integration server",
|
|
||||||
"A conference call could not be started because the integrations server is not available": "A conference call could not be started because the integrations server is not available",
|
|
||||||
"Call in Progress": "Call in Progress",
|
"Call in Progress": "Call in Progress",
|
||||||
"A call is currently being placed!": "A call is currently being placed!",
|
"A call is currently being placed!": "A call is currently being placed!",
|
||||||
"A call is already in progress!": "A call is already in progress!",
|
"A call is already in progress!": "A call is already in progress!",
|
||||||
|
@ -1585,7 +1583,7 @@
|
||||||
"Recent Conversations": "Recent Conversations",
|
"Recent Conversations": "Recent Conversations",
|
||||||
"Suggestions": "Suggestions",
|
"Suggestions": "Suggestions",
|
||||||
"Recently Direct Messaged": "Recently Direct Messaged",
|
"Recently Direct Messaged": "Recently Direct Messaged",
|
||||||
"If you can't find someone, ask them for their username, share your username (%(userId)s) or <a>profile link</a>.": "If you can't find someone, ask them for their username, share your username (%(userId)s) or <a>profile link</a>.",
|
"Start a conversation with someone using their name, username (like <userId/>) or email address.": "Start a conversation with someone using their name, username (like <userId/>) or email address.",
|
||||||
"Go": "Go",
|
"Go": "Go",
|
||||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.",
|
"If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.": "If you can't find someone, ask them for their username (e.g. @user:server.com) or <a>share this room</a>.",
|
||||||
"You added a new session '%(displayName)s', which is requesting encryption keys.": "You added a new session '%(displayName)s', which is requesting encryption keys.",
|
"You added a new session '%(displayName)s', which is requesting encryption keys.": "You added a new session '%(displayName)s', which is requesting encryption keys.",
|
||||||
|
@ -1595,6 +1593,16 @@
|
||||||
"Ignore request": "Ignore request",
|
"Ignore request": "Ignore request",
|
||||||
"Loading session info...": "Loading session info...",
|
"Loading session info...": "Loading session info...",
|
||||||
"Encryption key request": "Encryption key request",
|
"Encryption key request": "Encryption key request",
|
||||||
|
"a new master key signature": "a new master key signature",
|
||||||
|
"a new cross-signing key signature": "a new cross-signing key signature",
|
||||||
|
"a device cross-signing signature": "a device cross-signing signature",
|
||||||
|
"a key signature": "a key signature",
|
||||||
|
"Riot encountered an error during upload of:": "Riot encountered an error during upload of:",
|
||||||
|
"Upload completed": "Upload completed",
|
||||||
|
"Cancelled signature upload": "Cancelled signature upload",
|
||||||
|
"Unabled to upload": "Unabled to upload",
|
||||||
|
"Signature upload success": "Signature upload success",
|
||||||
|
"Signature upload failed": "Signature upload failed",
|
||||||
"You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.",
|
"You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.": "You've previously used Riot on %(host)s with lazy loading of members enabled. In this version lazy loading is disabled. As the local cache is not compatible between these two settings, Riot needs to resync your account.",
|
||||||
"If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.",
|
"If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.": "If the other version of Riot is still open in another tab, please close it as using Riot on the same host with both lazy loading enabled and disabled simultaneously will cause issues.",
|
||||||
"Incompatible local cache": "Incompatible local cache",
|
"Incompatible local cache": "Incompatible local cache",
|
||||||
|
|
|
@ -430,6 +430,11 @@ export default class WidgetUtils {
|
||||||
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (app.type === 'jitsi') {
|
||||||
|
console.log("Replacing Jitsi widget URL with local wrapper");
|
||||||
|
app.url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
|
||||||
|
}
|
||||||
|
|
||||||
app.url = encodeUri(app.url, params);
|
app.url = encodeUri(app.url, params);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
@ -468,4 +473,31 @@ export default class WidgetUtils {
|
||||||
|
|
||||||
return encodeURIComponent(`${widgetLocation}::${widgetUrl}`);
|
return encodeURIComponent(`${widgetLocation}::${widgetUrl}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean}) {
|
||||||
|
// NB. we can't just encodeURIComponent all of these because the $ signs need to be there
|
||||||
|
const queryString = [
|
||||||
|
'conferenceDomain=$domain',
|
||||||
|
'conferenceId=$conferenceId',
|
||||||
|
'isAudioOnly=$isAudioOnly',
|
||||||
|
'displayName=$matrix_display_name',
|
||||||
|
'avatarUrl=$matrix_avatar_url',
|
||||||
|
'userId=$matrix_user_id',
|
||||||
|
].join('&');
|
||||||
|
|
||||||
|
let currentUrl = window.location.href.split('#')[0];
|
||||||
|
if (!currentUrl.startsWith("https://") && !opts.forLocalRender) {
|
||||||
|
// Use an external wrapper if we're not locally rendering the widget. This is usually
|
||||||
|
// the URL that will end up in the widget event, so we want to make sure it's relatively
|
||||||
|
// safe to send.
|
||||||
|
// We'll end up using a local render URL when we see a Jitsi widget anyways, so this is
|
||||||
|
// really just for backwards compatibility and to appease the spec.
|
||||||
|
currentUrl = "https://riot.im/app";
|
||||||
|
}
|
||||||
|
if (!currentUrl.endsWith('/')) {
|
||||||
|
currentUrl = `${currentUrl}/`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentUrl + "jitsi.html#" + queryString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
140
src/widgets/WidgetApi.ts
Normal file
140
src/widgets/WidgetApi.ts
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Dev note: This is largely inspired by Dimension. Used with permission.
|
||||||
|
// https://github.com/turt2live/matrix-dimension/blob/4f92d560266635e5a3c824606215b84e8c0b19f5/web/app/shared/services/scalar/scalar-widget.api.ts
|
||||||
|
|
||||||
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
|
||||||
|
export enum Capability {
|
||||||
|
Screenshot = "m.capability.screenshot",
|
||||||
|
Sticker = "m.sticker",
|
||||||
|
AlwaysOnScreen = "m.always_on_screen",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum KnownWidgetActions {
|
||||||
|
GetSupportedApiVersions = "supported_api_versions",
|
||||||
|
TakeScreenshot = "screenshot",
|
||||||
|
GetCapabilities = "capabilities",
|
||||||
|
SendEvent = "send_event",
|
||||||
|
UpdateVisibility = "visibility",
|
||||||
|
ReceiveOpenIDCredentials = "openid_credentials",
|
||||||
|
SetAlwaysOnScreen = "set_always_on_screen",
|
||||||
|
}
|
||||||
|
export type WidgetAction = KnownWidgetActions | string;
|
||||||
|
|
||||||
|
export enum WidgetApiType {
|
||||||
|
ToWidget = "toWidget",
|
||||||
|
FromWidget = "fromWidget",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WidgetRequest {
|
||||||
|
api: WidgetApiType;
|
||||||
|
widgetId: string;
|
||||||
|
requestId: string;
|
||||||
|
data: any;
|
||||||
|
action: WidgetAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ToWidgetRequest extends WidgetRequest {
|
||||||
|
api: WidgetApiType.ToWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FromWidgetRequest extends WidgetRequest {
|
||||||
|
api: WidgetApiType.FromWidget;
|
||||||
|
response: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles Riot <--> Widget interactions for embedded/standalone widgets.
|
||||||
|
*/
|
||||||
|
export class WidgetApi {
|
||||||
|
private origin: string;
|
||||||
|
private inFlightRequests: {[requestId: string]: (reply: FromWidgetRequest) => void} = {};
|
||||||
|
private readyPromise: Promise<any>;
|
||||||
|
private readyPromiseResolve: () => void;
|
||||||
|
|
||||||
|
constructor(currentUrl: string, private widgetId: string, private requestedCapabilities: string[]) {
|
||||||
|
this.origin = new URL(currentUrl).origin;
|
||||||
|
|
||||||
|
this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
|
||||||
|
|
||||||
|
window.addEventListener("message", event => {
|
||||||
|
if (event.origin !== this.origin) return; // ignore: invalid origin
|
||||||
|
if (!event.data) return; // invalid schema
|
||||||
|
if (event.data.widgetId !== this.widgetId) return; // not for us
|
||||||
|
|
||||||
|
const payload = <WidgetRequest>event.data;
|
||||||
|
if (payload.api === WidgetApiType.ToWidget && payload.action) {
|
||||||
|
console.log(`[WidgetAPI] Got request: ${JSON.stringify(payload)}`);
|
||||||
|
|
||||||
|
if (payload.action === KnownWidgetActions.GetCapabilities) {
|
||||||
|
this.onCapabilitiesRequest(<ToWidgetRequest>payload);
|
||||||
|
this.readyPromiseResolve();
|
||||||
|
} else {
|
||||||
|
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
|
||||||
|
}
|
||||||
|
} else if (payload.api === WidgetApiType.FromWidget && this.inFlightRequests[payload.requestId]) {
|
||||||
|
console.log(`[WidgetAPI] Got reply: ${JSON.stringify(payload)}`);
|
||||||
|
const handler = this.inFlightRequests[payload.requestId];
|
||||||
|
delete this.inFlightRequests[payload.requestId];
|
||||||
|
handler(<FromWidgetRequest>payload);
|
||||||
|
} else {
|
||||||
|
console.warn(`[WidgetAPI] Unhandled payload: ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public waitReady(): Promise<any> {
|
||||||
|
return this.readyPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private replyToRequest(payload: ToWidgetRequest, reply: any) {
|
||||||
|
if (!window.parent) return;
|
||||||
|
|
||||||
|
const request = JSON.parse(JSON.stringify(payload));
|
||||||
|
request.response = reply;
|
||||||
|
|
||||||
|
window.parent.postMessage(request, this.origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onCapabilitiesRequest(payload: ToWidgetRequest) {
|
||||||
|
return this.replyToRequest(payload, {capabilities: this.requestedCapabilities});
|
||||||
|
}
|
||||||
|
|
||||||
|
public callAction(action: WidgetAction, payload: any, callback: (reply: FromWidgetRequest) => void) {
|
||||||
|
if (!window.parent) return;
|
||||||
|
|
||||||
|
const request: FromWidgetRequest = {
|
||||||
|
api: WidgetApiType.FromWidget,
|
||||||
|
widgetId: this.widgetId,
|
||||||
|
action: action,
|
||||||
|
requestId: randomString(160),
|
||||||
|
data: payload,
|
||||||
|
response: {}, // Not used at this layer - it's used when the client responds
|
||||||
|
};
|
||||||
|
this.inFlightRequests[request.requestId] = callback;
|
||||||
|
|
||||||
|
console.log(`[WidgetAPI] Sending request: `, request);
|
||||||
|
window.parent.postMessage(request, "*");
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAlwaysOnScreen(onScreen: boolean): Promise<any> {
|
||||||
|
return new Promise<any>(resolve => {
|
||||||
|
this.callAction(KnownWidgetActions.SetAlwaysOnScreen, {value: onScreen}, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue