feat(iios): generic interaction-annotation primitive (backs emoji reactions)
Adds IiosInteractionAnnotation — an actor attaches an OPAQUE (annotationType, value)
label to an interaction. The kernel stores + aggregates them but never interprets the
strings (the chat app writes type "reaction" / value = emoji); the same primitive backs
pins/saves/flags/tags later. No chat vocabulary in kernel code — verified by grep.
- MessageService.toggleAnnotation(): governed (participant-only via new OPA rule
iios.interaction.annotate), idempotent toggle keyed on
(scope, interaction, actor, type, value); history DTO carries aggregated
{ type, value, users[] } groups.
- Gateway: `annotate` event → broadcasts `annotation` (refreshed user list) to the room.
- DevOpaPort: annotate allowed only for thread members (real OPA can tighten later).
- Migration add_interaction_annotations (additive; dev data untouched).
Tests: 178 pass (+3: toggle/aggregate, coexisting values, non-member denied).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -107,6 +107,26 @@ export class MessageGateway implements OnGatewayInit, OnGatewayConnection {
|
||||
return msg;
|
||||
}
|
||||
|
||||
@SubscribeMessage('annotate')
|
||||
async annotate(
|
||||
@ConnectedSocket() client: Socket,
|
||||
@MessageBody() body: { threadId: string; interactionId: string; type: string; value: string },
|
||||
) {
|
||||
const { principal } = client.data as SocketState;
|
||||
const r = await this.messages.toggleAnnotation(body.interactionId, principal, body.type, body.value);
|
||||
// Broadcast the refreshed user list for this (interaction,type,value) so every client re-renders.
|
||||
this.server.to(r.threadId).emit('annotation', {
|
||||
threadId: r.threadId,
|
||||
interactionId: r.interactionId,
|
||||
type: r.type,
|
||||
value: r.value,
|
||||
op: r.op,
|
||||
users: r.users,
|
||||
userId: principal.userId,
|
||||
});
|
||||
return r;
|
||||
}
|
||||
|
||||
@SubscribeMessage('read')
|
||||
async read(@ConnectedSocket() client: Socket, @MessageBody() body: { threadId: string; interactionId: string }) {
|
||||
const { principal } = client.data as SocketState;
|
||||
|
||||
Reference in New Issue
Block a user