@ttt-productions/chat-react 0.13.15 → 0.13.17
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/README.md +3 -3
- package/dist/context/ChatNameResolverContext.d.ts +3 -26
- package/dist/context/ChatNameResolverContext.d.ts.map +1 -1
- package/dist/context/ChatNameResolverContext.js +3 -19
- package/dist/context/ChatNameResolverContext.js.map +1 -1
- package/dist/firestore/queries.js +1 -1
- package/dist/firestore/queries.js.map +1 -1
- package/dist/hooks/useChatMessages.d.ts +3 -7
- package/dist/hooks/useChatMessages.d.ts.map +1 -1
- package/dist/hooks/useChatMessages.js +20 -22
- package/dist/hooks/useChatMessages.js.map +1 -1
- package/dist/index.d.ts +32 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -14
- package/dist/index.js.map +1 -1
- package/dist/realtime/channel-client.d.ts +0 -248
- package/dist/realtime/channel-client.d.ts.map +1 -1
- package/dist/realtime/channel-client.js +102 -464
- package/dist/realtime/channel-client.js.map +1 -1
- package/dist/realtime/diagnostics.d.ts +0 -62
- package/dist/realtime/diagnostics.d.ts.map +1 -1
- package/dist/realtime/diagnostics.js +4 -89
- package/dist/realtime/diagnostics.js.map +1 -1
- package/dist/realtime/inbox-client.d.ts +0 -71
- package/dist/realtime/inbox-client.d.ts.map +1 -1
- package/dist/realtime/inbox-client.js +29 -109
- package/dist/realtime/inbox-client.js.map +1 -1
- package/dist/realtime/initial-load-budget.d.ts +1 -16
- package/dist/realtime/initial-load-budget.d.ts.map +1 -1
- package/dist/realtime/initial-load-budget.js +1 -20
- package/dist/realtime/initial-load-budget.js.map +1 -1
- package/dist/realtime/map.d.ts +0 -28
- package/dist/realtime/map.d.ts.map +1 -1
- package/dist/realtime/map.js +3 -37
- package/dist/realtime/map.js.map +1 -1
- package/dist/realtime/shared.d.ts +0 -79
- package/dist/realtime/shared.d.ts.map +1 -1
- package/dist/realtime/shared.js +2 -80
- package/dist/realtime/shared.js.map +1 -1
- package/dist/realtime/socket.d.ts +0 -15
- package/dist/realtime/socket.d.ts.map +1 -1
- package/dist/realtime/socket.js +1 -15
- package/dist/realtime/socket.js.map +1 -1
- package/dist/realtime/transport.d.ts +1 -25
- package/dist/realtime/transport.d.ts.map +1 -1
- package/dist/realtime/transport.js +1 -14
- package/dist/realtime/transport.js.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.d.ts +4 -21
- package/dist/realtime/useRealtimeChatMessages.d.ts.map +1 -1
- package/dist/realtime/useRealtimeChatMessages.js +5 -27
- package/dist/realtime/useRealtimeChatMessages.js.map +1 -1
- package/dist/realtime/wire.d.ts +0 -58
- package/dist/realtime/wire.d.ts.map +1 -1
- package/dist/realtime/wire.js +0 -22
- package/dist/realtime/wire.js.map +1 -1
- package/dist/types.d.ts +3 -41
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +1 -1
- package/dist/ui/ChatShell.d.ts +5 -47
- package/dist/ui/ChatShell.d.ts.map +1 -1
- package/dist/ui/ChatShell.js +39 -73
- package/dist/ui/ChatShell.js.map +1 -1
- package/dist/ui/Composer.d.ts +1 -14
- package/dist/ui/Composer.d.ts.map +1 -1
- package/dist/ui/Composer.js +12 -25
- package/dist/ui/Composer.js.map +1 -1
- package/dist/ui/MessageItemDefault.d.ts +2 -6
- package/dist/ui/MessageItemDefault.d.ts.map +1 -1
- package/dist/ui/MessageItemDefault.js +26 -56
- package/dist/ui/MessageItemDefault.js.map +1 -1
- package/dist/ui/MessageList.d.ts +3 -26
- package/dist/ui/MessageList.d.ts.map +1 -1
- package/dist/ui/MessageList.js +15 -28
- package/dist/ui/MessageList.js.map +1 -1
- package/dist/ui/MessageText.d.ts +0 -13
- package/dist/ui/MessageText.d.ts.map +1 -1
- package/dist/ui/MessageText.js.map +1 -1
- package/dist/ui/SenderName.d.ts +0 -5
- package/dist/ui/SenderName.d.ts.map +1 -1
- package/dist/ui/SenderName.js +3 -8
- package/dist/ui/SenderName.js.map +1 -1
- package/dist/ui/menus.d.ts +1 -1
- package/dist/ui/menus.d.ts.map +1 -1
- package/dist/ui/menus.js +5 -7
- package/dist/ui/menus.js.map +1 -1
- package/package.json +10 -10
- package/src/styles/chat.css +2 -15
- package/dist/realtime/index.d.ts +0 -19
- package/dist/realtime/index.d.ts.map +0 -1
- package/dist/realtime/index.js +0 -15
- package/dist/realtime/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"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"}
|
package/dist/realtime/wire.js
CHANGED
|
@@ -1,28 +1,7 @@
|
|
|
1
|
-
// Chat realtime WIRE PROTOCOL (Contract C) — the client side of the Channel +
|
|
2
|
-
// Inbox Durable Object protocol. The canonical protocol declarations (subprotocol,
|
|
3
|
-
// frame-kind maps, close codes, the neutral conversation reference, grant scope,
|
|
4
|
-
// the send and mark-read contracts, client-agreed limits) are owned by `@ttt-productions/chat-schemas`; this file consumes them
|
|
5
|
-
// and adds the client-side raw row/frame shapes the transport maps into the UI
|
|
6
|
-
// message shape. The client and the worker runtime agree on the wire by both
|
|
7
|
-
// importing that one contract; neither imports the other.
|
|
8
|
-
//
|
|
9
|
-
// Frame shape on the socket is `{ v, type, payload }` (NOT realtime-core's
|
|
10
|
-
// `{ v, kind, payload }`): the worker runtime `send`/`broadcast` emit
|
|
11
|
-
// `JSON.stringify({ v: 1, type, payload })` and read `env.type` + `env.payload`.
|
|
12
|
-
// We keep that field name here to match; realtime-core's envelope schema is the
|
|
13
|
-
// generic transport primitive, the chat protocol chose `type` for its frames.
|
|
14
|
-
// Re-export the canonical wire contract so this package's public surface is
|
|
15
|
-
// unchanged. `CLIENT_FRAME`/`SERVER_FRAME` are this package's historical names
|
|
16
|
-
// for the frame-kind maps (`CLIENT_KINDS`/`SERVER_KINDS` in the contract).
|
|
17
|
-
export { CHAT_SUBPROTOCOL, CHAT_WIRE_VERSION, CHAT_CLOSE_CODES, CLIENT_KINDS as CLIENT_FRAME, SERVER_KINDS as SERVER_FRAME,
|
|
18
|
-
// Correlated send-rejection contract (SERVER_FRAME.SEND_REJECTED payload).
|
|
19
|
-
ChatSendRejectedPayloadSchema, CHAT_SEND_REJECTION_CODES, CHAT_SEND_REJECTION_RETRYABLE, } from '@ttt-productions/chat-schemas';
|
|
20
1
|
import { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';
|
|
21
|
-
/** Build an outbound `{ v, type, payload }` frame (the wire format the DO reads). */
|
|
22
2
|
export function buildFrame(type, payload) {
|
|
23
3
|
return JSON.stringify({ v: CHAT_WIRE_VERSION, type, payload });
|
|
24
4
|
}
|
|
25
|
-
/** Parse an inbound frame, tolerating malformed JSON. Returns null on garbage. */
|
|
26
5
|
export function parseFrame(raw) {
|
|
27
6
|
try {
|
|
28
7
|
const parsed = JSON.parse(raw);
|
|
@@ -34,7 +13,6 @@ export function parseFrame(raw) {
|
|
|
34
13
|
return null;
|
|
35
14
|
}
|
|
36
15
|
}
|
|
37
|
-
/** A snapshot with `registry` is an inbox snapshot; one with `lastMessageSeq` is a channel snapshot. */
|
|
38
16
|
export function isInboxSnapshot(p) {
|
|
39
17
|
return Array.isArray(p.registry);
|
|
40
18
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/realtime/wire.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AA6DlE,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAgC;IACvE,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,UAAU,UAAU,CACxB,GAAW;IAEX,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAY,CAAC;QAC1C,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,IAAI,CAAC;QACvD,OAAO,MAA0E,CAAC;IACpF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,CAA0C;IAE1C,OAAO,KAAK,CAAC,OAAO,CAAE,CAAuB,CAAC,QAAQ,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["import { CHAT_WIRE_VERSION } from '@ttt-productions/chat-schemas';\nimport type {\n ChatConversationRef,\n ChatMarkReadResultPayload,\n ChatMessageRevisionKind,\n ChatSendRejectedPayload,\n} from '@ttt-productions/chat-schemas';\n\nexport type RevisionKind = ChatMessageRevisionKind;\n\nexport interface WireMessageRow {\n seq: number;\n senderUid: string;\n clientMessageId: string;\n text: string;\n createdAt: number;\n epoch: number;\n referencedUids?: string[];\n moderationKind?: RevisionKind | null;\n messageRevision?: number;\n}\n\nexport interface WireChannelSnapshot {\n lastMessageSeq: number;\n readSeq: number;\n resync?: boolean;\n delta?: WireMessageRow[];\n}\n\nexport interface WireRegistryEntry {\n channelRef: string;\n kind: string;\n state: 'active' | 'tombstoned';\n registryVersion: number;\n ref?: ChatConversationRef | null;\n unread?: boolean;\n archived?: boolean;\n}\n\nexport interface WireInboxSnapshot {\n registry: WireRegistryEntry[];\n hasUnread: boolean;\n}\n\n// Chat frames carry `type`, not realtime-core's `kind`: the deployed Worker emits and reads `type`.\nexport type ServerFrame =\n | { type: 'message'; payload: { message: WireMessageRow } }\n | { type: 'ack'; payload: { clientMessageId?: string; seq?: number; readSeq?: number } }\n | { type: 'history-page'; payload: { messages: WireMessageRow[] } }\n | { type: 'presence'; payload: { online: string[] } }\n | { type: 'typing'; payload: { uid: string; at: number } }\n | { type: 'snapshot'; payload: WireChannelSnapshot | WireInboxSnapshot }\n | { type: 'unread'; payload: WireInboxSnapshot | { hasUnread: boolean } }\n | { type: 'error'; payload: { code: string; retryAfterMs?: number } }\n | { type: 'mark-read-result'; payload: ChatMarkReadResultPayload }\n | { type: 'send-rejected'; payload: ChatSendRejectedPayload }\n | {\n type: 'revision';\n payload: { messageSeq: number; kind: RevisionKind; messageRevision: number };\n };\n\nexport function buildFrame(type: string, payload: Record<string, unknown>): string {\n return JSON.stringify({ v: CHAT_WIRE_VERSION, type, payload });\n}\n\nexport function parseFrame(\n raw: string,\n): { v?: number; type?: string; payload?: Record<string, unknown> } | null {\n try {\n const parsed = JSON.parse(raw) as unknown;\n if (!parsed || typeof parsed !== 'object') return null;\n return parsed as { v?: number; type?: string; payload?: Record<string, unknown> };\n } catch {\n return null;\n }\n}\n\nexport function isInboxSnapshot(\n p: WireChannelSnapshot | WireInboxSnapshot,\n): p is WireInboxSnapshot {\n return Array.isArray((p as WireInboxSnapshot).registry);\n}\n"]}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,30 +1,9 @@
|
|
|
1
|
-
import type { ReactNode } from
|
|
2
|
-
import type { ChatMessageV1 } from
|
|
3
|
-
import type { ChatConversationRef } from
|
|
4
|
-
/**
|
|
5
|
-
* Which transport backs a chat thread.
|
|
6
|
-
* - `firestore` — the Firestore newest-window + cursor path (default).
|
|
7
|
-
* - `realtime` — the Cloudflare Durable Object socket; the grant is the data-access
|
|
8
|
-
* authority there.
|
|
9
|
-
*/
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { ChatMessageV1 } from '@ttt-productions/chat-core';
|
|
3
|
+
import type { ChatConversationRef } from '@ttt-productions/chat-schemas';
|
|
10
4
|
export type ChatTransportMode = 'firestore' | 'realtime';
|
|
11
|
-
/**
|
|
12
|
-
* Realtime (Durable Object) transport handle. The concrete socket client is built
|
|
13
|
-
* in this package (`realtime/transport.ts`); chat-react consumes it through this
|
|
14
|
-
* config so the UI stays transport-agnostic. The app builds the `client` via
|
|
15
|
-
* `createRealtimeChatClient({ endpoint, channelRef, grantProvider, ... })` and
|
|
16
|
-
* passes it here. `client` stays structurally typed (`unknown` at the config seam)
|
|
17
|
-
* so `ChatCoreConfig` does not pull the realtime module's types into every firestore
|
|
18
|
-
* consumer — the realtime hook narrows it at the call site.
|
|
19
|
-
*/
|
|
20
5
|
export interface ChatRealtimeTransportConfig {
|
|
21
|
-
/** The conversation the socket is scoped to, as the app maps it onto the neutral reference. */
|
|
22
6
|
channelRef: ChatConversationRef;
|
|
23
|
-
/**
|
|
24
|
-
* The realtime client handle from `createRealtimeChatClient(...)`. Drives the
|
|
25
|
-
* channel socket (subscribe / send / ack / typing / presence / history). The UI
|
|
26
|
-
* never reaches into the socket directly — it goes through `useRealtimeChatMessages`.
|
|
27
|
-
*/
|
|
28
7
|
client: unknown;
|
|
29
8
|
}
|
|
30
9
|
export type ChatCoreConfig = {
|
|
@@ -34,28 +13,11 @@ export type ChatCoreConfig = {
|
|
|
34
13
|
currentUserId: string;
|
|
35
14
|
currentUserDisplayName?: string;
|
|
36
15
|
isAdmin: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Which transport backs this thread. Defaults to `'firestore'` (the current
|
|
39
|
-
* path) so existing call sites are unchanged. See {@link ChatTransportMode}.
|
|
40
|
-
*/
|
|
41
16
|
transport?: ChatTransportMode;
|
|
42
|
-
/** Required iff `transport === 'realtime'`; ignored on the firestore transport. */
|
|
43
17
|
realtime?: ChatRealtimeTransportConfig;
|
|
44
|
-
/**
|
|
45
|
-
* Whether the current user may read this conversation — the app's ONE access fact,
|
|
46
|
-
* decided by the app's own rule for the conversation's kind (the package never decides
|
|
47
|
-
* access). False renders the no-access state and reads nothing. On the `realtime`
|
|
48
|
-
* transport the grant is the data authority, and a terminal denial from the grant
|
|
49
|
-
* closes the conversation even when this is true.
|
|
50
|
-
*/
|
|
51
18
|
allowed: boolean;
|
|
52
19
|
createdAtField?: string;
|
|
53
20
|
pageSize?: number;
|
|
54
|
-
/**
|
|
55
|
-
* Firestore transport only: the app's own per-message facts, read from the stored message
|
|
56
|
-
* document and merged onto the mapped message's `meta`. The package never reads an app field
|
|
57
|
-
* itself; the app parses its own stored shape here and reads the result back from `meta`.
|
|
58
|
-
*/
|
|
59
21
|
messageMeta?: (doc: Record<string, unknown>) => Record<string, unknown> | undefined;
|
|
60
22
|
};
|
|
61
23
|
export type MessageRenderer = (m: ChatMessageV1) => ReactNode;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEzE,MAAM,MAAM,iBAAiB,GAAG,WAAW,GAAG,UAAU,CAAC;AAEzD,MAAM,WAAW,2BAA2B;IAC1C,UAAU,EAAE,mBAAmB,CAAC;IAChC,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,MAAM,cAAc,GAAG;IAC3B,kBAAkB,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,SAAS,CAAC,EAAE,iBAAiB,CAAC;IAC9B,QAAQ,CAAC,EAAE,2BAA2B,CAAC;IACvC,OAAO,EAAE,OAAO,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CACrF,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,EAAE,aAAa,KAAK,SAAS,CAAC;AAE9D,MAAM,MAAM,uBAAuB,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC"}
|
package/dist/types.js
CHANGED
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["import type { ReactNode } from 'react';\nimport type { ChatMessageV1 } from '@ttt-productions/chat-core';\nimport type { ChatConversationRef } from '@ttt-productions/chat-schemas';\n\nexport type ChatTransportMode = 'firestore' | 'realtime';\n\nexport interface ChatRealtimeTransportConfig {\n channelRef: ChatConversationRef;\n client: unknown;\n}\n\nexport type ChatCoreConfig = {\n chatCollectionPath: string | string[];\n messagesSubcollection?: string;\n threadId: string;\n currentUserId: string;\n currentUserDisplayName?: string;\n isAdmin: boolean;\n transport?: ChatTransportMode;\n realtime?: ChatRealtimeTransportConfig;\n allowed: boolean;\n createdAtField?: string;\n pageSize?: number;\n messageMeta?: (doc: Record<string, unknown>) => Record<string, unknown> | undefined;\n};\n\nexport type MessageRenderer = (m: ChatMessageV1) => ReactNode;\n\nexport type MessageRendererRegistry = Record<string, MessageRenderer>;\n"]}
|
package/dist/ui/ChatShell.d.ts
CHANGED
|
@@ -1,34 +1,15 @@
|
|
|
1
|
-
import * as React from
|
|
2
|
-
import type { ChatMessageGroupKey, ChatMessageV1, ModerationHandlers } from
|
|
3
|
-
import type { ChatCoreConfig, MessageRendererRegistry } from
|
|
4
|
-
import { type ChatScrollContainerProps } from
|
|
5
|
-
/**
|
|
6
|
-
* A read the chat could not complete, handed to the app's `renderLoadError`:
|
|
7
|
-
* - `initial` — nothing loaded: the first open failed (the realtime client's attempts
|
|
8
|
-
* failed for its budget, or the Firestore listener failed before any message). It
|
|
9
|
-
* takes the place of the message list.
|
|
10
|
-
* - `live` — the Firestore listener failed after messages loaded. The messages stay,
|
|
11
|
-
* with this band above them.
|
|
12
|
-
* - `older` — an older page failed to load. It renders at the top of the list, which
|
|
13
|
-
* stops asking for older pages until `retry`.
|
|
14
|
-
* `error` is the read's own error (null on the realtime transport); `retry` re-runs what
|
|
15
|
-
* failed — the realtime transport also keeps retrying in the background. `retrying` is true
|
|
16
|
-
* while a retry of this failure is running and the failure is still shown (an older page being
|
|
17
|
-
* read again), so the app's Retry shows its pending state. A retried initial or live failure
|
|
18
|
-
* gives way to the opening state instead.
|
|
19
|
-
*/
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import type { ChatMessageGroupKey, ChatMessageV1, ModerationHandlers } from '@ttt-productions/chat-core';
|
|
3
|
+
import type { ChatCoreConfig, MessageRendererRegistry } from '../types.js';
|
|
4
|
+
import { type ChatScrollContainerProps } from './MessageList.js';
|
|
20
5
|
export type ChatLoadFailure = {
|
|
21
|
-
scope:
|
|
6
|
+
scope: 'initial' | 'live' | 'older';
|
|
22
7
|
error: Error | null;
|
|
23
8
|
retry: () => void;
|
|
24
9
|
retrying: boolean;
|
|
25
10
|
};
|
|
26
11
|
export type ChatShellProps = {
|
|
27
12
|
config: ChatCoreConfig;
|
|
28
|
-
/**
|
|
29
|
-
* Renders every failed read (see {@link ChatLoadFailure}) — the app's own error state and
|
|
30
|
-
* copy. Required: a chat that cannot load must say so, and the words are the app's.
|
|
31
|
-
*/
|
|
32
13
|
renderLoadError: (failure: ChatLoadFailure) => React.ReactNode;
|
|
33
14
|
header?: React.ReactNode;
|
|
34
15
|
onSend?: (text: string) => Promise<void>;
|
|
@@ -39,38 +20,15 @@ export type ChatShellProps = {
|
|
|
39
20
|
autoFocus?: boolean;
|
|
40
21
|
renderAboveMessages?: () => React.ReactNode;
|
|
41
22
|
renderBelowMessages?: () => React.ReactNode;
|
|
42
|
-
/** If provided, replaces the Composer entirely */
|
|
43
23
|
renderFooter?: () => React.ReactNode;
|
|
44
24
|
onSenderClick?: (senderId: string) => void;
|
|
45
|
-
/** The app's per-message grouping fact and its sender-line adornment (forwarded to MessageList). */
|
|
46
25
|
messageGroupKey?: ChatMessageGroupKey;
|
|
47
26
|
renderSenderAdornment?: (m: ChatMessageV1) => React.ReactNode;
|
|
48
27
|
composerDisabled?: boolean;
|
|
49
|
-
/** Fill the parent's height instead of the default fixed-height card. Lays the Card out
|
|
50
|
-
* as `flex flex-col h-full` with the message list flexing to fill (scrolls inside), so a
|
|
51
|
-
* full-height page panel doesn't grow unbounded or waste space on a fixed 400px box.
|
|
52
|
-
* The consumer must give ChatShell a bounded-height parent (e.g. an `h-full` panel). */
|
|
53
28
|
fillHeight?: boolean;
|
|
54
|
-
/**
|
|
55
|
-
* Called with whether the conversation is READY — its access allowed, its initial data loaded
|
|
56
|
-
* with no failure, and (realtime) the channel current since its socket last opened — each time
|
|
57
|
-
* that changes, and with `false` when the shell unmounts. A consumer that lets someone act on
|
|
58
|
-
* what the conversation says (an agreement, an offer) offers and accepts that action only while
|
|
59
|
-
* it is true; reading the conversation needs no readiness.
|
|
60
|
-
*/
|
|
61
29
|
onReadinessChange?: (ready: boolean) => void;
|
|
62
|
-
/** Advanced override for the scrollable region's class (forwarded to MessageList).
|
|
63
|
-
* Usually unnecessary — prefer `fillHeight`. */
|
|
64
30
|
scrollClassName?: string;
|
|
65
|
-
/** Renders the scrollable message region (forwarded to MessageList) in place of the default div,
|
|
66
|
-
* so the consumer can give it its own surface component. */
|
|
67
31
|
scrollContainer?: React.ComponentType<ChatScrollContainerProps>;
|
|
68
32
|
};
|
|
69
|
-
/**
|
|
70
|
-
* ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the
|
|
71
|
-
* two transports live in separate inner components (each calls exactly ONE data
|
|
72
|
-
* hook) and both render the shared presentational `ChatShellView`. The default
|
|
73
|
-
* (undefined / 'firestore') transport is byte-for-byte the previous behavior.
|
|
74
|
-
*/
|
|
75
33
|
export declare function ChatShell(props: ChatShellProps): React.JSX.Element;
|
|
76
34
|
//# sourceMappingURL=ChatShell.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatShell.d.ts","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"ChatShell.d.ts","sourceRoot":"","sources":["../../src/ui/ChatShell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EACV,mBAAmB,EACnB,aAAa,EACb,kBAAkB,EACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,cAAc,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAa3E,OAAO,EAAe,KAAK,wBAAwB,EAAE,MAAM,kBAAkB,CAAC;AAI9E,MAAM,MAAM,eAAe,GAAG;IAC5B,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC;IACpC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,cAAc,CAAC;IAEvB,eAAe,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,KAAK,CAAC,SAAS,CAAC;IAE/D,MAAM,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAEzB,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEzC,aAAa,CAAC,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,KAAK,CAAC,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,uBAAuB,CAAC;IAC3C,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAE9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,mBAAmB,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAC5C,YAAY,CAAC,EAAE,MAAM,KAAK,CAAC,SAAS,CAAC;IAErC,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,eAAe,CAAC,EAAE,mBAAmB,CAAC;IACtC,qBAAqB,CAAC,EAAE,CAAC,CAAC,EAAE,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC;IAE9D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,iBAAiB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,KAAK,CAAC,aAAa,CAAC,wBAAwB,CAAC,CAAC;CACjE,CAAC;AAqBF,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,qBAK9C"}
|
package/dist/ui/ChatShell.js
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import * as React from
|
|
4
|
-
import { Card, CardHeader, CardContent, CardFooter, Skeleton, Spinner } from
|
|
5
|
-
import { KeyboardAvoidingView } from
|
|
6
|
-
import { useChatMessages } from
|
|
7
|
-
import { useRealtimeChatMessages } from
|
|
8
|
-
import { MessageList } from
|
|
9
|
-
import { Composer } from
|
|
10
|
-
import { ThreadActions } from
|
|
11
|
-
|
|
12
|
-
* ChatShell dispatches on `config.transport`. Hooks must be unconditional, so the
|
|
13
|
-
* two transports live in separate inner components (each calls exactly ONE data
|
|
14
|
-
* hook) and both render the shared presentational `ChatShellView`. The default
|
|
15
|
-
* (undefined / 'firestore') transport is byte-for-byte the previous behavior.
|
|
16
|
-
*/
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Card, CardHeader, CardContent, CardFooter, Skeleton, Spinner, } from '@ttt-productions/ui-core/react';
|
|
5
|
+
import { KeyboardAvoidingView } from '@ttt-productions/mobile-core/react';
|
|
6
|
+
import { useChatMessages } from '../hooks/useChatMessages.js';
|
|
7
|
+
import { useRealtimeChatMessages } from '../realtime/useRealtimeChatMessages.js';
|
|
8
|
+
import { MessageList } from './MessageList.js';
|
|
9
|
+
import { Composer } from './Composer.js';
|
|
10
|
+
import { ThreadActions } from './menus.js';
|
|
11
|
+
// Hooks must be unconditional, so each transport is a separate component with one data hook.
|
|
17
12
|
export function ChatShell(props) {
|
|
18
|
-
if (props.config.transport ===
|
|
13
|
+
if (props.config.transport === 'realtime') {
|
|
19
14
|
return _jsx(RealtimeChatShell, { ...props });
|
|
20
15
|
}
|
|
21
16
|
return _jsx(FirestoreChatShell, { ...props });
|
|
@@ -28,8 +23,10 @@ function FirestoreChatShell(props) {
|
|
|
28
23
|
await onSend(text);
|
|
29
24
|
}, [onSend]);
|
|
30
25
|
const { error, olderError, retry, messages, isFetchingOlder } = r;
|
|
31
|
-
const loadFailure = React.useMemo(() =>
|
|
32
|
-
|
|
26
|
+
const loadFailure = React.useMemo(() => error
|
|
27
|
+
? { scope: messages.length > 0 ? 'live' : 'initial', error, retry, retrying: false }
|
|
28
|
+
: null, [error, messages.length, retry]);
|
|
29
|
+
const olderFailure = React.useMemo(() => olderError ? { scope: 'older', error: olderError, retry, retrying: isFetchingOlder } : null, [olderError, retry, isFetchingOlder]);
|
|
33
30
|
return (_jsx(ChatShellView, { ...props, resolved: {
|
|
34
31
|
allowed: r.allowed,
|
|
35
32
|
isInitialLoading: r.isInitialLoading,
|
|
@@ -51,18 +48,15 @@ function RealtimeChatShell(props) {
|
|
|
51
48
|
}
|
|
52
49
|
const r = useRealtimeChatMessages(client);
|
|
53
50
|
const { initialLoadFailed, retry } = r;
|
|
54
|
-
const loadFailure = React.useMemo(() => (initialLoadFailed ? { scope:
|
|
51
|
+
const loadFailure = React.useMemo(() => (initialLoadFailed ? { scope: 'initial', error: null, retry, retrying: false } : null), [initialLoadFailed, retry]);
|
|
55
52
|
const send = React.useCallback(async (text) => {
|
|
56
53
|
const ok = r.send(text);
|
|
57
|
-
// C-B8: surface a closed-socket failure so the Composer keeps the user's text
|
|
58
|
-
// instead of clearing it on a no-op send.
|
|
59
54
|
if (!ok)
|
|
60
|
-
throw new Error(
|
|
55
|
+
throw new Error('chat-send-failed');
|
|
61
56
|
if (onSend)
|
|
62
|
-
await onSend(text);
|
|
57
|
+
await onSend(text);
|
|
63
58
|
}, [r, onSend]);
|
|
64
59
|
return (_jsx(ChatShellView, { ...props, resolved: {
|
|
65
|
-
// The app's access fact AND the grant's: a terminal denial closes it either way.
|
|
66
60
|
allowed: config.allowed && r.allowed,
|
|
67
61
|
isInitialLoading: r.isInitialLoading,
|
|
68
62
|
transportCurrent: r.isSynced,
|
|
@@ -82,9 +76,8 @@ function RealtimeChatShell(props) {
|
|
|
82
76
|
}
|
|
83
77
|
function ChatShellView(props) {
|
|
84
78
|
const { config, header, renderMessage, messageRenderers, handlers, composerPlaceholder, autoFocus = false, renderAboveMessages, renderBelowMessages, renderFooter, onSenderClick, messageGroupKey, renderSenderAdornment, composerDisabled, fillHeight = false, scrollClassName, scrollContainer, renderLoadError, onReadinessChange, resolved, } = props;
|
|
85
|
-
const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, loadFailure, olderFailure, send, retrySend, readAck, status, typing, signalTyping } = resolved;
|
|
86
|
-
|
|
87
|
-
const initialFailure = loadFailure?.scope === "initial" ? loadFailure : null;
|
|
79
|
+
const { allowed, isInitialLoading, messages, fetchOlder, hasOlder, isFetchingOlder, loadFailure, olderFailure, send, retrySend, readAck, status, typing, signalTyping, } = resolved;
|
|
80
|
+
const initialFailure = loadFailure?.scope === 'initial' ? loadFailure : null;
|
|
88
81
|
const ready = allowed && !isInitialLoading && loadFailure === null && resolved.transportCurrent;
|
|
89
82
|
const readinessListener = React.useRef(onReadinessChange);
|
|
90
83
|
React.useEffect(() => {
|
|
@@ -99,43 +92,36 @@ function ChatShellView(props) {
|
|
|
99
92
|
const [focused, setFocused] = React.useState(true);
|
|
100
93
|
const lastAckedRef = React.useRef(0);
|
|
101
94
|
const leaseHeldRef = React.useRef(false);
|
|
102
|
-
// The latest authoritative seq currently rendered (optimistic rows have no seq).
|
|
103
95
|
const latestSeq = React.useMemo(() => {
|
|
104
96
|
let max = 0;
|
|
105
97
|
for (const m of messages) {
|
|
106
|
-
const seq = typeof m.meta?.seq ===
|
|
98
|
+
const seq = typeof m.meta?.seq === 'number' ? m.meta?.seq : 0;
|
|
107
99
|
if (seq > max)
|
|
108
100
|
max = seq;
|
|
109
101
|
}
|
|
110
102
|
return max;
|
|
111
103
|
}, [messages]);
|
|
112
|
-
// Track tab focus/visibility so the read-ack "focused" lease releases on blur/hide.
|
|
113
104
|
React.useEffect(() => {
|
|
114
|
-
const compute = () => typeof document ===
|
|
115
|
-
(document.visibilityState !==
|
|
105
|
+
const compute = () => typeof document === 'undefined' ||
|
|
106
|
+
(document.visibilityState !== 'hidden' &&
|
|
107
|
+
(typeof document.hasFocus !== 'function' || document.hasFocus()));
|
|
116
108
|
setFocused(compute());
|
|
117
109
|
const onChange = () => setFocused(compute());
|
|
118
|
-
window.addEventListener(
|
|
119
|
-
window.addEventListener(
|
|
120
|
-
document.addEventListener(
|
|
110
|
+
window.addEventListener('focus', onChange);
|
|
111
|
+
window.addEventListener('blur', onChange);
|
|
112
|
+
document.addEventListener('visibilitychange', onChange);
|
|
121
113
|
return () => {
|
|
122
|
-
window.removeEventListener(
|
|
123
|
-
window.removeEventListener(
|
|
124
|
-
document.removeEventListener(
|
|
114
|
+
window.removeEventListener('focus', onChange);
|
|
115
|
+
window.removeEventListener('blur', onChange);
|
|
116
|
+
document.removeEventListener('visibilitychange', onChange);
|
|
125
117
|
};
|
|
126
118
|
}, []);
|
|
127
|
-
// C-B6: advance the authoritative read cursor when the reader is actually at the
|
|
128
|
-
// latest message (channel mounted + tab focused + scrolled to bottom); release the
|
|
129
|
-
// focus lease exactly once when they stop. No-op on the firestore path (no readAck).
|
|
130
119
|
React.useEffect(() => {
|
|
131
120
|
if (!readAck || latestSeq <= 0)
|
|
132
121
|
return;
|
|
133
122
|
if (focused && atBottom) {
|
|
134
123
|
leaseHeldRef.current = true;
|
|
135
124
|
if (latestSeq > lastAckedRef.current) {
|
|
136
|
-
// Advance the local marker ONLY if the frame was actually sent — a drop during a
|
|
137
|
-
// disconnect window must stay un-acked so it retries (the client also re-sends the
|
|
138
|
-
// cursor on reconnect). (M2)
|
|
139
125
|
if (readAck(latestSeq, true))
|
|
140
126
|
lastAckedRef.current = latestSeq;
|
|
141
127
|
}
|
|
@@ -145,49 +131,29 @@ function ChatShellView(props) {
|
|
|
145
131
|
readAck(latestSeq, false);
|
|
146
132
|
}
|
|
147
133
|
}, [readAck, latestSeq, atBottom, focused]);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const cardClassName = fillHeight ? "w-full flex flex-col h-full min-h-0" : "w-full";
|
|
151
|
-
const contentClassName = fillHeight ? "p-0 flex-1 min-h-0 flex flex-col" : "p-0";
|
|
134
|
+
const cardClassName = fillHeight ? 'w-full flex flex-col h-full min-h-0' : 'w-full';
|
|
135
|
+
const contentClassName = fillHeight ? 'p-0 flex-1 min-h-0 flex flex-col' : 'p-0';
|
|
152
136
|
if (!allowed) {
|
|
153
137
|
return _jsx("div", { className: "p-4 text-sm opacity-70", children: "You don't have access to this thread." });
|
|
154
138
|
}
|
|
155
139
|
if (isInitialLoading || initialFailure) {
|
|
156
|
-
// Before the first authoritative snapshot/history result, show an HONEST working
|
|
157
|
-
// state — a visible "Opening chat…" spinner in a polite live region — NOT a blank
|
|
158
|
-
// parchment/skeleton box (which read as an empty chat) and NOT the reconnect banner
|
|
159
|
-
// (which must not compete with the initial-opening message). Only the MessageList
|
|
160
|
-
// region is replaced: the consumer's header + actions + the three render-slots +
|
|
161
|
-
// footer still render, and `onReadinessChange` tells a consumer whose slots carry
|
|
162
|
-
// actions that the conversation is not ready to act on.
|
|
163
140
|
return (_jsxs(Card, { className: cardClassName, children: [(header || handlers) && (_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-4 space-y-0", children: [_jsx("div", { children: header }), _jsx("div", { className: "flex flex-col items-end gap-2", children: _jsx(ThreadActions, { threadId: config.threadId, isAdmin: config.isAdmin, handlers: handlers }) })] })), (() => {
|
|
164
|
-
// The wrapper (and its border) renders only when the slot actually
|
|
165
|
-
// produced content — a slot returning null (e.g. a collapsed
|
|
166
|
-
// conversation-files section) must not paint an empty bordered strip.
|
|
167
141
|
const aboveMessages = renderAboveMessages?.() ?? null;
|
|
168
142
|
return aboveMessages != null && aboveMessages !== false ? (_jsx("div", { className: "border-b", children: aboveMessages })) : null;
|
|
169
|
-
})(), _jsx(CardContent, { className: contentClassName, children: initialFailure ? (renderLoadError(initialFailure)) : (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground", role: "status", "aria-live": "polite", children: [_jsx(Spinner, { size: "sm" }), _jsx("span", { children: "Opening chat\u2026" })] })) }), renderBelowMessages &&
|
|
143
|
+
})(), _jsx(CardContent, { className: contentClassName, children: initialFailure ? (renderLoadError(initialFailure)) : (_jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-12 text-sm text-muted-foreground", role: "status", "aria-live": "polite", children: [_jsx(Spinner, { size: "sm" }), _jsx("span", { children: "Opening chat\u2026" })] })) }), renderBelowMessages && _jsx("div", { className: "border-t", children: renderBelowMessages() }), renderFooter ? (_jsx(CardFooter, { className: "border-t", children: renderFooter() })) : (_jsx(CardFooter, { className: "border-t", children: _jsx(Skeleton, { className: "h-10 w-full" }) }))] }));
|
|
170
144
|
}
|
|
171
|
-
return (_jsxs(Card, { className: cardClassName, children: [(header || handlers) && (_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-4 space-y-0", children: [_jsx("div", { children: header }), _jsx("div", { className: "flex flex-col items-end gap-2", children: _jsx(ThreadActions, { threadId: config.threadId, isAdmin: config.isAdmin, handlers: handlers }) })] })), _jsx(ChatConnectionBanner, { status: status }), renderAboveMessages &&
|
|
145
|
+
return (_jsxs(Card, { className: cardClassName, children: [(header || handlers) && (_jsxs(CardHeader, { className: "flex flex-row items-start justify-between gap-4 space-y-0", children: [_jsx("div", { children: header }), _jsx("div", { className: "flex flex-col items-end gap-2", children: _jsx(ThreadActions, { threadId: config.threadId, isAdmin: config.isAdmin, handlers: handlers }) })] })), _jsx(ChatConnectionBanner, { status: status }), renderAboveMessages && _jsx("div", { className: "border-b", children: renderAboveMessages() }), loadFailure?.scope === 'live' && (_jsx("div", { className: "border-b", children: renderLoadError(loadFailure) })), _jsx(CardContent, { className: contentClassName, children: _jsx(MessageList, { messages: messages, currentUserId: config.currentUserId, isAdmin: config.isAdmin, isFetchingOlder: isFetchingOlder, hasOlder: hasOlder, onLoadOlder: () => fetchOlder(), renderMessage: renderMessage, messageRenderers: messageRenderers, showScrollToBottom: showScrollToBottom, onScrollToBottom: () => setShowScrollToBottom(false), onAtBottomChange: setAtBottom, fillHeight: fillHeight, scrollClassName: scrollClassName, scrollContainer: scrollContainer, handlers: handlers, onSenderClick: onSenderClick, messageGroupKey: messageGroupKey, renderSenderAdornment: renderSenderAdornment, onRetrySend: retrySend, olderLoadErrorRow: olderFailure ? renderLoadError(olderFailure) : null }) }), renderBelowMessages && _jsx("div", { className: "border-t", children: renderBelowMessages() }), renderFooter ? (_jsx(CardFooter, { className: "border-t", children: renderFooter() })) : (_jsxs(CardFooter, { className: "border-t flex-col items-stretch gap-1", children: [_jsx(TypingIndicator, { typing: typing }), _jsx(KeyboardAvoidingView, { padding: true, offset: 8, className: "w-full", children: _jsx(Composer, { onSend: send, onTyping: signalTyping, disabled: composerDisabled, autoFocus: autoFocus, placeholder: composerPlaceholder }) })] }))] }));
|
|
172
146
|
}
|
|
173
|
-
/**
|
|
174
|
-
* R13 — connection-state banner. The realtime path sets `status`; once the first
|
|
175
|
-
* snapshot has loaded (ChatShellView is past `isInitialLoading`), a dropped/unreachable
|
|
176
|
-
* socket surfaces here instead of a silently-empty thread. Renders nothing on the
|
|
177
|
-
* firestore path (`status` undefined) or while live (`open`).
|
|
178
|
-
*/
|
|
179
147
|
function ChatConnectionBanner({ status }) {
|
|
180
|
-
if (status !==
|
|
148
|
+
if (status !== 'reconnecting' && status !== 'closed')
|
|
181
149
|
return null;
|
|
182
|
-
return (_jsx("div", { className: "px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b", role: "status", "aria-live": "polite", children: status ===
|
|
183
|
-
?
|
|
184
|
-
:
|
|
150
|
+
return (_jsx("div", { className: "px-3 py-1.5 text-xs text-center bg-muted text-muted-foreground border-b", role: "status", "aria-live": "polite", children: status === 'reconnecting'
|
|
151
|
+
? 'Reconnecting… messages may be delayed.'
|
|
152
|
+
: 'Disconnected — messages may be out of date.' }));
|
|
185
153
|
}
|
|
186
|
-
/** R14 — typing indicator. `typing` is the set of OTHER users typing (the transport
|
|
187
|
-
* excludes the local user). Generic copy (no name resolution) for v1. */
|
|
188
154
|
function TypingIndicator({ typing }) {
|
|
189
155
|
if (!typing || typing.length === 0)
|
|
190
156
|
return null;
|
|
191
|
-
return (_jsx("div", { className: "text-xs italic text-muted-foreground", "aria-live": "polite", children: typing.length === 1 ?
|
|
157
|
+
return (_jsx("div", { className: "text-xs italic text-muted-foreground", "aria-live": "polite", children: typing.length === 1 ? 'Someone is typing…' : 'Several people are typing…' }));
|
|
192
158
|
}
|
|
193
159
|
//# sourceMappingURL=ChatShell.js.map
|