Compare commits

...

6 Commits

Author SHA1 Message Date
CrescentLeaf
a3d5e93240 feat(wip): Chat impl 2025-09-14 00:37:03 +08:00
CrescentLeaf
ed494413fd feat(wip): Chat.getInfo 2025-09-14 00:36:51 +08:00
CrescentLeaf
557234841d ui: ChatFragment 使用分面板的樣式 2025-09-14 00:18:56 +08:00
CrescentLeaf
ea17ab2ddd chore: rename ChatFragment. js -> ts 2025-09-14 00:12:50 +08:00
CrescentLeaf
20ef8a8514 chore: make lint happy 2025-09-14 00:11:13 +08:00
CrescentLeaf
124879f11f ui: AppMobile 界面長寬修正 2025-09-13 23:50:38 +08:00
7 changed files with 178 additions and 123 deletions

View File

@@ -1,6 +1,6 @@
import Client from "../api/Client.ts" import Client from "../api/Client.ts"
import data from "../Data.ts" import data from "../Data.ts"
import ChatFragment from "./chat/ChatFragment.jsx" import ChatFragment from "./chat/ChatFragment.tsx"
import ContactsListItem from "./main/ContactsListItem.jsx" import ContactsListItem from "./main/ContactsListItem.jsx"
import RecentsListItem from "./main/RecentsListItem.jsx" import RecentsListItem from "./main/RecentsListItem.jsx"
import useEventListener from './useEventListener.ts' import useEventListener from './useEventListener.ts'
@@ -9,7 +9,7 @@ import RecentChat from "../api/client_data/RecentChat.ts"
import Avatar from "./Avatar.tsx" import Avatar from "./Avatar.tsx"
import * as React from 'react' import * as React from 'react'
import { Button, ButtonIcon, Dialog, NavigationRail, TextField } from "mdui" import { Dialog, NavigationRail, TextField } from "mdui"
import Split from 'split.js' import Split from 'split.js'
import 'mdui/jsx.zh-cn.d.ts' import 'mdui/jsx.zh-cn.d.ts'
import { checkApiSuccessOrSncakbar } from "./snackbar.ts" import { checkApiSuccessOrSncakbar } from "./snackbar.ts"

View File

