feat(rooms): Mark as Unread (MSC2867) + Low Priority rooms
Two Matrix protocol gaps (Phase A), gate-green (683 tests): - Mark as Unread: m.marked_unread room account data (+ com.famedly.marked_unread fallback), a new markedUnreadAtom binder that seeds from account data and clears on our own read receipt (MSC2867). RoomNavItem gains Mark as Unread / Read menu items and lights the row dot for a marked room. Tested. - Low Priority: m.lowpriority room tag mirroring favourites — a context-menu toggle (mutually exclusive with Favorite) and a collapsed Low Priority category sorted to the bottom of the Home room list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -223,6 +223,7 @@ const factoryRoomIdByUnread =
|
||||
|
||||
const DEFAULT_CATEGORY_ID = makeNavCategoryId('home', 'room');
|
||||
const FAVORITES_CATEGORY_ID = makeNavCategoryId('home', 'favorite');
|
||||
const LOW_PRIORITY_CATEGORY_ID = makeNavCategoryId('home', 'lowpriority');
|
||||
export function Home() {
|
||||
const mx = useMatrixClient();
|
||||
useNavToActivePathMapper('home');
|
||||
@@ -261,18 +262,21 @@ export function Home() {
|
||||
const roomToUnread = useAtomValue(roomToUnreadAtom);
|
||||
const [sortMenuAnchor, setSortMenuAnchor] = useState<RectCords>();
|
||||
|
||||
const { favoriteRooms, otherRooms } = useMemo(() => {
|
||||
const { favoriteRooms, lowPriorityRooms, otherRooms } = useMemo(() => {
|
||||
const favs: string[] = [];
|
||||
const low: string[] = [];
|
||||
const others: string[] = [];
|
||||
rooms.forEach((rId) => {
|
||||
const room = mx.getRoom(rId);
|
||||
if (room?.tags?.['m.favourite']) {
|
||||
favs.push(rId);
|
||||
} else if (room?.tags?.['m.lowpriority']) {
|
||||
low.push(rId);
|
||||
} else {
|
||||
others.push(rId);
|
||||
}
|
||||
});
|
||||
return { favoriteRooms: favs, otherRooms: others };
|
||||
return { favoriteRooms: favs, lowPriorityRooms: low, otherRooms: others };
|
||||
}, [mx, rooms]);
|
||||
|
||||
const sortedFavoriteRooms = useMemo(() => {
|
||||
@@ -297,6 +301,28 @@ export function Home() {
|
||||
});
|
||||
}, [mx, sortedFavoriteRooms, filterQuery]);
|
||||
|
||||
const sortedLowPriorityRooms = useMemo(() => {
|
||||
const isClosed = closedCategories.has(LOW_PRIORITY_CATEGORY_ID);
|
||||
const items = Array.from(lowPriorityRooms).sort(
|
||||
isClosed ? factoryRoomIdByActivity(mx) : factoryRoomIdByAtoZ(mx),
|
||||
);
|
||||
if (isClosed) {
|
||||
return items.filter((rId) => roomsWithUnreadSet.has(rId) || rId === selectedRoomId);
|
||||
}
|
||||
return items;
|
||||
}, [mx, lowPriorityRooms, closedCategories, roomsWithUnreadSet, selectedRoomId]);
|
||||
|
||||
const filteredLowPriorityRooms = useMemo(() => {
|
||||
if (!filterQuery.trim()) return sortedLowPriorityRooms;
|
||||
const query = filterQuery.toLowerCase();
|
||||
const localNames = getLocalRoomNamesContent(mx);
|
||||
return sortedLowPriorityRooms.filter((rId) => {
|
||||
const localName = localNames.rooms[rId];
|
||||
const matrixName = mx.getRoom(rId)?.name ?? '';
|
||||
return (localName ?? matrixName).toLowerCase().includes(query);
|
||||
});
|
||||
}, [mx, sortedLowPriorityRooms, filterQuery]);
|
||||
|
||||
const sortedRooms = useMemo(() => {
|
||||
const isClosed = closedCategories.has(DEFAULT_CATEGORY_ID);
|
||||
let comparator: (a: string, b: string) => number;
|
||||
@@ -349,6 +375,13 @@ export function Home() {
|
||||
overscan: 10,
|
||||
});
|
||||
|
||||
const lowVirtualizer = useVirtualizer({
|
||||
count: filteredLowPriorityRooms.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 38,
|
||||
overscan: 10,
|
||||
});
|
||||
|
||||
const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) =>
|
||||
closedCategories.has(categoryId),
|
||||
);
|
||||
@@ -638,6 +671,43 @@ export function Home() {
|
||||
})}
|
||||
</div>
|
||||
</NavCategory>
|
||||
{lowPriorityRooms.length > 0 && (
|
||||
<NavCategory>
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
closed={closedCategories.has(LOW_PRIORITY_CATEGORY_ID)}
|
||||
data-category-id={LOW_PRIORITY_CATEGORY_ID}
|
||||
onClick={handleCategoryClick}
|
||||
>
|
||||
Low Priority
|
||||
</RoomNavCategoryButton>
|
||||
</NavCategoryHeader>
|
||||
<div style={{ position: 'relative', height: lowVirtualizer.getTotalSize() }}>
|
||||
{lowVirtualizer.getVirtualItems().map((vItem) => {
|
||||
const roomId = filteredLowPriorityRooms[vItem.index];
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
return (
|
||||
<VirtualTile
|
||||
virtualItem={vItem}
|
||||
key={roomId}
|
||||
ref={lowVirtualizer.measureElement}
|
||||
>
|
||||
<RoomNavItem
|
||||
room={room}
|
||||
selected={selectedRoomId === roomId}
|
||||
linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
|
||||
notificationMode={getRoomNotificationMode(
|
||||
notificationPreferences,
|
||||
room.roomId,
|
||||
)}
|
||||
/>
|
||||
</VirtualTile>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</NavCategory>
|
||||
)}
|
||||
</Box>
|
||||
</PageNavContent>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user