fix(sidebar): the room filter hides empty sections and says when nothing matches (#289) #387

Merged
jared merged 1 commits from fix/room-filter-empty into lotus 2026-10-18 00:45:29 -04:00
+185 -134
View File
@@ -255,6 +255,8 @@ function HomeCustomSection({
return (localName ?? matrixName).toLowerCase().includes(query); return (localName ?? matrixName).toLowerCase().includes(query);
}); });
}, [mx, section.rooms, closed, roomsWithUnreadSet, selectedRoomId, filterQuery]); }, [mx, section.rooms, closed, roomsWithUnreadSet, selectedRoomId, filterQuery]);
// [Gitea #289] While filtering, a section with no matches is hidden.
if (filterQuery.trim() && items.length === 0) return null;
// Sections are user-curated and small, so they render plainly — no // Sections are user-curated and small, so they render plainly — no
// virtualizer per section (a late-mounted one never measured its scroller). // virtualizer per section (a late-mounted one never measured its scroller).
return ( return (
@@ -443,6 +445,26 @@ export function Home() {
}); });
}, [mx, sortedRooms, filterQuery]); }, [mx, sortedRooms, filterQuery]);
// [Gitea #289] While filtering, hide empty sections; with no match at all,
// say so instead of leaving bare headers.
const filtering = filterQuery.trim().length > 0;
const customSectionMatch = useMemo(() => {
if (!filtering) return true;
const query = filterQuery.toLowerCase();
const localNames = getLocalRoomNamesContent(mx);
return customSections.some((section) =>
section.rooms.some((rId) =>
(localNames.rooms[rId] ?? mx.getRoom(rId)?.name ?? '').toLowerCase().includes(query),
),
);
}, [mx, customSections, filtering, filterQuery]);
const noFilterMatch =
filtering &&
!customSectionMatch &&
filteredFavoriteRooms.length === 0 &&
filteredRooms.length === 0 &&
filteredLowPriorityRooms.length === 0;
const favVirtualizer = useVirtualizer({ const favVirtualizer = useVirtualizer({
count: filteredFavoriteRooms.length, count: filteredFavoriteRooms.length,
getScrollElement: () => scrollRef.current, getScrollElement: () => scrollRef.current,
@@ -580,7 +602,7 @@ export function Home() {
/> />
</Box> </Box>
</NavCategory> </NavCategory>
{favoriteRooms.length > 0 && ( {favoriteRooms.length > 0 && (!filtering || filteredFavoriteRooms.length > 0) && (
<NavCategory> <NavCategory>
<NavCategoryHeader> <NavCategoryHeader>
<RoomNavCategoryButton <RoomNavCategoryButton
@@ -629,142 +651,171 @@ export function Home() {
notificationPreferences={notificationPreferences} notificationPreferences={notificationPreferences}
/> />
))} ))}
<NavCategory> {noFilterMatch && (
<NavCategoryHeader> <NavCategory>
<RoomNavCategoryButton <Box
closed={closedCategories.has(DEFAULT_CATEGORY_ID)} direction="Column"
data-category-id={DEFAULT_CATEGORY_ID} alignItems="Center"
onClick={handleCategoryClick} gap="200"
style={{ padding: config.space.S400 }}
> >
Rooms <Text size="T300" priority="300" align="Center">
</RoomNavCategoryButton> No rooms match &ldquo;{filterQuery.trim()}&rdquo;
{!closedCategories.has(DEFAULT_CATEGORY_ID) && ( </Text>
<> <Button
<IconButton size="300"
aria-expanded={!!sortMenuAnchor} variant="Secondary"
aria-haspopup="menu" fill="Soft"
variant="Background" radii="300"
fill="None" onClick={() => setFilterQuery('')}
size="300" >
radii="300" <Text size="B300">Clear filter</Text>
onClick={(evt: React.MouseEvent<HTMLButtonElement>) => { </Button>
const cords = evt.currentTarget.getBoundingClientRect(); </Box>
setSortMenuAnchor((current) => (current ? undefined : cords)); </NavCategory>
}} )}
aria-label="Sort rooms" {(!filtering || filteredRooms.length > 0) && (
> <NavCategory>
<Icon src={Icons.Sort} size="100" /> <NavCategoryHeader>
</IconButton> <RoomNavCategoryButton
<PopOut closed={closedCategories.has(DEFAULT_CATEGORY_ID)}
anchor={sortMenuAnchor} data-category-id={DEFAULT_CATEGORY_ID}
position="Bottom" onClick={handleCategoryClick}
align="End" >
offset={6} Rooms
content={ </RoomNavCategoryButton>
<FocusTrap {!closedCategories.has(DEFAULT_CATEGORY_ID) && (
focusTrapOptions={{ <>
initialFocus: false, <IconButton
onDeactivate: () => setSortMenuAnchor(undefined), aria-expanded={!!sortMenuAnchor}
clickOutsideDeactivates: true, aria-haspopup="menu"
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', variant="Background"
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', fill="None"
escapeDeactivates: stopPropagation, size="300"
}} radii="300"
> onClick={(evt: React.MouseEvent<HTMLButtonElement>) => {
<Menu style={{ maxWidth: toRem(180), width: '100vw' }}> const cords = evt.currentTarget.getBoundingClientRect();
<Box setSortMenuAnchor((current) => (current ? undefined : cords));
direction="Column" }}
gap="100" aria-label="Sort rooms"
style={{ padding: config.space.S100 }} >
> <Icon src={Icons.Sort} size="100" />
<MenuItem </IconButton>
onClick={() => { <PopOut
setHomeRoomSort('recent'); anchor={sortMenuAnchor}
setSortMenuAnchor(undefined); position="Bottom"
}} align="End"
size="300" offset={6}
after={ content={
homeRoomSort === 'recent' ? ( <FocusTrap
<Icon size="100" src={Icons.Check} /> focusTrapOptions={{
) : undefined initialFocus: false,
} onDeactivate: () => setSortMenuAnchor(undefined),
radii="300" clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown',
isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp',
escapeDeactivates: stopPropagation,
}}
>
<Menu style={{ maxWidth: toRem(180), width: '100vw' }}>
<Box
direction="Column"
gap="100"
style={{ padding: config.space.S100 }}
> >
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <MenuItem
Recent Activity onClick={() => {
</Text> setHomeRoomSort('recent');
</MenuItem> setSortMenuAnchor(undefined);
<MenuItem }}
onClick={() => { size="300"
setHomeRoomSort('alpha'); after={
setSortMenuAnchor(undefined); homeRoomSort === 'recent' ? (
}} <Icon size="100" src={Icons.Check} />
size="300" ) : undefined
after={ }
homeRoomSort === 'alpha' ? ( radii="300"
<Icon size="100" src={Icons.Check} /> >
) : undefined <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
} Recent Activity
radii="300" </Text>
> </MenuItem>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <MenuItem
A → Z onClick={() => {
</Text> setHomeRoomSort('alpha');
</MenuItem> setSortMenuAnchor(undefined);
<MenuItem }}
onClick={() => { size="300"
setHomeRoomSort('unread'); after={
setSortMenuAnchor(undefined); homeRoomSort === 'alpha' ? (
}} <Icon size="100" src={Icons.Check} />
size="300" ) : undefined
after={ }
homeRoomSort === 'unread' ? ( radii="300"
<Icon size="100" src={Icons.Check} /> >
) : undefined <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
} A → Z
radii="300" </Text>
> </MenuItem>
<Text style={{ flexGrow: 1 }} as="span" size="T300" truncate> <MenuItem
Unread First onClick={() => {
</Text> setHomeRoomSort('unread');
</MenuItem> setSortMenuAnchor(undefined);
</Box> }}
</Menu> size="300"
</FocusTrap> after={
} homeRoomSort === 'unread' ? (
/> <Icon size="100" src={Icons.Check} />
</> ) : undefined
)} }
</NavCategoryHeader> radii="300"
<div >
style={{ <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
position: 'relative', Unread First
height: virtualizer.getTotalSize(), </Text>
}} </MenuItem>
> </Box>
{virtualizer.getVirtualItems().map((vItem) => { </Menu>
const roomId = filteredRooms[vItem.index]; </FocusTrap>
const room = mx.getRoom(roomId); }
if (!room) return null;
const selected = selectedRoomId === roomId;
return (
<VirtualTile virtualItem={vItem} key={roomId} ref={virtualizer.measureElement}>
<RoomNavItem
room={room}
selected={selected}
linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
notificationMode={getRoomNotificationMode(
notificationPreferences,
room.roomId,
)}
/> />
</VirtualTile> </>
); )}
})} </NavCategoryHeader>
</div> <div
</NavCategory> style={{
{lowPriorityRooms.length > 0 && ( position: 'relative',
height: virtualizer.getTotalSize(),
}}
>
{virtualizer.getVirtualItems().map((vItem) => {
const roomId = filteredRooms[vItem.index];
const room = mx.getRoom(roomId);
if (!room) return null;
const selected = selectedRoomId === roomId;
return (
<VirtualTile
virtualItem={vItem}
key={roomId}
ref={virtualizer.measureElement}
>
<RoomNavItem
room={room}
selected={selected}
linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
notificationMode={getRoomNotificationMode(
notificationPreferences,
room.roomId,
)}
/>
</VirtualTile>
);
})}
</div>
</NavCategory>
)}
{lowPriorityRooms.length > 0 && (!filtering || filteredLowPriorityRooms.length > 0) && (
<NavCategory> <NavCategory>
<NavCategoryHeader> <NavCategoryHeader>
<RoomNavCategoryButton <RoomNavCategoryButton