e2ee-chat
Version:
A simple realtime chat SDK for web and mobile apps using socket.io with support for end-to-end encryption and multi-tenant backend integration.
238 lines (211 loc) • 6.76 kB
JavaScript
const { useEffect, useState, useRef } = require("react");
const {
connectSocket,
sendMessage,
requestTakeover,
disconnectSocket,
markMessagesAsRead,
getUnreadCounts,
} = require("./socket");
const { encryptMessage, decryptMessage } = require("./encryption");
const { fetchHistory } = require("./api");
function useChat({
serverUrl,
roomId,
userId,
userType,
secretKey,
apiKey,
receiverId,
onMessage,
}) {
const [messages, setMessages] = useState([]);
const [joined, setJoined] = useState(false);
const [activeHandler, setActiveHandler] = useState(null);
const [unreadCount, setUnreadCount] = useState(0);
const prevSessionRef = useRef({ roomId: null, userId: null });
// // Function to fetch unread counts
// const fetchUnreadCounts = async () => {
// try {
// const counts = await getUnreadCounts(serverUrl, userId, apiKey);
// setUnreadCount(counts[roomId] || 0);
// } catch (error) {
// console.error("Failed to fetch unread counts:", error);
// }
// };
// // In the SDK's useChat.js
// const fetchUnreadCounts = async () => {
// try {
// const counts = await getUnreadCounts(serverUrl, userId, apiKey);
// // Set the current room's unread count
// setUnreadCount(counts[roomId] || 0);
// // Return the entire counts object for the component to use
// return counts || {};
// } catch (error) {
// console.error("Failed to fetch unread counts:", error);
// return {};
// }
// };
// In useChat.js
const fetchUnreadCounts = async () => {
try {
// Add validation for required parameters
if (!serverUrl || !userId || !apiKey) {
console.error("SDK: Missing required parameters in useChat:", {
serverUrl,
userId,
apiKey,
});
return {};
}
console.log("SDK: Fetching unread counts with params:", {
serverUrl,
userId,
apiKey,
});
const counts = await getUnreadCounts(serverUrl, userId, apiKey);
// Set the current room's unread count
if (roomId) {
setUnreadCount(counts[roomId] || 0);
}
return counts || {};
} catch (error) {
console.error("SDK: Failed to fetch unread counts:", error);
return {};
}
};
useEffect(() => {
const isFirstLoad = prevSessionRef.current.roomId === null;
const hasChanged =
prevSessionRef.current.roomId !== roomId ||
prevSessionRef.current.userId !== userId;
const shouldConnect =
roomId && userId && secretKey && (isFirstLoad || hasChanged);
if (!shouldConnect) return;
console.log("Connecting to chat with:", {
serverUrl,
roomId,
userId,
userType,
hasChanged,
isFirstLoad,
});
prevSessionRef.current = { roomId, userId };
// Reset state before connecting
setMessages([]);
setJoined(false);
setActiveHandler(null);
setUnreadCount(0);
// Disconnect any existing socket
disconnectSocket();
// Small delay to ensure clean state
setTimeout(() => {
connectSocket({
serverUrl,
roomId,
userId,
userType,
apiKey,
onJoin: async ({ activeHandler }) => {
console.log("Socket joined successfully:", { activeHandler });
setJoined(true);
setActiveHandler(activeHandler);
try {
const cleanUrl = serverUrl.split("?")[0];
console.log("Fetching history from:", cleanUrl);
const history = await fetchHistory(cleanUrl, roomId, apiKey);
const messageArray = Array.isArray(history) ? history : [];
const decrypted = messageArray.map((msg) => {
try {
return {
...msg,
decryptedText: decryptMessage(msg.encryptedText, secretKey),
};
} catch (decryptError) {
console.error(
"Failed to decrypt message in history:",
decryptError
);
console.error("Message:", msg);
return {
...msg,
decryptedText: "[Decryption failed]",
decryptionError: true,
};
}
});
setMessages(decrypted);
// Mark messages as read when joining
markMessagesAsRead({ roomId, userId, apiKey });
// Fetch initial unread count
fetchUnreadCounts();
} catch (error) {
console.error("Failed to fetch or decrypt chat history:", error);
setMessages([]);
}
},
onMessage: (msg) => {
try {
const decryptedText = decryptMessage(msg.encryptedText, secretKey);
const messageWithDecryption = { ...msg, decryptedText };
setMessages((prev) => [...prev, messageWithDecryption]);
// Call the external onMessage callback if provided
if (onMessage) {
onMessage(messageWithDecryption);
}
// Increment unread count if message is from someone else
if (msg.senderId !== userId) {
setUnreadCount((prev) => prev + 1);
}
} catch (error) {
console.error("Failed to decrypt incoming message:", error);
console.error("Message data:", msg);
// Still add the message but with an error indicator
const messageWithError = {
...msg,
decryptedText: "[Decryption failed]",
decryptionError: true,
};
setMessages((prev) => [...prev, messageWithError]);
if (onMessage) {
onMessage(messageWithError);
}
}
},
onHandlerChanged: ({ newHandlerId }) => {
setActiveHandler(newHandlerId);
},
onMessagesRead: () => {
setUnreadCount(0);
},
});
}, 100);
return () => {
disconnectSocket();
};
}, [serverUrl, roomId, userId, userType, secretKey, apiKey]);
const send = (text, receiverId) => {
const encrypted = encryptMessage(text, secretKey);
sendMessage({
roomId,
senderId: userId,
receiverId,
encryptedText: encrypted,
apiKey,
});
};
const takeover = () => {
requestTakeover({ roomId, newHandlerId: userId, apiKey });
};
return {
messages,
sendMessage: send,
takeover,
activeHandler,
joined,
unreadCount,
markMessagesAsRead: () => markMessagesAsRead({ roomId, userId, apiKey }),
fetchUnreadCounts,
};
}
module.exports = useChat;