@@ -1,6 +1,6 @@
import Client from "../api/Client.ts" import Client from "../api/Client.ts"
import data from "../Data.ts" import data from "../Data.ts"
import ChatFragment from "./chat/ChatFragment.jsx" import ChatFragment from "./chat/ChatFragment.tsx"
import ContactsListItem from "./main/ContactsListItem.jsx" import ContactsListItem from "./main/ContactsListItem.jsx"
import RecentsListItem from "./main/RecentsListItem.jsx" import RecentsListItem from "./main/RecentsListItem.jsx"
import useEventListener from './useEventListener.ts' import useEventListener from './useEventListener.ts'
@@ -9,8 +9,7 @@ import RecentChat from "../api/client_data/RecentChat.ts"
import Avatar from "./Avatar.tsx" import Avatar from "./Avatar.tsx"
import * as React from 'react' import * as React from 'react'
import { Button, ButtonIcon, Dialog, NavigationBar, TextField } from "mdui" import { Dialog, NavigationBar, TextField } from "mdui"
import Split from 'split.js'
import 'mdui/jsx.zh-cn.d.ts' import 'mdui/jsx.zh-cn.d.ts'
import { checkApiSuccessOrSncakbar } from "./snackbar.ts" import { checkApiSuccessOrSncakbar } from "./snackbar.ts"
@@ -60,7 +59,7 @@ export default function AppMobile() {
} as unknown as { [key: string]: User[] }) } as unknown as { [key: string]: User[] })
const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents') const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents')
const navigationBarRef: React.MutableRefObject<NavigationRail | null> = React.useRef(null) const navigationBarRef: React.MutableRefObject<NavigationBar | null> = React.useRef(null)
useEventListener(navigationBarRef, 'change', (event) => { useEventListener(navigationBarRef, 'change', (event) => {
setNavigationItemSelected((event.target as HTMLElement as NavigationBar).value as string) setNavigationItemSelected((event.target as HTMLElement as NavigationBar).value as string)
}) })
@@ -100,7 +99,7 @@ export default function AppMobile() {
<div style={{ <div style={{
display: "flex", display: "flex",
position: 'relative', position: 'relative',
width: '100%', width: 'var(--whitesilk-window-width)',
height: 'var(--whitesilk-window-height)', height: 'var(--whitesilk-window-height)',
}}> }}>
<LoginDialog <LoginDialog
@@ -121,61 +120,65 @@ export default function AppMobile() {
userProfileDialogRef={userProfileDialogRef} userProfileDialogRef={userProfileDialogRef}
user={myUserProfileCache} /> user={myUserProfileCache} />
<mdui-navigation-bar label-visibility="selected" value="Recents" ref={navigationBarRef}> <mdui-navigation-bar scroll-target="#SideBar" label-visibility="selected" value="Recents" ref={navigationBarRef}>
<mdui-navigation-bar-item icon="watch_later--outlined" value="Recents"></mdui-navigation-bar-item> <mdui-navigation-bar-item icon="watch_later--outlined" value="Recents"></mdui-navigation-bar-item>
<mdui-navigation-bar-item icon="contacts--outlined" value="Contacts"></mdui-navigation-bar-item> <mdui-navigation-bar-item icon="contacts--outlined" value="Contacts"></mdui-navigation-bar-item>
</mdui-navigation-bar> </mdui-navigation-bar>
{ <div style={{
// 最近聊天 display: 'flex',
<mdui-list style={{ height: 'calc(100% - 80px)',
height: 'calc(var(--whitesilk-window-height) - 80px)', width: '100%',
overflowY: 'auto', }} id="SideBar">
marginLeft: '10px', {
marginRight: '10px', // 最近聊天
width: '100%', <mdui-list style={{
display: navigationItemSelected == "Recents" ? undefined : 'none' overflowY: 'auto',
}}> marginLeft: '10px',
{ marginRight: '10px',
recentsList.map((v) => width: '100%',
<RecentsListItem display: navigationItemSelected == "Recents" ? undefined : 'none'
key={v.id} }}>
nickName={v.title}
avatar={v.avatar}
content={v.content} />
)
}
</mdui-list>
}
{
// 联系人列表
<mdui-list style={{
height: 'calc(var(--whitesilk-window-height) - 80px)',
overflowY: 'auto',
marginLeft: '10px',
marginRight: '10px',
width: '100%',
display: navigationItemSelected == "Contacts" ? undefined : 'none'
}}>
<mdui-collapse accordion value={Object.keys(contactsMap)[0]}>
{ {
Object.keys(contactsMap).map((v) => recentsList.map((v) =>
<mdui-collapse-item key={v} value={v}> <RecentsListItem
<mdui-list-subheader slot="header">{v}</mdui-list-subheader> key={v.id}
{ nickName={v.title}
contactsMap[v].map((v2) => avatar={v.avatar}
<ContactsListItem content={v.content} />
key={v2.id}
nickName={v2.nickname}
avatar={v2.avatar} />
)
}
</mdui-collapse-item>
) )
} }
</mdui-collapse> </mdui-list>
</mdui-list> }
} {
// 联系人列表
<mdui-list style={{
overflowY: 'auto',
marginLeft: '10px',
marginRight: '10px',
width: '100%',
display: navigationItemSelected == "Contacts" ? undefined : 'none'
}}>
<mdui-collapse accordion value={Object.keys(contactsMap)[0]}>
{
Object.keys(contactsMap).map((v) =>
<mdui-collapse-item key={v} value={v}>
<mdui-list-subheader slot="header">{v}</mdui-list-subheader>
{
contactsMap[v].map((v2) =>
<ContactsListItem
key={v2.id}
nickName={v2.nickname}
avatar={v2.avatar} />
)
}
</mdui-collapse-item>
)
}
</mdui-collapse>
</mdui-list>
}
</div>
</div> </div>
) )
} }

