Skip to content

Commit

Permalink
Merge pull request #40 from joaoreider/simple-chat
Browse files Browse the repository at this point in the history
feat(web): add a text chat using the existing connection
  • Loading branch information
brunocroh authored Jun 11, 2024
2 parents 9f7277a + 056606e commit 7507536
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 72 deletions.
6 changes: 6 additions & 0 deletions apps/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,9 @@
@apply bg-background text-foreground;
}
}

@layer utilities {
.h-content {
height: calc(100vh - 125px); /* Adjust the value to match the header height */
}
}
70 changes: 70 additions & 0 deletions apps/web/app/room/[slug]/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client'

import React, { useRef, useState } from 'react'
import { MessageCircle } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { MessageItem } from './message-item'

export type Message = {
sender: 'me' | 'other'
content: string
}

type Chat = {
messages: Message[]
onSend: (message: string) => void
}

export const Chat: React.FC<Chat> = ({ messages, onSend }) => {
const [inputValue, setInputValue] = useState('')

const sendMessage = () => {
const message = inputValue.trim()
if (message) {
onSend(message)
setInputValue('')
}
}

const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
sendMessage()
}
}

const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(event.target.value)
}

return (
<div className="align-center flex h-full w-full flex-col items-center justify-center gap-2">
<ScrollArea className="flex w-full flex-1 items-center gap-2 rounded-lg border p-4">
{!messages.length ? (
<div className="mt-6 flex flex-col items-center ">
<MessageCircle className="text-muted" />
<span className="italic text-muted">No messages yet</span>
</div>
) : (
messages.map((message, index) => (
<div key={index} className="w-full">
<MessageItem message={message} />
</div>
))
)}
</ScrollArea>
<div className="flex w-full flex-row gap-2 rounded-lg align-baseline">
<Input
className="flex-shrink"
placeholder="Type a message..."
value={inputValue}
onKeyDown={handleKeyPress}
onChange={handleInputChange}
/>
<Button onClick={sendMessage}>Send</Button>
</div>
</div>
)
}
28 changes: 28 additions & 0 deletions apps/web/app/room/[slug]/components/message-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import clsx from 'clsx'
import { Message } from './chat'

type MessageItem = {
message: Message
}

export const MessageItem: React.FC<MessageItem> = ({ message }) => {
const isMe = message.sender === 'me'

return (
<div
className={clsx(
'flex items-center gap-3',
isMe ? 'justify-end' : 'justify-start'
)}
>
<span
className={clsx(
'm-1 max-w-xs rounded-md p-3',
isMe ? 'bg-accent' : 'bg-purple-950'
)}
>
<span className="text-sm">{message.content}</span>
</span>
</div>
)
}
7 changes: 7 additions & 0 deletions apps/web/app/room/[slug]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default function Layout({
children,
}: {
children: React.ReactNode
}): JSX.Element {
return <div className="h-content">{children}</div>
}
122 changes: 76 additions & 46 deletions apps/web/app/room/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import useWebSocket from 'react-use-websocket'
import { usePathname } from 'next/navigation'
import { clsx } from 'clsx'
import Peer from 'simple-peer'
import { Chat, Message } from '@/app/room/[slug]/components/chat'
import Countdown from '@/components/countdown'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
Expand All @@ -31,6 +32,7 @@ export default function Page(): JSX.Element {
const [loading, setLoading] = useState(true)
const [videoReady, setVideoReady] = useState(false)
const [error, setError] = useState('')
const [messages, setMessages] = useState<Message[]>([])

const {
muted,
Expand Down Expand Up @@ -104,6 +106,13 @@ export default function Page(): JSX.Element {
}
})

peerRef.current?.on('data', (message) => {
setMessages((prevMessages) => [
...prevMessages,
{ sender: 'other', content: message.toString() },
])
})

