Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 1bbd273

Browse files
authored
Merge pull request #5247 from matrix-org/t3chguy/feat/room-list-widgets
Left Panel Widget support
2 parents fb3b5d2 + 4e12aec commit 1bbd273

File tree

13 files changed

+385
-49
lines changed

13 files changed

+385
-49
lines changed

res/css/_components.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
@import "./structures/_HeaderButtons.scss";
1414
@import "./structures/_HomePage.scss";
1515
@import "./structures/_LeftPanel.scss";
16+
@import "./structures/_LeftPanelWidget.scss";
1617
@import "./structures/_MainSplit.scss";
1718
@import "./structures/_MatrixChat.scss";
1819
@import "./structures/_MyGroups.scss";
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
.mx_LeftPanelWidget {
18+
// largely based on RoomSublist
19+
margin-left: 8px;
20+
margin-bottom: 4px;
21+
22+
.mx_LeftPanelWidget_headerContainer {
23+
display: flex;
24+
align-items: center;
25+
26+
height: 24px;
27+
color: $roomlist-header-color;
28+
margin-top: 4px;
29+
30+
.mx_LeftPanelWidget_stickable {
31+
flex: 1;
32+
max-width: 100%;
33+
34+
display: flex;
35+
align-items: center;
36+
}
37+
38+
.mx_LeftPanelWidget_headerText {
39+
flex: 1;
40+
max-width: calc(100% - 16px);
41+
line-height: $font-16px;
42+
font-size: $font-13px;
43+
font-weight: 600;
44+
45+
// Ellipsize any text overflow
46+
text-overflow: ellipsis;
47+
overflow: hidden;
48+
white-space: nowrap;
49+
50+
.mx_LeftPanelWidget_collapseBtn {
51+
display: inline-block;
52+
position: relative;
53+
width: 14px;
54+
height: 14px;
55+
margin-right: 6px;
56+
57+
&::before {
58+
content: '';
59+
width: 18px;
60+
height: 18px;
61+
position: absolute;
62+
mask-position: center;
63+
mask-size: contain;
64+
mask-repeat: no-repeat;
65+
background-color: $roomlist-header-color;
66+
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
67+
}
68+
69+
&.mx_LeftPanelWidget_collapseBtn_collapsed::before {
70+
transform: rotate(-90deg);
71+
}
72+
}
73+
}
74+
}
75+
76+
.mx_LeftPanelWidget_resizeBox {
77+
position: relative;
78+
79+
display: flex;
80+
flex-direction: column;
81+
overflow: visible; // let the resize handle out
82+
}
83+
84+
.mx_AppTileFullWidth {
85+
flex: 1 0 0;
86+
overflow: hidden;
87+
// need this to be flex otherwise the overflow hidden from above
88+
// sometimes vertically centers the clipped list ... no idea why it would do this
89+
// as the box model should be top aligned. Happens in both FF and Chromium
90+
display: flex;
91+
flex-direction: column;
92+
box-sizing: border-box;
93+
94+
mask-image: linear-gradient(0deg, transparent, black 4px);
95+
}
96+
97+
.mx_LeftPanelWidget_resizerHandle {
98+
cursor: ns-resize;
99+
border-radius: 3px;
100+
101+
// Override styles from library
102+
width: unset !important;
103+
height: 4px !important;
104+
105+
position: absolute;
106+
top: -24px !important; // override from library - puts it in the margin-top of the headerContainer
107+
108+
// Together, these make the bar 64px wide
109+
// These are also overridden from the library
110+
left: calc(50% - 32px) !important;
111+
right: calc(50% - 32px) !important;
112+
}
113+
114+
&:hover .mx_LeftPanelWidget_resizerHandle {
115+
opacity: 0.8;
116+
background-color: $primary-fg-color;
117+
}
118+
119+
.mx_LeftPanelWidget_maximizeButton {
120+
margin-left: 8px;
121+
margin-right: 7px;
122+
position: relative;
123+
width: 24px;
124+
height: 24px;
125+
border-radius: 32px;
126+
127+
&::before {
128+
content: '';
129+
width: 16px;
130+
height: 16px;
131+
position: absolute;
132+
top: 4px;
133+
left: 4px;
134+
mask-position: center;
135+
mask-size: contain;
136+
mask-repeat: no-repeat;
137+
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg');
138+
background: $muted-fg-color;
139+
}
140+
}
141+
}
142+
143+
.mx_LeftPanelWidget_maximizeButtonTooltip {
144+
margin-top: -3px;
145+
}

