From 942f40292eab6ea2c3d6025b8ff44653eb788f36 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Wed, 1 Jul 2026 07:24:52 +0530 Subject: [PATCH] feat(demo): P3.5 inbox sidebar + inbox smoke (P3 complete) message-demo gains a per-person Inbox rail (useInbox, done/snooze); sending surfaces a NEEDS_REPLY item in the other pane; reading auto-resolves it. smoke-inbox.mjs proves the event-driven flow end-to-end (send -> relay -> projector -> inbox -> read -> DONE). Realtime smoke still passes. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/message-demo/package.json | 1 + apps/message-demo/src/App.tsx | 30 ++++++++- packages/iios-service/scripts/smoke-inbox.mjs | 65 +++++++++++++++++++ pnpm-lock.yaml | 3 + 4 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 packages/iios-service/scripts/smoke-inbox.mjs diff --git a/apps/message-demo/package.json b/apps/message-demo/package.json index 437b8ab..a9099ba 100644 --- a/apps/message-demo/package.json +++ b/apps/message-demo/package.json @@ -10,6 +10,7 @@ "dependencies": { "@insignia/iios-kernel-client": "workspace:*", "@insignia/iios-message-web": "workspace:*", + "@insignia/iios-inbox-web": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/apps/message-demo/src/App.tsx b/apps/message-demo/src/App.tsx index 13a1472..65569b6 100644 --- a/apps/message-demo/src/App.tsx +++ b/apps/message-demo/src/App.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; import { MessageProvider, useThread, useMessages } from '@insignia/iios-message-web'; +import { InboxProvider, useInbox } from '@insignia/iios-inbox-web'; const SERVICE = 'http://localhost:3200'; const APP_ID = 'portal-demo'; @@ -83,12 +84,35 @@ function ChatInner({ ); } +function InboxSidebar() { + const { items, done, snooze } = useInbox({ state: 'OPEN' }); + return ( +
+ Inbox ({items.length}) + {items.length === 0 &&
nothing to reply to
} + {items.map((i) => ( +
+
{i.title}
+
{i.kind}
+ + +
+ ))} +
+ ); +} + function Pane(props: { token: string | null; label: string; threadId: string | null; onCreated?: (id: string) => void }) { if (!props.token) return
loading {props.label}…
; return ( - - - +
+ + + + + + +
); } diff --git a/packages/iios-service/scripts/smoke-inbox.mjs b/packages/iios-service/scripts/smoke-inbox.mjs new file mode 100644 index 0000000..384711a --- /dev/null +++ b/packages/iios-service/scripts/smoke-inbox.mjs @@ -0,0 +1,65 @@ +// P3 inbox smoke: Alice sends → Bob's inbox gets a NEEDS_REPLY item → Bob reads +// → item auto-resolves to DONE. Requires the service running (relay timer on), +// IIOS_DEV_TOKENS not needed (we sign locally). Run: node scripts/smoke-inbox.mjs +import 'dotenv/config'; +import { io } from 'socket.io-client'; +import jwt from 'jsonwebtoken'; + +const SERVICE = process.env.SMOKE_URL ?? 'http://localhost:3200'; +const APP_ID = 'portal-demo'; +const SECRET = JSON.parse(process.env.APP_SECRETS ?? '{"portal-demo":"dev-secret"}')[APP_ID]; + +const sign = (u) => jwt.sign({ sub: u, name: u, appId: APP_ID, orgId: `org_${APP_ID}` }, SECRET, { algorithm: 'HS256', expiresIn: '1h' }); +const connect = (u) => io(`${SERVICE}/message`, { auth: { token: sign(u) }, transports: ['websocket'], forceNew: true }); +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +const assert = (c, m) => { + if (!c) { console.error('✗', m); process.exit(1); } + console.log('✓', m); +}; + +async function inbox(token, state) { + const r = await fetch(`${SERVICE}/v1/inbox/items${state ? `?state=${state}` : ''}`, { + headers: { authorization: `Bearer ${token}` }, + }); + if (!r.ok) throw new Error(`inbox ${r.status}`); + return r.json(); +} +async function pollUntil(fn, ms = 8000) { + const end = Date.now() + ms; + while (Date.now() < end) { + const v = await fn(); + if (v) return v; + await sleep(300); + } + return null; +} + +const bobToken = sign('bob'); +const alice = connect('alice'); +const bob = connect('bob'); + +try { + const { threadId } = await alice.emitWithAck('open_thread', {}); + await bob.emitWithAck('open_thread', { threadId }); // Bob joins → participant + const sent = await alice.emitWithAck('send_message', { threadId, content: 'hi bob' }); + + const open = await pollUntil(async () => { + const items = await inbox(bobToken, 'OPEN'); + return items.find((i) => i.threadId === threadId && i.kind === 'NEEDS_REPLY') ?? null; + }); + assert(open, `Bob's inbox got an OPEN NEEDS_REPLY item (${open?.id})`); + + await bob.emitWithAck('read', { threadId, interactionId: sent.id }); + const done = await pollUntil(async () => { + const items = await inbox(bobToken); + const it = items.find((i) => i.id === open.id); + return it && it.state === 'DONE' ? it : null; + }); + assert(done, "Bob's item auto-resolved to DONE after reading"); + + console.log('\nP3 inbox smoke: PASS'); +} finally { + alice.close(); + bob.close(); +} +process.exit(0); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9182184..134f9fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: apps/message-demo: dependencies: + '@insignia/iios-inbox-web': + specifier: workspace:* + version: link:../../packages/iios-inbox-web '@insignia/iios-kernel-client': specifier: workspace:* version: link:../../packages/iios-kernel-client