* chore: add dnd kit deps * chore: patch dnd kit to fix ts error * feat(sc): add drag-and-drop to room list item and wrapper * feat(sc): make the room list header a droppable element * feat(sc): add dnd to room list view * feat(tags): can tag room as CHAT * feat(vm): implement `changeRoomSection` * feat(sc): disable dragging in flat list * fix: disable keyboard navigation when dragging element * test(sc): update snapshots * test(sc): add dnd test * test(e2e): add e2e tests for room drag and drop * test(vm): add tests for changeRoomSection * fix: remove focus visible when dropping with the mouse * test(playwright): update existing screenshots * chore(sc): move numbers out of main build The Ew RecorderWorklet imports shared component bundle. However if the bundle uses some deps using document/window which, the worklet will not work. The solution is to put the used functions into a separate bundle. * doc(sc): add subpath import into README * doc: typo barrel/bundle * test: improve test expect * refactor: add utils to section tag * fix: incorrect check in tagRoom * fix: add doc about dndkit tunning
129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
/*
|
|
* Copyright 2026 Element Creations Ltd.
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
* Please see LICENSE files in the repository root for full details.
|
|
*/
|
|
|
|
import React, { type JSX, type ReactNode } from "react";
|
|
|
|
import { useViewModel, type ViewModel } from "../../core/viewmodel";
|
|
import { RoomListPrimaryFilters, type FilterId } from "../RoomListPrimaryFilters";
|
|
import { RoomListLoadingSkeleton } from "./RoomListLoadingSkeleton";
|
|
import { RoomListEmptyStateView } from "./RoomListEmptyStateView";
|
|
import { VirtualizedRoomListView, type RoomListViewState } from "../VirtualizedRoomListView";
|
|
import { type Room, type RoomListItemViewModel } from "../VirtualizedRoomListView/RoomListItemWrapper/RoomListItemView";
|
|
import { type RoomListSectionHeaderViewModel } from "../VirtualizedRoomListView/RoomListSectionHeaderView";
|
|
import { type ToastType, RoomListToast } from "./RoomListToast";
|
|
import styles from "./RoomListView.module.css";
|
|
import { Flex } from "../../core/utils/Flex";
|
|
|
|
export type RoomListSection = {
|
|
/** Unique identifier for the section */
|
|
id: string;
|
|
/** Array of room IDs that belong to this section */
|
|
roomIds: string[];
|
|
};
|
|
|
|
/**
|
|
* Snapshot for the room list view
|
|
*/
|
|
export type RoomListViewSnapshot = {
|
|
/** Whether the rooms are currently loading */
|
|
isLoadingRooms: boolean;
|
|
/** Whether the room list is empty */
|
|
isRoomListEmpty: boolean;
|
|
/** Array of filter IDs */
|
|
filterIds: FilterId[];
|
|
/** Currently active filter ID (if any) */
|
|
activeFilterId?: FilterId;
|
|
/** Room list state */
|
|
roomListState: RoomListViewState;
|
|
/** Array of sections in the room list */
|
|
sections: RoomListSection[];
|
|
/** Optional description for the empty state */
|
|
emptyStateDescription?: string;
|
|
/** Optional action element for the empty state */
|
|
emptyStateAction?: ReactNode;
|
|
/** Whether the user can create rooms */
|
|
canCreateRoom?: boolean;
|
|
/** Whether the room list is displayed as a flat list */
|
|
isFlatList: boolean;
|
|
/** Optional toast to display */
|
|
toast?: ToastType;
|
|
};
|
|
|
|
/**
|
|
* Actions interface for room list operations
|
|
*/
|
|
export interface RoomListViewActions {
|
|
/** Called when a filter is toggled */
|
|
onToggleFilter: (filterId: FilterId) => void;
|
|
/** Called to create a new chat room */
|
|
createChatRoom: () => void;
|
|
/** Called to create a new room */
|
|
createRoom: () => void;
|
|
/**
|
|
* Get view model for a specific room (virtualization API)
|
|
* Allow undefined to be returned if we don't have a view model for the room. In this case the room will not be rendered.
|
|
*/
|
|
getRoomItemViewModel: (roomId: string) => RoomListItemViewModel | undefined;
|
|
/** Called when the visible range changes (virtualization API) */
|
|
updateVisibleRooms: (startIndex: number, endIndex: number) => void;
|
|
/** Get view model for a specific section header (virtualization API) */
|
|
getSectionHeaderViewModel: (sectionId: string) => RoomListSectionHeaderViewModel;
|
|
/** Called to close the toast message */
|
|
closeToast: () => void;
|
|
/** Called to change the section of a room */
|
|
changeRoomSection: (roomId: string, tag: string) => void;
|
|
}
|
|
|
|
/**
|
|
* The view model type for the room list view
|
|
*/
|
|
export type RoomListViewModel = ViewModel<RoomListViewSnapshot, RoomListViewActions>;
|
|
|
|
/**
|
|
* Props for RoomListView component
|
|
*/
|
|
export interface RoomListViewProps {
|
|
/** The view model containing all data and callbacks */
|
|
vm: RoomListViewModel;
|
|
/** Render function for room avatar */
|
|
renderAvatar: (room: Room) => ReactNode;
|
|
/** Optional callback for keyboard events on the room list */
|
|
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
|
|
}
|
|
|
|
/**
|
|
* Room list view component that manages filters, loading states, empty states, and the room list.
|
|
*/
|
|
export const RoomListView: React.FC<RoomListViewProps> = ({ vm, renderAvatar, onKeyDown }): JSX.Element => {
|
|
const snapshot = useViewModel(vm);
|
|
let listBody: ReactNode;
|
|
|
|
if (snapshot.isLoadingRooms) {
|
|
listBody = <RoomListLoadingSkeleton />;
|
|
} else if (snapshot.isRoomListEmpty) {
|
|
listBody = <RoomListEmptyStateView vm={vm} />;
|
|
} else {
|
|
listBody = <VirtualizedRoomListView vm={vm} renderAvatar={renderAvatar} onKeyDown={onKeyDown} />;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div>
|
|
<RoomListPrimaryFilters
|
|
filterIds={snapshot.filterIds}
|
|
activeFilterId={snapshot.activeFilterId}
|
|
onToggleFilter={vm.onToggleFilter}
|
|
/>
|
|
</div>
|
|
<Flex direction="column" className={styles.list}>
|
|
{listBody}
|
|
{snapshot.toast && <RoomListToast type={snapshot.toast} onClose={vm.closeToast} />}
|
|
</Flex>
|
|
</>
|
|
);
|
|
};
|