Merge branch 'develop' into hs/purge-irc-hack

This commit is contained in:
Will Hunt 2018-10-03 19:39:14 +01:00 committed by GitHub
commit 17915b5082
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
199 changed files with 11956 additions and 10603 deletions

View file

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import url from 'url';
import { _t } from '../../../languageHandler';
import WidgetUtils from "../../../WidgetUtils";
import WidgetUtils from "../../../utils/WidgetUtils";
export default class AppPermission extends React.Component {
constructor(props) {

View file

@ -1,5 +1,6 @@
/**
Copyright 2017 Vector Creations Ltd
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.
@ -31,8 +32,9 @@ import sdk from '../../../index';
import AppPermission from './AppPermission';
import AppWarning from './AppWarning';
import MessageSpinner from './MessageSpinner';
import WidgetUtils from '../../../WidgetUtils';
import WidgetUtils from '../../../utils/WidgetUtils';
import dis from '../../../dispatcher';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@ -40,9 +42,13 @@ const ENABLE_REACT_PERF = false;
export default class AppTile extends React.Component {
constructor(props) {
super(props);
// The key used for PersistedElement
this._persistKey = 'widget_' + this.props.id;
this.state = this._getNewState(props);
this._onWidgetAction = this._onWidgetAction.bind(this);
this._onAction = this._onAction.bind(this);
this._onMessage = this._onMessage.bind(this);
this._onLoaded = this._onLoaded.bind(this);
this._onEditClick = this._onEditClick.bind(this);
@ -50,7 +56,6 @@ export default class AppTile extends React.Component {
this._onSnapshotClick = this._onSnapshotClick.bind(this);
this.onClickMenuBar = this.onClickMenuBar.bind(this);
this._onMinimiseClick = this._onMinimiseClick.bind(this);
this._onInitialLoad = this._onInitialLoad.bind(this);
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
@ -66,9 +71,12 @@ export default class AppTile extends React.Component {
_getNewState(newProps) {
const widgetPermissionId = [newProps.room.roomId, encodeURIComponent(newProps.url)].join('_');
const hasPermissionToLoad = localStorage.getItem(widgetPermissionId);
const PersistedElement = sdk.getComponent("elements.PersistedElement");
return {
initialising: true, // True while we are mangling the widget URL
loading: this.props.waitForIframeLoad, // True while the iframe content is loading
// True while the iframe content is loading
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
widgetUrl: this._addWurlParams(newProps.url),
widgetPermissionId: widgetPermissionId,
// Assume that widget has permission to load if we are the user who
@ -77,9 +85,6 @@ export default class AppTile extends React.Component {
error: null,
deleting: false,
widgetPageTitle: newProps.widgetPageTitle,
allowedCapabilities: (this.props.whitelistCapabilities && this.props.whitelistCapabilities.length > 0) ?
this.props.whitelistCapabilities : [],
requestedCapabilities: [],
};
}
@ -89,7 +94,7 @@ export default class AppTile extends React.Component {
* @return {Boolean} True if capability supported
*/
_hasCapability(capability) {
return this.state.allowedCapabilities.some((c) => {return c === capability;});
return ActiveWidgetStore.widgetHasCapability(this.props.id, capability);
}
/**
@ -112,8 +117,9 @@ export default class AppTile extends React.Component {
const params = qs.parse(u.query);
// Append widget ID to query parameters
params.widgetId = this.props.id;
// Append current / parent URL
params.parentUrl = window.location.href;
// Append current / parent URL, minus the hash because that will change when
// we view a different room (ie. may change for persistent widgets)
params.parentUrl = window.location.href.split('#', 2)[0];
u.search = undefined;
u.query = params;
@ -142,30 +148,22 @@ export default class AppTile extends React.Component {
window.addEventListener('message', this._onMessage, false);
// Widget action listeners
this.dispatcherRef = dis.register(this._onWidgetAction);
}
componentDidUpdate() {
// Allow parents to access widget messaging
if (this.props.collectWidgetMessaging) {
this.props.collectWidgetMessaging(this.widgetMessaging);
}
this.dispatcherRef = dis.register(this._onAction);
}
componentWillUnmount() {
// Widget action listeners
dis.unregister(this.dispatcherRef);
// Widget postMessage listeners
try {
if (this.widgetMessaging) {
this.widgetMessaging.stop();
}
} catch (e) {
console.error('Failed to stop listening for widgetMessaging events', e.message);
}
// Jitsi listener
window.removeEventListener('message', this._onMessage);
// if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) {
ActiveWidgetStore.destroyPersistentWidget();
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}
}
/**
@ -286,7 +284,7 @@ export default class AppTile extends React.Component {
_onSnapshotClick(e) {
console.warn("Requesting widget snapshot");
this.widgetMessaging.getScreenshot()
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
.catch((err) => {
console.error("Failed to get screenshot", err);
})
@ -319,15 +317,18 @@ export default class AppTile extends React.Component {
return;
}
this.setState({deleting: true});
MatrixClientPeg.get().sendStateEvent(
WidgetUtils.setRoomWidget(
this.props.room.roomId,
'im.vector.modular.widgets',
{}, // empty content
this.props.id,
).then(() => {
return WidgetUtils.waitForRoomWidget(this.props.id, this.props.room.roomId, false);
}).catch((e) => {
).catch((e) => {
console.error('Failed to delete widget', e);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to remove widget', '', ErrorDialog, {
title: _t('Failed to remove widget'),
description: _t('An error ocurred whilst trying to remove the widget from the room'),
});
}).finally(() => {
this.setState({deleting: false});
});
@ -344,19 +345,20 @@ export default class AppTile extends React.Component {
* Called when widget iframe has finished loading
*/
_onLoaded() {
if (!this.widgetMessaging) {
this._onInitialLoad();
if (!ActiveWidgetStore.getWidgetMessaging(this.props.id)) {
this._setupWidgetMessaging();
}
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);
this.setState({loading: false});
}
/**
* Called on initial load of the widget iframe
*/
_onInitialLoad() {
this.widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
this.widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.id} requested capabilities:`, requestedCapabilities);
_setupWidgetMessaging() {
// FIXME: There's probably no reason to do this here: it should probably be done entirely
// in ActiveWidgetStore.
const widgetMessaging = new WidgetMessaging(this.props.id, this.props.url, this.refs.appFrame.contentWindow);
ActiveWidgetStore.setWidgetMessaging(this.props.id, widgetMessaging);
widgetMessaging.getCapabilities().then((requestedCapabilities) => {
console.log(`Widget ${this.props.id} requested capabilities: ` + requestedCapabilities);
requestedCapabilities = requestedCapabilities || [];
// Allow whitelisted capabilities
@ -368,16 +370,15 @@ export default class AppTile extends React.Component {
}, this.props.whitelistCapabilities);
if (requestedWhitelistCapabilies.length > 0 ) {
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties:`,
requestedWhitelistCapabilies);
console.warn(`Widget ${this.props.id} allowing requested, whitelisted properties: ` +
requestedWhitelistCapabilies,
);
}
}
// TODO -- Add UI to warn about and optionally allow requested capabilities
this.setState({
requestedCapabilities,
allowedCapabilities: this.state.allowedCapabilities.concat(requestedWhitelistCapabilies),
});
ActiveWidgetStore.setWidgetCapabilities(this.props.id, requestedWhitelistCapabilies);
if (this.props.onCapabilityRequest) {
this.props.onCapabilityRequest(requestedCapabilities);
@ -387,7 +388,7 @@ export default class AppTile extends React.Component {
});
}
_onWidgetAction(payload) {
_onAction(payload) {
if (payload.widgetId === this.props.id) {
switch (payload.action) {
case 'm.sticker':
@ -435,6 +436,11 @@ export default class AppTile extends React.Component {
console.warn('Revoking permission to load widget - ', this.state.widgetUrl);
localStorage.removeItem(this.state.widgetPermissionId);
this.setState({hasPermissionToLoad: false});
// Force the widget to be non-persistent
ActiveWidgetStore.destroyPersistentWidget();
const PersistedElement = sdk.getComponent("elements.PersistedElement");
PersistedElement.destroyElement(this._persistKey);
}
formatAppTileName() {
@ -527,6 +533,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">
@ -535,20 +543,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
@ -565,11 +573,24 @@ export default class AppTile extends React.Component {
></iframe>
</div>
);
// if the widget would be allowed to remian on screen, we must put it in
// a PersistedElement from the get-go, otherwise the iframe will be
// re-mounted later when we do.
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
const PersistedElement = sdk.getComponent("elements.PersistedElement");
// Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place
appTileBody = <div className="mx_AppTile_persistedWrapper">
<PersistedElement persistKey={this._persistKey}>
{appTileBody}
</PersistedElement>
</div>;
}
}
} 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}
@ -597,8 +618,17 @@ export default class AppTile extends React.Component {
const reloadWidgetIcon = 'img/button-refresh.svg';
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
let appTileClass;
if (this.props.miniMode) {
appTileClass = 'mx_AppTile_mini';
} else if (this.props.fullWidth) {
appTileClass = 'mx_AppTileFullWidth';
} else {
appTileClass = 'mx_AppTile';
}
return (
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
<div className={appTileClass} id={this.props.id}>
{ this.props.showMenubar &&
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
@ -682,6 +712,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
@ -734,4 +766,5 @@ AppTile.defaultProps = {
handleMinimisePointerEvents: false,
whitelistCapabilities: [],
userWidget: false,
miniMode: false,
};

View file

@ -139,8 +139,11 @@ module.exports = React.createClass({
</div>
{ editableItems }
{ this.props.canEdit ?
// This is slightly evil; we want a new instance of
// EditableItem when the list grows. To make sure it's
// reset to its initial state.
<EditableItem
key={-1}
key={editableItems.length}
initialValue={this.props.newItem}
onAdd={this.onItemAdded}
onChange={this.onNewItemChanged}

View file

@ -61,7 +61,7 @@ module.exports = React.createClass({
},
componentWillReceiveProps: function(nextProps) {
if (nextProps.initialValue !== this.props.initialValue || nextProps.initialValue !== this.value) {
if (nextProps.initialValue !== this.props.initialValue) {
this.value = nextProps.initialValue;
if (this.refs.editable_div) {
this.showPlaceholder(!this.value);

View file

@ -14,30 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const React = require('react');
const ReactDOM = require('react-dom');
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import ResizeObserver from 'resize-observer-polyfill';
import dis from '../../../dispatcher';
// Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and
// pass in a custom control as the actual body.
const ContainerId = "mx_PersistedElement";
function getContainer(containerId) {
return document.getElementById(containerId);
}
function getOrCreateContainer() {
let container = document.getElementById(ContainerId);
function getOrCreateContainer(containerId) {
let container = getContainer(containerId);
if (!container) {
container = document.createElement("div");
container.id = ContainerId;
container.id = containerId;
document.body.appendChild(container);
}
return container;
}
// Greater than that of the ContextualMenu
const PE_Z_INDEX = 5000;
/*
* Class of component that renders its children in a separate ReactDOM virtual tree
* in a container element appended to document.body.
@ -50,14 +54,57 @@ const PE_Z_INDEX = 5000;
* bounding rect as the parent of PE.
*/
export default class PersistedElement extends React.Component {
static propTypes = {
// Unique identifier for this PersistedElement instance
// Any PersistedElements with the same persistKey will use
// the same DOM container.
persistKey: PropTypes.string.isRequired,
};
constructor() {
super();
this.collectChildContainer = this.collectChildContainer.bind(this);
this.collectChild = this.collectChild.bind(this);
this._repositionChild = this._repositionChild.bind(this);
this._onAction = this._onAction.bind(this);
this.resizeObserver = new ResizeObserver(this._repositionChild);
// Annoyingly, a resize observer is insufficient, since we also care
// about when the element moves on the screen without changing its
// dimensions. Doesn't look like there's a ResizeObserver equivalent
// for this, so we bodge it by listening for document resize and
// the timeline_resize action.
window.addEventListener('resize', this._repositionChild);
this._dispatcherRef = dis.register(this._onAction);
}
/**
* Removes the DOM elements created when a PersistedElement with the given
* persistKey was mounted. The DOM elements will be re-added if another
* PeristedElement is mounted in the future.
*
* @param {string} persistKey Key used to uniquely identify this PersistedElement
*/
static destroyElement(persistKey) {
const container = getContainer('mx_persistedElement_' + persistKey);
if (container) {
container.remove();
}
}
static isMounted(persistKey) {
return Boolean(getContainer('mx_persistedElement_' + persistKey));
}
collectChildContainer(ref) {
if (this.childContainer) {
this.resizeObserver.unobserve(this.childContainer);
}
this.childContainer = ref;
if (ref) {
this.resizeObserver.observe(ref);
}
}
collectChild(ref) {
@ -75,6 +122,19 @@ export default class PersistedElement extends React.Component {
componentWillUnmount() {
this.updateChildVisibility(this.child, false);
this.resizeObserver.disconnect();
window.removeEventListener('resize', this._repositionChild);
dis.unregister(this._dispatcherRef);
}
_onAction(payload) {
if (payload.action === 'timeline_resize') {
this._repositionChild();
}
}
_repositionChild() {
this.updateChildPosition(this.child, this.childContainer);
}
updateChild() {
@ -97,18 +157,16 @@ export default class PersistedElement extends React.Component {
left: parentRect.left + 'px',
width: parentRect.width + 'px',
height: parentRect.height + 'px',
zIndex: PE_Z_INDEX,
});
}
render() {
const content = <div ref={this.collectChild}>
const content = <div ref={this.collectChild} style={this.props.style}>
{this.props.children}
</div>;
ReactDOM.render(content, getOrCreateContainer());
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
return <div ref={this.collectChildContainer}></div>;
}
}

View file

@ -0,0 +1,96 @@
/*
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(),
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
};
},
componentWillMount: function() {
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
},
componentWillUnmount: function() {
if (this._roomStoreToken) {
this._roomStoreToken.remove();
}
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
},
_onRoomViewStoreUpdate: function(payload) {
if (RoomViewStore.getRoomId() === this.state.roomId) return;
this.setState({
roomId: RoomViewStore.getRoomId(),
});
},
_onActiveWidgetStoreUpdate: function() {
this.setState({
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(),
});
},
render: function() {
if (this.state.persistentWidgetId) {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
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;
},
});

View file

@ -62,6 +62,8 @@ const Pill = React.createClass({
room: PropTypes.instanceOf(Room),
// Whether to include an avatar in the pill
shouldShowPillAvatar: PropTypes.bool,
// Whether to render this pill as if it were highlit by a selection
isSelected: PropTypes.bool,
},
@ -185,6 +187,9 @@ const Pill = React.createClass({
getContent: () => {
return {avatar_url: resp.avatar_url};
},
getDirectionalContent: function() {
return this.getContent();
},
};
this.setState({member});
}).catch((err) => {
@ -268,6 +273,7 @@ const Pill = React.createClass({
const classes = classNames(pillClass, {
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
"mx_UserPill_selected": this.props.isSelected,
});
if (this.state.pillType) {