@enfin/chat 1.2.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 +224 -0
- package/dist/assets/music/i_phone_message.mp3 +0 -0
- package/dist/call/WebRTCManager.d.ts +54 -0
- package/dist/call/WebRTCManager.js +274 -0
- package/dist/call/signaling.d.ts +3 -0
- package/dist/call/signaling.js +24 -0
- package/dist/call/types.d.ts +25 -0
- package/dist/call/types.js +13 -0
- package/dist/components/Chat.d.ts +14 -0
- package/dist/components/Chat.js +452 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +2 -0
- package/dist/context/ChatContext.d.ts +59 -0
- package/dist/context/ChatContext.js +647 -0
- package/dist/esm/call/WebRTCManager.d.ts +54 -0
- package/dist/esm/call/WebRTCManager.js +274 -0
- package/dist/esm/call/signaling.d.ts +3 -0
- package/dist/esm/call/signaling.js +24 -0
- package/dist/esm/call/types.d.ts +25 -0
- package/dist/esm/call/types.js +13 -0
- package/dist/esm/components/Chat.d.ts +14 -0
- package/dist/esm/components/Chat.js +452 -0
- package/dist/esm/components/index.d.ts +3 -0
- package/dist/esm/components/index.js +2 -0
- package/dist/esm/context/ChatContext.d.ts +59 -0
- package/dist/esm/context/ChatContext.js +647 -0
- package/dist/esm/hooks/index.d.ts +6 -0
- package/dist/esm/hooks/index.js +6 -0
- package/dist/esm/hooks/useCall.d.ts +15 -0
- package/dist/esm/hooks/useCall.js +38 -0
- package/dist/esm/hooks/useChat.d.ts +23 -0
- package/dist/esm/hooks/useChat.js +40 -0
- package/dist/esm/hooks/useMessages.d.ts +17 -0
- package/dist/esm/hooks/useMessages.js +38 -0
- package/dist/esm/hooks/usePresence.d.ts +4 -0
- package/dist/esm/hooks/usePresence.js +12 -0
- package/dist/esm/hooks/useRooms.d.ts +9 -0
- package/dist/esm/hooks/useRooms.js +19 -0
- package/dist/esm/hooks/useSocket.d.ts +7 -0
- package/dist/esm/hooks/useSocket.js +92 -0
- package/dist/esm/index.d.ts +11 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/utils/index.d.ts +2 -0
- package/dist/esm/utils/index.js +2 -0
- package/dist/esm/utils/ringtone.d.ts +2 -0
- package/dist/esm/utils/ringtone.js +39 -0
- package/dist/esm/utils/testUtils.d.ts +102 -0
- package/dist/esm/utils/testUtils.js +153 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.js +6 -0
- package/dist/hooks/useCall.d.ts +15 -0
- package/dist/hooks/useCall.js +38 -0
- package/dist/hooks/useChat.d.ts +23 -0
- package/dist/hooks/useChat.js +40 -0
- package/dist/hooks/useMessages.d.ts +17 -0
- package/dist/hooks/useMessages.js +38 -0
- package/dist/hooks/usePresence.d.ts +4 -0
- package/dist/hooks/usePresence.js +12 -0
- package/dist/hooks/useRooms.d.ts +9 -0
- package/dist/hooks/useRooms.js +19 -0
- package/dist/hooks/useSocket.d.ts +7 -0
- package/dist/hooks/useSocket.js +92 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/public/music/i_phone_message.mp3 +0 -0
- package/dist/style.css +1226 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/ringtone.d.ts +2 -0
- package/dist/utils/ringtone.js +39 -0
- package/dist/utils/testUtils.d.ts +102 -0
- package/dist/utils/testUtils.js +153 -0
- package/package.json +44 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useChatContext } from '../context/ChatContext';
|
|
3
|
+
export function useCall() {
|
|
4
|
+
const { activeCall, callState, callError, remoteStream, isMuted, busyBanner, clearBusyBanner, startCall, endCall, acceptCall, rejectCall, mute, unmute, } = useChatContext();
|
|
5
|
+
const handleStartCall = useCallback(async (roomId, participantId) => {
|
|
6
|
+
await startCall(roomId, participantId);
|
|
7
|
+
}, [startCall]);
|
|
8
|
+
const handleEndCall = useCallback(async () => {
|
|
9
|
+
await endCall();
|
|
10
|
+
}, [endCall]);
|
|
11
|
+
const handleAcceptCall = useCallback(async () => {
|
|
12
|
+
await acceptCall();
|
|
13
|
+
}, [acceptCall]);
|
|
14
|
+
const handleRejectCall = useCallback(async () => {
|
|
15
|
+
await rejectCall();
|
|
16
|
+
}, [rejectCall]);
|
|
17
|
+
const handleMute = useCallback(() => {
|
|
18
|
+
mute();
|
|
19
|
+
}, [mute]);
|
|
20
|
+
const handleUnmute = useCallback(() => {
|
|
21
|
+
unmute();
|
|
22
|
+
}, [unmute]);
|
|
23
|
+
return {
|
|
24
|
+
activeCall,
|
|
25
|
+
callState,
|
|
26
|
+
callError,
|
|
27
|
+
remoteStream,
|
|
28
|
+
busyBanner,
|
|
29
|
+
clearBusyBanner,
|
|
30
|
+
startCall: handleStartCall,
|
|
31
|
+
endCall: handleEndCall,
|
|
32
|
+
acceptCall: handleAcceptCall,
|
|
33
|
+
rejectCall: handleRejectCall,
|
|
34
|
+
mute: handleMute,
|
|
35
|
+
unmute: handleUnmute,
|
|
36
|
+
isMuted,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export declare function useChat(): {
|
|
2
|
+
userId: string;
|
|
3
|
+
userName: string;
|
|
4
|
+
messages: import("@enfin/chat-shared").Message[];
|
|
5
|
+
rooms: import("@enfin/chat-shared").Room[];
|
|
6
|
+
users: import("../context/ChatContext").ChatUser[];
|
|
7
|
+
currentRoom: import("@enfin/chat-shared").Room;
|
|
8
|
+
typingUsers: import("@enfin/chat-shared").PresenceStatus[];
|
|
9
|
+
connected: boolean;
|
|
10
|
+
loading: boolean;
|
|
11
|
+
sending: boolean;
|
|
12
|
+
error: string;
|
|
13
|
+
sendMessage: (roomId: string, content: string, fileMetadata?: {
|
|
14
|
+
fileUrl?: string;
|
|
15
|
+
fileType?: string;
|
|
16
|
+
fileName?: string;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
selectRoom: (roomId: string) => void;
|
|
19
|
+
refreshUsers: () => Promise<void>;
|
|
20
|
+
emitTyping: (roomId: string, typing: boolean) => void;
|
|
21
|
+
debouncedEmitTyping: (roomId: string, typing: boolean) => void;
|
|
22
|
+
stopTypingSoon: (roomId: string) => void;
|
|
23
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react';
|
|
2
|
+
import { useChatContext } from '../context/ChatContext';
|
|
3
|
+
export function useChat() {
|
|
4
|
+
const { userId, userName, messages, rooms, users, currentRoom, typingUsers, connected, loading, error, sendMessage, selectRoom, refreshUsers, emitTyping, debouncedEmitTyping, stopTypingSoon, } = useChatContext();
|
|
5
|
+
const [sending, setSending] = useState(false);
|
|
6
|
+
const handleSendMessage = useCallback(async (roomId, content, fileMetadata) => {
|
|
7
|
+
setSending(true);
|
|
8
|
+
try {
|
|
9
|
+
await sendMessage(roomId, content, fileMetadata);
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
setSending(false);
|
|
13
|
+
}
|
|
14
|
+
}, [sendMessage]);
|
|
15
|
+
const handleSelectRoom = useCallback((roomId) => {
|
|
16
|
+
selectRoom(roomId);
|
|
17
|
+
}, [selectRoom]);
|
|
18
|
+
const filteredMessages = currentRoom
|
|
19
|
+
? messages.filter(m => m.roomId === currentRoom._id)
|
|
20
|
+
: [];
|
|
21
|
+
return {
|
|
22
|
+
userId,
|
|
23
|
+
userName,
|
|
24
|
+
messages: filteredMessages,
|
|
25
|
+
rooms,
|
|
26
|
+
users,
|
|
27
|
+
currentRoom,
|
|
28
|
+
typingUsers,
|
|
29
|
+
connected,
|
|
30
|
+
loading,
|
|
31
|
+
sending,
|
|
32
|
+
error,
|
|
33
|
+
sendMessage: handleSendMessage,
|
|
34
|
+
selectRoom: handleSelectRoom,
|
|
35
|
+
refreshUsers,
|
|
36
|
+
emitTyping,
|
|
37
|
+
debouncedEmitTyping,
|
|
38
|
+
stopTypingSoon,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Message } from '@enfin/chat-shared';
|
|
2
|
+
interface UseMessagesOptions {
|
|
3
|
+
roomId: string;
|
|
4
|
+
limit?: number;
|
|
5
|
+
autoFetch?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function useMessages(options: UseMessagesOptions): {
|
|
8
|
+
messages: Message[];
|
|
9
|
+
loading: boolean;
|
|
10
|
+
currentRoom: import("@enfin/chat-shared").Room;
|
|
11
|
+
sendMessage: (content: string, fileMetadata?: {
|
|
12
|
+
fileUrl?: string;
|
|
13
|
+
fileType?: string;
|
|
14
|
+
fileName?: string;
|
|
15
|
+
}) => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useChatContext } from '../context/ChatContext';
|
|
3
|
+
export function useMessages(options) {
|
|
4
|
+
const { messages: allMessages, currentRoom, loading, sendMessage, loadRoomMessages, connected, } = useChatContext();
|
|
5
|
+
const { roomId: targetRoomId, autoFetch = true, } = options;
|
|
6
|
+
// Live updates: filter messages for the target room. Since the SDK keeps
|
|
7
|
+
// a single shared message state in context, any consumer of useMessages
|
|
8
|
+
// automatically receives new messages from socket `message:new` events.
|
|
9
|
+
const roomMessages = allMessages.filter(m => m.roomId === targetRoomId);
|
|
10
|
+
// Track whether we've already fetched the initial history for this room.
|
|
11
|
+
const fetchedForRoomRef = useRef(null);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (!autoFetch || !connected)
|
|
14
|
+
return;
|
|
15
|
+
if (fetchedForRoomRef.current === targetRoomId)
|
|
16
|
+
return;
|
|
17
|
+
fetchedForRoomRef.current = targetRoomId;
|
|
18
|
+
loadRoomMessages(targetRoomId);
|
|
19
|
+
}, [autoFetch, connected, targetRoomId, loadRoomMessages]);
|
|
20
|
+
// Reset the fetched-marker when the roomId changes so refetch happens.
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
return () => {
|
|
23
|
+
fetchedForRoomRef.current = null;
|
|
24
|
+
};
|
|
25
|
+
}, [targetRoomId]);
|
|
26
|
+
const handleSend = useCallback(async (content, fileMetadata) => {
|
|
27
|
+
if (!currentRoom || currentRoom._id !== targetRoomId) {
|
|
28
|
+
throw new Error('Cannot send message to a different room');
|
|
29
|
+
}
|
|
30
|
+
await sendMessage(targetRoomId, content, fileMetadata);
|
|
31
|
+
}, [currentRoom, targetRoomId, sendMessage]);
|
|
32
|
+
return {
|
|
33
|
+
messages: roomMessages,
|
|
34
|
+
loading,
|
|
35
|
+
currentRoom: currentRoom?._id === targetRoomId ? currentRoom : undefined,
|
|
36
|
+
sendMessage: handleSend,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useChatContext } from '../context/ChatContext';
|
|
3
|
+
export function usePresence() {
|
|
4
|
+
const { users, refreshUsers } = useChatContext();
|
|
5
|
+
const handleRefreshUsers = useCallback(async () => {
|
|
6
|
+
await refreshUsers();
|
|
7
|
+
}, [refreshUsers]);
|
|
8
|
+
return {
|
|
9
|
+
users,
|
|
10
|
+
refreshUsers: handleRefreshUsers,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Room } from '@enfin/chat-shared';
|
|
2
|
+
export declare function useRooms(): {
|
|
3
|
+
rooms: Room[];
|
|
4
|
+
currentRoom: Room;
|
|
5
|
+
loading: boolean;
|
|
6
|
+
connected: boolean;
|
|
7
|
+
createRoom: (name: string, type: "direct" | "group", members: string[]) => Promise<Room>;
|
|
8
|
+
selectRoom: (roomId: string) => void;
|
|
9
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useChatContext } from '../context/ChatContext';
|
|
3
|
+
export function useRooms() {
|
|
4
|
+
const { rooms, currentRoom, loading, connected, createRoom, selectRoom, } = useChatContext();
|
|
5
|
+
const handleCreateRoom = useCallback(async (name, type, members) => {
|
|
6
|
+
return createRoom(name, type, members);
|
|
7
|
+
}, [createRoom]);
|
|
8
|
+
const handleSelectRoom = useCallback((roomId) => {
|
|
9
|
+
selectRoom(roomId);
|
|
10
|
+
}, [selectRoom]);
|
|
11
|
+
return {
|
|
12
|
+
rooms,
|
|
13
|
+
currentRoom,
|
|
14
|
+
loading,
|
|
15
|
+
connected,
|
|
16
|
+
createRoom: handleCreateRoom,
|
|
17
|
+
selectRoom: handleSelectRoom,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function useSocket(): {
|
|
2
|
+
emit: <T = any>(event: string, data?: T) => Promise<unknown>;
|
|
3
|
+
on: (event: string, callback: (data: any) => void) => () => void;
|
|
4
|
+
off: (event: string, callback?: (data: any) => void) => void;
|
|
5
|
+
onAny: (callback: (event: string, ...args: any[]) => void) => () => void;
|
|
6
|
+
connected: any;
|
|
7
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { useChatContext } from '../context/ChatContext';
|
|
3
|
+
export function useSocket() {
|
|
4
|
+
const { socket } = useChatContext();
|
|
5
|
+
const [socketState, setSocketState] = useState(socket);
|
|
6
|
+
const cleanupRefs = useRef(new Map());
|
|
7
|
+
const callbackIdRef = useRef(0);
|
|
8
|
+
// Keep the live socket ref in sync
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
setSocketState(socket);
|
|
11
|
+
return () => {
|
|
12
|
+
// Cleanup any listeners when socket changes
|
|
13
|
+
cleanupRefs.current.forEach(cleanup => cleanup());
|
|
14
|
+
cleanupRefs.current.clear();
|
|
15
|
+
};
|
|
16
|
+
}, [socket]);
|
|
17
|
+
const emit = useCallback((event, data) => {
|
|
18
|
+
if (!socketState?.connected) {
|
|
19
|
+
console.warn(`[useSocket] Cannot emit '${event}' — socket not connected`);
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
socketState.emit(event, data, (err, ...args) => {
|
|
24
|
+
if (err)
|
|
25
|
+
reject(err);
|
|
26
|
+
else
|
|
27
|
+
resolve(args[0]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}, [socketState]);
|
|
31
|
+
const on = useCallback((event, callback) => {
|
|
32
|
+
if (!socketState?.connected) {
|
|
33
|
+
console.warn(`[useSocket] Cannot listen on '${event}' — socket not connected`);
|
|
34
|
+
return () => { };
|
|
35
|
+
}
|
|
36
|
+
// Generate a unique ID for this listener
|
|
37
|
+
const listenerId = `${event}-${callbackIdRef.current++}`;
|
|
38
|
+
const wrappedCallback = (data) => {
|
|
39
|
+
callback(data);
|
|
40
|
+
};
|
|
41
|
+
// Add the listener
|
|
42
|
+
socketState.on(event, wrappedCallback);
|
|
43
|
+
// Store cleanup function
|
|
44
|
+
const cleanup = () => {
|
|
45
|
+
socketState.off(event, wrappedCallback);
|
|
46
|
+
cleanupRefs.current.delete(listenerId);
|
|
47
|
+
};
|
|
48
|
+
cleanupRefs.current.set(listenerId, cleanup);
|
|
49
|
+
// Return cleanup function
|
|
50
|
+
return cleanup;
|
|
51
|
+
}, [socketState]);
|
|
52
|
+
const off = useCallback((event, callback) => {
|
|
53
|
+
if (!socketState?.connected) {
|
|
54
|
+
console.warn(`[useSocket] Cannot unlisten from '${event}' — socket not connected`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (callback) {
|
|
58
|
+
// If callback is provided, remove it specifically
|
|
59
|
+
socketState.off(event, callback);
|
|
60
|
+
// Also remove any related cleanup refs
|
|
61
|
+
cleanupRefs.current.forEach((cleanup, key) => {
|
|
62
|
+
if (key.startsWith(`${event}-`))
|
|
63
|
+
cleanup();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Remove all listeners for this event
|
|
68
|
+
socketState.removeAllListeners(event);
|
|
69
|
+
cleanupRefs.current.forEach((cleanup, key) => {
|
|
70
|
+
if (key.startsWith(`${event}-`))
|
|
71
|
+
cleanup();
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}, [socketState]);
|
|
75
|
+
// Helper for listening to all socket events via window events
|
|
76
|
+
const onAny = useCallback((callback) => {
|
|
77
|
+
const handler = (e) => {
|
|
78
|
+
callback(e.detail.event, e.detail.data);
|
|
79
|
+
};
|
|
80
|
+
window.addEventListener('chat:socket-event', handler);
|
|
81
|
+
return () => {
|
|
82
|
+
window.removeEventListener('chat:socket-event', handler);
|
|
83
|
+
};
|
|
84
|
+
}, []);
|
|
85
|
+
return {
|
|
86
|
+
emit,
|
|
87
|
+
on,
|
|
88
|
+
off,
|
|
89
|
+
onAny,
|
|
90
|
+
connected: socketState?.connected || false,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from '@enfin/chat-shared';
|
|
2
|
+
export { ChatProvider, useChatContext } from './context/ChatContext';
|
|
3
|
+
export type { ChatUser } from './context/ChatContext';
|
|
4
|
+
export { useChat, useRooms, useMessages, useCall, useSocket, usePresence } from './hooks';
|
|
5
|
+
import { Chat } from './components';
|
|
6
|
+
export { Chat };
|
|
7
|
+
export type { ChatProps } from './components';
|
|
8
|
+
export default Chat;
|
|
9
|
+
export * from './utils';
|
|
10
|
+
export type { Message, Room, PresenceStatus, AudioCall, ChatConfig, } from '@enfin/chat-shared';
|
|
11
|
+
export declare const SDK_VERSION = "1.0.0";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// @enfin/chat - React Frontend SDK
|
|
2
|
+
// ============================================
|
|
3
|
+
export * from '@enfin/chat-shared';
|
|
4
|
+
// Context & Provider
|
|
5
|
+
export { ChatProvider, useChatContext } from './context/ChatContext';
|
|
6
|
+
// Hooks
|
|
7
|
+
export { useChat, useRooms, useMessages, useCall, useSocket, usePresence } from './hooks';
|
|
8
|
+
// Default UI Components
|
|
9
|
+
import { Chat } from './components';
|
|
10
|
+
export { Chat };
|
|
11
|
+
export default Chat;
|
|
12
|
+
// Utilities (including test utilities)
|
|
13
|
+
export * from './utils';
|
|
14
|
+
// SDK Version
|
|
15
|
+
export const SDK_VERSION = '1.0.0';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
2
|
+
import defaultRingtoneUrl from '../assets/music/i_phone_message.mp3';
|
|
3
|
+
let audioEl = null;
|
|
4
|
+
let currentUrl = null;
|
|
5
|
+
function getAudio(url) {
|
|
6
|
+
const target = url || defaultRingtoneUrl;
|
|
7
|
+
if (!audioEl || currentUrl !== target) {
|
|
8
|
+
if (audioEl) {
|
|
9
|
+
audioEl.pause();
|
|
10
|
+
audioEl.src = '';
|
|
11
|
+
}
|
|
12
|
+
audioEl = new Audio(target);
|
|
13
|
+
audioEl.loop = true;
|
|
14
|
+
audioEl.preload = 'auto';
|
|
15
|
+
currentUrl = target;
|
|
16
|
+
}
|
|
17
|
+
return audioEl;
|
|
18
|
+
}
|
|
19
|
+
export function startRingtone(url) {
|
|
20
|
+
try {
|
|
21
|
+
const audio = getAudio(url);
|
|
22
|
+
audio.currentTime = 0;
|
|
23
|
+
const playPromise = audio.play();
|
|
24
|
+
if (playPromise && typeof playPromise.catch === 'function') {
|
|
25
|
+
playPromise.catch((err) => {
|
|
26
|
+
console.log('[RINGTONE] play() rejected:', err?.message || err);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.log('[RINGTONE] start failed:', err?.message || err);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function stopRingtone() {
|
|
35
|
+
if (!audioEl)
|
|
36
|
+
return;
|
|
37
|
+
audioEl.pause();
|
|
38
|
+
audioEl.currentTime = 0;
|
|
39
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Socket } from 'socket.io-client';
|
|
2
|
+
/**
|
|
3
|
+
* Test utilities for developers to create test users and simulate interactions
|
|
4
|
+
*
|
|
5
|
+
* This utility allows developers to:
|
|
6
|
+
* 1. Create multiple test users that can chat with each other
|
|
7
|
+
* 2. Send messages between test users
|
|
8
|
+
* 3. Make audio calls between test users
|
|
9
|
+
* 4. Test custom socket events
|
|
10
|
+
*/
|
|
11
|
+
export interface TestUser {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
socket: Socket;
|
|
15
|
+
rooms: string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a test user with automatic socket connection
|
|
19
|
+
*
|
|
20
|
+
* @param serverUrl - Chat server URL
|
|
21
|
+
* @param userId - User ID (optional, generated if not provided)
|
|
22
|
+
* @param userName - User name
|
|
23
|
+
* @returns Test user object with socket connection
|
|
24
|
+
*/
|
|
25
|
+
export declare function createTestUser(serverUrl: string, userName: string, userId?: string): TestUser;
|
|
26
|
+
/**
|
|
27
|
+
* Create multiple test users for testing
|
|
28
|
+
*
|
|
29
|
+
* @param serverUrl - Chat server URL
|
|
30
|
+
* @param userNames - Array of user names
|
|
31
|
+
* @returns Array of test users
|
|
32
|
+
*/
|
|
33
|
+
export declare function createTestUsers(serverUrl: string, userNames: string[]): TestUser[];
|
|
34
|
+
/**
|
|
35
|
+
* Create a test room between users
|
|
36
|
+
*
|
|
37
|
+
* @param users - Users to add to the room
|
|
38
|
+
* @param roomName - Room name
|
|
39
|
+
* @param isDirect - Whether this is a direct chat
|
|
40
|
+
* @returns Promise resolving to the created room
|
|
41
|
+
*/
|
|
42
|
+
export declare function createTestRoom(users: TestUser[], roomName: string, isDirect?: boolean): Promise<any>;
|
|
43
|
+
/**
|
|
44
|
+
* Send a message from one user to another
|
|
45
|
+
*
|
|
46
|
+
* @param from - Sending user
|
|
47
|
+
* @param roomId - Room ID
|
|
48
|
+
* @param content - Message content
|
|
49
|
+
* @returns Promise resolving when message is sent
|
|
50
|
+
*/
|
|
51
|
+
export declare function sendMessage(from: TestUser, roomId: string, content: string): Promise<any>;
|
|
52
|
+
/**
|
|
53
|
+
* Start an audio call
|
|
54
|
+
*
|
|
55
|
+
* @param from - Initiating user
|
|
56
|
+
* @param to - Target user
|
|
57
|
+
* @param roomId - Room ID
|
|
58
|
+
* @returns Promise resolving when call is initiated
|
|
59
|
+
*/
|
|
60
|
+
export declare function startCall(from: TestUser, to: TestUser, roomId: string): Promise<any>;
|
|
61
|
+
/**
|
|
62
|
+
* Accept an incoming call
|
|
63
|
+
*
|
|
64
|
+
* @param to - User accepting call
|
|
65
|
+
* @param callId - Call ID
|
|
66
|
+
* @returns Promise resolving when call is accepted
|
|
67
|
+
*/
|
|
68
|
+
export declare function acceptCall(to: TestUser, callId: string): Promise<any>;
|
|
69
|
+
/**
|
|
70
|
+
* End a call
|
|
71
|
+
*
|
|
72
|
+
* @param by - User ending call
|
|
73
|
+
* @param callId - Call ID
|
|
74
|
+
* @returns Promise resolving when call is ended
|
|
75
|
+
*/
|
|
76
|
+
export declare function endCall(by: TestUser, callId: string): Promise<any>;
|
|
77
|
+
/**
|
|
78
|
+
* Listen for socket events (useful for testing)
|
|
79
|
+
*
|
|
80
|
+
* @param socket - Socket to listen on
|
|
81
|
+
* @param event - Event name
|
|
82
|
+
* @param callback - Callback function
|
|
83
|
+
* @returns Cleanup function to remove listener
|
|
84
|
+
*/
|
|
85
|
+
export declare function onSocketEvent(socket: Socket, event: string, callback: (data: any) => void): () => void;
|
|
86
|
+
/**
|
|
87
|
+
* Listen to custom socket events (exposed by ChatProvider)
|
|
88
|
+
*
|
|
89
|
+
* @param eventName - Event name to listen to
|
|
90
|
+
* @param callback - Callback function
|
|
91
|
+
* @returns Cleanup function
|
|
92
|
+
*/
|
|
93
|
+
export declare function onCustomSocketEvent(eventName: string, callback: (data: any) => void): () => void;
|
|
94
|
+
/**
|
|
95
|
+
* Emit custom event through the socket
|
|
96
|
+
*
|
|
97
|
+
* @param user - User to emit from
|
|
98
|
+
* @param event - Event name
|
|
99
|
+
* @param data - Event data
|
|
100
|
+
* @returns Promise resolving when event is sent
|
|
101
|
+
*/
|
|
102
|
+
export declare function emitCustomEvent(user: TestUser, event: string, data?: any): Promise<any>;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import io from 'socket.io-client';
|
|
2
|
+
/**
|
|
3
|
+
* Create a test user with automatic socket connection
|
|
4
|
+
*
|
|
5
|
+
* @param serverUrl - Chat server URL
|
|
6
|
+
* @param userId - User ID (optional, generated if not provided)
|
|
7
|
+
* @param userName - User name
|
|
8
|
+
* @returns Test user object with socket connection
|
|
9
|
+
*/
|
|
10
|
+
export function createTestUser(serverUrl, userName, userId) {
|
|
11
|
+
const id = userId || `user_${Math.random().toString(36).substr(2, 9)}`;
|
|
12
|
+
const socket = io(`${serverUrl}/chat`, {
|
|
13
|
+
auth: { userId: id, userName, tenantId: id },
|
|
14
|
+
transports: ['websocket'],
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
id,
|
|
18
|
+
name: userName,
|
|
19
|
+
socket,
|
|
20
|
+
rooms: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Create multiple test users for testing
|
|
25
|
+
*
|
|
26
|
+
* @param serverUrl - Chat server URL
|
|
27
|
+
* @param userNames - Array of user names
|
|
28
|
+
* @returns Array of test users
|
|
29
|
+
*/
|
|
30
|
+
export function createTestUsers(serverUrl, userNames) {
|
|
31
|
+
return userNames.map((name, index) => {
|
|
32
|
+
const userId = `test_user_${String(index + 1).padStart(2, '0')}`;
|
|
33
|
+
return createTestUser(serverUrl, name, userId);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a test room between users
|
|
38
|
+
*
|
|
39
|
+
* @param users - Users to add to the room
|
|
40
|
+
* @param roomName - Room name
|
|
41
|
+
* @param isDirect - Whether this is a direct chat
|
|
42
|
+
* @returns Promise resolving to the created room
|
|
43
|
+
*/
|
|
44
|
+
export async function createTestRoom(users, roomName, isDirect = false) {
|
|
45
|
+
const roomId = `room_${Date.now()}`;
|
|
46
|
+
if (isDirect && users.length !== 2) {
|
|
47
|
+
throw new Error('Direct rooms require exactly 2 users');
|
|
48
|
+
}
|
|
49
|
+
// Create room (first user creates it)
|
|
50
|
+
await users[0].socket.emitWithAck('room:create', {
|
|
51
|
+
name: roomName,
|
|
52
|
+
type: isDirect ? 'direct' : 'group',
|
|
53
|
+
members: users.map(u => u.id),
|
|
54
|
+
});
|
|
55
|
+
// Wait a bit for room to propagate
|
|
56
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
57
|
+
// Join all users to the room
|
|
58
|
+
for (const user of users) {
|
|
59
|
+
await user.socket.emitWithAck('join', { roomId: roomId });
|
|
60
|
+
user.rooms.push(roomId);
|
|
61
|
+
}
|
|
62
|
+
return { id: roomId, name: roomName, type: isDirect ? 'direct' : 'group' };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Send a message from one user to another
|
|
66
|
+
*
|
|
67
|
+
* @param from - Sending user
|
|
68
|
+
* @param roomId - Room ID
|
|
69
|
+
* @param content - Message content
|
|
70
|
+
* @returns Promise resolving when message is sent
|
|
71
|
+
*/
|
|
72
|
+
export async function sendMessage(from, roomId, content) {
|
|
73
|
+
return from.socket.emitWithAck('message:send', {
|
|
74
|
+
roomId,
|
|
75
|
+
content,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Start an audio call
|
|
80
|
+
*
|
|
81
|
+
* @param from - Initiating user
|
|
82
|
+
* @param to - Target user
|
|
83
|
+
* @param roomId - Room ID
|
|
84
|
+
* @returns Promise resolving when call is initiated
|
|
85
|
+
*/
|
|
86
|
+
export async function startCall(from, to, roomId) {
|
|
87
|
+
return from.socket.emitWithAck('call:initiate', {
|
|
88
|
+
roomId,
|
|
89
|
+
participantId: to.id,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Accept an incoming call
|
|
94
|
+
*
|
|
95
|
+
* @param to - User accepting call
|
|
96
|
+
* @param callId - Call ID
|
|
97
|
+
* @returns Promise resolving when call is accepted
|
|
98
|
+
*/
|
|
99
|
+
export async function acceptCall(to, callId) {
|
|
100
|
+
return to.socket.emitWithAck('call:accept', { callId });
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* End a call
|
|
104
|
+
*
|
|
105
|
+
* @param by - User ending call
|
|
106
|
+
* @param callId - Call ID
|
|
107
|
+
* @returns Promise resolving when call is ended
|
|
108
|
+
*/
|
|
109
|
+
export async function endCall(by, callId) {
|
|
110
|
+
return by.socket.emitWithAck('call:end', { callId });
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Listen for socket events (useful for testing)
|
|
114
|
+
*
|
|
115
|
+
* @param socket - Socket to listen on
|
|
116
|
+
* @param event - Event name
|
|
117
|
+
* @param callback - Callback function
|
|
118
|
+
* @returns Cleanup function to remove listener
|
|
119
|
+
*/
|
|
120
|
+
export function onSocketEvent(socket, event, callback) {
|
|
121
|
+
socket.on(event, callback);
|
|
122
|
+
return () => socket.off(event, callback);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Listen to custom socket events (exposed by ChatProvider)
|
|
126
|
+
*
|
|
127
|
+
* @param eventName - Event name to listen to
|
|
128
|
+
* @param callback - Callback function
|
|
129
|
+
* @returns Cleanup function
|
|
130
|
+
*/
|
|
131
|
+
export function onCustomSocketEvent(eventName, callback) {
|
|
132
|
+
const handler = (event) => {
|
|
133
|
+
if (event.detail.event === eventName) {
|
|
134
|
+
callback(event.detail.data);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
window.addEventListener('chat:socket-event', handler);
|
|
138
|
+
return () => window.removeEventListener('chat:socket-event', handler);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Emit custom event through the socket
|
|
142
|
+
*
|
|
143
|
+
* @param user - User to emit from
|
|
144
|
+
* @param event - Event name
|
|
145
|
+
* @param data - Event data
|
|
146
|
+
* @returns Promise resolving when event is sent
|
|
147
|
+
*/
|
|
148
|
+
export function emitCustomEvent(user, event, data) {
|
|
149
|
+
if (user.socket.connected) {
|
|
150
|
+
return user.socket.emitWithAck(event, data);
|
|
151
|
+
}
|
|
152
|
+
return Promise.reject('Socket not connected');
|
|
153
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare function useCall(): {
|
|
2
|
+
activeCall: import("@enfin/chat-shared").AudioCall;
|
|
3
|
+
callState: import("../call/WebRTCManager").WebRTCManagerState;
|
|
4
|
+
callError: string;
|
|
5
|
+
remoteStream: MediaStream;
|
|
6
|
+
busyBanner: string;
|
|
7
|
+
clearBusyBanner: () => void;
|
|
8
|
+
startCall: (roomId: string, participantId: string) => Promise<void>;
|
|
9
|
+
endCall: () => Promise<void>;
|
|
10
|
+
acceptCall: () => Promise<void>;
|
|
11
|
+
rejectCall: () => Promise<void>;
|
|
12
|
+
mute: () => void;
|
|
13
|
+
unmute: () => void;
|
|
14
|
+
isMuted: boolean;
|
|
15
|
+
};
|