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 data from "../Data.ts"
import ChatFragment from "./chat/ChatFragment.jsx"
import ChatFragment from "./chat/ChatFragment.tsx"
import ContactsListItem from "./main/ContactsListItem.jsx"
import RecentsListItem from "./main/RecentsListItem.jsx"
import useEventListener from './useEventListener.ts'
@@ -9,7 +9,7 @@ import RecentChat from "../api/client_data/RecentChat.ts"
import Avatar from "./Avatar.tsx"
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 'mdui/jsx.zh-cn.d.ts'
import { checkApiSuccessOrSncakbar } from "./snackbar.ts"

View File

@@ -1,6 +1,6 @@
import Client from "../api/Client.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 RecentsListItem from "./main/RecentsListItem.jsx"
import useEventListener from './useEventListener.ts'
@@ -9,8 +9,7 @@ import RecentChat from "../api/client_data/RecentChat.ts"
import Avatar from "./Avatar.tsx"
import * as React from 'react'
import { Button, ButtonIcon, Dialog, NavigationBar, TextField } from "mdui"
import Split from 'split.js'
import { Dialog, NavigationBar, TextField } from "mdui"
import 'mdui/jsx.zh-cn.d.ts'
import { checkApiSuccessOrSncakbar } from "./snackbar.ts"
@@ -60,7 +59,7 @@ export default function AppMobile() {
} as unknown as { [key: string]: User[] })
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) => {
setNavigationItemSelected((event.target as HTMLElement as NavigationBar).value as string)
})
@@ -100,7 +99,7 @@ export default function AppMobile() {
<div style={{
display: "flex",
position: 'relative',
width: '100%',
width: 'var(--whitesilk-window-width)',
height: 'var(--whitesilk-window-height)',
}}>
<LoginDialog
@@ -121,61 +120,65 @@ export default function AppMobile() {
userProfileDialogRef={userProfileDialogRef}
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="contacts--outlined" value="Contacts"></mdui-navigation-bar-item>
</mdui-navigation-bar>
{
// 最近聊天
<mdui-list style={{
height: 'calc(var(--whitesilk-window-height) - 80px)',
overflowY: 'auto',
marginLeft: '10px',
marginRight: '10px',
width: '100%',
display: navigationItemSelected == "Recents" ? undefined : 'none'
}}>
{
recentsList.map((v) =>
<RecentsListItem
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]}>
<div style={{
display: 'flex',
height: 'calc(100% - 80px)',
width: '100%',
}} id="SideBar">
{
// 最近聊天
<mdui-list style={{
overflowY: 'auto',
marginLeft: '10px',
marginRight: '10px',
width: '100%',
display: navigationItemSelected == "Recents" ? undefined : 'none'
}}>
{
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>
recentsList.map((v) =>
<RecentsListItem
key={v.id}
nickName={v.title}
avatar={v.avatar}
content={v.content} />
)
}
</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>
)
}

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.getMyInfo" |
"Chat.getInfo" |
"Chat.sendMessage" |
"Chat.getMessageHistory"

View File

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

View File

@@ -6,6 +6,7 @@ import config from '../config.ts'
import ChatBean from './ChatBean.ts'
import { SQLInputValue } from "node:sqlite"
import chalk from "chalk"
import User from "./User.ts"
/**
* Chat.ts - Wrapper and manager
@@ -20,7 +21,11 @@ export default class Chat {
db.exec(`
CREATE TABLE IF NOT EXISTS ${Chat.table_name} (
/* 序号 */ 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
);
`)
@@ -31,15 +36,35 @@ export default class Chat {
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)
if (beans.length == 0)
throw new Error(`找不到 id 为 ${id} 的 Chat`)
return null
else if (beans.length > 1)
console.error(chalk.red(`警告: 查询 id = ${id} 时, 查询到多个相同 ID 的 Chat`))
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
constructor(bean: ChatBean) {
this.bean = bean