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 @@ - - - - - - image/svg+xml - - - - - - - - - - - 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",