RoomList: move room list header to shared components (#31675)

* chore: ignore jest-sonar.xml in gitconfig

* chore: add missing rtl types to shared component

* chore: add `symbol` to `Disposables.trackListener`

* feat: add room list header view to shared components

* fix: change `Space Settings` to `Space settings`

* feat: add room list header view model

* chore: remove old room list header

* chore: update i18n

* test: fix Room-test

* test: update playwright screenshot

* fix: remove extra margin at the top of Sort title in room options

* test: fix room status bar test

* fix: change for correct copyright

* refactor: use `Disposables#track` instead of manually disposing the listener

* refactor: avoid to recompute all the snapshot of `RoomListHeaderViewModel`

* wip

* fix: make header buttons the same size than figma

* test: update shared component snapshots

* test: update shared component screenshots

* test: update EW screenshots
This commit is contained in:
Florian Duros
2026-01-21 10:06:01 +01:00
committed by GitHub
parent d2d61a3203
commit 9edddce149
55 changed files with 1831 additions and 1688 deletions

View File

@@ -1,224 +0,0 @@
/*
* Copyright 2025 New Vector 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 { useCallback } from "react";
import { JoinRule, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import PosthogTrackers from "../../../PosthogTrackers";
import { Action } from "../../../dispatcher/actions";
import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
import {
getMetaSpaceName,
type MetaSpace,
type SpaceKey,
UPDATE_HOME_BEHAVIOUR,
UPDATE_SELECTED_SPACE,
} from "../../../stores/spaces";
import SpaceStore from "../../../stores/spaces/SpaceStore";
import {
shouldShowSpaceSettings,
showCreateNewRoom,
showSpaceInvite,
showSpacePreferences,
showSpaceSettings,
} from "../../../utils/space";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { createRoom, hasCreateRoomRights } from "./utils";
import { type SortOption, useSorter } from "./useSorter";
/**
* Hook to get the active space and its title.
*/
function useSpace(): { activeSpace: Room | null; title: string } {
const [spaceKey, activeSpace] = useEventEmitterState<[SpaceKey, Room | null]>(
SpaceStore.instance,
UPDATE_SELECTED_SPACE,
() => [SpaceStore.instance.activeSpace, SpaceStore.instance.activeSpaceRoom],
);
const spaceName = useTypedEventEmitterState(activeSpace ?? undefined, RoomEvent.Name, () => activeSpace?.name);
const allRoomsInHome = useEventEmitterState(
SpaceStore.instance,
UPDATE_HOME_BEHAVIOUR,
() => SpaceStore.instance.allRoomsInHome,
);
const title = spaceName ?? getMetaSpaceName(spaceKey as MetaSpace, allRoomsInHome);
return {
activeSpace,
title,
};
}
export interface RoomListHeaderViewState {
/**
* The title of the room list
*/
title: string;
/**
* Whether to display the compose menu
* True if the user can create rooms
*/
displayComposeMenu: boolean;
/**
* Whether to display the space menu
* True if there is an active space
*/
displaySpaceMenu: boolean;
/**
* Whether the user can create rooms
*/
canCreateRoom: boolean;
/**
* Whether the user can create video rooms
*/
canCreateVideoRoom: boolean;
/**
* Whether the user can invite in the active space
*/
canInviteInSpace: boolean;
/**
* Whether the user can access space settings
*/
canAccessSpaceSettings: boolean;
/**
* Create a chat room
* @param e - The click event
*/
createChatRoom: (e: Event) => void;
/**
* Create a room
* @param e - The click event
*/
createRoom: (e: Event) => void;
/**
* Create a video room
*/
createVideoRoom: () => void;
/**
* Open the active space home
*/
openSpaceHome: () => void;
/**
* Display the space invite dialog
*/
inviteInSpace: () => void;
/**
* Open the space preferences
*/
openSpacePreferences: () => void;
/**
* Open the space settings
*/
openSpaceSettings: () => void;
/**
* Change the sort order of the room-list.
*/
sort: (option: SortOption) => void;
/**
* The currently active sort option.
*/
activeSortOption: SortOption;
}
/**
* View model for the RoomListHeader.
*/
export function useRoomListHeaderViewModel(): RoomListHeaderViewState {
const matrixClient = useMatrixClientContext();
const { activeSpace, title } = useSpace();
const isSpaceRoom = Boolean(activeSpace);
const canCreateRoom = hasCreateRoomRights(matrixClient, activeSpace);
const canCreateVideoRoom = useFeatureEnabled("feature_video_rooms") && canCreateRoom;
const displayComposeMenu = canCreateRoom;
const displaySpaceMenu = isSpaceRoom;
const canInviteInSpace = Boolean(
activeSpace?.getJoinRule() === JoinRule.Public || activeSpace?.canInvite(matrixClient.getSafeUserId()),
);
const canAccessSpaceSettings = Boolean(activeSpace && shouldShowSpaceSettings(activeSpace));
/* Actions */
const { activeSortOption, sort } = useSorter();
const createChatRoom = useCallback((e: Event) => {
defaultDispatcher.fire(Action.CreateChat);
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
}, []);
const createRoomMemoized = useCallback(
(e: Event) => {
createRoom(activeSpace);
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
},
[activeSpace],
);
const elementCallVideoRoomsEnabled = useFeatureEnabled("feature_element_call_video_rooms");
const createVideoRoom = useCallback(() => {
const type = elementCallVideoRoomsEnabled ? RoomType.UnstableCall : RoomType.ElementVideo;
if (activeSpace) {
showCreateNewRoom(activeSpace, type);
} else {
defaultDispatcher.dispatch({
action: Action.CreateRoom,
type,
});
}
}, [activeSpace, elementCallVideoRoomsEnabled]);
const openSpaceHome = useCallback(() => {
// openSpaceHome is only available when there is an active space
if (!activeSpace) return;
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: activeSpace.roomId,
metricsTrigger: undefined,
});
}, [activeSpace]);
const inviteInSpace = useCallback(() => {
// inviteInSpace is only available when there is an active space
if (!activeSpace) return;
showSpaceInvite(activeSpace);
}, [activeSpace]);
const openSpacePreferences = useCallback(() => {
// openSpacePreferences is only available when there is an active space
if (!activeSpace) return;
showSpacePreferences(activeSpace);
}, [activeSpace]);
const openSpaceSettings = useCallback(() => {
// openSpaceSettings is only available when there is an active space
if (!activeSpace) return;
showSpaceSettings(activeSpace);
}, [activeSpace]);
return {
title,
displayComposeMenu,
displaySpaceMenu,
canCreateRoom,
canCreateVideoRoom,
canInviteInSpace,
canAccessSpaceSettings,
createChatRoom,
createRoom: createRoomMemoized,
createVideoRoom,
openSpaceHome,
inviteInSpace,
openSpacePreferences,
openSpaceSettings,
activeSortOption,
sort,
};
}

