@ai-matrx/messaging 0.10.4 → 0.11.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 +64 -0
- package/README.md +33 -0
- package/dist/index.cjs +102 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +101 -1
- package/dist/index.d.ts +101 -1
- package/dist/index.js +102 -13
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +149 -16
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +126 -1
- package/dist/react.d.ts +126 -1
- package/dist/react.js +149 -16
- package/dist/react.js.map +1 -1
- package/dist/styles.css +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
1
1
|
# Changelog — `@ai-matrx/messaging`
|
|
2
2
|
|
|
3
|
+
## 0.11.0 — 2026-09-10
|
|
4
|
+
|
|
5
|
+
### THE ARCHIVED-ITEMS LAW reaches conversations
|
|
6
|
+
|
|
7
|
+
> "everything should have an archive filter, and the default should always hide archived, but
|
|
8
|
+
> seeing archived items should be one or two clicks away … this is a system wide decision for
|
|
9
|
+
> every single item everywhere in our system, for every single table and every single page."
|
|
10
|
+
> — Arman, 2026-09-09 (`common-docs/policies/archived-items.md`)
|
|
11
|
+
|
|
12
|
+
This package could ARCHIVE a conversation — `setConversationFlags({ isArchived: true })` — and
|
|
13
|
+
had no read side for one. No filter, no parameter, no affordance anywhere could show it again,
|
|
14
|
+
and `get_dm_conversations_with_details` hardcoded the exclusion with no parameter of its own.
|
|
15
|
+
An archived conversation was not hidden. It was unreachable.
|
|
16
|
+
|
|
17
|
+
- **`MessagingArchiveFilter`** — the platform tri-state (`active` | `archived` | `all`), the
|
|
18
|
+
same three words as matrx-frontend's `lib/entity-list`, the `agx_`/`wfx_` list RPCs and
|
|
19
|
+
`@ai-matrx/design-system`'s `ArchiveFilter`. Shipped with
|
|
20
|
+
`DEFAULT_MESSAGING_ARCHIVE_FILTER` (`active`) and `toMessagingArchiveFilter()`, which
|
|
21
|
+
narrows an untrusted value and falls back to the default rather than silently widening a
|
|
22
|
+
list to archived rows. Three states, never a boolean: a boolean cannot say "archived only".
|
|
23
|
+
- **`repository.listConversations({ archived })`** passes `p_archived` to the RPC, which
|
|
24
|
+
gained that parameter on 2026-09-09 (register row R1). The axis is a REQUEST to the reader,
|
|
25
|
+
never a client-side sieve — so `hasMore`, the cursor and any count describe the rows the
|
|
26
|
+
caller actually asked for.
|
|
27
|
+
- **`repository.countArchivedConversations()`** answers the reveal's label. It is CAPPED and
|
|
28
|
+
says so: `{ count, exact }`, and `exact: false` must render "12+", never a confident "12".
|
|
29
|
+
- **The engine** carries the axis on every read — first page, "load more", and the reconnect
|
|
30
|
+
backfill — with `setArchiveFilter()` re-reading page one and refreshing the count. Flipping
|
|
31
|
+
the filter returns the store to NOT-LOADED, not to "loaded and empty": leaving the old rows
|
|
32
|
+
up renders active conversations under an "Archived" heading, and an empty "loaded" list
|
|
33
|
+
tells someone with forty archived chats she has none.
|
|
34
|
+
- **`ConversationList`** renders the reveal — **"Archived (N)"** → the archive → "← Back to
|
|
35
|
+
active conversations". One click, and it appears only when it would do something: while
|
|
36
|
+
hiding, when something is there to reveal; while showing, always, so the way back is never
|
|
37
|
+
missing. A button that changes nothing is a dead end, not an affordance.
|
|
38
|
+
- **`useConversations()`** returns `archiveFilter`, `setArchiveFilter` and `archivedCount`.
|
|
39
|
+
|
|
40
|
+
### Consumer action
|
|
41
|
+
|
|
42
|
+
- **Nothing breaks.** Every existing signature is unchanged and every default is the old
|
|
43
|
+
behaviour: a host that passes nothing still hides archived conversations.
|
|
44
|
+
- **Delete your local archive split.** A host that worked around this gap by fetching
|
|
45
|
+
everything and partitioning `ConversationSummary.isArchived` in its own component is now
|
|
46
|
+
reading rows the server would not have sent, and its counts and pagination describe a
|
|
47
|
+
different set from the one on screen. Drop the local split and the `include_archived`-style
|
|
48
|
+
boolean beside it; `ConversationList` carries the control, and a host list built on
|
|
49
|
+
`useConversations()` reads `archiveFilter` / `setArchiveFilter` / `archivedCount`.
|
|
50
|
+
- **`<MessagingProvider archiveFilter={…}>` is the knob** (root law 6, and clause 6 of the
|
|
51
|
+
law itself): the initial state is an org/user setting, not this package's taste. Wire it to
|
|
52
|
+
whatever your setting resolves to; omit it and you get the platform default, `active`.
|
|
53
|
+
|
|
54
|
+
## 0.10.5 — 2026-09-09
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- Membership and new-conversation realtime refresh failures now reach the engine diagnostic
|
|
59
|
+
handler with their original permission classification instead of escaping as unhandled
|
|
60
|
+
promises. Inbox reads do not start after disposal, and late completions do not update the
|
|
61
|
+
disposed engine's store.
|
|
62
|
+
|
|
63
|
+
### Consumer action (C28)
|
|
64
|
+
|
|
65
|
+
Upgrade the package. No host API changes or database permission changes.
|
|
66
|
+
|
|
3
67
|
## 0.10.4 — 2026-09-08
|
|
4
68
|
|
|
5
69
|
### Fixed
|
package/README.md
CHANGED
|
@@ -333,6 +333,39 @@ import { createMessagingEngine, createMessagingRepository } from "@ai-matrx/mess
|
|
|
333
333
|
|
|
334
334
|
---
|
|
335
335
|
|
|
336
|
+
## Archived conversations
|
|
337
|
+
|
|
338
|
+
THE ARCHIVED-ITEMS LAW (Arman, 2026-09-09 — `common-docs/policies/archived-items.md`): every
|
|
339
|
+
list over an entity that can be archived carries an archive filter, the default hides archived
|
|
340
|
+
rows, and revealing them is one or two clicks. Three states, and no more:
|
|
341
|
+
|
|
342
|
+
| value | what the list shows |
|
|
343
|
+
|---|---|
|
|
344
|
+
| `active` | only un-archived conversations (the default) |
|
|
345
|
+
| `archived` | only archived conversations |
|
|
346
|
+
| `all` | every conversation |
|
|
347
|
+
|
|
348
|
+
**The filter is a REQUEST to the reader, never a client-side sieve.** It travels as
|
|
349
|
+
`p_archived` on `get_dm_conversations_with_details`, so `hasMore`, the pagination cursor and
|
|
350
|
+
every count describe the rows the caller actually asked for. A host that fetches everything and
|
|
351
|
+
partitions `ConversationSummary.isArchived` in its own component is reading rows the server
|
|
352
|
+
would not have sent, and its counts describe a different set from the one on screen.
|
|
353
|
+
|
|
354
|
+
`ConversationList` already carries the control — **"Archived (N)"**, one click, and a way back.
|
|
355
|
+
The count comes from `countArchivedConversations()` and is CAPPED: `exact: false` renders "N+",
|
|
356
|
+
never a confident total. A host building its own list reads `archiveFilter`, `setArchiveFilter`
|
|
357
|
+
and `archivedCount` from `useConversations()`.
|
|
358
|
+
|
|
359
|
+
The initial state is a KNOB, not this package's taste (root law 6, and clause 6 of the law):
|
|
360
|
+
|
|
361
|
+
```tsx
|
|
362
|
+
<MessagingProvider archiveFilter={settings.lists.archivedDefault} … >
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Omit it and you get the platform default, `active`.
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
336
369
|
## The database contract
|
|
337
370
|
|
|
338
371
|
One canonical schema, in Matrx Main, under the platform's DB conventions:
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
+
DEFAULT_MESSAGING_ARCHIVE_FILTER: () => DEFAULT_MESSAGING_ARCHIVE_FILTER,
|
|
23
24
|
MESSAGING_EVENTS: () => MESSAGING_EVENTS,
|
|
24
25
|
MESSAGING_RPC_SCHEMA: () => MESSAGING_RPC_SCHEMA,
|
|
25
26
|
MESSAGING_SCHEMA: () => MESSAGING_SCHEMA,
|
|
@@ -65,6 +66,7 @@ __export(src_exports, {
|
|
|
65
66
|
resolveActor: () => resolveActor,
|
|
66
67
|
splitText: () => splitText,
|
|
67
68
|
summarizeText: () => summarizeText,
|
|
69
|
+
toMessagingArchiveFilter: () => toMessagingArchiveFilter,
|
|
68
70
|
unreadCutoff: () => unreadCutoff
|
|
69
71
|
});
|
|
70
72
|
module.exports = __toCommonJS(src_exports);
|
|
@@ -986,6 +988,17 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
|
|
|
986
988
|
};
|
|
987
989
|
}
|
|
988
990
|
|
|
991
|
+
// src/core/types.ts
|
|
992
|
+
var asConversationId = (value) => value;
|
|
993
|
+
var asMessageId = (value) => value;
|
|
994
|
+
var asUserId = (value) => value;
|
|
995
|
+
var asOrganizationId = (value) => value;
|
|
996
|
+
var asClientMessageId = (value) => value;
|
|
997
|
+
var DEFAULT_MESSAGING_ARCHIVE_FILTER = "active";
|
|
998
|
+
function toMessagingArchiveFilter(value, fallback = DEFAULT_MESSAGING_ARCHIVE_FILTER) {
|
|
999
|
+
return value === "active" || value === "archived" || value === "all" ? value : fallback;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
989
1002
|
// src/core/store.ts
|
|
990
1003
|
function timeOf(message) {
|
|
991
1004
|
const stamp = message.editedAt ?? message.createdAt;
|
|
@@ -1016,6 +1029,8 @@ function createMessagingStore() {
|
|
|
1016
1029
|
let conversations = [];
|
|
1017
1030
|
let hasMoreConversations = false;
|
|
1018
1031
|
let hasLoadedConversations = false;
|
|
1032
|
+
let archiveFilter = DEFAULT_MESSAGING_ARCHIVE_FILTER;
|
|
1033
|
+
let archivedCount = null;
|
|
1019
1034
|
let threads = /* @__PURE__ */ new Map();
|
|
1020
1035
|
let activeConversationId = null;
|
|
1021
1036
|
const listeners = /* @__PURE__ */ new Set();
|
|
@@ -1028,7 +1043,9 @@ function createMessagingStore() {
|
|
|
1028
1043
|
hasLoadedConversations,
|
|
1029
1044
|
threads,
|
|
1030
1045
|
activeConversationId,
|
|
1031
|
-
totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length
|
|
1046
|
+
totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length,
|
|
1047
|
+
archiveFilter,
|
|
1048
|
+
archivedCount
|
|
1032
1049
|
};
|
|
1033
1050
|
return cached;
|
|
1034
1051
|
}
|
|
@@ -1075,6 +1092,18 @@ function createMessagingStore() {
|
|
|
1075
1092
|
hasLoadedConversations = true;
|
|
1076
1093
|
emit();
|
|
1077
1094
|
},
|
|
1095
|
+
setArchiveFilter(next) {
|
|
1096
|
+
if (next === archiveFilter) return;
|
|
1097
|
+
archiveFilter = next;
|
|
1098
|
+
conversations = [];
|
|
1099
|
+
hasMoreConversations = false;
|
|
1100
|
+
hasLoadedConversations = false;
|
|
1101
|
+
emit();
|
|
1102
|
+
},
|
|
1103
|
+
setArchivedCount(value) {
|
|
1104
|
+
archivedCount = value;
|
|
1105
|
+
emit();
|
|
1106
|
+
},
|
|
1078
1107
|
appendConversations(items, hasMore) {
|
|
1079
1108
|
const byId = new Map(conversations.map((item) => [item.conversation.id, item]));
|
|
1080
1109
|
items.forEach((item) => byId.set(item.conversation.id, item));
|
|
@@ -1280,10 +1309,21 @@ function createMessagingEngine(options) {
|
|
|
1280
1309
|
openChannels.get(message.conversationId)?.send(MESSAGING_EVENTS.message, message);
|
|
1281
1310
|
}
|
|
1282
1311
|
async function reloadInbox() {
|
|
1283
|
-
|
|
1312
|
+
if (disposed) return;
|
|
1313
|
+
const page = await repository.listConversations({
|
|
1314
|
+
limit: conversationPageSize,
|
|
1315
|
+
archived: store.snapshot().archiveFilter
|
|
1316
|
+
});
|
|
1317
|
+
if (disposed) return;
|
|
1284
1318
|
conversationCursor = page.nextCursor;
|
|
1285
1319
|
store.setConversations(page.items, page.hasMore);
|
|
1286
1320
|
}
|
|
1321
|
+
async function reloadArchivedCount() {
|
|
1322
|
+
if (disposed) return;
|
|
1323
|
+
const value = await repository.countArchivedConversations();
|
|
1324
|
+
if (disposed) return;
|
|
1325
|
+
store.setArchivedCount(value);
|
|
1326
|
+
}
|
|
1287
1327
|
async function backfillConversation(id) {
|
|
1288
1328
|
const thread = store.snapshot().threads.get(id);
|
|
1289
1329
|
const since = thread?.latestAt ?? null;
|
|
@@ -1310,7 +1350,13 @@ function createMessagingEngine(options) {
|
|
|
1310
1350
|
outbox,
|
|
1311
1351
|
identity,
|
|
1312
1352
|
async start() {
|
|
1353
|
+
if (options.archiveFilter !== void 0) {
|
|
1354
|
+
store.setArchiveFilter(options.archiveFilter);
|
|
1355
|
+
}
|
|
1313
1356
|
await reloadInbox();
|
|
1357
|
+
void reloadArchivedCount().catch(
|
|
1358
|
+
(error) => reportError(error, "refreshArchivedCount")
|
|
1359
|
+
);
|
|
1314
1360
|
if (disposed || inboxChannel !== null) return;
|
|
1315
1361
|
inboxChannel = manager.open({
|
|
1316
1362
|
topic: inboxTopic(identity.userId),
|
|
@@ -1325,7 +1371,7 @@ function createMessagingEngine(options) {
|
|
|
1325
1371
|
const message = projectMessage(row, identity.organizationId);
|
|
1326
1372
|
const known = store.snapshot().conversations.some((item) => item.conversation.id === message.conversationId);
|
|
1327
1373
|
if (!known) {
|
|
1328
|
-
void reloadInbox();
|
|
1374
|
+
void reloadInbox().catch((error) => reportError(error, "inboxRefresh"));
|
|
1329
1375
|
return;
|
|
1330
1376
|
}
|
|
1331
1377
|
store.ingest(message);
|
|
@@ -1341,7 +1387,7 @@ function createMessagingEngine(options) {
|
|
|
1341
1387
|
filter: `user_id=eq.${identity.userId}`,
|
|
1342
1388
|
rowId: (row) => typeof row["id"] === "string" ? row["id"] : void 0,
|
|
1343
1389
|
onChange: () => {
|
|
1344
|
-
void reloadInbox();
|
|
1390
|
+
void reloadInbox().catch((error) => reportError(error, "inboxRefresh"));
|
|
1345
1391
|
}
|
|
1346
1392
|
}
|
|
1347
1393
|
],
|
|
@@ -1352,12 +1398,33 @@ function createMessagingEngine(options) {
|
|
|
1352
1398
|
}
|
|
1353
1399
|
});
|
|
1354
1400
|
},
|
|
1401
|
+
async setArchiveFilter(next) {
|
|
1402
|
+
if (next === store.snapshot().archiveFilter) return;
|
|
1403
|
+
store.setArchiveFilter(next);
|
|
1404
|
+
conversationCursor = null;
|
|
1405
|
+
try {
|
|
1406
|
+
await reloadInbox();
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
reportError(error, "setArchiveFilter");
|
|
1409
|
+
}
|
|
1410
|
+
await reloadArchivedCount().catch(
|
|
1411
|
+
(error) => reportError(error, "refreshArchivedCount")
|
|
1412
|
+
);
|
|
1413
|
+
},
|
|
1414
|
+
async refreshArchivedCount() {
|
|
1415
|
+
try {
|
|
1416
|
+
await reloadArchivedCount();
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
reportError(error, "refreshArchivedCount");
|
|
1419
|
+
}
|
|
1420
|
+
},
|
|
1355
1421
|
async loadMoreConversations() {
|
|
1356
1422
|
if (conversationCursor === null) return;
|
|
1357
1423
|
try {
|
|
1358
1424
|
const page = await repository.listConversations({
|
|
1359
1425
|
limit: conversationPageSize,
|
|
1360
|
-
cursor: conversationCursor
|
|
1426
|
+
cursor: conversationCursor,
|
|
1427
|
+
archived: store.snapshot().archiveFilter
|
|
1361
1428
|
});
|
|
1362
1429
|
conversationCursor = page.nextCursor;
|
|
1363
1430
|
store.appendConversations(page.items, page.hasMore);
|
|
@@ -1666,6 +1733,7 @@ function createMessagingRepository(options) {
|
|
|
1666
1733
|
identity,
|
|
1667
1734
|
async listConversations(args = {}) {
|
|
1668
1735
|
const limit = args.limit ?? 30;
|
|
1736
|
+
const archived = args.archived ?? DEFAULT_MESSAGING_ARCHIVE_FILTER;
|
|
1669
1737
|
const operation = "listConversations";
|
|
1670
1738
|
const rows = await withSessionRetry(
|
|
1671
1739
|
operation,
|
|
@@ -1675,7 +1743,12 @@ function createMessagingRepository(options) {
|
|
|
1675
1743
|
p_user_id: identity.userId,
|
|
1676
1744
|
p_limit: limit + 1,
|
|
1677
1745
|
p_before_sort_at: args.cursor?.beforeSortAt ?? null,
|
|
1678
|
-
p_before_conversation_id: args.cursor?.beforeConversationId ?? null
|
|
1746
|
+
p_before_conversation_id: args.cursor?.beforeConversationId ?? null,
|
|
1747
|
+
// THE ARCHIVED-ITEMS LAW, SERVER-side. `get_dm_conversations_with_details`
|
|
1748
|
+
// used to hardcode `is_archived IS FALSE` with no parameter at all,
|
|
1749
|
+
// so an archived conversation was not hidden — it was unreachable.
|
|
1750
|
+
// The RPC gained `p_archived` on 2026-09-09 (register row R1).
|
|
1751
|
+
p_archived: archived
|
|
1679
1752
|
},
|
|
1680
1753
|
operation
|
|
1681
1754
|
)
|
|
@@ -1695,6 +1768,29 @@ function createMessagingRepository(options) {
|
|
|
1695
1768
|
nextCursor: hasMore && last !== void 0 ? { beforeSortAt: last.sortAt, beforeConversationId: last.conversation.id } : null
|
|
1696
1769
|
};
|
|
1697
1770
|
},
|
|
1771
|
+
async countArchivedConversations(args = {}) {
|
|
1772
|
+
const limit = args.limit ?? 100;
|
|
1773
|
+
const operation = "countArchivedConversations";
|
|
1774
|
+
const rows = await withSessionRetry(
|
|
1775
|
+
operation,
|
|
1776
|
+
() => rpc(
|
|
1777
|
+
RPCS.conversationsWithDetails,
|
|
1778
|
+
{
|
|
1779
|
+
p_user_id: identity.userId,
|
|
1780
|
+
p_limit: limit + 1,
|
|
1781
|
+
p_before_sort_at: null,
|
|
1782
|
+
p_before_conversation_id: null,
|
|
1783
|
+
p_archived: "archived"
|
|
1784
|
+
},
|
|
1785
|
+
operation
|
|
1786
|
+
)
|
|
1787
|
+
);
|
|
1788
|
+
if (rows !== null && !Array.isArray(rows)) {
|
|
1789
|
+
throw invalidResponse(operation, `${RPCS.conversationsWithDetails} did not return rows`);
|
|
1790
|
+
}
|
|
1791
|
+
const found = (rows ?? []).length;
|
|
1792
|
+
return found > limit ? { count: limit, exact: false } : { count: found, exact: true };
|
|
1793
|
+
},
|
|
1698
1794
|
async getConversation(id) {
|
|
1699
1795
|
const operation = "getConversation";
|
|
1700
1796
|
const { data, error } = await withSessionRetry(
|
|
@@ -1928,11 +2024,4 @@ function createMessagingRepository(options) {
|
|
|
1928
2024
|
};
|
|
1929
2025
|
return repository;
|
|
1930
2026
|
}
|
|
1931
|
-
|
|
1932
|
-
// src/core/types.ts
|
|
1933
|
-
var asConversationId = (value) => value;
|
|
1934
|
-
var asMessageId = (value) => value;
|
|
1935
|
-
var asUserId = (value) => value;
|
|
1936
|
-
var asOrganizationId = (value) => value;
|
|
1937
|
-
var asClientMessageId = (value) => value;
|
|
1938
2027
|
//# sourceMappingURL=index.cjs.map
|