@poolse/react 2.1.6 → 2.3.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/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { PoolseConfig, Poolse, Me, Uuid, Conversation, ConversationCreateRequest, Membership, MemberRole, Message, MessageCreateRequest, RealtimeStatus, Attachment, AttachmentUploadInput, PoolseUserProfile } from '@poolse/sdk';
2
+ import { PoolseConfig, Poolse, Me, Uuid, Conversation, ConversationCreateRequest, Membership, MemberRole, Message, MessageCreateRequest, RealtimeStatus, Attachment, AttachmentUploadInput, PoolseUserProfile, VoiceStatus, VoiceParticipant, VoiceRoomOptions, IncomingCall, OutgoingCall } from '@poolse/sdk';
3
3
  export { ApiError, Attachment, AttachmentStatus, AttachmentUploadInput, AttachmentUploadRequest, AttachmentUploadResponse, AuthError, Conversation, ConversationCreateRequest, Me, MemberRole, Membership, Message, MessageCreateRequest, MessageType, NetworkError, PoolseConfig, PoolseError, PoolseUserProfile, RateLimitedError, RealtimeStatus, Uuid } from '@poolse/sdk';
4
4
  import { ReactNode } from 'react';
5
5
 
@@ -485,4 +485,102 @@ declare function sameDay(a: string, b: string): boolean;
485
485
  */
486
486
  declare function formatDayLabel(iso: string, now?: Date): string;
487
487
 
