Iterate PR

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2020-10-09 08:42:21 +01:00
parent b8080a7d2d
commit ada6d1aa46
24 changed files with 273 additions and 707 deletions

View file

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

View file

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

View file

@ -130,7 +130,7 @@ limitations under the License.
}
&.mx_AccessibleButton_disabled {
padding: 10px 12px;
padding-right: 12px;
&::after {
content: unset;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="14" y="2" width="8" height="8" rx="2" fill="#0DBD8B"/>
<rect x="14" y="14" width="8" height="8" rx="2" fill="#0DBD8B"/>
<rect x="2" y="14" width="8" height="8" rx="2" fill="#0DBD8B"/>
<rect x="2" y="2" width="8" height="8" rx="2" fill="#0DBD8B"/>
</svg>

After

Width:  |  Height:  |  Size: 359 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 6C2 3.79086 3.79086 2 6 2H18C20.2091 2 22 3.79086 22 6V18C22 20.2091 20.2091 22 18 22H6C3.79086 22 2 20.2091 2 18V6ZM11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8ZM8 19C9.65685 19 11 17.6569 11 16C11 14.3431 9.65685 13 8 13C6.34315 13 5 14.3431 5 16C5 17.6569 6.34315 19 8 19ZM19 16C19 17.6569 17.6569 19 16 19C14.3431 19 13 17.6569 13 16C13 14.3431 14.3431 13 16 13C17.6569 13 19 14.3431 19 16ZM16 11C17.6569 11 19 9.65685 19 8C19 6.34315 17.6569 5 16 5C14.3431 5 13 6.34315 13 8C13 9.65685 14.3431 11 16 11Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 742 B

View file

@ -1,5 +0,0 @@
<svg width="3" height="15" viewBox="0 0 3 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3C2.32843 3 3 2.32843 3 1.5C3 0.671573 2.32843 0 1.5 0C0.671573 0 0 0.671573 0 1.5C0 2.32843 0.671573 3 1.5 3Z" fill="#9FA9BA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 9C2.32843 9 3 8.32843 3 7.5C3 6.67157 2.32843 6 1.5 6C0.671573 6 0 6.67157 0 7.5C0 8.32843 0.671573 9 1.5 9Z" fill="#9FA9BA"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 15C2.32843 15 3 14.3284 3 13.5C3 12.6716 2.32843 12 1.5 12C0.671573 12 0 12.6716 0 13.5C0 14.3284 0.671573 15 1.5 15Z" fill="#9FA9BA"/>
</svg>

Before

Width:  |  Height:  |  Size: 655 B

View file

@ -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<IProps, IState> {
@ -231,6 +234,7 @@ export default class RoomView extends React.Component<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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<IProps, IState> {
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}
/>
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
<div className={fadableSectionClasses}>

View file

@ -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<typeof IconizedContextMenu> {
app: IApp;
showUnpin?: boolean;
// override delete handler
onDeleteClick?(): void;
}
const RoomWidgetContextMenu: React.FC<IProps> = ({ onFinished, app, ...props}) => {
const {roomId} = useContext(RoomContext);
const RoomWidgetContextMenu: React.FC<IProps> = ({ 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 = <IconizedContextMenuOption onClick={onUnpinClick} label={_t("Unpin")} />;
}
let editButton;
if (canModify && WidgetUtils.isManagedByManager(app)) {
const onEditClick = () => {
WidgetUtils.editWidget(room, app);
};
editButton = <IconizedContextMenuOption onClick={onEditClick} label={_t("Edit")} />
}
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<IProps> = ({ onFinished, app, ...props}) =
}
let deleteButton;
if (WidgetUtils.canUserModifyWidgets(roomId)) {
if (onDeleteClick || canModify) {
const onDeleteClick = () => {
defaultDispatcher.dispatch<AppTileActionPayload>({
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 = <IconizedContextMenuOption onClick={onDeleteClick} label={_t("Remove for everyone")} />;
deleteButton = <IconizedContextMenuOption
onClick={onDeleteClick || onDeleteClick}
label={_t("Remove for everyone")}
/>;
}
const onRevokeClick = () => {
defaultDispatcher.dispatch<AppTileActionPayload>({
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 <IconizedContextMenu {...props} chevronFace={ChevronFace.None} onFinished={onFinished}>
<IconizedContextMenuOptionList>
{ unpinButton }
{ snapshotButton }
{ editButton }
{ deleteButton }
<IconizedContextMenuOption onClick={onRevokeClick} label={_t("Remove for me")} />
</IconizedContextMenuOptionList>

View file

@ -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(
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onEditClicked} key='edit'>
{_t("Edit")}
</MenuItem>,
);
}
if (this.props.onUnpinClicked) {
options.push(
<MenuItem className="mx_WidgetContextMenu_option" onClick={this.onUnpinClicked} key="unpin">
{_t("Unpin")}
</MenuItem>,
);
}
if (this.props.onReloadClicked) {
options.push(
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onReloadClicked} key='reload'>
{_t("Reload")}
</MenuItem>,
);
}
if (this.props.onSnapshotClicked) {
options.push(
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onSnapshotClicked} key='snap'>
{_t("Take picture")}
</MenuItem>,
);
}
if (this.props.onDeleteClicked) {
options.push(
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onDeleteClicked} key='delete'>
{_t("Remove for everyone")}
</MenuItem>,
);
}
// Push this last so it appears last. It's always present.
options.push(
<MenuItem className='mx_WidgetContextMenu_option' onClick={this.onRevokeClicked} key='revoke'>
{_t("Remove for me")}
</MenuItem>,
);
// Put separators between the options
if (options.length > 1) {
const length = options.length;
for (let i = 0; i < length - 1; i++) {
const sep = <hr key={i} className="mx_WidgetContextMenu_separator" />;
// 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 <div className="mx_WidgetContextMenu">{options}</div>;
}
}

View file

@ -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 <div />;
}
// 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 = (
<div className="mx_AppLoading_spinner_fadeIn">
<Spinner message={_t("Loading...")} />
const loadingElement = (
<div className="mx_AppLoading_spinner_fadeIn">
<Spinner message={_t("Loading...")} />
</div>
);
if (!this.state.hasPermissionToLoad) {
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass}>
<AppPermission
roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId}
url={this._sgWidget.embedUrl}
isRoomEncrypted={isEncrypted}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
);
if (!this.state.hasPermissionToLoad) {
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
} else if (this.state.initialising) {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
{ loadingElement }
</div>
);
} else {
if (this.isMixedContent()) {
appTileBody = (
<div className={appTileBodyClass}>
<AppPermission
roomId={this.props.room.roomId}
creatorUserId={this.props.creatorUserId}
url={this._sgWidget.embedUrl}
isRoomEncrypted={isEncrypted}
onPermissionGranted={this._grantWidgetPermission}
/>
</div>
);
} else if (this.state.initialising) {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
{ loadingElement }
<AppWarning errorMsg="Error - Mixed content" />
</div>
);
} else {
if (this.isMixedContent()) {
appTileBody = (
<div className={appTileBodyClass}>
<AppWarning errorMsg="Error - Mixed content" />
</div>
);
} else {
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
{ this.state.loading && loadingElement }
<iframe
allow={iframeFeatures}
ref={this._iframeRefChange}
src={this._sgWidget.embedUrl}
allowFullScreen={true}
sandbox={sandboxFlags}
/>
</div>
);
// if the widget would be allowed to remain on screen, we must put it in
// a PersistedElement from the get-go, otherwise the iframe will be
// re-mounted later when we do.
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
const PersistedElement = sdk.getComponent("elements.PersistedElement");
// Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place
appTileBody = <div className="mx_AppTile_persistedWrapper">
<PersistedElement persistKey={this._persistKey}>
{appTileBody}
</PersistedElement>
</div>;
}
appTileBody = (
<div className={appTileBodyClass + (this.state.loading ? 'mx_AppLoading' : '')}>
{ this.state.loading && loadingElement }
<iframe
allow={iframeFeatures}
ref={this._iframeRefChange}
src={this._sgWidget.embedUrl}
allowFullScreen={true}
sandbox={sandboxFlags}
/>
</div>
);
// if the widget would be allowed to remain on screen, we must put it in
// a PersistedElement from the get-go, otherwise the iframe will be
// re-mounted later when we do.
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) {
const PersistedElement = sdk.getComponent("elements.PersistedElement");
// Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place
appTileBody = <div className="mx_AppTile_persistedWrapper">
<PersistedElement persistKey={this._persistKey}>
{appTileBody}
</PersistedElement>
</div>;
}
}
}
const showMinimiseButton = this.props.showMinimise && this.props.show;
const showMaximiseButton = this.props.showMinimise && !this.props.show;
let appTileClasses;
if (this.props.miniMode) {
appTileClasses = {mx_AppTile_mini: true};
@ -548,73 +386,36 @@ export default class AppTile extends React.Component {
} else {
appTileClasses = {mx_AppTile: true};
}
appTileClasses.mx_AppTile_minimised = !this.props.show;
appTileClasses = classNames(appTileClasses);
const menuBarClasses = classNames({
mx_AppTileMenuBar: true,
mx_AppTileMenuBar_expanded: this.props.show,
});
let contextMenu;
if (this.state.menuDisplayed) {
const elementRect = this._contextMenuButton.current.getBoundingClientRect();
const canUserModify = this._canUserModify();
const showEditButton = Boolean(this._sgWidget.isManagedByManager && canUserModify);
const showDeleteButton = (this.props.showDelete === undefined || this.props.showDelete) && canUserModify;
const showPictureSnapshotButton = this.props.show && this._sgWidget.widgetApi &&
this._sgWidget.widgetApi.hasCapability(MatrixCapabilities.Screenshots);
const WidgetContextMenu = sdk.getComponent('views.context_menus.WidgetContextMenu');
contextMenu = (
<ContextMenu {...aboveLeftOf(elementRect, null)} onFinished={this._closeContextMenu}>
<WidgetContextMenu
onUnpinClicked={
ActiveWidgetStore.getWidgetPersistence(this.props.app.id) ? null : this._onUnpinClicked
}
onRevokeClicked={this._onRevokeClicked}
onEditClicked={showEditButton ? this._onEditClick : undefined}
onDeleteClicked={showDeleteButton ? this._onDeleteClick : undefined}
onSnapshotClicked={showPictureSnapshotButton ? this._onSnapshotClick : undefined}
onReloadClicked={this.props.showReload ? this._onReloadWidgetClick : undefined}
onFinished={this._closeContextMenu}
/>
</ContextMenu>
<RoomWidgetContextMenu
{...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)}
app={this.props.app}
onFinished={this._closeContextMenu}
showUnpin={!this.props.userWidget}
/>
);
}
return <React.Fragment>
<div className={appTileClasses} id={this.props.app.id}>
{ this.props.showMenubar &&
<div ref={this._menu_bar} className={menuBarClasses} onClick={this.onClickMenuBar}>
<div className="mx_AppTileMenuBar">
<span className="mx_AppTileMenuBarTitle" style={{pointerEvents: (this.props.handleMinimisePointerEvents ? 'all' : false)}}>
{ /* Minimise widget */ }
{ showMinimiseButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_minimise"
title={_t('Minimize widget')}
onClick={this._onMinimiseClick}
/> }
{ /* Maximise widget */ }
{ showMaximiseButton && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_maximise"
title={_t('Maximize widget')}
onClick={this._onMinimiseClick}
/> }
{ /* Title */ }
{ this.props.showTitle && this._getTileTitle() }
</span>
<span className="mx_AppTileMenuBarWidgets">
{ /* Popout widget */ }
{ this.props.showPopout && <AccessibleButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
title={_t('Popout widget')}
onClick={this._onPopoutWidgetClick}
/> }
{ /* Context menu */ }
{ <ContextMenuButton
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
label={_t('More options')}
label={_t("Options")}
isExpanded={this.state.menuDisplayed}
inputRef={this._contextMenuButton}
onClick={this._onContextMenuClick}
@ -645,8 +446,6 @@ AppTile.propTypes = {
creatorUserId: PropTypes.string,
waitForIframeLoad: PropTypes.bool,
showMenubar: PropTypes.bool,
// Should the AppTile render itself
show: PropTypes.bool,
// Optional onEditClickHandler (overrides default behaviour)
onEditClick: PropTypes.func,
// Optional onDeleteClickHandler (overrides default behaviour)
@ -655,19 +454,10 @@ AppTile.propTypes = {
onMinimiseClick: PropTypes.func,
// Optionally hide the tile title
showTitle: PropTypes.bool,
// Optionally hide the tile minimise icon
showMinimise: PropTypes.bool,
// Optionally handle minimise button pointer events (default false)
handleMinimisePointerEvents: PropTypes.bool,
// Optionally hide the delete icon
showDelete: PropTypes.bool,
// Optionally hide the popout widget icon
showPopout: PropTypes.bool,
// Optionally show the reload widget icon
// This is not currently intended for use with production widgets. However
// it can be useful when developing persistent widgets in order to avoid
// having to reload all of Element to get new widget content.
showReload: PropTypes.bool,
// Widget capabilities to allow by default (without user confirmation)
// NOTE -- Use with caution. This is intended to aid better integration / UX
// basic widget capabilities, e.g. injecting sticker message events.
@ -680,10 +470,7 @@ AppTile.defaultProps = {
waitForIframeLoad: true,
showMenubar: true,
showTitle: true,
showMinimise: true,
showDelete: true,
showPopout: true,
showReload: false,
handleMinimisePointerEvents: false,
whitelistCapabilities: [],
userWidget: false,

View file

@ -173,3 +173,5 @@ export default class PersistedElement extends React.Component {
return <div ref={this.collectChildContainer} />;
}
}
export const getPersistKey = (appId: string) => 'widget_' + appId;

View file

@ -74,13 +74,10 @@ export default class PersistentApp extends React.Component {
fullWidth={true}
room={persistentWidgetInRoom}
userId={MatrixClientPeg.get().credentials.userId}
show={true}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={capWhitelist}
showDelete={false}
showMinimise={false}
miniMode={true}
showMenubar={false}
/>;

View file

@ -62,67 +62,22 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
// Don't render anything as we are about to transition
if (!app || isPinned) return null;
const header = <React.Fragment>
<h2>{ WidgetUtils.getWidgetName(app) }</h2>
</React.Fragment>;
const canModify = WidgetUtils.canUserModifyWidgets(room.roomId);
let contextMenu;
if (menuDisplayed) {
const rect = handle.current.getBoundingClientRect();
contextMenu = (
<RoomWidgetContextMenu
chevronFace={ChevronFace.None}
right={window.innerWidth - rect.right}
bottom={window.innerHeight - rect.top}
right={window.innerWidth - rect.right - 12}
top={rect.bottom + 12}
onFinished={closeMenu}
app={app}
/>
);
}
const onPinClick = () => {
WidgetStore.instance.pinWidget(app.id);
};
const onEditClick = () => {
WidgetUtils.editWidget(room, app);
};
let editButton;
if (canModify) {
editButton = <AccessibleButton kind="secondary" onClick={onEditClick}>
{ _t("Edit") }
</AccessibleButton>;
}
const pinButtonClasses = canModify ? "" : "mx_WidgetCard_widePinButton";
let pinButton;
if (WidgetStore.instance.canPin(app.id)) {
pinButton = <AccessibleButton
kind="secondary"
onClick={onPinClick}
className={pinButtonClasses}
>
{ _t("Pin to room") }
</AccessibleButton>;
} else {
pinButton = <AccessibleTooltipButton
title={_t("You can only pin 2 widgets at a time")}
tooltipClassName="mx_WidgetCard_maxPinnedTooltip"
kind="secondary"
className={pinButtonClasses}
disabled
>
{ _t("Pin to room") }
</AccessibleTooltipButton>;
}
const footer = <React.Fragment>
{ editButton }
{ pinButton }
const header = <React.Fragment>
<h2>{ WidgetUtils.getWidgetName(app) }</h2>
<ContextMenuButton
kind="secondary"
className="mx_WidgetCard_optionsButton"
@ -131,16 +86,12 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
isExpanded={menuDisplayed}
label={_t("Options")}
/>
{ contextMenu }
</React.Fragment>;
return <BaseCard
header={header}
footer={footer}
className={classNames("mx_WidgetCard", {
mx_WidgetCard_noEdit: !canModify,
})}
className="mx_WidgetCard"
onClose={onClose}
previousPhase={RightPanelPhases.RoomSummary}
withoutScrollContainer

View file

@ -24,10 +24,8 @@ import AppTile from '../elements/AppTile';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import * as ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
@ -101,15 +99,6 @@ export default class AppsDrawer extends React.Component {
});
};
_canUserModify() {
try {
return WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
} catch (err) {
console.error(err);
return false;
}
}
_launchManageIntegrations() {
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll();
@ -118,12 +107,9 @@ export default class AppsDrawer extends React.Component {
}
}
onClickAddWidget = (e) => {
e.preventDefault();
this._launchManageIntegrations();
};
render() {
if (!this.props.showApps) return <div />;
const apps = this.state.apps.map((app, index, arr) => {
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
@ -133,7 +119,6 @@ export default class AppsDrawer extends React.Component {
fullWidth={arr.length < 2}
room={this.props.room}
userId={this.props.userId}
show={this.props.showApps}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
@ -145,21 +130,6 @@ export default class AppsDrawer extends React.Component {
return <div />;
}
let addWidget;
if (this.props.showApps &&
this._canUserModify()
) {
addWidget = <AccessibleButton
onClick={this.onClickAddWidget}
className={this.state.apps.length<2 ?
'mx_AddWidget_button mx_AddWidget_button_full_width' :
'mx_AddWidget_button'
}
title={_t('Add a widget')}>
[+] { _t('Add a widget') }
</AccessibleButton>;
}
let spinner;
if (
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
@ -191,7 +161,6 @@ export default class AppsDrawer extends React.Component {
{ apps }
{ spinner }
</PersistentVResizer>
{ this._canUserModify() && addWidget }
</div>
);
}

View file

@ -42,6 +42,8 @@ export default class RoomHeader extends React.Component {
onLeaveClick: PropTypes.func,
onCancelClick: PropTypes.func,
e2eStatus: PropTypes.string,
onAppsClick: PropTypes.func,
appsShown: PropTypes.bool,
};
static defaultProps = {
@ -230,6 +232,17 @@ export default class RoomHeader extends React.Component {
title={_t("Forget room")} />;
}
let appsButton;
if (this.props.onAppsClick) {
appsButton =
<AccessibleTooltipButton
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
})}
onClick={this.props.onAppsClick}
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")} />;
}
let searchButton;
if (this.props.onSearchClick && this.props.inRoom) {
searchButton =
@ -243,6 +256,7 @@ export default class RoomHeader extends React.Component {
<div className="mx_RoomHeader_buttons">
{ pinnedEventsButton }
{ forgetButton }
{ appsButton }
{ searchButton }
</div>;

View file

@ -272,13 +272,10 @@ export default class Stickerpicker extends React.Component {
userId={MatrixClientPeg.get().credentials.userId}
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
waitForIframeLoad={true}
show={true}
showMenubar={true}
onEditClick={this._launchManageIntegrations}
onDeleteClick={this._removeStickerpickerWidgets}
showTitle={false}
showMinimise={true}
showDelete={false}
showCancel={false}
showPopout={false}
onMinimiseClick={this._onHideStickersClick}

View file

@ -94,14 +94,4 @@ export enum Action {
* Trigged after the phase of the right panel is set. Should be used with AfterRightPanelPhaseChangePayload.
*/
AfterRightPanelPhaseChange = "after_right_panel_phase_change",
/**
* Requests that the AppTile deletes the widget. Should be used with the AppTileActionPayload.
*/
AppTileDelete = "appTile_delete",
/**
* Requests that the AppTile revokes the widget. Should be used with the AppTileActionPayload.
*/
AppTileRevoke = "appTile_revoke",
}

View file

@ -1,23 +0,0 @@
/*
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 { ActionPayload } from "../payloads";
import { Action } from "../actions";
export interface AppTileActionPayload extends ActionPayload {
action: Action.AppTileDelete | Action.AppTileRevoke;
widgetId: string;
}

View file

@ -1104,6 +1104,8 @@
"(~%(count)s results)|one": "(~%(count)s result)",
"Join Room": "Join Room",
"Forget room": "Forget room",
"Hide Widgets": "Hide Widgets",
"Show Widgets": "Show Widgets",
"Search": "Search",
"Invites": "Invites",
"Favourites": "Favourites",
@ -1358,9 +1360,6 @@
"You cancelled verification.": "You cancelled verification.",
"Verification cancelled": "Verification cancelled",
"Compare emoji": "Compare emoji",
"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",
"Sunday": "Sunday",
"Monday": "Monday",
"Tuesday": "Tuesday",
@ -1379,6 +1378,7 @@
"Error decrypting audio": "Error decrypting audio",
"React": "React",
"Reply": "Reply",
"Edit": "Edit",
"Message Actions": "Message Actions",
"Attachment": "Attachment",
"Error decrypting attachment": "Error decrypting attachment",
@ -1476,8 +1476,6 @@
"Delete widget": "Delete widget",
"Failed to remove widget": "Failed to remove widget",
"An error ocurred whilst trying to remove the widget from the room": "An error ocurred whilst trying to remove the widget from the room",
"Minimize widget": "Minimize widget",
"Maximize widget": "Maximize widget",
"Popout widget": "Popout widget",
"More options": "More options",
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",

View file

@ -494,4 +494,16 @@ export default class WidgetUtils {
IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
}
}
static isManagedByManager(app) {
if (WidgetUtils.isScalarUrl(app.url)) {
const managers = IntegrationManagers.sharedInstance();
if (managers.hasManager()) {
// TODO: Pick the right manager for the widget
const defaultManager = managers.getPrimaryManager();
return WidgetUtils.isScalarUrl(defaultManager.apiUrl);
}
}
return false;
}
}