View File

@@ -1,62 +0,0 @@
/*
Copyright 2025 New Vector 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 { useState } from "react";
import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3";
import { SortingAlgorithm } from "../../../stores/room-list-v3/skip-list/sorters";
import SettingsStore from "../../../settings/SettingsStore";
/**
* Sorting options made available to the view.
*/
export const enum SortOption {
Activity = SortingAlgorithm.Recency,
AToZ = SortingAlgorithm.Alphabetic,
}
/**
* {@link SortOption} holds almost the same information as
* {@link SortingAlgorithm}. This is done intentionally to
* prevent the view from having a dependence on the
* model (which is the store in this case).
*/
const sortingAlgorithmToSortingOption = {
[SortingAlgorithm.Alphabetic]: SortOption.AToZ,
[SortingAlgorithm.Recency]: SortOption.Activity,
};
const sortOptionToSortingAlgorithm = {
[SortOption.AToZ]: SortingAlgorithm.Alphabetic,
[SortOption.Activity]: SortingAlgorithm.Recency,
};
interface SortState {
sort: (option: SortOption) => void;
activeSortOption: SortOption;
}
/**
* This hook does two things:
* - Provides a way to track the currently active sort option.
* - Provides a function to resort the room list.
*/
export function useSorter(): SortState {
const [activeSortingAlgorithm, setActiveSortingAlgorithm] = useState(() =>
SettingsStore.getValue("RoomList.preferredSorting"),
);
const sort = (option: SortOption): void => {
const sortingAlgorithm = sortOptionToSortingAlgorithm[option];
RoomListStoreV3.instance.resort(sortingAlgorithm);
setActiveSortingAlgorithm(sortingAlgorithm);
};
return {
sort,
activeSortOption: sortingAlgorithmToSortingOption[activeSortingAlgorithm!],
};
}

