2020-04-30 13:21:50 -06: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.
|
|
|
|
|
*/
|
|
|
|
|
|
2022-09-07 16:42:39 +01:00
|
|
|
import { Room } from "matrix-js-sdk/src/models/room";
|
2020-05-08 12:53:05 -06:00
|
|
|
import classNames from "classnames";
|
2022-06-30 17:14:49 +02:00
|
|
|
import { Dispatcher } from "flux";
|
2021-10-22 17:23:32 -05:00
|
|
|
import { Enable, Resizable } from "re-resizable";
|
|
|
|
|
import { Direction } from "re-resizable/lib/resizer";
|
2022-06-30 17:14:49 +02:00
|
|
|
import * as React from "react";
|
|
|
|
|
import { ComponentType, createRef, ReactComponentElement } from "react";
|
2021-10-22 17:23:32 -05:00
|
|
|
|
2022-06-30 17:14:49 +02:00
|
|
|
import { polyfillTouchEvent } from "../../../@types/polyfill";
|
|
|
|
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
2020-07-10 16:10:05 -06:00
|
|
|
import { RovingAccessibleButton, RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
|
2022-06-30 17:14:49 +02:00
|
|
|
import { Action } from "../../../dispatcher/actions";
|
|
|
|
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|
|
|
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
|
|
|
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
|
|
|
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
2020-05-08 12:53:05 -06:00
|
|
|
import { _t } from "../../../languageHandler";
|
2022-06-30 17:14:49 +02:00
|
|
|
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
|
|
|
|
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
|
|
|
|
import { ListAlgorithm, SortAlgorithm } from "../../../stores/room-list/algorithms/models";
|
2020-06-03 21:16:53 -06:00
|
|
|
import { ListLayout } from "../../../stores/room-list/ListLayout";
|
2022-06-30 17:14:49 +02:00
|
|
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
|
|
|
|
import RoomListLayoutStore from "../../../stores/room-list/RoomListLayoutStore";
|
2022-09-07 16:42:39 +01:00
|
|
|
import RoomListStore, { LISTS_UPDATE_EVENT, LISTS_LOADING_EVENT } from "../../../stores/room-list/RoomListStore";
|
2022-06-30 17:14:49 +02:00
|
|
|
import { arrayFastClone, arrayHasOrderChange } from "../../../utils/arrays";
|
|
|
|
|
import { objectExcluding, objectHasDiff } from "../../../utils/objects";
|
|
|
|
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
2021-11-29 17:42:53 +00:00
|
|
|
import ContextMenu, {
|
2020-07-07 15:01:27 +01:00
|
|
|
ChevronFace,
|
2020-07-17 18:16:00 +01:00
|
|
|
ContextMenuTooltipButton,
|
2020-07-05 18:23:57 +01:00
|
|
|
StyledMenuItemCheckbox,
|
|
|
|
|
StyledMenuItemRadio,
|
|
|
|
|
} from "../../structures/ContextMenu";
|
2022-06-30 17:14:49 +02:00
|
|
|
import AccessibleButton from "../../views/elements/AccessibleButton";
|
2020-07-01 01:50:31 +01:00
|
|
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
2021-03-08 17:48:08 +00:00
|
|
|
import ExtraTile from "./ExtraTile";
|
2022-09-07 16:42:39 +01:00
|
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
|
|
|
|
import { SlidingSyncManager } from "../../../SlidingSyncManager";
|
2022-06-30 17:14:49 +02:00
|
|
|
import NotificationBadge from "./NotificationBadge";
|
|
|
|
|
import RoomTile from "./RoomTile";
|
2020-04-30 13:21:50 -06:00
|
|
|
|
2020-07-10 18:30:52 +02:00
|
|
|
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
|
2020-06-25 16:03:56 -06:00
|
|
|
const RESIZE_HANDLE_HEIGHT = 4; // As defined by CSS
|
2020-07-08 15:11:47 +02:00
|
|
|
export const HEADER_HEIGHT = 32; // As defined by CSS
|
2020-06-17 22:09:59 -06:00
|
|
|
|
|
|
|
|
const MAX_PADDING_HEIGHT = SHOW_N_BUTTON_HEIGHT + RESIZE_HANDLE_HEIGHT;
|
|
|
|
|
|
2020-07-07 19:36:26 -06:00
|
|
|
// HACK: We really shouldn't have to do this.
|
|
|
|
|
polyfillTouchEvent();
|
|
|
|
|
|
2021-11-30 18:08:46 +00:00
|
|
|
export interface IAuxButtonProps {
|
|
|
|
|
tabIndex: number;
|
|
|
|
|
dispatcher?: Dispatcher<ActionPayload>;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-30 13:21:50 -06:00
|
|
|
interface IProps {
|
|
|
|
|
forRooms: boolean;
|
|
|
|
|
startAsHidden: boolean;
|
|
|
|
|
label: string;
|
2021-11-30 18:08:46 +00:00
|
|
|
AuxButtonComponent?: ComponentType<IAuxButtonProps>;
|
2020-06-11 14:39:28 -06:00
|
|
|
isMinimized: boolean;
|
2020-06-22 14:52:17 -06:00
|
|
|
tagId: TagID;
|
2020-11-02 17:22:45 +00:00
|
|
|
showSkeleton?: boolean;
|
2021-04-16 09:43:59 +01:00
|
|
|
alwaysVisible?: boolean;
|
2021-12-06 09:25:14 +00:00
|
|
|
forceExpanded?: boolean;
|
2021-05-03 22:02:33 +05:30
|
|
|
resizeNotifier: ResizeNotifier;
|
2021-03-08 17:48:08 +00:00
|
|
|
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
2021-05-28 10:31:42 +01:00
|
|
|
onListCollapse?: (isExpanded: boolean) => void;
|
2020-04-30 13:21:50 -06:00
|
|
|
}
|
|
|
|
|
|
2020-07-10 18:31:53 +02:00
|
|
|
// TODO: Use re-resizer's NumberSize when it is exposed as the type
|
|
|
|
|
interface ResizeDelta {
|
2020-07-10 18:48:54 +02:00
|
|
|
width: number;
|
|
|
|
|
height: number;
|
2020-07-10 18:31:53 +02:00
|
|
|
}
|
|
|
|
|
|
2020-07-01 23:06:26 +01:00
|
|
|
type PartialDOMRect = Pick<DOMRect, "left" | "top" | "height">;
|
|
|
|
|
|
2020-04-30 13:21:50 -06:00
|
|
|
interface IState {
|
2020-07-01 23:06:26 +01:00
|
|
|
contextMenuPosition: PartialDOMRect;
|
2020-06-25 16:03:56 -06:00
|
|
|
isResizing: boolean;
|
2020-07-09 18:32:28 +01:00
|
|
|
isExpanded: boolean; // used for the for expand of the sublist when the room list is being filtered
|
2020-07-10 18:48:54 +02:00
|
|
|
height: number;
|
2020-07-23 21:23:45 -06:00
|
|
|
rooms: Room[];
|
2022-09-07 16:42:39 +01:00
|
|
|
roomsLoading: boolean;
|
2020-04-30 13:21:50 -06:00
|
|
|
}
|
|
|
|
|
|
2020-07-17 15:46:46 -06:00
|
|
|
export default class RoomSublist extends React.Component<IProps, IState> {
|
2020-07-02 22:21:10 +01:00
|
|
|
private headerButton = createRef<HTMLDivElement>();
|
|
|
|
|
private sublistRef = createRef<HTMLDivElement>();
|
2021-05-28 14:59:14 +01:00
|
|
|
private tilesRef = createRef<HTMLDivElement>();
|
2020-07-07 10:34:42 +01:00
|
|
|
private dispatcherRef: string;
|
2020-07-08 18:27:50 -06:00
|
|
|
private layout: ListLayout;
|
2020-07-10 18:48:54 +02:00
|
|
|
private heightAtStart: number;
|
2020-07-27 17:41:23 -06:00
|
|
|
private notificationState: ListNotificationState;
|
2020-07-02 22:21:10 +01:00
|
|
|
|
2022-09-07 16:42:39 +01:00
|
|
|
private slidingSyncMode: boolean;
|
|
|
|
|
|
2022-12-16 12:29:59 +00:00
|
|
|
public constructor(props: IProps) {
|
2020-06-08 13:42:18 -06:00
|
|
|
super(props);
|
2022-09-07 16:42:39 +01:00
|
|
|
// when this setting is toggled it restarts the app so it's safe to not watch this.
|
|
|
|
|
this.slidingSyncMode = SettingsStore.getValue("feature_sliding_sync");
|
2020-06-08 13:42:18 -06:00
|
|
|
|
2020-07-08 18:27:50 -06:00
|
|
|
this.layout = RoomListLayoutStore.instance.getLayoutFor(this.props.tagId);
|
2020-07-10 18:36:33 +02:00
|
|
|
this.heightAtStart = 0;
|
2020-07-27 17:33:27 -06:00
|
|
|
this.notificationState = RoomNotificationStateStore.instance.getListState(this.props.tagId);
|
2020-06-08 13:42:18 -06:00
|
|
|
this.state = {
|
2020-07-01 23:06:26 +01:00
|
|
|
contextMenuPosition: null,
|
2020-06-25 16:03:56 -06:00
|
|
|
isResizing: false,
|
2022-06-30 17:14:49 +02:00
|
|
|
isExpanded: !this.layout.isCollapsed,
|
2020-07-23 21:23:45 -06:00
|
|
|
height: 0, // to be fixed in a moment, we need `rooms` to calculate this.
|
2020-07-30 15:06:04 -06:00
|
|
|
rooms: arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []),
|
2022-09-07 16:42:39 +01:00
|
|
|
roomsLoading: false,
|
2020-06-08 13:42:18 -06:00
|
|
|
};
|
2020-07-23 21:23:45 -06:00
|
|
|
// Why Object.assign() and not this.state.height? Because TypeScript says no.
|
2021-06-29 13:11:58 +01:00
|
|
|
this.state = Object.assign(this.state, { height: this.calculateInitialHeight() });
|
2020-05-08 12:53:05 -06:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private calculateInitialHeight(): number {
|
2020-07-10 18:35:07 +02:00
|
|
|
const requestedVisibleTiles = Math.max(Math.floor(this.layout.visibleTiles), this.layout.minVisibleTiles);
|
|
|
|
|
const tileCount = Math.min(this.numTiles, requestedVisibleTiles);
|
2020-07-10 16:10:05 -06:00
|
|
|
return this.layout.tilesToPixelsWithPadding(tileCount, this.padding);
|
2020-07-10 18:35:07 +02:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private get padding(): number {
|
2020-07-10 18:35:07 +02:00
|
|
|
let padding = RESIZE_HANDLE_HEIGHT;
|
|
|
|
|
// this is used for calculating the max height of the whole container,
|
2020-07-14 21:49:51 +01:00
|
|
|
// and takes into account whether there should be room reserved for the show more/less button
|
|
|
|
|
// when fully expanded. We can't rely purely on the layout's defaultVisible tile count
|
2020-07-13 12:59:09 -06:00
|
|
|
// because there are conditions in which we need to know that the 'show more' button
|
|
|
|
|
// is present while well under the default tile limit.
|
2020-07-14 21:49:51 +01:00
|
|
|
const needsShowMore = this.numTiles > this.numVisibleTiles;
|
|
|
|
|
|
|
|
|
|
// ...but also check this or we'll miss if the section is expanded and we need a
|
|
|
|
|
// 'show less'
|
|
|
|
|
const needsShowLess = this.numTiles > this.layout.defaultVisibleTiles;
|
|
|
|
|
|
|
|
|
|
if (needsShowMore || needsShowLess) {
|
2020-07-10 18:35:07 +02:00
|
|
|
padding += SHOW_N_BUTTON_HEIGHT;
|
|
|
|
|
}
|
|
|
|
|
return padding;
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-08 17:48:08 +00:00
|
|
|
private get extraTiles(): ReactComponentElement<typeof ExtraTile>[] | null {
|
|
|
|
|
if (this.props.extraTiles) {
|
|
|
|
|
return this.props.extraTiles;
|
2020-07-24 14:36:06 -06:00
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-08 12:53:05 -06:00
|
|
|
private get numTiles(): number {
|
2020-07-24 14:36:06 -06:00
|
|
|
return RoomSublist.calcNumTiles(this.state.rooms, this.extraTiles);
|
2020-07-10 18:37:58 +02:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private static calcNumTiles(rooms: Room[], extraTiles: any[]): number {
|
2020-07-23 21:23:45 -06:00
|
|
|
return (rooms || []).length + (extraTiles || []).length;
|
2020-05-08 12:53:05 -06:00
|
|
|
}
|
|
|
|
|
|
2020-06-30 19:14:36 -06:00
|
|
|
private get numVisibleTiles(): number {
|
2022-09-07 16:42:39 +01:00
|
|
|
if (this.slidingSyncMode) {
|
|
|
|
|
return this.state.rooms.length;
|
|
|
|
|
}
|
2020-07-09 13:07:13 -06:00
|
|
|
const nVisible = Math.ceil(this.layout.visibleTiles);
|
2020-06-30 19:14:36 -06:00
|
|
|
return Math.min(nVisible, this.numTiles);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
|
2022-06-30 17:14:49 +02:00
|
|
|
const prevExtraTiles = prevProps.extraTiles;
|
2020-07-10 18:37:58 +02:00
|
|
|
// as the rooms can come in one by one we need to reevaluate
|
|
|
|
|
// the amount of available rooms to cap the amount of requested visible rooms by the layout
|
2020-07-24 14:36:06 -06:00
|
|
|
if (RoomSublist.calcNumTiles(prevState.rooms, prevExtraTiles) !== this.numTiles) {
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ height: this.calculateInitialHeight() });
|
2020-07-10 18:37:58 +02:00
|
|
|
}
|
2020-06-08 13:42:18 -06:00
|
|
|
}
|
|
|
|
|
|
2020-07-23 22:13:32 -06:00
|
|
|
public shouldComponentUpdate(nextProps: Readonly<IProps>, nextState: Readonly<IState>): boolean {
|
2020-07-30 14:08:18 -06:00
|
|
|
if (objectHasDiff(this.props, nextProps)) {
|
2020-07-23 22:13:32 -06:00
|
|
|
// Something we don't care to optimize has updated, so update.
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Do the same check used on props for state, without the rooms we're going to no-op
|
|
|
|
|
const prevStateNoRooms = objectExcluding(this.state, ["rooms"]);
|
|
|
|
|
const nextStateNoRooms = objectExcluding(nextState, ["rooms"]);
|
2020-07-30 14:08:18 -06:00
|
|
|
if (objectHasDiff(prevStateNoRooms, nextStateNoRooms)) {
|
2020-07-23 22:13:32 -06:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we're supposed to handle extra tiles, take the performance hit and re-render all the
|
|
|
|
|
// time so we don't have to consider them as part of the visible room optimization.
|
2021-03-08 17:48:08 +00:00
|
|
|
const prevExtraTiles = this.props.extraTiles || [];
|
2022-06-30 17:14:49 +02:00
|
|
|
const nextExtraTiles = nextProps.extraTiles || [];
|
2020-07-23 22:13:32 -06:00
|
|
|
if (prevExtraTiles.length > 0 || nextExtraTiles.length > 0) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we're about to update the height of the list, we don't really care about which rooms
|
|
|
|
|
// are visible or not for no-op purposes, so ensure that the height calculation runs through.
|
2020-07-24 14:36:06 -06:00
|
|
|
if (RoomSublist.calcNumTiles(nextState.rooms, nextExtraTiles) !== this.numTiles) {
|
2020-07-23 22:13:32 -06:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-24 10:20:00 -06:00
|
|
|
// Before we go analyzing the rooms, we can see if we're collapsed. If we're collapsed, we don't need
|
|
|
|
|
// to render anything. We do this after the height check though to ensure that the height gets appropriately
|
|
|
|
|
// calculated for when/if we become uncollapsed.
|
|
|
|
|
if (!nextState.isExpanded) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-23 22:31:52 -06:00
|
|
|
// Quickly double check we're not about to break something due to the number of rooms changing.
|
|
|
|
|
if (this.state.rooms.length !== nextState.rooms.length) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-23 22:13:32 -06:00
|
|
|
// Finally, determine if the room update (as presumably that's all that's left) is within
|
|
|
|
|
// our visible range. If it is, then do a render. If the update is outside our visible range
|
|
|
|
|
// then we can skip the update.
|
|
|
|
|
//
|
|
|
|
|
// We also optimize for order changing here: if the update did happen in our visible range
|
|
|
|
|
// but doesn't result in the list re-sorting itself then there's no reason for us to update
|
|
|
|
|
// on our own.
|
|
|
|
|
const prevSlicedRooms = this.state.rooms.slice(0, this.numVisibleTiles);
|
|
|
|
|
const nextSlicedRooms = nextState.rooms.slice(0, this.numVisibleTiles);
|
|
|
|
|
if (arrayHasOrderChange(prevSlicedRooms, nextSlicedRooms)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Finally, nothing happened so no-op the update
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public componentDidMount(): void {
|
2021-04-15 15:51:00 +01:00
|
|
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
|
|
|
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
2022-09-07 16:42:39 +01:00
|
|
|
RoomListStore.instance.on(LISTS_LOADING_EVENT, this.onListsLoading);
|
|
|
|
|
|
2021-05-28 14:59:14 +01:00
|
|
|
// Using the passive option to not block the main thread
|
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#improving_scrolling_performance_with_passive_listeners
|
2021-05-28 17:37:29 +01:00
|
|
|
this.tilesRef.current?.addEventListener("scroll", this.onScrollPrevent, { passive: true });
|
2021-04-15 15:51:00 +01:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
public componentWillUnmount(): void {
|
2020-07-07 10:34:42 +01:00
|
|
|
defaultDispatcher.unregister(this.dispatcherRef);
|
2020-07-23 21:23:45 -06:00
|
|
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onListsUpdated);
|
2022-09-07 16:42:39 +01:00
|
|
|
RoomListStore.instance.off(LISTS_LOADING_EVENT, this.onListsLoading);
|
2021-05-28 17:37:29 +01:00
|
|
|
this.tilesRef.current?.removeEventListener("scroll", this.onScrollPrevent);
|
2020-06-10 16:04:27 -06:00
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onListsLoading = (tagId: TagID, isLoading: boolean): void => {
|
2022-09-07 16:42:39 +01:00
|
|
|
if (this.props.tagId !== tagId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
this.setState({
|
|
|
|
|
roomsLoading: isLoading,
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onListsUpdated = (): void => {
|
2022-11-07 13:45:34 +00:00
|
|
|
const stateUpdates = {} as IState;
|
2020-07-24 14:36:06 -06:00
|
|
|
|
2020-07-23 21:23:45 -06:00
|
|
|
const currentRooms = this.state.rooms;
|
2020-07-30 15:06:04 -06:00
|
|
|
const newRooms = arrayFastClone(RoomListStore.instance.orderedLists[this.props.tagId] || []);
|
2020-07-23 21:23:45 -06:00
|
|
|
if (arrayHasOrderChange(currentRooms, newRooms)) {
|
2020-07-24 14:36:06 -06:00
|
|
|
stateUpdates.rooms = newRooms;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (Object.keys(stateUpdates).length > 0) {
|
|
|
|
|
this.setState(stateUpdates);
|
2020-07-23 21:23:45 -06:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onAction = (payload: ActionPayload): void => {
|
2022-01-12 20:12:28 +00:00
|
|
|
if (payload.action === Action.ViewRoom && payload.show_room_tile && this.state.rooms) {
|
2020-07-07 10:34:42 +01:00
|
|
|
// 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-23 21:23:45 -06:00
|
|
|
const roomIndex = this.state.rooms.findIndex((r) => r.roomId === payload.room_id);
|
2020-07-07 10:34:42 +01:00
|
|
|
|
2020-07-09 18:32:28 +01:00
|
|
|
if (!this.state.isExpanded && roomIndex > -1) {
|
2020-07-07 10:34:42 +01:00
|
|
|
this.toggleCollapsed();
|
|
|
|
|
}
|
2020-07-07 10:35:16 +01:00
|
|
|
// extend the visible section to include the room if it is entirely invisible
|
2020-07-07 10:34:42 +01:00
|
|
|
if (roomIndex >= this.numVisibleTiles) {
|
2020-07-08 18:27:50 -06:00
|
|
|
this.layout.visibleTiles = this.layout.tilesWithPadding(roomIndex + 1, MAX_PADDING_HEIGHT);
|
2020-07-07 10:34:42 +01:00
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private applyHeightChange(newHeight: number): void {
|
2020-07-10 18:36:33 +02:00
|
|
|
const heightInTiles = Math.ceil(this.layout.pixelsToTiles(newHeight - this.padding));
|
|
|
|
|
this.layout.visibleTiles = Math.min(this.numTiles, heightInTiles);
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-07 19:36:26 -06:00
|
|
|
private onResize = (
|
|
|
|
|
e: MouseEvent | TouchEvent,
|
|
|
|
|
travelDirection: Direction,
|
|
|
|
|
refToElement: HTMLDivElement,
|
2020-07-10 18:31:53 +02:00
|
|
|
delta: ResizeDelta,
|
2023-01-12 13:25:14 +00:00
|
|
|
): void => {
|
2020-07-10 18:36:33 +02:00
|
|
|
const newHeight = this.heightAtStart + delta.height;
|
|
|
|
|
this.applyHeightChange(newHeight);
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ height: newHeight });
|
2020-06-03 21:52:05 -06:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onResizeStart = (): void => {
|
2020-07-10 18:36:33 +02:00
|
|
|
this.heightAtStart = this.state.height;
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ isResizing: true });
|
2020-06-25 16:03:56 -06:00
|
|
|
};
|
|
|
|
|
|
2020-07-10 18:42:51 +02:00
|
|
|
private onResizeStop = (
|
|
|
|
|
e: MouseEvent | TouchEvent,
|
|
|
|
|
travelDirection: Direction,
|
|
|
|
|
refToElement: HTMLDivElement,
|
|
|
|
|
delta: ResizeDelta,
|
2023-01-12 13:25:14 +00:00
|
|
|
): void => {
|
2020-07-10 18:42:51 +02:00
|
|
|
const newHeight = this.heightAtStart + delta.height;
|
2020-07-10 18:36:33 +02:00
|
|
|
this.applyHeightChange(newHeight);
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ isResizing: false, height: newHeight });
|
2020-06-25 16:03:56 -06:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onShowAllClick = async (): Promise<void> => {
|
2022-09-07 16:42:39 +01:00
|
|
|
if (this.slidingSyncMode) {
|
|
|
|
|
const count = RoomListStore.instance.getCount(this.props.tagId);
|
2023-01-18 17:19:12 +00:00
|
|
|
await SlidingSyncManager.instance.ensureListRegistered(this.props.tagId, {
|
2022-09-07 16:42:39 +01:00
|
|
|
ranges: [[0, count]],
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-07-14 09:38:31 +01:00
|
|
|
// read number of visible tiles before we mutate it
|
|
|
|
|
const numVisibleTiles = this.numVisibleTiles;
|
2020-07-10 18:36:33 +02:00
|
|
|
const newHeight = this.layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
|
|
|
|
|
this.applyHeightChange(newHeight);
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ height: newHeight }, () => {
|
2020-07-14 09:38:31 +01:00
|
|
|
// focus the top-most new room
|
|
|
|
|
this.focusRoomTile(numVisibleTiles);
|
2020-07-10 18:36:33 +02:00
|
|
|
});
|
2020-06-03 21:52:05 -06:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onShowLessClick = (): void => {
|
2020-07-10 18:36:33 +02:00
|
|
|
const newHeight = this.layout.tilesToPixelsWithPadding(this.layout.defaultVisibleTiles, this.padding);
|
|
|
|
|
this.applyHeightChange(newHeight);
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ height: newHeight });
|
2020-07-07 17:07:51 +01:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private focusRoomTile = (index: number): void => {
|
2020-07-07 17:07:51 +01:00
|
|
|
if (!this.sublistRef.current) return;
|
2020-07-17 15:43:29 -06:00
|
|
|
const elements = this.sublistRef.current.querySelectorAll<HTMLDivElement>(".mx_RoomTile");
|
2020-07-07 17:07:51 +01:00
|
|
|
const element = elements && elements[index];
|
|
|
|
|
if (element) {
|
|
|
|
|
element.focus();
|
|
|
|
|
}
|
2020-06-15 20:00:09 -06:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onOpenMenuClick = (ev: React.MouseEvent): void => {
|
2020-06-09 21:12:49 -06:00
|
|
|
ev.preventDefault();
|
|
|
|
|
ev.stopPropagation();
|
2020-07-01 23:06:26 +01:00
|
|
|
const target = ev.target as HTMLButtonElement;
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
|
2020-07-01 23:06:26 +01:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onContextMenu = (ev: React.MouseEvent): void => {
|
2020-07-01 23:06:26 +01:00
|
|
|
ev.preventDefault();
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
this.setState({
|
|
|
|
|
contextMenuPosition: {
|
|
|
|
|
left: ev.clientX,
|
|
|
|
|
top: ev.clientY,
|
|
|
|
|
height: 0,
|
|
|
|
|
},
|
|
|
|
|
});
|
2020-06-09 21:12:49 -06:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onCloseMenu = (): void => {
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ contextMenuPosition: null });
|
2020-06-09 21:12:49 -06:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onUnreadFirstChanged = (): void => {
|
2020-06-22 14:52:17 -06:00
|
|
|
const isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
2020-06-11 22:04:10 -06:00
|
|
|
const newAlgorithm = isUnreadFirst ? ListAlgorithm.Natural : ListAlgorithm.Importance;
|
2021-07-16 09:15:56 +01:00
|
|
|
RoomListStore.instance.setListOrder(this.props.tagId, newAlgorithm);
|
2020-10-19 14:34:47 +01:00
|
|
|
this.forceUpdate(); // because if the sublist doesn't have any changes then we will miss the list order change
|
2020-06-11 22:04:10 -06:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onTagSortChanged = async (sort: SortAlgorithm): Promise<void> => {
|
2022-08-09 20:46:59 +01:00
|
|
|
RoomListStore.instance.setTagSorting(this.props.tagId, sort);
|
2021-12-17 11:08:56 -06:00
|
|
|
this.forceUpdate();
|
2020-06-09 21:12:49 -06:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onMessagePreviewChanged = (): void => {
|
2020-07-08 18:27:50 -06:00
|
|
|
this.layout.showPreviews = !this.layout.showPreviews;
|
2020-06-09 21:12:49 -06:00
|
|
|
this.forceUpdate(); // because the layout doesn't trigger a re-render
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onBadgeClick = (ev: React.MouseEvent): void => {
|
2020-07-02 19:48:06 +01:00
|
|
|
ev.preventDefault();
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
|
|
|
|
|
let room;
|
|
|
|
|
if (this.props.tagId === DefaultTagID.Invite) {
|
2020-07-02 19:56:41 +01:00
|
|
|
// switch to first room as that'll be the top of the list for the user
|
2020-07-23 21:23:45 -06:00
|
|
|
room = this.state.rooms && this.state.rooms[0];
|
2020-07-02 19:48:06 +01:00
|
|
|
} else {
|
2020-07-02 19:56:41 +01:00
|
|
|
// find the first room with a count of the same colour as the badge count
|
2022-06-30 17:14:49 +02:00
|
|
|
room = RoomListStore.instance.orderedLists[this.props.tagId].find((r: Room) => {
|
2020-07-27 17:33:27 -06:00
|
|
|
const notifState = this.notificationState.getForRoom(r);
|
|
|
|
|
return notifState.count > 0 && notifState.color === this.notificationState.color;
|
2020-07-02 19:48:06 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (room) {
|
2022-05-03 22:04:37 +01:00
|
|
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
2021-11-25 17:49:43 -03:00
|
|
|
action: Action.ViewRoom,
|
2020-07-02 19:48:06 +01:00
|
|
|
room_id: room.roomId,
|
2020-07-05 01:07:46 +01:00
|
|
|
show_room_tile: true, // to make sure the room gets scrolled into view
|
2022-02-17 18:03:27 +00:00
|
|
|
metricsTrigger: "WebRoomListNotificationBadge",
|
|
|
|
|
metricsViaKeyboard: ev.type !== "click",
|
2020-07-02 19:48:06 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onHeaderClick = (): void => {
|
2020-07-13 17:17:05 +01:00
|
|
|
const possibleSticky = this.headerButton.current.parentElement;
|
2020-06-15 19:47:25 -06:00
|
|
|
const sublist = possibleSticky.parentElement.parentElement;
|
2020-07-08 15:11:47 +02:00
|
|
|
const list = sublist.parentElement.parentElement;
|
2020-07-17 15:22:18 -06:00
|
|
|
// the scrollTop is capped at the height of the header in LeftPanel, the top header is always sticky
|
2021-06-09 17:59:41 +01:00
|
|
|
const listScrollTop = Math.round(list.scrollTop);
|
|
|
|
|
const isAtTop = listScrollTop <= Math.round(HEADER_HEIGHT);
|
|
|
|
|
const isAtBottom = listScrollTop >= Math.round(list.scrollHeight - list.offsetHeight);
|
2020-07-17 15:46:46 -06:00
|
|
|
const isStickyTop = possibleSticky.classList.contains("mx_RoomSublist_headerContainer_stickyTop");
|
|
|
|
|
const isStickyBottom = possibleSticky.classList.contains("mx_RoomSublist_headerContainer_stickyBottom");
|
2020-07-13 17:17:05 +01:00
|
|
|
|
|
|
|
|
if ((isStickyBottom && !isAtBottom) || (isStickyTop && !isAtTop)) {
|
2020-06-15 19:47:25 -06:00
|
|
|
// is sticky - jump to list
|
2021-06-29 13:11:58 +01:00
|
|
|
sublist.scrollIntoView({ behavior: "smooth" });
|
2020-06-15 19:47:25 -06:00
|
|
|
} else {
|
|
|
|
|
// on screen - toggle collapse
|
2020-07-13 17:17:05 +01:00
|
|
|
const isExpanded = this.state.isExpanded;
|
2020-07-02 22:21:10 +01:00
|
|
|
this.toggleCollapsed();
|
2020-07-13 17:17:05 +01:00
|
|
|
// if the bottom list is collapsed then scroll it in so it doesn't expand off screen
|
|
|
|
|
if (!isExpanded && isStickyBottom) {
|
|
|
|
|
setImmediate(() => {
|
2021-06-29 13:11:58 +01:00
|
|
|
sublist.scrollIntoView({ behavior: "smooth" });
|
2020-07-13 17:17:05 +01:00
|
|
|
});
|
|
|
|
|
}
|
2020-07-02 22:21:10 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private toggleCollapsed = (): void => {
|
2021-12-06 09:25:14 +00:00
|
|
|
if (this.props.forceExpanded) return;
|
2020-07-09 18:32:28 +01:00
|
|
|
this.layout.isCollapsed = this.state.isExpanded;
|
2021-06-29 13:11:58 +01:00
|
|
|
this.setState({ isExpanded: !this.layout.isCollapsed });
|
2021-05-28 10:31:42 +01:00
|
|
|
if (this.props.onListCollapse) {
|
2021-06-29 13:11:58 +01:00
|
|
|
this.props.onListCollapse(!this.layout.isCollapsed);
|
2021-05-28 10:31:42 +01:00
|
|
|
}
|
2020-07-02 22:21:10 +01:00
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onHeaderKeyDown = (ev: React.KeyboardEvent): void => {
|
2021-03-01 21:43:00 +13:00
|
|
|
const action = getKeyBindingsManager().getRoomListAction(ev);
|
2021-02-28 20:13:34 +13:00
|
|
|
switch (action) {
|
2022-01-31 16:55:45 +01:00
|
|
|
case KeyBindingAction.CollapseRoomListSection:
|
2020-07-02 22:21:10 +01:00
|
|
|
ev.stopPropagation();
|
2020-07-09 18:32:28 +01:00
|
|
|
if (this.state.isExpanded) {
|
2021-02-28 20:13:34 +13:00
|
|
|
// Collapse the room sublist if it isn't already
|
2020-07-02 22:21:10 +01:00
|
|
|
this.toggleCollapsed();
|
|
|
|
|
}
|
|
|
|
|
break;
|
2022-01-31 16:55:45 +01:00
|
|
|
case KeyBindingAction.ExpandRoomListSection: {
|
2020-07-02 22:21:10 +01:00
|
|
|
ev.stopPropagation();
|
2020-07-09 18:32:28 +01:00
|
|
|
if (!this.state.isExpanded) {
|
2021-02-28 20:13:34 +13:00
|
|
|
// Expand the room sublist if it isn't already
|
2020-07-02 22:21:10 +01:00
|
|
|
this.toggleCollapsed();
|
|
|
|
|
} else if (this.sublistRef.current) {
|
|
|
|
|
// otherwise focus the first room
|
2020-07-17 15:43:29 -06:00
|
|
|
const element = this.sublistRef.current.querySelector(".mx_RoomTile") as HTMLDivElement;
|
2020-07-02 22:21:10 +01:00
|
|
|
if (element) {
|
|
|
|
|
element.focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onKeyDown = (ev: React.KeyboardEvent): void => {
|
2022-02-28 17:05:52 +01:00
|
|
|
const action = getKeyBindingsManager().getAccessibilityAction(ev);
|
|
|
|
|
switch (action) {
|
|
|
|
|
// On ArrowLeft go to the sublist header
|
|
|
|
|
case KeyBindingAction.ArrowLeft:
|
2020-07-02 22:21:10 +01:00
|
|
|
ev.stopPropagation();
|
|
|
|
|
this.headerButton.current.focus();
|
|
|
|
|
break;
|
2022-02-28 17:05:52 +01:00
|
|
|
// Consume ArrowRight so it doesn't cause focus to get sent to composer
|
|
|
|
|
case KeyBindingAction.ArrowRight:
|
2020-07-02 22:21:10 +01:00
|
|
|
ev.stopPropagation();
|
2020-06-15 19:47:25 -06:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2020-06-30 19:14:36 -06:00
|
|
|
private renderVisibleTiles(): React.ReactElement[] {
|
2021-12-06 09:25:14 +00:00
|
|
|
if (!this.state.isExpanded && !this.props.forceExpanded) {
|
2020-06-30 19:14:36 -06:00
|
|
|
// don't waste time on rendering
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2020-06-15 19:47:25 -06:00
|
|
|
|
2020-05-08 12:53:05 -06:00
|
|
|
const tiles: React.ReactElement[] = [];
|
|
|
|
|
|
2020-07-23 21:23:45 -06:00
|
|
|
if (this.state.rooms) {
|
2021-12-06 09:25:14 +00:00
|
|
|
let visibleRooms = this.state.rooms;
|
|
|
|
|
if (!this.props.forceExpanded) {
|
|
|
|
|
visibleRooms = visibleRooms.slice(0, this.numVisibleTiles);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-30 19:14:36 -06:00
|
|
|
for (const room of visibleRooms) {
|
2020-08-29 01:11:08 +01:00
|
|
|
tiles.push(
|
|
|
|
|
<RoomTile
|
|
|
|
|
room={room}
|
|
|
|
|
key={`room-${room.roomId}`}
|
|
|
|
|
showMessagePreview={this.layout.showPreviews}
|
|
|
|
|
isMinimized={this.props.isMinimized}
|
|
|
|
|
tag={this.props.tagId}
|
|
|
|
|
/>,
|
|
|
|
|
);
|
2020-05-08 12:53:05 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-24 14:36:06 -06:00
|
|
|
if (this.extraTiles) {
|
|
|
|
|
// HACK: We break typing here, but this 'extra tiles' property shouldn't exist.
|
|
|
|
|
(tiles as any[]).push(...this.extraTiles);
|
2020-07-07 06:53:17 -06:00
|
|
|
}
|
|
|
|
|
|
2020-07-02 09:04:38 -06: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.
|
2021-12-06 09:25:14 +00:00
|
|
|
if (tiles.length > this.numVisibleTiles && !this.props.forceExpanded) {
|
2020-07-02 09:04:38 -06:00
|
|
|
return tiles.slice(0, this.numVisibleTiles);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-08 12:53:05 -06:00
|
|
|
return tiles;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-09 21:12:49 -06:00
|
|
|
private renderMenu(): React.ReactElement {
|
2022-07-21 16:17:36 +03:00
|
|
|
if (this.props.tagId === DefaultTagID.Suggested || this.props.tagId === DefaultTagID.SavedItems) return null; // not sortable
|
2021-12-23 12:01:52 +00:00
|
|
|
|
2020-06-09 21:12:49 -06:00
|
|
|
let contextMenu = null;
|
2020-07-01 23:06:26 +01:00
|
|
|
if (this.state.contextMenuPosition) {
|
2022-09-07 16:42:39 +01:00
|
|
|
let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
|
|
|
|
|
let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
|
|
|
|
|
if (this.slidingSyncMode) {
|
2023-01-18 17:19:12 +00:00
|
|
|
const slidingList = SlidingSyncManager.instance.slidingSync.getListParams(this.props.tagId);
|
2023-01-20 10:31:44 +00:00
|
|
|
isAlphabetical = (slidingList?.sort || [])[0] === "by_name";
|
|
|
|
|
isUnreadFirst = (slidingList?.sort || [])[0] === "by_notification_level";
|
2022-09-07 16:42:39 +01:00
|
|
|
}
|
2020-07-06 20:37:04 -06:00
|
|
|
|
2020-07-07 06:52:44 -06:00
|
|
|
// Invites don't get some nonsense options, so only add them if we have to.
|
|
|
|
|
let otherSections = null;
|
2020-07-06 20:37:04 -06:00
|
|
|
if (this.props.tagId !== DefaultTagID.Invite) {
|
2020-07-07 06:52:44 -06:00
|
|
|
otherSections = (
|
|
|
|
|
<React.Fragment>
|
|
|
|
|
<hr />
|
|
|
|
|
<div>
|
2021-07-19 22:43:11 +01:00
|
|
|
<div className="mx_RoomSublist_contextMenu_title">{_t("Appearance")}</div>
|
2020-07-07 14:10:58 +01:00
|
|
|
<StyledMenuItemCheckbox
|
|
|
|
|
onClose={this.onCloseMenu}
|
2020-07-07 06:52:44 -06:00
|
|
|
onChange={this.onUnreadFirstChanged}
|
|
|
|
|
checked={isUnreadFirst}
|
|
|
|
|
>
|
2021-07-19 22:43:11 +01:00
|
|
|
{_t("Show rooms with unread messages first")}
|
2020-07-07 14:10:58 +01:00
|
|
|
</StyledMenuItemCheckbox>
|
|
|
|
|
<StyledMenuItemCheckbox
|
|
|
|
|
onClose={this.onCloseMenu}
|
2020-07-07 06:52:44 -06:00
|
|
|
onChange={this.onMessagePreviewChanged}
|
2020-07-08 18:27:50 -06:00
|
|
|
checked={this.layout.showPreviews}
|
2020-07-07 06:52:44 -06:00
|
|
|
>
|
2021-07-19 22:43:11 +01:00
|
|
|
{_t("Show previews of messages")}
|
2020-07-07 14:10:58 +01:00
|
|
|
</StyledMenuItemCheckbox>
|
2020-07-07 06:52:44 -06:00
|
|
|
</div>
|
|
|
|
|
</React.Fragment>
|
2020-07-06 20:37:04 -06:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-09 21:12:49 -06:00
|
|
|
contextMenu = (
|
|
|
|
|
<ContextMenu
|
2020-07-01 23:56:57 +01:00
|
|
|
chevronFace={ChevronFace.None}
|
2020-07-01 23:06:26 +01:00
|
|
|
left={this.state.contextMenuPosition.left}
|
|
|
|
|
top={this.state.contextMenuPosition.top + this.state.contextMenuPosition.height}
|
2020-06-09 21:12:49 -06:00
|
|
|
onFinished={this.onCloseMenu}
|
|
|
|
|
>
|
2020-07-17 15:46:46 -06:00
|
|
|
<div className="mx_RoomSublist_contextMenu">
|
2020-06-09 21:12:49 -06:00
|
|
|
<div>
|
2021-07-19 22:43:11 +01:00
|
|
|
<div className="mx_RoomSublist_contextMenu_title">{_t("Sort by")}</div>
|
2020-07-05 18:23:57 +01:00
|
|
|
<StyledMenuItemRadio
|
2020-07-06 10:18:49 +01:00
|
|
|
onClose={this.onCloseMenu}
|
2020-06-11 22:04:10 -06:00
|
|
|
onChange={() => this.onTagSortChanged(SortAlgorithm.Recent)}
|
|
|
|
|
checked={!isAlphabetical}
|
2020-06-22 14:52:17 -06:00
|
|
|
name={`mx_${this.props.tagId}_sortBy`}
|
2020-06-11 22:04:10 -06:00
|
|
|
>
|
2021-07-19 22:43:11 +01:00
|
|
|
{_t("Activity")}
|
2020-07-05 18:23:57 +01:00
|
|
|
</StyledMenuItemRadio>
|
|
|
|
|
<StyledMenuItemRadio
|
2020-07-06 10:18:49 +01:00
|
|
|
onClose={this.onCloseMenu}
|
2020-06-11 22:04:10 -06:00
|
|
|
onChange={() => this.onTagSortChanged(SortAlgorithm.Alphabetic)}
|
|
|
|
|
checked={isAlphabetical}
|
2020-06-22 14:52:17 -06:00
|
|
|
name={`mx_${this.props.tagId}_sortBy`}
|
2020-06-11 22:04:10 -06:00
|
|
|
>
|
2021-07-19 22:43:11 +01:00
|
|
|
{_t("A-Z")}
|
2020-07-05 18:23:57 +01:00
|
|
|
</StyledMenuItemRadio>
|
2020-06-09 21:12:49 -06:00
|
|
|
</div>
|
2021-07-19 22:43:11 +01:00
|
|
|
{otherSections}
|
2020-06-09 21:12:49 -06:00
|
|
|
</div>
|
|
|
|
|
</ContextMenu>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<React.Fragment>
|
2020-07-17 18:16:00 +01:00
|
|
|
<ContextMenuTooltipButton
|
2020-07-17 15:46:46 -06:00
|
|
|
className="mx_RoomSublist_menuButton"
|
2020-06-09 21:12:49 -06:00
|
|
|
onClick={this.onOpenMenuClick}
|
2020-07-17 18:16:00 +01:00
|
|
|
title={_t("List options")}
|
2020-07-01 23:06:26 +01:00
|
|
|
isExpanded={!!this.state.contextMenuPosition}
|
2020-06-09 21:12:49 -06:00
|
|
|
/>
|
2021-07-19 22:43:11 +01:00
|
|
|
{contextMenu}
|
2020-06-09 21:12:49 -06:00
|
|
|
</React.Fragment>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-08 12:53:05 -06:00
|
|
|
private renderHeader(): React.ReactElement {
|
2020-04-30 13:21:50 -06:00
|
|
|
return (
|
2020-07-05 01:07:46 +01:00
|
|
|
<RovingTabIndexWrapper inputRef={this.headerButton}>
|
2021-07-19 22:43:11 +01:00
|
|
|
{({ onFocus, isActive, ref }) => {
|
2020-05-08 12:53:05 -06:00
|
|
|
const tabIndex = isActive ? 0 : -1;
|
|
|
|
|
|
2020-07-05 01:07:46 +01:00
|
|
|
let ariaLabel = _t("Jump to first unread room.");
|
|
|
|
|
if (this.props.tagId === DefaultTagID.Invite) {
|
|
|
|
|
ariaLabel = _t("Jump to first invite.");
|
|
|
|
|
}
|
|
|
|
|
|
2020-07-02 19:48:06 +01:00
|
|
|
const badge = (
|
|
|
|
|
<NotificationBadge
|
|
|
|
|
forceCount={true}
|
2020-07-27 17:33:27 -06:00
|
|
|
notification={this.notificationState}
|
2020-07-02 19:48:06 +01:00
|
|
|
onClick={this.onBadgeClick}
|
|
|
|
|
tabIndex={tabIndex}
|
2020-07-05 01:07:46 +01:00
|
|
|
aria-label={ariaLabel}
|
2021-09-10 10:58:13 +01:00
|
|
|
showUnsentTooltip={true}
|
2020-07-02 19:48:06 +01:00
|
|
|
/>
|
|
|
|
|
);
|
2020-05-08 12:53:05 -06:00
|
|
|
|
2020-06-09 21:12:49 -06:00
|
|
|
let addRoomButton = null;
|
2021-11-30 18:08:46 +00:00
|
|
|
if (this.props.AuxButtonComponent) {
|
|
|
|
|
const AuxButtonComponent = this.props.AuxButtonComponent;
|
|
|
|
|
addRoomButton = <AuxButtonComponent tabIndex={tabIndex} />;
|
2020-06-09 21:12:49 -06:00
|
|
|
}
|
|
|
|
|
|
2020-06-15 19:47:25 -06:00
|
|
|
const collapseClasses = classNames({
|
2020-07-17 15:46:46 -06:00
|
|
|
mx_RoomSublist_collapseBtn: true,
|
2022-07-01 08:53:47 +01:00
|
|
|
mx_RoomSublist_collapseBtn_collapsed: !this.state.isExpanded && !this.props.forceExpanded,
|
2020-06-15 19:47:25 -06:00
|
|
|
});
|
|
|
|
|
|
2020-06-09 21:12:49 -06:00
|
|
|
const classes = classNames({
|
2020-07-17 15:46:46 -06:00
|
|
|
mx_RoomSublist_headerContainer: true,
|
|
|
|
|
mx_RoomSublist_headerContainer_withAux: !!addRoomButton,
|
2020-06-09 21:12:49 -06:00
|
|
|
});
|
2020-05-08 12:53:05 -06:00
|
|
|
|
2020-07-17 15:46:46 -06:00
|
|
|
const badgeContainer = <div className="mx_RoomSublist_badgeContainer">{badge}</div>;
|
2020-06-22 12:51:53 -06:00
|
|
|
|
2020-07-17 19:00:02 +01:00
|
|
|
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
2020-07-17 18:54:09 +01:00
|
|
|
if (this.props.isMinimized) {
|
|
|
|
|
Button = AccessibleTooltipButton;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-22 12:51:53 -06:00
|
|
|
// 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 12:53:05 -06:00
|
|
|
return (
|
2020-08-29 01:11:08 +01:00
|
|
|
<div
|
|
|
|
|
className={classes}
|
|
|
|
|
onKeyDown={this.onHeaderKeyDown}
|
|
|
|
|
onFocus={onFocus}
|
|
|
|
|
aria-label={this.props.label}
|
|
|
|
|
>
|
2022-03-09 13:18:17 -05:00
|
|
|
<div className="mx_RoomSublist_stickableContainer">
|
|
|
|
|
<div className="mx_RoomSublist_stickable">
|
|
|
|
|
<Button
|
|
|
|
|
onFocus={onFocus}
|
|
|
|
|
inputRef={ref}
|
|
|
|
|
tabIndex={tabIndex}
|
|
|
|
|
className="mx_RoomSublist_headerText"
|
|
|
|
|
role="treeitem"
|
|
|
|
|
aria-expanded={this.state.isExpanded}
|
|
|
|
|
aria-level={1}
|
|
|
|
|
onClick={this.onHeaderClick}
|
|
|
|
|
onContextMenu={this.onContextMenu}
|
|
|
|
|
title={this.props.isMinimized ? this.props.label : undefined}
|
|
|
|
|
>
|
|
|
|
|
<span className={collapseClasses} />
|
|
|
|
|
<span>{this.props.label}</span>
|
|
|
|
|
</Button>
|
|
|
|
|
{this.renderMenu()}
|
|
|
|
|
{this.props.isMinimized ? null : badgeContainer}
|
|
|
|
|
{this.props.isMinimized ? null : addRoomButton}
|
|
|
|
|
</div>
|
2020-06-08 13:42:18 -06:00
|
|
|
</div>
|
2021-07-19 22:43:11 +01:00
|
|
|
{this.props.isMinimized ? badgeContainer : null}
|
|
|
|
|
{this.props.isMinimized ? addRoomButton : null}
|
2020-05-08 12:53:05 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
2021-07-19 22:43:11 +01:00
|
|
|
}}
|
2020-05-08 12:53:05 -06:00
|
|
|
</RovingTabIndexWrapper>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-12 13:25:14 +00:00
|
|
|
private onScrollPrevent(e: Event): void {
|
2020-07-12 08:49:04 +01:00
|
|
|
// the RoomTile calls scrollIntoView and the browser may scroll a div we do not wish to be scrollable
|
2020-08-03 16:02:26 +01:00
|
|
|
// this fixes https://github.com/vector-im/element-web/issues/14413
|
2020-07-12 08:49:04 +01:00
|
|
|
(e.target as HTMLDivElement).scrollTop = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-08 12:53:05 -06:00
|
|
|
public render(): React.ReactElement {
|
2020-06-30 19:14:36 -06:00
|
|
|
const visibleTiles = this.renderVisibleTiles();
|
2022-11-30 15:18:10 +00:00
|
|
|
const hidden = !this.state.rooms.length && !this.props.extraTiles?.length && this.props.alwaysVisible !== true;
|
2020-05-08 12:53:05 -06:00
|
|
|
const classes = classNames({
|
2020-07-17 15:46:46 -06:00
|
|
|
mx_RoomSublist: true,
|
|
|
|
|
mx_RoomSublist_hasMenuOpen: !!this.state.contextMenuPosition,
|
|
|
|
|
mx_RoomSublist_minimized: this.props.isMinimized,
|
2022-11-30 15:18:10 +00:00
|
|
|
mx_RoomSublist_hidden: hidden,
|
2020-05-08 12:53:05 -06:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let content = null;
|
2022-09-07 16:42:39 +01:00
|
|
|
if (this.state.roomsLoading) {
|
|
|
|
|
content = <div className="mx_RoomSublist_skeletonUI" />;
|
|
|
|
|
} else if (visibleTiles.length > 0 && this.props.forceExpanded) {
|
2021-12-07 15:54:30 +00:00
|
|
|
content = (
|
|
|
|
|
<div className="mx_RoomSublist_resizeBox mx_RoomSublist_resizeBox_forceExpanded">
|
2021-12-06 09:25:14 +00:00
|
|
|
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
|
|
|
|
{visibleTiles}
|
2022-12-12 12:24:14 +01:00
|
|
|
</div>
|
2021-12-06 09:25:14 +00:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
} else if (visibleTiles.length > 0) {
|
2020-07-08 18:27:50 -06:00
|
|
|
const layout = this.layout; // to shorten calls
|
2020-06-09 19:48:31 -06:00
|
|
|
|
2020-07-10 18:35:07 +02:00
|
|
|
const minTiles = Math.min(layout.minVisibleTiles, this.numTiles);
|
|
|
|
|
const showMoreAtMinHeight = minTiles < this.numTiles;
|
|
|
|
|
const minHeightPadding = RESIZE_HANDLE_HEIGHT + (showMoreAtMinHeight ? SHOW_N_BUTTON_HEIGHT : 0);
|
|
|
|
|
const minTilesPx = layout.tilesToPixelsWithPadding(minTiles, minHeightPadding);
|
2020-08-29 01:11:08 +01:00
|
|
|
const maxTilesPx = layout.tilesToPixelsWithPadding(this.numTiles, this.padding);
|
2020-06-25 10:07:23 -06:00
|
|
|
const showMoreBtnClasses = classNames({
|
2020-07-17 15:46:46 -06:00
|
|
|
mx_RoomSublist_showNButton: true,
|
2020-06-25 10:07:23 -06:00
|
|
|
});
|
|
|
|
|
|
2020-06-09 19:48:31 -06:00
|
|
|
// If we're hiding rooms, show a 'show more' button to the user. This button
|
2020-06-15 20:00:09 -06: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;
|
2022-09-07 16:42:39 +01:00
|
|
|
const hasMoreSlidingSync =
|
|
|
|
|
this.slidingSyncMode && RoomListStore.instance.getCount(this.props.tagId) > this.state.rooms.length;
|
2020-07-10 18:38:32 +02:00
|
|
|
|
2022-09-07 16:42:39 +01:00
|
|
|
if (maxTilesPx > this.state.height || hasMoreSlidingSync) {
|
2020-07-14 21:49:51 +01:00
|
|
|
// the height of all the tiles is greater than the section height: we need a 'show more' button
|
2020-07-10 18:38:32 +02:00
|
|
|
const nonPaddedHeight = this.state.height - RESIZE_HANDLE_HEIGHT - SHOW_N_BUTTON_HEIGHT;
|
|
|
|
|
const amountFullyShown = Math.floor(nonPaddedHeight / this.layout.tileHeight);
|
2022-09-07 16:42:39 +01:00
|
|
|
let numMissing = this.numTiles - amountFullyShown;
|
|
|
|
|
if (this.slidingSyncMode) {
|
|
|
|
|
numMissing = RoomListStore.instance.getCount(this.props.tagId) - amountFullyShown;
|
|
|
|
|
}
|
2021-06-29 13:11:58 +01:00
|
|
|
const label = _t("Show %(count)s more", { count: numMissing });
|
2020-07-17 15:46:46 -06:00
|
|
|
let showMoreText = <span className="mx_RoomSublist_showNButtonText">{label}</span>;
|
2020-06-11 14:39:28 -06:00
|
|
|
if (this.props.isMinimized) showMoreText = null;
|
2020-06-15 20:00:09 -06:00
|
|
|
showNButton = (
|
2020-08-05 09:56:31 +01:00
|
|
|
<RovingAccessibleButton
|
|
|
|
|
role="treeitem"
|
|
|
|
|
onClick={this.onShowAllClick}
|
|
|
|
|
className={showMoreBtnClasses}
|
|
|
|
|
aria-label={label}
|
|
|
|
|
>
|
2020-07-17 15:46:46 -06:00
|
|
|
<span className="mx_RoomSublist_showMoreButtonChevron mx_RoomSublist_showNButtonChevron">
|
2021-07-19 22:43:11 +01:00
|
|
|
{/* set by CSS masking */}
|
2020-06-09 19:48:31 -06:00
|
|
|
</span>
|
2021-07-19 22:43:11 +01:00
|
|
|
{showMoreText}
|
2020-07-07 17:47:21 +01:00
|
|
|
</RovingAccessibleButton>
|
2020-06-09 19:48:31 -06:00
|
|
|
);
|
2020-07-10 18:35:07 +02:00
|
|
|
} else if (this.numTiles > this.layout.defaultVisibleTiles) {
|
2020-06-15 20:00:09 -06:00
|
|
|
// we have all tiles visible - add a button to show less
|
2020-08-05 09:56:31 +01:00
|
|
|
const label = _t("Show less");
|
2020-07-17 15:46:46 -06:00
|
|
|
let showLessText = <span className="mx_RoomSublist_showNButtonText">{label}</span>;
|
2020-06-15 20:00:09 -06:00
|
|
|
if (this.props.isMinimized) showLessText = null;
|
|
|
|
|
showNButton = (
|
2020-08-05 09:56:31 +01:00
|
|
|
<RovingAccessibleButton
|
|
|
|
|
role="treeitem"
|
|
|
|
|
onClick={this.onShowLessClick}
|
|
|
|
|
className={showMoreBtnClasses}
|
|
|
|
|
aria-label={label}
|
|
|
|
|
>
|
2020-07-17 15:46:46 -06:00
|
|
|
<span className="mx_RoomSublist_showLessButtonChevron mx_RoomSublist_showNButtonChevron">
|
2021-07-19 22:43:11 +01:00
|
|
|
{/* set by CSS masking */}
|
2020-06-15 20:00:09 -06:00
|
|
|
</span>
|
2021-07-19 22:43:11 +01:00
|
|
|
{showLessText}
|
2020-07-07 17:47:21 +01:00
|
|
|
</RovingAccessibleButton>
|
2020-06-15 20:00:09 -06:00
|
|
|
);
|
2020-06-09 19:48:31 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Figure out if we need a handle
|
2020-07-07 19:36:26 -06: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-06-30 19:14:36 -06:00
|
|
|
if (layout.visibleTiles >= this.numTiles && this.numTiles <= layout.minVisibleTiles) {
|
2020-07-07 19:36:26 -06:00
|
|
|
// we're at a minimum, don't have a bottom handle
|
|
|
|
|
handles.bottom = false;
|
2020-06-03 21:16:53 -06:00
|
|
|
}
|
2020-06-04 09:57:16 -06:00
|
|
|
|
2020-06-09 19:48:31 -06: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).
|
2020-06-03 21:52:05 -06:00
|
|
|
|
2020-07-09 13:07:13 -06:00
|
|
|
const handleWrapperClasses = classNames({
|
2020-07-17 15:46:46 -06:00
|
|
|
mx_RoomSublist_resizerHandles: true,
|
|
|
|
|
mx_RoomSublist_resizerHandles_showNButton: !!showNButton,
|
2020-07-09 13:07:13 -06:00
|
|
|
});
|
2020-06-09 19:48:31 -06:00
|
|
|
|
2020-05-08 12:53:05 -06:00
|
|
|
content = (
|
2020-07-09 13:07:13 -06:00
|
|
|
<React.Fragment>
|
|
|
|
|
<Resizable
|
2021-06-29 13:11:58 +01:00
|
|
|
size={{ height: this.state.height } as any}
|
2020-07-09 13:07:13 -06:00
|
|
|
minHeight={minTilesPx}
|
|
|
|
|
maxHeight={maxTilesPx}
|
|
|
|
|
onResizeStart={this.onResizeStart}
|
|
|
|
|
onResizeStop={this.onResizeStop}
|
|
|
|
|
onResize={this.onResize}
|
|
|
|
|
handleWrapperClass={handleWrapperClasses}
|
2021-06-29 13:11:58 +01:00
|
|
|
handleClasses={{ bottom: "mx_RoomSublist_resizerHandle" }}
|
2020-07-17 15:46:46 -06:00
|
|
|
className="mx_RoomSublist_resizeBox"
|
2020-07-09 13:07:13 -06:00
|
|
|
enable={handles}
|
|
|
|
|
>
|
2021-05-28 14:59:14 +01:00
|
|
|
<div className="mx_RoomSublist_tiles" ref={this.tilesRef}>
|
2021-07-19 22:43:11 +01:00
|
|
|
{visibleTiles}
|
2020-07-10 18:29:39 +02:00
|
|
|
</div>
|
2021-07-19 22:43:11 +01:00
|
|
|
{showNButton}
|
2020-07-09 13:07:13 -06:00
|
|
|
</Resizable>
|
|
|
|
|
</React.Fragment>
|
2020-06-18 14:32:43 +01:00
|
|
|
);
|
2020-11-02 17:22:45 +00:00
|
|
|
} else if (this.props.showSkeleton && this.state.isExpanded) {
|
|
|
|
|
content = <div className="mx_RoomSublist_skeletonUI" />;
|
2020-05-08 12:53:05 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
2020-07-02 22:21:10 +01:00
|
|
|
ref={this.sublistRef}
|
2020-05-08 12:53:05 -06:00
|
|
|
className={classes}
|
|
|
|
|
role="group"
|
2022-11-30 15:18:10 +00:00
|
|
|
aria-hidden={hidden}
|
2020-05-08 12:53:05 -06:00
|
|
|
aria-label={this.props.label}
|
2020-07-02 22:21:10 +01:00
|
|
|
onKeyDown={this.onKeyDown}
|
2020-05-08 12:53:05 -06:00
|
|
|
>
|
2021-07-19 22:43:11 +01:00
|
|
|
{this.renderHeader()}
|
|
|
|
|
{content}
|
2020-04-30 13:21:50 -06:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|