Skip to content

Commit

Permalink
feat: ui improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Stormix committed Nov 10, 2023
1 parent 4656b69 commit c1facb2
Show file tree
Hide file tree
Showing 26 changed files with 1,177 additions and 303 deletions.
Binary file modified client/bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion client/components.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
},
"aliases": {
"components": "@/components",
"utils": "@/utils"
"utils": "@/lib/utils"
}
}
4 changes: 3 additions & 1 deletion client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<title>MSN - an omegle clone</title>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">

</head>
<body>
<div id="root"></div>
Expand Down
10 changes: 9 additions & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,27 @@
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.3.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-toast": "^1.1.5",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"lucide-react": "^0.292.0",
"nanoid": "^5.0.3",
"peerjs": "^1.5.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.48.2",
"react-use-websocket": "^4.5.0",
"tailwind-merge": "^2.0.0",
"tailwindcss": "^3.3.5",
"tailwindcss-animate": "^1.0.7"
"tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4",
"zustand": "^4.4.6"
},
"devDependencies": {
"@types/node": "^20.9.0",
Expand Down
172 changes: 2 additions & 170 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,179 +1,11 @@
import Peer, { MediaConnection } from 'peerjs'
import { useEffect, useRef, useState } from 'react'
import useWebSocket from 'react-use-websocket'
import Home from './components/pages/home'
import Layout from './components/template/layout'
import { Button } from './components/ui/button'
import { Textarea } from './components/ui/textarea'
import { useToast } from './components/ui/use-toast'
import { useUserMedia } from './hooks/useUserMedia'

interface Message {
sender: string
message: string
}

const App = () => {
const meRef = useRef<HTMLVideoElement>(null)
const strangerRef = useRef<HTMLVideoElement>(null)
const [id, setId] = useState<string | null>(null)
const [strangerId, setStrangerId] = useState<string | null>(null)
const [messages, setMessages] = useState<Message[]>([])
const [message, setMessage] = useState<string>('')

const addMessage = (message: Message) => {
setMessages((messages) => [...messages, message])
}

const { toast } = useToast()

const ws = useWebSocket('wss://omegle-server.lab.stormix.dev', {
onOpen: () => {
console.log('opened')
},
shouldReconnect: () => true,
onMessage: (e) => {
const { data: raw } = e
const data = JSON.parse(raw)

switch (data.type) {
case 'message':
addMessage({
sender: data.payload.id,
message: data.payload.message
})
break
case 'id':
setId(data.id)
break
case 'offer':
setStrangerId(data.payload)
toast({
title: 'Stranger Found',
description: 'Calling ' + data.payload
})
break
case 'error':
toast({
title: 'Error',
description: data.payload,
variant: 'destructive'
})
break
}
}
})

const [peer, setPeer] = useState<Peer | null>(null)
const [call, setCall] = useState<MediaConnection | null>(null)

const { stream } = useUserMedia({
audio: true,
video: true
})

if (stream && meRef.current) {
meRef.current.srcObject = stream
}

useEffect(() => {
if (stream && !peer && id) {
setPeer(
new Peer(id, {
host: 'peer.lab.stormix.dev',
port: 443,
path: '/',
debug: 3
})
)
}
}, [peer, stream, id])

useEffect(() => {
if (peer) {
peer.on('open', (id) => {
console.log('My peer ID is: ' + id)
})

peer.on('call', (call) => {
if (!stream) return
setStrangerId(call.peer)
call.answer(stream)
call.on('stream', (remoteStream) => {
if (strangerRef.current) {
strangerRef.current.srcObject = remoteStream
}
})
})
}
}, [peer, stream])

useEffect(() => {
if (peer && strangerId) {
const call = peer.call(strangerId, stream as MediaStream)
call.on('stream', (remoteStream) => {
if (strangerRef.current) {
strangerRef.current.srcObject = remoteStream
}
})
setCall(call)
}
}, [peer, stream, strangerId])

const startCall = () => {
ws.sendJsonMessage({
id,
type: 'call'
})
}

return (
<>
<Layout>
<div className="flex flex-row h-full gap-8">
<div className="w-1/4 flex flex-col gap-8">
<div className="bg-accent text-white h-72 w-full">
<video autoPlay playsInline className="w-full h-full" id="stranger-video" ref={strangerRef} />
</div>
<div className="bg-accent text-white h-72 w-full">
<video autoPlay playsInline muted className="w-full h-full" id="me-video" ref={meRef} />
</div>
</div>
<div className="flex-grow flex flex-col h-full">
<div className="flex-grow">
{messages.map((message, i) => (
<div key={i} className="flex flex-col gap-4 p-4">
<span className="font-bold">{message.sender}: </span>
<span>{message.message}</span>
</div>
))}
</div>
<div className="flex flex-row gap-4">
<Textarea value={message} onChange={(e) => setMessage(e.target.value)} />
<div className="flex flex-col gap-4">
<Button
disabled={!message || !strangerId}
onClick={() => {
ws.sendJsonMessage({
type: 'message',
payload: {
id: strangerId,
message
}
})

addMessage({
sender: 'me',
message
})
}}
>
Send
</Button>
<Button onClick={() => startCall()}>{call ? 'Skip' : 'Call'}</Button>
</div>
</div>
</div>
</div>
<Home />
</Layout>
</>
)
Expand Down
106 changes: 106 additions & 0 deletions client/src/components/molecules/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { Button } from '@/components/ui/button'
import { Form, FormControl, FormDescription, FormField, FormItem, FormMessage } from '@/components/ui/form'
import { useChatStore } from '@/lib/store'
import { cn } from '@/lib/utils'
import { useOmegle } from '@/providers/omegle-provider'
import { zodResolver } from '@hookform/resolvers/zod'
import { useEffect, useRef } from 'react'
import { useForm } from 'react-hook-form'
import { z } from 'zod'
import { Textarea } from '../ui/textarea'
import { useToast } from '../ui/use-toast'
import NameDialog from './name-dialog'

const formSchema = z.object({
message: z.string().min(1)
})

const Chat = () => {
const ref = useRef<HTMLDivElement>(null)
const { messages, name } = useChatStore()
const { sendMessage, strangerId } = useOmegle()
const { toast } = useToast()
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
message: ''
}
})

