Archived
feat: rich message notifications and conversation jump
This commit is contained in:
1 parent
0fc9faa72d
commit
b124e57cdb
10 files changed
+254
-35
No files matched your search
@@ -1,12 +1,15 @@
|
||||
# FMCRAFT Chat 桌面版
|
||||
# FMCRAFT Chat Desktop
|
||||
|
||||
Windows 7 及以上可用的 FMCRAFT Chat 桌面客户端(当前提供 x64 便携版)。使用 Electron 22(最后一个支持 Windows 7/8/8.1 的 Electron 版本)直接嵌入网页。
|
||||
Windows 7 及以上可用的 FMCRAFT Chat Desktop 桌面客户端,提供 x64 / ia32 便携版。使用 Electron 22(最后一个支持 Windows 7/8/8.1 的 Electron 版本)直接嵌入网页。
|
||||
|
||||
## 功能
|
||||
|
||||
- 直接加载 `config.ini` 中配置的网页地址
|
||||
- Windows 消息通知
|
||||
- 通过 Windows 托盘气泡发送,兼容 Windows 7/8/8.1/10/11
|
||||
- Windows 10/11 使用系统 Toast,Windows 7/8/8.1 使用托盘气泡
|
||||
- 通知左侧显示发送人头像
|
||||
- 通知标题显示会话名称,正文显示消息内容
|
||||
- 点击通知会打开窗口并跳转到对应会话
|
||||
- 任务栏闪烁
|
||||
- 本地音效播放(不依赖网页音频权限)
|
||||
- 三项通知开关均可在设置中关闭,默认全部开启
|
||||
@@ -26,7 +29,7 @@ flash=true
|
||||
sound=true
|
||||
```
|
||||
|
||||
程序会优先读写可执行文件同目录下的 `config.ini`。如果该目录不可写(例如安装在 `Program Files`),会自动回退到 `%APPDATA%\FMCRAFT Chat\config.ini`,设置页会显示实际使用的路径。
|
||||
程序会优先读写可执行文件同目录下的 `config.ini`。如果该目录不可写(例如安装在 `Program Files`),会自动回退到 `%APPDATA%\FMCRAFT Chat Desktop\config.ini`,设置页会显示实际使用的路径。
|
||||
|
||||
## 开发运行
|
||||
|
||||
@@ -38,10 +41,17 @@ npm start
|
||||
## 打包
|
||||
|
||||
```bash
|
||||
# 64 位
|
||||
npm run dist:x64
|
||||
|
||||
# 32 位(会自动下载并准备 ia32 Electron)
|
||||
npm run dist:ia32
|
||||
```
|
||||
|
||||
生成文件在 `release/` 目录,可直接分发 `FMCRAFT-Chat-1.0.0-x64.exe` 便携版。
|
||||
生成文件在 `release/` 目录:
|
||||
|
||||
- `FMCRAFT-Chat-1.0.0-x64.exe`
|
||||
- `FMCRAFT-Chat-1.0.0-ia32.exe`
|
||||
|
||||
## 通知开关
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 59 KiB |
@@ -10,9 +10,12 @@ const {
|
||||
nativeImage,
|
||||
} = require('electron')
|
||||
const fs = require('node:fs')
|
||||
const os = require('node:os')
|
||||
const path = require('node:path')
|
||||
|
||||
const APP_ID = 'top.fmcraft.chat.desktop'
|
||||
// 没有开始菜单快捷方式时,Windows Toast 会直接显示 AUMID。
|
||||
// 使用可读名称作为 AUMID,确保通知顶部显示 FMCRAFT Chat Desktop。
|
||||
const APP_ID = 'FMCRAFT Chat Desktop'
|
||||
const DEFAULT_URL = 'https://chat.fmcraft.top'
|
||||
const DEFAULT_CONFIG = {
|
||||
url: DEFAULT_URL,
|
||||
@@ -35,7 +38,7 @@ let pendingSound = false
|
||||
let appIcon = null
|
||||
|
||||
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
|
||||
app.setName('FMCRAFT Chat')
|
||||
app.setName('FMCRAFT Chat Desktop')
|
||||
|
||||
function defaultConfigText() {
|
||||
return [
|
||||
@@ -150,7 +153,7 @@ function createMainWindow() {
|
||||
height: 820,
|
||||
minWidth: 960,
|
||||
minHeight: 600,
|
||||
title: 'FMCRAFT Chat',
|
||||
title: 'FMCRAFT Chat Desktop',
|
||||
icon: appIcon || iconPath,
|
||||
backgroundColor: '#0b0e14',
|
||||
webPreferences: {
|
||||
@@ -164,7 +167,7 @@ function createMainWindow() {
|
||||
|
||||
mainWindow.loadURL(config.url).catch((err) => {
|
||||
dialog.showErrorBox(
|
||||
'无法打开 FMCRAFT Chat',
|
||||
'无法打开 FMCRAFT Chat Desktop',
|
||||
`请检查 config.ini 中的页面地址是否可访问。\n\n${String(err && err.message ? err.message : err)}`
|
||||
)
|
||||
})
|
||||
@@ -230,36 +233,85 @@ function playSound(enabled = config.sound) {
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function showWindowsNotification(title, body) {
|
||||
// Windows 7/8 没有 Toast;便携版在 Windows 10/11 上也不一定有开始菜单快捷方式。
|
||||
// 统一使用托盘气泡,保证 Windows 7 及以上都能收到提醒。
|
||||
if (process.platform === 'win32' && tray) {
|
||||
const activeNotifications = new Set()
|
||||
let lastBalloonConversationId = null
|
||||
|
||||
function isLegacyWindows() {
|
||||
return process.platform === 'win32' && /^6\./.test(os.release())
|
||||
}
|
||||
|
||||
function useTrayBalloon() {
|
||||
return process.platform === 'win32' && (process.env.FMCRAFT_FORCE_BALLOON === '1' || isLegacyWindows())
|
||||
}
|
||||
|
||||
function openConversationFromNotification(conversationId) {
|
||||
showMainWindow()
|
||||
if (!conversationId || !mainWindow || mainWindow.isDestroyed()) return
|
||||
const run = () => {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return
|
||||
const id = JSON.stringify(String(conversationId))
|
||||
mainWindow.webContents
|
||||
.executeJavaScript(`window.desktopOpenConversation && window.desktopOpenConversation(${id})`)
|
||||
.catch(() => {})
|
||||
}
|
||||
if (mainWindow.webContents.isLoading()) {
|
||||
mainWindow.webContents.once('did-finish-load', run)
|
||||
} else {
|
||||
setTimeout(run, 80)
|
||||
}
|
||||
}
|
||||
|
||||
function showWindowsNotification({ title, body, icon, conversationId }) {
|
||||
const iconImage = icon || appIcon || iconPath
|
||||
if (useTrayBalloon() && tray) {
|
||||
lastBalloonConversationId = conversationId || null
|
||||
tray.displayBalloon({
|
||||
title,
|
||||
content: body,
|
||||
icon: appIcon || iconPath,
|
||||
icon: iconImage,
|
||||
noSound: true,
|
||||
respectQuietTime: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!Notification.isSupported()) return
|
||||
if (!Notification.isSupported()) {
|
||||
if (tray) {
|
||||
lastBalloonConversationId = conversationId || null
|
||||
tray.displayBalloon({
|
||||
title,
|
||||
content: body,
|
||||
icon: iconImage,
|
||||
noSound: true,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
const notification = new Notification({
|
||||
title,
|
||||
body,
|
||||
icon: appIcon || iconPath,
|
||||
icon: iconImage,
|
||||
silent: true,
|
||||
})
|
||||
notification.on('click', showMainWindow)
|
||||
activeNotifications.add(notification)
|
||||
notification.on('click', () => openConversationFromNotification(conversationId))
|
||||
notification.on('close', () => activeNotifications.delete(notification))
|
||||
notification.show()
|
||||
}
|
||||
|
||||
function handleDesktopNotification(payload, override) {
|
||||
const options = override ? { ...config, ...override } : config
|
||||
const title = String(payload && payload.title ? payload.title : 'FMCRAFT Chat').slice(0, 120)
|
||||
const title = String(payload && payload.title ? payload.title : 'FMCRAFT Chat Desktop').slice(0, 120)
|
||||
const body = String(payload && payload.body ? payload.body : '').slice(0, 500)
|
||||
const conversationId =
|
||||
payload && typeof payload.conversationId === 'string' ? payload.conversationId : null
|
||||
|
||||
if (options.windows) showWindowsNotification(title, body)
|
||||
let icon = null
|
||||
if (payload && typeof payload.avatarUrl === 'string' && payload.avatarUrl.startsWith('data:image/')) {
|
||||
icon = nativeImage.createFromDataURL(payload.avatarUrl)
|
||||
if (icon.isEmpty()) icon = null
|
||||
}
|
||||
|
||||
if (options.windows) showWindowsNotification({ title, body, icon, conversationId })
|
||||
if (options.flash && mainWindow && !mainWindow.isDestroyed() && !mainWindow.isFocused()) {
|
||||
mainWindow.flashFrame(true)
|
||||
}
|
||||
@@ -270,7 +322,7 @@ function updateTrayMenu() {
|
||||
if (!tray || tray.isDestroyed()) return
|
||||
tray.setContextMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{ label: '显示 FMCRAFT Chat', click: showMainWindow },
|
||||
{ label: '显示 FMCRAFT Chat Desktop', click: showMainWindow },
|
||||
{ label: '通知设置…', click: openSettingsWindow },
|
||||
{ type: 'separator' },
|
||||
{
|
||||
@@ -310,10 +362,10 @@ function createTray() {
|
||||
try {
|
||||
const image = nativeImage.createFromPath(trayIconPath)
|
||||
tray = new Tray(image)
|
||||
tray.setToolTip('FMCRAFT Chat')
|
||||
tray.setToolTip('FMCRAFT Chat Desktop')
|
||||
tray.on('double-click', showMainWindow)
|
||||
tray.on('click', showMainWindow)
|
||||
tray.on('balloon-click', showMainWindow)
|
||||
tray.on('balloon-click', () => openConversationFromNotification(lastBalloonConversationId))
|
||||
updateTrayMenu()
|
||||
} catch (err) {
|
||||
console.warn('无法创建托盘图标:', err)
|
||||
@@ -355,12 +407,12 @@ function createMenu() {
|
||||
label: '帮助',
|
||||
submenu: [
|
||||
{
|
||||
label: '关于 FMCRAFT Chat',
|
||||
label: '关于 FMCRAFT Chat Desktop',
|
||||
click: () => {
|
||||
dialog.showMessageBox({
|
||||
type: 'info',
|
||||
title: '关于 FMCRAFT Chat',
|
||||
message: 'FMCRAFT Chat 桌面版',
|
||||
title: '关于 FMCRAFT Chat Desktop',
|
||||
message: 'FMCRAFT Chat Desktop',
|
||||
detail: `版本 ${app.getVersion()}\n适用于 Windows 7 及以上。\n\n配置文件:${configPath}`,
|
||||
buttons: ['确定'],
|
||||
})
|
||||
@@ -386,7 +438,7 @@ function openSettingsWindow() {
|
||||
resizable: true,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
title: 'FMCRAFT Chat 通知设置',
|
||||
title: 'FMCRAFT Chat Desktop 通知设置',
|
||||
icon: appIcon || iconPath,
|
||||
parent: mainWindow || undefined,
|
||||
autoHideMenuBar: true,
|
||||
@@ -427,8 +479,8 @@ function registerIpc() {
|
||||
const options = values && typeof values === 'object' ? values : config
|
||||
handleDesktopNotification(
|
||||
{
|
||||
title: 'FMCRAFT Chat 通知测试',
|
||||
body: '如果你看到这条通知,说明桌面通知工作正常。',
|
||||
title: 'FMCRAFT Chat Desktop',
|
||||
body: '通知测试:如果你看到这条消息,说明桌面通知工作正常。',
|
||||
},
|
||||
options
|
||||
)
|
||||
|
||||
Generated
+70
-1
@@ -10,7 +10,8 @@
|
||||
"license": "UNLICENSED",
|
||||
"devDependencies": {
|
||||
"electron": "22.3.27",
|
||||
"electron-builder": "24.13.3"
|
||||
"electron-builder": "24.13.3",
|
||||
"rcedit": "^5.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@develar/schema-utils": {
|
||||
@@ -1291,6 +1292,31 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn-windows-exe": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn-windows-exe/-/cross-spawn-windows-exe-1.2.0.tgz",
|
||||
"integrity": "sha512-mkLtJJcYbDCxEG7Js6eUnUNndWjyUZwJ3H7bErmmtOYU/Zb99DyUkpamuIZE0b3bhmJyZ7D90uS6f+CGxRRjOw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/malept"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/subscription/pkg/npm-cross-spawn-windows-exe?utm_medium=referral&utm_source=npm_fund"
|
||||
}
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@malept/cross-spawn-promise": "^1.1.0",
|
||||
"is-wsl": "^2.2.0",
|
||||
"which": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"dev": true,
|
||||
@@ -2300,6 +2326,22 @@
|
||||
"is-ci": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/is-docker": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
|
||||
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"is-docker": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"dev": true,
|
||||
@@ -2308,6 +2350,19 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-wsl": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
|
||||
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-docker": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"dev": true,
|
||||
@@ -2820,6 +2875,20 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/rcedit": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/rcedit/-/rcedit-5.0.2.tgz",
|
||||
"integrity": "sha512-dgysxaeXZ4snLpPjn8aVtHvZDCx+aRcvZbaWBgl1poU6OPustMvOkj9a9ZqASQ6i5Y5szJ13LSvglEOwrmgUxA==",
|
||||
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn-windows-exe": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-config-file": {
|
||||
"version": "6.3.2",
|
||||
"dev": true,
|
||||
|
||||
+7
-4
@@ -8,16 +8,19 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"dist": "electron-builder --win portable --x64",
|
||||
"dist:x64": "electron-builder --win portable --x64"
|
||||
"dist": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-win.ps1 -Arch x64",
|
||||
"dist:x64": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-win.ps1 -Arch x64",
|
||||
"prepare:ia32": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/prepare-ia32.ps1",
|
||||
"dist:ia32": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/build-win.ps1 -Arch ia32"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "22.3.27",
|
||||
"electron-builder": "24.13.3"
|
||||
"electron-builder": "24.13.3",
|
||||
"rcedit": "^5.0.2"
|
||||
},
|
||||
"build": {
|
||||
"appId": "top.fmcraft.chat.desktop",
|
||||
"productName": "FMCRAFT Chat",
|
||||
"productName": "FMCRAFT Chat Desktop",
|
||||
"electronDist": "node_modules/electron/dist",
|
||||
"npmRebuild": false,
|
||||
"compression": "normal",
|
||||
|
||||
@@ -9,6 +9,7 @@ contextBridge.exposeInMainWorld('desktopBridge', {
|
||||
body: typeof payload.body === 'string' ? payload.body : '',
|
||||
conversationId:
|
||||
typeof payload.conversationId === 'string' ? payload.conversationId : undefined,
|
||||
avatarUrl: typeof payload.avatarUrl === 'string' ? payload.avatarUrl : undefined,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import path from 'node:path'
|
||||
import { rcedit } from 'rcedit'
|
||||
|
||||
const exePath = process.argv[2]
|
||||
if (!exePath) {
|
||||
console.error('Usage: node scripts/brand-exe.mjs <path-to-exe>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await rcedit(exePath, {
|
||||
icon: path.resolve('assets/icon.ico'),
|
||||
'version-string': {
|
||||
CompanyName: 'FMCRAFT',
|
||||
FileDescription: 'FMCRAFT Chat Desktop',
|
||||
InternalName: 'FMCRAFT Chat Desktop',
|
||||
OriginalFilename: path.basename(exePath),
|
||||
ProductName: 'FMCRAFT Chat Desktop',
|
||||
LegalCopyright: 'Copyright © FMCRAFT',
|
||||
},
|
||||
'file-version': '1.0.0',
|
||||
'product-version': '1.0.0',
|
||||
})
|
||||
console.log(`branded ${exePath}`)
|
||||
@@ -0,0 +1,39 @@
|
||||
param(
|
||||
[ValidateSet('x64', 'ia32')]
|
||||
[string]$Arch = 'x64'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$desktop = Split-Path -Parent $PSScriptRoot
|
||||
Set-Location $desktop
|
||||
|
||||
if (-not $env:ELECTRON_MIRROR) {
|
||||
$env:ELECTRON_MIRROR = 'https://npmmirror.com/mirrors/electron/'
|
||||
}
|
||||
if (-not $env:ELECTRON_BUILDER_BINARIES_MIRROR) {
|
||||
$env:ELECTRON_BUILDER_BINARIES_MIRROR = 'https://npmmirror.com/mirrors/electron-builder-binaries/'
|
||||
}
|
||||
$env:CSC_IDENTITY_AUTO_DISCOVERY = 'false'
|
||||
|
||||
if ($Arch -eq 'ia32') {
|
||||
& (Join-Path $PSScriptRoot 'prepare-ia32.ps1')
|
||||
$electronDist = 'node_modules/electron-ia32/dist'
|
||||
$outDir = 'release/win-ia32-unpacked'
|
||||
} else {
|
||||
$electronDist = 'node_modules/electron/dist'
|
||||
$outDir = 'release/win-unpacked'
|
||||
}
|
||||
|
||||
Write-Host "Building unpacked app ($Arch)..."
|
||||
& .\node_modules\.bin\electron-builder.cmd --win --$Arch --dir --config.electronDist=$electronDist
|
||||
|
||||
$exe = Join-Path $outDir 'FMCRAFT Chat Desktop.exe'
|
||||
if (!(Test-Path $exe)) {
|
||||
throw "未找到打包后的 exe:$exe"
|
||||
}
|
||||
node scripts\brand-exe.mjs $exe
|
||||
|
||||
Write-Host "Building portable exe ($Arch)..."
|
||||
& .\node_modules\.bin\electron-builder.cmd --win portable --$Arch --prepackaged $outDir
|
||||
|
||||
Write-Host "Done: release\FMCRAFT-Chat-1.0.0-$Arch.exe"
|
||||
@@ -0,0 +1,22 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$version = '22.3.27'
|
||||
$url = "https://npmmirror.com/mirrors/electron/$version/electron-v$version-win32-ia32.zip"
|
||||
$zip = Join-Path $env:TEMP "electron-v$version-win32-ia32.zip"
|
||||
$desktop = Split-Path -Parent $PSScriptRoot
|
||||
$target = Join-Path $desktop 'node_modules\electron-ia32'
|
||||
$dist = Join-Path $target 'dist'
|
||||
|
||||
if (!(Test-Path $zip) -or (Get-Item $zip).Length -lt 90000000) {
|
||||
Write-Host "Downloading Electron $version ia32..."
|
||||
curl.exe -L --fail --retry 3 -o $zip $url
|
||||
}
|
||||
|
||||
if (Test-Path $dist) { Remove-Item $dist -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $dist -Force | Out-Null
|
||||
Write-Host "Extracting Electron ia32..."
|
||||
Expand-Archive -Path $zip -DestinationPath $dist -Force
|
||||
|
||||
Set-Content -Path (Join-Path $target 'path.txt') -Value 'electron.exe' -NoNewline -Encoding ascii
|
||||
Set-Content -Path (Join-Path $dist 'version') -Value $version -NoNewline -Encoding ascii
|
||||
Write-Host "Electron ia32 ready: $dist"
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FMCRAFT Chat 通知设置</title>
|
||||
<title>FMCRAFT Chat Desktop 通知设置</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
@@ -90,7 +90,7 @@
|
||||
<input type="checkbox" id="windows" />
|
||||
<span>
|
||||
<strong>Windows 消息通知</strong>
|
||||
<span>通过 Windows 托盘气泡提醒新消息,兼容 Windows 7 及以上。</span>
|
||||
<span>Windows 10/11 使用系统通知,Windows 7/8 使用托盘气泡;显示发送人头像和会话名称。</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
|
||||
Reference in new issue
Block a user