feat: per-message read receipt avatars showing each user s last-read position

This commit is contained in:
root
2026-05-15 18:56:17 -04:00
parent bf544ebc84
commit 74963b6bf2
6 changed files with 170 additions and 0 deletions
@@ -0,0 +1,4 @@
import { createContext, useContext } from 'react';
export const ReadPositionsContext = createContext<Map<string, string[]>>(new Map());
export const useReadPositions = () => useContext(ReadPositionsContext);
+5
View File
@@ -89,6 +89,8 @@ import { useSetting } from '../../state/hooks/settings';
import { MessageLayout, settingsAtom } from '../../state/settings';
import { useMatrixEventRenderer } from '../../hooks/useMatrixEventRenderer';
import { Reactions, Message, Event, EncryptedContent } from './message';
import { ReadPositionsContext } from './ReadPositionsContext';
import { useRoomReadPositions } from '../../hooks/useRoomReadPositions';
import { useMemberEventParser } from '../../hooks/useMemberEventParser';
import * as customHtmlCss from '../../styles/CustomHtml.css';
import { RoomIntro } from '../../components/room-intro';
@@ -441,6 +443,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const [messageSpacing] = useSetting(settingsAtom, 'messageSpacing');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
const direct = useIsDirectRoom();
const readPositions = useRoomReadPositions(room);
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
@@ -1837,6 +1840,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
};
return (
<ReadPositionsContext.Provider value={readPositions}>
<Box grow="Yes" style={{ position: 'relative' }}>
{unreadInfo?.readUptoEventId && !unreadInfo?.inLiveTimeline && (
<TimelineFloat position="Top">
@@ -1962,5 +1966,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
</TimelineFloat>
)}
</Box>
</ReadPositionsContext.Provider>
);
}
+13
View File
@@ -63,6 +63,8 @@ import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { useRecentEmoji } from '../../../hooks/useRecentEmoji';
import * as css from './styles.css';
import { EventReaders } from '../../../components/event-readers';
import { ReadReceiptAvatars } from '../../../components/read-receipt-avatars';
import { useReadPositions } from '../ReadPositionsContext';
import { TextViewer } from '../../../components/text-viewer';
import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback';
import { EmojiBoard } from '../../../components/emoji-board';
@@ -721,6 +723,10 @@ export const Message = as<'div', MessageProps>(
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const senderId = mEvent.getSender() ?? '';
const readPositions = useReadPositions();
const readReceiptUsers = hideReadReceipts
? []
: (readPositions.get(mEvent.getId() ?? '') ?? []);
const [hover, setHover] = useState(false);
const { hoverProps } = useHover({ onHoverChange: setHover });
@@ -833,6 +839,13 @@ export const Message = as<'div', MessageProps>(
children
)}
{reactions}
{readReceiptUsers.length > 0 && (
<ReadReceiptAvatars
room={room}
eventId={mEvent.getId() ?? ''}
userIds={readReceiptUsers}
/>
)}
</Box>
);