Compare commits
9 Commits
8fbf84d5dc
...
19ed8c0357
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19ed8c0357 | ||
|
|
a66d137773 | ||
|
|
484a5efb99 | ||
|
|
75dfced90f | ||
|
|
668e84e102 | ||
|
|
505e629f30 | ||
|
|
895ea6e4e1 | ||
|
|
856aeb868a | ||
|
|
ae0e7fee95 |
@@ -186,7 +186,7 @@ export default class LingChairClient {
|
||||
throw new CallbackError(re)
|
||||
}
|
||||
getBaseHttpUrl() {
|
||||
const url = new URL(this.server_url)
|
||||
const url = new URL(this.client.io.opts.host || (this.server_url == '' ? `${window.location.protocol}//${window.location.host}` : this.server_url))
|
||||
return (({
|
||||
'ws:': 'http:',
|
||||
'wss:': 'https:',
|
||||
|
||||
@@ -37,5 +37,5 @@ const onResize = () => {
|
||||
window.addEventListener('resize', onResize)
|
||||
onResize()
|
||||
|
||||
const config = await fetch('config.json').then((re) => re.json())
|
||||
const config = await fetch('/config.json').then((re) => re.json())
|
||||
config.title && (document.title = config.title)
|
||||
|
||||
@@ -2,18 +2,23 @@ import { Dialog } from 'mdui'
|
||||
import 'pinch-zoom-element'
|
||||
import React from "react"
|
||||
|
||||
export default function ImageViewer() {
|
||||
export default function ImageViewer({ ...props }: React.ImgHTMLAttributes<HTMLImageElement>) {
|
||||
const dialogRef = React.useRef<Dialog>()
|
||||
|
||||
|
||||
return <mdui-dialog ref={dialogRef} fullscreen="fullscreen">
|
||||
<mdui-button-icon icon="open_in_new"
|
||||
onclick="window.open(document.querySelector('#image-viewer-dialog-inner > *').src, '_blank')">
|
||||
<mdui-button-icon icon="open_in_new" onClick={() => window.open(props.src, '_blank')}>
|
||||
</mdui-button-icon>
|
||||
<mdui-button-icon icon="close" onClick={() => dialogRef.current!.open = false}>
|
||||
</mdui-button-icon>
|
||||
{
|
||||
// @ts-ignore 注册了这个元素
|
||||
<pinch-zoom id="image-viewer-dialog-inner" style="width: var(--whitesilk-window-width); height: var(--whitesilk-window-height);"></pinch-zoom>
|
||||
<pinch-zoom id="image-viewer-dialog-inner" style={{
|
||||
width: 'var(--whitesilk-window-width)',
|
||||
height: 'var(--whitesilk-window-height)',
|
||||
}}>
|
||||
<img {...props}></img>
|
||||
{/* @ts-ignore 注册了这个元素 */}
|
||||
</pinch-zoom>
|
||||
}
|
||||
</mdui-dialog>
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ 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, LoaderFunction, Outlet, Route, RouterProvider, Routes } from "react-router"
|
||||
import { BrowserRouter, createBrowserRouter, Link, LoaderFunction, Outlet, Route, RouterProvider, Routes, useNavigate } from "react-router"
|
||||
import LoginDialog from "./main-page/LoginDialog.tsx"
|
||||
import useAsyncEffect from "../utils/useAsyncEffect.ts"
|
||||
import performAuth from "../performAuth.ts"
|
||||
@@ -23,6 +23,8 @@ import UserOrChatInfoDialogLoader from "./routers/UserOrChatInfoDialogDataLoader
|
||||
import ChatFragmentDialog from "./routers/ChatFragmentDialog.tsx"
|
||||
import EffectOnly from "./EffectOnly.tsx"
|
||||
import MainSharedReducer from "./MainSharedReducer.ts"
|
||||
import gotoUserInfo from "./routers/gotoUserInfo.ts"
|
||||
import EditMyProfileDialog from "./routers/EditMyProfileDialog.tsx"
|
||||
|
||||
function Root() {
|
||||
const [myProfileCache, setMyProfileCache] = React.useState<UserMySelf>()
|
||||
@@ -52,6 +54,8 @@ function Root() {
|
||||
const [showRegisterDialog, setShowRegisterDialog] = React.useState(false)
|
||||
const [showAddFavourtieChatDialog, setShowAddFavourtieChatDialog] = React.useState(false)
|
||||
|
||||
const nav = useNavigate()
|
||||
|
||||
const [state, dispatch] = React.useReducer(MainSharedReducer, {
|
||||
favouriteChats: [],
|
||||
currentSelectedChatId: '',
|
||||
@@ -116,7 +120,7 @@ function Root() {
|
||||
<mdui-list style={{
|
||||
padding: '10px',
|
||||
}}>
|
||||
<mdui-list-item rounded>
|
||||
<mdui-list-item rounded onClick={() => gotoUserInfo(nav, myProfileCache!.getId())}>
|
||||
<span>{myProfileCache?.getNickName()}</span>
|
||||
<AvatarMySelf slot="icon" />
|
||||
</mdui-list-item>
|
||||
@@ -126,8 +130,6 @@ function Root() {
|
||||
}}></mdui-divider>
|
||||
<mdui-list-item rounded icon="person_add">添加收藏对话</mdui-list-item>
|
||||
<mdui-list-item rounded icon="group_add">创建新的群组</mdui-list-item>
|
||||
<Link to="/info/user?id=0960bd15-4527-4000-97a8-73110160296f"><mdui-list-item rounded icon="group_add">我是测试</mdui-list-item></Link>
|
||||
<Link to="/info/chat?id=priv_0960bd15_4527_4000_97a8_73110160296f__0960bd15_4527_4000_97a8_73110160296f"><mdui-list-item rounded icon="group_add">我是测试2</mdui-list-item></Link>
|
||||
</mdui-list>
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
@@ -232,6 +234,15 @@ export default function Main() {
|
||||
Component: UserOrChatInfoDialog,
|
||||
loader: UserOrChatInfoDialogLoader,
|
||||
},
|
||||
{
|
||||
path: 'settings',
|
||||
children: [
|
||||
{
|
||||
path: 'edit_profile',
|
||||
Component: EditMyProfileDialog,
|
||||
}
|
||||
],
|
||||
},
|
||||
/* {
|
||||
path: 'chat',
|
||||
Component: ChatFragmentDialog,
|
||||
|
||||
104
client/ui/routers/EditMyProfileDialog.tsx
Normal file
104
client/ui/routers/EditMyProfileDialog.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import { CallbackError, UserMySelf } from "lingchair-client-protocol"
|
||||
import ClientCache from "../../ClientCache"
|
||||
import AvatarMySelf from "../AvatarMySelf"
|
||||
import useRouterDialogRef from "./useRouterDialogRef"
|
||||
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 { TextField } from "mdui"
|
||||
import * as React from 'react'
|
||||
|
||||
export default function EditMyProfileDialog() {
|
||||
const dialogRef = useRouterDialogRef()
|
||||
const nav = useNavigate()
|
||||
|
||||
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={dialogRef}>
|
||||
<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={() => nav(-1)}>取消</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>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { dialog } from "mdui"
|
||||
import useRouterDialogRef from "./useRouterDialogRef"
|
||||
import { useLoaderData } from "react-router"
|
||||
import { useLoaderData, useNavigate } from "react-router"
|
||||
import { CallbackError } from "lingchair-client-protocol"
|
||||
import showSnackbar from "../../utils/showSnackbar"
|
||||
import Avatar from "../Avatar"
|
||||
@@ -10,14 +10,19 @@ import * as React from 'react'
|
||||
import UserOrChatInfoDialogLoader from "./UserOrChatInfoDialogDataLoader"
|
||||
import MainSharedReducer from "../MainSharedReducer"
|
||||
import ClientCache from "../../ClientCache"
|
||||
import getClient from "../../getClient"
|
||||
|
||||
export default function UserOrChatInfoDialog() {
|
||||
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
||||
state: context.state,
|
||||
}))
|
||||
|
||||
const nav = useNavigate()
|
||||
|
||||
const dialogRef = useRouterDialogRef()
|
||||
const { chat, id } = useLoaderData<typeof UserOrChatInfoDialogLoader>()
|
||||
const { chat, id, mySelf } = useLoaderData<typeof UserOrChatInfoDialogLoader>()
|
||||
|
||||
const isMySelf = mySelf?.getId() == id
|
||||
|
||||
const favourited = React.useMemo(() => shared.state.favouriteChats.map((v) => v.getId()).indexOf(chat.getId() || '') != -1, [chat, shared.state.favouriteChats])
|
||||
|
||||
@@ -27,7 +32,7 @@ export default function UserOrChatInfoDialog() {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<Avatar src={chat.getAvatarFileHash()} text={chat.getTitle()} style={{
|
||||
<Avatar src={getClient().getUrlForFileByHash(chat.getAvatarFileHash())} text={chat.getTitle()} style={{
|
||||
width: '50px',
|
||||
height: '50px',
|
||||
}} />
|
||||
@@ -53,37 +58,44 @@ export default function UserOrChatInfoDialog() {
|
||||
marginTop: "10px",
|
||||
}}></mdui-divider>
|
||||
<mdui-list>
|
||||
<mdui-list-item icon={favourited ? "favorite_border" : "favorite"} rounded onClick={() => dialog({
|
||||
headline: favourited ? "取消收藏对话" : "收藏对话",
|
||||
description: favourited ? "确定从收藏对话列表中移除吗? (虽然这不会导致聊天记录丢失)" : "确定要添加到收藏对话列表吗?",
|
||||
actions: [
|
||||
{
|
||||
text: "取消",
|
||||
onClick: () => {
|
||||
return true
|
||||
{
|
||||
isMySelf && <mdui-list-item icon="edit" rounded onClick={() => nav('/settings/edit_profile')}>
|
||||
编辑资料
|
||||
</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>
|
||||
{
|
||||
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={() => {
|
||||
|
||||
}}>打开对话</mdui-list-item>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { LoaderFunctionArgs } from "react-router"
|
||||
import getClient from "../../getClient"
|
||||
import { Chat } from "lingchair-client-protocol"
|
||||
import ClientCache from "../../ClientCache"
|
||||
|
||||
export default async function UserOrChatInfoDialogLoader({ params, request }: LoaderFunctionArgs) {
|
||||
const searchParams = new URL(request.url).searchParams
|
||||
@@ -16,6 +17,7 @@ export default async function UserOrChatInfoDialogLoader({ params, request }: Lo
|
||||
id = await chat.getTheOtherUserIdOrThrow()
|
||||
|
||||
return {
|
||||
mySelf: await ClientCache.getMySelf(),
|
||||
chat,
|
||||
id,
|
||||
}
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import { Dialog } from "mdui"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
||||
import sleep from "../../utils/sleep"
|
||||
import { useBlocker, useNavigate } from "react-router"
|
||||
import { BlockerFunction, useBlocker, useNavigate } from "react-router"
|
||||
import * as React from 'react'
|
||||
|
||||
export default function useRouterDialogRef() {
|
||||
const dialogRef = React.useRef<Dialog>()
|
||||
const proceedRef = React.useRef<() => void>()
|
||||
const shouldBlock = React.useRef(true)
|
||||
const nav = useNavigate()
|
||||
const blocker = useBlocker(({ currentLocation, nextLocation }) => shouldBlock.current && currentLocation.pathname !== nextLocation.pathname)
|
||||
const blocker = useBlocker(React.useCallback<BlockerFunction>(() => shouldBlock.current, []))
|
||||
|
||||
// 避免用户手动返回导致动画丢失
|
||||
React.useEffect(() => {
|
||||
if (blocker.state === "blocked") {
|
||||
proceedRef.current = blocker.proceed
|
||||
// 这个让姐姐来就好啦
|
||||
dialogRef.current!.open = false
|
||||
}
|
||||
}, [blocker])
|
||||
}, [blocker.state])
|
||||
|
||||
useAsyncEffect(async () => {
|
||||
await sleep(10)
|
||||
dialogRef.current!.open = true
|
||||
dialogRef.current!.addEventListener('closed', async () => {
|
||||
await sleep(10)
|
||||
// 无论如何, 让姐姐先解除返回限制, 这样才能出去嘛
|
||||
shouldBlock.current = false
|
||||
nav(-1)
|
||||
await sleep(10)
|
||||
proceedRef.current ? proceedRef.current() : nav(-1)
|
||||
})
|
||||
}, [])
|
||||
return dialogRef
|
||||
|
||||
@@ -22,5 +22,10 @@
|
||||
"file-type": "21.0.0",
|
||||
"lingchair-internal-shared": "*",
|
||||
"socket.io": "4.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/express-fileupload": "^1.5.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,22 @@ import fs from 'node:fs/promises'
|
||||
// @ts-types="npm:@types/express-fileupload"
|
||||
import fileUpload from 'express-fileupload'
|
||||
import FileUploadMiddleware from "./fileupload-middleware.ts"
|
||||
import chalk from "chalk"
|
||||
|
||||
export default async function createLingChairServer() {
|
||||
const app = express()
|
||||
|
||||
app.use((req, res, next) => {
|
||||
const start = Date.now()
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start
|
||||
console.log(`${chalk.grey('[求]')} ${req.socket.remoteAddress} <- ${req.originalUrl} [${res.statusCode}] (with ${req.method}, ${duration}ms)`)
|
||||
})
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
app.use('/', express.static(config.data_path + '/page_compiled'))
|
||||
app.use(cookieParser())
|
||||
app.get('/config.json', (req, res) => {
|
||||
@@ -41,15 +54,15 @@ export default async function createLingChairServer() {
|
||||
) {
|
||||
return next()
|
||||
}
|
||||
|
||||
|
||||
res.sendFile(path.resolve(config.data_path + '/page_compiled/index.html'))
|
||||
})
|
||||
|
||||
await fs.mkdir(config.data_path + '/upload_cache', { recursive: true })
|
||||
try {
|
||||
await fs.rmdir(config.data_path + '/upload_cache')
|
||||
// deno-lint-ignore no-empty
|
||||
} catch (_) {}
|
||||
// deno-lint-ignore no-empty
|
||||
} catch (_) { }
|
||||
app.use(fileUpload({
|
||||
limits: { fileSize: 2 * 1024 * 1024 * 1024 },
|
||||
useTempFiles: true,
|
||||
|
||||
Reference in New Issue
Block a user