Iterate PR
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
b8080a7d2d
commit
ada6d1aa46
24 changed files with 273 additions and 707 deletions
|
@ -55,7 +55,6 @@
|
||||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_TagTileContextMenu.scss";
|
@import "./views/context_menus/_TagTileContextMenu.scss";
|
||||||
@import "./views/context_menus/_WidgetContextMenu.scss";
|
|
||||||
@import "./views/dialogs/_AddressPickerDialog.scss";
|
@import "./views/dialogs/_AddressPickerDialog.scss";
|
||||||
@import "./views/dialogs/_Analytics.scss";
|
@import "./views/dialogs/_Analytics.scss";
|
||||||
@import "./views/dialogs/_BugReportDialog.scss";
|
@import "./views/dialogs/_BugReportDialog.scss";
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -130,7 +130,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_AccessibleButton_disabled {
|
&.mx_AccessibleButton_disabled {
|
||||||
padding: 10px 12px;
|
padding-right: 12px;
|
||||||
&::after {
|
&::after {
|
||||||
content: unset;
|
content: unset;
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,9 +157,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomSummaryCard_Button {
|
.mx_RoomSummaryCard_Button {
|
||||||
padding-left: 12px;
|
padding: 6px 24px 6px 12px;
|
||||||
padding-top: 6px;
|
|
||||||
padding-bottom: 6px;
|
|
||||||
color: $tertiary-fg-color;
|
color: $tertiary-fg-color;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
|
|
@ -24,29 +24,29 @@ limitations under the License.
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_WidgetCard_noEdit {
|
.mx_BaseCard_header {
|
||||||
.mx_AccessibleButton_kind_secondary {
|
display: inline-flex;
|
||||||
margin: 0 12px;
|
|
||||||
|
|
||||||
&:first-child {
|
& > h2 {
|
||||||
// expand the Pin to room primary action
|
margin-right: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_WidgetCard_optionsButton {
|
.mx_WidgetCard_optionsButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 18px;
|
margin-right: 44px;
|
||||||
width: 26px;
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
min-width: 20px; // prevent crushing by the flexbox
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
top: 6px;
|
top: 0;
|
||||||
left: 20px;
|
left: 4px;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
|
@ -55,6 +55,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_WidgetCard_maxPinnedTooltip {
|
.mx_WidgetCard_maxPinnedTooltip {
|
||||||
background-color: $notice-primary-color;
|
background-color: $notice-primary-color;
|
||||||
|
|
|
@ -118,12 +118,6 @@ $MiniAppTileHeight: 200px;
|
||||||
height: $MiniAppTileHeight;
|
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_AppTile .mx_AppTile_persistedWrapper,
|
||||||
.mx_AppTileFullWidth .mx_AppTile_persistedWrapper,
|
.mx_AppTileFullWidth .mx_AppTile_persistedWrapper,
|
||||||
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
||||||
|
@ -143,11 +137,7 @@ $MiniAppTileHeight: 200px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTileMenuBar_expanded {
|
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -179,31 +169,12 @@ $MiniAppTileHeight: 200px;
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_minimise {
|
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout { // TODO replace icon
|
||||||
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 {
|
|
||||||
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
|
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu {
|
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu {
|
||||||
mask-image: url('$(res)/img/icon_context.svg');
|
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTileMenuBarWidgetDelete {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTileMenuBarWidget:hover {
|
|
||||||
border: 1px solid $primary-fg-color;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileBody {
|
.mx_AppTileBody {
|
||||||
|
|
|
@ -241,6 +241,13 @@ limitations under the License.
|
||||||
width: 26px;
|
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 {
|
.mx_RoomHeader_searchButton::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
|
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
|
||||||
}
|
}
|
||||||
|
|
6
res/img/element-icons/room/apps.svg
Normal file
6
res/img/element-icons/room/apps.svg
Normal 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 |
|
@ -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 |
|
@ -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 |
|
@ -71,6 +71,8 @@ import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import TintableSvg from "../views/elements/TintableSvg";
|
import TintableSvg from "../views/elements/TintableSvg";
|
||||||
import {XOR} from "../../@types/common";
|
import {XOR} from "../../@types/common";
|
||||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
|
import WidgetStore from "../../stores/WidgetStore";
|
||||||
|
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -180,6 +182,7 @@ export interface IState {
|
||||||
e2eStatus?: E2EStatus;
|
e2eStatus?: E2EStatus;
|
||||||
rejecting?: boolean;
|
rejecting?: boolean;
|
||||||
rejectError?: Error;
|
rejectError?: Error;
|
||||||
|
hasPinnedWidgets: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomView extends React.Component<IProps, IState> {
|
export default class RoomView extends React.Component<IProps, IState> {
|
||||||
|
@ -231,6 +234,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
canReply: false,
|
canReply: false,
|
||||||
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
useIRCLayout: SettingsStore.getValue("useIRCLayout"),
|
||||||
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
|
||||||
|
hasPinnedWidgets: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
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.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this.rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this.onRightPanelStoreUpdate);
|
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.showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null,
|
||||||
this.onReadReceiptsChange);
|
this.onReadReceiptsChange);
|
||||||
this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
|
this.layoutWatcherRef = SettingsStore.watchSetting("useIRCLayout", null, this.onLayoutChange);
|
||||||
|
@ -262,6 +268,18 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.onRoomViewStoreUpdate(true);
|
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 = () => {
|
private onReadReceiptsChange = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
|
showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId),
|
||||||
|
@ -584,7 +602,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.rightPanelStoreToken.remove();
|
this.rightPanelStoreToken.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
WidgetEchoStore.removeListener('update', this.onWidgetEchoStoreUpdate);
|
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||||
|
WidgetStore.instance.removeListener(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||||
|
|
||||||
if (this.showReadReceiptsWatchRef) {
|
if (this.showReadReceiptsWatchRef) {
|
||||||
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
SettingsStore.unwatchSetting(this.showReadReceiptsWatchRef);
|
||||||
|
@ -828,6 +847,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.calculateRecommendedVersion(room);
|
this.calculateRecommendedVersion(room);
|
||||||
this.updateE2EStatus(room);
|
this.updateE2EStatus(room);
|
||||||
this.updatePermissions(room);
|
this.updatePermissions(room);
|
||||||
|
this.checkWidgets(room);
|
||||||
};
|
};
|
||||||
|
|
||||||
private async calculateRecommendedVersion(room: Room) {
|
private async calculateRecommendedVersion(room: Room) {
|
||||||
|
@ -1357,6 +1377,13 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusComposer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onAppsClick = () => {
|
||||||
|
dis.dispatch({
|
||||||
|
action: "appsDrawer", // TODO should this go into the RVS?
|
||||||
|
show: !this.state.showApps,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onLeaveClick = () => {
|
private onLeaveClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'leave_room',
|
action: 'leave_room',
|
||||||
|
@ -2060,6 +2087,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
onForgetClick={(myMembership === "leave") ? this.onForgetClick : null}
|
||||||
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
onLeaveClick={(myMembership === "join") ? this.onLeaveClick : null}
|
||||||
e2eStatus={this.state.e2eStatus}
|
e2eStatus={this.state.e2eStatus}
|
||||||
|
onAppsClick={this.state.hasPinnedWidgets ? this.onAppsClick : null}
|
||||||
|
appsShown={this.state.showApps}
|
||||||
/>
|
/>
|
||||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||||
<div className={fadableSectionClasses}>
|
<div className={fadableSectionClasses}>
|
||||||
|
|
|
@ -20,27 +20,58 @@ import {MatrixCapabilities} from "matrix-widget-api";
|
||||||
import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu";
|
import IconizedContextMenu, {IconizedContextMenuOption, IconizedContextMenuOptionList} from "./IconizedContextMenu";
|
||||||
import {ChevronFace} from "../../structures/ContextMenu";
|
import {ChevronFace} from "../../structures/ContextMenu";
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import {IApp} from "../../../stores/WidgetStore";
|
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|
||||||
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
|
|
||||||
import {Action} from "../../../dispatcher/actions";
|
|
||||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||||
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
|
import {WidgetMessagingStore} from "../../../stores/widgets/WidgetMessagingStore";
|
||||||
import RoomContext from "../../../contexts/RoomContext";
|
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> {
|
interface IProps extends React.ComponentProps<typeof IconizedContextMenu> {
|
||||||
app: IApp;
|
app: IApp;
|
||||||
|
showUnpin?: boolean;
|
||||||
|
// override delete handler
|
||||||
|
onDeleteClick?(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomWidgetContextMenu: React.FC<IProps> = ({ onFinished, app, ...props}) => {
|
const RoomWidgetContextMenu: React.FC<IProps> = ({ onFinished, app, onDeleteClick, showUnpin, ...props}) => {
|
||||||
const {roomId} = useContext(RoomContext);
|
const {room, roomId} = useContext(RoomContext);
|
||||||
|
|
||||||
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForId(app.id);
|
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;
|
let snapshotButton;
|
||||||
if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
|
if (widgetMessaging?.hasCapability(MatrixCapabilities.Screenshots)) {
|
||||||
const onSnapshotClick = () => {
|
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();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,29 +79,45 @@ const RoomWidgetContextMenu: React.FC<IProps> = ({ onFinished, app, ...props}) =
|
||||||
}
|
}
|
||||||
|
|
||||||
let deleteButton;
|
let deleteButton;
|
||||||
if (WidgetUtils.canUserModifyWidgets(roomId)) {
|
if (onDeleteClick || canModify) {
|
||||||
const onDeleteClick = () => {
|
const onDeleteClick = () => {
|
||||||
defaultDispatcher.dispatch<AppTileActionPayload>({
|
// Show delete confirmation dialog
|
||||||
action: Action.AppTileDelete,
|
Modal.createTrackedDialog('Delete Widget', '', QuestionDialog, {
|
||||||
widgetId: app.id,
|
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();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
deleteButton = <IconizedContextMenuOption onClick={onDeleteClick} label={_t("Remove for everyone")} />;
|
deleteButton = <IconizedContextMenuOption
|
||||||
|
onClick={onDeleteClick || onDeleteClick}
|
||||||
|
label={_t("Remove for everyone")}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onRevokeClick = () => {
|
const onRevokeClick = () => {
|
||||||
defaultDispatcher.dispatch<AppTileActionPayload>({
|
console.info("Revoking permission for widget to load: " + app.eventId);
|
||||||
action: Action.AppTileRevoke,
|
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||||
widgetId: app.id,
|
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();
|
onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
return <IconizedContextMenu {...props} chevronFace={ChevronFace.None} onFinished={onFinished}>
|
return <IconizedContextMenu {...props} chevronFace={ChevronFace.None} onFinished={onFinished}>
|
||||||
<IconizedContextMenuOptionList>
|
<IconizedContextMenuOptionList>
|
||||||
|
{ unpinButton }
|
||||||
{ snapshotButton }
|
{ snapshotButton }
|
||||||
|
{ editButton }
|
||||||
{ deleteButton }
|
{ deleteButton }
|
||||||
<IconizedContextMenuOption onClick={onRevokeClick} label={_t("Remove for me")} />
|
<IconizedContextMenuOption onClick={onRevokeClick} label={_t("Remove for me")} />
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
|
|
|
@ -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>;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,55 +22,48 @@ import React, {createRef} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import AccessibleButton from './AccessibleButton';
|
import AccessibleButton from './AccessibleButton';
|
||||||
import Modal from '../../../Modal';
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import AppPermission from './AppPermission';
|
import AppPermission from './AppPermission';
|
||||||
import AppWarning from './AppWarning';
|
import AppWarning from './AppWarning';
|
||||||
import Spinner from './Spinner';
|
import Spinner from './Spinner';
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {aboveLeftOf, ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
import {aboveLeftOf, ContextMenuButton} from "../../structures/ContextMenu";
|
||||||
import PersistedElement from "./PersistedElement";
|
import PersistedElement, {getPersistKey} from "./PersistedElement";
|
||||||
import {WidgetType} from "../../../widgets/WidgetType";
|
import {WidgetType} from "../../../widgets/WidgetType";
|
||||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
import {SettingLevel} from "../../../settings/SettingLevel";
|
||||||
import WidgetStore from "../../../stores/WidgetStore";
|
|
||||||
import {Action} from "../../../dispatcher/actions";
|
|
||||||
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
import {StopGapWidget} from "../../../stores/widgets/StopGapWidget";
|
||||||
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
import {ElementWidgetActions} from "../../../stores/widgets/ElementWidgetActions";
|
||||||
import {MatrixCapabilities} from "matrix-widget-api";
|
import {MatrixCapabilities} from "matrix-widget-api";
|
||||||
|
import RoomWidgetContextMenu from "../context_menus/RoomWidgetContextMenu";
|
||||||
|
|
||||||
export default class AppTile extends React.Component {
|
export default class AppTile extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
// The key used for PersistedElement
|
// 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 = new StopGapWidget(this.props);
|
||||||
this._sgWidget.on("ready", this._onWidgetReady);
|
this._sgWidget.on("ready", this._onWidgetReady);
|
||||||
this.iframe = null; // ref to the iframe (callback style)
|
this.iframe = null; // ref to the iframe (callback style)
|
||||||
|
|
||||||
this.state = this._getNewState(props);
|
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._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.
|
* Set initial component state when the App wUrl (widget URL) is being updated.
|
||||||
* Component props *must* be passed (rather than relying on this.props).
|
* 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
|
* @return {Object} Updated component state to be set with setState
|
||||||
*/
|
*/
|
||||||
_getNewState(newProps) {
|
_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 {
|
return {
|
||||||
initialising: true, // True while we are mangling the widget URL
|
initialising: true, // True while we are mangling the widget URL
|
||||||
// True while the iframe content is loading
|
// True while the iframe content is loading
|
||||||
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
loading: this.props.waitForIframeLoad && !PersistedElement.isMounted(this._persistKey),
|
||||||
// Assume that widget has permission to load if we are the user who
|
// 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
|
// 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,
|
error: null,
|
||||||
deleting: false,
|
|
||||||
widgetPageTitle: newProps.widgetPageTitle,
|
widgetPageTitle: newProps.widgetPageTitle,
|
||||||
menuDisplayed: false,
|
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() {
|
isMixedContent() {
|
||||||
const parentContentProtocol = window.location.protocol;
|
const parentContentProtocol = window.location.protocol;
|
||||||
const u = url.parse(this.props.app.url);
|
const u = url.parse(this.props.app.url);
|
||||||
|
@ -114,7 +114,7 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
// Only fetch IM token on mount if we're showing and have permission to load
|
// 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();
|
this._startWidget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,6 +135,8 @@ export default class AppTile extends React.Component {
|
||||||
if (this._sgWidget) {
|
if (this._sgWidget) {
|
||||||
this._sgWidget.stop();
|
this._sgWidget.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsStore.unwatchSetting(this._allowedWidgetsWatchRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetWidget(newProps) {
|
_resetWidget(newProps) {
|
||||||
|
@ -165,21 +167,8 @@ export default class AppTile extends React.Component {
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
UNSAFE_componentWillReceiveProps(nextProps) { // eslint-disable-line camelcase
|
||||||
if (nextProps.app.url !== this.props.app.url) {
|
if (nextProps.app.url !== this.props.app.url) {
|
||||||
this._getNewState(nextProps);
|
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) {
|
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.
|
* Ends all widget interaction, such as cancelling calls and disabling webcams.
|
||||||
* @private
|
* @private
|
||||||
|
@ -244,57 +204,6 @@ export default class AppTile extends React.Component {
|
||||||
this._sgWidget.stop();
|
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 = () => {
|
_onWidgetReady = () => {
|
||||||
this.setState({loading: false});
|
this.setState({loading: false});
|
||||||
if (WidgetType.JITSI.matches(this.props.app.type)) {
|
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) {
|
if (payload.widgetId === this.props.app.id) {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case 'm.sticker':
|
case 'm.sticker':
|
||||||
|
@ -312,19 +221,11 @@ export default class AppTile extends React.Component {
|
||||||
console.warn('Ignoring sticker message. Invalid capability');
|
console.warn('Ignoring sticker message. Invalid capability');
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Action.AppTileDelete:
|
|
||||||
this._onDeleteClick();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Action.AppTileRevoke:
|
|
||||||
this._onRevokeClicked();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_grantWidgetPermission() {
|
_grantWidgetPermission = () => {
|
||||||
const roomId = this.props.room.roomId;
|
const roomId = this.props.room.roomId;
|
||||||
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
console.info("Granting permission for widget to load: " + this.props.app.eventId);
|
||||||
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
const current = SettingsStore.getValue("allowedWidgets", roomId);
|
||||||
|
@ -338,26 +239,7 @@ export default class AppTile extends React.Component {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
// We don't really need to do anything about this - the user will just hit the button again.
|
// 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() {
|
formatAppTileName() {
|
||||||
let appTileName = "No name";
|
let appTileName = "No name";
|
||||||
|
@ -367,29 +249,6 @@ export default class AppTile extends React.Component {
|
||||||
return appTileName;
|
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
|
* Whether we're using a local version of the widget rather than loading the
|
||||||
* actual widget URL
|
* actual widget URL
|
||||||
|
@ -415,16 +274,11 @@ export default class AppTile extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMinimiseClick(e) {
|
// TODO replace with full screen interactions
|
||||||
if (this.props.onMinimiseClick) {
|
_onPopoutWidgetClick = () => {
|
||||||
this.props.onMinimiseClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onPopoutWidgetClick() {
|
|
||||||
// Ensure Jitsi conferences are closed on pop-out, to not confuse the user to join them
|
// 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).
|
// 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(() => {
|
this._endWidgetActions().then(() => {
|
||||||
if (this.iframe) {
|
if (this.iframe) {
|
||||||
// Reload iframe
|
// Reload iframe
|
||||||
|
@ -437,13 +291,7 @@ export default class AppTile extends React.Component {
|
||||||
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
// window.open(this._getPopoutUrl(), '_blank', 'noopener=yes');
|
||||||
Object.assign(document.createElement('a'),
|
Object.assign(document.createElement('a'),
|
||||||
{ target: '_blank', href: this._sgWidget.popoutUrl, rel: 'noreferrer noopener'}).click();
|
{ 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 = () => {
|
_onContextMenuClick = () => {
|
||||||
this.setState({ menuDisplayed: true });
|
this.setState({ menuDisplayed: true });
|
||||||
|
@ -456,11 +304,6 @@ export default class AppTile extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
let appTileBody;
|
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
|
// 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
|
// 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
|
// this would only be for content hosted on the same origin as the element client: anything
|
||||||
|
@ -475,7 +318,6 @@ export default class AppTile extends React.Component {
|
||||||
|
|
||||||
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' ');
|
||||||
|
|
||||||
if (this.props.show) {
|
|
||||||
const loadingElement = (
|
const loadingElement = (
|
||||||
<div className="mx_AppLoading_spinner_fadeIn">
|
<div className="mx_AppLoading_spinner_fadeIn">
|
||||||
<Spinner message={_t("Loading...")} />
|
<Spinner message={_t("Loading...")} />
|
||||||
|
@ -535,10 +377,6 @@ export default class AppTile extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const showMinimiseButton = this.props.showMinimise && this.props.show;
|
|
||||||
const showMaximiseButton = this.props.showMinimise && !this.props.show;
|
|
||||||
|
|
||||||
let appTileClasses;
|
let appTileClasses;
|
||||||
if (this.props.miniMode) {
|
if (this.props.miniMode) {
|
||||||
|
@ -548,73 +386,36 @@ export default class AppTile extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
appTileClasses = {mx_AppTile: true};
|
appTileClasses = {mx_AppTile: true};
|
||||||
}
|
}
|
||||||
appTileClasses.mx_AppTile_minimised = !this.props.show;
|
|
||||||
appTileClasses = classNames(appTileClasses);
|
appTileClasses = classNames(appTileClasses);
|
||||||
|
|
||||||
const menuBarClasses = classNames({
|
|
||||||
mx_AppTileMenuBar: true,
|
|
||||||
mx_AppTileMenuBar_expanded: this.props.show,
|
|
||||||
});
|
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (this.state.menuDisplayed) {
|
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 = (
|
||||||
<ContextMenu {...aboveLeftOf(elementRect, null)} onFinished={this._closeContextMenu}>
|
<RoomWidgetContextMenu
|
||||||
<WidgetContextMenu
|
{...aboveLeftOf(this._contextMenuButton.current.getBoundingClientRect(), null)}
|
||||||
onUnpinClicked={
|
app={this.props.app}
|
||||||
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}
|
onFinished={this._closeContextMenu}
|
||||||
|
showUnpin={!this.props.userWidget}
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
<div className={appTileClasses} id={this.props.app.id}>
|
<div className={appTileClasses} id={this.props.app.id}>
|
||||||
{ this.props.showMenubar &&
|
{ 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)}}>
|
<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() }
|
{ this.props.showTitle && this._getTileTitle() }
|
||||||
</span>
|
</span>
|
||||||
<span className="mx_AppTileMenuBarWidgets">
|
<span className="mx_AppTileMenuBarWidgets">
|
||||||
{ /* Popout widget */ }
|
|
||||||
{ this.props.showPopout && <AccessibleButton
|
{ this.props.showPopout && <AccessibleButton
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_popout"
|
||||||
title={_t('Popout widget')}
|
title={_t('Popout widget')}
|
||||||
onClick={this._onPopoutWidgetClick}
|
onClick={this._onPopoutWidgetClick}
|
||||||
/> }
|
/> }
|
||||||
{ /* Context menu */ }
|
|
||||||
{ <ContextMenuButton
|
{ <ContextMenuButton
|
||||||
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
className="mx_AppTileMenuBar_iconButton mx_AppTileMenuBar_iconButton_menu"
|
||||||
label={_t('More options')}
|
label={_t("Options")}
|
||||||
isExpanded={this.state.menuDisplayed}
|
isExpanded={this.state.menuDisplayed}
|
||||||
inputRef={this._contextMenuButton}
|
inputRef={this._contextMenuButton}
|
||||||
onClick={this._onContextMenuClick}
|
onClick={this._onContextMenuClick}
|
||||||
|
@ -645,8 +446,6 @@ AppTile.propTypes = {
|
||||||
creatorUserId: PropTypes.string,
|
creatorUserId: PropTypes.string,
|
||||||
waitForIframeLoad: PropTypes.bool,
|
waitForIframeLoad: PropTypes.bool,
|
||||||
showMenubar: PropTypes.bool,
|
showMenubar: PropTypes.bool,
|
||||||
// Should the AppTile render itself
|
|
||||||
show: PropTypes.bool,
|
|
||||||
// Optional onEditClickHandler (overrides default behaviour)
|
// Optional onEditClickHandler (overrides default behaviour)
|
||||||
onEditClick: PropTypes.func,
|
onEditClick: PropTypes.func,
|
||||||
// Optional onDeleteClickHandler (overrides default behaviour)
|
// Optional onDeleteClickHandler (overrides default behaviour)
|
||||||
|
@ -655,19 +454,10 @@ AppTile.propTypes = {
|
||||||
onMinimiseClick: PropTypes.func,
|
onMinimiseClick: PropTypes.func,
|
||||||
// Optionally hide the tile title
|
// Optionally hide the tile title
|
||||||
showTitle: PropTypes.bool,
|
showTitle: PropTypes.bool,
|
||||||
// Optionally hide the tile minimise icon
|
|
||||||
showMinimise: PropTypes.bool,
|
|
||||||
// Optionally handle minimise button pointer events (default false)
|
// Optionally handle minimise button pointer events (default false)
|
||||||
handleMinimisePointerEvents: PropTypes.bool,
|
handleMinimisePointerEvents: PropTypes.bool,
|
||||||
// Optionally hide the delete icon
|
|
||||||
showDelete: PropTypes.bool,
|
|
||||||
// Optionally hide the popout widget icon
|
// Optionally hide the popout widget icon
|
||||||
showPopout: PropTypes.bool,
|
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)
|
// Widget capabilities to allow by default (without user confirmation)
|
||||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||||
// basic widget capabilities, e.g. injecting sticker message events.
|
// basic widget capabilities, e.g. injecting sticker message events.
|
||||||
|
@ -680,10 +470,7 @@ AppTile.defaultProps = {
|
||||||
waitForIframeLoad: true,
|
waitForIframeLoad: true,
|
||||||
showMenubar: true,
|
showMenubar: true,
|
||||||
showTitle: true,
|
showTitle: true,
|
||||||
showMinimise: true,
|
|
||||||
showDelete: true,
|
|
||||||
showPopout: true,
|
showPopout: true,
|
||||||
showReload: false,
|
|
||||||
handleMinimisePointerEvents: false,
|
handleMinimisePointerEvents: false,
|
||||||
whitelistCapabilities: [],
|
whitelistCapabilities: [],
|
||||||
userWidget: false,
|
userWidget: false,
|
||||||
|
|
|
@ -173,3 +173,5 @@ export default class PersistedElement extends React.Component {
|
||||||
return <div ref={this.collectChildContainer} />;
|
return <div ref={this.collectChildContainer} />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getPersistKey = (appId: string) => 'widget_' + appId;
|
||||||
|
|
|
@ -74,13 +74,10 @@ export default class PersistentApp extends React.Component {
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
room={persistentWidgetInRoom}
|
room={persistentWidgetInRoom}
|
||||||
userId={MatrixClientPeg.get().credentials.userId}
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
show={true}
|
|
||||||
creatorUserId={app.creatorUserId}
|
creatorUserId={app.creatorUserId}
|
||||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||||
waitForIframeLoad={app.waitForIframeLoad}
|
waitForIframeLoad={app.waitForIframeLoad}
|
||||||
whitelistCapabilities={capWhitelist}
|
whitelistCapabilities={capWhitelist}
|
||||||
showDelete={false}
|
|
||||||
showMinimise={false}
|
|
||||||
miniMode={true}
|
miniMode={true}
|
||||||
showMenubar={false}
|
showMenubar={false}
|
||||||
/>;
|
/>;
|
||||||
|
|
|
@ -62,67 +62,22 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
// Don't render anything as we are about to transition
|
// Don't render anything as we are about to transition
|
||||||
if (!app || isPinned) return null;
|
if (!app || isPinned) return null;
|
||||||
|
|
||||||
const header = <React.Fragment>
|
|
||||||
<h2>{ WidgetUtils.getWidgetName(app) }</h2>
|
|
||||||
</React.Fragment>;
|
|
||||||
|
|
||||||
const canModify = WidgetUtils.canUserModifyWidgets(room.roomId);
|
|
||||||
|
|
||||||
let contextMenu;
|
let contextMenu;
|
||||||
if (menuDisplayed) {
|
if (menuDisplayed) {
|
||||||
const rect = handle.current.getBoundingClientRect();
|
const rect = handle.current.getBoundingClientRect();
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<RoomWidgetContextMenu
|
<RoomWidgetContextMenu
|
||||||
chevronFace={ChevronFace.None}
|
chevronFace={ChevronFace.None}
|
||||||
right={window.innerWidth - rect.right}
|
right={window.innerWidth - rect.right - 12}
|
||||||
bottom={window.innerHeight - rect.top}
|
top={rect.bottom + 12}
|
||||||
onFinished={closeMenu}
|
onFinished={closeMenu}
|
||||||
app={app}
|
app={app}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPinClick = () => {
|
const header = <React.Fragment>
|
||||||
WidgetStore.instance.pinWidget(app.id);
|
<h2>{ WidgetUtils.getWidgetName(app) }</h2>
|
||||||
};
|
|
||||||
|
|
||||||
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 }
|
|
||||||
<ContextMenuButton
|
<ContextMenuButton
|
||||||
kind="secondary"
|
kind="secondary"
|
||||||
className="mx_WidgetCard_optionsButton"
|
className="mx_WidgetCard_optionsButton"
|
||||||
|
@ -131,16 +86,12 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
|
||||||
isExpanded={menuDisplayed}
|
isExpanded={menuDisplayed}
|
||||||
label={_t("Options")}
|
label={_t("Options")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
||||||
return <BaseCard
|
return <BaseCard
|
||||||
header={header}
|
header={header}
|
||||||
footer={footer}
|
className="mx_WidgetCard"
|
||||||
className={classNames("mx_WidgetCard", {
|
|
||||||
mx_WidgetCard_noEdit: !canModify,
|
|
||||||
})}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
previousPhase={RightPanelPhases.RoomSummary}
|
previousPhase={RightPanelPhases.RoomSummary}
|
||||||
withoutScrollContainer
|
withoutScrollContainer
|
||||||
|
|
|
@ -24,10 +24,8 @@ import AppTile from '../elements/AppTile';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import * as ScalarMessaging from '../../../ScalarMessaging';
|
import * as ScalarMessaging from '../../../ScalarMessaging';
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||||
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
|
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() {
|
_launchManageIntegrations() {
|
||||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||||
IntegrationManagers.sharedInstance().openAll();
|
IntegrationManagers.sharedInstance().openAll();
|
||||||
|
@ -118,12 +107,9 @@ export default class AppsDrawer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onClickAddWidget = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this._launchManageIntegrations();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (!this.props.showApps) return <div />;
|
||||||
|
|
||||||
const apps = this.state.apps.map((app, index, arr) => {
|
const apps = this.state.apps.map((app, index, arr) => {
|
||||||
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
|
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}
|
fullWidth={arr.length < 2}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
userId={this.props.userId}
|
userId={this.props.userId}
|
||||||
show={this.props.showApps}
|
|
||||||
creatorUserId={app.creatorUserId}
|
creatorUserId={app.creatorUserId}
|
||||||
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||||
waitForIframeLoad={app.waitForIframeLoad}
|
waitForIframeLoad={app.waitForIframeLoad}
|
||||||
|
@ -145,21 +130,6 @@ export default class AppsDrawer extends React.Component {
|
||||||
return <div />;
|
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;
|
let spinner;
|
||||||
if (
|
if (
|
||||||
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
|
||||||
|
@ -191,7 +161,6 @@ export default class AppsDrawer extends React.Component {
|
||||||
{ apps }
|
{ apps }
|
||||||
{ spinner }
|
{ spinner }
|
||||||
</PersistentVResizer>
|
</PersistentVResizer>
|
||||||
{ this._canUserModify() && addWidget }
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@ export default class RoomHeader extends React.Component {
|
||||||
onLeaveClick: PropTypes.func,
|
onLeaveClick: PropTypes.func,
|
||||||
onCancelClick: PropTypes.func,
|
onCancelClick: PropTypes.func,
|
||||||
e2eStatus: PropTypes.string,
|
e2eStatus: PropTypes.string,
|
||||||
|
onAppsClick: PropTypes.func,
|
||||||
|
appsShown: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -230,6 +232,17 @@ export default class RoomHeader extends React.Component {
|
||||||
title={_t("Forget room")} />;
|
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;
|
let searchButton;
|
||||||
if (this.props.onSearchClick && this.props.inRoom) {
|
if (this.props.onSearchClick && this.props.inRoom) {
|
||||||
searchButton =
|
searchButton =
|
||||||
|
@ -243,6 +256,7 @@ export default class RoomHeader extends React.Component {
|
||||||
<div className="mx_RoomHeader_buttons">
|
<div className="mx_RoomHeader_buttons">
|
||||||
{ pinnedEventsButton }
|
{ pinnedEventsButton }
|
||||||
{ forgetButton }
|
{ forgetButton }
|
||||||
|
{ appsButton }
|
||||||
{ searchButton }
|
{ searchButton }
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
|
|
@ -272,13 +272,10 @@ export default class Stickerpicker extends React.Component {
|
||||||
userId={MatrixClientPeg.get().credentials.userId}
|
userId={MatrixClientPeg.get().credentials.userId}
|
||||||
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
creatorUserId={stickerpickerWidget.sender || MatrixClientPeg.get().credentials.userId}
|
||||||
waitForIframeLoad={true}
|
waitForIframeLoad={true}
|
||||||
show={true}
|
|
||||||
showMenubar={true}
|
showMenubar={true}
|
||||||
onEditClick={this._launchManageIntegrations}
|
onEditClick={this._launchManageIntegrations}
|
||||||
onDeleteClick={this._removeStickerpickerWidgets}
|
onDeleteClick={this._removeStickerpickerWidgets}
|
||||||
showTitle={false}
|
showTitle={false}
|
||||||
showMinimise={true}
|
|
||||||
showDelete={false}
|
|
||||||
showCancel={false}
|
showCancel={false}
|
||||||
showPopout={false}
|
showPopout={false}
|
||||||
onMinimiseClick={this._onHideStickersClick}
|
onMinimiseClick={this._onHideStickersClick}
|
||||||
|
|
|
@ -94,14 +94,4 @@ export enum Action {
|
||||||
* Trigged after the phase of the right panel is set. Should be used with AfterRightPanelPhaseChangePayload.
|
* Trigged after the phase of the right panel is set. Should be used with AfterRightPanelPhaseChangePayload.
|
||||||
*/
|
*/
|
||||||
AfterRightPanelPhaseChange = "after_right_panel_phase_change",
|
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",
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1104,6 +1104,8 @@
|
||||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||||
"Join Room": "Join Room",
|
"Join Room": "Join Room",
|
||||||
"Forget room": "Forget room",
|
"Forget room": "Forget room",
|
||||||
|
"Hide Widgets": "Hide Widgets",
|
||||||
|
"Show Widgets": "Show Widgets",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"Invites": "Invites",
|
"Invites": "Invites",
|
||||||
"Favourites": "Favourites",
|
"Favourites": "Favourites",
|
||||||
|
@ -1358,9 +1360,6 @@
|
||||||
"You cancelled verification.": "You cancelled verification.",
|
"You cancelled verification.": "You cancelled verification.",
|
||||||
"Verification cancelled": "Verification cancelled",
|
"Verification cancelled": "Verification cancelled",
|
||||||
"Compare emoji": "Compare emoji",
|
"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",
|
"Sunday": "Sunday",
|
||||||
"Monday": "Monday",
|
"Monday": "Monday",
|
||||||
"Tuesday": "Tuesday",
|
"Tuesday": "Tuesday",
|
||||||
|
@ -1379,6 +1378,7 @@
|
||||||
"Error decrypting audio": "Error decrypting audio",
|
"Error decrypting audio": "Error decrypting audio",
|
||||||
"React": "React",
|
"React": "React",
|
||||||
"Reply": "Reply",
|
"Reply": "Reply",
|
||||||
|
"Edit": "Edit",
|
||||||
"Message Actions": "Message Actions",
|
"Message Actions": "Message Actions",
|
||||||
"Attachment": "Attachment",
|
"Attachment": "Attachment",
|
||||||
"Error decrypting attachment": "Error decrypting attachment",
|
"Error decrypting attachment": "Error decrypting attachment",
|
||||||
|
@ -1476,8 +1476,6 @@
|
||||||
"Delete widget": "Delete widget",
|
"Delete widget": "Delete widget",
|
||||||
"Failed to remove widget": "Failed to remove 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",
|
"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",
|
"Popout widget": "Popout widget",
|
||||||
"More options": "More options",
|
"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",
|
"Use the <a>Desktop app</a> to see all encrypted files": "Use the <a>Desktop app</a> to see all encrypted files",
|
||||||
|
|
|
@ -494,4 +494,16 @@ export default class WidgetUtils {
|
||||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(room, 'type_' + app.type, app.id);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue