Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/dpsah/6785.3

 Conflicts:
	src/components/structures/RoomView.tsx
This commit is contained in:
Michael Telatynski 2020-09-09 15:16:19 +01:00
commit a1f1334250
81 changed files with 3611 additions and 805 deletions

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {CSSProperties, useRef, useState} from "react";
import React, {CSSProperties, RefObject, useRef, useState} from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
@ -416,8 +416,8 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
return menuOptions;
};
export const useContextMenu = () => {
const button = useRef(null);
export const useContextMenu = (): [boolean, RefObject<HTMLElement>, () => void, () => void, (val: boolean) => void] => {
const button = useRef<HTMLElement>(null);
const [isOpen, setIsOpen] = useState(false);
const open = () => {
setIsOpen(true);

View file

@ -23,6 +23,8 @@ import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg";
import { _t } from '../../languageHandler';
import BaseCard from "../views/right_panel/BaseCard";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
/*
* Component which shows the filtered file using a TimelinePanel
@ -30,6 +32,7 @@ import { _t } from '../../languageHandler';
class FilePanel extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
};
// This is used to track if a decrypted event was a live event and should be
@ -188,18 +191,26 @@ class FilePanel extends React.Component {
render() {
if (MatrixClientPeg.get().isGuest()) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
return <BaseCard
className="mx_FilePanel mx_RoomView_messageListWrapper"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<div className="mx_RoomView_empty">
{ _t("You must <a>register</a> to use this functionality",
{},
{ 'a': (sub) => <a href="#/register" key="sub">{ sub }</a> })
}
</div>
</div>;
</BaseCard>;
} else if (this.noRoom) {
return <div className="mx_FilePanel mx_RoomView_messageListWrapper">
return <BaseCard
className="mx_FilePanel mx_RoomView_messageListWrapper"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<div className="mx_RoomView_empty">{ _t("You must join the room to see its files") }</div>
</div>;
</BaseCard>;
}
// wrap a TimelinePanel with the jump-to-event bits turned off.
@ -215,7 +226,12 @@ class FilePanel extends React.Component {
// console.log("rendering TimelinePanel for timelineSet " + this.state.timelineSet.room.roomId + " " +
// "(" + this.state.timelineSet._timelines.join(", ") + ")" + " with key " + this.props.roomId);
return (
<div className="mx_FilePanel" role="tabpanel">
<BaseCard
className="mx_FilePanel"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
withoutScrollContainer
>
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}
@ -226,13 +242,17 @@ class FilePanel extends React.Component {
resizeNotifier={this.props.resizeNotifier}
empty={emptyState}
/>
</div>
</BaseCard>
);
} else {
return (
<div className="mx_FilePanel" role="tabpanel">
<BaseCard
className="mx_FilePanel"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<Loader />
</div>
</BaseCard>
);
}
}

View file

@ -17,14 +17,21 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from "prop-types";
import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index";
import BaseCard from "../views/right_panel/BaseCard";
/*
* Component which shows the global notification list using a TimelinePanel
*/
class NotificationPanel extends React.Component {
static propTypes = {
onClose: PropTypes.func.isRequired,
};
render() {
// wrap a TimelinePanel with the jump-to-event bits turned off.
const TimelinePanel = sdk.getComponent("structures.TimelinePanel");
@ -35,28 +42,27 @@ class NotificationPanel extends React.Component {
<p>{_t('You have no visible notifications in this room.')}</p>
</div>);
let content;
const timelineSet = MatrixClientPeg.get().getNotifTimelineSet();
if (timelineSet) {
return (
<div className="mx_NotificationPanel" role="tabpanel">
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview={false}
tileShape="notif"
empty={emptyState}
/>
</div>
content = (
<TimelinePanel
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview={false}
tileShape="notif"
empty={emptyState}
/>
);
} else {
console.error("No notifTimelineSet available!");
return (
<div className="mx_NotificationPanel" role="tabpanel">
<Loader />
</div>
);
content = <Loader />;
}
return <BaseCard className="mx_NotificationPanel" onClose={this.props.onClose} withoutScrollContainer>
{ content }
</BaseCard>;
}
}

View file

@ -32,6 +32,9 @@ import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPa
import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import defaultDispatcher from "../../dispatcher/dispatcher";
export default class RightPanel extends React.Component {
static get propTypes() {
@ -47,10 +50,10 @@ export default class RightPanel extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
...RightPanelStore.getSharedInstance().roomPanelPhaseParams,
phase: this._getPhaseFromProps(),
isUserPrivilegedInGroup: null,
member: this._getUserForPanel(),
verificationRequest: RightPanelStore.getSharedInstance().roomPanelPhaseParams.verificationRequest,
};
this.onAction = this.onAction.bind(this);
this.onRoomStateMember = this.onRoomStateMember.bind(this);
@ -102,10 +105,6 @@ export default class RightPanel extends React.Component {
}
return RightPanelPhases.RoomMemberInfo;
} else {
if (!RIGHT_PANEL_PHASES_NO_ARGS.includes(rps.roomPanelPhase)) {
dis.dispatch({action: Action.SetRightPanelPhase, phase: RightPanelPhases.RoomMemberList});
return RightPanelPhases.RoomMemberList;
}
return rps.roomPanelPhase;
}
}
@ -186,6 +185,7 @@ export default class RightPanel extends React.Component {
event: payload.event,
verificationRequest: payload.verificationRequest,
verificationRequestPromise: payload.verificationRequestPromise,
widgetId: payload.widgetId,
});
}
}
@ -213,6 +213,14 @@ export default class RightPanel extends React.Component {
}
};
onClose = () => {
// the RightPanelStore has no way of knowing which mode room/group it is in, so we handle closing here
defaultDispatcher.dispatch({
action: Action.ToggleRightPanel,
type: this.props.groupId ? "group" : "room",
});
};
render() {
const MemberList = sdk.getComponent('rooms.MemberList');
const UserInfo = sdk.getComponent('right_panel.UserInfo');
@ -225,36 +233,42 @@ export default class RightPanel extends React.Component {
const GroupRoomInfo = sdk.getComponent('groups.GroupRoomInfo');
let panel = <div />;
const roomId = this.props.room ? this.props.room.roomId : undefined;
switch (this.state.phase) {
case RightPanelPhases.RoomMemberList:
if (this.props.room.roomId) {
panel = <MemberList roomId={this.props.room.roomId} key={this.props.room.roomId} />;
if (roomId) {
panel = <MemberList roomId={roomId} key={roomId} onClose={this.onClose} />;
}
break;
case RightPanelPhases.GroupMemberList:
if (this.props.groupId) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
}
break;
case RightPanelPhases.GroupRoomList:
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
break;
case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.EncryptionPanel:
panel = <UserInfo
user={this.state.member}
roomId={this.props.room.roomId}
key={this.props.room.roomId || this.state.member.userId}
room={this.props.room}
key={roomId || this.state.member.userId}
onClose={this.onCloseUserInfo}
phase={this.state.phase}
verificationRequest={this.state.verificationRequest}
verificationRequestPromise={this.state.verificationRequestPromise}
/>;
break;
case RightPanelPhases.Room3pidMemberInfo:
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.room.roomId} />;
panel = <ThirdPartyMemberInfo event={this.state.event} key={roomId} />;
break;
case RightPanelPhases.GroupMemberInfo:
panel = <UserInfo
user={this.state.member}
@ -262,17 +276,28 @@ export default class RightPanel extends React.Component {
key={this.state.member.userId}
onClose={this.onCloseUserInfo} />;
break;
case RightPanelPhases.GroupRoomInfo:
panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId}
key={this.state.groupRoomId} />;
break;
case RightPanelPhases.NotificationPanel:
panel = <NotificationPanel />;
panel = <NotificationPanel onClose={this.onClose} />;
break;
case RightPanelPhases.FilePanel:
panel = <FilePanel roomId={this.props.room.roomId} resizeNotifier={this.props.resizeNotifier} />;
panel = <FilePanel roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} />;
break;
case RightPanelPhases.RoomSummary:
panel = <RoomSummaryCard room={this.props.room} onClose={this.onClose} />;
break;
case RightPanelPhases.Widget:
panel = <WidgetCard room={this.props.room} widgetId={this.state.widgetId} onClose={this.onClose} />;
break;
}

