我弄了一坨史山, 可能在下一个 commit 会撤销更改, 或者继续完善

This commit is contained in:
CrescentLeaf
2025-12-20 17:30:14 +08:00
parent 76d518f229
commit 989933d07c
10 changed files with 472 additions and 368 deletions

View File

@@ -19,6 +19,7 @@ import FavouriteChatsList from "./main-page/FavouriteChatsList.tsx"
import RecentChatsList from "./main-page/RecentChatsList.tsx" import RecentChatsList from "./main-page/RecentChatsList.tsx"
import UserOrChatInfoDialog from "./routers/UserOrChatInfoDialog.tsx" import UserOrChatInfoDialog from "./routers/UserOrChatInfoDialog.tsx"
import UserOrChatInfoDialogLoader from "./routers/UserOrChatInfoDialogDataLoader.ts" import UserOrChatInfoDialogLoader from "./routers/UserOrChatInfoDialogDataLoader.ts"
import ChatInfoDialogDataLoader from "./routers/ChatInfoDialogDataLoader.ts"
import ChatFragmentDialog from "./routers/ChatFragmentDialog.tsx" import ChatFragmentDialog from "./routers/ChatFragmentDialog.tsx"
import EffectOnly from "./EffectOnly.tsx" import EffectOnly from "./EffectOnly.tsx"
import MainSharedReducer from "./MainSharedReducer.ts" import MainSharedReducer from "./MainSharedReducer.ts"
@@ -29,6 +30,7 @@ import Split from 'split.js'
import data from "../data.ts" import data from "../data.ts"
import LazyChatFragment from "./chat-fragment/LazyChatFragment.tsx" import LazyChatFragment from "./chat-fragment/LazyChatFragment.tsx"
import AddFavourtieChatDialog from "./routers/AddFavourtieChatDialog.tsx" import AddFavourtieChatDialog from "./routers/AddFavourtieChatDialog.tsx"
import RouterDialogsContextWrapper from './routers/RouterDialogsContextWrapper.tsx'
function Root() { function Root() {
const [myProfileCache, setMyProfileCache] = React.useState<UserMySelf>() const [myProfileCache, setMyProfileCache] = React.useState<UserMySelf>()
@@ -117,140 +119,144 @@ function Root() {
}, []) }, [])
return ( return (
<MainSharedContext.Provider value={sharedContext}> <RouterDialogsContextWrapper>
<div style={{ <MainSharedContext.Provider value={sharedContext}>
display: "flex", <div style={{
position: 'relative', display: "flex",
flexDirection: isMobileUI() ? 'column' : 'row', position: 'relative',
width: '100%', flexDirection: isMobileUI() ? 'column' : 'row',
height: 'var(--whitesilk-window-height)', width: '100%',
}}> height: 'var(--whitesilk-window-height)',
{ }}>
// 将子路由渲染到此处 {
<Outlet /> // 将子路由渲染到此处
} <Outlet />
<LoginDialog open={showLoginDialog} /> }
<RegisterDialog open={showRegisterDialog} /> <LoginDialog open={showLoginDialog} />
<mdui-navigation-drawer ref={drawerRef} modal close-on-esc close-on-overlay-click> <RegisterDialog open={showRegisterDialog} />
<mdui-list style={{ <mdui-navigation-drawer ref={drawerRef} modal close-on-esc close-on-overlay-click>
padding: '10px', <mdui-list style={{
}}> padding: '10px',
<mdui-list-item rounded onClick={() => gotoUserInfo(nav, myProfileCache!.getId())}> }}>
<span>{myProfileCache?.getNickName()}</span> <mdui-list-item rounded onClick={() => gotoUserInfo(nav, myProfileCache!.getId())}>
<AvatarMySelf slot="icon" /> <span>{myProfileCache?.getNickName()}</span>
</mdui-list-item> <AvatarMySelf slot="icon" />
<mdui-list-item rounded icon="manage_accounts"></mdui-list-item> </mdui-list-item>
<mdui-divider style={{ <mdui-list-item rounded icon="manage_accounts"></mdui-list-item>
margin: '10px', <mdui-divider style={{
}}></mdui-divider> margin: '10px',
<mdui-list-item rounded icon="settings"></mdui-list-item> }}></mdui-divider>
<mdui-list-item rounded icon="person_add" onClick={() => nav('/add/favourite_chat')}></mdui-list-item> <mdui-list-item rounded icon="settings"></mdui-list-item>
<mdui-list-item rounded icon="group_add"></mdui-list-item> <mdui-list-item rounded icon="person_add" onClick={() => nav('/add/favourite_chat')}></mdui-list-item>
</mdui-list> <mdui-list-item rounded icon="group_add"></mdui-list-item>
<div style={{ </mdui-list>
flexGrow: 1, <div style={{
}}></div> flexGrow: 1,
<span style={{ }}></div>
padding: '10px', <span style={{
fontSize: 'small', padding: '10px',
}}> fontSize: 'small',
LingChair Web v{__APP_VERSION__}<br /> }}>
Build: <a href={`https://codeberg.org/CrescentLeaf/LingChair/src/commit/${__GIT_HASH_FULL__}`}>{__GIT_HASH__}</a> ({__BUILD_TIME__})<br /> LingChair Web v{__APP_VERSION__}<br />
Codeberg <a href="https://codeberg.org/CrescentLeaf/LingChair"></a> Build: <a href={`https://codeberg.org/CrescentLeaf/LingChair/src/commit/${__GIT_HASH_FULL__}`}>{__GIT_HASH__}</a> ({__BUILD_TIME__})<br />
</span> Codeberg <a href="https://codeberg.org/CrescentLeaf/LingChair"></a>
</mdui-navigation-drawer> </span>
{ </mdui-navigation-drawer>
/** {
* Default: 侧边列表提供列表切换 /**
*/ * Default: 侧边列表提供列表切换
!isMobileUI() ? */
<mdui-navigation-rail ref={navigationRef} contained value="Recents"> !isMobileUI() ?
<mdui-button-icon slot="top" icon="menu" onClick={() => drawerRef.current!.open = true}></mdui-button-icon> <mdui-navigation-rail ref={navigationRef} contained value="Recents">
<mdui-button-icon slot="top" icon="menu" onClick={() => drawerRef.current!.open = true}></mdui-button-icon>
<mdui-navigation-rail-item icon="watch_later--outlined" active-icon="watch_later--filled" value="Recents"></mdui-navigation-rail-item>
<mdui-navigation-rail-item icon="favorite_border" active-icon="favorite" value="Favourites"></mdui-navigation-rail-item> <mdui-navigation-rail-item icon="watch_later--outlined" active-icon="watch_later--filled" value="Recents"></mdui-navigation-rail-item>
<mdui-navigation-rail-item icon="chat--outlined" active-icon="chat--filled" value="AllChats"></mdui-navigation-rail-item> <mdui-navigation-rail-item icon="favorite_border" active-icon="favorite" value="Favourites"></mdui-navigation-rail-item>
</mdui-navigation-rail> <mdui-navigation-rail-item icon="chat--outlined" active-icon="chat--filled" value="AllChats"></mdui-navigation-rail-item>
</mdui-navigation-rail>
/**
* Mobile: 底部导航栏提供列表切换
*/
: <mdui-top-app-bar style={{
position: 'sticky',
marginTop: '3px',
marginRight: '6px',
marginLeft: '15px',
top: '0px',
}}>
<mdui-button-icon icon="menu" onClick={() => drawerRef.current!.open = true}></mdui-button-icon>
<mdui-top-app-bar-title>{
({
Recents: "最近对话",
Favourites: "收藏对话",
AllChats: "所有对话",
})[currentShowPage]
}</mdui-top-app-bar-title>
<div style={{
flexGrow: 1,
}}></div>
</mdui-top-app-bar>
}
{
/**
* Mobile: 指定高度的容器
* Default: 侧边列表
*/
<div style={isMobileUI() ? {
height: 'calc(100% - 80px - 67px)',
marginLeft: '15px',
marginRight: '15px',
marginTop: '5px',
marginBottom: '5px',
} : {
paddingRight: '8px',
}} id="SideBar">
<RecentChatsList style={{
display: currentShowPage == 'Recents' ? undefined : 'none'
}} />
<FavouriteChatsList style={{
display: currentShowPage == 'Favourites' ? undefined : 'none'
}} />
<AllChatsList style={{
display: currentShowPage == 'AllChats' ? undefined : 'none'
}} />
</div>
}
{
!isMobileUI() && <div id="ChatFragment" style={{
display: "flex",
width: '100%'
}}>
{
(state.currentSelectedChatId && state.currentSelectedChatId != '')
? <LazyChatFragment openedWithRouter={false} chatId={state.currentSelectedChatId!} />
: <div style={{
width: '100%',
textAlign: 'center',
alignSelf: 'center',
}}>
......
</div>
}
</div>
}
{
/** /**
* Mobile: 底部导航栏提供列表切换 * Mobile: 底部导航栏提供列表切换
* Default: 侧边列表提供列表切换
*/ */
: <mdui-top-app-bar style={{ isMobileUI() && <mdui-navigation-bar ref={navigationRef} label-visibility="selected" value="Recents" style={{
position: 'sticky', position: 'sticky',
marginTop: '3px', bottom: '0',
marginRight: '6px',
marginLeft: '15px',
top: '0px',
}}> }}>
<mdui-button-icon icon="menu" onClick={() => drawerRef.current!.open = true}></mdui-button-icon> <mdui-navigation-bar-item icon="watch_later--outlined" active-icon="watch_later--filled" value="Recents"></mdui-navigation-bar-item>
<mdui-top-app-bar-title>{ <mdui-navigation-bar-item icon="favorite_border" active-icon="favorite" value="Favourites"></mdui-navigation-bar-item>
({ <mdui-navigation-bar-item icon="chat--outlined" active-icon="chat--filled" value="AllChats"></mdui-navigation-bar-item>
Recents: "最近对话", </mdui-navigation-bar>
Favourites: "收藏对话", }
AllChats: "所有对话", </div>
})[currentShowPage] </MainSharedContext.Provider>
}</mdui-top-app-bar-title> </RouterDialogsContextWrapper>
<div style={{
flexGrow: 1,
}}></div>
</mdui-top-app-bar>
}
{
/**
* Mobile: 指定高度的容器
* Default: 侧边列表
*/
<div style={isMobileUI() ? {
display: 'flex',
height: 'calc(100% - 80px - 67px)',
width: '100%',
} : {
paddingRight: '8px',
}} id="SideBar">
<RecentChatsList style={{
display: currentShowPage == 'Recents' ? undefined : 'none'
}} />
<FavouriteChatsList style={{
display: currentShowPage == 'Favourites' ? undefined : 'none'
}} />
<AllChatsList style={{
display: currentShowPage == 'AllChats' ? undefined : 'none'
}} />
</div>
}
{
<div id="ChatFragment" style={{
display: "flex",
width: '100%'
}}>
{
(state.currentSelectedChatId && state.currentSelectedChatId != '')
? <LazyChatFragment openedWithRouter={false} chatId={state.currentSelectedChatId!} />
: <div style={{
width: '100%',
textAlign: 'center',
alignSelf: 'center',
}}>
......
</div>
}
</div>
}
{
/**
* Mobile: 底部导航栏提供列表切换
* Default: 侧边列表提供列表切换
*/
isMobileUI() && <mdui-navigation-bar ref={navigationRef} label-visibility="selected" value="Recents" style={{
position: 'sticky',
bottom: '0',
}}>
<mdui-navigation-bar-item icon="watch_later--outlined" active-icon="watch_later--filled" value="Recents"></mdui-navigation-bar-item>
<mdui-navigation-bar-item icon="favorite_border" active-icon="favorite" value="Favourites"></mdui-navigation-bar-item>
<mdui-navigation-bar-item icon="chat--outlined" active-icon="chat--filled" value="AllChats"></mdui-navigation-bar-item>
</mdui-navigation-bar>
}
</div>
</MainSharedContext.Provider>
) )
} }
@@ -302,6 +308,13 @@ export default function Main() {
{ {
path: 'chat', path: 'chat',
Component: ChatFragmentDialog, Component: ChatFragmentDialog,
children: [
{
path: 'info',
Component: UserOrChatInfoDialog,
loader: ChatInfoDialogDataLoader,
},
],
}, },
], ],
}]) }])

