@ai-matrx/messaging 0.10.5 → 0.11.1
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 +57 -0
- package/README.md +33 -0
- package/dist/index.cjs +98 -11
- 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 +98 -11
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +145 -14
- 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 +145 -14
- package/dist/react.js.map +1 -1
- package/dist/styles.css +7 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# Changelog — `@ai-matrx/messaging`
|
|
2
2
|
|
|
3
|
+
## 0.11.1
|
|
4
|
+
|
|
5
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
+
`git diff npm/messaging/v0.11.0..npm/messaging/v0.11.1 -- apps/shared/messaging`).
|
|
7
|
+
No source changes intended and no consumer action required.
|
|
8
|
+
|
|
9
|
+
## 0.11.0 — 2026-09-10
|
|
10
|
+
|
|
11
|
+
### THE ARCHIVED-ITEMS LAW reaches conversations
|
|
12
|
+
|
|
13
|
+
> "everything should have an archive filter, and the default should always hide archived, but
|
|
14
|
+
> seeing archived items should be one or two clicks away … this is a system wide decision for
|
|
15
|
+
> every single item everywhere in our system, for every single table and every single page."
|
|
16
|
+
> — Arman, 2026-09-09 (`common-docs/policies/archived-items.md`)
|
|
17
|
+
|
|
18
|
+
This package could ARCHIVE a conversation — `setConversationFlags({ isArchived: true })` — and
|
|
19
|
+
had no read side for one. No filter, no parameter, no affordance anywhere could show it again,
|
|
20
|
+
and `get_dm_conversations_with_details` hardcoded the exclusion with no parameter of its own.
|
|
21
|
+
An archived conversation was not hidden. It was unreachable.
|
|
22
|
+
|
|
23
|
+
- **`MessagingArchiveFilter`** — the platform tri-state (`active` | `archived` | `all`), the
|
|
24
|
+
same three words as matrx-frontend's `lib/entity-list`, the `agx_`/`wfx_` list RPCs and
|
|
25
|
+
`@ai-matrx/design-system`'s `ArchiveFilter`. Shipped with
|
|
26
|
+
`DEFAULT_MESSAGING_ARCHIVE_FILTER` (`active`) and `toMessagingArchiveFilter()`, which
|
|
27
|
+
narrows an untrusted value and falls back to the default rather than silently widening a
|
|
28
|
+
list to archived rows. Three states, never a boolean: a boolean cannot say "archived only".
|
|
29
|
+
- **`repository.listConversations({ archived })`** passes `p_archived` to the RPC, which
|
|
30
|
+
gained that parameter on 2026-09-09 (register row R1). The axis is a REQUEST to the reader,
|
|
31
|
+
never a client-side sieve — so `hasMore`, the cursor and any count describe the rows the
|
|
32
|
+
caller actually asked for.
|
|
33
|
+
- **`repository.countArchivedConversations()`** answers the reveal's label. It is CAPPED and
|
|
34
|
+
says so: `{ count, exact }`, and `exact: false` must render "12+", never a confident "12".
|
|
35
|
+
- **The engine** carries the axis on every read — first page, "load more", and the reconnect
|
|
36
|
+
backfill — with `setArchiveFilter()` re-reading page one and refreshing the count. Flipping
|
|
37
|
+
the filter returns the store to NOT-LOADED, not to "loaded and empty": leaving the old rows
|
|
38
|
+
up renders active conversations under an "Archived" heading, and an empty "loaded" list
|
|
39
|
+
tells someone with forty archived chats she has none.
|
|
40
|
+
- **`ConversationList`** renders the reveal — **"Archived (N)"** → the archive → "← Back to
|
|
41
|
+
active conversations". One click, and it appears only when it would do something: while
|
|
42
|
+
hiding, when something is there to reveal; while showing, always, so the way back is never
|
|
43
|
+
missing. A button that changes nothing is a dead end, not an affordance.
|
|
44
|
+
- **`useConversations()`** returns `archiveFilter`, `setArchiveFilter` and `archivedCount`.
|
|
45
|
+
|
|
46
|
+
### Consumer action
|
|
47
|
+
|
|
48
|
+
- **Nothing breaks.** Every existing signature is unchanged and every default is the old
|
|
49
|
+
behaviour: a host that passes nothing still hides archived conversations.
|
|
50
|
+
- **Delete your local archive split.** A host that worked around this gap by fetching
|
|
51
|
+
everything and partitioning `ConversationSummary.isArchived` in its own component is now
|
|
52
|
+
reading rows the server would not have sent, and its counts and pagination describe a
|
|
53
|
+
different set from the one on screen. Drop the local split and the `include_archived`-style
|
|
54
|
+
boolean beside it; `ConversationList` carries the control, and a host list built on
|
|
55
|
+
`useConversations()` reads `archiveFilter` / `setArchiveFilter` / `archivedCount`.
|
|
56
|
+
- **`<MessagingProvider archiveFilter={…}>` is the knob** (root law 6, and clause 6 of the
|
|
57
|
+
law itself): the initial state is an org/user setting, not this package's taste. Wire it to
|
|
58
|
+
whatever your setting resolves to; omit it and you get the platform default, `active`.
|
|
59
|
+
|
|
3
60
|
## 0.10.5 — 2026-09-09
|
|
4
61
|
|
|
5
62
|
### 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));
|
|
@@ -1281,11 +1310,20 @@ function createMessagingEngine(options) {
|
|
|
1281
1310
|
}
|
|
1282
1311
|
async function reloadInbox() {
|
|
1283
1312
|
if (disposed) return;
|
|
1284
|
-
const page = await repository.listConversations({
|
|
1313
|
+
const page = await repository.listConversations({
|
|
1314
|
+
limit: conversationPageSize,
|
|
1315
|
+
archived: store.snapshot().archiveFilter
|
|
1316
|
+
});
|
|
1285
1317
|
if (disposed) return;
|
|
1286
1318
|
conversationCursor = page.nextCursor;
|
|
1287
1319
|
store.setConversations(page.items, page.hasMore);
|
|
1288
1320
|
}
|
|
1321
|
+
async function reloadArchivedCount() {
|
|
1322
|
+
if (disposed) return;
|
|
1323
|
+
const value = await repository.countArchivedConversations();
|
|
1324
|
+
if (disposed) return;
|
|
1325
|
+
store.setArchivedCount(value);
|
|
1326
|
+
}
|
|
1289
1327
|
async function backfillConversation(id) {
|
|
1290
1328
|
const thread = store.snapshot().threads.get(id);
|
|
1291
1329
|
const since = thread?.latestAt ?? null;
|
|
@@ -1312,7 +1350,13 @@ function createMessagingEngine(options) {
|
|
|
1312
1350
|
outbox,
|
|
1313
1351
|
identity,
|
|
1314
1352
|
async start() {
|
|
1353
|
+
if (options.archiveFilter !== void 0) {
|
|
1354
|
+
store.setArchiveFilter(options.archiveFilter);
|
|
1355
|
+
}
|
|
1315
1356
|
await reloadInbox();
|
|
1357
|
+
void reloadArchivedCount().catch(
|
|
1358
|
+
(error) => reportError(error, "refreshArchivedCount")
|
|
1359
|
+
);
|
|
1316
1360
|
if (disposed || inboxChannel !== null) return;
|
|
1317
1361
|
inboxChannel = manager.open({
|
|
1318
1362
|
topic: inboxTopic(identity.userId),
|
|
@@ -1354,12 +1398,33 @@ function createMessagingEngine(options) {
|
|
|
1354
1398
|
}
|
|
1355
1399
|
});
|
|
1356
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
|
+
},
|
|
1357
1421
|
async loadMoreConversations() {
|
|
1358
1422
|
if (conversationCursor === null) return;
|
|
1359
1423
|
try {
|
|
1360
1424
|
const page = await repository.listConversations({
|
|
1361
1425
|
limit: conversationPageSize,
|
|
1362
|
-
cursor: conversationCursor
|
|
1426
|
+
cursor: conversationCursor,
|
|
1427
|
+
archived: store.snapshot().archiveFilter
|
|
1363
1428
|
});
|
|
1364
1429
|
conversationCursor = page.nextCursor;
|
|
1365
1430
|
store.appendConversations(page.items, page.hasMore);
|
|
@@ -1668,6 +1733,7 @@ function createMessagingRepository(options) {
|
|
|
1668
1733
|
identity,
|
|
1669
1734
|
async listConversations(args = {}) {
|
|
1670
1735
|
const limit = args.limit ?? 30;
|
|
1736
|
+
const archived = args.archived ?? DEFAULT_MESSAGING_ARCHIVE_FILTER;
|
|
1671
1737
|
const operation = "listConversations";
|
|
1672
1738
|
const rows = await withSessionRetry(
|
|
1673
1739
|
operation,
|
|
@@ -1677,7 +1743,12 @@ function createMessagingRepository(options) {
|
|
|
1677
1743
|
p_user_id: identity.userId,
|
|
1678
1744
|
p_limit: limit + 1,
|
|
1679
1745
|
p_before_sort_at: args.cursor?.beforeSortAt ?? null,
|
|
1680
|
-
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
|
|
1681
1752
|
},
|
|
1682
1753
|
operation
|
|
1683
1754
|
)
|
|
@@ -1697,6 +1768,29 @@ function createMessagingRepository(options) {
|
|
|
1697
1768
|
nextCursor: hasMore && last !== void 0 ? { beforeSortAt: last.sortAt, beforeConversationId: last.conversation.id } : null
|
|
1698
1769
|
};
|
|
1699
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
|
+
},
|
|
1700
1794
|
async getConversation(id) {
|
|
1701
1795
|
const operation = "getConversation";
|
|
1702
1796
|
const { data, error } = await withSessionRetry(
|
|
@@ -1930,11 +2024,4 @@ function createMessagingRepository(options) {
|
|
|
1930
2024
|
};
|
|
1931
2025
|
return repository;
|
|
1932
2026
|
}
|
|
1933
|
-
|
|
1934
|
-
// src/core/types.ts
|
|
1935
|
-
var asConversationId = (value) => value;
|
|
1936
|
-
var asMessageId = (value) => value;
|
|
1937
|
-
var asUserId = (value) => value;
|
|
1938
|
-
var asOrganizationId = (value) => value;
|
|
1939
|
-
var asClientMessageId = (value) => value;
|
|
1940
2027
|
//# sourceMappingURL=index.cjs.map
|