666 lines
28 KiB
TypeScript
666 lines
28 KiB
TypeScript
/*
|
|
Copyright 2024 New Vector Ltd.
|
|
Copyright 2015-2018 , 2020, 2021 The Matrix.org Foundation C.I.C.
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
|
|
Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import { EventType, RoomType, Room } from "matrix-js-sdk/src/matrix";
|
|
import React, { ComponentType, createRef, ReactComponentElement, SyntheticEvent } from "react";
|
|
|
|
import { IState as IRovingTabIndexState, RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
|
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
|
import { Action } from "../../../dispatcher/actions";
|
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
|
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
|
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
|
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
|
import { _t, _td, TranslationKey } from "../../../languageHandler";
|
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|
import PosthogTrackers from "../../../PosthogTrackers";
|
|
import SettingsStore from "../../../settings/SettingsStore";
|
|
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
|
import { UIComponent } from "../../../settings/UIFeature";
|
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
|
import { ITagMap } from "../../../stores/room-list/algorithms/models";
|
|
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
|
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
|
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
|
|
import {
|
|
isMetaSpace,
|
|
ISuggestedRoom,
|
|
MetaSpace,
|
|
SpaceKey,
|
|
UPDATE_SELECTED_SPACE,
|
|
UPDATE_SUGGESTED_ROOMS,
|
|
} from "../../../stores/spaces";
|
|
import SpaceStore from "../../../stores/spaces/SpaceStore";
|
|
import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
|
|
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
|
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
|
import { shouldShowSpaceInvite, showAddExistingRooms, showCreateNewRoom, showSpaceInvite } from "../../../utils/space";
|
|
import { ChevronFace, ContextMenuTooltipButton, MenuProps, useContextMenu } from "../../structures/ContextMenu";
|
|
import RoomAvatar from "../avatars/RoomAvatar";
|
|
import { BetaPill } from "../beta/BetaCard";
|
|
import IconizedContextMenu, {
|
|
IconizedContextMenuOption,
|
|
IconizedContextMenuOptionList,
|
|
} from "../context_menus/IconizedContextMenu";
|
|
import ExtraTile from "./ExtraTile";
|
|
import RoomSublist, { IAuxButtonProps } from "./RoomSublist";
|
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
|
import AccessibleButton from "../elements/AccessibleButton";
|
|
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
|
|
|
|
interface IProps {
|
|
onKeyDown: (ev: React.KeyboardEvent, state: IRovingTabIndexState) => void;
|
|
onFocus: (ev: React.FocusEvent) => void;
|
|
onBlur: (ev: React.FocusEvent) => void;
|
|
onResize: () => void;
|
|
onListCollapse?: (isExpanded: boolean) => void;
|
|
resizeNotifier: ResizeNotifier;
|
|
isMinimized: boolean;
|
|
activeSpace: SpaceKey;
|
|
}
|
|
|
|
interface IState {
|
|
sublists: ITagMap;
|
|
currentRoomId?: string;
|
|
suggestedRooms: ISuggestedRoom[];
|
|
}
|
|
|
|
export const TAG_ORDER: TagID[] = [
|
|
DefaultTagID.Invite,
|
|
DefaultTagID.Favourite,
|
|
DefaultTagID.DM,
|
|
DefaultTagID.Untagged,
|
|
DefaultTagID.Conference,
|
|
DefaultTagID.LowPriority,
|
|
DefaultTagID.ServerNotice,
|
|
DefaultTagID.Suggested,
|
|
];
|
|
const ALWAYS_VISIBLE_TAGS: TagID[] = [DefaultTagID.DM, DefaultTagID.Untagged];
|
|
|
|
interface ITagAesthetics {
|
|
sectionLabel: TranslationKey;
|
|
sectionLabelRaw?: string;
|
|
AuxButtonComponent?: ComponentType<IAuxButtonProps>;
|
|
isInvite: boolean;
|
|
defaultHidden: boolean;
|
|
}
|
|
|
|
type TagAestheticsMap = Partial<{
|
|
[tagId in TagID]: ITagAesthetics;
|
|
}>;
|
|
|
|
const auxButtonContextMenuPosition = (handle: HTMLDivElement): MenuProps => {
|
|
const rect = handle.getBoundingClientRect();
|
|
return {
|
|
chevronFace: ChevronFace.None,
|
|
left: rect.left - 7,
|
|
top: rect.top + rect.height,
|
|
};
|
|
};
|
|
|
|
const DmAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex, dispatcher = defaultDispatcher }) => {
|
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
|
const activeSpace = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
|
|
return SpaceStore.instance.activeSpaceRoom;
|
|
});
|
|
|
|
const showCreateRooms = shouldShowComponent(UIComponent.CreateRooms);
|
|
const showInviteUsers = shouldShowComponent(UIComponent.InviteUsers);
|
|
|
|
if (activeSpace && (showCreateRooms || showInviteUsers)) {
|
|
let contextMenu: JSX.Element | undefined;
|
|
if (menuDisplayed && handle.current) {
|
|
const canInvite = shouldShowSpaceInvite(activeSpace);
|
|
|
|
contextMenu = (
|
|
<IconizedContextMenu {...auxButtonContextMenuPosition(handle.current)} onFinished={closeMenu} compact>
|
|
<IconizedContextMenuOptionList first>
|
|
{showCreateRooms && (
|
|
<IconizedContextMenuOption
|
|
label={_t("action|start_new_chat")}
|
|
iconClassName="mx_RoomList_iconStartChat"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenu();
|
|
defaultDispatcher.dispatch({ action: "view_create_chat" });
|
|
PosthogTrackers.trackInteraction(
|
|
"WebRoomListRoomsSublistPlusMenuCreateChatItem",
|
|
e,
|
|
);
|
|
}}
|
|
/>
|
|
)}
|
|
{showInviteUsers && (
|
|
<IconizedContextMenuOption
|
|
label={_t("action|invite_to_space")}
|
|
iconClassName="mx_RoomList_iconInvite"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenu();
|
|
showSpaceInvite(activeSpace);
|
|
}}
|
|
disabled={!canInvite}
|
|
title={canInvite ? undefined : _t("spaces|error_no_permission_invite")}
|
|
/>
|
|
)}
|
|
</IconizedContextMenuOptionList>
|
|
</IconizedContextMenu>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<ContextMenuTooltipButton
|
|
tabIndex={tabIndex}
|
|
onClick={openMenu}
|
|
className="mx_RoomSublist_auxButton"
|
|
aria-label={_t("action|add_people")}
|
|
title={_t("action|add_people")}
|
|
isExpanded={menuDisplayed}
|
|
ref={handle}
|
|
/>
|
|
|
|
{contextMenu}
|
|
</>
|
|
);
|
|
} else if (!activeSpace && showCreateRooms) {
|
|
return (
|
|
<AccessibleButton
|
|
tabIndex={tabIndex}
|
|
onClick={(e) => {
|
|
dispatcher.dispatch({ action: "view_create_chat" });
|
|
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateChatItem", e);
|
|
}}
|
|
className="mx_RoomSublist_auxButton"
|
|
aria-label={_t("action|start_chat")}
|
|
title={_t("action|start_chat")}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const UntaggedAuxButton: React.FC<IAuxButtonProps> = ({ tabIndex }) => {
|
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLDivElement>();
|
|
const activeSpace = useEventEmitterState<Room | null>(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => {
|
|
return SpaceStore.instance.activeSpaceRoom;
|
|
});
|
|
|
|
const showCreateRoom = shouldShowComponent(UIComponent.CreateRooms);
|
|
const showExploreRooms = shouldShowComponent(UIComponent.ExploreRooms);
|
|
|
|
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
|
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
|
|
|
|
let contextMenuContent: JSX.Element | undefined;
|
|
if (menuDisplayed && activeSpace) {
|
|
const canAddRooms = activeSpace.currentState.maySendStateEvent(
|
|
EventType.SpaceChild,
|
|
MatrixClientPeg.safeGet().getSafeUserId(),
|
|
);
|
|
|
|
contextMenuContent = (
|
|
<IconizedContextMenuOptionList first>
|
|
<IconizedContextMenuOption
|
|
label={_t("action|explore_rooms")}
|
|
iconClassName="mx_RoomList_iconExplore"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenu();
|
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
|
action: Action.ViewRoom,
|
|
room_id: activeSpace.roomId,
|
|
metricsTrigger: undefined, // other
|
|
});
|
|
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
|
|
}}
|
|
/>
|
|
{showCreateRoom ? (
|
|
<>
|
|
<IconizedContextMenuOption
|
|
label={_t("action|new_room")}
|
|
iconClassName="mx_RoomList_iconNewRoom"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenu();
|
|
showCreateNewRoom(activeSpace);
|
|
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
|
}}
|
|
disabled={!canAddRooms}
|
|
title={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")}
|
|
/>
|
|
{videoRoomsEnabled && (
|
|
<IconizedContextMenuOption
|
|
label={_t("action|new_video_room")}
|
|
iconClassName="mx_RoomList_iconNewVideoRoom"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenu();
|
|
showCreateNewRoom(
|
|
activeSpace,
|
|
elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo,
|
|
);
|
|
}}
|
|
disabled={!canAddRooms}
|
|
title={canAddRooms ? undefined : _t("spaces|error_no_permission_create_room")}
|
|
>
|
|
<BetaPill />
|
|
</IconizedContextMenuOption>
|
|
)}
|
|
<IconizedContextMenuOption
|
|
label={_t("action|add_existing_room")}
|
|
iconClassName="mx_RoomList_iconAddExistingRoom"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenu();
|
|
showAddExistingRooms(activeSpace);
|
|
}}
|
|
disabled={!canAddRooms}
|
|
title={canAddRooms ? undefined : _t("spaces|error_no_permission_add_room")}
|
|
/>
|
|
</>
|
|
) : null}
|
|
</IconizedContextMenuOptionList>
|
|
);
|
|
} else if (menuDisplayed) {
|
|
contextMenuContent = (
|
|
<IconizedContextMenuOptionList first>
|
|
{showCreateRoom && (
|
|
<>
|
|
<IconizedContextMenuOption
|
|
label={_t("action|new_room")}
|
|
iconClassName="mx_RoomList_iconNewRoom"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenu();
|
|
defaultDispatcher.dispatch({ action: "view_create_room" });
|
|
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuCreateRoomItem", e);
|
|
}}
|
|
/>
|
|
{videoRoomsEnabled && (
|
|
<IconizedContextMenuOption
|
|
label={_t("action|new_video_room")}
|
|
iconClassName="mx_RoomList_iconNewVideoRoom"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenu();
|
|
defaultDispatcher.dispatch({
|
|
action: "view_create_room",
|
|
type: elementCallVideoRoomsEnabled
|
|
? RoomType.UnstableCall
|
|
: RoomType.ElementVideo,
|
|
});
|
|
}}
|
|
>
|
|
<BetaPill />
|
|
</IconizedContextMenuOption>
|
|
)}
|
|
</>
|
|
)}
|
|
{showExploreRooms ? (
|
|
<IconizedContextMenuOption
|
|
label={_t("action|explore_public_rooms")}
|
|
iconClassName="mx_RoomList_iconExplore"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
closeMenu();
|
|
PosthogTrackers.trackInteraction("WebRoomListRoomsSublistPlusMenuExploreRoomsItem", e);
|
|
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
|
}}
|
|
/>
|
|
) : null}
|
|
</IconizedContextMenuOptionList>
|
|
);
|
|
}
|
|
|
|
let contextMenu: JSX.Element | null = null;
|
|
if (menuDisplayed && handle.current) {
|
|
contextMenu = (
|
|
<IconizedContextMenu {...auxButtonContextMenuPosition(handle.current)} onFinished={closeMenu} compact>
|
|
{contextMenuContent}
|
|
</IconizedContextMenu>
|
|
);
|
|
}
|
|
|
|
if (showCreateRoom || showExploreRooms) {
|
|
return (
|
|
<>
|
|
<ContextMenuTooltipButton
|
|
tabIndex={tabIndex}
|
|
onClick={openMenu}
|
|
className="mx_RoomSublist_auxButton"
|
|
aria-label={_t("room_list|add_room_label")}
|
|
title={_t("room_list|add_room_label")}
|
|
isExpanded={menuDisplayed}
|
|
ref={handle}
|
|
/>
|
|
|
|
{contextMenu}
|
|
</>
|
|
);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const TAG_AESTHETICS: TagAestheticsMap = {
|
|
[DefaultTagID.Invite]: {
|
|
sectionLabel: _td("action|invites_list"),
|
|
isInvite: true,
|
|
defaultHidden: false,
|
|
},
|
|
[DefaultTagID.Favourite]: {
|
|
sectionLabel: _td("common|favourites"),
|
|
isInvite: false,
|
|
defaultHidden: false,
|
|
},
|
|
[DefaultTagID.DM]: {
|
|
sectionLabel: _td("common|people"),
|
|
isInvite: false,
|
|
defaultHidden: false,
|
|
AuxButtonComponent: DmAuxButton,
|
|
},
|
|
[DefaultTagID.Conference]: {
|
|
sectionLabel: _td("voip|metaspace_video_rooms|conference_room_section"),
|
|
isInvite: false,
|
|
defaultHidden: false,
|
|
},
|
|
[DefaultTagID.Untagged]: {
|
|
sectionLabel: _td("common|rooms"),
|
|
isInvite: false,
|
|
defaultHidden: false,
|
|
AuxButtonComponent: UntaggedAuxButton,
|
|
},
|
|
[DefaultTagID.LowPriority]: {
|
|
sectionLabel: _td("common|low_priority"),
|
|
isInvite: false,
|
|
defaultHidden: false,
|
|
},
|
|
[DefaultTagID.ServerNotice]: {
|
|
sectionLabel: _td("common|system_alerts"),
|
|
isInvite: false,
|
|
defaultHidden: false,
|
|
},
|
|
|
|
[DefaultTagID.Suggested]: {
|
|
sectionLabel: _td("room_list|suggested_rooms_heading"),
|
|
isInvite: false,
|
|
defaultHidden: false,
|
|
},
|
|
};
|
|
|
|
export default class RoomList extends React.PureComponent<IProps, IState> {
|
|
private dispatcherRef?: string;
|
|
private treeRef = createRef<HTMLDivElement>();
|
|
|
|
public static contextType = MatrixClientContext;
|
|
declare public context: React.ContextType<typeof MatrixClientContext>;
|
|
|
|
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
|
|
super(props, context);
|
|
|
|
this.state = {
|
|
sublists: {},
|
|
suggestedRooms: SpaceStore.instance.suggestedRooms,
|
|
};
|
|
}
|
|
|
|
public componentDidMount(): void {
|
|
this.dispatcherRef = defaultDispatcher.register(this.onAction);
|
|
SdkContextClass.instance.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
|
SpaceStore.instance.on(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
|
|
this.updateLists(); // trigger the first update
|
|
}
|
|
|
|
public componentWillUnmount(): void {
|
|
SpaceStore.instance.off(UPDATE_SUGGESTED_ROOMS, this.updateSuggestedRooms);
|
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
|
|
defaultDispatcher.unregister(this.dispatcherRef);
|
|
SdkContextClass.instance.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
|
}
|
|
|
|
private onRoomViewStoreUpdate = (): void => {
|
|
this.setState({
|
|
currentRoomId: SdkContextClass.instance.roomViewStore.getRoomId() ?? undefined,
|
|
});
|
|
};
|
|
|
|
private onAction = (payload: ActionPayload): void => {
|
|
if (payload.action === Action.ViewRoomDelta) {
|
|
const viewRoomDeltaPayload = payload as ViewRoomDeltaPayload;
|
|
const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
|
|
if (!currentRoomId) return;
|
|
const room = this.getRoomDelta(currentRoomId, viewRoomDeltaPayload.delta, viewRoomDeltaPayload.unread);
|
|
if (room) {
|
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
|
action: Action.ViewRoom,
|
|
room_id: room.roomId,
|
|
show_room_tile: true, // to make sure the room gets scrolled into view
|
|
metricsTrigger: "WebKeyboardShortcut",
|
|
metricsViaKeyboard: true,
|
|
});
|
|
}
|
|
} else if (payload.action === Action.PstnSupportUpdated) {
|
|
this.updateLists();
|
|
}
|
|
};
|
|
|
|
private getRoomDelta = (roomId: string, delta: number, unread = false): Room => {
|
|
const lists = RoomListStore.instance.orderedLists;
|
|
const rooms: Room[] = [];
|
|
TAG_ORDER.forEach((t) => {
|
|
let listRooms = lists[t];
|
|
|
|
if (unread) {
|
|
// filter to only notification rooms (and our current active room so we can index properly)
|
|
listRooms = listRooms.filter((r) => {
|
|
const state = RoomNotificationStateStore.instance.getRoomState(r);
|
|
return state.room.roomId === roomId || state.isUnread;
|
|
});
|
|
}
|
|
|
|
rooms.push(...listRooms);
|
|
});
|
|
|
|
const currentIndex = rooms.findIndex((r) => r.roomId === roomId);
|
|
// use slice to account for looping around the start
|
|
const [room] = rooms.slice((currentIndex + delta) % rooms.length);
|
|
return room;
|
|
};
|
|
|
|
private updateSuggestedRooms = (suggestedRooms: ISuggestedRoom[]): void => {
|
|
this.setState({ suggestedRooms });
|
|
};
|
|
|
|
private updateLists = (): void => {
|
|
const newLists = RoomListStore.instance.orderedLists;
|
|
const previousListIds = Object.keys(this.state.sublists);
|
|
const newListIds = Object.keys(newLists);
|
|
|
|
let doUpdate = arrayHasDiff(previousListIds, newListIds);
|
|
if (!doUpdate) {
|
|
// so we didn't have the visible sublists change, but did the contents of those
|
|
// sublists change significantly enough to break the sticky headers? Probably, so
|
|
// let's check the length of each.
|
|
for (const tagId of newListIds) {
|
|
const oldRooms = this.state.sublists[tagId];
|
|
const newRooms = newLists[tagId];
|
|
if (oldRooms.length !== newRooms.length) {
|
|
doUpdate = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (doUpdate) {
|
|
// We have to break our reference to the room list store if we want to be able to
|
|
// diff the object for changes, so do that.
|
|
// @ts-ignore - ITagMap is ts-ignored so this will have to be too
|
|
const newSublists = objectWithOnly(newLists, newListIds);
|
|
const sublists = objectShallowClone(newSublists, (k, v) => arrayFastClone(v));
|
|
|
|
this.setState({ sublists }, () => {
|
|
this.props.onResize();
|
|
});
|
|
}
|
|
};
|
|
|
|
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
|
|
return this.state.suggestedRooms.map((room) => {
|
|
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("empty_room");
|
|
const avatar = (
|
|
<RoomAvatar
|
|
oobData={{
|
|
name,
|
|
avatarUrl: room.avatar_url,
|
|
}}
|
|
size="32px"
|
|
/>
|
|
);
|
|
const viewRoom = (ev: SyntheticEvent): void => {
|
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
|
action: Action.ViewRoom,
|
|
room_alias: room.canonical_alias || room.aliases?.[0],
|
|
room_id: room.room_id,
|
|
via_servers: room.viaServers,
|
|
oob_data: {
|
|
avatarUrl: room.avatar_url,
|
|
name,
|
|
},
|
|
metricsTrigger: "RoomList",
|
|
metricsViaKeyboard: ev.type !== "click",
|
|
});
|
|
};
|
|
return (
|
|
<ExtraTile
|
|
isMinimized={this.props.isMinimized}
|
|
isSelected={this.state.currentRoomId === room.room_id}
|
|
displayName={name}
|
|
avatar={avatar}
|
|
onClick={viewRoom}
|
|
key={`suggestedRoomTile_${room.room_id}`}
|
|
/>
|
|
);
|
|
});
|
|
}
|
|
|
|
private renderSublists(): React.ReactElement[] {
|
|
// show a skeleton UI if the user is in no rooms and they are not filtering and have no suggested rooms
|
|
const showSkeleton =
|
|
!this.state.suggestedRooms?.length &&
|
|
Object.values(RoomListStore.instance.orderedLists).every((list) => !list?.length);
|
|
|
|
return TAG_ORDER.map((orderedTagId) => {
|
|
let extraTiles: ReactComponentElement<typeof ExtraTile>[] | undefined;
|
|
if (orderedTagId === DefaultTagID.Suggested) {
|
|
extraTiles = this.renderSuggestedRooms();
|
|
}
|
|
|
|
const aesthetics = TAG_AESTHETICS[orderedTagId];
|
|
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
|
|
|
|
let alwaysVisible = ALWAYS_VISIBLE_TAGS.includes(orderedTagId);
|
|
if (
|
|
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId !== DefaultTagID.Favourite) ||
|
|
(this.props.activeSpace === MetaSpace.People && orderedTagId !== DefaultTagID.DM) ||
|
|
(this.props.activeSpace === MetaSpace.Orphans && orderedTagId === DefaultTagID.DM) ||
|
|
(this.props.activeSpace === MetaSpace.VideoRooms && orderedTagId === DefaultTagID.DM) ||
|
|
(!isMetaSpace(this.props.activeSpace) &&
|
|
orderedTagId === DefaultTagID.DM &&
|
|
!SettingsStore.getValue("Spaces.showPeopleInSpace", this.props.activeSpace))
|
|
) {
|
|
alwaysVisible = false;
|
|
}
|
|
|
|
let forceExpanded = false;
|
|
if (
|
|
(this.props.activeSpace === MetaSpace.Favourites && orderedTagId === DefaultTagID.Favourite) ||
|
|
(this.props.activeSpace === MetaSpace.People && orderedTagId === DefaultTagID.DM)
|
|
) {
|
|
forceExpanded = true;
|
|
}
|
|
// The cost of mounting/unmounting this component offsets the cost
|
|
// of keeping it in the DOM and hiding it when it is not required
|
|
return (
|
|
<RoomSublist
|
|
key={`sublist-${orderedTagId}`}
|
|
tagId={orderedTagId}
|
|
forRooms={true}
|
|
startAsHidden={aesthetics.defaultHidden}
|
|
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
|
|
AuxButtonComponent={aesthetics.AuxButtonComponent}
|
|
isMinimized={this.props.isMinimized}
|
|
showSkeleton={showSkeleton}
|
|
extraTiles={extraTiles}
|
|
resizeNotifier={this.props.resizeNotifier}
|
|
alwaysVisible={alwaysVisible}
|
|
onListCollapse={this.props.onListCollapse}
|
|
forceExpanded={forceExpanded}
|
|
/>
|
|
);
|
|
});
|
|
}
|
|
|
|
public focus(): void {
|
|
// focus the first focusable element in this aria treeview widget
|
|
const treeItems = this.treeRef.current?.querySelectorAll<HTMLElement>('[role="treeitem"]');
|
|
if (!treeItems) return;
|
|
[...treeItems].find((e) => e.offsetParent !== null)?.focus();
|
|
}
|
|
|
|
public render(): React.ReactNode {
|
|
const sublists = this.renderSublists();
|
|
return (
|
|
<RovingTabIndexProvider handleHomeEnd handleUpDown onKeyDown={this.props.onKeyDown}>
|
|
{({ onKeyDownHandler }) => (
|
|
<div
|
|
onFocus={this.props.onFocus}
|
|
onBlur={this.props.onBlur}
|
|
onKeyDown={(ev) => {
|
|
const navAction = getKeyBindingsManager().getNavigationAction(ev);
|
|
if (
|
|
navAction === KeyBindingAction.NextLandmark ||
|
|
navAction === KeyBindingAction.PreviousLandmark
|
|
) {
|
|
LandmarkNavigation.findAndFocusNextLandmark(
|
|
Landmark.ROOM_LIST,
|
|
navAction === KeyBindingAction.PreviousLandmark,
|
|
);
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
return;
|
|
}
|
|
onKeyDownHandler(ev);
|
|
}}
|
|
className="mx_RoomList"
|
|
role="tree"
|
|
aria-label={_t("common|rooms")}
|
|
ref={this.treeRef}
|
|
>
|
|
{sublists}
|
|
</div>
|
|
)}
|
|
</RovingTabIndexProvider>
|
|
);
|
|
}
|
|
}
|