Merge pull request #2056 from matrix-org/dbkr/tiny_jitsi_follows_you_between_rooms
Implement always-on-screen capability for widgets
This commit is contained in:
commit
149a935594
10 changed files with 216 additions and 68 deletions
|
@ -84,6 +84,7 @@
|
|||
"react-beautiful-dnd": "^4.0.1",
|
||||
"react-dom": "^15.6.0",
|
||||
"react-gemini-scrollbar": "matrix-org/react-gemini-scrollbar#5e97aef",
|
||||
"resize-observer-polyfill": "^1.5.0",
|
||||
"slate": "0.33.4",
|
||||
"slate-react": "^0.12.4",
|
||||
"slate-html-serializer": "^0.6.1",
|
||||
|
|
|
@ -54,6 +54,10 @@ limitations under the License.
|
|||
|
||||
}
|
||||
|
||||
.mx_LeftPanel .mx_AppTileFullWidth {
|
||||
height: 132px;
|
||||
}
|
||||
|
||||
.mx_LeftPanel .mx_RoomList_scrollbar {
|
||||
order: 1;
|
||||
|
||||
|
|
|
@ -126,6 +126,12 @@ limitations under the License.
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_AppTileBody_mini {
|
||||
height: 132px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_AppTileBody iframe {
|
||||
width: 100%;
|
||||
height: 280px;
|
||||
|
|
|
@ -164,6 +164,7 @@ export default class AppTile extends React.Component {
|
|||
PersistedElement.destroyElement(this._persistKey);
|
||||
ActiveWidgetStore.delWidgetMessaging(this.props.id);
|
||||
ActiveWidgetStore.delWidgetCapabilities(this.props.id);
|
||||
ActiveWidgetStore.delRoomId(this.props.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -343,6 +344,7 @@ export default class AppTile extends React.Component {
|
|||
if (!ActiveWidgetStore.getWidgetMessaging(this.props.id)) {
|
||||
this._setupWidgetMessaging();
|
||||
}
|
||||
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
|
||||
this.setState({loading: false});
|
||||
}
|
||||
|
||||
|
@ -522,6 +524,8 @@ export default class AppTile extends React.Component {
|
|||
// (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/)
|
||||
const iframeFeatures = "microphone; camera; encrypted-media;";
|
||||
|
||||
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
||||
|
||||
if (this.props.show) {
|
||||
const loadingElement = (
|
||||
<div className="mx_AppLoading_spinner_fadeIn">
|
||||
|
@ -530,20 +534,20 @@ export default class AppTile extends React.Component {
|
|||
);
|
||||
if (this.state.initialising) {
|
||||
appTileBody = (
|
||||
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ loadingElement }
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.hasPermissionToLoad == true) {
|
||||
if (this.isMixedContent()) {
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<div className={appTileBodyClass}>
|
||||
<AppWarning errorMsg="Error - Mixed content" />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
appTileBody = (
|
||||
<div className={'mx_AppTileBody ' + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
|
||||
{ this.state.loading && loadingElement }
|
||||
{ /*
|
||||
The "is" attribute in the following iframe tag is needed in order to enable rendering of the
|
||||
|
@ -573,7 +577,7 @@ export default class AppTile extends React.Component {
|
|||
} else {
|
||||
const isRoomEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
|
||||
appTileBody = (
|
||||
<div className="mx_AppTileBody">
|
||||
<div className={appTileBodyClass}>
|
||||
<AppPermission
|
||||
isRoomEncrypted={isRoomEncrypted}
|
||||
url={this.state.widgetUrl}
|
||||
|
@ -686,6 +690,8 @@ AppTile.propTypes = {
|
|||
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
|
||||
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
|
||||
fullWidth: PropTypes.bool,
|
||||
// Optional. If set, renders a smaller view of the widget
|
||||
miniMode: PropTypes.bool,
|
||||
// UserId of the current user
|
||||
userId: PropTypes.string.isRequired,
|
||||
// UserId of the entity that added / modified the widget
|
||||
|
@ -738,4 +744,5 @@ AppTile.defaultProps = {
|
|||
handleMinimisePointerEvents: false,
|
||||
whitelistCapabilities: [],
|
||||
userWidget: false,
|
||||
miniMode: false,
|
||||
};
|
||||
|
|
|
@ -14,9 +14,11 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const PropTypes = require('prop-types');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -62,6 +64,9 @@ export default class PersistedElement extends React.Component {
|
|||
super();
|
||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
||||
this.collectChild = this.collectChild.bind(this);
|
||||
this._onContainerResize = this._onContainerResize.bind(this);
|
||||
|
||||
this.resizeObserver = new ResizeObserver(this._onContainerResize);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,7 +88,13 @@ export default class PersistedElement extends React.Component {
|
|||
}
|
||||
|
||||
collectChildContainer(ref) {
|
||||
if (this.childContainer) {
|
||||
this.resizeObserver.unobserve(this.childContainer);
|
||||
}
|
||||
this.childContainer = ref;
|
||||
if (ref) {
|
||||
this.resizeObserver.observe(ref);
|
||||
}
|
||||
}
|
||||
|
||||
collectChild(ref) {
|
||||
|
@ -101,6 +112,11 @@ export default class PersistedElement extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.updateChildVisibility(this.child, false);
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
_onContainerResize() {
|
||||
this.updateChildPosition(this.child, this.childContainer);
|
||||
}
|
||||
|
||||
updateChild() {
|
||||
|
|
87
src/components/views/elements/PersistentApp.js
Normal file
87
src/components/views/elements/PersistentApp.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
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.
|
||||
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 from 'react';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PersistentApp',
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
};
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._roomStoreToken) {
|
||||
this._roomStoreToken.remove();
|
||||
}
|
||||
},
|
||||
|
||||
_onRoomViewStoreUpdate: function(payload) {
|
||||
if (RoomViewStore.getRoomId() === this.state.roomId) return;
|
||||
this.setState({
|
||||
roomId: RoomViewStore.getRoomId(),
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (ActiveWidgetStore.getPersistentWidgetId()) {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(ActiveWidgetStore.getPersistentWidgetId());
|
||||
if (this.state.roomId !== persistentWidgetInRoomId) {
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
||||
});
|
||||
const app = WidgetUtils.makeAppConfig(
|
||||
appEvent.getStateKey(), appEvent.getContent(), appEvent.sender, persistentWidgetInRoomId,
|
||||
);
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
|
||||
const AppTile = sdk.getComponent('elements.AppTile');
|
||||
return <AppTile
|
||||
key={app.id}
|
||||
id={app.id}
|
||||
url={app.url}
|
||||
name={app.name}
|
||||
type={app.type}
|
||||
fullWidth={true}
|
||||
room={persistentWidgetInRoom}
|
||||
userId={MatrixClientPeg.get().credentials.userId}
|
||||
show={true}
|
||||
creatorUserId={app.creatorUserId}
|
||||
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
|
||||
waitForIframeLoad={app.waitForIframeLoad}
|
||||
whitelistCapabilities={capWhitelist}
|
||||
showDelete={false}
|
||||
showMinimise={false}
|
||||
miniMode={true}
|
||||
/>;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
|
@ -29,7 +29,6 @@ import ScalarAuthClient from '../../../ScalarAuthClient';
|
|||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
const MAX_WIDGETS = 2;
|
||||
|
@ -107,55 +106,6 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Encodes a URI according to a set of template variables. Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { '$bar': 'baz' }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
encodeUri: function(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
pathTemplate = pathTemplate.replace(
|
||||
key, encodeURIComponent(variables[key]),
|
||||
);
|
||||
}
|
||||
return pathTemplate;
|
||||
},
|
||||
|
||||
_initAppConfig: function(appId, app, sender) {
|
||||
const user = MatrixClientPeg.get().getUser(this.props.userId);
|
||||
const params = {
|
||||
'$matrix_user_id': this.props.userId,
|
||||
'$matrix_room_id': this.props.room.roomId,
|
||||
'$matrix_display_name': user ? user.displayName : this.props.userId,
|
||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||
|
||||
// TODO: Namespace themes through some standard
|
||||
'$theme': SettingsStore.getValue("theme"),
|
||||
};
|
||||
|
||||
app.id = appId;
|
||||
app.name = app.name || app.type;
|
||||
|
||||
if (app.data) {
|
||||
Object.keys(app.data).forEach((key) => {
|
||||
params['$' + key] = app.data[key];
|
||||
});
|
||||
|
||||
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
||||
}
|
||||
|
||||
app.url = this.encodeUri(app.url, params);
|
||||
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
|
||||
|
||||
return app;
|
||||
},
|
||||
|
||||
onRoomStateEvents: function(ev, state) {
|
||||
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
|
||||
return;
|
||||
|
@ -165,7 +115,7 @@ module.exports = React.createClass({
|
|||
|
||||
_getApps: function() {
|
||||
return WidgetUtils.getRoomWidgets(this.props.room).map((ev) => {
|
||||
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||
return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender, this.props.room.roomId);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -213,15 +163,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", this.props.room.room_id);
|
||||
|
||||
const apps = this.state.apps.map((app, index, arr) => {
|
||||
const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : [];
|
||||
|
||||
// Obviously anyone that can add a widget can claim it's a jitsi widget,
|
||||
// so this doesn't really offer much over the set of domains we load
|
||||
// widgets from at all, but it probably makes sense for sanity.
|
||||
if (app.type == 'jitsi') capWhitelist.push("m.always_on_screen");
|
||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
||||
|
||||
return (<AppTile
|
||||
key={app.id}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
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.
|
||||
|
@ -92,7 +92,8 @@ module.exports = React.createClass({
|
|||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
const PersistentApp = sdk.getComponent('elements.PersistentApp');
|
||||
return <PersistentApp />;
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -32,6 +32,9 @@ class ActiveWidgetStore {
|
|||
|
||||
// A WidgetMessaging instance for each widget ID
|
||||
this._widgetMessagingByWidgetId = {};
|
||||
|
||||
// What room ID each widget is associated with (if it's a room widget)
|
||||
this._roomIdByWidgetId = {};
|
||||
}
|
||||
|
||||
setWidgetPersistence(widgetId, val) {
|
||||
|
@ -46,6 +49,10 @@ class ActiveWidgetStore {
|
|||
return this._persistentWidgetId === widgetId;
|
||||
}
|
||||
|
||||
getPersistentWidgetId() {
|
||||
return this._persistentWidgetId;
|
||||
}
|
||||
|
||||
setWidgetCapabilities(widgetId, caps) {
|
||||
this._capsByWidgetId[widgetId] = caps;
|
||||
}
|
||||
|
@ -76,6 +83,18 @@ class ActiveWidgetStore {
|
|||
delete this._widgetMessagingByWidgetId[widgetId];
|
||||
}
|
||||
}
|
||||
|
||||
getRoomId(widgetId) {
|
||||
return this._roomIdByWidgetId[widgetId];
|
||||
}
|
||||
|
||||
setRoomId(widgetId, roomId) {
|
||||
this._roomIdByWidgetId[widgetId] = roomId;
|
||||
}
|
||||
|
||||
delRoomId(widgetId) {
|
||||
delete this._roomIdByWidgetId[widgetId];
|
||||
}
|
||||
}
|
||||
|
||||
if (global.singletonActiveWidgetStore === undefined) {
|
||||
|
|
|
@ -19,6 +19,27 @@ import MatrixClientPeg from '../MatrixClientPeg';
|
|||
import SdkConfig from "../SdkConfig";
|
||||
import dis from '../dispatcher';
|
||||
import * as url from "url";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
/**
|
||||
* Encodes a URI according to a set of template variables. Variables will be
|
||||
* passed through encodeURIComponent.
|
||||
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
|
||||
* @param {Object} variables The key/value pairs to replace the template
|
||||
* variables with. E.g. { '$bar': 'baz' }.
|
||||
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
|
||||
*/
|
||||
function encodeUri(pathTemplate, variables) {
|
||||
for (const key in variables) {
|
||||
if (!variables.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
pathTemplate = pathTemplate.replace(
|
||||
key, encodeURIComponent(variables[key]),
|
||||
);
|
||||
}
|
||||
return pathTemplate;
|
||||
}
|
||||
|
||||
export default class WidgetUtils {
|
||||
/* Returns true if user is able to send state events to modify widgets in this room
|
||||
|
@ -324,4 +345,47 @@ export default class WidgetUtils {
|
|||
});
|
||||
return client.setAccountData('m.widgets', userWidgets);
|
||||
}
|
||||
|
||||
static makeAppConfig(appId, app, sender, roomId) {
|
||||
const myUserId = MatrixClientPeg.get().credentials.userId;
|
||||
const user = MatrixClientPeg.get().getUser(myUserId);
|
||||
const params = {
|
||||
'$matrix_user_id': myUserId,
|
||||
'$matrix_room_id': roomId,
|
||||
'$matrix_display_name': user ? user.displayName : myUserId,
|
||||
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
|
||||
|
||||
// TODO: Namespace themes through some standard
|
||||
'$theme': SettingsStore.getValue("theme"),
|
||||
};
|
||||
|
||||
app.id = appId;
|
||||
app.name = app.name || app.type;
|
||||
|
||||
if (app.data) {
|
||||
Object.keys(app.data).forEach((key) => {
|
||||
params['$' + key] = app.data[key];
|
||||
});
|
||||
|
||||
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
|
||||
}
|
||||
|
||||
app.url = encodeUri(app.url, params);
|
||||
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
static getCapWhitelistForAppTypeInRoomId(appType, roomId) {
|
||||
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId);
|
||||
|
||||
const capWhitelist = enableScreenshots ? ["m.capability.screenshot"] : [];
|
||||
|
||||
// Obviously anyone that can add a widget can claim it's a jitsi widget,
|
||||
// so this doesn't really offer much over the set of domains we load
|
||||
// widgets from at all, but it probably makes sense for sanity.
|
||||
if (appType == 'jitsi') capWhitelist.push("m.always_on_screen");
|
||||
|
||||
return capWhitelist;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue