ajaxter-chat 3.0.17 → 3.1.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.
Files changed (143) hide show
  1. package/dist/components/BlockList/index.d.ts +1 -0
  2. package/dist/components/BlockList/index.d.ts.map +1 -0
  3. package/dist/components/BlockList/index.js +55 -28
  4. package/dist/components/BlockList/index.js.map +1 -0
  5. package/dist/components/CallScreen/index.d.ts +1 -0
  6. package/dist/components/CallScreen/index.d.ts.map +1 -0
  7. package/dist/components/CallScreen/index.js +107 -39
  8. package/dist/components/CallScreen/index.js.map +1 -0
  9. package/dist/components/ChatScreen/index.d.ts +1 -0
  10. package/dist/components/ChatScreen/index.d.ts.map +1 -0
  11. package/dist/components/ChatScreen/index.js +493 -294
  12. package/dist/components/ChatScreen/index.js.map +1 -0
  13. package/dist/components/ChatWidget.d.ts +1 -0
  14. package/dist/components/ChatWidget.d.ts.map +1 -0
  15. package/dist/components/ChatWidget.js +350 -255
  16. package/dist/components/ChatWidget.js.map +1 -0
  17. package/dist/components/EmojiPicker/index.d.ts +1 -0
  18. package/dist/components/EmojiPicker/index.d.ts.map +1 -0
  19. package/dist/components/EmojiPicker/index.js +19 -7
  20. package/dist/components/EmojiPicker/index.js.map +1 -0
  21. package/dist/components/ErrorBoundary/index.d.ts +20 -0
  22. package/dist/components/ErrorBoundary/index.d.ts.map +1 -0
  23. package/dist/components/ErrorBoundary/index.js +76 -0
  24. package/dist/components/ErrorBoundary/index.js.map +1 -0
  25. package/dist/components/HomeScreen/index.d.ts +1 -0
  26. package/dist/components/HomeScreen/index.d.ts.map +1 -0
  27. package/dist/components/HomeScreen/index.js +236 -158
  28. package/dist/components/HomeScreen/index.js.map +1 -0
  29. package/dist/components/MaintenanceView/index.d.ts +1 -0
  30. package/dist/components/MaintenanceView/index.d.ts.map +1 -0
  31. package/dist/components/MaintenanceView/index.js +28 -12
  32. package/dist/components/MaintenanceView/index.js.map +1 -0
  33. package/dist/components/MiniCallBar/index.d.ts +1 -0
  34. package/dist/components/MiniCallBar/index.d.ts.map +1 -0
  35. package/dist/components/MiniCallBar/index.js +85 -37
  36. package/dist/components/MiniCallBar/index.js.map +1 -0
  37. package/dist/components/PermissionsGateScreen/index.d.ts +1 -0
  38. package/dist/components/PermissionsGateScreen/index.d.ts.map +1 -0
  39. package/dist/components/PermissionsGateScreen/index.js +82 -28
  40. package/dist/components/PermissionsGateScreen/index.js.map +1 -0
  41. package/dist/components/RecentChatsScreen/index.d.ts +1 -0
  42. package/dist/components/RecentChatsScreen/index.d.ts.map +1 -0
  43. package/dist/components/RecentChatsScreen/index.js +79 -19
  44. package/dist/components/RecentChatsScreen/index.js.map +1 -0
  45. package/dist/components/SlideNavMenu.d.ts +1 -0
  46. package/dist/components/SlideNavMenu.d.ts.map +1 -0
  47. package/dist/components/SlideNavMenu.js +82 -63
  48. package/dist/components/SlideNavMenu.js.map +1 -0
  49. package/dist/components/Tabs/BottomTabs.d.ts +1 -0
  50. package/dist/components/Tabs/BottomTabs.d.ts.map +1 -0
  51. package/dist/components/Tabs/BottomTabs.js +34 -19
  52. package/dist/components/Tabs/BottomTabs.js.map +1 -0
  53. package/dist/components/TicketDetailScreen/index.d.ts +1 -0
  54. package/dist/components/TicketDetailScreen/index.d.ts.map +1 -0
  55. package/dist/components/TicketDetailScreen/index.js +66 -27
  56. package/dist/components/TicketDetailScreen/index.js.map +1 -0
  57. package/dist/components/TicketFormScreen/index.d.ts +1 -0
  58. package/dist/components/TicketFormScreen/index.d.ts.map +1 -0
  59. package/dist/components/TicketFormScreen/index.js +99 -49
  60. package/dist/components/TicketFormScreen/index.js.map +1 -0
  61. package/dist/components/TicketScreen/index.d.ts +1 -0
  62. package/dist/components/TicketScreen/index.d.ts.map +1 -0
  63. package/dist/components/TicketScreen/index.js +95 -26
  64. package/dist/components/TicketScreen/index.js.map +1 -0
  65. package/dist/components/UserListScreen/index.d.ts +1 -0
  66. package/dist/components/UserListScreen/index.d.ts.map +1 -0
  67. package/dist/components/UserListScreen/index.js +127 -53
  68. package/dist/components/UserListScreen/index.js.map +1 -0
  69. package/dist/components/ViewerBlockedScreen/index.d.ts +1 -0
  70. package/dist/components/ViewerBlockedScreen/index.d.ts.map +1 -0
  71. package/dist/components/ViewerBlockedScreen/index.js +113 -61
  72. package/dist/components/ViewerBlockedScreen/index.js.map +1 -0
  73. package/dist/config/index.d.ts +1 -0
  74. package/dist/config/index.d.ts.map +1 -0
  75. package/dist/config/index.js +7 -2
  76. package/dist/config/index.js.map +1 -0
  77. package/dist/hooks/useChat.d.ts +9 -1
  78. package/dist/hooks/useChat.d.ts.map +1 -0
  79. package/dist/hooks/useChat.js +60 -18
  80. package/dist/hooks/useChat.js.map +1 -0
  81. package/dist/hooks/useRemoteConfig.d.ts +1 -0
  82. package/dist/hooks/useRemoteConfig.d.ts.map +1 -0
  83. package/dist/hooks/useRemoteConfig.js +12 -8
  84. package/dist/hooks/useRemoteConfig.js.map +1 -0
  85. package/dist/hooks/useSocket.d.ts +40 -0
  86. package/dist/hooks/useSocket.d.ts.map +1 -0
  87. package/dist/hooks/useSocket.js +190 -0
  88. package/dist/hooks/useSocket.js.map +1 -0
  89. package/dist/hooks/useWebRTC.d.ts +10 -2
  90. package/dist/hooks/useWebRTC.d.ts.map +1 -0
  91. package/dist/hooks/useWebRTC.js +101 -69
  92. package/dist/hooks/useWebRTC.js.map +1 -0
  93. package/dist/index.d.ts +6 -0
  94. package/dist/index.d.ts.map +1 -0
  95. package/dist/index.js +67 -21
  96. package/dist/index.js.map +1 -0
  97. package/dist/types/index.d.ts +1 -0
  98. package/dist/types/index.d.ts.map +1 -0
  99. package/dist/types/index.js +3 -1
  100. package/dist/types/index.js.map +1 -0
  101. package/dist/utils/chat.d.ts +1 -0
  102. package/dist/utils/chat.d.ts.map +1 -0
  103. package/dist/utils/chat.js +17 -7
  104. package/dist/utils/chat.js.map +1 -0
  105. package/dist/utils/fileName.d.ts +1 -0
  106. package/dist/utils/fileName.d.ts.map +1 -0
  107. package/dist/utils/fileName.js +5 -1
  108. package/dist/utils/fileName.js.map +1 -0
  109. package/dist/utils/messageSound.d.ts +1 -0
  110. package/dist/utils/messageSound.d.ts.map +1 -0
  111. package/dist/utils/messageSound.js +9 -3
  112. package/dist/utils/messageSound.js.map +1 -0
  113. package/dist/utils/presenceStatus.d.ts +1 -0
  114. package/dist/utils/presenceStatus.d.ts.map +1 -0
  115. package/dist/utils/presenceStatus.js +11 -4
  116. package/dist/utils/presenceStatus.js.map +1 -0
  117. package/dist/utils/privacyConsent.d.ts +1 -0
  118. package/dist/utils/privacyConsent.d.ts.map +1 -0
  119. package/dist/utils/privacyConsent.js +9 -3
  120. package/dist/utils/privacyConsent.js.map +1 -0
  121. package/dist/utils/reenableRequest.d.ts +1 -0
  122. package/dist/utils/reenableRequest.d.ts.map +1 -0
  123. package/dist/utils/reenableRequest.js +5 -1
  124. package/dist/utils/reenableRequest.js.map +1 -0
  125. package/dist/utils/theme.d.ts +1 -0
  126. package/dist/utils/theme.d.ts.map +1 -0
  127. package/dist/utils/theme.js +10 -4
  128. package/dist/utils/theme.js.map +1 -0
  129. package/dist/utils/widgetPermissions.d.ts +1 -0
  130. package/dist/utils/widgetPermissions.d.ts.map +1 -0
  131. package/dist/utils/widgetPermissions.js +13 -5
  132. package/dist/utils/widgetPermissions.js.map +1 -0
  133. package/dist/utils/widgetSession.d.ts +1 -0
  134. package/dist/utils/widgetSession.d.ts.map +1 -0
  135. package/dist/utils/widgetSession.js +9 -3
  136. package/dist/utils/widgetSession.js.map +1 -0
  137. package/package.json +3 -3
  138. package/src/components/ChatWidget.tsx +291 -269
  139. package/src/components/ErrorBoundary/index.tsx +62 -0
  140. package/src/hooks/useChat.ts +59 -12
  141. package/src/hooks/useSocket.ts +228 -0
  142. package/src/hooks/useWebRTC.ts +99 -64
  143. package/src/index.ts +7 -2