if (isHost) {
peerRef.current?.on('signal', (signalData) => {
if (peerRef.current?.connected) return
Expand Down Expand Up @@ -193,59 +202,80 @@ export default function Page(): JSX.Element {
location.replace(`/room/queue`)
}, [stopAllStreaming])

const handleSendMessage = useCallback((newMessage: string) => {
if (peerRef.current && peerRef.current.connected) {
peerRef.current.send(newMessage)
}
setMessages((prevMessages) => [
...prevMessages,
{ sender: 'me', content: newMessage },
])
}, [])

return (
<section className="container flex h-full flex-col content-center items-center justify-center gap-4">
<section className="container flex h-full flex-col content-center items-center justify-center">
<Countdown onFinishTime={handleHangUp} startTime={600_000} />
<div className="flex w-full flex-col items-center ">
<div className={clsx('flex flex-col gap-4', !error && 'invisible')}>
<h3 className="text-lg">{error}</h3>
<Button onClick={handleBackToQueue}>Back to queue</Button>
</div>
<div className={clsx(!loading && 'invisible')}>
<h2>Loading...</h2>
</div>
<div className="flex w-full flex-col items-center">
{error && (
<div className="flex flex-col gap-4">
<h3 className="text-lg">{error}</h3>
<Button onClick={handleBackToQueue}>Back to queue</Button>
</div>
)}
{loading && (
<div>
<h2>Loading...</h2>
</div>
)}
<div
className={clsx(
'flex gap-2 md:flex-row md:items-center',
'flex w-full flex-col justify-center gap-2 p-1 md:flex-row',
!connected && 'invisible'
)}
>
<Card className="border-slate-5 bg-slate-6 w-3/4 border border-b-0 md:w-1/2 ">
<CardContent className="p-5">
<VideoPlayer
ref={videoRef}
activeAudioDevice={selectedAudioDevice}
setActiveAudioDevice={(deviceId) =>
handleInputChange(deviceId, 'audio')
}
activeVideoDevice={selectedVideoDevice}
setActiveVideoDevice={(deviceId) =>
handleInputChange(deviceId, 'video')
}
audioDevices={audioDevices}
videoDevices={videoDevices}
outputDevices={outputDevices}
activeOutputDevice={selectedOutputDevice}
setActiveOutputDevice={(deviceId) =>
switchAudioOutput(deviceId)
}
muted={muted}
videoOff={videoOff}
onMute={toggleMute}
onVideoOff={toggleVideo}
onTurnOff={handleHangUp}
/>
</CardContent>
</Card>
<Card className="border-slate-5 bg-slate-6 w-3/4 border border-b-0 md:w-1/2 md:self-start ">
<CardContent className="h-full p-5">
<VideoPlayer
remote
ref={remoteRef}
activeOutputDevice={selectedOutputDevice}
/>
</CardContent>
</Card>
<div className="flex flex-row gap-2 md:flex-col-reverse">
<Card className="border-slate-5 bg-slate-6 border border-b-0 md:self-start">
<CardContent className="p-3">
<VideoPlayer
ref={videoRef}
activeAudioDevice={selectedAudioDevice}
setActiveAudioDevice={(deviceId) =>
handleInputChange(deviceId, 'audio')
}
activeVideoDevice={selectedVideoDevice}
setActiveVideoDevice={(deviceId) =>
handleInputChange(deviceId, 'video')
}
audioDevices={audioDevices}
videoDevices={videoDevices}
outputDevices={outputDevices}
activeOutputDevice={selectedOutputDevice}
setActiveOutputDevice={(deviceId) =>
switchAudioOutput(deviceId)
}
muted={muted}
videoOff={videoOff}
onMute={toggleMute}
onVideoOff={toggleVideo}
onTurnOff={handleHangUp}
/>
</CardContent>
</Card>
<Card className="border-slate-5 bg-slate-6 border border-b-0 md:self-start">
<CardContent className="p-3">
<VideoPlayer
remote
ref={remoteRef}
activeOutputDevice={selectedOutputDevice}
/>
</CardContent>
</Card>
</div>
<div>
{connected && (
<Chat onSend={handleSendMessage} messages={messages} />
)}
</div>
</div>
</div>
</section>
Expand Down
47 changes: 47 additions & 0 deletions apps/web/components/ui/scroll-area.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client'

import * as React from 'react'
import { cn } from '@/lib/utils'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'

const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn('relative overflow-hidden', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName

const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
'flex touch-none select-none transition-colors',
orientation === 'vertical' &&
'h-full w-2.5 border-l border-l-transparent p-[1px]',
orientation === 'horizontal' &&
'h-2.5 flex-col border-t border-t-transparent p-[1px]',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

export { ScrollArea, ScrollBar }
4 changes: 2 additions & 2 deletions apps/web/components/video-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export const VideoPlayer = forwardRef<

return (
<>
<div className="m-0 size-full w-full max-w-[900px] overflow-hidden">
<div className="flex size-full h-full w-full flex-col gap-3 overflow-hidden">
<div className="relative flex flex-col items-center">
<video
className={clsx('w-full rounded-lg ', {
Expand Down Expand Up @@ -143,7 +143,7 @@ export const VideoPlayer = forwardRef<
)}
</div>
{!remote && (
<div className="mt-5 flex justify-center">
<div className="flex justify-center">
<div className="flex w-full flex-col gap-2 sm:flex-row md:w-fit md:gap-6">
<Select
onValueChange={setActiveAudioDevice}
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dependencies": {
"@next/third-parties": "^14.2.3",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@repo/env-config": "workspace:*",
"@radix-ui/react-slot": "^1.0.2",
Expand Down
Loading

0 comments on commit 7507536

Please sign in to comment.