From 12034411d4481652d840b0ee34330091b2f29a82 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Sep 2020 10:47:51 +0100 Subject: [PATCH 001/131] Fix right-alignment between sticky and non-sticky room list headers Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 090a64904c..b47d328965 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -139,7 +139,7 @@ export default class LeftPanel extends React.Component { const bottomEdge = list.offsetHeight + list.scrollTop; const sublists = list.querySelectorAll(".mx_RoomSublist"); - const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles + const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles const headerStickyWidth = list.clientWidth - headerRightMargin; // We track which styles we want on a target before making the changes to avoid From 7fd6d4498fa2279c2027e067bc2e61d1d2e2d17f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Sep 2020 11:00:19 +0100 Subject: [PATCH 002/131] Make bottom sticky room list header dynamic Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_RoomSublist.scss | 4 ---- src/components/structures/LeftPanel.tsx | 9 +++++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/res/css/views/rooms/_RoomSublist.scss b/res/css/views/rooms/_RoomSublist.scss index 543940fb78..82bba40167 100644 --- a/res/css/views/rooms/_RoomSublist.scss +++ b/res/css/views/rooms/_RoomSublist.scss @@ -59,10 +59,6 @@ limitations under the License. width: calc(100% - 22px); } - &.mx_RoomSublist_headerContainer_stickyBottom { - bottom: 0; - } - // We don't have a top style because the top is dependent on the room list header's // height, and is therefore calculated in JS. // The class, mx_RoomSublist_headerContainer_stickyTop, is applied though. diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index b47d328965..455e807bc9 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -210,10 +210,19 @@ export default class LeftPanel extends React.Component { if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) { header.classList.add("mx_RoomSublist_headerContainer_stickyBottom"); } + + const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight); + const newBottom = `${offset}px`; + if (header.style.bottom !== newBottom) { + header.style.bottom = newBottom; + } } else { if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) { header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom"); } + if (header.style.bottom) { + header.style.removeProperty('bottom'); + } } if (style.stickyTop || style.stickyBottom) { From c8b99b54e0f1161a91cfba9d39bc1b87caad2c85 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Sep 2020 11:00:53 +0100 Subject: [PATCH 003/131] Fix TS definitions Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/accessibility/RovingTabIndex.tsx | 2 +- src/hooks/useLocalStorageState.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index b1dbb56a01..bde626a96a 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -204,7 +204,7 @@ export const RovingTabIndexProvider: React.FC = ({children, handleHomeEn // onFocus should be called when the index gained focus in any manner // isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}` // ref should be passed to a DOM node which will be used for DOM compareDocumentPosition -export const useRovingTabIndex = (inputRef: Ref): [FocusHandler, boolean, Ref] => { +export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] => { const context = useContext(RovingTabIndexContext); let ref = useRef(null); diff --git a/src/hooks/useLocalStorageState.ts b/src/hooks/useLocalStorageState.ts index ce3b574f86..943db9ab4e 100644 --- a/src/hooks/useLocalStorageState.ts +++ b/src/hooks/useLocalStorageState.ts @@ -26,7 +26,7 @@ const getValue = (key: string, initialValue: T): T => { }; // Hook behaving like useState but persisting the value to localStorage. Returns same as useState -export const useLocalStorageState = (key: string, initialValue: T) => { +export const useLocalStorageState = (key: string, initialValue: T): [T, Dispatch>] => { const lsKey = "mx_" + key; const [value, setValue] = useState(getValue(lsKey, initialValue)); From 2e6bad8b07571aac2e304c3cebd61fa9f47991a8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Sep 2020 11:01:19 +0100 Subject: [PATCH 004/131] Convert WidgetUtils to TS and improve typing Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/stores/WidgetStore.ts | 5 ++ src/utils/{WidgetUtils.js => WidgetUtils.ts} | 71 +++++++++++--------- 2 files changed, 46 insertions(+), 30 deletions(-) rename src/utils/{WidgetUtils.js => WidgetUtils.ts} (90%) diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index 10327ce4e9..4e6e5e626c 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -31,11 +31,16 @@ interface IState {} export interface IApp { id: string; + url: string; type: string; + name: string; roomId: string; eventId: string; creatorUserId: string; waitForIframeLoad?: boolean; + data?: { + title?: string; + }; // eslint-disable-next-line camelcase avatar_url: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765 } diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.ts similarity index 90% rename from src/utils/WidgetUtils.js rename to src/utils/WidgetUtils.ts index d1daba7ca5..efdfbbb3cc 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.ts @@ -2,6 +2,7 @@ Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd Copyright 2019 Travis Ralston +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. @@ -16,15 +17,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import * as url from "url"; + import {MatrixClientPeg} from '../MatrixClientPeg'; import SdkConfig from "../SdkConfig"; import dis from '../dispatcher/dispatcher'; -import * as url from "url"; import WidgetEchoStore from '../stores/WidgetEchoStore'; - -// How long we wait for the state event echo to come back from the server -// before waitFor[Room/User]Widget rejects its promise -const WIDGET_WAIT_TIME = 20000; import SettingsStore from "../settings/SettingsStore"; import ActiveWidgetStore from "../stores/ActiveWidgetStore"; import {IntegrationManagers} from "../integrations/IntegrationManagers"; @@ -33,6 +31,19 @@ import {Room} from "matrix-js-sdk/src/models/room"; import {WidgetType} from "../widgets/WidgetType"; import {objectClone} from "./objects"; import {_t} from "../languageHandler"; +import {IApp} from "../stores/WidgetStore"; + +// How long we wait for the state event echo to come back from the server +// before waitFor[Room/User]Widget rejects its promise +const WIDGET_WAIT_TIME = 20000; + +export interface IWidget { + id: string; + type: string; + sender: string; + state_key: string; + content: IApp; +} export default class WidgetUtils { /* Returns true if user is able to send state events to modify widgets in this room @@ -41,7 +52,7 @@ export default class WidgetUtils { * @return Boolean -- true if the user can modify widgets in this room * @throws Error -- specifies the error reason */ - static canUserModifyWidgets(roomId) { + static canUserModifyWidgets(roomId: string): boolean { if (!roomId) { console.warn('No room ID specified'); return false; @@ -80,7 +91,7 @@ export default class WidgetUtils { * @param {[type]} testUrlString URL to check * @return {Boolean} True if specified URL is a scalar URL */ - static isScalarUrl(testUrlString) { + static isScalarUrl(testUrlString: string): boolean { if (!testUrlString) { console.error('Scalar URL check failed. No URL specified'); return false; @@ -123,7 +134,7 @@ export default class WidgetUtils { * @returns {Promise} that resolves when the widget is in the * requested state according to the `add` param */ - static waitForUserWidget(widgetId, add) { + static waitForUserWidget(widgetId: string, add: boolean): Promise { return new Promise((resolve, reject) => { // Tests an account data event, returning true if it's in the state // we're waiting for it to be in @@ -170,7 +181,7 @@ export default class WidgetUtils { * @returns {Promise} that resolves when the widget is in the * requested state according to the `add` param */ - static waitForRoomWidget(widgetId, roomId, add) { + static waitForRoomWidget(widgetId: string, roomId: string, add: boolean): Promise { return new Promise((resolve, reject) => { // Tests a list of state events, returning true if it's in the state // we're waiting for it to be in @@ -213,7 +224,7 @@ export default class WidgetUtils { }); } - static setUserWidget(widgetId, widgetType: WidgetType, widgetUrl, widgetName, widgetData) { + static setUserWidget(widgetId: string, widgetType: WidgetType, widgetUrl: string, widgetName: string, widgetData: object) { const content = { type: widgetType.preferred, url: widgetUrl, @@ -257,7 +268,7 @@ export default class WidgetUtils { }); } - static setRoomWidget(roomId, widgetId, widgetType: WidgetType, widgetUrl, widgetName, widgetData) { + static setRoomWidget(roomId: string, widgetId: string, widgetType: WidgetType, widgetUrl: string, widgetName: string, widgetData: object) { let content; const addingWidget = Boolean(widgetUrl); @@ -307,7 +318,7 @@ export default class WidgetUtils { * Get user specific widgets (not linked to a specific room) * @return {object} Event content object containing current / active user widgets */ - static getUserWidgets() { + static getUserWidgets(): Record { const client = MatrixClientPeg.get(); if (!client) { throw new Error('User not logged in'); @@ -323,7 +334,7 @@ export default class WidgetUtils { * Get user specific widgets (not linked to a specific room) as an array * @return {[object]} Array containing current / active user widgets */ - static getUserWidgetsArray() { + static getUserWidgetsArray(): IWidget[] { return Object.values(WidgetUtils.getUserWidgets()); } @@ -331,7 +342,7 @@ export default class WidgetUtils { * Get active stickerpicker widgets (stickerpickers are user widgets by nature) * @return {[object]} Array containing current / active stickerpicker widgets */ - static getStickerpickerWidgets() { + static getStickerpickerWidgets(): IWidget[] { const widgets = WidgetUtils.getUserWidgetsArray(); return widgets.filter((widget) => widget.content && widget.content.type === "m.stickerpicker"); } @@ -340,12 +351,12 @@ export default class WidgetUtils { * Get all integration manager widgets for this user. * @returns {Object[]} An array of integration manager user widgets. */ - static getIntegrationManagerWidgets() { + static getIntegrationManagerWidgets(): IWidget[] { const widgets = WidgetUtils.getUserWidgetsArray(); return widgets.filter(w => w.content && w.content.type === "m.integration_manager"); } - static getRoomWidgetsOfType(room: Room, type: WidgetType) { + static getRoomWidgetsOfType(room: Room, type: WidgetType): IWidget[] { const widgets = WidgetUtils.getRoomWidgets(room); return (widgets || []).filter(w => { const content = w.getContent(); @@ -353,14 +364,14 @@ export default class WidgetUtils { }); } - static removeIntegrationManagerWidgets() { + static removeIntegrationManagerWidgets(): Promise { const client = MatrixClientPeg.get(); if (!client) { throw new Error('User not logged in'); } const widgets = client.getAccountData('m.widgets'); if (!widgets) return; - const userWidgets = widgets.getContent() || {}; + const userWidgets: IWidget[] = widgets.getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { if (widget.content && widget.content.type === "m.integration_manager") { delete userWidgets[key]; @@ -369,7 +380,7 @@ export default class WidgetUtils { return client.setAccountData('m.widgets', userWidgets); } - static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string) { + static addIntegrationManagerWidget(name: string, uiUrl: string, apiUrl: string): Promise { return WidgetUtils.setUserWidget( "integration_manager_" + (new Date().getTime()), WidgetType.INTEGRATION_MANAGER, @@ -383,14 +394,14 @@ export default class WidgetUtils { * Remove all stickerpicker widgets (stickerpickers are user widgets by nature) * @return {Promise} Resolves on account data updated */ - static removeStickerpickerWidgets() { + static removeStickerpickerWidgets(): Promise { const client = MatrixClientPeg.get(); if (!client) { throw new Error('User not logged in'); } const widgets = client.getAccountData('m.widgets'); if (!widgets) return; - const userWidgets = widgets.getContent() || {}; + const userWidgets: Record = widgets.getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { if (widget.content && widget.content.type === 'm.stickerpicker') { delete userWidgets[key]; @@ -399,7 +410,7 @@ export default class WidgetUtils { return client.setAccountData('m.widgets', userWidgets); } - static makeAppConfig(appId, app, senderUserId, roomId, eventId) { + static makeAppConfig(appId: string, app: IApp, senderUserId: string, roomId: string | null, eventId: string): IApp { if (!senderUserId) { throw new Error("Widgets must be created by someone - provide a senderUserId"); } @@ -413,7 +424,7 @@ export default class WidgetUtils { return app; } - static getCapWhitelistForAppTypeInRoomId(appType, roomId) { + static getCapWhitelistForAppTypeInRoomId(appType: string, roomId: string): Capability[] { const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId); const capWhitelist = enableScreenshots ? [Capability.Screenshot] : []; @@ -429,7 +440,7 @@ export default class WidgetUtils { return capWhitelist; } - static getWidgetSecurityKey(widgetId, widgetUrl, isUserWidget) { + static getWidgetSecurityKey(widgetId: string, widgetUrl: string, isUserWidget: boolean): string { let widgetLocation = ActiveWidgetStore.getRoomId(widgetId); if (isUserWidget) { @@ -450,7 +461,7 @@ export default class WidgetUtils { return encodeURIComponent(`${widgetLocation}::${widgetUrl}`); } - static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string}={}) { + static getLocalJitsiWrapperUrl(opts: {forLocalRender?: boolean, auth?: string} = {}) { // NB. we can't just encodeURIComponent all of these because the $ signs need to be there const queryStringParts = [ 'conferenceDomain=$domain', @@ -466,7 +477,7 @@ export default class WidgetUtils { } const queryString = queryStringParts.join('&'); - let baseUrl = window.location; + let baseUrl = window.location.href; if (window.location.protocol !== "https:" && !opts.forLocalRender) { // Use an external wrapper if we're not locally rendering the widget. This is usually // the URL that will end up in the widget event, so we want to make sure it's relatively @@ -479,15 +490,15 @@ export default class WidgetUtils { return url.href; } - static getWidgetName(app) { + static getWidgetName(app?: IApp): string { return app?.name?.trim() || _t("Unknown App"); } - static getWidgetDataTitle(app) { + static getWidgetDataTitle(app?: IApp): string { return app?.data?.title?.trim() || ""; } - static editWidget(room, app) { + static editWidget(room: Room, app: IApp): void { // TODO: Open the right manager for the widget if (SettingsStore.getValue("feature_many_integration_managers")) { IntegrationManagers.sharedInstance().openAll(room, 'type_' + app.type, app.id); @@ -496,7 +507,7 @@ export default class WidgetUtils { } } - static snapshotWidget(app) { + static snapshotWidget(app: IApp): void { console.log("Requesting widget snapshot"); ActiveWidgetStore.getWidgetMessaging(app.id).getScreenshot().catch((err) => { console.error("Failed to get screenshot", err); From f699be971e72600f6ed6c3567d91cd6df8c6a7b4 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Sep 2020 11:02:41 +0100 Subject: [PATCH 005/131] Prepare AppTile to be usable outside of rooms for User Widgets Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/elements/AppTile.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 6aaeab060f..f74f23c09d 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -104,6 +104,7 @@ export default class AppTile extends React.Component { const hasPermissionToLoad = () => { if (this._usingLocalWidget()) return true; + if (!newProps.room) return true; // user widgets always have permissions const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", newProps.room.roomId); return !!currentlyAllowedWidgets[newProps.app.eventId]; }; @@ -419,7 +420,9 @@ export default class AppTile extends React.Component { ActiveWidgetStore.delWidgetMessaging(this.props.app.id); this._setupWidgetMessaging(); - ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId); + if (this.props.room) { + ActiveWidgetStore.setRoomId(this.props.app.id, this.props.room.roomId); + } this.setState({loading: false}); } @@ -589,7 +592,7 @@ export default class AppTile extends React.Component { const myUser = MatrixClientPeg.get().getUser(myUserId); const vars = Object.assign(targetData, this.props.app.data, { 'matrix_user_id': myUserId, - 'matrix_room_id': this.props.room.roomId, + 'matrix_room_id': this.props.room ? this.props.room.roomId : null, 'matrix_display_name': myUser ? myUser.displayName : myUserId, 'matrix_avatar_url': myUser ? MatrixClientPeg.get().mxcUrlToHttp(myUser.avatarUrl) : '', @@ -754,6 +757,7 @@ export default class AppTile extends React.Component { ); if (!this.state.hasPermissionToLoad) { + // only possible for room widgets, can assert this.props.room here const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId); appTileBody = (
@@ -904,7 +908,9 @@ AppTile.displayName = 'AppTile'; AppTile.propTypes = { app: PropTypes.object.isRequired, - room: PropTypes.object.isRequired, + // If room is not specified then it is an account level widget + // which bypasses permission prompts as it was added explicitly by that user + room: PropTypes.object, // Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer. // This should be set to true when there is only one widget in the app drawer, otherwise it should be false. fullWidth: PropTypes.bool, From 956a3bf69dbdd9460ce9bd8ae163b37794c651d6 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 24 Sep 2020 09:28:49 +0100 Subject: [PATCH 006/131] Implement Left Panel User Widget Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_components.scss | 1 + res/css/structures/_LeftPanelWidget.scss | 145 ++++++++++++++++ src/components/structures/LeftPanel.tsx | 2 + src/components/structures/LeftPanelWidget.tsx | 160 ++++++++++++++++++ src/settings/Settings.ts | 4 + src/utils/WidgetUtils.ts | 6 +- 6 files changed, 315 insertions(+), 3 deletions(-) create mode 100644 res/css/structures/_LeftPanelWidget.scss create mode 100644 src/components/structures/LeftPanelWidget.tsx diff --git a/res/css/_components.scss b/res/css/_components.scss index 35b4c1b965..992279910a 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -13,6 +13,7 @@ @import "./structures/_HeaderButtons.scss"; @import "./structures/_HomePage.scss"; @import "./structures/_LeftPanel.scss"; +@import "./structures/_LeftPanelWidget.scss"; @import "./structures/_MainSplit.scss"; @import "./structures/_MatrixChat.scss"; @import "./structures/_MyGroups.scss"; diff --git a/res/css/structures/_LeftPanelWidget.scss b/res/css/structures/_LeftPanelWidget.scss new file mode 100644 index 0000000000..4df651d7b6 --- /dev/null +++ b/res/css/structures/_LeftPanelWidget.scss @@ -0,0 +1,145 @@ +/* +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. +*/ + +.mx_LeftPanelWidget { + // largely based on RoomSublist + margin-left: 8px; + margin-bottom: 4px; + + .mx_LeftPanelWidget_headerContainer { + display: flex; + align-items: center; + + height: 24px; + color: $roomlist-header-color; + margin-top: 4px; + + .mx_LeftPanelWidget_stickable { + flex: 1; + max-width: 100%; + + display: flex; + align-items: center; + } + + .mx_LeftPanelWidget_headerText { + flex: 1; + max-width: calc(100% - 16px); + line-height: $font-16px; + font-size: $font-13px; + font-weight: 600; + + // Ellipsize any text overflow + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + + .mx_LeftPanelWidget_collapseBtn { + display: inline-block; + position: relative; + width: 14px; + height: 14px; + margin-right: 6px; + + &::before { + content: ''; + width: 18px; + height: 18px; + position: absolute; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + background-color: $roomlist-header-color; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + } + + &.mx_LeftPanelWidget_collapseBtn_collapsed::before { + transform: rotate(-90deg); + } + } + } + } + + .mx_LeftPanelWidget_resizeBox { + position: relative; + + display: flex; + flex-direction: column; + overflow: visible; // let the resize handle out + } + + .mx_AppTileFullWidth { + flex: 1 0 0; + overflow: hidden; + // need this to be flex otherwise the overflow hidden from above + // sometimes vertically centers the clipped list ... no idea why it would do this + // as the box model should be top aligned. Happens in both FF and Chromium + display: flex; + flex-direction: column; + box-sizing: border-box; + + mask-image: linear-gradient(0deg, transparent, black 4px); + } + + .mx_LeftPanelWidget_resizerHandle { + cursor: ns-resize; + border-radius: 3px; + + // Override styles from library + width: unset !important; + height: 4px !important; + + position: absolute; + top: -24px !important; // override from library - puts it in the margin-top of the headerContainer + + // Together, these make the bar 64px wide + // These are also overridden from the library + left: calc(50% - 32px) !important; + right: calc(50% - 32px) !important; + } + + &:hover .mx_LeftPanelWidget_resizerHandle { + opacity: 0.8; + background-color: $primary-fg-color; + } + + .mx_LeftPanelWidget_maximizeButton { + margin-left: 8px; + margin-right: 7px; + position: relative; + width: 24px; + height: 24px; + border-radius: 32px; + + &::before { + content: ''; + width: 16px; + height: 16px; + position: absolute; + top: 4px; + left: 4px; + mask-position: center; + mask-size: contain; + mask-repeat: no-repeat; + mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); + background: $muted-fg-color; + } + } +} + +.mx_LeftPanelWidget_maximizeButtonTooltip { + margin-top: -3px; +} diff --git a/src/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 455e807bc9..104430f049 100644 --- a/src/components/structures/LeftPanel.tsx +++ b/src/components/structures/LeftPanel.tsx @@ -38,6 +38,7 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { MatrixClientPeg } from "../../MatrixClientPeg"; import RoomListNumResults from "../views/rooms/RoomListNumResults"; +import LeftPanelWidget from "./LeftPanelWidget"; interface IProps { isMinimized: boolean; @@ -432,6 +433,7 @@ export default class LeftPanel extends React.Component { {roomList}
+ { !this.props.isMinimized && } ); diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx new file mode 100644 index 0000000000..d969c53bca --- /dev/null +++ b/src/components/structures/LeftPanelWidget.tsx @@ -0,0 +1,160 @@ +/* +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, useEffect, useMemo} from "react"; +import {Resizable} from "re-resizable"; +import classNames from "classnames"; + +import {_t} from "../../languageHandler"; +import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; +import AccessibleButton from "../views/elements/AccessibleButton"; +import {useRovingTabIndex} from "../../accessibility/RovingTabIndex"; +import {Key} from "../../Keyboard"; +import {useLocalStorageState} from "../../hooks/useLocalStorageState"; +import MatrixClientContext from "../../contexts/MatrixClientContext"; +import WidgetUtils, {IWidget} from "../../utils/WidgetUtils"; +import {useAccountData} from "../../hooks/useAccountData"; +import AppTile from "../views/elements/AppTile"; +import {useSettingValue} from "../../hooks/useSettings"; + +interface IProps { + onResize(): void; +} + +const MIN_HEIGHT = 100; +const MAX_HEIGHT = 500; +const INITIAL_HEIGHT = 280; + +const LeftPanelWidget: React.FC = ({ onResize }) => { + const cli = useContext(MatrixClientContext); + + const mWidgetsEvent = useAccountData>(cli, "m.widgets"); + const leftPanelWidgetId = useSettingValue("Widgets.leftPanel"); + const app = useMemo(() => { + if (!mWidgetsEvent || !leftPanelWidgetId) return null; + const widgetConfig = Object.values(mWidgetsEvent).find(w => w.id === leftPanelWidgetId); + if (!widgetConfig) return null; + + return WidgetUtils.makeAppConfig( + widgetConfig.state_key, + widgetConfig.content, + widgetConfig.sender, + null, + widgetConfig.id); + }, [cli, mWidgetsEvent, leftPanelWidgetId]); + + const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT); + const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true); + useEffect(onResize, [expanded]); + + const [onFocus, isActive, ref] = useRovingTabIndex(); + const tabIndex = isActive ? 0 : -1; + + if (!app) return null; + + let auxButton = null; + if (1) { + auxButton = ( + { + console.log("@@ Maximise Left Panel Widget") + }} + className="mx_LeftPanelWidget_maximizeButton" + tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip" + title={_t("Maximize")} + /> + ); + } + + let content; + if (expanded) { + content = { + setHeight(height + d.height); + }} + handleWrapperClass="mx_LeftPanelWidget_resizerHandles" + handleClasses={{top: "mx_LeftPanelWidget_resizerHandle"}} + className="mx_LeftPanelWidget_resizeBox" + enable={{ top: true }} + > + + ; + } + + return
+
{ + switch (ev.key) { + case Key.ARROW_LEFT: + ev.stopPropagation(); + setExpanded(false); + break; + case Key.ARROW_RIGHT: { + ev.stopPropagation(); + setExpanded(true); + break; + } + } + }} + > +
+ { + setExpanded(e => !e); + }} + > + + { WidgetUtils.getWidgetName(app) } + + { auxButton } +
+
+ + { content } +
; +}; + +export default LeftPanelWidget; diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 737c882919..dd638ea7a8 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -620,6 +620,10 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_ROOM_OR_ACCOUNT, default: {}, }, + "Widgets.leftPanel": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + default: null, + }, [UIFeature.AdvancedEncryption]: { supportedLevels: LEVELS_UI_FEATURE, default: true, diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index efdfbbb3cc..338b856edf 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -42,7 +42,7 @@ export interface IWidget { type: string; sender: string; state_key: string; - content: IApp; + content: Partial; } export default class WidgetUtils { @@ -410,7 +410,7 @@ export default class WidgetUtils { return client.setAccountData('m.widgets', userWidgets); } - static makeAppConfig(appId: string, app: IApp, senderUserId: string, roomId: string | null, eventId: string): IApp { + static makeAppConfig(appId: string, app: Partial, senderUserId: string, roomId: string | null, eventId: string): IApp { if (!senderUserId) { throw new Error("Widgets must be created by someone - provide a senderUserId"); } @@ -421,7 +421,7 @@ export default class WidgetUtils { app.eventId = eventId; app.name = app.name || app.type; - return app; + return app as IApp; } static getCapWhitelistForAppTypeInRoomId(appType: string, roomId: string): Capability[] { From f3e6f43c3d658a4c43b2f0f5c879fe3abc653cd2 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 24 Sep 2020 09:34:19 +0100 Subject: [PATCH 007/131] Improve maxHeight Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanelWidget.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx index d969c53bca..1f1a963f8f 100644 --- a/src/components/structures/LeftPanelWidget.tsx +++ b/src/components/structures/LeftPanelWidget.tsx @@ -35,7 +35,7 @@ interface IProps { } const MIN_HEIGHT = 100; -const MAX_HEIGHT = 500; +const MAX_HEIGHT = 500; // or 50% of the window height const INITIAL_HEIGHT = 280; const LeftPanelWidget: React.FC = ({ onResize }) => { @@ -85,7 +85,7 @@ const LeftPanelWidget: React.FC = ({ onResize }) => { content = { setHeight(height + d.height); From 0ef08945cf5ae26bb8c8ac0bfa432450eb3862cf Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 24 Sep 2020 09:38:05 +0100 Subject: [PATCH 008/131] i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/i18n/strings/en_EN.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d7360430ae..82e7f070aa 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2056,6 +2056,7 @@ "Explore Public Rooms": "Explore Public Rooms", "Create a Group Chat": "Create a Group Chat", "Explore rooms": "Explore rooms", + "Maximize": "Maximize", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", From c65d8be7d80b1943fd70aa5f0b3f4cfa472f7db0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 24 Sep 2020 09:49:30 +0100 Subject: [PATCH 009/131] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanelWidget.tsx | 33 +++++++------------ src/utils/WidgetUtils.ts | 26 +++++++++++++-- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx index 1f1a963f8f..f12c5ca2f9 100644 --- a/src/components/structures/LeftPanelWidget.tsx +++ b/src/components/structures/LeftPanelWidget.tsx @@ -54,7 +54,7 @@ const LeftPanelWidget: React.FC = ({ onResize }) => { widgetConfig.sender, null, widgetConfig.id); - }, [cli, mWidgetsEvent, leftPanelWidgetId]); + }, [mWidgetsEvent, leftPanelWidgetId]); const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT); const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true); @@ -65,21 +65,6 @@ const LeftPanelWidget: React.FC = ({ onResize }) => { if (!app) return null; - let auxButton = null; - if (1) { - auxButton = ( - { - console.log("@@ Maximise Left Panel Widget") - }} - className="mx_LeftPanelWidget_maximizeButton" - tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip" - title={_t("Maximize")} - /> - ); - } - let content; if (expanded) { content = = ({ onResize }) => { return
{ switch (ev.key) { case Key.ARROW_LEFT: @@ -149,7 +131,16 @@ const LeftPanelWidget: React.FC = ({ onResize }) => { })} /> { WidgetUtils.getWidgetName(app) } - { auxButton } + + {/* Code for the maximise button for once we have full screen widgets */} + {/* { + }} + className="mx_LeftPanelWidget_maximizeButton" + tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip" + title={_t("Maximize")} + />*/}
diff --git a/src/utils/WidgetUtils.ts b/src/utils/WidgetUtils.ts index 338b856edf..bcf4324d6f 100644 --- a/src/utils/WidgetUtils.ts +++ b/src/utils/WidgetUtils.ts @@ -41,6 +41,7 @@ export interface IWidget { id: string; type: string; sender: string; + // eslint-disable-next-line camelcase state_key: string; content: Partial; } @@ -224,7 +225,13 @@ export default class WidgetUtils { }); } - static setUserWidget(widgetId: string, widgetType: WidgetType, widgetUrl: string, widgetName: string, widgetData: object) { + static setUserWidget( + widgetId: string, + widgetType: WidgetType, + widgetUrl: string, + widgetName: string, + widgetData: object, + ) { const content = { type: widgetType.preferred, url: widgetUrl, @@ -268,7 +275,14 @@ export default class WidgetUtils { }); } - static setRoomWidget(roomId: string, widgetId: string, widgetType: WidgetType, widgetUrl: string, widgetName: string, widgetData: object) { + static setRoomWidget( + roomId: string, + widgetId: string, + widgetType: WidgetType, + widgetUrl: string, + widgetName: string, + widgetData: object, + ) { let content; const addingWidget = Boolean(widgetUrl); @@ -410,7 +424,13 @@ export default class WidgetUtils { return client.setAccountData('m.widgets', userWidgets); } - static makeAppConfig(appId: string, app: Partial, senderUserId: string, roomId: string | null, eventId: string): IApp { + static makeAppConfig( + appId: string, + app: Partial, + senderUserId: string, + roomId: string | null, + eventId: string, + ): IApp { if (!senderUserId) { throw new Error("Widgets must be created by someone - provide a senderUserId"); } From f7959fe64f36edd1b7178cb8b4dd512a1d2063a8 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 24 Sep 2020 09:55:42 +0100 Subject: [PATCH 010/131] i18n and delint one last time Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/LeftPanelWidget.tsx | 2 -- src/i18n/strings/en_EN.json | 1 - 2 files changed, 3 deletions(-) diff --git a/src/components/structures/LeftPanelWidget.tsx b/src/components/structures/LeftPanelWidget.tsx index f12c5ca2f9..e8dfe955a8 100644 --- a/src/components/structures/LeftPanelWidget.tsx +++ b/src/components/structures/LeftPanelWidget.tsx @@ -18,8 +18,6 @@ import React, {useContext, useEffect, useMemo} from "react"; import {Resizable} from "re-resizable"; import classNames from "classnames"; -import {_t} from "../../languageHandler"; -import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleButton from "../views/elements/AccessibleButton"; import {useRovingTabIndex} from "../../accessibility/RovingTabIndex"; import {Key} from "../../Keyboard"; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 82e7f070aa..d7360430ae 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2056,7 +2056,6 @@ "Explore Public Rooms": "Explore Public Rooms", "Create a Group Chat": "Create a Group Chat", "Explore rooms": "Explore rooms", - "Maximize": "Maximize", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", 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 011/131] 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 012/131] 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 013/131] 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 014/131] 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 015/131] 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 016/131] 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 } -