添加双语注释
This commit is contained in:
1 parent
9227d94e79
commit
4ceba6a609
14 files changed
+542
-74
No files matched your search
+32
-8
@@ -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;'"]
|
||||
+16
-1
@@ -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' ||
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
});
|
||||
+88
-1
@@ -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
|
||||
}
|
||||
+31
@@ -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(/<br\s*\/?>(\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');
|
||||
|
||||
+125
-51
@@ -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');
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
+40
@@ -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
|
||||
|
||||
@@ -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');
|
||||
|
||||
+8
-1
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+14
-1
@@ -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) {
|
||||
|
||||
+35
-4
@@ -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
|
||||
|
||||
+35
-2
@@ -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) {
|
||||
|
||||
+9
-5
@@ -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, '<br>')
|
||||
|
||||
Reference in new issue
Block a user