Первоначальная версия VidConf
This commit is contained in:
120
frontend/src/components/room/ChatPanel.tsx
Normal file
120
frontend/src/components/room/ChatPanel.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from 'react'
|
||||
import { AlertCircle, Send, X } from 'lucide-react'
|
||||
import type { ChatConnectionStatus, ChatMessageOut } from '@/hooks/useChat'
|
||||
import { formatLocalTime } from '@/lib/localTime'
|
||||
|
||||
interface ChatPanelProps {
|
||||
messages: ChatMessageOut[]
|
||||
status: ChatConnectionStatus
|
||||
statusMessage: string | null
|
||||
onSend: (text: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Панель чата комнаты конференции (см. design/mockups/room.html, блок
|
||||
* `.chat-panel`). Всегда в развёрнутом виде рендерится только пока сама
|
||||
* панель открыта — сворачивание/разворачивание и счётчик непрочитанных
|
||||
* управляются на уровне RoomPage (WS-соединение живёт независимо от того,
|
||||
* открыта ли панель, — иначе при сворачивании терялась бы история).
|
||||
*
|
||||
* Textarea, а не `<input>` из макета — сознательное отступление ради
|
||||
* Enter/Shift+Enter (перенос строки), стили сохранены визуально идентичными
|
||||
* пилюле-полю из макета.
|
||||
*/
|
||||
export function ChatPanel({ messages, status, statusMessage, onSend, onClose }: ChatPanelProps) {
|
||||
const [draft, setDraft] = useState('')
|
||||
const listRef = useRef<HTMLDivElement>(null)
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
||||
|
||||
// Автоскролл к последнему сообщению — только если пользователь и так был
|
||||
// внизу списка, чтобы не мешать чтению прокрученной вверх истории.
|
||||
useEffect(() => {
|
||||
const el = listRef.current
|
||||
if (!el) return
|
||||
const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight
|
||||
if (distanceFromBottom < 120) {
|
||||
el.scrollTop = el.scrollHeight
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
function handleSend() {
|
||||
const text = draft.trim()
|
||||
if (!text || status !== 'open') return
|
||||
onSend(text)
|
||||
setDraft('')
|
||||
if (textareaRef.current) textareaRef.current.style.height = 'auto'
|
||||
}
|
||||
|
||||
function handleKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
|
||||
if (event.key === 'Enter' && !event.shiftKey) {
|
||||
event.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
function handleDraftChange(event: ChangeEvent<HTMLTextAreaElement>) {
|
||||
setDraft(event.target.value)
|
||||
// Авторасширение textarea до 4 строк, дальше — внутренний скролл.
|
||||
const el = event.target
|
||||
el.style.height = 'auto'
|
||||
el.style.height = `${Math.min(el.scrollHeight, 96)}px`
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="chat-panel">
|
||||
<div className="chat-head">
|
||||
<h2>Чат встречи</h2>
|
||||
<button type="button" aria-label="Свернуть чат" onClick={onClose}>
|
||||
<X className="lucide" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{statusMessage && (
|
||||
<p className="chat-status-banner">
|
||||
<AlertCircle className="lucide" aria-hidden="true" /> {statusMessage}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="chat-messages" ref={listRef}>
|
||||
{messages.length === 0 && status === 'open' && !statusMessage && (
|
||||
<p className="chat-empty">Сообщений пока нет — начните обсуждение</p>
|
||||
)}
|
||||
{messages.map((message) => (
|
||||
<div className="msg" key={message.id}>
|
||||
<div className="msg-meta">
|
||||
<span className="msg-author">
|
||||
{message.author_name}
|
||||
{message.is_guest && <span className="msg-guest-badge">гость</span>}
|
||||
</span>
|
||||
<span className="msg-time">{formatLocalTime(message.created_at)}</span>
|
||||
</div>
|
||||
<div className="msg-bubble">{message.text}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form
|
||||
className="chat-input-row"
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault()
|
||||
handleSend()
|
||||
}}
|
||||
>
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
rows={1}
|
||||
placeholder="Написать сообщение…"
|
||||
value={draft}
|
||||
onChange={handleDraftChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={status !== 'open'}
|
||||
maxLength={2000}
|
||||
/>
|
||||
<button type="submit" aria-label="Отправить" disabled={!draft.trim() || status !== 'open'}>
|
||||
<Send className="lucide" aria-hidden="true" />
|
||||
</button>
|
||||
</form>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user