Implement Left Panel User Widget
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
f699be971e
commit
956a3bf69d
6 changed files with 315 additions and 3 deletions
|
@ -13,6 +13,7 @@
|
||||||
@import "./structures/_HeaderButtons.scss";
|
@import "./structures/_HeaderButtons.scss";
|
||||||
@import "./structures/_HomePage.scss";
|
@import "./structures/_HomePage.scss";
|
||||||
@import "./structures/_LeftPanel.scss";
|
@import "./structures/_LeftPanel.scss";
|
||||||
|
@import "./structures/_LeftPanelWidget.scss";
|
||||||
@import "./structures/_MainSplit.scss";
|
@import "./structures/_MainSplit.scss";
|
||||||
@import "./structures/_MatrixChat.scss";
|
@import "./structures/_MatrixChat.scss";
|
||||||
@import "./structures/_MyGroups.scss";
|
@import "./structures/_MyGroups.scss";
|
||||||
|
|
145
res/css/structures/_LeftPanelWidget.scss
Normal file
145
res/css/structures/_LeftPanelWidget.scss
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
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_LeftPanelWidget {
|
||||||
|
// largely based on RoomSublist
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_headerContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 24px;
|
||||||
|
color: $roomlist-header-color;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_stickable {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_headerText {
|
||||||
|
flex: 1;
|
||||||
|
max-width: calc(100% - 16px);
|
||||||
|
line-height: $font-16px;
|
||||||
|
font-size: $font-13px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
// Ellipsize any text overflow
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_collapseBtn {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-right: 6px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
position: absolute;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background-color: $roomlist-header-color;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_LeftPanelWidget_collapseBtn_collapsed::before {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_resizeBox {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: visible; // let the resize handle out
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AppTileFullWidth {
|
||||||
|
flex: 1 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
// need this to be flex otherwise the overflow hidden from above
|
||||||
|
// sometimes vertically centers the clipped list ... no idea why it would do this
|
||||||
|
// as the box model should be top aligned. Happens in both FF and Chromium
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
mask-image: linear-gradient(0deg, transparent, black 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_resizerHandle {
|
||||||
|
cursor: ns-resize;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
// Override styles from library
|
||||||
|
width: unset !important;
|
||||||
|
height: 4px !important;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: -24px !important; // override from library - puts it in the margin-top of the headerContainer
|
||||||
|
|
||||||
|
// Together, these make the bar 64px wide
|
||||||
|
// These are also overridden from the library
|
||||||
|
left: calc(50% - 32px) !important;
|
||||||
|
right: calc(50% - 32px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .mx_LeftPanelWidget_resizerHandle {
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_maximizeButton {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 7px;
|
||||||
|
position: relative;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 32px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg');
|
||||||
|
background: $muted-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_maximizeButtonTooltip {
|
||||||
|
margin-top: -3px;
|
||||||
|
}
|
|
@ -38,6 +38,7 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||||
|
import LeftPanelWidget from "./LeftPanelWidget";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
@ -432,6 +433,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
{roomList}
|
{roomList}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{ !this.props.isMinimized && <LeftPanelWidget onResize={this.onResize} /> }
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
160
src/components/structures/LeftPanelWidget.tsx
Normal file
160
src/components/structures/LeftPanelWidget.tsx
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
/*
|
||||||
|
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, useMemo} from "react";
|
||||||
|
import {Resizable} from "re-resizable";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
import {_t} from "../../languageHandler";
|
||||||
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
|
import {useRovingTabIndex} from "../../accessibility/RovingTabIndex";
|
||||||
|
import {Key} from "../../Keyboard";
|
||||||
|
import {useLocalStorageState} from "../../hooks/useLocalStorageState";
|
||||||
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
import WidgetUtils, {IWidget} from "../../utils/WidgetUtils";
|
||||||
|
import {useAccountData} from "../../hooks/useAccountData";
|
||||||
|
import AppTile from "../views/elements/AppTile";
|
||||||
|
import {useSettingValue} from "../../hooks/useSettings";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onResize(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MIN_HEIGHT = 100;
|
||||||
|
const MAX_HEIGHT = 500;
|
||||||
|
const INITIAL_HEIGHT = 280;
|
||||||
|
|
||||||
|
const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
|
const mWidgetsEvent = useAccountData<Record<string, IWidget>>(cli, "m.widgets");
|
||||||
|
const leftPanelWidgetId = useSettingValue("Widgets.leftPanel");
|
||||||
|
const app = useMemo(() => {
|
||||||
|
if (!mWidgetsEvent || !leftPanelWidgetId) return null;
|
||||||
|
const widgetConfig = Object.values(mWidgetsEvent).find(w => w.id === leftPanelWidgetId);
|
||||||
|
if (!widgetConfig) return null;
|
||||||
|
|
||||||
|
return WidgetUtils.makeAppConfig(
|
||||||
|
widgetConfig.state_key,
|
||||||
|
widgetConfig.content,
|
||||||
|
widgetConfig.sender,
|
||||||
|
null,
|
||||||
|
widgetConfig.id);
|
||||||
|
}, [cli, mWidgetsEvent, leftPanelWidgetId]);
|
||||||
|
|
||||||
|
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
|
||||||
|
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
|
||||||
|
useEffect(onResize, [expanded]);
|
||||||
|
|
||||||
|
const [onFocus, isActive, ref] = useRovingTabIndex();
|
||||||
|
const tabIndex = isActive ? 0 : -1;
|
||||||
|
|
||||||
|
if (!app) return null;
|
||||||
|
|
||||||
|
let auxButton = null;
|
||||||
|
if (1) {
|
||||||
|
auxButton = (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
onClick={() => {
|
||||||
|
console.log("@@ Maximise Left Panel Widget")
|
||||||
|
}}
|
||||||
|
className="mx_LeftPanelWidget_maximizeButton"
|
||||||
|
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
|
||||||
|
title={_t("Maximize")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (expanded) {
|
||||||
|
content = <Resizable
|
||||||
|
size={{height} as any}
|
||||||
|
minHeight={MIN_HEIGHT}
|
||||||
|
maxHeight={MAX_HEIGHT}
|
||||||
|
onResize={onResize}
|
||||||
|
onResizeStop={(e, dir, ref, d) => {
|
||||||
|
setHeight(height + d.height);
|
||||||
|
}}
|
||||||
|
handleWrapperClass="mx_LeftPanelWidget_resizerHandles"
|
||||||
|
handleClasses={{top: "mx_LeftPanelWidget_resizerHandle"}}
|
||||||
|
className="mx_LeftPanelWidget_resizeBox"
|
||||||
|
enable={{ top: true }}
|
||||||
|
>
|
||||||
|
<AppTile
|
||||||
|
app={app}
|
||||||
|
fullWidth
|
||||||
|
show
|
||||||
|
showMenubar={false}
|
||||||
|
userWidget
|
||||||
|
userId={cli.getUserId()}
|
||||||
|
creatorUserId={app.creatorUserId}
|
||||||
|
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
|
||||||
|
waitForIframeLoad={app.waitForIframeLoad}
|
||||||
|
/>
|
||||||
|
</Resizable>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_LeftPanelWidget">
|
||||||
|
<div
|
||||||
|
onFocus={onFocus}
|
||||||
|
className={classNames({
|
||||||
|
"mx_LeftPanelWidget_headerContainer": true,
|
||||||
|
"mx_LeftPanelWidget_headerContainer_withAux": !!auxButton,
|
||||||
|
})}
|
||||||
|
onKeyDown={(ev: React.KeyboardEvent) => {
|
||||||
|
switch (ev.key) {
|
||||||
|
case Key.ARROW_LEFT:
|
||||||
|
ev.stopPropagation();
|
||||||
|
setExpanded(false);
|
||||||
|
break;
|
||||||
|
case Key.ARROW_RIGHT: {
|
||||||
|
ev.stopPropagation();
|
||||||
|
setExpanded(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="mx_LeftPanelWidget_stickable">
|
||||||
|
<AccessibleButton
|
||||||
|
onFocus={onFocus}
|
||||||
|
inputRef={ref}
|
||||||
|
tabIndex={tabIndex}
|
||||||
|
className="mx_LeftPanelWidget_headerText"
|
||||||
|
role="treeitem"
|
||||||
|
aria-expanded={expanded}
|
||||||
|
aria-level={1}
|
||||||
|
onClick={() => {
|
||||||
|
setExpanded(e => !e);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className={classNames({
|
||||||
|
"mx_LeftPanelWidget_collapseBtn": true,
|
||||||
|
"mx_LeftPanelWidget_collapseBtn_collapsed": !expanded,
|
||||||
|
})} />
|
||||||
|
<span>{ WidgetUtils.getWidgetName(app) }</span>
|
||||||
|
</AccessibleButton>
|
||||||
|
{ auxButton }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ content }
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LeftPanelWidget;
|
|
@ -620,6 +620,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
supportedLevels: LEVELS_ROOM_OR_ACCOUNT,
|
||||||
default: {},
|
default: {},
|
||||||
},
|
},
|
||||||
|
"Widgets.leftPanel": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
[UIFeature.AdvancedEncryption]: {
|
[UIFeature.AdvancedEncryption]: {
|
||||||
supportedLevels: LEVELS_UI_FEATURE,
|
supportedLevels: LEVELS_UI_FEATURE,
|
||||||
default: true,
|
default: true,
|
||||||
|
|
|
@ -42,7 +42,7 @@ export interface IWidget {
|
||||||
type: string;
|
type: string;
|
||||||
sender: string;
|
sender: string;
|
||||||
state_key: string;
|
state_key: string;
|
||||||
content: IApp;
|
content: Partial<IApp>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class WidgetUtils {
|
export default class WidgetUtils {
|
||||||
|
@ -410,7 +410,7 @@ export default class WidgetUtils {
|
||||||
return client.setAccountData('m.widgets', userWidgets);
|
return client.setAccountData('m.widgets', userWidgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
static makeAppConfig(appId: string, app: IApp, senderUserId: string, roomId: string | null, eventId: string): IApp {
|
static makeAppConfig(appId: string, app: Partial<IApp>, senderUserId: string, roomId: string | null, eventId: string): IApp {
|
||||||
if (!senderUserId) {
|
if (!senderUserId) {
|
||||||
throw new Error("Widgets must be created by someone - provide a senderUserId");
|
throw new Error("Widgets must be created by someone - provide a senderUserId");
|
||||||
}
|
}
|
||||||
|
@ -421,7 +421,7 @@ export default class WidgetUtils {
|
||||||
app.eventId = eventId;
|
app.eventId = eventId;
|
||||||
app.name = app.name || app.type;
|
app.name = app.name || app.type;
|
||||||
|
|
||||||
return app;
|
return app as IApp;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getCapWhitelistForAppTypeInRoomId(appType: string, roomId: string): Capability[] {
|
static getCapWhitelistForAppTypeInRoomId(appType: string, roomId: string): Capability[] {
|
||||||
|
|
Loading…
Reference in a new issue