Create new right panel cards

This commit is contained in:
Michael Telatynski 2020-09-08 08:48:03 +01:00
parent eb7f6f4c4b
commit 31cca5e0f2
14 changed files with 844 additions and 43 deletions

View file

@ -155,9 +155,12 @@
@import "./views/messages/_UnknownBody.scss"; @import "./views/messages/_UnknownBody.scss";
@import "./views/messages/_ViewSourceEvent.scss"; @import "./views/messages/_ViewSourceEvent.scss";
@import "./views/messages/_common_CryptoEvent.scss"; @import "./views/messages/_common_CryptoEvent.scss";
@import "./views/right_panel/_BaseCard.scss";
@import "./views/right_panel/_EncryptionInfo.scss"; @import "./views/right_panel/_EncryptionInfo.scss";
@import "./views/right_panel/_RoomSummaryCard.scss";
@import "./views/right_panel/_UserInfo.scss"; @import "./views/right_panel/_UserInfo.scss";
@import "./views/right_panel/_VerificationPanel.scss"; @import "./views/right_panel/_VerificationPanel.scss";
@import "./views/right_panel/_WidgetCard.scss";
@import "./views/room_settings/_AliasSettings.scss"; @import "./views/room_settings/_AliasSettings.scss";
@import "./views/rooms/_AppsDrawer.scss"; @import "./views/rooms/_AppsDrawer.scss";
@import "./views/rooms/_Autocomplete.scss"; @import "./views/rooms/_Autocomplete.scss";

View file

@ -0,0 +1,164 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_BaseCard {
padding: 0 8px;
overflow: hidden;
display: flex;
flex-direction: column;
flex: 1;
.mx_BaseCard_header {
margin: 8px 0;
h2 {
margin: 0 44px;
font-size: $font-18px;
font-weight: $font-semi-bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mx_BaseCard_back, .mx_BaseCard_close {
position: absolute;
background-color: rgba(141, 151, 165, 0.2); // TODO
height: 20px;
width: 20px;
margin: 12px;
top: 0;
&::before {
content: "";
position: absolute;
height: 16px;
width: 16px;
top: 2px;
left: 2px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $rightpanel-button-color;
}
}
.mx_BaseCard_back {
border-radius: 4px;
left: 0;
&::before {
transform: rotate(90deg);
mask-size: 20px;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); // TODO
}
}
.mx_BaseCard_close {
border-radius: 10px;
right: 0;
&::before {
mask-image: url('$(res)/img/icons-close.svg'); // TODO
}
}
}
.mx_AutoHideScrollbar {
// collapse the margin into a padding to move the scrollbar into the right gutter
margin-right: -8px;
padding-right: 8px;
min-height: 0;
width: 100%;
height: 100%;
}
.mx_BaseCard_Group {
margin: 20px 0 16px;
& > * {
margin-left: 10px;
margin-right: 10px;
}
h1 {
color: $tertiary-fg-color;
font-size: $font-12px;
font-weight: 500;
}
.mx_BaseCard_Button {
padding: 10px 38px 10px 12px;
margin: 0;
position: relative;
font-size: $font-13px;
height: 20px;
line-height: 20px;
border-radius: 8px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&:hover {
background-color: rgba(141, 151, 165, 0.1);
}
&::after {
content: '';
position: absolute;
top: 10px;
right: 6px;
height: 20px;
width: 20px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $icon-button-color;
transform: rotate(270deg);
mask-size: 20px;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); // TODO
}
}
}
.mx_BaseCard_footer {
padding-top: 4px;
text-align: center;
.mx_AccessibleButton_kind_secondary {
color: $secondary-fg-color;
background-color: rgba(141, 151, 165, 0.2); // TODO
font-weight: $font-semi-bold;
font-size: $font-14px;
min-width: 70px;
& + .mx_AccessibleButton_kind_secondary {
margin-left: 16px;
}
}
}
}
.mx_FilePanel,
.mx_UserInfo,
.mx_NotificationPanel,
.mx_MemberList {
&.mx_BaseCard {
padding: 32px 0 0;
.mx_AutoHideScrollbar {
margin-right: unset;
padding-right: unset;
}
}
}

View file