@@ -0,0 +1,62 @@
1
+ import React, { Component, ErrorInfo, ReactNode } from 'react';
2
+
3
+ interface Props {
4
+ children: ReactNode;
5
+ fallback?: ReactNode;
6
+ primaryColor?: string;
7
+ onError?: (error: Error, info: ErrorInfo) => void;
8
+ }
9
+
10
+ interface State {
11
+ hasError: boolean;
12
+ error: Error | null;
13
+ }
14
+
15
+ export class ErrorBoundary extends Component<Props, State> {
16
+ constructor(props: Props) {
17
+ super(props);
18
+ this.state = { hasError: false, error: null };
19
+ }
20
+
21
+ static getDerivedStateFromError(error: Error): State {
22
+ return { hasError: true, error };
23
+ }
24
+
25
+ componentDidCatch(error: Error, info: ErrorInfo) {
26
+ console.error('[ChatWidget] Uncaught error:', error, info.componentStack);
27
+ this.props.onError?.(error, info);
28
+ }
29
+
30
+ handleReset = () => {
31
+ this.setState({ hasError: false, error: null });
32
+ };
33
+
34
+ render() {
35
+ if (this.state.hasError) {
36
+ if (this.props.fallback) return this.props.fallback;
37
+ const color = this.props.primaryColor ?? '#2563EB';
38
+ return (
39
+ <div style={{
40
+ display: 'flex', flexDirection: 'column', alignItems: 'center',
41
+ justifyContent: 'center', height: '100%', padding: 32, textAlign: 'center', gap: 12,
42
+ }}>
43
+ <div style={{ fontSize: 40 }}>⚠️</div>
44
+ <p style={{ fontWeight: 700, color: '#1a2332', margin: 0 }}>Something went wrong</p>
45
+ <p style={{ fontSize: 13, color: '#7b8fa1', lineHeight: 1.6, margin: 0 }}>
46
+ {this.state.error?.message ?? 'An unexpected error occurred'}
47
+ </p>
48
+ <button
49
+ onClick={this.handleReset}
50
+ style={{
51
+ padding: '9px 20px', borderRadius: 10, border: 'none',
52
+ background: color, color: '#fff', cursor: 'pointer', fontWeight: 700, fontSize: 14,
53
+ }}
54
+ >
55
+ Try Again
56
+ </button>
57
+ </div>
58
+ );
59
+ }
60
+ return this.props.children;
61
+ }
62
+ }
@@ -1,19 +1,29 @@
1
- import { useState, useCallback } from 'react';
1
+ import { useState, useCallback, useRef } from 'react';
2
2
  import { ChatMessage, ChatUser } from '../types';