View file

@ -56,6 +56,7 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions";
import {SettingLevel} from "../../settings/SettingLevel";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import {IMatrixClientCreds} from "../../MatrixClientPeg";
import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel";
@ -1424,7 +1425,10 @@ export default class RoomView extends React.Component<IProps, IState> {
};
private onSettingsClick = () => {
dis.dispatch({ action: 'open_room_settings' });
dis.dispatch({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomSummary,
});
};
private onCancelClick = () => {

View file

@ -104,8 +104,8 @@ class TimelinePanel extends React.Component {
// shape property to be passed to EventTiles
tileShape: PropTypes.string,
// placeholder text to use if the timeline is empty
empty: PropTypes.string,
// placeholder to use if the timeline is empty
empty: PropTypes.node,
// whether to show reactions for an event
showReactions: PropTypes.bool,

View file

@ -124,7 +124,11 @@ export default class LoginComponent extends React.Component {
'm.login.cas': () => this._renderSsoStep("cas"),
'm.login.sso': () => this._renderSsoStep("sso"),
};
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this._initLoginLogic();
}

View file

@ -37,7 +37,7 @@ interface IOptionListProps {
}
interface IOptionProps extends React.ComponentProps<typeof MenuItem> {
iconClassName: string;
iconClassName?: string;
}
interface ICheckboxProps extends React.ComponentProps<typeof MenuItemCheckbox> {
@ -92,7 +92,7 @@ export const IconizedContextMenuCheckbox: React.FC<ICheckboxProps> = ({
export const IconizedContextMenuOption: React.FC<IOptionProps> = ({label, iconClassName, ...props}) => {
return <MenuItem {...props} label={label}>
<span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} />
{ iconClassName && <span className={classNames("mx_IconizedContextMenu_icon", iconClassName)} /> }
<span className="mx_IconizedContextMenu_label">{label}</span>
</MenuItem>;
};

View file

@ -26,6 +26,9 @@ export default class WidgetContextMenu extends React.Component {
// Callback for when the revoke button is clicked. Required.
onRevokeClicked: PropTypes.func.isRequired,
// Callback for when the unpin button is clicked. Required.
onUnpinClicked: PropTypes.func.isRequired,
// Callback for when the snapshot button is clicked. Button not shown
// without a callback.
onSnapshotClicked: PropTypes.func,
@ -70,6 +73,8 @@ export default class WidgetContextMenu extends React.Component {
this.proxyClick(this.props.onRevokeClicked);
};
onUnpinClicked = () => this.proxyClick(this.props.onUnpinClicked);
render() {
const options = [];
@ -81,6 +86,12 @@ export default class WidgetContextMenu extends React.Component {
);
}
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'>

View file

@ -42,6 +42,8 @@ import {WidgetType} from "../../../widgets/WidgetType";
import {Capability} from "../../../widgets/WidgetApi";
import {sleep} from "../../../utils/promise";
import {SettingLevel} from "../../../settings/SettingLevel";
import WidgetStore from "../../../stores/WidgetStore";
import {Action} from "../../../dispatcher/actions";
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
const ENABLE_REACT_PERF = false;
@ -310,35 +312,12 @@ export default class AppTile extends React.Component {
if (this.props.onEditClick) {
this.props.onEditClick();
} else {
// TODO: Open the right manager for the widget
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
'type_' + this.props.app.type,
this.props.app.id,
);
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(
this.props.room,
'type_' + this.props.app.type,
this.props.app.id,
);
}
WidgetUtils.editWidget(this.props.room, this.props.app);
}
}
_onSnapshotClick() {
console.log("Requesting widget snapshot");
ActiveWidgetStore.getWidgetMessaging(this.props.app.id).getScreenshot()
.catch((err) => {
console.error("Failed to get screenshot", err);
})
.then((screenshot) => {
dis.dispatch({
action: 'picture_snapshot',
file: screenshot,
}, true);
});
WidgetUtils.snapshotWidget(this.props.app);
}
/**
@ -419,6 +398,10 @@ export default class AppTile extends React.Component {
}
}
_onUnpinClicked = () => {
WidgetStore.instance.unpinWidget(this.props.app.id);
}
_onRevokeClicked() {
console.info("Revoke widget permissions - %s", this.props.app.id);
this._revokeWidgetPermission();
@ -490,12 +473,20 @@ export default class AppTile extends React.Component {
if (payload.widgetId === this.props.app.id) {
switch (payload.action) {
case 'm.sticker':
if (this._hasCapability('m.sticker')) {
dis.dispatch({action: 'post_sticker_message', data: payload.data});
} else {
console.warn('Ignoring sticker message. Invalid capability');
}
break;
if (this._hasCapability('m.sticker')) {
dis.dispatch({action: 'post_sticker_message', data: payload.data});
} else {
console.warn('Ignoring sticker message. Invalid capability');
}
break;
case Action.AppTileDelete:
this._onDeleteClick();
break;
case Action.AppTileRevoke:
this._onRevokeClicked();
break;
}
}
}
@ -626,7 +617,10 @@ export default class AppTile extends React.Component {
if (WidgetType.JITSI.matches(this.props.app.type)) {
console.log("Replacing Jitsi widget URL with local wrapper");
url = WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: true});
url = WidgetUtils.getLocalJitsiWrapperUrl({
forLocalRender: true,
auth: this.props.app.data ? this.props.app.data.auth : null,
});
url = this._addWurlParams(url);
} else {
url = this._getSafeUrl(this.state.widgetUrl);
@ -637,7 +631,10 @@ export default class AppTile extends React.Component {
_getPopoutUrl() {
if (WidgetType.JITSI.matches(this.props.app.type)) {
return this._templatedUrl(
WidgetUtils.getLocalJitsiWrapperUrl({forLocalRender: false}),
WidgetUtils.getLocalJitsiWrapperUrl({
forLocalRender: false,
auth: this.props.app.data ? this.props.app.data.auth : null,
}),
this.props.app.type,
);
} else {
@ -833,6 +830,7 @@ export default class AppTile extends React.Component {
contextMenu = (
<ContextMenu {...aboveLeftOf(elementRect, null)} onFinished={this._closeContextMenu}>
<WidgetContextMenu
onUnpinClicked={this._onUnpinClicked}
onRevokeClicked={this._onRevokeClicked}
onEditClicked={showEditButton ? this._onEditClick : undefined}
onDeleteClicked={showDeleteButton ? this._onDeleteClick : undefined}

View file

@ -1,63 +0,0 @@
/*
Copyright 2017 New Vector Ltd
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 {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
export default class ManageIntegsButton extends React.Component {
constructor(props) {
super(props);
}
onManageIntegrations = (ev) => {
ev.preventDefault();
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
if (SettingsStore.getValue("feature_many_integration_managers")) {
managers.openAll(this.props.room);
} else {
managers.getPrimaryManager().open(this.props.room);
}
}
};
render() {
let integrationsButton = <div />;
if (IntegrationManagers.sharedInstance().hasManager()) {
integrationsButton = (
<AccessibleTooltipButton
className='mx_RoomHeader_button mx_RoomHeader_manageIntegsButton'
title={_t("Manage Integrations")}
onClick={this.onManageIntegrations}
/>
);
}
return integrationsButton;
}
}
ManageIntegsButton.propTypes = {
room: PropTypes.object.isRequired,
};

View file

@ -76,7 +76,7 @@ export default class PersistentApp extends React.Component {
userId={MatrixClientPeg.get().credentials.userId}
show={true}
creatorUserId={app.creatorUserId}
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={capWhitelist}
showDelete={false}

View file

@ -57,11 +57,14 @@ export default class PowerSelector extends React.Component {
customValue: this.props.value,
selectValue: 0,
};
this._initStateFromProps(this.props);
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
this._initStateFromProps(this.props);
}
// eslint-disable-next-line camelcase
UNSAFE_componentWillReceiveProps(newProps) {
this._initStateFromProps(newProps);

View file

@ -0,0 +1,93 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ReactNode} from 'react';
import classNames from 'classnames';
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {_t} from "../../../languageHandler";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import {Action} from "../../../dispatcher/actions";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
interface IProps {
header?: ReactNode;
footer?: ReactNode;
className?: string;
withoutScrollContainer?: boolean;
previousPhase?: RightPanelPhases;
onClose?(): void;
}
interface IGroupProps {
className?: string;
title: string;
}
export const Group: React.FC<IGroupProps> = ({ className, title, children }) => {
return <div className={classNames("mx_BaseCard_Group", className)}>
<h1>{title}</h1>
{children}
</div>;
};
const BaseCard: React.FC<IProps> = ({
onClose,
className,
header,
footer,
withoutScrollContainer,
previousPhase,
children,
}) => {
let backButton;
if (previousPhase) {
const onBackClick = () => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: previousPhase,
});
};
backButton = <AccessibleButton className="mx_BaseCard_back" onClick={onBackClick} title={_t("Back")} />;
}
let closeButton;
if (onClose) {
closeButton = <AccessibleButton className="mx_BaseCard_close" onClick={onClose} title={_t("Close")} />;
}
if (!withoutScrollContainer) {
children = <AutoHideScrollbar>
{ children }
</AutoHideScrollbar>;
}
return (
<div className={classNames("mx_BaseCard", className)}>
<div className="mx_BaseCard_header">
{ backButton }
{ closeButton }
{ header }
</div>
{ children }
{ footer && <div className="mx_BaseCard_footer">{ footer }</div> }
</div>
);
};
export default BaseCard;

View file

@ -96,8 +96,7 @@ export default abstract class HeaderButtons extends React.Component<IProps, ISta
public abstract renderButtons(): JSX.Element[];
public render() {
// inline style as this will be swapped around in future commits
return <div className="mx_HeaderButtons" role="tablist">
return <div className="mx_HeaderButtons">
{this.renderButtons()}
</div>;
}

View file

@ -19,14 +19,18 @@ limitations under the License.
*/
import React from 'react';
import { _t } from '../../../languageHandler';
import {_t} from '../../../languageHandler';
import HeaderButton from './HeaderButton';
import HeaderButtons, {HeaderKind} from './HeaderButtons';
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads";
import RightPanelStore from "../../../stores/RightPanelStore";
const MEMBER_PHASES = [
const ROOM_INFO_PHASES = [
RightPanelPhases.RoomSummary,
RightPanelPhases.Widget,
RightPanelPhases.FilePanel,
RightPanelPhases.RoomMemberList,
RightPanelPhases.RoomMemberInfo,
RightPanelPhases.EncryptionPanel,
@ -54,22 +58,21 @@ export default class RoomHeaderButtons extends HeaderButtons {
}
}
private onMembersClicked = () => {
if (this.state.phase === RightPanelPhases.RoomMemberInfo) {
// send the active phase to trigger a toggle
// XXX: we should pass refireParams here but then it won't collapse as we desire it to
this.setPhase(RightPanelPhases.RoomMemberInfo);
private onRoomSummaryClicked = () => {
// use roomPanelPhase rather than this.state.phase as it remembers the latest one if we close
const lastPhase = RightPanelStore.getSharedInstance().roomPanelPhase;
if (ROOM_INFO_PHASES.includes(lastPhase)) {
if (this.state.phase === lastPhase) {
this.setPhase(lastPhase);
} else {
this.setPhase(lastPhase, RightPanelStore.getSharedInstance().roomPanelPhaseParams);
}
} else {
// This toggles for us, if needed
this.setPhase(RightPanelPhases.RoomMemberList);
this.setPhase(RightPanelPhases.RoomSummary);
}
};
private onFilesClicked = () => {
// This toggles for us, if needed
this.setPhase(RightPanelPhases.FilePanel);
};
private onNotificationsClicked = () => {
// This toggles for us, if needed
this.setPhase(RightPanelPhases.NotificationPanel);
@ -77,24 +80,22 @@ export default class RoomHeaderButtons extends HeaderButtons {
public renderButtons() {
return [
<HeaderButton key="membersButton" name="membersButton"
title={_t('Members')}
isHighlighted={this.isPhase(MEMBER_PHASES)}
onClick={this.onMembersClicked}
analytics={['Right Panel', 'Member List Button', 'click']}
/>,
<HeaderButton key="filesButton" name="filesButton"
title={_t('Files')}
isHighlighted={this.isPhase(RightPanelPhases.FilePanel)}
onClick={this.onFilesClicked}
analytics={['Right Panel', 'File List Button', 'click']}
/>,
<HeaderButton key="notifsButton" name="notifsButton"
<HeaderButton
key="notifsButton"
name="notifsButton"
title={_t('Notifications')}
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
onClick={this.onNotificationsClicked}
analytics={['Right Panel', 'Notification List Button', 'click']}
/>,
<HeaderButton
key="roomSummaryButton"
name="roomSummaryButton"
title={_t('Room Info')}
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
onClick={this.onRoomSummaryClicked}
analytics={['Right Panel', 'Room Summary Button', 'click']}
/>,
];
}
}

View file

@ -0,0 +1,243 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useCallback, useState, useEffect, useContext} from "react";
import classNames from "classnames";
import {Room} from "matrix-js-sdk/src/models/room";
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useIsEncrypted } from '../../../hooks/useIsEncrypted';
import BaseCard, { Group } from "./BaseCard";
import { _t } from '../../../languageHandler';
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import Modal from "../../../Modal";
import ShareDialog from '../dialogs/ShareDialog';
import {useEventEmitter} from "../../../hooks/useEventEmitter";
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import TextWithTooltip from "../elements/TextWithTooltip";
import BaseAvatar from "../avatars/BaseAvatar";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import WidgetStore, {IApp} from "../../../stores/WidgetStore";
interface IProps {
room: Room;
onClose(): void;
}
interface IAppsSectionProps {
room: Room;
}
interface IButtonProps {
className: string;
onClick(): void;
}
const Button: React.FC<IButtonProps> = ({ children, className, onClick }) => {
return <AccessibleButton
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", className)}
onClick={onClick}
>
{ children }
</AccessibleButton>;
};
export const useWidgets = (room: Room) => {
const [apps, setApps] = useState<IApp[]>(WidgetStore.instance.getApps(room));
const updateApps = useCallback(() => {
// Copy the array so that we always trigger a re-render, as some updates mutate the array of apps/settings
setApps([...WidgetStore.instance.getApps(room)]);
}, [room]);
useEffect(updateApps, [room]);
useEventEmitter(WidgetEchoStore, "update", updateApps);
useEventEmitter(WidgetStore.instance, room.roomId, updateApps);
return apps;
};
const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
const cli = useContext(MatrixClientContext);
const apps = useWidgets(room);
const onManageIntegrations = () => {
const managers = IntegrationManagers.sharedInstance();
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
if (SettingsStore.getValue("feature_many_integration_managers")) {
managers.openAll(room);
} else {
managers.getPrimaryManager().open(room);
}
}
};
return <Group className="mx_RoomSummaryCard_appsGroup" title={_t("Apps")}>
{ apps.map(app => {
const name = WidgetUtils.getWidgetName(app);
const dataTitle = WidgetUtils.getWidgetDataTitle(app);
const subtitle = dataTitle && " - " + dataTitle;
let iconUrls = [require("../../../../res/img/element-icons/room/default_app.svg")];
// heuristics for some better icons until Widgets support their own icons
if (app.type.includes("meeting") || app.type.includes("calendar")) {
iconUrls = [require("../../../../res/img/element-icons/room/default_cal.svg")];
} else if (app.type.includes("pad") || app.type.includes("doc") || app.type.includes("calc")) {
iconUrls = [require("../../../../res/img/element-icons/room/default_doc.svg")];
} else if (app.type.includes("clock")) {
iconUrls = [require("../../../../res/img/element-icons/room/default_clock.svg")];
}
if (app.avatar_url) { // MSC2765
iconUrls.unshift(getHttpUriForMxc(cli.getHomeserverUrl(), app.avatar_url, 20, 20, "crop"));
}
const isPinned = WidgetStore.instance.isPinned(app.id);
const classes = classNames("mx_RoomSummaryCard_icon_app", {
mx_RoomSummaryCard_icon_app_pinned: isPinned,
});
if (isPinned) {
const onClick = () => {
WidgetStore.instance.unpinWidget(app.id);
};
return <AccessibleTooltipButton
key={app.id}
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", classes)}
onClick={onClick}
title={_t("Unpin app")}
>
<BaseAvatar name={app.id} urls={iconUrls} width={20} height={20} />
<span>{name}</span>
{ subtitle }
</AccessibleTooltipButton>
}
const onOpenWidgetClick = () => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.Widget,
refireParams: {
widgetId: app.id,
},
});
};
return (
<Button key={app.id} className={classes} onClick={onOpenWidgetClick}>
<BaseAvatar name={app.id} urls={iconUrls} width={20} height={20} />
<span>{name}</span>
{ subtitle }
</Button>
);
}) }
<AccessibleButton kind="link" onClick={onManageIntegrations}>
{ apps.length > 0 ? _t("Edit apps, bridges & bots") : _t("Add apps, bridges & bots") }
</AccessibleButton>
</Group>;
};
const onRoomMembersClick = () => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
});
};
const onRoomFilesClick = () => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.FilePanel,
});
};
const onRoomSettingsClick = () => {
defaultDispatcher.dispatch({ action: "open_room_settings" });
};
const useMemberCount = (room: Room) => {
const [count, setCount] = useState(room.getJoinedMembers().length);
useEventEmitter(room.currentState, "RoomState.members", () => {
setCount(room.getJoinedMembers().length);
});
return count;
};
const RoomSummaryCard: React.FC<IProps> = ({ room, onClose }) => {
const cli = useContext(MatrixClientContext);
const onShareRoomClick = () => {
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
target: room,
});
};
const isRoomEncrypted = useIsEncrypted(cli, room);
const alias = room.getCanonicalAlias() || room.getAltAliases()[0] || "";
const header = <React.Fragment>
<div className="mx_RoomSummaryCard_avatar" role="presentation">
<RoomAvatar room={room} height={54} width={54} viewAvatarOnClick />
<TextWithTooltip
tooltip={isRoomEncrypted ? _t("Encrypted") : _t("Not encrypted")}
class={classNames("mx_RoomSummaryCard_e2ee", {
mx_RoomSummaryCard_e2ee_secure: isRoomEncrypted,
})}
/>
</div>
<h2 title={room.name}>{ room.name }</h2>
<div className="mx_RoomSummaryCard_alias" title={alias}>
{ alias }
</div>
</React.Fragment>;
const memberCount = useMemberCount(room);
return <BaseCard header={header} className="mx_RoomSummaryCard" onClose={onClose}>
<Group title={_t("About")} className="mx_RoomSummaryCard_aboutGroup">
<Button className="mx_RoomSummaryCard_icon_people" onClick={onRoomMembersClick}>
{_t("%(count)s people", { count: memberCount })}
</Button>
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
{_t("Show files")}
</Button>
<Button className="mx_RoomSummaryCard_icon_share" onClick={onShareRoomClick}>
{_t("Share room")}
</Button>
<Button className="mx_RoomSummaryCard_icon_settings" onClick={onRoomSettingsClick}>
{_t("Room settings")}
</Button>
</Group>
<AppsSection room={room} />
</BaseCard>;
};
export default RoomSummaryCard;

