This repository has been archived on 2026-05-04. You can view files and clone it, but cannot push or open issues or pull requests.
Galaxy-Strike-Online/client/src/views/GameInterface/tabs/ChatTab.jsx
2026-04-03 03:56:14 +03:00

311 lines
9.7 KiB
JavaScript

import React, { useState, useEffect, useRef } from "react";
import { useSocket } from "../../../hooks/useSocket";
import "./styles/ChatTab.css";
import PlayerManager from "../../../services/PlayerManager";
const ChatTab = () => {
const { socket } = useSocket();
const [activeChat, setActiveChat] = useState("global");
const [searchQuery, setSearchQuery] = useState("");
const [searchResults, setSearchResults] = useState([]);
const [messages, setMessages] = useState([]);
const [friends, setFriends] = useState([]);
const [inputValue, setInputValue] = useState("");
const [showSidebar, setShowSidebar] = useState(true);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const [confirmUnfriend, setConfirmUnfriend] = useState(null);
const [onlineList, setOnlineList] = useState(
PlayerManager.onlinePlayers || [],
);
const messagesEndRef = useRef(null);
const activeChatRef = useRef(activeChat);
useEffect(() => {
activeChatRef.current = activeChat;
}, [activeChat]);
useEffect(() => {
const handleResize = () => {
const mobile = window.innerWidth <= 768;
setIsMobile(mobile);
if (!mobile) setShowSidebar(true);
};
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
if (!socket) return;
socket.emit("friend:get_list");
if (activeChat === "global") {
socket.emit("chat:get_global_history");
} else {
socket.emit("chat:get_private_history", { friendId: activeChat });
}
}, [socket, activeChat]);
useEffect(() => {
if (!socket) return;
const interval = setInterval(() => {
setOnlineList([...PlayerManager.onlinePlayers]);
}, 3000);
const handleNewMessage = (msg) => {
const currentActive = activeChatRef.current;
const isGlobalMatch = currentActive === "global" && msg.type === "global";
const isPrivateMatch =
currentActive !== "global" &&
msg.type === "private" &&
(msg.senderId === currentActive || msg.receiverId === currentActive);
if (isGlobalMatch || isPrivateMatch) {
setMessages((prev) => {
if (prev.some((m) => m.id === msg.id)) return prev;
return [...prev, msg];
});
}
};
const handleHistory = (history) => setMessages(history);
const handleSearchResults = (results) => setSearchResults(results);
const handleFriendList = (list) => setFriends(list);
socket.on("chat:new_message", handleNewMessage);
socket.on("chat:global_history", handleHistory);
socket.on("chat:private_history", handleHistory);
socket.on("player:search_results", handleSearchResults);
socket.on("friend:list", handleFriendList);
return () => {
clearInterval(interval);
socket.off("chat:new_message", handleNewMessage);
socket.off("chat:global_history", handleHistory);
socket.off("chat:private_history", handleHistory);
socket.off("player:search_results", handleSearchResults);
socket.off("friend:list", handleFriendList);
};
}, [socket]);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
const handleSearch = (e) => {
const query = e.target.value;
setSearchQuery(query);
if (query.length > 1) {
socket.emit("player:search", { query });
} else {
setSearchResults([]);
}
};
const addFriend = (player) => {
socket.emit("friend:add", { friendId: player.id });
setSearchQuery("");
setSearchResults([]);
};
const removeFriend = () => {
if (confirmUnfriend) {
socket.emit("friend:remove", { friendId: confirmUnfriend.id });
if (activeChat === confirmUnfriend.id) setActiveChat("global");
setConfirmUnfriend(null);
}
};
const sendMessage = () => {
if (!inputValue.trim() || !socket) return;
socket.emit("chat:send_message", {
content: inputValue,
type: activeChat === "global" ? "global" : "private",
receiverId: activeChat === "global" ? null : activeChat,
});
setInputValue("");
};
const selectChat = (id) => {
setActiveChat(id);
if (isMobile) setShowSidebar(false);
};
const getChatName = () => {
if (activeChat === "global") return "GLOBAL_SYSTEM_CHAT";
const friend = friends.find((f) => f.id === activeChat);
return friend ? friend.username : "UNKNOWN_PILOT";
};
const isFriendOnline = (username) => {
return onlineList.includes(username);
};
return (
<div className={`chat-container ${isMobile ? "mobile" : ""}`}>
{confirmUnfriend && (
<div className="modal-overlay">
<div className="modal-content">
<h3>TERMINATE_CONTACT</h3>
<p>
Are you sure you want to remove {confirmUnfriend.username} from
your contacts?
</p>
<div className="modal-actions">
<button className="confirm-btn" onClick={removeFriend}>
CONFIRM
</button>
<button
className="cancel-btn"
onClick={() => setConfirmUnfriend(null)}
>
CANCEL
</button>
</div>
</div>
</div>
)}
<aside
className={`chat-sidebar ${isMobile && !showSidebar ? "hidden" : ""}`}
>
<div className="search-section">
<div className="card-tag">USER_SEARCH</div>
<div className="search-input-wrapper">
<input
type="text"
placeholder="SEARCH_PILOTS..."
value={searchQuery}
onChange={handleSearch}
/>
</div>
{searchResults.length > 0 && (
<div className="search-results-dropdown">
{searchResults.map((player) => (
<div
key={player.id}
className="search-result-item"
onClick={() => addFriend(player)}
>
<span>
{player.username} (LVL {player.level})
</span>
<i className="fas fa-plus"></i>
</div>
))}
</div>
)}
</div>
<div className="chats-list">
<div
className={`chat-item ${activeChat === "global" ? "active" : ""}`}
onClick={() => selectChat("global")}
>
<div className="chat-item-main">
<i className="fas fa-globe"></i>
<span>GLOBAL_CHANNEL</span>
</div>
</div>
<div className="friends-section-label">
<span className="label-text">CONTACTS</span>
<span className="label-line"></span>
</div>
<div className="friends-list">
{friends.map((friend) => (
<div
key={friend.id}
className={`chat-item ${activeChat === friend.id ? "active" : ""}`}
>
<div
className="chat-item-main"
onClick={() => selectChat(friend.id)}
>
<div
className={`status-dot ${isFriendOnline(friend.username) ? "online" : "offline"}`}
></div>
<span>{friend.username}</span>
</div>
<button
className="unfriend-btn"
onClick={(e) => {
e.stopPropagation();
setConfirmUnfriend(friend);
}}
>
<i className="fas fa-user-minus"></i>
</button>
</div>
))}
</div>
</div>
</aside>
<main className={`chat-main ${isMobile && showSidebar ? "hidden" : ""}`}>
<div className="chat-header">
{isMobile && (
<button className="back-btn" onClick={() => setShowSidebar(true)}>
<i className="fas fa-chevron-left"></i>
</button>
)}
<div className="active-chat-info">
<i
className={
activeChat === "global" ? "fas fa-globe" : "fas fa-user"
}
></i>
<h3>{getChatName()}</h3>
</div>
</div>
<div className="chat-messages">
{messages.length === 0 && (
<div className="message system">
<span className="msg-text">
NO_LOGS_FOUND. SECURE_LINE_READY...
</span>
</div>
)}
{messages.map((msg, index) => (
<div
key={msg.id || index}
className={`message ${msg.senderName === "System" ? "system" : ""}`}
>
<span className="msg-time">
[
{new Date(msg.createdAt).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
]
</span>
<span className="msg-author">{msg.senderName}:</span>
<span className="msg-text">{msg.content}</span>
</div>
))}
<div ref={messagesEndRef} />
</div>
<div className="chat-input-area">
<input
type="text"
placeholder="TYPE_MESSAGE..."
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && sendMessage()}
/>
<button className="send-btn" onClick={sendMessage}>
<i className="fas fa-paper-plane"></i>
</button>
</div>
</main>
</div>
);
};
export default ChatTab;