Added SocialManager, Updated Chat.

This commit is contained in:
MaksSlyzar 2026-04-02 22:41:25 +03:00
parent 1de6fc980d
commit 69f5523454
6 changed files with 513 additions and 107 deletions

View File

@ -12,6 +12,7 @@ const ChatTab = () => {
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [showSidebar, setShowSidebar] = useState(true); const [showSidebar, setShowSidebar] = useState(true);
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768); const [isMobile, setIsMobile] = useState(window.innerWidth <= 768);
const [confirmUnfriend, setConfirmUnfriend] = useState(null);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
@ -80,6 +81,14 @@ const ChatTab = () => {
setSearchResults([]); setSearchResults([]);
}; };
const removeFriend = () => {
if (confirmUnfriend) {
socket.emit("friend:remove", { friendId: confirmUnfriend.id });
if (activeChat === confirmUnfriend.id) setActiveChat("global");
setConfirmUnfriend(null);
}
};
const sendMessage = () => { const sendMessage = () => {
if (!inputValue.trim() || !socket) return; if (!inputValue.trim() || !socket) return;
socket.emit("chat:send_message", { socket.emit("chat:send_message", {
@ -88,8 +97,6 @@ const ChatTab = () => {
receiverId: activeChat === "global" ? null : activeChat, receiverId: activeChat === "global" ? null : activeChat,
}); });
setInputValue(""); setInputValue("");
console.log(activeChat === "global" ? null : activeChat);
}; };
const selectChat = (id) => { const selectChat = (id) => {
@ -105,6 +112,29 @@ const ChatTab = () => {
return ( return (
<div className={`chat-container ${isMobile ? "mobile" : ""}`}> <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 <aside
className={`chat-sidebar ${isMobile && !showSidebar ? "hidden" : ""}`} className={`chat-sidebar ${isMobile && !showSidebar ? "hidden" : ""}`}
> >
@ -142,9 +172,11 @@ const ChatTab = () => {
className={`chat-item ${activeChat === "global" ? "active" : ""}`} className={`chat-item ${activeChat === "global" ? "active" : ""}`}
onClick={() => selectChat("global")} onClick={() => selectChat("global")}
> >
<div className="chat-item-main">
<i className="fas fa-globe"></i> <i className="fas fa-globe"></i>
<span>GLOBAL_CHANNEL</span> <span>GLOBAL_CHANNEL</span>
</div> </div>
</div>
<div className="friends-section-label"> <div className="friends-section-label">
<span className="label-text">CONTACTS</span> <span className="label-text">CONTACTS</span>
@ -156,6 +188,9 @@ const ChatTab = () => {
<div <div
key={friend.id} key={friend.id}
className={`chat-item ${activeChat === friend.id ? "active" : ""}`} className={`chat-item ${activeChat === friend.id ? "active" : ""}`}
>
<div
className="chat-item-main"
onClick={() => selectChat(friend.id)} onClick={() => selectChat(friend.id)}
> >
<div <div
@ -163,6 +198,16 @@ const ChatTab = () => {
></div> ></div>
<span>{friend.username}</span> <span>{friend.username}</span>
</div> </div>
<button
className="unfriend-btn"
onClick={(e) => {
e.stopPropagation();
setConfirmUnfriend(friend);
}}
>
<i className="fas fa-user-minus"></i>
</button>
</div>
))} ))}
</div> </div>
</div> </div>

View File

@ -14,6 +14,7 @@
flex-direction: column; flex-direction: column;
transition: all 0.3s ease; transition: all 0.3s ease;
z-index: 2; z-index: 2;
background: rgba(10, 15, 24, 0.6);
} }
.chat-main { .chat-main {
@ -21,9 +22,303 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background: rgba(0, 0, 0, 0.2); background: rgba(0, 0, 0, 0.2);
position: relative;
} }
/* МОБІЛЬНА ВЕРСІЯ */ /* SEARCH SECTION */
.search-section {
padding: 20px 15px 15px;
border-bottom: 1px solid var(--border-color);
position: relative;
z-index: 10;
}
.search-input-wrapper {
display: flex;
gap: 5px;
margin-top: 10px;
}
.search-input-wrapper input {
flex: 1;
background: #05080c;
border: 1px solid var(--border-color);
color: #fff;
padding: 8px;
font-size: 12px;
font-family: "Geologica", sans-serif;
}
.search-results-dropdown {
position: absolute;
top: 100%;
left: 15px;
right: 15px;
background: #0a0f18;
border: 1px solid var(--primary-color);
border-top: none;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.8);
}
.search-result-item {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
font-size: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.search-result-item:hover {
background: rgba(0, 212, 255, 0.1);
}
/* CHAT LIST & ITEMS */
.chats-list {
flex: 1;
overflow-y: auto;
}
.friends-section-label {
padding: 20px 15px 10px;
display: flex;
align-items: center;
gap: 10px;
opacity: 0.4;
}
.label-text {
font-size: 10px;
letter-spacing: 2px;
font-weight: bold;
}
.label-line {
flex: 1;
height: 1px;
background: var(--border-color);
}
.chat-item {
padding: 12px 15px;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.02);
transition: all 0.2s;
}
.chat-item:hover {
background: rgba(255, 255, 255, 0.03);
}
.chat-item.active {
background: rgba(0, 212, 255, 0.1);
box-shadow: inset 4px 0 0 var(--primary-color);
}
.chat-item-main {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.status-dot.online {
background: #00ff88;
box-shadow: 0 0 5px #00ff88;
}
.status-dot.offline {
background: #444;
}
.unfriend-btn {
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.2);
padding: 8px;
cursor: pointer;
transition: all 0.2s;
opacity: 0;
}
.chat-item:hover .unfriend-btn {
opacity: 1;
}
.unfriend-btn:hover {
color: #ff3e3e;
text-shadow: 0 0 8px rgba(255, 62, 62, 0.4);
}
/* CHAT MAIN AREA */
.chat-header {
padding: 15px;
display: flex;
align-items: center;
border-bottom: 1px solid var(--border-color);
background: rgba(10, 15, 24, 0.8);
}
.active-chat-info {
display: flex;
align-items: center;
gap: 10px;
}
.active-chat-info i {
color: var(--primary-color);
}
.chat-messages {
flex: 1;
padding: 15px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
}
.message {
font-size: 13px;
line-height: 1.4;
}
.msg-time {
color: rgba(255, 255, 255, 0.3);
margin-right: 8px;
font-family: monospace;
}
.msg-author {
color: var(--primary-color);
font-weight: bold;
margin-right: 8px;
}
.message.system .msg-author {
color: #ff3e3e;
}
.message.system .msg-text {
color: #888;
font-style: italic;
}
.chat-input-area {
padding: 15px;
background: #05080c;
display: flex;
gap: 10px;
border-top: 1px solid var(--border-color);
}
.chat-input-area input {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
color: #fff;
padding: 10px 15px;
outline: none;
}
.send-btn {
background: transparent;
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 0 20px;
cursor: pointer;
transition: all 0.2s;
}
.send-btn:hover {
background: var(--primary-color);
color: #000;
}
/* MODAL STYLES */
.modal-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
backdrop-filter: blur(4px);
}
.modal-content {
background: #0a0f18;
border: 1px solid #ff3e3e;
padding: 25px;
width: 90%;
max-width: 350px;
text-align: center;
box-shadow: 0 0 30px rgba(255, 62, 62, 0.15);
}
.modal-content h3 {
color: #ff3e3e;
margin-bottom: 15px;
font-size: 14px;
letter-spacing: 2px;
}
.modal-content p {
color: #aaa;
font-size: 13px;
margin-bottom: 25px;
}
.modal-actions {
display: flex;
gap: 10px;
}
.confirm-btn,
.cancel-btn {
flex: 1;
padding: 10px;
font-size: 11px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.confirm-btn {
background: rgba(255, 62, 62, 0.1);
border: 1px solid #ff3e3e;
color: #ff3e3e;
}
.confirm-btn:hover {
background: #ff3e3e;
color: #fff;
}
.cancel-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
color: #fff;
}
/* MOBILE RESPONSIVENESS */
@media (max-width: 768px) { @media (max-width: 768px) {
.chat-sidebar { .chat-sidebar {
width: 100%; width: 100%;
@ -57,41 +352,19 @@
} }
} }
/* Стилі заголовків та вводу */ /* Контейнер елемента списку */
.search-section {
padding: 20px 15px 15px;
border-bottom: 1px solid var(--border-color);
}
.search-input-wrapper {
display: flex;
gap: 5px;
margin-top: 10px;
}
.search-input-wrapper input {
flex: 1;
background: #05080c;
border: 1px solid var(--border-color);
color: #fff;
padding: 8px;
font-size: 12px;
}
.add-friend-btn {
background: var(--primary-color);
border: none;
width: 35px;
cursor: pointer;
}
.chat-item { .chat-item {
padding: 15px 20px; padding: 12px 15px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; justify-content: space-between;
cursor: pointer; cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.02); border-bottom: 1px solid rgba(255, 255, 255, 0.02);
transition: all 0.2s;
}
.chat-item:hover {
background: rgba(255, 255, 255, 0.03);
} }
.chat-item.active { .chat-item.active {
@ -99,84 +372,42 @@
box-shadow: inset 4px 0 0 var(--primary-color); box-shadow: inset 4px 0 0 var(--primary-color);
} }
.chat-header { .chat-item-main {
padding: 15px;
display: flex; display: flex;
align-items: center; align-items: center;
border-bottom: 1px solid var(--border-color); gap: 12px;
background: rgba(10, 15, 24, 0.8);
}
.chat-messages {
flex: 1; flex: 1;
padding: 15px;
overflow-y: auto;
} }
.chat-input-area { .unfriend-btn {
padding: 15px;
background: #05080c;
display: flex;
gap: 10px;
}
.chat-input-area input {
flex: 1;
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border-color);
color: #fff;
padding: 10px;
}
.send-btn {
background: transparent; background: transparent;
border: 1px solid var(--primary-color); border: none;
color: var(--primary-color); color: rgba(255, 255, 255, 0.3);
padding: 0 15px; padding: 8px;
}
.search-section {
position: relative;
z-index: 10;
}
.search-results-dropdown {
position: absolute;
top: 100%;
left: 15px;
right: 15px;
background: #0a0f18;
border: 1px solid var(--primary-color);
border-top: none;
max-height: 200px;
overflow-y: auto;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
}
.search-result-item {
padding: 10px;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer; cursor: pointer;
font-size: 12px; transition: all 0.2s;
color: #fff; opacity: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
} }
.search-result-item:hover { @media (min-width: 769px) {
background: rgba(0, 212, 255, 0.1); .chat-item:hover .unfriend-btn {
opacity: 1;
}
} }
.search-result-item i { @media (max-width: 768px) {
color: var(--primary-color); .unfriend-btn {
opacity: 1;
color: rgba(255, 255, 255, 0.5);
padding: 12px;
}
.chat-item {
padding: 15px;
}
} }
.message.system .msg-author { .unfriend-btn:hover {
color: #ff3e3e; color: #ff3e3e;
} text-shadow: 0 0 8px rgba(255, 62, 62, 0.4);
.message.system .msg-text {
color: #aaa;
font-style: italic;
} }