View File

@@ -1,65 +0,0 @@
import Message from "./Message.jsx"
import MessageContainer from "./MessageContainer.jsx"
import * as React from 'react'
export default function ChatFragment({ ...props } = {}) {
const messageList = React.useState([])
return (
<div style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
}} {...props}>
<mdui-top-app-bar style={{
position: 'sticky',
}}>
<mdui-button-icon icon="menu"></mdui-button-icon>
<mdui-top-app-bar-title>Title</mdui-top-app-bar-title>
<mdui-button-icon icon="more_vert"></mdui-button-icon>
</mdui-top-app-bar>
<div style={{
display: "flex",
flexDirection: "column",
height: "100%",
}}>
<div style={{
display: "flex",
justifyContent: "center",
}}>
<mdui-button variant="text">加載更多</mdui-button>
</div>
<MessageContainer>
</MessageContainer>
{
// 输入框
}
<div style={{
display: 'flex',
alignItems: 'center',
paddingBottom: '0.1rem',
paddingTop: '0.1rem',
height: '4rem',
position: 'sticky',
bottom: '2px',
marginLeft: '5px',
marginRight: '4px',
backgroundColor: 'rgb(var(--mdui-color-background))',
}}>
<mdui-text-field variant="outlined" placeholder="喵呜~" autosize max-rows="1" style={{
marginRight: '10px',
}}></mdui-text-field>
<mdui-button-icon slot="end-icon" icon="more_vert" style={{
marginRight: '6px',
}}></mdui-button-icon>
<mdui-button-icon icon="send" style={{
marginRight: '7px',
}}></mdui-button-icon>
</div>
</div>
</div >
)
}

View File

@@ -0,0 +1,84 @@
import { Tab } from "mdui"
import useEventListener from "../useEventListener.ts"
import Message from "./Message.jsx"
import MessageContainer from "./MessageContainer.jsx"
import * as React from 'react'
export default function ChatFragment({ ...props } = {}) {
const messageList = React.useState([])
const [tabItemSelected, setTabItemSelected] = React.useState('Chat')
const tabRef: React.MutableRefObject<Tab | null> = React.useRef(null)
useEventListener(tabRef, 'change', (event) => {
setTabItemSelected((event.target as HTMLElement as Tab).value as string)
})
return (
<div style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
overflowY: 'auto',
}} {...props}>
<mdui-tabs ref={tabRef} value="Chat" style={{
position: 'sticky',
display: "flex",
flexDirection: "column",
height: "100%",
}}>
<mdui-tab value="Chat">Title</mdui-tab>
<mdui-tab value="Settings"></mdui-tab>
<mdui-tab-panel slot="panel" value="Chat" style={{
display: tabItemSelected == "Chat" ? "flex" : "none",
flexDirection: "column",
height: "100%",
}}>
<div style={{
display: "flex",
justifyContent: "center",
marginTop: "15px"
}}>
<mdui-button variant="text"></mdui-button>
</div>
<MessageContainer>
</MessageContainer>
{
// 输入框
}
<div style={{
display: 'flex',
alignItems: 'center',
paddingBottom: '0.1rem',
paddingTop: '0.1rem',
height: '4rem',
position: 'sticky',
bottom: '2px',
marginLeft: '5px',
marginRight: '4px',
backgroundColor: 'rgb(var(--mdui-color-background))',
}}>
<mdui-text-field variant="outlined" placeholder="喵呜~" autosize max-rows={1} style={{
marginRight: '10px',
}}></mdui-text-field>
<mdui-button-icon slot="end-icon" icon="more_vert" style={{
marginRight: '6px',
}}></mdui-button-icon>
<mdui-button-icon icon="send" style={{
marginRight: '7px',
}}></mdui-button-icon>
</div>
</mdui-tab-panel>
<mdui-tab-panel slot="panel" value="Settings" style={{
display: tabItemSelected == "Settings" ? "flex" : "none",
flexDirection: "column",
height: "100%",
}}>
Work in progress...
</mdui-tab-panel>
</mdui-tabs>
</div>
)
}

