143 lines
4.8 KiB
JavaScript
143 lines
4.8 KiB
JavaScript
import { useState, useEffect, useCallback, useRef } from "react";
|
||
import { useParams, useNavigate } from "react-router-dom";
|
||
import { useTranslation } from 'react-i18next';
|
||
import { useChats } from "../hooks/useChats";
|
||
import { useChatSocket } from "../hooks/useChatSocket";
|
||
import { getPostForChat } from "../stores/chatPostStore";
|
||
import { setActiveChatId } from "../stores/activeChatStore";
|
||
import { useUnread } from "../context/UnreadContext";
|
||
import { useAuthContext } from "../context/AuthContext";
|
||
import { getImageUrl } from "../config/authConstants";
|
||
import ChatHeader from "../components/chat/ChatHeader";
|
||
import MessageBubble from "../components/chat/MessageBubble";
|
||
import ChatInputBar from "../components/chat/ChatInputBar";
|
||
import Toast from "../components/Toast";
|
||
|
||
export default function ChatPage() {
|
||
const { t } = useTranslation();
|
||
const { chatId } = useParams();
|
||
const navigate = useNavigate();
|
||
const { fetchMessages, markAsRead } = useChats();
|
||
const { clearChatUnread } = useUnread();
|
||
const { user } = useAuthContext();
|
||
const [messages, setMessages] = useState([]);
|
||
const [input, setInput] = useState("");
|
||
const [showEmoji, setShowEmoji] = useState(false);
|
||
const [loadError, setLoadError] = useState("");
|
||
const [sendError, setSendError] = useState("");
|
||
const messagesEndRef = useRef(null);
|
||
|
||
const myUserId = user?.id;
|
||
|
||
/** Мержит новое сообщение по id — защита от дублей (REST + WS эхо). */
|
||
const addMessage = useCallback((msg) => {
|
||
setMessages((prev) => {
|
||
if (msg.id != null && prev.some((m) => m.id === msg.id)) return prev;
|
||
return [...prev, msg];
|
||
});
|
||
}, []);
|
||
|
||
const handleIncoming = useCallback((msg) => {
|
||
addMessage(msg);
|
||
}, [addMessage]);
|
||
|
||
const { sendMessage, connected } = useChatSocket(chatId, handleIncoming);
|
||
|
||
useEffect(() => {
|
||
if (!chatId) return;
|
||
|
||
setActiveChatId(chatId);
|
||
setLoadError("");
|
||
|
||
fetchMessages(chatId)
|
||
.then((list) => {
|
||
// Мержим историю с уже пришедшими по сокету сообщениями,
|
||
// а не затираем их — иначе можно потерять сообщение, пришедшее
|
||
// по WS, пока REST-запрос ещё letел.
|
||
setMessages((prev) => {
|
||
const byId = new Map(list.map((m) => [m.id, m]));
|
||
const extra = prev.filter((m) => !byId.has(m.id));
|
||
return [...list, ...extra];
|
||
});
|
||
})
|
||
.catch(() => setLoadError(t('chat.loadError')));
|
||
|
||
markAsRead(chatId);
|
||
clearChatUnread(chatId);
|
||
|
||
return () => setActiveChatId(null);
|
||
}, [chatId, fetchMessages, markAsRead, clearChatUnread, t]);
|
||
|
||
useEffect(() => {
|
||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||
}, [messages]);
|
||
|
||
const handleSend = useCallback((e) => {
|
||
e.preventDefault();
|
||
const trimmed = input.trim();
|
||
if (!trimmed) return;
|
||
|
||
const sent = sendMessage(trimmed);
|
||
if (!sent) {
|
||
setSendError(t('chat.sendError'));
|
||
return;
|
||
}
|
||
setInput("");
|
||
}, [input, sendMessage, t]);
|
||
|
||
const handleEmojiSelect = useCallback((emoji) => {
|
||
setInput((prev) => prev + emoji.native);
|
||
}, []);
|
||
|
||
const post = getPostForChat(chatId);
|
||
const postImg = getImageUrl(post?.images?.[0]);
|
||
|
||
return (
|
||
<div className="f-app">
|
||
<div className="chat-page">
|
||
<ChatHeader
|
||
title={post?.title || t('chat.title')}
|
||
onBack={() => navigate("/chats")}
|
||
avatar={postImg ? (
|
||
<img src={postImg} alt="" className="chat-header-avatar-img" />
|
||
) : (
|
||
post?.emoji || "📋"
|
||
)}
|
||
statusLabel={connected ? t('chat.online') : t('chat.offline')}
|
||
statusConnected={connected}
|
||
/>
|
||
|
||
<div className="chat-messages">
|
||
{loadError && <div className="chat-empty">{loadError}</div>}
|
||
{!loadError && messages.length === 0 && (
|
||
<div className="chat-empty">{t('chat.noMessages')}</div>
|
||
)}
|
||
{messages.map((m) => (
|
||
<MessageBubble key={m.id} message={m} isMine={m.senderId === myUserId} />
|
||
))}
|
||
<div ref={messagesEndRef} />
|
||
</div>
|
||
|
||
<ChatInputBar
|
||
input={input}
|
||
onInputChange={setInput}
|
||
onSend={handleSend}
|
||
onEmojiSelect={handleEmojiSelect}
|
||
showEmoji={showEmoji}
|
||
onToggleEmoji={() => setShowEmoji((v) => !v)}
|
||
onCloseEmoji={() => setShowEmoji(false)}
|
||
connected={connected}
|
||
placeholder={t('chat.messagePlaceholder')}
|
||
/>
|
||
</div>
|
||
|
||
{sendError && (
|
||
<Toast
|
||
message={sendError}
|
||
type="error"
|
||
onDismiss={() => setSendError("")}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
} |