@veroai/chat 0.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.
- package/README.md +579 -0
- package/dist/chunk-JI6KXOLF.js +920 -0
- package/dist/chunk-JI6KXOLF.js.map +1 -0
- package/dist/client-C63XKtNw.d.ts +502 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.d.ts +187 -0
- package/dist/react/index.js +371 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +62 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { C as ChatEvents, a as CallAction, b as CallType } from './client-C63XKtNw.js';
|
|
2
|
+
export { A as AgentConfig, c as ApiError, d as ApiResponse, e as CallEvent, f as ChatApi, g as ChatClient, h as ChatClientConfig, i as Conversation, j as ConversationType, k as CreateConversationParams, G as GetMessagesParams, M as Message, l as MessageType, N as NewMessageEvent, P as PaginatedMessages, m as Participant, n as PresenceEvent, o as PresenceStatus, R as ReadReceipt, p as ReadReceiptEvent, q as RoomInfo, S as SendMessageParams, T as TypingEvent, U as User, r as UserPresence, W as WebSocketEventType, s as WebSocketMessage } from './client-C63XKtNw.js';
|
|
3
|
+
import EventEmitter from 'eventemitter3';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* VeroAI Chat WebSocket Manager
|
|
7
|
+
*
|
|
8
|
+
* Handles WebSocket connection, reconnection, and message handling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
interface WebSocketConfig {
|
|
12
|
+
url: string;
|
|
13
|
+
getToken: () => string | null | Promise<string | null>;
|
|
14
|
+
autoReconnect?: boolean;
|
|
15
|
+
reconnectInterval?: number;
|
|
16
|
+
maxReconnectAttempts?: number;
|
|
17
|
+
heartbeatInterval?: number;
|
|
18
|
+
}
|
|
19
|
+
type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'reconnecting';
|
|
20
|
+
/**
|
|
21
|
+
* WebSocket connection manager with auto-reconnect
|
|
22
|
+
*/
|
|
23
|
+
declare class WebSocketManager extends EventEmitter<ChatEvents> {
|
|
24
|
+
private config;
|
|
25
|
+
private ws;
|
|
26
|
+
private state;
|
|
27
|
+
private reconnectAttempts;
|
|
28
|
+
private reconnectTimer;
|
|
29
|
+
private heartbeatTimer;
|
|
30
|
+
private pendingMessages;
|
|
31
|
+
constructor(config: WebSocketConfig);
|
|
32
|
+
/**
|
|
33
|
+
* Get current connection state
|
|
34
|
+
*/
|
|
35
|
+
getState(): ConnectionState;
|
|
36
|
+
/**
|
|
37
|
+
* Check if connected
|
|
38
|
+
*/
|
|
39
|
+
isConnected(): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Connect to the WebSocket server
|
|
42
|
+
*/
|
|
43
|
+
connect(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Disconnect from the WebSocket server
|
|
46
|
+
*/
|
|
47
|
+
disconnect(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Send a message through the WebSocket
|
|
50
|
+
*/
|
|
51
|
+
send(type: string, payload: unknown): void;
|
|
52
|
+
/**
|
|
53
|
+
* Send typing indicator
|
|
54
|
+
*/
|
|
55
|
+
sendTypingStart(conversationId: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Stop typing indicator
|
|
58
|
+
*/
|
|
59
|
+
sendTypingStop(conversationId: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Subscribe to a conversation for real-time updates
|
|
62
|
+
*/
|
|
63
|
+
subscribeToConversation(conversationId: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* Unsubscribe from a conversation
|
|
66
|
+
*/
|
|
67
|
+
unsubscribeFromConversation(conversationId: string): void;
|
|
68
|
+
/**
|
|
69
|
+
* Update presence status
|
|
70
|
+
*/
|
|
71
|
+
updatePresence(status: string, statusMessage?: string): void;
|
|
72
|
+
/**
|
|
73
|
+
* Send call notification (ring, accept, reject, end)
|
|
74
|
+
* Note: Actual WebRTC signaling is handled by LiveKit
|
|
75
|
+
*/
|
|
76
|
+
sendCallNotification(conversationId: string, action: CallAction, callType?: CallType, roomName?: string): void;
|
|
77
|
+
private handleClose;
|
|
78
|
+
private scheduleReconnect;
|
|
79
|
+
private handleMessage;
|
|
80
|
+
private flushPendingMessages;
|
|
81
|
+
private startHeartbeat;
|
|
82
|
+
private stopHeartbeat;
|
|
83
|
+
private clearTimers;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { CallAction, CallType, ChatEvents, WebSocketManager };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { g as ChatClient, U as User, i as Conversation, o as PresenceStatus, h as ChatClientConfig, M as Message, S as SendMessageParams } from '../client-C63XKtNw.js';
|
|
4
|
+
import 'eventemitter3';
|
|
5
|
+
|
|
6
|
+
interface ChatContextValue {
|
|
7
|
+
/** The ChatClient instance */
|
|
8
|
+
client: ChatClient | null;
|
|
9
|
+
/** Whether the WebSocket is connected */
|
|
10
|
+
isConnected: boolean;
|
|
11
|
+
/** Current user profile */
|
|
12
|
+
currentUser: User | null;
|
|
13
|
+
/** List of conversations */
|
|
14
|
+
conversations: Conversation[];
|
|
15
|
+
/** Loading state for conversations */
|
|
16
|
+
isLoadingConversations: boolean;
|
|
17
|
+
/** Refresh conversations from server */
|
|
18
|
+
refreshConversations: () => Promise<void>;
|
|
19
|
+
/** Connect to WebSocket */
|
|
20
|
+
connect: () => Promise<void>;
|
|
21
|
+
/** Disconnect from WebSocket */
|
|
22
|
+
disconnect: () => void;
|
|
23
|
+
/** Update current user's status */
|
|
24
|
+
updateStatus: (status: PresenceStatus, statusMessage?: string) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
interface ChatProviderProps {
|
|
27
|
+
children: React.ReactNode;
|
|
28
|
+
/** ChatClient configuration */
|
|
29
|
+
config: ChatClientConfig;
|
|
30
|
+
/** Auto-fetch conversations on mount */
|
|
31
|
+
autoFetchConversations?: boolean;
|
|
32
|
+
/** Auto-fetch current user on mount */
|
|
33
|
+
autoFetchCurrentUser?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* ChatProvider - Provides chat functionality to React components
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* function App() {
|
|
41
|
+
* return (
|
|
42
|
+
* <ChatProvider
|
|
43
|
+
* config={{
|
|
44
|
+
* apiUrl: 'https://api.veroai.dev',
|
|
45
|
+
* wsUrl: 'wss://ws.veroai.dev',
|
|
46
|
+
* token: authToken,
|
|
47
|
+
* }}
|
|
48
|
+
* >
|
|
49
|
+
* <ChatApp />
|
|
50
|
+
* </ChatProvider>
|
|
51
|
+
* );
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function ChatProvider({ children, config, autoFetchConversations, autoFetchCurrentUser, }: ChatProviderProps): react_jsx_runtime.JSX.Element;
|
|
56
|
+
/**
|
|
57
|
+
* useChat - Access chat context
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```tsx
|
|
61
|
+
* function ChatList() {
|
|
62
|
+
* const { conversations, isConnected } = useChat();
|
|
63
|
+
* return <div>{conversations.map(c => <div key={c.id}>{c.name}</div>)}</div>;
|
|
64
|
+
* }
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
declare function useChat(): ChatContextValue;
|
|
68
|
+
/**
|
|
69
|
+
* useChatClient - Access the ChatClient instance directly
|
|
70
|
+
*/
|
|
71
|
+
declare function useChatClient(): ChatClient | null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* useConversation Hook
|
|
75
|
+
*
|
|
76
|
+
* Manage a single conversation with real-time updates
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
interface UseConversationOptions {
|
|
80
|
+
/** Auto-fetch messages on mount */
|
|
81
|
+
autoFetchMessages?: boolean;
|
|
82
|
+
/** Number of messages to fetch initially */
|
|
83
|
+
initialMessageLimit?: number;
|
|
84
|
+
/** Auto-subscribe to real-time updates */
|
|
85
|
+
autoSubscribe?: boolean;
|
|
86
|
+
}
|
|
87
|
+
interface UseConversationReturn {
|
|
88
|
+
/** The conversation object */
|
|
89
|
+
conversation: Conversation | null;
|
|
90
|
+
/** Messages in the conversation */
|
|
91
|
+
messages: Message[];
|
|
92
|
+
/** Whether messages are loading */
|
|
93
|
+
isLoading: boolean;
|
|
94
|
+
/** Whether more messages are available */
|
|
95
|
+
hasMore: boolean;
|
|
96
|
+
/** Users currently typing */
|
|
97
|
+
typingUsers: string[];
|
|
98
|
+
/** Send a message */
|
|
99
|
+
sendMessage: (params: SendMessageParams) => Promise<Message>;
|
|
100
|
+
/** Send a text message (convenience) */
|
|
101
|
+
send: (content: string) => Promise<Message>;
|
|
102
|
+
/** Load more (older) messages */
|
|
103
|
+
loadMore: () => Promise<void>;
|
|
104
|
+
/** Refresh messages from server */
|
|
105
|
+
refresh: () => Promise<void>;
|
|
106
|
+
/** Mark conversation as read */
|
|
107
|
+
markAsRead: () => Promise<void>;
|
|
108
|
+
/** Start typing indicator */
|
|
109
|
+
startTyping: () => void;
|
|
110
|
+
/** Stop typing indicator */
|
|
111
|
+
stopTyping: () => void;
|
|
112
|
+
/** Error if any */
|
|
113
|
+
error: Error | null;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* useConversation - Manage a single conversation
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```tsx
|
|
120
|
+
* function ChatRoom({ conversationId }) {
|
|
121
|
+
* const {
|
|
122
|
+
* messages,
|
|
123
|
+
* send,
|
|
124
|
+
* isLoading,
|
|
125
|
+
* typingUsers,
|
|
126
|
+
* } = useConversation(conversationId);
|
|
127
|
+
*
|
|
128
|
+
* return (
|
|
129
|
+
* <div>
|
|
130
|
+
* {messages.map(m => <Message key={m.id} message={m} />)}
|
|
131
|
+
* {typingUsers.length > 0 && <div>Someone is typing...</div>}
|
|
132
|
+
* <input onKeyDown={(e) => e.key === 'Enter' && send(e.target.value)} />
|
|
133
|
+
* </div>
|
|
134
|
+
* );
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
declare function useConversation(conversationId: string | undefined, options?: UseConversationOptions): UseConversationReturn;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* usePresence Hook
|
|
142
|
+
*
|
|
143
|
+
* Track and manage user presence/online status
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
interface UsePresenceReturn {
|
|
147
|
+
/** Online users */
|
|
148
|
+
onlineUsers: User[];
|
|
149
|
+
/** Whether loading online users */
|
|
150
|
+
isLoading: boolean;
|
|
151
|
+
/** Get presence status for a specific user */
|
|
152
|
+
getUserStatus: (userId: string) => PresenceStatus;
|
|
153
|
+
/** Refresh online users list */
|
|
154
|
+
refresh: () => Promise<void>;
|
|
155
|
+
/** Error if any */
|
|
156
|
+
error: Error | null;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* usePresence - Track online users and presence
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```tsx
|
|
163
|
+
* function OnlineUsers() {
|
|
164
|
+
* const { onlineUsers, getUserStatus } = usePresence();
|
|
165
|
+
*
|
|
166
|
+
* return (
|
|
167
|
+
* <div>
|
|
168
|
+
* {onlineUsers.map(user => (
|
|
169
|
+
* <div key={user.id}>
|
|
170
|
+
* {user.firstName} - {getUserStatus(user.id)}
|
|
171
|
+
* </div>
|
|
172
|
+
* ))}
|
|
173
|
+
* </div>
|
|
174
|
+
* );
|
|
175
|
+
* }
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
declare function usePresence(): UsePresenceReturn;
|
|
179
|
+
/**
|
|
180
|
+
* useUserPresence - Track presence for a specific user
|
|
181
|
+
*/
|
|
182
|
+
declare function useUserPresence(userId: string | undefined): {
|
|
183
|
+
status: PresenceStatus;
|
|
184
|
+
isOnline: boolean;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export { type ChatContextValue, ChatProvider, type ChatProviderProps, type UseConversationOptions, type UseConversationReturn, type UsePresenceReturn, useChat, useChatClient, useConversation, usePresence, useUserPresence };
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { ChatClient } from '../chunk-JI6KXOLF.js';
|
|
2
|
+
import { createContext, useState, useRef, useEffect, useCallback, useContext } from 'react';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
var ChatContext = createContext(null);
|
|
6
|
+
function ChatProvider({
|
|
7
|
+
children,
|
|
8
|
+
config,
|
|
9
|
+
autoFetchConversations = true,
|
|
10
|
+
autoFetchCurrentUser = true
|
|
11
|
+
}) {
|
|
12
|
+
const [client, setClient] = useState(null);
|
|
13
|
+
const [isConnected, setIsConnected] = useState(false);
|
|
14
|
+
const [currentUser, setCurrentUser] = useState(null);
|
|
15
|
+
const [conversations, setConversations] = useState([]);
|
|
16
|
+
const [isLoadingConversations, setIsLoadingConversations] = useState(false);
|
|
17
|
+
const clientRef = useRef(null);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const chatClient = new ChatClient({
|
|
20
|
+
...config,
|
|
21
|
+
autoConnect: false
|
|
22
|
+
// We'll connect manually to handle state
|
|
23
|
+
});
|
|
24
|
+
clientRef.current = chatClient;
|
|
25
|
+
setClient(chatClient);
|
|
26
|
+
chatClient.on("connected", () => setIsConnected(true));
|
|
27
|
+
chatClient.on("disconnected", () => setIsConnected(false));
|
|
28
|
+
chatClient.on("conversation:created", (conv) => {
|
|
29
|
+
setConversations((prev) => [conv, ...prev]);
|
|
30
|
+
});
|
|
31
|
+
chatClient.on("conversation:updated", (conv) => {
|
|
32
|
+
setConversations(
|
|
33
|
+
(prev) => prev.map((c) => c.id === conv.id ? conv : c)
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
chatClient.on("message:new", ({ conversationId }) => {
|
|
37
|
+
setConversations(
|
|
38
|
+
(prev) => prev.map(
|
|
39
|
+
(c) => c.id === conversationId ? { ...c, lastMessageAt: (/* @__PURE__ */ new Date()).toISOString() } : c
|
|
40
|
+
)
|
|
41
|
+
);
|
|
42
|
+
});
|
|
43
|
+
if (config.wsUrl && config.autoConnect !== false) {
|
|
44
|
+
chatClient.connect().catch(console.error);
|
|
45
|
+
}
|
|
46
|
+
return () => {
|
|
47
|
+
chatClient.disconnect();
|
|
48
|
+
chatClient.removeAllListeners();
|
|
49
|
+
};
|
|
50
|
+
}, [config.apiUrl, config.wsUrl, config.token]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!client || !autoFetchCurrentUser) return;
|
|
53
|
+
client.getCurrentUser().then(setCurrentUser).catch(console.error);
|
|
54
|
+
}, [client, autoFetchCurrentUser]);
|
|
55
|
+
const refreshConversations = useCallback(async () => {
|
|
56
|
+
if (!client) return;
|
|
57
|
+
setIsLoadingConversations(true);
|
|
58
|
+
try {
|
|
59
|
+
const convs = await client.listConversations();
|
|
60
|
+
setConversations(convs);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error("[ChatProvider] Failed to fetch conversations:", error);
|
|
63
|
+
} finally {
|
|
64
|
+
setIsLoadingConversations(false);
|
|
65
|
+
}
|
|
66
|
+
}, [client]);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (autoFetchConversations && client) {
|
|
69
|
+
refreshConversations();
|
|
70
|
+
}
|
|
71
|
+
}, [autoFetchConversations, client, refreshConversations]);
|
|
72
|
+
const connect = useCallback(async () => {
|
|
73
|
+
if (client) {
|
|
74
|
+
await client.connect();
|
|
75
|
+
}
|
|
76
|
+
}, [client]);
|
|
77
|
+
const disconnect = useCallback(() => {
|
|
78
|
+
client?.disconnect();
|
|
79
|
+
}, [client]);
|
|
80
|
+
const updateStatus = useCallback(
|
|
81
|
+
async (status, statusMessage) => {
|
|
82
|
+
if (client) {
|
|
83
|
+
await client.updateStatus(status, statusMessage);
|
|
84
|
+
setCurrentUser(
|
|
85
|
+
(prev) => prev ? { ...prev, status, statusMessage } : prev
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
[client]
|
|
90
|
+
);
|
|
91
|
+
const value = {
|
|
92
|
+
client,
|
|
93
|
+
isConnected,
|
|
94
|
+
currentUser,
|
|
95
|
+
conversations,
|
|
96
|
+
isLoadingConversations,
|
|
97
|
+
refreshConversations,
|
|
98
|
+
connect,
|
|
99
|
+
disconnect,
|
|
100
|
+
updateStatus
|
|
101
|
+
};
|
|
102
|
+
return /* @__PURE__ */ jsx(ChatContext.Provider, { value, children });
|
|
103
|
+
}
|
|
104
|
+
function useChat() {
|
|
105
|
+
const context = useContext(ChatContext);
|
|
106
|
+
if (!context) {
|
|
107
|
+
throw new Error("useChat must be used within a ChatProvider");
|
|
108
|
+
}
|
|
109
|
+
return context;
|
|
110
|
+
}
|
|
111
|
+
function useChatClient() {
|
|
112
|
+
const { client } = useChat();
|
|
113
|
+
return client;
|
|
114
|
+
}
|
|
115
|
+
function useConversation(conversationId, options = {}) {
|
|
116
|
+
const {
|
|
117
|
+
autoFetchMessages = true,
|
|
118
|
+
initialMessageLimit = 50,
|
|
119
|
+
autoSubscribe = true
|
|
120
|
+
} = options;
|
|
121
|
+
const { client, conversations } = useChat();
|
|
122
|
+
const [conversation, setConversation] = useState(null);
|
|
123
|
+
const [messages, setMessages] = useState([]);
|
|
124
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
125
|
+
const [hasMore, setHasMore] = useState(true);
|
|
126
|
+
const [typingUsers, setTypingUsers] = useState([]);
|
|
127
|
+
const [error, setError] = useState(null);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!conversationId) {
|
|
130
|
+
setConversation(null);
|
|
131
|
+
setMessages([]);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const existing = conversations.find((c) => c.id === conversationId);
|
|
135
|
+
if (existing) {
|
|
136
|
+
setConversation(existing);
|
|
137
|
+
} else if (client) {
|
|
138
|
+
client.getConversation(conversationId).then(setConversation).catch((err) => {
|
|
139
|
+
setError(err);
|
|
140
|
+
console.error("[useConversation] Failed to fetch conversation:", err);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}, [conversationId, conversations, client]);
|
|
144
|
+
const fetchMessages = useCallback(async (before) => {
|
|
145
|
+
if (!client || !conversationId) return;
|
|
146
|
+
setIsLoading(true);
|
|
147
|
+
setError(null);
|
|
148
|
+
try {
|
|
149
|
+
const result = await client.getMessages(conversationId, {
|
|
150
|
+
limit: initialMessageLimit,
|
|
151
|
+
before
|
|
152
|
+
});
|
|
153
|
+
if (before) {
|
|
154
|
+
setMessages((prev) => [...prev, ...result.messages]);
|
|
155
|
+
} else {
|
|
156
|
+
setMessages(result.messages);
|
|
157
|
+
}
|
|
158
|
+
setHasMore(result.hasMore);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
161
|
+
console.error("[useConversation] Failed to fetch messages:", err);
|
|
162
|
+
} finally {
|
|
163
|
+
setIsLoading(false);
|
|
164
|
+
}
|
|
165
|
+
}, [client, conversationId, initialMessageLimit]);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
if (autoFetchMessages && conversationId && client) {
|
|
168
|
+
fetchMessages();
|
|
169
|
+
}
|
|
170
|
+
}, [autoFetchMessages, conversationId, client, fetchMessages]);
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!client || !conversationId || !autoSubscribe) return;
|
|
173
|
+
client.subscribeToConversation(conversationId);
|
|
174
|
+
const handleNewMessage = ({ message, conversationId: convId }) => {
|
|
175
|
+
if (convId === conversationId) {
|
|
176
|
+
setMessages((prev) => [message, ...prev]);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
const handleMessageUpdated = (message) => {
|
|
180
|
+
if (message.conversationId === conversationId) {
|
|
181
|
+
setMessages(
|
|
182
|
+
(prev) => prev.map((m) => m.id === message.id ? message : m)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const handleMessageDeleted = (messageId, convId) => {
|
|
187
|
+
if (convId === conversationId) {
|
|
188
|
+
setMessages((prev) => prev.filter((m) => m.id !== messageId));
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const handleTypingStart = ({ conversationId: convId, userId }) => {
|
|
192
|
+
if (convId === conversationId) {
|
|
193
|
+
setTypingUsers(
|
|
194
|
+
(prev) => prev.includes(userId) ? prev : [...prev, userId]
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const handleTypingStop = ({ conversationId: convId, userId }) => {
|
|
199
|
+
if (convId === conversationId) {
|
|
200
|
+
setTypingUsers((prev) => prev.filter((id) => id !== userId));
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
client.on("message:new", handleNewMessage);
|
|
204
|
+
client.on("message:updated", handleMessageUpdated);
|
|
205
|
+
client.on("message:deleted", handleMessageDeleted);
|
|
206
|
+
client.on("typing:start", handleTypingStart);
|
|
207
|
+
client.on("typing:stop", handleTypingStop);
|
|
208
|
+
return () => {
|
|
209
|
+
client.unsubscribeFromConversation(conversationId);
|
|
210
|
+
client.off("message:new", handleNewMessage);
|
|
211
|
+
client.off("message:updated", handleMessageUpdated);
|
|
212
|
+
client.off("message:deleted", handleMessageDeleted);
|
|
213
|
+
client.off("typing:start", handleTypingStart);
|
|
214
|
+
client.off("typing:stop", handleTypingStop);
|
|
215
|
+
};
|
|
216
|
+
}, [client, conversationId, autoSubscribe]);
|
|
217
|
+
const sendMessage = useCallback(
|
|
218
|
+
async (params) => {
|
|
219
|
+
if (!client || !conversationId) {
|
|
220
|
+
throw new Error("No conversation selected");
|
|
221
|
+
}
|
|
222
|
+
return client.sendMessage(conversationId, params);
|
|
223
|
+
},
|
|
224
|
+
[client, conversationId]
|
|
225
|
+
);
|
|
226
|
+
const send = useCallback(
|
|
227
|
+
async (content) => {
|
|
228
|
+
return sendMessage({ content });
|
|
229
|
+
},
|
|
230
|
+
[sendMessage]
|
|
231
|
+
);
|
|
232
|
+
const loadMore = useCallback(async () => {
|
|
233
|
+
if (!hasMore || isLoading || messages.length === 0) return;
|
|
234
|
+
const oldestMessage = messages[messages.length - 1];
|
|
235
|
+
await fetchMessages(oldestMessage.createdAt);
|
|
236
|
+
}, [hasMore, isLoading, messages, fetchMessages]);
|
|
237
|
+
const refresh = useCallback(async () => {
|
|
238
|
+
setMessages([]);
|
|
239
|
+
setHasMore(true);
|
|
240
|
+
await fetchMessages();
|
|
241
|
+
}, [fetchMessages]);
|
|
242
|
+
const markAsRead = useCallback(async () => {
|
|
243
|
+
if (!client || !conversationId) return;
|
|
244
|
+
await client.markConversationRead(conversationId);
|
|
245
|
+
}, [client, conversationId]);
|
|
246
|
+
const startTyping = useCallback(() => {
|
|
247
|
+
if (conversationId) {
|
|
248
|
+
client?.sendTypingStart(conversationId);
|
|
249
|
+
}
|
|
250
|
+
}, [client, conversationId]);
|
|
251
|
+
const stopTyping = useCallback(() => {
|
|
252
|
+
if (conversationId) {
|
|
253
|
+
client?.sendTypingStop(conversationId);
|
|
254
|
+
}
|
|
255
|
+
}, [client, conversationId]);
|
|
256
|
+
return {
|
|
257
|
+
conversation,
|
|
258
|
+
messages,
|
|
259
|
+
isLoading,
|
|
260
|
+
hasMore,
|
|
261
|
+
typingUsers,
|
|
262
|
+
sendMessage,
|
|
263
|
+
send,
|
|
264
|
+
loadMore,
|
|
265
|
+
refresh,
|
|
266
|
+
markAsRead,
|
|
267
|
+
startTyping,
|
|
268
|
+
stopTyping,
|
|
269
|
+
error
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function usePresence() {
|
|
273
|
+
const { client } = useChat();
|
|
274
|
+
const [onlineUsers, setOnlineUsers] = useState([]);
|
|
275
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
276
|
+
const [error, setError] = useState(null);
|
|
277
|
+
const [presenceMap, setPresenceMap] = useState(/* @__PURE__ */ new Map());
|
|
278
|
+
const refresh = useCallback(async () => {
|
|
279
|
+
if (!client) return;
|
|
280
|
+
setIsLoading(true);
|
|
281
|
+
setError(null);
|
|
282
|
+
try {
|
|
283
|
+
const users = await client.getOnlineUsers();
|
|
284
|
+
setOnlineUsers(users);
|
|
285
|
+
const newMap = /* @__PURE__ */ new Map();
|
|
286
|
+
users.forEach((user) => {
|
|
287
|
+
if (user.status) {
|
|
288
|
+
newMap.set(user.id, user.status);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
setPresenceMap(newMap);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
294
|
+
console.error("[usePresence] Failed to fetch online users:", err);
|
|
295
|
+
} finally {
|
|
296
|
+
setIsLoading(false);
|
|
297
|
+
}
|
|
298
|
+
}, [client]);
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (client) {
|
|
301
|
+
refresh();
|
|
302
|
+
}
|
|
303
|
+
}, [client, refresh]);
|
|
304
|
+
useEffect(() => {
|
|
305
|
+
if (!client) return;
|
|
306
|
+
const handlePresenceUpdate = ({ userId, status }) => {
|
|
307
|
+
setPresenceMap((prev) => {
|
|
308
|
+
const newMap = new Map(prev);
|
|
309
|
+
newMap.set(userId, status);
|
|
310
|
+
return newMap;
|
|
311
|
+
});
|
|
312
|
+
if (status === "offline") {
|
|
313
|
+
setOnlineUsers((prev) => prev.filter((u) => u.id !== userId));
|
|
314
|
+
} else {
|
|
315
|
+
setOnlineUsers((prev) => {
|
|
316
|
+
const exists = prev.some((u) => u.id === userId);
|
|
317
|
+
if (!exists) {
|
|
318
|
+
refresh();
|
|
319
|
+
}
|
|
320
|
+
return prev.map(
|
|
321
|
+
(u) => u.id === userId ? { ...u, status } : u
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
client.on("presence:updated", handlePresenceUpdate);
|
|
327
|
+
return () => {
|
|
328
|
+
client.off("presence:updated", handlePresenceUpdate);
|
|
329
|
+
};
|
|
330
|
+
}, [client, refresh]);
|
|
331
|
+
const getUserStatus = useCallback(
|
|
332
|
+
(userId) => {
|
|
333
|
+
return presenceMap.get(userId) || "offline";
|
|
334
|
+
},
|
|
335
|
+
[presenceMap]
|
|
336
|
+
);
|
|
337
|
+
return {
|
|
338
|
+
onlineUsers,
|
|
339
|
+
isLoading,
|
|
340
|
+
getUserStatus,
|
|
341
|
+
refresh,
|
|
342
|
+
error
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
function useUserPresence(userId) {
|
|
346
|
+
const { client } = useChat();
|
|
347
|
+
const [status, setStatus] = useState("offline");
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
if (!client || !userId) return;
|
|
350
|
+
client.getUser(userId).then((user) => {
|
|
351
|
+
setStatus(user.status || "offline");
|
|
352
|
+
}).catch(console.error);
|
|
353
|
+
const handlePresenceUpdate = ({ userId: eventUserId, status: newStatus }) => {
|
|
354
|
+
if (eventUserId === userId) {
|
|
355
|
+
setStatus(newStatus);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
client.on("presence:updated", handlePresenceUpdate);
|
|
359
|
+
return () => {
|
|
360
|
+
client.off("presence:updated", handlePresenceUpdate);
|
|
361
|
+
};
|
|
362
|
+
}, [client, userId]);
|
|
363
|
+
return {
|
|
364
|
+
status,
|
|
365
|
+
isOnline: status !== "offline"
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export { ChatProvider, useChat, useChatClient, useConversation, usePresence, useUserPresence };
|
|
370
|
+
//# sourceMappingURL=index.js.map
|
|
371
|
+
//# sourceMappingURL=index.js.map
|