3
3
 
4
- export function useChat(initialMessages: ChatMessage[] = []) {
4
+ export interface UseChatOptions {
5
+ onEmitMessage?: (msg: ChatMessage) => void;
6
+ onEmitPause?: (roomId: string, targetUid: string, paused: boolean) => void;
7
+ onEmitReport?: (roomId: string) => void;
8
+ }
9
+
10
+ export function useChat(initialMessages: ChatMessage[] = [], opts: UseChatOptions = {}) {
5
11
  const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
6
12
  const [activeUser, setActiveUser] = useState<ChatUser | null>(null);
7
13
  const [isPaused, setIsPaused] = useState(false);
8
14
  const [isReported, setIsReported] = useState(false);
15
+ const activeUserRef = useRef<ChatUser | null>(null);
16
+ const isPausedRef = useRef(false);
17
+
18
+ const { onEmitMessage, onEmitPause, onEmitReport } = opts;
9
19
 
10
20
  const selectUser = useCallback((user: ChatUser, history: ChatMessage[] = []) => {
11
21
  setActiveUser(user);
22
+ activeUserRef.current = user;
12
23
  setMessages(history);
13
24
  setIsPaused(false);
25
+ isPausedRef.current = false;
14
26
  setIsReported(false);
15
- // TODO: socket.emit('join', { roomId: user.uid });
16
- // TODO: socket.on('message', msg => setMessages(prev => [...prev, msg]));
17
27
  }, []);
18
28
 
19
29
  const sendMessage = useCallback((
@@ -21,11 +31,12 @@ export function useChat(initialMessages: ChatMessage[] = []) {
21
31
  type: ChatMessage['type'] = 'text',
22
32
  extra: Partial<ChatMessage> = {}
23
33
  ) => {
24
- if (!activeUser || isPaused) return;
34
+ const user = activeUserRef.current;
35
+ if (!user || isPausedRef.current) return;
25
36
  const msg: ChatMessage = {
26
37
  id: `msg_${Date.now()}_${Math.random().toString(36).slice(2)}`,
27
38
  senderId: 'me',
28
- receiverId: activeUser.uid,
39
+ receiverId: user.uid,
29
40
  text,
30
41
  timestamp: new Date().toISOString(),
31
42
  type,
@@ -33,16 +44,52 @@ export function useChat(initialMessages: ChatMessage[] = []) {
33
44
  ...extra,
34
45
  };
35
46
  setMessages(prev => [...prev, msg]);
36
- // TODO: socket.emit('message', msg);
37
- }, [activeUser, isPaused]);
47
+ onEmitMessage?.(msg);
48
+ }, [onEmitMessage]);
49
+
50
+ const receiveMessage = useCallback((msg: ChatMessage) => {
51
+ setMessages(prev => {
52
+ if (prev.some(m => m.id === msg.id)) return prev;
53
+ return [...prev, msg];
54
+ });
55
+ }, []);
56
+
57
+ const updateMessageStatus = useCallback((messageId: string, status: ChatMessage['status']) => {
58
+ setMessages(prev => prev.map(m => m.id === messageId ? { ...m, status } : m));
59
+ }, []);
60
+
61
+ const togglePause = useCallback(() => {
62
+ const user = activeUserRef.current;
63
+ if (!user) return;
64
+ const next = !isPausedRef.current;
65
+ setIsPaused(next);
66
+ isPausedRef.current = next;
67
+ const roomId = [user.uid, 'me'].sort().join('_');
68
+ onEmitPause?.(roomId, user.uid, next);
69
+ }, [onEmitPause]);
70
+
71
+ const reportChat = useCallback(() => {
72
+ const user = activeUserRef.current;
73
+ setIsReported(true);
74
+ if (user) {
75
+ const roomId = [user.uid, 'me'].sort().join('_');
76
+ onEmitReport?.(roomId);
77
+ }
78
+ }, [onEmitReport]);
38
79
 
39
- const togglePause = useCallback(() => setIsPaused(p => !p), []);
40
- const reportChat = useCallback(() => { setIsReported(true); /* TODO: API call */ }, []);
41
- const clearChat = useCallback(() => { setMessages([]); setActiveUser(null); }, []);
80
+ const clearChat = useCallback(() => {
81
+ setMessages([]);
82
+ setActiveUser(null);
83
+ activeUserRef.current = null;
84
+ setIsPaused(false);
85
+ isPausedRef.current = false;
86
+ setIsReported(false);
87
+ }, []);
42
88
 
43
89
  return {
44
90
  messages, activeUser, isPaused, isReported,
45
- selectUser, sendMessage, togglePause, reportChat, clearChat,
91
+ selectUser, sendMessage, receiveMessage, updateMessageStatus,
92
+ togglePause, reportChat, clearChat,
46
93
  setMessages,
47
94
  };
48
95
  }
@@ -0,0 +1,228 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useCallback, useState } from 'react';
4
+ import { ChatMessage } from '../types';
5
+
6
+ export type SocketStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
7
+
8
+ export interface SocketMessageAck {
9
+ messageId: string;
10
+ status: 'delivered' | 'read';
11
+ }
12
+
13
+ export interface UseSocketOptions {
14
+ widgetId: string;
15
+ viewerUid: string;
16
+ /** WebSocket server URL — defaults to http://localhost:3005 */
17
+ serverUrl?: string;
18
+ onMessage?: (msg: ChatMessage) => void;
19
+ onMessageAck?: (ack: SocketMessageAck) => void;
20
+ onChatPaused?: (roomId: string, paused: boolean) => void;
21
+ onCallOffer?: (offer: RTCSessionDescriptionInit, fromUid: string, callId: string) => void;
22
+ onCallAnswer?: (answer: RTCSessionDescriptionInit, fromUid: string, callId: string) => void;
23
+ onIceCandidate?: (candidate: RTCIceCandidateInit, fromUid: string) => void;
24
+ onCallEnd?: (callId: string) => void;
25
+ onError?: (code: string, message: string) => void;
26
+ onUserStatus?: (uid: string, status: 'online' | 'away' | 'offline') => void;
27
+ }
28
+
29
+ const SOCKET_URL = 'http://localhost:3005';
30
+ const RECONNECT_DELAY = 3000;
31
+ const MAX_RECONNECT = 5;
32
+
33
+ export function useSocket(opts: UseSocketOptions) {
34
+ const {
35
+ widgetId, viewerUid, serverUrl = SOCKET_URL,
36
+ onMessage, onMessageAck, onChatPaused,
37
+ onCallOffer, onCallAnswer, onIceCandidate, onCallEnd,
38
+ onError, onUserStatus,
39
+ } = opts;
40
+
41
+ const [status, setStatus] = useState<SocketStatus>('disconnected');
42
+ const wsRef = useRef<WebSocket | null>(null);
43
+ const reconnectCount = useRef(0);
44
+ const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
45
+ const currentRoomRef = useRef<string | null>(null);
46
+ const mountedRef = useRef(true);
47
+
48
+ const send = useCallback((event: string, payload: unknown) => {
49
+ if (wsRef.current?.readyState === WebSocket.OPEN) {
50
+ wsRef.current.send(JSON.stringify({ event, payload }));
51
+ }
52
+ }, []);
53
+
54
+ const connect = useCallback(() => {
55
+ if (!mountedRef.current) return;
56
+ if (wsRef.current) {
57
+ wsRef.current.onclose = null;
58
+ wsRef.current.close();
59
+ }
60
+ setStatus('connecting');
61
+
62
+ const url = `${serverUrl}?widgetId=${encodeURIComponent(widgetId)}&uid=${encodeURIComponent(viewerUid)}`;
63
+ let ws: WebSocket;
64
+ try {
65
+ ws = new WebSocket(url);
66
+ } catch (e) {
67
+ setStatus('error');
68
+ return;
69
+ }
70
+ wsRef.current = ws;
71
+
72
+ ws.onopen = () => {
73
+ if (!mountedRef.current) { ws.close(); return; }
74
+ reconnectCount.current = 0;
75
+ setStatus('connected');
76
+ // Re-join current room if we were in one
77
+ if (currentRoomRef.current) {
78
+ send('join', { roomId: currentRoomRef.current, widgetId });
79
+ }
80
+ };
81
+
82
+ ws.onmessage = (event) => {
83
+ let parsed: { event: string; payload: unknown };
84
+ try { parsed = JSON.parse(event.data as string); }
85
+ catch { return; }
86
+ const { event: ev, payload } = parsed;
87
+ switch (ev) {
88
+ case 'message':
89
+ onMessage?.(payload as ChatMessage);
90
+ break;
91
+ case 'message:ack':
92
+ onMessageAck?.(payload as SocketMessageAck);
93
+ break;
94
+ case 'chat:paused': {
95
+ const p = payload as { roomId: string; paused: boolean };
96
+ onChatPaused?.(p.roomId, p.paused);
97
+ break;
98
+ }
99
+ case 'call-offer': {
100
+ const p = payload as { offer: RTCSessionDescriptionInit; from: string; callId: string };
101
+ onCallOffer?.(p.offer, p.from, p.callId);
102
+ break;
103
+ }
104
+ case 'call-answer': {
105
+ const p = payload as { answer: RTCSessionDescriptionInit; from: string; callId: string };
106
+ onCallAnswer?.(p.answer, p.from, p.callId);
107
+ break;
108
+ }
109
+ case 'ice-candidate': {
110
+ const p = payload as { candidate: RTCIceCandidateInit; from: string };
111
+ onIceCandidate?.(p.candidate, p.from);
112
+ break;
113
+ }
114
+ case 'call-end': {
115
+ const p = payload as { callId: string };
116
+ onCallEnd?.(p.callId);
117
+ break;
118
+ }
119
+ case 'user:status': {
120
+ const p = payload as { uid: string; status: 'online' | 'away' | 'offline' };
121
+ onUserStatus?.(p.uid, p.status);
122
+ break;
123
+ }
124
+ case 'error': {
125
+ const p = payload as { code: string; message: string };
126
+ onError?.(p.code, p.message);
127
+ break;
128
+ }
129
+ }
130
+ };
131
+
132
+ ws.onerror = () => {
133
+ if (!mountedRef.current) return;
134
+ setStatus('error');
135
+ };
136
+
137
+ ws.onclose = () => {
138
+ if (!mountedRef.current) return;
139
+ setStatus('disconnected');
140
+ if (reconnectCount.current < MAX_RECONNECT) {
141
+ reconnectCount.current++;
142
+ reconnectTimer.current = setTimeout(connect, RECONNECT_DELAY * reconnectCount.current);
143
+ }
144
+ };
145
+ }, [widgetId, viewerUid, serverUrl, send, onMessage, onMessageAck, onChatPaused, onCallOffer, onCallAnswer, onIceCandidate, onCallEnd, onError, onUserStatus]);
146
+
147
+ useEffect(() => {
148
+ mountedRef.current = true;
149
+ if (widgetId && viewerUid) connect();
150
+ return () => {
151
+ mountedRef.current = false;
152
+ if (reconnectTimer.current) clearTimeout(reconnectTimer.current);
153
+ wsRef.current?.close();
154
+ };
155
+ // eslint-disable-next-line react-hooks/exhaustive-deps
156
+ }, [widgetId, viewerUid, serverUrl]);
157
+
158
+ const joinRoom = useCallback((roomId: string) => {
159
+ currentRoomRef.current = roomId;
160
+ send('join', { roomId, widgetId });
161
+ }, [send, widgetId]);
162
+
163
+ const leaveRoom = useCallback((roomId: string) => {
164
+ currentRoomRef.current = null;
165
+ send('leave', { roomId });
166
+ }, [send]);
167
+
168
+ const emitMessage = useCallback((msg: ChatMessage) => {
169
+ send('message', msg);
170
+ }, [send]);
171
+
172
+ const emitTypingStart = useCallback((roomId: string) => {
173
+ send('typing_start', { roomId, userId: viewerUid });
174
+ }, [send, viewerUid]);
175
+
176
+ const emitTypingStop = useCallback((roomId: string) => {
177
+ send('typing_stop', { roomId, userId: viewerUid });
178
+ }, [send, viewerUid]);
179
+
180
+ const emitPauseToggle = useCallback((roomId: string, targetUserId: string, paused: boolean) => {
181
+ send('chat:pause', { roomId, targetUserId, paused });
182
+ }, [send]);
183
+
184
+ const emitReport = useCallback((roomId: string, reason?: string) => {
185
+ send('chat:report', { roomId, reporterId: viewerUid, reason });
186
+ }, [send, viewerUid]);
187
+
188
+ const emitBlock = useCallback((blockedUid: string) => {
189
+ send('user:block', { blockedUid });
190
+ }, [send]);
191
+
192
+ const emitUnblock = useCallback((blockedUid: string) => {
193
+ send('user:unblock', { blockedUid });
194
+ }, [send]);
195
+
196
+ const emitTransfer = useCallback((fromRoomId: string, toUserId: string, note?: string) => {
197
+ send('transfer', { fromRoomId, toUserId, note });
198
+ }, [send]);
199
+
200
+ const emitCallOffer = useCallback((offer: RTCSessionDescriptionInit, toUid: string, callId: string) => {
201
+ send('call-offer', { offer, to: toUid, from: viewerUid, callId });
202
+ }, [send, viewerUid]);
203
+
204
+ const emitCallAnswer = useCallback((answer: RTCSessionDescriptionInit, toUid: string, callId: string) => {
205
+ send('call-answer', { answer, to: toUid, callId });
206
+ }, [send]);
207
+
208
+ const emitIceCandidate = useCallback((candidate: RTCIceCandidateInit, toUid: string) => {
209
+ send('ice-candidate', { candidate, to: toUid });
210
+ }, [send]);
211
+
212
+ const emitCallEnd = useCallback((callId: string, toUid?: string) => {
213
+ send('call-end', { callId, to: toUid });
214
+ }, [send]);
215
+
216
+ const emitAddParticipant = useCallback((roomId: string, uid: string) => {
217
+ send('room:add-participant', { roomId, uid });
218
+ }, [send]);
219
+
220
+ return {
221
+ status,
222
+ joinRoom, leaveRoom,
223
+ emitMessage, emitTypingStart, emitTypingStop,
224
+ emitPauseToggle, emitReport, emitBlock, emitUnblock, emitTransfer,
225
+ emitCallOffer, emitCallAnswer, emitIceCandidate, emitCallEnd,
226
+ emitAddParticipant,
227
+ };
228
+ }
@@ -1,130 +1,165 @@
1
1
  import { useState, useRef, useCallback, useEffect } from 'react';