View file

@ -20,7 +20,7 @@ limitations under the License.
import React, {useCallback, useMemo, useState, useEffect, useContext} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {Group, RoomMember, User} from 'matrix-js-sdk';
import {Group, RoomMember, User, Room} from 'matrix-js-sdk';
import dis from '../../../dispatcher/dispatcher';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
@ -31,7 +31,6 @@ import AccessibleButton from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
import {EventTimeline} from "matrix-js-sdk";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import RoomViewStore from "../../../stores/RoomViewStore";
import MultiInviter from "../../../utils/MultiInviter";
import GroupStore from "../../../stores/GroupStore";
@ -46,6 +45,7 @@ import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
import {Action} from "../../../dispatcher/actions";
import {useIsEncrypted} from "../../../hooks/useIsEncrypted";
import BaseCard from "./BaseCard";
const _disambiguateDevices = (devices) => {
const names = Object.create(null);
@ -451,7 +451,7 @@ const _isMuted = (member, powerLevelContent) => {
return member.powerLevel < levelToSend;
};
const useRoomPowerLevels = (cli, room) => {
export const useRoomPowerLevels = (cli, room) => {
const [powerLevels, setPowerLevels] = useState({});
const update = useCallback(() => {
@ -1364,16 +1364,9 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
</React.Fragment>;
};
const UserInfoHeader = ({onClose, member, e2eStatus}) => {
const UserInfoHeader = ({member, e2eStatus}) => {
const cli = useContext(MatrixClientContext);
let closeButton;
if (onClose) {
closeButton = <AccessibleButton className="mx_UserInfo_cancel" onClick={onClose} title={_t('Close')}>
<div />
</AccessibleButton>;
}
const onMemberAvatarClick = useCallback(() => {
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
if (!avatarUrl) return;
@ -1448,7 +1441,6 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
const displayName = member.name || member.displayname;
return <React.Fragment>
{ closeButton }
{ avatarElement }
<div className="mx_UserInfo_container mx_UserInfo_separator">
@ -1471,11 +1463,9 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
</React.Fragment>;
};
const UserInfo = ({user, groupId, roomId, onClose, phase=RightPanelPhases.RoomMemberInfo, ...props}) => {
const UserInfo = ({user, groupId, room, onClose, phase=RightPanelPhases.RoomMemberInfo, ...props}) => {
const cli = useContext(MatrixClientContext);
// Load room if we are given a room id and memoize it - this can be undefined for User Info/Group Member Info
const room = useMemo(() => roomId ? cli.getRoom(roomId) : null, [cli, roomId]);
// fetch latest room member if we have a room, so we don't show historical information, falling back to user
const member = useMemo(() => room ? (room.getMember(user.userId) || user) : user, [room, user]);
@ -1510,15 +1500,16 @@ const UserInfo = ({user, groupId, roomId, onClose, phase=RightPanelPhases.RoomMe
break;
}
return (
<div className={classes.join(" ")} role="tabpanel">
<AutoHideScrollbar className="mx_UserInfo_scrollContainer">
<UserInfoHeader member={member} e2eStatus={e2eStatus} onClose={onClose} />
let previousPhase: RightPanelPhases;
// We have no previousPhase for when viewing a UserInfo from a Group or without a Room at this time
if (room) {
previousPhase = RightPanelPhases.RoomMemberList;
}
{ content }
</AutoHideScrollbar>
</div>
);
const header = <UserInfoHeader member={member} e2eStatus={e2eStatus} onClose={onClose} />;
return <BaseCard className={classes.join(" ")} header={header} onClose={onClose} previousPhase={previousPhase}>
{ content }
</BaseCard>;
};
UserInfo.propTypes = {
@ -1529,7 +1520,7 @@ UserInfo.propTypes = {
]).isRequired,
group: PropTypes.instanceOf(Group),
groupId: PropTypes.string,
roomId: PropTypes.string,
room: PropTypes.instanceOf(Room),
onClose: PropTypes.func,
};

View file

@ -0,0 +1,205 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useContext, useEffect} from "react";
import {Room} from "matrix-js-sdk/src/models/room";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import BaseCard from "./BaseCard";
import WidgetUtils from "../../../utils/WidgetUtils";
import AccessibleButton from "../elements/AccessibleButton";
import AppTile from "../elements/AppTile";
import {_t} from "../../../languageHandler";
import {useWidgets} from "./RoomSummaryCard";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import {Action} from "../../../dispatcher/actions";
import WidgetStore from "../../../stores/WidgetStore";
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
import {ChevronFace, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import {AppTileActionPayload} from "../../../dispatcher/payloads/AppTileActionPayload";
import {Capability} from "../../../widgets/WidgetApi";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import classNames from "classnames";
interface IProps {
room: Room;
widgetId: string;
onClose(): void;
}
const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
const cli = useContext(MatrixClientContext);
const apps = useWidgets(room);
const app = apps.find(a => a.id === widgetId);
const isPinned = app && WidgetStore.instance.isPinned(app.id);
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
useEffect(() => {
if (!app || isPinned) {
// stop showing this card
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomSummary,
});
}
}, [app, isPinned]);
// 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) {
let snapshotButton;
if (ActiveWidgetStore.widgetHasCapability(app.id, Capability.Screenshot)) {
const onSnapshotClick = () => {
WidgetUtils.snapshotWidget(app);
closeMenu();
};
snapshotButton = <IconizedContextMenuOption onClick={onSnapshotClick} label={_t("Take a picture")} />;
}
let deleteButton;
if (canModify) {
const onDeleteClick = () => {
defaultDispatcher.dispatch<AppTileActionPayload>({
action: Action.AppTileDelete,
widgetId: app.id,
});
closeMenu();
};
deleteButton = <IconizedContextMenuOption onClick={onDeleteClick} label={_t("Remove for everyone")} />;
}
const onRevokeClick = () => {
defaultDispatcher.dispatch<AppTileActionPayload>({
action: Action.AppTileRevoke,
widgetId: app.id,
});
closeMenu();
};
const rect = handle.current.getBoundingClientRect();
contextMenu = (
<IconizedContextMenu
chevronFace={ChevronFace.None}
right={window.innerWidth - rect.right}
bottom={window.innerHeight - rect.top}
onFinished={closeMenu}
>
<IconizedContextMenuOptionList>
{ snapshotButton }
{ deleteButton }
<IconizedContextMenuOption onClick={onRevokeClick} label={_t("Remove for me")} />
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
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 apps at a time")}
tooltipClassName="mx_WidgetCard_maxPinnedTooltip"
kind="secondary"
className={pinButtonClasses}
disabled
>
{ _t("Pin to room") }
</AccessibleTooltipButton>;
}
const footer = <React.Fragment>
{ editButton }
{ pinButton }
<ContextMenuButton
kind="secondary"
className="mx_WidgetCard_optionsButton"
inputRef={handle}
onClick={openMenu}
isExpanded={menuDisplayed}
label={_t("Options")}
/>
{ contextMenu }
</React.Fragment>;
return <BaseCard
header={header}
footer={footer}
className={classNames("mx_WidgetCard", {
mx_WidgetCard_noEdit: !canModify,
})}
onClose={onClose}
previousPhase={RightPanelPhases.RoomSummary}
withoutScrollContainer
>
<AppTile
app={app}
fullWidth
show
showMenubar={false}
room={room}
userId={cli.getUserId()}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, room.roomId)}
/>
</BaseCard>;
};
export default WidgetCard;

View file

@ -17,9 +17,10 @@ limitations under the License.
import React, {useState} from 'react';
import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import classNames from 'classnames';
import {Resizable} from "re-resizable";
import AppTile from '../elements/AppTile';
import Modal from '../../../Modal';
import dis from '../../../dispatcher/dispatcher';
import * as sdk from '../../../index';
import * as ScalarMessaging from '../../../ScalarMessaging';
@ -29,13 +30,9 @@ import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton';
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
import classNames from 'classnames';
import {Resizable} from "re-resizable";
import {useLocalStorageState} from "../../../hooks/useLocalStorageState";
import ResizeNotifier from "../../../utils/ResizeNotifier";
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
import WidgetStore from "../../../stores/WidgetStore";
export default class AppsDrawer extends React.Component {
static propTypes = {
@ -61,17 +58,13 @@ export default class AppsDrawer extends React.Component {
componentDidMount() {
ScalarMessaging.startListening();
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
WidgetEchoStore.on('update', this._updateApps);
WidgetStore.instance.on(this.props.room.roomId, this._updateApps);
this.dispatcherRef = dis.register(this.onAction);
}
componentWillUnmount() {
ScalarMessaging.stopListening();
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
WidgetEchoStore.removeListener('update', this._updateApps);
WidgetStore.instance.off(this.props.room.roomId, this._updateApps);
if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
}
@ -100,28 +93,11 @@ export default class AppsDrawer extends React.Component {
}
};
onRoomStateEvents = (ev, state) => {
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
return;
}
this._updateApps();
};
_getApps() {
const widgets = WidgetEchoStore.getEchoedRoomWidgets(
this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room),
);
return widgets.map((ev) => {
return WidgetUtils.makeAppConfig(
ev.getStateKey(), ev.getContent(), ev.getSender(), ev.getRoomId(), ev.getId(),
);
});
}
_getApps = () => WidgetStore.instance.getApps(this.props.room, true);
_updateApps = () => {
const apps = this._getApps();
this.setState({
apps: apps,
apps: this._getApps(),
});
};
@ -144,18 +120,6 @@ export default class AppsDrawer extends React.Component {
onClickAddWidget = (e) => {
e.preventDefault();
// Display a warning dialog if the max number of widgets have already been added to the room
const apps = this._getApps();
if (apps && apps.length >= MAX_WIDGETS) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
const errorMsg = `The maximum number of ${MAX_WIDGETS} widgets have already been added to this room.`;
console.error(errorMsg);
Modal.createDialog(ErrorDialog, {
title: _t('Cannot add any more widgets'),
description: _t('The maximum permitted number of widgets have already been added to this room.'),
});
return;
}
this._launchManageIntegrations();
};
@ -171,7 +135,7 @@ export default class AppsDrawer extends React.Component {
userId={this.props.userId}
show={this.props.showApps}
creatorUserId={app.creatorUserId}
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={capWhitelist}
/>);
@ -243,7 +207,7 @@ const PersistentVResizer = ({
resizeNotifier,
children,
}) => {
const [height, setHeight] = useLocalStorageState("pvr_" + id, 100);
const [height, setHeight] = useLocalStorageState("pvr_" + id, 280); // old fixed height was 273px
const [resizing, setResizing] = useState(false);
return <Resizable

View file

@ -20,13 +20,14 @@ import React from 'react';
import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher/dispatcher';
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {isValid3pidInvite} from "../../../RoomInvite";
import rate_limited_func from "../../../ratelimitedfunc";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import * as sdk from "../../../index";
import CallHandler from "../../../CallHandler";
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import BaseCard from "../right_panel/BaseCard";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
@ -438,7 +439,13 @@ export default class MemberList extends React.Component {
render() {
if (this.state.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return <div className="mx_MemberList"><Spinner /></div>;
return <BaseCard
className="mx_MemberList"
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<Spinner />
</BaseCard>;
}
const SearchBox = sdk.getComponent('structures.SearchBox');
@ -485,25 +492,29 @@ export default class MemberList extends React.Component {
/>;
}
return (
<div className="mx_MemberList" role="tabpanel">
{ inviteButton }
<AutoHideScrollbar>
<div className="mx_MemberList_wrapper">
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined}
getChildCount={this._getChildCountJoined} />
{ invitedHeader }
{ invitedSection }
</div>
</AutoHideScrollbar>
<SearchBox className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
placeholder={ _t('Filter room members') }
onSearch={ this.onSearchQueryChanged } />
</div>
const footer = (
<SearchBox
className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
placeholder={ _t('Filter room members') }
onSearch={ this.onSearchQueryChanged } />
);
return <BaseCard
className="mx_MemberList"
header={inviteButton}
footer={footer}
onClose={this.props.onClose}
previousPhase={RightPanelPhases.RoomSummary}
>
<div className="mx_MemberList_wrapper">
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
createOverflowElement={this._createOverflowTileJoined}
getChildren={this._getChildrenJoined}
getChildCount={this._getChildCountJoined} />
{ invitedHeader }
{ invitedSection }
</div>
</BaseCard>;
}
onInviteButtonClick = () => {

View file

@ -18,14 +18,11 @@ limitations under the License.
import React, {createRef} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import Modal from "../../../Modal";
import RateLimitedFunc from '../../../ratelimitedfunc';
import { linkifyElement } from '../../../HtmlUtils';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
import SettingsStore from "../../../settings/SettingsStore";
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
@ -114,13 +111,6 @@ export default class RoomHeader extends React.Component {
this.forceUpdate();
};
onShareRoomClick = (ev) => {
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
target: this.props.room,
});
};
_hasUnreadPins() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
@ -150,7 +140,6 @@ export default class RoomHeader extends React.Component {
render() {
let searchStatus = null;
let cancelButton = null;
let settingsButton = null;
let pinnedEventsButton = null;
if (this.props.onCancelClick) {
@ -214,14 +203,6 @@ export default class RoomHeader extends React.Component {
/>;
}
if (this.props.onSettingsClick) {
settingsButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_settingsButton"
onClick={this.props.onSettingsClick}
title={_t("Settings")} />;
}
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
let pinsIndicator = null;
if (this._hasUnreadPins()) {
@ -258,26 +239,9 @@ export default class RoomHeader extends React.Component {
title={_t("Search")} />;
}
let shareRoomButton;
if (this.props.inRoom) {
shareRoomButton =
<AccessibleTooltipButton
className="mx_RoomHeader_button mx_RoomHeader_shareButton"
onClick={this.onShareRoomClick}
title={_t('Share room')} />;
}
let manageIntegsButton;
if (this.props.room && this.props.room.roomId && this.props.inRoom) {
manageIntegsButton = <ManageIntegsButton room={this.props.room} />;
}
const rightRow =
<div className="mx_RoomHeader_buttons">
{ settingsButton }
{ pinnedEventsButton }
{ shareRoomButton }
{ manageIntegsButton }
{ forgetButton }
{ searchButton }
</div>;