From 989933d07c08ed8ec15a5a4a53b3c97328775a2a Mon Sep 17 00:00:00 2001 From: CrescentLeaf Date: Sat, 20 Dec 2025 17:30:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=91=E5=BC=84=E4=BA=86=E4=B8=80=E5=9D=A8?= =?UTF-8?q?=E5=8F=B2=E5=B1=B1,=20=E5=8F=AF=E8=83=BD=E5=9C=A8=E4=B8=8B?= =?UTF-8?q?=E4=B8=80=E4=B8=AA=20commit=20=E4=BC=9A=E6=92=A4=E9=94=80?= =?UTF-8?q?=E6=9B=B4=E6=94=B9,=20=E6=88=96=E8=80=85=E7=BB=A7=E7=BB=AD?= =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ui/Main.tsx | 271 +++++------ client/ui/chat-fragment/ChatFragment.tsx | 432 +++++++++--------- client/ui/main-page/AllChatsList.tsx | 1 + client/ui/main-page/FavouriteChatsList.tsx | 1 + client/ui/main-page/RecentChatsList.tsx | 1 + client/ui/routers/ChatFragmentDialog.tsx | 21 +- client/ui/routers/ChatInfoDialogDataLoader.ts | 20 + client/ui/routers/RouterDialogsContext.ts | 5 + .../routers/RouterDialogsContextWrapper.tsx | 62 +++ client/ui/routers/useRouterDialogRef.ts | 26 +- 10 files changed, 472 insertions(+), 368 deletions(-) create mode 100644 client/ui/routers/ChatInfoDialogDataLoader.ts create mode 100644 client/ui/routers/RouterDialogsContext.ts create mode 100644 client/ui/routers/RouterDialogsContextWrapper.tsx diff --git a/client/ui/Main.tsx b/client/ui/Main.tsx index 273c63b..48bbd4e 100644 --- a/client/ui/Main.tsx +++ b/client/ui/Main.tsx @@ -19,6 +19,7 @@ import FavouriteChatsList from "./main-page/FavouriteChatsList.tsx" import RecentChatsList from "./main-page/RecentChatsList.tsx" import UserOrChatInfoDialog from "./routers/UserOrChatInfoDialog.tsx" import UserOrChatInfoDialogLoader from "./routers/UserOrChatInfoDialogDataLoader.ts" +import ChatInfoDialogDataLoader from "./routers/ChatInfoDialogDataLoader.ts" import ChatFragmentDialog from "./routers/ChatFragmentDialog.tsx" import EffectOnly from "./EffectOnly.tsx" import MainSharedReducer from "./MainSharedReducer.ts" @@ -29,6 +30,7 @@ import Split from 'split.js' import data from "../data.ts" import LazyChatFragment from "./chat-fragment/LazyChatFragment.tsx" import AddFavourtieChatDialog from "./routers/AddFavourtieChatDialog.tsx" +import RouterDialogsContextWrapper from './routers/RouterDialogsContextWrapper.tsx' function Root() { const [myProfileCache, setMyProfileCache] = React.useState() @@ -117,140 +119,144 @@ function Root() { }, []) return ( - -
- { - // 将子路由渲染到此处 - - } - - - - - gotoUserInfo(nav, myProfileCache!.getId())}> - {myProfileCache?.getNickName()} - - - 账号设置 - - 客户端设置 - nav('/add/favourite_chat')}>添加收藏对话 - 创建新的群组 - -
- - LingChair Web v{__APP_VERSION__}
- Build: {__GIT_HASH__} ({__BUILD_TIME__})
- 在 Codeberg 上查看源代码 -
-
- { - /** - * Default: 侧边列表提供列表切换 - */ - !isMobileUI() ? - - drawerRef.current!.open = true}> - - - - - + + +
+ { + // 将子路由渲染到此处 + + } + + + + + gotoUserInfo(nav, myProfileCache!.getId())}> + {myProfileCache?.getNickName()} + + + 账号设置 + + 客户端设置 + nav('/add/favourite_chat')}>添加收藏对话 + 创建新的群组 + +
+ + LingChair Web v{__APP_VERSION__}
+ Build: {__GIT_HASH__} ({__BUILD_TIME__})
+ 在 Codeberg 上查看源代码 +
+
+ { + /** + * Default: 侧边列表提供列表切换 + */ + !isMobileUI() ? + + drawerRef.current!.open = true}> + + + + + + /** + * Mobile: 底部导航栏提供列表切换 + */ + : + drawerRef.current!.open = true}> + { + ({ + Recents: "最近对话", + Favourites: "收藏对话", + AllChats: "所有对话", + })[currentShowPage] + } +
+
+ } + { + /** + * Mobile: 指定高度的容器 + * Default: 侧边列表 + */ + + } + { + !isMobileUI() &&
+ { + (state.currentSelectedChatId && state.currentSelectedChatId != '') + ? + :
+ 选择以开始对话...... +
+ } +
+ } + { /** * Mobile: 底部导航栏提供列表切换 + * Default: 侧边列表提供列表切换 */ - : - drawerRef.current!.open = true}> - { - ({ - Recents: "最近对话", - Favourites: "收藏对话", - AllChats: "所有对话", - })[currentShowPage] - } -
-
- } - { - /** - * Mobile: 指定高度的容器 - * Default: 侧边列表 - */ - - } - { -
- { - (state.currentSelectedChatId && state.currentSelectedChatId != '') - ? - :
- 选择以开始对话...... -
- } -
- } - { - /** - * Mobile: 底部导航栏提供列表切换 - * Default: 侧边列表提供列表切换 - */ - isMobileUI() && - 最近对话 - 收藏对话 - 全部对话 - - } -
-
+ 最近对话 + 收藏对话 + 全部对话 + + } +
+
+ ) } @@ -302,6 +308,13 @@ export default function Main() { { path: 'chat', Component: ChatFragmentDialog, + children: [ + { + path: 'info', + Component: UserOrChatInfoDialog, + loader: ChatInfoDialogDataLoader, + }, + ], }, ], }]) diff --git a/client/ui/chat-fragment/ChatFragment.tsx b/client/ui/chat-fragment/ChatFragment.tsx index c29ac05..9c8e013 100644 --- a/client/ui/chat-fragment/ChatFragment.tsx +++ b/client/ui/chat-fragment/ChatFragment.tsx @@ -2,9 +2,8 @@ import { $, Tab, TextField } from "mdui" import useEventListener from "../../utils/useEventListener" import useEffectRef from "../../utils/useEffectRef" import isMobileUI from "../../utils/isMobileUI" -import { useLocation, useNavigate } from "react-router" +import { Outlet, useLocation, useNavigate, NavigateFunction } from "react-router" import { Chat } from "lingchair-client-protocol" -import gotoChatInfo from "../routers/gotoChatInfo" import Preference from "../preference/Preference" import PreferenceHeader from "../preference/PreferenceHeader" import PreferenceLayout from "../preference/PreferenceLayout" @@ -14,6 +13,10 @@ import TextFieldPreference from "../preference/TextFieldPreference" import * as React from 'react' import ChatMessageContainer from "./ChatMessageContainer" +function gotoChatInfo(nav: NavigateFunction, id: string) { + nav('/chat/info?id=' + id) +} + interface MduiTabFitSizeArgs extends React.HTMLAttributes { value: string } @@ -44,59 +47,60 @@ export default function ChatFragment({ const chatPanelRef = React.useRef() const inputRef = React.useRef() - return
- ((ref) => { - $(ref.current!.shadowRoot).append(``) - $(tabRef.current!.shadowRoot).append(``) - ; (!isMobileUI()) && $(tabRef.current!.shadowRoot).append(``) - }, [])} style={{ - position: 'sticky', - display: "flex", - flexDirection: "column", + return ( +
- { - openedWithRouter && nav(-1)} style={{ - alignSelf: 'center', - marginLeft: '5px', - marginRight: '5px', - }}> - } - ((ref) => { + $(ref.current!.shadowRoot).append(``) + $(tabRef.current!.shadowRoot).append(``) + ; (!isMobileUI()) && $(tabRef.current!.shadowRoot).append(``) + }, [])} style={{ position: 'sticky', display: "flex", flexDirection: "column", - height: "100%", - width: '100%', - overflowX: 'auto', }}> { - chatInfo.isMember() ? <> - {chatInfo.getTitle()} - {chatInfo.getType() == 'group' && chatInfo.isAdmin() && 加入请求} - {chatInfo.getType() == 'group' && 群组成员} - - : {chatInfo.getTitle()} + openedWithRouter && nav(-1)} style={{ + alignSelf: 'center', + marginLeft: '5px', + marginRight: '5px', + }}> } - {chatInfo.getType() == 'group' && 设置} + + { + chatInfo.isMember() ? <> + {chatInfo.getTitle()} + {chatInfo.getType() == 'group' && chatInfo.isAdmin() && 加入请求} + {chatInfo.getType() == 'group' && 群组成员} + + : {chatInfo.getTitle()} + } + {chatInfo.getType() == 'group' && 设置} +
{ window.open('/chat?id=' + chatInfo.getId(), '_blank') - }} style={{ alignSelf: 'center', marginLeft: '5px', marginRight: '5px', }}> { - + }} style={{ alignSelf: 'center', marginLeft: '5px', @@ -108,189 +112,189 @@ export default function ChatFragment({ marginRight: '5px', }}>
- - -
- {/* 非群成员 */} -
-
- { - const scrollTop = (e.target as HTMLDivElement).scrollTop - if (scrollTop == 0) { - // 加载更多 - } - }}> -
- {/* 这里显示一些提示 */} -
- - { - // 输入框 - } -
{ - // 文件拽入 }}> - { - if (inputRef.current?.value.trim() == '') { - // 清空缓存的文件 - } - }} onKeyDown={(event: KeyboardEvent) => { - if (event.ctrlKey && event.key == 'Enter') { - // 发送消息 - } - }} onPaste={(event: ClipboardEvent) => { - for (const item of event.clipboardData?.items || []) { - if (item.kind == 'file') { - event.preventDefault() - const file = item.getAsFile() as File - // 添加文件 +
+ {/* 非群成员 */} +
+ + { + const scrollTop = (e.target as HTMLDivElement).scrollTop + if (scrollTop == 0) { + // 加载更多 + } + }}> +
+ {/* 这里显示一些提示 */} +
+ + { + // 输入框 + } +
{ + // 文件拽入 + }}> + { + if (inputRef.current?.value.trim() == '') { + // 清空缓存的文件 } - } - }} style={{ - marginRight: '10px', - marginTop: '3px', - marginBottom: '3px', - }}> - { - // 添加文件 - }}> - { - // 发送消息 - }}> + }} onKeyDown={(event: KeyboardEvent) => { + if (event.ctrlKey && event.key == 'Enter') { + // 发送消息 + } + }} onPaste={(event: ClipboardEvent) => { + for (const item of event.clipboardData?.items || []) { + if (item.kind == 'file') { + event.preventDefault() + const file = item.getAsFile() as File + // 添加文件 + } + } + }} style={{ + marginRight: '10px', + marginTop: '3px', + marginBottom: '3px', + }}> + { + // 添加文件 + }}> + { + // 发送消息 + }}> +
+ +
+
+
+ { + chatInfo.getType() == 'group' && + {/* */} + + } + { + chatInfo.getType() == 'group' && + {/* {chatInfo.isAdmin() && } */} + + } +
- +
-
-
- { - chatInfo.getType() == 'group' && - {/* */} - - } - { - chatInfo.getType() == 'group' && - {/* {chatInfo.isAdmin() && } */} - - } - -
- -
- { - /* chatInfo.getType() == 'group' && - - - { - uploadChatAvatarRef.current!.click() - }} /> - - - - - - - { - groupPreferenceStore.state.new_member_join_method == 'answered_and_allowed_by_admin' - && + + + { + uploadChatAvatarRef.current!.click() + }} /> + + + + + - } - - */ - } - { - chatInfo.getType() == 'private' && ( -
- 未制作 -
- ) - } -
-
+ disabled={true || !chatInfo.isAdmin()} + state={groupPreferenceStore.state.allow_new_member_from_invitation || false} /> + + { + groupPreferenceStore.state.new_member_join_method == 'answered_and_allowed_by_admin' + && + } + + */ + } + { + chatInfo.getType() == 'private' && ( +
+ 未制作 +
+ ) + } + +
+ ) } diff --git a/client/ui/main-page/AllChatsList.tsx b/client/ui/main-page/AllChatsList.tsx index f24f4ba..13895fd 100644 --- a/client/ui/main-page/AllChatsList.tsx +++ b/client/ui/main-page/AllChatsList.tsx @@ -58,6 +58,7 @@ export default function AllChatsList({ ...props }: React.HTMLAttributes { + useEventListener(dialogRef, 'open', () => { const shadow = dialogRef.current!.shadowRoot as ShadowRoot const panel = shadow.querySelector(".panel") as HTMLElement panel.style.padding = '0' @@ -21,7 +24,15 @@ export default function ChatFragmentDialog() { body.style.display = 'flex' }, []) - return - - + return (<> + +
+ +
+
+ + ) } diff --git a/client/ui/routers/ChatInfoDialogDataLoader.ts b/client/ui/routers/ChatInfoDialogDataLoader.ts new file mode 100644 index 0000000..1f0f2ae --- /dev/null +++ b/client/ui/routers/ChatInfoDialogDataLoader.ts @@ -0,0 +1,20 @@ +import { LoaderFunctionArgs } from "react-router" +import getClient from "../../getClient" +import { Chat } from "lingchair-client-protocol" +import ClientCache from "../../ClientCache" + +export default async function ChatInfoDialogDataLoader({ params, request }: LoaderFunctionArgs) { + const searchParams = new URL(request.url).searchParams + + let id = searchParams.get('id') + const chat = await Chat.getByIdOrThrow(getClient(), id!) + + if (chat.getType() == 'private') + id = await chat.getTheOtherUserIdOrThrow() + + return { + mySelf: await ClientCache.getMySelf(), + chat, + id, + } +} diff --git a/client/ui/routers/RouterDialogsContext.ts b/client/ui/routers/RouterDialogsContext.ts new file mode 100644 index 0000000..6558873 --- /dev/null +++ b/client/ui/routers/RouterDialogsContext.ts @@ -0,0 +1,5 @@ +import * as React from 'react' + +const RouterDialogsContext = React.createContext(() => {}) + +export default RouterDialogsContext diff --git a/client/ui/routers/RouterDialogsContextWrapper.tsx b/client/ui/routers/RouterDialogsContextWrapper.tsx new file mode 100644 index 0000000..20e4d6f --- /dev/null +++ b/client/ui/routers/RouterDialogsContextWrapper.tsx @@ -0,0 +1,62 @@ +import { Dialog } from 'mdui' +import * as React from 'react' +import RouterDialogsContext from './RouterDialogsContext' +import { BlockerFunction, useBlocker, useNavigate } from "react-router" +import sleep from "../../utils/sleep" + +const routerDialogsList = [] + +export default function RouterDialogsContextWrapper({ children }: React.HTMLAttributes) { + const proceedRef = React.useRef<() => void>() + const nav = useNavigate() + // 进入子路由不会拦截上一个路由对话框的关闭 + // 没有路由对话框不会拦截 + const blocker = useBlocker(React.useCallback(({ nextLocation, currentLocation }) => { + // 只有当有对话框时,才检查路由变化 + if (routerDialogsList.length === 0) { + return false // 没有对话框,允许所有导航 + } + + // 检查是否是同一个路由 + if (nextLocation.pathname === currentLocation.pathname) { + return false // 相同路由,允许 + } + + // 检查是否是子路由 + if (nextLocation.pathname.startsWith(currentLocation.pathname + '/')) { + return false // 是子路由,允许 + } + + // 其他情况:阻止导航 + return true + }, [])) + + // 避免用户手动返回导致动画丢失 + React.useEffect(() => { + if (blocker.state === "blocked") { + console.log(location) + console.log(routerDialogsList[routerDialogsList.length - 1].current) + console.log(blocker) + proceedRef.current = blocker.proceed + // 这个让姐姐来就好啦 + routerDialogsList.length != 0 && (routerDialogsList[routerDialogsList.length - 1].current!.open = false) + } + }, [blocker.state]) + + // 注册 + // 理应在 Effect 里 + function registerRouterDialog(ref: React.MutableRefObject) { + routerDialogsList.push(ref) + // 正常情况下不可能同时关掉两个对话框 + // 不过要是真有的话, 再说吧 + ref.current!.addEventListener('closed', async () => { + routerDialogsList.splice(routerDialogsList.length - 1, 1) + await sleep(10) + proceedRef.current ? proceedRef.current() : nav(-1) + }) + } + + return + { children } + +} diff --git a/client/ui/routers/useRouterDialogRef.ts b/client/ui/routers/useRouterDialogRef.ts index 8144ed9..a9d385b 100644 --- a/client/ui/routers/useRouterDialogRef.ts +++ b/client/ui/routers/useRouterDialogRef.ts @@ -3,31 +3,17 @@ import useAsyncEffect from "../../utils/useAsyncEffect" import sleep from "../../utils/sleep" import { BlockerFunction, useBlocker, useNavigate } from "react-router" import * as React from 'react' +import RouterDialogsContext from './RouterDialogsContext' export default function useRouterDialogRef() { - const dialogRef = React.useRef() - const proceedRef = React.useRef<() => void>() - const shouldBlock = React.useRef(true) - const nav = useNavigate() - const blocker = useBlocker(React.useCallback(() => shouldBlock.current, [])) - - // 避免用户手动返回导致动画丢失 - React.useEffect(() => { - if (blocker.state === "blocked") { - proceedRef.current = blocker.proceed - // 这个让姐姐来就好啦 - dialogRef.current!.open = false - } - }, [blocker.state]) - + const dialogRef = React.useRef(RouterDialogsContext) + const registerRouterDialog = React.useContext(RouterDialogsContext) + useAsyncEffect(async () => { + registerRouterDialog(dialogRef) await sleep(10) dialogRef.current!.open = true - dialogRef.current!.addEventListener('closed', async () => { - shouldBlock.current = false - await sleep(10) - proceedRef.current ? proceedRef.current() : nav(-1) - }) }, []) + return dialogRef }