488
- export { type GroupPosition, PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseUserState, computeGroupPosition, formatDayLabel, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
488
+ interface UseVoiceRoom {
489
+ status: VoiceStatus;
490
+ /** Everyone currently in the room, oldest join first. */
491
+ participants: VoiceParticipant[];
492
+ muted: boolean;
493
+ error: Error | null;
494
+ /** Acquire the mic and connect. Safe to call twice. */
495
+ join: () => Promise<void>;
496
+ leave: () => void;
497
+ toggleMute: () => void;
498
+ }
499
+ /**
500
+ * Discord-style voice room for a conversation: join, see who else is
501
+ * in, watch who's talking, mute yourself.
502
+ *
503
+ * The room is torn down when the conversation changes or the component
504
+ * unmounts, so a navigation away hangs up rather than leaving a phantom
505
+ * participant behind.
506
+ *
507
+ * const voice = useVoiceRoom(conversationId);
508
+ * <Button onPress={voice.join} title="Join Voice" />
509
+ *
510
+ * For ringing someone who hasn't joined yet, see `useCalls`.
511
+ */
512
+ declare function useVoiceRoom(conversationId: string | null, opts?: VoiceRoomOptions): UseVoiceRoom;
513
+
514
+ /** Where a call is in its lifecycle, from this device's point of view. */
515
+ type CallPhase = 'idle'
516
+ /** We placed a call and are waiting for an answer. */
517
+ | 'ringing-out'
518
+ /** Someone is ringing us. */
519
+ | 'ringing-in'
520
+ /** Both sides agreed; the voice room should be joined now. */
521
+ | 'active'
522
+ /** The far side said no. */
523
+ | 'declined'
524
+ /** The person we called is already on another call. */
525
+ | 'busy'
526
+ /** Nobody picked up before the ring timeout elapsed. */
527
+ | 'timed-out';
528
+ interface UseCallsOptions {
529
+ /**
530
+ * How long a ring may go unanswered, in milliseconds, before it is
531
+ * given up on. Defaults to 45s, roughly what the mainstream call apps
532
+ * use. Pass `0` to ring indefinitely.
533
+ *
534
+ * The server deliberately holds no timer — only the UI knows when it
535
+ * stopped waiting — so this is what actually ends an unanswered call.
536
+ * Both directions are covered: an outbound ring cancels itself, and an
537
+ * inbound one gives up too, otherwise a caller that crashed mid-ring
538
+ * would leave the callee's phone ringing forever.
539
+ */
540
+ ringTimeoutMs?: number;
541
+ }
542
+ interface UseCalls {
543
+ phase: CallPhase;
544
+ /** Set while `phase` is `ringing-in`. */
545
+ incoming: IncomingCall | null;
546
+ /** Set while `phase` is `ringing-out` or `active`. */
547
+ outgoing: OutgoingCall | null;
548
+ /**
549
+ * Conversation whose voice room should be joined once `phase` turns
550
+ * `active` — the same id for both caller and callee.
551
+ */
552
+ activeConversationId: string | null;
553
+ error: Error | null;
554
+ /** Ring every other member of a conversation. */
555
+ call: (conversationId: string) => Promise<void>;
556
+ /** Answer the inbound ring. */
557
+ accept: () => Promise<void>;
558
+ /** Reject the inbound ring. */
559
+ decline: () => Promise<void>;
560
+ /** Hang up an outbound ring before it's answered. */
561
+ cancel: () => Promise<void>;
562
+ /** Drop back to idle — call after leaving the room, or to clear a
563
+ * `declined` / `busy` / `timed-out` state the UI has finished
564
+ * showing. */
565
+ reset: () => void;
566
+ }
567
+ /**
568
+ * WhatsApp-style calling: ring a conversation, show an incoming-call
569
+ * screen, accept or decline.
570
+ *
571
+ * This hook is only the *invitation* half. Audio starts when you join
572
+ * the voice room, which the host app does once `phase` is `active`:
573
+ *
574
+ * const calls = useCalls(myUserId);
575
+ * const voice = useVoiceRoom(calls.activeConversationId);
576
+ *
577
+ * useEffect(() => {
578
+ * if (calls.phase === 'active') void voice.join();
579
+ * }, [calls.phase]);
580
+ *
581
+ * Keeping them separate means a "join voice" button and a ringing call
582
+ * share exactly one implementation of the media path.
583
+ */
584
+ declare function useCalls(userId: string | null, opts?: UseCallsOptions): UseCalls;
585
+
586
+ export { type CallPhase, type GroupPosition, PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseCalls, type UseUserState, type UseVoiceRoom, computeGroupPosition, formatDayLabel, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useCalls, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser, useVoiceRoom };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { PoolseConfig, Poolse, Me, Uuid, Conversation, ConversationCreateRequest, Membership, MemberRole, Message, MessageCreateRequest, RealtimeStatus, Attachment, AttachmentUploadInput, PoolseUserProfile } from '@poolse/sdk';
2
+ import { PoolseConfig, Poolse, Me, Uuid, Conversation, ConversationCreateRequest, Membership, MemberRole, Message, MessageCreateRequest, RealtimeStatus, Attachment, AttachmentUploadInput, PoolseUserProfile, VoiceStatus, VoiceParticipant, VoiceRoomOptions, IncomingCall, OutgoingCall } from '@poolse/sdk';
3
3
  export { ApiError, Attachment, AttachmentStatus, AttachmentUploadInput, AttachmentUploadRequest, AttachmentUploadResponse, AuthError, Conversation, ConversationCreateRequest, Me, MemberRole, Membership, Message, MessageCreateRequest, MessageType, NetworkError, PoolseConfig, PoolseError, PoolseUserProfile, RateLimitedError, RealtimeStatus, Uuid } from '@poolse/sdk';
4
4
  import { ReactNode } from 'react';
5
5
 
@@ -485,4 +485,102 @@ declare function sameDay(a: string, b: string): boolean;
485
485
  */
486
486
  declare function formatDayLabel(iso: string, now?: Date): string;
487
487
 
488
- export { type GroupPosition, PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseUserState, computeGroupPosition, formatDayLabel, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
488
+ interface UseVoiceRoom {
489
+ status: VoiceStatus;
490
+ /** Everyone currently in the room, oldest join first. */
491
+ participants: VoiceParticipant[];
492
+ muted: boolean;
493
+ error: Error | null;
494
+ /** Acquire the mic and connect. Safe to call twice. */
495
+ join: () => Promise<void>;
496
+ leave: () => void;
497
+ toggleMute: () => void;
498
+ }
499
+ /**
500
+ * Discord-style voice room for a conversation: join, see who else is
501
+ * in, watch who's talking, mute yourself.
502
+ *
503
+ * The room is torn down when the conversation changes or the component
504
+ * unmounts, so a navigation away hangs up rather than leaving a phantom
505
+ * participant behind.
506
+ *
507
+ * const voice = useVoiceRoom(conversationId);
508
+ * <Button onPress={voice.join} title="Join Voice" />
509
+ *
510
+ * For ringing someone who hasn't joined yet, see `useCalls`.
511
+ */
512
+ declare function useVoiceRoom(conversationId: string | null, opts?: VoiceRoomOptions): UseVoiceRoom;
513
+
514
+ /** Where a call is in its lifecycle, from this device's point of view. */
515
+ type CallPhase = 'idle'
516
+ /** We placed a call and are waiting for an answer. */
517
+ | 'ringing-out'
518
+ /** Someone is ringing us. */
519
+ | 'ringing-in'
520
+ /** Both sides agreed; the voice room should be joined now. */
521
+ | 'active'
522
+ /** The far side said no. */
523
+ | 'declined'
524
+ /** The person we called is already on another call. */
525
+ | 'busy'
526
+ /** Nobody picked up before the ring timeout elapsed. */
527
+ | 'timed-out';
528
+ interface UseCallsOptions {
529
+ /**
530
+ * How long a ring may go unanswered, in milliseconds, before it is
531
+ * given up on. Defaults to 45s, roughly what the mainstream call apps
532
+ * use. Pass `0` to ring indefinitely.
533
+ *
534
+ * The server deliberately holds no timer — only the UI knows when it
535
+ * stopped waiting — so this is what actually ends an unanswered call.
536
+ * Both directions are covered: an outbound ring cancels itself, and an
537
+ * inbound one gives up too, otherwise a caller that crashed mid-ring
538
+ * would leave the callee's phone ringing forever.
539
+ */
540
+ ringTimeoutMs?: number;
541
+ }
542
+ interface UseCalls {
543
+ phase: CallPhase;
544
+ /** Set while `phase` is `ringing-in`. */
545
+ incoming: IncomingCall | null;
546
+ /** Set while `phase` is `ringing-out` or `active`. */
547
+ outgoing: OutgoingCall | null;
548
+ /**
549
+ * Conversation whose voice room should be joined once `phase` turns
550
+ * `active` — the same id for both caller and callee.
551
+ */
552
+ activeConversationId: string | null;
553
+ error: Error | null;
554
+ /** Ring every other member of a conversation. */
555
+ call: (conversationId: string) => Promise<void>;
556
+ /** Answer the inbound ring. */
557
+ accept: () => Promise<void>;
558
+ /** Reject the inbound ring. */
559
+ decline: () => Promise<void>;
560
+ /** Hang up an outbound ring before it's answered. */
561
+ cancel: () => Promise<void>;
562
+ /** Drop back to idle — call after leaving the room, or to clear a
563
+ * `declined` / `busy` / `timed-out` state the UI has finished
564
+ * showing. */
565
+ reset: () => void;
566
+ }
567
+ /**
568
+ * WhatsApp-style calling: ring a conversation, show an incoming-call
569
+ * screen, accept or decline.
570
+ *
571
+ * This hook is only the *invitation* half. Audio starts when you join
572
+ * the voice room, which the host app does once `phase` is `active`:
573
+ *
574
+ * const calls = useCalls(myUserId);
575
+ * const voice = useVoiceRoom(calls.activeConversationId);
576
+ *
577
+ * useEffect(() => {
578
+ * if (calls.phase === 'active') void voice.join();
579
+ * }, [calls.phase]);
580
+ *
581
+ * Keeping them separate means a "join voice" button and a ringing call
582
+ * share exactly one implementation of the media path.
583
+ */
584
+ declare function useCalls(userId: string | null, opts?: UseCallsOptions): UseCalls;
585
+
586
+ export { type CallPhase, type GroupPosition, PoolseProvider, type PoolseProviderProps, type ReactionMap, type UploadItem, type UploadItemStatus, type UseCalls, type UseUserState, type UseVoiceRoom, computeGroupPosition, formatDayLabel, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useCalls, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser, useVoiceRoom };
package/dist/index.js CHANGED
@@ -1062,7 +1062,186 @@ function formatDayLabel(iso, now = /* @__PURE__ */ new Date()) {
1062
1062
  }
1063
1063
  return d.toLocaleDateString(void 0, { day: "numeric", month: "short", year: "numeric" });
1064
1064
  }
1065
+ function useVoiceRoom(conversationId, opts = {}) {
1066
+ const poolse = usePoolse();
1067
+ const [status, setStatus] = useState("idle");
1068
+ const [participants, setParticipants] = useState([]);
1069
+ const [muted, setMuted] = useState(false);
1070
+ const [error, setError] = useState(null);
1071
+ const optsRef = useRef(opts);
1072
+ const room = useMemo(
1073
+ () => conversationId ? poolse.realtime.voice(conversationId, optsRef.current) : null,
1074
+ [poolse, conversationId]
1075
+ );
1076
+ useEffect(() => {
1077
+ if (!room) return;
1078
+ setStatus(room.getStatus());
1079
+ setParticipants(room.getParticipants());
1080
+ setMuted(room.isMuted());
1081
+ const offStatus = room.onStatus(setStatus);
1082
+ const offRoster = room.onParticipants(setParticipants);
1083
+ const offError = room.onError(setError);
1084
+ return () => {
1085
+ offStatus();
1086
+ offRoster();
1087
+ offError();
1088
+ room.leave();
1089
+ };
1090
+ }, [room]);
1091
+ const join = useCallback(async () => {
1092
+ if (!room) return;
1093
+ setError(null);
1094
+ try {
1095
+ await room.join();
1096
+ } catch (err) {
1097
+ setError(err instanceof Error ? err : new Error(String(err)));
1098
+ }
1099
+ }, [room]);
1100
+ const leave = useCallback(() => {
1101
+ room?.leave();
1102
+ }, [room]);
1103
+ const toggleMute = useCallback(() => {
1104
+ if (!room) return;
1105
+ setMuted(room.toggleMute());
1106
+ }, [room]);
1107
+ return { status, participants, muted, error, join, leave, toggleMute };
1108
+ }
1109
+ function useCalls(userId, opts = {}) {
1110
+ const ringTimeoutMs = opts.ringTimeoutMs ?? 45e3;
1111
+ const poolse = usePoolse();
1112
+ const [phase, setPhase] = useState("idle");
1113
+ const [incoming, setIncoming] = useState(null);
1114
+ const [outgoing, setOutgoing] = useState(null);
1115
+ const [activeConversationId, setActiveConversationId] = useState(null);
1116
+ const [error, setError] = useState(null);
1117
+ const actionsRef = useRef({
1118
+ cancel: async () => {
1119
+ },
1120
+ decline: async () => {
1121
+ }
1122
+ });
1123
+ const calls = useMemo(() => userId ? poolse.realtime.calls(userId) : null, [poolse, userId]);
1124
+ useEffect(() => {
1125
+ if (!calls) return;
1126
+ const offIncoming = calls.onIncoming((call2) => {
1127
+ setPhase((current) => {
1128
+ if (current !== "idle") {
1129
+ void calls.markBusy(call2).catch(() => {
1130
+ });
1131
+ return current;
1132
+ }
1133
+ setIncoming(call2);
1134
+ return "ringing-in";
1135
+ });
1136
+ });
1137
+ const offAccepted = calls.onAccepted((call2) => {
1138
+ setPhase("active");
1139
+ setActiveConversationId(call2.conversationId);
1140
+ });
1141
+ const offDeclined = calls.onDeclined(() => {
1142
+ setPhase("declined");
1143
+ setOutgoing(null);
1144
+ });
1145
+ const offBusy = calls.onBusy(() => {
1146
+ setPhase("busy");
1147
+ setOutgoing(null);
1148
+ });
1149
+ const offCancelled = calls.onCancelled((call2) => {
1150
+ setIncoming((current) => {
1151
+ if (current && current.callId !== call2.callId) return current;
1152
+ setPhase("idle");
1153
+ return null;
1154
+ });
1155
+ });
1156
+ return () => {
1157
+ offIncoming();
1158
+ offAccepted();
1159
+ offDeclined();
1160
+ offBusy();
1161
+ offCancelled();
1162
+ };
1163
+ }, [calls]);
1164
+ const call = useCallback(
1165
+ async (conversationId) => {
1166
+ if (!calls) return;
1167
+ setError(null);
1168
+ try {
1169
+ const placed = await calls.invite(conversationId);
1170
+ setOutgoing(placed);
1171
+ setPhase("ringing-out");
1172
+ } catch (err) {
1173
+ setError(err instanceof Error ? err : new Error(String(err)));
1174
+ setPhase("idle");
1175
+ }
1176
+ },
1177
+ [calls]
1178
+ );
1179
+ const accept = useCallback(async () => {
1180
+ if (!calls || !incoming) return;
1181
+ try {
1182
+ await calls.accept(incoming);
1183
+ setActiveConversationId(incoming.conversationId);
1184
+ setPhase("active");
1185
+ setIncoming(null);
1186
+ } catch (err) {
1187
+ setError(err instanceof Error ? err : new Error(String(err)));
1188
+ }
1189
+ }, [calls, incoming]);
1190
+ const decline = useCallback(async () => {
1191
+ if (!calls || !incoming) return;
1192
+ try {
1193
+ await calls.decline(incoming);
1194
+ } catch (err) {
1195
+ setError(err instanceof Error ? err : new Error(String(err)));
1196
+ } finally {
1197
+ setIncoming(null);
1198
+ setPhase("idle");
1199
+ }
1200
+ }, [calls, incoming]);
1201
+ const cancel = useCallback(async () => {
1202
+ if (!calls || !outgoing) return;
1203
+ try {
1204
+ await calls.cancel(outgoing);
1205
+ } catch (err) {
1206
+ setError(err instanceof Error ? err : new Error(String(err)));
1207
+ } finally {
1208
+ setOutgoing(null);
1209
+ setPhase("idle");
1210
+ }
1211
+ }, [calls, outgoing]);
1212
+ actionsRef.current = { cancel, decline };
1213
+ useEffect(() => {
1214
+ if (ringTimeoutMs <= 0) return;
1215
+ if (phase !== "ringing-out" && phase !== "ringing-in") return;
1216
+ const outbound = phase === "ringing-out";
1217
+ const timer = setTimeout(() => {
1218
+ void (outbound ? actionsRef.current.cancel() : actionsRef.current.decline()).finally(() => {
1219
+ if (outbound) setPhase("timed-out");
1220
+ });
1221
+ }, ringTimeoutMs);
1222
+ return () => clearTimeout(timer);
1223
+ }, [phase, ringTimeoutMs]);
1224
+ const reset = useCallback(() => {
1225
+ setPhase("idle");
1226
+ setIncoming(null);
1227
+ setOutgoing(null);
1228
+ setActiveConversationId(null);
1229
+ setError(null);
1230
+ }, []);
1231
+ return {
1232
+ phase,
1233
+ incoming,
1234
+ outgoing,
1235
+ activeConversationId,
1236
+ error,
1237
+ call,
1238
+ accept,
1239
+ decline,
1240
+ cancel,
1241
+ reset
1242
+ };
1243
+ }
1065
1244
 
1066
- export { PoolseProvider, computeGroupPosition, formatDayLabel, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser };
1245
+ export { PoolseProvider, computeGroupPosition, formatDayLabel, sameDay, sameGroup, useAttachmentUpload, useAttachmentUrl, useCalls, useConversation, useConversations, useMe, useMembers, useMessages, usePoolse, usePresence, useReactions, useRealtimeStatus, useThread, useTyping, useUser, useVoiceRoom };
1067
1246
  //# sourceMappingURL=index.js.map
1068
1247
  //# sourceMappingURL=index.js.map