From ed30750f6382ecd3bf1bc4458d53706b859722a0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Sep 2020 15:38:35 +0100 Subject: [PATCH 01/51] Extract RoomWidgetContextMenu Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/ContextMenu.tsx | 5 +- .../context_menus/RoomWidgetContextMenu.tsx | 79 +++++++++++++++++++ .../views/right_panel/WidgetCard.tsx | 50 +----------- 3 files changed, 86 insertions(+), 48 deletions(-) create mode 100644 src/components/views/context_menus/RoomWidgetContextMenu.tsx diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx index 884f77aba5..fa0d6682dd 100644 --- a/src/components/structures/ContextMenu.tsx +++ b/src/components/structures/ContextMenu.tsx @@ -416,8 +416,9 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None return menuOptions; }; -export const useContextMenu = (): [boolean, RefObject, () => void, () => void, (val: boolean) => void] => { - const button = useRef(null); +type ContextMenuTuple = [boolean, RefObject, () => void, () => void, (val: boolean) => void]; +export const useContextMenu = (): ContextMenuTuple => { + const button = useRef(null); const [isOpen, setIsOpen] = useState(false); const open = () => { setIsOpen(true); diff --git a/src/components/views/context_menus/RoomWidgetContextMenu.tsx b/src/components/views/context_menus/RoomWidgetContextMenu.tsx new file mode 100644 index 0000000000..5d5e88197e --- /dev/null +++ b/src/components/views/context_menus/RoomWidgetContextMenu.tsx @@ -0,0 +1,79 @@ +/* +Copyright 2020 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, {useContext} from "react"; + +import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu"; +import {ChevronFace} from "../../structures/ContextMenu"; +import {_t} from "../../../languageHandler"; +import {IApp} from "../../../stores/WidgetStore"; +import defaultDispatcher from "../../../dispatcher/dispatcher"; +import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload"; +import {Action} from "../../../dispatcher/actions"; +import {Capability} from "../../../widgets/WidgetApi"; +import WidgetUtils from "../../../utils/WidgetUtils"; +import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; +import RoomContext from "../../../contexts/RoomContext"; + +interface IProps extends React.ComponentProps { + app: IApp; +} + +const RoomWidgetContextMenu: React.FC = ({ onFinished, app, ...props}) => { + const {roomId} = useContext(RoomContext); + + let snapshotButton; + if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) { + const onSnapshotClick = () => { + WidgetUtils.snapshotWidget(app); + onFinished(); + }; + + snapshotButton = ; + } + + let deleteButton; + if (WidgetUtils.canUserModifyWidgets(roomId)) { + const onDeleteClick = () => { + defaultDispatcher.dispatch({ + action: Action.AppTileDelete, + widgetId: app.id, + }); + onFinished(); + }; + + deleteButton = ; + } + + const onRevokeClick = () => { + defaultDispatcher.dispatch({ + action: Action.AppTileRevoke, + widgetId: app.id, + }); + onFinished(); + }; + + return + + { snapshotButton } + { deleteButton } + + + ; +}; + +export default RoomWidgetContextMenu; + diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index 1677494708..230e71c000 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -29,16 +29,10 @@ import defaultDispatcher from "../../../dispatcher/dispatcher"; import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import {Action} from "../../../dispatcher/actions"; import WidgetStore from "../../../stores/WidgetStore"; -import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu"; -import IconizedContextMenu, { - IconizedContextMenuOption, - IconizedContextMenuOptionList, -} from "../context_menus/IconizedContextMenu"; -import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload"; -import {Capability} from "../../../widgets/WidgetApi"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import classNames from "classnames"; +import RoomWidgetContextMenu from "../context_menus/RoomWidgetContextMenu"; interface IProps { room: Room; @@ -76,51 +70,15 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { let contextMenu; if (menuDisplayed) { - let snapshotButton; - if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) { - const onSnapshotClick = () => { - WidgetUtils.snapshotWidget(app); - closeMenu(); - }; - - snapshotButton = ; - } - - let deleteButton; - if (canModify) { - const onDeleteClick = () => { - defaultDispatcher.dispatch({ - action: Action.AppTileDelete, - widgetId: app.id, - }); - closeMenu(); - }; - - deleteButton = ; - } - - const onRevokeClick = () => { - defaultDispatcher.dispatch({ - action: Action.AppTileRevoke, - widgetId: app.id, - }); - closeMenu(); - }; - const rect = handle.current.getBoundingClientRect(); contextMenu = ( - - - { snapshotButton } - { deleteButton } - - - + app={app} + /> ); } From 23d95df30b72314494e02d6fcf92c1979298f2db Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Sep 2020 17:08:41 +0100 Subject: [PATCH 02/51] Rebuild the room summary card's widgets section Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/right_panel/_RoomSummaryCard.scss | 54 ++++++- .../views/right_panel/RoomSummaryCard.tsx | 150 +++++++++++------- src/i18n/strings/en_EN.json | 12 +- 3 files changed, 144 insertions(+), 72 deletions(-) diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index 0031d3a64c..8878302435 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -109,9 +109,57 @@ limitations under the License. } .mx_RoomSummaryCard_appsGroup { + .mx_RoomSummaryCard_widgetRow { + margin: 0; + display: flex; + + .mx_RoomSummaryCard_app_pinToggle, + .mx_RoomSummaryCard_app_options { + position: relative; + height: 20px; + width: 20px; + padding: 10px; + border-radius: 8px; + + &:hover { + background-color: rgba(141, 151, 165, 0.1); + } + + &::before { + content: ''; + position: absolute; + height: 20px; + width: 20px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: 16px; + background-color: $icon-button-color; + } + } + + .mx_RoomSummaryCard_app_pinToggle { + &::before { + mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); + } + + &.mx_RoomSummaryCard_app_pinned { + &::before { + background-color: $accent-color; + } + } + } + + .mx_RoomSummaryCard_app_options { + &::before { + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + } + } + } + .mx_RoomSummaryCard_Button { padding-left: 12px; color: $tertiary-fg-color; + flex: 1; span { color: $primary-fg-color; @@ -127,12 +175,6 @@ limitations under the License. content: unset; } } - - .mx_RoomSummaryCard_icon_app_pinned::after { - mask-image: url('$(res)/img/element-icons/room/pin-upright.svg'); - background-color: $accent-color; - transform: unset; - } } .mx_AccessibleButton_kind_link { diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 95b159deed..b821ca2bbd 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -43,6 +43,9 @@ import WidgetStore, {IApp} from "../../../stores/WidgetStore"; import { E2EStatus } from "../../../utils/ShieldUtils"; import RoomContext from "../../../contexts/RoomContext"; import {UIFeature} from "../../../settings/UIFeature"; +import {ContextMenuButton} from "../../../accessibility/context_menu/ContextMenuButton"; +import {ChevronFace, useContextMenu} from "../../structures/ContextMenu"; +import RoomWidgetContextMenu from "../context_menus/RoomWidgetContextMenu"; interface IProps { room: Room; @@ -82,8 +85,93 @@ export const useWidgets = (room: Room) => { return apps; }; -const AppsSection: React.FC = ({ room }) => { +interface IAppRowProps { + app: IApp; +} + +const AppRow: React.FC = ({ app }) => { const cli = useContext(MatrixClientContext); + + const name = WidgetUtils.getWidgetName(app); + const dataTitle = WidgetUtils.getWidgetDataTitle(app); + const subtitle = dataTitle && " - " + dataTitle; + + let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")]; + // heuristics for some better icons until Widgets support their own icons + if (app.type.includes("meeting") || app.type.includes("calendar")) { + iconUrls = [require("../../../../res/img/element-icons/room/default_cal.svg")]; + } else if (app.type.includes("pad") || app.type.includes("doc") || app.type.includes("calc")) { + iconUrls = [require("../../../../res/img/element-icons/room/default_doc.svg")]; + } else if (app.type.includes("clock")) { + iconUrls = [require("../../../../res/img/element-icons/room/default_clock.svg")]; + } + + if (app.avatar_url) { // MSC2765 + iconUrls.unshift(getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop")); + } + + const onOpenWidgetClick = () => { + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.Widget, + refireParams: { + widgetId: app.id, + }, + }); + }; + + const isPinned = WidgetStore.instance.isPinned(app.id); + const togglePin = isPinned + ? () => { WidgetStore.instance.unpinWidget(app.id); } + : () => { WidgetStore.instance.pinWidget(app.id); }; + + const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); + let contextMenu; + if (menuDisplayed) { + const rect = handle.current.getBoundingClientRect(); + contextMenu = ; + } + + return
+ + + {name} + { subtitle } + + + + + + + { contextMenu } +
; +}; + +const AppsSection: React.FC = ({ room }) => { const apps = useWidgets(room); const onManageIntegrations = () => { @@ -100,65 +188,7 @@ const AppsSection: React.FC = ({ room }) => { }; return - { apps.map(app => { - const name = WidgetUtils.getWidgetName(app); - const dataTitle = WidgetUtils.getWidgetDataTitle(app); - const subtitle = dataTitle && " - " + dataTitle; - - let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")]; - // heuristics for some better icons until Widgets support their own icons - if (app.type.includes("meeting") || app.type.includes("calendar")) { - iconUrls = [require("../../../../res/img/element-icons/room/default_cal.svg")]; - } else if (app.type.includes("pad") || app.type.includes("doc") || app.type.includes("calc")) { - iconUrls = [require("../../../../res/img/element-icons/room/default_doc.svg")]; - } else if (app.type.includes("clock")) { - iconUrls = [require("../../../../res/img/element-icons/room/default_clock.svg")]; - } - - if (app.avatar_url) { // MSC2765 - iconUrls.unshift(getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop")); - } - - const isPinned = WidgetStore.instance.isPinned(app.id); - const classes = classNames("mx_RoomSummaryCard_icon_app", { - mx_RoomSummaryCard_icon_app_pinned: isPinned, - }); - - if (isPinned) { - const onClick = () => { - WidgetStore.instance.unpinWidget(app.id); - }; - - return - - {name} - { subtitle } - - } - - const onOpenWidgetClick = () => { - defaultDispatcher.dispatch({ - action: Action.SetRightPanelPhase, - phase: RightPanelPhases.Widget, - refireParams: { - widgetId: app.id, - }, - }); - }; - - return ( - - ); - }) } + { apps.map(app => ) } { apps.length > 0 ? _t("Edit widgets, bridges & bots") : _t("Add widgets, bridges & bots") } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 65374ea3ec..f1c8317c3c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1274,8 +1274,10 @@ "Yours, or the other users’ session": "Yours, or the other users’ session", "Members": "Members", "Room Info": "Room Info", + "You can't view pinned widgets in the right panel": "You can't view pinned widgets in the right panel", + "Unpin": "Unpin", + "Options": "Options", "Widgets": "Widgets", - "Unpin app": "Unpin app", "Edit widgets, bridges & bots": "Edit widgets, bridges & bots", "Add widgets, bridges & bots": "Add widgets, bridges & bots", "Not encrypted": "Not encrypted", @@ -1298,7 +1300,6 @@ "Invite": "Invite", "Share Link to User": "Share Link to User", "Direct message": "Direct message", - "Options": "Options", "Demote yourself?": "Demote yourself?", "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.", "Demote": "Demote", @@ -1362,9 +1363,6 @@ "You cancelled verification.": "You cancelled verification.", "Verification cancelled": "Verification cancelled", "Compare emoji": "Compare emoji", - "Take a picture": "Take a picture", - "Remove for everyone": "Remove for everyone", - "Remove for me": "Remove for me", "Edit": "Edit", "Pin to room": "Pin to room", "You can only pin 2 widgets at a time": "You can only pin 2 widgets at a time", @@ -1924,12 +1922,14 @@ "Source URL": "Source URL", "Collapse Reply Thread": "Collapse Reply Thread", "Report Content": "Report Content", + "Take a picture": "Take a picture", + "Remove for everyone": "Remove for everyone", + "Remove for me": "Remove for me", "Clear status": "Clear status", "Update status": "Update status", "Set status": "Set status", "Set a new status...": "Set a new status...", "View Community": "View Community", - "Unpin": "Unpin", "Reload": "Reload", "Take picture": "Take picture", "This room is public": "This room is public", From edfef2df0b40d469ed5a18528c956677c89c9021 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 30 Sep 2020 17:09:45 +0100 Subject: [PATCH 03/51] Increase max pinned widgets to 3 Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/stores/WidgetStore.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index 10327ce4e9..26e3f70b57 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -45,6 +45,8 @@ interface IRoomWidgets { pinned: Record; } +const MAX_PINNED = 3; + // TODO consolidate WidgetEchoStore into this // TODO consolidate ActiveWidgetStore into this export default class WidgetStore extends AsyncStoreWithClient { @@ -169,7 +171,7 @@ export default class WidgetStore extends AsyncStoreWithClient { const roomInfo = this.getRoom(roomId); return roomInfo && Object.keys(roomInfo.pinned).filter(k => { return roomInfo.widgets.some(app => app.id === k); - }).length < 2; + }).length < MAX_PINNED; } public pinWidget(widgetId: string) { From 29e3881fb3bfc02ba091c3673f82f5b6f12fa0f7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Oct 2020 10:36:45 +0100 Subject: [PATCH 04/51] Iterate design and fix post-merge conflict Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/right_panel/_BaseCard.scss | 7 +++++++ res/css/views/right_panel/_RoomSummaryCard.scss | 16 +++++++++++----- .../context_menus/RoomWidgetContextMenu.tsx | 8 +++++--- .../views/right_panel/RoomSummaryCard.tsx | 2 +- src/i18n/strings/en_EN.json | 2 +- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/res/css/views/right_panel/_BaseCard.scss b/res/css/views/right_panel/_BaseCard.scss index 26f846fe0a..b254b651e8 100644 --- a/res/css/views/right_panel/_BaseCard.scss +++ b/res/css/views/right_panel/_BaseCard.scss @@ -129,6 +129,13 @@ limitations under the License. mask-size: 20px; mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); } + + &.mx_AccessibleButton_disabled { + padding: 10px 12px; + &::after { + content: unset; + } + } } } diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index 8878302435..73fcc255e3 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -116,9 +116,9 @@ limitations under the License. .mx_RoomSummaryCard_app_pinToggle, .mx_RoomSummaryCard_app_options { position: relative; - height: 20px; - width: 20px; - padding: 10px; + height: 16px; + width: 16px; + padding: 8px; border-radius: 8px; &:hover { @@ -128,8 +128,8 @@ limitations under the License. &::before { content: ''; position: absolute; - height: 20px; - width: 20px; + height: 16px; + width: 16px; mask-repeat: no-repeat; mask-position: center; mask-size: 16px; @@ -158,6 +158,8 @@ limitations under the License. .mx_RoomSummaryCard_Button { padding-left: 12px; + padding-top: 6px; + padding-bottom: 6px; color: $tertiary-fg-color; flex: 1; @@ -174,6 +176,10 @@ limitations under the License. &::before { content: unset; } + + &::after { + top: 6px; // re-align based on the height change + } } } diff --git a/src/components/views/context_menus/RoomWidgetContextMenu.tsx b/src/components/views/context_menus/RoomWidgetContextMenu.tsx index 5d5e88197e..1757498f4d 100644 --- a/src/components/views/context_menus/RoomWidgetContextMenu.tsx +++ b/src/components/views/context_menus/RoomWidgetContextMenu.tsx @@ -15,6 +15,7 @@ limitations under the License. */ import React, {useContext} from "react"; +import {MatrixCapabilities} from "matrix-widget-api"; import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu"; import {ChevronFace} from "../../structures/ContextMenu"; @@ -23,9 +24,8 @@ import {IApp} from "../../../stores/WidgetStore"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload"; import {Action} from "../../../dispatcher/actions"; -import {Capability} from "../../../widgets/WidgetApi"; import WidgetUtils from "../../../utils/WidgetUtils"; -import ActiveWidgetStore from "../../../stores/ActiveWidgetStore"; +import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore"; import RoomContext from "../../../contexts/RoomContext"; interface IProps extends React.ComponentProps { @@ -35,8 +35,10 @@ interface IProps extends React.ComponentProps { const RoomWidgetContextMenu: React.FC = ({ onFinished, app, ...props}) => { const {roomId} = useContext(RoomContext); + const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id); + let snapshotButton; - if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) { + if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) { const onSnapshotClick = () => { WidgetUtils.snapshotWidget(app); onFinished(); diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index b821ca2bbd..0c6e002d2b 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -143,7 +143,7 @@ const AppRow: React.FC = ({ app }) => { className="mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_app" onClick={onOpenWidgetClick} // only show a tooltip if the widget is pinned - title={isPinned ? _t("You can't view pinned widgets in the right panel") : ""} + title={isPinned ? _t("Unpin a widget to view it in this panel") : ""} forceHide={!isPinned} disabled={isPinned} > diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 44b410d9a9..ea287894a5 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1273,7 +1273,7 @@ "Yours, or the other users’ session": "Yours, or the other users’ session", "Members": "Members", "Room Info": "Room Info", - "You can't view pinned widgets in the right panel": "You can't view pinned widgets in the right panel", + "Unpin a widget to view it in this panel": "Unpin a widget to view it in this panel", "Unpin": "Unpin", "Options": "Options", "Widgets": "Widgets", From dbb011b8f1d555e4193fc1e1b06106110c471e03 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 7 Oct 2020 10:53:17 +0100 Subject: [PATCH 05/51] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/right_panel/WidgetCard.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/views/right_panel/WidgetCard.tsx b/src/components/views/right_panel/WidgetCard.tsx index 104a9b9878..230e71c000 100644 --- a/src/components/views/right_panel/WidgetCard.tsx +++ b/src/components/views/right_panel/WidgetCard.tsx @@ -32,9 +32,6 @@ import WidgetStore from "../../../stores/WidgetStore"; import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import classNames from "classnames"; -import dis from "../../../dispatcher/dispatcher"; -import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; -import { MatrixCapabilities } from "matrix-widget-api"; import RoomWidgetContextMenu from "../context_menus/RoomWidgetContextMenu"; interface IProps { From ada6d1aa46ee909eb27e055faa255193a3416d05 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 9 Oct 2020 08:42:21 +0100 Subject: [PATCH 06/51] Iterate PR Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_components.scss | 1 - .../context_menus/_WidgetContextMenu.scss | 36 -- res/css/views/right_panel/_BaseCard.scss | 2 +- .../views/right_panel/_RoomSummaryCard.scss | 4 +- res/css/views/right_panel/_WidgetCard.scss | 49 ++- res/css/views/rooms/_AppsDrawer.scss | 33 +- res/css/views/rooms/_RoomHeader.scss | 7 + res/img/element-icons/room/apps.svg | 6 + res/img/element-icons/room/integrations.svg | 3 - res/img/icon_context.svg | 5 - src/components/structures/RoomView.tsx | 33 +- .../context_menus/RoomWidgetContextMenu.tsx | 77 +++- .../views/context_menus/WidgetContextMenu.js | 142 ------ src/components/views/elements/AppTile.js | 413 +++++------------- .../views/elements/PersistedElement.js | 2 + .../views/elements/PersistentApp.js | 3 - .../views/right_panel/WidgetCard.tsx | 59 +-- src/components/views/rooms/AppsDrawer.js | 35 +- src/components/views/rooms/RoomHeader.js | 14 + src/components/views/rooms/Stickerpicker.js | 3 - src/dispatcher/actions.ts | 10 - .../payloads/AppTileActionPayload.ts | 23 - src/i18n/strings/en_EN.json | 8 +- src/utils/WidgetUtils.js | 12 + 24 files changed, 273 insertions(+), 707 deletions(-) delete mode 100644 res/css/views/context_menus/_WidgetContextMenu.scss create mode 100644 res/img/element-icons/room/apps.svg delete mode 100644 res/img/element-icons/room/integrations.svg delete mode 100644 res/img/icon_context.svg delete mode 100644 src/components/views/context_menus/WidgetContextMenu.js delete mode 100644 src/dispatcher/payloads/AppTileActionPayload.ts diff --git a/res/css/_components.scss b/res/css/_components.scss index 06cdbdcb4b..e103bd90ce 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -55,7 +55,6 @@ @import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_TagTileContextMenu.scss"; -@import "./views/context_menus/_WidgetContextMenu.scss"; @import "./views/dialogs/_AddressPickerDialog.scss"; @import "./views/dialogs/_Analytics.scss"; @import "./views/dialogs/_BugReportDialog.scss"; diff --git a/res/css/views/context_menus/_WidgetContextMenu.scss b/res/css/views/context_menus/_WidgetContextMenu.scss deleted file mode 100644 index 60b7b93f99..0000000000 --- a/res/css/views/context_menus/_WidgetContextMenu.scss +++ /dev/null @@ -1,36 +0,0 @@ -/* -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/right_panel/_BaseCard.scss b/res/css/views/right_panel/_BaseCard.scss index cfc8b335dd..9a5a59bda8 100644 --- a/res/css/views/right_panel/_BaseCard.scss +++ b/res/css/views/right_panel/_BaseCard.scss @@ -130,7 +130,7 @@ limitations under the License. } &.mx_AccessibleButton_disabled { - padding: 10px 12px; + padding-right: 12px; &::after { content: unset; } diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index 73fcc255e3..ab7807d2a2 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -157,9 +157,7 @@ limitations under the License. } .mx_RoomSummaryCard_Button { - padding-left: 12px; - padding-top: 6px; - padding-bottom: 6px; + padding: 6px 24px 6px 12px; color: $tertiary-fg-color; flex: 1; diff --git a/res/css/views/right_panel/_WidgetCard.scss b/res/css/views/right_panel/_WidgetCard.scss index 315fd5213c..a90e744a5a 100644 --- a/res/css/views/right_panel/_WidgetCard.scss +++ b/res/css/views/right_panel/_WidgetCard.scss @@ -24,34 +24,35 @@ limitations under the License. border: 0; } - &.mx_WidgetCard_noEdit { - .mx_AccessibleButton_kind_secondary { - margin: 0 12px; + .mx_BaseCard_header { + display: inline-flex; - &:first-child { - // expand the Pin to room primary action - flex-grow: 1; - } + & > h2 { + margin-right: 0; + flex-grow: 1; } - } - .mx_WidgetCard_optionsButton { - position: relative; - height: 18px; - width: 26px; - - &::before { - content: ""; - position: absolute; - width: 20px; + .mx_WidgetCard_optionsButton { + position: relative; + margin-right: 44px; height: 20px; - top: 6px; - left: 20px; - mask-repeat: no-repeat; - mask-position: center; - mask-size: contain; - mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); - background-color: $secondary-fg-color; + width: 20px; + min-width: 20px; // prevent crushing by the flexbox + padding: 0; + + &::before { + content: ""; + position: absolute; + width: 20px; + height: 20px; + top: 0; + left: 4px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); + background-color: $secondary-fg-color; + } } } } diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss index 451704bd88..79892fa7a2 100644 --- a/res/css/views/rooms/_AppsDrawer.scss +++ b/res/css/views/rooms/_AppsDrawer.scss @@ -118,12 +118,6 @@ $MiniAppTileHeight: 200px; height: $MiniAppTileHeight; } -.mx_AppTile.mx_AppTile_minimised, -.mx_AppTileFullWidth.mx_AppTile_minimised, -.mx_AppTile_mini.mx_AppTile_minimised { - height: 14px; -} - .mx_AppTile .mx_AppTile_persistedWrapper, .mx_AppTileFullWidth .mx_AppTile_persistedWrapper, .mx_AppTile_mini .mx_AppTile_persistedWrapper { @@ -143,11 +137,7 @@ $MiniAppTileHeight: 200px; flex-direction: row; align-items: center; justify-content: space-between; - cursor: pointer; width: 100%; -} - -.mx_AppTileMenuBar_expanded { padding-bottom: 5px; } @@ -179,31 +169,12 @@ $MiniAppTileHeight: 200px; margin: 0 3px; } -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_minimise { - mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); - background-color: $accent-color; -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_maximise { - mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); - background-color: $accent-color; -} - -.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout { +.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout { // TODO replace icon mask-image: url('$(res)/img/feather-customised/widget/external-link.svg'); } .mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu { - mask-image: url('$(res)/img/icon_context.svg'); -} - -.mx_AppTileMenuBarWidgetDelete { - filter: none; -} - -.mx_AppTileMenuBarWidget:hover { - border: 1px solid $primary-fg-color; - border-radius: 2px; + mask-image: url('$(res)/img/element-icons/room/ellipsis.svg'); } .mx_AppTileBody { diff --git a/res/css/views/rooms/_RoomHeader.scss b/res/css/views/rooms/_RoomHeader.scss index d240877507..a23a44906f 100644 --- a/res/css/views/rooms/_RoomHeader.scss +++ b/res/css/views/rooms/_RoomHeader.scss @@ -241,6 +241,13 @@ limitations under the License. width: 26px; } +.mx_RoomHeader_appsButton::before { + mask-image: url('$(res)/img/element-icons/room/apps.svg'); +} +.mx_RoomHeader_appsButton_highlight::before { + background-color: $accent-color; +} + .mx_RoomHeader_searchButton::before { mask-image: url('$(res)/img/element-icons/room/search-inset.svg'); } diff --git a/res/img/element-icons/room/apps.svg b/res/img/element-icons/room/apps.svg new file mode 100644 index 0000000000..c90704752c --- /dev/null +++ b/res/img/element-icons/room/apps.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/img/element-icons/room/integrations.svg b/res/img/element-icons/room/integrations.svg deleted file mode 100644 index 3a39506411..0000000000 --- a/res/img/element-icons/room/integrations.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/img/icon_context.svg b/res/img/icon_context.svg deleted file mode 100644 index 600c5bbd1d..0000000000 --- a/res/img/icon_context.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index fcb2d274c1..f245e89208 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -71,6 +71,8 @@ import RoomHeader from "../views/rooms/RoomHeader"; import TintableSvg from "../views/elements/TintableSvg"; import {XOR} from "../../@types/common"; import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; +import WidgetStore from "../../stores/WidgetStore"; +import {UPDATE_EVENT} from "../../stores/AsyncStore"; const DEBUG = false; let debuglog = function(msg: string) {}; @@ -180,6 +182,7 @@ export interface IState { e2eStatus?: E2EStatus; rejecting?: boolean; rejectError?: Error; + hasPinnedWidgets: boolean; } export default class RoomView extends React.Component { @@ -231,6 +234,7 @@ export default class RoomView extends React.Component { canReply: false, useIRCLayout: SettingsStore.getValue("useIRCLayout"), matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), + hasPinnedWidgets: false, }; this.dispatcherRef = dis.register(this.onAction); @@ -250,7 +254,9 @@ export default class RoomView extends React.Component { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate); - WidgetEchoStore.on('update', this.onWidgetEchoStoreUpdate); + WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); + WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate); + this.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null, this.onReadReceiptsChange); this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange); @@ -262,6 +268,18 @@ export default class RoomView extends React.Component { this.onRoomViewStoreUpdate(true); } + private onWidgetStoreUpdate = () => { + if (this.state.room) { + this.checkWidgets(this.state.room); + } + } + + private checkWidgets = (room) => { + this.setState({ + hasPinnedWidgets: WidgetStore.instance.getApps(room, true).length > 0, + }) + }; + private onReadReceiptsChange = () => { this.setState({ showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId), @@ -584,7 +602,8 @@ export default class RoomView extends React.Component { this.rightPanelStoreToken.remove(); } - WidgetEchoStore.removeListener('update', this.onWidgetEchoStoreUpdate); + WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate); + WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate); if (this.showReadReceiptsWatchRef) { SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef); @@ -828,6 +847,7 @@ export default class RoomView extends React.Component { this.calculateRecommendedVersion(room); this.updateE2EStatus(room); this.updatePermissions(room); + this.checkWidgets(room); }; private async calculateRecommendedVersion(room: Room) { @@ -1357,6 +1377,13 @@ export default class RoomView extends React.Component { dis.fire(Action.FocusComposer); }; + private onAppsClick = () => { + dis.dispatch({ + action: "appsDrawer", // TODO should this go into the RVS? + show: !this.state.showApps, + }); + }; + private onLeaveClick = () => { dis.dispatch({ action: 'leave_room', @@ -2060,6 +2087,8 @@ export default class RoomView extends React.Component { onForgetClick={(myMembership === "leave") ? this.onForgetClick : null} onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null} e2eStatus={this.state.e2eStatus} + onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null} + appsShown={this.state.showApps} />
diff --git a/src/components/views/context_menus/RoomWidgetContextMenu.tsx b/src/components/views/context_menus/RoomWidgetContextMenu.tsx index 1757498f4d..e904605faa 100644 --- a/src/components/views/context_menus/RoomWidgetContextMenu.tsx +++ b/src/components/views/context_menus/RoomWidgetContextMenu.tsx @@ -20,27 +20,58 @@ import {MatrixCapabilities} from "matrix-widget-api"; import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu"; import {ChevronFace} from "../../structures/ContextMenu"; import {_t} from "../../../languageHandler"; -import {IApp} from "../../../stores/WidgetStore"; -import defaultDispatcher from "../../../dispatcher/dispatcher"; -import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload"; -import {Action} from "../../../dispatcher/actions"; +import WidgetStore, {IApp} from "../../../stores/WidgetStore"; import WidgetUtils from "../../../utils/WidgetUtils"; import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore"; import RoomContext from "../../../contexts/RoomContext"; +import dis from "../../../dispatcher/dispatcher"; +import SettingsStore from "../../../settings/SettingsStore"; +import {SettingLevel} from "../../../settings/SettingLevel"; +import Modal from "../../../Modal"; +import QuestionDialog from "../dialogs/QuestionDialog"; interface IProps extends React.ComponentProps { app: IApp; + showUnpin?: boolean; + // override delete handler + onDeleteClick?(): void; } -const RoomWidgetContextMenu: React.FC = ({ onFinished, app, ...props}) => { - const {roomId} = useContext(RoomContext); +const RoomWidgetContextMenu: React.FC = ({ onFinished, app, onDeleteClick, showUnpin, ...props}) => { + const {room, roomId} = useContext(RoomContext); const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id); + const canModify = WidgetUtils.canUserModifyWidgets(roomId); + + let unpinButton; + if (showUnpin) { + const onUnpinClick = () => { + WidgetStore.instance.unpinWidget(app.id); + }; + + unpinButton = ; + } + + let editButton; + if (canModify && WidgetUtils.isManagedByManager(app)) { + const onEditClick = () => { + WidgetUtils.editWidget(room, app); + }; + + editButton = + } let snapshotButton; if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) { const onSnapshotClick = () => { - WidgetUtils.snapshotWidget(app); + widgetMessaging?.takeScreenshot().then(data => { + dis.dispatch({ + action: 'picture_snapshot', + file: data.screenshot, + }); + }).catch(err => { + console.error("Failed to take screenshot: ", err); + }); onFinished(); }; @@ -48,29 +79,45 @@ const RoomWidgetContextMenu: React.FC = ({ onFinished, app, ...props}) = } let deleteButton; - if (WidgetUtils.canUserModifyWidgets(roomId)) { + if (onDeleteClick || canModify) { const onDeleteClick = () => { - defaultDispatcher.dispatch({ - action: Action.AppTileDelete, - widgetId: app.id, + // Show delete confirmation dialog + Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, { + title: _t("Delete Widget"), + description: _t( + "Deleting a widget removes it for all users in this room." + + " Are you sure you want to delete this widget?"), + button: _t("Delete widget"), + onFinished: (confirmed) => { + if (!confirmed) return; + WidgetUtils.setRoomWidget(roomId, app.id); + }, }); onFinished(); }; - deleteButton = ; + deleteButton = ; } const onRevokeClick = () => { - defaultDispatcher.dispatch({ - action: Action.AppTileRevoke, - widgetId: app.id, + console.info("Revoking permission for widget to load: " + app.eventId); + const current = SettingsStore.getValue("allowedWidgets", roomId); + current[app.eventId] = false; + SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).catch(err => { + console.error(err); + // We don't really need to do anything about this - the user will just hit the button again. }); onFinished(); }; return + { unpinButton } { snapshotButton } + { editButton } { deleteButton } diff --git a/src/components/views/context_menus/WidgetContextMenu.js b/src/components/views/context_menus/WidgetContextMenu.js deleted file mode 100644 index 6ed32daa5c..0000000000 --- a/src/components/views/context_menus/WidgetContextMenu.js +++ /dev/null @@ -1,142 +0,0 @@ -/* -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 {_t} from '../../../languageHandler'; -import {MenuItem} from "../../structures/ContextMenu"; - -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 unpin button is clicked. If absent, unpin will be hidden. - onUnpinClicked: PropTypes.func, - - // 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); - }; - - onUnpinClicked = () => this.proxyClick(this.props.onUnpinClicked); - - render() { - const options = []; - - if (this.props.onEditClicked) { - options.push( - - {_t("Edit")} - , - ); - } - - if (this.props.onUnpinClicked) { - options.push( - - {_t("Unpin")} - , - ); - } - - 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 3405d4ff16..a60c18cb0d 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,55 +22,48 @@ import React, {createRef} from 'react'; import PropTypes from 'prop-types'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import AccessibleButton from './AccessibleButton'; -import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; import AppPermission from './AppPermission'; import AppWarning from './AppWarning'; import Spinner from './Spinner'; -import WidgetUtils from '../../../utils/WidgetUtils'; import dis from '../../../dispatcher/dispatcher'; import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import classNames from 'classnames'; import SettingsStore from "../../../settings/SettingsStore"; -import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; -import PersistedElement from "./PersistedElement"; +import {aboveLeftOf, ContextMenuButton} from "../../structures/ContextMenu"; +import PersistedElement, {getPersistKey} from "./PersistedElement"; import {WidgetType} from "../../../widgets/WidgetType"; import {SettingLevel} from "../../../settings/SettingLevel"; -import WidgetStore from "../../../stores/WidgetStore"; -import {Action} from "../../../dispatcher/actions"; import {StopGapWidget} from "../../../stores/widgets/StopGapWidget"; import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions"; import {MatrixCapabilities} from "matrix-widget-api"; +import RoomWidgetContextMenu from "../context_menus/RoomWidgetContextMenu"; export default class AppTile extends React.Component { constructor(props) { super(props); // The key used for PersistedElement - this._persistKey = 'widget_' + this.props.app.id; + this._persistKey = getPersistKey(this.props.app.id); this._sgWidget = new StopGapWidget(this.props); this._sgWidget.on("ready", this._onWidgetReady); this.iframe = null; // ref to the iframe (callback style) this.state = this._getNewState(props); - - this._onAction = this._onAction.bind(this); - this._onEditClick = this._onEditClick.bind(this); - this._onDeleteClick = this._onDeleteClick.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); - this._grantWidgetPermission = this._grantWidgetPermission.bind(this); - this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this); - this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this); - this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this); - this._contextMenuButton = createRef(); - this._menu_bar = createRef(); + + this._allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange); } + // This is a function to make the impact of calling SettingsStore slightly less + hasPermissionToLoad = (props) => { + if (this._usingLocalWidget()) return true; + + const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId); + return !!currentlyAllowedWidgets[props.app.eventId]; + }; + /** * Set initial component state when the App wUrl (widget URL) is being updated. * Component props *must* be passed (rather than relying on this.props). @@ -78,28 +71,35 @@ export default class AppTile extends React.Component { * @return {Object} Updated component state to be set with setState */ _getNewState(newProps) { - // This is a function to make the impact of calling SettingsStore slightly less - const hasPermissionToLoad = () => { - if (this._usingLocalWidget()) return true; - - const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId); - return !!currentlyAllowedWidgets[newProps.app.eventId]; - }; - return { initialising: true, // True while we are mangling the widget URL // True while the iframe content is loading loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey), // Assume that widget has permission to load if we are the user who // added it to the room, or if explicitly granted by the user - hasPermissionToLoad: newProps.userId === newProps.creatorUserId || hasPermissionToLoad(), + hasPermissionToLoad: newProps.userId === newProps.creatorUserId || this.hasPermissionToLoad(newProps), error: null, - deleting: false, widgetPageTitle: newProps.widgetPageTitle, menuDisplayed: false, }; } + onAllowedWidgetsChange = () => { + const hasPermissionToLoad = + this.props.userId === this.prop.creatorUserId || this.hasPermissionToLoad(this.props); + + if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { + // Force the widget to be non-persistent (able to be deleted/forgotten) + ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); + PersistedElement.destroyElement(this._persistKey); + this._sgWidget.stop(); + } + + this.setState({ + hasPermissionToLoad, + }); + }; + isMixedContent() { const parentContentProtocol = window.location.protocol; const u = url.parse(this.props.app.url); @@ -114,7 +114,7 @@ export default class AppTile extends React.Component { componentDidMount() { // Only fetch IM token on mount if we're showing and have permission to load - if (this.props.show && this.state.hasPermissionToLoad) { + if (this.state.hasPermissionToLoad) { this._startWidget(); } @@ -135,6 +135,8 @@ export default class AppTile extends React.Component { if (this._sgWidget) { this._sgWidget.stop(); } + + SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef); } _resetWidget(newProps) { @@ -165,21 +167,8 @@ export default class AppTile extends React.Component { UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase if (nextProps.app.url !== this.props.app.url) { this._getNewState(nextProps); - if (this.props.show && this.state.hasPermissionToLoad) { - this._resetWidget(nextProps); - } - } - - if (nextProps.show && !this.props.show) { - // We assume that persisted widgets are loaded and don't need a spinner. - if (this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey)) { - this.setState({ - loading: true, - }); - } - // Start the widget now that we're showing if we already have permission to load if (this.state.hasPermissionToLoad) { - this._startWidget(); + this._resetWidget(nextProps); } } @@ -190,35 +179,6 @@ export default class AppTile extends React.Component { } } - _canUserModify() { - // User widgets should always be modifiable by their creator - if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) { - return true; - } - // Check if the current user can modify widgets in the current room - return WidgetUtils.canUserModifyWidgets(this.props.room.roomId); - } - - _onEditClick() { - console.log("Edit widget ID ", this.props.app.id); - if (this.props.onEditClick) { - this.props.onEditClick(); - } else { - WidgetUtils.editWidget(this.props.room, this.props.app); - } - } - - _onSnapshotClick() { - this._sgWidget.widgetApi.takeScreenshot().then(data => { - dis.dispatch({ - action: 'picture_snapshot', - file: data.screenshot, - }); - }).catch(err => { - console.error("Failed to take screenshot: ", err); - }); - } - /** * Ends all widget interaction, such as cancelling calls and disabling webcams. * @private @@ -244,57 +204,6 @@ export default class AppTile extends React.Component { this._sgWidget.stop(); } - /* If user has permission to modify widgets, delete the widget, - * otherwise revoke access for the widget to load in the user's browser - */ - _onDeleteClick() { - if (this.props.onDeleteClick) { - this.props.onDeleteClick(); - } else if (this._canUserModify()) { - // Show delete confirmation dialog - const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, { - title: _t("Delete Widget"), - description: _t( - "Deleting a widget removes it for all users in this room." + - " Are you sure you want to delete this widget?"), - button: _t("Delete widget"), - onFinished: (confirmed) => { - if (!confirmed) { - return; - } - this.setState({deleting: true}); - - this._endWidgetActions().then(() => { - return WidgetUtils.setRoomWidget( - this.props.room.roomId, - this.props.app.id, - ); - }).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}); - }); - }, - }); - } - } - - _onUnpinClicked = () => { - WidgetStore.instance.unpinWidget(this.props.app.id); - } - - _onRevokeClicked() { - console.info("Revoke widget permissions - %s", this.props.app.id); - this._revokeWidgetPermission(); - } - _onWidgetReady = () => { this.setState({loading: false}); if (WidgetType.JITSI.matches(this.props.app.type)) { @@ -302,7 +211,7 @@ export default class AppTile extends React.Component { } }; - _onAction(payload) { + _onAction = payload => { if (payload.widgetId === this.props.app.id) { switch (payload.action) { case 'm.sticker': @@ -312,19 +221,11 @@ export default class AppTile extends React.Component { console.warn('Ignoring sticker message. Invalid capability'); } break; - - case Action.AppTileDelete: - this._onDeleteClick(); - break; - - case Action.AppTileRevoke: - this._onRevokeClicked(); - break; } } - } + }; - _grantWidgetPermission() { + _grantWidgetPermission = () => { const roomId = this.props.room.roomId; console.info("Granting permission for widget to load: " + this.props.app.eventId); const current = SettingsStore.getValue("allowedWidgets", roomId); @@ -338,26 +239,7 @@ export default class AppTile extends React.Component { console.error(err); // We don't really need to do anything about this - the user will just hit the button again. }); - } - - _revokeWidgetPermission() { - const roomId = this.props.room.roomId; - console.info("Revoking permission for widget to load: " + this.props.app.eventId); - const current = SettingsStore.getValue("allowedWidgets", roomId); - current[this.props.app.eventId] = false; - SettingsStore.setValue("allowedWidgets", roomId, SettingLevel.ROOM_ACCOUNT, current).then(() => { - this.setState({hasPermissionToLoad: false}); - - // Force the widget to be non-persistent (able to be deleted/forgotten) - ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); - const PersistedElement = sdk.getComponent("elements.PersistedElement"); - PersistedElement.destroyElement(this._persistKey); - this._sgWidget.stop(); - }).catch(err => { - console.error(err); - // We don't really need to do anything about this - the user will just hit the button again. - }); - } + }; formatAppTileName() { let appTileName = "No name"; @@ -367,29 +249,6 @@ export default class AppTile extends React.Component { return appTileName; } - onClickMenuBar(ev) { - ev.preventDefault(); - - // Ignore clicks on menu bar children - if (ev.target !== this._menu_bar.current) { - return; - } - - // Toggle the view state of the apps drawer - if (this.props.userWidget) { - this._onMinimiseClick(); - } else { - if (this.props.show) { - // if we were being shown, end the widget as we're about to be minimized. - this._endWidgetActions(); - } - dis.dispatch({ - action: 'appsDrawer', - show: !this.props.show, - }); - } - } - /** * Whether we're using a local version of the widget rather than loading the * actual widget URL @@ -415,16 +274,11 @@ export default class AppTile extends React.Component { ); } - _onMinimiseClick(e) { - if (this.props.onMinimiseClick) { - this.props.onMinimiseClick(); - } - } - - _onPopoutWidgetClick() { + // TODO replace with full screen interactions + _onPopoutWidgetClick = () => { // Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them // twice from the same computer, which Jitsi can have problems with (audio echo/gain-loop). - if (WidgetType.JITSI.matches(this.props.app.type) && this.props.show) { + if (WidgetType.JITSI.matches(this.props.app.type)) { this._endWidgetActions().then(() => { if (this.iframe) { // Reload iframe @@ -437,13 +291,7 @@ export default class AppTile extends React.Component { // window.open(this._getPopoutUrl(), '_blank', 'noopener=yes'); Object.assign(document.createElement('a'), { target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener'}).click(); - } - - _onReloadWidgetClick() { - // Reload iframe in this way to avoid cross-origin restrictions - // eslint-disable-next-line no-self-assign - this.iframe.src = this.iframe.src; - } + }; _onContextMenuClick = () => { this.setState({ menuDisplayed: true }); @@ -456,11 +304,6 @@ export default class AppTile extends React.Component { render() { let appTileBody; - // Don't render widget if it is in the process of being deleted - if (this.state.deleting) { - return
; - } - // Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin // 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 element client: anything @@ -475,71 +318,66 @@ export default class AppTile extends React.Component { const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' '); - if (this.props.show) { - const loadingElement = ( -
- + const loadingElement = ( +
+ +
+ ); + if (!this.state.hasPermissionToLoad) { + const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); + appTileBody = ( +
+
); - if (!this.state.hasPermissionToLoad) { - const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); + } else if (this.state.initialising) { + appTileBody = ( +
+ { loadingElement } +
+ ); + } else { + if (this.isMixedContent()) { appTileBody = (
- -
- ); - } else if (this.state.initialising) { - appTileBody = ( -
- { loadingElement } +
); } else { - if (this.isMixedContent()) { - appTileBody = ( -
- -
- ); - } else { - appTileBody = ( -
- { this.state.loading && loadingElement } -