From ba71d66db890a158709cebd6a5c2940ca04567b2 Mon Sep 17 00:00:00 2001 From: CrescentLeaf Date: Sun, 19 Oct 2025 18:23:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8A=A0=E5=85=A5=E5=AF=B9=E8=AF=9D?= =?UTF-8?q?=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/api/ApiDeclare.ts | 6 +- client/api/client_data/JoinRequest.ts | 8 + client/ui/chat/ChatFragment.tsx | 39 ++- client/ui/chat/JoinRequestsList.tsx | 104 ++++++++ client/ui/chat/JoinRequestsListItem.tsx | 41 +++ server/api/ApiDeclare.ts | 4 + server/api/ChatApi.ts | 328 ++++++++++++++++-------- server/data/Chat.ts | 117 ++++++--- 8 files changed, 500 insertions(+), 147 deletions(-) create mode 100644 client/api/client_data/JoinRequest.ts create mode 100644 client/ui/chat/JoinRequestsList.tsx create mode 100644 client/ui/chat/JoinRequestsListItem.tsx diff --git a/client/api/ApiDeclare.ts b/client/api/ApiDeclare.ts index 9f9b0cd..896402e 100644 --- a/client/api/ApiDeclare.ts +++ b/client/api/ApiDeclare.ts @@ -17,7 +17,7 @@ export type CallMethod = "User.getMyRecentChats" | "Chat.getInfo" | - + "Chat.updateSettings" | "Chat.createGroup" | @@ -25,6 +25,10 @@ export type CallMethod = "Chat.getIdForPrivate" | "Chat.getAnotherUserIdFromPrivate" | + "Chat.processJoinRequest" | + "Chat.sendJoinRequest" | + "Chat.getJoinRequests" | + "Chat.sendMessage" | "Chat.getMessageHistory" | diff --git a/client/api/client_data/JoinRequest.ts b/client/api/client_data/JoinRequest.ts new file mode 100644 index 0000000..5d3dfe7 --- /dev/null +++ b/client/api/client_data/JoinRequest.ts @@ -0,0 +1,8 @@ +export default class JoinRequest { + declare user_id: string + declare title: string + declare avatar?: string + declare reason?: string + + [key: string]: unknown +} diff --git a/client/ui/chat/ChatFragment.tsx b/client/ui/chat/ChatFragment.tsx index 19a50b9..3c5c406 100644 --- a/client/ui/chat/ChatFragment.tsx +++ b/client/ui/chat/ChatFragment.tsx @@ -27,6 +27,7 @@ import Preference from '../preference/Preference.tsx' import GroupSettings from "../../api/client_data/GroupSettings.ts" import PreferenceUpdater from "../preference/PreferenceUpdater.ts" import SystemMessage from "./SystemMessage.tsx" +import JoinRequestsList from "./JoinRequestsList.tsx"; interface Args extends React.HTMLAttributes { target: string @@ -86,7 +87,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC setChatInfo(chatInfo) if (chatInfo.is_member) - await loadMore() + await loadMore() setTabItemSelected(chatInfo.is_member ? "Chat" : "RequestJoin") if (re.data!.type == 'group') { @@ -253,10 +254,13 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC marginRight: '5px', }}> } - { - chatInfo.title - } - {chatInfo.type == 'group' && 入群申请} + { + chatInfo.is_member ? <> + {chatInfo.title} + {chatInfo.type == 'group' && chatInfo.is_admin && 加入请求} + + : {chatInfo.title} + } 设置
+ +
+ { + const re = await Client.invoke("Chat.sendJoinRequest", { + token: data.access_token, + target: target, + }) + if (re.code != 200) + return checkApiSuccessOrSncakbar(re, "发送加入请求失败") + + snackbar({ + message: '发送成功!', + placement: 'top', + }) + }}>请求加入对话 +
+
- 未制作 + {tabItemSelected == "NewMemberRequests" && } } { + target: string +} + +export default function JoinRequestsList({ + target, + ...props +}: Args) { + const searchRef = React.useRef(null) + const [searchText, setSearchText] = React.useState('') + const [updateJoinRequests, setUpdateJoinRequests] = React.useState([]) + + useEventListener(searchRef, 'input', (e) => { + setSearchText((e.target as unknown as TextField).value) + }) + + useAsyncEffect(async () => { + async function updateJoinRequests() { + const re = await Client.invoke("Chat.getJoinRequests", { + token: data.access_token, + target: target, + }) + if (re.code != 200) + return checkApiSuccessOrSncakbar(re, "获取加入请求列表失败") + + setUpdateJoinRequests(re.data!.join_requests as JoinRequest[]) + } + updateJoinRequests() + EventBus.on('JoinRequestsList.updateJoinRequests', () => updateJoinRequests()) + setTimeout(() => updateJoinRequests(), 15 * 1000) + }) + + async function removeJoinRequest(userId: string) { + const re = await Client.invoke("Chat.processJoinRequest", { + token: data.access_token, + chat_id: target, + user_id: userId, + action: 'remove', + }) + if (re.code != 200) + return checkApiSuccessOrSncakbar(re, "删除加入请求失败") + + EventBus.emit('JoinRequestsList.updateJoinRequests') + } + async function acceptJoinRequest(userId: string) { + const re = await Client.invoke("Chat.processJoinRequest", { + token: data.access_token, + chat_id: target, + user_id: userId, + action: 'accept', + }) + if (re.code != 200) + return checkApiSuccessOrSncakbar(re, "通过加入请求失败") + + EventBus.emit('JoinRequestsList.updateJoinRequests') + } + + return + + + EventBus.emit('JoinRequestsList.updateJoinRequests')}>刷新 + + { + updateJoinRequests.filter((joinRequest) => + searchText == '' || + joinRequest.title.includes(searchText) || + joinRequest.reason?.includes(searchText) || + joinRequest.user_id.includes(searchText) + ).map((v) => + + ) + } + +} \ No newline at end of file diff --git a/client/ui/chat/JoinRequestsListItem.tsx b/client/ui/chat/JoinRequestsListItem.tsx new file mode 100644 index 0000000..e1e0bdd --- /dev/null +++ b/client/ui/chat/JoinRequestsListItem.tsx @@ -0,0 +1,41 @@ +import { $ } from "mdui/jq" +import RecentChat from "../../api/client_data/RecentChat.ts" +import Avatar from "../Avatar.tsx" +import React from 'react' +import JoinRequest from "../../api/client_data/JoinRequest.ts" + +interface Args extends React.HTMLAttributes { + joinRequest: JoinRequest + acceptJoinRequest: (userId: string) => any + removeJoinRequest: (userId: string) => any +} + +export default function JoinRequestsListItem({ joinRequest, acceptJoinRequest, removeJoinRequest }: Args) { + const { user_id, title, avatar, reason } = joinRequest + + const itemRef = React.useRef(null) + React.useEffect(() => { + $(itemRef.current!.shadowRoot).find('.headline').css('margin-top', '3px') + }) + return ( + + {title} + + 请求原因: {reason || "无"} +
+ acceptJoinRequest(user_id)}> + removeJoinRequest(user_id)}> +
+
+ ) +} diff --git a/server/api/ApiDeclare.ts b/server/api/ApiDeclare.ts index 96ee4b4..daeea21 100644 --- a/server/api/ApiDeclare.ts +++ b/server/api/ApiDeclare.ts @@ -25,6 +25,10 @@ export type CallMethod = "Chat.getIdForPrivate" | "Chat.getAnotherUserIdFromPrivate" | + "Chat.processJoinRequest" | + "Chat.sendJoinRequest" | + "Chat.getJoinRequests" | + "Chat.sendMessage" | "Chat.getMessageHistory" | diff --git a/server/api/ChatApi.ts b/server/api/ChatApi.ts index 5448ab3..f9b9e1b 100644 --- a/server/api/ChatApi.ts +++ b/server/api/ChatApi.ts @@ -18,71 +18,10 @@ export default class ChatApi extends BaseApi { } override onInit(): void { /** - * 獲取對話訊息 - * @param token 令牌 - * @param target 目標對話 + * ====================================================== + * 对话消息 + * ====================================================== */ - this.registerEvent("Chat.getInfo", (args, { deviceId }) => { - if (this.checkArgsMissing(args, ['token', 'target'])) return { - msg: "参数缺失", - code: 400, - } - - const token = TokenManager.decode(args.token as string) - if (!this.checkToken(token, deviceId)) return { - code: 401, - msg: "令牌无效", - } - - const chat = Chat.findById(args.target as string) - if (chat == null) return { - code: 404, - msg: "对话不存在", - } - - // 私聊 - if (chat!.bean.type == 'private') { - if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { - code: 403, - msg: "用户无权访问此对话", - } - const mine = User.findById(token.author) as User - - return { - code: 200, - msg: "成功", - data: { - id: args.target as string, - type: chat.bean.type, - title: chat.getTitle(mine), - avatar: chat.getAvatarFileHash(mine) ? "uploaded_files/" + chat.getAvatarFileHash(mine) : undefined, - settings: JSON.parse(chat.bean.settings), - is_member: true, - is_admin: true, - } - } - } - 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, - settings: JSON.parse(chat.bean.settings), - is_member: UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id), - is_admin: chat.checkUserIsAdmin(token.author), - } - } - } - - return { - code: 404, - msg: "找不到对话", - } - }) /** * 發送訊息 * @param token 令牌 @@ -217,6 +156,146 @@ export default class ChatApi extends BaseApi { }, } }) + /** + * ====================================================== + * 加入对话申请 + * ====================================================== + */ + /** + * 获取所有的加入对话申请 + * @param token 令牌 + * @param target ID + */ + this.registerEvent("Chat.getJoinRequests", (args, { deviceId }) => { + if (this.checkArgsMissing(args, ['token', 'target'])) return { + msg: "参数缺失", + code: 400, + } + + const token = TokenManager.decode(args.token as string) + if (!this.checkToken(token, deviceId)) return { + code: 401, + msg: "令牌无效", + } + + const chat = Chat.findById(args.target as string) + if (chat == null) return { + code: 404, + msg: "对话不存在", + } + if (!chat.checkUserIsAdmin(token.author)) return { + code: 403, + msg: "没有此权限", + } + + return { + code: 200, + msg: '成功', + data: { + join_requests: chat.getJoinRequests().map((v) => { + const user = User.findById(v.user_id as string) + return { + user_id: user?.bean.id, + reason: v.reason, + title: user!.getNickName(), + avatar: user!.getAvatarFileHash() ? "uploaded_files/" + user!.getAvatarFileHash() : null, + } + }), + } + } + }) + /** + * 处理加入对话申请 + * @param token 令牌 + */ + this.registerEvent("Chat.processJoinRequest", (args, { deviceId }) => { + if (this.checkArgsMissing(args, ['token', 'chat_id', 'user_id', 'action'])) return { + msg: "参数缺失", + code: 400, + } + const action = args.action as string + + const token = TokenManager.decode(args.token as string) + if (!this.checkToken(token, deviceId)) return { + code: 401, + msg: "令牌无效", + } + + const chat = Chat.findById(args.chat_id as string) + if (chat == null) return { + code: 404, + msg: "对话不存在", + } + if (!chat.checkUserIsAdmin(token.author)) return { + code: 403, + msg: "没有此权限", + } + + const admin = User.findById(token.author) + + if (chat.getJoinRequests().map((v) => v.user_id).indexOf(args.user_id as string) != -1) { + const user = User.findById(args.user_id as string) + if (user == null) { + chat.removeJoinRequests([ + args.user_id as string, + ]) + } else { + if (action == 'accept') { + chat.addMembers([ + args.user_id as string, + ]) + MessagesManager.getInstanceForChat(chat).addSystemMessage(`${user.getNickName()} 经 ${admin?.getNickName()} 批准加入了对话`) + } + if (action == 'accept' || action == 'remove') + chat.removeJoinRequests([ + args.user_id as string, + ]) + } + } + + return { + code: 200, + msg: '成功', + } + }) + /** + * 加入群组 + * @param token 令牌 + * @param target ID + */ + this.registerEvent("Chat.sendJoinRequest", (args, { deviceId }) => { + if (this.checkArgsMissing(args, ['token', 'target'])) return { + msg: "参数缺失", + code: 400, + } + + const token = TokenManager.decode(args.token as string) + if (!this.checkToken(token, deviceId)) return { + code: 401, + msg: "令牌无效", + } + + const chat = Chat.findById(args.target as string) + if (chat == null) return { + code: 404, + msg: "对话不存在", + } + + chat.addJoinRequest(token.author, args.reason as string) + + return { + code: 200, + msg: '成功', + data: { + chat_id: chat.bean.id, + } + } + }) + /** + * ====================================================== + * 创建对话 + * ====================================================== + */ /** * 获取私聊的 ChatId * @param token 令牌 @@ -243,44 +322,6 @@ export default class ChatApi extends BaseApi { } const chat = ChatPrivate.findOrCreateForPrivate(user, targetUser) - return { - code: 200, - msg: '成功', - data: { - chat_id: chat.bean.id, - } - } - }) - /** - * 加入群组 - * @param token 令牌 - * @param target ID - */ - this.registerEvent("Chat.requestJoinGroup", (args, { deviceId }) => { - if (this.checkArgsMissing(args, ['token', 'target'])) 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 chat = Chat.findById(args.target as string) - if (chat == null) return { - code: 404, - msg: "对话不存在", - } - if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { - code: 403, - msg: "用户无权访问此对话", - } - - - return { code: 200, msg: '成功', @@ -311,14 +352,14 @@ export default class ChatApi extends BaseApi { msg: "令牌无效", } const user = User.findById(token.author) as User - - const haveId = args.id && (args.id as string) != '' + + const haveId = args.id && ((args.id as string) != '') if (haveId && Chat.findById(args.id as string) != null) return { msg: "对话 ID 已被占用", code: 403, } - - const chat = ChatGroup.createGroup(haveId ? undefined : args.id as string) + + const chat = ChatGroup.createGroup(haveId ? args.id as string : undefined) chat.setTitle(args.title as string) chat.addMembers([ user.bean.id, @@ -337,6 +378,77 @@ export default class ChatApi extends BaseApi { } } }) + /** + * ====================================================== + * 对话信息 + * ====================================================== + */ + /** + * 獲取對話訊息 + * @param token 令牌 + * @param target 目標對話 + */ + this.registerEvent("Chat.getInfo", (args, { deviceId }) => { + if (this.checkArgsMissing(args, ['token', 'target'])) return { + msg: "参数缺失", + code: 400, + } + + const token = TokenManager.decode(args.token as string) + if (!this.checkToken(token, deviceId)) return { + code: 401, + msg: "令牌无效", + } + + const chat = Chat.findById(args.target as string) + if (chat == null) return { + code: 404, + msg: "对话不存在", + } + + // 私聊 + if (chat!.bean.type == 'private') { + if (!UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id)) return { + code: 403, + msg: "用户无权访问此对话", + } + const mine = User.findById(token.author) as User + + return { + code: 200, + msg: "成功", + data: { + id: args.target as string, + type: chat.bean.type, + title: chat.getTitle(mine), + avatar: chat.getAvatarFileHash(mine) ? "uploaded_files/" + chat.getAvatarFileHash(mine) : undefined, + settings: JSON.parse(chat.bean.settings), + is_member: true, + is_admin: true, + } + } + } + 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, + settings: JSON.parse(chat.bean.settings), + is_member: UserChatLinker.checkUserIsLinkedToChat(token.author, chat!.bean.id), + is_admin: chat.checkUserIsAdmin(token.author), + } + } + } + + return { + code: 404, + msg: "找不到对话", + } + }) /** * 更新设定 * @param token 令牌 @@ -355,7 +467,7 @@ export default class ChatApi extends BaseApi { msg: "令牌无效", } const user = User.findById(token.author) as User - + const chat = Chat.findById(args.target as string) if (chat == null) return { code: 404, @@ -413,7 +525,7 @@ export default class ChatApi extends BaseApi { user_id: chat.getAnotherUserForPrivate(user)?.bean.id } } - + return { code: 403, msg: "非私聊对话", diff --git a/server/data/Chat.ts b/server/data/Chat.ts index bb4c7e2..7ded1ba 100644 --- a/server/data/Chat.ts +++ b/server/data/Chat.ts @@ -17,12 +17,11 @@ import DataWrongError from '../api/DataWrongError.ts' * Manage the database by itself (static) */ export default class Chat { - static table_name: string = "Chat" private static database: DatabaseSync = Chat.init() private static init(): DatabaseSync { const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, 'Chats.db')) db.exec(` - CREATE TABLE IF NOT EXISTS ${Chat.table_name} ( + CREATE TABLE IF NOT EXISTS Chat ( /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, /* 类型 */ type TEXT NOT NULL, /* ID */ id TEXT NOT NULL, @@ -30,24 +29,16 @@ export default class Chat { /* 头像 */ avatar BLOB, /* 设置 */ settings TEXT NOT NULL ); - `) - db.exec(` - CREATE TABLE IF NOT EXISTS ChatAdmin ( - /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, - /* 用戶 ID */ user_id TEXT NOT NULL, - /* Chat ID */ chat_id TEXT NOT NULL, - /* 管理权限 */ permissions TEXT NOT NULL - ); `) return db } - protected static findAllBeansByCondition(condition: string, ...args: SQLInputValue[]): ChatBean[] { - return this.database.prepare(`SELECT * FROM ${Chat.table_name} WHERE ${condition}`).all(...args) as unknown as ChatBean[] + protected static findAllChatBeansByCondition(condition: string, ...args: SQLInputValue[]): ChatBean[] { + return this.database.prepare(`SELECT * FROM Chat WHERE ${condition}`).all(...args) as unknown as ChatBean[] } static findById(id: string) { - const beans = this.findAllBeansByCondition('id = ?', id) + const beans = this.findAllChatBeansByCondition('id = ?', id) if (beans.length == 0) return null else if (beans.length > 1) @@ -56,12 +47,12 @@ export default class Chat { } static create(chatId: string, type: ChatType) { - if (this.findAllBeansByCondition('id = ?', chatId).length > 0) + if (this.findAllChatBeansByCondition('id = ?', chatId).length > 0) throw new DataWrongError(`对话 ID ${chatId} 已被使用`) const chat = new Chat( - Chat.findAllBeansByCondition( + Chat.findAllChatBeansByCondition( 'count = ?', - Chat.database.prepare(`INSERT INTO ${Chat.table_name} ( + Chat.database.prepare(`INSERT INTO Chat ( type, id, title, @@ -82,46 +73,101 @@ export default class Chat { declare bean: ChatBean constructor(bean: ChatBean) { this.bean = bean + + Chat.database.exec(` + CREATE TABLE IF NOT EXISTS ${this.getAdminsTableName()} ( + /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, + /* 用戶 ID */ user_id TEXT NOT NULL, + /* 管理权限 */ permissions TEXT NOT NULL + ); + `) + Chat.database.exec(` + CREATE TABLE IF NOT EXISTS ${this.getJoinRequestsTableName()} ( + /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, + /* 用戶 ID */ user_id TEXT NOT NULL, + /* 请求原因 */ reason TEXT + ); + `) + } + protected getAdminsTableName() { + return 'admins_' + this.bean.id.replaceAll('-', '_') + } + protected getJoinRequestsTableName() { + return 'join_requests_' + this.bean.id.replaceAll('-', '_') } setAttr(key: string, value: SQLInputValue): void { - Chat.database.prepare(`UPDATE ${Chat.table_name} SET ${key} = ? WHERE id = ?`).run(value, this.bean.id) + Chat.database.prepare(`UPDATE Chat SET ${key} = ? WHERE id = ?`).run(value, this.bean.id) this.bean[key] = value } + /** + * ====================================================== + * 加入对话请求 + * ====================================================== + */ + + addJoinRequest(userId: string, reason?: string) { + if (this.findAllJoinRequestsByCondition('user_id = ?', userId).length == 0) + Chat.database.prepare(`INSERT INTO ${this.getJoinRequestsTableName()} ( + user_id, + reason + ) VALUES (?, ?);`).run( + userId, + reason || null + ) + } + removeJoinRequests(userIds: string[]) { + userIds.forEach((userId) => Chat.database.prepare(`DELETE FROM ${this.getJoinRequestsTableName()} WHERE user_id = ?`).run(userId)) + } + getJoinRequests() { + return Chat.database.prepare(`SELECT * FROM ${this.getJoinRequestsTableName()}`).all() + } + protected findAllJoinRequestsByCondition(condition: string, ...args: SQLInputValue[]) { + return Chat.database.prepare(`SELECT * FROM ${this.getAdminsTableName()} WHERE ${condition}`).all(...args) + } + + /** + * ====================================================== + * 对话管理员 + * ====================================================== + */ + addAdmin(userId: string, permission: string[] | string) { if (!this.checkUserIsAdmin(userId)) - Chat.database.prepare(`INSERT INTO ChatAdmin ( + Chat.database.prepare(`INSERT INTO ${this.getAdminsTableName()} ( user_id, - chat_id, permissions - ) VALUES (?, ?, ?);`).run( + ) VALUES (?, ?);`).run( userId, - this.bean.id, '[]' ) this.setAdminPermissions(userId, permission) } - checkUserIsAdmin(userId: string) { - return Chat.findAllAdminsByCondition('user_id = ? AND chat_id = ?', userId, this.bean.id).length != 0 + return this.findAllAdminsByCondition('user_id = ?', userId).length != 0 } getAdmins() { - return Chat.findAllAdminsByCondition('chat_id = ?', this.bean.id).map((v) => v.user_id) as string[] + return Chat.database.prepare(`SELECT * FROM ${this.getAdminsTableName()}`).all().map((v) => v.user_id) as string[] } - protected static findAllAdminsByCondition(condition: string, ...args: SQLInputValue[]) { - return this.database.prepare(`SELECT * FROM ChatAdmin WHERE ${condition}`).all(...args) + protected findAllAdminsByCondition(condition: string, ...args: SQLInputValue[]) { + return Chat.database.prepare(`SELECT * FROM ${this.getAdminsTableName()} WHERE ${condition}`).all(...args) } - setAdminPermissions(userId: string, permission: string[] | string) { - Chat.database.prepare(`UPDATE ChatAdmin SET permissions = ? WHERE user_id = ? AND chat_id = ?`).run( - userId, - this.bean.id, - permission instanceof Array ? JSON.stringify(permission) : permission + Chat.database.prepare(`UPDATE ${this.getAdminsTableName()} SET permissions = ? WHERE user_id = ?`).run( + userId, + permission instanceof Array ? JSON.stringify(permission) : permission ) } removeAdmins(userIds: string[]) { - userIds.forEach((v) => Chat.database.prepare(`DELETE FROM ChatAdmin WHERE user_id = ? AND chat_id = ?`).run(v, this.bean.id)) + userIds.forEach((v) => Chat.database.prepare(`DELETE FROM ${this.getAdminsTableName()} WHERE user_id = ?`).run(v)) } + + /** + * ====================================================== + * 对话成员 + * ====================================================== + */ + getMembersList() { return UserChatLinker.getChatMembers(this.bean.id) } @@ -131,6 +177,13 @@ export default class Chat { removeMembers(userIds: string[]) { userIds.forEach((v) => UserChatLinker.unlinkUserAndChat(v, this.bean.id)) } + + /** + * ====================================================== + * 对话信息 + * ====================================================== + */ + getAnotherUserForPrivate(userMySelf: User) { const members = this.getMembersList() const user_a_id = members[0]