useEffect(() => {
// When a new message is received, scroll to the bottom of the chat
ref?.current?.scrollTo(0, ref?.current?.scrollHeight)
}, [messages])

const onSubmit = (data: z.infer<typeof formSchema>) => {
if (!strangerId) {
toast({
title: 'Error',
description: 'You are not connected to a chat.',
variant: 'destructive'
})
return
}
sendMessage?.(data.message)
form.reset()
}

return (
<div className="flex-grow flex flex-col h-full w-full max-h-full">
<div className="flex gap-2">
<span>
You're connected as <b>{name}</b>
</span>
<NameDialog />
</div>
<h3>Chat log: </h3>
<div className="flex-grow flex flex-col gap-2 overflow-y-auto h-5/6 py-8" ref={ref}>
{messages.map((message, i) => (
<div key={i} className="flex flex-col gap-2 ">
<span
className={cn('font-bold', {
'text-accent': message.sender === strangerId
})}
>
{message.sender}:{' '}
</span>
<span>{message.message}</span>
</div>
))}
</div>
<div className="flex w-full">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className=" flex flex-col md:flex-row gap-4 w-full items-start">
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem className="flex-grow">
<FormControl>
<Textarea
placeholder="Type your message here"
{...field}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
form.handleSubmit(onSubmit)()
}
}}
/>
</FormControl>
<FormDescription>This is your public display name.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" size={'lg'}>
Send message
</Button>
</form>
</Form>
</div>
</div>
)
}

export default Chat
5 changes: 4 additions & 1 deletion client/src/components/molecules/header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { ModeToggle } from './mode-toggle'

const Header = () => {
return (
<header>
<header className="flex justify-between">
<h1>Header</h1>
<ModeToggle />
</header>
)
}
Expand Down
26 changes: 26 additions & 0 deletions client/src/components/molecules/mode-toggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Moon, Sun } from 'lucide-react'

import { Button } from '@/components/ui/button'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { useTheme } from '@/providers/theme-provider'

export function ModeToggle() {
const { setTheme } = useTheme()

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
Loading

0 comments on commit c1facb2

Please sign in to comment.