View File

@@ -1,169 +0,0 @@
/*
* Copyright 2025 New Vector 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, useState } from "react";
import { IconButton, Menu, MenuItem } from "@vector-im/compound-web";
import ComposeIcon from "@vector-im/compound-design-tokens/assets/web/icons/compose";
import UserAddIcon from "@vector-im/compound-design-tokens/assets/web/icons/user-add";
import ChevronDownIcon from "@vector-im/compound-design-tokens/assets/web/icons/chevron-down";
import RoomIcon from "@vector-im/compound-design-tokens/assets/web/icons/room";
import HomeIcon from "@vector-im/compound-design-tokens/assets/web/icons/home";
import PreferencesIcon from "@vector-im/compound-design-tokens/assets/web/icons/preferences";
import SettingsIcon from "@vector-im/compound-design-tokens/assets/web/icons/settings";
import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call";
import ChatIcon from "@vector-im/compound-design-tokens/assets/web/icons/chat";
import { Flex } from "@element-hq/web-shared-components";
import { _t } from "../../../../languageHandler";
import {
type RoomListHeaderViewState,
useRoomListHeaderViewModel,
} from "../../../viewmodels/roomlist/RoomListHeaderViewModel";
import { RoomListOptionsMenu } from "./RoomListOptionsMenu";
/**
* The header view for the room list
* The space name is displayed and a compose menu is shown if the user can create rooms
*/
export function RoomListHeaderView(): JSX.Element {
const vm = useRoomListHeaderViewModel();
return (
<Flex
as="header"
className="mx_RoomListHeaderView"
aria-label={_t("room|context_menu|title")}
justify="space-between"
align="center"
data-testid="room-list-header"
>
<Flex className="mx_RoomListHeaderView_title" align="center" gap="var(--cpd-space-1x)">
<h1 title={vm.title}>{vm.title}</h1>
{vm.displaySpaceMenu && <SpaceMenu vm={vm} />}
</Flex>
<Flex align="center" gap="var(--cpd-space-2x)">
<div className="mx_RoomListHeaderView_ReleaseAnnouncementAnchor">
<RoomListOptionsMenu vm={vm} />
</div>
{/* If we don't display the compose menu, it means that the user can only send DM */}
<div className="mx_RoomListHeaderView_ReleaseAnnouncementAnchor">
{vm.displayComposeMenu ? (
<ComposeMenu vm={vm} />
) : (
<IconButton
onClick={(e) => vm.createChatRoom(e.nativeEvent)}
tooltip={_t("action|new_conversation")}
>
<ComposeIcon color="var(--cpd-color-icon-secondary)" aria-hidden />
</IconButton>
)}
</div>
</Flex>
</Flex>
);
}
interface SpaceMenuProps {
/**
* The view model for the room list header
*/
vm: RoomListHeaderViewState;
}
/**
* The space menu for the room list header
*/
function SpaceMenu({ vm }: SpaceMenuProps): JSX.Element {
const [open, setOpen] = useState(false);
return (
<Menu
open={open}
onOpenChange={setOpen}
title={vm.title}
side="right"
align="start"
trigger={
<IconButton className="mx_SpaceMenu_button" aria-label={_t("room_list|open_space_menu")} size="20px">
<ChevronDownIcon color="var(--cpd-color-icon-secondary)" />
</IconButton>
}
>
<MenuItem
Icon={HomeIcon}
label={_t("room_list|space_menu|home")}
onSelect={vm.openSpaceHome}
hideChevron={true}
/>
{vm.canInviteInSpace && (
<MenuItem
Icon={UserAddIcon}
label={_t("action|invite")}
onSelect={vm.inviteInSpace}
hideChevron={true}
/>
)}
<MenuItem
Icon={PreferencesIcon}
label={_t("common|preferences")}
onSelect={vm.openSpacePreferences}
hideChevron={true}
/>
{vm.canAccessSpaceSettings && (
<MenuItem
Icon={SettingsIcon}
label={_t("room_list|space_menu|space_settings")}
onSelect={vm.openSpaceSettings}
hideChevron={true}
/>
)}
</Menu>
);
}
interface ComposeMenuProps {
/**
* The view model for the room list header
*/
vm: RoomListHeaderViewState;
}
/**
* The compose menu for the room list header
*/
function ComposeMenu({ vm }: ComposeMenuProps): JSX.Element {
const [open, setOpen] = useState(false);
return (
<Menu
open={open}
onOpenChange={setOpen}
showTitle={false}
title={_t("action|open_menu")}
side="right"
align="start"
trigger={
<IconButton tooltip={_t("action|new_conversation")}>
<ComposeIcon color="var(--cpd-color-icon-secondary)" aria-hidden />
</IconButton>
}
>
<MenuItem Icon={ChatIcon} label={_t("action|start_chat")} onSelect={vm.createChatRoom} hideChevron={true} />
{vm.canCreateRoom && (
<MenuItem Icon={RoomIcon} label={_t("action|new_room")} onSelect={vm.createRoom} hideChevron={true} />
)}
{vm.canCreateVideoRoom && (
<MenuItem
Icon={VideoCallIcon}
label={_t("action|new_video_room")}
onSelect={vm.createVideoRoom}
hideChevron={true}
/>
)}
</Menu>
);
}

