fix(sidebar): the room filter hides empty sections and says when nothing matches (#289) #387
+185
-134
@@ -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 “{filterQuery.trim()}”
|
||||||
{!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
|
||||||
|
|||||||
Reference in New Issue
Block a user