diff --git a/app/src/components/AppLayout.tsx b/app/src/components/AppLayout.tsx
index 833f86d6..0adac60d 100644
--- a/app/src/components/AppLayout.tsx
+++ b/app/src/components/AppLayout.tsx
@@ -4,6 +4,10 @@ import Main from "./Main";
import SideBar from "./side-bar/SideBar";
import { DESKTOP_VIEW, MOBILE_VIEW } from "@constants";
import { useRightSideBarContext } from "@features/groups/contexts/RightSideBarProvider";
+import { useEffect, useState } from "react";
+import { useSocket } from "@hooks/useSocket";
+import { callStatusEmitter } from "@features/calls/context/callStatusEmitter";
+import CallLayout from "@features/calls/CallLayout";
const StyledApp = styled.div<{
$isChatOpen: boolean;
@@ -40,7 +44,17 @@ function AppLayout() {
const { chatId } = useParams();
const isChatOpen = !!chatId;
const { isRightSideBarOpen } = useRightSideBarContext();
+ const [isCollapsed, setIsCollapsed] = useState(false);
+ const [callStatus, setCallStatus] = useState<
+ "inactive" | "active" | "calling" | "incoming" | "ended"
+ >("inactive");
+ useEffect(() => {
+ const handler = (status: typeof callStatus) => setCallStatus(status);
+ callStatusEmitter.on("update", handler);
+
+ return () => callStatusEmitter.off("update", handler);
+ }, []);
return (
+ {callStatus != "inactive" && (
+
+ )}
{isRightSideBarOpen && }
diff --git a/app/src/components/side-bar/groups/AddMoreMembers.tsx b/app/src/components/side-bar/groups/AddMoreMembers.tsx
index 102cf7f2..dbd24ca5 100644
--- a/app/src/components/side-bar/groups/AddMoreMembers.tsx
+++ b/app/src/components/side-bar/groups/AddMoreMembers.tsx
@@ -30,7 +30,6 @@ function AddMoreMembers() {
const dispatch = useDispatch();
if (isPending) return;
- console.log(chatType);
function handleClick() {
addGroupMembers({
diff --git a/app/src/constants.ts b/app/src/constants.ts
index a6a2f2c3..66f674b9 100644
--- a/app/src/constants.ts
+++ b/app/src/constants.ts
@@ -5,6 +5,9 @@ const {
VITE_PORT: PORT,
VITE_ENV: ENV,
VITE_BACKEND_STORGAE: STATIC_MEDIA_URL,
+ TURN_URL,
+ VITE_TURN_USERNAME: TURN_USERNAME,
+ VITE_TURN_PASSWORD: TURN_PASSWORD
} = import.meta.env;
const MOBILE_VIEW = "(max-width: 800px)";
@@ -21,4 +24,7 @@ export {
MOBILE_VIEW,
DESKTOP_VIEW,
MAX_STORY_SIZE,
+ TURN_URL,
+ TURN_USERNAME,
+ TURN_PASSWORD
};
diff --git a/app/src/features/authentication/signup/hooks/useSignup.ts b/app/src/features/authentication/signup/hooks/useSignup.ts
index 66ad2bf3..e594a0b6 100644
--- a/app/src/features/authentication/signup/hooks/useSignup.ts
+++ b/app/src/features/authentication/signup/hooks/useSignup.ts
@@ -6,19 +6,15 @@ export function useSignup() {
mutate: signup,
isSuccess,
isPending,
- isError,
+ isError
} = useMutation({
- mutationFn: Signup,
-
- onSuccess: (email) => {
- console.log(email);
- },
+ mutationFn: Signup
});
return {
signup,
isPending,
isSuccess,
- isError,
+ isError
};
}
diff --git a/app/src/features/calls/CallLayout.tsx b/app/src/features/calls/CallLayout.tsx
index c2c4a2ef..99470a8b 100644
--- a/app/src/features/calls/CallLayout.tsx
+++ b/app/src/features/calls/CallLayout.tsx
@@ -7,6 +7,7 @@ import { useCallContext } from "./hooks/useCallContext";
import { getChatByID } from "@features/chats/utils/helpers";
import { useAppSelector } from "@hooks/useGlobalState";
import { useSocket } from "@hooks/useSocket";
+import { EnableSpeaker } from "./SpeakerEnable";
const ModalContainer = styled.div`
position: fixed;
@@ -193,21 +194,19 @@ const ActiveHeaderOpen = styled.div`
type PropsType = {
isCollapsed: boolean;
setIsCollapsed: (arg0: boolean) => void;
- chatId: string | undefined;
callStatus: string | undefined;
};
export default function CallLayout({
isCollapsed,
setIsCollapsed,
- chatId,
callStatus
}: PropsType) {
- const { endCall } = useCallContext();
- const { acceptCall } = useSocket();
+ const { acceptCall, finishCall } = useSocket();
+ const { endCall, chatId } = useCallContext();
const chats = useAppSelector((state) => state.chats.chats);
const chat = getChatByID({
- chatID: chatId ?? "",
+ chatID: chatId.current ?? "",
chats: chats
});
@@ -249,16 +248,30 @@ export default function CallLayout({
accept
)}
-
- endCall()}
- $bgColor="var(--color-error)"
- $bgColorHover="var(--color-error-shade)"
- >
- {getIcon("EndCall")}
-
- end call
-
+
+ {callStatus === "incoming" ? (
+
+ endCall()}
+ $bgColor="var(--color-error)"
+ $bgColorHover="var(--color-error-shade)"
+ >
+ {getIcon("EndCall")}
+
+ end call
+
+ ) : (
+
+ finishCall()}
+ $bgColor="var(--color-error)"
+ $bgColorHover="var(--color-error-shade)"
+ >
+ {getIcon("EndCall")}
+
+ end call
+
+ )}
diff --git a/app/src/features/calls/SpeakerEnable.tsx b/app/src/features/calls/SpeakerEnable.tsx
new file mode 100644
index 00000000..16f32892
--- /dev/null
+++ b/app/src/features/calls/SpeakerEnable.tsx
@@ -0,0 +1,48 @@
+import React, { useEffect } from "react";
+import { useCallContext } from "./hooks/useCallContext";
+
+interface EnableSpeakerProps {
+ children?: React.ReactNode;
+}
+
+export const EnableSpeaker: React.FC = ({ children }) => {
+ const { clientId } = useCallContext();
+
+ useEffect(() => {
+ const playStreamsToSpeaker = () => {
+ if (!clientId?.current) {
+ console.warn("clientId is not available or has no current value.");
+ return;
+ }
+
+ clientId.current.forEach((clientData, clientIdKey) => {
+ if (!clientData || !clientData.connection) {
+ console.warn(`No connection found for client: ${clientIdKey}`);
+ return;
+ }
+
+ // Get all tracks from the remote connection
+ const receivers = clientData.connection.getReceivers();
+ const remoteStream = new MediaStream(
+ receivers.map((receiver) => receiver.track).filter(Boolean)
+ );
+
+ if (remoteStream && remoteStream.getTracks().length > 0) {
+ const audio = new Audio();
+ audio.srcObject = remoteStream;
+ audio.play().catch((err) => {
+ console.error(
+ `Failed to play audio for client: ${clientIdKey}`,
+ err
+ );
+ });
+ console.log(`Playing audio for client: ${clientIdKey}`);
+ }
+ });
+ };
+
+ playStreamsToSpeaker();
+ }, [clientId]);
+
+ return {children}
;
+};
diff --git a/app/src/features/calls/call.ts b/app/src/features/calls/call.ts
deleted file mode 100644
index c7d9f28f..00000000
--- a/app/src/features/calls/call.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-export let callState = "idle";
-const stunServers = {
- iceServers: [
- { urls: ["stun:stun.l.google.com:19302", "stun:stun.l.google.com:5349"] }
- ]
-};
-export async function getVoiceInput(): Promise {
- try {
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
- return stream;
- } catch (error) {
- console.error("Error accessing microphone:", error);
- return null;
- }
-}
-
-const RemoteStream: MediaStream = new MediaStream();
-export const connectToPeer = async (
- peerConnection: RTCPeerConnection | null,
- localStream: MediaStream | null
-) => {
- callState = "connecting";
- peerConnection = new RTCPeerConnection(stunServers);
- if (!localStream) localStream = await getVoiceInput();
- console.log(localStream);
- localStream?.getTracks().forEach((t) => {
- peerConnection.addTrack(t, localStream);
- });
-
- peerConnection.ontrack = (e) => {
- e.streams[0]?.getTracks().forEach((track) => RemoteStream.addTrack(track));
- };
-
- await new Promise((resolve) => {
- const timeout = setTimeout(() => {
- resolve(null);
- }, 10000);
-
- peerConnection.onicecandidate = (event) => {
- if (!event.candidate) {
- clearTimeout(timeout);
- resolve(null);
- }
- };
- });
- const offer = await peerConnection.createOffer();
- await peerConnection.setLocalDescription(offer);
- return JSON.stringify(offer);
-};
-export const createOffer = async (
- peerConnection: RTCPeerConnection | null,
- localStream: MediaStream | null
-) => {
- peerConnection = new RTCPeerConnection(stunServers);
- if (!localStream) localStream = await getVoiceInput();
- console.log(localStream);
- localStream?.getTracks().forEach((t) => {
- peerConnection.addTrack(t, localStream);
- });
-
- peerConnection.ontrack = (e) => {
- e.streams[0]?.getTracks().forEach((track) => RemoteStream.addTrack(track));
- };
-
- const offer = await peerConnection.createOffer();
- await peerConnection.setLocalDescription(offer);
-
- return JSON.stringify(offer);
-};
-export const handleIceCandidates = async (
- peerConnection: RTCPeerConnection,
- sendCandidate: (candidate: string) => void
-) => {
- return new Promise((resolve) => {
- const timeout = setTimeout(() => {
- resolve(null);
- }, 10000);
-
- peerConnection.onicecandidate = (event) => {
- if (!event.candidate) {
- clearTimeout(timeout);
- resolve(null);
- }
- sendCandidate(JSON.stringify(event.candidate));
- };
- });
-};
-export const createAnswer = async (offer: string) => {
- await connectToPeer();
- const offer_parsed = JSON.parse(offer);
- await peerConnection.setRemoteDescription(offer_parsed);
- const answer = await peerConnection.createAnswer();
- await peerConnection.setLocalDescription(answer);
- return JSON.stringify(answer);
-};
-export const startCall = async (answer: string) => {
- callState = "ongoing";
- const offer_parsed = JSON.parse(answer);
- await peerConnection.setRemoteDescription(offer_parsed);
-};
diff --git a/app/src/features/calls/context/CallProvider.tsx b/app/src/features/calls/context/CallProvider.tsx
index c35b6dd6..fb4d6320 100644
--- a/app/src/features/calls/context/CallProvider.tsx
+++ b/app/src/features/calls/context/CallProvider.tsx
@@ -1,34 +1,92 @@
import React, { ReactNode, useCallback, useRef } from "react";
import { CallContext } from "./CallContext";
import { useAppSelector } from "@hooks/useGlobalState";
-import { createOffer, getVoiceInput } from "../call";
import { callStatusEmitter } from "./callStatusEmitter";
import { CallStatus } from "types/calls";
-
+import { TURN_USERNAME, TURN_PASSWORD } from "@constants";
+const Servers = {
+ iceServers: [
+ { urls: ["stun:stun.l.google.com:19302", "stun:stun.l.google.com:5349"] },
+ {
+ urls: ["stun:stun1.l.google.com:19302", "stun:stun2.l.google.com:19302"]
+ },
+ {
+ urls: "turn:global.relay.metered.ca:80",
+ username: TURN_USERNAME,
+ credential: TURN_PASSWORD
+ },
+ {
+ urls: "turn:global.relay.metered.ca:80?transport=tcp",
+ username: TURN_USERNAME,
+ credential: TURN_PASSWORD
+ },
+ {
+ urls: "turn:global.relay.metered.ca:443",
+ username: TURN_USERNAME,
+ credential: TURN_PASSWORD
+ },
+ {
+ urls: "turns:global.relay.metered.ca:443?transport=tcp",
+ username: TURN_USERNAME,
+ credential: TURN_PASSWORD
+ }
+ ]
+};
export const CallProvider: React.FC<{ children: ReactNode }> = ({
children
}) => {
const userId = useAppSelector((state) => state.user.userInfo.id);
-
- const callAccepted = useRef(null);
const callIdRef = useRef(null);
const senderIdRef = useRef(null);
const chatIdRef = useRef(null);
- const peerConnection = useRef(null);
const localStream = useRef(null);
- const offer = useRef(null);
- const answer = useRef(null);
+ const offer = useRef(null);
const callStatus = useRef("inactive");
+ const clientIdRef = useRef<
+ Map
+ >(new Map());
- const setCallStatus = (status: typeof callStatus.current) => {
+ const addClientId = useCallback(
+ (
+ clientId: string,
+ connection: RTCPeerConnection | null,
+ offerSent: boolean
+ ) => {
+ clientIdRef.current.set(clientId, { connection, offerSent });
+ },
+ []
+ );
+
+ const removeClientId = useCallback((clientId: string) => {
+ if (clientIdRef.current.has(clientId)) {
+ clientIdRef.current.delete(clientId);
+ }
+ }, []);
+
+ const clearClientIds = useCallback(() => {
+ clientIdRef.current.clear();
+ }, []);
+ const setChatId = useCallback((chatId: string) => {
+ chatIdRef.current = chatId;
+ }, []);
+ const hasClientId = useCallback(
+ (clientId: string, offerSent: boolean | null = null) => {
+ const clientData = clientIdRef.current.get(clientId);
+ return (
+ !!clientData &&
+ (offerSent === null || clientData.offerSent === offerSent)
+ );
+ },
+ []
+ );
+
+ const setCallStatus = useCallback((status: CallStatus) => {
callStatus.current = status;
- console.log(status);
callStatusEmitter.emit("update", status);
- };
+ }, []);
const joinCall = useCallback(
(newSenderId: string, newChatId: string, newCallId: string) => {
- console.log(callIdRef.current, newCallId);
if (!callIdRef.current) {
if (newSenderId === userId) {
setCallStatus("calling");
@@ -40,7 +98,7 @@ export const CallProvider: React.FC<{ children: ReactNode }> = ({
chatIdRef.current = newChatId;
}
},
- [userId]
+ [setCallStatus, userId]
);
const endCall = useCallback(() => {
@@ -48,52 +106,194 @@ export const CallProvider: React.FC<{ children: ReactNode }> = ({
callIdRef.current = null;
senderIdRef.current = null;
chatIdRef.current = null;
- callAccepted.current = null;
- peerConnection.current?.close();
- peerConnection.current = null;
+ if (localStream.current) {
+ localStream.current.getTracks().forEach((track) => {
+ track.stop();
+ });
+ }
+ clientIdRef.current.forEach((clientData, clientId) => {
+ if (clientData.connection) {
+ clientData.connection.close();
+ }
+
+ removeClientId(clientId);
+ });
+ const remoteAudioElements = document.querySelectorAll("audio");
+ remoteAudioElements.forEach((audio) => audio.remove());
+
localStream.current = null;
- }, []);
+ clearClientIds();
+ }, [clearClientIds, removeClientId, setCallStatus]);
const acceptCall = useCallback(() => {
- setCallStatus("active");
- callAccepted.current = callIdRef.current;
- }, []);
+ if (callIdRef.current && chatIdRef.current) {
+ if (
+ senderIdRef.current === userId &&
+ (!clientIdRef.current || clientIdRef.current.size === 0)
+ ) {
+ setCallStatus("calling");
+ } else {
+ setCallStatus("active");
+ }
+ }
+ }, [setCallStatus, userId]);
- const startPeerConnection = useCallback(async () => {
- localStream.current = await getVoiceInput();
- offer.current = await createOffer(
- peerConnection.current,
- localStream.current
- );
- }, []);
+ const startPeerConnection = useCallback(
+ async (clientId: string) => {
+ if (!clientId) return null;
+ if (!localStream.current) {
+ localStream.current = await navigator.mediaDevices.getUserMedia({
+ audio: true,
+ video: false
+ });
+ }
+
+ if (!localStream.current) {
+ alert("Failed to get voice input.");
+ endCall();
+ return null;
+ }
+ const peerConnection = new RTCPeerConnection(Servers);
+ localStream.current.getTracks().forEach((track) => {
+ peerConnection?.addTrack(track, localStream.current);
+ });
+ peerConnection.ontrack = (event) => {
+ if (event.track.kind === "audio") {
+ const remoteAudio = document.createElement("audio");
+ remoteAudio.srcObject = event.streams[0];
+ remoteAudio.autoplay = true;
+ remoteAudio.controls = true;
+ document.body.appendChild(remoteAudio);
+ }
+ };
+ const offer = await peerConnection.createOffer();
+ if (!hasClientId(clientId) || hasClientId(clientId, false)) {
+ await peerConnection.setLocalDescription(offer);
+ addClientId(clientId, peerConnection, true);
+ setCallStatus("active");
+ return offer;
+ }
+ return null;
+ },
+ [addClientId, endCall, hasClientId, setCallStatus]
+ );
+
+ const recieveICE = useCallback(
+ async (candidate: RTCIceCandidateInit, senderId: string) => {
+ if (hasClientId(senderId, true)) {
+ const clientData = clientIdRef.current.get(senderId);
+ if (clientData) {
+ if (!clientData.connection) return;
+ clientData.connection.addIceCandidate(candidate);
+ }
+ }
+ },
+ [hasClientId]
+ );
+
+ const recieveAnswer = useCallback(
+ (data: RTCSessionDescriptionInit, senderId: string) => {
+ if (hasClientId(senderId, true)) {
+ const clientData = clientIdRef.current.get(senderId);
+ if (clientData && clientData.connection) {
+ clientData.connection.setRemoteDescription(data);
+ setCallStatus("active");
+ }
+ }
+ },
+ [hasClientId, setCallStatus]
+ );
+ const createAnswer = useCallback(
+ async (data: RTCSessionDescriptionInit, senderId: string) => {
+ if (!senderId || !data) {
+ console.warn("Invalid senderId or data provided.");
+ return null;
+ }
- const recieveICE = useCallback(async (candidate: RTCIceCandidateInit) => {
- if (peerConnection.current) {
try {
- await peerConnection.current.addIceCandidate(candidate);
- console.log("ICE candidate added successfully.");
+ const peerConnection = new RTCPeerConnection(Servers);
+ if (!localStream.current) {
+ localStream.current = await navigator.mediaDevices.getUserMedia({
+ audio: true,
+ video: false
+ });
+ }
+
+ if (!localStream.current) {
+ alert("Failed to get voice input.");
+ endCall();
+ return null;
+ }
+ localStream.current.getTracks().forEach((track) => {
+ peerConnection.addTrack(track, localStream.current);
+ });
+ await peerConnection.setRemoteDescription(data);
+ const answer = await peerConnection.createAnswer();
+ await peerConnection.setLocalDescription(answer);
+ peerConnection.ontrack = (event) => {
+ if (event.track.kind === "audio") {
+ const remoteAudio = document.createElement("audio");
+ remoteAudio.srcObject = event.streams[0];
+ remoteAudio.autoplay = true;
+ remoteAudio.controls = true;
+ document.body.appendChild(remoteAudio);
+ }
+ };
+ peerConnection.oniceconnectionstatechange = () => {
+ const state = peerConnection.iceConnectionState;
+
+ switch (state) {
+ case "connected":
+ console.log("Peer connection established.");
+ break;
+ case "disconnected":
+ console.warn("Peer connection disconnected.");
+ break;
+ case "failed":
+ console.error("Peer connection failed. Restarting ICE?");
+ break;
+ case "closed":
+ console.log("Peer connection closed.");
+ break;
+ default:
+ console.log("ICE connection state:", state);
+ break;
+ }
+ };
+
+ addClientId(senderId, peerConnection, false);
+ return answer;
} catch (error) {
- console.error("Failed to add ICE candidate:", error);
+ console.error("Error creating answer:", error);
+ return null;
}
+ },
+ [addClientId, endCall]
+ );
+
+ const getPeerConnection = useCallback((clientId: string) => {
+ const clientData = clientIdRef.current.get(clientId);
+ if (clientData) {
+ return clientData.connection;
}
+ return null;
}, []);
- const recieveAnswer = useCallback(() => {}, []);
-
const contextValue = {
- peerConnection: peerConnection,
callId: callIdRef,
senderId: senderIdRef,
chatId: chatIdRef,
startPeerConnection,
- offer: offer,
- answer: answer,
+ offer,
+ clientId: clientIdRef,
+ setChatId,
acceptCall,
- callAccepted: callAccepted,
joinCall,
+ createAnswer,
endCall,
recieveICE,
- recieveAnswer
+ recieveAnswer,
+ getPeerConnection
};
return (
diff --git a/app/src/features/chats/ChatInput.tsx b/app/src/features/chats/ChatInput.tsx
index 831f6814..eb7d7192 100644
--- a/app/src/features/chats/ChatInput.tsx
+++ b/app/src/features/chats/ChatInput.tsx
@@ -96,7 +96,6 @@ function ChatInput() {
setError("");
}
- console.log(currChat?.messagingPermission);
const isGroupWithoutPostPermission =
currChat?.type === "group" &&
@@ -115,7 +114,7 @@ function ChatInput() {
return (
<>
{isFilePreviewOpen && file && (
-
+
)}
{isBlocked ? null : (
diff --git a/app/src/features/chats/Topbar.tsx b/app/src/features/chats/Topbar.tsx
index 7baebe61..f9c435aa 100644
--- a/app/src/features/chats/Topbar.tsx
+++ b/app/src/features/chats/Topbar.tsx
@@ -1,29 +1,22 @@
import { useCallback, useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import styled from "styled-components";
-
import Avatar from "@components/Avatar";
import Icon from "@components/Icon";
import SearchBar from "@features/search/components/SearchBar";
import PinnedMessages from "@features/pin-messages/components/PinnedMessages";
-import CallLayout from "@features/calls/CallLayout";
-
import { getIcon } from "@data/icons";
import { setChatIsBlocked } from "@state/messages/chats";
-
import { useSocket } from "@hooks/useSocket";
import { useAppDispatch, useAppSelector } from "@hooks/useGlobalState";
import { useChatMembers } from "./hooks/useChatMembers";
import { useBlock } from "@features/privacy-settings/hooks/useBlock";
import { useRightSideBarContext } from "@features/groups/contexts/RightSideBarProvider";
-
import { getElapsedTime } from "@utils/helpers";
import { getChatByID } from "./utils/helpers";
import { updateSideBarView } from "@state/side-bar/sideBar";
import { sideBarPages } from "types/sideBar";
-
import { resetActiveThread } from "@state/messages/channels";
-import { callStatusEmitter } from "@features/calls/context/callStatusEmitter";
const Container = styled.div<{ $hasMargin?: boolean }>`
position: absolute;
@@ -126,18 +119,10 @@ function Topbar() {
const dispatch = useAppDispatch();
const { activeThread } = useAppSelector((state) => state.channelsThreads);
const [isSearching, setIsSearching] = useState(false);
- const [isCollapsed, setIsCollapsed] = useState(false);
const { createVoiceCall } = useSocket();
const [callStatus, setCallStatus] = useState<
"inactive" | "active" | "calling" | "incoming" | "ended"
>("inactive");
-
- useEffect(() => {
- const handler = (status: typeof callStatus) => setCallStatus(status);
- callStatusEmitter.on("update", handler);
-
- return () => callStatusEmitter.off("update", handler);
- }, []);
const startCall = () => {
if (chatId && callStatus === "inactive") {
createVoiceCall({ chatId });
@@ -215,15 +200,7 @@ function Topbar() {
return (
<>
- {callStatus != "inactive" && (
-
- )}
-
+
{!activeThread && (
) => {
- console.log("file");
if (e.target.files) {
console.log("file", e.target.files[0]);
setFile(e.target.files[0]);
- event.target.value = "";
+ e.target.value = "";
}
setIsFilePreviewOpen(true);
diff --git a/app/src/features/chats/utils/helpers.ts b/app/src/features/chats/utils/helpers.ts
index e2dc5db7..1308bb23 100644
--- a/app/src/features/chats/utils/helpers.ts
+++ b/app/src/features/chats/utils/helpers.ts
@@ -151,6 +151,6 @@ export const decryptMessage = async ({
return new TextDecoder().decode(decryptedBuffer);
} catch (err: any) {
- return Error(`Decryption error: ${err.message}`);
+ console.error(`Decryption error: ${err.message}`);
}
};
diff --git a/app/src/main.tsx b/app/src/main.tsx
index 6496bcc5..81323678 100644
--- a/app/src/main.tsx
+++ b/app/src/main.tsx
@@ -1,4 +1,4 @@
-// import { StrictMode } from "react";
+import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
@@ -16,10 +16,10 @@ async function enableMocking() {
enableMocking().then(() => {
createRoot(document.getElementById("root")!).render(
- //
-
-
-
- //
+
+
+
+
+
);
});
diff --git a/app/src/sockets/SocketProvider.tsx b/app/src/sockets/SocketProvider.tsx
index f7fde1b4..219014bf 100644
--- a/app/src/sockets/SocketProvider.tsx
+++ b/app/src/sockets/SocketProvider.tsx
@@ -1,4 +1,4 @@
-import { useState, useEffect, ReactNode, useCallback } from "react";
+import { useEffect, ReactNode, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { Dispatch } from "redux";
@@ -61,17 +61,17 @@ interface AckCreateGroup {
}
function SocketProvider({ children }: SocketProviderProps) {
- const [isConnected, setIsConnected] = useState(false);
const {
callId,
joinCall,
recieveICE,
recieveAnswer,
- peerConnection,
+ setChatId,
+ getPeerConnection,
+ createAnswer,
startPeerConnection,
offer,
- acceptCall: setAcceptedCall,
- callAccepted
+ acceptCall: setAcceptedCall
} = useCallContext();
const navigate = useNavigate();
const dispatch = useDispatch();
@@ -84,82 +84,93 @@ function SocketProvider({ children }: SocketProviderProps) {
const { decrypt } = useEncryptDecrypt();
const { chat } = useChat();
- const handleIceCandidates = useCallback(async () => {
- return new Promise((resolve) => {
- const timeout = setTimeout(() => {
- resolve(null);
- }, 10000);
- if (peerConnection && isConnected && socket && callId) {
- peerConnection.onicecandidate = (event) => {
- if (!event.candidate) {
- clearTimeout(timeout);
- resolve(null);
- }
- socket.emit("SIGNAL-SERVER", {
- type: "ICE",
- voiceCallId: callId,
- data: event.candidate
- });
- };
- }
- });
- }, [peerConnection, isConnected, socket, callId]);
- const sendOffer = useCallback(() => {
- if (offer && isConnected && socket && callId) {
- socket.emit("SIGNAL-SERVER", {
- type: "OFFER",
- voiceCallId: callId,
- data: offer
+ const handleIceCandidates = useCallback(
+ async (clientId: string) => {
+ return new Promise((resolve) => {
+ const timeout = setTimeout(() => {
+ resolve(null);
+ }, 10000);
+ if (socket?.connected && socket && callId.current) {
+ const peerConnection = getPeerConnection(clientId);
+ if (!peerConnection) return;
+ peerConnection.onicecandidate = (event) => {
+ if (!event.candidate) {
+ clearTimeout(timeout);
+ resolve(null);
+ }
+ socket.emit("SIGNAL-SERVER", {
+ type: "ICE",
+ voiceCallId: callId.current,
+ data: event.candidate,
+ targetId: clientId
+ });
+ };
+ }
});
- }
- }, [offer, isConnected, socket, callId]);
+ },
+ [socket, callId, getPeerConnection]
+ );
+ const sendOffer = useCallback(
+ (clientId: string, offer: RTCSessionDescriptionInit) => {
+ if (
+ socket &&
+ offer &&
+ socket?.connected &&
+ socket &&
+ callId.current &&
+ clientId
+ ) {
+ socket.emit("SIGNAL-SERVER", {
+ type: "OFFER",
+ voiceCallId: callId.current,
+ data: offer,
+ targetId: clientId
+ });
+ }
+ },
+ [socket, callId]
+ );
const acceptCall = useCallback(() => {
- console.log(callId, callAccepted, isConnected, socket);
- if (isConnected && socket && callId.current && !callAccepted.current) {
- console.log("acceptCall", callId.current);
+ if (socket?.connected && socket && callId.current) {
setAcceptedCall();
socket.emit("JOIN-CALL", { voiceCallId: callId.current });
}
- }, [callAccepted, isConnected, socket, callId, setAcceptedCall]);
+ }, [socket, callId, setAcceptedCall]);
+ const finishCall = useCallback(() => {
+ if (socket?.connected && socket && callId.current) {
+ socket.emit("LEAVE", { voiceCallId: callId.current });
+ endCall();
+ }
+ }, [socket, callId, endCall]);
const sendAnswer = useCallback(
- (
- answer: string,
- callback?: (response: { success: boolean; error?: string }) => void
- ) => {
- if (isConnected && socket) {
- socket.emit(
- "SEND_ANSWER",
- { answer },
- (response: { success: boolean; error?: string }) => {
- if (callback) {
- callback(response);
- } else if (!response.success) {
- console.error("Failed to send answer:", response.error);
- } else {
- console.log("Answer sent successfully");
- }
- }
- );
- } else {
- console.warn("Cannot send answer: not connected to socket server");
+ (clientId: string, answer: RTCSessionDescriptionInit) => {
+ if (
+ socket &&
+ offer &&
+ socket?.connected &&
+ socket &&
+ callId.current &&
+ clientId
+ ) {
+ socket.emit("SIGNAL-SERVER", {
+ type: "ANSWER",
+ voiceCallId: callId.current,
+ data: answer,
+ targetId: clientId
+ });
}
},
- [isConnected, socket]
+ [socket, callId, offer]
);
useEffect(() => {
if (!socket) return;
- if (!socket.connected) socket.connect();
const onConnect = () => {
- setIsConnected(true);
- console.log("Socket connected");
-
socket.io.engine.on("close", (reason) => {
console.log("Socket connection closed:", reason);
});
};
const onReceiveMessage = (message: MessageInterface) => {
- console.log("Received message:", message);
if (!chat) {
console.warn("No chat context available for decryption");
return;
@@ -226,17 +237,6 @@ function SocketProvider({ children }: SocketProviderProps) {
socket.on("typing", (isTyping, message) =>
handleIsTyping(dispatch, isTyping, message.chatId)
);
-
- // socket.on("RECIEVE_OFFER", async (offer) => {
- // console.log(offer);
- // const answer = await createAnswer(offer);
- // sendAnswer(answer);
- // });
- // socket.on("RECEIVE_ANSWER", async (answer: string) => {
- // console.log(answer);
- // startCall(answer);
- // });
-
socket.on(
"DELETE_MESSAGE_SERVER",
({ chatId, id }: { chatId: string; id: string }) => {
@@ -282,22 +282,43 @@ function SocketProvider({ children }: SocketProviderProps) {
voiceCallId: string;
chatId: string;
}) => {
- console.log("call id", voiceCallId);
joinCall(snederId, chatId, voiceCallId);
- console.log(snederId, userId);
if (snederId === userId) acceptCall();
}
);
- socket.on("CLIENT-JOINED", () => {
- console.log("CLIENT-JOINED");
- startPeerConnection();
- sendOffer();
- handleIceCandidates();
+ socket.on("CLIENT-JOINED", async ({ clientId }) => {
+ const offer = await startPeerConnection(clientId);
+ try {
+ if (offer && clientId) {
+ sendOffer(clientId, offer);
+ handleIceCandidates(clientId);
+ } else throw new Error("Failed to send offer");
+ } catch {
+ console.error("Failed to send offer");
+ }
});
- socket.on("SIGNAL-SERVER", async ({ type, data }) => {
- console.log(typeof data);
- if (type === "ANSWER") recieveAnswer(data);
- if (type === "ICE") recieveICE(data);
+ socket.on(
+ "SIGNAL-CLIENT",
+ async ({ type, voiceCallId, data, senderId }) => {
+ if (voiceCallId == callId.current) {
+ if (type === "OFFER") {
+ const answer = await createAnswer(data, senderId);
+ if (answer) {
+ sendAnswer(senderId, answer);
+ handleIceCandidates(senderId);
+ } else {
+ console.error("Failed to create answer");
+ }
+ }
+ if (type === "ANSWER") recieveAnswer(data, senderId);
+ if (type === "ICE") recieveICE(data, senderId);
+ }
+ }
+ );
+ socket.on("CLIENT-LEFT", ({ voiceCallId, clientId }) => {
+ if (voiceCallId === callId.current) {
+ endCall(clientId);
+ }
});
socket.emit("typing");
@@ -313,10 +334,10 @@ function SocketProvider({ children }: SocketProviderProps) {
socket.off("typing");
// socket.io.engine.off("close");
};
- }, [socket, chat, decrypt, dispatch, queryClient, user]);
+ }, [socket, chat]);
const sendMessage = (sentMessage: MessageInterface) => {
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
console.log("messageToSend", sentMessage);
socket.emit(
"SEND_MESSAGE",
@@ -377,7 +398,7 @@ function SocketProvider({ children }: SocketProviderProps) {
content: string,
chatId: string
) => {
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit(
"EDIT_MESSAGE_CLIENT",
{ messageId, content, chatId },
@@ -402,13 +423,12 @@ function SocketProvider({ children }: SocketProviderProps) {
);
}
};
-
const pinMessageSocket = (
chatId: string,
messageId: string,
userId: string
) => {
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit("PIN_MESSAGE_CLIENT", { messageId, chatId, userId });
} else {
console.warn("Cannot pin message: not connected to socket server");
@@ -420,29 +440,20 @@ function SocketProvider({ children }: SocketProviderProps) {
messageId: string,
userId: string
) => {
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit("UNPIN_MESSAGE_CLIENT", { messageId, chatId, userId });
} else {
console.warn("Cannot unpin message: not connected to socket server");
}
};
const createVoiceCall = ({ chatId }: { chatId: string }) => {
- console.log("create call", isConnected, socket, chatId, callId.current);
- if (isConnected && socket && !callId.current) {
+ if (socket?.connected && socket && !callId.current) {
+ setChatId(chatId);
socket.emit("CREATE-CALL", {
chatId
});
}
};
- // const startConnection = async (callId: string) => {
- // const offer = await connectToPeer();
- // if (isConnected && socket) {
- // console.log(offer);
- // socket.emit("SEND_OFFER", { offer });
- // } else {
- // console.warn("Cannot unpin message: not connected to socket server");
- // }
- // };
function createGroupOrChannel({
type,
@@ -453,13 +464,12 @@ function SocketProvider({ children }: SocketProviderProps) {
name: string;
members: string[];
}) {
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit(
"CREATE_GROUP_CHANNEL",
{ type, name, members },
({ success, data, error }: AckCreateGroup) => {
if (success) {
- console.log("Group/Channel ID:", data?._id);
navigate(`/${data?._id}`);
} else {
toast.error(error || `Failed to create ${type}`);
@@ -480,7 +490,7 @@ function SocketProvider({ children }: SocketProviderProps) {
chatId: string;
users: string[];
}) {
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit(
"ADD_MEMBERS_CLIENT",
{ chatId, users },
@@ -506,8 +516,7 @@ function SocketProvider({ children }: SocketProviderProps) {
chatId: string;
members: string[];
}) {
- console.log(chatId, members);
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit(
"ADD_ADMINS_CLIENT",
{ chatId, members },
@@ -533,7 +542,7 @@ function SocketProvider({ children }: SocketProviderProps) {
messageId: string;
chatId: string;
}) {
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit(
"DELETE_MESSAGE_CLIENT",
{ messageId, chatId },
@@ -551,12 +560,11 @@ function SocketProvider({ children }: SocketProviderProps) {
}
function leaveGroup({ chatId }: { chatId: string }) {
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit(
"LEAVE_GROUP_CHANNEL_CLIENT",
{ chatId },
({ success, message, error }: AckCreateGroup) => {
- console.log(message, error, success);
if (success) {
toast.success(message);
queryClient.invalidateQueries({ queryKey: ["chats"] });
@@ -580,7 +588,7 @@ function SocketProvider({ children }: SocketProviderProps) {
members: string[];
}) {
console.log(chatId, members);
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit(
"REMOVE_MEMBERS_CLIENT",
{ chatId, members },
@@ -608,7 +616,7 @@ function SocketProvider({ children }: SocketProviderProps) {
type: "post" | "download";
who: "admins" | "everyone";
}) {
- if (isConnected && socket) {
+ if (socket?.connected && socket) {
socket.emit(
"SET_PERMISSION_CLIENT",
{ chatId, type, who },
@@ -662,7 +670,6 @@ function SocketProvider({ children }: SocketProviderProps) {
"SET_PRIVACY_CLIENT",
{ chatId, privacy },
({ success, message, error }: AckCreateGroup) => {
- console.log(message, error, success);
if (success) {
toast.success(message);
queryClient.invalidateQueries({ queryKey: ["chats"] });
@@ -680,7 +687,7 @@ function SocketProvider({ children }: SocketProviderProps) {
return (
{children}
diff --git a/app/src/types/calls.ts b/app/src/types/calls.ts
index 4c75782f..a877a533 100644
--- a/app/src/types/calls.ts
+++ b/app/src/types/calls.ts
@@ -2,15 +2,40 @@ export interface CallContextType {
callId: React.RefObject;
senderId: React.RefObject;
chatId: React.RefObject;
- callAccepted: React.RefObject;
+ clientId: React.RefObject<
+ Map<
+ string,
+ {
+ connection: RTCPeerConnection | null;
+ offerSent: boolean;
+ }
+ >
+ >;
+ // callAccepted: React.RefObject;
joinCall: (newCallId: string, newSenderId: string, newChatId: string) => void;
endCall: () => void;
+
acceptCall: () => void;
- startPeerConnection: () => Promise;
- offer: React.RefObject;
- answer: React.RefObject;
- peerConnection: React.RefObject;
+ startPeerConnection: (
+ clientId: string
+ ) => Promise;
+ offer: React.RefObject;
+ getPeerConnection: (clientId: string) => RTCPeerConnection | null;
//recieveAnswer: (answer: string) => Promise;
- recieveICE: (candidate: RTCIceCandidateInit) => Promise;
+ recieveICE: (
+ candidate: RTCIceCandidateInit,
+ senderId: string
+ ) => Promise;
+ createAnswer: (
+ data: RTCSessionDescriptionInit,
+ senderId: string
+ ) => Promise;
+ setChatId: (chatId: string) => void;
+ recieveAnswer: (answer: RTCSessionDescriptionInit, senderId: string) => void;
}
-export type CallStatus = "inactive" | "active" | "calling" | "incoming" | "ended";
\ No newline at end of file
+export type CallStatus =
+ | "inactive"
+ | "active"
+ | "calling"
+ | "incoming"
+ | "ended";
diff --git a/app/src/types/socket.ts b/app/src/types/socket.ts
index 36624587..5b544ac4 100644
--- a/app/src/types/socket.ts
+++ b/app/src/types/socket.ts
@@ -65,6 +65,7 @@ export interface SocketContextType {
deleteGroup: ({ chatId }: { chatId: string }) => void;
createVoiceCall: ({ chatId }: { chatId: string }) => void;
acceptCall: (callId: string | null) => void;
+ finishCall: () => void;
}
export interface SocketProviderProps {
diff --git a/app/src/utils/socket.tsx b/app/src/utils/socket.tsx
index 17c07f52..b5250a09 100644
--- a/app/src/utils/socket.tsx
+++ b/app/src/utils/socket.tsx
@@ -3,24 +3,20 @@ import { io, Socket } from "socket.io-client";
import { useUser } from "@features/authentication/login/hooks/useUser";
import { DefaultEventsMap } from "@socket.io/component-emitter";
-let socket: Socket | null = null;
-
function initializeSocket(
userId: string,
sessionId: string | null
): Socket {
- if (!socket) {
- socket = io(`${import.meta.env.VITE_SOCKET_BACKEND_API}`, {
- query: {
- userId
- },
- auth: {
- sessionId
- }
- });
-
- console.log("Socket initialized");
- }
+ const socket = io(`${import.meta.env.VITE_SOCKET_BACKEND_API}`, {
+ query: {
+ userId
+ },
+ auth: {
+ sessionId
+ }
+ });
+
+ console.log("Socket initialized");
return socket;
}
@@ -37,9 +33,9 @@ function useSocket() {
socketRef.current = initializeSocket(user._id, sessionId);
return () => {
- console.log("Disconnecting socket");
+ ("Disconnecting socket");
socketRef.current?.disconnect();
- socket = null;
+ socketRef.current = null;
};
}
}, [user, isPending]);