View File

@@ -1,68 +0,0 @@
/*
* Copyright 2025 New Vector 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 { IconButton, Menu, MenuTitle, Tooltip, RadioMenuItem } from "@vector-im/compound-web";
import React, { type Ref, type JSX, useState, useCallback } from "react";
import OverflowHorizontalIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";
import { _t } from "../../../../languageHandler";
import { SortOption } from "../../../viewmodels/roomlist/useSorter";
import { type RoomListHeaderViewState } from "../../../viewmodels/roomlist/RoomListHeaderViewModel";
interface MenuTriggerProps extends React.ComponentProps<typeof IconButton> {
ref?: Ref<HTMLButtonElement>;
}
const MenuTrigger = ({ ref, ...props }: MenuTriggerProps): JSX.Element => (
<Tooltip label={_t("room_list|room_options")}>
<IconButton aria-label={_t("room_list|room_options")} {...props} ref={ref}>
<OverflowHorizontalIcon color="var(--cpd-color-icon-secondary)" />
</IconButton>
</Tooltip>
);
interface Props {
/**
* The view model for the room list view
*/
vm: RoomListHeaderViewState;
}
export function RoomListOptionsMenu({ vm }: Props): JSX.Element {
const [open, setOpen] = useState(false);
const onActivitySelected = useCallback(() => {
vm.sort(SortOption.Activity);
}, [vm]);
const onAtoZSelected = useCallback(() => {
vm.sort(SortOption.AToZ);
}, [vm]);
return (
<Menu
open={open}
onOpenChange={setOpen}
title={_t("room_list|room_options")}
showTitle={false}
align="start"
trigger={<MenuTrigger />}
>
<MenuTitle title={_t("room_list|sort")} />
<RadioMenuItem
label={_t("room_list|sort_type|activity")}
checked={vm.activeSortOption === SortOption.Activity}
onSelect={onActivitySelected}
/>
<RadioMenuItem
label={_t("room_list|sort_type|atoz")}
checked={vm.activeSortOption === SortOption.AToZ}
onSelect={onAtoZSelected}
/>
</Menu>
);
}

