Skip to content
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,9 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# text editors
.zed
.vscode

certificates
98 changes: 57 additions & 41 deletions src/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,70 @@ import Greeting from "@/components/sections/Greeting";
import NotConnected from "@/components/sections/NotConnected";
import StartChatting from "@/components/sections/StartChatting";
import WaitingForChat from "@/components/sections/WaitingForChat";
import { VideoChat } from "@/components/resusable/VideoChat";
import { useChatSocket } from "@/hooks/useChatSocket";
import { useWebRTC } from "@/hooks/useWebRTC";

export default function ChatPage() {

const {
user,
partner,
messages,
isConnected,
isWaiting,
readIndex,
setUserName,
findPartner,
sendMessage,
readMessage,
startTyping,
stopTyping,
partnerTyping,
disconnect,
} = useChatSocket();
const {
socket,
user,
partner,
messages,
isConnected,
isWaiting,
readIndex,
setUserName,
findPartner,
sendMessage,
readMessage,
startTyping,
stopTyping,
partnerTyping,
disconnect,
} = useChatSocket();

let content;
const {
requestVideoCall,
incomingCall,
declineIncomingCall,
webRTCState,
startVideoCall,
// endVideoCall
} = useWebRTC(socket, partner);

if (!isConnected) {
content = <NotConnected/>;
} else { // if connected to server
if (!user) {
content = <Greeting onSubmit={setUserName} />;
} else { // if connected to server and username is set
if (!isWaiting) {
if (!partner) { // if user hasn't allowed to find matches
content = <StartChatting name={user} onConnect={findPartner} />;
} else { // if matches found
content = <Chat partner={partner}
onMessage={sendMessage} messages={messages}
onStop={disconnect} onReconnect={findPartner}
readIndex={readIndex} readMessage={readMessage}
startTyping={startTyping} stopTyping={stopTyping} partnerTyping={partnerTyping} />;
let content;

if (!isConnected) {
content = <NotConnected />;
} else { // if connected to server
if (!user) {
content = <Greeting onSubmit={setUserName} />;
} else { // if connected to server and username is set
if (!isWaiting) {
if (!partner) { // if user hasn't allowed to find matches
content = <StartChatting name={user} onConnect={findPartner} />;
} else { // if matches found
content = <Chat partner={partner}
onMessage={sendMessage} messages={messages}
onVideoCall={requestVideoCall} incomingCall={incomingCall}
declineIncomingCall={declineIncomingCall}
startVideoCall={startVideoCall} onStop={disconnect}
onReconnect={findPartner} readIndex={readIndex}
readMessage={readMessage} startTyping={startTyping}
stopTyping={stopTyping} partnerTyping={partnerTyping} />;
}
} else { // if user is waiting for a partner
content = <WaitingForChat />;
}
}
} else { // if user is waiting for a partner
content = <WaitingForChat />;
}
}
}

return (
<main className="w-full h-full font-display">
{content}
</main>
);
return (
<main className="w-full h-full font-display">
{content}
{webRTCState.connected && <VideoChat webRTCState={webRTCState} />}
</main>
);
}
43 changes: 43 additions & 0 deletions src/components/resusable/VideoChat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { type WebRTCState } from "@/hooks/useWebRTC";

type VideoChatProps = {
webRTCState: WebRTCState,
endVideoCall: (localEvent: boolean) => void
};

export function VideoChat({ webRTCState, endVideoCall }: VideoChatProps) {
return (
<div className="flex flex-col items-center">
<div className="flex flex-row relative">
{webRTCState.remoteStream && (
<video
className="rounded-md mx-2 my-3"
autoPlay
playsInline
ref={video => {
if (video) video.srcObject = webRTCState.remoteStream;
}}
/>
)}

{webRTCState.localStream && (
<video
className="rounded-md mx-2 my-3 absolute bottom-5 right-5 w-28"
autoPlay
playsInline
muted
ref={video => {
if (video) video.srcObject = webRTCState.localStream;
}}
/>
)}

</div>
<button
className="px-3 py-2 bg-red-600 hover:bg-red-400 transition-colors rounded-3xl text-white"
onClick={() => endVideoCall(true)}>
End Call
</button>
</div>
)
}
38 changes: 29 additions & 9 deletions src/components/sections/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,29 @@ import { Message } from "@/types/messages";
import { ChangeEvent, FormEvent, KeyboardEventHandler, useCallback, useEffect, useRef, useState } from "react";
import ChatInput from "../resusable/ChatInput";
import { ChatDisplay } from "../resusable/ChatDisplay";
import CallIcon from "@/assets/icons/call";

type ChatProps = {
partner: string,
messages: Message[],
readIndex: number | null,
partnerTyping: boolean,
onMessage: (message: string | null, image: string | null, reply: number | null) => void,
onVideoCall: () => void,
startVideoCall: () => void,
incomingCall: boolean,
ongoingCall: boolean,
declineIncomingCall: () => void,
onStop: () => void,
onReconnect: () => void,
readMessage: (messageId: number) => void,
startTyping: () => void,
stopTyping: () => void,
}

export default function Chat({
partner, messages, readIndex, partnerTyping, onMessage, onStop,
onReconnect, readMessage, startTyping, stopTyping }: ChatProps
export default function Chat({
partner, messages, readIndex, partnerTyping, onMessage, onVideoCall, incomingCall, ongoingCall, startVideoCall, declineIncomingCall,
onStop, onReconnect, readMessage, startTyping, stopTyping }: ChatProps
) {

const message = useRef<HTMLTextAreaElement>(null);
Expand Down Expand Up @@ -112,18 +118,18 @@ export default function Chat({
scrollToBottom();
});
let ref = null;
if(chatInput.current) {
if (chatInput.current) {
observer.observe(chatInput.current)
ref = chatInput.current;
}

return () => {
if(ref) observer.unobserve(ref)
if (ref) observer.unobserve(ref)
}
}, [chatInput.current?.height, scrollToBottom])

const onMessageChange: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
if(e.key === "Enter" && e.shiftKey === false) {
if (e.key === "Enter" && e.shiftKey === false) {
e.preventDefault();
submitMessage();
return;
Expand Down Expand Up @@ -153,7 +159,7 @@ export default function Chat({
setAttachment("");
}

if(replyingTo !== null) {
if (replyingTo !== null) {
reply = replyingTo;
setReplyingTo(null);
}
Expand Down Expand Up @@ -192,7 +198,7 @@ export default function Chat({

function clearAttachment() {
setAttachment("");
if(fileinput.current) fileinput.current.value = "";
if (fileinput.current) fileinput.current.value = "";
message.current?.focus();
}

Expand All @@ -204,7 +210,7 @@ export default function Chat({
return (
<section className={"w-full flex justify-center items-end relative font-text"}>

<ChatDisplay chatBottom={chatBottom} chatHeightOffset={chatHeightOffset} messages={messages} partner={partner} readIndex={readIndex} replyTo={replyTo}/>
<ChatDisplay chatBottom={chatBottom} chatHeightOffset={chatHeightOffset} messages={messages} partner={partner} readIndex={readIndex} replyTo={replyTo} />

{unread > 0 && <button onClick={scrollToBottom} className="fixed bottom-[5em]">
<div className="bg-foreground font-[800] font-mono text-background text-[0.8em] w-5 h-5 flex justify-center items-center rounded-[50%] absolute right-0">{unread}</div>
Expand All @@ -217,6 +223,20 @@ export default function Chat({
<h4 className="text-[0.8em]">You&apos;re connected to</h4>
<h2>{partner} {partnerTyping && <small className="font-text">Typing...</small>}</h2>
</div>
{incomingCall && <div className="flex gap-2 ml-auto">
Incoming Video Call
<button className="p-2 rounded-full bg-green-500" onClick={startVideoCall}>Yes</button>
<button className="p-2 rounded-full bg-red-500" onClick={declineIncomingCall}>No</button>
</div>}
{ongoingCall && <div className="flex gap-2 ml-auto">
In a Video Call 00:00 (insert dynamic counter here)
</div>}
<div className="flex gap-2 ml-auto mr-8">
{!(incomingCall || ongoingCall) && <button onClick={onVideoCall}>
<CallIcon />
</button>
}
</div>
<div className="flex gap-2">
<button onClick={onRefresh}><RefreshIcon /></button>
<button onClick={onStop}><ExitIcon /></button>
Expand Down
7 changes: 4 additions & 3 deletions src/hooks/useChatSocket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const useChatSocket = () => {
const selfPk = await getKey(KeyTransaction.SELF_PK);
const sharedSecret = await deriveSharedSecret(selfPk!, partnerPk!);
const decryptedMsg = data.body ? await decryptMessage(data.body, sharedSecret) : null;
const decryptedImg = data.image? await decryptMessage(data.image, sharedSecret): null;
const decryptedImg = data.image ? await decryptMessage(data.image, sharedSecret) : null;

data.body = decryptedMsg;
data.image = decryptedImg;
Expand Down Expand Up @@ -124,8 +124,8 @@ export const useChatSocket = () => {
const selfPk = await getKey(KeyTransaction.SELF_PK);
const sharedSecret = await deriveSharedSecret(selfPk!, partnerPk!);
const encryptedMsg = message ? await encryptMessage(message, sharedSecret) : null;
const encryptedImg = image? await encryptMessage(image, sharedSecret, true): null;
const encryptedImg = image ? await encryptMessage(image, sharedSecret, true) : null;

socket.emit(ClientEvents.SEND_MESSAGE, { message: encryptedMsg, image: encryptedImg, reply });
}
}, [socket, partner]);
Expand Down Expand Up @@ -153,6 +153,7 @@ export const useChatSocket = () => {
}, [socket]);

return {
socket,
user,
partner,
messages,
Expand Down
Loading