diff --git a/client/api/ApiDeclare.ts b/client/api/ApiDeclare.ts index d8d6174..b01b66d 100644 --- a/client/api/ApiDeclare.ts +++ b/client/api/ApiDeclare.ts @@ -8,6 +8,7 @@ export type CallMethod = "User.setAvatar" | "User.getMyInfo" | + "Chat.getInfo" | "Chat.sendMessage" | "Chat.getMessageHistory" diff --git a/client/ui/chat/ChatFragment.tsx b/client/ui/chat/ChatFragment.tsx index aff38fb..14e1a6f 100644 --- a/client/ui/chat/ChatFragment.tsx +++ b/client/ui/chat/ChatFragment.tsx @@ -1,12 +1,24 @@ import { Tab } from "mdui" import useEventListener from "../useEventListener.ts" -import Message from "./Message.jsx" +import Element_Message from "./Message.jsx" import MessageContainer from "./MessageContainer.jsx" import * as React from 'react' +import Client from "../../api/Client.ts" +import Message from "../../api/client_data/Message.ts" +import Chat from "../../api/client_data/Chat.ts" +import data from "../../Data.ts" +import { checkApiSuccessOrSncakbar } from "../snackbar.ts" -export default function ChatFragment({ ...props } = {}) { - const messageList = React.useState([]) +interface Args extends React.HTMLAttributes { + target: string, +} + +export default function ChatFragment({ target, ...props }: Args) { + const [messagesList, setMessagesList] = React.useState([] as Message[]) + const [chatInfo, setChatInfo] = React.useState({ + title: '加載中...' + } as Chat) const [tabItemSelected, setTabItemSelected] = React.useState('Chat') const tabRef: React.MutableRefObject = React.useRef(null) @@ -14,6 +26,18 @@ export default function ChatFragment({ ...props } = {}) { setTabItemSelected((event.target as HTMLElement as Tab).value as string) }) + React.useEffect(() => { + ;(async () => { + const re = await Client.invoke('Chat.getInfo', { + token: data.access_token, + target: target, + }) + if (re.code != 200) + return checkApiSuccessOrSncakbar(re, "對話錯誤") + setChatInfo(re.data as Chat) + })() + }, [target]) + return (
- Title + { + chatInfo.title + } 設定 { contactsMap: { [key: string]: User[] } display: boolean + openChatFragment: (id: string) => void } export default function ContactsList({ contactsMap, display, + openChatFragment, ...props }: Args) { return + 添加聯絡人 { Object.keys(contactsMap).map((v) => @@ -26,9 +31,9 @@ export default function ContactsList({ { contactsMap[v].map((v2) => + contact={v2} /> ) } diff --git a/client/ui/main/ContactsListItem.jsx b/client/ui/main/ContactsListItem.jsx deleted file mode 100644 index a2e4105..0000000 --- a/client/ui/main/ContactsListItem.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import Avatar from "../Avatar.tsx" - -export default function ContactsListItem({ nickName, avatar }) { - return ( - - {nickName} - - - ) -} diff --git a/client/ui/main/ContactsListItem.tsx b/client/ui/main/ContactsListItem.tsx new file mode 100644 index 0000000..5555e0a --- /dev/null +++ b/client/ui/main/ContactsListItem.tsx @@ -0,0 +1,23 @@ +import User from "../../api/client_data/User.ts" +import Avatar from "../Avatar.tsx" + +interface Args extends React.HTMLAttributes { + contact: User + openChatFragment: (id: string) => void +} + +export default function ContactsListItem({ contact, openChatFragment }: Args) { + const { id, nickname, avatar } = contact + return ( + openChatFragment(id)}> + {nickname} + + + ) +} diff --git a/client/ui/main/RecentsList.tsx b/client/ui/main/RecentsList.tsx index df98f19..a4a4130 100644 --- a/client/ui/main/RecentsList.tsx +++ b/client/ui/main/RecentsList.tsx @@ -1,30 +1,29 @@ import RecentChat from "../../api/client_data/RecentChat.ts" -import User from "../../api/client_data/User.ts" -import ContactsListItem from "./ContactsListItem.jsx" -import RecentsListItem from "./RecentsListItem.jsx" +import RecentsListItem from "./RecentsListItem.tsx" interface Args extends React.HTMLAttributes { recentsList: RecentChat[] display: boolean + openChatFragment: (id: string) => void } export default function RecentsList({ recentsList, display, + openChatFragment, ...props }: Args) { return + }} {...props}> { recentsList.map((v) => + recentChat={v} /> ) } diff --git a/client/ui/main/RecentsListItem.jsx b/client/ui/main/RecentsListItem.tsx similarity index 54% rename from client/ui/main/RecentsListItem.jsx rename to client/ui/main/RecentsListItem.tsx index ff98779..8aaaee4 100644 --- a/client/ui/main/RecentsListItem.jsx +++ b/client/ui/main/RecentsListItem.tsx @@ -1,13 +1,20 @@ +import RecentChat from "../../api/client_data/RecentChat.ts" import Avatar from "../Avatar.tsx" -export default function RecentsListItem({ nickName, avatar, content }) { +interface Args extends React.HTMLAttributes { + recentChat: RecentChat + openChatFragment: (id: string) => void +} + +export default function RecentsListItem({ recentChat, openChatFragment }: Args) { + const { id, title, avatar, content } = recentChat return ( - {nickName} - + }} onClick={() => openChatFragment(id)}> + {title} + { - + if (this.checkArgsMissing(args, ['token', 'target'])) return { + msg: "參數缺失", + code: 400, + } + + const token = TokenManager.decode(args.token as string) + if (!this.checkToken(token)) return { + code: 401, + msg: "令牌無效", + } + + const chat = Chat.findById(args.target as string) + if (chat == null) return { + code: 404, + msg: "對話不存在", + } + + // 私聊 + if (chat!.bean.type == 'private') { + const targetId = args.target as string + const target = User.findById(targetId) + const mine = User.findById(token.author) as User + if (target == null) return { + code: 404, + msg: "找不到用户", + } + + return { + code: 200, + msg: "成功", + data: { + type: chat.bean.type, + title: ChatPrivate.fromChat(chat).getTitleForPrivate(mine, target) + } + } + } + return { code: 501, - msg: "未實現", + msg: "not implmented", } }) this.registerEvent("Chat.sendMessage", (args) => { + if (this.checkArgsMissing(args, ['token', 'target'])) return { + msg: "參數缺失", + code: 400, + } + + const token = TokenManager.decode(args.token as string) + if (!this.checkToken(token)) return { + code: 401, + msg: "令牌無效", + } return { code: 501, @@ -21,6 +70,16 @@ export default class UserApi extends BaseApi { } }) this.registerEvent("Chat.getMessageHistory", (args) => { + if (this.checkArgsMissing(args, ['token', 'target'])) return { + msg: "參數缺失", + code: 400, + } + + const token = TokenManager.decode(args.token as string) + if (!this.checkToken(token)) return { + code: 401, + msg: "令牌無效", + } return { code: 501, diff --git a/server/data/ChatPrivate.ts b/server/data/ChatPrivate.ts index 4f9daf2..6fc63ae 100644 --- a/server/data/ChatPrivate.ts +++ b/server/data/ChatPrivate.ts @@ -1,7 +1,12 @@ +import chalk from "chalk" import Chat from "./Chat.ts" -import User from "./User.ts"; +import User from "./User.ts" export default class ChatPrivate extends Chat { + static fromChat(chat: Chat) { + return new ChatPrivate(chat.bean) + } + static getChatIdByUsersId(userIdA: string, userIdB: string) { return [userIdA, userIdB].sort().join('-') } @@ -9,8 +14,16 @@ export default class ChatPrivate extends Chat { static createForPrivate(userA: User, userB: User) { return this.create(this.getChatIdByUsersId(userA.bean.id, userB.bean.id), 'private') } - static findForPrivate() { - + static findForPrivate(userA: User, userB: User) { + return this.fromChat(this.findById(this.getChatIdByUsersId(userA.bean.id, userB.bean.id)) as Chat) + } + static findOrCreateForPrivate(userA: User, userB: User) { + let a = this.findForPrivate(userA, userB) + if (a == null) { + this.createForPrivate(userA, userB) + a = this.findForPrivate(userA, userB) as ChatPrivate + } + return a } getTitleForPrivate(user: User, targetUser: User) { diff --git a/server/data/Message.ts b/server/data/MessageBean.ts similarity index 54% rename from server/data/Message.ts rename to server/data/MessageBean.ts index 38b0a2a..c197001 100644 --- a/server/data/Message.ts +++ b/server/data/MessageBean.ts @@ -1,7 +1,7 @@ -export default class Message { +export default class MessageBean { declare id: number declare text: string - declare user_id: string + declare user_id?: string [key: string]: unknown } diff --git a/server/data/MessagesManager.ts b/server/data/MessagesManager.ts index 4e00627..ff1c86f 100644 --- a/server/data/MessagesManager.ts +++ b/server/data/MessagesManager.ts @@ -5,22 +5,59 @@ import chalk from "chalk" import config from "../config.ts" import Chat from "./Chat.ts" +import MessageBean from "./MessageBean.ts" export default class MessagesManager { - static table_name: string = "Messages" static database: DatabaseSync = this.init() - + private static init(): DatabaseSync { - const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, this.table_name + '.db')) + const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, 'Messages.db')) return db } - static getInstance(chat: Chat) { + static getInstanceForChat(chat: Chat) { return new MessagesManager(chat) } - + declare chat: Chat constructor(chat: Chat) { this.chat = chat + + MessagesManager.database.exec(` + CREATE TABLE IF NOT EXISTS ${this.getTableName()} ( + /* 序号, MessageId */ id INTEGER PRIMARY KEY AUTOINCREMENT, + /* 消息文本 */ text TEXT NOT NULL, + /* 发送者 */ user_id TEXT NOT NULL, + ); + `) + } + protected getTableName() { + return `messages_${this.chat.bean.id}` + } + addMessage({ + text, + user_id + }: { + text: string, + user_id?: string + }) { + MessagesManager.database.prepare(`INSERT INTO ${this.getTableName()} ( + text, + user_id + ) VALUES (?, ?);`).run( + text, + user_id || null + ) + } + addSystemMessage(text: string) { + this.addMessage({ + text + }) + } + getMessages(limit: number = 15, offset: number = 0) { + return MessagesManager.database.prepare(`SELECT * FROM ${this.getTableName()} ORDER BY id DESC LIMIT ? OFFSET ?;`).all(limit, offset) as unknown as MessageBean[] + } + getMessagesWithPage(limit: number = 15, page: number = 0) { + return this.getMessages(limit, limit * page) } }