View File

@ -0,0 +1,84 @@
const { Player, Friend } = require("../models");
const notificationManager = require("./NotificationManager");
const { Op } = require("sequelize");
class SocialManager {
constructor() {
this.io = null;
}
init(io) {
this.io = io;
}
async searchPlayers(query, excludeId) {
return await Player.findAll({
where: {
username: { [Op.like]: `%${query}%` },
id: { [Op.ne]: excludeId },
},
limit: 5,
attributes: ["id", "username", "level"],
});
}
async sendFriendRequest(sender, targetId) {
await notificationManager.send({
playerId: targetId,
type: "friend_request",
title: "NEW FRIEND REQUEST",
message: `${sender.username} wants to add you as a friend.`,
data: { fromId: sender.id },
priority: "normal",
});
}
async acceptFriendRequest(myId, friendId, notificationId) {
const exists = await Friend.findOne({
where: { playerId: myId, friendId: friendId },
});
if (!exists) {
await Friend.bulkCreate([
{ playerId: myId, friendId: friendId },
{ playerId: friendId, friendId: myId },
]);
await notificationManager.delete(notificationId, myId);
await this.broadcastFriendListUpdate(myId);
await this.broadcastFriendListUpdate(friendId);
return true;
}
return false;
}
async getFriendList(playerId) {
const player = await Player.findByPk(playerId, {
include: [
{
model: Player,
as: "Friends",
attributes: ["id", "username", "level"],
},
],
});
return player?.Friends || [];
}
async broadcastFriendListUpdate(playerId) {
if (!this.io) return;
const list = await this.getFriendList(playerId);
const targetSocket = [...this.io.sockets.sockets.values()].find(
(s) => s.user?.id === playerId,
);
if (targetSocket) {
targetSocket.emit("friend:list", list);
}
}
}
module.exports = new SocialManager();

