diff --git a/package.json b/package.json
index 8c0ec922f2e..570f57abb55 100644
--- a/package.json
+++ b/package.json
@@ -87,6 +87,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",
"sanitize-html": "^1.14.1",
"text-encoding-utf-8": "^1.0.1",
"url": "^0.11.0",
diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss
index 96ed5878ac4..bedec0e3630 100644
--- a/res/css/structures/_LeftPanel.scss
+++ b/res/css/structures/_LeftPanel.scss
@@ -54,6 +54,10 @@ limitations under the License.
}
+.mx_LeftPanel .mx_AppTileFullWidth {
+ height: 132px;
+}
+
.mx_LeftPanel .mx_RoomList_scrollbar {
order: 1;
diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss
index 28d432686d8..64318536721 100644
--- a/res/css/views/rooms/_AppsDrawer.scss
+++ b/res/css/views/rooms/_AppsDrawer.scss
@@ -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;
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index cf69727b154..e287abd07ac 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -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 = (
@@ -530,20 +534,20 @@ export default class AppTile extends React.Component {
);
if (this.state.initialising) {
appTileBody = (
-
+
{ loadingElement }
);
} else if (this.state.hasPermissionToLoad == true) {
if (this.isMixedContent()) {
appTileBody = (
-
+
);
} else {
appTileBody = (
-
+
{ 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 = (
-
+
{
+ 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 ;
+ }
+ }
+ return null;
+ },
+});
+
diff --git a/src/components/views/rooms/AppsDrawer.js b/src/components/views/rooms/AppsDrawer.js
index da8c558cb55..aa086f82605 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -107,55 +107,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 +116,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 +164,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 (
);
}
- return null;
+ const PersistentApp = sdk.getComponent('elements.PersistentApp');
+ return ;
},
});
diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js
index 7c179aef841..01d5f156015 100644
--- a/src/stores/ActiveWidgetStore.js
+++ b/src/stores/ActiveWidgetStore.js
@@ -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) {
diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.js
index ab5b5b01307..98239b3cecc 100644
--- a/src/utils/WidgetUtils.js
+++ b/src/utils/WidgetUtils.js
@@ -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;
+ }
}