@stapel/chat-react 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/MODULE.md +118 -0
- package/README.md +126 -0
- package/dist/api/chatApi.d.ts +49 -0
- package/dist/api/chatApi.d.ts.map +1 -0
- package/dist/api/chatApi.js +46 -0
- package/dist/api/chatApi.js.map +1 -0
- package/dist/api/extensions.d.ts +19 -0
- package/dist/api/extensions.d.ts.map +1 -0
- package/dist/api/extensions.js +19 -0
- package/dist/api/extensions.js.map +1 -0
- package/dist/api/generated/schema.d.ts +547 -0
- package/dist/api/generated/schema.d.ts.map +1 -0
- package/dist/api/generated/schema.js +2 -0
- package/dist/api/generated/schema.js.map +1 -0
- package/dist/api/types.d.ts +75 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/default/ConversationListPanel.d.ts +22 -0
- package/dist/default/ConversationListPanel.d.ts.map +1 -0
- package/dist/default/ConversationListPanel.js +49 -0
- package/dist/default/ConversationListPanel.js.map +1 -0
- package/dist/default/ConversationThreadPanel.d.ts +21 -0
- package/dist/default/ConversationThreadPanel.d.ts.map +1 -0
- package/dist/default/ConversationThreadPanel.js +51 -0
- package/dist/default/ConversationThreadPanel.js.map +1 -0
- package/dist/default/ErrorAlert.d.ts +8 -0
- package/dist/default/ErrorAlert.d.ts.map +1 -0
- package/dist/default/ErrorAlert.js +26 -0
- package/dist/default/ErrorAlert.js.map +1 -0
- package/dist/default/StartChatButton.d.ts +17 -0
- package/dist/default/StartChatButton.d.ts.map +1 -0
- package/dist/default/StartChatButton.js +18 -0
- package/dist/default/StartChatButton.js.map +1 -0
- package/dist/default/index.d.ts +21 -0
- package/dist/default/index.d.ts.map +1 -0
- package/dist/default/index.js +18 -0
- package/dist/default/index.js.map +1 -0
- package/dist/flows/errors.d.ts +12 -0
- package/dist/flows/errors.d.ts.map +1 -0
- package/dist/flows/errors.js +13 -0
- package/dist/flows/errors.js.map +1 -0
- package/dist/flows/freshness.d.ts +69 -0
- package/dist/flows/freshness.d.ts.map +1 -0
- package/dist/flows/freshness.js +227 -0
- package/dist/flows/freshness.js.map +1 -0
- package/dist/flows/registry.d.ts +26 -0
- package/dist/flows/registry.d.ts.map +1 -0
- package/dist/flows/registry.js +24 -0
- package/dist/flows/registry.js.map +1 -0
- package/dist/headless/ChatProvider.d.ts +22 -0
- package/dist/headless/ChatProvider.d.ts.map +1 -0
- package/dist/headless/ChatProvider.js +18 -0
- package/dist/headless/ChatProvider.js.map +1 -0
- package/dist/headless/ConversationList.d.ts +51 -0
- package/dist/headless/ConversationList.d.ts.map +1 -0
- package/dist/headless/ConversationList.js +47 -0
- package/dist/headless/ConversationList.js.map +1 -0
- package/dist/headless/ConversationThread.d.ts +50 -0
- package/dist/headless/ConversationThread.d.ts.map +1 -0
- package/dist/headless/ConversationThread.js +64 -0
- package/dist/headless/ConversationThread.js.map +1 -0
- package/dist/headless/MessageComposer.d.ts +39 -0
- package/dist/headless/MessageComposer.d.ts.map +1 -0
- package/dist/headless/MessageComposer.js +45 -0
- package/dist/headless/MessageComposer.js.map +1 -0
- package/dist/headless/StartDirectChat.d.ts +40 -0
- package/dist/headless/StartDirectChat.d.ts.map +1 -0
- package/dist/headless/StartDirectChat.js +46 -0
- package/dist/headless/StartDirectChat.js.map +1 -0
- package/dist/i18n/errorsMap.d.ts +10 -0
- package/dist/i18n/errorsMap.d.ts.map +1 -0
- package/dist/i18n/errorsMap.js +20 -0
- package/dist/i18n/errorsMap.js.map +1 -0
- package/dist/i18n/es.d.ts +23 -0
- package/dist/i18n/es.d.ts.map +1 -0
- package/dist/i18n/es.js +75 -0
- package/dist/i18n/es.js.map +1 -0
- package/dist/i18n/generated/errors.es.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.es.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.es.gen.js +58 -0
- package/dist/i18n/generated/errors.es.gen.js.map +1 -0
- package/dist/i18n/generated/errors.gen.d.ts +359 -0
- package/dist/i18n/generated/errors.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.gen.js +183 -0
- package/dist/i18n/generated/errors.gen.js.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts +16 -0
- package/dist/i18n/generated/errors.ru.gen.d.ts.map +1 -0
- package/dist/i18n/generated/errors.ru.gen.js +58 -0
- package/dist/i18n/generated/errors.ru.gen.js.map +1 -0
- package/dist/i18n/keys.d.ts +78 -0
- package/dist/i18n/keys.d.ts.map +1 -0
- package/dist/i18n/keys.js +130 -0
- package/dist/i18n/keys.js.map +1 -0
- package/dist/i18n/ru.d.ts +28 -0
- package/dist/i18n/ru.d.ts.map +1 -0
- package/dist/i18n/ru.js +80 -0
- package/dist/i18n/ru.js.map +1 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/model/context.d.ts +11 -0
- package/dist/model/context.d.ts.map +1 -0
- package/dist/model/context.js +16 -0
- package/dist/model/context.js.map +1 -0
- package/dist/model/limits.d.ts +12 -0
- package/dist/model/limits.d.ts.map +1 -0
- package/dist/model/limits.js +12 -0
- package/dist/model/limits.js.map +1 -0
- package/dist/model/mutations.d.ts +66 -0
- package/dist/model/mutations.d.ts.map +1 -0
- package/dist/model/mutations.js +156 -0
- package/dist/model/mutations.js.map +1 -0
- package/dist/model/queries.d.ts +33 -0
- package/dist/model/queries.d.ts.map +1 -0
- package/dist/model/queries.js +93 -0
- package/dist/model/queries.js.map +1 -0
- package/dist/model/queryKeys.d.ts +8 -0
- package/dist/model/queryKeys.d.ts.map +1 -0
- package/dist/model/queryKeys.js +26 -0
- package/dist/model/queryKeys.js.map +1 -0
- package/dist/model/readMarker.d.ts +25 -0
- package/dist/model/readMarker.d.ts.map +1 -0
- package/dist/model/readMarker.js +31 -0
- package/dist/model/readMarker.js.map +1 -0
- package/dist/model/runtime.d.ts +50 -0
- package/dist/model/runtime.d.ts.map +1 -0
- package/dist/model/runtime.js +27 -0
- package/dist/model/runtime.js.map +1 -0
- package/dist/model/threadWindow.d.ts +95 -0
- package/dist/model/threadWindow.d.ts.map +1 -0
- package/dist/model/threadWindow.js +100 -0
- package/dist/model/threadWindow.js.map +1 -0
- package/dist/nav/manifest.d.ts +23 -0
- package/dist/nav/manifest.d.ts.map +1 -0
- package/dist/nav/manifest.js +15 -0
- package/dist/nav/manifest.js.map +1 -0
- package/dist/realtime/chatSocket.d.ts +93 -0
- package/dist/realtime/chatSocket.d.ts.map +1 -0
- package/dist/realtime/chatSocket.js +199 -0
- package/dist/realtime/chatSocket.js.map +1 -0
- package/dist/realtime/frames.d.ts +123 -0
- package/dist/realtime/frames.d.ts.map +1 -0
- package/dist/realtime/frames.js +123 -0
- package/dist/realtime/frames.js.map +1 -0
- package/dist/realtime/streams.d.ts +49 -0
- package/dist/realtime/streams.d.ts.map +1 -0
- package/dist/realtime/streams.js +52 -0
- package/dist/realtime/streams.js.map +1 -0
- package/llms.txt +89 -0
- package/manifest.json +733 -0
- package/nav-manifest.json +25 -0
- package/package.json +114 -0
- package/src/analytics/generated/events.json +7 -0
- package/src/api/chatApi.ts +142 -0
- package/src/api/extensions.ts +18 -0
- package/src/api/generated/schema.ts +549 -0
- package/src/api/types.ts +87 -0
- package/src/default/ConversationListPanel.tsx +163 -0
- package/src/default/ConversationThreadPanel.tsx +245 -0
- package/src/default/ErrorAlert.tsx +43 -0
- package/src/default/StartChatButton.tsx +78 -0
- package/src/default/index.ts +20 -0
- package/src/flows/errors.ts +16 -0
- package/src/flows/freshness.ts +315 -0
- package/src/flows/registry.ts +36 -0
- package/src/headless/ChatProvider.tsx +23 -0
- package/src/headless/ConversationList.tsx +92 -0
- package/src/headless/ConversationThread.tsx +107 -0
- package/src/headless/MessageComposer.tsx +86 -0
- package/src/headless/StartDirectChat.tsx +75 -0
- package/src/i18n/errorsMap.ts +31 -0
- package/src/i18n/es.ts +95 -0
- package/src/i18n/generated/errors.es.gen.ts +64 -0
- package/src/i18n/generated/errors.gen.ts +214 -0
- package/src/i18n/generated/errors.json +408 -0
- package/src/i18n/generated/errors.ru.gen.ts +64 -0
- package/src/i18n/keys.ts +152 -0
- package/src/i18n/ru.ts +98 -0
- package/src/index.ts +189 -0
- package/src/model/context.tsx +25 -0
- package/src/model/limits.ts +11 -0
- package/src/model/mutations.ts +208 -0
- package/src/model/queries.ts +120 -0
- package/src/model/queryKeys.ts +32 -0
- package/src/model/readMarker.ts +32 -0
- package/src/model/runtime.ts +90 -0
- package/src/model/threadWindow.ts +173 -0
- package/src/nav/manifest.ts +36 -0
- package/src/realtime/chatSocket.ts +304 -0
- package/src/realtime/frames.ts +229 -0
- package/src/realtime/streams.ts +85 -0
- package/tsconfig.json +26 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# @stapel/chat-react
|
package/MODULE.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @stapel/chat-react — module guide
|
|
2
|
+
|
|
3
|
+
Headless React pair for **stapel-chat**. This is the human companion to the
|
|
4
|
+
generated `llms.txt` (agent context) and `manifest.json` (machine catalog).
|
|
5
|
+
|
|
6
|
+
## The one thing to understand first: the transport seam
|
|
7
|
+
|
|
8
|
+
stapel-chat can deliver the same journal two ways — the REST history, and its
|
|
9
|
+
own resumable WebSocket protocol (`ws/chat/<uuid:conversation_id>`, exported by
|
|
10
|
+
`stapel_chat.routing` since 0.2.2). This pair wires **both**, behind one hook,
|
|
11
|
+
and no component above that hook can tell which is running:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
useChatFreshness(streamKey, mapToQueryKeys, { fallbackRefetchInterval })
|
|
15
|
+
├── socket → hello{last_seq} → welcome → replay… → replay_done → live
|
|
16
|
+
│ frames, seq-deduped; error{resync} → re-hydrate
|
|
17
|
+
└── polling → a visibility-aware tick, exponential backoff on failures
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Both ends do the same thing with what they learn: **refetch the thread query**,
|
|
21
|
+
whose query function advances the window BY SEQ
|
|
22
|
+
(`GET …/messages?direction=prev&anchor=<tip>`). A socket frame does not carry
|
|
23
|
+
its payload into the cache — it says "there is something after your tip" and
|
|
24
|
+
the store goes and gets it. That is why the screens, and their tests, are
|
|
25
|
+
written once.
|
|
26
|
+
|
|
27
|
+
Writes never go over the socket. The `send` frame is typed (a mirror must be
|
|
28
|
+
complete) and never emitted: its refusals carry socket-local codes
|
|
29
|
+
(`empty`, `too_long`, …) that are not in the module's error registry, so they
|
|
30
|
+
have no i18n key and no remediation. `POST …/messages` answers with the
|
|
31
|
+
persisted row and a real error envelope.
|
|
32
|
+
|
|
33
|
+
**When `@stapel/realtime` phase 1 lands**, `src/flows/freshness.ts` is the whole
|
|
34
|
+
migration: `createChatSocket` goes away, `useSignalInvalidate` takes its place,
|
|
35
|
+
and this pair's tests must stay green with no edits. Nothing above that file
|
|
36
|
+
imports `realtime/`, so the blast radius is checkable rather than promised.
|
|
37
|
+
|
|
38
|
+
## Layers
|
|
39
|
+
|
|
40
|
+
- **api/** — `createChatApi(client)`; types alias the generated
|
|
41
|
+
`components["schemas"]` from stapel-chat's own `docs/schema.json` (never
|
|
42
|
+
parallel hand-written bodies). The support-operator half of the module
|
|
43
|
+
(`/support/queue`, assign / resolve / reopen) is deliberately absent — it is
|
|
44
|
+
an operator console, not the buyer-and-seller surface. `api/extensions.ts`
|
|
45
|
+
records the one request field that is NOT exposed and why.
|
|
46
|
+
- **realtime/** — the protocol, typed (`frames.ts`), the resumable client
|
|
47
|
+
(`chatSocket.ts`), and the stream keys plus the URL rule (`streams.ts`). No
|
|
48
|
+
React, no `@stapel/core`: plain protocol the substrate can subsume.
|
|
49
|
+
- **model/** — `chatQueryKeys` (one factory, `["chat"]` namespace),
|
|
50
|
+
`createChatRuntime` (which also resolves WHERE the socket is),
|
|
51
|
+
`threadWindow.ts` (the merge rules — the store's real logic, pure and
|
|
52
|
+
directly testable), `readMarker.ts`, queries and mutations.
|
|
53
|
+
- **flows/** — the transport seam (`freshness.ts`) and the error fold. The flow
|
|
54
|
+
REGISTRY is a zero-flow shim: stapel-chat annotates no `@flow_step`, and its
|
|
55
|
+
multi-step-ness lives in the transport, not in a server-declared funnel.
|
|
56
|
+
- **headless/** — render-prop components: `<ConversationList>`,
|
|
57
|
+
`<ConversationThread>`, `<MessageComposer>`, `<StartDirectChat>`, plus
|
|
58
|
+
`<ChatProvider>`. shadcn-copyable (frontend-standard §7).
|
|
59
|
+
- **default/** — the opt-in antd skin (`@stapel/chat-react/default`).
|
|
60
|
+
- **i18n/** — `CHAT_I18N_KEYS` + en bundle; the generated backend error bundle
|
|
61
|
+
is merged in so every `error.*` code has a fallback. See "Localization" below
|
|
62
|
+
for the one thing that is unusual here.
|
|
63
|
+
- **nav/** — one MEMBER entry (`chat.conversations`). The thread route is not a
|
|
64
|
+
menu destination and is mounted by the container under its own `:id` child.
|
|
65
|
+
|
|
66
|
+
## The thread store, in three rules
|
|
67
|
+
|
|
68
|
+
1. **`seq` is the order.** Gapless and total; ordering by `created_at` is an
|
|
69
|
+
upstream anti-pattern. Every merge sorts, dedupes and detects holes by seq.
|
|
70
|
+
2. **A window is contiguous or it is rebuilt.** A tail page that does not touch
|
|
71
|
+
the tip (or that the paginator flags as truncated with `has_prev`) is a
|
|
72
|
+
hole; the window is re-read from the newest page rather than stitched. That
|
|
73
|
+
is the REST twin of the socket's `error{resync}`.
|
|
74
|
+
3. **The same row can arrive twice** — the sender's own REST answer and the
|
|
75
|
+
socket's fan-out of it. `mergeMessage` drops a seq the window already holds,
|
|
76
|
+
which is what makes both paths idempotent.
|
|
77
|
+
|
|
78
|
+
## Localization — the twelve keys this pair owns
|
|
79
|
+
|
|
80
|
+
stapel-chat ships **English only**: it has no `translations/` directory, so its
|
|
81
|
+
12 error keys cannot appear in any locale catalogue upstream. `gen:errors` runs
|
|
82
|
+
for this pair with `ERRORS_LOCALE_EXEMPT_OWNERS=stapel_chat` and
|
|
83
|
+
`ERRORS_CATALOG_DIR` pointed at stapel-core's catalogue: the 42 cross-cutting
|
|
84
|
+
keys come from core in ru/es (typed `Partial`, so the gap is visible to
|
|
85
|
+
TypeScript), and the pair authors the 12 chat-owned texts in
|
|
86
|
+
`src/i18n/{ru,es}.ts` beside its UI copy. `test/i18n.test.ts` proves every
|
|
87
|
+
registry code resolves to a sentence in all three locales. When upstream ships
|
|
88
|
+
`translations/errors.{ru,es}.json`, those twelve lines are deleted and the
|
|
89
|
+
generated bundle covers them — the keys and the texts do not move.
|
|
90
|
+
|
|
91
|
+
Unlike notifications-react, the `es` subpath here carries hand-written UI copy
|
|
92
|
+
too: a marketplace's buyer-to-seller chat is the surface where a half-translated
|
|
93
|
+
screen shows most.
|
|
94
|
+
|
|
95
|
+
## Extension seams (frontend-standard §7)
|
|
96
|
+
|
|
97
|
+
- The client is injected via `<ChatProvider>` / core's `StapelConfigProvider`
|
|
98
|
+
(per-module override) — pairs never hard-import a client.
|
|
99
|
+
- The socket transport is injected: `createChatRuntime({ realtime: { webSocket } })`
|
|
100
|
+
takes any factory with `send`/`close` plus four callbacks, which is how the
|
|
101
|
+
tests drive the protocol frame by frame without a network.
|
|
102
|
+
- `realtime.socketUrl: null` turns the socket half off for a deployment that has
|
|
103
|
+
no sockets (WSGI, no channel layer). Everything keeps working on the timer.
|
|
104
|
+
- The headless layer is fully replaceable (copy-and-own); the antd skin is a
|
|
105
|
+
separate entry point nobody has to import.
|
|
106
|
+
|
|
107
|
+
## Not in this version
|
|
108
|
+
|
|
109
|
+
- **Attachments.** `SendMessageRequest.attachments` exists on the wire; wiring
|
|
110
|
+
it means wiring CDN upload rights into chat, and a control that is visible
|
|
111
|
+
but does nothing is worse than one that is absent.
|
|
112
|
+
- **The support console.** `/support/queue`, assign / resolve / reopen — an
|
|
113
|
+
operator surface, not this one.
|
|
114
|
+
- **A thread scoped to a listing.** `CreateConversationRequest.scope_key` is
|
|
115
|
+
ignored by the server (it resolves the scope itself), and a direct thread is
|
|
116
|
+
keyed by the participant PAIR — so buyer↔seller is one conversation across
|
|
117
|
+
every listing. A host that wants the listing named says so in the first
|
|
118
|
+
message.
|
package/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# @stapel/chat-react
|
|
2
|
+
|
|
3
|
+
Headless React pair for **stapel-chat** (frontend-standard §2). Business +
|
|
4
|
+
state only in the main entry, zero visual opinion — any design layers on top.
|
|
5
|
+
Built on `@stapel/core` (typed client + `StapelApiError` envelope, token
|
|
6
|
+
refresh, verification-403 interception, i18n engine, analytics seam, TanStack
|
|
7
|
+
Query).
|
|
8
|
+
|
|
9
|
+
Its backend delivers the same journal two ways — the REST history and its own
|
|
10
|
+
resumable WebSocket protocol — and this pair wires **both, behind one seam**, so
|
|
11
|
+
a deployment with sockets and one without run the same screens. See `MODULE.md`
|
|
12
|
+
for the layer map and the seam's replacement criterion.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pnpm add @stapel/chat-react @stapel/core @tanstack/react-query react
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Wire the app once
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
import { createI18n, StapelProvider } from "@stapel/core";
|
|
24
|
+
import { ChatProvider, createChatRuntime, registerChatI18n } from "@stapel/chat-react";
|
|
25
|
+
|
|
26
|
+
const runtime = createChatRuntime({
|
|
27
|
+
baseUrl: "/chat/api/v1",
|
|
28
|
+
// Sockets are derived from baseUrl's origin (`/ws/chat/`, the module's
|
|
29
|
+
// canonical mount). On a deployment that has none, say so — it goes
|
|
30
|
+
// straight to polling instead of failing a handshake six times first:
|
|
31
|
+
// realtime: { socketUrl: null },
|
|
32
|
+
});
|
|
33
|
+
const i18n = createI18n({ locale: "en" });
|
|
34
|
+
registerChatI18n(i18n);
|
|
35
|
+
|
|
36
|
+
export function Root({ children }: { children: React.ReactNode }) {
|
|
37
|
+
return (
|
|
38
|
+
<StapelProvider client={runtime.client} i18n={i18n} cacheVersion="0.1.0">
|
|
39
|
+
<ChatProvider runtime={runtime}>{children}</ChatProvider>
|
|
40
|
+
</StapelProvider>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The inbox
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { ConversationList } from "@stapel/chat-react";
|
|
49
|
+
import { matchList } from "@stapel/core";
|
|
50
|
+
|
|
51
|
+
<ConversationList>
|
|
52
|
+
{({ state }) =>
|
|
53
|
+
matchList(state, {
|
|
54
|
+
loading: () => <Spinner />,
|
|
55
|
+
failed: (error) => <ErrorPanel error={error} />,
|
|
56
|
+
empty: () => <p>{t("chat.list.empty")}</p>,
|
|
57
|
+
ready: (rows) => <List rows={rows} />, // rows carry `unread_count`
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
</ConversationList>;
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## A thread
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
import { ConversationThread, MessageComposer } from "@stapel/chat-react";
|
|
67
|
+
|
|
68
|
+
<ConversationThread conversationId={id}>
|
|
69
|
+
{({ state, hasOlder, loadOlder, transport }) => (
|
|
70
|
+
/* `transport` is "socket" | "polling" | "idle" — a LABEL. Nothing about
|
|
71
|
+
the thread's behaviour depends on it; that is the seam's whole point. */
|
|
72
|
+
...
|
|
73
|
+
)}
|
|
74
|
+
</ConversationThread>;
|
|
75
|
+
|
|
76
|
+
<MessageComposer conversationId={id}>
|
|
77
|
+
{({ value, setValue, availability, send }) => (
|
|
78
|
+
/* `availability` is blocked-with-a-reason or available — a disabled
|
|
79
|
+
control here always has a sentence to show. */
|
|
80
|
+
...
|
|
81
|
+
)}
|
|
82
|
+
</MessageComposer>;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The thread replays, then stays live; the read marker advances to the tip while
|
|
86
|
+
it is mounted and never moves backwards. Sending is REST — the persisted row
|
|
87
|
+
comes back with its `seq` and is folded into the window.
|
|
88
|
+
|
|
89
|
+
## "Message the seller"
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { StartDirectChat } from "@stapel/chat-react";
|
|
93
|
+
|
|
94
|
+
<StartDirectChat sellerId={listing.seller_id} viewerId={me?.id} onOpened={(c) => navigate(`/account/chat/${c.id}`)}>
|
|
95
|
+
{({ availability, start }) => ...}
|
|
96
|
+
</StartDirectChat>;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Get-or-create: a direct thread is keyed by the participant pair under a unique
|
|
100
|
+
constraint, so pressing twice — or writing to the same seller about a second
|
|
101
|
+
listing — lands in the same conversation.
|
|
102
|
+
|
|
103
|
+
## The antd skin (opt-in)
|
|
104
|
+
|
|
105
|
+
```tsx
|
|
106
|
+
import {
|
|
107
|
+
ConversationListPanel,
|
|
108
|
+
ConversationThreadPanel,
|
|
109
|
+
StartChatButton,
|
|
110
|
+
} from "@stapel/chat-react/default";
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Importing the subpath is the opt-in; consumers who bring their own visuals
|
|
114
|
+
never pull `antd` into their bundle.
|
|
115
|
+
|
|
116
|
+
## Locales
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { registerChatI18nRu } from "@stapel/chat-react/i18n/ru";
|
|
120
|
+
import { registerChatI18nEs } from "@stapel/chat-react/i18n/es";
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Opt-in subpaths, so a host that ships one language carries one language. Both
|
|
124
|
+
locales are complete over the pair's UI copy and over every backend error code
|
|
125
|
+
the module can raise — including the twelve stapel-chat owns and does not
|
|
126
|
+
localize upstream (see `MODULE.md`).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { StapelClient } from "@stapel/core";
|
|
2
|
+
import type { ChatMessage, Conversation, ConversationKind, ConversationListParams, ConversationPage, MessageHistoryParams, MessagePage, SendMessageRequest } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* The pair's typed operation surface — one method per stapel-chat endpoint a
|
|
5
|
+
* buyer/seller client may call, bound to the injected {@link StapelClient}
|
|
6
|
+
* (the per-module override seam of frontend-standard §7.2). Paths are relative
|
|
7
|
+
* to the runtime's `baseUrl` (`/chat/api/v1`).
|
|
8
|
+
*
|
|
9
|
+
* **WRITES NEVER GO OVER THE SOCKET.** stapel-chat's `ChatConsumer` accepts a
|
|
10
|
+
* `send` frame, and this pair deliberately does not use it: a message is a
|
|
11
|
+
* durable row with a locked `seq` allocation, and the REST path is the one
|
|
12
|
+
* that answers with the persisted row (and with a real error envelope when it
|
|
13
|
+
* refuses). The socket is a delivery convenience over that journal — see
|
|
14
|
+
* `src/realtime/`.
|
|
15
|
+
*
|
|
16
|
+
* The support-operator half of the module (`/support/queue`, assign / resolve
|
|
17
|
+
* / reopen) is intentionally absent: it is an operator console, not the
|
|
18
|
+
* buyer-and-seller surface this pair ships (spec §4.5 "Not in MVP").
|
|
19
|
+
*
|
|
20
|
+
* These operations will be GENERATED from schema.json operationIds by gen-api
|
|
21
|
+
* v2; until then they are hand-authored here (the ONE legal home of path
|
|
22
|
+
* strings — `stapel/no-string-paths` §2.3 carve-out).
|
|
23
|
+
*/
|
|
24
|
+
export interface ChatApi {
|
|
25
|
+
readonly client: StapelClient;
|
|
26
|
+
/** A page of the caller's conversations, newest activity first. */
|
|
27
|
+
conversations(params?: ConversationListParams): Promise<ConversationPage>;
|
|
28
|
+
/** One conversation (participant-only; 403 otherwise). */
|
|
29
|
+
conversation(conversationId: string): Promise<Conversation>;
|
|
30
|
+
/**
|
|
31
|
+
* Get-or-create a conversation. For `direct`, `participantIds` must name
|
|
32
|
+
* exactly one OTHER user: the thread is keyed by the (order-independent)
|
|
33
|
+
* participant pair, so a second call returns the same row — the idempotency
|
|
34
|
+
* "message the seller" is built on.
|
|
35
|
+
*/
|
|
36
|
+
createConversation(kind: ConversationKind, participantIds?: readonly string[]): Promise<Conversation>;
|
|
37
|
+
/** A page of message history, anchored on `seq` (see {@link MessageHistoryParams}). */
|
|
38
|
+
messages(conversationId: string, params?: MessageHistoryParams): Promise<MessagePage>;
|
|
39
|
+
/** Append a message. Resolves with the persisted row, carrying its `seq`. */
|
|
40
|
+
sendMessage(conversationId: string, body: SendMessageRequest): Promise<ChatMessage>;
|
|
41
|
+
/**
|
|
42
|
+
* Advance the caller's read marker. The server never moves it backwards
|
|
43
|
+
* (`services.mark_read`), and neither does this pair — see
|
|
44
|
+
* `model/readMarker.ts`.
|
|
45
|
+
*/
|
|
46
|
+
markRead(conversationId: string, uptoSeq: number): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export declare function createChatApi(client: StapelClient): ChatApi;
|
|
49
|
+
//# sourceMappingURL=chatApi.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatApi.d.ts","sourceRoot":"","sources":["../../src/api/chatApi.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAwB,MAAM,cAAc,CAAC;AACvE,OAAO,KAAK,EACV,WAAW,EACX,YAAY,EACZ,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,EAChB,oBAAoB,EACpB,WAAW,EACX,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAgCpB;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAE9B,mEAAmE;IACnE,aAAa,CAAC,MAAM,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1E,0DAA0D;IAC1D,YAAY,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IAC5D;;;;;OAKG;IACH,kBAAkB,CAChB,IAAI,EAAE,gBAAgB,EACtB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,GACjC,OAAO,CAAC,YAAY,CAAC,CAAC;IACzB,uFAAuF;IACvF,QAAQ,CACN,cAAc,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,WAAW,CAAC,CAAC;IACxB,6EAA6E;IAC7E,WAAW,CACT,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,WAAW,CAAC,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,cAAc,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,OAAO,CA2C3D"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSRF rule for cookie-authenticated browser clients (mirrors auth-react): the
|
|
3
|
+
* simplest SPA rule is to always send `X-Requested-With: XMLHttpRequest` on
|
|
4
|
+
* mutating requests. Header-token clients ignore it; it is harmless there, so
|
|
5
|
+
* every mutation carries it.
|
|
6
|
+
*/
|
|
7
|
+
const CSRF_HEADERS = {
|
|
8
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
9
|
+
};
|
|
10
|
+
function mutating(options) {
|
|
11
|
+
return {
|
|
12
|
+
...options,
|
|
13
|
+
headers: { ...CSRF_HEADERS, ...options?.headers },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Anchor/direction/limit → a query object, omitting what was not asked for. */
|
|
17
|
+
function pageQuery(params) {
|
|
18
|
+
const query = {};
|
|
19
|
+
if (params?.anchor !== undefined)
|
|
20
|
+
query.anchor = params.anchor;
|
|
21
|
+
if (params?.direction !== undefined)
|
|
22
|
+
query.direction = params.direction;
|
|
23
|
+
if (params?.limit !== undefined)
|
|
24
|
+
query.limit = params.limit;
|
|
25
|
+
return query;
|
|
26
|
+
}
|
|
27
|
+
export function createChatApi(client) {
|
|
28
|
+
const conversationPath = (id) => `/conversations/${encodeURIComponent(id)}`;
|
|
29
|
+
return {
|
|
30
|
+
client,
|
|
31
|
+
conversations: (params) => client.get("/conversations", { query: pageQuery(params) }),
|
|
32
|
+
conversation: (conversationId) => client.get(conversationPath(conversationId)),
|
|
33
|
+
createConversation: (kind, participantIds) => client.post("/conversations", {
|
|
34
|
+
kind,
|
|
35
|
+
...(participantIds !== undefined
|
|
36
|
+
? { participant_ids: [...participantIds] }
|
|
37
|
+
: {}),
|
|
38
|
+
}, mutating()),
|
|
39
|
+
messages: (conversationId, params) => client.get(`${conversationPath(conversationId)}/messages`, {
|
|
40
|
+
query: pageQuery(params),
|
|
41
|
+
}),
|
|
42
|
+
sendMessage: (conversationId, body) => client.post(`${conversationPath(conversationId)}/messages`, body, mutating()),
|
|
43
|
+
markRead: (conversationId, uptoSeq) => client.post(`${conversationPath(conversationId)}/read`, { upto_seq: uptoSeq }, mutating()),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=chatApi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chatApi.js","sourceRoot":"","sources":["../../src/api/chatApi.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,MAAM,YAAY,GAA2B;IAC3C,kBAAkB,EAAE,gBAAgB;CACrC,CAAC;AAEF,SAAS,QAAQ,CACf,OAAuD;IAEvD,OAAO;QACL,GAAG,OAAO;QACV,OAAO,EAAE,EAAE,GAAG,YAAY,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE;KAClD,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,SAAS,SAAS,CAChB,MAAiE;IAEjE,MAAM,KAAK,GAAoC,EAAE,CAAC;IAClD,IAAI,MAAM,EAAE,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC/D,IAAI,MAAM,EAAE,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;IACxE,IAAI,MAAM,EAAE,KAAK,KAAK,SAAS;QAAE,KAAK,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC5D,OAAO,KAAK,CAAC;AACf,CAAC;AA0DD,MAAM,UAAU,aAAa,CAAC,MAAoB;IAChD,MAAM,gBAAgB,GAAG,CAAC,EAAU,EAAU,EAAE,CAC9C,kBAAkB,kBAAkB,CAAC,EAAE,CAAC,EAAE,CAAC;IAE7C,OAAO;QACL,MAAM;QAEN,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE,CACxB,MAAM,CAAC,GAAG,CAAC,gBAAgB,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;QAE5D,YAAY,EAAE,CAAC,cAAc,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;QAE9E,kBAAkB,EAAE,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,CAC3C,MAAM,CAAC,IAAI,CACT,gBAAgB,EAChB;YACE,IAAI;YACJ,GAAG,CAAC,cAAc,KAAK,SAAS;gBAC9B,CAAC,CAAC,EAAE,eAAe,EAAE,CAAC,GAAG,cAAc,CAAC,EAAE;gBAC1C,CAAC,CAAC,EAAE,CAAC;SACR,EACD,QAAQ,EAAE,CACX;QAEH,QAAQ,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,EAAE,CACnC,MAAM,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,cAAc,CAAC,WAAW,EAAE;YACzD,KAAK,EAAE,SAAS,CAAC,MAAM,CAAC;SACzB,CAAC;QAEJ,WAAW,EAAE,CAAC,cAAc,EAAE,IAAI,EAAE,EAAE,CACpC,MAAM,CAAC,IAAI,CACT,GAAG,gBAAgB,CAAC,cAAc,CAAC,WAAW,EAC9C,IAAI,EACJ,QAAQ,EAAE,CACX;QAEH,QAAQ,EAAE,CAAC,cAAc,EAAE,OAAO,EAAE,EAAE,CACpC,MAAM,CAAC,IAAI,CACT,GAAG,gBAAgB,CAAC,cAAc,CAAC,OAAO,EAC1C,EAAE,QAAQ,EAAE,OAAO,EAAE,EACrB,QAAQ,EAAE,CACX;KACJ,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-authored API surface the codegen does not (yet) cover. Everything that
|
|
3
|
+
* CAN be derived from schema.json belongs in the generated operations
|
|
4
|
+
* (`api/chatApi.ts`), not here.
|
|
5
|
+
*
|
|
6
|
+
* ── One field the operation surface deliberately does NOT expose ────────────
|
|
7
|
+
*
|
|
8
|
+
* `CreateConversationRequest.scope_key` exists in the schema and its own
|
|
9
|
+
* description says the rest: *"Ignored — the scope is resolved server-side
|
|
10
|
+
* from the request"* (`views.ConversationListCreateView.post` calls
|
|
11
|
+
* `get_scope_provider().resolve(request)` and never reads the field). A client
|
|
12
|
+
* that sends a listing id there would believe it had scoped the thread to a
|
|
13
|
+
* listing; the server would key the direct thread by the participant PAIR, as
|
|
14
|
+
* it always does. Rather than ship a parameter that silently does nothing,
|
|
15
|
+
* `createConversation` has no `scopeKey` argument at all — the listing context
|
|
16
|
+
* belongs in the first MESSAGE, which is durable, visible, and actually read.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=extensions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extensions.d.ts","sourceRoot":"","sources":["../../src/api/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hand-authored API surface the codegen does not (yet) cover. Everything that
|
|
3
|
+
* CAN be derived from schema.json belongs in the generated operations
|
|
4
|
+
* (`api/chatApi.ts`), not here.
|
|
5
|
+
*
|
|
6
|
+
* ── One field the operation surface deliberately does NOT expose ────────────
|
|
7
|
+
*
|
|
8
|
+
* `CreateConversationRequest.scope_key` exists in the schema and its own
|
|
9
|
+
* description says the rest: *"Ignored — the scope is resolved server-side
|
|
10
|
+
* from the request"* (`views.ConversationListCreateView.post` calls
|
|
11
|
+
* `get_scope_provider().resolve(request)` and never reads the field). A client
|
|
12
|
+
* that sends a listing id there would believe it had scoped the thread to a
|
|
13
|
+
* listing; the server would key the direct thread by the participant PAIR, as
|
|
14
|
+
* it always does. Rather than ship a parameter that silently does nothing,
|
|
15
|
+
* `createConversation` has no `scopeKey` argument at all — the listing context
|
|
16
|
+
* belongs in the first MESSAGE, which is durable, visible, and actually read.
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=extensions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extensions.js","sourceRoot":"","sources":["../../src/api/extensions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,EAAE,CAAC"}
|