Files
nodecrypt/client/js/room.js
T

370 lines
10 KiB
JavaScript

// Room management logic for NodeCrypt web client
// NodeCrypt 网页客户端的房间管理逻辑
import {
createAvatarSVG
} from './util.avatar.js';
import {
renderChatArea,
addSystemMsg,
updateChatInputStyle
} from './chat.js';
import {
renderMainHeader,
renderUserList
} from './ui.js';
import {
escapeHTML
} from './util.string.js';
import {
$id,
createElement
} from './util.dom.js';
let roomsData = [];
let activeRoomIndex = -1;
// Get a new room data object
// 获取一个新的房间数据对象
export function getNewRoomData() {
return {
roomName: '',
userList: [],
userMap: {},
myId: null,
myUserName: '',
chat: null,
messages: [],
prevUserList: [],
knownUserIds: new Set(),
unreadCount: 0,
privateChatTargetId: null,
privateChatTargetName: null
}
}
// Switch to another room by index
// 切换到指定索引的房间
export function switchRoom(index) {
if (index < 0 || index >= roomsData.length) return;
activeRoomIndex = index;
const rd = roomsData[index];
if (typeof rd.unreadCount === 'number') rd.unreadCount = 0;
const sidebarUsername = document.getElementById('sidebar-username');
if (sidebarUsername) sidebarUsername.textContent = rd.myUserName;
setSidebarAvatar(rd.myUserName);
renderRooms(index);
renderMainHeader();
renderUserList(false);
renderChatArea();
updateChatInputStyle()
}
// Set the sidebar avatar
// 设置侧边栏头像
export function setSidebarAvatar(userName) {
if (!userName) return;
const svg = createAvatarSVG(userName);
const el = $id('sidebar-user-avatar');
if (el) {
const cleanSvg = svg.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '');
el.innerHTML = cleanSvg
}
}
// Render the room list
// 渲染房间列表
export function renderRooms(activeId = 0) {
const roomList = $id('room-list');
roomList.innerHTML = '';
roomsData.forEach((rd, i) => {
const div = createElement('div', {
class: 'room' + (i === activeId ? ' active' : ''),
onclick: () => switchRoom(i)
});
const safeRoomName = escapeHTML(rd.roomName);
let unreadHtml = '';
if (rd.unreadCount && i !== activeId) {
unreadHtml = `<span class="room-unread-badge">${rd.unreadCount>99?'99+':rd.unreadCount}</span>`
}
div.innerHTML = `<div class="info"><div class="title">#${safeRoomName}</div></div>${unreadHtml}`;
roomList.appendChild(div)
})
}
// Join a room
// 加入一个房间
export function joinRoom(userName, roomName, password, modal = null, onResult) {
const newRd = getNewRoomData();
newRd.roomName = roomName;
newRd.myUserName = userName;
newRd.password = password;
roomsData.push(newRd);
const idx = roomsData.length - 1;
switchRoom(idx);
const sidebarUsername = $id('sidebar-username');
if (sidebarUsername) sidebarUsername.textContent = userName;
setSidebarAvatar(userName);
let closed = false;
const callbacks = {
onServerClosed: () => {
setStatus('Node connection closed');
if (onResult && !closed) {
closed = true;
onResult(false)
}
},
onServerSecured: () => {
setStatus('Secure connection to node');
if (modal) modal.remove();
else {
const loginContainer = $id('login-container');
if (loginContainer) loginContainer.style.display = 'none';
const chatContainer = $id('chat-container');
if (chatContainer) chatContainer.style.display = ''
}
if (onResult && !closed) {
closed = true;
onResult(true)
}
addSystemMsg("Server connection secured")
},
onClientSecured: (user) => handleClientSecured(idx, user),
onClientList: (list, selfId) => handleClientList(idx, list, selfId),
onClientLeft: (clientId) => handleClientLeft(idx, clientId),
onClientMessage: (msg) => handleClientMessage(idx, msg)
};
const chatInst = new window.NodeCrypt(window.config, callbacks);
chatInst.setCredentials(userName, roomName, password);
chatInst.connect();
roomsData[idx].chat = chatInst
}
// Handle the client list update
// 处理客户端列表更新
export function handleClientList(idx, list, selfId) {
const rd = roomsData[idx];
if (!rd) return;
const oldUserIds = new Set((rd.userList || []).map(u => u.clientId));
const newUserIds = new Set(list.map(u => u.clientId));
for (const oldId of oldUserIds) {
if (!newUserIds.has(oldId)) {
handleClientLeft(idx, oldId)
}
}
rd.userList = list;
rd.userMap = {};
list.forEach(u => {
rd.userMap[u.clientId] = u
});
rd.myId = selfId;
if (activeRoomIndex === idx) {
renderUserList(false);
renderMainHeader()
}
rd.initCount = (rd.initCount || 0) + 1;
if (rd.initCount === 2) {
rd.isInitialized = true;
rd.knownUserIds = new Set(list.map(u => u.clientId))
}
}
// Handle client secured event
// 处理客户端安全连接事件
export function handleClientSecured(idx, user) {
const rd = roomsData[idx];
if (!rd) return;
rd.userMap[user.clientId] = user;
const existingUserIndex = rd.userList.findIndex(u => u.clientId === user.clientId);
if (existingUserIndex === -1) {
rd.userList.push(user)
} else {
rd.userList[existingUserIndex] = user
}
if (activeRoomIndex === idx) {
renderUserList(false);
renderMainHeader()
}
if (!rd.isInitialized) {
return
}
const isNew = !rd.knownUserIds.has(user.clientId);
if (isNew) {
rd.knownUserIds.add(user.clientId);
const name = user.userName || user.username || user.name || 'Anonymous';
const msg = `${name}joined`;
rd.messages.push({
type: 'system',
text: msg
});
if (activeRoomIndex === idx) addSystemMsg(msg, true);
if (window.notifyMessage) {
window.notifyMessage(rd.roomName, 'system', msg)
}
}
}
// Handle client left event
// 处理客户端离开事件
export function handleClientLeft(idx, clientId) {
const rd = roomsData[idx];
if (!rd) return;
if (rd.privateChatTargetId === clientId) {
rd.privateChatTargetId = null;
rd.privateChatTargetName = null;
if (activeRoomIndex === idx) {
updateChatInputStyle()
}
}
const user = rd.userMap[clientId];
const name = user ? (user.userName || user.username || user.name || 'Anonymous') : 'Anonymous';
const msg = `${name}left`;
rd.messages.push({
type: 'system',
text: msg
});
if (activeRoomIndex === idx) addSystemMsg(msg, true);
rd.userList = rd.userList.filter(u => u.clientId !== clientId);
delete rd.userMap[clientId];
if (activeRoomIndex === idx) {
renderUserList(false);
renderMainHeader()
}
}
// Handle client message event
// 处理客户端消息事件
export function handleClientMessage(idx, msg) {
const newRd = roomsData[idx];
if (!newRd) return;
if (msg.userName === newRd.myUserName && !msg.type.includes('_private')) {
const currentRd = roomsData[idx];
if (currentRd && msg.clientId === currentRd.myId && msg.type.includes('_private')) {} else if (msg.clientId === newRd.myId) {
return
}
}
let msgType = msg.type || 'text';
// Handle file messages
if (msgType.startsWith('file_')) {
// Import handleFileMessage function if needed
if (window.handleFileMessage) {
window.handleFileMessage(msg.data, msgType.includes('_private'));
}
// Only add file_start messages to chat history
if (msgType === 'file_start' || msgType === 'file_start_private') {
let realUserName = msg.userName;
if (!realUserName && msg.clientId && newRd.userMap[msg.clientId]) {
realUserName = newRd.userMap[msg.clientId].userName || newRd.userMap[msg.clientId].username || newRd.userMap[msg.clientId].name
}
roomsData[idx].messages.push({
type: 'other',
text: msg.data,
userName: realUserName,
avatar: realUserName,
msgType: msgType.includes('_private') ? 'file_private' : 'file',
timestamp: Date.now()
});
const notificationMsgType = msgType.includes('_private') ? 'private file' : 'file';
if (window.notifyMessage) {
window.notifyMessage(newRd.roomName, notificationMsgType, `${msg.data.fileName}`, realUserName)
}
if (activeRoomIndex !== idx) {
roomsData[idx].unreadCount = (roomsData[idx].unreadCount || 0) + 1;
renderRooms(activeRoomIndex)
} else {
renderChatArea()
}
}
return;
}
// Handle legacy image detection
if (!msgType.includes('_private') && msg.data && msg.data.startsWith('data:image/')) {
msgType = 'image'
}
let realUserName = msg.userName;
if (!realUserName && msg.clientId && newRd.userMap[msg.clientId]) {
realUserName = newRd.userMap[msg.clientId].userName || newRd.userMap[msg.clientId].username || newRd.userMap[msg.clientId].name
}
roomsData[idx].messages.push({
type: 'other',
text: msg.data,
userName: realUserName,
avatar: realUserName,
msgType,
timestamp: Date.now()
});
const notificationMsgType = msgType.includes('_private') ? `private ${msgType.split('_')[0]}` : msgType;
if (window.notifyMessage) {
window.notifyMessage(newRd.roomName, notificationMsgType, msg.data, realUserName)
}
if (activeRoomIndex !== idx) {
roomsData[idx].unreadCount = (roomsData[idx].unreadCount || 0) + 1;
renderRooms(activeRoomIndex)
} else {
renderChatArea()
}
}
// Toggle private chat with a user
// 切换与某用户的私聊
export function togglePrivateChat(targetId, targetName) {
const rd = roomsData[activeRoomIndex];
if (!rd) return;
if (rd.privateChatTargetId === targetId) {
rd.privateChatTargetId = null;
rd.privateChatTargetName = null
} else {
rd.privateChatTargetId = targetId;
rd.privateChatTargetName = targetName
}
renderUserList();
updateChatInputStyle()
}
// Set the status bar text
// 设置状态栏文本
export function setStatus(text) {
let statusBar = $id('status-bar');
if (!statusBar) {
statusBar = createElement('div', {
id: 'status-bar',
style: 'color:green;padding:4px 10px;font-size:13px;'
});
document.body.appendChild(statusBar)
}
statusBar.innerText = text
}
// Exit the current room
// 退出当前房间
export function exitRoom() {
if (activeRoomIndex >= 0 && roomsData[activeRoomIndex]) {
const chatInst = roomsData[activeRoomIndex].chat;
if (chatInst && typeof chatInst.destruct === 'function') {
chatInst.destruct()
} else if (chatInst && typeof chatInst.disconnect === 'function') {
chatInst.disconnect()
}
roomsData[activeRoomIndex].chat = null;
roomsData.splice(activeRoomIndex, 1);
if (roomsData.length > 0) {
switchRoom(0);
return true
} else {
return false
}
}
return false
}
export {
roomsData,
activeRoomIndex
};