2
2
  import { CallSession, CallState, ChatUser } from '../types';
3
3
 
4
+ export interface UseWebRTCOptions {
5
+ onOfferReady?: (offer: RTCSessionDescriptionInit, toUid: string, callId: string) => void;
6
+ onAnswerReady?: (answer: RTCSessionDescriptionInit, toUid: string, callId: string) => void;
7
+ onIceCandidateReady?: (candidate: RTCIceCandidateInit, toUid: string) => void;
8
+ onCallEnded?: (callId: string, toUid?: string) => void;
9
+ }
10
+
4
11
  const ICE_SERVERS: RTCIceServer[] = [
5
12
  { urls: 'stun:stun.l.google.com:19302' },
6
13
  { urls: 'stun:stun1.l.google.com:19302' },
7
14
  ];
8
15
 
9
- export function useWebRTC() {
16
+ export function useWebRTC(opts: UseWebRTCOptions = {}) {
17
+ const { onOfferReady, onAnswerReady, onIceCandidateReady, onCallEnded } = opts;
18
+
10
19
  const [session, setSession] = useState<CallSession>({
11
- state: 'idle',
12
- peer: null,
13
- startedAt: null,
14
- isMuted: false,
20
+ state: 'idle',
21
+ peer: null,
22
+ startedAt: null,
23
+ isMuted: false,
15
24
  isCameraOn: false,
16
25
  });
17
26
 
18
- const pcRef = useRef<RTCPeerConnection | null>(null);
19
- const localStream = useRef<MediaStream | null>(null);
20
- const localVideoRef = useRef<HTMLVideoElement | null>(null);
27
+ const pcRef = useRef<RTCPeerConnection | null>(null);
28
+ const localStream = useRef<MediaStream | null>(null);
29
+ const localVideoRef = useRef<HTMLVideoElement | null>(null);
21
30
  const remoteVideoRef = useRef<HTMLVideoElement | null>(null);
31
+ const currentCallId = useRef<string | null>(null);
32
+ const peerUidRef = useRef<string | null>(null);
22
33
 
23
- const updateSession = (patch: Partial<CallSession>) =>
24
- setSession(prev => ({ ...prev, ...patch }));
25
-
26
- /** Start an outgoing call */
27
- const startCall = useCallback(async (peer: ChatUser, withVideo = false) => {
28
- updateSession({ state: 'calling', peer });
29
-
30
- const stream = await navigator.mediaDevices.getUserMedia({
31
- audio: true,
32
- video: withVideo,
33
- });
34
- localStream.current = stream;
35
- if (localVideoRef.current) localVideoRef.current.srcObject = stream;
34
+ const updateSession = useCallback((patch: Partial<CallSession>) =>
35
+ setSession(prev => ({ ...prev, ...patch })), []);
36
36
 
37
+ const buildPc = useCallback((peer: ChatUser) => {
37
38
  const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
38
39
  pcRef.current = pc;
39
-
40
- stream.getTracks().forEach(t => pc.addTrack(t, stream));
40
+ peerUidRef.current = peer.uid;
41
41
 
42
42
  pc.ontrack = e => {
43
43
  if (remoteVideoRef.current) remoteVideoRef.current.srcObject = e.streams[0];
44
- updateSession({ state: 'connected', startedAt: new Date(), isCameraOn: withVideo });
44
+ updateSession({ state: 'connected', startedAt: new Date() });
45
45
  };
46
46
 
47
47
  pc.onicecandidate = e => {
48
- if (e.candidate) {
49
- // TODO: socket.emit('ice-candidate', { candidate: e.candidate, to: peer.uid });
50
- console.log('[WebRTC] ICE candidate ready — send via signalling:', e.candidate);
48
+ if (e.candidate && peerUidRef.current) {
49
+ onIceCandidateReady?.(e.candidate.toJSON(), peerUidRef.current);
51
50
  }
52
51
  };
53
52
 
54
- const offer = await pc.createOffer();
55
- await pc.setLocalDescription(offer);
56
- // TODO: socket.emit('call-offer', { offer, to: peer.uid, from: 'me' });
57
- console.log('[WebRTC] Offer created send via signalling server:', offer);
58
- }, []);
53
+ pc.onconnectionstatechange = () => {
54
+ if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {
55
+ updateSession({ state: 'ended' });
56
+ setTimeout(() => updateSession({ state: 'idle' }), 1800);
57
+ }
58
+ };
59
+
60
+ return pc;
61
+ }, [onIceCandidateReady, updateSession]);
62
+
63
+ const startCall = useCallback(async (peer: ChatUser, withVideo = false) => {
64
+ const callId = `call_${Date.now()}`;
65
+ currentCallId.current = callId;
66
+ updateSession({ state: 'calling', peer });
67
+
68
+ let stream: MediaStream;
69
+ try {
70
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: withVideo });
71
+ } catch (err) {
72
+ console.error('[WebRTC] getUserMedia failed:', err);
73
+ updateSession({ state: 'idle' });
74
+ return;
75
+ }
76
+ localStream.current = stream;
77
+ if (localVideoRef.current) localVideoRef.current.srcObject = stream;
78
+
79
+ const pc = buildPc(peer);
80
+ stream.getTracks().forEach(t => pc.addTrack(t, stream));
81
+
82
+ try {
83
+ const offer = await pc.createOffer();
84
+ await pc.setLocalDescription(offer);
85
+ onOfferReady?.(offer, peer.uid, callId);
86
+ updateSession({ isCameraOn: withVideo });
87
+ } catch (err) {
88
+ console.error('[WebRTC] createOffer failed:', err);
89
+ }
90
+ }, [buildPc, onOfferReady, updateSession]);
59
91
 
60
- /** Accept an incoming call offer */
61
92
  const acceptCall = useCallback(async (
62
93
  offer: RTCSessionDescriptionInit,
63
94
  peer: ChatUser,
95
+ callId: string,
64
96
  withVideo = false
65
97
  ) => {
98
+ currentCallId.current = callId;
66
99
  updateSession({ state: 'connected', peer, startedAt: new Date(), isCameraOn: withVideo });
67
100
 
68
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: withVideo });
101
+ let stream: MediaStream;
102
+ try {
103
+ stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: withVideo });
104
+ } catch (err) {
105
+ console.error('[WebRTC] getUserMedia failed:', err);
106
+ return;
107
+ }
69
108
  localStream.current = stream;
