@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/dist/react.d.cts
CHANGED
|
@@ -208,7 +208,11 @@ interface Page<TItem, TCursor> {
|
|
|
208
208
|
/** What the host injects. Identity ONLY — every hard part is in the package. */
|
|
209
209
|
interface MessagingIdentity {
|
|
210
210
|
readonly userId: UserId;
|
|
211
|
-
|
|
211
|
+
/**
|
|
212
|
+
* The active organization, or `null` when none is selected. Reads never
|
|
213
|
+
* depend on it; it is only where a NEW conversation lands.
|
|
214
|
+
*/
|
|
215
|
+
readonly organizationId: OrganizationId | null;
|
|
212
216
|
}
|
|
213
217
|
interface DraftMessage {
|
|
214
218
|
readonly conversationId: ConversationId;
|
|
@@ -525,7 +529,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
|
|
|
525
529
|
* errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
|
|
526
530
|
* warns and retries; it is never a red error.
|
|
527
531
|
*/
|
|
528
|
-
type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
|
|
532
|
+
type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
|
|
533
|
+
/** A NEW conversation needs an organization to land in and none is selected. */
|
|
534
|
+
| "organization-required" | "unknown";
|
|
529
535
|
declare class MessagingError extends Error {
|
|
530
536
|
readonly code: MessagingErrorCode;
|
|
531
537
|
/** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
|
|
@@ -817,15 +823,21 @@ interface RepositoryOptions {
|
|
|
817
823
|
*/
|
|
818
824
|
client: MessagingSupabaseClient;
|
|
819
825
|
/**
|
|
820
|
-
* The active organization
|
|
821
|
-
*
|
|
826
|
+
* The active organization, or `null`/omitted when none is selected.
|
|
827
|
+
*
|
|
828
|
+
* 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
|
|
829
|
+
* read here is answered by RLS as the session's user; the organization is
|
|
830
|
+
* only the DESTINATION of a NEW conversation. Without one, starting a
|
|
831
|
+
* conversation is refused with `organization-required` (the host asks the
|
|
832
|
+
* person to choose), and a message or member added to an EXISTING
|
|
833
|
+
* conversation is stamped with that conversation's own organization.
|
|
822
834
|
*
|
|
823
835
|
* 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
|
|
824
836
|
* acting is read from `client`'s own session — see `session-identity.ts` for
|
|
825
837
|
* the class that closed and why a handed-in id could not be made safe by
|
|
826
838
|
* convention. A caller that still passes `identity` is IGNORED and told so.
|
|
827
839
|
*/
|
|
828
|
-
organizationId
|
|
840
|
+
organizationId?: string | null | undefined;
|
|
829
841
|
/**
|
|
830
842
|
* INTERNAL. The React provider already runs one session reader for its own
|
|
831
843
|
* render loop and hands it here so the two cannot answer differently and so
|
|
@@ -846,8 +858,11 @@ interface RepositoryOptions {
|
|
|
846
858
|
now?: (() => number) | undefined;
|
|
847
859
|
}
|
|
848
860
|
interface MessagingRepository {
|
|
849
|
-
/**
|
|
850
|
-
|
|
861
|
+
/**
|
|
862
|
+
* The active organization new conversations land in, or `null` when none is
|
|
863
|
+
* selected — reads work either way.
|
|
864
|
+
*/
|
|
865
|
+
readonly organizationId: OrganizationId | null;
|
|
851
866
|
/**
|
|
852
867
|
* Who the session says is acting — awaited, because the first `getSession()`
|
|
853
868
|
* is a real read. Rejects with `session-unavailable` when nobody is signed
|
|
@@ -1004,11 +1019,18 @@ interface MessagingSnapshot {
|
|
|
1004
1019
|
} | null;
|
|
1005
1020
|
/** Conversations whose last open failed, keyed by id. Cleared on the next open. */
|
|
1006
1021
|
readonly conversationReadFailures: ReadonlyMap<ConversationId, ConversationReadFailure>;
|
|
1022
|
+
/**
|
|
1023
|
+
* The inbox read FAILED (and no later read succeeded). Without it a failed
|
|
1024
|
+
* first read left `hasLoadedConversations` false forever — a skeleton that
|
|
1025
|
+
* never resolves is a screen telling a lie (RC-B12 round 11).
|
|
1026
|
+
*/
|
|
1027
|
+
readonly conversationsLoadError: string | null;
|
|
1007
1028
|
}
|
|
1008
1029
|
interface MessagingStore {
|
|
1009
1030
|
snapshot(): MessagingSnapshot;
|
|
1010
1031
|
subscribe(listener: (snapshot: MessagingSnapshot) => void): () => void;
|
|
1011
1032
|
setConversations(items: readonly ConversationSummary[], hasMore: boolean): void;
|
|
1033
|
+
setConversationsLoadError(message: string | null): void;
|
|
1012
1034
|
/**
|
|
1013
1035
|
* Switch the archive axis. Clears the list and marks it UNLOADED: the rows
|
|
1014
1036
|
* on screen belong to the old filter, and leaving them there while the new
|
|
@@ -1118,6 +1140,8 @@ interface MessagingEngine {
|
|
|
1118
1140
|
* refreshes the archived count the reveal label prints.
|
|
1119
1141
|
*/
|
|
1120
1142
|
setArchiveFilter(next: MessagingArchiveFilter): Promise<void>;
|
|
1143
|
+
/** Read the inbox again (the failed-read view's Try again). Never throws; a failure lands in the snapshot. */
|
|
1144
|
+
refreshInbox(): Promise<void>;
|
|
1121
1145
|
/** Re-read how many archived conversations exist, for the reveal's label. */
|
|
1122
1146
|
refreshArchivedCount(): Promise<void>;
|
|
1123
1147
|
/** Open a conversation: load its thread and subscribe to its channel. */
|
|
@@ -1269,7 +1293,12 @@ interface MessagingProviderProps {
|
|
|
1269
1293
|
* (DD-241). There is no `userId` prop, on purpose.
|
|
1270
1294
|
*/
|
|
1271
1295
|
client: MessagingSupabaseClient | null | undefined;
|
|
1272
|
-
/**
|
|
1296
|
+
/**
|
|
1297
|
+
* The active organization, or `null` when none is selected. Messaging runs
|
|
1298
|
+
* without one: every read is the person's own. It is the destination of a
|
|
1299
|
+
* NEW conversation (refused as `organization-required` without it) and of
|
|
1300
|
+
* AI runs (the AI actions do not render without it).
|
|
1301
|
+
*/
|
|
1273
1302
|
organizationId: string | null | undefined;
|
|
1274
1303
|
/**
|
|
1275
1304
|
* The Matrx API transport for AI features. Omit to run messaging without AI;
|
|
@@ -1356,6 +1385,10 @@ interface UseConversationsResult {
|
|
|
1356
1385
|
hasMore: boolean;
|
|
1357
1386
|
/** True only on the FIRST load; a refresh must not blank the list. */
|
|
1358
1387
|
isInitialLoading: boolean;
|
|
1388
|
+
/** The inbox read failed; the list shows the failure (never a skeleton forever, never "empty"). */
|
|
1389
|
+
loadError: string | null;
|
|
1390
|
+
/** Read the inbox again. */
|
|
1391
|
+
retry: () => void;
|
|
1359
1392
|
totalUnreadConversations: number;
|
|
1360
1393
|
loadMore: () => void;
|
|
1361
1394
|
select: (id: ConversationId) => void;
|
|
@@ -1778,9 +1811,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
|
|
|
1778
1811
|
|
|
1779
1812
|
declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
|
|
1780
1813
|
declare function projectMessageAction(value: unknown): MessageAction | null;
|
|
1781
|
-
declare function projectMessage(row: Record<string, unknown>,
|
|
1814
|
+
declare function projectMessage(row: Record<string, unknown>,
|
|
1815
|
+
/** The active organization, or `null` when none is selected (reads never need one). */
|
|
1816
|
+
fallbackOrganizationId: OrganizationId | null): Message;
|
|
1782
1817
|
declare function projectParticipantRole(value: unknown): ParticipantRole;
|
|
1783
|
-
declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
|
|
1818
|
+
declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
|
|
1819
|
+
/** The active organization, or `null` when none is selected (reads never need one). */
|
|
1820
|
+
fallbackOrganizationId: OrganizationId | null): ConversationSummary;
|
|
1784
1821
|
|
|
1785
1822
|
/**
|
|
1786
1823
|
* MATRX REFERENCES — a message can name a platform entity, and everything it
|
package/dist/react.d.ts
CHANGED
|
@@ -208,7 +208,11 @@ interface Page<TItem, TCursor> {
|
|
|
208
208
|
/** What the host injects. Identity ONLY — every hard part is in the package. */
|
|
209
209
|
interface MessagingIdentity {
|
|
210
210
|
readonly userId: UserId;
|
|
211
|
-
|
|
211
|
+
/**
|
|
212
|
+
* The active organization, or `null` when none is selected. Reads never
|
|
213
|
+
* depend on it; it is only where a NEW conversation lands.
|
|
214
|
+
*/
|
|
215
|
+
readonly organizationId: OrganizationId | null;
|
|
212
216
|
}
|
|
213
217
|
interface DraftMessage {
|
|
214
218
|
readonly conversationId: ConversationId;
|
|
@@ -525,7 +529,9 @@ declare function createOutbox(options: OutboxOptions): Outbox;
|
|
|
525
529
|
* errors in 0.6 seconds. A missing session is a NORMAL lifecycle moment — it
|
|
526
530
|
* warns and retries; it is never a red error.
|
|
527
531
|
*/
|
|
528
|
-
type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
|
|
532
|
+
type MessagingErrorCode = "session-unavailable" | "contention" | "forbidden" | "not-found" | "conflict" | "transport" | "invalid-response" | "misconfigured"
|
|
533
|
+
/** A NEW conversation needs an organization to land in and none is selected. */
|
|
534
|
+
| "organization-required" | "unknown";
|
|
529
535
|
declare class MessagingError extends Error {
|
|
530
536
|
readonly code: MessagingErrorCode;
|
|
531
537
|
/** The remedy. Nothing fails silently, and nothing fails without saying what to do. */
|
|
@@ -817,15 +823,21 @@ interface RepositoryOptions {
|
|
|
817
823
|
*/
|
|
818
824
|
client: MessagingSupabaseClient;
|
|
819
825
|
/**
|
|
820
|
-
* The active organization
|
|
821
|
-
*
|
|
826
|
+
* The active organization, or `null`/omitted when none is selected.
|
|
827
|
+
*
|
|
828
|
+
* 🚨 READS NEVER NEED IT (access belongs to the person, 2026-09-23). Every
|
|
829
|
+
* read here is answered by RLS as the session's user; the organization is
|
|
830
|
+
* only the DESTINATION of a NEW conversation. Without one, starting a
|
|
831
|
+
* conversation is refused with `organization-required` (the host asks the
|
|
832
|
+
* person to choose), and a message or member added to an EXISTING
|
|
833
|
+
* conversation is stamped with that conversation's own organization.
|
|
822
834
|
*
|
|
823
835
|
* 🚨 THERE IS NO `userId` HERE, AND THERE WILL NOT BE ONE (DD-241). Who is
|
|
824
836
|
* acting is read from `client`'s own session — see `session-identity.ts` for
|
|
825
837
|
* the class that closed and why a handed-in id could not be made safe by
|
|
826
838
|
* convention. A caller that still passes `identity` is IGNORED and told so.
|
|
827
839
|
*/
|
|
828
|
-
organizationId
|
|
840
|
+
organizationId?: string | null | undefined;
|
|
829
841
|
/**
|
|
830
842
|
* INTERNAL. The React provider already runs one session reader for its own
|
|
831
843
|
* render loop and hands it here so the two cannot answer differently and so
|
|
@@ -846,8 +858,11 @@ interface RepositoryOptions {
|
|
|
846
858
|
now?: (() => number) | undefined;
|
|
847
859
|
}
|
|
848
860
|
interface MessagingRepository {
|
|
849
|
-
/**
|
|
850
|
-
|
|
861
|
+
/**
|
|
862
|
+
* The active organization new conversations land in, or `null` when none is
|
|
863
|
+
* selected — reads work either way.
|
|
864
|
+
*/
|
|
865
|
+
readonly organizationId: OrganizationId | null;
|
|
851
866
|
/**
|
|
852
867
|
* Who the session says is acting — awaited, because the first `getSession()`
|
|
853
868
|
* is a real read. Rejects with `session-unavailable` when nobody is signed
|
|
@@ -1004,11 +1019,18 @@ interface MessagingSnapshot {
|
|
|
1004
1019
|
} | null;
|
|
1005
1020
|
/** Conversations whose last open failed, keyed by id. Cleared on the next open. */
|
|
1006
1021
|
readonly conversationReadFailures: ReadonlyMap<ConversationId, ConversationReadFailure>;
|
|
1022
|
+
/**
|
|
1023
|
+
* The inbox read FAILED (and no later read succeeded). Without it a failed
|
|
1024
|
+
* first read left `hasLoadedConversations` false forever — a skeleton that
|
|
1025
|
+
* never resolves is a screen telling a lie (RC-B12 round 11).
|
|
1026
|
+
*/
|
|
1027
|
+
readonly conversationsLoadError: string | null;
|
|
1007
1028
|
}
|
|
1008
1029
|
interface MessagingStore {
|
|
1009
1030
|
snapshot(): MessagingSnapshot;
|
|
1010
1031
|
subscribe(listener: (snapshot: MessagingSnapshot) => void): () => void;
|
|
1011
1032
|
setConversations(items: readonly ConversationSummary[], hasMore: boolean): void;
|
|
1033
|
+
setConversationsLoadError(message: string | null): void;
|
|
1012
1034
|
/**
|
|
1013
1035
|
* Switch the archive axis. Clears the list and marks it UNLOADED: the rows
|
|
1014
1036
|
* on screen belong to the old filter, and leaving them there while the new
|
|
@@ -1118,6 +1140,8 @@ interface MessagingEngine {
|
|
|
1118
1140
|
* refreshes the archived count the reveal label prints.
|
|
1119
1141
|
*/
|
|
1120
1142
|
setArchiveFilter(next: MessagingArchiveFilter): Promise<void>;
|
|
1143
|
+
/** Read the inbox again (the failed-read view's Try again). Never throws; a failure lands in the snapshot. */
|
|
1144
|
+
refreshInbox(): Promise<void>;
|
|
1121
1145
|
/** Re-read how many archived conversations exist, for the reveal's label. */
|
|
1122
1146
|
refreshArchivedCount(): Promise<void>;
|
|
1123
1147
|
/** Open a conversation: load its thread and subscribe to its channel. */
|
|
@@ -1269,7 +1293,12 @@ interface MessagingProviderProps {
|
|
|
1269
1293
|
* (DD-241). There is no `userId` prop, on purpose.
|
|
1270
1294
|
*/
|
|
1271
1295
|
client: MessagingSupabaseClient | null | undefined;
|
|
1272
|
-
/**
|
|
1296
|
+
/**
|
|
1297
|
+
* The active organization, or `null` when none is selected. Messaging runs
|
|
1298
|
+
* without one: every read is the person's own. It is the destination of a
|
|
1299
|
+
* NEW conversation (refused as `organization-required` without it) and of
|
|
1300
|
+
* AI runs (the AI actions do not render without it).
|
|
1301
|
+
*/
|
|
1273
1302
|
organizationId: string | null | undefined;
|
|
1274
1303
|
/**
|
|
1275
1304
|
* The Matrx API transport for AI features. Omit to run messaging without AI;
|
|
@@ -1356,6 +1385,10 @@ interface UseConversationsResult {
|
|
|
1356
1385
|
hasMore: boolean;
|
|
1357
1386
|
/** True only on the FIRST load; a refresh must not blank the list. */
|
|
1358
1387
|
isInitialLoading: boolean;
|
|
1388
|
+
/** The inbox read failed; the list shows the failure (never a skeleton forever, never "empty"). */
|
|
1389
|
+
loadError: string | null;
|
|
1390
|
+
/** Read the inbox again. */
|
|
1391
|
+
retry: () => void;
|
|
1359
1392
|
totalUnreadConversations: number;
|
|
1360
1393
|
loadMore: () => void;
|
|
1361
1394
|
select: (id: ConversationId) => void;
|
|
@@ -1778,9 +1811,13 @@ declare function formatLastSeen(lastSeenMs: number | null, now?: number): string
|
|
|
1778
1811
|
|
|
1779
1812
|
declare function projectUserSummary(row: Record<string, unknown>): UserSummary;
|
|
1780
1813
|
declare function projectMessageAction(value: unknown): MessageAction | null;
|
|
1781
|
-
declare function projectMessage(row: Record<string, unknown>,
|
|
1814
|
+
declare function projectMessage(row: Record<string, unknown>,
|
|
1815
|
+
/** The active organization, or `null` when none is selected (reads never need one). */
|
|
1816
|
+
fallbackOrganizationId: OrganizationId | null): Message;
|
|
1782
1817
|
declare function projectParticipantRole(value: unknown): ParticipantRole;
|
|
1783
|
-
declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
|
|
1818
|
+
declare function projectConversationSummary(row: Record<string, unknown>, viewerId: UserId,
|
|
1819
|
+
/** The active organization, or `null` when none is selected (reads never need one). */
|
|
1820
|
+
fallbackOrganizationId: OrganizationId | null): ConversationSummary;
|
|
1784
1821
|
|
|
1785
1822
|
/**
|
|
1786
1823
|
* MATRX REFERENCES — a message can name a platform entity, and everything it
|
package/dist/react.js
CHANGED
|
@@ -762,7 +762,7 @@ function projectMessage(row, fallbackOrganizationId) {
|
|
|
762
762
|
id: requiredStr(row, "id", operation),
|
|
763
763
|
conversationId: requiredStr(row, "conversation_id", operation),
|
|
764
764
|
senderId: requiredStr(row, "sender_id", operation),
|
|
765
|
-
organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
|
|
765
|
+
organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
|
|
766
766
|
content: typeof row["content"] === "string" ? row["content"] : "",
|
|
767
767
|
kind: MESSAGE_KINDS.has(kindRaw) ? kindRaw : "text",
|
|
768
768
|
// A row read from the DB is at least `sent`. `sending`/`failed` are outbox
|
|
@@ -810,7 +810,7 @@ function projectConversationSummary(row, viewerId, fallbackOrganizationId) {
|
|
|
810
810
|
groupName,
|
|
811
811
|
groupImageUrl,
|
|
812
812
|
createdBy: createdBy === null ? null : createdBy,
|
|
813
|
-
organizationId: str(row, "organization_id") ?? fallbackOrganizationId,
|
|
813
|
+
organizationId: str(row, "organization_id") ?? fallbackOrganizationId ?? "",
|
|
814
814
|
createdAt,
|
|
815
815
|
updatedAt,
|
|
816
816
|
metadata: jsonObject(row["metadata"])
|
|
@@ -876,6 +876,7 @@ function createMessagingStore() {
|
|
|
876
876
|
let threads = /* @__PURE__ */ new Map();
|
|
877
877
|
let activeConversationId = null;
|
|
878
878
|
let conversationReadFailures = /* @__PURE__ */ new Map();
|
|
879
|
+
let conversationsLoadError = null;
|
|
879
880
|
const listeners = /* @__PURE__ */ new Set();
|
|
880
881
|
let cached = null;
|
|
881
882
|
function snapshot() {
|
|
@@ -889,7 +890,8 @@ function createMessagingStore() {
|
|
|
889
890
|
totalUnreadConversations: conversations.filter((item) => item.unreadCount > 0).length,
|
|
890
891
|
archiveFilter,
|
|
891
892
|
archivedCount,
|
|
892
|
-
conversationReadFailures
|
|
893
|
+
conversationReadFailures,
|
|
894
|
+
conversationsLoadError
|
|
893
895
|
};
|
|
894
896
|
return cached;
|
|
895
897
|
}
|
|
@@ -934,6 +936,12 @@ function createMessagingStore() {
|
|
|
934
936
|
conversations = normalizeConversations(items);
|
|
935
937
|
hasMoreConversations = hasMore;
|
|
936
938
|
hasLoadedConversations = true;
|
|
939
|
+
conversationsLoadError = null;
|
|
940
|
+
emit();
|
|
941
|
+
},
|
|
942
|
+
setConversationsLoadError(message) {
|
|
943
|
+
if (message === conversationsLoadError) return;
|
|
944
|
+
conversationsLoadError = message;
|
|
937
945
|
emit();
|
|
938
946
|
},
|
|
939
947
|
setArchiveFilter(next) {
|
|
@@ -1143,7 +1151,11 @@ function createMessagingEngine(options) {
|
|
|
1143
1151
|
optimisticMessage({
|
|
1144
1152
|
conversationId: entry.draft.conversationId,
|
|
1145
1153
|
senderId: identity.userId,
|
|
1146
|
-
|
|
1154
|
+
// Client-only until the confirmed row replaces it: the active
|
|
1155
|
+
// organization, else the conversation's own.
|
|
1156
|
+
organizationId: identity.organizationId ?? store.snapshot().conversations.find(
|
|
1157
|
+
(item) => item.conversation.id === entry.draft.conversationId
|
|
1158
|
+
)?.conversation.organizationId ?? "",
|
|
1147
1159
|
content: entry.draft.content,
|
|
1148
1160
|
clientMessageId: entry.clientMessageId,
|
|
1149
1161
|
...entry.draft.kind !== void 0 ? { kind: entry.draft.kind } : {},
|
|
@@ -1162,10 +1174,20 @@ function createMessagingEngine(options) {
|
|
|
1162
1174
|
}
|
|
1163
1175
|
async function reloadInbox() {
|
|
1164
1176
|
if (disposed) return;
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1177
|
+
let page;
|
|
1178
|
+
try {
|
|
1179
|
+
page = await repository.listConversations({
|
|
1180
|
+
limit: conversationPageSize,
|
|
1181
|
+
archived: store.snapshot().archiveFilter
|
|
1182
|
+
});
|
|
1183
|
+
} catch (error) {
|
|
1184
|
+
if (!disposed) {
|
|
1185
|
+
store.setConversationsLoadError(
|
|
1186
|
+
error instanceof Error ? error.message : typeof error === "string" ? error : "The conversation list could not be read."
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
throw error;
|
|
1190
|
+
}
|
|
1169
1191
|
if (disposed) return;
|
|
1170
1192
|
conversationCursor = page.nextCursor;
|
|
1171
1193
|
store.setConversations(page.items, page.hasMore);
|
|
@@ -1250,6 +1272,13 @@ function createMessagingEngine(options) {
|
|
|
1250
1272
|
}
|
|
1251
1273
|
});
|
|
1252
1274
|
},
|
|
1275
|
+
async refreshInbox() {
|
|
1276
|
+
try {
|
|
1277
|
+
await reloadInbox();
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
reportError(error, "inboxRefresh");
|
|
1280
|
+
}
|
|
1281
|
+
},
|
|
1253
1282
|
async setArchiveFilter(next) {
|
|
1254
1283
|
if (next === store.snapshot().archiveFilter) return;
|
|
1255
1284
|
store.setArchiveFilter(next);
|
|
@@ -1632,12 +1661,15 @@ var RPCS = {
|
|
|
1632
1661
|
};
|
|
1633
1662
|
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";
|
|
1634
1663
|
var CONVERSATION_COLUMNS = "id,type,group_name,group_image_url,created_by,organization_id,created_at,updated_at,metadata";
|
|
1664
|
+
function optionalOrg(organizationId) {
|
|
1665
|
+
return typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
|
|
1666
|
+
}
|
|
1635
1667
|
function requireOrg(organizationId, operation) {
|
|
1636
|
-
if (
|
|
1668
|
+
if (organizationId === null) {
|
|
1637
1669
|
throw new MessagingError(
|
|
1638
|
-
"
|
|
1639
|
-
`${operation}: no
|
|
1640
|
-
"
|
|
1670
|
+
"organization-required",
|
|
1671
|
+
`${operation}: no organization selected`,
|
|
1672
|
+
"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."
|
|
1641
1673
|
);
|
|
1642
1674
|
}
|
|
1643
1675
|
return organizationId;
|
|
@@ -1649,7 +1681,7 @@ function sleep(ms) {
|
|
|
1649
1681
|
return new Promise((resolve) => setTimeout(resolve, jittered));
|
|
1650
1682
|
}
|
|
1651
1683
|
function createMessagingRepository(options) {
|
|
1652
|
-
const org =
|
|
1684
|
+
const org = optionalOrg(options.organizationId);
|
|
1653
1685
|
const handedIn = options.identity;
|
|
1654
1686
|
if (handedIn !== void 0 && handedIn !== null && handedIn.userId !== void 0) {
|
|
1655
1687
|
options.onDiagnostic?.(
|
|
@@ -1719,6 +1751,11 @@ function createMessagingRepository(options) {
|
|
|
1719
1751
|
});
|
|
1720
1752
|
return found;
|
|
1721
1753
|
}
|
|
1754
|
+
async function organizationOfExisting(conversationId) {
|
|
1755
|
+
if (org !== null) return org;
|
|
1756
|
+
const conversation = await repository.getConversation(conversationId);
|
|
1757
|
+
return conversation.organizationId;
|
|
1758
|
+
}
|
|
1722
1759
|
const repository = {
|
|
1723
1760
|
organizationId: org,
|
|
1724
1761
|
actingUserId,
|
|
@@ -1851,6 +1888,7 @@ function createMessagingRepository(options) {
|
|
|
1851
1888
|
},
|
|
1852
1889
|
async getOrCreateDirectConversation(otherUserId) {
|
|
1853
1890
|
const operation = "getOrCreateDirectConversation";
|
|
1891
|
+
const destination = requireOrg(org, operation);
|
|
1854
1892
|
const me = await actingUser(operation);
|
|
1855
1893
|
const id = await withSessionRetry(
|
|
1856
1894
|
operation,
|
|
@@ -1862,7 +1900,7 @@ function createMessagingRepository(options) {
|
|
|
1862
1900
|
// not a preference.
|
|
1863
1901
|
p_user1_id: me,
|
|
1864
1902
|
p_user2_id: otherUserId,
|
|
1865
|
-
p_organization_id:
|
|
1903
|
+
p_organization_id: destination
|
|
1866
1904
|
},
|
|
1867
1905
|
operation
|
|
1868
1906
|
)
|
|
@@ -1874,13 +1912,14 @@ function createMessagingRepository(options) {
|
|
|
1874
1912
|
},
|
|
1875
1913
|
async createGroupConversation({ name, memberIds }) {
|
|
1876
1914
|
const operation = "createGroupConversation";
|
|
1915
|
+
const destination = requireOrg(org, operation);
|
|
1877
1916
|
const me = await actingUser(operation);
|
|
1878
1917
|
const { data, error } = await withSessionRetry(
|
|
1879
1918
|
operation,
|
|
1880
1919
|
async () => db().from(TABLES.conversations).insert({
|
|
1881
1920
|
type: "group",
|
|
1882
1921
|
group_name: name,
|
|
1883
|
-
organization_id:
|
|
1922
|
+
organization_id: destination,
|
|
1884
1923
|
created_by: me
|
|
1885
1924
|
}).select("id").single()
|
|
1886
1925
|
);
|
|
@@ -1895,7 +1934,7 @@ function createMessagingRepository(options) {
|
|
|
1895
1934
|
conversation_id: conversationId,
|
|
1896
1935
|
user_id: userId,
|
|
1897
1936
|
role: userId === me ? "owner" : "member",
|
|
1898
|
-
organization_id:
|
|
1937
|
+
organization_id: destination,
|
|
1899
1938
|
created_by: me
|
|
1900
1939
|
}))
|
|
1901
1940
|
);
|
|
@@ -1905,10 +1944,11 @@ function createMessagingRepository(options) {
|
|
|
1905
1944
|
async insertMessage(draft, clientMessageId) {
|
|
1906
1945
|
const operation = "insertMessage";
|
|
1907
1946
|
const me = await actingUser(operation);
|
|
1947
|
+
const destination = await organizationOfExisting(draft.conversationId);
|
|
1908
1948
|
const { data, error } = await db().from(TABLES.messages).insert({
|
|
1909
1949
|
conversation_id: draft.conversationId,
|
|
1910
1950
|
sender_id: me,
|
|
1911
|
-
organization_id:
|
|
1951
|
+
organization_id: destination,
|
|
1912
1952
|
created_by: me,
|
|
1913
1953
|
content: draft.content,
|
|
1914
1954
|
message_type: draft.kind ?? "text",
|
|
@@ -1969,12 +2009,13 @@ function createMessagingRepository(options) {
|
|
|
1969
2009
|
async addMembers(conversationId, memberIds) {
|
|
1970
2010
|
const operation = "addMembers";
|
|
1971
2011
|
const me = await actingUser(operation);
|
|
2012
|
+
const destination = await organizationOfExisting(conversationId);
|
|
1972
2013
|
const { error } = await db().from(TABLES.participants).insert(
|
|
1973
2014
|
memberIds.map((userId) => ({
|
|
1974
2015
|
conversation_id: conversationId,
|
|
1975
2016
|
user_id: userId,
|
|
1976
2017
|
role: "member",
|
|
1977
|
-
organization_id:
|
|
2018
|
+
organization_id: destination,
|
|
1978
2019
|
created_by: me
|
|
1979
2020
|
}))
|
|
1980
2021
|
);
|
|
@@ -2085,7 +2126,7 @@ function MessagingProvider(props) {
|
|
|
2085
2126
|
}, [strayUserId]);
|
|
2086
2127
|
const session = useSessionUserId(client, props.onDiagnostic);
|
|
2087
2128
|
const userId = session.snapshot.userId;
|
|
2088
|
-
const ready = client != null && userId !== null
|
|
2129
|
+
const ready = client != null && userId !== null;
|
|
2089
2130
|
const hostProvidesRealtime = useIsRealtimeProviderMounted();
|
|
2090
2131
|
const runtime = /* @__PURE__ */ jsx(
|
|
2091
2132
|
MessagingRuntime,
|
|
@@ -2121,12 +2162,13 @@ function MessagingRuntime(props) {
|
|
|
2121
2162
|
actionRegistry.register(handler);
|
|
2122
2163
|
});
|
|
2123
2164
|
}, [actionRegistry, handlers]);
|
|
2124
|
-
const ready = client != null && manager !== null && sessionIdentity !== null && userId !== null
|
|
2165
|
+
const ready = client != null && manager !== null && sessionIdentity !== null && userId !== null;
|
|
2166
|
+
const activeOrganizationId = typeof organizationId === "string" && organizationId.length > 0 ? organizationId : null;
|
|
2125
2167
|
const engine = useMemo(() => {
|
|
2126
2168
|
if (!ready || manager === null || sessionIdentity === null || userId === null) return null;
|
|
2127
2169
|
const identity = {
|
|
2128
2170
|
userId,
|
|
2129
|
-
organizationId
|
|
2171
|
+
organizationId: activeOrganizationId
|
|
2130
2172
|
};
|
|
2131
2173
|
const report = (event) => {
|
|
2132
2174
|
(diagnosticRef.current ?? defaultDiagnostics)(event);
|
|
@@ -2137,7 +2179,7 @@ function MessagingRuntime(props) {
|
|
|
2137
2179
|
// supabase-shape.ts); everything below this line works against the rich
|
|
2138
2180
|
// contract, which `supabase-shape.test.ts` proves a real client honors.
|
|
2139
2181
|
client,
|
|
2140
|
-
organizationId,
|
|
2182
|
+
organizationId: activeOrganizationId,
|
|
2141
2183
|
// THE SAME READER the render loop above uses. Two readers would be two
|
|
2142
2184
|
// answers during the tick an account switch takes.
|
|
2143
2185
|
sessionIdentity,
|
|
@@ -2170,14 +2212,14 @@ function MessagingRuntime(props) {
|
|
|
2170
2212
|
}
|
|
2171
2213
|
});
|
|
2172
2214
|
return built;
|
|
2173
|
-
}, [ready, manager, sessionIdentity, userId,
|
|
2215
|
+
}, [ready, manager, sessionIdentity, userId, activeOrganizationId]);
|
|
2174
2216
|
useEffect(() => {
|
|
2175
2217
|
if (engine === null) return void 0;
|
|
2176
2218
|
void engine.start().catch((error) => {
|
|
2177
2219
|
(diagnosticRef.current ?? defaultDiagnostics)({
|
|
2178
2220
|
level: "error",
|
|
2179
2221
|
message: `Messaging failed to start: ${String(error?.message ?? error)}`,
|
|
2180
|
-
remedy: "Check the signed-in session
|
|
2222
|
+
remedy: "Check the signed-in session, then reload the surface."
|
|
2181
2223
|
});
|
|
2182
2224
|
});
|
|
2183
2225
|
return () => {
|
|
@@ -2191,9 +2233,10 @@ function MessagingRuntime(props) {
|
|
|
2191
2233
|
const sourceFeature = props.sourceFeature;
|
|
2192
2234
|
const ai = useMemo(() => {
|
|
2193
2235
|
if (transport === void 0 || agents === void 0 || !ready) return null;
|
|
2236
|
+
if (activeOrganizationId === null) return null;
|
|
2194
2237
|
return createMessagingAi({
|
|
2195
2238
|
transport,
|
|
2196
|
-
organizationId,
|
|
2239
|
+
organizationId: activeOrganizationId,
|
|
2197
2240
|
agents,
|
|
2198
2241
|
...maxTranscriptMessages !== void 0 ? { maxTranscriptMessages } : {},
|
|
2199
2242
|
...sourceApp !== void 0 ? { sourceApp } : {},
|
|
@@ -2203,7 +2246,7 @@ function MessagingRuntime(props) {
|
|
|
2203
2246
|
transport,
|
|
2204
2247
|
agents,
|
|
2205
2248
|
ready,
|
|
2206
|
-
|
|
2249
|
+
activeOrganizationId,
|
|
2207
2250
|
maxTranscriptMessages,
|
|
2208
2251
|
sourceApp,
|
|
2209
2252
|
sourceFeature
|
|
@@ -2254,7 +2297,7 @@ function useRequiredMessagingHost() {
|
|
|
2254
2297
|
const host = useMessagingHost();
|
|
2255
2298
|
if (host === null) {
|
|
2256
2299
|
throw new Error(
|
|
2257
|
-
"[@ai-matrx/messaging] No messaging host. Wrap this tree in <MessagingProvider client={supabase}
|
|
2300
|
+
"[@ai-matrx/messaging] No messaging host. Wrap this tree in <MessagingProvider client={supabase} organizationId={orgId}> and render it only once auth has resolved."
|
|
2258
2301
|
);
|
|
2259
2302
|
}
|
|
2260
2303
|
return host;
|
|
@@ -2379,7 +2422,11 @@ function useConversations() {
|
|
|
2379
2422
|
hasMore: snapshot?.hasMoreConversations ?? false,
|
|
2380
2423
|
// "Not loaded yet" is a FACT from the store, never a guess. Showing
|
|
2381
2424
|
// "No conversations yet" before the first read is a screen telling a lie.
|
|
2382
|
-
isInitialLoading: snapshot === null || !snapshot.hasLoadedConversations,
|
|
2425
|
+
isInitialLoading: snapshot === null || !snapshot.hasLoadedConversations && snapshot.conversationsLoadError === null,
|
|
2426
|
+
loadError: snapshot?.conversationsLoadError ?? null,
|
|
2427
|
+
retry: () => {
|
|
2428
|
+
void host?.engine.refreshInbox();
|
|
2429
|
+
},
|
|
2383
2430
|
totalUnreadConversations: snapshot?.totalUnreadConversations ?? 0,
|
|
2384
2431
|
loadMore: () => {
|
|
2385
2432
|
void host?.engine.loadMoreConversations();
|
|
@@ -2644,7 +2691,8 @@ function useMessageAction(message) {
|
|
|
2644
2691
|
const context = {
|
|
2645
2692
|
messageId: message.id,
|
|
2646
2693
|
actorId: host.identity.userId,
|
|
2647
|
-
|
|
2694
|
+
// The active organization, else the organization the message lives in.
|
|
2695
|
+
organizationId: host.identity.organizationId ?? message.organizationId,
|
|
2648
2696
|
choice
|
|
2649
2697
|
};
|
|
2650
2698
|
host.actions.execute(action, context).then(setReceipt).catch((cause) => {
|
|
@@ -2965,6 +3013,7 @@ function EmptyState(props) {
|
|
|
2965
3013
|
}
|
|
2966
3014
|
|
|
2967
3015
|
// src/react/components.tsx
|
|
3016
|
+
import { ErrorBox } from "@ai-matrx/design-system";
|
|
2968
3017
|
import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
2969
3018
|
var AI_LABELS = {
|
|
2970
3019
|
catchUp: "Catch me up",
|
|
@@ -2982,7 +3031,9 @@ function ConversationList(props) {
|
|
|
2982
3031
|
activeConversationId,
|
|
2983
3032
|
archiveFilter,
|
|
2984
3033
|
setArchiveFilter,
|
|
2985
|
-
archivedCount
|
|
3034
|
+
archivedCount,
|
|
3035
|
+
loadError,
|
|
3036
|
+
retry
|
|
2986
3037
|
} = useConversations();
|
|
2987
3038
|
const RowChrome = useMessagingHost()?.wrapConversationRow ?? null;
|
|
2988
3039
|
const [query, setQuery] = useState2("");
|
|
@@ -3047,7 +3098,20 @@ function ConversationList(props) {
|
|
|
3047
3098
|
}
|
|
3048
3099
|
) }) : null,
|
|
3049
3100
|
/* @__PURE__ */ jsxs2("div", { className: "mx-msg__scroll", children: [
|
|
3050
|
-
isInitialLoading ? /* @__PURE__ */ jsx4(ConversationSkeleton, {}) : visible.length === 0 ?
|
|
3101
|
+
isInitialLoading ? /* @__PURE__ */ jsx4(ConversationSkeleton, {}) : loadError !== null && visible.length === 0 ? (
|
|
3102
|
+
// A failed read resolves to the failure — never a skeleton forever,
|
|
3103
|
+
// never "No conversations yet" (RC-B12 round 11). ErrorBox is the
|
|
3104
|
+
// design-system error slot: the host fills it with its menu.
|
|
3105
|
+
/* @__PURE__ */ jsx4(
|
|
3106
|
+
ReadFailureBox,
|
|
3107
|
+
{
|
|
3108
|
+
title: "Couldn't load your conversations",
|
|
3109
|
+
message: loadError,
|
|
3110
|
+
operation: "List your conversations",
|
|
3111
|
+
onRetry: retry
|
|
3112
|
+
}
|
|
3113
|
+
)
|
|
3114
|
+
) : visible.length === 0 ? /* @__PURE__ */ jsx4(
|
|
3051
3115
|
EmptyState,
|
|
3052
3116
|
{
|
|
3053
3117
|
title: query.length > 0 ? "No matches" : showingArchive ? "No archived conversations" : "No conversations yet",
|
|
@@ -3224,10 +3288,11 @@ function ConversationView(props) {
|
|
|
3224
3288
|
body: "It doesn't exist, or you aren't a member of it."
|
|
3225
3289
|
}
|
|
3226
3290
|
) : conversation.error !== null && messages.length === 0 ? /* @__PURE__ */ jsx4(
|
|
3227
|
-
|
|
3291
|
+
ReadFailureBox,
|
|
3228
3292
|
{
|
|
3229
3293
|
title: "Couldn't load this conversation",
|
|
3230
|
-
|
|
3294
|
+
message: conversation.error.message || "Check your connection, then open it again.",
|
|
3295
|
+
operation: "Open this conversation"
|
|
3231
3296
|
}
|
|
3232
3297
|
) : messages.length === 0 ? /* @__PURE__ */ jsx4(EmptyState, { title: "No messages yet", body: "Say something to get started." }) : /* @__PURE__ */ jsxs2("div", { className: "mx-msg__messages", children: [
|
|
3233
3298
|
conversation.hasMoreOlder ? /* @__PURE__ */ jsx4(
|
|
@@ -3558,6 +3623,29 @@ function MessagingInbox(props) {
|
|
|
3558
3623
|
}
|
|
3559
3624
|
);
|
|
3560
3625
|
}
|
|
3626
|
+
function ReadFailureBox({
|
|
3627
|
+
title,
|
|
3628
|
+
message,
|
|
3629
|
+
operation,
|
|
3630
|
+
onRetry
|
|
3631
|
+
}) {
|
|
3632
|
+
return /* @__PURE__ */ jsxs2("div", { className: "mx-msg__empty", role: "alert", children: [
|
|
3633
|
+
/* @__PURE__ */ jsx4(
|
|
3634
|
+
ErrorBox,
|
|
3635
|
+
{
|
|
3636
|
+
as: "div",
|
|
3637
|
+
title,
|
|
3638
|
+
message,
|
|
3639
|
+
operation,
|
|
3640
|
+
origin: "messaging",
|
|
3641
|
+
style: { color: "var(--mx-msg-danger)", fontWeight: 600 },
|
|
3642
|
+
children: title
|
|
3643
|
+
}
|
|
3644
|
+
),
|
|
3645
|
+
/* @__PURE__ */ jsx4("p", { children: message }),
|
|
3646
|
+
onRetry ? /* @__PURE__ */ jsx4("button", { type: "button", className: "mx-msg__button mx-msg__button--ghost", onClick: onRetry, children: "Try again" }) : null
|
|
3647
|
+
] });
|
|
3648
|
+
}
|
|
3561
3649
|
export {
|
|
3562
3650
|
AgentTag,
|
|
3563
3651
|
AlertIcon,
|