From 44168b47041652f6677642549376210104b2c05e Mon Sep 17 00:00:00 2001 From: CrescentLeaf Date: Sat, 27 Dec 2025 23:04:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=A8=E5=B1=80=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86=EF=BC=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/app-state/AddFavourtieChatDialog.tsx | 44 +++++++ client/ui/app-state/AppStateContext.ts | 25 ++++ .../ui/app-state/AppStateContextWrapper.tsx | 85 +++++++++++++ client/ui/app-state/ChatFragmentDialog.tsx | 26 ++++ client/ui/app-state/EditMyProfileDialog.tsx | 100 +++++++++++++++ client/ui/app-state/UserOrChatInfoDialog.tsx | 116 ++++++++++++++++++ 6 files changed, 396 insertions(+) create mode 100644 client/ui/app-state/AddFavourtieChatDialog.tsx create mode 100644 client/ui/app-state/AppStateContext.ts create mode 100644 client/ui/app-state/AppStateContextWrapper.tsx create mode 100644 client/ui/app-state/ChatFragmentDialog.tsx create mode 100644 client/ui/app-state/EditMyProfileDialog.tsx create mode 100644 client/ui/app-state/UserOrChatInfoDialog.tsx diff --git a/client/ui/app-state/AddFavourtieChatDialog.tsx b/client/ui/app-state/AddFavourtieChatDialog.tsx new file mode 100644 index 0000000..9372655 --- /dev/null +++ b/client/ui/app-state/AddFavourtieChatDialog.tsx @@ -0,0 +1,44 @@ +import * as React from 'react' +import { Button, Dialog, snackbar, TextField } from "mdui" +import { data, useNavigate } 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' +import ClientCache from '../../ClientCache' +import AppStateContext from './AppStateContext' + +export default function AddFavourtieChatDialog({ useRef }: { useRef: React.MutableRefObject }) { + const inputTargetRef = React.useRef(null) + + useEventListener(useRef, 'closed', () => { + inputTargetRef.current!.value = '' + }) + + async function addFavouriteChat() { + try { + await (await ClientCache.getMySelf())!.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() + }}> + useRef.current!.open = false}>取消 + addFavouriteChat()}>添加 + + ) +} diff --git a/client/ui/app-state/AppStateContext.ts b/client/ui/app-state/AppStateContext.ts new file mode 100644 index 0000000..bf5be36 --- /dev/null +++ b/client/ui/app-state/AppStateContext.ts @@ -0,0 +1,25 @@ +import { Chat, User } from 'lingchair-client-protocol' +import { Dialog } from 'mdui' +import * as React from 'react' + +type AppState = { + openChatInfo: (chat: Chat | string) => void, + openUserInfo: (user: Chat | User | string) => void, + openEditMyProfile: () => void, + openAddFavouriteChat: () => void, + openChat: (chat: string | Chat) => void, + closeChat: () => void, +} + +const AppStateContext = React.createContext({ + openChatInfo: () => {}, + openUserInfo: () => {}, + openEditMyProfile: () => {}, + openAddFavouriteChat: () => {}, + openChat: () => {}, + closeChat: () => {}, +}) + +export type { AppState } + +export default AppStateContext diff --git a/client/ui/app-state/AppStateContextWrapper.tsx b/client/ui/app-state/AppStateContextWrapper.tsx new file mode 100644 index 0000000..55f9e35 --- /dev/null +++ b/client/ui/app-state/AppStateContextWrapper.tsx @@ -0,0 +1,85 @@ +import { $, Dialog } from "mdui" +import AppStateContext, { AppState } from "./AppStateContext" +import { Chat, User } from "lingchair-client-protocol" +import getClient from "../../getClient" +import UserOrChatInfoDialog from "./UserOrChatInfoDialog" +import useEffectRef from "../../utils/useEffectRef" +import EditMyProfileDialog from "./EditMyProfileDialog" +import AddFavourtieChatDialog from "./AddFavourtieChatDialog" +import * as React from 'react' +import { useContextSelector } from "use-context-selector" +import MainSharedContext, { Shared } from "../MainSharedContext" +import ChatFragmentDialog from "./ChatFragmentDialog" + +export default function DialogContextWrapper({ children, useRef }: { children: React.ReactNode, useRef: React.MutableRefObject }) { + const [userOrChatInfoDialogState, setUserOrChatInfoDialogState] = React.useState([]) + const lastUserOrChatInfoDialogStateRef = React.useRef() + const userOrChatInfoDialogRef = useEffectRef((ref) => { + ref.current!.addEventListener('closed', () => { + setUserOrChatInfoDialogState([]) + }) + ref.current!.addEventListener('overlay-click', () => { + ref.current!.open = false + }) + }, []) + React.useEffect(() => { + userOrChatInfoDialogState.length != 0 && (lastUserOrChatInfoDialogStateRef.current = userOrChatInfoDialogState[userOrChatInfoDialogState.length - 1]) + userOrChatInfoDialogRef.current!.open = userOrChatInfoDialogState.length != 0 + }, [userOrChatInfoDialogState]) + + const editMyProfileDialogRef = React.useRef() + const addFavouriteChatDialogRef = React.useRef() + + const setCurrentSelectedChatId = useContextSelector( + MainSharedContext, + (context: Shared) => context.setCurrentSelectedChatId + ) + const currentSelectedChatId = useContextSelector( + MainSharedContext, + (context: Shared) => context.state.currentSelectedChatId + ) + const [useChatFragmentDialog, setUseChatFragmentDialog] = React.useState(false) + const chatFragmentDialogRef = React.useRef() + + return setCurrentSelectedChatId('')) + } + else + setCurrentSelectedChatId('') + } + }}> + + + + {useChatFragmentDialog && currentSelectedChatId && currentSelectedChatId != '' && } + {children} + +} diff --git a/client/ui/app-state/ChatFragmentDialog.tsx b/client/ui/app-state/ChatFragmentDialog.tsx new file mode 100644 index 0000000..e990069 --- /dev/null +++ b/client/ui/app-state/ChatFragmentDialog.tsx @@ -0,0 +1,26 @@ +import { Dialog } from "mdui" +import * as React from 'react' +import LazyChatFragment from "../chat-fragment/LazyChatFragment" + +export default function ChatFragmentDialog({ chatId, useRef }: { chatId: string, useRef: React.MutableRefObject }) { + React.useEffect(() => { + const shadow = useRef.current!.shadowRoot as ShadowRoot + const panel = shadow.querySelector(".panel") as HTMLElement + panel.style.padding = '0' + panel.style.color = 'inherit' + panel.style.backgroundColor = 'rgb(var(--mdui-color-background))' + panel.style.setProperty('--mdui-color-background', 'inherit') + const body = shadow.querySelector(".body") as HTMLElement + body.style.height = '100%' + body.style.display = 'flex' + }, [chatId]) + + return +
+ +
+
+} diff --git a/client/ui/app-state/EditMyProfileDialog.tsx b/client/ui/app-state/EditMyProfileDialog.tsx new file mode 100644 index 0000000..5c738e5 --- /dev/null +++ b/client/ui/app-state/EditMyProfileDialog.tsx @@ -0,0 +1,100 @@ +import { CallbackError, UserMySelf } from "lingchair-client-protocol" +import ClientCache from "../../ClientCache" +import AvatarMySelf from "../AvatarMySelf" +import useAsyncEffect from "../../utils/useAsyncEffect" +import getClient from "../../getClient" +import { useNavigate } from "react-router" +import showSnackbar from "../../utils/showSnackbar" +import useEventListener from "../../utils/useEventListener" +import { Dialog, TextField } from "mdui" +import * as React from 'react' + +export default function EditMyProfileDialog({ useRef }: { useRef: React.MutableRefObject }) { + const [mySelf, setMySelf] = React.useState() + useAsyncEffect(async () => setMySelf(await ClientCache.getMySelf() as UserMySelf)) + + const chooseAvatarFileRef = React.useRef(null) + + const editNickNameRef = React.useRef(null) + const editUserNameRef = React.useRef(null) + + useEventListener(chooseAvatarFileRef, 'change', async (_) => { + const file = chooseAvatarFileRef.current!.files?.[0] as File + if (file == null) return + + try { + const hash = await getClient().uploadFile({ + fileName: 'UserAvatar', + fileData: file, + }) + await mySelf?.setAvatarFileHashOrThrow(hash) + showSnackbar({ + message: "修改成功, 刷新页面以更新", + }) + } catch (e) { + console.error(e) + if (e instanceof CallbackError) + showSnackbar({ + message: '上传头像失败: ' + e.message + }) + showSnackbar({ + message: '上传头像失败: ' + (e instanceof Error ? e.message : e) + }) + } + }) + + return ( + +
+ +
+ +
+ { + chooseAvatarFileRef.current!.value = '' + chooseAvatarFileRef.current!.click() + }} style={{ + width: '50px', + height: '50px', + }} /> + +
+ + + { + const input = e.target as HTMLInputElement + input.select() + input.setSelectionRange(0, 1145141919810) + }}> + + + useRef.current!.open = false}>取消 + { + try { + await mySelf?.updateProfileOrThrow({ + nickname: editNickNameRef.current?.value, + username: editUserNameRef.current?.value, + }) + } catch (e) { + if (e instanceof CallbackError) + showSnackbar({ + message: '更新资料失败: ' + e.message + }) + } + showSnackbar({ + message: "修改成功, 刷新页面以更新", + }) + }}>更新 +
+ ) +} \ No newline at end of file diff --git a/client/ui/app-state/UserOrChatInfoDialog.tsx b/client/ui/app-state/UserOrChatInfoDialog.tsx new file mode 100644 index 0000000..2558b05 --- /dev/null +++ b/client/ui/app-state/UserOrChatInfoDialog.tsx @@ -0,0 +1,116 @@ +import { Dialog, dialog } from "mdui" +import { useLoaderData, useNavigate } from "react-router" +import { CallbackError, Chat } from "lingchair-client-protocol" +import showSnackbar from "../../utils/showSnackbar" +import Avatar from "../Avatar" +import { useContextSelector } from "use-context-selector" +import MainSharedContext, { Shared } from "../MainSharedContext" +import * as React from 'react' +import ClientCache from "../../ClientCache" +import getClient from "../../getClient" +import isMobileUI from "../../utils/isMobileUI" +import useEffectRef from "../../utils/useEffectRef" +import useAsyncEffect from "../../utils/useAsyncEffect" +import AppStateContext from "./AppStateContext" + +export default function UserOrChatInfoDialog({ chat, useRef }: { chat?: Chat, useRef: React.MutableRefObject }) { + const favouriteChats = useContextSelector( + MainSharedContext, + (context: Shared) => context.state.favouriteChats + ) + const setCurrentSelectedChatId = useContextSelector( + MainSharedContext, + (context: Shared) => context.setCurrentSelectedChatId + ) + + const AppState = React.useContext(AppStateContext) + + const [isMySelf, setIsMySelf] = React.useState(false) + const [id, setId] = React.useState('') + useAsyncEffect(async () => { + setIsMySelf(await ClientCache.getMySelf().then((re) => { + const id = re?.getId()! + setId(id) + return Chat.getOrCreatePrivateChat(getClient(), id) + }).then((re) => re?.getId()) == chat?.getId()) + }, [chat]) + + const favourited = React.useMemo(() => favouriteChats.map((v) => v.getId()).indexOf(chat?.getId() || '') != -1, [chat, favouriteChats]) + + return ( + +
+ +
+ {chat?.getTitle() + (isMySelf ? ' (我)' : '')} + ({chat?.getType()}) ID: {chat?.getType() == 'private' ? id : chat?.getId()} +
+
+ + + { + isMySelf && AppState.openEditMyProfile()}> + 编辑资料 + + } + { + !isMySelf && 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 ? '取消收藏' : '收藏对话'} + } + { + AppState.openChat(chat!) + }}>打开对话 + +
+ ) +}