70
109
  if (localVideoRef.current) localVideoRef.current.srcObject = stream;
71
110
 
72
- const pc = new RTCPeerConnection({ iceServers: ICE_SERVERS });
73
- pcRef.current = pc;
111
+ const pc = buildPc(peer);
74
112
  stream.getTracks().forEach(t => pc.addTrack(t, stream));
75
- pc.ontrack = e => {
76
- if (remoteVideoRef.current) remoteVideoRef.current.srcObject = e.streams[0];
77
- };
78
- pc.onicecandidate = e => {
79
- if (e.candidate) {
80
- // TODO: socket.emit('ice-candidate', { candidate: e.candidate, to: peer.uid });
81
- }
82
- };
83
113
 
84
114
  await pc.setRemoteDescription(offer);
85
115
  const answer = await pc.createAnswer();
86
116
  await pc.setLocalDescription(answer);
87
- // TODO: socket.emit('call-answer', { answer, to: peer.uid });
117
+ onAnswerReady?.(answer, peer.uid, callId);
118
+ }, [buildPc, onAnswerReady, updateSession]);
119
+
120
+ const addIceCandidate = useCallback(async (candidate: RTCIceCandidateInit) => {
121
+ try {
122
+ await pcRef.current?.addIceCandidate(new RTCIceCandidate(candidate));
123
+ } catch (err) {
124
+ console.warn('[WebRTC] addIceCandidate failed:', err);
125
+ }
88
126
  }, []);
