From 20986af1ba8633cd32a91e5f46a450e3e5656a51 Mon Sep 17 00:00:00 2001 From: CrescentLeaf Date: Sun, 7 Dec 2025 18:31:42 +0800 Subject: [PATCH] =?UTF-8?q?(WIP)=20=E9=87=8D=E6=9E=84=E5=AE=A2=E6=88=B7?= =?UTF-8?q?=E7=AB=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ClientCache.ts | 23 +++ client/env.d.ts | 8 + client/getClient.ts | 5 - client/init.ts | 10 +- client/mdui.d.ts | 1 - client/package.json | 4 +- client/style.css | 4 + client/ui/AvatarMySelf.tsx | 2 + client/ui/Main.tsx | 136 ++++++++++---- client/ui/MainSharedContext.ts | 21 ++- .../ui/main-page/AddFavourtieChatDialog.tsx | 46 +++++ client/ui/main-page/AllChatsList.tsx | 24 +-- client/ui/main-page/ContactsList.tsx | 164 ----------------- client/ui/main-page/ContactsListItem.tsx | 27 --- client/ui/main-page/FavouriteChatsList.tsx | 170 ++++++++++++++++++ .../ui/main-page/FavouriteChatsListItem.tsx | 28 +++ client/ui/main-page/LoginDialog.tsx | 16 +- client/ui/main-page/RecentChatsList.tsx | 83 +++++++++ client/ui/main-page/RecentsList.tsx | 86 --------- client/ui/main-page/RecentsListItem.tsx | 13 +- client/ui/main-page/RegisterDialog.tsx | 13 +- client/ui/routers/ChatInfoDialog.tsx | 144 +++++++++++++++ client/vite.config.ts | 33 +++- 23 files changed, 714 insertions(+), 347 deletions(-) create mode 100644 client/ClientCache.ts create mode 100644 client/env.d.ts delete mode 100644 client/mdui.d.ts create mode 100644 client/ui/main-page/AddFavourtieChatDialog.tsx delete mode 100644 client/ui/main-page/ContactsList.tsx delete mode 100644 client/ui/main-page/ContactsListItem.tsx create mode 100644 client/ui/main-page/FavouriteChatsList.tsx create mode 100644 client/ui/main-page/FavouriteChatsListItem.tsx create mode 100644 client/ui/main-page/RecentChatsList.tsx delete mode 100644 client/ui/main-page/RecentsList.tsx create mode 100644 client/ui/routers/ChatInfoDialog.tsx diff --git a/client/ClientCache.ts b/client/ClientCache.ts new file mode 100644 index 0000000..531cfe9 --- /dev/null +++ b/client/ClientCache.ts @@ -0,0 +1,23 @@ +import { Chat, User } from "lingchair-client-protocol" +import getClient from "./getClient" + +type CouldCached = User | Chat | null +export default class ClientCache { + static caches: { [key: string]: CouldCached } = {} + + static async getUser(id: string) { + const k = 'user_' + id + if (this.caches[k] != null) + return this.caches[k] as User | null + this.caches[k] = await User.getById(getClient(), id) + return this.caches[k] + } + + static async getChat(id: string) { + const k = 'chat_' + id + if (this.caches[k] != null) + return this.caches[k] as Chat | null + this.caches[k] = await Chat.getById(getClient(), id) + return this.caches[k] + } +} diff --git a/client/env.d.ts b/client/env.d.ts new file mode 100644 index 0000000..4824669 --- /dev/null +++ b/client/env.d.ts @@ -0,0 +1,8 @@ +/// +/// + +declare const __APP_VERSION__: string +declare const __GIT_HASH__: string +declare const __GIT_HASH_FULL__: string +declare const __GIT_BRANCH__: string +declare const __BUILD_TIME__: string diff --git a/client/getClient.ts b/client/getClient.ts index fae8721..de2d692 100644 --- a/client/getClient.ts +++ b/client/getClient.ts @@ -13,11 +13,6 @@ const client = new LingChairClient({ device_id: data.device_id, auto_fresh_token: true, }) -try { - await performAuth({}) -} catch (_) { - console.log(_) -} export default function getClient() { return client diff --git a/client/init.ts b/client/init.ts index fcc9ce3..99f2767 100644 --- a/client/init.ts +++ b/client/init.ts @@ -2,7 +2,7 @@ import 'mdui/mdui.css' import 'mdui' import { breakpoint } from "mdui" -import './mdui.d.ts' +import './env.d.ts' import * as React from 'react' import ReactDOM from 'react-dom/client' @@ -16,6 +16,14 @@ import './ui/chat-elements/chat-text-container.ts' import './ui/chat-elements/chat-quote.ts' import Main from "./ui/Main.tsx" +import performAuth from './performAuth.ts' + +try { + await performAuth({}) +} catch (e) { + console.log("验证失败", e) +} + ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(React.createElement(Main)) const onResize = () => { diff --git a/client/mdui.d.ts b/client/mdui.d.ts deleted file mode 100644 index 61a00ab..0000000 --- a/client/mdui.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/client/package.json b/client/package.json index 3390fa0..5eafd06 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,7 @@ { "name": "lingchair-client", "type": "module", + "version": "0.1.0-alpha", "scripts": { "build": "npx vite build", "build-watch": "npx vite --watch build" @@ -16,7 +17,8 @@ "react-router": "7.10.1", "socket.io-client": "4.8.1", "split.js": "1.3.2", - "ua-parser-js": "2.0.6" + "ua-parser-js": "2.0.6", + "use-context-selector": "2.0.0" }, "devDependencies": { "@rollup/wasm-node": "4.48.0", diff --git a/client/style.css b/client/style.css index 5289234..d523361 100644 --- a/client/style.css +++ b/client/style.css @@ -67,3 +67,7 @@ html { .gutter.gutter-horizontal { cursor: col-resize; } + +a { + color: rgb(var(--mdui-color-primary)); +} diff --git a/client/ui/AvatarMySelf.tsx b/client/ui/AvatarMySelf.tsx index a473ae2..e4fdd1a 100644 --- a/client/ui/AvatarMySelf.tsx +++ b/client/ui/AvatarMySelf.tsx @@ -3,6 +3,7 @@ import useAsyncEffect from "../utils/useAsyncEffect.ts" import Avatar from "./Avatar.tsx" import getClient from "../getClient.ts" import React from "react" +import sleep from "../utils/sleep.ts" interface Args extends React.HTMLAttributes { avatarRef?: React.LegacyRef @@ -21,6 +22,7 @@ export default function AvatarMySelf({ }) useAsyncEffect(async () => { + await sleep(200) const mySelf = await UserMySelf.getMySelfOrThrow(getClient()) setArgs({ text: mySelf.getNickName(), diff --git a/client/ui/Main.tsx b/client/ui/Main.tsx index 97929c5..4217303 100644 --- a/client/ui/Main.tsx +++ b/client/ui/Main.tsx @@ -3,16 +3,26 @@ import useEventListener from "../utils/useEventListener.ts" import AvatarMySelf from "./AvatarMySelf.tsx" import MainSharedContext from './MainSharedContext.ts' import * as React from 'react' -import { BrowserRouter, Outlet, Route, Routes } from "react-router" +import { BrowserRouter, Link, Outlet, Route, Routes } from "react-router" import LoginDialog from "./main-page/LoginDialog.tsx" import useAsyncEffect from "../utils/useAsyncEffect.ts" import performAuth from "../performAuth.ts" -import { CallbackError } from "lingchair-client-protocol" +import { CallbackError, Chat, UserMySelf } from "lingchair-client-protocol" import showCircleProgressDialog from "./showCircleProgressDialog.ts" import RegisterDialog from "./main-page/RegisterDialog.tsx" import sleep from "../utils/sleep.ts" +import { $, NavigationDrawer } from "mdui" +import getClient from "../getClient.ts" +import showSnackbar from "../utils/showSnackbar.ts" +import AllChatsList from "./main-page/AllChatsList.tsx" +import FavouriteChatsList from "./main-page/FavouriteChatsList.tsx" +import AddFavourtieChatDialog from "./main-page/AddFavourtieChatDialog.tsx" +import RecentChatsList from "./main-page/RecentChatsList.tsx" +import ChatInfoDialog from "./routers/ChatInfoDialog.tsx" export default function Main() { + const [myProfileCache, setMyProfileCache] = React.useState() + // 多页面切换 const navigationRef = React.useRef() const [currentShowPage, setCurrentShowPage] = React.useState('Recents') @@ -21,25 +31,60 @@ export default function Main() { setCurrentShowPage((event.target as HTMLElementWithValue).value) }) + const drawerRef = React.useRef() + React.useEffect(() => { + $(drawerRef.current!.shadowRoot).append(` + + `) + }, []) + const [showLoginDialog, setShowLoginDialog] = React.useState(false) const [showRegisterDialog, setShowRegisterDialog] = React.useState(false) + const [showAddFavourtieChatDialog, setShowAddFavourtieChatDialog] = React.useState(false) - // TODO - const [currentSelectedChatId, setCurrentSelectedChatId] = React.useState(false) + const [currentSelectedChatId, setCurrentSelectedChatId] = React.useState('') + + const [favouriteChats, setFavouriteChats] = React.useState([]) const sharedContext = { - ui_functions: React.useRef({ - + functions_lazy: React.useRef({ + updateFavouriteChats: () => { }, + updateRecentChats: () => { }, + updateAllChats: () => { }, }), + favouriteChats, + setFavouriteChats, + setShowLoginDialog, setShowRegisterDialog, + setShowAddFavourtieChatDialog, + + currentSelectedChatId, + setCurrentSelectedChatId, + + myProfileCache, } useAsyncEffect(async () => { - const waitingForAuth = showCircleProgressDialog("验证中...") + const waitingForAuth = showCircleProgressDialog("加载中...") try { await performAuth({}) + + try { + setMyProfileCache(await UserMySelf.getMySelfOrThrow(getClient())) + } catch (e) { + if (e instanceof CallbackError) + showSnackbar({ + message: '获取资料失败: ' + e.message + }) + } } catch (e) { if (e instanceof CallbackError) if (e.code == 401 || e.code == 400) @@ -50,6 +95,13 @@ export default function Main() { waitingForAuth.open = false }) + const subRoutes = <> + + } /> + } /> + + + return ( @@ -68,27 +120,47 @@ export default function Main() { } + + + + + {myProfileCache?.getNickName()} + + + 账号设置 + + 添加收藏对话 + 创建新的群组 + 我是测试 + 我是测试2 + +
+ + LingChair Web v{__APP_VERSION__}
+ Build: {__GIT_HASH__} ({__BUILD_TIME__})
+ 在 Codeberg 上查看源代码 +
+
{ /** * Default: 侧边列表提供列表切换 */ !isMobileUI() ? - - - + drawerRef.current!.open = true}> - + - - - - - 添加收藏对话 - 创建群组 - - /** * Mobile: 底部导航栏提供列表切换 @@ -100,26 +172,17 @@ export default function Main() { marginLeft: '15px', top: '0px', }}> + drawerRef.current!.open = true}> { ({ Recents: "最近对话", - Contacts: "收藏对话", + Favourites: "收藏对话", AllChats: "所有对话", })[currentShowPage] }
- - - - 添加收藏对话 - 创建群组 - - - - - } { @@ -132,7 +195,15 @@ export default function Main() { height: 'calc(100% - 80px - 67px)', width: '100%', } : {}} id="SideBar"> - + + + } { @@ -145,12 +216,13 @@ export default function Main() { bottom: '0', }}> 最近对话 - 收藏对话 + 收藏对话 全部对话 } )}> + {subRoutes}
diff --git a/client/ui/MainSharedContext.ts b/client/ui/MainSharedContext.ts index 843f237..ad4da5e 100644 --- a/client/ui/MainSharedContext.ts +++ b/client/ui/MainSharedContext.ts @@ -1,12 +1,23 @@ -import { createContext } from 'react' - -type shared = { - ui_functions: React.MutableRefObject<{ +import { Chat, UserMySelf } from "lingchair-client-protocol" +import { createContext } from "use-context-selector" +type Shared = { + functions_lazy: React.MutableRefObject<{ + updateFavouriteChats: () => void + updateRecentChats: () => void + updateAllChats: () => void }> + favouriteChats: Chat[] + setFavouriteChats: React.Dispatch> setShowLoginDialog: React.Dispatch> setShowRegisterDialog: React.Dispatch> + setShowAddFavourtieChatDialog: React.Dispatch> + setCurrentSelectedChatId: React.Dispatch> + myProfileCache?: UserMySelf + currentSelectedChatId: string } -const MainSharedContext = createContext({} as shared) +const MainSharedContext = createContext({} as Shared) export default MainSharedContext + +export type { Shared } diff --git a/client/ui/main-page/AddFavourtieChatDialog.tsx b/client/ui/main-page/AddFavourtieChatDialog.tsx new file mode 100644 index 0000000..948c2c6 --- /dev/null +++ b/client/ui/main-page/AddFavourtieChatDialog.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { Button, Dialog, snackbar, TextField } from "mdui" +import { data } from 'react-router' +import { useContextSelector } from 'use-context-selector' +import MainSharedContext, { Shared } from '../MainSharedContext' +import showSnackbar from '../../utils/showSnackbar' +import { CallbackError } from 'lingchair-client-protocol' +import useEventListener from '../../utils/useEventListener' + +export default function AddFavourtieChatDialog({ ...props }: { open: boolean } & React.HTMLAttributes) { + const shared = useContextSelector(MainSharedContext, (context: Shared) => ({ + myProfileCache: context.myProfileCache, + setShowAddFavourtieChatDialog: context.setShowAddFavourtieChatDialog, + })) + + const dialogRef = React.useRef() + useEventListener(dialogRef, 'closed', () => shared.setShowAddFavourtieChatDialog(false)) + + const inputTargetRef = React.useRef(null) + + async function addFavouriteChat() { + try { + shared.myProfileCache!.addFavouriteChatsOrThrow([inputTargetRef.current!.value]) + inputTargetRef.current!.value = '' + showSnackbar({ + message: '添加成功!' + }) + } catch (e) { + if (e instanceof CallbackError) + showSnackbar({ + message: '添加收藏对话失败: ' + e.message + }) + } + } + + return ( + + { + if (event.key == 'Enter') + addFavouriteChat() + }}> + shared.setShowAddFavourtieChatDialog(false)}>取消 + addFavouriteChat()}>添加 + + ) +} diff --git a/client/ui/main-page/AllChatsList.tsx b/client/ui/main-page/AllChatsList.tsx index 54e0e9b..8d707fb 100644 --- a/client/ui/main-page/AllChatsList.tsx +++ b/client/ui/main-page/AllChatsList.tsx @@ -3,19 +3,19 @@ import React from "react" import AllChatsListItem from "./AllChatsListItem.tsx" import useEventListener from "../../utils/useEventListener.ts" import useAsyncEffect from "../../utils/useAsyncEffect.ts" -import { CallbackError, Chat, User, UserMySelf } from "lingchair-client-protocol" +import { CallbackError, Chat, UserMySelf } from "lingchair-client-protocol" import getClient from "../../getClient.ts" import showSnackbar from "../../utils/showSnackbar.ts" import isMobileUI from "../../utils/isMobileUI.ts" - -interface Args extends React.HTMLAttributes { - display: boolean - currentChatId: string - openChatInfoDialog: (chat: Chat) => void -} +import { useContextSelector } from "use-context-selector" +import MainSharedContext, { Shared } from "../MainSharedContext.ts" export default function AllChatsList({ ...props }: React.HTMLAttributes) { - + const shared = useContextSelector(MainSharedContext, (context: Shared) => ({ + myProfileCache: context.myProfileCache, + functions_lazy: context.functions_lazy, + })) + const searchRef = React.useRef(null) const [searchText, setSearchText] = React.useState('') const [allChatsList, setAllChatsList] = React.useState([]) @@ -27,16 +27,19 @@ export default function AllChatsList({ ...props }: React.HTMLAttributes { async function updateAllChats() { try { - setAllChatsList(await (await UserMySelf.getMySelfOrThrow(getClient())).getMyAllChatsOrThrow()) + setAllChatsList(await shared.myProfileCache!.getMyAllChatsOrThrow()) } catch (e) { if (e instanceof CallbackError) - if (e.code == 401 || e.code == 400) + if (e.code != 401 && e.code != 400) showSnackbar({ message: '获取所有对话失败: ' + e.message }) } } updateAllChats() + + shared.functions_lazy.current.updateAllChats = updateAllChats + return () => { } }) @@ -48,6 +51,7 @@ export default function AllChatsList({ ...props }: React.HTMLAttributes { - display: boolean - openChatInfoDialog: (chat: Chat) => void - addContactDialogRef: React.MutableRefObject - createGroupDialogRef: React.MutableRefObject - setSharedFavouriteChats: React.Dispatch> - currentChatId: string -} - -export default function ContactsList({ - display, - openChatInfoDialog, - addContactDialogRef, - createGroupDialogRef, - setSharedFavouriteChats, - currentChatId, - ...props -}: Args) { - const searchRef = React.useRef(null) - const [isMultiSelecting, setIsMultiSelecting] = React.useState(false) - const [searchText, setSearchText] = React.useState('') - const [contactsList, setContactsList] = React.useState([]) - const [checkedList, setCheckedList] = React.useState<{ [key: string]: boolean }>({}) - - useEventListener(searchRef, 'input', (e) => { - setSearchText((e.target as unknown as TextField).value) - }) - - React.useEffect(() => { - async function updateContacts() { - const re = await Client.invoke("User.getMyContacts", { - token: data.access_token, - }) - if (re.code != 200) { - if (re.code != 401 && re.code != 400) checkApiSuccessOrSncakbar(re, "获取收藏对话列表失败") - return - } - const ls = re.data!.contacts_list as Chat[] - setContactsList(ls) - setSharedFavouriteChats(ls) - } - updateContacts() - EventBus.on('ContactsList.updateContacts', () => updateContacts()) - return () => { - EventBus.off('ContactsList.updateContacts') - } - // 警告: 不添加 deps 導致無限執行 - }, []) - - return -
- - addContactDialogRef.current!.open = true}>添加收藏对话 - EventBus.emit('ContactsList.updateContacts')}>刷新 - { - if (isMultiSelecting) - setCheckedList({}) - setIsMultiSelecting(!isMultiSelecting) - }}>{isMultiSelecting ? "关闭多选" : "多选模式"} - { - isMultiSelecting && <> - dialog({ - headline: "删除所选", - description: "确定要删除所选的收藏对话吗? 这并不会删除您的聊天记录, 也不会丢失对话成员身份", - closeOnEsc: true, - closeOnOverlayClick: true, - actions: [ - { - text: "取消", - onClick: () => { - return true - }, - }, - { - text: "确定", - onClick: async () => { - const ls = Object.keys(checkedList).filter((chatId) => checkedList[chatId] == true) - const re = await Client.invoke("User.removeContacts", { - token: data.access_token, - targets: ls, - }) - if (re.code != 200) - checkApiSuccessOrSncakbar(re, "删除所选收藏失败") - else { - setCheckedList({}) - setIsMultiSelecting(false) - EventBus.emit('ContactsList.updateContacts') - snackbar({ - message: "已删除所选", - placement: "top", - action: "撤销操作", - onActionClick: async () => { - const re = await Client.invoke("User.addContacts", { - token: data.access_token, - targets: ls, - }) - if (re.code != 200) - checkApiSuccessOrSncakbar(re, "恢复所选收藏失败") - EventBus.emit('ContactsList.updateContacts') - } - }) - } - }, - } - ], - })}>删除所选 - - } -
-
- - { - contactsList.filter((chat) => - searchText == '' || - chat.title.includes(searchText) || - chat.id.includes(searchText) - ).map((v) => - { - if (isMultiSelecting) - setCheckedList({ - ...checkedList, - [v.id]: !checkedList[v.id], - }) - else - openChatInfoDialog(v) - }} - key={v.id} - contact={v} /> - ) - } -
-} \ No newline at end of file diff --git a/client/ui/main-page/ContactsListItem.tsx b/client/ui/main-page/ContactsListItem.tsx deleted file mode 100644 index a03e2da..0000000 --- a/client/ui/main-page/ContactsListItem.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import Chat from "../../api/client_data/Chat.ts" -import getUrlForFileByHash from "../../getUrlForFileByHash.ts" -import Avatar from "../Avatar.tsx" -import React from 'react' - -interface Args extends React.HTMLAttributes { - contact: Chat - active?: boolean -} - -export default function ContactsListItem({ contact, ...prop }: Args) { - const { id, title, avatar_file_hash } = contact - const ref = React.useRef(null) - - return ( - - {title} - - - ) -} diff --git a/client/ui/main-page/FavouriteChatsList.tsx b/client/ui/main-page/FavouriteChatsList.tsx new file mode 100644 index 0000000..b523dba --- /dev/null +++ b/client/ui/main-page/FavouriteChatsList.tsx @@ -0,0 +1,170 @@ +import React from "react" +import FavouriteChatsListItem from "./FavouriteChatsListItem.tsx" +import { dialog, TextField } from "mdui" +import useAsyncEffect from "../../utils/useAsyncEffect.ts" +import useEventListener from "../../utils/useEventListener.ts" +import { CallbackError, Chat, UserMySelf } from "lingchair-client-protocol" +import showSnackbar from "../../utils/showSnackbar.ts" +import getClient from "../../getClient.ts" +import { useContextSelector } from "use-context-selector" +import MainSharedContext, { Shared } from "../MainSharedContext.ts" +import isMobileUI from "../../utils/isMobileUI.ts" + +export default function FavouriteChatsList({ ...props }: React.HTMLAttributes) { + const shared = useContextSelector(MainSharedContext, (context: Shared) => ({ + myProfileCache: context.myProfileCache, + setShowAddFavourtieChatDialog: context.setShowAddFavourtieChatDialog, + functions_lazy: context.functions_lazy, + currentSelectedChatId: context.currentSelectedChatId, + values_lazy: context.values_lazy, + })) + + const searchRef = React.useRef(null) + const [isMultiSelecting, setIsMultiSelecting] = React.useState(false) + const [searchText, setSearchText] = React.useState('') + const [favouriteChatsList, setFavouriteChatsList] = React.useState([]) + const [checkedList, setCheckedList] = React.useState<{ [key: string]: boolean }>({}) + + useEventListener(searchRef, 'input', (e) => { + setSearchText((e.target as unknown as TextField).value) + }) + + useAsyncEffect(async () => { + async function updateFavouriteChats() { + try { + const ls = await shared.myProfileCache!.getMyFavouriteChatsOrThrow() + setFavouriteChatsList(ls) + shared.favourite_chats + } catch (e) { + if (e instanceof CallbackError) + if (e.code != 401 && e.code != 400) + showSnackbar({ + message: '获取收藏对话失败: ' + e.message + }) + console.log(e) + } + } + updateFavouriteChats() + + shared.functions_lazy.current.updateFavouriteChats = updateFavouriteChats + + return () => { + } + }, [shared.myProfileCache]) + + return +
+ + shared.setShowAddFavourtieChatDialog(true)}>添加收藏 + shared.functions_lazy.current.updateFavouriteChats()}>刷新列表 + { + if (isMultiSelecting) + setCheckedList({}) + setIsMultiSelecting(!isMultiSelecting) + }}>{isMultiSelecting ? "关闭多选" : "多选模式"} + { + isMultiSelecting && <> + dialog({ + headline: "移除收藏对话", + description: "确定将所选对话从收藏中移除吗? 这不会导致对话被删除.", + closeOnEsc: true, + closeOnOverlayClick: true, + actions: [ + { + text: "取消", + onClick: () => { + return true + }, + }, + { + text: "确定", + onClick: async () => { + const ls = Object.keys(checkedList).filter((chatId) => checkedList[chatId] == true) + + try { + shared.myProfileCache!.removeFavouriteChatsOrThrow(ls) + + setCheckedList({}) + setIsMultiSelecting(false) + + shared.functions_lazy.current.updateFavouriteChats() + + showSnackbar({ + message: "已删除所选", + action: "撤销操作", + onActionClick: async () => { + try { + shared.myProfileCache!.addFavouriteChatsOrThrow(ls) + } catch (e) { + if (e instanceof CallbackError) + showSnackbar({ + message: '撤销删除收藏失败: ' + e.message + }) + } + shared.functions_lazy.current.updateFavouriteChats() + } + }) + } catch (e) { + if (e instanceof CallbackError) + showSnackbar({ + message: '删除收藏对话失败: ' + e.message + }) + } + }, + } + ], + })}>删除所选 + + } +
+
+ + { + favouriteChatsList.filter((chat) => + searchText == '' || + chat.getTitle().includes(searchText) || + chat.getId().includes(searchText) + ).map((v) => + { + if (isMultiSelecting) + setCheckedList({ + ...checkedList, + [v.getId()]: !checkedList[v.getId()], + }) + else + openChatInfoDialog(v) + }} + key={v.getId()} + chat={v} /> + ) + } +
+} \ No newline at end of file diff --git a/client/ui/main-page/FavouriteChatsListItem.tsx b/client/ui/main-page/FavouriteChatsListItem.tsx new file mode 100644 index 0000000..811a588 --- /dev/null +++ b/client/ui/main-page/FavouriteChatsListItem.tsx @@ -0,0 +1,28 @@ +import { Chat } from "lingchair-client-protocol" +import Avatar from "../Avatar.tsx" +import React from 'react' +import getClient from "../../getClient.ts" + +interface Args extends React.HTMLAttributes { + chat: Chat + active?: boolean +} + +export default function FavouriteChatsListItem({ chat, active, ...prop }: Args) { + const title = chat.getTitle() + + const ref = React.useRef(null) + + return ( + + {title} + + + ) +} diff --git a/client/ui/main-page/LoginDialog.tsx b/client/ui/main-page/LoginDialog.tsx index 57090c3..7da5d58 100644 --- a/client/ui/main-page/LoginDialog.tsx +++ b/client/ui/main-page/LoginDialog.tsx @@ -1,18 +1,26 @@ import * as React from 'react' -import { Button, Dialog, TextField } from "mdui" +import { Dialog, TextField } from "mdui" import performAuth from '../../performAuth.ts' import showSnackbar from '../../utils/showSnackbar.ts' -import MainSharedContext from '../MainSharedContext.ts' +import MainSharedContext, { Shared } from '../MainSharedContext.ts' +import { useContextSelector } from 'use-context-selector' +import useEventListener from '../../utils/useEventListener.ts' export default function LoginDialog({ ...props }: { open: boolean } & React.HTMLAttributes) { - const shared = React.useContext(MainSharedContext) + const shared = useContextSelector(MainSharedContext, (context: Shared) => ({ + setShowRegisterDialog: context.setShowRegisterDialog, + setShowLoginDialog: context.setShowLoginDialog + })) + + const dialogRef = React.useRef() + useEventListener(dialogRef, 'closed', () => shared.setShowLoginDialog(false)) const loginInputAccountRef = React.useRef(null) const loginInputPasswordRef = React.useRef(null) return ( - +
) { + const shared = useContextSelector(MainSharedContext, (context: Shared) => ({ + myProfileCache: context.myProfileCache, + functions_lazy: context.functions_lazy, + currentSelectedChatId: context.currentSelectedChatId, + })) + + const searchRef = React.useRef(null) + const [searchText, setSearchText] = React.useState('') + const [recentsList, setRecentsList] = React.useState([]) + + useEventListener(searchRef, 'input', (e) => { + setSearchText((e.target as unknown as TextField).value) + }) + + useAsyncEffect(async () => { + async function updateRecents() { + try { + setRecentsList(await shared.myProfileCache!.getMyRecentChats()) + } catch (e) { + if (e instanceof CallbackError) + if (e.code != 401 && e.code != 400) + showSnackbar({ + message: '获取最近对话失败: ' + e.message + }) + } + } + updateRecents() + + shared.functions_lazy.current.updateRecentChats = updateRecents + + const id = setInterval(() => updateRecents(), 15 * 1000) + return () => { + clearInterval(id) + } + }) + + return + + { + recentsList.filter((chat) => + searchText == '' || + chat.getTitle().includes(searchText) || + chat.getId().includes(searchText) || + chat.getContent().includes(searchText) + ).map((v) => + openChatFragment(v.getId())} + key={v.getId()} + recentChat={v} /> + ) + } + +} \ No newline at end of file diff --git a/client/ui/main-page/RecentsList.tsx b/client/ui/main-page/RecentsList.tsx deleted file mode 100644 index 2a19fc4..0000000 --- a/client/ui/main-page/RecentsList.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { TextField } from "mdui" -import RecentChat from "../../api/client_data/RecentChat.ts" -import useEventListener from "../useEventListener.ts" -import RecentsListItem from "./RecentsListItem.tsx" -import React from "react" -import useAsyncEffect from "../useAsyncEffect.ts" -import Client from "../../api/Client.ts" -import { checkApiSuccessOrSncakbar } from "../snackbar.ts"; -import data from "../../Data.ts"; -import EventBus from "../../EventBus.ts"; -import isMobileUI from "../isMobileUI.ts"; - -interface Args extends React.HTMLAttributes { - display: boolean - currentChatId: string - openChatFragment: (id: string) => void -} - -export default function RecentsList({ - currentChatId, - display, - openChatFragment, - ...props -}: Args) { - const searchRef = React.useRef(null) - const [searchText, setSearchText] = React.useState('') - const [recentsList, setRecentsList] = React.useState([]) - - useEventListener(searchRef, 'input', (e) => { - setSearchText((e.target as unknown as TextField).value) - }) - - useAsyncEffect(async () => { - async function updateRecents() { - const re = await Client.invoke("User.getMyRecentChats", { - token: data.access_token, - }) - if (re.code != 200) { - if (re.code != 401 && re.code != 400) checkApiSuccessOrSncakbar(re, "获取最近对话列表失败") - return - } - - setRecentsList(re.data!.recent_chats as RecentChat[]) - } - updateRecents() - EventBus.on('RecentsList.updateRecents', () => updateRecents()) - const id = setInterval(() => updateRecents(), 15 * 1000) - return () => { - EventBus.off('RecentsList.updateRecents') - clearInterval(id) - } - }) - - return - - { - recentsList.filter((chat) => - searchText == '' || - chat.title.includes(searchText) || - chat.id.includes(searchText) || - chat.content.includes(searchText) - ).map((v) => - openChatFragment(v.id)} - key={v.id} - recentChat={v} /> - ) - } - -} \ No newline at end of file diff --git a/client/ui/main-page/RecentsListItem.tsx b/client/ui/main-page/RecentsListItem.tsx index 391d540..2d888af 100644 --- a/client/ui/main-page/RecentsListItem.tsx +++ b/client/ui/main-page/RecentsListItem.tsx @@ -1,17 +1,16 @@ import { $ } from "mdui/jq" -import RecentChat from "../../api/client_data/RecentChat.ts" import Avatar from "../Avatar.tsx" import React from 'react' -import getUrlForFileByHash from "../../getUrlForFileByHash.ts" +import getClient from "../../getClient.ts" +import RecentChat from "lingchair-client-protocol/RecentChat.ts" interface Args extends React.HTMLAttributes { recentChat: RecentChat - openChatFragment: (id: string) => void active?: boolean } -export default function RecentsListItem({ recentChat, openChatFragment, active }: Args) { - const { id, title, avatar_file_hash, content } = recentChat +export default function RecentsListItem({ recentChat, active, ...props }: Args) { + const { id, title, avatar_file_hash, content } = recentChat.bean const itemRef = React.useRef(null) React.useEffect(() => { @@ -21,9 +20,9 @@ export default function RecentsListItem({ recentChat, openChatFragment, active } openChatFragment(id)} active={active} ref={itemRef}> + }} active={active} ref={itemRef} {...props}> {title} - + ) { - const shared = React.useContext(MainSharedContext) + const shared = useContextSelector(MainSharedContext, (context: Shared) => ({ + setShowRegisterDialog: context.setShowRegisterDialog + })) + + const dialogRef = React.useRef() + useEventListener(dialogRef, 'closed', () => shared.setShowRegisterDialog(false)) const registerInputUserNameRef = React.useRef(null) const registerInputNickNameRef = React.useRef(null) const registerInputPasswordRef = React.useRef(null) return ( - +
) { + const shared = useContextSelector(MainSharedContext, (context: Shared) => ({ + myProfileCache: context.myProfileCache, + favouriteChats: context.favouriteChats, + })) + + const [chat, setChat] = React.useState() + const [userId, setUserId] = React.useState() + + const [searchParams] = useSearchParams() + let currentLocation = useLocation() + const navigate = useNavigate() + function back() { + navigate(-1) + } + const dialogRef = React.useRef() + useEventListener(dialogRef, 'overlay-click', () => back()) + const id = searchParams.get('id') + + const [favourited, setIsFavourited] = React.useState(false) + React.useEffect(() => { + setIsFavourited(shared.favouriteChats.map((v) => v.getId()).indexOf(chat?.getId() || '') != -1) + }, [chat, shared]) + + React.useEffect(() => { + console.log(currentLocation) + }, [currentLocation]) + + useAsyncEffect(async () => { + console.log(id, currentLocation.pathname) + try { + if (!currentLocation.pathname.startsWith('/info/')) { + dialogRef.current!.open = false + return + } + + if (id == null) { + dialogRef.current!.open = false + return back() + } + + if (currentLocation.pathname.startsWith('/info/user')) { + setChat(await Chat.getOrCreatePrivateChatOrThrow(getClient(), id)) + setUserId(id) + } else + setChat(await Chat.getByIdOrThrow(getClient(), id)) + dialogRef.current!.open = true + } catch (e) { + if (e instanceof CallbackError) + showSnackbar({ + message: '打开资料卡失败: ' + e.message + }) + console.log(e) + back() + } + }, [id, currentLocation]) + + if (!currentLocation.pathname.startsWith('/info/')) + return null + + const avatarUrl = getClient().getUrlForFileByHash(chat?.getAvatarFileHash())! + + return ( + +
+ avatarUrl && openImageViewer(avatarUrl)} /> +
+ {chat?.getTitle()} + ({chat?.getType()}) ID: {chat?.getType() == 'private' ? userId : chat?.getId()} +
+
+ + + + dialog({ + headline: favourited ? "取消收藏对话" : "收藏对话", + description: favourited ? "确定从收藏对话列表中移除吗? (虽然这不会导致聊天记录丢失)" : "确定要添加到收藏对话列表吗?", + actions: [ + { + text: "取消", + onClick: () => { + return true + }, + }, + { + text: "确定", + onClick: () => { + ; (async () => { + const re = await Client.invoke(favourited ? "User.removeContacts" : "User.addContacts", { + token: data.access_token, + targets: [ + chat!.id + ], + }) + if (re.code != 200) + checkApiSuccessOrSncakbar(re, favourited ? "取消收藏失败" : "收藏失败") + EventBus.emit('ContactsList.updateContacts') + })() + return true + }, + } + ], + })}>{favourited ? '取消收藏' : '收藏对话'} + { + chatInfoDialogRef.current!.open = false + openChatFragment(chat!.id) + }}>打开对话 + +
+ ) +} + diff --git a/client/vite.config.ts b/client/vite.config.ts index 8147abf..d8f528b 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -2,6 +2,36 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import config from '../server/config.ts' import { nodePolyfills } from 'vite-plugin-node-polyfills' +import { execSync } from 'child_process' +import fs from 'node:fs/promises' + +const gitHash = execSync('git rev-parse --short HEAD') + .toString() + .trim() +const gitFullHash = execSync('git rev-parse HEAD') + .toString() + .trim() +const gitBranch = execSync('git rev-parse --abbrev-ref HEAD') + .toString() + .trim() +const versionEnv = { + define: { + __APP_VERSION__: JSON.stringify(JSON.parse(await fs.readFile('package.json', 'utf-8')).version), + __GIT_HASH__: JSON.stringify(gitHash), + __GIT_HASH_FULL__: JSON.stringify(gitFullHash), + __GIT_BRANCH__: JSON.stringify(gitBranch), + __BUILD_TIME__: JSON.stringify(new Date().toLocaleString('zh-CN')), + } +} + +function gitHashPlugin() { + return { + name: 'git-hash-plugin', + config() { + return versionEnv + } + } +} // https://vite.dev/config/ export default defineConfig({ @@ -14,7 +44,8 @@ export default defineConfig({ global: true, process: true, }, - }) + }), + gitHashPlugin(), ], build: { sourcemap: true,