ape-im-sdk-react 1.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/dist/index.mjs ADDED
@@ -0,0 +1,668 @@
1
+ // src/context.tsx
2
+ import { createContext, useContext, useState, useCallback, useEffect } from "react";
3
+ import { jsx } from "react/jsx-runtime";
4
+ var ApeIMContext = createContext(null);
5
+ function ApeIMProvider({
6
+ children,
7
+ apiUrl = "https://www.apeinstantmessenger.com",
8
+ theme = "system",
9
+ onConnect,
10
+ onDisconnect
11
+ }) {
12
+ const [user, setUser] = useState(null);
13
+ const [sessionToken, setSessionToken] = useState(null);
14
+ const [isLoading, setIsLoading] = useState(true);
15
+ const [isWidgetOpen, setIsWidgetOpen] = useState(false);
16
+ const normalizedApiUrl = apiUrl.endsWith("/") ? apiUrl.slice(0, -1) : apiUrl;
17
+ useEffect(() => {
18
+ const storedToken = localStorage.getItem("ape_im_sdk_session");
19
+ if (storedToken) {
20
+ validateSession(storedToken);
21
+ } else {
22
+ setIsLoading(false);
23
+ }
24
+ }, []);
25
+ const validateSession = async (token) => {
26
+ try {
27
+ const res = await fetch(`${normalizedApiUrl}/api/users/me`, {
28
+ headers: { "X-Session-Token": token }
29
+ });
30
+ if (res.ok) {
31
+ const userData = await res.json();
32
+ setUser(userData);
33
+ setSessionToken(token);
34
+ onConnect?.(userData);
35
+ } else {
36
+ localStorage.removeItem("ape_im_sdk_session");
37
+ }
38
+ } catch (err) {
39
+ console.error("Session validation failed:", err);
40
+ } finally {
41
+ setIsLoading(false);
42
+ }
43
+ };
44
+ const connect = useCallback(async () => {
45
+ if (typeof window === "undefined") {
46
+ console.warn("Ape IM SDK: Cannot connect in SSR environment");
47
+ return;
48
+ }
49
+ if (!window.ethereum) {
50
+ console.warn("Ape IM SDK: No wallet detected");
51
+ alert("Please install MetaMask or another Web3 wallet to use chat.");
52
+ return;
53
+ }
54
+ setIsLoading(true);
55
+ try {
56
+ const accounts = await window.ethereum.request({
57
+ method: "eth_requestAccounts"
58
+ });
59
+ if (!accounts || accounts.length === 0) {
60
+ throw new Error("No accounts returned from wallet");
61
+ }
62
+ const walletAddress = accounts[0];
63
+ const res = await fetch(`${normalizedApiUrl}/api/auth/login`, {
64
+ method: "POST",
65
+ headers: { "Content-Type": "application/json" },
66
+ body: JSON.stringify({ walletAddress })
67
+ });
68
+ if (!res.ok) {
69
+ const errorText = await res.text();
70
+ throw new Error(`Login failed: ${errorText}`);
71
+ }
72
+ const data = await res.json();
73
+ setUser(data.user);
74
+ setSessionToken(data.sessionToken);
75
+ localStorage.setItem("ape_im_sdk_session", data.sessionToken);
76
+ onConnect?.(data.user);
77
+ } catch (err) {
78
+ console.error("Ape IM SDK: Connection failed:", err);
79
+ setIsLoading(false);
80
+ throw err;
81
+ } finally {
82
+ setIsLoading(false);
83
+ }
84
+ }, [normalizedApiUrl, onConnect]);
85
+ const disconnect = useCallback(() => {
86
+ if (sessionToken) {
87
+ fetch(`${normalizedApiUrl}/api/auth/logout`, {
88
+ method: "POST",
89
+ headers: { "X-Session-Token": sessionToken }
90
+ }).catch(console.error);
91
+ }
92
+ setUser(null);
93
+ setSessionToken(null);
94
+ localStorage.removeItem("ape_im_sdk_session");
95
+ onDisconnect?.();
96
+ }, [normalizedApiUrl, sessionToken, onDisconnect]);
97
+ const openWidget = useCallback(() => setIsWidgetOpen(true), []);
98
+ const closeWidget = useCallback(() => setIsWidgetOpen(false), []);
99
+ const value = {
100
+ user,
101
+ isConnected: !!user,
102
+ isLoading,
103
+ sessionToken,
104
+ apiUrl: normalizedApiUrl,
105
+ theme,
106
+ connect,
107
+ disconnect,
108
+ openWidget,
109
+ closeWidget,
110
+ isWidgetOpen
111
+ };
112
+ return /* @__PURE__ */ jsx(ApeIMContext.Provider, { value, children });
113
+ }
114
+ function useApeIM() {
115
+ const context = useContext(ApeIMContext);
116
+ if (!context) {
117
+ throw new Error("useApeIM must be used within an ApeIMProvider");
118
+ }
119
+ return context;
120
+ }
121
+
122
+ // src/ChatWidget.tsx
123
+ import { useState as useState4, useEffect as useEffect4 } from "react";
124
+
125
+ // src/hooks/useConversations.ts
126
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
127
+ function useConversations() {
128
+ const { apiUrl, sessionToken, isConnected } = useApeIM();
129
+ const [conversations, setConversations] = useState2([]);
130
+ const [isLoading, setIsLoading] = useState2(true);
131
+ const [error, setError] = useState2(null);
132
+ const fetchConversations = useCallback2(async () => {
133
+ if (!sessionToken) return;
134
+ setIsLoading(true);
135
+ try {
136
+ const res = await fetch(`${apiUrl}/api/conversations`, {
137
+ headers: { "X-Session-Token": sessionToken }
138
+ });
139
+ if (!res.ok) throw new Error("Failed to fetch conversations");
140
+ const data = await res.json();
141
+ setConversations(data);
142
+ setError(null);
143
+ } catch (err) {
144
+ setError(err instanceof Error ? err : new Error("Unknown error"));
145
+ } finally {
146
+ setIsLoading(false);
147
+ }
148
+ }, [apiUrl, sessionToken]);
149
+ useEffect2(() => {
150
+ if (isConnected) {
151
+ fetchConversations();
152
+ } else {
153
+ setConversations([]);
154
+ setIsLoading(false);
155
+ }
156
+ }, [isConnected, fetchConversations]);
157
+ const startConversation = useCallback2(async (userId) => {
158
+ if (!sessionToken) throw new Error("Not authenticated");
159
+ const res = await fetch(`${apiUrl}/api/conversations`, {
160
+ method: "POST",
161
+ headers: {
162
+ "Content-Type": "application/json",
163
+ "X-Session-Token": sessionToken
164
+ },
165
+ body: JSON.stringify({ participantId: userId })
166
+ });
167
+ if (!res.ok) throw new Error("Failed to create conversation");
168
+ const conversation = await res.json();
169
+ await fetchConversations();
170
+ return conversation;
171
+ }, [apiUrl, sessionToken, fetchConversations]);
172
+ const createGroup = useCallback2(async (name, participantIds) => {
173
+ if (!sessionToken) throw new Error("Not authenticated");
174
+ const res = await fetch(`${apiUrl}/api/conversations/group`, {
175
+ method: "POST",
176
+ headers: {
177
+ "Content-Type": "application/json",
178
+ "X-Session-Token": sessionToken
179
+ },
180
+ body: JSON.stringify({ name, participantIds })
181
+ });
182
+ if (!res.ok) throw new Error("Failed to create group");
183
+ const conversation = await res.json();
184
+ await fetchConversations();
185
+ return conversation;
186
+ }, [apiUrl, sessionToken, fetchConversations]);
187
+ return {
188
+ conversations,
189
+ isLoading,
190
+ error,
191
+ startConversation,
192
+ createGroup,
193
+ refetch: fetchConversations
194
+ };
195
+ }
196
+
197
+ // src/hooks/useChat.ts
198
+ import { useState as useState3, useEffect as useEffect3, useCallback as useCallback3, useRef } from "react";
199
+ function useChat(conversationId) {
200
+ const { apiUrl, sessionToken, user } = useApeIM();
201
+ const [messages, setMessages] = useState3([]);
202
+ const [isLoading, setIsLoading] = useState3(true);
203
+ const [isTyping, setIsTyping] = useState3([]);
204
+ const [participants, setParticipants] = useState3([]);
205
+ const wsRef = useRef(null);
206
+ const typingTimeoutsRef = useRef(/* @__PURE__ */ new Map());
207
+ useEffect3(() => {
208
+ if (!conversationId || !sessionToken) return;
209
+ const fetchMessages = async () => {
210
+ setIsLoading(true);
211
+ try {
212
+ const res = await fetch(`${apiUrl}/api/conversations/${conversationId}/messages`, {
213
+ headers: { "X-Session-Token": sessionToken }
214
+ });
215
+ if (res.ok) {
216
+ const data = await res.json();
217
+ setMessages(data.messages || data);
218
+ }
219
+ } finally {
220
+ setIsLoading(false);
221
+ }
222
+ };
223
+ const fetchParticipants = async () => {
224
+ try {
225
+ const res = await fetch(`${apiUrl}/api/conversations/${conversationId}`, {
226
+ headers: { "X-Session-Token": sessionToken }
227
+ });
228
+ if (res.ok) {
229
+ const data = await res.json();
230
+ setParticipants(data.participants?.map((p) => p.user) || []);
231
+ }
232
+ } catch (err) {
233
+ console.error("Failed to fetch participants:", err);
234
+ }
235
+ };
236
+ fetchMessages();
237
+ fetchParticipants();
238
+ }, [conversationId, apiUrl, sessionToken]);
239
+ useEffect3(() => {
240
+ if (!sessionToken) return;
241
+ const wsUrl = apiUrl.replace(/^http/, "ws") + "/ws";
242
+ const ws = new WebSocket(wsUrl);
243
+ wsRef.current = ws;
244
+ ws.onopen = () => {
245
+ ws.send(JSON.stringify({ type: "auth", sessionToken }));
246
+ };
247
+ ws.onmessage = (event) => {
248
+ try {
249
+ const data = JSON.parse(event.data);
250
+ if (data.type === "message" && data.data?.conversationId === conversationId) {
251
+ setMessages((prev) => [...prev, data.data]);
252
+ }
253
+ if (data.type === "typing" && data.data?.conversationId === conversationId) {
254
+ const typingUser = data.data.user;
255
+ if (typingUser && typingUser.id !== user?.id) {
256
+ setIsTyping((prev) => {
257
+ if (prev.find((u) => u.id === typingUser.id)) return prev;
258
+ return [...prev, typingUser];
259
+ });
260
+ const existingTimeout = typingTimeoutsRef.current.get(typingUser.id);
261
+ if (existingTimeout) clearTimeout(existingTimeout);
262
+ const timeout = setTimeout(() => {
263
+ setIsTyping((prev) => prev.filter((u) => u.id !== typingUser.id));
264
+ typingTimeoutsRef.current.delete(typingUser.id);
265
+ }, 3e3);
266
+ typingTimeoutsRef.current.set(typingUser.id, timeout);
267
+ }
268
+ }
269
+ } catch (err) {
270
+ console.error("WebSocket message parse error:", err);
271
+ }
272
+ };
273
+ return () => {
274
+ ws.close();
275
+ typingTimeoutsRef.current.forEach((timeout) => clearTimeout(timeout));
276
+ typingTimeoutsRef.current.clear();
277
+ };
278
+ }, [apiUrl, sessionToken, conversationId, user?.id]);
279
+ const sendMessage = useCallback3(async (content, formatting) => {
280
+ if (!conversationId || !sessionToken) throw new Error("Not ready to send");
281
+ const res = await fetch(`${apiUrl}/api/conversations/${conversationId}/messages`, {
282
+ method: "POST",
283
+ headers: {
284
+ "Content-Type": "application/json",
285
+ "X-Session-Token": sessionToken
286
+ },
287
+ body: JSON.stringify({ content, formatting })
288
+ });
289
+ if (!res.ok) throw new Error("Failed to send message");
290
+ const data = await res.json();
291
+ setMessages((prev) => [...prev, data.message]);
292
+ }, [apiUrl, sessionToken, conversationId]);
293
+ const sendReaction = useCallback3(async (messageId, emoji) => {
294
+ if (!sessionToken) throw new Error("Not authenticated");
295
+ await fetch(`${apiUrl}/api/messages/${messageId}/reactions`, {
296
+ method: "POST",
297
+ headers: {
298
+ "Content-Type": "application/json",
299
+ "X-Session-Token": sessionToken
300
+ },
301
+ body: JSON.stringify({ emoji })
302
+ });
303
+ }, [apiUrl, sessionToken]);
304
+ const markAsRead = useCallback3(async () => {
305
+ if (!conversationId || !sessionToken) return;
306
+ await fetch(`${apiUrl}/api/conversations/${conversationId}/read`, {
307
+ method: "POST",
308
+ headers: { "X-Session-Token": sessionToken }
309
+ });
310
+ }, [apiUrl, sessionToken, conversationId]);
311
+ const sendTypingIndicator = useCallback3(() => {
312
+ if (wsRef.current?.readyState === WebSocket.OPEN && conversationId) {
313
+ wsRef.current.send(JSON.stringify({
314
+ type: "typing",
315
+ conversationId
316
+ }));
317
+ }
318
+ }, [conversationId]);
319
+ return {
320
+ messages,
321
+ isLoading,
322
+ sendMessage,
323
+ sendReaction,
324
+ isTyping,
325
+ participants,
326
+ markAsRead
327
+ };
328
+ }
329
+
330
+ // src/ChatWidget.tsx
331
+ import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
332
+ var ApeIcon = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 40 40", width: "32", height: "32", fill: "none", children: [
333
+ /* @__PURE__ */ jsx2("circle", { cx: "20", cy: "20", r: "18", fill: "#1D3FE7" }),
334
+ /* @__PURE__ */ jsx2("path", { d: "M12 22c0-4.4 3.6-8 8-8s8 3.6 8 8", stroke: "white", strokeWidth: "2", strokeLinecap: "round" }),
335
+ /* @__PURE__ */ jsx2("circle", { cx: "15", cy: "18", r: "2", fill: "white" }),
336
+ /* @__PURE__ */ jsx2("circle", { cx: "25", cy: "18", r: "2", fill: "white" }),
337
+ /* @__PURE__ */ jsx2("path", { d: "M17 26c1.5 1.5 4.5 1.5 6 0", stroke: "white", strokeWidth: "2", strokeLinecap: "round" })
338
+ ] });
339
+ function ChatWidget({
340
+ position = "bottom-right",
341
+ offset = { x: 20, y: 20 },
342
+ defaultOpen = false,
343
+ bubbleSize = 60,
344
+ zIndex = 9999
345
+ }) {
346
+ const { user, isConnected, connect, isLoading, isWidgetOpen, openWidget, closeWidget, theme } = useApeIM();
347
+ const { conversations } = useConversations();
348
+ const [selectedConversation, setSelectedConversation] = useState4(null);
349
+ const { messages, sendMessage, isTyping } = useChat(selectedConversation?.id || null);
350
+ const [messageInput, setMessageInput] = useState4("");
351
+ const [isOpen, setIsOpen] = useState4(defaultOpen);
352
+ useEffect4(() => {
353
+ if (isWidgetOpen) setIsOpen(true);
354
+ }, [isWidgetOpen]);
355
+ const handleClose = () => {
356
+ setIsOpen(false);
357
+ closeWidget();
358
+ };
359
+ const handleSend = async () => {
360
+ if (!messageInput.trim() || !selectedConversation) return;
361
+ await sendMessage(messageInput);
362
+ setMessageInput("");
363
+ };
364
+ const isDark = theme === "dark" || theme === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches;
365
+ const positionStyles = {
366
+ "bottom-right": { bottom: offset.y, right: offset.x },
367
+ "bottom-left": { bottom: offset.y, left: offset.x }
368
+ };
369
+ const containerStyle = {
370
+ position: "fixed",
371
+ zIndex,
372
+ fontFamily: '"DM Sans", -apple-system, BlinkMacSystemFont, sans-serif',
373
+ ...positionStyles[position]
374
+ };
375
+ const bubbleStyle = {
376
+ width: bubbleSize,
377
+ height: bubbleSize,
378
+ borderRadius: "50%",
379
+ background: "linear-gradient(135deg, #1D3FE7 0%, #142B99 100%)",
380
+ border: "3px solid #E8EDFF",
381
+ boxShadow: "0 4px 20px rgba(29, 63, 231, 0.4)",
382
+ cursor: "pointer",
383
+ display: "flex",
384
+ alignItems: "center",
385
+ justifyContent: "center",
386
+ transition: "transform 0.2s, box-shadow 0.2s"
387
+ };
388
+ const windowStyle = {
389
+ width: 360,
390
+ height: 500,
391
+ background: isDark ? "#1a1a1a" : "#c0c0c0",
392
+ border: `2px solid ${isDark ? "#404040" : "#808080"}`,
393
+ boxShadow: isDark ? "4px 4px 0 #000, inset 1px 1px 0 #404040" : "4px 4px 0 #404040, inset 1px 1px 0 #fff",
394
+ display: "flex",
395
+ flexDirection: "column",
396
+ borderRadius: 0
397
+ };
398
+ const titleBarStyle = {
399
+ background: "linear-gradient(90deg, #1D3FE7 0%, #142B99 100%)",
400
+ padding: "6px 8px",
401
+ display: "flex",
402
+ alignItems: "center",
403
+ justifyContent: "space-between",
404
+ color: "white",
405
+ fontWeight: "bold",
406
+ fontSize: "12px"
407
+ };
408
+ if (!isOpen) {
409
+ return /* @__PURE__ */ jsx2("div", { style: containerStyle, children: /* @__PURE__ */ jsx2(
410
+ "button",
411
+ {
412
+ onClick: () => setIsOpen(true),
413
+ style: bubbleStyle,
414
+ onMouseEnter: (e) => {
415
+ e.currentTarget.style.transform = "scale(1.1)";
416
+ e.currentTarget.style.boxShadow = "0 6px 24px rgba(29, 63, 231, 0.5)";
417
+ },
418
+ onMouseLeave: (e) => {
419
+ e.currentTarget.style.transform = "scale(1)";
420
+ e.currentTarget.style.boxShadow = "0 4px 20px rgba(29, 63, 231, 0.4)";
421
+ },
422
+ "aria-label": "Open Ape IM",
423
+ "data-testid": "button-open-chat-widget",
424
+ children: /* @__PURE__ */ jsx2(ApeIcon, {})
425
+ }
426
+ ) });
427
+ }
428
+ return /* @__PURE__ */ jsx2("div", { style: containerStyle, children: /* @__PURE__ */ jsxs("div", { style: windowStyle, children: [
429
+ /* @__PURE__ */ jsxs("div", { style: titleBarStyle, children: [
430
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: [
431
+ /* @__PURE__ */ jsx2(ApeIcon, {}),
432
+ /* @__PURE__ */ jsx2("span", { children: "Ape Instant Messenger" })
433
+ ] }),
434
+ /* @__PURE__ */ jsx2(
435
+ "button",
436
+ {
437
+ onClick: handleClose,
438
+ style: {
439
+ background: "#c0c0c0",
440
+ border: "2px outset #fff",
441
+ width: 18,
442
+ height: 18,
443
+ cursor: "pointer",
444
+ display: "flex",
445
+ alignItems: "center",
446
+ justifyContent: "center",
447
+ fontFamily: "monospace",
448
+ fontSize: 10,
449
+ fontWeight: "bold"
450
+ },
451
+ "data-testid": "button-close-chat-widget",
452
+ children: "\xD7"
453
+ }
454
+ )
455
+ ] }),
456
+ /* @__PURE__ */ jsx2("div", { style: { flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }, children: !isConnected ? /* @__PURE__ */ jsxs("div", { style: {
457
+ flex: 1,
458
+ display: "flex",
459
+ flexDirection: "column",
460
+ alignItems: "center",
461
+ justifyContent: "center",
462
+ padding: 24,
463
+ gap: 16,
464
+ background: isDark ? "#2a2a2a" : "#e0e0e0"
465
+ }, children: [
466
+ /* @__PURE__ */ jsx2(ApeIcon, {}),
467
+ /* @__PURE__ */ jsx2("p", { style: {
468
+ textAlign: "center",
469
+ fontSize: 13,
470
+ color: isDark ? "#ccc" : "#333",
471
+ margin: 0
472
+ }, children: "Connect your wallet to start chatting" }),
473
+ /* @__PURE__ */ jsx2(
474
+ "button",
475
+ {
476
+ onClick: connect,
477
+ disabled: isLoading,
478
+ style: {
479
+ background: "linear-gradient(135deg, #1D3FE7 0%, #142B99 100%)",
480
+ color: "white",
481
+ border: "2px outset #5577ff",
482
+ padding: "10px 24px",
483
+ fontSize: 12,
484
+ fontWeight: "bold",
485
+ cursor: isLoading ? "wait" : "pointer",
486
+ opacity: isLoading ? 0.7 : 1
487
+ },
488
+ "data-testid": "button-connect-wallet",
489
+ children: isLoading ? "Connecting..." : "Connect Wallet"
490
+ }
491
+ )
492
+ ] }) : !selectedConversation ? /* @__PURE__ */ jsxs("div", { style: { flex: 1, overflow: "auto", background: isDark ? "#2a2a2a" : "#e0e0e0" }, children: [
493
+ /* @__PURE__ */ jsx2("div", { style: {
494
+ padding: "8px 12px",
495
+ fontSize: 11,
496
+ fontWeight: "bold",
497
+ color: isDark ? "#888" : "#666",
498
+ textTransform: "uppercase"
499
+ }, children: "Conversations" }),
500
+ conversations.length === 0 ? /* @__PURE__ */ jsx2("div", { style: {
501
+ padding: 24,
502
+ textAlign: "center",
503
+ color: isDark ? "#666" : "#888",
504
+ fontSize: 12
505
+ }, children: "No conversations yet" }) : conversations.map((conv) => /* @__PURE__ */ jsxs(
506
+ "button",
507
+ {
508
+ onClick: () => setSelectedConversation(conv),
509
+ style: {
510
+ width: "100%",
511
+ display: "flex",
512
+ alignItems: "center",
513
+ gap: 10,
514
+ padding: "10px 12px",
515
+ background: "transparent",
516
+ border: "none",
517
+ borderBottom: `1px solid ${isDark ? "#404040" : "#c0c0c0"}`,
518
+ cursor: "pointer",
519
+ textAlign: "left"
520
+ },
521
+ "data-testid": `conversation-${conv.id}`,
522
+ children: [
523
+ /* @__PURE__ */ jsx2("div", { style: {
524
+ width: 36,
525
+ height: 36,
526
+ borderRadius: "50%",
527
+ background: "#1D3FE7",
528
+ display: "flex",
529
+ alignItems: "center",
530
+ justifyContent: "center",
531
+ color: "white",
532
+ fontSize: 14,
533
+ fontWeight: "bold"
534
+ }, children: (conv.name || conv.participants?.[0]?.user?.username || "?")[0].toUpperCase() }),
535
+ /* @__PURE__ */ jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [
536
+ /* @__PURE__ */ jsx2("div", { style: {
537
+ fontSize: 13,
538
+ fontWeight: 500,
539
+ color: isDark ? "#fff" : "#000",
540
+ whiteSpace: "nowrap",
541
+ overflow: "hidden",
542
+ textOverflow: "ellipsis"
543
+ }, children: conv.name || conv.participants?.map((p) => p.user?.username).join(", ") || "Chat" }),
544
+ conv.lastMessage && /* @__PURE__ */ jsx2("div", { style: {
545
+ fontSize: 11,
546
+ color: isDark ? "#888" : "#666",
547
+ whiteSpace: "nowrap",
548
+ overflow: "hidden",
549
+ textOverflow: "ellipsis"
550
+ }, children: conv.lastMessage.content })
551
+ ] })
552
+ ]
553
+ },
554
+ conv.id
555
+ ))
556
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
557
+ /* @__PURE__ */ jsx2(
558
+ "button",
559
+ {
560
+ onClick: () => setSelectedConversation(null),
561
+ style: {
562
+ display: "flex",
563
+ alignItems: "center",
564
+ gap: 6,
565
+ padding: "8px 12px",
566
+ background: isDark ? "#1a1a1a" : "#d0d0d0",
567
+ border: "none",
568
+ borderBottom: `1px solid ${isDark ? "#404040" : "#a0a0a0"}`,
569
+ cursor: "pointer",
570
+ fontSize: 12,
571
+ color: isDark ? "#ccc" : "#333"
572
+ },
573
+ "data-testid": "button-back-to-conversations",
574
+ children: "\u2190 Back"
575
+ }
576
+ ),
577
+ /* @__PURE__ */ jsxs("div", { style: {
578
+ flex: 1,
579
+ overflow: "auto",
580
+ padding: 12,
581
+ background: isDark ? "#2a2a2a" : "#e8e8e8",
582
+ display: "flex",
583
+ flexDirection: "column",
584
+ gap: 8
585
+ }, children: [
586
+ messages.map((msg) => /* @__PURE__ */ jsx2(
587
+ "div",
588
+ {
589
+ style: {
590
+ display: "flex",
591
+ justifyContent: msg.senderId === user?.id ? "flex-end" : "flex-start"
592
+ },
593
+ children: /* @__PURE__ */ jsx2("div", { style: {
594
+ maxWidth: "80%",
595
+ padding: "8px 12px",
596
+ borderRadius: 4,
597
+ background: msg.senderId === user?.id ? "#1D3FE7" : isDark ? "#404040" : "#fff",
598
+ color: msg.senderId === user?.id ? "#fff" : isDark ? "#fff" : "#000",
599
+ fontSize: 13,
600
+ boxShadow: "1px 1px 2px rgba(0,0,0,0.1)"
601
+ }, children: msg.content })
602
+ },
603
+ msg.id
604
+ )),
605
+ isTyping.length > 0 && /* @__PURE__ */ jsxs("div", { style: {
606
+ fontSize: 11,
607
+ color: isDark ? "#888" : "#666",
608
+ fontStyle: "italic"
609
+ }, children: [
610
+ isTyping.map((u) => u.username).join(", "),
611
+ " typing..."
612
+ ] })
613
+ ] }),
614
+ /* @__PURE__ */ jsxs("div", { style: {
615
+ display: "flex",
616
+ gap: 8,
617
+ padding: 8,
618
+ background: isDark ? "#1a1a1a" : "#c0c0c0",
619
+ borderTop: `1px solid ${isDark ? "#404040" : "#a0a0a0"}`
620
+ }, children: [
621
+ /* @__PURE__ */ jsx2(
622
+ "input",
623
+ {
624
+ type: "text",
625
+ value: messageInput,
626
+ onChange: (e) => setMessageInput(e.target.value),
627
+ onKeyDown: (e) => e.key === "Enter" && handleSend(),
628
+ placeholder: "Type a message...",
629
+ style: {
630
+ flex: 1,
631
+ padding: "8px 12px",
632
+ border: `2px inset ${isDark ? "#000" : "#808080"}`,
633
+ background: isDark ? "#0a0a0a" : "#fff",
634
+ color: isDark ? "#fff" : "#000",
635
+ fontSize: 13,
636
+ outline: "none"
637
+ },
638
+ "data-testid": "input-message"
639
+ }
640
+ ),
641
+ /* @__PURE__ */ jsx2(
642
+ "button",
643
+ {
644
+ onClick: handleSend,
645
+ style: {
646
+ background: "linear-gradient(135deg, #1D3FE7 0%, #142B99 100%)",
647
+ color: "white",
648
+ border: "2px outset #5577ff",
649
+ padding: "8px 16px",
650
+ fontSize: 12,
651
+ fontWeight: "bold",
652
+ cursor: "pointer"
653
+ },
654
+ "data-testid": "button-send-message",
655
+ children: "Send"
656
+ }
657
+ )
658
+ ] })
659
+ ] }) })
660
+ ] }) });
661
+ }
662
+ export {
663
+ ApeIMProvider,
664
+ ChatWidget,
665
+ useApeIM,
666
+ useChat,
667
+ useConversations
668
+ };