89
127
 
90
- /** Hang up */
91
128
  const endCall = useCallback(() => {
129
+ const callId = currentCallId.current;
130
+ const peerUid = peerUidRef.current ?? undefined;
92
131
  localStream.current?.getTracks().forEach(t => t.stop());
93
132
  pcRef.current?.close();
94
133
  pcRef.current = null;
95
134
  localStream.current = null;
135
+ currentCallId.current = null;
136
+ if (callId) onCallEnded?.(callId, peerUid);
96
137
  updateSession({ state: 'ended', peer: null, startedAt: null });
97
138
  setTimeout(() => updateSession({ state: 'idle' }), 1800);
98
- }, []);
139
+ }, [onCallEnded, updateSession]);
99
140
 
100
141
  const toggleMute = useCallback(() => {
101
142
  if (!localStream.current) return;
102
- const enabled = !session.isMuted;
103
- localStream.current.getAudioTracks().forEach(t => { t.enabled = enabled; });
104
- updateSession({ isMuted: !session.isMuted });
105
- }, [session.isMuted]);
143
+ const muted = !session.isMuted;
144
+ localStream.current.getAudioTracks().forEach(t => { t.enabled = !muted; });
145
+ updateSession({ isMuted: muted });
146
+ }, [session.isMuted, updateSession]);
106
147
 
