first commit
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
# Architecture
|
||||
|
||||
## The one idea
|
||||
|
||||
Three product surfaces — **Messaging**, **Inbox**, **Support** — are built on **one kernel**:
|
||||
a headless React SDK talking to a **BFF**. Everything a user sees is a projection of the same
|
||||
data model, exactly like the IIOS "everything is an interaction" spine (see
|
||||
[`IIOS_INTEGRATION.md`](IIOS_INTEGRATION.md)).
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ Browser (Next.js app · apps/web) │
|
||||
│ │
|
||||
│ Components (nav / message / inbox / support / overlays) │
|
||||
│ │ use hooks, never fetch directly │
|
||||
│ ┌────▼───────────────────────────────────────────┐ │
|
||||
│ │ @lynkd/messaging-inbox-sdk │ │
|
||||
│ │ Provider · hooks · theme · feature flags │ │
|
||||
│ │ BffClient ────────────────┐ │ │
|
||||
│ └──────────────────────────────┼───────────────────┘ │
|
||||
└──────────────────────────────────┼─────────────────────────────────────┘
|
||||
│ fetch /api/bff/** (the ONLY seam)
|
||||
┌──────────────────────────────────▼─────────────────────────────────────┐
|
||||
│ BFF (Next.js route handlers · apps/web/app/api/bff/**) │
|
||||
│ bootstrap · channels · messages · threads · reactions · inbox · │
|
||||
│ support/tickets · search · notifications │
|
||||
│ │ │
|
||||
│ ┌────▼─────────────────────────┐ (swap this box for real) │
|
||||
│ │ Mock store (in-memory) │ ──► IIOS service / NestJS / Supabase │
|
||||
│ └──────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Layers
|
||||
|
||||
### 1. SDK — `packages/messaging-inbox-sdk`
|
||||
Framework-agnostic-ish React layer (needs React 18/19). No knowledge of Next.js, no backend
|
||||
coupling.
|
||||
|
||||
- **`types.ts`** — the domain model. `User`, `Channel` (channel/private/dm/group_dm),
|
||||
`Message` (+ parts, reactions, attachments, thread metadata), `InboxItem`, `Ticket`,
|
||||
`Notification`, `SearchResult`, `Bootstrap`.
|
||||
- **`config.ts`** — `FeatureFlags` + `ThemeConfig` + `resolveConfig()` (defaults → env → props).
|
||||
- **`theme.ts`** — turns a `ThemeConfig` into CSS variables and applies them to `<html>`.
|
||||
- **`client.ts`** — `BffClient`: one typed method per BFF endpoint. **The only place that
|
||||
does `fetch`.**
|
||||
- **`provider.tsx`** — `<MessagingInboxProvider>` holds config, the client, bootstrap data,
|
||||
and theme/scheme state. Exposes `useSdk`, `useFeature(s)`, `useTheme`.
|
||||
- **`hooks.ts`** — data hooks: `useChannels`, `useMessages`, `useThread`, `useInbox`,
|
||||
`useTickets`, `useTicket`, `useSearch`, `useNotifications`, `useAvailability`, `useTyping`.
|
||||
|
||||
### 2. BFF — `apps/web/app/api/bff/**`
|
||||
Next.js **Route Handlers**. This *is* the Backend-For-Frontend: it shapes data specifically
|
||||
for this UI, keeps secrets/tokens server-side, and decouples the client from the real backend.
|
||||
Today each handler calls the in-memory **mock store**; each maps 1:1 to a `BffClient` method.
|
||||
|
||||
### 3. Mock store — `apps/web/lib/mock/`
|
||||
`data.ts` is the seed (a full LynkedUp-flavoured workspace). `store.ts` is a mutable singleton
|
||||
(persists across requests in one Node process) implementing send/react/pin/save/patch/reply/
|
||||
search. **This is the box you replace to go real** — nothing above it changes.
|
||||
|
||||
### 4. App shell + components — `apps/web/components`
|
||||
- `nav/` — `WorkspaceRail` (Slack-style far-left), `Sidebar` (sectioned channels + user card),
|
||||
`Header` (title + search + theme + notifications + user).
|
||||
- `message/` — `ChannelView`, `MessageItem`, `Composer`, `ThreadPanel`, `EmojiPicker`.
|
||||
- `inbox/` — `InboxView`.
|
||||
- `support/` — `SupportView` (master-detail console).
|
||||
- `overlays/` — `CommandPalette`, `NotificationsPanel`, `ProfileDrawer`, `ThemingPanel`,
|
||||
`HuddleBar`.
|
||||
- `ui/primitives.tsx` — `Avatar`, `PresenceDot`, `CountBadge`, `Chip`, `IconButton`.
|
||||
- `AppShell.tsx` composes rail + sidebar + main + thread panel + overlays.
|
||||
|
||||
## Request lifecycle (send a message)
|
||||
|
||||
1. `Composer` calls `send(body)` from `useMessages(channelId)`.
|
||||
2. The hook calls `client.sendMessage(channelId, body)` → `POST /api/bff/channels/:id/messages`.
|
||||
3. The route handler calls `store.send(...)`, which appends a `Message` and bumps thread/channel
|
||||
metadata, and returns it.
|
||||
4. The hook appends the returned message to local state → it renders instantly.
|
||||
|
||||
Reactions, pins, saves, inbox transitions, and ticket replies follow the same shape.
|
||||
|
||||
## Config lifecycle (flags + theme)
|
||||
|
||||
1. `app/layout.tsx` (server) calls `getServerConfig()` → `resolveConfig({ env: process.env })`.
|
||||
2. The resolved **plain** `SdkConfig` is passed to `<Providers config=…>` (client) and the
|
||||
theme is inlined into `<head>` for a flash-free first paint.
|
||||
3. `MessagingInboxProvider` applies theme vars to `<html>`, manages light/dark, and exposes
|
||||
flags via `useFeature`.
|
||||
|
||||
## Why these choices
|
||||
|
||||
See [`../.claude/memory/architecture-decisions.md`](../.claude/memory/architecture-decisions.md)
|
||||
for the rationale (workspace layout, BFF seam, server-resolved config, CSS-variable theming,
|
||||
data-driven flags, and why the NestJS/DB backend is documented rather than built for this demo).
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
# The BFF (Backend-For-Frontend)
|
||||
|
||||
The UI never talks to a real backend. It calls **its own BFF** — Next.js Route Handlers under
|
||||
`apps/web/app/api/bff/**`. This is the seam that keeps the frontend independent of whatever
|
||||
backend you eventually run (IIOS service, a NestJS BFF, Supabase, …).
|
||||
|
||||
## Why a BFF
|
||||
|
||||
- **One shaped API per frontend.** Endpoints return exactly what the UI needs, nothing more.
|
||||
- **Secrets stay server-side.** Tokens/keys for the real backend live in the route handlers,
|
||||
never in the browser bundle.
|
||||
- **Backend independence.** Change the handlers; the SDK + components don't move.
|
||||
|
||||
## Endpoint contract
|
||||
|
||||
Base path: `/api/bff`. All JSON. (These mirror the `BffClient` methods in the SDK.)
|
||||
|
||||
| Method | Path | Body / Query | Returns |
|
||||
|---|---|---|---|
|
||||
| GET | `/bootstrap` | — | `{ me, workspace, workspaces, users, sections, channels }` |
|
||||
| GET | `/channels` | — | `{ channels }` |
|
||||
| GET | `/channels/:id/messages` | — | `{ messages }` |
|
||||
| POST | `/channels/:id/messages` | `{ body, parentId?, scheduledFor? }` | `{ message }` (201) |
|
||||
| GET | `/channels/:id/threads/:parentId` | — | `{ parent, replies }` |
|
||||
| POST | `/channels/:id/messages/:mid/reactions` | `{ emoji }` | `{ message }` |
|
||||
| POST | `/channels/:id/messages/:mid/pin` | — | `{ message }` |
|
||||
| POST | `/channels/:id/messages/:mid/save` | — | `{ message }` |
|
||||
| GET | `/inbox` | `?state=OPEN\|SNOOZED\|DONE\|ARCHIVED` | `{ items }` |
|
||||
| PATCH | `/inbox/:id` | `{ state }` | `{ item }` |
|
||||
| GET | `/support/tickets` | `?scope=mine\|assigned\|all` | `{ tickets }` |
|
||||
| GET | `/support/tickets/:id` | — | `{ ticket }` |
|
||||
| PATCH | `/support/tickets/:id` | `{ state }` | `{ ticket }` |
|
||||
| POST | `/support/tickets/:id/reply` | `{ body }` | `{ ticket }` |
|
||||
| GET | `/support/availability` | — | `{ agents }` |
|
||||
| GET | `/search` | `?q=` | `{ results }` |
|
||||
| GET | `/notifications` | — | `{ notifications }` |
|
||||
|
||||
## The mock store
|
||||
|
||||
`apps/web/lib/mock/store.ts` is a module singleton seeded from `data.ts`. It persists across
|
||||
requests in one Node process (resets on restart). Each handler is a thin adapter:
|
||||
|
||||
```ts
|
||||
// app/api/bff/channels/[id]/messages/route.ts
|
||||
export async function POST(req, { params }) {
|
||||
const { id } = await params;
|
||||
const { body, parentId, scheduledFor } = await req.json();
|
||||
const message = store.send(id, body, { parentId, scheduledFor });
|
||||
return NextResponse.json({ message }, { status: 201 });
|
||||
}
|
||||
```
|
||||
|
||||
## Swapping in a real backend
|
||||
|
||||
Two options:
|
||||
|
||||
**A. Keep the BFF inside Next (recommended).** Replace the `store.*` calls with `fetch()` to
|
||||
your backend inside each handler. Add auth there. Example against IIOS:
|
||||
|
||||
```ts
|
||||
// app/api/bff/channels/[id]/messages/route.ts
|
||||
export async function GET(_req, { params }) {
|
||||
const { id } = await params;
|
||||
const token = await getIiosToken(); // server-side only
|
||||
const res = await fetch(`${process.env.IIOS_URL}/v1/threads/${id}/messages`, {
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
return NextResponse.json({ messages: data.messages.map(toSdkMessage) }); // map shapes
|
||||
}
|
||||
```
|
||||
|
||||
**B. Standalone NestJS BFF.** Point the SDK at it with `NEXT_PUBLIC_BFF_BASE_URL=https://bff…`
|
||||
and implement the same 17 endpoints in NestJS. The `BffClient` prefixes `/api/bff`, so mount
|
||||
your Nest routes under that prefix (or adjust `client.ts`).
|
||||
|
||||
Either way the SDK and every component are unchanged. See
|
||||
[`IIOS_INTEGRATION.md`](IIOS_INTEGRATION.md) for the full mapping.
|
||||
@@ -0,0 +1,83 @@
|
||||
# Feature flags
|
||||
|
||||
Every feature is gated by a flag. Flags default **on**. Override per-flag with an env var, or
|
||||
per-mount with a `flags` prop on `<MessagingInboxProvider>`.
|
||||
|
||||
## How it works
|
||||
|
||||
- Env var name = `NEXT_PUBLIC_FEATURE_` + the flag key in `UPPER_SNAKE_CASE`.
|
||||
e.g. `scheduledSend` → `NEXT_PUBLIC_FEATURE_SCHEDULED_SEND`.
|
||||
- Accepted truthy values: `true`, `1`, `on`, `yes`. Falsy: `false`, `0`, `off`, `no`.
|
||||
- Resolution order (last wins): built-in defaults → env → Provider props.
|
||||
- In components: `const on = useFeature("huddles")` or `const flags = useFeatureFlags()`.
|
||||
|
||||
## The flags
|
||||
|
||||
### Top-level surfaces
|
||||
| Flag | Env | Effect when off |
|
||||
|---|---|---|
|
||||
| `messaging` | `NEXT_PUBLIC_FEATURE_MESSAGING` | Hides channels/DMs in the sidebar & rail |
|
||||
| `inbox` | `NEXT_PUBLIC_FEATURE_INBOX` | Removes the Inbox nav + queue |
|
||||
| `support` | `NEXT_PUBLIC_FEATURE_SUPPORT` | Removes the Support Center surface |
|
||||
| `activity` | `NEXT_PUBLIC_FEATURE_ACTIVITY` | Reserved activity feed |
|
||||
|
||||
### Messaging sub-features
|
||||
| Flag | Effect when off |
|
||||
|---|---|
|
||||
| `threads` | No "reply in thread", no thread panel/summaries |
|
||||
| `reactions` | No emoji reactions or the reaction picker |
|
||||
| `mentions` | No @-mention button/highlighting |
|
||||
| `pins` | No pin action or pinned bar |
|
||||
| `savedItems` | No save/bookmark action or Saved page |
|
||||
| `fileUpload` | Hides the attach button |
|
||||
| `richComposer` | Composer drops its formatting toolbar |
|
||||
| `slashCommands` | Hides the `/` command affordance |
|
||||
| `scheduledSend` | Removes schedule-send menu + Drafts |
|
||||
| `drafts` | Hides Drafts & sent |
|
||||
| `huddles` | Removes huddle/call buttons + the huddle bar |
|
||||
| `presence` | Hides presence dots |
|
||||
| `typingIndicator` | No "… is typing" |
|
||||
| `readReceipts` | (reserved) |
|
||||
| `emojiPicker` | Removes emoji picker buttons |
|
||||
| `channelBrowser` | Hides "Add channels" / browse |
|
||||
| `directMessages` | Hides the DM section |
|
||||
| `groupDms` | (reserved) treats group DMs like channels |
|
||||
|
||||
### Support sub-features
|
||||
| Flag | Effect when off |
|
||||
|---|---|
|
||||
| `supportTickets` | (reserved) core tickets always render in Support |
|
||||
| `supportEscalate` | Removes the Escalate action |
|
||||
| `supportCallback` | Removes "Request callback" |
|
||||
| `supportAvailability` | Removes the online/offline availability pill |
|
||||
|
||||
### Global
|
||||
| Flag | Effect when off |
|
||||
|---|---|
|
||||
| `search` | Removes the header search + palette trigger |
|
||||
| `commandPalette` | (reserved) ⌘K behaviour |
|
||||
| `notifications` | Removes the bell + panel |
|
||||
| `themeToggle` | Removes the light/dark toggle |
|
||||
| `themingPanel` | Removes the live Appearance panel |
|
||||
| `workspaceSwitcher` | Removes the far-left workspace rail switcher |
|
||||
| `aiAssist` | (reserved) AI assist affordances |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# A lean messaging-only build
|
||||
NEXT_PUBLIC_FEATURE_SUPPORT=false
|
||||
NEXT_PUBLIC_FEATURE_INBOX=false
|
||||
NEXT_PUBLIC_FEATURE_HUDDLES=false
|
||||
NEXT_PUBLIC_FEATURE_SCHEDULED_SEND=false
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Per-mount override (e.g. embed a read-only chat)
|
||||
<MessagingInboxProvider flags={{ richComposer: false, huddles: false, support: false }}>
|
||||
…
|
||||
</MessagingInboxProvider>
|
||||
```
|
||||
|
||||
Add a new flag: extend `FeatureFlags` + `defaultFlags` in
|
||||
`packages/messaging-inbox-sdk/src/config.ts`; the env mapping is automatic.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Wiring the demo to the real IIOS service
|
||||
|
||||
This app talks to a pluggable **BFF backend**. Today it defaults to the in-memory mock; an
|
||||
**IIOS adapter is implemented** and switches on with one env var. This doc explains what's built,
|
||||
how to turn it on, and what still needs IIOS-side endpoints.
|
||||
|
||||
> Reference: `D:\iios\docs\IIOS_API_AND_SDK_GUIDE.md` (endpoints, auth, DTOs).
|
||||
|
||||
## The seam
|
||||
|
||||
Every BFF route calls `getBackend()` — never the store directly. `BFF_BACKEND` picks the impl:
|
||||
|
||||
```
|
||||
apps/web/lib/backend/
|
||||
Backend.ts the interface every route uses (all async)
|
||||
mock.ts MockBackend — wraps the in-memory store (default)
|
||||
iios.ts IiosBackend — proxies the real IIOS service
|
||||
index.ts getBackend() → mock | iios (from BFF_BACKEND)
|
||||
apps/web/lib/iios/
|
||||
client.ts server-only IIOS REST client (dev-token auth, lazy token mint)
|
||||
map.ts IIOS → SDK type mappers (toMessage / toInboxItem / toTicket)
|
||||
```
|
||||
|
||||
The SDK, hooks, and every UI component are unchanged between mock and IIOS — all translation lives
|
||||
in `lib/iios/map.ts`.
|
||||
|
||||
## Turn it on
|
||||
|
||||
1. **Run IIOS** (from `D:\iios`, needs Docker for Postgres):
|
||||
```bash
|
||||
docker compose up -d # Postgres on :5434
|
||||
pnpm install
|
||||
pnpm -F @insignia/iios-service prisma:generate
|
||||
pnpm -F @insignia/iios-service prisma:migrate
|
||||
cd packages/iios-service && IIOS_DEV_TOKENS=1 pnpm start # → :3200
|
||||
```
|
||||
2. **Point this app at it** (`apps/web/.env.local`):
|
||||
```bash
|
||||
BFF_BACKEND="iios"
|
||||
IIOS_URL="http://localhost:3200"
|
||||
IIOS_APP_ID="portal-demo"
|
||||
IIOS_USER_ID="james"
|
||||
IIOS_ORG_ID="org_A"
|
||||
```
|
||||
3. `pnpm dev` → the BFF now mints a dev token and proxies IIOS. `IIOS_*` are **server-only**
|
||||
(no `NEXT_PUBLIC_`), so the token never reaches the browser.
|
||||
|
||||
## ✅ Verified live (no Docker required)
|
||||
|
||||
The IIOS path has been run **end-to-end** against a real IIOS service + real Postgres, and the
|
||||
round-trip through this app's BFF was confirmed (a real ingested message came back through
|
||||
`GET /api/bff/channels/:threadId/messages`, and `/api/bff/support/tickets` returned IIOS's real
|
||||
data instead of the mock set). Docker isn't needed — a native PostgreSQL works.
|
||||
|
||||
Exact steps that worked on Windows (PostgreSQL 18 already installed):
|
||||
|
||||
```bash
|
||||
# 1. Create the iios role + database on the existing Postgres (port 5432)
|
||||
# (as the postgres superuser)
|
||||
psql -U postgres -c "CREATE ROLE iios LOGIN PASSWORD 'iios' CREATEDB;"
|
||||
psql -U postgres -c "CREATE DATABASE iios OWNER iios;"
|
||||
|
||||
# 2. Point IIOS at it — packages/iios-service/.env
|
||||
# DATABASE_URL="postgresql://iios:iios@localhost:5432/iios?schema=public"
|
||||
# (the repo default is :5434 via docker-compose; :5432 is fine too)
|
||||
# JWT_SECRET=... · APP_SECRETS={"portal-demo":"dev-secret"} · IIOS_DEV_TOKENS=1
|
||||
|
||||
# 3. From D:\iios — allow prisma/esbuild builds in pnpm-workspace.yaml, then:
|
||||
pnpm install
|
||||
pnpm -r build
|
||||
node packages/iios-service/node_modules/prisma/build/index.js generate --schema packages/iios-service/prisma/schema.prisma
|
||||
node packages/iios-service/node_modules/prisma/build/index.js migrate deploy --schema packages/iios-service/prisma/schema.prisma
|
||||
node packages/iios-service/dist/main.js # → iios-service listening on :3200
|
||||
|
||||
# 4. Point THIS app at it — apps/web/.env.local
|
||||
BFF_BACKEND=iios
|
||||
IIOS_URL=http://localhost:3200
|
||||
# then `pnpm dev` (restart) — the BFF now mints a dev token and proxies IIOS.
|
||||
```
|
||||
|
||||
> The mapper in `map.ts` was validated against the real IIOS message shape
|
||||
> (`{interactionId, actorId, occurredAt, parts:[{kind,bodyText}]}`). A **fresh IIOS DB is empty**
|
||||
> and IIOS has no channel-directory endpoint, so `bootstrap`/`listChannels` stay on mock (the demo
|
||||
> keeps its rich sidebar) while messages/inbox/tickets read from IIOS. Seed IIOS (via
|
||||
> `POST /v1/interactions/ingest` or its smoke scripts) to see live data flow through.
|
||||
|
||||
## What's wired vs. delegated
|
||||
|
||||
| BFF method | IIOS backing | Notes |
|
||||
|---|---|---|
|
||||
| `channelMessages` / `thread` | `GET /v1/threads/:id/messages` | maps parts→body, parent links |
|
||||
| `send` | `POST /v1/threads/:id/messages` (+ `idempotency-key`) | |
|
||||
| `listInbox` / `patchInbox` | `GET`/`PATCH /v1/inbox/items` | |
|
||||
| `listTickets` / `getTicket` / `patchTicket` | `/v1/support/tickets` | `getTicket` lists+finds (no single-GET documented) |
|
||||
| `replyTicket` | `POST` to the ticket's linked thread | |
|
||||
| `bootstrap` / `listChannels` / `createChannel` | **delegated to mock** | IIOS has no portal/channel-directory endpoint yet; build one, or compose from `/v1/threads` |
|
||||
| `react` / `pin` / `save` | **delegated to mock** | IIOS has no reaction/pin/save endpoints; keep local or add them |
|
||||
| `search` / `notifications` / `availability` | **delegated to mock** | add IIOS endpoints then swap |
|
||||
|
||||
Delegating keeps the UI fully functional while you fill the gaps — each is a small, isolated change
|
||||
in `iios.ts`.
|
||||
|
||||
## Auth
|
||||
|
||||
`lib/iios/client.ts#mintToken()` calls `POST /v1/dev/token` and caches the JWT (~100 min). For
|
||||
production, replace `mintToken()` with a real Session-Broker exchange (PPT), keep it server-side,
|
||||
and attach `Authorization: Bearer …` exactly as now.
|
||||
|
||||
## Shape mapping
|
||||
|
||||
Keep this app's `types.ts` as the UI contract; translate in `map.ts` (already defensive about field
|
||||
names). Example:
|
||||
|
||||
```ts
|
||||
export function toMessage(m: any, channelId: string): Message {
|
||||
const text = m.content ?? m.body ?? m.parts?.find(p => (p.kind ?? p.partType) === "TEXT")?.bodyText ?? "";
|
||||
return { id: String(m.id ?? m.interactionId), channelId,
|
||||
authorId: String(m.senderActorId ?? m.actorId ?? "unknown"),
|
||||
ts: Date.parse(m.sentAt ?? m.occurredAt) || Date.now(),
|
||||
body: String(text), parentId: m.parentInteractionId ?? null, reactions: [], deliveryState: "sent" };
|
||||
}
|
||||
```
|
||||
|
||||
## Tenancy & safety (carried by IIOS)
|
||||
|
||||
IIOS enforces tenant isolation (cross-tenant → 403), fail-closed policy/consent gates, and
|
||||
idempotency. The BFF just forwards the scoped token — don't reimplement those client-side. The UI's
|
||||
optimistic updates already tolerate a rejected write (surface an error, refetch).
|
||||
|
||||
## Realtime (optional upgrade)
|
||||
|
||||
Swap the mock `useTyping` + refetch for the IIOS Socket.IO client (`iios-kernel-client`'s
|
||||
`MessageSocket`) and feed `message`/`receipt` events into the same hook state. The component tree
|
||||
doesn't change.
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
# SDK reference — `@lynkd/messaging-inbox-sdk`
|
||||
|
||||
Headless React building blocks for messaging, inbox and support. Wrap your tree in the
|
||||
provider, then use the hooks. Bring your own UI, or reuse the demo components in
|
||||
`apps/web/components`.
|
||||
|
||||
## Install & provider
|
||||
|
||||
```tsx
|
||||
import { MessagingInboxProvider } from "@lynkd/messaging-inbox-sdk";
|
||||
|
||||
<MessagingInboxProvider
|
||||
bffBaseUrl="" // "" = same-origin /api/bff
|
||||
brandName="LynkedUp Pro"
|
||||
theme={{ mode: "dark", accent: "#FDA913", radiusCard: "20px" }}
|
||||
flags={{ huddles: true, support: true }}
|
||||
>
|
||||
<App />
|
||||
</MessagingInboxProvider>
|
||||
```
|
||||
|
||||
Prefer resolving config on the server and passing a single `config` prop (as the demo does via
|
||||
`getServerConfig()`), so env-driven flags/theme reach the client as plain JSON.
|
||||
|
||||
## Context hooks
|
||||
|
||||
| Hook | Returns |
|
||||
|---|---|
|
||||
| `useSdk()` | `{ config, flags, client, me, users, channels, userById, channelById, scheme, setScheme, toggleScheme, theme, setThemeOverrides, resetTheme, loading, refresh }` |
|
||||
| `useFeature(name)` | `boolean` for one flag |
|
||||
| `useFeatureFlags()` | the whole `FeatureFlags` object |
|
||||
| `useTheme()` | `{ scheme, setScheme, toggleScheme, theme, setThemeOverrides, resetTheme }` |
|
||||
|
||||
## Data hooks
|
||||
|
||||
| Hook | Purpose |
|
||||
|---|---|
|
||||
| `useChannels()` | `{ all, starred, channels, dms, totalUnread, totalMentions, me }` |
|
||||
| `useChannel(id)` | one `Channel` |
|
||||
| `useMessages(channelId)` | `{ messages, pinned, send, react, pin, save, reload, loading }` |
|
||||
| `useThread(channelId, parentId)` | `{ parent, replies, reply, reload }` |
|
||||
| `useTyping(channelId)` | `ID[]` currently typing (mock) |
|
||||
| `useInbox(state?)` | `{ items, state, setState, markDone, snooze, archive, reload }` |
|
||||
| `useTickets(scope?)` | `{ tickets, patch, reload }` |
|
||||
| `useTicket(id)` | `{ ticket, reply, setState, reload }` |
|
||||
| `useAvailability()` | `AgentAvailability[]` |
|
||||
| `useSearch()` | `{ query, setQuery, results, loading }` (debounced) |
|
||||
| `useNotifications()` | `{ notifications, unread }` |
|
||||
|
||||
## Minimal chat example
|
||||
|
||||
```tsx
|
||||
import { useChannels, useMessages } from "@lynkd/messaging-inbox-sdk";
|
||||
|
||||
function Chat() {
|
||||
const { channels } = useChannels();
|
||||
const active = channels[0]?.id ?? null;
|
||||
const { messages, send } = useMessages(active);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul>{messages.map((m) => <li key={m.id}>{m.body}</li>)}</ul>
|
||||
<form onSubmit={(e) => { e.preventDefault();
|
||||
const f = e.currentTarget.elements.namedItem("t") as HTMLInputElement;
|
||||
send(f.value); f.value = ""; }}>
|
||||
<input name="t" placeholder="Message…" />
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Direct client (no React)
|
||||
|
||||
```ts
|
||||
import { BffClient } from "@lynkd/messaging-inbox-sdk";
|
||||
const client = new BffClient(""); // base URL
|
||||
const { channels } = await client.listChannels();
|
||||
await client.sendMessage(channels[0].id, "Hi");
|
||||
```
|
||||
|
||||
## Types
|
||||
|
||||
Import any domain type: `User, Channel, Message, RichBlock, Attachment, Reaction, InboxItem,
|
||||
Ticket, CallbackRequest, AgentAvailability, Notification, SearchResult, Bootstrap, PresenceState,
|
||||
ChannelKind, InboxKind, InboxState, TicketState, TicketPriority` — plus config types
|
||||
`FeatureFlags, ThemeConfig, ThemeTokens, SdkConfig`.
|
||||
|
||||
## Publishing the SDK
|
||||
|
||||
The package is workspace-consumed as TS source. To publish standalone, add a build step (e.g.
|
||||
`tsup src/index.ts --dts --format esm,cjs`) and point `main`/`module`/`types` at `dist/`. The
|
||||
public surface is the `index.ts` barrel.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Local setup — step by step
|
||||
|
||||
A complete, from-scratch guide to running this demo on your machine. No database, no external
|
||||
services, no accounts. It's a static demo: mock data ships in code and every feature works.
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
| Tool | Version | Check |
|
||||
|---|---|---|
|
||||
| Node.js | ≥ 20 (tested on 24) | `node -v` |
|
||||
| pnpm | ≥ 8 (tested on 11) | `pnpm -v` |
|
||||
| Git | any | `git --version` |
|
||||
|
||||
Don't have pnpm? `npm install -g pnpm` (or `corepack enable`).
|
||||
|
||||
## 2. Install
|
||||
|
||||
```bash
|
||||
cd message-inbox-web-frontend-sdk
|
||||
pnpm install
|
||||
```
|
||||
|
||||
This installs the workspace (the SDK in `packages/` + the app in `apps/web`). First install pulls
|
||||
Next.js/React/Tailwind — on a slow connection it can take a few minutes.
|
||||
|
||||
> **If install or `pnpm dev` fails with a corepack error** (e.g. "Server answered with HTTP 404 …
|
||||
> pnpm-X.Y.Z.tgz"): this repo intentionally has **no** `packageManager` pin, so just use your local
|
||||
> pnpm. If your shell forces corepack, run `corepack disable` once, then `pnpm install` again.
|
||||
>
|
||||
> **If install times out on a large tarball** (slow network), retry with a longer timeout:
|
||||
> `pnpm install --fetch-timeout 2400000 --fetch-retries 10 --network-concurrency 2`.
|
||||
|
||||
## 3. Run
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open **http://localhost:4300**. You land in `#general` with a full mock workspace.
|
||||
|
||||
To run only the web app: `pnpm --filter @lynkd/web dev`.
|
||||
To change the port: edit `dev`/`start` scripts in `apps/web/package.json` (`next dev -p 4300`).
|
||||
|
||||
## 4. Configure (optional)
|
||||
|
||||
Everything works with defaults. To customize, copy the example env and edit:
|
||||
|
||||
```bash
|
||||
cp apps/web/.env.example apps/web/.env.local
|
||||
```
|
||||
|
||||
- **Feature flags** — `NEXT_PUBLIC_FEATURE_*` (all default on). e.g. `NEXT_PUBLIC_FEATURE_SUPPORT=false`.
|
||||
- **Theme** — `NEXT_PUBLIC_THEME_MODE`, `NEXT_PUBLIC_THEME_ACCENT`, `NEXT_PUBLIC_THEME_RADIUS_CARD`, …
|
||||
- **Branding** — `NEXT_PUBLIC_BRAND_NAME`.
|
||||
|
||||
Restart `pnpm dev` after changing env. Full references: [`FEATURE_FLAGS.md`](FEATURE_FLAGS.md),
|
||||
[`THEMING.md`](THEMING.md).
|
||||
|
||||
## 5. Two-minute demo tour
|
||||
|
||||
1. **Send** a message in `#general` (rich composer: select text → **Bold**/Italic/Code; ⏎ to send).
|
||||
2. **Hover** a message → react 🎉, reply in thread, pin, save, or the **⋯** menu.
|
||||
3. **⌘K / Ctrl+K** → search channels, people, messages, tickets.
|
||||
4. **Add channels** (sidebar) → create one; it appears and opens instantly.
|
||||
5. **Inbox** → work the queue (done / snooze / archive).
|
||||
6. **Support** → open **TCK-1042**, reply, **Resolve** (watch the list + stats update live).
|
||||
7. **Theme** → top-right ☀️/🌙, or the **Appearance** panel (accent, radius, gradient, System mode).
|
||||
8. **Start a huddle** (video icon) → floating call bar with working mute/video/share toggles.
|
||||
9. Resize the window narrow — the layout stays responsive (sidebar → drawer, support → single pane).
|
||||
|
||||
## 6. Common tasks
|
||||
|
||||
| Task | Where |
|
||||
|---|---|
|
||||
| Add/seed mock data | `apps/web/lib/mock/data.ts` |
|
||||
| Add a BFF endpoint | `apps/web/app/api/bff/<path>/route.ts` (+ `store` method + `BffClient` method) |
|
||||
| Add a feature flag | `packages/messaging-inbox-sdk/src/config.ts` |
|
||||
| Change default theme | `packages/messaging-inbox-sdk/src/config.ts` (`darkTokens`/`lightTokens`) |
|
||||
| Add a page | `apps/web/app/(app)/<route>/page.tsx` |
|
||||
|
||||
## 7. Build for production
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
pnpm start # serves the built app on :4300
|
||||
```
|
||||
|
||||
> The demo is not production-hardened, and the Next.js version is pinned for the demo (has a known
|
||||
> CVE). Bump Next and wire a real backend (see [`IIOS_INTEGRATION.md`](IIOS_INTEGRATION.md)) before
|
||||
> deploying.
|
||||
|
||||
## 8. Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
|---|---|
|
||||
| corepack 404 on pnpm | `corepack disable`, re-run (no pin in this repo) |
|
||||
| install times out | add `--fetch-timeout 2400000 --network-concurrency 2` |
|
||||
| port 4300 in use | change the port in `apps/web/package.json` scripts |
|
||||
| `store.X is not a function` after editing the store | bump `__lynkdStore_vN` in `lib/mock/store.ts` or restart `pnpm dev` (HMR singleton) |
|
||||
| styles look unstyled | ensure `pnpm dev` compiled Tailwind (check the terminal); hard-reload |
|
||||
| sent messages vanished | expected on server restart — the mock store is in-memory |
|
||||
@@ -0,0 +1,73 @@
|
||||
# Theming
|
||||
|
||||
The whole app is driven by **CSS variables** set on `<html>`. Tailwind colors map to those
|
||||
variables, so switching light/dark or rebranding is just different values — no component edits.
|
||||
Defaults match the LynkedUp Pro dashboard (dark + amber `#FDA913`).
|
||||
|
||||
## Three ways to theme
|
||||
|
||||
1. **Env (build/deploy time)** — `NEXT_PUBLIC_THEME_*`.
|
||||
2. **Provider props (per mount)** — `<MessagingInboxProvider theme={{ … }}>`.
|
||||
3. **Live in-app** — the **Appearance** panel (⚙️ by your name) sets accent/radius/gradient/mode
|
||||
and persists to `localStorage`.
|
||||
|
||||
## Env variables
|
||||
|
||||
| Var | Default | Notes |
|
||||
|---|---|---|
|
||||
| `NEXT_PUBLIC_THEME_MODE` | `dark` | `light` \| `dark` \| `system` |
|
||||
| `NEXT_PUBLIC_THEME_ACCENT` | `#FDA913` | any CSS color; overrides accent in both schemes |
|
||||
| `NEXT_PUBLIC_THEME_ACCENT_FG` | `#1a1205` | text/icon color on accent surfaces |
|
||||
| `NEXT_PUBLIC_THEME_RADIUS_CARD` | `20px` | card/panel radius |
|
||||
| `NEXT_PUBLIC_THEME_RADIUS_CONTROL` | `10px` | button/input radius |
|
||||
| `NEXT_PUBLIC_THEME_FONT_SANS` | Inter stack | any font-family string |
|
||||
| `NEXT_PUBLIC_THEME_GRADIENT` | amber→coral | brand mark / hero gradient |
|
||||
|
||||
## Token reference
|
||||
|
||||
Semantic tokens (Tailwind class → CSS var). Use these, never raw hex.
|
||||
|
||||
| Purpose | Tailwind | CSS var | Dark | Light |
|
||||
|---|---|---|---|---|
|
||||
| App background | `bg-bg` | `--c-bg` | `#060608` | `#f6f7f9` |
|
||||
| Sidebar / bars | `bg-surface` | `--c-surface` | `#08080b` | `#ffffff` |
|
||||
| Panels / cards | `bg-panel` | `--c-panel` | `#0e0e13` | `#ffffff` |
|
||||
| Elevated (menus) | `bg-elevated` | `--c-elevated` | `#15151c` | `#ffffff` |
|
||||
| Hover wash | `bg-hover` | `--c-hover` | white 5.5% | ink 4.5% |
|
||||
| Border | `border-border` | `--c-border` | white 8% | ink 10% |
|
||||
| Strong border | `border-border-strong` | `--c-border-strong` | white 14% | ink 16% |
|
||||
| Primary text | `text-ink` | `--c-ink` | `#ededf1` | `#14151a` |
|
||||
| Muted text | `text-muted` | `--c-muted` | `#8c8c94` | `#5c6069` |
|
||||
| Dim text | `text-dim` | `--c-dim` | `#63636b` | `#8b909a` |
|
||||
| Accent | `bg-accent` / `text-accent` | `--c-accent` | `#FDA913` | `#e8920a` |
|
||||
| Accent text | `text-accent-fg` | `--c-accent-fg` | `#1a1205` | `#ffffff` |
|
||||
| Accent tint | `bg-accent-soft` | `--c-accent-soft` | amber 12% | amber 12% |
|
||||
| Success/Warning/Danger/Info | `*-success/warning/danger/info` | `--c-success` … | — | — |
|
||||
| Card radius | `rounded-card` | `--r-card` | 20px | 20px |
|
||||
| Control radius | `rounded-control` | `--r-control` | 10px | 10px |
|
||||
|
||||
## Rebrand in 30 seconds
|
||||
|
||||
```bash
|
||||
# apps/web/.env.local
|
||||
NEXT_PUBLIC_THEME_MODE="light"
|
||||
NEXT_PUBLIC_THEME_ACCENT="#6c5ce7"
|
||||
NEXT_PUBLIC_THEME_RADIUS_CARD="14px"
|
||||
NEXT_PUBLIC_THEME_RADIUS_CONTROL="8px"
|
||||
NEXT_PUBLIC_THEME_GRADIENT="linear-gradient(135deg,#6c5ce7,#00cec9)"
|
||||
NEXT_PUBLIC_BRAND_NAME="Acme"
|
||||
```
|
||||
|
||||
Reload — sidebar, buttons, chips, gradient mark, focus rings, and both color schemes update.
|
||||
|
||||
## Editing the base palette
|
||||
|
||||
Change the `darkTokens` / `lightTokens` / `defaultTheme` objects in
|
||||
`packages/messaging-inbox-sdk/src/config.ts`. Keep the matching fallbacks in
|
||||
`apps/web/app/globals.css` (`:root { … }`) in sync so pre-hydration paint matches.
|
||||
|
||||
## How light/dark actually toggles
|
||||
|
||||
`theme.ts#applyTheme` writes the token vars to `document.documentElement.style` and toggles the
|
||||
`dark` class (Tailwind `darkMode: 'class'`). The provider persists the chosen scheme to
|
||||
`localStorage` (`lynkd.scheme`) and theme overrides to `lynkd.theme`.
|
||||
Reference in New Issue
Block a user