View File

@@ -6,18 +6,20 @@ Please see LICENSE files in the repository root for full details.
*/
import React, { useState, useCallback } from "react";
import { Flex } from "@element-hq/web-shared-components";
import { Flex, RoomListHeaderView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../../settings/UIFeature";
import { RoomListSearch } from "./RoomListSearch";
import { RoomListHeaderView } from "./RoomListHeaderView";
import { RoomListView } from "./RoomListView";
import { _t } from "../../../../languageHandler";
import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation";
import { type IState as IRovingTabIndexState } from "../../../../accessibility/RovingTabIndex";
import { RoomListHeaderViewModel } from "../../../../viewmodels/room-list/RoomListHeaderViewModel";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import SpaceStore from "../../../../stores/spaces/SpaceStore";
type RoomListPanelProps = {
/**
@@ -58,6 +60,11 @@ export const RoomListPanel: React.FC<RoomListPanelProps> = ({ activeSpace }) =>
[focusedElement],
);
const matrixClient = useMatrixClientContext();
const vm = useCreateAutoDisposedViewModel(
() => new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }),
);
return (
<Flex
as="nav"
@@ -70,7 +77,7 @@ export const RoomListPanel: React.FC<RoomListPanelProps> = ({ activeSpace }) =>
onKeyDown={onKeyDown}
>
{displayRoomSearch && <RoomListSearch activeSpace={activeSpace} />}
<RoomListHeaderView />
<RoomListHeaderView vm={vm} />
<RoomListView />
</Flex>
);

View File

@@ -93,14 +93,12 @@
"maximise": "Maximise",
"mention": "Mention",
"minimise": "Minimise",
"new_conversation": "New conversation",
"new_room": "New room",
"new_video_room": "New video room",
"next": "Next",
"no": "No",
"ok": "OK",
"open": "Open",
"open_menu": "Open menu",
"pin": "Pin",
"proceed": "Proceed",
"quote": "Quote",
@@ -2202,7 +2200,6 @@
"mark_unread": "Mark as unread"
},
"notification_options": "Notification options",
"open_space_menu": "Open space menu",
"primary_filters": "Room list filters",
"redacting_messages_status": {
"one": "Currently removing messages in %(count)s room",
@@ -2212,26 +2209,16 @@
"more_options": "More Options",
"open_room": "Open room %(roomName)s"
},
"room_options": "Room Options",
"show_less": "Show less",
"show_n_more": {
"one": "Show %(count)s more",
"other": "Show %(count)s more"
},
"show_previews": "Show previews of messages",
"sort": "Sort",
"sort_by": "Sort by",
"sort_by_activity": "Activity",
"sort_by_alphabet": "A-Z",
"sort_type": {
"activity": "Activity",
"atoz": "A-Z"
},
"sort_unread_first": "Show rooms with unread messages first",
"space_menu": {
"home": "Space home",
"space_settings": "Space Settings"
},
"space_menu_label": "%(spaceName)s menu",
"sublist_options": "List options",
"suggested_rooms_heading": "Suggested Rooms"

View File

@@ -0,0 +1,241 @@
/*
* Copyright 2025 New Vector 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 { JoinRule, type MatrixClient, type Room, RoomEvent, RoomType } from "matrix-js-sdk/src/matrix";
import {
BaseViewModel,
type RoomListHeaderViewSnapshot,
type RoomListHeaderViewModel as RoomListHeaderViewModelInterface,
type SortOption,
} from "@element-hq/web-shared-components";
import defaultDispatcher from "../../dispatcher/dispatcher";
import PosthogTrackers from "../../PosthogTrackers";
import { Action } from "../../dispatcher/actions";
import { getMetaSpaceName, type MetaSpace, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import { type SpaceStoreClass } from "../../stores/spaces/SpaceStore";
import {
shouldShowSpaceSettings,
showCreateNewRoom,
showSpaceInvite,
showSpacePreferences,
showSpaceSettings,
} from "../../utils/space";
import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { createRoom, hasCreateRoomRights } from "../../components/viewmodels/roomlist/utils";
import SettingsStore from "../../settings/SettingsStore";
import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3";
import { SortingAlgorithm } from "../../stores/room-list-v3/skip-list/sorters";
export interface Props {
/**
* The Matrix client instance.
*/
matrixClient: MatrixClient;
spaceStore: SpaceStoreClass;
}
/**
* ViewModel for the RoomListHeader.
* Manages the state and actions for the room list header.
*/
export class RoomListHeaderViewModel
extends BaseViewModel<RoomListHeaderViewSnapshot, Props>
implements RoomListHeaderViewModelInterface
{
/**
* Reference to the currently active space.
* Used to manage event listeners.
*/
private activeSpace: Room | null;
public constructor(props: Props) {
super(props, getInitialSnapshot(props.spaceStore, props.matrixClient));
// Listen for video rooms feature flag changes
const settingsFeatureVideoRef = SettingsStore.watchSetting(
"feature_video_rooms",
null,
this.onVideoRoomsFeatureFlagChange,
);
this.disposables.track(() => SettingsStore.unwatchSetting(settingsFeatureVideoRef));
// Listen for space changes
this.disposables.trackListener(props.spaceStore, UPDATE_SELECTED_SPACE, this.onSpaceChange);
this.disposables.trackListener(props.spaceStore, UPDATE_HOME_BEHAVIOUR, this.onHomeBehaviourChange);
// Listen for space name changes
this.activeSpace = props.spaceStore.activeSpaceRoom;
if (this.activeSpace) {
this.disposables.trackListener(this.activeSpace, RoomEvent.Name, this.onSpaceNameChange);
}
}
/**
* Handles space change events.
*/
private readonly onSpaceChange = (): void => {
const activeSpace = this.props.spaceStore.activeSpaceRoom;
this.activeSpace?.off(RoomEvent.Name, this.onSpaceNameChange);
this.activeSpace = activeSpace;
// Add new room listener if needed
if (this.activeSpace) {
this.disposables.trackListener(this.activeSpace, RoomEvent.Name, this.onSpaceNameChange);
}
this.snapshot.merge({
...computeHeaderSpaceState(this.props.spaceStore, this.props.matrixClient),
});
};
/**
* Handles home behaviour change events.
*/
private readonly onHomeBehaviourChange = (): void => {
this.snapshot.merge({ title: getHeaderTitle(this.props.spaceStore) });
};
/**
* Handles space name change events.
*/
private onSpaceNameChange = (): void => {
this.snapshot.merge({ title: getHeaderTitle(this.props.spaceStore) });
};
/**
* Handles video rooms feature flag change events.
*/
private readonly onVideoRoomsFeatureFlagChange = (): void => {
this.snapshot.merge({
canCreateVideoRoom: getCanCreateVideoRoom(this.snapshot.current.canCreateRoom),
});
};
public createChatRoom = (e: Event): void => {
defaultDispatcher.fire(Action.CreateChat);
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateChatItem", e);
};
public createRoom = (e: Event): void => {
createRoom(this.activeSpace);
PosthogTrackers.trackInteraction("WebRoomListHeaderPlusMenuCreateRoomItem", e);
};
public createVideoRoom = (): void => {
const type = SettingsStore.getValue("feature_element_call_video_rooms")
? RoomType.UnstableCall
: RoomType.ElementVideo;
if (this.activeSpace) {
showCreateNewRoom(this.activeSpace, type);
} else {
defaultDispatcher.dispatch({
action: Action.CreateRoom,
type,
});
}
};
public openSpaceHome = (): void => {
if (!this.activeSpace) return;
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: this.activeSpace.roomId,
metricsTrigger: undefined,
});
};
public inviteInSpace = (): void => {
if (!this.activeSpace) return;
showSpaceInvite(this.activeSpace);
};
public openSpacePreferences = (): void => {
if (!this.activeSpace) return;
showSpacePreferences(this.activeSpace);
};
public openSpaceSettings = (): void => {
if (!this.activeSpace) return;
showSpaceSettings(this.activeSpace);
};
public sort = (option: SortOption): void => {
const sortingAlgorithm = option === "recent" ? SortingAlgorithm.Recency : SortingAlgorithm.Alphabetic;
RoomListStoreV3.instance.resort(sortingAlgorithm);
this.snapshot.merge({ activeSortOption: option });
};
}
/**
* Get the initial snapshot for the RoomListHeaderViewModel.
* @param spaceStore - The space store instance.
* @param matrixClient - The Matrix client instance.
* @returns
*/
function getInitialSnapshot(spaceStore: SpaceStoreClass, matrixClient: MatrixClient): RoomListHeaderViewSnapshot {
const sortingAlgorithm = SettingsStore.getValue("RoomList.preferredSorting");
const activeSortOption =
sortingAlgorithm === SortingAlgorithm.Recency ? ("recent" as const) : ("alphabetical" as const);
return {
activeSortOption,
...computeHeaderSpaceState(spaceStore, matrixClient),
};
}
/**
* Get the header title based on the active space.
* @param spaceStore - The space store instance.
*/
function getHeaderTitle(spaceStore: SpaceStoreClass): string {
const activeSpace = spaceStore.activeSpaceRoom;
const spaceName = activeSpace?.name;
return spaceName ?? getMetaSpaceName(spaceStore.activeSpace as MetaSpace, spaceStore.allRoomsInHome);
}
/**
* Determine if the user can create a video room.
* @param canCreateRoom - Whether the user can create a room.
*/
function getCanCreateVideoRoom(canCreateRoom: boolean): boolean {
return SettingsStore.getValue("feature_video_rooms") && canCreateRoom;
}
/**
* Computes the header space state based on the active space and user permissions.
* @param spaceStore - The space store instance.
* @param matrixClient - The Matrix client instance.
* @returns The header space state containing title, permissions, and display flags.
*/
function computeHeaderSpaceState(
spaceStore: SpaceStoreClass,
matrixClient: MatrixClient,
): Omit<RoomListHeaderViewSnapshot, "activeSortOption"> {
const activeSpace = spaceStore.activeSpaceRoom;
const title = getHeaderTitle(spaceStore);
const canCreateRoom = hasCreateRoomRights(matrixClient, activeSpace);
const canCreateVideoRoom = getCanCreateVideoRoom(canCreateRoom);
const displayComposeMenu = canCreateRoom;
const displaySpaceMenu = Boolean(activeSpace);
const canInviteInSpace = Boolean(
activeSpace?.getJoinRule() === JoinRule.Public || activeSpace?.canInvite(matrixClient.getSafeUserId()),
);
const canAccessSpaceSettings = Boolean(activeSpace && shouldShowSpaceSettings(activeSpace));
return {
title,
canCreateRoom,
canCreateVideoRoom,
displayComposeMenu,
displaySpaceMenu,
canInviteInSpace,
canAccessSpaceSettings,
};
}