107
148
  const toggleCamera = useCallback(() => {
108
149
  if (!localStream.current) return;
109
- const enabled = !session.isCameraOn;
110
- localStream.current.getVideoTracks().forEach(t => { t.enabled = enabled; });
111
- updateSession({ isCameraOn: enabled });
112
- }, [session.isCameraOn]);
150
+ const on = !session.isCameraOn;
151
+ localStream.current.getVideoTracks().forEach(t => { t.enabled = on; });
152
+ updateSession({ isCameraOn: on });
153
+ }, [session.isCameraOn, updateSession]);
113
154
 
114
- // Cleanup on unmount
115
155
  useEffect(() => () => {
116
156
  localStream.current?.getTracks().forEach(t => t.stop());
117
157
  pcRef.current?.close();
118
158
  }, []);
119
159
 
120
160
  return {
121
- session,
122
- localVideoRef,
123
- remoteVideoRef,
124
- startCall,
125
- acceptCall,
126
- endCall,
127
- toggleMute,
128
- toggleCamera,
161
+ session, localVideoRef, remoteVideoRef,
162
+ startCall, acceptCall, addIceCandidate, endCall,
163
+ toggleMute, toggleCamera,
129
164
  };
130
165
  }
package/src/index.ts CHANGED
@@ -10,10 +10,12 @@ export { MaintenanceView } from './components/MaintenanceView';
10
10
  export { BottomTabs } from './components/Tabs/BottomTabs';
