diff --git a/package.json b/package.json
index c58e84fa81..111fb97387 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss
index 96ed5878ac..bedec0e363 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 28d432686d..6431853672 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 cf69727b15..e287abd07a 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 da8c558cb5..80f899f8e6 100644
--- a/src/components/views/rooms/AppsDrawer.js
+++ b/src/components/views/rooms/AppsDrawer.js
@@ -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 (
);
}
- return null;
+ const PersistentApp = sdk.getComponent('elements.PersistentApp');
+ return ;
},
});
diff --git a/src/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js
index 7c179aef84..01d5f15601 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 ab5b5b0130..98239b3cec 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;
+ }
}