feat: P1 features — voice speed, private receipts, room filter, favorites, invite link, poll creation
P1-5: Voice message playback speed toggle (0.75×/1×/1.5×/2×) in AudioContent.tsx P1-10: Private read receipts toggle in Privacy settings; wired to notifications.ts P1-3: Room filter input on Home tab and DMs tab (client-side, clears on tab switch) P1-8: Favorite rooms via m.favourite tag — Favorites section in Home sidebar, star/unstar in right-click menu P1-9: Room invite link + QR code in room settings (Share Room tile, api.qrserver.com QR) P1-6: Poll creation modal in composer (PollCreator.tsx, sends m.poll.start) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopOut,
|
||||
@@ -181,6 +182,7 @@ export function Direct() {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const directs = useDirectRooms();
|
||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||
const [filterQuery, setFilterQuery] = useState('');
|
||||
const roomsWithUnreadSet = useAtomValue(
|
||||
useMemo(
|
||||
() =>
|
||||
@@ -216,8 +218,14 @@ export function Direct() {
|
||||
return items;
|
||||
}, [mx, directs, closedCategories, roomsWithUnreadSet, selectedRoomId]);
|
||||
|
||||
const filteredDirects = useMemo(() => {
|
||||
if (!filterQuery.trim()) return sortedDirects;
|
||||
const q = filterQuery.toLowerCase();
|
||||
return sortedDirects.filter((rId) => (mx.getRoom(rId)?.name ?? '').toLowerCase().includes(q));
|
||||
}, [mx, sortedDirects, filterQuery]);
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: sortedDirects.length,
|
||||
count: filteredDirects.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 38,
|
||||
overscan: 10,
|
||||
@@ -253,6 +261,34 @@ export function Direct() {
|
||||
</NavButton>
|
||||
</NavItem>
|
||||
</NavCategory>
|
||||
<NavCategory>
|
||||
<Box style={{ padding: `0 ${config.space.S200}`, paddingBottom: config.space.S100 }}>
|
||||
<Input
|
||||
value={filterQuery}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setFilterQuery(e.target.value)
|
||||
}
|
||||
placeholder="Filter DMs…"
|
||||
variant="Surface"
|
||||
size="300"
|
||||
radii="300"
|
||||
after={
|
||||
filterQuery ? (
|
||||
<IconButton
|
||||
onClick={() => setFilterQuery('')}
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Background"
|
||||
fill="None"
|
||||
aria-label="Clear filter"
|
||||
>
|
||||
<Icon size="50" src={Icons.Cross} />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</NavCategory>
|
||||
<NavCategory>
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
@@ -270,7 +306,7 @@ export function Direct() {
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((vItem) => {
|
||||
const roomId = sortedDirects[vItem.index];
|
||||
const roomId = filteredDirects[vItem.index];
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
const selected = selectedRoomId === roomId;
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react';
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
MouseEventHandler,
|
||||
forwardRef,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Avatar,
|
||||
@@ -7,6 +14,7 @@ import {
|
||||
Icon,
|
||||
IconButton,
|
||||
Icons,
|
||||
Input,
|
||||
Menu,
|
||||
MenuItem,
|
||||
PopOut,
|
||||
@@ -66,6 +74,7 @@ import {
|
||||
import { UseStateProvider } from '../../../components/UseStateProvider';
|
||||
import { JoinAddressPrompt } from '../../../components/join-address-prompt';
|
||||
import { _RoomSearchParams } from '../../paths';
|
||||
import { getLocalRoomNamesContent } from '../../../hooks/useRoomMeta';
|
||||
|
||||
type HomeMenuProps = {
|
||||
requestClose: () => void;
|
||||
@@ -201,12 +210,14 @@ function HomeEmpty() {
|
||||
}
|
||||
|
||||
const DEFAULT_CATEGORY_ID = makeNavCategoryId('home', 'room');
|
||||
const FAVORITES_CATEGORY_ID = makeNavCategoryId('home', 'favorite');
|
||||
export function Home() {
|
||||
const mx = useMatrixClient();
|
||||
useNavToActivePathMapper('home');
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const rooms = useHomeRooms();
|
||||
const notificationPreferences = useRoomsNotificationPreferencesContext();
|
||||
const [filterQuery, setFilterQuery] = useState<string>('');
|
||||
// Perf-3: only re-render when the set of rooms WITH unread changes, not on count updates
|
||||
const roomsWithUnreadSet = useAtomValue(
|
||||
useMemo(
|
||||
@@ -235,8 +246,32 @@ export function Home() {
|
||||
const noRoomToDisplay = rooms.length === 0;
|
||||
const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom());
|
||||
|
||||
const { favoriteRooms, otherRooms } = useMemo(() => {
|
||||
const favs: string[] = [];
|
||||
const others: string[] = [];
|
||||
rooms.forEach((rId) => {
|
||||
const room = mx.getRoom(rId);
|
||||
if (room?.tags?.['m.favourite']) {
|
||||
favs.push(rId);
|
||||
} else {
|
||||
others.push(rId);
|
||||
}
|
||||
});
|
||||
return { favoriteRooms: favs, otherRooms: others };
|
||||
}, [mx, rooms]);
|
||||
|
||||
const sortedFavoriteRooms = useMemo(
|
||||
() =>
|
||||
Array.from(favoriteRooms).sort(
|
||||
closedCategories.has(FAVORITES_CATEGORY_ID)
|
||||
? factoryRoomIdByActivity(mx)
|
||||
: factoryRoomIdByAtoZ(mx),
|
||||
),
|
||||
[mx, favoriteRooms, closedCategories],
|
||||
);
|
||||
|
||||
const sortedRooms = useMemo(() => {
|
||||
const items = Array.from(rooms).sort(
|
||||
const items = Array.from(otherRooms).sort(
|
||||
closedCategories.has(DEFAULT_CATEGORY_ID)
|
||||
? factoryRoomIdByActivity(mx)
|
||||
: factoryRoomIdByAtoZ(mx),
|
||||
@@ -245,10 +280,28 @@ export function Home() {
|
||||
return items.filter((rId) => roomsWithUnreadSet.has(rId) || rId === selectedRoomId);
|
||||
}
|
||||
return items;
|
||||
}, [mx, rooms, closedCategories, roomsWithUnreadSet, selectedRoomId]);
|
||||
}, [mx, otherRooms, closedCategories, roomsWithUnreadSet, selectedRoomId]);
|
||||
|
||||
const filteredRooms = useMemo(() => {
|
||||
if (!filterQuery.trim()) return sortedRooms;
|
||||
const query = filterQuery.toLowerCase();
|
||||
const localNames = getLocalRoomNamesContent(mx);
|
||||
return sortedRooms.filter((rId) => {
|
||||
const localName = localNames.rooms[rId];
|
||||
const matrixName = mx.getRoom(rId)?.name ?? '';
|
||||
return (localName ?? matrixName).toLowerCase().includes(query);
|
||||
});
|
||||
}, [mx, sortedRooms, filterQuery]);
|
||||
|
||||
const favVirtualizer = useVirtualizer({
|
||||
count: sortedFavoriteRooms.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 38,
|
||||
overscan: 10,
|
||||
});
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: sortedRooms.length,
|
||||
count: filteredRooms.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 38,
|
||||
overscan: 10,
|
||||
@@ -338,6 +391,73 @@ export function Home() {
|
||||
</NavLink>
|
||||
</NavItem>
|
||||
</NavCategory>
|
||||
<NavCategory>
|
||||
<Box
|
||||
style={{ padding: `0 ${config.space.S200}`, paddingBottom: config.space.S100 }}
|
||||
direction="Column"
|
||||
gap="100"
|
||||
>
|
||||
<Input
|
||||
value={filterQuery}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setFilterQuery(e.target.value)}
|
||||
placeholder="Filter rooms…"
|
||||
variant="Surface"
|
||||
size="300"
|
||||
radii="300"
|
||||
after={
|
||||
filterQuery ? (
|
||||
<IconButton
|
||||
onClick={() => setFilterQuery('')}
|
||||
size="300"
|
||||
radii="300"
|
||||
variant="Background"
|
||||
fill="None"
|
||||
aria-label="Clear filter"
|
||||
>
|
||||
<Icon size="50" src={Icons.Cross} />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
</NavCategory>
|
||||
{sortedFavoriteRooms.length > 0 && (
|
||||
<NavCategory>
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
closed={closedCategories.has(FAVORITES_CATEGORY_ID)}
|
||||
data-category-id={FAVORITES_CATEGORY_ID}
|
||||
onClick={handleCategoryClick}
|
||||
>
|
||||
Favorites
|
||||
</RoomNavCategoryButton>
|
||||
</NavCategoryHeader>
|
||||
<div style={{ position: 'relative', height: favVirtualizer.getTotalSize() }}>
|
||||
{favVirtualizer.getVirtualItems().map((vItem) => {
|
||||
const roomId = sortedFavoriteRooms[vItem.index];
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
return (
|
||||
<VirtualTile
|
||||
virtualItem={vItem}
|
||||
key={vItem.index}
|
||||
ref={favVirtualizer.measureElement}
|
||||
>
|
||||
<RoomNavItem
|
||||
room={room}
|
||||
selected={selectedRoomId === roomId}
|
||||
linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
|
||||
notificationMode={getRoomNotificationMode(
|
||||
notificationPreferences,
|
||||
room.roomId,
|
||||
)}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</NavCategory>
|
||||
)}
|
||||
<NavCategory>
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
@@ -355,7 +475,7 @@ export function Home() {
|
||||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((vItem) => {
|
||||
const roomId = sortedRooms[vItem.index];
|
||||
const roomId = filteredRooms[vItem.index];
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
const selected = selectedRoomId === roomId;
|
||||
|
||||
Reference in New Issue
Block a user