diff --git a/res/css/_components.scss b/res/css/_components.scss
index 40a2c576d0..45c0443cfb 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -48,6 +48,7 @@
@import "./views/context_menus/_StatusMessageContextMenu.scss";
@import "./views/context_menus/_TagTileContextMenu.scss";
@import "./views/context_menus/_TopLeftMenu.scss";
+@import "./views/context_menus/_WidgetContextMenu.scss";
@import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss";
@import "./views/dialogs/_ChangelogDialog.scss";
diff --git a/res/css/views/context_menus/_WidgetContextMenu.scss b/res/css/views/context_menus/_WidgetContextMenu.scss
new file mode 100644
index 0000000000..60b7b93f99
--- /dev/null
+++ b/res/css/views/context_menus/_WidgetContextMenu.scss
@@ -0,0 +1,36 @@
+/*
+Copyright 2019 The Matrix.org Foundaction 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.
+*/
+
+.mx_WidgetContextMenu {
+ padding: 6px;
+
+ .mx_WidgetContextMenu_option {
+ padding: 3px 6px 3px 6px;
+ cursor: pointer;
+ white-space: nowrap;
+ }
+
+ .mx_WidgetContextMenu_separator {
+ margin-top: 0;
+ margin-bottom: 0;
+ border-bottom-style: none;
+ border-left-style: none;
+ border-right-style: none;
+ border-top-style: solid;
+ border-top-width: 1px;
+ border-color: $menu-border-color;
+ }
+}
diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss
index 6f5e3abade..a3fe573ad0 100644
--- a/res/css/views/rooms/_AppsDrawer.scss
+++ b/res/css/views/rooms/_AppsDrawer.scss
@@ -153,40 +153,12 @@ $AppsDrawerBodyHeight: 273px;
background-color: $accent-color;
}
-.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_reload {
- mask-image: url('$(res)/img/feather-customised/widget/refresh.svg');
- mask-size: 100%;
-}
-
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout {
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
}
-.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_snapshot {
- mask-image: url('$(res)/img/feather-customised/widget/camera.svg');
-}
-
-.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_edit {
- mask-image: url('$(res)/img/feather-customised/widget/edit.svg');
-}
-
-.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_delete {
- mask-image: url('$(res)/img/feather-customised/widget/bin.svg');
- background-color: $warning-color;
-}
-
-.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_cancel {
- mask-image: url('$(res)/img/feather-customised/widget/x-circle.svg');
-}
-
-/* delete ? */
-.mx_AppTileMenuBarWidget {
- cursor: pointer;
- width: 10px;
- height: 10px;
- padding: 1px;
- transition-duration: 500ms;
- border: 1px solid transparent;
+.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu {
+ mask-image: url('$(res)/img/icon_context.svg');
}
.mx_AppTileMenuBarWidgetDelete {
diff --git a/res/img/feather-customised/widget/bin.svg b/res/img/feather-customised/widget/bin.svg
deleted file mode 100644
index 7616d8931b..0000000000
--- a/res/img/feather-customised/widget/bin.svg
+++ /dev/null
@@ -1,65 +0,0 @@
-
-
diff --git a/res/img/feather-customised/widget/camera.svg b/res/img/feather-customised/widget/camera.svg
deleted file mode 100644
index 5502493068..0000000000
--- a/res/img/feather-customised/widget/camera.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/res/img/feather-customised/widget/edit.svg b/res/img/feather-customised/widget/edit.svg
deleted file mode 100644
index 749e83f982..0000000000
--- a/res/img/feather-customised/widget/edit.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/res/img/feather-customised/widget/refresh.svg b/res/img/feather-customised/widget/refresh.svg
deleted file mode 100644
index 0994bbdd52..0000000000
--- a/res/img/feather-customised/widget/refresh.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/res/img/feather-customised/widget/x-circle.svg b/res/img/feather-customised/widget/x-circle.svg
deleted file mode 100644
index 951407b39c..0000000000
--- a/res/img/feather-customised/widget/x-circle.svg
+++ /dev/null
@@ -1,6 +0,0 @@
-
diff --git a/src/components/views/context_menus/WidgetContextMenu.js b/src/components/views/context_menus/WidgetContextMenu.js
new file mode 100644
index 0000000000..43e7e172cc
--- /dev/null
+++ b/src/components/views/context_menus/WidgetContextMenu.js
@@ -0,0 +1,134 @@
+/*
+Copyright 2019 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 from 'react';
+import PropTypes from 'prop-types';
+import sdk from '../../../index';
+import {_t} from '../../../languageHandler';
+
+export default class WidgetContextMenu extends React.Component {
+ static propTypes = {
+ onFinished: PropTypes.func,
+
+ // Callback for when the revoke button is clicked. Required.
+ onRevokeClicked: PropTypes.func.isRequired,
+
+ // Callback for when the snapshot button is clicked. Button not shown
+ // without a callback.
+ onSnapshotClicked: PropTypes.func,
+
+ // Callback for when the reload button is clicked. Button not shown
+ // without a callback.
+ onReloadClicked: PropTypes.func,
+
+ // Callback for when the edit button is clicked. Button not shown
+ // without a callback.
+ onEditClicked: PropTypes.func,
+
+ // Callback for when the delete button is clicked. Button not shown
+ // without a callback.
+ onDeleteClicked: PropTypes.func,
+ };
+
+ proxyClick(fn) {
+ fn();
+ if (this.props.onFinished) this.props.onFinished();
+ }
+
+ // XXX: It's annoying that our context menus require us to hit onFinished() to close :(
+
+ onEditClicked = () => {
+ this.proxyClick(this.props.onEditClicked);
+ };
+
+ onReloadClicked = () => {
+ this.proxyClick(this.props.onReloadClicked);
+ };
+
+ onSnapshotClicked = () => {
+ this.proxyClick(this.props.onSnapshotClicked);
+ };
+
+ onDeleteClicked = () => {
+ this.proxyClick(this.props.onDeleteClicked);
+ };
+
+ onRevokeClicked = () => {
+ this.proxyClick(this.props.onRevokeClicked);
+ };
+
+ render() {
+ const AccessibleButton = sdk.getComponent("views.elements.AccessibleButton");
+
+ const options = [];
+
+ if (this.props.onEditClicked) {
+ options.push(
+
+ {_t("Edit")}
+ ,
+ );
+ }
+
+ if (this.props.onReloadClicked) {
+ options.push(
+
+ {_t("Reload")}
+ ,
+ );
+ }
+
+ if (this.props.onSnapshotClicked) {
+ options.push(
+
+ {_t("Take picture")}
+ ,
+ );
+ }
+
+ if (this.props.onDeleteClicked) {
+ options.push(
+
+ {_t("Remove for everyone")}
+ ,
+ );
+ }
+
+ // Push this last so it appears last. It's always present.
+ options.push(
+
+ {_t("Remove for me")}
+ ,
+ );
+
+ // Put separators between the options
+ if (options.length > 1) {
+ const length = options.length;
+ for (let i = 0; i < length - 1; i++) {
+ const sep =
;
+
+ // Insert backwards so the insertions don't affect our math on where to place them.
+ // We also use our cached length to avoid worrying about options.length changing
+ options.splice(length - 1 - i, 0, sep);
+ }
+ }
+
+ return
{options}
;
+ }
+}
diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js
index 2f30c6e78f..746631a99e 100644
--- a/src/components/views/elements/AppTile.js
+++ b/src/components/views/elements/AppTile.js
@@ -35,6 +35,7 @@ import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
import classNames from 'classnames';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
+import {createMenu} from "../../structures/ContextualMenu";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@@ -52,7 +53,7 @@ export default class AppTile extends React.Component {
this._onLoaded = this._onLoaded.bind(this);
this._onEditClick = this._onEditClick.bind(this);
this._onDeleteClick = this._onDeleteClick.bind(this);
- this._onCancelClick = this._onCancelClick.bind(this);
+ this._onRevokeClicked = this._onRevokeClicked.bind(this);
this._onSnapshotClick = this._onSnapshotClick.bind(this);
this.onClickMenuBar = this.onClickMenuBar.bind(this);
this._onMinimiseClick = this._onMinimiseClick.bind(this);
@@ -271,7 +272,7 @@ export default class AppTile extends React.Component {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
}
- _onEditClick(e) {
+ _onEditClick() {
console.log("Edit widget ID ", this.props.id);
if (this.props.onEditClick) {
this.props.onEditClick();
@@ -293,7 +294,7 @@ export default class AppTile extends React.Component {
}
}
- _onSnapshotClick(e) {
+ _onSnapshotClick() {
console.warn("Requesting widget snapshot");
ActiveWidgetStore.getWidgetMessaging(this.props.id).getScreenshot()
.catch((err) => {
@@ -360,13 +361,9 @@ export default class AppTile extends React.Component {
}
}
- _onCancelClick() {
- if (this.props.onDeleteClick) {
- this.props.onDeleteClick();
- } else {
- console.log("Revoke widget permissions - %s", this.props.id);
- this._revokeWidgetPermission();
- }
+ _onRevokeClicked() {
+ console.log("Revoke widget permissions - %s", this.props.id);
+ this._revokeWidgetPermission();
}
/**
@@ -544,18 +541,59 @@ export default class AppTile extends React.Component {
}
}
- _onPopoutWidgetClick(e) {
+ _onPopoutWidgetClick() {
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
Object.assign(document.createElement('a'),
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click();
}
- _onReloadWidgetClick(e) {
+ _onReloadWidgetClick() {
// Reload iframe in this way to avoid cross-origin restrictions
this.refs.appFrame.src = this.refs.appFrame.src;
}
+ _getMenuOptions(ev) {
+ // TODO: This block of code gets copy/pasted a lot. We should make that happen less.
+ const menuOptions = {};
+ const buttonRect = ev.target.getBoundingClientRect();
+ // The window X and Y offsets are to adjust position when zoomed in to page
+ const buttonLeft = buttonRect.left + window.pageXOffset;
+ const buttonTop = buttonRect.top + window.pageYOffset;
+ // Align the right edge of the menu to the left edge of the button
+ menuOptions.right = window.innerWidth - buttonLeft;
+ // Align the menu vertically on whichever side of the button has more
+ // space available.
+ if (buttonTop < window.innerHeight / 2) {
+ menuOptions.top = buttonTop;
+ } else {
+ menuOptions.bottom = window.innerHeight - buttonTop;
+ }
+ return menuOptions;
+ }
+
+ _onContextMenuClick = (ev) => {
+ const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
+ const menuOptions = {
+ ...this._getMenuOptions(ev),
+
+ // A revoke handler is always required
+ onRevokeClicked: this._onRevokeClicked,
+ };
+
+ const canUserModify = this._canUserModify();
+ const showEditButton = Boolean(this._scalarClient && canUserModify);
+ const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
+ const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
+
+ if (showEditButton) menuOptions.onEditClicked = this._onEditClick;
+ if (showDeleteButton) menuOptions.onDeleteClicked = this._onDeleteClick;
+ if (showPictureSnapshotButton) menuOptions.onSnapshotClicked = this._onSnapshotClick;
+ if (this.props.showReload) menuOptions.onReloadClicked = this._onReloadWidgetClick;
+
+ createMenu(WidgetContextMenu, menuOptions);
+ };
+
render() {
let appTileBody;
@@ -565,7 +603,7 @@ export default class AppTile extends React.Component {
}
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
- // because that would allow the iframe to prgramatically remove the sandbox attribute, but
+ // because that would allow the iframe to programmatically remove the sandbox attribute, but
// this would only be for content hosted on the same origin as the riot client: anything
// hosted on the same origin as the client will get the same access as if you clicked
// a link to it.
@@ -645,13 +683,6 @@ export default class AppTile extends React.Component {
}
}
- // editing is done in scalar
- const canUserModify = this._canUserModify();
- const showEditButton = Boolean(this._scalarClient && canUserModify);
- const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
- const showCancelButton = (this.props.showCancel === undefined || this.props.showCancel) && !showDeleteButton;
- // Picture snapshot - only show button when apps are maximised.
- const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
const showMinimiseButton = this.props.showMinimise && this.props.show;
const showMaximiseButton = this.props.showMinimise && !this.props.show;
@@ -690,41 +721,17 @@ export default class AppTile extends React.Component {
{ this.props.showTitle && this._getTileTitle() }
- { /* Reload widget */ }
- { this.props.showReload && }
{ /* Popout widget */ }
{ this.props.showPopout && }
- { /* Snapshot widget */ }
- { showPictureSnapshotButton && }
- { /* Edit widget */ }
- { showEditButton && }
- { /* Delete widget */ }
- { showDeleteButton && }
- { /* Cancel widget */ }
- { showCancelButton && }
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 8b71a1e182..ac0e43e604 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -1205,10 +1205,8 @@
"An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room",
"Minimize apps": "Minimize apps",
"Maximize apps": "Maximize apps",
- "Reload widget": "Reload widget",
"Popout widget": "Popout widget",
- "Picture": "Picture",
- "Revoke widget access": "Revoke widget access",
+ "More options": "More options",
"Create new room": "Create new room",
"Unblacklist": "Unblacklist",
"Blacklist": "Blacklist",
@@ -1565,6 +1563,10 @@
"Hide": "Hide",
"Home": "Home",
"Sign in": "Sign in",
+ "Reload": "Reload",
+ "Take picture": "Take picture",
+ "Remove for everyone": "Remove for everyone",
+ "Remove for me": "Remove for me",
"powered by Matrix": "powered by Matrix",
"This homeserver would like to make sure you are not a robot.": "This homeserver would like to make sure you are not a robot.",
"Custom Server Options": "Custom Server Options",