@ai-matrx/messaging 0.13.1 → 0.15.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 +59 -0
- package/README.md +5 -1
- package/dist/index.cjs +59 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -9
- package/dist/index.d.ts +37 -9
- package/dist/index.js +59 -18
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +121 -33
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +47 -10
- package/dist/react.d.ts +47 -10
- package/dist/react.js +121 -33
- package/dist/react.js.map +1 -1
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog — `@ai-matrx/messaging`
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **A failed inbox read is never a skeleton forever, and never "empty"** (RC-B12
|
|
8
|
+
round 11). When the first conversation-list read failed,
|
|
9
|
+
`hasLoadedConversations` stayed false and the list showed its skeleton for as
|
|
10
|
+
long as it was open. The store now carries `conversationsLoadError`; the list
|
|
11
|
+
resolves to "Couldn't load your conversations" with a **Try again**
|
|
12
|
+
(`useConversations().retry`, `engine.refreshInbox()`), and "No conversations
|
|
13
|
+
yet" only follows a read that succeeded.
|
|
14
|
+
- **Read failures render through the design-system error slot.** "Couldn't load
|
|
15
|
+
this conversation" and the inbox failure are drawn with `ErrorBox` from
|
|
16
|
+
`@ai-matrx/design-system` (new dependency), so the host fills them with its
|
|
17
|
+
error actions — in AI Matrx, the Alchemy Menu. They were `EmptyState`s before,
|
|
18
|
+
in the same grey as "No messages yet".
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `MessagingSnapshot.conversationsLoadError`, `MessagingStore.setConversationsLoadError`,
|
|
23
|
+
`MessagingEngine.refreshInbox()`, `useConversations().loadError` / `.retry`.
|
|
24
|
+
|
|
25
|
+
## 0.14.1
|
|
26
|
+
|
|
27
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
28
|
+
`git diff npm/messaging/v0.14.0..npm/messaging/v0.14.1 -- apps/shared/messaging`).
|
|
29
|
+
No source changes intended and no consumer action required.
|
|
30
|
+
|
|
31
|
+
## 0.14.0
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- **Messaging runs with no organization selected** (access belongs to the
|
|
36
|
+
person, 2026-09-23). `<MessagingProvider organizationId={null}>` used to stay
|
|
37
|
+
inert — every inbox and conversation spun forever — and
|
|
38
|
+
`createMessagingRepository` threw at construction without an org, although
|
|
39
|
+
no read here filters by organization. Now the provider starts and every read
|
|
40
|
+
works as the signed-in person. The organization is only a write destination:
|
|
41
|
+
- starting a NEW conversation (`getOrCreateDirectConversation`,
|
|
42
|
+
`createGroupConversation`, `useConversations().startDirect`) without one is
|
|
43
|
+
refused with the new error code **`organization-required`**, before
|
|
44
|
+
anything reaches the database;
|
|
45
|
+
- a message or member added to an EXISTING conversation with no active
|
|
46
|
+
organization is stamped with that conversation's own organization;
|
|
47
|
+
- AI actions do not render without an organization (a run is filed under one).
|
|
48
|
+
- `MessagingIdentity.organizationId` and `MessagingRepository.organizationId`
|
|
49
|
+
are now `OrganizationId | null`; `RepositoryOptions.organizationId` is
|
|
50
|
+
optional. `MessagingErrorCode` gains `"organization-required"`.
|
|
51
|
+
|
|
52
|
+
### Consumer action
|
|
53
|
+
|
|
54
|
+
- Pass the active organization as before, including `null` when none is
|
|
55
|
+
selected — do not gate `<MessagingProvider>` on having one.
|
|
56
|
+
- Where the host starts a conversation, ask the person to choose an
|
|
57
|
+
organization when none is selected (or handle `error.code ===
|
|
58
|
+
"organization-required"`).
|
|
59
|
+
- Code that read `host.identity.organizationId` as non-null must handle `null`.
|
|
60
|
+
|
|
3
61
|
## 0.13.1
|
|
4
62
|
|
|
5
63
|
### Fixed
|
|
@@ -781,3 +839,4 @@ consumer):
|
|
|
781
839
|
sign-in reports `session-unavailable` instead of recovering.
|
|
782
840
|
- This package supersedes host code that talks to `communication.dm_*` directly. Anything still
|
|
783
841
|
reading those tables outside the package should be moved onto it or deleted.
|
|
842
|
+
|
package/README.md
CHANGED
|
@@ -82,7 +82,11 @@ notifications race it into two "direct" conversations for one pair, forever.
|
|
|
82
82
|
RLS plus auth-checked SECURITY DEFINER RPCs. There is not one permission branch in this package —
|
|
83
83
|
an RPC that says no becomes a `forbidden` error, never a client-side re-decision. And every
|
|
84
84
|
conversation and message write carries an explicit `organization_id`; the package refuses the
|
|
85
|
-
write rather than letting an unscoped row reach the database.
|
|
85
|
+
write rather than letting an unscoped row reach the database. The organization is a write
|
|
86
|
+
DESTINATION only: `organizationId={null}` is a valid state in which every read works (the inbox,
|
|
87
|
+
a deep-linked conversation, replying — a reply is stamped with its conversation's organization),
|
|
88
|
+
and starting a NEW conversation is refused as `organization-required` so the host can ask the
|
|
89
|
+
person to choose one.
|
|
86
90
|
|
|
87
91
|
### 7. A missing session is a lifecycle moment, not an error.
|
|
88
92
|
|
package/dist/index.cjs
CHANGED
|
@@ -931,7 +931,7 @@ function projectMessage(row, fallbackOrganizationId) {
|
|
|
931
931
|
id: requiredStr(row, "id", operation),
|
|
932
932
|
conversationId: requiredStr(row, "conversation_id", operation),
|
|
933
933
|
senderId: requiredStr(row, "sender_id", operation),
|
|
934
|
-
organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
|
|
934
|
+
organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
|
|
935
935
|
content: typeof row["content"] === "string" ? row["content"] : "",
|
|
936
936
|
kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
|
|
937
937
|
// A row read from the DB is at least `sent`. `sending`/`failed` are outbox
|
|
@@ -979,7 +979,7 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
|
|
|
979
979
|
groupName,
|
|
980
980
|
groupImageUrl,
|
|
981
981
|
createdBy: createdBy === null ? null : createdBy,
|
|
982
|
-
organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
|
|
982
|
+
organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
|
|
983
983
|
createdAt,
|
|
984
984
|
updatedAt,
|
|
985
985
|
metadata: jsonObject(row["metadata"])
|
|
@@ -1045,6 +1045,7 @@ function createMessagingStore() {
|
|
|
1045
1045
|
let threads = /* @__PURE__ */ new Map();
|
|
1046
1046
|
let activeConversationId = null;
|
|
1047
1047
|
let conversationReadFailures = /* @__PURE__ */ new Map();
|
|
1048
|
+
let conversationsLoadError = null;
|
|
1048
1049
|
const listeners = /* @__PURE__ */ new Set();
|
|
1049
1050
|
let cached = null;
|
|
1050
1051
|
function snapshot() {
|
|
@@ -1058,7 +1059,8 @@ function createMessagingStore() {
|
|
|
1058
1059
|
totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length,
|
|
1059
1060
|
archiveFilter,
|
|
1060
1061
|
archivedCount,
|
|
1061
|
-
conversationReadFailures
|
|
1062
|
+
conversationReadFailures,
|
|
1063
|
+
conversationsLoadError
|
|
1062
1064
|
};
|
|
1063
1065
|
return cached;
|
|
1064
1066
|
}
|
|
@@ -1103,6 +1105,12 @@ function createMessagingStore() {
|
|
|
1103
1105
|
conversations = normalizeConversations(items);
|
|
1104
1106
|
hasMoreConversations = hasMore;
|
|
1105
1107
|
hasLoadedConversations = true;
|
|
1108
|
+
conversationsLoadError = null;
|
|
1109
|
+
emit();
|
|
1110
|
+
},
|
|
1111
|
+
setConversationsLoadError(message) {
|
|
1112
|
+
if (message === conversationsLoadError) return;
|
|
1113
|
+
conversationsLoadError = message;
|
|
1106
1114
|
emit();
|
|
1107
1115
|
},
|
|
1108
1116
|
setArchiveFilter(next) {
|
|
@@ -1312,7 +1320,11 @@ function createMessagingEngine(options) {
|
|
|
1312
1320
|
optimisticMessage({
|
|
1313
1321
|
conversationId: entry.draft.conversationId,
|
|
1314
1322
|
senderId: identity.userId,
|
|
1315
|
-
|
|
1323
|
+
// Client-only until the confirmed row replaces it: the active
|
|
1324
|
+
// organization, else the conversation's own.
|
|
1325
|
+
organizationId: identity.organizationId ?? store.snapshot().conversations.find(
|
|
1326
|
+
(item) => item.conversation.id === entry.draft.conversationId
|
|
1327
|
+
)?.conversation.organizationId ?? "",
|
|
1316
1328
|
content: entry.draft.content,
|
|
1317
1329
|
clientMessageId: entry.clientMessageId,
|
|
1318
1330
|
...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
|
|
@@ -1331,10 +1343,20 @@ function createMessagingEngine(options) {
|
|
|
1331
1343
|
}
|
|
1332
1344
|
async function reloadInbox() {
|
|
1333
1345
|
if (disposed) return;
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1346
|
+
let page;
|
|
1347
|
+
try {
|
|
1348
|
+
page = await repository.listConversations({
|
|
1349
|
+
limit: conversationPageSize,
|
|
1350
|
+
archived: store.snapshot().archiveFilter
|
|
1351
|
+
});
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
if (!disposed) {
|
|
1354
|
+
store.setConversationsLoadError(
|
|
1355
|
+
error instanceof Error ? error.message : typeof error === "string" ? error : "The conversation list could not be read."
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
throw error;
|
|
1359
|
+
}
|
|
1338
1360
|
if (disposed) return;
|
|
1339
1361
|
conversationCursor = page.nextCursor;
|
|
1340
1362
|
store.setConversations(page.items, page.hasMore);
|
|
@@ -1419,6 +1441,13 @@ function createMessagingEngine(options) {
|
|
|
1419
1441
|
}
|
|
1420
1442
|
});
|
|
1421
1443
|
},
|
|
1444
|
+
async refreshInbox() {
|
|
1445
|
+
try {
|
|
1446
|
+
await reloadInbox();
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
reportError(error, "inboxRefresh");
|
|
1449
|
+
}
|
|
1450
|
+
},
|
|
1422
1451
|
async setArchiveFilter(next) {
|
|
1423
1452
|
if (next === store.snapshot().archiveFilter) return;
|
|
1424
1453
|
store.setArchiveFilter(next);
|
|
@@ -1844,12 +1873,15 @@ var RPCS = {
|
|
|
1844
1873
|
};
|
|
1845
1874
|
var MESSAGE_COLUMNS = "id,conversation_id,sender_id,organization_id,content,message_type,status,reply_to_id,client_message_id,action_data,media_url,media_thumbnail_url,media_metadata,created_at,edited_at,deleted_at,deleted_for_everyone,metadata";
|
|
1846
1875
|
var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
|
|
1876
|
+
function optionalOrg(organizationId) {
|
|
1877
|
+
return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
|
|
1878
|
+
}
|
|
1847
1879
|
function requireOrg(organizationId, operation) {
|
|
1848
|
-
if (
|
|
1880
|
+
if (organizationId === null) {
|
|
1849
1881
|
throw new MessagingError(
|
|
1850
|
-
"
|
|
1851
|
-
`${operation}: no
|
|
1852
|
-
"
|
|
1882
|
+
"organization-required",
|
|
1883
|
+
`${operation}: no organization selected`,
|
|
1884
|
+
"A new conversation is filed under one organization. Ask the person to choose one, then pass it as organizationId on <MessagingProvider>. Reading and replying to existing conversations never needs it."
|
|
1853
1885
|
);
|
|
1854
1886
|
}
|
|
1855
1887
|
return organizationId;
|
|
@@ -1861,7 +1893,7 @@ function sleep(ms) {
|
|
|
1861
1893
|
return new Promise((resolve) => setTimeout(resolve, jittered));
|
|
1862
1894
|
}
|
|
1863
1895
|
function createMessagingRepository(options) {
|
|
1864
|
-
const org =
|
|
1896
|
+
const org = optionalOrg(options.organizationId);
|
|
1865
1897
|
const handedIn = options.identity;
|
|
1866
1898
|
if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
|
|
1867
1899
|
options.onDiagnostic?.(
|
|
@@ -1931,6 +1963,11 @@ function createMessagingRepository(options) {
|
|
|
1931
1963
|
});
|
|
1932
1964
|
return found;
|
|
1933
1965
|
}
|
|
1966
|
+
async function organizationOfExisting(conversationId) {
|
|
1967
|
+
if (org !== null) return org;
|
|
1968
|
+
const conversation = await repository.getConversation(conversationId);
|
|
1969
|
+
return conversation.organizationId;
|
|
1970
|
+
}
|
|
1934
1971
|
const repository = {
|
|
1935
1972
|
organizationId: org,
|
|
1936
1973
|
actingUserId,
|
|
@@ -2063,6 +2100,7 @@ function createMessagingRepository(options) {
|
|
|
2063
2100
|
},
|
|
2064
2101
|
async getOrCreateDirectConversation(otherUserId) {
|
|
2065
2102
|
const operation = "getOrCreateDirectConversation";
|
|
2103
|
+
const destination = requireOrg(org, operation);
|
|
2066
2104
|
const me = await actingUser(operation);
|
|
2067
2105
|
const id = await withSessionRetry(
|
|
2068
2106
|
operation,
|
|
@@ -2074,7 +2112,7 @@ function createMessagingRepository(options) {
|
|
|
2074
2112
|
// not a preference.
|
|
2075
2113
|
p_user1_id: me,
|
|
2076
2114
|
p_user2_id: otherUserId,
|
|
2077
|
-
p_organization_id:
|
|
2115
|
+
p_organization_id: destination
|
|
2078
2116
|
},
|
|
2079
2117
|
operation
|
|
2080
2118
|
)
|
|
@@ -2086,13 +2124,14 @@ function createMessagingRepository(options) {
|
|
|
2086
2124
|
},
|
|
2087
2125
|
async createGroupConversation({ name, memberIds }) {
|
|
2088
2126
|
const operation = "createGroupConversation";
|
|
2127
|
+
const destination = requireOrg(org, operation);
|
|
2089
2128
|
const me = await actingUser(operation);
|
|
2090
2129
|
const { data, error } = await withSessionRetry(
|
|
2091
2130
|
operation,
|
|
2092
2131
|
async () => db().from(TABLES.conversations).insert({
|
|
2093
2132
|
type: "group",
|
|
2094
2133
|
group_name: name,
|
|
2095
|
-
organization_id:
|
|
2134
|
+
organization_id: destination,
|
|
2096
2135
|
created_by: me
|
|
2097
2136
|
}).select("id").single()
|
|
2098
2137
|
);
|
|
@@ -2107,7 +2146,7 @@ function createMessagingRepository(options) {
|
|
|
2107
2146
|
conversation_id: conversationId,
|
|
2108
2147
|
user_id: userId,
|
|
2109
2148
|
role: userId === me ? "owner" : "member",
|
|
2110
|
-
organization_id:
|
|
2149
|
+
organization_id: destination,
|
|
2111
2150
|
created_by: me
|
|
2112
2151
|
}))
|
|
2113
2152
|
);
|
|
@@ -2117,10 +2156,11 @@ function createMessagingRepository(options) {
|
|
|
2117
2156
|
async insertMessage(draft, clientMessageId) {
|
|
2118
2157
|
const operation = "insertMessage";
|
|
2119
2158
|
const me = await actingUser(operation);
|
|
2159
|
+
const destination = await organizationOfExisting(draft.conversationId);
|
|
2120
2160
|
const { data, error } = await db().from(TABLES.messages).insert({
|
|
2121
2161
|
conversation_id: draft.conversationId,
|
|
2122
2162
|
sender_id: me,
|
|
2123
|
-
organization_id:
|
|
2163
|
+
organization_id: destination,
|
|
2124
2164
|
created_by: me,
|
|
2125
2165
|
content: draft.content,
|
|
2126
2166
|
message_type: draft.kind ?? "text",
|
|
@@ -2181,12 +2221,13 @@ function createMessagingRepository(options) {
|
|
|
2181
2221
|
async addMembers(conversationId, memberIds) {
|
|
2182
2222
|
const operation = "addMembers";
|
|
2183
2223
|
const me = await actingUser(operation);
|
|
2224
|
+
const destination = await organizationOfExisting(conversationId);
|
|
2184
2225
|
const { error } = await db().from(TABLES.participants).insert(
|
|
2185
2226
|
memberIds.map((userId) => ({
|
|
2186
2227
|
conversation_id: conversationId,
|
|
2187
2228
|
user_id: userId,
|
|
2188
2229
|
role: "member",
|
|
2189
|
-
organization_id:
|
|
2230
|
+
organization_id: destination,
|
|
2190
2231
|
created_by: me
|
|
2191
2232
|
}))
|
|
2192
2233
|
);
|