@ -0,0 +1,157 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_RoomSummaryCard {
// shrink left gutter by 12 and instead add it as margin to all things except the buttons
// as their hover effect should go into the gutter
& > * { // TODO consolidate this as the standard effect
margin-left: 10px;
margin-right: 10px;
}
.mx_AutoHideScrollbar {
margin-left: 0;
}
.mx_BaseCard_header {
text-align: center;
margin-top: 20px;
h2 {
font-weight: $font-semi-bold;
font-size: $font-18px;
margin: 12px 0 4px;
}
.mx_RoomSummaryCard_alias {
font-size: $font-13px;
color: $secondary-fg-color;
}
h2, .mx_RoomSummaryCard_alias {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.mx_RoomSummaryCard_avatar {
display: inline-flex;
.mx_RoomSummaryCard_e2ee {
display: inline-block;
position: relative;
width: 54px;
height: 54px;
border-radius: 50%;
background-color: #737D8C;
margin-top: -3px; // alignment
margin-left: -10px; // overlap
border: 3px solid $dark-panel-bg-color;
&::before {
content: '';
position: absolute;
top: 13px;
left: 13px;
height: 28px;
width: 28px;
mask-size: cover;
mask-repeat: no-repeat;
mask-position: center;
mask-image: url('$(res)/img/e2e/disabled.svg');
background-color: #ffffff;
}
}
.mx_RoomSummaryCard_e2ee_secure{
background-color: #5ABFF2;
&::before {
mask-image: url('$(res)/img/e2e/normal.svg');
}
}
}
}
.mx_RoomSummaryCard_aboutGroup {
.mx_RoomSummaryCard_Button {
padding-left: 48px;
&::before {
content: '';
position: absolute;
top: 8px;
left: 8px;
height: 24px;
width: 24px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $icon-button-color;
}
}
}
.mx_RoomSummaryCard_appsGroup {
.mx_RoomSummaryCard_Button {
padding-left: 10px;
color: $tertiary-fg-color;
span {
color: $primary-fg-color;
}
img {
vertical-align: top;
margin-right: 18px;
border-radius: 4px;
}
&::before {
content: unset;
}
}
.mx_RoomSummaryCard_icon_app_pinned::after {
mask-image: url('$(res)/img/element-icons/room/pin2.svg');
background-color: $accent-color;
transform: unset;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
margin-top: 12px;
margin-bottom: 12px;
font-size: $font-13px;
font-weight: $font-semi-bold;
}
}
.mx_RoomSummaryCard_icon_people::before {
mask-image: url("$(res)/img/element-icons/room/members.svg");
}
.mx_RoomSummaryCard_icon_files::before {
mask-image: url('$(res)/img/element-icons/room/files.svg');
}
.mx_RoomSummaryCard_icon_share::before {
mask-image: url('$(res)/img/element-icons/room/share.svg');
}
.mx_RoomSummaryCard_icon_settings::before {
mask-image: url('$(res)/img/element-icons/settings.svg');
}

View file

@ -0,0 +1,25 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_WidgetCard {
height: 100%;
display: contents;
.mx_AppTileFullWidth {
height: 100%;
border: 0;
}
}

View file

@ -236,10 +236,6 @@ limitations under the License.
} }
} }
.mx_RoomHeader_settingsButton::before {
mask-image: url('$(res)/img/element-icons/settings.svg');
}
.mx_RoomHeader_forgetButton::before { .mx_RoomHeader_forgetButton::before {
mask-image: url('$(res)/img/element-icons/leave.svg'); mask-image: url('$(res)/img/element-icons/leave.svg');
width: 26px; width: 26px;
@ -249,14 +245,6 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/room/search-inset.svg'); mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
} }
.mx_RoomHeader_shareButton::before {
mask-image: url('$(res)/img/element-icons/room/share.svg');
}
.mx_RoomHeader_manageIntegsButton::before {
mask-image: url('$(res)/img/element-icons/room/integrations.svg');
}
.mx_RoomHeader_showPanel { .mx_RoomHeader_showPanel {
height: 16px; height: 16px;
} }

View file