View File

@ -12,6 +12,7 @@ const app = express();
const economyService = require("./game/EconomyService.js"); const economyService = require("./game/EconomyService.js");
const NotificationManager = require("./game/NotificationManager.js"); const NotificationManager = require("./game/NotificationManager.js");
const AdminManager = require("./game/AdminManager.js"); const AdminManager = require("./game/AdminManager.js");
const SocialManager = require("./game/SocialManager.js");
app.use( app.use(
cors({ cors({
@ -85,6 +86,7 @@ server.listen(config.port, async () => {
NotificationManager.init(io); NotificationManager.init(io);
AdminManager.init(io); AdminManager.init(io);
economyService.init(io); economyService.init(io);
SocialManager.init(io);
await registerInApi(); await registerInApi();
setInterval(sendHeartbeat, HEARTBEAT_INTERVAL); setInterval(sendHeartbeat, HEARTBEAT_INTERVAL);
console.log(`Server running on ${config.host}. PORT: ${config.port}`); console.log(`Server running on ${config.host}. PORT: ${config.port}`);

View File

@ -0,0 +1,44 @@
const socialManager = require("../../game/SocialManager");
module.exports = (io, socket) => {
if (!socialManager.io) socialManager.init(io);
socket.on("player:search", async ({ query }) => {
try {
const players = await socialManager.searchPlayers(query, socket.user.id);
socket.emit("player:search_results", players);
} catch (e) {
console.error(e);
}
});
socket.on("friend:add", async ({ friendId }) => {
try {
await socialManager.sendFriendRequest(socket.user, friendId);
} catch (e) {
socket.emit("error", { message: "FAILED_TO_SEND_REQUEST" });
}
});
socket.on("friend:accept", async ({ friendId, notificationId }) => {
try {
await socialManager.acceptFriendRequest(
socket.user.id,
friendId,
notificationId,
);
} catch (e) {
console.error(e);
socket.emit("error", { message: "FAILED_TO_ACCEPT_FRIEND" });
}
});
socket.on("friend:get_list", async () => {
try {
const list = await socialManager.getFriendList(socket.user.id);
socket.emit("friend:list", list);
} catch (e) {
console.error(e);
}
});
};

View File

@ -5,8 +5,8 @@ const craftingHandler = require("./handlers/craftingHandler");
const adminHandler = require("./handlers/adminHandler"); const adminHandler = require("./handlers/adminHandler");
const dungeonHandler = require("./handlers/dungeonHandler"); const dungeonHandler = require("./handlers/dungeonHandler");
const chatHandler = require("./handlers/chatHandler"); const chatHandler = require("./handlers/chatHandler");
const friendHandler = require("./handlers/friendHandler");
const notificationHandler = require("./handlers/notificationHandler"); const notificationHandler = require("./handlers/notificationHandler");
const socialHandler = require("./handlers/socialHandler");
const initSockets = (io) => { const initSockets = (io) => {
io.use(socketAuth); io.use(socketAuth);
@ -18,7 +18,7 @@ const initSockets = (io) => {
adminHandler(io, socket); adminHandler(io, socket);
dungeonHandler(io, socket); dungeonHandler(io, socket);
chatHandler(io, socket); chatHandler(io, socket);
friendHandler(io, socket); socialHandler(io, socket);
notificationHandler(io, socket); notificationHandler(io, socket);
}); });
}; };