Merge pull request #5247 from matrix-org/t3chguy/feat/room-list-widgets

Left Panel Widget support
pull/21833/head
Michael Telatynski 2020-10-21 12:59:41 +01:00 committed by GitHub
commit 1bbd273b01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 385 additions and 49 deletions

View File

@ -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";

View File

@ -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;
}

View File

@ -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.

View File

@ -205,7 +205,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({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<HTMLElement>(null);

View File

@ -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<IProps, IState> {
const bottomEdge = list.offsetHeight + list.scrollTop;
const sublists = list.querySelectorAll<HTMLDivElement>(".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<IProps, IState> {
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<IProps, IState> {
{roomList}
</div>
</div>
{ !this.props.isMinimized && <LeftPanelWidget onResize={this.onResize} /> }
</aside>
</div>
);

View File

@ -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<IProps> = ({ onResize }) => {
const cli = useContext(MatrixClientContext);
const mWidgetsEvent = useAccountData<Record<string, IWidgetEvent>>(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 = <Resizable
size={{height} as any}
minHeight={MIN_HEIGHT}
maxHeight={Math.min(window.innerHeight / 2, MAX_HEIGHT)}
onResize={onResize}
onResizeStop={(e, dir, ref, d) => {
setHeight(height + d.height);
}}
handleWrapperClass="mx_LeftPanelWidget_resizerHandles"
handleClasses={{top: "mx_LeftPanelWidget_resizerHandle"}}
className="mx_LeftPanelWidget_resizeBox"
enable={{ top: true }}
>
<AppTile
app={app}
fullWidth
show
showMenubar={false}
userWidget
userId={cli.getUserId()}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
/>
</Resizable>;
}
return <div className="mx_LeftPanelWidget">
<div
onFocus={onFocus}
className="mx_LeftPanelWidget_headerContainer"
onKeyDown={(ev: React.KeyboardEvent) => {
switch (ev.key) {
case Key.ARROW_LEFT:
ev.stopPropagation();
setExpanded(false);
break;
case Key.ARROW_RIGHT: {
ev.stopPropagation();
setExpanded(true);
break;
}
}
}}
>
<div className="mx_LeftPanelWidget_stickable">
<AccessibleButton
onFocus={onFocus}
inputRef={ref}
tabIndex={tabIndex}
className="mx_LeftPanelWidget_headerText"
role="treeitem"
aria-expanded={expanded}
aria-level={1}
onClick={() => {
setExpanded(e => !e);
}}
>
<span className={classNames({
"mx_LeftPanelWidget_collapseBtn": true,
"mx_LeftPanelWidget_collapseBtn_collapsed": !expanded,
})} />
<span>{ WidgetUtils.getWidgetName(app) }</span>
</AccessibleButton>
{/* Code for the maximise button for once we have full screen widgets */}
{/*<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={() => {
}}
className="mx_LeftPanelWidget_maximizeButton"
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
title={_t("Maximize")}
/>*/}
</div>
</div>
{ content }
</div>;
};
export default LeftPanelWidget;

View File

@ -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 {
</div>
);
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 = (
<div className={appTileBodyClass}>
@ -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,

View File

@ -26,7 +26,7 @@ const getValue = <T>(key: string, initialValue: T): T => {
};
// Hook behaving like useState but persisting the value to localStorage. Returns same as useState
export const useLocalStorageState = <T>(key: string, initialValue: T) => {
export const useLocalStorageState = <T>(key: string, initialValue: T): [T, Dispatch<SetStateAction<T>>] => {
const lsKey = "mx_" + key;
const [value, setValue] = useState<T>(getValue(lsKey, initialValue));

View File

@ -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(

View File

@ -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,

View File

@ -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
}

View File

@ -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

View File

@ -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<IApp>;
}
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<void> {
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<void> {
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<string, IWidgetEvent> {
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<void> {
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<void> {
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<void> {
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<string, IWidgetEvent> = 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<IApp>,
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);