From 4ceba6a60977c1e2b73643c293444ee6fd5ee608 Mon Sep 17 00:00:00 2001
From: shuaiplus <2327005759@qq.com>
Date: Sat, 24 May 2025 18:38:48 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8F=8C=E8=AF=AD=E6=B3=A8?=
=?UTF-8?q?=E9=87=8A?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Dockerfile | 40 ++++++++--
server/server.js | 17 ++++-
vite.config.js | 48 ++++++++++++
web/NodeCrypt.js | 89 +++++++++++++++++++++-
web/chat.js | 31 ++++++++
web/main.js | 176 ++++++++++++++++++++++++++++++-------------
web/room.js | 40 ++++++++++
web/ui.js | 43 +++++++++++
web/util.avatar.js | 9 ++-
web/util.dom.js | 18 +++++
web/util.emoji.js | 15 +++-
web/util.image.js | 39 +++++++++-
web/util.settings.js | 37 ++++++++-
web/util.string.js | 14 ++--
14 files changed, 542 insertions(+), 74 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 68a2491..920fe9c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,47 +1,64 @@
+# Use a Node.js 18 Alpine base image for the backend builder
+# 使用 Node.js 18 Alpine 基础镜像作为后端构建器
FROM docker.1ms.run/node:18-alpine AS backend-builder
WORKDIR /app
-# 只复制package.json先安装依赖,利用Docker缓存层
+# Copy package.json first to leverage Docker's caching
+# 首先复制 package.json 以利用 Docker 的缓存
COPY server/package.json ./server/
+# Install production dependencies
+# 安装生产依赖
RUN cd server && npm install --production --no-package-lock --no-audit
-# 只复制server.js文件
+# Copy only the server.js file
+# 仅复制 server.js 文件
COPY server/server.js ./server/
+# Frontend build stage
# 前端构建阶段
FROM docker.1ms.run/node:18-alpine AS frontend-builder
WORKDIR /app
-# 复制前端项目依赖和配置文件
+# Copy frontend dependencies and configuration files
+# 复制前端依赖和配置文件
COPY package.json package-lock.json* ./
+# Install dependencies
# 安装依赖
RUN npm ci --no-audit || npm install --no-audit
+# Copy source code and configuration files
# 复制源代码和配置文件
COPY vite.config.js ./
COPY web/ ./web/
+# Build the frontend
# 构建前端
RUN npm run build:docker
+# Second stage: Minimal image
# 第二阶段:极小镜像
FROM docker.1ms.run/alpine:3.16
-# 安装最小化版本的Node.js和Nginx
+# Install minimal Node.js and Nginx
+# 安装最小化版本的 Node.js 和 Nginx
RUN apk add --no-cache nodejs nginx && \
mkdir -p /app/server /app/web /run/nginx && \
- # 清理apk缓存
+ # Clean up apk cache
+ # 清理 apk 缓存
rm -rf /var/cache/apk/*
+# Copy server files and static files
# 复制服务器文件和静态文件
COPY --from=backend-builder /app/server/node_modules /app/server/node_modules
COPY --from=backend-builder /app/server/*.js /app/server/
-# 从前端构建阶段复制构建好的文件,而不是复制dist目录
+# Copy built frontend files from the frontend build stage
+# 从前端构建阶段复制构建好的文件,而不是复制 dist 目录
COPY --from=frontend-builder /app/dist/ /app/web/
-# 优化的Nginx配置
+# Optimized Nginx configuration
+# 优化的 Nginx 配置
RUN cat > /etc/nginx/nginx.conf <<'EOF'
worker_processes 1;
worker_rlimit_nofile 512;
@@ -53,6 +70,7 @@ http {
include mime.types;
default_type application/octet-stream;
+ # Optimization settings
# 优化设置
sendfile on;
tcp_nopush on;
@@ -62,10 +80,12 @@ http {
client_max_body_size 1M;
client_body_buffer_size 128k;
- # 禁用访问日志以减少I/O
+ # Disable access logs to reduce I/O
+ # 禁用访问日志以减少 I/O
access_log off;
error_log /dev/null;
+ # Disable unnecessary features
# 禁用不需要的功能
server_tokens off;
@@ -74,6 +94,7 @@ http {
server_name localhost;
# Serve static files
+ # 提供静态文件服务
location / {
root /app/web;
index index.html;
@@ -81,6 +102,7 @@ http {
}
# Proxy WebSocket requests to Node.js backend
+ # 将 WebSocket 请求代理到 Node.js 后端
location /ws {
proxy_pass http://127.0.0.1:8088;
proxy_http_version 1.1;
@@ -94,9 +116,11 @@ EOF
EXPOSE 80
+# Set low memory environment variables and remove unsupported options
# 设置低内存环境变量,去除不支持的选项
ENV NODE_OPTIONS="--max-old-space-size=64" \
NODE_ENV="production"
+# Run in the foreground and combine commands to reduce the number of processes
# 使用前台运行并合并命令减少进程数
CMD ["sh", "-c", "node --expose-gc --unhandled-rejections=strict /app/server/server.js & nginx -g 'daemon off;'"]
\ No newline at end of file
diff --git a/server/server.js b/server/server.js
index b5196d2..90a981e 100644
--- a/server/server.js
+++ b/server/server.js
@@ -5,6 +5,8 @@
const crypto = require('crypto');
const ws = require('ws');
+// Generate a new RSA key pair
+// 生成一个新的 RSA 密钥对
const generateRSAKeyPair = () => {
try {
console.log('Generating new RSA keypair...');
@@ -62,6 +64,8 @@ console.log('server started', config.wsHost, config.wsPort);
var clients = {};
var channels = {};
+// WebSocket server connection event handler
+// WebSocket 服务器连接事件处理程序
wss.on('connection', (connection) => {
if (
@@ -256,6 +260,8 @@ wss.on('connection', (connection) => {
});
+// Process encrypted messages
+// 处理加密消息
const processEncryptedMessage = (clientId, message) => {
let decrypted = null;
@@ -288,6 +294,8 @@ const processEncryptedMessage = (clientId, message) => {
}
};
+// Handle channel join requests
+// 处理加入频道请求
const handleJoinChannel = (clientId, decrypted) => {
if (
!isString(decrypted.p) ||
@@ -314,6 +322,8 @@ const handleJoinChannel = (clientId, decrypted) => {
}
};
+// Handle client messages
+// 处理客户端消息
const handleClientMessage = (clientId, decrypted) => {
if (
!isString(decrypted.p) ||
@@ -345,6 +355,8 @@ const handleClientMessage = (clientId, decrypted) => {
}
};
+// Handle channel messages
+// 处理频道消息
const handleChannelMessage = (clientId, decrypted) => {
if (
!isObject(decrypted.p) ||
@@ -381,6 +393,8 @@ const handleChannelMessage = (clientId, decrypted) => {
}
};
+// Broadcast member list to channel
+// 向频道广播成员列表
const broadcastMemberList = (channel) => {
try {
const members = channels[channel];
@@ -409,7 +423,8 @@ const broadcastMemberList = (channel) => {
-
+// Log events with timestamps and levels
+// 记录带时间戳和级别的事件
const logEvent = (source, message, level) => {
if (
level !== 'debug' ||
diff --git a/vite.config.js b/vite.config.js
index 17a384d..520d875 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -2,22 +2,50 @@ import {
defineConfig
} from 'vite';
export default defineConfig({
+ // Root directory
+ // 根目录
root: 'web',
+ // Base URL
+ // 基础 URL
base: './',
+ // Plugins
+ // 插件
plugins: [],
+ // Build options
+ // 构建选项
build: {
+ // Output directory
+ // 输出目录
outDir: '../dist',
+ // Empty output directory
+ // 清空输出目录
emptyOutDir: true,
+ // Minify
+ // 压缩
minify: 'terser',
+ // Terser options
+ // Terser 选项
terserOptions: {
compress: {
+ // Drop console statements
+ // 删除 console 语句
drop_console: false,
+ // Drop debugger statements
+ // 删除 debugger 语句
drop_debugger: false
}
},
+ // Rollup options
+ // Rollup 选项
rollupOptions: {
+ // Input file
+ // 输入文件
input: 'web/index.html',
+ // Output options
+ // 输出选项
output: {
+ // Manual chunks
+ // 手动分块
manualChunks: (id) => {
if (id.includes('node_modules')) {
if (/aes-js|elliptic|js-chacha20|js-sha256/.test(id)) {
@@ -38,20 +66,40 @@ export default defineConfig({
},
},
},
+ // Sourcemap
+ // Sourcemap
sourcemap: false,
+ // CSS code split
+ // CSS 代码分割
cssCodeSplit: true,
+ // Chunk size warning limit
+ // 代码块大小警告限制
chunkSizeWarningLimit: 1000,
},
+ // Resolve options
+ // 解析选项
resolve: {
+ // Alias
+ // 别名
alias: {
buffer: 'buffer',
},
},
+ // Server options
+ // 服务器选项
server: {
+ // HMR
+ // 热模块替换
hmr: true,
+ // Open browser
+ // 打开浏览器
open: true,
},
+ // Optimize dependencies
+ // 优化依赖
optimizeDeps: {
+ // Include
+ // 包含
include: ['buffer', 'aes-js', 'elliptic', 'js-chacha20', 'js-sha256', '@dicebear/core', '@dicebear/micah'],
},
});
\ No newline at end of file
diff --git a/web/NodeCrypt.js b/web/NodeCrypt.js
index 983bde6..900e394 100644
--- a/web/NodeCrypt.js
+++ b/web/NodeCrypt.js
@@ -1,4 +1,7 @@
-import {
+// NodeCrypt core cryptographic client for secure chat
+// NodeCrypt 安全聊天的核心加密客户端
+
+import {
sha256
} from 'js-sha256';
import {
@@ -12,7 +15,12 @@ import {
Buffer
} from 'buffer';
window.Buffer = Buffer;
+
+// Main NodeCrypt class for secure communication
+// 用于安全通信的 NodeCrypt 主类
class NodeCrypt {
+ // Initialize NodeCrypt instance
+ // 初始化 NodeCrypt 实例
constructor(config = {}, callbacks = {}) {
this.config = {
rsaPublic: config.rsaPublic || '',
@@ -63,6 +71,9 @@ class NodeCrypt {
this.encryptClientMessage = this.encryptClientMessage.bind(this);
this.decryptClientMessage = this.decryptClientMessage.bind(this)
}
+
+ // Set user credentials (username, channel, password)
+ // 设置用户凭证(用户名、频道、密码)
setCredentials(username, channel, password) {
this.logEvent('setCredentials');
try {
@@ -77,6 +88,9 @@ class NodeCrypt {
}
return (true)
}
+
+ // Connect to the server
+ // 连接到服务器
connect() {
if (!this.credentials) {
return (false)
@@ -99,6 +113,9 @@ class NodeCrypt {
}
return (true)
}
+
+ // Clean up and disconnect
+ // 清理并断开连接
destruct() {
this.logEvent('destruct');
this.stopReconnect();
@@ -139,6 +156,9 @@ class NodeCrypt {
this.channel = {};
return (true)
}
+
+ // WebSocket open event handler
+ // WebSocket 连接打开事件处理
async onOpen() {
this.logEvent('onOpen');
this.startPing();
@@ -153,6 +173,9 @@ class NodeCrypt {
this.logEvent('onOpen', error, 'error')
}
}
+
+ // WebSocket message event handler
+ // WebSocket 消息事件处理
async onMessage(event) {
if (!event || !this.isString(event.data)) {
return
@@ -330,6 +353,9 @@ class NodeCrypt {
}
}
}
+
+ // WebSocket error event handler
+ // WebSocket 错误事件处理
async onError(event) {
this.logEvent('onError', event, 'error');
this.disconnect();
@@ -344,6 +370,9 @@ class NodeCrypt {
}
}
}
+
+ // WebSocket close event handler
+ // WebSocket 关闭事件处理
async onClose(event) {
this.logEvent('onClose', event);
this.disconnect();
@@ -358,6 +387,9 @@ class NodeCrypt {
}
}
}
+
+ // Log events for debugging
+ // 记录事件日志用于调试
logEvent(source, message, level) {
if (this.config.debug) {
const date = new Date(),
@@ -365,12 +397,21 @@ class NodeCrypt {
console.log('[' + dateString + ']', (level ? level.toUpperCase() : 'INFO'), source + (message ? ':' : ''), (message ? message : ''))
}
}
+
+ // Check if connection is open
+ // 检查连接是否已打开
isOpen() {
return (this.connection && this.connection.readyState && this.connection.readyState === WebSocket.OPEN ? true : false)
}
+
+ // Check if connection is closed
+ // 检查连接是否已关闭
isClosed() {
return (!this.connection || !this.connection.readyState || this.connection.readyState === WebSocket.CLOSED ? true : false)
}
+
+ // Start reconnect timer
+ // 启动重连定时器
startReconnect() {
this.stopReconnect();
this.logEvent('startReconnect');
@@ -379,6 +420,9 @@ class NodeCrypt {
this.connect()
}, this.config.reconnectDelay)
}
+
+ // Stop reconnect timer
+ // 停止重连定时器
stopReconnect() {
if (this.reconnect) {
this.logEvent('stopReconnect');
@@ -386,6 +430,9 @@ class NodeCrypt {
this.reconnect = null
}
}
+
+ // Start ping timer
+ // 启动心跳定时器
startPing() {
this.stopPing();
this.logEvent('startPing');
@@ -393,6 +440,9 @@ class NodeCrypt {
this.sendMessage('ping')
}, this.config.pingInterval)
}
+
+ // Stop ping timer
+ // 停止心跳定时器
stopPing() {
if (this.ping) {
this.logEvent('stopPing');
@@ -400,6 +450,9 @@ class NodeCrypt {
this.ping = null
}
}
+
+ // Disconnect from server
+ // 从服务器断开连接
disconnect() {
this.stopReconnect();
this.stopPing();
@@ -412,6 +465,9 @@ class NodeCrypt {
}
}
}
+
+ // Send a message to the server
+ // 向服务器发送消息
sendMessage(message) {
try {
if (this.isOpen()) {
@@ -423,6 +479,9 @@ class NodeCrypt {
}
return (false)
}
+
+ // Send a message to all channels
+ // 向所有频道发送消息
sendChannelMessage(type, data) {
if (this.serverShared) {
try {
@@ -456,6 +515,9 @@ class NodeCrypt {
}
return (false)
}
+
+ // Encrypt a message for the server
+ // 加密发送给服务器的消息
encryptServerMessage(message, key) {
let encrypted = '';
try {
@@ -471,6 +533,9 @@ class NodeCrypt {
}
return (encrypted)
}
+
+ // Decrypt a message from the server
+ // 解密来自服务器的消息
decryptServerMessage(message, key) {
let decrypted = {};
try {
@@ -482,6 +547,9 @@ class NodeCrypt {
}
return (decrypted)
}
+
+ // Encrypt a message for a client
+ // 加密发送给客户端的消息
encryptClientMessage(message, key) {
let encrypted = '';
try {
@@ -498,6 +566,9 @@ class NodeCrypt {
}
return (encrypted)
}
+
+ // Decrypt a message from a client
+ // 解密来自客户端的消息
decryptClientMessage(message, key) {
let decrypted = {};
try {
@@ -509,6 +580,9 @@ class NodeCrypt {
}
return (decrypted)
}
+
+ // XOR two hex strings
+ // 对两个十六进制字符串进行异或
xorHex(a, b) {
let result = '',
hexLength = Math.min(a.length, b.length);
@@ -517,15 +591,27 @@ class NodeCrypt {
}
return (result)
}
+
+ // Check if value is a string
+ // 检查值是否为字符串
isString(value) {
return (value && Object.prototype.toString.call(value) === '[object String]' ? true : false)
}
+
+ // Check if value is an array
+ // 检查值是否为数组
isArray(value) {
return (value && Object.prototype.toString.call(value) === '[object Array]' ? true : false)
}
+
+ // Check if value is an object
+ // 检查值是否为对象
isObject(value) {
return (value && Object.prototype.toString.call(value) === '[object Object]' ? true : false)
}
+
+ // Handle server public key
+ // 处理服务器公钥
async handleServerKey(serverKey) {
this.logEvent('handleServerKey', 'Received server key');
localStorage.removeItem(this.SERVER_KEY_STORAGE);
@@ -534,6 +620,7 @@ class NodeCrypt {
return true
}
};
+
if (typeof window !== 'undefined') {
window.NodeCrypt = NodeCrypt
}
\ No newline at end of file
diff --git a/web/chat.js b/web/chat.js
index 3885675..632b1d8 100644
--- a/web/chat.js
+++ b/web/chat.js
@@ -1,3 +1,5 @@
+// Import necessary modules
+// 导入必要的模块
import {
createAvatarSVG
} from './util.avatar.js';
@@ -17,6 +19,9 @@ import {
addClass,
removeClass
} from './util.dom.js';
+
+// Render the chat area
+// 渲染聊天区域
export function renderChatArea() {
const chatArea = $id('chat-area');
if (!chatArea) return;
@@ -31,6 +36,9 @@ export function renderChatArea() {
else addOtherMsg(m.text, m.userName, m.avatar, true, m.msgType || 'text', m.timestamp)
})
}
+
+// Add a message to the chat area
+// 添加消息到聊天区域
export function addMsg(text, isHistory = false, msgType = 'text', timestamp = null) {
let ts = isHistory ? timestamp : (timestamp || Date.now());
if (!ts) return;
@@ -60,6 +68,9 @@ export function addMsg(text, isHistory = false, msgType = 'text', timestamp = nu
chatArea.appendChild(div);
chatArea.scrollTop = chatArea.scrollHeight
}
+
+// Add a message from another user to the chat area
+// 添加来自其他用户的消息到聊天区域
export function addOtherMsg(msg, userName = '', avatar = '', isHistory = false, msgType = 'text', timestamp = null) {
if (!userName && activeRoomIndex >= 0) {
const rd = roomsData[activeRoomIndex];
@@ -109,6 +120,9 @@ export function addOtherMsg(msg, userName = '', avatar = '', isHistory = false,
chatArea.appendChild(bubbleWrap);
chatArea.scrollTop = chatArea.scrollHeight
}
+
+// Add a system message to the chat area
+// 添加系统消息到聊天区域
export function addSystemMsg(text, isHistory = false, timestamp = null) {
if (!isHistory && activeRoomIndex >= 0) {
const ts = timestamp || Date.now();
@@ -127,6 +141,9 @@ export function addSystemMsg(text, isHistory = false, timestamp = null) {
chatArea.appendChild(div);
chatArea.scrollTop = chatArea.scrollHeight
}
+
+// Update the style of the chat input area
+// 更新聊天输入区域的样式
export function updateChatInputStyle() {
const rd = roomsData[activeRoomIndex];
const chatInputArea = $('.chat-input-area');
@@ -145,6 +162,9 @@ export function updateChatInputStyle() {
const html = inputMessageInput.innerHTML.replace(/
(\s*)?/gi, '').replace(/ /g, '').replace(/\u200B/g, '').trim();
placeholder.style.opacity = (html === '') ? '1' : '0'
}
+
+// Setup image preview functionality
+// 设置图片预览功能
export function setupImagePreview() {
on($id('chat-area'), 'click', function(e) {
const target = e.target;
@@ -153,6 +173,9 @@ export function setupImagePreview() {
}
})
}
+
+// Show the image modal
+// 显示图片模态框
export function showImageModal(src) {
const modal = createElement('div', {
class: 'img-modal-bg'
@@ -230,6 +253,9 @@ export function showImageModal(src) {
on($('.img-modal-close', modal), 'click', cleanup);
updateTransform()
}
+
+// Automatically adjust the height of the input area
+// 自动调整输入区域的高度
export function autoGrowInput() {
const input = $('.input-message-input');
if (!input) return;
@@ -237,6 +263,8 @@ export function autoGrowInput() {
input.style.height = input.scrollHeight + 'px'
}
+// Handle pasting text as plain text
+// 处理粘贴为纯文本
function handlePasteAsPlainText(element) {
if (!element) return;
on(element, 'paste', function(e) {
@@ -262,6 +290,9 @@ function handlePasteAsPlainText(element) {
}
})
}
+
+// Setup input placeholder functionality
+// 设置输入框占位符功能
export function setupInputPlaceholder() {
const input = $('.input-message-input');
const placeholder = $('.input-field-placeholder');
diff --git a/web/main.js b/web/main.js
index 503f6fb..dcaa942 100644
--- a/web/main.js
+++ b/web/main.js
@@ -1,96 +1,146 @@
+// 导入 NodeCrypt 模块(加密功能模块)
+// Import the NodeCrypt module (used for encryption)
import './NodeCrypt.js';
+
+// 从 util.image.js 中导入设置图片发送的函数
+// Import setupImageSend function from util.image.js
import {
setupImageSend
} from './util.image.js';
+
+// 从 util.emoji.js 中导入设置表情选择器的函数
+// Import setupEmojiPicker function from util.emoji.js
import {
setupEmojiPicker
} from './util.emoji.js';
+
+// 从 util.settings.js 中导入设置面板的功能函数
+// Import functions for settings panel from util.settings.js
import {
- openSettingsPanel,
- closeSettingsPanel,
- initSettings,
- notifyMessage
+ openSettingsPanel, // 打开设置面板 / Open settings panel
+ closeSettingsPanel, // 关闭设置面板 / Close settings panel
+ initSettings, // 初始化设置 / Initialize settings
+ notifyMessage // 通知信息提示 / Display notification message
} from './util.settings.js';
+
+// 从 util.dom.js 中导入常用 DOM 操作函数
+// Import common DOM manipulation functions from util.dom.js
import {
- $,
- $id,
- removeClass
+ $, // 简化的 document.querySelector / Simplified selector
+ $id, // document.getElementById 的简写 / Shortcut for getElementById
+ removeClass // 移除类名 / Remove a CSS class
} from './util.dom.js';
+
+// 从 room.js 中导入房间管理相关变量和函数
+// Import room-related variables and functions from room.js
import {
- roomsData,
- activeRoomIndex,
- joinRoom
+ roomsData, // 当前所有房间的数据 / Data of all rooms
+ activeRoomIndex, // 当前激活的房间索引 / Index of the active room
+ joinRoom // 加入房间的函数 / Function to join a room
} from './room.js';
+
+// 从 chat.js 中导入聊天功能相关的函数
+// Import chat-related functions from chat.js
import {
- addMsg,
- addSystemMsg,
- setupImagePreview,
- setupInputPlaceholder
+ addMsg, // 添加普通消息到聊天窗口 / Add a normal message to chat
+ addSystemMsg, // 添加系统消息 / Add a system message
+ setupImagePreview, // 设置图片预览功能 / Setup image preview
+ setupInputPlaceholder // 设置输入框的占位提示 / Setup placeholder for input box
} from './chat.js';
+
+// 从 ui.js 中导入 UI 界面相关的功能
+// Import user interface functions from ui.js
import {
- renderUserList,
- renderMainHeader,
- setupMoreBtnMenu,
- preventSpaceInput,
- loginFormHandler,
- openLoginModal,
- setupTabs,
- autofillRoomPwd
+ renderUserList, // 渲染用户列表 / Render user list
+ renderMainHeader, // 渲染主标题栏 / Render main header
+ setupMoreBtnMenu, // 设置更多按钮的下拉菜单 / Setup "more" button menu
+ preventSpaceInput, // 防止输入空格 / Prevent space input in form fields
+ loginFormHandler, // 登录表单提交处理器 / Login form handler
+ openLoginModal, // 打开登录窗口 / Open login modal
+ setupTabs, // 设置页面标签切换 / Setup tab switching
+ autofillRoomPwd // 自动填充房间密码 / Autofill room password
} from './ui.js';
+
+// 设置全局配置参数
+// Set global configuration parameters
window.config = {
- wsAddress: 'wss://crypt.works/ws',
- debug: false
+ wsAddress: 'wss://crypt.works/ws', // WebSocket 服务器地址 / WebSocket server address
+ debug: false // 是否开启调试模式 / Enable debug mode
};
+
+// 把一些函数挂载到 window 对象上供其他模块使用
+// Expose functions to the global window object for accessibility
window.addSystemMsg = addSystemMsg;
window.joinRoom = joinRoom;
window.notifyMessage = notifyMessage;
window.setupEmojiPicker = setupEmojiPicker;
+
+// 当 DOM 内容加载完成后执行初始化逻辑
+// Run initialization logic when the DOM content is fully loaded
window.addEventListener('DOMContentLoaded', () => {
- const loginContainer = $id('login-container');
- const chatContainer = $id('chat-container');
- const loginForm = $id('login-form');
+ const loginContainer = $id('login-container'); // 登录容器 / Login container
+ const chatContainer = $id('chat-container'); // 聊天容器 / Chat container
+ const loginForm = $id('login-form'); // 登录表单 / Login form
+
if (loginForm) {
+ // 监听登录表单提交事件 / Listen to login form submission
loginForm.addEventListener('submit', loginFormHandler(null))
}
- const joinBtn = $('.join-room');
+
+ const joinBtn = $('.join-room'); // 加入房间按钮 / Join room button
if (joinBtn) {
- joinBtn.onclick = openLoginModal
+ joinBtn.onclick = openLoginModal; // 点击打开登录窗口 / Click to open login modal
}
+
+ // 阻止用户输入用户名、房间名和密码时输入空格
+ // Prevent space input for username, room name, and password fields
preventSpaceInput($id('userName'));
preventSpaceInput($id('roomName'));
preventSpaceInput($id('password'));
+
+ // 初始化辅助功能和界面设置
+ // Initialize autofill, input placeholders, and menus
autofillRoomPwd();
setupInputPlaceholder();
setupMoreBtnMenu();
setupImagePreview();
setupEmojiPicker();
initSettings();
- const settingsBtn = $id('settings-btn');
+
+ const settingsBtn = $id('settings-btn'); // 设置按钮 / Settings button
if (settingsBtn) {
settingsBtn.onclick = (e) => {
- e.stopPropagation();
- openSettingsPanel()
+ e.stopPropagation(); // 阻止事件冒泡 / Stop event from bubbling
+ openSettingsPanel(); // 打开设置面板 / Open settings panel
}
}
+
+ // 点击其他地方时关闭设置面板
+ // Close settings panel when clicking outside
document.addEventListener('click', (ev) => {
const panel = $id('settings-panel');
if (panel && panel.style.display === 'block') {
const card = panel.querySelector('.settings-panel-card');
if (card && !card.contains(ev.target) && ev.target.id !== 'settings-btn') {
- closeSettingsPanel()
+ closeSettingsPanel();
}
}
});
- const input = document.querySelector('.input-message-input');
+
+ const input = document.querySelector('.input-message-input'); // 消息输入框 / Message input box
if (input) {
- input.focus();
+ input.focus(); // 自动聚焦 / Auto focus
input.addEventListener('keydown', (e) => {
+ // 按下 Enter 键并且不按 Shift,表示发送消息
+ // Pressing Enter (without Shift) sends the message
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
- const text = input.innerText.trim();
- const rd = roomsData[activeRoomIndex];
+ const text = input.innerText.trim(); // 获取输入的文本 / Get input text
+ const rd = roomsData[activeRoomIndex]; // 当前房间数据 / Current room data
if (text && rd && rd.chat) {
if (rd.privateChatTargetId) {
+ // 私聊消息加密并发送
+ // Encrypt and send private message
const targetClient = rd.chat.channel[rd.privateChatTargetId];
if (targetClient && targetClient.shared) {
const clientMessagePayload = {
@@ -106,28 +156,37 @@ window.addEventListener('DOMContentLoaded', () => {
};
const encryptedMessageForServer = rd.chat.encryptServerMessage(serverRelayPayload, rd.chat.serverShared);
rd.chat.sendMessage(encryptedMessageForServer);
- addMsg(text, false, 'text_private')
+ addMsg(text, false, 'text_private');
} else {
addSystemMsg(`Cannot send private message to ${rd.privateChatTargetName}.User might not be fully connected.`)
}
} else {
+ // 公共频道消息发送
+ // Send public message
rd.chat.sendChannelMessage('text', text);
- addMsg(text)
+ addMsg(text);
}
+ // 清空输入框并触发 input 事件
+ // Clear input and trigger input event
input.innerText = '';
- input.dispatchEvent(new Event('input'))
+ input.dispatchEvent(new Event('input'));
}
}
- })
+ });
}
+
+ // 设置发送图片功能
+ // Setup image sending functionality
setupImageSend({
- inputSelector: '.input-message-input',
- attachBtnSelector: '.chat-attach-btn',
- fileInputSelector: '.new-message-wrapper input[type="file"]',
+ inputSelector: '.input-message-input', // 消息输入框选择器 / Message input selector
+ attachBtnSelector: '.chat-attach-btn', // 附件按钮选择器 / Attach button selector
+ fileInputSelector: '.new-message-wrapper input[type="file"]', // 文件输入框选择器 / File input selector
onSend: (dataUrl) => {
const rd = roomsData[activeRoomIndex];
if (rd && rd.chat) {
if (rd.privateChatTargetId) {
+ // 私聊图片加密并发送
+ // Encrypt and send private image
const targetClient = rd.chat.channel[rd.privateChatTargetId];
if (targetClient && targetClient.shared) {
const clientMessagePayload = {
@@ -143,41 +202,56 @@ window.addEventListener('DOMContentLoaded', () => {
};
const encryptedMessageForServer = rd.chat.encryptServerMessage(serverRelayPayload, rd.chat.serverShared);
rd.chat.sendMessage(encryptedMessageForServer);
- addMsg(dataUrl, false, 'image_private')
+ addMsg(dataUrl, false, 'image_private');
} else {
addSystemMsg(`Cannot send private image to ${rd.privateChatTargetName}.User might not be fully connected.`)
}
} else {
+ // 公共频道发送图片
+ // Send image to public channel
rd.chat.sendChannelMessage('image', dataUrl);
- addMsg(dataUrl, false, 'image')
+ addMsg(dataUrl, false, 'image');
}
}
}
});
+
+ // 判断是否为移动端
+ // Check if the device is mobile
const isMobile = () => window.innerWidth <= 768;
+
+ // 渲染主界面元素
+ // Render main UI elements
renderMainHeader();
renderUserList();
setupTabs();
+
const roomList = $id('room-list');
const sidebar = $id('sidebar');
const rightbar = $id('rightbar');
const sidebarMask = $id('mobile-sidebar-mask');
const rightbarMask = $id('mobile-rightbar-mask');
+
+ // 在移动端点击房间列表后关闭侧边栏
+ // On mobile, clicking room list closes sidebar
if (roomList) {
roomList.addEventListener('click', () => {
if (isMobile()) {
sidebar?.classList.remove('mobile-open');
- sidebarMask?.classList.remove('active')
+ sidebarMask?.classList.remove('active');
}
- })
+ });
}
+
+ // 在移动端点击成员标签后关闭右侧面板
+ // On mobile, clicking member tabs closes right panel
const memberTabs = $id('member-tabs');
if (memberTabs) {
memberTabs.addEventListener('click', () => {
if (isMobile()) {
removeClass(rightbar, 'mobile-open');
- removeClass(rightbarMask, 'active')
+ removeClass(rightbarMask, 'active');
}
- })
+ });
}
-});
\ No newline at end of file
+});
diff --git a/web/room.js b/web/room.js
index 7fb092e..ac7ecd2 100644
--- a/web/room.js
+++ b/web/room.js
@@ -1,3 +1,6 @@
+// Room management logic for NodeCrypt web client
+// NodeCrypt 网页客户端的房间管理逻辑
+
import {
createAvatarSVG
} from './util.avatar.js';
@@ -19,6 +22,9 @@ import {
} from './util.dom.js';
let roomsData = [];
let activeRoomIndex = -1;
+
+// Get a new room data object
+// 获取一个新的房间数据对象
export function getNewRoomData() {
return {
roomName: '',
@@ -35,6 +41,9 @@ export function getNewRoomData() {
privateChatTargetName: null
}
}
+
+// Switch to another room by index
+// 切换到指定索引的房间
export function switchRoom(index) {
if (index < 0 || index >= roomsData.length) return;
activeRoomIndex = index;
@@ -49,6 +58,9 @@ export function switchRoom(index) {
renderChatArea();
updateChatInputStyle()
}
+
+// Set the sidebar avatar
+// 设置侧边栏头像
export function setSidebarAvatar(userName) {
if (!userName) return;
const svg = createAvatarSVG(userName);
@@ -58,6 +70,9 @@ export function setSidebarAvatar(userName) {
el.innerHTML = cleanSvg
}
}
+
+// Render the room list
+// 渲染房间列表
export function renderRooms(activeId = 0) {
const roomList = $id('room-list');
roomList.innerHTML = '';
@@ -75,6 +90,9 @@ export function renderRooms(activeId = 0) {
roomList.appendChild(div)
})
}
+
+// Join a room
+// 加入一个房间
export function joinRoom(userName, roomName, password, modal = null, onResult) {
const newRd = getNewRoomData();
newRd.roomName = roomName;
@@ -120,6 +138,9 @@ export function joinRoom(userName, roomName, password, modal = null, onResult) {
chatInst.connect();
roomsData[idx].chat = chatInst
}
+
+// Handle the client list update
+// 处理客户端列表更新
export function handleClientList(idx, list, selfId) {
const rd = roomsData[idx];
if (!rd) return;
@@ -146,6 +167,9 @@ export function handleClientList(idx, list, selfId) {
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;
@@ -178,6 +202,9 @@ export function handleClientSecured(idx, user) {
}
}
}
+
+// Handle client left event
+// 处理客户端离开事件
export function handleClientLeft(idx, clientId) {
const rd = roomsData[idx];
if (!rd) return;
@@ -203,6 +230,9 @@ export function handleClientLeft(idx, clientId) {
renderMainHeader()
}
}
+
+// Handle client message event
+// 处理客户端消息事件
export function handleClientMessage(idx, msg) {
const newRd = roomsData[idx];
if (!newRd) return;
@@ -239,6 +269,9 @@ export function handleClientMessage(idx, msg) {
renderChatArea()
}
}
+
+// Toggle private chat with a user
+// 切换与某用户的私聊
export function togglePrivateChat(targetId, targetName) {
const rd = roomsData[activeRoomIndex];
if (!rd) return;
@@ -252,6 +285,9 @@ export function togglePrivateChat(targetId, targetName) {
renderUserList();
updateChatInputStyle()
}
+
+// Set the status bar text
+// 设置状态栏文本
export function setStatus(text) {
let statusBar = $id('status-bar');
if (!statusBar) {
@@ -263,6 +299,9 @@ export function setStatus(text) {
}
statusBar.innerText = text
}
+
+// Exit the current room
+// 退出当前房间
export function exitRoom() {
if (activeRoomIndex >= 0 && roomsData[activeRoomIndex]) {
const chatInst = roomsData[activeRoomIndex].chat;
@@ -282,6 +321,7 @@ export function exitRoom() {
}
return false
}
+
export {
roomsData,
activeRoomIndex
diff --git a/web/ui.js b/web/ui.js
index fef4960..e54103a 100644
--- a/web/ui.js
+++ b/web/ui.js
@@ -1,3 +1,6 @@
+// UI logic for NodeCrypt web client
+// NodeCrypt 网页客户端的 UI 逻辑
+
import {
createAvatarSVG
} from './util.avatar.js';
@@ -13,6 +16,9 @@ import {
import {
$id
} from './util.dom.js';
+
+// Render the main header
+// 渲染主标题栏
export function renderMainHeader() {
const rd = roomsData[activeRoomIndex];
let roomName = rd ? rd.roomName : 'Room';
@@ -25,6 +31,9 @@ export function renderMainHeader() {
setupMoreBtnMenu();
setupMobileUIHandlers()
}
+
+// Setup mobile UI event handlers
+// 设置移动端 UI 事件处理
export function setupMobileUIHandlers() {
const sidebar = document.getElementById('sidebar');
const rightbar = document.getElementById('rightbar');
@@ -90,6 +99,9 @@ export function setupMobileUIHandlers() {
}
})
}
+
+// Render the user/member list
+// 渲染用户/成员列表
export function renderUserList(updateHeader = false) {
const userListEl = $id('member-list');
if (!userListEl) return;
@@ -104,6 +116,9 @@ export function renderUserList(updateHeader = false) {
renderMainHeader()
}
}
+
+// Create a user list item
+// 创建一个用户列表项
export function createUserItem(user, isMe) {
const div = document.createElement('div');
const rd = roomsData[activeRoomIndex];
@@ -123,6 +138,9 @@ export function createUserItem(user, isMe) {
}
return div
}
+
+// Setup the 'more' button menu
+// 设置“更多”按钮菜单
export function setupMoreBtnMenu() {
const btn = $id('more-btn');
const menu = $id('more-menu');
@@ -131,6 +149,8 @@ export function setupMoreBtnMenu() {
let animating = false;
let isMouseMoveBound = false;
+ // Mouse move event for closing menu if mouse leaves
+ // 鼠标移出菜单区域时关闭菜单
function onMouseMove(ev) {
if (!menuRect) return;
const mx = ev.clientX,
@@ -144,6 +164,8 @@ export function setupMoreBtnMenu() {
}
}
+ // Bind mousemove event
+ // 绑定鼠标移动事件
function bindMouseMove() {
if (!isMouseMoveBound) {
window.addEventListener('mousemove', onMouseMove);
@@ -151,6 +173,8 @@ export function setupMoreBtnMenu() {
}
}
+ // Unbind mousemove event
+ // 解绑鼠标移动事件
function unbindMouseMove() {
if (isMouseMoveBound) {
window.removeEventListener('mousemove', onMouseMove);
@@ -158,6 +182,8 @@ export function setupMoreBtnMenu() {
}
}
+ // Open the menu
+ // 打开菜单
function openMenu() {
menu.classList.remove('close');
menu.classList.add('open');
@@ -166,6 +192,8 @@ export function setupMoreBtnMenu() {
bindMouseMove()
}
+ // Close the menu
+ // 关闭菜单
function closeMenu() {
if (animating) return;
animating = true;
@@ -226,6 +254,9 @@ export function setupMoreBtnMenu() {
animating = false
})
}
+
+// Prevent space and special character input
+// 禁止输入空格和特殊字符
export function preventSpaceInput(input) {
if (!input) return;
input.addEventListener('keydown', function(e) {
@@ -239,6 +270,9 @@ export function preventSpaceInput(input) {
})
})
}
+
+// Login form submit handler
+// 登录表单提交处理函数
export function loginFormHandler(modal) {
return function(e) {
e.preventDefault();
@@ -296,6 +330,9 @@ export function loginFormHandler(modal) {
})
}
}
+
+// Open the login modal dialog
+// 打开登录弹窗
export function openLoginModal() {
const modal = document.createElement('div');
modal.className = 'login-modal';
@@ -309,6 +346,9 @@ export function openLoginModal() {
form.addEventListener('submit', loginFormHandler(modal));
autofillRoomPwd('userName-modal')
}
+
+// Setup member list tabs
+// 设置成员列表标签页
export function setupTabs() {
const tabs = document.getElementById("member-tabs").children;
for (let i = 0; i < tabs.length; i++) {
@@ -318,6 +358,9 @@ export function setupTabs() {
}
}
}
+
+// Autofill room and password from URL
+// 从 URL 自动填充房间和密码
export function autofillRoomPwd(formPrefix = '') {
const params = new URLSearchParams(window.location.search);
const room = params.get('node');
diff --git a/web/util.avatar.js b/web/util.avatar.js
index 9a33644..f1a410d 100644
--- a/web/util.avatar.js
+++ b/web/util.avatar.js
@@ -1,12 +1,19 @@
+// Import dicebear avatar libraries
+// 导入 dicebear 头像库
import * as dicebearCore from '@dicebear/core';
import * as dicebearMicah from '@dicebear/micah';
+// Predefined background colors
+// 预设背景色数组
const bgColors = ["f87171", "fb923c", "09acf4", "fb923c", "f472b6", "a78bfa", "34d399"];
-
+// Pick a color based on seed string
+// 根据种子字符串选择颜色
function pickColor(seed) {
let hash = 0;
for (let i = 0; i < seed.length; i++) hash = seed.charCodeAt(i) + ((hash << 5) - hash);
return bgColors[Math.abs(hash) % bgColors.length]
}
+// Create SVG avatar for user name
+// 为用户名生成 SVG 头像
export function createAvatarSVG(userName) {
return dicebearCore.createAvatar(dicebearMicah, {
seed: userName,
diff --git a/web/util.dom.js b/web/util.dom.js
index aa74d64..a8002a7 100644
--- a/web/util.dom.js
+++ b/web/util.dom.js
@@ -1,12 +1,20 @@
+// Query single DOM element
+// 查询单个 DOM 元素
export function $(selector, parent = document) {
return parent.querySelector(selector)
}
+// Query multiple DOM elements
+// 查询多个 DOM 元素
export function $$(selector, parent = document) {
return parent.querySelectorAll(selector)
}
+// Get element by ID
+// 通过 ID 获取元素
export function $id(id) {
return document.getElementById(id)
}
+// Create a DOM element with attributes and content
+// 创建带属性和内容的 DOM 元素
export function createElement(tag, attrs = {}, content = '') {
const el = document.createElement(tag);
Object.entries(attrs).forEach(([key, value]) => {
@@ -27,20 +35,30 @@ export function createElement(tag, attrs = {}, content = '') {
}
return el
}
+// Add event listener
+// 添加事件监听器
export function on(target, event, handler, options) {
const el = typeof target === 'string' ? $(target) : target;
if (el) el.addEventListener(event, handler, options)
}
+// Remove event listener
+// 移除事件监听器
export function off(target, event, handler) {
const el = typeof target === 'string' ? $(target) : target;
if (el) el.removeEventListener(event, handler)
}
+// Add class(es) to element
+// 为元素添加类名
export function addClass(el, ...classNames) {
el.classList.add(...classNames)
}
+// Remove class(es) from element
+// 移除元素的类名
export function removeClass(el, ...classNames) {
el.classList.remove(...classNames)
}
+// Toggle class on element
+// 切换元素的类名
export function toggleClass(el, className, force) {
el.classList.toggle(className, force)
}
\ No newline at end of file
diff --git a/web/util.emoji.js b/web/util.emoji.js
index 32fe257..60cd1bb 100644
--- a/web/util.emoji.js
+++ b/web/util.emoji.js
@@ -1,8 +1,12 @@
+// Import DOM helpers
+// 导入 DOM 辅助函数
import {
$,
on
} from './util.dom.js';
import 'emoji-picker-element';
+// Add emoji picker styles to document
+// 向文档添加 emoji 选择器样式
const addEmojiPickerStyles = () => {
if (document.querySelector('#emoji-picker-styles')) return;
const style = document.createElement('style');
@@ -10,6 +14,8 @@ const addEmojiPickerStyles = () => {
style.textContent = `emoji-picker{--background:#fff;--border-color:rgba(0,0,0,0.1);--border-radius:10px;--emoji-padding:0.4rem;--category-emoji-size:1.2rem;--font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;position:absolute;bottom:60px;left:22px;z-index:5;box-shadow:0 3px 12px rgba(0,0,0,0.15);animation:emoji-panel-fade-in 0.18s;width:320px;display:none}@keyframes emoji-panel-fade-in{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}`;
document.head.appendChild(style)
};
+// Setup emoji picker for chat input
+// 为聊天输入框设置 emoji 选择器
export function setupEmojiPicker({
btnSelector = '.chat-emoji-btn',
inputSelector = '.input-message-input'
@@ -25,14 +31,20 @@ export function setupEmojiPicker({
picker.style.display = 'none';
btn.parentNode.style.position = 'relative';
btn.parentNode.appendChild(picker);
+ // Emoji click event
+ // 监听 emoji 点击事件
picker.addEventListener('emoji-click', event => {
insertEmoji(input, event.detail.unicode);
picker.style.display = 'none'
});
+ // Button click toggles picker
+ // 按钮点击切换选择器显示
on(btn, 'click', (ev) => {
ev.stopPropagation();
picker.style.display = picker.style.display === 'none' ? 'block' : 'none'
});
+ // Hide picker when clicking outside
+ // 点击外部隐藏选择器
on(document, 'click', (ev) => {
if (!picker.contains(ev.target) && ev.target !== btn) {
picker.style.display = 'none'
@@ -43,7 +55,8 @@ export function setupEmojiPicker({
console.error('Failed to initialize emoji picker:', error)
}
}
-
+// Insert emoji into input at cursor
+// 在光标处插入 emoji 到输入框
function insertEmoji(input, emoji) {
input.focus();
if (document.getSelection && window.getSelection) {
diff --git a/web/util.image.js b/web/util.image.js
index 5a06ad6..5091ec6 100644
--- a/web/util.image.js
+++ b/web/util.image.js
@@ -1,42 +1,59 @@
+// Import DOM helpers
+// 导入 DOM 辅助函数
import {
$,
on,
createElement
} from './util.dom.js';
+// Process and compress image file
+// 处理并压缩图片文件
export async function processImage(file, callback) {
+ // Create image element for loading file
+ // 创建图片元素用于加载文件
const img = new Image();
img.onload = function() {
- const maxW = 1280,
- maxH = 1280;
+ const maxW = 1280, // Max width
+ maxH = 1280; // 最大宽度/高度
let w = img.naturalWidth,
h = img.naturalHeight;
+ // Resize if too large
+ // 如果图片过大则缩放
if (w > maxW || h > maxH) {
const scale = Math.min(maxW / w, maxH / h);
w = Math.round(w * scale);
h = Math.round(h * scale)
}
+ // Create canvas for drawing image
+ // 创建画布用于绘制图片
const canvas = createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
let dataUrl;
+ // Export as webp
+ // 导出为 webp
dataUrl = canvas.toDataURL('image/webp', 0.80);
callback(dataUrl)
};
+ // Read file as data URL
+ // 以 dataURL 方式读取文件
const reader = new FileReader();
reader.onload = function(e) {
img.src = e.target.result
};
reader.readAsDataURL(file)
}
-
+// Translate message key
+// 翻译消息 key
function t(key) {
const messages = {
- tooLarge: 'Image is too large (over 5MB)',
+ tooLarge: 'Image is too large (over 5MB)', // 图片过大(超过5MB)
};
return messages[key] || key
}
+// Setup image send logic for UI
+// 设置图片发送相关 UI 逻辑
export function setupImageSend({
inputSelector,
attachBtnSelector,
@@ -48,11 +65,19 @@ export function setupImageSend({
const fileInput = $(fileInputSelector);
if (fileInput) fileInput.setAttribute('accept', 'image/*');
if (attachBtn && fileInput) {
+ // Click attach triggers file input
+ // 点击附件按钮触发文件选择
on(attachBtn, 'click', () => fileInput.click());
+ // Handle file input change
+ // 处理文件选择变化
on(fileInput, 'change', async function() {
if (!fileInput.files || !fileInput.files.length) return;
const file = fileInput.files[0];
+ // Only allow image files
+ // 只允许图片文件
if (!file.type.startsWith('image/')) return;
+ // Check file size
+ // 检查文件大小
if (file.size > 5 * 1024 * 1024) {
alert(t('tooLarge'));
return
@@ -64,12 +89,18 @@ export function setupImageSend({
})
}
if (input) {
+ // Paste image from clipboard
+ // 从剪贴板粘贴图片
on(input, 'paste', function(e) {
if (!e.clipboardData) return;
for (const item of e.clipboardData.items) {
+ // Only handle image type
+ // 只处理图片类型
if (item.type.startsWith('image/')) {
const file = item.getAsFile();
if (!file) continue;
+ // Check file size
+ // 检查文件大小
if (file.size > 5 * 1024 * 1024) {
alert(t('tooLarge'));
continue
diff --git a/web/util.settings.js b/web/util.settings.js
index 42216a4..81a76f7 100644
--- a/web/util.settings.js
+++ b/web/util.settings.js
@@ -1,3 +1,5 @@
+// Import DOM utility functions
+// 导入 DOM 工具函数
import {
$,
$$,
@@ -8,11 +10,15 @@ import {
addClass,
removeClass
} from './util.dom.js';
+// Default settings
+// 默认设置
const DEFAULT_SETTINGS = {
notify: false,
sound: false
};
+// Load settings from localStorage
+// 从 localStorage 加载设置
function loadSettings() {
let s = localStorage.getItem('settings');
try {
@@ -26,6 +32,8 @@ function loadSettings() {
}
}
+// Save settings to localStorage
+// 保存设置到 localStorage
function saveSettings(settings) {
const {
notify,
@@ -37,10 +45,14 @@ function saveSettings(settings) {
}))
}
+// Apply settings to the document
+// 应用设置到文档
function applySettings(settings) {
document.documentElement.lang = 'en'
}
+// Ask for browser notification permission
+// 请求浏览器通知权限
function askNotificationPermission(callback) {
if (Notification.requestPermission.length === 0) {
Notification.requestPermission().then(callback)
@@ -49,6 +61,8 @@ function askNotificationPermission(callback) {
}
}
+// Setup the settings panel UI
+// 设置设置面板 UI
function setupSettingsPanel() {
let panel = $id('settings-panel');
if (!panel) {
@@ -99,13 +113,15 @@ function setupSettingsPanel() {
settings.sound = e.target.checked;
if (settings.sound) {
settings.notify = false;
- if (notifyCheckbox) notifyCheckbox.checked = false
+ if (notifyCheckbox) notifyCheckbox.checked = false;
}
saveSettings(settings);
applySettings(settings)
- })
+ });
}
+// Open the settings panel
+// 打开设置面板
function openSettingsPanel() {
setupSettingsPanel();
const panel = $id('settings-panel');
@@ -129,6 +145,7 @@ function openSettingsPanel() {
const mx = ev.clientX,
my = ev.clientY;
const inCard = mx >= cardRect.left - safe && mx <= cardRect.right + safe && my >= cardRect.top - safe && my <= cardRect.bottom + safe;
+ const btnRect = btn.getBoundingClientRect();
const inBtn = mx >= btnRect.left - safe && mx <= btnRect.right + safe && my >= btnRect.top - safe && my <= btnRect.bottom + safe;
if (!inCard && !inBtn) {
closeSettingsPanel()
@@ -152,6 +169,8 @@ function openSettingsPanel() {
panel._unbind = unbindMouseMove
}
+// Close the settings panel
+// 关闭设置面板
function closeSettingsPanel() {
const panel = $id('settings-panel');
if (!panel) return;
@@ -166,16 +185,25 @@ function closeSettingsPanel() {
}, 180)
}
+// Initialize settings on page load
+// 页面加载时初始化设置
function initSettings() {
const settings = loadSettings();
applySettings(settings)
}
+
+// Maximum notification text length
+// 通知文本最大长度
const MAX_NOTIFY_TEXT_LEN = 100;
+// Truncate text for notifications
+// 截断通知文本
function truncateText(text) {
return text.length > MAX_NOTIFY_TEXT_LEN ? text.slice(0, MAX_NOTIFY_TEXT_LEN) + '...' : text
}
+// Play sound notification
+// 播放声音通知
function playSoundNotification() {
try {
const ctx = new(window.AudioContext || window.webkitAudioContext)();
@@ -195,6 +223,8 @@ function playSoundNotification() {
}
}
+// Show desktop notification
+// 显示桌面通知
function showDesktopNotification(roomName, text, msgType, sender) {
if (!('Notification' in window) || Notification.permission !== 'granted') return;
let body;
@@ -216,6 +246,9 @@ function showDesktopNotification(roomName, text, msgType, sender) {
body
})
}
+
+// Notify message entry point
+// 通知消息主入口
export function notifyMessage(roomName, msgType, text, sender) {
const settings = loadSettings();
if (settings.notify) {
diff --git a/web/util.string.js b/web/util.string.js
index d625f96..cecb190 100644
--- a/web/util.string.js
+++ b/web/util.string.js
@@ -1,15 +1,19 @@
+// Escape HTML special characters
+// 转义 HTML 特殊字符
export function escapeHTML(str) {
if (typeof str !== 'string') return '';
return str.replace(/[&<>"']/g, function(c) {
return {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- '\'': '''
+ '&': '&', // & to & // & 转为 &
+ '<': '<', // < to < // < 转为 <
+ '>': '>', // > to > // > 转为 >
+ '"': '"', // " to " // " 转为 "
+ '\'': ''' // ' to ' // ' 转为 '
} [c]
})
}
+// Convert text to HTML, preserving line breaks
+// 将文本转换为 HTML,保留换行符
export function textToHTML(text) {
if (typeof text !== 'string') return '';
return escapeHTML(text).replace(/\n/g, '
')