Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const StyledAvatar = styled.div<{ $image?: string; $size?: string }>`
font-weight: bold;
font-size: 1rem;
text-transform: uppercase;
z-index: 1;
`;

type PropsType = {
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const StyledMain = styled.main`
position: relative;
overflow: hidden;
background: var(--color-chat-wallpaper-1);

padding-top: 1rem;
&::before {
content: "";
position: absolute;
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/side-bar/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const StyledSidebar = styled.aside<{ $isExiting: boolean }>`
background-color: var(--color-background);
overflow: hidden;
position: relative;

padding-top: 1rem;
display: flex;
flex-direction: column;

Expand Down
10 changes: 10 additions & 0 deletions app/src/data/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ enum icons {
Info,
Phone,
ArrowForward,
Mute,
EndCall,
}

type iconStrings = keyof typeof icons;
Expand Down Expand Up @@ -390,6 +392,14 @@ const iconImports: Record<iconStrings, IconConfig> = {
importFn: () => import("@mui/icons-material/ArrowForward"),
defaultProps: { fontSize: "large" },
},
Mute: {
importFn: () => import("@mui/icons-material/Mic"),
defaultProps: { fontSize: "large" },
},
EndCall: {
importFn: () => import("@mui/icons-material/CallEnd"),
defaultProps: { fontSize: "large" },
},
};

const iconCache = new Map<string, React.ReactElement>();
Expand Down
254 changes: 254 additions & 0 deletions app/src/features/calls/CallLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import styled from "styled-components";
import { peerConnection } from "./call";
import { STATIC_MEDIA_URL } from "@constants";

import { getAvatarName } from "utils/helpers";
import { createPortal } from "react-dom";
import Icon from "@components/Icon";
import { getIcon } from "@data/icons";
import { useState } from "react";

const ModalContainer = styled.div`
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
opacity: 1;
transition: opacity 0.15s ease;
z-index: 6;
`;

const ModalBackdrop = styled.div`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
background-color: rgba(0, 0, 0, 0.25);
`;

const ModalDialog = styled.div`
border-radius: 5px;
position: relative;
display: inline-flex;
flex-direction: column;
width: 100%;
max-width: 35rem;
min-width: 17.5rem;
margin: 2rem auto;
background-color: var(--color-background);
box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow);
transform: translate3d(0, -1rem, 0);
transition:
transform 0.2s ease,
opacity 0.2s ease;
`;
const ButtonsContainer = styled.div`
display: flex;
position: absolute;
bottom: 1rem;
-webkit-user-select: none;
user-select: none;
`;
const AvatarContainer = styled.div`
border-radius: inherit;
overflow: hidden;
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-image: linear-gradient(
var(--color-white) -125%,
var(--color-user)
);
`;
const ModalContent = styled.div`
border-radius: inherit;
display: flex;
flex-direction: column;
align-items: center;
height: 80vh;
padding: 0;
`;
const StyledAvatar = styled.div<{ $image?: string }>`
border-radius: inherit;
width: 100%;
height: 100%;

background: ${({ $image }) =>
$image
? `url(${STATIC_MEDIA_URL + $image}) center/cover no-repeat`
: "var(--color-avatar)"};

color: white;

display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 5rem;
text-transform: uppercase;
`;
const RoundButton = styled.button<{
$bgColor?: string;
$bgColorHover?: string;
}>`
outline: none !important;
display: flex;
align-items: center;
justify-content: center;
height: 3.5rem;
border-radius: 50%;
border: 0;
background-color: ${({ $bgColor }) => $bgColor || "rgba(0, 0, 0, 0)"};
background-size: cover;
padding: 0.625rem;
color: #fff;
flex-shrink: 0;
position: relative;
overflow: hidden;
transition:
background-color 0.15s,
color 0.15s;
text-decoration: none !important;
text-transform: uppercase;
text-align: right;
font-size: 3.5rem;
font-weight: 200;
&:hover {
background-color: ${({ $bgColorHover }) =>
$bgColorHover || "rgba(0, 0, 0, 0.1)"};
}
`;

const ButtonContainer = styled.div`
width: 5rem;
display: flex;
flex-direction: column;
align-items: center;
`;
const ButtonText = styled.div`
color: #fff;
font-size: 0.75rem;
text-transform: lowercase;
margin-top: 0.25rem;
white-space: nowrap;
`;
const NameContainer = styled.div`
position: absolute;
top: 1rem;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 0;
padding-top: 4rem;
padding-bottom: 2rem;
margin-bottom: auto;
color: #fff;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
`;
const TopBar = styled.div`
top: 0.5rem;
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
color: #fff;
position: absolute;
padding: 0.5rem;
text-align: right;
`;
const ActiveHeaderOpen = styled.div`
position: fixed;
margin-bottom: 0.5rem;
top: 0;
left: 0;
height: 1rem;
width: 100%;
z-index: 6;
display: flex;
justify-content: center;
font-weight: 500;
font-size: 0.875rem;
color: #fff;
align-items: center;
padding: 0 1rem;
background: linear-gradient(135deg, rgb(49, 82, 232), rgb(143, 74, 172));
transform: translateY(0);
`;
type PropsType = {
image?: string | undefined;
name: string | undefined;
isCollapsed: boolean;
setIsCollapsed: (arg0: boolean) => void;
};

export default function CallLayout({
name,
image,
isCollapsed,
setIsCollapsed,
}: PropsType) {
return (
<>
{!isCollapsed
? createPortal(
<ModalContainer>
<ModalBackdrop> </ModalBackdrop>
<ModalDialog>
<ModalContent>
<AvatarContainer>
<StyledAvatar $image={image}>
{!image && getAvatarName(name)}
</StyledAvatar>
<TopBar>
<RoundButton onClick={() => setIsCollapsed(true)}>
&times;
</RoundButton>
</TopBar>
<NameContainer>
<h1>{name}</h1>
<span>state</span>
</NameContainer>
<ButtonsContainer>
<ButtonContainer>
<RoundButton>{getIcon("Mute")}</RoundButton>
<ButtonText>unmute</ButtonText>
</ButtonContainer>
<ButtonContainer>
<RoundButton
$bgColor="var(--color-error)"
$bgColorHover="var(--color-error-shade)"
>
{getIcon("EndCall")}
</RoundButton>
<ButtonText>end call</ButtonText>
</ButtonContainer>
</ButtonsContainer>
</AvatarContainer>
</ModalContent>
</ModalDialog>
</ModalContainer>,
document.body,
)
: createPortal(
<ActiveHeaderOpen onClick={() => setIsCollapsed(false)}>
<span>{name}</span>
</ActiveHeaderOpen>,
document.body,
)}
</>
);
}
62 changes: 62 additions & 0 deletions app/src/features/calls/call.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
export let callState = "idle";
const stunServers = {
iceServers: [
{ urls: ["stun:stun.l.google.com:19302", "stun:stun.l.google.com:5349"] },
],
};
async function getVoiceInput(): Promise<MediaStream | null> {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
return stream;
} catch (error) {
console.error("Error accessing microphone:", error);
return null;
}
}
export let peerConnection: RTCPeerConnection;
let localStream: MediaStream | null;
let RemoteStream: MediaStream;
export const connectToPeer = async () => {
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 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);
//start call
};
Loading