From 840f6d7ff36de855bbb09638841e2d896ccd3852 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:24:03 +0100 Subject: [PATCH] feat(matrix-web): add read receipt indicators - Add ReadReceipt type for tracking who read messages - Track read receipts via Matrix SDK receipt events - Display single checkmark for sent messages - Display blue double checkmarks when message is read - Tooltip shows names of readers - Auto-refresh timeline when receipts are received Co-Authored-By: Claude Opus 4.5 --- .../src/lib/components/chat/Message.svelte | 24 +++++++-- .../apps/web/src/lib/matrix/store.svelte.ts | 54 ++++++++++++++++++- apps/matrix/apps/web/src/lib/matrix/types.ts | 11 ++++ 3 files changed, 85 insertions(+), 4 deletions(-) diff --git a/apps/matrix/apps/web/src/lib/components/chat/Message.svelte b/apps/matrix/apps/web/src/lib/components/chat/Message.svelte index 2bf503ba7..16bb809a2 100644 --- a/apps/matrix/apps/web/src/lib/components/chat/Message.svelte +++ b/apps/matrix/apps/web/src/lib/components/chat/Message.svelte @@ -16,6 +16,8 @@ Lock, Warning, Smiley, + Check, + Checks, } from '@manacore/shared-icons'; interface Props { @@ -486,11 +488,27 @@ {/if} - +
- {formattedTime()} + {formattedTime()} + + {#if message.isOwn} + {#if message.readBy && message.readBy.length > 0} + + {:else} + + {/if} + {/if}
diff --git a/apps/matrix/apps/web/src/lib/matrix/store.svelte.ts b/apps/matrix/apps/web/src/lib/matrix/store.svelte.ts index 35f705587..b99046294 100644 --- a/apps/matrix/apps/web/src/lib/matrix/store.svelte.ts +++ b/apps/matrix/apps/web/src/lib/matrix/store.svelte.ts @@ -7,6 +7,7 @@ import type { SimpleMessage, MessageType, MessageReaction, + ReadReceipt, RoomMember, VerificationStatus, DeviceInfo, @@ -300,6 +301,14 @@ class MatrixStore { // Also trigger room list update for DMs this._rooms = this._client!.getRooms(); }); + + // Read receipt updates + this._client.on(sdk.RoomEvent.Receipt, (event, room) => { + // Update timeline if we're in this room to refresh read receipts + if (room.roomId === this._currentRoomId) { + this._timeline = [...(room.getLiveTimeline().getEvents() || [])]; + } + }); } /** @@ -1328,6 +1337,10 @@ class MatrixStore { // Collect reactions for this message const reactions = this.getReactionsForEvent(event.getId() || '', timeline); + // Get read receipts for this message (only for own messages) + const isOwn = event.getSender() === this._client?.getUserId(); + const readBy = isOwn ? this.getReadReceiptsForEvent(event) : undefined; + return { id: event.getId() || '', sender: event.getSender() || '', @@ -1336,13 +1349,14 @@ class MatrixStore { formattedBody: content.formatted_body, timestamp: event.getTs(), type: msgtype as MessageType, - isOwn: event.getSender() === this._client?.getUserId(), + isOwn, replyTo: replyToId, replyToBody, edited: !!event.replacingEvent(), redacted: isRedacted, media, reactions: reactions.length > 0 ? reactions : undefined, + readBy: readBy && readBy.length > 0 ? readBy : undefined, }; } @@ -1398,6 +1412,44 @@ class MatrixStore { return reactions.sort((a, b) => b.count - a.count); } + /** + * Get read receipts for a specific event + */ + private getReadReceiptsForEvent(event: MatrixEvent): ReadReceipt[] { + const eventId = event.getId(); + const roomId = event.getRoomId(); + if (!eventId || !roomId || !this._client) return []; + + const room = this._client.getRoom(roomId); + if (!room) return []; + + const myUserId = this._client.getUserId(); + const receipts: ReadReceipt[] = []; + + // Get all members who have read up to or past this event + const members = room.getJoinedMembers(); + for (const member of members) { + // Skip self + if (member.userId === myUserId) continue; + + // Get the user's read receipt + const receiptEvent = room.getEventReadUpTo(member.userId); + if (!receiptEvent) continue; + + // Check if their read receipt is at or after this event + const receiptEventObj = room.findEventById(receiptEvent); + if (receiptEventObj && receiptEventObj.getTs() >= event.getTs()) { + receipts.push({ + userId: member.userId, + userName: member.name || member.userId.split(':')[0].substring(1), + timestamp: receiptEventObj.getTs(), + }); + } + } + + return receipts; + } + /** * Get display name for message sender */ diff --git a/apps/matrix/apps/web/src/lib/matrix/types.ts b/apps/matrix/apps/web/src/lib/matrix/types.ts index 15f41ac61..2a29f14cb 100644 --- a/apps/matrix/apps/web/src/lib/matrix/types.ts +++ b/apps/matrix/apps/web/src/lib/matrix/types.ts @@ -39,6 +39,15 @@ export interface MessageReaction { includesMe: boolean; } +/** + * Read receipt info for a message + */ +export interface ReadReceipt { + userId: string; + userName: string; + timestamp: number; +} + /** * Simplified message for UI rendering */ @@ -57,6 +66,8 @@ export interface SimpleMessage { redacted?: boolean; media?: MediaInfo; reactions?: MessageReaction[]; + // Read receipts + readBy?: ReadReceipt[]; } export type MessageType =