@ -32,6 +32,9 @@ import {RightPanelPhases, RIGHT_PANEL_PHASES_NO_ARGS} from "../../stores/RightPa
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions"; 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 { export default class RightPanel extends React.Component {
static get propTypes() { static get propTypes() {
@ -182,6 +185,7 @@ export default class RightPanel extends React.Component {
event: payload.event, event: payload.event,
verificationRequest: payload.verificationRequest, verificationRequest: payload.verificationRequest,
verificationRequestPromise: payload.verificationRequestPromise, verificationRequestPromise: payload.verificationRequestPromise,
widgetId: payload.widgetId,
}); });
} }
} }
@ -209,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() { render() {
const MemberList = sdk.getComponent('rooms.MemberList'); const MemberList = sdk.getComponent('rooms.MemberList');
const UserInfo = sdk.getComponent('right_panel.UserInfo'); const UserInfo = sdk.getComponent('right_panel.UserInfo');
@ -225,17 +237,24 @@ export default class RightPanel extends React.Component {
switch (this.state.phase) { switch (this.state.phase) {
case RightPanelPhases.RoomMemberList: case RightPanelPhases.RoomMemberList:
if (this.props.room.roomId) { if (this.props.room.roomId) {
panel = <MemberList roomId={this.props.room.roomId} key={this.props.room.roomId} />; panel = <MemberList
roomId={this.props.room.roomId}
key={this.props.room.roomId}
onClose={this.onClose}
/>;
} }
break; break;
case RightPanelPhases.GroupMemberList: case RightPanelPhases.GroupMemberList:
if (this.props.groupId) { if (this.props.groupId) {
panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />; panel = <GroupMemberList groupId={this.props.groupId} key={this.props.groupId} />;
} }
break; break;
case RightPanelPhases.GroupRoomList: case RightPanelPhases.GroupRoomList:
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />; panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
break; break;
case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.RoomMemberInfo:
case RightPanelPhases.EncryptionPanel: case RightPanelPhases.EncryptionPanel:
panel = <UserInfo panel = <UserInfo
@ -248,9 +267,11 @@ export default class RightPanel extends React.Component {
verificationRequestPromise={this.state.verificationRequestPromise} verificationRequestPromise={this.state.verificationRequestPromise}
/>; />;
break; break;
case RightPanelPhases.Room3pidMemberInfo: case RightPanelPhases.Room3pidMemberInfo:
panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.room.roomId} />; panel = <ThirdPartyMemberInfo event={this.state.event} key={this.props.room.roomId} />;
break; break;
case RightPanelPhases.GroupMemberInfo: case RightPanelPhases.GroupMemberInfo:
panel = <UserInfo panel = <UserInfo
user={this.state.member} user={this.state.member}
@ -258,17 +279,31 @@ export default class RightPanel extends React.Component {
key={this.state.member.userId} key={this.state.member.userId}
onClose={this.onCloseUserInfo} />; onClose={this.onCloseUserInfo} />;
break; break;
case RightPanelPhases.GroupRoomInfo: case RightPanelPhases.GroupRoomInfo:
panel = <GroupRoomInfo panel = <GroupRoomInfo
groupRoomId={this.state.groupRoomId} groupRoomId={this.state.groupRoomId}
groupId={this.props.groupId} groupId={this.props.groupId}
key={this.state.groupRoomId} />; key={this.state.groupRoomId} />;
break; break;
case RightPanelPhases.NotificationPanel: case RightPanelPhases.NotificationPanel:
panel = <NotificationPanel />; panel = <NotificationPanel onClose={this.onClose} />;
break; break;
case RightPanelPhases.FilePanel: case RightPanelPhases.FilePanel:
panel = <FilePanel roomId={this.props.room.roomId} resizeNotifier={this.props.resizeNotifier} />; panel = <FilePanel
roomId={this.props.room.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; break;
} }

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 abstract renderButtons(): JSX.Element[];
public render() { public render() {
// inline style as this will be swapped around in future commits return <div className="mx_HeaderButtons">
return <div className="mx_HeaderButtons" role="tablist">
{this.renderButtons()} {this.renderButtons()}
</div>; </div>;
} }

View file

@ -26,7 +26,10 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {ActionPayload} from "../../../dispatcher/payloads"; import {ActionPayload} from "../../../dispatcher/payloads";
const MEMBER_PHASES = [ const ROOM_INFO_PHASES = [
RightPanelPhases.RoomSummary,
RightPanelPhases.Widget,
RightPanelPhases.FilePanel,
RightPanelPhases.RoomMemberList, RightPanelPhases.RoomMemberList,
RightPanelPhases.RoomMemberInfo, RightPanelPhases.RoomMemberInfo,
RightPanelPhases.EncryptionPanel, RightPanelPhases.EncryptionPanel,
@ -54,20 +57,10 @@ export default class RoomHeaderButtons extends HeaderButtons {
} }
} }
private onMembersClicked = () => { // TODO make it restore whatever widget they were on last
if (this.state.phase === RightPanelPhases.RoomMemberInfo) { private onRoomSummaryClicked = () => {
// 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);
} else {
// This toggles for us, if needed // 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 = () => { private onNotificationsClicked = () => {
@ -77,19 +70,17 @@ export default class RoomHeaderButtons extends HeaderButtons {
public renderButtons() { public renderButtons() {
return [ return [
<HeaderButton key="membersButton" name="membersButton" <HeaderButton
title={_t('Members')} key="roomSummaryButton"
isHighlighted={this.isPhase(MEMBER_PHASES)} name="roomSummaryButton"
onClick={this.onMembersClicked} title={_t('Room Info')}
analytics={['Right Panel', 'Member List Button', 'click']} isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
onClick={this.onRoomSummaryClicked}
analytics={['Right Panel', 'Room Summary Button', 'click']}
/>, />,
<HeaderButton key="filesButton" name="filesButton" <HeaderButton
title={_t('Files')} key="notifsButton"
isHighlighted={this.isPhase(RightPanelPhases.FilePanel)} name="notifsButton"
onClick={this.onFilesClicked}
analytics={['Right Panel', 'File List Button', 'click']}
/>,
<HeaderButton key="notifsButton" name="notifsButton"
title={_t('Notifications')} title={_t('Notifications')}
isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)} isHighlighted={this.isPhase(RightPanelPhases.NotificationPanel)}
onClick={this.onNotificationsClicked} onClick={this.onNotificationsClicked}

View file

@ -0,0 +1,231 @@
/*
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 {IApp, WidgetStore} 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") : _t("Add applications") }
</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 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>;
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: room.getJoinedMembers().length })}
</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

@ -0,0 +1,107 @@
/*
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";
interface IProps {
room: Room;
widgetId: string;
onClose(): void;
}
const onFinished = () => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomSummary,
});
}
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);
useEffect(() => {
if (!app || isPinned) {
// TODO maybe we should do this in the ActiveWidgetStore instead
onFinished();
}
}, [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 onPinClick = () => {
WidgetStore.instance.pinWidget(app.id);
};
const onEditClick = () => {
WidgetUtils.editWidget(room, app);
};
const footer = <React.Fragment>
<AccessibleButton kind="secondary" onClick={onPinClick}>
{ _t("Pin to room") }
</AccessibleButton>
<AccessibleButton kind="secondary" onClick={onEditClick}>
{ _t("Edit") }
</AccessibleButton>
</React.Fragment>;
return <BaseCard
header={header}
footer={footer}
className="mx_WidgetCard"
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

@ -34,4 +34,5 @@ export interface SetRightPanelPhaseRefireParams {
groupRoomId?: string; groupRoomId?: string;
// XXX: The type for event should 'view_3pid_invite' action's payload // XXX: The type for event should 'view_3pid_invite' action's payload
event?: any; event?: any;
widgetId?: string;
} }

View file

@ -607,4 +607,8 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td("Enable experimental, compact IRC style layout"), displayName: _td("Enable experimental, compact IRC style layout"),
default: false, default: false,
}, },
"Widgets.pinned": {
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
default: [],
},
}; };

View file

@ -22,6 +22,8 @@ export enum RightPanelPhases {
NotificationPanel = 'NotificationPanel', NotificationPanel = 'NotificationPanel',
RoomMemberInfo = 'RoomMemberInfo', RoomMemberInfo = 'RoomMemberInfo',
EncryptionPanel = 'EncryptionPanel', EncryptionPanel = 'EncryptionPanel',
RoomSummary = 'RoomSummary',
Widget = 'Widget',
Room3pidMemberInfo = 'Room3pidMemberInfo', Room3pidMemberInfo = 'Room3pidMemberInfo',
// Group stuff // Group stuff
@ -34,6 +36,7 @@ export enum RightPanelPhases {
// These are the phases that are safe to persist (the ones that don't require additional // These are the phases that are safe to persist (the ones that don't require additional
// arguments). // arguments).
export const RIGHT_PANEL_PHASES_NO_ARGS = [ export const RIGHT_PANEL_PHASES_NO_ARGS = [
RightPanelPhases.RoomSummary,
RightPanelPhases.NotificationPanel, RightPanelPhases.NotificationPanel,
RightPanelPhases.FilePanel, RightPanelPhases.FilePanel,
RightPanelPhases.RoomMemberList, RightPanelPhases.RoomMemberList,