fix(messaging): a sent message no longer shows "Seen" instantly

Two independent defects; either alone caused it, in DMs, groups and channels.

1. useMessages reported a read of the newest NON-PENDING message regardless of
   author. Sending therefore made the client immediately mark its own message
   read, the server echoed a receipt for it, and the sender's bubble showed
   "Seen" before anyone had opened the thread. You do not read your own message:
   lastReadableId now skips your own.

2. The guard meant to catch exactly this — `e.actorId !== currentActorId` — could
   never fire: the receipt carries the reader's IIOS actor UUID while clients hold
   a userId, so the comparison was always true and every receipt, including your
   own, counted as the other side. The gateway now also emits userId on READ and
   DELIVERED receipts (matching what `annotation` already did), and ReceiptEvent
   carries it as optional so older servers still typecheck.

MockAdapter.markRead was a no-op, so it could not exercise any of this; it now
echoes a receipt like a real server. Regression test verified to fail without
the fix. SDK 0.1.14; suite 17 files / 101 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 17:46:42 +05:30
parent eb0ace8ad7
commit 272c6acd31
6 changed files with 59 additions and 8 deletions
@@ -159,7 +159,9 @@ export class MessageGateway implements OnGatewayInit, OnGatewayConnection, OnGat
async read(@ConnectedSocket() client: Socket, @MessageBody() body: { threadId: string; interactionId: string }) {
const { principal } = client.data as SocketState;
const r = await this.messages.markRead(body.threadId, principal, body.interactionId);
this.server.to(body.threadId).emit('receipt', { interactionId: r.interactionId, actorId: r.actorId, kind: 'READ' });
// userId as well as actorId: actorId is an IIOS actor UUID, but clients hold the caller's
// USERID, so without this they cannot tell their own receipt from someone else's.
this.server.to(body.threadId).emit('receipt', { interactionId: r.interactionId, actorId: r.actorId, userId: principal.userId, kind: 'READ' });
return { ok: true };
}
@@ -167,7 +169,9 @@ export class MessageGateway implements OnGatewayInit, OnGatewayConnection, OnGat
async delivered(@ConnectedSocket() client: Socket, @MessageBody() body: { threadId: string; interactionId: string }) {
const { principal } = client.data as SocketState;
const r = await this.messages.markDelivered(body.threadId, principal, body.interactionId);
this.server.to(body.threadId).emit('receipt', { interactionId: r.interactionId, actorId: r.actorId, kind: 'DELIVERED' });
// userId as well as actorId: actorId is an IIOS actor UUID, but clients hold the caller's
// USERID, so without this they cannot tell their own receipt from someone else's.
this.server.to(body.threadId).emit('receipt', { interactionId: r.interactionId, actorId: r.actorId, userId: principal.userId, kind: 'DELIVERED' });
return { ok: true };
}