Compare commits

..

6 Commits

Author SHA1 Message Date
CrescentLeaf
3351d7dc4e 改 state 为 reducer state, Context 共享数据修改, 完善资料卡对话框逻辑, 完善 2025-12-13 18:05:09 +08:00
CrescentLeaf
dee8a24f0b util: 仅副作用组件 2025-12-13 18:04:15 +08:00
CrescentLeaf
3a7fe53038 wip: 对话页面 2025-12-13 18:03:59 +08:00
CrescentLeaf
16bacea5e3 使用新的打开资料卡方式 2025-12-13 18:03:35 +08:00
CrescentLeaf
22bf643d5e fix: 蠢到家行为之 getTheOtherUserIdOrThrow 写成了 updateSettings 2025-12-13 18:02:43 +08:00
CrescentLeaf
bb065f1ecc 版本号 = 0.1.0 2025-12-13 14:28:57 +08:00
19 changed files with 240 additions and 56 deletions

View File

@@ -198,7 +198,7 @@ export default class Chat extends BaseClientObject {
}
}
async getTheOtherUserIdOrThrow() {
const re = await this.client.invoke("Chat.updateSettings", {
const re = await this.client.invoke("Chat.getAnotherUserIdFromPrivate", {
token: this.client.access_token,
target: this.bean.id,
})

View File

@@ -1,5 +1,6 @@
{
"name": "lingchair-client-protocol",
"version": "0.1.0",
"type": "module",
"main": "./main.ts",
"dependencies": {

View File

@@ -1,9 +1,17 @@
import { Chat, User } from "lingchair-client-protocol"
import { Chat, User, UserMySelf } from "lingchair-client-protocol"
import getClient from "./getClient"
type CouldCached = User | Chat | null
export default class ClientCache {
static caches: { [key: string]: CouldCached } = {}
static async getMySelf() {
const k = 'usermyself'
if (this.caches[k] != null)
return this.caches[k] as UserMySelf | null
this.caches[k] = await UserMySelf.getMySelf(getClient())
return this.caches[k] as UserMySelf | null
}
static async getUser(id: string) {
const k = 'user_' + id

View File

@@ -1,7 +1,7 @@
{
"name": "lingchair-client",
"type": "module",
"version": "0.1.0-alpha",
"version": "0.1.0",
"scripts": {
"build": "npx vite build",
"build-watch": "npx vite --watch build"

6
client/ui/EffectOnly.tsx Normal file
View File

@@ -0,0 +1,6 @@
import * as React from 'react'
export default function EffectOnly({ effect, deps }: { effect: React.EffectCallback, deps?: React.DependencyList }) {
React.useEffect(effect, deps)
return null
}

View File

@@ -3,11 +3,11 @@ import useEventListener from "../utils/useEventListener.ts"
import AvatarMySelf from "./AvatarMySelf.tsx"
import MainSharedContext from './MainSharedContext.ts'
import * as React from 'react'
import { BrowserRouter, createBrowserRouter, Link, Outlet, Route, RouterProvider, Routes } from "react-router"
import { BrowserRouter, createBrowserRouter, Link, LoaderFunction, Outlet, Route, RouterProvider, Routes } from "react-router"
import LoginDialog from "./main-page/LoginDialog.tsx"
import useAsyncEffect from "../utils/useAsyncEffect.ts"
import performAuth from "../performAuth.ts"
import { CallbackError, Chat, UserMySelf } from "lingchair-client-protocol"
import { CallbackError, Chat, User, UserMySelf } from "lingchair-client-protocol"
import showCircleProgressDialog from "./showCircleProgressDialog.ts"
import RegisterDialog from "./main-page/RegisterDialog.tsx"
import sleep from "../utils/sleep.ts"
@@ -19,8 +19,12 @@ import FavouriteChatsList from "./main-page/FavouriteChatsList.tsx"
import AddFavourtieChatDialog from "./main-page/AddFavourtieChatDialog.tsx"
import RecentChatsList from "./main-page/RecentChatsList.tsx"
import UserOrChatInfoDialog from "./routers/UserOrChatInfoDialog.tsx"
import UserOrChatInfoDialogLoader from "./routers/UserOrChatInfoDialogDataLoader.ts"
import ChatFragmentDialog from "./routers/ChatFragmentDialog.tsx"
import EffectOnly from "./EffectOnly.tsx"
import MainSharedReducer from "./MainSharedReducer.ts"
export default function Main() {
function Root() {
const [myProfileCache, setMyProfileCache] = React.useState<UserMySelf>()
// 多页面切换
@@ -48,9 +52,10 @@ export default function Main() {
const [showRegisterDialog, setShowRegisterDialog] = React.useState(false)
const [showAddFavourtieChatDialog, setShowAddFavourtieChatDialog] = React.useState(false)
const [currentSelectedChatId, setCurrentSelectedChatId] = React.useState('')
const [favouriteChats, setFavouriteChats] = React.useState<Chat[]>([])
const [state, dispatch] = React.useReducer(MainSharedReducer, {
favouriteChats: [],
currentSelectedChatId: '',
})
const sharedContext = {
functions_lazy: React.useRef({
@@ -58,17 +63,14 @@ export default function Main() {
updateRecentChats: () => { },
updateAllChats: () => { },
}),
favouriteChats,
setFavouriteChats,
state,
setFavouriteChats: (chats: Chat[]) => dispatch({ type: 'update_favourite_chat', data: chats }),
setShowLoginDialog,
setShowRegisterDialog,
setShowAddFavourtieChatDialog,
currentSelectedChatId,
setCurrentSelectedChatId,
myProfileCache,
setCurrentSelectedChatId: (id: string) => dispatch({ type: 'update_selected_chat_id', data: id }),
}
useAsyncEffect(async () => {
@@ -94,7 +96,7 @@ export default function Main() {
waitingForAuth.open = false
})
const Root = (
return (
<MainSharedContext.Provider value={sharedContext}>
<div style={{
display: "flex",
@@ -212,12 +214,29 @@ export default function Main() {
</div>
</MainSharedContext.Provider>
)
}
export default function Main() {
const router = createBrowserRouter([{
path: "/",
element: Root,
Component: Root,
hydrateFallbackElement: <EffectOnly effect={() => {
const wait = showCircleProgressDialog("请稍后...")
return () => {
wait.open = false
}
}} deps={[]} />,
children: [
{ path: 'info/:type', Component: UserOrChatInfoDialog, },
{
path: 'info/:type',
Component: UserOrChatInfoDialog,
loader: UserOrChatInfoDialogLoader,
},
/* {
path: 'chat',
Component: ChatFragmentDialog,
loader: UserOrChatInfoDialogLoader,
}, */
],
}])

View File

@@ -1,5 +1,6 @@
import { Chat, UserMySelf } from "lingchair-client-protocol"
import { createContext } from "use-context-selector"
import { SharedState } from "./MainSharedReducer"
type Shared = {
functions_lazy: React.MutableRefObject<{
@@ -7,17 +8,13 @@ type Shared = {
updateRecentChats: () => void
updateAllChats: () => void
}>
favouriteChats: Chat[]
setFavouriteChats: React.Dispatch<React.SetStateAction<Chat[]>>
state: SharedState
setShowLoginDialog: React.Dispatch<React.SetStateAction<boolean>>
setShowRegisterDialog: React.Dispatch<React.SetStateAction<boolean>>
setShowAddFavourtieChatDialog: React.Dispatch<React.SetStateAction<boolean>>
currentSelectedChatId: string
setCurrentSelectedChatId: React.Dispatch<React.SetStateAction<string>>
myProfileCache?: UserMySelf
}
const MainSharedContext = createContext({} as Shared)

View File

@@ -0,0 +1,21 @@
import { Chat, UserMySelf } from "lingchair-client-protocol"
export interface SharedState {
favouriteChats: Chat[]
currentSelectedChatId: string
}
type Action =
| { type: 'update_favourite_chat', data: Chat[] }
| { type: 'update_selected_chat_id', data: string }
export default function MainSharedReducer(state: SharedState, action: Action): SharedState {
switch (action.type) {
case 'update_favourite_chat':
return { ...state, favouriteChats: action.data }
case 'update_selected_chat_id':
return { ...state, currentSelectedChatId: action.data }
default:
return state
}
}

View File

@@ -0,0 +1,14 @@
export default function ProgressDialogInner({ children, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div {...props} style={{
display: 'flex',
alignItems: 'center',
...props.style
}} >
<mdui-circular-progress style={{
marginLeft: '3px',
}}></mdui-circular-progress>
<span style={{
marginLeft: '20px',
}}>{ children }</span>
</div>
}

View File

View File

@@ -3,24 +3,27 @@ import React from "react"
import AllChatsListItem from "./AllChatsListItem.tsx"
import useEventListener from "../../utils/useEventListener.ts"
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
import { CallbackError, Chat, UserMySelf } from "lingchair-client-protocol"
import getClient from "../../getClient.ts"
import { CallbackError, Chat } from "lingchair-client-protocol"
import showSnackbar from "../../utils/showSnackbar.ts"
import isMobileUI from "../../utils/isMobileUI.ts"
import { useContextSelector } from "use-context-selector"
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
import ClientCache from "../../ClientCache.ts"
import gotoChatInfo from "../routers/gotoChatInfo.ts"
import { useNavigate } from "react-router"
export default function AllChatsList({ ...props }: React.HTMLAttributes<HTMLElement>) {
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
myProfileCache: context.myProfileCache,
functions_lazy: context.functions_lazy,
currentSelectedChatId: context.currentSelectedChatId,
state: context.state,
}))
const searchRef = React.useRef<HTMLElement>(null)
const [searchText, setSearchText] = React.useState('')
const [allChatsList, setAllChatsList] = React.useState<Chat[]>([])
const nav = useNavigate()
useEventListener(searchRef, 'input', (e) => {
setSearchText((e.target as unknown as TextField).value)
})
@@ -28,7 +31,7 @@ export default function AllChatsList({ ...props }: React.HTMLAttributes<HTMLElem
useAsyncEffect(async () => {
async function updateAllChats() {
try {
setAllChatsList(await shared.myProfileCache!.getMyAllChatsOrThrow())
setAllChatsList(await (await ClientCache.getMySelf())!.getMyAllChatsOrThrow())
} catch (e) {
if (e instanceof CallbackError)
if (e.code != 401 && e.code != 400)
@@ -40,7 +43,7 @@ export default function AllChatsList({ ...props }: React.HTMLAttributes<HTMLElem
updateAllChats()
shared.functions_lazy.current.updateAllChats = updateAllChats
return () => {
}
})
@@ -69,10 +72,10 @@ export default function AllChatsList({ ...props }: React.HTMLAttributes<HTMLElem
chat.getId().includes(searchText)
).map((v) =>
<AllChatsListItem
active={isMobileUI() ? false : shared.currentSelectedChatId == v.getId()}
active={isMobileUI() ? false : shared.state.currentSelectedChatId == v.getId()}
key={v.getId()}
onClick={() => {
openChatInfoDialog(v)
gotoChatInfo(nav, v.getId())
}}
chat={v} />
)

View File

@@ -3,19 +3,19 @@ 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 { CallbackError, Chat } 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"
import gotoChatInfo from "../routers/gotoChatInfo.ts"
import ClientCache from "../../ClientCache.ts"
import { useNavigate } from "react-router"
export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HTMLElement>) {
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
myProfileCache: context.myProfileCache,
setShowAddFavourtieChatDialog: context.setShowAddFavourtieChatDialog,
favouriteChats: context.favouriteChats,
currentSelectedChatId: context.currentSelectedChatId,
state: context.state,
functions_lazy: context.functions_lazy,
}))
@@ -25,6 +25,8 @@ export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HT
const [favouriteChatsList, setFavouriteChatsList] = React.useState<Chat[]>([])
const [checkedList, setCheckedList] = React.useState<{ [key: string]: boolean }>({})
const nav = useNavigate()
useEventListener(searchRef, 'input', (e) => {
setSearchText((e.target as unknown as TextField).value)
})
@@ -32,9 +34,8 @@ export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HT
useAsyncEffect(async () => {
async function updateFavouriteChats() {
try {
const ls = await shared.myProfileCache!.getMyFavouriteChatsOrThrow()
const ls = await (await ClientCache.getMySelf())!.getMyFavouriteChatsOrThrow()
setFavouriteChatsList(ls)
shared.favouriteChats
} catch (e) {
if (e instanceof CallbackError)
if (e.code != 401 && e.code != 400)
@@ -50,7 +51,7 @@ export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HT
return () => {
}
}, [shared.myProfileCache])
}, [])
return <mdui-list style={{
overflowY: 'auto',
@@ -106,7 +107,7 @@ export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HT
const ls = Object.keys(checkedList).filter((chatId) => checkedList[chatId] == true)
try {
shared.myProfileCache!.removeFavouriteChatsOrThrow(ls)
(await ClientCache.getMySelf())!.removeFavouriteChatsOrThrow(ls)
setCheckedList({})
setIsMultiSelecting(false)
@@ -118,7 +119,7 @@ export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HT
action: "撤销操作",
onActionClick: async () => {
try {
shared.myProfileCache!.addFavouriteChatsOrThrow(ls)
(await ClientCache.getMySelf())!.addFavouriteChatsOrThrow(ls)
} catch (e) {
if (e instanceof CallbackError)
showSnackbar({
@@ -152,7 +153,7 @@ export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HT
chat.getId().includes(searchText)
).map((v) =>
<FavouriteChatsListItem
active={isMultiSelecting ? checkedList[v.getId()] == true : (isMobileUI() ? false : shared.currentSelectedChatId == v.getId())}
active={isMultiSelecting ? checkedList[v.getId()] == true : (isMobileUI() ? false : shared.state.currentSelectedChatId == v.getId())}
onClick={() => {
if (isMultiSelecting)
setCheckedList({
@@ -160,7 +161,7 @@ export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HT
[v.getId()]: !checkedList[v.getId()],
})
else
openChatInfoDialog(v)
gotoChatInfo(nav, v.getId())
}}
key={v.getId()}
chat={v} />

View File

@@ -10,12 +10,12 @@ import { CallbackError } from "lingchair-client-protocol"
import { useContextSelector } from "use-context-selector"
import showSnackbar from "../../utils/showSnackbar.ts"
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
import ClientCache from "../../ClientCache.ts"
export default function RecentChatsList({ ...props }: React.HTMLAttributes<HTMLElement>) {
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
myProfileCache: context.myProfileCache,
functions_lazy: context.functions_lazy,
currentSelectedChatId: context.currentSelectedChatId,
state: context.state,
}))
const searchRef = React.useRef<HTMLElement>(null)
@@ -29,7 +29,7 @@ export default function RecentChatsList({ ...props }: React.HTMLAttributes<HTMLE
useAsyncEffect(async () => {
async function updateRecents() {
try {
setRecentsList(await shared.myProfileCache!.getMyRecentChats())
setRecentsList(await (await ClientCache.getMySelf())!.getMyRecentChats())
} catch (e) {
if (e instanceof CallbackError)
if (e.code != 401 && e.code != 400)
@@ -73,8 +73,8 @@ export default function RecentChatsList({ ...props }: React.HTMLAttributes<HTMLE
chat.getContent().includes(searchText)
).map((v) =>
<RecentsListItem
active={isMobileUI() ? false : shared.currentSelectedChatId == v.getId()}
openChatFragment={() => openChatFragment(v.getId())}
active={isMobileUI() ? false : shared.state.currentSelectedChatId == v.getId()}
onClick={() => {}}
key={v.getId()}
recentChat={v} />
)

View File

@@ -0,0 +1,8 @@
import useRouterDialogRef from "./useRouterDialogRef"
import * as React from 'react'
export default function ChatFragmentDialog() {
const dialogRef = useRouterDialogRef()
return <mdui-dialog fullscreen ref={dialogRef}></mdui-dialog>
}

View File

@@ -1,24 +1,97 @@
import { dialog } from "mdui"
import useRouterDialogRef from "./useRouterDialogRef"
import { BlockerFunction, useBlocker, useLocation, useNavigate, useParams, useSearchParams } from "react-router"
import useAsyncEffect from "../../utils/useAsyncEffect"
import { CallbackError, Chat } from "lingchair-client-protocol"
import { useLoaderData } from "react-router"
import { CallbackError } from "lingchair-client-protocol"
import showSnackbar from "../../utils/showSnackbar"
import getClient from "../../getClient"
import Avatar from "../Avatar"
import { useContextSelector } from "use-context-selector"
import MainSharedContext, { Shared } from "../MainSharedContext"
import * as React from 'react'
import UserOrChatInfoDialogLoader from "./UserOrChatInfoDialogDataLoader"
import MainSharedReducer from "../MainSharedReducer"
import ClientCache from "../../ClientCache"
export default function UserOrChatInfoDialog() {
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
myProfileCache: context.myProfileCache,
favouriteChats: context.favouriteChats,
state: context.state,
}))
const dialogRef = useRouterDialogRef()
const { chat, id } = useLoaderData<typeof UserOrChatInfoDialogLoader>()
const location = useLocation()
const favourited = React.useMemo(() => shared.state.favouriteChats.map((v) => v.getId()).indexOf(chat.getId() || '') != -1, [chat, shared.state.favouriteChats])
return (
<mdui-dialog close-on-overlay-click close-on-esc ref={dialogRef}>
<div style={{
display: 'flex',
alignItems: 'center',
}}>
<Avatar src={chat.getAvatarFileHash()} text={chat.getTitle()} style={{
width: '50px',
height: '50px',
}} />
<div style={{
display: 'flex',
marginLeft: '15px',
marginRight: '15px',
fontSize: '16.5px',
flexDirection: 'column',
wordBreak: 'break-word',
}}>
<span style={{
fontSize: '16.5px'
}}>{chat.getTitle()}</span>
<span style={{
fontSize: '10.5px',
marginTop: '3px',
color: 'rgb(var(--mdui-color-secondary))',
}}>({chat.getType()}) ID: {chat.getType() == 'private' ? id : chat.getId()}</span>
</div>
</div>
<mdui-divider style={{
marginTop: "10px",
}}></mdui-divider>
<mdui-list>
<mdui-list-item icon={favourited ? "favorite_border" : "favorite"} rounded onClick={() => dialog({
headline: favourited ? "取消收藏对话" : "收藏对话",
description: favourited ? "确定从收藏对话列表中移除吗? (虽然这不会导致聊天记录丢失)" : "确定要添加到收藏对话列表吗?",
actions: [
{
text: "取消",
onClick: () => {
return true
},
},
{
text: "确定",
onClick: () => {
; (async () => {
try {
if (favourited)
await (await ClientCache.getMySelf())!.removeFavouriteChatsOrThrow([chat.getId()])
else
await (await ClientCache.getMySelf())!.addFavouriteChatsOrThrow([chat.getId()])
} catch (e) {
if (e instanceof CallbackError)
showSnackbar({
message: (favourited ? "取消收藏对话" : "收藏对话") + '失败: ' + e.message
})
}
})()
return true
},
}
],
})}>{favourited ? '取消收藏' : '收藏对话'}</mdui-list-item>
<mdui-list-item icon="chat" rounded onClick={() => {
}}></mdui-list-item>
</mdui-list>
</mdui-dialog>
)
/* const location = useLocation()
const searchParams = useSearchParams()
const params = useParams()
@@ -42,5 +115,5 @@ export default function UserOrChatInfoDialog() {
.join('<br><br>')
}}></span>
</mdui-dialog>
)
) */
}

View File

@@ -0,0 +1,22 @@
import { LoaderFunctionArgs } from "react-router"
import getClient from "../../getClient"
import { Chat } from "lingchair-client-protocol"
export default async function UserOrChatInfoDialogLoader({ params, request }: LoaderFunctionArgs) {
const searchParams = new URL(request.url).searchParams
let id = searchParams.get('id')
let chat: Chat
if (params.type == 'user')
chat = await Chat.getOrCreatePrivateChatOrThrow(getClient(), id!)
else
chat = await Chat.getByIdOrThrow(getClient(), id!)
if (chat.getType() == 'private')
id = await chat.getTheOtherUserIdOrThrow()
return {
chat,
id,
}
}

View File

@@ -0,0 +1,5 @@
import { NavigateFunction } from "react-router"
export default function gotoChatInfo(nav: NavigateFunction, id: string) {
nav('/info/chat?id=' + id)
}

View File

@@ -0,0 +1,5 @@
import { NavigateFunction } from "react-router"
export default function gotoUserInfo(nav: NavigateFunction, id: string) {
nav('/info/user?id=' + id)
}

View File

@@ -1,5 +1,6 @@
{
"name": "lingchair",
"version": "0.1.0",
"type": "module",
"workspaces": [
"./mdui_patched",