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';
|
2020-07-07 19:47:21 +03:00
|
|
|
import {RovingAccessibleButton, RovingTabIndexWrapper} from "../../../accessibility/RovingTabIndex";
|
2020-05-08 21:53:05 +03:00
|
|
|
import { _t } from "../../../languageHandler";
|
|
|
|
import AccessibleButton from "../../views/elements/AccessibleButton";
|
|
|
|
import RoomTile2 from "./RoomTile2";
|
2020-06-04 06:16:53 +03:00
|
|
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
2020-07-05 20:23:57 +03:00
|
|
|
import {
|
2020-07-07 17:01:27 +03:00
|
|
|
ChevronFace,
|
2020-07-05 20:23:57 +03:00
|
|
|
ContextMenu,
|
|
|
|
ContextMenuButton,
|
|
|
|
StyledMenuItemCheckbox,
|
|
|
|
StyledMenuItemRadio,
|
|
|
|
} from "../../structures/ContextMenu";
|
2020-06-12 07:04:10 +03:00
|
|
|
import RoomListStore from "../../../stores/room-list/RoomListStore2";
|
|
|
|
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
2020-07-01 04:42:02 +03:00
|
|
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
2020-07-02 21:48:06 +03:00
|
|
|
import dis from "../../../dispatcher/dispatcher";
|
2020-06-30 22:34:44 +03:00
|
|
|
import NotificationBadge from "./NotificationBadge";
|
2020-07-02 22:26:16 +03:00
|
|
|
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
2020-07-01 03:50:31 +03:00
|
|
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
2020-07-03 00:21:10 +03:00
|
|
|
import { Key } from "../../../Keyboard";
|
2020-07-07 12:34:42 +03:00
|
|
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|
|
|
import {ActionPayload} from "../../../dispatcher/payloads";
|
2020-07-08 04:36:26 +03:00
|
|
|
import { Enable, Resizable } from "re-resizable";
|
|
|
|
import { Direction } from "re-resizable/lib/resizer";
|
|
|
|
import { polyfillTouchEvent } from "../../../@types/polyfill";
|
2020-07-09 04:26:25 +03:00
|
|
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
2020-07-09 03:27:50 +03:00
|
|
|
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
2020-04-30 22:21:50 +03:00
|
|
|
|
2020-06-29 05:03:04 +03:00
|
|
|
// TODO: Remove banner on launch: https://github.com/vector-im/riot-web/issues/14231
|
|
|
|
// TODO: Rename on launch: https://github.com/vector-im/riot-web/issues/14231
|
|
|
|
|
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-07-10 19:30:52 +03:00
|
|
|
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
2020-06-26 01:03:56 +03:00
|
|
|
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
2020-07-08 16:11:47 +03:00
|
|
|
export const HEADER_HEIGHT = 32; // As defined by CSS
|
2020-06-18 07:09:59 +03:00
|
|
|
|
|
|
|
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
|
|
|
|
|
2020-07-08 04:36:26 +03:00
|
|
|
// HACK: We really shouldn't have to do this.
|
|
|
|
polyfillTouchEvent();
|
|
|
|
|
2020-04-30 22:21:50 +03:00
|
|
|
interface IProps {
|
|
|
|
forRooms: boolean;
|
|
|
|
rooms?: Room[];
|
|
|
|
startAsHidden: boolean;
|
|
|
|
label: string;
|
|
|
|
onAddRoom?: () => void;
|
|
|
|
addRoomLabel: string;
|
2020-06-11 23:39:28 +03:00
|
|
|
isMinimized: boolean;
|
2020-06-22 23:52:17 +03:00
|
|
|
tagId: TagID;
|
2020-07-07 00:04:30 +03:00
|
|
|
onResize: () => void;
|
2020-04-30 22:21:50 +03:00
|
|
|
|
2020-07-02 18:04:38 +03:00
|
|
|
// TODO: Don't use this. It's for community invites, and community invites shouldn't be here.
|
|
|
|
// You should feel bad if you use this.
|
|
|
|
extraBadTilesThatShouldntExist?: React.ReactElement[];
|
|
|
|
|
2020-06-29 05:03:04 +03:00
|
|
|
// TODO: Account for https://github.com/vector-im/riot-web/issues/14179
|
2020-04-30 22:21:50 +03:00
|
|
|
}
|
|
|
|
|
2020-07-10 19:31:53 +03:00
|
|
|
// TODO: Use re-resizer's NumberSize when it is exposed as the type
|
|
|
|
interface ResizeDelta {
|
|
|
|
width: number,
|
|
|
|
height: number,
|
|
|
|
}
|
|
|
|
|
2020-07-02 01:06:26 +03:00
|
|
|
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
|
|
|
|
|
2020-04-30 22:21:50 +03:00
|
|
|
interface IState {
|
2020-06-08 22:42:18 +03:00
|
|
|
notificationState: ListNotificationState;
|
2020-07-02 01:06:26 +03:00
|
|
|
contextMenuPosition: PartialDOMRect;
|
2020-06-26 01:03:56 +03:00
|
|
|
isResizing: 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-07-03 00:21:10 +03:00
|
|
|
private headerButton = createRef<HTMLDivElement>();
|
|
|
|
private sublistRef = createRef<HTMLDivElement>();
|
2020-07-07 12:34:42 +03:00
|
|
|
private dispatcherRef: string;
|
2020-07-09 03:27:50 +03:00
|
|
|
private layout: ListLayout;
|
2020-07-03 00:21:10 +03:00
|
|
|
|
2020-06-08 22:42:18 +03:00
|
|
|
constructor(props: IProps) {
|
|
|
|
super(props);
|
|
|
|
|
2020-07-09 03:27:50 +03:00
|
|
|
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
|
|
|
|
2020-06-08 22:42:18 +03:00
|
|
|
this.state = {
|
2020-07-09 04:26:25 +03:00
|
|
|
notificationState: RoomNotificationStateStore.instance.getListState(this.props.tagId),
|
2020-07-02 01:06:26 +03:00
|
|
|
contextMenuPosition: null,
|
2020-06-26 01:03:56 +03:00
|
|
|
isResizing: false,
|
2020-06-08 22:42:18 +03:00
|
|
|
};
|
|
|
|
this.state.notificationState.setRooms(this.props.rooms);
|
2020-07-07 12:34:42 +03:00
|
|
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
2020-05-08 21:53:05 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
private get numTiles(): number {
|
2020-07-02 18:04:38 +03:00
|
|
|
return (this.props.rooms || []).length + (this.props.extraBadTilesThatShouldntExist || []).length;
|
2020-05-08 21:53:05 +03:00
|
|
|
}
|
|
|
|
|
2020-07-01 04:14:36 +03:00
|
|
|
private get numVisibleTiles(): number {
|
2020-07-09 22:07:13 +03:00
|
|
|
const nVisible = Math.ceil(this.layout.visibleTiles);
|
2020-07-01 04:14:36 +03:00
|
|
|
return Math.min(nVisible, this.numTiles);
|
|
|
|
}
|
|
|
|
|
2020-06-08 22:42:18 +03:00
|
|
|
public componentDidUpdate() {
|
|
|
|
this.state.notificationState.setRooms(this.props.rooms);
|
|
|
|
}
|
|
|
|
|
2020-06-11 01:04:27 +03:00
|
|
|
public componentWillUnmount() {
|
|
|
|
this.state.notificationState.destroy();
|
2020-07-07 12:34:42 +03:00
|
|
|
defaultDispatcher.unregister(this.dispatcherRef);
|
2020-06-11 01:04:27 +03:00
|
|
|
}
|
|
|
|
|
2020-07-07 12:34:42 +03:00
|
|
|
private onAction = (payload: ActionPayload) => {
|
|
|
|
if (payload.action === "view_room" && payload.show_room_tile && this.props.rooms) {
|
|
|
|
// XXX: we have to do this a tick later because we have incorrect intermediate props during a room change
|
|
|
|
// where we lose the room we are changing from temporarily and then it comes back in an update right after.
|
|
|
|
setImmediate(() => {
|
2020-07-09 03:27:50 +03:00
|
|
|
const isCollapsed = this.layout.isCollapsed;
|
2020-07-07 12:34:42 +03:00
|
|
|
const roomIndex = this.props.rooms.findIndex((r) => r.roomId === payload.room_id);
|
|
|
|
|
|
|
|
if (isCollapsed && roomIndex > -1) {
|
|
|
|
this.toggleCollapsed();
|
|
|
|
}
|
2020-07-07 12:35:16 +03:00
|
|
|
// extend the visible section to include the room if it is entirely invisible
|
2020-07-07 12:34:42 +03:00
|
|
|
if (roomIndex >= this.numVisibleTiles) {
|
2020-07-09 03:27:50 +03:00
|
|
|
this.layout.visibleTiles = this.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT);
|
2020-07-07 12:34:42 +03:00
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-05-08 21:53:05 +03:00
|
|
|
private onAddRoom = (e) => {
|
|
|
|
e.stopPropagation();
|
|
|
|
if (this.props.onAddRoom) this.props.onAddRoom();
|
|
|
|
};
|
|
|
|
|
2020-07-08 04:36:26 +03:00
|
|
|
private onResize = (
|
|
|
|
e: MouseEvent | TouchEvent,
|
|
|
|
travelDirection: Direction,
|
|
|
|
refToElement: HTMLDivElement,
|
2020-07-10 19:31:53 +03:00
|
|
|
delta: ResizeDelta,
|
2020-07-08 04:36:26 +03:00
|
|
|
) => {
|
|
|
|
// Do some sanity checks, but in reality we shouldn't need these.
|
|
|
|
if (travelDirection !== "bottom") return;
|
|
|
|
if (delta.height === 0) return; // something went wrong, so just ignore it.
|
|
|
|
|
|
|
|
// NOTE: the movement in the MouseEvent (not present on a TouchEvent) is inaccurate
|
|
|
|
// for our purposes. The delta provided by the library is also a change *from when
|
|
|
|
// resizing started*, meaning it is fairly useless for us. This is why we just use
|
|
|
|
// the client height and run with it.
|
|
|
|
|
2020-07-09 03:27:50 +03:00
|
|
|
const heightBefore = this.layout.visibleTiles;
|
|
|
|
const heightInTiles = this.layout.pixelsToTiles(refToElement.clientHeight);
|
|
|
|
this.layout.setVisibleTilesWithin(heightInTiles, this.numTiles);
|
|
|
|
if (heightBefore === this.layout.visibleTiles) return; // no-op
|
2020-06-04 06:52:05 +03:00
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
|
|
|
};
|
|
|
|
|
2020-06-26 01:03:56 +03:00
|
|
|
private onResizeStart = () => {
|
|
|
|
this.setState({isResizing: true});
|
|
|
|
};
|
|
|
|
|
|
|
|
private onResizeStop = () => {
|
|
|
|
this.setState({isResizing: false});
|
|
|
|
};
|
|
|
|
|
2020-06-04 06:52:05 +03:00
|
|
|
private onShowAllClick = () => {
|
2020-07-07 19:07:51 +03:00
|
|
|
const numVisibleTiles = this.numVisibleTiles;
|
2020-07-09 03:27:50 +03:00
|
|
|
this.layout.visibleTiles = this.layout.tilesWithPadding(this.numTiles, MAX_PADDING_HEIGHT);
|
2020-06-04 06:52:05 +03:00
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
2020-07-07 19:07:51 +03:00
|
|
|
setImmediate(this.focusRoomTile, numVisibleTiles); // focus the tile after the current bottom one
|
2020-06-04 06:52:05 +03:00
|
|
|
};
|
|
|
|
|
2020-06-16 05:00:09 +03:00
|
|
|
private onShowLessClick = () => {
|
2020-07-09 03:27:50 +03:00
|
|
|
this.layout.visibleTiles = this.layout.defaultVisibleTiles;
|
2020-06-16 05:00:09 +03:00
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
2020-07-07 19:07:51 +03:00
|
|
|
// focus will flow to the show more button here
|
|
|
|
};
|
|
|
|
|
|
|
|
private focusRoomTile = (index: number) => {
|
|
|
|
if (!this.sublistRef.current) return;
|
|
|
|
const elements = this.sublistRef.current.querySelectorAll<HTMLDivElement>(".mx_RoomTile2");
|
|
|
|
const element = elements && elements[index];
|
|
|
|
if (element) {
|
|
|
|
element.focus();
|
|
|
|
}
|
2020-06-16 05:00:09 +03:00
|
|
|
};
|
|
|
|
|
2020-07-02 01:56:57 +03:00
|
|
|
private onOpenMenuClick = (ev: React.MouseEvent) => {
|
2020-06-10 06:12:49 +03:00
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
2020-07-02 01:06:26 +03:00
|
|
|
const target = ev.target as HTMLButtonElement;
|
|
|
|
this.setState({contextMenuPosition: target.getBoundingClientRect()});
|
|
|
|
};
|
|
|
|
|
|
|
|
private onContextMenu = (ev: React.MouseEvent) => {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
this.setState({
|
|
|
|
contextMenuPosition: {
|
|
|
|
left: ev.clientX,
|
|
|
|
top: ev.clientY,
|
|
|
|
height: 0,
|
|
|
|
},
|
|
|
|
});
|
2020-06-10 06:12:49 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
private onCloseMenu = () => {
|
2020-07-02 01:06:26 +03:00
|
|
|
this.setState({contextMenuPosition: null});
|
2020-06-10 06:12:49 +03:00
|
|
|
};
|
|
|
|
|
2020-06-12 07:04:10 +03:00
|
|
|
private onUnreadFirstChanged = async () => {
|
2020-06-22 23:52:17 +03:00
|
|
|
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
2020-06-12 07:04:10 +03:00
|
|
|
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
|
2020-06-22 23:52:17 +03:00
|
|
|
await RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
|
2020-06-12 07:04:10 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
private onTagSortChanged = async (sort: SortAlgorithm) => {
|
2020-06-22 23:52:17 +03:00
|
|
|
await RoomListStore.instance.setTagSorting(this.props.tagId, sort);
|
2020-06-10 06:12:49 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
private onMessagePreviewChanged = () => {
|
2020-07-09 03:27:50 +03:00
|
|
|
this.layout.showPreviews = !this.layout.showPreviews;
|
2020-06-10 06:12:49 +03:00
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
|
|
|
};
|
|
|
|
|
2020-07-02 21:48:06 +03:00
|
|
|
private onBadgeClick = (ev: React.MouseEvent) => {
|
|
|
|
ev.preventDefault();
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
let room;
|
|
|
|
if (this.props.tagId === DefaultTagID.Invite) {
|
2020-07-02 21:56:41 +03:00
|
|
|
// switch to first room as that'll be the top of the list for the user
|
2020-07-02 21:48:06 +03:00
|
|
|
room = this.props.rooms && this.props.rooms[0];
|
|
|
|
} else {
|
2020-07-02 21:56:41 +03:00
|
|
|
// find the first room with a count of the same colour as the badge count
|
2020-07-02 21:48:06 +03:00
|
|
|
room = this.props.rooms.find((r: Room) => {
|
|
|
|
const notifState = this.state.notificationState.getForRoom(r);
|
|
|
|
return notifState.count > 0 && notifState.color === this.state.notificationState.color;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (room) {
|
|
|
|
dis.dispatch({
|
|
|
|
action: 'view_room',
|
|
|
|
room_id: room.roomId,
|
2020-07-05 03:07:46 +03:00
|
|
|
show_room_tile: true, // to make sure the room gets scrolled into view
|
2020-07-02 21:48:06 +03:00
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-06-16 04:47:25 +03:00
|
|
|
private onHeaderClick = (ev: React.MouseEvent<HTMLDivElement>) => {
|
|
|
|
let target = ev.target as HTMLDivElement;
|
|
|
|
if (!target.classList.contains('mx_RoomSublist2_headerText')) {
|
|
|
|
// If we don't have the headerText class, the user clicked the span in the headerText.
|
|
|
|
target = target.parentElement as HTMLDivElement;
|
|
|
|
}
|
|
|
|
|
|
|
|
const possibleSticky = target.parentElement;
|
|
|
|
const sublist = possibleSticky.parentElement.parentElement;
|
2020-07-08 16:11:47 +03:00
|
|
|
const list = sublist.parentElement.parentElement;
|
|
|
|
// the scrollTop is capped at the height of the header in LeftPanel2
|
|
|
|
const isAtTop = list.scrollTop <= HEADER_HEIGHT;
|
|
|
|
const isSticky = possibleSticky.classList.contains('mx_RoomSublist2_headerContainer_sticky');
|
|
|
|
if (isSticky && !isAtTop) {
|
2020-06-16 04:47:25 +03:00
|
|
|
// is sticky - jump to list
|
|
|
|
sublist.scrollIntoView({behavior: 'smooth'});
|
|
|
|
} else {
|
|
|
|
// on screen - toggle collapse
|
2020-07-03 00:21:10 +03:00
|
|
|
this.toggleCollapsed();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private toggleCollapsed = () => {
|
2020-07-09 03:27:50 +03:00
|
|
|
this.layout.isCollapsed = !this.layout.isCollapsed;
|
2020-07-03 00:21:10 +03:00
|
|
|
this.forceUpdate(); // because the layout doesn't trigger an update
|
2020-07-06 23:05:06 +03:00
|
|
|
setImmediate(() => this.props.onResize()); // needs to happen when the DOM is updated
|
2020-07-03 00:21:10 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
2020-07-09 03:27:50 +03:00
|
|
|
const isCollapsed = this.layout && this.layout.isCollapsed;
|
2020-07-03 00:21:10 +03:00
|
|
|
switch (ev.key) {
|
|
|
|
case Key.ARROW_LEFT:
|
|
|
|
ev.stopPropagation();
|
|
|
|
if (!isCollapsed) {
|
|
|
|
// On ARROW_LEFT collapse the room sublist if it isn't already
|
|
|
|
this.toggleCollapsed();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case Key.ARROW_RIGHT: {
|
|
|
|
ev.stopPropagation();
|
|
|
|
if (isCollapsed) {
|
|
|
|
// On ARROW_RIGHT expand the room sublist if it isn't already
|
|
|
|
this.toggleCollapsed();
|
|
|
|
} else if (this.sublistRef.current) {
|
|
|
|
// otherwise focus the first room
|
|
|
|
const element = this.sublistRef.current.querySelector(".mx_RoomTile2") as HTMLDivElement;
|
|
|
|
if (element) {
|
|
|
|
element.focus();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
private onKeyDown = (ev: React.KeyboardEvent) => {
|
|
|
|
switch (ev.key) {
|
|
|
|
// On ARROW_LEFT go to the sublist header
|
|
|
|
case Key.ARROW_LEFT:
|
|
|
|
ev.stopPropagation();
|
|
|
|
this.headerButton.current.focus();
|
|
|
|
break;
|
|
|
|
// Consume ARROW_RIGHT so it doesn't cause focus to get sent to composer
|
|
|
|
case Key.ARROW_RIGHT:
|
|
|
|
ev.stopPropagation();
|
2020-06-16 04:47:25 +03:00
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2020-07-01 04:14:36 +03:00
|
|
|
private renderVisibleTiles(): React.ReactElement[] {
|
2020-07-09 03:27:50 +03:00
|
|
|
if (this.layout && this.layout.isCollapsed) {
|
2020-07-01 04:14:36 +03:00
|
|
|
// don't waste time on rendering
|
|
|
|
return [];
|
|
|
|
}
|
2020-06-16 04:47:25 +03:00
|
|
|
|
2020-05-08 21:53:05 +03:00
|
|
|
const tiles: React.ReactElement[] = [];
|
|
|
|
|
|
|
|
if (this.props.rooms) {
|
2020-07-01 04:14:36 +03:00
|
|
|
const visibleRooms = this.props.rooms.slice(0, this.numVisibleTiles);
|
|
|
|
for (const room of visibleRooms) {
|
2020-06-05 06:21:04 +03:00
|
|
|
tiles.push(
|
|
|
|
<RoomTile2
|
|
|
|
room={room}
|
|
|
|
key={`room-${room.roomId}`}
|
2020-07-09 03:27:50 +03:00
|
|
|
showMessagePreview={this.layout.showPreviews}
|
2020-06-11 23:39:28 +03:00
|
|
|
isMinimized={this.props.isMinimized}
|
2020-06-22 23:52:17 +03:00
|
|
|
tag={this.props.tagId}
|
2020-06-05 06:21:04 +03:00
|
|
|
/>
|
|
|
|
);
|
2020-05-08 21:53:05 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-07 15:53:17 +03:00
|
|
|
if (this.props.extraBadTilesThatShouldntExist) {
|
|
|
|
tiles.push(...this.props.extraBadTilesThatShouldntExist);
|
|
|
|
}
|
|
|
|
|
2020-07-02 18:04:38 +03:00
|
|
|
// We only have to do this because of the extra tiles. We do it conditionally
|
|
|
|
// to avoid spending cycles on slicing. It's generally fine to do this though
|
|
|
|
// as users are unlikely to have more than a handful of tiles when the extra
|
|
|
|
// tiles are used.
|
|
|
|
if (tiles.length > this.numVisibleTiles) {
|
|
|
|
return tiles.slice(0, this.numVisibleTiles);
|
|
|
|
}
|
|
|
|
|
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;
|
2020-07-02 01:06:26 +03:00
|
|
|
if (this.state.contextMenuPosition) {
|
2020-06-22 23:52:17 +03:00
|
|
|
const isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
|
|
|
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
2020-07-07 05:37:04 +03:00
|
|
|
|
2020-07-07 15:52:44 +03:00
|
|
|
// Invites don't get some nonsense options, so only add them if we have to.
|
|
|
|
let otherSections = null;
|
2020-07-07 05:37:04 +03:00
|
|
|
if (this.props.tagId !== DefaultTagID.Invite) {
|
2020-07-07 15:52:44 +03:00
|
|
|
otherSections = (
|
|
|
|
<React.Fragment>
|
|
|
|
<hr />
|
|
|
|
<div>
|
|
|
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Unread rooms")}</div>
|
2020-07-07 16:10:58 +03:00
|
|
|
<StyledMenuItemCheckbox
|
|
|
|
onClose={this.onCloseMenu}
|
2020-07-07 15:52:44 +03:00
|
|
|
onChange={this.onUnreadFirstChanged}
|
|
|
|
checked={isUnreadFirst}
|
|
|
|
>
|
|
|
|
{_t("Always show first")}
|
2020-07-07 16:10:58 +03:00
|
|
|
</StyledMenuItemCheckbox>
|
2020-07-07 15:52:44 +03:00
|
|
|
</div>
|
|
|
|
<hr />
|
|
|
|
<div>
|
|
|
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Show")}</div>
|
2020-07-07 16:10:58 +03:00
|
|
|
<StyledMenuItemCheckbox
|
|
|
|
onClose={this.onCloseMenu}
|
2020-07-07 15:52:44 +03:00
|
|
|
onChange={this.onMessagePreviewChanged}
|
2020-07-09 03:27:50 +03:00
|
|
|
checked={this.layout.showPreviews}
|
2020-07-07 15:52:44 +03:00
|
|
|
>
|
|
|
|
{_t("Message preview")}
|
2020-07-07 16:10:58 +03:00
|
|
|
</StyledMenuItemCheckbox>
|
2020-07-07 15:52:44 +03:00
|
|
|
</div>
|
|
|
|
</React.Fragment>
|
2020-07-07 05:37:04 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-10 06:12:49 +03:00
|
|
|
contextMenu = (
|
|
|
|
<ContextMenu
|
2020-07-02 01:56:57 +03:00
|
|
|
chevronFace={ChevronFace.None}
|
2020-07-02 01:06:26 +03:00
|
|
|
left={this.state.contextMenuPosition.left}
|
|
|
|
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
2020-06-10 06:12:49 +03:00
|
|
|
onFinished={this.onCloseMenu}
|
|
|
|
>
|
|
|
|
<div className="mx_RoomSublist2_contextMenu">
|
|
|
|
<div>
|
|
|
|
<div className='mx_RoomSublist2_contextMenu_title'>{_t("Sort by")}</div>
|
2020-07-05 20:23:57 +03:00
|
|
|
<StyledMenuItemRadio
|
2020-07-06 12:18:49 +03:00
|
|
|
onClose={this.onCloseMenu}
|
2020-06-12 07:04:10 +03:00
|
|
|
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
|
|
|
checked={!isAlphabetical}
|
2020-06-22 23:52:17 +03:00
|
|
|
name={`mx_${this.props.tagId}_sortBy`}
|
2020-06-12 07:04:10 +03:00
|
|
|
>
|
|
|
|
{_t("Activity")}
|
2020-07-05 20:23:57 +03:00
|
|
|
</StyledMenuItemRadio>
|
|
|
|
<StyledMenuItemRadio
|
2020-07-06 12:18:49 +03:00
|
|
|
onClose={this.onCloseMenu}
|
2020-06-12 07:04:10 +03:00
|
|
|
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
|
|
|
|
checked={isAlphabetical}
|
2020-06-22 23:52:17 +03:00
|
|
|
name={`mx_${this.props.tagId}_sortBy`}
|
2020-06-12 07:04:10 +03:00
|
|
|
>
|
|
|
|
{_t("A-Z")}
|
2020-07-05 20:23:57 +03:00
|
|
|
</StyledMenuItemRadio>
|
2020-06-10 06:12:49 +03:00
|
|
|
</div>
|
2020-07-07 05:37:04 +03:00
|
|
|
{otherSections}
|
2020-06-10 06:12:49 +03:00
|
|
|
</div>
|
|
|
|
</ContextMenu>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<React.Fragment>
|
|
|
|
<ContextMenuButton
|
|
|
|
className="mx_RoomSublist2_menuButton"
|
|
|
|
onClick={this.onOpenMenuClick}
|
|
|
|
label={_t("List options")}
|
2020-07-02 01:06:26 +03:00
|
|
|
isExpanded={!!this.state.contextMenuPosition}
|
2020-06-10 06:12:49 +03:00
|
|
|
/>
|
|
|
|
{contextMenu}
|
|
|
|
</React.Fragment>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-05-08 21:53:05 +03:00
|
|
|
private renderHeader(): React.ReactElement {
|
|
|
|
return (
|
2020-07-05 03:07:46 +03:00
|
|
|
<RovingTabIndexWrapper inputRef={this.headerButton}>
|
2020-05-08 21:53:05 +03:00
|
|
|
{({onFocus, isActive, ref}) => {
|
|
|
|
const tabIndex = isActive ? 0 : -1;
|
|
|
|
|
2020-07-05 03:07:46 +03:00
|
|
|
let ariaLabel = _t("Jump to first unread room.");
|
|
|
|
if (this.props.tagId === DefaultTagID.Invite) {
|
|
|
|
ariaLabel = _t("Jump to first invite.");
|
|
|
|
}
|
|
|
|
|
2020-07-02 21:48:06 +03:00
|
|
|
const badge = (
|
|
|
|
<NotificationBadge
|
|
|
|
forceCount={true}
|
|
|
|
notification={this.state.notificationState}
|
|
|
|
onClick={this.onBadgeClick}
|
|
|
|
tabIndex={tabIndex}
|
2020-07-05 03:07:46 +03:00
|
|
|
aria-label={ariaLabel}
|
2020-07-02 21:48:06 +03:00
|
|
|
/>
|
|
|
|
);
|
2020-06-05 06:21:04 +03:00
|
|
|
|
2020-06-10 06:12:49 +03:00
|
|
|
let addRoomButton = null;
|
|
|
|
if (!!this.props.onAddRoom) {
|
|
|
|
addRoomButton = (
|
2020-07-01 03:50:31 +03:00
|
|
|
<AccessibleTooltipButton
|
2020-06-10 06:12:49 +03:00
|
|
|
tabIndex={tabIndex}
|
|
|
|
onClick={this.onAddRoom}
|
|
|
|
className="mx_RoomSublist2_auxButton"
|
|
|
|
aria-label={this.props.addRoomLabel || _t("Add room")}
|
2020-07-01 03:50:31 +03:00
|
|
|
title={this.props.addRoomLabel}
|
|
|
|
tooltipClassName={"mx_RoomSublist2_addRoomTooltip"}
|
2020-06-10 06:12:49 +03:00
|
|
|
/>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-06-16 04:47:25 +03:00
|
|
|
const collapseClasses = classNames({
|
|
|
|
'mx_RoomSublist2_collapseBtn': true,
|
2020-07-09 03:27:50 +03:00
|
|
|
'mx_RoomSublist2_collapseBtn_collapsed': this.layout && this.layout.isCollapsed,
|
2020-06-16 04:47:25 +03:00
|
|
|
});
|
|
|
|
|
2020-06-10 06:12:49 +03:00
|
|
|
const classes = classNames({
|
|
|
|
'mx_RoomSublist2_headerContainer': true,
|
|
|
|
'mx_RoomSublist2_headerContainer_withAux': !!addRoomButton,
|
|
|
|
});
|
2020-05-08 21:53:05 +03:00
|
|
|
|
2020-06-22 21:51:53 +03:00
|
|
|
const badgeContainer = (
|
|
|
|
<div className="mx_RoomSublist2_badgeContainer">
|
|
|
|
{badge}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
// Note: the addRoomButton conditionally gets moved around
|
|
|
|
// the DOM depending on whether or not the list is minimized.
|
|
|
|
// If we're minimized, we want it below the header so it
|
|
|
|
// doesn't become sticky.
|
|
|
|
// The same applies to the notification badge.
|
2020-05-08 21:53:05 +03:00
|
|
|
return (
|
2020-07-05 03:07:46 +03:00
|
|
|
<div className={classes} onKeyDown={this.onHeaderKeyDown} onFocus={onFocus} aria-label={this.props.label}>
|
2020-07-03 00:21:10 +03:00
|
|
|
<div className="mx_RoomSublist2_stickable">
|
2020-06-14 04:07:19 +03:00
|
|
|
<AccessibleButton
|
2020-07-02 01:06:26 +03:00
|
|
|
onFocus={onFocus}
|
2020-06-14 04:07:19 +03:00
|
|
|
inputRef={ref}
|
|
|
|
tabIndex={tabIndex}
|
2020-07-03 00:21:10 +03:00
|
|
|
className="mx_RoomSublist2_headerText"
|
2020-06-14 04:07:19 +03:00
|
|
|
role="treeitem"
|
2020-07-09 03:27:50 +03:00
|
|
|
aria-expanded={!this.layout.isCollapsed}
|
2020-06-14 04:07:19 +03:00
|
|
|
aria-level={1}
|
2020-06-16 04:47:25 +03:00
|
|
|
onClick={this.onHeaderClick}
|
2020-07-02 01:06:26 +03:00
|
|
|
onContextMenu={this.onContextMenu}
|
2020-06-14 04:07:19 +03:00
|
|
|
>
|
2020-06-16 04:47:25 +03:00
|
|
|
<span className={collapseClasses} />
|
2020-06-14 04:07:19 +03:00
|
|
|
<span>{this.props.label}</span>
|
|
|
|
</AccessibleButton>
|
|
|
|
{this.renderMenu()}
|
2020-06-22 21:51:53 +03:00
|
|
|
{this.props.isMinimized ? null : badgeContainer}
|
2020-06-25 19:54:51 +03:00
|
|
|
{this.props.isMinimized ? null : addRoomButton}
|
2020-06-08 22:42:18 +03:00
|
|
|
</div>
|
2020-06-22 21:51:53 +03:00
|
|
|
{this.props.isMinimized ? badgeContainer : null}
|
|
|
|
{this.props.isMinimized ? addRoomButton : null}
|
2020-05-08 21:53:05 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}}
|
|
|
|
</RovingTabIndexWrapper>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public render(): React.ReactElement {
|
2020-06-29 05:03:04 +03:00
|
|
|
// TODO: Error boundary: https://github.com/vector-im/riot-web/issues/14185
|
2020-05-08 21:53:05 +03:00
|
|
|
|
2020-07-01 04:14:36 +03:00
|
|
|
const visibleTiles = this.renderVisibleTiles();
|
2020-05-08 21:53:05 +03:00
|
|
|
|
|
|
|
const classes = classNames({
|
2020-06-05 01:34:04 +03:00
|
|
|
'mx_RoomSublist2': true,
|
2020-07-02 01:06:26 +03:00
|
|
|
'mx_RoomSublist2_hasMenuOpen': !!this.state.contextMenuPosition,
|
2020-06-11 23:39:28 +03:00
|
|
|
'mx_RoomSublist2_minimized': this.props.isMinimized,
|
2020-05-08 21:53:05 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
let content = null;
|
2020-07-01 04:14:36 +03:00
|
|
|
if (visibleTiles.length > 0) {
|
2020-07-09 03:27:50 +03:00
|
|
|
const layout = this.layout; // to shorten calls
|
2020-06-10 04:48:31 +03:00
|
|
|
|
2020-07-09 17:43:20 +03:00
|
|
|
const maxTilesFactored = layout.tilesWithResizerBoxFactor(this.numTiles);
|
2020-06-25 19:07:23 +03:00
|
|
|
const showMoreBtnClasses = classNames({
|
|
|
|
'mx_RoomSublist2_showNButton': true,
|
2020-07-09 17:43:20 +03:00
|
|
|
'mx_RoomSublist2_isCutting': this.state.isResizing && layout.visibleTiles < maxTilesFactored,
|
2020-06-25 19:07:23 +03:00
|
|
|
});
|
|
|
|
|
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-16 05:00:09 +03:00
|
|
|
// floats above the resize handle, if we have one present. If the user has all
|
|
|
|
// tiles visible, it becomes 'show less'.
|
|
|
|
let showNButton = null;
|
2020-07-01 20:59:32 +03:00
|
|
|
if (this.numTiles > visibleTiles.length) {
|
2020-06-04 06:52:05 +03:00
|
|
|
// we have a cutoff condition - add the button to show all
|
2020-07-01 04:14:36 +03:00
|
|
|
const numMissing = this.numTiles - visibleTiles.length;
|
2020-06-11 23:39:28 +03:00
|
|
|
let showMoreText = (
|
2020-06-16 05:00:09 +03:00
|
|
|
<span className='mx_RoomSublist2_showNButtonText'>
|
2020-06-11 23:39:28 +03:00
|
|
|
{_t("Show %(count)s more", {count: numMissing})}
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
if (this.props.isMinimized) showMoreText = null;
|
2020-06-16 05:00:09 +03:00
|
|
|
showNButton = (
|
2020-07-07 19:47:21 +03:00
|
|
|
<RovingAccessibleButton onClick={this.onShowAllClick} className={showMoreBtnClasses}>
|
2020-06-16 05:00:09 +03:00
|
|
|
<span className='mx_RoomSublist2_showMoreButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
2020-06-10 04:48:31 +03:00
|
|
|
{/* set by CSS masking */}
|
|
|
|
</span>
|
2020-06-11 23:39:28 +03:00
|
|
|
{showMoreText}
|
2020-07-07 19:47:21 +03:00
|
|
|
</RovingAccessibleButton>
|
2020-06-10 04:48:31 +03:00
|
|
|
);
|
2020-07-09 03:27:50 +03:00
|
|
|
} else if (this.numTiles <= visibleTiles.length && this.numTiles > this.layout.defaultVisibleTiles) {
|
2020-06-16 05:00:09 +03:00
|
|
|
// we have all tiles visible - add a button to show less
|
|
|
|
let showLessText = (
|
|
|
|
<span className='mx_RoomSublist2_showNButtonText'>
|
|
|
|
{_t("Show less")}
|
|
|
|
</span>
|
|
|
|
);
|
|
|
|
if (this.props.isMinimized) showLessText = null;
|
|
|
|
showNButton = (
|
2020-07-07 19:47:21 +03:00
|
|
|
<RovingAccessibleButton onClick={this.onShowLessClick} className={showMoreBtnClasses}>
|
2020-06-16 05:00:09 +03:00
|
|
|
<span className='mx_RoomSublist2_showLessButtonChevron mx_RoomSublist2_showNButtonChevron'>
|
|
|
|
{/* set by CSS masking */}
|
|
|
|
</span>
|
|
|
|
{showLessText}
|
2020-07-07 19:47:21 +03:00
|
|
|
</RovingAccessibleButton>
|
2020-06-16 05:00:09 +03:00
|
|
|
);
|
2020-06-10 04:48:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Figure out if we need a handle
|
2020-07-08 04:36:26 +03:00
|
|
|
const handles: Enable = {
|
|
|
|
bottom: true, // the only one we need, but the others must be explicitly false
|
|
|
|
bottomLeft: false,
|
|
|
|
bottomRight: false,
|
|
|
|
left: false,
|
|
|
|
right: false,
|
|
|
|
top: false,
|
|
|
|
topLeft: false,
|
|
|
|
topRight: false,
|
|
|
|
};
|
2020-07-01 04:14:36 +03:00
|
|
|
if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) {
|
2020-07-08 04:36:26 +03:00
|
|
|
// we're at a minimum, don't have a bottom handle
|
|
|
|
handles.bottom = false;
|
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).
|
|
|
|
|
|
|
|
// The padding is variable though, so figure out what we need padding for.
|
|
|
|
let padding = 0;
|
2020-07-09 22:07:13 +03:00
|
|
|
//if (showNButton) padding += SHOW_N_BUTTON_HEIGHT;
|
|
|
|
//padding += RESIZE_HANDLE_HEIGHT; // always append the handle height
|
2020-06-10 04:48:31 +03:00
|
|
|
|
2020-07-01 04:14:36 +03:00
|
|
|
const relativeTiles = layout.tilesWithPadding(this.numTiles, padding);
|
2020-06-18 07:09:59 +03:00
|
|
|
const minTilesPx = layout.calculateTilesToPixelsMin(relativeTiles, layout.minVisibleTiles, padding);
|
2020-07-01 04:14:36 +03:00
|
|
|
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, padding);
|
2020-06-18 07:09:59 +03:00
|
|
|
const tilesWithoutPadding = Math.min(relativeTiles, layout.visibleTiles);
|
|
|
|
const tilesPx = layout.calculateTilesToPixelsMin(relativeTiles, tilesWithoutPadding, padding);
|
2020-06-10 04:48:31 +03:00
|
|
|
|
2020-07-09 22:07:13 +03:00
|
|
|
const handleWrapperClasses = classNames({
|
|
|
|
'mx_RoomSublist2_resizerHandles': true,
|
|
|
|
'mx_RoomSublist2_resizerHandles_showNButton': !!showNButton,
|
|
|
|
});
|
|
|
|
|
2020-07-08 04:36:26 +03:00
|
|
|
const dimensions = {
|
|
|
|
height: tilesPx,
|
|
|
|
};
|
2020-05-08 21:53:05 +03:00
|
|
|
content = (
|
2020-07-09 22:07:13 +03:00
|
|
|
<React.Fragment>
|
|
|
|
<Resizable
|
|
|
|
size={dimensions as any}
|
|
|
|
minHeight={minTilesPx}
|
|
|
|
maxHeight={maxTilesPx}
|
|
|
|
onResizeStart={this.onResizeStart}
|
|
|
|
onResizeStop={this.onResizeStop}
|
|
|
|
onResize={this.onResize}
|
|
|
|
handleWrapperClass={handleWrapperClasses}
|
|
|
|
handleClasses={{bottom: "mx_RoomSublist2_resizerHandle"}}
|
|
|
|
className="mx_RoomSublist2_resizeBox"
|
|
|
|
enable={handles}
|
|
|
|
>
|
2020-07-10 19:29:39 +03:00
|
|
|
<div className="mx_RoomSublist2_tiles">
|
|
|
|
{visibleTiles}
|
|
|
|
</div>
|
|
|
|
{showNButton}
|
2020-07-09 22:07:13 +03:00
|
|
|
</Resizable>
|
|
|
|
</React.Fragment>
|
2020-06-18 16:32:43 +03:00
|
|
|
);
|
2020-05-08 21:53:05 +03:00
|
|
|
}
|
2020-04-30 22:21:50 +03:00
|
|
|
|
|
|
|
return (
|
2020-05-08 21:53:05 +03:00
|
|
|
<div
|
2020-07-03 00:21:10 +03:00
|
|
|
ref={this.sublistRef}
|
2020-05-08 21:53:05 +03:00
|
|
|
className={classes}
|
|
|
|
role="group"
|
|
|
|
aria-label={this.props.label}
|
2020-07-03 00:21:10 +03:00
|
|
|
onKeyDown={this.onKeyDown}
|
2020-05-08 21:53:05 +03:00
|
|
|
>
|
|
|
|
{this.renderHeader()}
|
|
|
|
{content}
|
2020-04-30 22:21:50 +03:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|