11
11
  export { EmojiPicker } from './components/EmojiPicker';
12
12
  export { SlideNavMenu } from './components/SlideNavMenu';
13
+ export { ErrorBoundary } from './components/ErrorBoundary';
13
14
 
14
15
  export { useChat } from './hooks/useChat';
15
16
  export { useWebRTC } from './hooks/useWebRTC';
16
17
  export { useRemoteConfig } from './hooks/useRemoteConfig';
18
+ export { useSocket } from './hooks/useSocket';
17
19
 
18
20
  export { shouldShowPrivacyNotice, dismissPrivacyNotice, getPrivacyDismissedAt } from './utils/privacyConsent';
19
21
  export { submitReenableRequest } from './utils/reenableRequest';
@@ -31,6 +33,9 @@ export type {
31
33
  CallSession, CallState,
32
34
  ChatStatus, ChatType, UserType, OnlineStatus,
33
35
  Screen, BottomTab, UserListContext, MessageType,
34
- LocalEnvConfig,
35
- PresenceStatus,
36
+ LocalEnvConfig, PresenceStatus,
36
37
  } from './types';
38
+
39
+ export type { UseSocketOptions, SocketStatus, SocketMessageAck } from './hooks/useSocket';
40
+ export type { UseChatOptions } from './hooks/useChat';
41
+ export type { UseWebRTCOptions } from './hooks/useWebRTC';