res/css/views/rooms/_RoomSublist.scss

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,6 @@ limitations under the License.
5959
width: calc(100% - 22px);
6060
}
6161

62-
&.mx_RoomSublist_headerContainer_stickyBottom {
63-
bottom: 0;
64-
}
65-
6662
// We don't have a top style because the top is dependent on the room list header's
6763
// height, and is therefore calculated in JS.
6864
// The class, mx_RoomSublist_headerContainer_stickyTop, is applied though.

src/accessibility/RovingTabIndex.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
205205
// onFocus should be called when the index gained focus in any manner
206206
// isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}`
207207
// ref should be passed to a DOM node which will be used for DOM compareDocumentPosition
208-
export const useRovingTabIndex = (inputRef: Ref): [FocusHandler, boolean, Ref] => {
208+
export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] => {
209209
const context = useContext(RovingTabIndexContext);
210210
let ref = useRef<HTMLElement>(null);
211211

src/components/structures/LeftPanel.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
3838
import { OwnProfileStore } from "../../stores/OwnProfileStore";
3939
import { MatrixClientPeg } from "../../MatrixClientPeg";
4040
import RoomListNumResults from "../views/rooms/RoomListNumResults";
41+
import LeftPanelWidget from "./LeftPanelWidget";
4142

4243
interface IProps {
4344
isMinimized: boolean;
@@ -142,7 +143,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
142143
const bottomEdge = list.offsetHeight + list.scrollTop;
143144
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist");
144145

145-
const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles
146+
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
146147
const headerStickyWidth = list.clientWidth - headerRightMargin;
147148

148149
// We track which styles we want on a target before making the changes to avoid
@@ -213,10 +214,19 @@ export default class LeftPanel extends React.Component<IProps, IState> {
213214
if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
214215
header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
215216
}
217+
218+
const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight);
219+
const newBottom = `${offset}px`;
220+
if (header.style.bottom !== newBottom) {
221+
header.style.bottom = newBottom;
222+
}
216223
} else {
217224
if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
218225
header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom");
219226
}
227+
if (header.style.bottom) {
228+
header.style.removeProperty('bottom');
229+
}
220230
}
221231

