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:
@@ -303,6 +303,7 @@ model IiosScope {
|
||||
channels IiosChannel[]
|
||||
threads IiosThread[]
|
||||
interactions IiosInteraction[]
|
||||
annotations IiosInteractionAnnotation[]
|
||||
inboxItems IiosInboxItem[]
|
||||
supportQueues IiosSupportQueue[]
|
||||
tickets IiosTicket[]
|
||||
@@ -348,6 +349,7 @@ model IiosActorRef {
|
||||
threadsCreated IiosThread[] @relation("ThreadCreatedBy")
|
||||
participations IiosThreadParticipant[]
|
||||
interactions IiosInteraction[]
|
||||
annotations IiosInteractionAnnotation[]
|
||||
receipts IiosMessageReceipt[]
|
||||
unreadCounters IiosUnreadCounter[]
|
||||
inboxItemsOwned IiosInboxItem[]
|
||||
@@ -444,6 +446,7 @@ model IiosInteraction {
|
||||
|
||||
parts IiosMessagePart[]
|
||||
receipts IiosMessageReceipt[]
|
||||
annotations IiosInteractionAnnotation[]
|
||||
inboxItems IiosInboxItem[]
|
||||
ticketsCreatedFrom IiosTicket[]
|
||||
|
||||
@@ -451,6 +454,26 @@ model IiosInteraction {
|
||||
@@index([threadId, occurredAt])
|
||||
}
|
||||
|
||||
/// A generic annotation an actor attaches to an interaction. Both `annotationType`
|
||||
/// and `value` are OPAQUE, app-supplied strings the kernel stores and aggregates
|
||||
/// but never interprets — e.g. the chat app writes type "reaction" / value "👍".
|
||||
/// The same primitive backs pins, saves, flags, tags later. No chat vocabulary here.
|
||||
model IiosInteractionAnnotation {
|
||||
id String @id @default(cuid())
|
||||
scopeId String
|
||||
scope IiosScope @relation(fields: [scopeId], references: [id], onDelete: Cascade)
|
||||
targetInteractionId String
|
||||
target IiosInteraction @relation(fields: [targetInteractionId], references: [id], onDelete: Cascade)
|
||||
actorId String
|
||||
actor IiosActorRef @relation(fields: [actorId], references: [id])
|
||||
annotationType String
|
||||
value String
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([scopeId, targetInteractionId, actorId, annotationType, value])
|
||||
@@index([targetInteractionId])
|
||||
}
|
||||
|
||||
model IiosMessagePart {
|
||||
id String @id @default(cuid())
|
||||
interactionId String
|
||||
|
||||
Reference in New Issue
Block a user