diff --git a/client/ClientCache.ts b/client/ClientCache.ts index 531cfe9..dd10628 100644 --- a/client/ClientCache.ts +++ b/client/ClientCache.ts @@ -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 diff --git a/client/ui/Main.tsx b/client/ui/Main.tsx index ea263fa..a633e86 100644 --- a/client/ui/Main.tsx +++ b/client/ui/Main.tsx @@ -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() // 多页面切换 @@ -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([]) + 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 (
) +} +export default function Main() { const router = createBrowserRouter([{ path: "/", - element: Root, + Component: Root, + hydrateFallbackElement: { + 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, + }, */ ], }]) diff --git a/client/ui/MainSharedContext.ts b/client/ui/MainSharedContext.ts index 5e5c178..8271755 100644 --- a/client/ui/MainSharedContext.ts +++ b/client/ui/MainSharedContext.ts @@ -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> + state: SharedState setShowLoginDialog: React.Dispatch> setShowRegisterDialog: React.Dispatch> setShowAddFavourtieChatDialog: React.Dispatch> - currentSelectedChatId: string setCurrentSelectedChatId: React.Dispatch> - - myProfileCache?: UserMySelf } const MainSharedContext = createContext({} as Shared) diff --git a/client/ui/MainSharedReducer.ts b/client/ui/MainSharedReducer.ts new file mode 100644 index 0000000..045f9be --- /dev/null +++ b/client/ui/MainSharedReducer.ts @@ -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 + } +} diff --git a/client/ui/ProgressDialogInner.tsx b/client/ui/ProgressDialogInner.tsx new file mode 100644 index 0000000..2348ad2 --- /dev/null +++ b/client/ui/ProgressDialogInner.tsx @@ -0,0 +1,14 @@ +export default function ProgressDialogInner({ children, ...props }: React.HTMLAttributes) { + return
+ + { children } +
+} diff --git a/client/ui/routers/UserOrChatInfoDialog.tsx b/client/ui/routers/UserOrChatInfoDialog.tsx index 2cccd15..e6471dc 100644 --- a/client/ui/routers/UserOrChatInfoDialog.tsx +++ b/client/ui/routers/UserOrChatInfoDialog.tsx @@ -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() - const location = useLocation() + const favourited = React.useMemo(() => shared.state.favouriteChats.map((v) => v.getId()).indexOf(chat.getId() || '') != -1, [chat, shared.state.favouriteChats]) + + return ( + +
+ +
+ {chat.getTitle()} + ({chat.getType()}) ID: {chat.getType() == 'private' ? id : chat.getId()} +
+
+ + + 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 ? '取消收藏' : '收藏对话'} + { + + }}>打开对话 + +
+ ) + + /* const location = useLocation() const searchParams = useSearchParams() const params = useParams() @@ -42,5 +115,5 @@ export default function UserOrChatInfoDialog() { .join('

') }}> - ) + ) */ } diff --git a/client/ui/routers/UserOrChatInfoDialogDataLoader.ts b/client/ui/routers/UserOrChatInfoDialogDataLoader.ts new file mode 100644 index 0000000..e559871 --- /dev/null +++ b/client/ui/routers/UserOrChatInfoDialogDataLoader.ts @@ -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, + } +}