@dubsdotapp/expo 0.5.16 → 0.5.18
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.mts +2081 -0
- package/dist/index.d.ts +2081 -0
- package/dist/index.js +2102 -170
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2052 -123
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -4
- package/src/chat/hooks.ts +320 -0
- package/src/chat/index.ts +40 -0
- package/src/chat/provider.tsx +213 -0
- package/src/chat/socket.ts +175 -0
- package/src/chat/types.ts +146 -0
- package/src/client.ts +182 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useEnterJackpot.ts +80 -0
- package/src/hooks/useJackpot.ts +37 -0
- package/src/hooks/useJackpotHistory.ts +34 -0
- package/src/index.ts +55 -0
- package/src/types.ts +60 -0
- package/src/ui/game/JoinGameSheet.tsx +173 -16
- package/src/ui/index.ts +4 -0
- package/src/ui/jackpot/JackpotCard.tsx +417 -0
- package/src/ui/jackpot/JackpotSheet.tsx +683 -0
- package/src/ui/jackpot/JackpotWidget.tsx +74 -0
- package/src/ui/jackpot/index.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dubsdotapp/expo",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.18",
|
|
4
4
|
"description": "React Native SDK for the Dubs betting platform",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -24,20 +24,22 @@
|
|
|
24
24
|
"build": "tsup",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"dev": "tsup --watch",
|
|
27
|
-
"clean": "rm -rf dist"
|
|
27
|
+
"clean": "rm -rf dist",
|
|
28
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"bs58": "^5.0.0",
|
|
31
32
|
"expo-crypto": "~14.0.0",
|
|
33
|
+
"socket.io-client": "^4.8.3",
|
|
32
34
|
"tweetnacl": "^1.0.3"
|
|
33
35
|
},
|
|
34
36
|
"peerDependencies": {
|
|
35
37
|
"@solana/web3.js": "^1.90.0",
|
|
36
38
|
"expo-device": ">=6.0.0",
|
|
39
|
+
"expo-notifications": ">=0.28.0",
|
|
37
40
|
"expo-secure-store": ">=13.0.0",
|
|
38
41
|
"react": ">=18.0.0",
|
|
39
|
-
"react-native": ">=0.72.0"
|
|
40
|
-
"expo-notifications": ">=0.28.0"
|
|
42
|
+
"react-native": ">=0.72.0"
|
|
41
43
|
},
|
|
42
44
|
"peerDependenciesMeta": {
|
|
43
45
|
"expo-secure-store": {
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useDubs } from '../provider';
|
|
3
|
+
import { useChatContext } from './provider';
|
|
4
|
+
import type {
|
|
5
|
+
ChatMessage,
|
|
6
|
+
OnlineUser,
|
|
7
|
+
ChatConnectionStatus,
|
|
8
|
+
Conversation,
|
|
9
|
+
DirectMessage,
|
|
10
|
+
FriendUser,
|
|
11
|
+
FriendRequest,
|
|
12
|
+
SendMessageParams,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
// ── Connection ──
|
|
16
|
+
|
|
17
|
+
/** Get the current chat connection status */
|
|
18
|
+
export function useChatStatus(): ChatConnectionStatus {
|
|
19
|
+
return useChatContext().status;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── Global Chat ──
|
|
23
|
+
|
|
24
|
+
/** Get global chat messages with refetch */
|
|
25
|
+
export function useChatMessages(): {
|
|
26
|
+
messages: ChatMessage[];
|
|
27
|
+
loading: boolean;
|
|
28
|
+
refetch: () => Promise<void>;
|
|
29
|
+
} {
|
|
30
|
+
const { messages, refreshMessages } = useChatContext();
|
|
31
|
+
const [loading, setLoading] = useState(false);
|
|
32
|
+
|
|
33
|
+
const refetch = useCallback(async () => {
|
|
34
|
+
setLoading(true);
|
|
35
|
+
await refreshMessages();
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}, [refreshMessages]);
|
|
38
|
+
|
|
39
|
+
return { messages, loading, refetch };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Send a message to global chat (via socket for real-time, falls back to REST) */
|
|
43
|
+
export function useSendMessage(): {
|
|
44
|
+
send: (params: SendMessageParams) => void;
|
|
45
|
+
sendViaREST: (params: { message: string; replyToId?: number }) => Promise<void>;
|
|
46
|
+
} {
|
|
47
|
+
const { socket } = useChatContext();
|
|
48
|
+
const { client } = useDubs();
|
|
49
|
+
|
|
50
|
+
const send = useCallback(
|
|
51
|
+
(params: SendMessageParams) => {
|
|
52
|
+
socket.sendMessage(params);
|
|
53
|
+
},
|
|
54
|
+
[socket],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const sendViaREST = useCallback(
|
|
58
|
+
async (params: { message: string; replyToId?: number }) => {
|
|
59
|
+
await client.sendChatMessage(params);
|
|
60
|
+
},
|
|
61
|
+
[client],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return { send, sendViaREST };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Get online users and count */
|
|
68
|
+
export function useOnlineUsers(): { users: OnlineUser[]; count: number } {
|
|
69
|
+
const { onlineUsers, onlineCount } = useChatContext();
|
|
70
|
+
return { users: onlineUsers, count: onlineCount };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Get unread notification count */
|
|
74
|
+
export function useUnreadCount(): number {
|
|
75
|
+
return useChatContext().unreadCount;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── DMs ──
|
|
79
|
+
|
|
80
|
+
/** Get DM conversations inbox */
|
|
81
|
+
export function useConversations(): {
|
|
82
|
+
conversations: Conversation[];
|
|
83
|
+
loading: boolean;
|
|
84
|
+
refetch: () => Promise<void>;
|
|
85
|
+
} {
|
|
86
|
+
const { conversations, refreshConversations } = useChatContext();
|
|
87
|
+
const [loading, setLoading] = useState(false);
|
|
88
|
+
|
|
89
|
+
const refetch = useCallback(async () => {
|
|
90
|
+
setLoading(true);
|
|
91
|
+
await refreshConversations();
|
|
92
|
+
setLoading(false);
|
|
93
|
+
}, [refreshConversations]);
|
|
94
|
+
|
|
95
|
+
return { conversations, loading, refetch };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Get DM thread with a specific user by wallet address */
|
|
99
|
+
export function useDirectMessages(recipientWallet: string): {
|
|
100
|
+
messages: DirectMessage[];
|
|
101
|
+
loading: boolean;
|
|
102
|
+
otherUser: { id: number; username: string; avatar: string | null; walletAddress: string } | null;
|
|
103
|
+
send: (message: string) => void;
|
|
104
|
+
sendViaREST: (message: string) => Promise<void>;
|
|
105
|
+
markRead: () => void;
|
|
106
|
+
refetch: () => Promise<void>;
|
|
107
|
+
} {
|
|
108
|
+
const { client } = useDubs();
|
|
109
|
+
const { socket, refreshConversations } = useChatContext();
|
|
110
|
+
const [messages, setMessages] = useState<DirectMessage[]>([]);
|
|
111
|
+
const [otherUser, setOtherUser] = useState<{ id: number; username: string; avatar: string | null; walletAddress: string } | null>(null);
|
|
112
|
+
const [loading, setLoading] = useState(true);
|
|
113
|
+
|
|
114
|
+
const refetch = useCallback(async () => {
|
|
115
|
+
try {
|
|
116
|
+
setLoading(true);
|
|
117
|
+
const res = await client.getConversation(recipientWallet);
|
|
118
|
+
setMessages(res.messages);
|
|
119
|
+
setOtherUser(res.otherUser);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.error('[Dubs:useDirectMessages] Error loading:', err);
|
|
122
|
+
} finally {
|
|
123
|
+
setLoading(false);
|
|
124
|
+
}
|
|
125
|
+
}, [client, recipientWallet]);
|
|
126
|
+
|
|
127
|
+
// Load on mount and join DM room
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
refetch();
|
|
130
|
+
socket.joinDM(recipientWallet);
|
|
131
|
+
return () => {
|
|
132
|
+
socket.leaveDM(recipientWallet);
|
|
133
|
+
};
|
|
134
|
+
}, [recipientWallet, refetch, socket]);
|
|
135
|
+
|
|
136
|
+
// Listen for new DMs via socket
|
|
137
|
+
const socketRef = useRef(socket);
|
|
138
|
+
socketRef.current = socket;
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const currentSocket = socketRef.current;
|
|
142
|
+
const prevListeners = (currentSocket as any).listeners;
|
|
143
|
+
|
|
144
|
+
// Augment listeners to catch DM messages for this conversation
|
|
145
|
+
const originalOnDMNew = prevListeners?.onDMNewMessage;
|
|
146
|
+
currentSocket.setListeners({
|
|
147
|
+
...prevListeners,
|
|
148
|
+
onDMNewMessage: (msg: DirectMessage) => {
|
|
149
|
+
originalOnDMNew?.(msg);
|
|
150
|
+
// Check if this message belongs to our conversation
|
|
151
|
+
if (msg.senderWallet === recipientWallet || msg.isOwn) {
|
|
152
|
+
setMessages((prev) => {
|
|
153
|
+
if (prev.some((m) => m.id === msg.id)) return prev;
|
|
154
|
+
return [...prev, msg];
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
onDMMessageSent: (msg: DirectMessage) => {
|
|
159
|
+
setMessages((prev) => {
|
|
160
|
+
if (prev.some((m) => m.id === msg.id)) return prev;
|
|
161
|
+
return [...prev, { ...msg, isOwn: true }];
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}, [recipientWallet]);
|
|
166
|
+
|
|
167
|
+
const send = useCallback(
|
|
168
|
+
(message: string) => {
|
|
169
|
+
socket.sendDM({ recipientWallet, message });
|
|
170
|
+
},
|
|
171
|
+
[socket, recipientWallet],
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
const sendViaREST = useCallback(
|
|
175
|
+
async (message: string) => {
|
|
176
|
+
await client.sendDirectMessage({ recipientWallet, message });
|
|
177
|
+
await refetch();
|
|
178
|
+
await refreshConversations();
|
|
179
|
+
},
|
|
180
|
+
[client, recipientWallet, refetch, refreshConversations],
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const markRead = useCallback(() => {
|
|
184
|
+
socket.markDMRead(recipientWallet);
|
|
185
|
+
}, [socket, recipientWallet]);
|
|
186
|
+
|
|
187
|
+
return { messages, loading, otherUser, send, sendViaREST, markRead, refetch };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Social ──
|
|
191
|
+
|
|
192
|
+
/** Get friends list */
|
|
193
|
+
export function useFriends(): {
|
|
194
|
+
friends: FriendUser[];
|
|
195
|
+
loading: boolean;
|
|
196
|
+
refetch: () => Promise<void>;
|
|
197
|
+
} {
|
|
198
|
+
const { friends, refreshFriends } = useChatContext();
|
|
199
|
+
const [loading, setLoading] = useState(false);
|
|
200
|
+
|
|
201
|
+
const refetch = useCallback(async () => {
|
|
202
|
+
setLoading(true);
|
|
203
|
+
await refreshFriends();
|
|
204
|
+
setLoading(false);
|
|
205
|
+
}, [refreshFriends]);
|
|
206
|
+
|
|
207
|
+
return { friends, loading, refetch };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Get pending friend requests */
|
|
211
|
+
export function useFriendRequests(): {
|
|
212
|
+
requests: FriendRequest[];
|
|
213
|
+
loading: boolean;
|
|
214
|
+
refetch: () => Promise<void>;
|
|
215
|
+
} {
|
|
216
|
+
const { pendingRequests, refreshPendingRequests } = useChatContext();
|
|
217
|
+
const [loading, setLoading] = useState(false);
|
|
218
|
+
|
|
219
|
+
const refetch = useCallback(async () => {
|
|
220
|
+
setLoading(true);
|
|
221
|
+
await refreshPendingRequests();
|
|
222
|
+
setLoading(false);
|
|
223
|
+
}, [refreshPendingRequests]);
|
|
224
|
+
|
|
225
|
+
return { requests: pendingRequests, loading, refetch };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Search for users by username */
|
|
229
|
+
export function useSearchUsers(): {
|
|
230
|
+
results: FriendUser[];
|
|
231
|
+
loading: boolean;
|
|
232
|
+
search: (query: string) => Promise<void>;
|
|
233
|
+
clear: () => void;
|
|
234
|
+
} {
|
|
235
|
+
const { client } = useDubs();
|
|
236
|
+
const [results, setResults] = useState<FriendUser[]>([]);
|
|
237
|
+
const [loading, setLoading] = useState(false);
|
|
238
|
+
|
|
239
|
+
const search = useCallback(
|
|
240
|
+
async (query: string) => {
|
|
241
|
+
setLoading(true);
|
|
242
|
+
try {
|
|
243
|
+
const res = await client.searchUsers(query);
|
|
244
|
+
setResults(res.results);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
console.error('[Dubs:useSearchUsers] Error:', err);
|
|
247
|
+
} finally {
|
|
248
|
+
setLoading(false);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
[client],
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const clear = useCallback(() => setResults([]), []);
|
|
255
|
+
|
|
256
|
+
return { results, loading, search, clear };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Send a friend request */
|
|
260
|
+
export function useSendFriendRequest(): {
|
|
261
|
+
send: (targetUserId: number) => Promise<void>;
|
|
262
|
+
loading: boolean;
|
|
263
|
+
} {
|
|
264
|
+
const { client } = useDubs();
|
|
265
|
+
const { refreshPendingRequests } = useChatContext();
|
|
266
|
+
const [loading, setLoading] = useState(false);
|
|
267
|
+
|
|
268
|
+
const send = useCallback(
|
|
269
|
+
async (targetUserId: number) => {
|
|
270
|
+
setLoading(true);
|
|
271
|
+
try {
|
|
272
|
+
await client.sendFriendRequest(targetUserId);
|
|
273
|
+
} finally {
|
|
274
|
+
setLoading(false);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
[client, refreshPendingRequests],
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
return { send, loading };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Accept or reject a friend request */
|
|
284
|
+
export function useRespondToFriendRequest(): {
|
|
285
|
+
accept: (requestId: number) => Promise<void>;
|
|
286
|
+
reject: (requestId: number) => Promise<void>;
|
|
287
|
+
loading: boolean;
|
|
288
|
+
} {
|
|
289
|
+
const { client } = useDubs();
|
|
290
|
+
const { refreshFriends, refreshPendingRequests } = useChatContext();
|
|
291
|
+
const [loading, setLoading] = useState(false);
|
|
292
|
+
|
|
293
|
+
const accept = useCallback(
|
|
294
|
+
async (requestId: number) => {
|
|
295
|
+
setLoading(true);
|
|
296
|
+
try {
|
|
297
|
+
await client.acceptFriendRequest(requestId);
|
|
298
|
+
await Promise.all([refreshFriends(), refreshPendingRequests()]);
|
|
299
|
+
} finally {
|
|
300
|
+
setLoading(false);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
[client, refreshFriends, refreshPendingRequests],
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const reject = useCallback(
|
|
307
|
+
async (requestId: number) => {
|
|
308
|
+
setLoading(true);
|
|
309
|
+
try {
|
|
310
|
+
await client.rejectFriendRequest(requestId);
|
|
311
|
+
await refreshPendingRequests();
|
|
312
|
+
} finally {
|
|
313
|
+
setLoading(false);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
[client, refreshPendingRequests],
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
return { accept, reject, loading };
|
|
320
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Provider
|
|
2
|
+
export { ChatProvider, useChatContext } from './provider';
|
|
3
|
+
export type { ChatProviderProps, ChatContextValue } from './provider';
|
|
4
|
+
|
|
5
|
+
// Socket
|
|
6
|
+
export { ChatSocket } from './socket';
|
|
7
|
+
export type { ChatSocketConfig, ChatSocketListeners } from './socket';
|
|
8
|
+
|
|
9
|
+
// Hooks
|
|
10
|
+
export {
|
|
11
|
+
useChatStatus,
|
|
12
|
+
useChatMessages,
|
|
13
|
+
useSendMessage,
|
|
14
|
+
useOnlineUsers,
|
|
15
|
+
useUnreadCount,
|
|
16
|
+
useConversations,
|
|
17
|
+
useDirectMessages,
|
|
18
|
+
useFriends,
|
|
19
|
+
useFriendRequests,
|
|
20
|
+
useSearchUsers,
|
|
21
|
+
useSendFriendRequest,
|
|
22
|
+
useRespondToFriendRequest,
|
|
23
|
+
} from './hooks';
|
|
24
|
+
|
|
25
|
+
// Types
|
|
26
|
+
export type {
|
|
27
|
+
ChatMessage,
|
|
28
|
+
ChatMention,
|
|
29
|
+
ChatPayment,
|
|
30
|
+
DirectMessage,
|
|
31
|
+
Conversation,
|
|
32
|
+
FriendUser,
|
|
33
|
+
FriendRequest,
|
|
34
|
+
OnlineUser,
|
|
35
|
+
TypingEvent,
|
|
36
|
+
ChatNotification,
|
|
37
|
+
ChatConnectionStatus,
|
|
38
|
+
SendMessageParams,
|
|
39
|
+
SendDMParams,
|
|
40
|
+
} from './types';
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import React, { createContext, useContext, useEffect, useRef, useState, useCallback, useMemo } from 'react';
|
|
2
|
+
import { AppState, type AppStateStatus } from 'react-native';
|
|
3
|
+
import { useDubs } from '../provider';
|
|
4
|
+
import { ChatSocket } from './socket';
|
|
5
|
+
import type {
|
|
6
|
+
ChatMessage,
|
|
7
|
+
OnlineUser,
|
|
8
|
+
ChatConnectionStatus,
|
|
9
|
+
Conversation,
|
|
10
|
+
FriendUser,
|
|
11
|
+
FriendRequest,
|
|
12
|
+
DirectMessage,
|
|
13
|
+
ChatNotification,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
export interface ChatContextValue {
|
|
17
|
+
/** The underlying socket manager */
|
|
18
|
+
socket: ChatSocket;
|
|
19
|
+
/** Connection status */
|
|
20
|
+
status: ChatConnectionStatus;
|
|
21
|
+
/** Global chat messages (newest last) */
|
|
22
|
+
messages: ChatMessage[];
|
|
23
|
+
/** Currently online users */
|
|
24
|
+
onlineUsers: OnlineUser[];
|
|
25
|
+
/** Online user count */
|
|
26
|
+
onlineCount: number;
|
|
27
|
+
/** Unread notification count */
|
|
28
|
+
unreadCount: number;
|
|
29
|
+
/** DM conversations */
|
|
30
|
+
conversations: Conversation[];
|
|
31
|
+
/** Friends list */
|
|
32
|
+
friends: FriendUser[];
|
|
33
|
+
/** Pending friend requests */
|
|
34
|
+
pendingRequests: FriendRequest[];
|
|
35
|
+
/** Reload messages from REST */
|
|
36
|
+
refreshMessages: () => Promise<void>;
|
|
37
|
+
/** Reload conversations from REST */
|
|
38
|
+
refreshConversations: () => Promise<void>;
|
|
39
|
+
/** Reload friends from REST */
|
|
40
|
+
refreshFriends: () => Promise<void>;
|
|
41
|
+
/** Reload pending friend requests from REST */
|
|
42
|
+
refreshPendingRequests: () => Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const ChatContext = createContext<ChatContextValue | null>(null);
|
|
46
|
+
|
|
47
|
+
export interface ChatProviderProps {
|
|
48
|
+
children: React.ReactNode;
|
|
49
|
+
/** Set to false to disable auto-connect on mount. Default: true */
|
|
50
|
+
autoConnect?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Provides chat, DM, and social context to child components.
|
|
55
|
+
* Must be rendered inside a <DubsProvider>.
|
|
56
|
+
*/
|
|
57
|
+
export function ChatProvider({ children, autoConnect = true }: ChatProviderProps) {
|
|
58
|
+
const { client } = useDubs();
|
|
59
|
+
const socketRef = useRef(new ChatSocket());
|
|
60
|
+
|
|
61
|
+
const [status, setStatus] = useState<ChatConnectionStatus>('disconnected');
|
|
62
|
+
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
63
|
+
const [onlineUsers, setOnlineUsers] = useState<OnlineUser[]>([]);
|
|
64
|
+
const [onlineCount, setOnlineCount] = useState(0);
|
|
65
|
+
const [unreadCount, setUnreadCount] = useState(0);
|
|
66
|
+
const [conversations, setConversations] = useState<Conversation[]>([]);
|
|
67
|
+
const [friends, setFriends] = useState<FriendUser[]>([]);
|
|
68
|
+
const [pendingRequests, setPendingRequests] = useState<FriendRequest[]>([]);
|
|
69
|
+
|
|
70
|
+
// ── REST loaders ──
|
|
71
|
+
|
|
72
|
+
const refreshMessages = useCallback(async () => {
|
|
73
|
+
try {
|
|
74
|
+
const res = await client.getChatMessages({ limit: 30 });
|
|
75
|
+
// API returns newest-first — keep that order for inverted FlatList
|
|
76
|
+
setMessages(res.messages);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.error('[Dubs:ChatProvider] Failed to load messages:', err);
|
|
79
|
+
}
|
|
80
|
+
}, [client]);
|
|
81
|
+
|
|
82
|
+
const refreshConversations = useCallback(async () => {
|
|
83
|
+
try {
|
|
84
|
+
const res = await client.getConversations();
|
|
85
|
+
setConversations(res.conversations);
|
|
86
|
+
} catch (_) {
|
|
87
|
+
// DM service may not be deployed yet — silent fail
|
|
88
|
+
}
|
|
89
|
+
}, [client]);
|
|
90
|
+
|
|
91
|
+
const refreshFriends = useCallback(async () => {
|
|
92
|
+
try {
|
|
93
|
+
const res = await client.getFriends();
|
|
94
|
+
setFriends(res.friends);
|
|
95
|
+
} catch (_) {
|
|
96
|
+
// Social service may not be deployed yet — silent fail
|
|
97
|
+
}
|
|
98
|
+
}, [client]);
|
|
99
|
+
|
|
100
|
+
const refreshPendingRequests = useCallback(async () => {
|
|
101
|
+
try {
|
|
102
|
+
const res = await client.getPendingFriendRequests();
|
|
103
|
+
setPendingRequests(res.requests);
|
|
104
|
+
} catch (_) {
|
|
105
|
+
// Social service may not be deployed yet — silent fail
|
|
106
|
+
}
|
|
107
|
+
}, [client]);
|
|
108
|
+
|
|
109
|
+
// ── Socket setup ──
|
|
110
|
+
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
const token = client.getToken();
|
|
113
|
+
if (!autoConnect || !token) return;
|
|
114
|
+
|
|
115
|
+
const chatSocket = socketRef.current;
|
|
116
|
+
|
|
117
|
+
// Derive host from client baseUrl: strip /api/developer/v1
|
|
118
|
+
const baseUrl = (client as any).baseUrl as string;
|
|
119
|
+
const host = new URL(baseUrl).origin;
|
|
120
|
+
|
|
121
|
+
chatSocket.setListeners({
|
|
122
|
+
onConnectionChange: setStatus,
|
|
123
|
+
// Global chat
|
|
124
|
+
onNewMessage: (msg) => {
|
|
125
|
+
setMessages((prev) => {
|
|
126
|
+
// Deduplicate by id
|
|
127
|
+
if (prev.some((m) => m.id === msg.id)) return prev;
|
|
128
|
+
// Prepend — newest-first for inverted FlatList
|
|
129
|
+
return [msg, ...prev];
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
onOnlineUsers: setOnlineUsers,
|
|
133
|
+
onOnlineCount: setOnlineCount,
|
|
134
|
+
onUnreadCount: setUnreadCount,
|
|
135
|
+
// Notifications trigger conversation refresh
|
|
136
|
+
onDMNotification: () => {
|
|
137
|
+
refreshConversations();
|
|
138
|
+
},
|
|
139
|
+
onNotification: (n: ChatNotification) => {
|
|
140
|
+
setUnreadCount((prev) => prev + 1);
|
|
141
|
+
// Refresh relevant lists on social notifications
|
|
142
|
+
if (n.type === 'friend_request') refreshPendingRequests();
|
|
143
|
+
if (n.type === 'friend_request_accepted') refreshFriends();
|
|
144
|
+
},
|
|
145
|
+
onFriendRequestAccepted: () => refreshFriends(),
|
|
146
|
+
onFriendRemoved: () => refreshFriends(),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
chatSocket.connect({ host, token });
|
|
150
|
+
|
|
151
|
+
// Load initial data — friends/social loaded silently (may not be deployed yet)
|
|
152
|
+
refreshMessages();
|
|
153
|
+
refreshFriends().catch(() => {});
|
|
154
|
+
refreshPendingRequests().catch(() => {});
|
|
155
|
+
|
|
156
|
+
return () => {
|
|
157
|
+
chatSocket.disconnect();
|
|
158
|
+
};
|
|
159
|
+
}, [client, autoConnect, refreshMessages, refreshConversations, refreshFriends, refreshPendingRequests]);
|
|
160
|
+
|
|
161
|
+
// ── Reconnect on app foreground ──
|
|
162
|
+
|
|
163
|
+
useEffect(() => {
|
|
164
|
+
const handleAppState = (nextState: AppStateStatus) => {
|
|
165
|
+
if (nextState === 'active') {
|
|
166
|
+
const chatSocket = socketRef.current;
|
|
167
|
+
if (!chatSocket.isConnected()) {
|
|
168
|
+
const token = client.getToken();
|
|
169
|
+
if (token) {
|
|
170
|
+
const baseUrl = (client as any).baseUrl as string;
|
|
171
|
+
const host = new URL(baseUrl).origin;
|
|
172
|
+
chatSocket.connect({ host, token });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Refresh data to catch up on missed events
|
|
176
|
+
refreshMessages();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const sub = AppState.addEventListener('change', handleAppState);
|
|
181
|
+
return () => sub.remove();
|
|
182
|
+
}, [client, refreshMessages, refreshConversations]);
|
|
183
|
+
|
|
184
|
+
const value = useMemo<ChatContextValue>(
|
|
185
|
+
() => ({
|
|
186
|
+
socket: socketRef.current,
|
|
187
|
+
status,
|
|
188
|
+
messages,
|
|
189
|
+
onlineUsers,
|
|
190
|
+
onlineCount,
|
|
191
|
+
unreadCount,
|
|
192
|
+
conversations,
|
|
193
|
+
friends,
|
|
194
|
+
pendingRequests,
|
|
195
|
+
refreshMessages,
|
|
196
|
+
refreshConversations,
|
|
197
|
+
refreshFriends,
|
|
198
|
+
refreshPendingRequests,
|
|
199
|
+
}),
|
|
200
|
+
[status, messages, onlineUsers, onlineCount, unreadCount, conversations, friends, pendingRequests, refreshMessages, refreshConversations, refreshFriends, refreshPendingRequests],
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Access the chat context. Must be used inside <ChatProvider>. */
|
|
207
|
+
export function useChatContext(): ChatContextValue {
|
|
208
|
+
const ctx = useContext(ChatContext);
|
|
209
|
+
if (!ctx) {
|
|
210
|
+
throw new Error('useChatContext must be used inside a <ChatProvider>');
|
|
211
|
+
}
|
|
212
|
+
return ctx;
|
|
213
|
+
}
|