2020-04-30 22:21:50 +03:00
|
|
|
/*
|
|
|
|
Copyright 2015, 2016 OpenMarket Ltd
|
|
|
|
Copyright 2017, 2018 Vector Creations Ltd
|
|
|
|
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-05-08 21:53:05 +03:00
|
|
|
import { createRef } from "react";
|
2020-04-30 22:21:50 +03:00
|
|
|
import { Room } from "matrix-js-sdk/src/models/room";
|
2020-05-08 21:53:05 +03:00
|
|
|
import classNames from 'classnames';
|
|
|
|
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
|
|
|
import { _t } from "../../../languageHandler";
|
|
|
|
import AccessibleButton from "../../views/elements/AccessibleButton";
|
|
|
|
import RoomTile2 from "./RoomTile2";
|
2020-06-04 06:16:53 +03:00
|
|
|
import { ResizableBox, ResizeCallbackData } from "react-resizable";
|
|
|
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
2020-06-08 22:42:18 +03:00
|
|
|
import NotificationBadge, { ListNotificationState } from "./NotificationBadge";
|
2020-06-10 06:12:49 +03:00
|
|
|
import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
|
|
|
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
2020-04-30 22:21:50 +03:00
|
|
|
|
2020-05-14 21:53:00 +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. *
|
2020-05-21 20:53:16 +03:00
|
|
|
*******************************************************************/
|
2020-05-14 21:53:00 +03:00
|
|
|
|
2020-04-30 22:21:50 +03:00
|
|
|
interface IProps {
|
|
|
|
forRooms: boolean;
|
|
|
|
rooms?: Room[];
|
|
|
|
startAsHidden: boolean;
|
|
|
|
label: string;
|
|
|
|
onAddRoom?: () => void;
|
|
|
|
addRoomLabel: string;
|
|
|
|
isInvite: boolean;
|
2020-06-04 06:16:53 +03:00
|
|
|
layout: ListLayout;
|
2020-04-30 22:21:50 +03:00
|
|
|
|
|
|
|
// TODO: Collapsed state
|
|
|
|
// TODO: Group invites
|
|
|
|
// TODO: Calls
|
|
|
|
// TODO: forceExpand?
|
|
|
|
// TODO: Header clicking
|
|
|
|
// TODO: Spinner support for historical
|
|
|
|
}
|
|
|
|
|
|
|
|
interface IState {
|
2020-06-08 22:42:18 +03:00
|
|
|
notificationState: ListNotificationState;
|
2020-06-10 06:12:49 +03:00
|
|
|
menuDisplayed: boolean;
|
2020-04-30 22:21:50 +03:00
|
|
|
}
|
|
|
|
|
2020-05-04 18:13:35 +03:00
|
|
|
export default class RoomSublist2 extends React.Component<IProps, IState> {
|
2020-05-08 21:53:05 +03:00
|
|
|
private headerButton = createRef();
|
2020-06-10 06:12:49 +03:00
|
|
|
private menuButtonRef: React.RefObject<HTMLButtonElement> = createRef();
|
2020-05-08 21:53:05 +03:00
|
|
|
|
2020-06-08 22:42:18 +03:00
|
|
|
constructor(props: IProps) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
notificationState: new ListNotificationState(this.props.isInvite),
|
2020-06-10 06:12:49 +03:00
|
|
|
menuDisplayed: false,
|
2020-06-08 22:42:18 +03:00
|
|
|
};
|
|
|
|
this.state.notificationState.setRooms(this.props.rooms);
|
2020-05-08 21:53:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
private get numTiles(): number {
|
|
|
|
// TODO: Account for group invites
|
|
|
|
return (this.props.rooms || []).length;
|
|
|
|
}
|
|
|
|
|
2020-06-08 22:42:18 +03:00
|
|
|
public componentDidUpdate() {
|
|
|
|
this.state.notificationState.setRooms(this.props.rooms);
|
|
|
|
}
|
|
|
|
|
2020-05-08 21:53:05 +03:00
|
|
|
private onAddRoom = (e) => {
|
|
|
|
e.stopPropagation();
|
|
|
|
if (this.props.onAddRoom) this.props.onAddRoom();
|
|
|
|
};
|
|
|
|
|
2020-06-04 06:52:05 +03:00
|
|
|
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
|
2020-06-04 18:19:03 +03:00
|
|
|
const direction = e.movementY < 0 ? -1 : +1;
|
|
|
|
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
|
2020-06-04 06:52:05 +03:00
|
|
|
this.props.layout.visibleTiles += tileDiff;
|
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
|
|
|
};
|
|
|
|
|
|
|
|
private onShowAllClick = () => {
|
|
|
|
this.props.layout.visibleTiles = this.numTiles;
|
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
|
|
|
};
|
|
|
|
|
2020-06-10 06:12:49 +03:00
|
|
|
private onOpenMenuClick = (ev: InputEvent) => {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
this.setState({menuDisplayed: true});
|
|
|
|
};
|
|
|
|
|
|
|
|
private onCloseMenu = () => {
|
|
|
|
this.setState({menuDisplayed: false});
|
|
|
|
};
|
|
|
|
|
|
|
|
private onUnreadFirstChanged = () => {
|
|
|
|
// TODO: Support per-list algorithm changes
|
|
|
|
console.log("Unread first changed");
|
|
|
|
};
|
|
|
|
|
|
|
|
private onMessagePreviewChanged = () => {
|
|
|
|
this.props.layout.showPreviews = !this.props.layout.showPreviews;
|
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
|
|
|
};
|
|
|
|
|
2020-05-08 21:53:05 +03:00
|
|
|
private renderTiles(): React.ReactElement[] {
|
|
|
|
const tiles: React.ReactElement[] = [];
|
|
|
|
|
|
|
|
if (this.props.rooms) {
|
|
|
|
for (const room of this.props.rooms) {
|
2020-06-05 06:21:04 +03:00
|
|
|
tiles.push(
|
|
|
|
<RoomTile2
|
|
|
|
room={room}
|
|
|
|
key={`room-${room.roomId}`}
|
2020-06-10 06:12:49 +03:00
|
|
|
showMessagePreview={this.props.layout.showPreviews}
|
2020-06-05 06:21:04 +03:00
|
|
|
/>
|
|
|
|
);
|
2020-05-08 21:53:05 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return tiles;
|
2020-04-30 22:21:50 +03:00
|
|
|
}
|
|
|
|
|
2020-06-10 06:12:49 +03:00
|
|
|
private renderMenu(): React.ReactElement {
|
|
|
|
let contextMenu = null;
|
|
|
|
if (this.state.menuDisplayed) {
|
|
|
|
const elementRect = this.menuButtonRef.current.getBoundingClientRect();
|
|
|
|
contextMenu = (
|
|
|
|
<ContextMenu
|
|
|
|
chevronFace="none"
|
|
|
|
left={elementRect.left}
|
|
|
|
top={elementRect.top + elementRect.height}
|
|
|
|
onFinished={this.onCloseMenu}
|
|
|
|
>
|
|
|
|
<div className="mx_RoomSublist2_contextMenu">
|
|
|
|
<div>
|
|
|
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
|
|
|
|
TODO: Radios are blocked by https://github.com/matrix-org/matrix-react-sdk/pull/4731
|
|
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<div>
|
|
|
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
|
|
|
<StyledCheckbox
|
|
|
|
onChange={this.onUnreadFirstChanged}
|
|
|
|
checked={false/*TODO*/}
|
|
|
|
>
|
|
|
|
{_t("Always show first")}
|
|
|
|
</StyledCheckbox>
|
|
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<div>
|
|
|
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
|
|
|
<StyledCheckbox
|
|
|
|
onChange={this.onMessagePreviewChanged}
|
|
|
|
checked={this.props.layout.showPreviews}
|
|
|
|
>
|
|
|
|
{_t("Message preview")}
|
|
|
|
</StyledCheckbox>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</ContextMenu>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<React.Fragment>
|
|
|
|
<ContextMenuButton
|
|
|
|
className="mx_RoomSublist2_menuButton"
|
|
|
|
onClick={this.onOpenMenuClick}
|
|
|
|
inputRef={this.menuButtonRef}
|
|
|
|
label={_t("List options")}
|
|
|
|
isExpanded={this.state.menuDisplayed}
|
|
|
|
/>
|
|
|
|
{contextMenu}
|
|
|
|
</React.Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-08 21:53:05 +03:00
|
|
|
private renderHeader(): React.ReactElement {
|
|
|
|
// TODO: Title on collapsed
|
|
|
|
// TODO: Incoming call box
|
|
|
|
|
|
|
|
return (
|
|
|
|
<RovingTabIndexWrapper inputRef={this.headerButton}>
|
|
|
|
{({onFocus, isActive, ref}) => {
|
2020-05-14 21:53:00 +03:00
|
|
|
// TODO: Use onFocus
|
2020-05-08 21:53:05 +03:00
|
|
|
const tabIndex = isActive ? 0 : -1;
|
|
|
|
|
2020-05-14 21:53:00 +03:00
|
|
|
// TODO: Collapsed state
|
2020-06-08 22:42:18 +03:00
|
|
|
|
|
|
|
const badge = <NotificationBadge allowNoCount={false} notification={this.state.notificationState}/>;
|
2020-06-05 06:21:04 +03:00
|
|
|
|
2020-06-10 06:12:49 +03:00
|
|
|
let addRoomButton = null;
|
|
|
|
if (!!this.props.onAddRoom) {
|
|
|
|
addRoomButton = (
|
|
|
|
<AccessibleButton
|
|
|
|
tabIndex={tabIndex}
|
|
|
|
onClick={this.onAddRoom}
|
|
|
|
className="mx_RoomSublist2_auxButton"
|
|
|
|
aria-label={this.props.addRoomLabel || _t("Add room")}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const classes = classNames({
|
|
|
|
'mx_RoomSublist2_headerContainer': true,
|
|
|
|
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
|
|
|
|
});
|
2020-05-08 21:53:05 +03:00
|
|
|
|
2020-05-14 21:53:00 +03:00
|
|
|
// TODO: a11y (see old component)
|
2020-05-08 21:53:05 +03:00
|
|
|
return (
|
2020-06-10 06:12:49 +03:00
|
|
|
<div className={classes}>
|
2020-05-08 21:53:05 +03:00
|
|
|
<AccessibleButton
|
|
|
|
inputRef={ref}
|
|
|
|
tabIndex={tabIndex}
|
2020-06-05 01:34:04 +03:00
|
|
|
className={"mx_RoomSublist2_headerText"}
|
2020-05-08 21:53:05 +03:00
|
|
|
role="treeitem"
|
2020-06-08 18:57:39 +03:00
|
|
|
aria-level={1}
|
2020-05-08 21:53:05 +03:00
|
|
|
>
|
|
|
|
<span>{this.props.label}</span>
|
|
|
|
</AccessibleButton>
|
2020-06-08 22:42:18 +03:00
|
|
|
<div className="mx_RoomSublist2_badgeContainer">
|
|
|
|
{badge}
|
|
|
|
</div>
|
2020-06-10 06:12:49 +03:00
|
|
|
{this.renderMenu()}
|
|
|
|
{addRoomButton}
|
2020-05-08 21:53:05 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}}
|
|
|
|
</RovingTabIndexWrapper>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public render(): React.ReactElement {
|
2020-04-30 22:21:50 +03:00
|
|
|
// TODO: Proper rendering
|
2020-05-08 21:53:05 +03:00
|
|
|
// TODO: Error boundary
|
|
|
|
|
|
|
|
const tiles = this.renderTiles();
|
|
|
|
|
|
|
|
const classes = classNames({
|
|
|
|
// TODO: Proper collapse support
|
2020-06-05 01:34:04 +03:00
|
|
|
'mx_RoomSublist2': true,
|
|
|
|
'mx_RoomSublist2_collapsed': false, // len && isCollapsed
|
2020-06-10 06:12:49 +03:00
|
|
|
'mx_RoomSublist2_hasMenuOpen': this.state.menuDisplayed,
|
2020-05-08 21:53:05 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
let content = null;
|
|
|
|
if (tiles.length > 0) {
|
2020-06-10 04:48:31 +03:00
|
|
|
const layout = this.props.layout; // to shorten calls
|
|
|
|
|
2020-05-08 21:53:05 +03:00
|
|
|
// TODO: Lazy list rendering
|
|
|
|
// TODO: Whatever scrolling magic needs to happen here
|
2020-06-04 18:57:16 +03:00
|
|
|
|
2020-06-10 04:48:31 +03:00
|
|
|
const nVisible = Math.floor(layout.visibleTiles);
|
2020-06-04 18:57:16 +03:00
|
|
|
const visibleTiles = tiles.slice(0, nVisible);
|
2020-06-04 06:52:05 +03:00
|
|
|
|
|
|
|
// If we're hiding rooms, show a 'show more' button to the user. This button
|
2020-06-10 04:48:31 +03:00
|
|
|
// floats above the resize handle, if we have one present
|
|
|
|
let showMoreButton = null;
|
2020-06-04 18:57:16 +03:00
|
|
|
if (tiles.length > nVisible) {
|
2020-06-04 06:52:05 +03:00
|
|
|
// we have a cutoff condition - add the button to show all
|
2020-06-10 04:48:31 +03:00
|
|
|
const numMissing = tiles.length - visibleTiles.length;
|
|
|
|
showMoreButton = (
|
|
|
|
<div onClick={this.onShowAllClick} className='mx_RoomSublist2_showMoreButton'>
|
|
|
|
<span className='mx_RoomSublist2_showMoreButtonChevron'>
|
|
|
|
{/* set by CSS masking */}
|
|
|
|
</span>
|
|
|
|
<span className='mx_RoomSublist2_showMoreButtonText'>
|
|
|
|
{_t("Show %(count)s more", {count: numMissing})}
|
|
|
|
</span>
|
2020-06-04 06:52:05 +03:00
|
|
|
</div>
|
2020-06-10 04:48:31 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Figure out if we need a handle
|
|
|
|
let handles = ['s'];
|
|
|
|
if (layout.visibleTiles >= tiles.length && tiles.length <= layout.minVisibleTiles) {
|
|
|
|
handles = []; // no handles, we're at a minimum
|
2020-06-04 06:52:05 +03:00
|
|
|
}
|
2020-06-10 04:48:31 +03:00
|
|
|
|
|
|
|
// We have to account for padding so we can accommodate a 'show more' button and
|
|
|
|
// the resize handle, which are pinned to the bottom of the container. This is the
|
|
|
|
// easiest way to have a resize handle below the button as otherwise we're writing
|
|
|
|
// our own resize handling and that doesn't sound fun.
|
|
|
|
//
|
|
|
|
// The layout class has some helpers for dealing with padding, as we don't want to
|
|
|
|
// apply it in all cases. If we apply it in all cases, the resizing feels like it
|
|
|
|
// goes backwards and can become wildly incorrect (visibleTiles says 18 when there's
|
|
|
|
// only mathematically 7 possible).
|
|
|
|
|
|
|
|
const showMoreHeight = 32; // As defined by CSS
|
|
|
|
const resizeHandleHeight = 4; // As defined by CSS
|
|
|
|
|
|
|
|
// The padding is variable though, so figure out what we need padding for.
|
|
|
|
let padding = 0;
|
|
|
|
if (showMoreButton) padding += showMoreHeight;
|
|
|
|
if (handles.length > 0) padding += resizeHandleHeight;
|
|
|
|
|
|
|
|
const minTilesPx = layout.calculateTilesToPixelsMin(tiles.length, layout.minVisibleTiles, padding);
|
|
|
|
const maxTilesPx = layout.tilesToPixelsWithPadding(tiles.length, padding);
|
|
|
|
const tilesWithoutPadding = Math.min(tiles.length, layout.visibleTiles);
|
|
|
|
const tilesPx = layout.calculateTilesToPixelsMin(tiles.length, tilesWithoutPadding, padding);
|
|
|
|
|
2020-05-08 21:53:05 +03:00
|
|
|
content = (
|
2020-06-04 06:16:53 +03:00
|
|
|
<ResizableBox
|
|
|
|
width={-1}
|
|
|
|
height={tilesPx}
|
|
|
|
axis="y"
|
|
|
|
minConstraints={[-1, minTilesPx]}
|
|
|
|
maxConstraints={[-1, maxTilesPx]}
|
|
|
|
resizeHandles={handles}
|
|
|
|
onResize={this.onResize}
|
|
|
|
className="mx_RoomSublist2_resizeBox"
|
|
|
|
>
|
|
|
|
{visibleTiles}
|
2020-06-10 04:48:31 +03:00
|
|
|
{showMoreButton}
|
2020-06-04 06:16:53 +03:00
|
|
|
</ResizableBox>
|
2020-05-08 21:53:05 +03:00
|
|
|
)
|
|
|
|
}
|
2020-04-30 22:21:50 +03:00
|
|
|
|
2020-05-08 21:53:05 +03:00
|
|
|
// TODO: onKeyDown support
|
2020-04-30 22:21:50 +03:00
|
|
|
return (
|
2020-05-08 21:53:05 +03:00
|
|
|
<div
|
|
|
|
className={classes}
|
|
|
|
role="group"
|
|
|
|
aria-label={this.props.label}
|
|
|
|
>
|
|
|
|
{this.renderHeader()}
|
|
|
|
{content}
|
2020-04-30 22:21:50 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|