@poolse/react 1.0.6 → 2.0.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 +38 -0
- package/README.md +270 -36
- package/dist/index.cjs +46 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -21
- package/dist/index.d.ts +29 -21
- package/dist/index.js +46 -30
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -125,17 +125,19 @@ interface UseMembersState {
|
|
|
125
125
|
error: Error | null;
|
|
126
126
|
refetch: () => Promise<void>;
|
|
127
127
|
/**
|
|
128
|
-
* Add one or more users by external_id
|
|
129
|
-
*
|
|
128
|
+
* Add one or more users by `external_id`. Unknown ids are
|
|
129
|
+
* auto-provisioned on the server. Returned memberships are
|
|
130
|
+
* appended to local state on success.
|
|
130
131
|
*/
|
|
131
132
|
addMembers: (externalIds: string[], opts?: {
|
|
132
133
|
role?: MemberRole;
|
|
133
134
|
}) => Promise<Membership[]>;
|
|
134
135
|
/**
|
|
135
|
-
* Remove a member by
|
|
136
|
-
*
|
|
136
|
+
* Remove a member by `external_id`. The SDK translates to the
|
|
137
|
+
* internal user_id under the hood. Optimistically removes from
|
|
138
|
+
* state; rolls back on server error.
|
|
137
139
|
*/
|
|
138
|
-
removeMember: (
|
|
140
|
+
removeMember: (externalId: string) => Promise<void>;
|
|
139
141
|
}
|
|
140
142
|
/**
|
|
141
143
|
* Live-ish membership list for a conversation. Initial REST fetch on
|
|
@@ -286,12 +288,16 @@ interface UseThreadState {
|
|
|
286
288
|
declare function useThread(conversationId: Uuid, rootMessageId: Uuid): UseThreadState;
|
|
287
289
|
|
|
288
290
|
interface UseTypingState {
|
|
289
|
-
/** Set of user_ids currently typing. */
|
|
290
|
-
typing: Set<Uuid>;
|
|
291
291
|
/**
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
292
|
+
* Set of `external_id`s currently typing (your own user identifiers,
|
|
293
|
+
* the same string you'd pass to `userResolver`). Excludes the
|
|
294
|
+
* current user — they know they're typing.
|
|
295
|
+
*/
|
|
296
|
+
typing: Set<string>;
|
|
297
|
+
/**
|
|
298
|
+
* Send a typing ping to the server. Debounced inside the hook to
|
|
299
|
+
* once per 500 ms — the server squashes further with its own
|
|
300
|
+
* per-(user, conversation) window.
|
|
295
301
|
*/
|
|
296
302
|
signalTyping: () => void;
|
|
297
303
|
}
|
|
@@ -299,16 +305,17 @@ interface UseTypingState {
|
|
|
299
305
|
* Tracks who's typing in `conversationId`. Subscribes to the
|
|
300
306
|
* `typing:start` / `typing:stop` broadcasts; auto-cleans up entries
|
|
301
307
|
* when stop fires.
|
|
302
|
-
*
|
|
303
|
-
* `signalTyping` debounces on the client side to once per 500 ms so a
|
|
304
|
-
* fast-typing user doesn't flood the channel — poolse does its own
|
|
305
|
-
* server-side debounce too, so both layers are safe.
|
|
306
308
|
*/
|
|
307
309
|
declare function useTyping(conversationId: string): UseTypingState;
|
|
308
310
|
|
|
309
311
|
interface UsePresenceState {
|
|
310
|
-
/**
|
|
311
|
-
|
|
312
|
+
/**
|
|
313
|
+
* Set of `external_id`s currently online in this conversation —
|
|
314
|
+
* the tenant's own user identifiers. Empty set when no one is on
|
|
315
|
+
* the channel (note: an empty result during reconnect is normal;
|
|
316
|
+
* pair with `useRealtimeStatus()` if you need to distinguish).
|
|
317
|
+
*/
|
|
318
|
+
online: Set<string>;
|
|
312
319
|
}
|
|
313
320
|
/**
|
|
314
321
|
* Who's currently joined to `conversationId`'s channel. Backed by
|
|
@@ -428,16 +435,17 @@ interface UseUserState {
|
|
|
428
435
|
loading: boolean;
|
|
429
436
|
}
|
|
430
437
|
/**
|
|
431
|
-
* Resolve
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
*
|
|
438
|
+
* Resolve the tenant's `external_id` (your own user id) to
|
|
439
|
+
* `{ displayName, avatarUrl }` via the configured `userResolver`.
|
|
440
|
+
*
|
|
441
|
+
* Returns `{ profile: null, loading: false }` when:
|
|
442
|
+
* * no resolver is configured
|
|
435
443
|
* * resolver returned null (user not found)
|
|
436
444
|
* * resolver threw (logged once, then cached as null)
|
|
437
445
|
*
|
|
438
446
|
* Pass `null` / empty string to opt out (the hook becomes a no-op).
|
|
439
447
|
* Useful for self bubbles (you already know who you are).
|
|
440
448
|
*/
|
|
441
|
-
declare function useUser(
|
|
449
|
+
declare function useUser(externalId: string | null | undefined): UseUserState;
|
|
442
450
|
|
|
443
451
|
export { PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseUserState, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
|
package/dist/index.js
CHANGED
|
@@ -268,11 +268,14 @@ function useMembers(conversationId) {
|
|
|
268
268
|
[chat, conversationId]
|
|
269
269
|
);
|
|
270
270
|
const removeMember = useCallback(
|
|
271
|
-
async (
|
|
271
|
+
async (externalId) => {
|
|
272
272
|
const snapshot = membersRef.current;
|
|
273
|
-
|
|
273
|
+
const target = snapshot.find((m) => m.external_id === externalId);
|
|
274
|
+
if (!target) return;
|
|
275
|
+
const targetUserId = target.user_id;
|
|
276
|
+
setMembers((prev) => prev.filter((m) => m.external_id !== externalId));
|
|
274
277
|
try {
|
|
275
|
-
await chat.conversations.one(conversationId).removeMember(
|
|
278
|
+
await chat.conversations.one(conversationId).removeMember(targetUserId);
|
|
276
279
|
} catch (err) {
|
|
277
280
|
setMembers(snapshot);
|
|
278
281
|
throw err;
|
|
@@ -293,6 +296,8 @@ function useMessages(conversationId) {
|
|
|
293
296
|
const oldestSequenceRef = useRef(null);
|
|
294
297
|
const meIdRef = useRef(me?.id ?? null);
|
|
295
298
|
meIdRef.current = me?.id ?? null;
|
|
299
|
+
const meExternalIdRef = useRef(me?.external_id ?? null);
|
|
300
|
+
meExternalIdRef.current = me?.external_id ?? null;
|
|
296
301
|
useEffect(() => {
|
|
297
302
|
let cancelled = false;
|
|
298
303
|
setLoading(true);
|
|
@@ -377,6 +382,7 @@ function useMessages(conversationId) {
|
|
|
377
382
|
// here would briefly flash the row on the "other" side until
|
|
378
383
|
// the realtime echo replaced it with the canonical row.
|
|
379
384
|
sender_id: meIdRef.current,
|
|
385
|
+
sender_external_id: meExternalIdRef.current,
|
|
380
386
|
type: attrs.type ?? "text",
|
|
381
387
|
body: attrs.body ?? null,
|
|
382
388
|
reply_to_id: attrs.reply_to_id ?? null,
|
|
@@ -614,6 +620,7 @@ function useThread(conversationId, rootMessageId) {
|
|
|
614
620
|
tenant_id: "",
|
|
615
621
|
conversation_id: conversationId,
|
|
616
622
|
sender_id: null,
|
|
623
|
+
sender_external_id: null,
|
|
617
624
|
type: attrs.type ?? "text",
|
|
618
625
|
body: attrs.body ?? null,
|
|
619
626
|
reply_to_id: rootMessageId,
|
|
@@ -711,26 +718,30 @@ function useTyping(conversationId) {
|
|
|
711
718
|
const { me } = useMe();
|
|
712
719
|
const [typing, setTyping] = useState(/* @__PURE__ */ new Set());
|
|
713
720
|
const lastPingRef = useRef(0);
|
|
714
|
-
const
|
|
715
|
-
|
|
721
|
+
const meExtRef = useRef(me?.external_id ?? null);
|
|
722
|
+
meExtRef.current = me?.external_id ?? null;
|
|
716
723
|
useEffect(() => {
|
|
717
724
|
setTyping(/* @__PURE__ */ new Set());
|
|
718
725
|
const conv = chat.realtime.conversation(conversationId);
|
|
719
726
|
const offStart = conv.onTypingStart((evt) => {
|
|
720
|
-
|
|
727
|
+
const ext = evt.external_id;
|
|
728
|
+
if (!ext) return;
|
|
729
|
+
if (meExtRef.current && ext === meExtRef.current) return;
|
|
721
730
|
setTyping((prev) => {
|
|
722
|
-
if (prev.has(
|
|
731
|
+
if (prev.has(ext)) return prev;
|
|
723
732
|
const next = new Set(prev);
|
|
724
|
-
next.add(
|
|
733
|
+
next.add(ext);
|
|
725
734
|
return next;
|
|
726
735
|
});
|
|
727
736
|
});
|
|
728
737
|
const offStop = conv.onTypingStop((evt) => {
|
|
729
|
-
|
|
738
|
+
const ext = evt.external_id;
|
|
739
|
+
if (!ext) return;
|
|
740
|
+
if (meExtRef.current && ext === meExtRef.current) return;
|
|
730
741
|
setTyping((prev) => {
|
|
731
|
-
if (!prev.has(
|
|
742
|
+
if (!prev.has(ext)) return prev;
|
|
732
743
|
const next = new Set(prev);
|
|
733
|
-
next.delete(
|
|
744
|
+
next.delete(ext);
|
|
734
745
|
return next;
|
|
735
746
|
});
|
|
736
747
|
});
|
|
@@ -747,6 +758,14 @@ function useTyping(conversationId) {
|
|
|
747
758
|
}, [chat, conversationId]);
|
|
748
759
|
return { typing, signalTyping };
|
|
749
760
|
}
|
|
761
|
+
function externalIdsFrom(state) {
|
|
762
|
+
const out = [];
|
|
763
|
+
for (const presence of Object.values(state)) {
|
|
764
|
+
const meta = presence?.metas?.[0];
|
|
765
|
+
if (meta?.external_id) out.push(meta.external_id);
|
|
766
|
+
}
|
|
767
|
+
return out;
|
|
768
|
+
}
|
|
750
769
|
function usePresence(conversationId) {
|
|
751
770
|
const chat = usePoolse();
|
|
752
771
|
const [online, setOnline] = useState(/* @__PURE__ */ new Set());
|
|
@@ -755,13 +774,13 @@ function usePresence(conversationId) {
|
|
|
755
774
|
if (!conversationId) return;
|
|
756
775
|
const conv = chat.realtime.conversation(conversationId);
|
|
757
776
|
const applyState = (state) => {
|
|
758
|
-
setOnline(new Set(
|
|
777
|
+
setOnline(new Set(externalIdsFrom(state)));
|
|
759
778
|
};
|
|
760
779
|
const applyDiff = (diff) => {
|
|
761
780
|
setOnline((prev) => {
|
|
762
781
|
const next = new Set(prev);
|
|
763
|
-
if (diff.joins)
|
|
764
|
-
if (diff.leaves)
|
|
782
|
+
if (diff.joins) externalIdsFrom(diff.joins).forEach((ext) => next.add(ext));
|
|
783
|
+
if (diff.leaves) externalIdsFrom(diff.leaves).forEach((ext) => next.delete(ext));
|
|
765
784
|
return next;
|
|
766
785
|
});
|
|
767
786
|
};
|
|
@@ -864,14 +883,11 @@ function useAttachmentUpload() {
|
|
|
864
883
|
const inf = inFlightRef.current.get(localId);
|
|
865
884
|
if (inf) inf.controller.abort();
|
|
866
885
|
}, []);
|
|
867
|
-
const remove = useCallback(
|
|
868
|
-
(localId)
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
},
|
|
873
|
-
[]
|
|
874
|
-
);
|
|
886
|
+
const remove = useCallback((localId) => {
|
|
887
|
+
const inf = inFlightRef.current.get(localId);
|
|
888
|
+
if (inf) inf.controller.abort();
|
|
889
|
+
setQueue((q) => q.filter((it) => it.localId !== localId));
|
|
890
|
+
}, []);
|
|
875
891
|
const reset = useCallback(() => {
|
|
876
892
|
for (const v of inFlightRef.current.values()) v.controller.abort();
|
|
877
893
|
inFlightRef.current.clear();
|
|
@@ -921,32 +937,32 @@ function useAttachmentUrl(attachmentId) {
|
|
|
921
937
|
return { url, loading, error };
|
|
922
938
|
}
|
|
923
939
|
var EMPTY = { profile: null, loading: false };
|
|
924
|
-
function useUser(
|
|
940
|
+
function useUser(externalId) {
|
|
925
941
|
const chat = usePoolse();
|
|
926
942
|
const [state, setState] = useState(() => {
|
|
927
|
-
if (!
|
|
928
|
-
const cached = chat.users.peek(
|
|
943
|
+
if (!externalId) return EMPTY;
|
|
944
|
+
const cached = chat.users.peek(externalId);
|
|
929
945
|
if (cached === void 0) return { profile: null, loading: true };
|
|
930
946
|
return { profile: cached, loading: false };
|
|
931
947
|
});
|
|
932
948
|
useEffect(() => {
|
|
933
|
-
if (!
|
|
949
|
+
if (!externalId) {
|
|
934
950
|
setState(EMPTY);
|
|
935
951
|
return;
|
|
936
952
|
}
|
|
937
953
|
const apply = () => {
|
|
938
|
-
const cached = chat.users.peek(
|
|
954
|
+
const cached = chat.users.peek(externalId);
|
|
939
955
|
if (cached === void 0) {
|
|
940
956
|
setState({ profile: null, loading: true });
|
|
941
|
-
void chat.users.get(
|
|
957
|
+
void chat.users.get(externalId);
|
|
942
958
|
} else {
|
|
943
959
|
setState({ profile: cached, loading: false });
|
|
944
960
|
}
|
|
945
961
|
};
|
|
946
|
-
const off = chat.users.subscribe(
|
|
962
|
+
const off = chat.users.subscribe(externalId, apply);
|
|
947
963
|
apply();
|
|
948
964
|
return off;
|
|
949
|
-
}, [chat,
|
|
965
|
+
}, [chat, externalId]);
|
|
950
966
|
return state;
|
|
951
967
|
}
|
|
952
968
|
|