diff --git a/res/css/_components.scss b/res/css/_components.scss index e3f03dabb6..ad3cfbdcea 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/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/accessibility/RovingTabIndex.tsx b/src/accessibility/RovingTabIndex.tsx index 434b931296..b49a90d175 100644 --- a/src/accessibility/RovingTabIndex.tsx +++ b/src/accessibility/RovingTabIndex.tsx @@ -205,7 +205,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/components/structures/LeftPanel.tsx b/src/components/structures/LeftPanel.tsx index 00653995c9..4445ff3ff8 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; @@ -142,7 +143,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 @@ -213,10 +214,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) { @@ -425,6 +435,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..4daec76d08 --- /dev/null +++ b/src/components/structures/LeftPanelWidget.tsx @@ -0,0 +1,149 @@ +/* +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 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, {IWidgetEvent} 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; // or 50% of the window height +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); + }, [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 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) } + + + {/* Code for the maximise button for once we have full screen widgets */} + {/* { + }} + className="mx_LeftPanelWidget_maximizeButton" + tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip" + title={_t("Maximize")} + />*/} +
+
+ + { content } +
; +}; + +export default LeftPanelWidget; diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 063f333298..f3ebe24c15 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -61,6 +61,7 @@ export default class AppTile extends React.Component { // This is a function to make the impact of calling SettingsStore slightly less hasPermissionToLoad = (props) => { if (this._usingLocalWidget()) return true; + if (!props.room) return true; // user widgets always have permissions const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId); if (currentlyAllowedWidgets[props.app.eventId] === undefined) { @@ -335,6 +336,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 = (
@@ -446,7 +448,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, 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)); diff --git a/src/integrations/IntegrationManagers.ts b/src/integrations/IntegrationManagers.ts index 1791e92514..a29c74c5eb 100644 --- a/src/integrations/IntegrationManagers.ts +++ b/src/integrations/IntegrationManagers.ts @@ -120,7 +120,7 @@ export class IntegrationManagers { if (!data) return; const uiUrl = w.content['url']; - const apiUrl = data['api_url']; + const apiUrl = data['api_url'] as string; if (!apiUrl || !uiUrl) return; const manager = new IntegrationManagerInstance( diff --git a/src/settings/Settings.ts b/src/settings/Settings.ts index 8b96a2e819..c8430a1767 100644 --- a/src/settings/Settings.ts +++ b/src/settings/Settings.ts @@ -626,6 +626,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/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index 952cd49606..1caea659ca 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -16,6 +16,7 @@ limitations under the License. import { Room } from "matrix-js-sdk/src/models/room"; import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { IWidget } from "matrix-widget-api"; import { ActionPayload } from "../dispatcher/payloads"; import { AsyncStoreWithClient } from "./AsyncStoreWithClient"; @@ -31,13 +32,9 @@ import {UPDATE_EVENT} from "./AsyncStore"; interface IState {} -export interface IApp { - id: string; - type: string; +export interface IApp extends IWidget { roomId: string; eventId: string; - creatorUserId: string; - waitForIframeLoad?: boolean; // eslint-disable-next-line camelcase avatar_url: string; // MSC2765 https://github.com/matrix-org/matrix-doc/pull/2765 } diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 17302d0ab9..edce39d033 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -74,7 +74,7 @@ class ElementWidget extends Widget { if (WidgetType.JITSI.matches(this.type)) { return WidgetUtils.getLocalJitsiWrapperUrl({ forLocalRender: true, - auth: super.rawData?.auth, // this.rawData can call templateUrl, do this to prevent looping + auth: super.rawData?.auth as string, // this.rawData can call templateUrl, do this to prevent looping }); } return super.templateUrl; @@ -84,7 +84,7 @@ class ElementWidget extends Widget { if (WidgetType.JITSI.matches(this.type)) { return WidgetUtils.getLocalJitsiWrapperUrl({ forLocalRender: false, // The only important difference between this and templateUrl() - auth: super.rawData?.auth, + auth: super.rawData?.auth as string, }); } return this.templateUrl; // use this instead of super to ensure we get appropriate templating diff --git a/src/utils/WidgetUtils.js b/src/utils/WidgetUtils.ts similarity index 89% rename from src/utils/WidgetUtils.js rename to src/utils/WidgetUtils.ts index 93d132efaf..526c2d5ce7 100644 --- a/src/utils/WidgetUtils.js +++ b/src/utils/WidgetUtils.ts @@ -1,7 +1,6 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2018 New Vector Ltd Copyright 2019 Travis Ralston +Copyright 2017 - 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 +15,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"; @@ -32,7 +28,21 @@ import {Room} from "matrix-js-sdk/src/models/room"; import {WidgetType} from "../widgets/WidgetType"; import {objectClone} from "./objects"; import {_t} from "../languageHandler"; -import {MatrixCapabilities} from "matrix-widget-api"; +import {Capability, IWidgetData, MatrixCapabilities} from "matrix-widget-api"; +import {IApp} from "../stores/WidgetStore"; // TODO @@ + +// 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 IWidgetEvent { + id: string; + type: string; + sender: string; + // eslint-disable-next-line camelcase + state_key: string; + content: Partial; +} export default class WidgetUtils { /* Returns true if user is able to send state events to modify widgets in this room @@ -41,7 +51,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 +90,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 +133,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 +180,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 +223,13 @@ export default class WidgetUtils { }); } - static setUserWidget(widgetId, widgetType: WidgetType, widgetUrl, widgetName, widgetData) { + static setUserWidget( + widgetId: string, + widgetType: WidgetType, + widgetUrl: string, + widgetName: string, + widgetData: IWidgetData, + ) { const content = { type: widgetType.preferred, url: widgetUrl, @@ -257,7 +273,14 @@ 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 +330,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 +346,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(): IWidgetEvent[] { return Object.values(WidgetUtils.getUserWidgets()); } @@ -331,7 +354,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(): IWidgetEvent[] { const widgets = WidgetUtils.getUserWidgetsArray(); return widgets.filter((widget) => widget.content && widget.content.type === "m.stickerpicker"); } @@ -340,12 +363,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(): IWidgetEvent[] { 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): IWidgetEvent[] { const widgets = WidgetUtils.getRoomWidgets(room); return (widgets || []).filter(w => { const content = w.getContent(); @@ -353,14 +376,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: IWidgetEvent[] = widgets.getContent() || {}; Object.entries(userWidgets).forEach(([key, widget]) => { if (widget.content && widget.content.type === "m.integration_manager") { delete userWidgets[key]; @@ -369,7 +392,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 +406,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 +422,13 @@ export default class WidgetUtils { return client.setAccountData('m.widgets', userWidgets); } - static makeAppConfig(appId, app, senderUserId, roomId, eventId) { + 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"); } @@ -410,10 +439,10 @@ export default class WidgetUtils { app.eventId = eventId; app.name = app.name || app.type; - return app; + return app as IApp; } - static getCapWhitelistForAppTypeInRoomId(appType, roomId) { + static getCapWhitelistForAppTypeInRoomId(appType: string, roomId: string): Capability[] { const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", roomId); const capWhitelist = enableScreenshots ? [MatrixCapabilities.Screenshots] : []; @@ -428,7 +457,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) { @@ -449,7 +478,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 +495,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 +508,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);