Compare commits

...

10 Commits

Author SHA1 Message Date
CrescentLeaf
fb48c44655 ui: 移除 添加对话 输入框边距 2025-10-06 02:13:25 +08:00
CrescentLeaf
7378024235 feat: 添加任意对话, chore: 使用 User.create (createWithUserNameChecked 已移除) 2025-10-06 02:11:41 +08:00
CrescentLeaf
1c985f28a2 feat: 自动检验用户名和对话 ID 是否已经存在 2025-10-06 02:10:51 +08:00
CrescentLeaf
449c0a8806 feat: 创建群组, impl - 获取群组信息 2025-10-06 02:09:30 +08:00
CrescentLeaf
e1e42ea188 feat: 添加任意对话, 不局限于用户 2025-10-06 02:09:03 +08:00
CrescentLeaf
823eef76b0 feat: 所有对话列表中, 创建群组 2025-10-06 02:08:14 +08:00
CrescentLeaf
3b0b5ff032 feat: 创建群组对话框 2025-10-06 02:07:25 +08:00
CrescentLeaf
6112b4b207 fix: https cannot load pem 2025-10-06 00:56:42 +08:00
CrescentLeaf
9e8e967eb9 chore: remove useless code 2025-10-04 22:18:58 +08:00
CrescentLeaf
697082193f fix: missing contact type of contactsList 2025-10-04 22:18:58 +08:00
12 changed files with 164 additions and 49 deletions

View File

@@ -17,12 +17,6 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<mdui-snackbar close-on-outside-click id="public_snackbar"></mdui-snackbar>
<mdui-dialog close-on-overlay-click id="ErrorDialog">
<span slot="headline">错误</span>
<span slot="description" id="ErrorDialog_Message"></span>
</mdui-dialog>
<script nomodule> <script nomodule>
alert('很抱歉, 此应用无法在较旧的浏览器运行, 请使用基于 Chromium 89+ 的浏览器(内核)使用 :(') alert('很抱歉, 此应用无法在较旧的浏览器运行, 请使用基于 Chromium 89+ 的浏览器(内核)使用 :(')

View File

@@ -10,17 +10,6 @@ import './ui/custom-elements/chat-image.ts'
import './ui/custom-elements/chat-video.ts' import './ui/custom-elements/chat-video.ts'
import './ui/custom-elements/chat-file.ts' import './ui/custom-elements/chat-file.ts'
const urlParams = new URL(location.href).searchParams
// deno-lint-ignore no-window no-window-prefix
urlParams.get('debug') == 'true' && window.addEventListener('error', ({ message, filename, lineno, colno, error }) => {
const m = $("#ErrorDialog_Message")
const d = $("#ErrorDialog").get(0) as Dialog
const s = d.open
d.open = true
m.html((s ? `${m.html()}<br/><br/>` : '') + `${message} (${filename || 'unknown'}:${lineno}:${colno})`)
})
import App from './ui/App.tsx' import App from './ui/App.tsx'
import AppMobile from './ui/AppMobile.tsx' import AppMobile from './ui/AppMobile.tsx'
import isMobileUI from "./ui/isMobileUI.ts" import isMobileUI from "./ui/isMobileUI.ts"

View File

@@ -21,6 +21,7 @@ import useAsyncEffect from "./useAsyncEffect.ts"
import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx" import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx"
import Chat from "../api/client_data/Chat.ts" import Chat from "../api/client_data/Chat.ts"
import AddContactDialog from './dialog/AddContactDialog.tsx' import AddContactDialog from './dialog/AddContactDialog.tsx'
import CreateGroupDialog from './dialog/CreateGroupDialog.tsx'
import UserProfileDialog from "./dialog/UserProfileDialog.tsx" import UserProfileDialog from "./dialog/UserProfileDialog.tsx"
import DataCaches from "../api/DataCaches.ts" import DataCaches from "../api/DataCaches.ts"
@@ -62,6 +63,7 @@ export default function App() {
const [userInfo, setUserInfo] = React.useState(null as unknown as User) const [userInfo, setUserInfo] = React.useState(null as unknown as User)
const addContactDialogRef = React.useRef<Dialog>(null) const addContactDialogRef = React.useRef<Dialog>(null)
const createGroupDialogRef = React.useRef<Dialog>(null)
const chatInfoDialogRef = React.useRef<Dialog>(null) const chatInfoDialogRef = React.useRef<Dialog>(null)
const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat) const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat)
@@ -153,6 +155,9 @@ export default function App() {
<AddContactDialog <AddContactDialog
addContactDialogRef={addContactDialogRef} /> addContactDialogRef={addContactDialogRef} />
<CreateGroupDialog
createGroupDialogRef={createGroupDialogRef} />
<mdui-navigation-rail contained value="Recents" ref={navigationRailRef}> <mdui-navigation-rail contained value="Recents" ref={navigationRailRef}>
<mdui-button-icon slot="top"> <mdui-button-icon slot="top">
<Avatar src={myUserProfileCache?.avatar} text={myUserProfileCache?.nickname} avatarRef={openMyProfileDialogButtonRef} /> <Avatar src={myUserProfileCache?.avatar} text={myUserProfileCache?.nickname} avatarRef={openMyProfileDialogButtonRef} />
@@ -179,6 +184,7 @@ export default function App() {
<ContactsList <ContactsList
openChatInfoDialog={openChatInfoDialog} openChatInfoDialog={openChatInfoDialog}
addContactDialogRef={addContactDialogRef as any} addContactDialogRef={addContactDialogRef as any}
createGroupDialogRef={createGroupDialogRef}
display={navigationItemSelected == "Contacts"} /> display={navigationItemSelected == "Contacts"} />
} }
</div> </div>