View File

@@ -2,9 +2,8 @@ import { $, Tab, TextField } from "mdui"
import useEventListener from "../../utils/useEventListener" import useEventListener from "../../utils/useEventListener"
import useEffectRef from "../../utils/useEffectRef" import useEffectRef from "../../utils/useEffectRef"
import isMobileUI from "../../utils/isMobileUI" 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 { Chat } from "lingchair-client-protocol"
import gotoChatInfo from "../routers/gotoChatInfo"
import Preference from "../preference/Preference" import Preference from "../preference/Preference"
import PreferenceHeader from "../preference/PreferenceHeader" import PreferenceHeader from "../preference/PreferenceHeader"
import PreferenceLayout from "../preference/PreferenceLayout" import PreferenceLayout from "../preference/PreferenceLayout"
@@ -14,6 +13,10 @@ import TextFieldPreference from "../preference/TextFieldPreference"
import * as React from 'react' import * as React from 'react'
import ChatMessageContainer from "./ChatMessageContainer" import ChatMessageContainer from "./ChatMessageContainer"
function gotoChatInfo(nav: NavigateFunction, id: string) {
nav('/chat/info?id=' + id)
}
interface MduiTabFitSizeArgs extends React.HTMLAttributes<HTMLElement & Tab> { interface MduiTabFitSizeArgs extends React.HTMLAttributes<HTMLElement & Tab> {
value: string value: string
} }
@@ -44,59 +47,60 @@ export default function ChatFragment({
const chatPanelRef = React.useRef<HTMLElement>() const chatPanelRef = React.useRef<HTMLElement>()
const inputRef = React.useRef<TextField>() const inputRef = React.useRef<TextField>()
return <div style={{ return (
width: '100%', <div style={{
height: '100%', width: '100%',
display: 'flex', height: '100%',
flexDirection: 'column', display: 'flex',
overflowY: 'auto', flexDirection: 'column',
}}> overflowY: 'auto',
<mdui-tabs ref={useEffectRef<HTMLElement>((ref) => {
$(ref.current!.shadowRoot).append(`<style>.container::after { height: 0 !important; }</style>`)
$(tabRef.current!.shadowRoot).append(`<style>.container::after { height: 0 !important; }</style>`)
; (!isMobileUI()) && $(tabRef.current!.shadowRoot).append(`<style>.no-scroll-bar::-webkit-scrollbar{width:0px !important}*::-webkit-scrollbar{width:7px;height:10px}*::-webkit-scrollbar-track{width:6px;background:rgba(#101f1c,0.1);-webkit-border-radius:2em;-moz-border-radius:2em;border-radius:2em}*::-webkit-scrollbar-thumb{background-color:rgba(144,147,153,0.5);background-clip:padding-box;min-height:28px;-webkit-border-radius:2em;-moz-border-radius:2em;border-radius:2em;transition:background-color 0.3s;cursor:pointer}*::-webkit-scrollbar-thumb:hover{background-color:rgba(144,147,153,0.3)}</style>`)
}, [])} style={{
position: 'sticky',
display: "flex",
flexDirection: "column",
}}> }}>
{ <mdui-tabs ref={useEffectRef<HTMLElement>((ref) => {
openedWithRouter && <mdui-button-icon icon="arrow_back" onClick={() => nav(-1)} style={{ $(ref.current!.shadowRoot).append(`<style>.container::after { height: 0 !important; }</style>`)
alignSelf: 'center', $(tabRef.current!.shadowRoot).append(`<style>.container::after { height: 0 !important; }</style>`)
marginLeft: '5px', ; (!isMobileUI()) && $(tabRef.current!.shadowRoot).append(`<style>.no-scroll-bar::-webkit-scrollbar{width:0px !important}*::-webkit-scrollbar{width:7px;height:10px}*::-webkit-scrollbar-track{width:6px;background:rgba(#101f1c,0.1);-webkit-border-radius:2em;-moz-border-radius:2em;border-radius:2em}*::-webkit-scrollbar-thumb{background-color:rgba(144,147,153,0.5);background-clip:padding-box;min-height:28px;-webkit-border-radius:2em;-moz-border-radius:2em;border-radius:2em;transition:background-color 0.3s;cursor:pointer}*::-webkit-scrollbar-thumb:hover{background-color:rgba(144,147,153,0.3)}</style>`)
marginRight: '5px', }, [])} style={{
}}></mdui-button-icon>
}
<mdui-tabs ref={tabRef} value={tabItemSelected} style={{
position: 'sticky', position: 'sticky',
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
height: "100%",
width: '100%',
overflowX: 'auto',
}}> }}>
{ {
chatInfo.isMember() ? <> openedWithRouter && <mdui-button-icon icon="arrow_back" onClick={() => nav(-1)} style={{
<MduiTabFitSize value="Chat">{chatInfo.getTitle()}</MduiTabFitSize> alignSelf: 'center',
{chatInfo.getType() == 'group' && chatInfo.isAdmin() && <MduiTabFitSize value="NewMemberRequests"></MduiTabFitSize>} marginLeft: '5px',
{chatInfo.getType() == 'group' && <MduiTabFitSize value="GroupMembers"></MduiTabFitSize>} marginRight: '5px',
</> }}></mdui-button-icon>
: <MduiTabFitSize value="RequestJoin">{chatInfo.getTitle()}</MduiTabFitSize>
} }
{chatInfo.getType() == 'group' && <MduiTabFitSize value="Settings"></MduiTabFitSize>} <mdui-tabs ref={tabRef} value={tabItemSelected} style={{
position: 'sticky',
display: "flex",
flexDirection: "column",
height: "100%",
width: '100%',
overflowX: 'auto',
}}>
{
chatInfo.isMember() ? <>
<MduiTabFitSize value="Chat">{chatInfo.getTitle()}</MduiTabFitSize>
{chatInfo.getType() == 'group' && chatInfo.isAdmin() && <MduiTabFitSize value="NewMemberRequests"></MduiTabFitSize>}
{chatInfo.getType() == 'group' && <MduiTabFitSize value="GroupMembers"></MduiTabFitSize>}
</>
: <MduiTabFitSize value="RequestJoin">{chatInfo.getTitle()}</MduiTabFitSize>
}
{chatInfo.getType() == 'group' && <MduiTabFitSize value="Settings"></MduiTabFitSize>}
</mdui-tabs>
<div style={{ <div style={{
flexGrow: '1', flexGrow: '1',
}}></div> }}></div>
<mdui-button-icon icon="open_in_new" onClick={() => { <mdui-button-icon icon="open_in_new" onClick={() => {
window.open('/chat?id=' + chatInfo.getId(), '_blank') window.open('/chat?id=' + chatInfo.getId(), '_blank')
}} style={{ }} style={{
alignSelf: 'center', alignSelf: 'center',
marginLeft: '5px', marginLeft: '5px',
marginRight: '5px', marginRight: '5px',
}}></mdui-button-icon> }}></mdui-button-icon>
<mdui-button-icon icon="refresh" onClick={() => { <mdui-button-icon icon="refresh" onClick={() => {
}} style={{ }} style={{
alignSelf: 'center', alignSelf: 'center',
marginLeft: '5px', marginLeft: '5px',
@@ -108,189 +112,189 @@ export default function ChatFragment({
marginRight: '5px', marginRight: '5px',
}}></mdui-button-icon> }}></mdui-button-icon>
</mdui-tabs> </mdui-tabs>
</mdui-tabs> <mdui-tab-panel slot="panel" value="RequestJoin" style={{
<mdui-tab-panel slot="panel" value="RequestJoin" style={{ display: tabItemSelected == "RequestJoin" ? "flex" : "none",
display: tabItemSelected == "RequestJoin" ? "flex" : "none", flexDirection: "column",
flexDirection: "column", height: "100%",
height: "100%", justifyContent: 'center',
justifyContent: 'center',
alignItems: 'center',
}}>
<div>
{/* 非群成员 */}
</div>
</mdui-tab-panel>
<mdui-tab-panel slot="panel" value="Chat" ref={chatPanelRef} style={{
display: tabItemSelected == "Chat" ? "flex" : "none",
flexDirection: "column",
height: "100%",
}} onScroll={async (e: WheelEvent) => {
const scrollTop = (e.target as HTMLDivElement).scrollTop
if (scrollTop == 0) {
// 加载更多
}
}}>
<div style={{
display: 'flex',
justifyContent: "center",
paddingTop: "15px",
}}>
{/* 这里显示一些提示 */}
</div>
<ChatMessageContainer chatInfo={chatInfo} />
{
// 输入框
}
<div style={{
display: 'flex',
alignItems: 'center', alignItems: 'center',
paddingBottom: '2px',
paddingTop: '0.1rem',
position: 'sticky',
bottom: '0',
paddingLeft: '5px',
paddingRight: '4px',
backgroundColor: 'rgb(var(--mdui-color-surface))',
}} onDrop={(e) => {
// 文件拽入
}}> }}>
<mdui-text-field variant="outlined" placeholder="(。・ω・。)" autosize ref={inputRef} max-rows={6} onChange={() => { <div>
if (inputRef.current?.value.trim() == '') { {/* 非群成员 */}
// 清空缓存的文件 </div>
} </mdui-tab-panel>
}} onKeyDown={(event: KeyboardEvent) => { <mdui-tab-panel slot="panel" value="Chat" ref={chatPanelRef} style={{
if (event.ctrlKey && event.key == 'Enter') { display: tabItemSelected == "Chat" ? "flex" : "none",
// 发送消息 flexDirection: "column",
} height: "100%",
}} onPaste={(event: ClipboardEvent) => { }} onScroll={async (e: WheelEvent) => {
for (const item of event.clipboardData?.items || []) { const scrollTop = (e.target as HTMLDivElement).scrollTop
if (item.kind == 'file') { if (scrollTop == 0) {
event.preventDefault() // 加载更多
const file = item.getAsFile() as File }
// 添加文件 }}>
<div style={{
display: 'flex',
justifyContent: "center",
paddingTop: "15px",
}}>
{/* 这里显示一些提示 */}
</div>
<ChatMessageContainer chatInfo={chatInfo} />
{
// 输入框
}
<div style={{
display: 'flex',
alignItems: 'center',
paddingBottom: '2px',
paddingTop: '0.1rem',
position: 'sticky',
bottom: '0',
paddingLeft: '5px',
paddingRight: '4px',
backgroundColor: 'rgb(var(--mdui-color-surface))',
}} onDrop={(e) => {
// 文件拽入
}}>
<mdui-text-field variant="outlined" placeholder="(。・ω・。)" autosize ref={inputRef} max-rows={6} onChange={() => {
if (inputRef.current?.value.trim() == '') {
// 清空缓存的文件
} }
} }} onKeyDown={(event: KeyboardEvent) => {
}} style={{ if (event.ctrlKey && event.key == 'Enter') {
marginRight: '10px', // 发送消息
marginTop: '3px', }
marginBottom: '3px', }} onPaste={(event: ClipboardEvent) => {
}}></mdui-text-field> for (const item of event.clipboardData?.items || []) {
<mdui-button-icon slot="end-icon" icon="attach_file" style={{ if (item.kind == 'file') {
marginRight: '6px', event.preventDefault()
}} onClick={() => { const file = item.getAsFile() as File
// 添加文件 // 添加文件
}}></mdui-button-icon> }
<mdui-button-icon icon="send" style={{ }
marginRight: '7px', }} style={{
}} onClick={() => { marginRight: '10px',
// 发送消息 marginTop: '3px',
}}></mdui-button-icon> marginBottom: '3px',
}}></mdui-text-field>
<mdui-button-icon slot="end-icon" icon="attach_file" style={{
marginRight: '6px',
}} onClick={() => {
// 添加文件
}}></mdui-button-icon>
<mdui-button-icon icon="send" style={{
marginRight: '7px',
}} onClick={() => {
// 发送消息
}}></mdui-button-icon>
<div style={{
display: 'none'
}}>
<input accept="*/*" type="file" name="添加文件" multiple ></input>
</div>
</div>
</mdui-tab-panel>
{
chatInfo.getType() == 'group' && <mdui-tab-panel slot="panel" value="GroupMembers" style={{
display: tabItemSelected == "GroupMembers" ? "flex" : "none",
flexDirection: "column",
height: "100%",
}}>
{/* <GroupMembersList chat={chatInfo} /> */}
</mdui-tab-panel>
}
{
chatInfo.getType() == 'group' && <mdui-tab-panel slot="panel" value="NewMemberRequests" style={{
display: tabItemSelected == "NewMemberRequests" ? "flex" : "none",
flexDirection: "column",
height: "100%",
}}>
{/* {chatInfo.isAdmin() && <JoinRequestsList chat={chatInfo} />} */}
</mdui-tab-panel>
}
<mdui-tab-panel slot="panel" value="Settings" style={{
display: tabItemSelected == "Settings" ? "flex" : "none",
flexDirection: "column",
height: "100%",
}}>
<div style={{ <div style={{
display: 'none' display: 'none'
}}> }}>
<input accept="*/*" type="file" name="添加文件" multiple ></input> <input accept="image/*" type="file" name="上传对话头像"></input>
</div> </div>
</div> {
</mdui-tab-panel> /* chatInfo.getType() == 'group' && <PreferenceLayout>
{ <PreferenceUpdater.Provider value={groupPreferenceStore.createUpdater()}>
chatInfo.getType() == 'group' && <mdui-tab-panel slot="panel" value="GroupMembers" style={{ <PreferenceHeader
display: tabItemSelected == "GroupMembers" ? "flex" : "none", title="群组资料" />
flexDirection: "column", <Preference
height: "100%", title="上传新的头像"
}}> icon="image"
{/* <GroupMembersList chat={chatInfo} /> */} disabled={!chatInfo.isAdmin()}
</mdui-tab-panel> onClick={() => {
} uploadChatAvatarRef.current!.click()
{ }} />
chatInfo.getType() == 'group' && <mdui-tab-panel slot="panel" value="NewMemberRequests" style={{ <TextFieldPreference
display: tabItemSelected == "NewMemberRequests" ? "flex" : "none", title="设置群名称"
flexDirection: "column", icon="edit"
height: "100%", id="group_title"
}}> state={groupPreferenceStore.state.group_title || ''}
{/* {chatInfo.isAdmin() && <JoinRequestsList chat={chatInfo} />} */} disabled={!chatInfo.isAdmin()} />
</mdui-tab-panel> <TextFieldPreference
} title="设置群别名"
<mdui-tab-panel slot="panel" value="Settings" style={{ icon="edit"
display: tabItemSelected == "Settings" ? "flex" : "none", id="group_name"
flexDirection: "column", description="以便于添加, 可留空"
height: "100%", state={groupPreferenceStore.state.group_name || ''}
}}> disabled={!chatInfo.isAdmin()} />
<div style={{ <PreferenceHeader
display: 'none' title="入群设定" />
}}> <SwitchPreference
<input accept="image/*" type="file" name="上传对话头像"></input> title="允许入群"
</div> icon="person_add"
{ id="allow_new_member_join"
/* chatInfo.getType() == 'group' && <PreferenceLayout> disabled={!chatInfo.isAdmin()}
<PreferenceUpdater.Provider value={groupPreferenceStore.createUpdater()}> state={groupPreferenceStore.state.allow_new_member_join || false} />
<PreferenceHeader <SwitchPreference
title="群组资料" /> title="允许成员邀请"
<Preference description="目前压根没有这项功能, 甚至还不能查看成员列表, 以后再说吧"
title="上传新的头像" id="allow_new_member_from_invitation"
icon="image"
disabled={!chatInfo.isAdmin()}
onClick={() => {
uploadChatAvatarRef.current!.click()
}} />
<TextFieldPreference
title="设置群名称"
icon="edit"
id="group_title"
state={groupPreferenceStore.state.group_title || ''}
disabled={!chatInfo.isAdmin()} />
<TextFieldPreference
title="设置群别名"
icon="edit"
id="group_name"
description="以便于添加, 可留空"
state={groupPreferenceStore.state.group_name || ''}
disabled={!chatInfo.isAdmin()} />
<PreferenceHeader
title="入群设定" />
<SwitchPreference
title="允许入群"
icon="person_add"
id="allow_new_member_join"
disabled={!chatInfo.isAdmin()}
state={groupPreferenceStore.state.allow_new_member_join || false} />
<SwitchPreference
title="允许成员邀请"
description="目前压根没有这项功能, 甚至还不能查看成员列表, 以后再说吧"
id="allow_new_member_from_invitation"
icon="_"
disabled={true || !chatInfo.isAdmin()}
state={groupPreferenceStore.state.allow_new_member_from_invitation || false} />
<SelectPreference
title="入群验证方式"
icon="_"
id="new_member_join_method"
selections={{
disabled: "无需验证",
allowed_by_admin: "只需要管理员批准 (WIP)",
answered_and_allowed_by_admin: "需要回答问题并获得管理员批准 (WIP)",
}}
disabled={!chatInfo.isAdmin() || !groupPreferenceStore.state.allow_new_member_join}
state={groupPreferenceStore.state.new_member_join_method || 'disabled'} />
{
groupPreferenceStore.state.new_member_join_method == 'answered_and_allowed_by_admin'
&& <TextFieldPreference
title="设置问题"
icon="_" icon="_"
id="answered_and_allowed_by_admin_question" disabled={true || !chatInfo.isAdmin()}
description="WIP" state={groupPreferenceStore.state.allow_new_member_from_invitation || false} />
state={groupPreferenceStore.state.answered_and_allowed_by_admin_question || ''} <SelectPreference
disabled={true || !chatInfo.isAdmin()} /> title="入群验证方式"
} icon="_"
</PreferenceUpdater.Provider> id="new_member_join_method"
</PreferenceLayout> */ selections={{
} disabled: "无需验证",
{ allowed_by_admin: "只需要管理员批准 (WIP)",
chatInfo.getType() == 'private' && ( answered_and_allowed_by_admin: "需要回答问题并获得管理员批准 (WIP)",
<div> }}
disabled={!chatInfo.isAdmin() || !groupPreferenceStore.state.allow_new_member_join}
</div> state={groupPreferenceStore.state.new_member_join_method || 'disabled'} />
) {
} groupPreferenceStore.state.new_member_join_method == 'answered_and_allowed_by_admin'
</mdui-tab-panel> && <TextFieldPreference
</div > title="设置问题"
icon="_"
id="answered_and_allowed_by_admin_question"
description="WIP"
state={groupPreferenceStore.state.answered_and_allowed_by_admin_question || ''}
disabled={true || !chatInfo.isAdmin()} />
}
</PreferenceUpdater.Provider>
</PreferenceLayout> */
}
{
chatInfo.getType() == 'private' && (
<div>
</div>
)
}
</mdui-tab-panel>
</div>
)
} }

View File

@@ -58,6 +58,7 @@ export default function AllChatsList({ ...props }: React.HTMLAttributes<HTMLElem
...props?.style, ...props?.style,
}} {...props}> }} {...props}>
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{ <mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
padding: isMobileUI() ? '12px' : undefined,
paddingTop: '4px', paddingTop: '4px',
paddingBottom: '13px', paddingBottom: '13px',
position: 'sticky', position: 'sticky',

View File

@@ -68,6 +68,7 @@ export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HT
zIndex: '10', zIndex: '10',
}}> }}>
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{ <mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
padding: isMobileUI() ? '12px' : undefined,
paddingTop: '4px', paddingTop: '4px',
}}></mdui-text-field> }}></mdui-text-field>
<mdui-list-item rounded style={{ <mdui-list-item rounded style={{

View File

@@ -58,6 +58,7 @@ export default function RecentChatsList({ ...props }: React.HTMLAttributes<HTMLE
...props?.style, ...props?.style,
}} {...props}> }} {...props}>
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{ <mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
padding: isMobileUI() ? '12px' : undefined,
paddingTop: '4px', paddingTop: '4px',
marginBottom: '13px', marginBottom: '13px',
position: 'sticky', position: 'sticky',

View File

@@ -1,7 +1,10 @@
import { useSearchParams } from "react-router" import { useSearchParams, Outlet } from "react-router"
import useRouterDialogRef from "./useRouterDialogRef" import useRouterDialogRef from "./useRouterDialogRef"
import * as React from 'react' import * as React from 'react'
import LazyChatFragment from "../chat-fragment/LazyChatFragment" import LazyChatFragment from "../chat-fragment/LazyChatFragment"
import useEventListener from "../../utils/useEventListener"
import useAsyncEffect from "../../utils/useAsyncEffect"
import sleep from "../../utils/sleep"
export default function ChatFragmentDialog() { export default function ChatFragmentDialog() {
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
@@ -9,7 +12,7 @@ export default function ChatFragmentDialog() {
const dialogRef = useRouterDialogRef() const dialogRef = useRouterDialogRef()
React.useEffect(() => { useEventListener(dialogRef, 'open', () => {
const shadow = dialogRef.current!.shadowRoot as ShadowRoot const shadow = dialogRef.current!.shadowRoot as ShadowRoot
const panel = shadow.querySelector(".panel") as HTMLElement const panel = shadow.querySelector(".panel") as HTMLElement
panel.style.padding = '0' panel.style.padding = '0'
@@ -21,7 +24,15 @@ export default function ChatFragmentDialog() {
body.style.display = 'flex' body.style.display = 'flex'
}, []) }, [])
return <mdui-dialog fullscreen ref={dialogRef}> return (<>
<LazyChatFragment chatId={id!} openedWithRouter={true} /> <mdui-dialog fullscreen ref={dialogRef}>
</mdui-dialog> <div style={{
display: 'flex',
width: '100%',
}}>
<LazyChatFragment chatId={id!} openedWithRouter={true} />
</div>
</mdui-dialog>
<Outlet />
</>)
} }

View File

@@ -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,
}
}

View File

@@ -0,0 +1,5 @@
import * as React from 'react'
const RouterDialogsContext = React.createContext(() => {})
export default RouterDialogsContext

View File

@@ -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<HTMLElement>) {
const proceedRef = React.useRef<() => void>()
const nav = useNavigate()
// 进入子路由不会拦截上一个路由对话框的关闭
// 没有路由对话框不会拦截
const blocker = useBlocker(React.useCallback<BlockerFunction>(({ 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<Dialog>) {
routerDialogsList.push(ref)
// 正常情况下不可能同时关掉两个对话框
// 不过要是真有的话, 再说吧
ref.current!.addEventListener('closed', async () => {
routerDialogsList.splice(routerDialogsList.length - 1, 1)
await sleep(10)
proceedRef.current ? proceedRef.current() : nav(-1)
})
}
return <RouterDialogsContext.Provider value={registerRouterDialog}>
{ children }
</RouterDialogsContext.Provider>
}

View File

@@ -3,31 +3,17 @@ import useAsyncEffect from "../../utils/useAsyncEffect"
import sleep from "../../utils/sleep" import sleep from "../../utils/sleep"
import { BlockerFunction, useBlocker, useNavigate } from "react-router" import { BlockerFunction, useBlocker, useNavigate } from "react-router"
import * as React from 'react' import * as React from 'react'
import RouterDialogsContext from './RouterDialogsContext'
export default function useRouterDialogRef() { export default function useRouterDialogRef() {
const dialogRef = React.useRef<Dialog>() const dialogRef = React.useRef<Dialog>(RouterDialogsContext)
const proceedRef = React.useRef<() => void>() const registerRouterDialog = React.useContext(RouterDialogsContext)
const shouldBlock = React.useRef(true)
const nav = useNavigate()
const blocker = useBlocker(React.useCallback<BlockerFunction>(() => shouldBlock.current, []))
// 避免用户手动返回导致动画丢失
React.useEffect(() => {
if (blocker.state === "blocked") {
proceedRef.current = blocker.proceed
// 这个让姐姐来就好啦
dialogRef.current!.open = false
}
}, [blocker.state])
useAsyncEffect(async () => { useAsyncEffect(async () => {
registerRouterDialog(dialogRef)
await sleep(10) await sleep(10)
dialogRef.current!.open = true dialogRef.current!.open = true
dialogRef.current!.addEventListener('closed', async () => {
shouldBlock.current = false
await sleep(10)
proceedRef.current ? proceedRef.current() : nav(-1)
})
}, []) }, [])
return dialogRef return dialogRef
} }