222232
if (style.stickyTop || style.stickyBottom) {
@@ -425,6 +435,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
425435
{roomList}
426436
</div>
427437
</div>
438+
{ !this.props.isMinimized && <LeftPanelWidget onResize={this.onResize} /> }
428439
</aside>
429440
</div>
430441
);
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import React, {useContext, useEffect, useMemo} from "react";
18+
import {Resizable} from "re-resizable";
19+
import classNames from "classnames";
20+
21+
import AccessibleButton from "../views/elements/AccessibleButton";
22+
import {useRovingTabIndex} from "../../accessibility/RovingTabIndex";
23+
import {Key} from "../../Keyboard";
24+
import {useLocalStorageState} from "../../hooks/useLocalStorageState";
25+
import MatrixClientContext from "../../contexts/MatrixClientContext";
26+
import WidgetUtils, {IWidgetEvent} from "../../utils/WidgetUtils";
27+
import {useAccountData} from "../../hooks/useAccountData";
28+
import AppTile from "../views/elements/AppTile";
29+
import {useSettingValue} from "../../hooks/useSettings";
30+
31+
interface IProps {
32+
onResize(): void;
33+
}
34+
35+
const MIN_HEIGHT = 100;
36+
const MAX_HEIGHT = 500; // or 50% of the window height
37+
const INITIAL_HEIGHT = 280;
38+
39+
const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
40+
const cli = useContext(MatrixClientContext);
41+
42+
const mWidgetsEvent = useAccountData<Record<string, IWidgetEvent>>(cli, "m.widgets");
43+
const leftPanelWidgetId = useSettingValue("Widgets.leftPanel");
44+
const app = useMemo(() => {
45+
if (!mWidgetsEvent || !leftPanelWidgetId) return null;
46+
const widgetConfig = Object.values(mWidgetsEvent).find(w => w.id === leftPanelWidgetId);
47+
if (!widgetConfig) return null;
48+
49+
return WidgetUtils.makeAppConfig(
50+
widgetConfig.state_key,
51+
widgetConfig.content,
52+
widgetConfig.sender,
53+
null,
54+
widgetConfig.id);
55+
}, [mWidgetsEvent, leftPanelWidgetId]);
56+
57+
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
58+
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
59+
useEffect(onResize, [expanded]);
60+
61+
const [onFocus, isActive, ref] = useRovingTabIndex();
62+
const tabIndex = isActive ? 0 : -1;
63+
64+
if (!app) return null;
65+
66+
let content;
67+
if (expanded) {
68+
content = <Resizable
69+
size={{height} as any}
70+
minHeight={MIN_HEIGHT}
71+
maxHeight={Math.min(window.innerHeight / 2, MAX_HEIGHT)}
72+
onResize={onResize}
73+
onResizeStop={(e, dir, ref, d) => {
74+
setHeight(height + d.height);
75+
}}
76+
handleWrapperClass="mx_LeftPanelWidget_resizerHandles"
77+
handleClasses={{top: "mx_LeftPanelWidget_resizerHandle"}}
78+
className="mx_LeftPanelWidget_resizeBox"
79+
enable={{ top: true }}
80+
>
81+
<AppTile
82+
app={app}
83+
fullWidth
84+
show
85+
showMenubar={false}
86+
userWidget
87+
userId={cli.getUserId()}
88+
creatorUserId={app.creatorUserId}
89+
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
90+
waitForIframeLoad={app.waitForIframeLoad}
91+
/>
92+
</Resizable>;
93+
}
94+
95+
return <div className="mx_LeftPanelWidget">
96+
<div
97+
onFocus={onFocus}
98+
className="mx_LeftPanelWidget_headerContainer"
99+
onKeyDown={(ev: React.KeyboardEvent) => {
100+
switch (ev.key) {
101+
case Key.ARROW_LEFT:
102+
ev.stopPropagation();
103+
setExpanded(false);
104+
break;
105+
case Key.ARROW_RIGHT: {
106+
ev.stopPropagation();
107+
setExpanded(true);
108+
break;
109+
}
110+
}
111+
}}
112+
>
113+
<div className="mx_LeftPanelWidget_stickable">
114+
<AccessibleButton
115+
onFocus={onFocus}
116+
inputRef={ref}
117+
tabIndex={tabIndex}
118+
className="mx_LeftPanelWidget_headerText"
119+
role="treeitem"
120+
aria-expanded={expanded}
121+
aria-level={1}
122+
onClick={() => {
123+
setExpanded(e => !e);
124+
}}
125+
>
126+
<span className={classNames({
127+
"mx_LeftPanelWidget_collapseBtn": true,
128+
"mx_LeftPanelWidget_collapseBtn_collapsed": !expanded,
129+
})} />
130+
<span>{ WidgetUtils.getWidgetName(app) }</span>
131+
</AccessibleButton>
132+
133+
{/* Code for the maximise button for once we have full screen widgets */}
134+
{/*<AccessibleTooltipButton
135+
tabIndex={tabIndex}
136+
onClick={() => {
137+
}}
138+
className="mx_LeftPanelWidget_maximizeButton"
139+
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
140+
title={_t("Maximize")}
141+
/>*/}
142+
</div>
143+
</div>
144+
145+
{ content }
146+
</div>;
147+
};
148+
149+
export default LeftPanelWidget;

src/components/views/elements/AppTile.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export default class AppTile extends React.Component {
6161
// This is a function to make the impact of calling SettingsStore slightly less
6262
hasPermissionToLoad = (props) => {
6363
if (this._usingLocalWidget()) return true;
64+
if (!props.room) return true; // user widgets always have permissions
6465

6566
const currentlyAllowedWidgets = SettingsStore.getValue("allowedWidgets", props.room.roomId);
6667
if (currentlyAllowedWidgets[props.app.eventId] === undefined) {
@@ -335,6 +336,7 @@ export default class AppTile extends React.Component {
335336
</div>
336337
);
337338
if (!this.state.hasPermissionToLoad) {
339+
// only possible for room widgets, can assert this.props.room here
338340
const isEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
339341
appTileBody = (
340342
<div className={appTileBodyClass}>
@@ -446,7 +448,9 @@ AppTile.displayName = 'AppTile';
446448

447449
AppTile.propTypes = {
448450
app: PropTypes.object.isRequired,
449-
room: PropTypes.object.isRequired,
451+
// If room is not specified then it is an account level widget
452+
// which bypasses permission prompts as it was added explicitly by that user
453+
room: PropTypes.object,
450454
// Specifying 'fullWidth' as true will render the app tile to fill the width of the app drawer continer.
451455
// This should be set to true when there is only one widget in the app drawer, otherwise it should be false.
452456
fullWidth: PropTypes.bool,

0 commit comments

Comments
 (0)