2020-06-03 04:26:07 +03:00
|
|
|
/*
|
|
|
|
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 * as React from "react";
|
2020-06-24 05:59:26 +03:00
|
|
|
import { createRef } from "react";
|
2020-06-03 04:26:07 +03:00
|
|
|
import TagPanel from "./TagPanel";
|
|
|
|
import classNames from "classnames";
|
|
|
|
import dis from "../../dispatcher/dispatcher";
|
|
|
|
import { _t } from "../../languageHandler";
|
|
|
|
import RoomList2 from "../views/rooms/RoomList2";
|
|
|
|
import { Action } from "../../dispatcher/actions";
|
2020-06-05 01:34:04 +03:00
|
|
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
|
|
|
import BaseAvatar from '../views/avatars/BaseAvatar';
|
2020-06-08 07:06:41 +03:00
|
|
|
import UserMenuButton from "./UserMenuButton";
|
2020-06-09 05:33:21 +03:00
|
|
|
import RoomSearch from "./RoomSearch";
|
|
|
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
2020-06-09 02:11:58 +03:00
|
|
|
import RoomBreadcrumbs2 from "../views/rooms/RoomBreadcrumbs2";
|
|
|
|
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
|
|
|
|
import { UPDATE_EVENT } from "../../stores/AsyncStore";
|
2020-06-22 22:09:42 +03:00
|
|
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
2020-06-24 05:59:26 +03:00
|
|
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
|
|
import { throttle } from 'lodash';
|
|
|
|
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
2020-06-03 04:26:07 +03:00
|
|
|
|
|
|
|
/*******************************************************************
|
|
|
|
* CAUTION *
|
|
|
|
*******************************************************************
|
|
|
|
* This is a work in progress implementation and isn't complete or *
|
|
|
|
* even useful as a component. Please avoid using it until this *
|
|
|
|
* warning disappears. *
|
|
|
|
*******************************************************************/
|
|
|
|
|
|
|
|
interface IProps {
|
2020-06-11 23:39:28 +03:00
|
|
|
isMinimized: boolean;
|
2020-06-22 22:09:42 +03:00
|
|
|
resizeNotifier: ResizeNotifier;
|
2020-06-03 04:26:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
interface IState {
|
|
|
|
searchFilter: string; // TODO: Move search into room list?
|
2020-06-09 02:11:58 +03:00
|
|
|
showBreadcrumbs: boolean;
|
2020-06-03 04:26:07 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
export default class LeftPanel2 extends React.Component<IProps, IState> {
|
2020-06-22 22:09:42 +03:00
|
|
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
|
|
|
|
2020-06-03 04:26:07 +03:00
|
|
|
// TODO: Properly support TagPanel
|
|
|
|
// TODO: Properly support searching/filtering
|
|
|
|
// TODO: Properly support breadcrumbs
|
|
|
|
// TODO: a11y
|
|
|
|
// TODO: actually make this useful in general (match design proposals)
|
|
|
|
// TODO: Fadable support (is this still needed?)
|
|
|
|
|
|
|
|
constructor(props: IProps) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
searchFilter: "",
|
2020-06-09 02:11:58 +03:00
|
|
|
showBreadcrumbs: BreadcrumbsStore.instance.visible,
|
2020-06-03 04:26:07 +03:00
|
|
|
};
|
2020-06-09 02:11:58 +03:00
|
|
|
|
|
|
|
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
2020-06-22 22:09:42 +03:00
|
|
|
|
|
|
|
// We watch the middle panel because we don't actually get resized, the middle panel does.
|
|
|
|
// We listen to the noisy channel to avoid choppy reaction times.
|
|
|
|
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
|
2020-06-24 05:59:26 +03:00
|
|
|
|
|
|
|
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
|
2020-06-09 02:11:58 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
public componentWillUnmount() {
|
|
|
|
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
2020-06-22 22:09:42 +03:00
|
|
|
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
2020-06-24 05:59:26 +03:00
|
|
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
|
2020-06-03 04:26:07 +03:00
|
|
|
}
|
|
|
|
|
2020-06-24 05:59:26 +03:00
|
|
|
// TSLint wants this to be a member, but we don't want that.
|
|
|
|
// tslint:disable-next-line
|
|
|
|
private onRoomStateUpdate = throttle((ev: MatrixEvent) => {
|
|
|
|
const myUserId = MatrixClientPeg.get().getUserId();
|
|
|
|
if (ev.getType() === 'm.room.member' && ev.getSender() === myUserId && ev.getStateKey() === myUserId) {
|
|
|
|
// noinspection JSIgnoredPromiseFromCall
|
|
|
|
this.onProfileUpdate();
|
|
|
|
}
|
|
|
|
}, 200, {trailing: true, leading: true});
|
|
|
|
|
|
|
|
private onProfileUpdate = async () => {
|
|
|
|
// the store triggered an update, so force a layout update. We don't
|
|
|
|
// have any state to store here for that to magically happen.
|
|
|
|
this.forceUpdate();
|
|
|
|
};
|
|
|
|
|
2020-06-03 04:26:07 +03:00
|
|
|
private onSearch = (term: string): void => {
|
|
|
|
this.setState({searchFilter: term});
|
|
|
|
};
|
|
|
|
|
2020-06-09 05:33:21 +03:00
|
|
|
private onExplore = () => {
|
|
|
|
dis.fire(Action.ViewRoomDirectory);
|
2020-06-03 04:26:07 +03:00
|
|
|
};
|
|
|
|
|
2020-06-09 02:11:58 +03:00
|
|
|
private onBreadcrumbsUpdate = () => {
|
|
|
|
const newVal = BreadcrumbsStore.instance.visible;
|
|
|
|
if (newVal !== this.state.showBreadcrumbs) {
|
|
|
|
this.setState({showBreadcrumbs: newVal});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-06-22 22:09:42 +03:00
|
|
|
private handleStickyHeaders(list: HTMLDivElement) {
|
2020-06-13 20:54:40 +03:00
|
|
|
const rlRect = list.getBoundingClientRect();
|
|
|
|
const bottom = rlRect.bottom;
|
|
|
|
const top = rlRect.top;
|
|
|
|
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist2");
|
|
|
|
const headerHeight = 32; // Note: must match the CSS!
|
|
|
|
const headerRightMargin = 24; // calculated from margins and widths to align with non-sticky tiles
|
|
|
|
|
|
|
|
const headerStickyWidth = rlRect.width - headerRightMargin;
|
|
|
|
|
|
|
|
let gotBottom = false;
|
|
|
|
for (const sublist of sublists) {
|
|
|
|
const slRect = sublist.getBoundingClientRect();
|
|
|
|
|
2020-06-14 04:07:19 +03:00
|
|
|
const header = sublist.querySelector<HTMLDivElement>(".mx_RoomSublist2_stickable");
|
2020-06-13 20:54:40 +03:00
|
|
|
|
|
|
|
if (slRect.top + headerHeight > bottom && !gotBottom) {
|
|
|
|
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
|
|
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyBottom");
|
|
|
|
header.style.width = `${headerStickyWidth}px`;
|
2020-06-18 16:48:36 +03:00
|
|
|
header.style.top = `unset`;
|
2020-06-13 20:54:40 +03:00
|
|
|
gotBottom = true;
|
|
|
|
} else if (slRect.top < top) {
|
|
|
|
header.classList.add("mx_RoomSublist2_headerContainer_sticky");
|
|
|
|
header.classList.add("mx_RoomSublist2_headerContainer_stickyTop");
|
|
|
|
header.style.width = `${headerStickyWidth}px`;
|
|
|
|
header.style.top = `${rlRect.top}px`;
|
|
|
|
} else {
|
|
|
|
header.classList.remove("mx_RoomSublist2_headerContainer_sticky");
|
|
|
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyTop");
|
|
|
|
header.classList.remove("mx_RoomSublist2_headerContainer_stickyBottom");
|
|
|
|
header.style.width = `unset`;
|
2020-06-18 16:48:36 +03:00
|
|
|
header.style.top = `unset`;
|
2020-06-13 20:54:40 +03:00
|
|
|
}
|
|
|
|
}
|
2020-06-22 22:09:42 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Apply this on resize, init, etc for reliability
|
|
|
|
private onScroll = (ev: React.MouseEvent<HTMLDivElement>) => {
|
|
|
|
const list = ev.target as HTMLDivElement;
|
|
|
|
this.handleStickyHeaders(list);
|
|
|
|
};
|
|
|
|
|
|
|
|
private onResize = () => {
|
|
|
|
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
|
|
|
|
this.handleStickyHeaders(this.listContainerRef.current);
|
2020-06-13 20:54:40 +03:00
|
|
|
};
|
|
|
|
|
2020-06-05 01:34:04 +03:00
|
|
|
private renderHeader(): React.ReactNode {
|
2020-06-05 23:08:20 +03:00
|
|
|
// TODO: Update when profile info changes
|
2020-06-05 01:34:04 +03:00
|
|
|
// TODO: Presence
|
|
|
|
// TODO: Breadcrumbs toggle
|
|
|
|
// TODO: Menu button
|
2020-06-24 05:59:26 +03:00
|
|
|
const avatarSize = 32; // should match border-radius of the avatar
|
2020-06-09 02:11:58 +03:00
|
|
|
|
|
|
|
let breadcrumbs;
|
|
|
|
if (this.state.showBreadcrumbs) {
|
|
|
|
breadcrumbs = (
|
|
|
|
<div className="mx_LeftPanel2_headerRow mx_LeftPanel2_breadcrumbsContainer">
|
2020-06-11 23:39:28 +03:00
|
|
|
{this.props.isMinimized ? null : <RoomBreadcrumbs2 />}
|
2020-06-09 02:11:58 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-24 05:59:26 +03:00
|
|
|
let name = <span className="mx_LeftPanel2_userName">{OwnProfileStore.instance.displayName}</span>;
|
2020-06-11 23:39:28 +03:00
|
|
|
let buttons = (
|
|
|
|
<span className="mx_LeftPanel2_headerButtons">
|
|
|
|
<UserMenuButton />
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
if (this.props.isMinimized) {
|
|
|
|
name = null;
|
|
|
|
buttons = null;
|
|
|
|
}
|
|
|
|
|
2020-06-05 01:34:04 +03:00
|
|
|
return (
|
|
|
|
<div className="mx_LeftPanel2_userHeader">
|
|
|
|
<div className="mx_LeftPanel2_headerRow">
|
|
|
|
<span className="mx_LeftPanel2_userAvatarContainer">
|
|
|
|
<BaseAvatar
|
|
|
|
idName={MatrixClientPeg.get().getUserId()}
|
2020-06-24 05:59:26 +03:00
|
|
|
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
|
|
|
|
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
|
2020-06-05 01:34:04 +03:00
|
|
|
width={avatarSize}
|
|
|
|
height={avatarSize}
|
|
|
|
resizeMethod="crop"
|
|
|
|
className="mx_LeftPanel2_userAvatar"
|
|
|
|
/>
|
|
|
|
</span>
|
2020-06-11 23:39:28 +03:00
|
|
|
{name}
|
|
|
|
{buttons}
|
2020-06-05 01:34:04 +03:00
|
|
|
</div>
|
2020-06-09 02:11:58 +03:00
|
|
|
{breadcrumbs}
|
2020-06-03 04:26:07 +03:00
|
|
|
</div>
|
|
|
|
);
|
2020-06-05 01:34:04 +03:00
|
|
|
}
|
2020-06-03 04:26:07 +03:00
|
|
|
|
2020-06-09 05:33:21 +03:00
|
|
|
private renderSearchExplore(): React.ReactNode {
|
|
|
|
// TODO: Collapsed support
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="mx_LeftPanel2_filterContainer">
|
2020-06-11 23:39:28 +03:00
|
|
|
<RoomSearch onQueryUpdate={this.onSearch} isMinimized={this.props.isMinimized} />
|
2020-06-09 05:33:21 +03:00
|
|
|
<AccessibleButton
|
|
|
|
tabIndex={-1}
|
|
|
|
className='mx_LeftPanel2_exploreButton'
|
|
|
|
onClick={this.onExplore}
|
|
|
|
alt={_t("Explore rooms")}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-05 01:34:04 +03:00
|
|
|
public render(): React.ReactNode {
|
|
|
|
const tagPanel = (
|
|
|
|
<div className="mx_LeftPanel2_tagPanelContainer">
|
|
|
|
<TagPanel/>
|
2020-06-03 04:26:07 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
// TODO: Improve props for RoomList2
|
|
|
|
const roomList = <RoomList2
|
|
|
|
onKeyDown={() => {/*TODO*/}}
|
|
|
|
resizeNotifier={null}
|
|
|
|
collapsed={false}
|
|
|
|
searchFilter={this.state.searchFilter}
|
|
|
|
onFocus={() => {/*TODO*/}}
|
|
|
|
onBlur={() => {/*TODO*/}}
|
2020-06-11 23:39:28 +03:00
|
|
|
isMinimized={this.props.isMinimized}
|
2020-06-03 04:26:07 +03:00
|
|
|
/>;
|
|
|
|
|
|
|
|
// TODO: Conference handling / calls
|
|
|
|
|
|
|
|
const containerClasses = classNames({
|
2020-06-05 01:34:04 +03:00
|
|
|
"mx_LeftPanel2": true,
|
2020-06-11 23:39:28 +03:00
|
|
|
"mx_LeftPanel2_minimized": this.props.isMinimized,
|
2020-06-03 04:26:07 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={containerClasses}>
|
|
|
|
{tagPanel}
|
2020-06-05 01:34:04 +03:00
|
|
|
<aside className="mx_LeftPanel2_roomListContainer">
|
|
|
|
{this.renderHeader()}
|
2020-06-09 05:33:21 +03:00
|
|
|
{this.renderSearchExplore()}
|
2020-06-22 22:09:42 +03:00
|
|
|
<div
|
|
|
|
className="mx_LeftPanel2_actualRoomListContainer"
|
|
|
|
onScroll={this.onScroll}
|
|
|
|
ref={this.listContainerRef}
|
|
|
|
>{roomList}</div>
|
2020-06-03 04:26:07 +03:00
|
|
|
</aside>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|