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:
2026-06-02 19:31:30 -04:00
parent f07ff63ac1
commit afe957015b
11 changed files with 607 additions and 8 deletions
+38 -2
View File
@@ -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;