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 =