View File

@@ -8,6 +8,7 @@ export type CallMethod =
"User.setAvatar" | "User.setAvatar" |
"User.getMyInfo" | "User.getMyInfo" |
"Chat.getInfo" |
"Chat.sendMessage" | "Chat.sendMessage" |
"Chat.getMessageHistory" "Chat.getMessageHistory"

View File

@@ -6,6 +6,13 @@ export default class UserApi extends BaseApi {
return "Chat" return "Chat"
} }
override onInit(): void { override onInit(): void {
this.registerEvent("Chat.getInfo", (args) => {
return {
code: 501,
msg: "未實現",
}
})
this.registerEvent("Chat.sendMessage", (args) => { this.registerEvent("Chat.sendMessage", (args) => {
return { return {

View File

@@ -6,6 +6,7 @@ import config from '../config.ts'
import ChatBean from './ChatBean.ts' import ChatBean from './ChatBean.ts'
import { SQLInputValue } from "node:sqlite" import { SQLInputValue } from "node:sqlite"
import chalk from "chalk" import chalk from "chalk"
import User from "./User.ts"
/** /**
* Chat.ts - Wrapper and manager * Chat.ts - Wrapper and manager
@@ -20,7 +21,11 @@ export default class Chat {
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS ${Chat.table_name} ( CREATE TABLE IF NOT EXISTS ${Chat.table_name} (
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT, /* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
/* Chat ID, 哈希 */ id TEXT, /* Chat ID */ id TEXT NOT NULL,
/* 標題 (群組) */ title TEXT,
/* 頭像 (群組) */ avatar BLOB,
/* UserIdA (私信) */ user_a_id TEXT
/* UserIdB (私信) */ user_b_id TEXT,
/* 设置 */ settings TEXT NOT NULL /* 设置 */ settings TEXT NOT NULL
); );
`) `)
@@ -31,14 +36,34 @@ export default class Chat {
return this.database.prepare(`SELECT * FROM ${Chat.table_name} WHERE ${condition}`).all(...args) as unknown as ChatBean[] return this.database.prepare(`SELECT * FROM ${Chat.table_name} WHERE ${condition}`).all(...args) as unknown as ChatBean[]
} }
static findById(id: string): Chat { static findById(id: string) {
const beans = this.findAllBeansByCondition('id = ?', id) const beans = this.findAllBeansByCondition('id = ?', id)
if (beans.length == 0) if (beans.length == 0)
throw new Error(`找不到 id 为 ${id} 的 Chat`) return null
else if (beans.length > 1) else if (beans.length > 1)
console.error(chalk.red(`警告: 查询 id = ${id} 时, 查询到多个相同 ID 的 Chat`)) console.error(chalk.red(`警告: 查询 id = ${id} 时, 查询到多个相同 ID 的 Chat`))
return new Chat(beans[0]) return new Chat(beans[0])
} }
static create(chatId: string) {
const chat = new Chat(
Chat.findAllBeansByCondition(
'count = ?',
Chat.database.prepare(`INSERT INTO ${Chat.table_name} (
id,
settings
) VALUES (?, ?);`).run(
chatId,
"{}"
).lastInsertRowid
)[0]
)
return chat
}
static createFromTwoUsers(userA: User, userB: User) {
return this.create([userA.bean.id, userB.bean.id].sort().join('-'))
}
declare bean: ChatBean declare bean: ChatBean
constructor(bean: ChatBean) { constructor(bean: ChatBean) {