@ttt-productions/chat-core 0.11.1 → 0.11.3

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.
@@ -1,4 +1,4 @@
1
- import type { ChatMessageV1 } from "./types.js";
1
+ import type { ChatMessageGroupKey, ChatMessageV1 } from "./types.js";
2
2
  /**
3
3
  * Determine if `msg` is a "continuation" of `prev` for the purpose of
4
4
  * visually grouping consecutive messages from the same sender in the
@@ -11,8 +11,11 @@ import type { ChatMessageV1 } from "./types.js";
11
11
  * - `msg.createdAt - prev.createdAt` is in the inclusive range
12
12
  * `[0, GROUP_GAP_SEC * 1000]` milliseconds. Negative gaps
13
13
  * (out-of-order messages) are treated as not-a-continuation.
14
+ * - When the app supplies `groupKey`, both messages have the same key — a
15
+ * continuation hides the sender line, so a per-message fact the app shows
16
+ * there must break the group when it changes.
14
17
  *
15
18
  * Pure function. No side effects. Safe to call from server and client.
16
19
  */
17
- export declare function isContinuation(prev: ChatMessageV1 | undefined, msg: ChatMessageV1): boolean;
20
+ export declare function isContinuation(prev: ChatMessageV1 | undefined, msg: ChatMessageV1, groupKey?: ChatMessageGroupKey): boolean;
18
21
  //# sourceMappingURL=grouping.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"grouping.d.ts","sourceRoot":"","sources":["../src/grouping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,GAAG,EAAE,aAAa,GACjB,OAAO,CAMT"}