View File

@@ -20,6 +20,7 @@ import useAsyncEffect from "./useAsyncEffect.ts"
import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx" import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx"
import Chat from "../api/client_data/Chat.ts" import Chat from "../api/client_data/Chat.ts"
import AddContactDialog from './dialog/AddContactDialog.tsx' import AddContactDialog from './dialog/AddContactDialog.tsx'
import CreateGroupDialog from './dialog/CreateGroupDialog.tsx'
import UserProfileDialog from "./dialog/UserProfileDialog.tsx" import UserProfileDialog from "./dialog/UserProfileDialog.tsx"
import DataCaches from "../api/DataCaches.ts" import DataCaches from "../api/DataCaches.ts"
@@ -58,6 +59,7 @@ export default function AppMobile() {
}) })
const addContactDialogRef = React.useRef<Dialog>(null) const addContactDialogRef = React.useRef<Dialog>(null)
const createGroupDialogRef = React.useRef<Dialog>(null)
const chatInfoDialogRef = React.useRef<Dialog>(null) const chatInfoDialogRef = React.useRef<Dialog>(null)
const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat) const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat)
@@ -177,6 +179,9 @@ export default function AppMobile() {
<AddContactDialog <AddContactDialog
addContactDialogRef={addContactDialogRef} /> addContactDialogRef={addContactDialogRef} />
<CreateGroupDialog
createGroupDialogRef={createGroupDialogRef} />
<mdui-top-app-bar style={{ <mdui-top-app-bar style={{
position: 'sticky', position: 'sticky',
marginTop: '3px', marginTop: '3px',
@@ -221,6 +226,7 @@ export default function AppMobile() {
<ContactsList <ContactsList
openChatInfoDialog={openChatInfoDialog} openChatInfoDialog={openChatInfoDialog}
addContactDialogRef={addContactDialogRef as any} addContactDialogRef={addContactDialogRef as any}
createGroupDialogRef={createGroupDialogRef}
display={navigationItemSelected == "Contacts"} /> display={navigationItemSelected == "Contacts"} />
} }
</div> </div>

View File

@@ -15,11 +15,11 @@ interface Refs {
export default function AddContactDialog({ export default function AddContactDialog({
addContactDialogRef, addContactDialogRef,
}: Refs) { }: Refs) {
const inputUserAccountRef = React.useRef<TextField>(null) const inputTargetRef = React.useRef<TextField>(null)
async function addContact() { async function addContact() {
const re = await Client.invoke("User.addContact", { const re = await Client.invoke("User.addContact", {
account: inputUserAccountRef.current!.value, target: inputTargetRef.current!.value,
token: data.access_token, token: data.access_token,
}) })
@@ -30,14 +30,13 @@ export default function AddContactDialog({
}) })
EventBus.emit('ContactsList.updateContacts') EventBus.emit('ContactsList.updateContacts')
inputUserAccountRef.current!.value = '' inputTargetRef.current!.value = ''
addContactDialogRef.current!.open = false addContactDialogRef.current!.open = false
} }
return ( return (
<mdui-dialog close-on-overlay-click close-on-esc headline="添加對話" ref={addContactDialogRef}> <mdui-dialog close-on-overlay-click close-on-esc headline="添加對話" ref={addContactDialogRef}>
, ... <mdui-text-field clearable label="对话 ID / 用戶 ID / 用戶名" ref={inputTargetRef as any} onKeyDown={(event) => {
<mdui-text-field style={{ marginTop: "10px", }} clearable label="對方的 用戶 ID / 用戶名" ref={inputUserAccountRef as any} onKeyDown={(event) => {
if (event.key == 'Enter') if (event.key == 'Enter')
addContact() addContact()
}}></mdui-text-field> }}></mdui-text-field>

View File

@@ -0,0 +1,54 @@
import * as React from 'react'
import { Button, Dialog, TextField } from "mdui"
import useEventListener from "../useEventListener.ts"
import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts"
import Client from "../../api/Client.ts"
import * as CryptoJS from 'crypto-js'
import data from "../../Data.ts"
import EventBus from "../../EventBus.ts"
interface Refs {
createGroupDialogRef: React.MutableRefObject<Dialog | null>
}
export default function CreateGroupDialog({
createGroupDialogRef,
}: Refs) {
const inputGroupNameRef = React.useRef<TextField>(null)
const inputGroupIdRef = React.useRef<TextField>(null)
async function createGroup() {
const re = await Client.invoke("Chat.createGroup", {
title: inputGroupNameRef.current!.value,
id: inputGroupIdRef.current!.value,
token: data.access_token,
})
if (checkApiSuccessOrSncakbar(re, "添加失敗")) return
snackbar({
message: "创建成功!",
placement: "top",
})
EventBus.emit('ContactsList.updateContacts')
inputGroupNameRef.current!.value = ''
inputGroupIdRef.current!.value = ''
createGroupDialogRef.current!.open = false
}
return (
<mdui-dialog close-on-overlay-click close-on-esc headline="创建群组" ref={createGroupDialogRef}>
<mdui-text-field clearable label="群组名称" ref={inputGroupNameRef as any} onKeyDown={(event) => {
if (event.key == 'Enter')
inputGroupIdRef.current!.click()
}}></mdui-text-field>
<mdui-text-field style={{ marginTop: "10px", }} clearable label="群组 ID (可选)" ref={inputGroupIdRef as any} onKeyDown={(event) => {
if (event.key == 'Enter')
createGroup()
}}></mdui-text-field>
<mdui-button slot="action" variant="text" onClick={() => createGroupDialogRef.current!.open = false}></mdui-button>
<mdui-button slot="action" variant="text" onClick={() => createGroup()}></mdui-button>
</mdui-dialog>
)
}

View File

@@ -13,12 +13,14 @@ interface Args extends React.HTMLAttributes<HTMLElement> {
display: boolean display: boolean
openChatInfoDialog: (chat: Chat) => void openChatInfoDialog: (chat: Chat) => void
addContactDialogRef: React.MutableRefObject<Dialog> addContactDialogRef: React.MutableRefObject<Dialog>
createGroupDialogRef: React.MutableRefObject<Dialog>
} }
export default function ContactsList({ export default function ContactsList({
display, display,
openChatInfoDialog, openChatInfoDialog,
addContactDialogRef, addContactDialogRef,
createGroupDialogRef,
...props ...props
}: Args) { }: Args) {
const searchRef = React.useRef<HTMLElement>(null) const searchRef = React.useRef<HTMLElement>(null)
@@ -62,6 +64,10 @@ export default function ContactsList({
}} icon="person_add" onClick={() => addContactDialogRef.current!.open = true}></mdui-list-item> }} icon="person_add" onClick={() => addContactDialogRef.current!.open = true}></mdui-list-item>
<mdui-list-item rounded style={{ <mdui-list-item rounded style={{
width: '100%', width: '100%',
}} icon="group_add" onClick={() => createGroupDialogRef.current!.open = true}></mdui-list-item>
<mdui-list-item rounded style={{
width: '100%',
marginTop: '13px',
marginBottom: '15px', marginBottom: '15px',
}} icon="refresh" onClick={() => EventBus.emit('ContactsList.updateContacts')}></mdui-list-item> }} icon="refresh" onClick={() => EventBus.emit('ContactsList.updateContacts')}></mdui-list-item>
{/* <mdui-list-item rounded style={{ {/* <mdui-list-item rounded style={{

View File

@@ -7,7 +7,8 @@ import UserChatLinker from "../data/UserChatLinker.ts"
import ApiManager from "./ApiManager.ts" import ApiManager from "./ApiManager.ts"
import BaseApi from "./BaseApi.ts" import BaseApi from "./BaseApi.ts"
import TokenManager from "./TokenManager.ts" import TokenManager from "./TokenManager.ts"
import ChatPrivate from "../data/ChatPrivate.ts"; import ChatPrivate from "../data/ChatPrivate.ts"
import ChatGroup from "../data/ChatGroup.ts"
export default class ChatApi extends BaseApi { export default class ChatApi extends BaseApi {
override getName(): string { override getName(): string {
@@ -56,6 +57,18 @@ export default class ChatApi extends BaseApi {
} }
} }
} }
if (chat!.bean.type == 'group') {
return {
code: 200,
msg: "成功",
data: {
id: args.target as string,
type: chat.bean.type,
title: chat.getTitle(),
avatar: chat.getAvatarFileHash() ? "uploaded_files/" + chat.getAvatarFileHash() : undefined
}
}
}
return { return {
code: 501, code: 501,
@@ -230,6 +243,47 @@ export default class ChatApi extends BaseApi {
} }
} }
}) })
/**
* 创建群组
* @param token 令牌
* @param title 名称
* @param [id] 群组 ID
*/
this.registerEvent("Chat.createGroup", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token', 'title'])) return {
msg: "參數缺失",
code: 400,
}
if (this.checkArgsEmpty(args, ['title'])) return {
msg: "參數不得為空",
code: 400,
}
const token = TokenManager.decode(args.token as string)
if (!this.checkToken(token, deviceId)) return {
code: 401,
msg: "令牌無效",
}
const user = User.findById(token.author) as User
const haveId = args.id && (args.id as string) != ''
if (haveId && Chat.findById(args.id as string) != null) return
const chat = ChatGroup.createGroup(haveId ? undefined : args.id as string)
chat.setTitle(args.title as string)
chat.addMembers([
user.bean.id,
])
user.addContact(chat.bean.id)
return {
code: 200,
msg: '成功',
data: {
chat_id: chat.bean.id,
}
}
})
/** /**
* 从私聊获取对方的 UserId * 从私聊获取对方的 UserId
* @param token 令牌 * @param token 令牌

View File

@@ -102,7 +102,7 @@ export default class UserApi extends BaseApi {
const nickname: string = args.nickname as string const nickname: string = args.nickname as string
const password: string = args.password as string const password: string = args.password as string
const user = User.createWithUserNameChecked(username, password, nickname, null) const user = User.create(username, password, nickname, null)
return { return {
msg: "成功", msg: "成功",
@@ -251,6 +251,7 @@ export default class UserApi extends BaseApi {
const chat = Chat.findById(id) const chat = Chat.findById(id)
return { return {
id, id,
type: chat.bean.type,
title: chat?.getTitle(user) || "未知", title: chat?.getTitle(user) || "未知",
avatar: chat?.getAvatarFileHash(user) ? "uploaded_files/" + chat?.getAvatarFileHash(user) : undefined avatar: chat?.getAvatarFileHash(user) ? "uploaded_files/" + chat?.getAvatarFileHash(user) : undefined
} }
@@ -260,7 +261,7 @@ export default class UserApi extends BaseApi {
}) })
// 添加聯絡人 // 添加聯絡人
this.registerEvent("User.addContact", (args, { deviceId }) => { this.registerEvent("User.addContact", (args, { deviceId }) => {
if (this.checkArgsMissing(args, ['token']) || (args.chat_id == null && args.account == null)) return { if (this.checkArgsMissing(args, ['token', 'target'])) return {
msg: "參數缺失", msg: "參數缺失",
code: 400, code: 400,
} }
@@ -272,19 +273,19 @@ export default class UserApi extends BaseApi {
} }
const user = User.findById(token.author) as User const user = User.findById(token.author) as User
if (args.chat_id) const chat = Chat.findById(args.target as string)
user!.addContact(args.chat_id as string) const targetUser = User.findByAccount(args.target as string) as User
else if (args.account) { if (chat)
const targetUser = User.findByAccount(args.account as string) as User user!.addContact(chat.bean.id)
if (targetUser == null) { else if (targetUser) {
const privChat = ChatPrivate.findOrCreateForPrivate(user, targetUser)
user!.addContact(privChat.bean.id)
} else {
return { return {
msg: "找不到用戶", msg: "找不到目标",
code: 404, code: 404,
} }
} }
const chat = ChatPrivate.findOrCreateForPrivate(user, targetUser)
user!.addContact(chat.bean.id)
}
return { return {
msg: "成功", msg: "成功",

View File

@@ -9,6 +9,7 @@ import chalk from "chalk"
import User from "./User.ts" import User from "./User.ts"
import ChatType from "./ChatType.ts" import ChatType from "./ChatType.ts"
import UserChatLinker from "./UserChatLinker.ts" import UserChatLinker from "./UserChatLinker.ts"
import DataWrongError from '../api/DataWrongError.ts'
/** /**
* Chat.ts - Wrapper and manager * Chat.ts - Wrapper and manager
@@ -48,6 +49,8 @@ export default class Chat {
} }
static create(chatId: string, type: ChatType) { static create(chatId: string, type: ChatType) {
if (this.findAllBeansByCondition('id = ?', chatId).length > 0)
throw new DataWrongError(`对话ID ${chatId} 已被使用`)
const chat = new Chat( const chat = new Chat(
Chat.findAllBeansByCondition( Chat.findAllBeansByCondition(
'count = ?', 'count = ?',
@@ -101,6 +104,11 @@ export default class Chat {
return null return null
} }
setTitle(title: string) {
if (this.bean.type == 'private')
throw new Error('不允许对私聊进行命名')
this.setAttr('title', title)
}
getTitle(userMySelf?: User) { getTitle(userMySelf?: User) {
if (this.bean.type == 'group') return this.bean.title if (this.bean.type == 'group') return this.bean.title
if (this.bean.type == 'private') return this.getAnotherUserForPrivate(userMySelf as User)?.getNickName() if (this.bean.type == 'private') return this.getAnotherUserForPrivate(userMySelf as User)?.getNickName()

View File

@@ -10,11 +10,11 @@ import UserBean from './UserBean.ts'
import FileManager from './FileManager.ts' import FileManager from './FileManager.ts'
import { SQLInputValue } from "node:sqlite" import { SQLInputValue } from "node:sqlite"
import DataWrongError from "../api/DataWrongError.ts"
import ChatPrivate from "./ChatPrivate.ts" import ChatPrivate from "./ChatPrivate.ts"
import Chat from "./Chat.ts" import Chat from "./Chat.ts"
import ChatBean from "./ChatBean.ts" import ChatBean from "./ChatBean.ts"
import MapJson from "../MapJson.ts"; import MapJson from "../MapJson.ts"
import DataWrongError from '../api/DataWrongError.ts'
type UserBeanKey = keyof UserBean type UserBeanKey = keyof UserBean
@@ -45,18 +45,9 @@ export default class User {
return db return db
} }
static createWithUserNameChecked(userName: string | null, password: string, nickName: string, avatar: Buffer | null) { static create(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
if (userName && User.findAllBeansByCondition('username = ?', userName).length > 0) if (userName && User.findAllBeansByCondition('username = ?', userName).length > 0)
throw new DataWrongError(`用户名 ${userName} 已存在`) throw new DataWrongError(`用户名 ${userName} 已存在`)
return User.create(
userName,
password,
nickName,
avatar
)
}
static create(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
const user = new User( const user = new User(
User.findAllBeansByCondition( User.findAllBeansByCondition(
'count = ?', 'count = ?',
@@ -124,6 +115,8 @@ export default class User {
return this.bean.username return this.bean.username
} }
setUserName(userName: string) { setUserName(userName: string) {
if (User.findAllBeansByCondition('username = ?', userName).length > 0)
throw new DataWrongError(`用户名 ${userName} 已存在`)
this.setAttr("username", userName) this.setAttr("username", userName)
} }
updateRecentChat(chatId: string, content: string) { updateRecentChat(chatId: string, content: string) {

View File

@@ -15,6 +15,7 @@ import TokenManager from "./api/TokenManager.ts"
import UserChatLinker from "./data/UserChatLinker.ts" import UserChatLinker from "./data/UserChatLinker.ts"
import path from "node:path" import path from "node:path"
import cookieParser from 'cookie-parser' import cookieParser from 'cookie-parser'
import fs from 'node:fs/promises'
const app = express() const app = express()
app.use('/', express.static(config.data_path + '/page_compiled')) app.use('/', express.static(config.data_path + '/page_compiled'))
@@ -53,7 +54,11 @@ app.get('/uploaded_files/:hash', (req, res) => {
const httpServer: HttpServerLike = ( const httpServer: HttpServerLike = (
((config.server.use == 'http') && http.createServer(app)) || ((config.server.use == 'http') && http.createServer(app)) ||
((config.server.use == 'https') && https.createServer(config.server.https, app)) || ((config.server.use == 'https') && https.createServer({
...config.server.https,
key: await fs.readFile(config.server.https.key),
cert: await fs.readFile(config.server.https.cert),
}, app)) ||
http.createServer(app) http.createServer(app)
) )
const io = new SocketIo.Server(httpServer, { const io = new SocketIo.Server(httpServer, {