添加全局应用状态管理!
This commit is contained in:
44
client/ui/app-state/AddFavourtieChatDialog.tsx
Normal file
44
client/ui/app-state/AddFavourtieChatDialog.tsx
Normal file
@@ -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<Dialog | undefined> }) {
|
||||||
|
const inputTargetRef = React.useRef<TextField>(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 (
|
||||||
|
<mdui-dialog close-on-overlay-click close-on-esc headline="添加收藏对话" ref={useRef}>
|
||||||
|
<mdui-text-field clearable label="对话 / 用户 (ID 或 别名)" ref={inputTargetRef} onKeyDown={(event: KeyboardEvent) => {
|
||||||
|
if (event.key == 'Enter')
|
||||||
|
addFavouriteChat()
|
||||||
|
}}></mdui-text-field>
|
||||||
|
<mdui-button slot="action" variant="text" onClick={() => useRef.current!.open = false}>取消</mdui-button>
|
||||||
|
<mdui-button slot="action" variant="text" onClick={() => addFavouriteChat()}>添加</mdui-button>
|
||||||
|
</mdui-dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
25
client/ui/app-state/AppStateContext.ts
Normal file
25
client/ui/app-state/AppStateContext.ts
Normal file
@@ -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<AppState>({
|
||||||
|
openChatInfo: () => {},
|
||||||
|
openUserInfo: () => {},
|
||||||
|
openEditMyProfile: () => {},
|
||||||
|
openAddFavouriteChat: () => {},
|
||||||
|
openChat: () => {},
|
||||||
|
closeChat: () => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export type { AppState }
|
||||||
|
|
||||||
|
export default AppStateContext
|
||||||
85
client/ui/app-state/AppStateContextWrapper.tsx
Normal file
85
client/ui/app-state/AppStateContextWrapper.tsx
Normal file
@@ -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<AppState | undefined> }) {
|
||||||
|
const [userOrChatInfoDialogState, setUserOrChatInfoDialogState] = React.useState<Chat[]>([])
|
||||||
|
const lastUserOrChatInfoDialogStateRef = React.useRef<Chat>()
|
||||||
|
const userOrChatInfoDialogRef = useEffectRef<Dialog>((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<Dialog>()
|
||||||
|
const addFavouriteChatDialogRef = React.useRef<Dialog>()
|
||||||
|
|
||||||
|
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<Dialog>()
|
||||||
|
|
||||||
|
return <AppStateContext.Provider value={useRef.current = class {
|
||||||
|
static async openChatInfo(chat: Chat | string) {
|
||||||
|
if (!(chat instanceof Chat))
|
||||||
|
chat = (await Chat.getById(getClient(), chat))!
|
||||||
|
|
||||||
|
setUserOrChatInfoDialogState([...userOrChatInfoDialogState, chat])
|
||||||
|
}
|
||||||
|
static async openUserInfo(user: Chat | User | string) {
|
||||||
|
if (typeof user == 'string') user = (await Chat.getOrCreatePrivateChat(getClient(), user))!
|
||||||
|
else if (user instanceof User) user = (await Chat.getOrCreatePrivateChat(getClient(), user.getId()))!
|
||||||
|
return this.openChatInfo(user)
|
||||||
|
}
|
||||||
|
static openEditMyProfile() {
|
||||||
|
editMyProfileDialogRef.current!.open = true
|
||||||
|
}
|
||||||
|
static openAddFavouriteChat() {
|
||||||
|
addFavouriteChatDialogRef.current!.open = true
|
||||||
|
}
|
||||||
|
static async openChat(chat: string | Chat, inDialog?: boolean) {
|
||||||
|
if (chat instanceof Chat) chat = chat.getId()
|
||||||
|
|
||||||
|
setUseChatFragmentDialog(inDialog || false)
|
||||||
|
setUserOrChatInfoDialogState([])
|
||||||
|
setCurrentSelectedChatId(chat)
|
||||||
|
chatFragmentDialogRef.current!.open = true
|
||||||
|
}
|
||||||
|
static closeChat() {
|
||||||
|
if (chatFragmentDialogRef.current!.open) {
|
||||||
|
chatFragmentDialogRef.current!.open = false
|
||||||
|
$(chatFragmentDialogRef.current!).one('closed', () => setCurrentSelectedChatId(''))
|
||||||
|
}
|
||||||
|
else
|
||||||
|
setCurrentSelectedChatId('')
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<UserOrChatInfoDialog chat={userOrChatInfoDialogState[userOrChatInfoDialogState.length - 1] || lastUserOrChatInfoDialogStateRef.current} useRef={userOrChatInfoDialogRef} />
|
||||||
|
<EditMyProfileDialog useRef={editMyProfileDialogRef} />
|
||||||
|
<AddFavourtieChatDialog useRef={addFavouriteChatDialogRef} />
|
||||||
|
{useChatFragmentDialog && currentSelectedChatId && currentSelectedChatId != '' && <ChatFragmentDialog chatId={currentSelectedChatId} useRef={chatFragmentDialogRef} />}
|
||||||
|
{children}
|
||||||
|
</AppStateContext.Provider>
|
||||||
|
}
|
||||||
26
client/ui/app-state/ChatFragmentDialog.tsx
Normal file
26
client/ui/app-state/ChatFragmentDialog.tsx
Normal file
@@ -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<Dialog | undefined> }) {
|
||||||
|
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 <mdui-dialog fullscreen ref={useRef}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
}}>
|
||||||
|
<LazyChatFragment chatId={chatId} openedInDialog={true} />
|
||||||
|
</div>
|
||||||
|
</mdui-dialog>
|
||||||
|
}
|
||||||
100
client/ui/app-state/EditMyProfileDialog.tsx
Normal file
100
client/ui/app-state/EditMyProfileDialog.tsx
Normal file
@@ -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<Dialog | undefined> }) {
|
||||||
|
const [mySelf, setMySelf] = React.useState<UserMySelf>()
|
||||||
|
useAsyncEffect(async () => setMySelf(await ClientCache.getMySelf() as UserMySelf))
|
||||||
|
|
||||||
|
const chooseAvatarFileRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const editNickNameRef = React.useRef<TextField>(null)
|
||||||
|
const editUserNameRef = React.useRef<TextField>(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 (
|
||||||
|
<mdui-dialog close-on-overlay-click close-on-esc ref={useRef}>
|
||||||
|
<div style={{
|
||||||
|
display: "none"
|
||||||
|
}}>
|
||||||
|
<input type="file" name="选择头像" ref={chooseAvatarFileRef}
|
||||||
|
accept="image/*" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<AvatarMySelf onClick={() => {
|
||||||
|
chooseAvatarFileRef.current!.value = ''
|
||||||
|
chooseAvatarFileRef.current!.click()
|
||||||
|
}} style={{
|
||||||
|
width: '50px',
|
||||||
|
height: '50px',
|
||||||
|
}} />
|
||||||
|
<mdui-text-field variant="outlined" placeholder="昵称" ref={editNickNameRef} style={{
|
||||||
|
marginLeft: "15px",
|
||||||
|
}} value={mySelf?.getNickName()}></mdui-text-field>
|
||||||
|
</div>
|
||||||
|
<mdui-divider style={{
|
||||||
|
marginTop: "10px",
|
||||||
|
}}></mdui-divider>
|
||||||
|
|
||||||
|
<mdui-text-field style={{ marginTop: "10px", }} variant="outlined" label="用户 ID" value={mySelf?.getId() || ''} readonly onClick={(e: MouseEvent) => {
|
||||||
|
const input = e.target as HTMLInputElement
|
||||||
|
input.select()
|
||||||
|
input.setSelectionRange(0, 1145141919810)
|
||||||
|
}}></mdui-text-field>
|
||||||
|
<mdui-text-field style={{ marginTop: "20px", }} variant="outlined" label="用户名" value={mySelf?.getUserName() || ''} ref={editUserNameRef}></mdui-text-field>
|
||||||
|
|
||||||
|
<mdui-button slot="action" variant="text" onClick={() => useRef.current!.open = false}>取消</mdui-button>
|
||||||
|
<mdui-button slot="action" variant="text" onClick={async () => {
|
||||||
|
try {
|
||||||
|
await mySelf?.updateProfileOrThrow({
|
||||||
|
nickname: editNickNameRef.current?.value,
|
||||||
|
username: editUserNameRef.current?.value,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof CallbackError)
|
||||||
|
showSnackbar({
|
||||||
|
message: '更新资料失败: ' + e.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
showSnackbar({
|
||||||
|
message: "修改成功, 刷新页面以更新",
|
||||||
|
})
|
||||||
|
}}>更新</mdui-button>
|
||||||
|
</mdui-dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
116
client/ui/app-state/UserOrChatInfoDialog.tsx
Normal file
116
client/ui/app-state/UserOrChatInfoDialog.tsx
Normal file
@@ -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<Dialog | undefined> }) {
|
||||||
|
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 (
|
||||||
|
<mdui-dialog ref={useRef}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Avatar src={getClient().getUrlForFileByHash(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() + (isMySelf ? ' (我)' : '')}</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>
|
||||||
|
{
|
||||||
|
isMySelf && <mdui-list-item icon="edit" rounded onClick={() => AppState.openEditMyProfile()}>
|
||||||
|
编辑资料
|
||||||
|
</mdui-list-item>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!isMySelf && <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={async () => {
|
||||||
|
AppState.openChat(chat!)
|
||||||
|
}}>打开对话</mdui-list-item>
|
||||||
|
</mdui-list>
|
||||||
|
</mdui-dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user