1
+ {"version":3,"file":"grouping.d.ts","sourceRoot":"","sources":["../src/grouping.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGrE;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,aAAa,GAAG,SAAS,EAC/B,GAAG,EAAE,aAAa,EAClB,QAAQ,CAAC,EAAE,mBAAmB,GAC7B,OAAO,CAOT"}
package/dist/grouping.js CHANGED
@@ -11,16 +11,21 @@ import { GROUP_GAP_SEC } from "./types.js";
11
11
  * - `msg.createdAt - prev.createdAt` is in the inclusive range
12
12
  * `[0, GROUP_GAP_SEC * 1000]` milliseconds. Negative gaps
13
13
  * (out-of-order messages) are treated as not-a-continuation.
14
+ * - When the app supplies `groupKey`, both messages have the same key — a
15
+ * continuation hides the sender line, so a per-message fact the app shows
16
+ * there must break the group when it changes.
14
17
  *
15
18
  * Pure function. No side effects. Safe to call from server and client.
16
19
  */
17
- export function isContinuation(prev, msg) {
20
+ export function isContinuation(prev, msg, groupKey) {
18
21
  if (!prev)
19
22
  return false;
20
23
  if (msg.isSystemMessage || prev.isSystemMessage)
21
24
  return false;
22
25
  if (msg.senderId !== prev.senderId)
23
26
  return false;
27
+ if (groupKey && groupKey(msg) !== groupKey(prev))
28
+ return false;
24
29
  const gapMs = msg.createdAt - prev.createdAt;
25
30
  return gapMs >= 0 && gapMs <= GROUP_GAP_SEC * 1000;
26
31
  }
@@ -1 +1 @@
1
- {"version":3,"file":"grouping.js","sourceRoot":"","sources":["../src/grouping.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,cAAc,CAC5B,IAA+B,EAC/B,GAAkB;IAElB,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,eAAe;QAAE,OAAO,KAAK,CAAC;IAC9D,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACjD,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAC7C,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,aAAa,GAAG,IAAI,CAAC;AACrD,CAAC","sourcesContent":["import type { ChatMessageV1 } from \"./types.js\";\nimport { GROUP_GAP_SEC } from \"./types.js\";\n\n/**\n * Determine if `msg` is a \"continuation\" of `prev` for the purpose of\n * visually grouping consecutive messages from the same sender in the\n * message list.\n *\n * A message is a continuation when ALL of the following hold:\n * - `prev` exists.\n * - Neither `prev` nor `msg` is a system message.\n * - `prev` and `msg` have the same `senderId`.\n * - `msg.createdAt - prev.createdAt` is in the inclusive range\n * `[0, GROUP_GAP_SEC * 1000]` milliseconds. Negative gaps\n * (out-of-order messages) are treated as not-a-continuation.\n *\n * Pure function. No side effects. Safe to call from server and client.\n */\nexport function isContinuation(\n prev: ChatMessageV1 | undefined,\n msg: ChatMessageV1,\n): boolean {\n if (!prev) return false;\n if (msg.isSystemMessage || prev.isSystemMessage) return false;\n if (msg.senderId !== prev.senderId) return false;\n const gapMs = msg.createdAt - prev.createdAt;\n return gapMs >= 0 && gapMs <= GROUP_GAP_SEC * 1000;\n}\n"]}
1
+ {"version":3,"file":"grouping.js","sourceRoot":"","sources":["../src/grouping.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAC5B,IAA+B,EAC/B,GAAkB,EAClB,QAA8B;IAE9B,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,eAAe;QAAE,OAAO,KAAK,CAAC;IAC9D,IAAI,GAAG,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IACjD,IAAI,QAAQ,IAAI,QAAQ,CAAC,GAAG,CAAC,KAAK,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/D,MAAM,KAAK,GAAG,GAAG,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IAC7C,OAAO,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,aAAa,GAAG,IAAI,CAAC;AACrD,CAAC","sourcesContent":["import type { ChatMessageGroupKey, ChatMessageV1 } from \"./types.js\";\nimport { GROUP_GAP_SEC } from \"./types.js\";\n\n/**\n * Determine if `msg` is a \"continuation\" of `prev` for the purpose of\n * visually grouping consecutive messages from the same sender in the\n * message list.\n *\n * A message is a continuation when ALL of the following hold:\n * - `prev` exists.\n * - Neither `prev` nor `msg` is a system message.\n * - `prev` and `msg` have the same `senderId`.\n * - `msg.createdAt - prev.createdAt` is in the inclusive range\n * `[0, GROUP_GAP_SEC * 1000]` milliseconds. Negative gaps\n * (out-of-order messages) are treated as not-a-continuation.\n * - When the app supplies `groupKey`, both messages have the same key — a\n * continuation hides the sender line, so a per-message fact the app shows\n * there must break the group when it changes.\n *\n * Pure function. No side effects. Safe to call from server and client.\n */\nexport function isContinuation(\n prev: ChatMessageV1 | undefined,\n msg: ChatMessageV1,\n groupKey?: ChatMessageGroupKey,\n): boolean {\n if (!prev) return false;\n if (msg.isSystemMessage || prev.isSystemMessage) return false;\n if (msg.senderId !== prev.senderId) return false;\n if (groupKey && groupKey(msg) !== groupKey(prev)) return false;\n const gapMs = msg.createdAt - prev.createdAt;\n return gapMs >= 0 && gapMs <= GROUP_GAP_SEC * 1000;\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- export { MAX_CHAT_MESSAGE_LENGTH } from "./constants.js";
2
1
  export { GROUP_GAP_SEC } from "./types.js";
3
2
  export { isContinuation } from "./grouping.js";
4
- export type { ChatId, ChatThreadV1, ChatMessageV1, ChatAccessMode, ModerationHandlers, ChatNameResolver, ChatPrewarmSenders, } from "./types.js";
3
+ export type { ChatId, ChatThreadV1, ChatMessageV1, ChatMessageGroupKey, ModerationHandlers, ChatNameResolver, ChatNameResolution, ChatPrewarmSenders, } from "./types.js";
5
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,MAAM,EACN,YAAY,EACZ,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,GACrB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EACR,MAAM,EACN,YAAY,EACZ,aAAa,EACb,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,kBAAkB,GACrB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@
5
5
  // The chat React UI, hooks, Firestore-client adapter config, and render types
6
6
  // live in @ttt-productions/chat-react. The realtime wire contract and the pure
7
7
  // chat Zod schemas live in @ttt-productions/chat-schemas.
8
- export { MAX_CHAT_MESSAGE_LENGTH } from "./constants.js";
9
8
  export { GROUP_GAP_SEC } from "./types.js";
10
9
  export { isContinuation } from "./grouping.js";
11
10
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,8EAA8E;AAC9E,4EAA4E;AAC5E,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,0DAA0D;AAE1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAEzD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC","sourcesContent":["// Pure chat-core barrel. No React, no Firebase (client or admin) reachable from\n// here — safe for Cloud Functions, scripts, and future native/TV clients that\n// only need the contracts and grouping logic. Zero @ttt-productions/* deps.\n//\n// The chat React UI, hooks, Firestore-client adapter config, and render types\n// live in @ttt-productions/chat-react. The realtime wire contract and the pure\n// chat Zod schemas live in @ttt-productions/chat-schemas.\n\nexport { MAX_CHAT_MESSAGE_LENGTH } from \"./constants.js\";\n\nexport { GROUP_GAP_SEC } from \"./types.js\";\nexport { isContinuation } from \"./grouping.js\";\nexport type {\n ChatId,\n ChatThreadV1,\n ChatMessageV1,\n ChatAccessMode,\n ModerationHandlers,\n ChatNameResolver,\n ChatPrewarmSenders,\n} from \"./types.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,8EAA8E;AAC9E,4EAA4E;AAC5E,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,0DAA0D;AAE1D,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC","sourcesContent":["// Pure chat-core barrel. No React, no Firebase (client or admin) reachable from\n// here — safe for Cloud Functions, scripts, and future native/TV clients that\n// only need the contracts and grouping logic. Zero @ttt-productions/* deps.\n//\n// The chat React UI, hooks, Firestore-client adapter config, and render types\n// live in @ttt-productions/chat-react. The realtime wire contract and the pure\n// chat Zod schemas live in @ttt-productions/chat-schemas.\n\nexport { GROUP_GAP_SEC } from \"./types.js\";\nexport { isContinuation } from \"./grouping.js\";\nexport type {\n ChatId,\n ChatThreadV1,\n ChatMessageV1,\n ChatMessageGroupKey,\n ModerationHandlers,\n ChatNameResolver,\n ChatNameResolution,\n ChatPrewarmSenders,\n} from \"./types.js\";\n"]}
package/dist/types.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export type ChatId = string;
2
2
  export type ChatThreadV1 = {
3
- allowedUserIds: string[];
4
3
  participantUserIds?: string[];
5
4
  createdAt: number;
6
5
  lastMessageAt: number;
@@ -15,27 +14,16 @@ export type ChatMessageV1 = {
15
14
  text?: string;
16
15
  type?: string;
17
16
  isSystemMessage?: boolean;
17
+ /**
18
+ * Account ids a server-written message's text refers to, stored beside the text (never inside
19
+ * it). The UI prewarms and resolves them like senders; an account's anonymization rewrites them.
20
+ */
21
+ referencedUids?: string[];
18
22
  /** Moderation tombstone flag on the stored message (backend-written); consumers
19
23
  * render a tombstone instead of the content when true. */
20
24
  hidden?: boolean;
21
25
  meta?: Record<string, unknown>;
22
26
  };
23
- /**
24
- * Access mode controls how chat decides whether the current user can
25
- * read/write a thread.
26
- *
27
- * - "firestore-rules" — trust Firestore rules. canAccessThread returns true
28
- * for any signed-in user; if rules deny, onSnapshot will surface
29
- * permission-denied. Use this when access depends on data the client doesn't
30
- * reliably know up-front (entity membership, invite participation, etc.).
31
- *
32
- * - "explicit-allowlist" — access is enforced client-side via
33
- * threadAllowedUserIds. The list is required in this mode. Use this when the
34
- * consumer already knows the participants (admin/support threads).
35
- *
36
- * Admins (`isAdmin: true`) bypass both modes.
37
- */
38
- export type ChatAccessMode = "firestore-rules" | "explicit-allowlist";
39
27
  export type ModerationHandlers = {
40
28
  onReportMessage?: (messageId: string, reason?: string) => void | Promise<void>;
41
29
  onReportThread?: (threadId: string, reason?: string) => void | Promise<void>;
@@ -45,11 +33,34 @@ export type ModerationHandlers = {
45
33
  /** Max seconds between messages to be grouped as continuation */
46
34
  export declare const GROUP_GAP_SEC = 120;
47
35
  /**
48
- * Resolves a senderId to a display name synchronously from app-side cache.
49
- * Returns null if the sender is unknown or the cache hasn't loaded yet —
50
- * the chat UI will render a stable fallback ("User") in that case.
36
+ * The app's own per-message grouping fact: two consecutive messages from one sender group only
37
+ * when their keys are equal. `undefined` is an ordinary key value like any other.
38
+ */
39
+ export type ChatMessageGroupKey = (m: ChatMessageV1) => string | undefined;
40
+ /**
41
+ * What the app knows about a sender's display name right now. `pending` — the read has
42
+ * not answered; `unavailable` — the read answered and there is no person to name;
43
+ * `failed` — the read failed, and `retry` asks again. Only `resolved` carries a name:
44
+ * the chat UI never invents one for the other three, it hands them to the app's
45
+ * renderer (chat-react's `renderUnresolvedName`).
46
+ */
47
+ export type ChatNameResolution = {
48
+ status: 'resolved';
49
+ name: string;
50
+ } | {
51
+ status: 'pending';
52
+ } | {
53
+ status: 'unavailable';
54
+ } | {
55
+ status: 'failed';
56
+ retry: () => void;
57
+ };
58
+ /**
59
+ * Resolves a senderId synchronously from the app's own cache. Called during render,
60
+ * so it reads what the app already holds and never starts a fetch itself — the app
61
+ * fetches through {@link ChatPrewarmSenders}.
51
62
  */
52
- export type ChatNameResolver = (senderId: string) => string | null;
63
+ export type ChatNameResolver = (senderId: string) => ChatNameResolution;
53
64
  /**
54
65
  * Optional pre-warm callback. The chat UI calls this with the deduped list of
55
66
  * senderIds visible in the current message page so the consuming app can
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,YAAY,GAAG;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;8DAC0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAMF;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,cAAc,GAAG,iBAAiB,GAAG,oBAAoB,CAAC;AAMtE,MAAM,MAAM,kBAAkB,GAAG;IAC/B,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D,CAAC;AAMF,iEAAiE;AACjE,eAAO,MAAM,aAAa,MAAM,CAAC;AAMjC;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;AAEnE;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,MAAM,MAAM,YAAY,GAAG;IACzB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;8DAC0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC,CAAC;AAMF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D,CAAC;AAMF,iEAAiE;AACjE,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,EAAE,aAAa,KAAK,MAAM,GAAG,SAAS,CAAC;AAM3E;;;;;;GAMG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,aAAa,CAAA;CAAE,GACzB;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,IAAI,CAAA;CAAE,CAAC;AAE5C;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,CAAC;AAExE;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC"}
package/dist/types.js CHANGED
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // A message also carries NO reply pointer: no chat surface has an authoring
10
10
  // affordance for replying to a specific message, so a `replyTo` contract here
11
- // would be unreachable machinery (DJ ruling 2026-07-29).
11
+ // would be unreachable machinery.
12
12
  // ============================================
13
13
  // MESSAGE GROUPING (internal, exported for tests)
14
14
  // ============================================
package/dist/types.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,mBAAmB;AACnB,+CAA+C;AAC/C,EAAE;AACF,yEAAyE;AACzE,gFAAgF;AAChF,8EAA8E;AAC9E,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,yDAAyD;AA2DzD,+CAA+C;AAC/C,kDAAkD;AAClD,+CAA+C;AAE/C,iEAAiE;AACjE,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC","sourcesContent":["// ============================================\n// THREAD & MESSAGE\n// ============================================\n//\n// Chat is TEXT-ONLY. A message never carries a file: files belong to the\n// CONVERSATION (the Conversation Files list owned by the consuming app), not to\n// a message in its timeline. There is no attachment contract in this package.\n//\n// A message also carries NO reply pointer: no chat surface has an authoring\n// affordance for replying to a specific message, so a `replyTo` contract here\n// would be unreachable machinery (DJ ruling 2026-07-29).\n\nexport type ChatId = string;\n\nexport type ChatThreadV1 = {\n allowedUserIds: string[];\n participantUserIds?: string[];\n createdAt: number; // millis\n lastMessageAt: number; // millis\n status?: string; // opaque to chat-core\n meta?: Record<string, unknown>; // opaque\n};\n\nexport type ChatMessageV1 = {\n messageId: string;\n threadId: string;\n createdAt: number; // millis\n senderId: string;\n text?: string;\n type?: string; // optional for renderer registry\n isSystemMessage?: boolean;\n /** Moderation tombstone flag on the stored message (backend-written); consumers\n * render a tombstone instead of the content when true. */\n hidden?: boolean;\n meta?: Record<string, unknown>;\n};\n\n// ============================================\n// ACCESS\n// ============================================\n\n/**\n * Access mode controls how chat decides whether the current user can\n * read/write a thread.\n *\n * - \"firestore-rules\" — trust Firestore rules. canAccessThread returns true\n * for any signed-in user; if rules deny, onSnapshot will surface\n * permission-denied. Use this when access depends on data the client doesn't\n * reliably know up-front (entity membership, invite participation, etc.).\n *\n * - \"explicit-allowlist\" — access is enforced client-side via\n * threadAllowedUserIds. The list is required in this mode. Use this when the\n * consumer already knows the participants (admin/support threads).\n *\n * Admins (`isAdmin: true`) bypass both modes.\n */\nexport type ChatAccessMode = \"firestore-rules\" | \"explicit-allowlist\";\n\n// ============================================\n// MODERATION\n// ============================================\n\nexport type ModerationHandlers = {\n onReportMessage?: (messageId: string, reason?: string) => void | Promise<void>;\n onReportThread?: (threadId: string, reason?: string) => void | Promise<void>;\n onDeleteMessage?: (messageId: string) => void | Promise<void>; // admin only (gated)\n onDeleteThread?: (threadId: string) => void | Promise<void>; // admin only (gated)\n};\n\n// ============================================\n// MESSAGE GROUPING (internal, exported for tests)\n// ============================================\n\n/** Max seconds between messages to be grouped as continuation */\nexport const GROUP_GAP_SEC = 120;\n\n// ============================================\n// NAME RESOLUTION\n// ============================================\n\n/**\n * Resolves a senderId to a display name synchronously from app-side cache.\n * Returns null if the sender is unknown or the cache hasn't loaded yet —\n * the chat UI will render a stable fallback (\"User\") in that case.\n */\nexport type ChatNameResolver = (senderId: string) => string | null;\n\n/**\n * Optional pre-warm callback. The chat UI calls this with the deduped list of\n * senderIds visible in the current message page so the consuming app can\n * batch-fetch names into its cache. Implementations should be idempotent —\n * the same id list will be passed across re-renders.\n */\nexport type ChatPrewarmSenders = (senderIds: string[]) => void;\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,mBAAmB;AACnB,+CAA+C;AAC/C,EAAE;AACF,yEAAyE;AACzE,gFAAgF;AAChF,8EAA8E;AAC9E,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,kCAAkC;AA0ClC,+CAA+C;AAC/C,kDAAkD;AAClD,+CAA+C;AAE/C,iEAAiE;AACjE,MAAM,CAAC,MAAM,aAAa,GAAG,GAAG,CAAC","sourcesContent":["// ============================================\n// THREAD & MESSAGE\n// ============================================\n//\n// Chat is TEXT-ONLY. A message never carries a file: files belong to the\n// CONVERSATION (the Conversation Files list owned by the consuming app), not to\n// a message in its timeline. There is no attachment contract in this package.\n//\n// A message also carries NO reply pointer: no chat surface has an authoring\n// affordance for replying to a specific message, so a `replyTo` contract here\n// would be unreachable machinery.\n\nexport type ChatId = string;\n\nexport type ChatThreadV1 = {\n participantUserIds?: string[];\n createdAt: number; // millis\n lastMessageAt: number; // millis\n status?: string; // opaque to chat-core\n meta?: Record<string, unknown>; // opaque\n};\n\nexport type ChatMessageV1 = {\n messageId: string;\n threadId: string;\n createdAt: number; // millis\n senderId: string;\n text?: string;\n type?: string; // optional for renderer registry\n isSystemMessage?: boolean;\n /**\n * Account ids a server-written message's text refers to, stored beside the text (never inside\n * it). The UI prewarms and resolves them like senders; an account's anonymization rewrites them.\n */\n referencedUids?: string[];\n /** Moderation tombstone flag on the stored message (backend-written); consumers\n * render a tombstone instead of the content when true. */\n hidden?: boolean;\n meta?: Record<string, unknown>;\n};\n\n// ============================================\n// MODERATION\n// ============================================\n\nexport type ModerationHandlers = {\n onReportMessage?: (messageId: string, reason?: string) => void | Promise<void>;\n onReportThread?: (threadId: string, reason?: string) => void | Promise<void>;\n onDeleteMessage?: (messageId: string) => void | Promise<void>; // admin only (gated)\n onDeleteThread?: (threadId: string) => void | Promise<void>; // admin only (gated)\n};\n\n// ============================================\n// MESSAGE GROUPING (internal, exported for tests)\n// ============================================\n\n/** Max seconds between messages to be grouped as continuation */\nexport const GROUP_GAP_SEC = 120;\n\n/**\n * The app's own per-message grouping fact: two consecutive messages from one sender group only\n * when their keys are equal. `undefined` is an ordinary key value like any other.\n */\nexport type ChatMessageGroupKey = (m: ChatMessageV1) => string | undefined;\n\n// ============================================\n// NAME RESOLUTION\n// ============================================\n\n/**\n * What the app knows about a sender's display name right now. `pending` — the read has\n * not answered; `unavailable` — the read answered and there is no person to name;\n * `failed` — the read failed, and `retry` asks again. Only `resolved` carries a name:\n * the chat UI never invents one for the other three, it hands them to the app's\n * renderer (chat-react's `renderUnresolvedName`).\n */\nexport type ChatNameResolution =\n | { status: 'resolved'; name: string }\n | { status: 'pending' }\n | { status: 'unavailable' }\n | { status: 'failed'; retry: () => void };\n\n/**\n * Resolves a senderId synchronously from the app's own cache. Called during render,\n * so it reads what the app already holds and never starts a fetch itself — the app\n * fetches through {@link ChatPrewarmSenders}.\n */\nexport type ChatNameResolver = (senderId: string) => ChatNameResolution;\n\n/**\n * Optional pre-warm callback. The chat UI calls this with the deduped list of\n * senderIds visible in the current message page so the consuming app can\n * batch-fetch names into its cache. Implementations should be idempotent —\n * the same id list will be passed across re-renders.\n */\nexport type ChatPrewarmSenders = (senderIds: string[]) => void;\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/chat-core",
3
- "version": "0.11.1",
3
+ "version": "0.11.3",
4
4
  "description": "Pure chat contracts and grouping helpers for TTT Productions apps (no React, no Firebase)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,9 +27,6 @@
27
27
  "typecheck": "tsc --noEmit",
28
28
  "prepublishOnly": "npm run clean && npm run build"
29
29
  },
30
- "devDependencies": {
31
- "typescript": "^5.8.3"
32
- },
33
30
  "author": "DJ (TTT Productions)",
34
31
  "license": "MIT"
35
32
  }
@@ -1,7 +0,0 @@
1
- /**
2
- * Maximum chat message text length (characters). Consumer apps may override
3
- * by passing a custom value through `ChatCoreConfig`; this constant is the
4
- * package default and is also what chat-core's own internal schemas use.
5
- */
6
- export declare const MAX_CHAT_MESSAGE_LENGTH = 4000;
7
- //# sourceMappingURL=constants.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC"}
package/dist/constants.js DELETED
@@ -1,8 +0,0 @@
1
- // Chat-domain limits and operational constants.
2
- /**
3
- * Maximum chat message text length (characters). Consumer apps may override
4
- * by passing a custom value through `ChatCoreConfig`; this constant is the
5
- * package default and is also what chat-core's own internal schemas use.
6
- */
7
- export const MAX_CHAT_MESSAGE_LENGTH = 4000;
8
- //# sourceMappingURL=constants.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC","sourcesContent":["// Chat-domain limits and operational constants.\n\n/**\n * Maximum chat message text length (characters). Consumer apps may override\n * by passing a custom value through `ChatCoreConfig`; this constant is the\n * package default and is also what chat-core's own internal schemas use.\n */\nexport const MAX_CHAT_MESSAGE_LENGTH = 4000;\n"]}