Add message preview support to the new room list (#29784)
* Add message preview support to the new room list * Support showing message previews in the room list items * Add the secondary filters bar with the '...' menu, containing just the option for message previews for now * Change message preview toggle hook to update when setting is updated * Use new compund release * Unused i18n keys * Unused imports * Fix test & update snapshot * Fix more snapshots * Fix test Split into two tests that test setting & updating * Type import * Snapshots * Remove unnecessary Flex container and update screenshots as the room list has got shorter from the added bar * More snapshots & screenshots * More snapshots * Add test and remove active filter that's not done yet * Update snapshots & screenshots again * Other screenshot * Add more tests * Fix syntax * Fix tests * Use setter directly Co-authored-by: Michael Telatynski <7t3chguy@gmail.com> * Fix CSS * Remopve filter button css for now * Update to remove forwardRef * Add comment on why lack of TypedEventEmitter * snapshots again * Screenshots again * Use original screenshots, maybe they'll work now * Add comment --------- Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
@@ -16,11 +16,14 @@ import { _t } from "../../../languageHandler";
|
||||
import { type RoomNotificationState } from "../../../stores/notifications/RoomNotificationState";
|
||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { useEventEmitter, useEventEmitterState, useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { DefaultTagID } from "../../../stores/room-list/models";
|
||||
import { useCall, useConnectionState, useParticipantCount } from "../../../hooks/useCall";
|
||||
import { type ConnectionState } from "../../../models/Call";
|
||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { MessagePreviewStore } from "../../../stores/room-list/MessagePreviewStore";
|
||||
import { useMessagePreviewToggle } from "./useMessagePreviewToggle";
|
||||
|
||||
export interface RoomListItemViewState {
|
||||
/**
|
||||
@@ -60,6 +63,11 @@ export interface RoomListItemViewState {
|
||||
* Whether there are participants in the call.
|
||||
*/
|
||||
hasParticipantInCall: boolean;
|
||||
/**
|
||||
* Pre-rendered and translated preview for the latest message in the room, or undefined
|
||||
* if no preview should be shown.
|
||||
*/
|
||||
messagePreview: string | undefined;
|
||||
/**
|
||||
* Whether the notification decoration should be shown.
|
||||
*/
|
||||
@@ -104,6 +112,8 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
!invited &&
|
||||
(hasAccessToOptionsMenu(room) || hasAccessToNotificationMenu(room, matrixClient.isGuest(), isArchived));
|
||||
|
||||
const messagePreview = useRoomMessagePreview(room);
|
||||
|
||||
// Video room
|
||||
const isVideoRoom = room.isElementVideoRoom() || room.isCallRoom();
|
||||
// EC video call or video room
|
||||
@@ -134,6 +144,7 @@ export function useRoomListItemViewModel(room: Room): RoomListItemViewState {
|
||||
isVideoRoom,
|
||||
callConnectionState,
|
||||
hasParticipantInCall,
|
||||
messagePreview,
|
||||
showNotificationDecoration,
|
||||
};
|
||||
}
|
||||
@@ -190,3 +201,36 @@ function getA11yLabel(roomName: string, notificationState: RoomNotificationState
|
||||
return _t("room_list|room|open_room", { roomName });
|
||||
}
|
||||
}
|
||||
|
||||
function useRoomMessagePreview(room: Room): string | undefined {
|
||||
const { shouldShowMessagePreview } = useMessagePreviewToggle();
|
||||
const [previewText, setPreviewText] = useState<string | undefined>(undefined);
|
||||
|
||||
const updatePreview = useCallback(async () => {
|
||||
if (!shouldShowMessagePreview) {
|
||||
setPreviewText(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const roomIsDM = Boolean(DMRoomMap.shared().getUserIdForRoomId(room.roomId));
|
||||
// For the tag, we only care about whether the room is a DM or not as we don't show
|
||||
// display names in previewsd for DMs, so anything else we just say is 'untagged'
|
||||
// (even though it could actually be have other tags: we don't care about them).
|
||||
const messagePreview = await MessagePreviewStore.instance.getPreviewForRoom(
|
||||
room,
|
||||
roomIsDM ? DefaultTagID.DM : DefaultTagID.Untagged,
|
||||
);
|
||||
if (messagePreview) setPreviewText(messagePreview.text);
|
||||
}, [room, shouldShowMessagePreview]);
|
||||
|
||||
// MessagePreviewStore and the other AsyncStores need to be converted to TypedEventEmitter
|
||||
useEventEmitter(MessagePreviewStore.instance, MessagePreviewStore.getPreviewChangedEventName(room), () => {
|
||||
updatePreview();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
updatePreview();
|
||||
}, [updatePreview]);
|
||||
|
||||
return previewText;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* 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, useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { useSettingValue } from "../../../hooks/useSettings";
|
||||
|
||||
interface MessagePreviewToggleState {
|
||||
shouldShowMessagePreview: boolean;
|
||||
@@ -20,17 +21,12 @@ interface MessagePreviewToggleState {
|
||||
* - Provides a function to toggle message previews.
|
||||
*/
|
||||
export function useMessagePreviewToggle(): MessagePreviewToggleState {
|
||||
const [shouldShowMessagePreview, setShouldShowMessagePreview] = useState(() =>
|
||||
SettingsStore.getValue("RoomList.showMessagePreview"),
|
||||
);
|
||||
const shouldShowMessagePreview = useSettingValue("RoomList.showMessagePreview");
|
||||
|
||||
const toggleMessagePreview = useCallback((): void => {
|
||||
setShouldShowMessagePreview((current) => {
|
||||
const toggled = !current;
|
||||
SettingsStore.setValue("RoomList.showMessagePreview", null, SettingLevel.DEVICE, toggled);
|
||||
return toggled;
|
||||
});
|
||||
}, []);
|
||||
const toggled = !shouldShowMessagePreview;
|
||||
SettingsStore.setValue("RoomList.showMessagePreview", null, SettingLevel.DEVICE, toggled);
|
||||
}, [shouldShowMessagePreview]);
|
||||
|
||||
return { toggleMessagePreview, shouldShowMessagePreview };
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { RoomListItemMenuView } from "./RoomListItemMenuView";
|
||||
import { NotificationDecoration } from "../NotificationDecoration";
|
||||
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
|
||||
|
||||
interface RoomListItemViewPropsProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
interface RoomListItemViewProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
/**
|
||||
* The room to display
|
||||
*/
|
||||
@@ -33,7 +33,7 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
room,
|
||||
isSelected,
|
||||
...props
|
||||
}: RoomListItemViewPropsProps): JSX.Element {
|
||||
}: RoomListItemViewProps): JSX.Element {
|
||||
const vm = useRoomListItemViewModel(room);
|
||||
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
@@ -73,9 +73,12 @@ export const RoomListItemView = memo(function RoomListItemView({
|
||||
justify="space-between"
|
||||
>
|
||||
{/* We truncate the room name when too long. Title here is to show the full name on hover */}
|
||||
<span className="mx_RoomListItemView_roomName" title={vm.name}>
|
||||
{vm.name}
|
||||
</span>
|
||||
<div className="mx_RoomListItemView_text">
|
||||
<div className="mx_RoomListItemView_roomName" title={vm.name}>
|
||||
{vm.name}
|
||||
</div>
|
||||
<div className="mx_RoomListItemView_messagePreview">{vm.messagePreview}</div>
|
||||
</div>
|
||||
{showHoverDecoration ? (
|
||||
<RoomListItemMenuView
|
||||
room={room}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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, CheckboxMenuItem, Tooltip } from "@vector-im/compound-web";
|
||||
import React, { type Ref, type JSX, useState } from "react";
|
||||
import OverflowHorizontalIcon from "@vector-im/compound-design-tokens/assets/web/icons/overflow-horizontal";
|
||||
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { type RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
|
||||
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
|
||||
className="mx_RoomListSecondaryFilters_roomOptionsButton"
|
||||
aria-label={_t("room_list|room_options")}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<OverflowHorizontalIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The view model for the room list view
|
||||
*/
|
||||
vm: RoomListViewState;
|
||||
}
|
||||
|
||||
export function RoomListOptionsMenu({ vm }: Props): JSX.Element {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title={_t("room_list|room_options")}
|
||||
showTitle={false}
|
||||
align="start"
|
||||
trigger={<MenuTrigger />}
|
||||
>
|
||||
<MenuTitle title={_t("room_list|appearance")} />
|
||||
<CheckboxMenuItem
|
||||
label={_t("room_list|show_message_previews")}
|
||||
onSelect={vm.toggleMessagePreview}
|
||||
checked={vm.shouldShowMessagePreview}
|
||||
/>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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 } from "react";
|
||||
|
||||
import type { RoomListViewState } from "../../../viewmodels/roomlist/RoomListViewModel";
|
||||
import { Flex } from "../../../utils/Flex";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { RoomListOptionsMenu } from "./RoomListOptionsMenu";
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* The view model for the room list
|
||||
*/
|
||||
vm: RoomListViewState;
|
||||
}
|
||||
|
||||
/**
|
||||
* The secondary filters for the room list (eg. mentions only / invites only).
|
||||
*/
|
||||
export function RoomListSecondaryFilters({ vm }: Props): JSX.Element {
|
||||
return (
|
||||
<Flex
|
||||
aria-label={_t("room_list|secondary_filters")}
|
||||
className="mx_RoomListSecondaryFilters"
|
||||
align="center"
|
||||
gap="8px"
|
||||
>
|
||||
<RoomListOptionsMenu vm={vm} />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewM
|
||||
import { RoomList } from "./RoomList";
|
||||
import { EmptyRoomList } from "./EmptyRoomList";
|
||||
import { RoomListPrimaryFilters } from "./RoomListPrimaryFilters";
|
||||
import { RoomListSecondaryFilters } from "./RoomListSecondaryFilters";
|
||||
|
||||
/**
|
||||
* Host the room list and the (future) room filters
|
||||
@@ -22,6 +23,7 @@ export function RoomListView(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<RoomListPrimaryFilters vm={vm} />
|
||||
<RoomListSecondaryFilters vm={vm} />
|
||||
{isRoomListEmpty ? <EmptyRoomList vm={vm} /> : <RoomList vm={vm} />}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2102,6 +2102,7 @@
|
||||
"room_list": {
|
||||
"add_room_label": "Add room",
|
||||
"add_space_label": "Add space",
|
||||
"appearance": "Appearance",
|
||||
"breadcrumbs_empty": "No recently visited rooms",
|
||||
"breadcrumbs_label": "Recently visited rooms",
|
||||
"empty": {
|
||||
@@ -2152,7 +2153,10 @@
|
||||
"more_options": "More Options",
|
||||
"open_room": "Open room %(roomName)s"
|
||||
},
|
||||
"room_options": "Room Options",
|
||||
"secondary_filters": "Secondary filters",
|
||||
"show_less": "Show less",
|
||||
"show_message_previews": "Show message previews",
|
||||
"show_n_more": {
|
||||
"one": "Show %(count)s more",
|
||||
"other": "Show %(count)s more"
|
||||
|
||||
Reference in New Issue
Block a user