Compare commits
16 Commits
00371b1dda
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20986af1ba | ||
|
|
34d46a85f1 | ||
|
|
f8f66f0e33 | ||
|
|
58f0427350 | ||
|
|
e3db26323b | ||
|
|
4788434445 | ||
|
|
07bc4a6654 | ||
|
|
bd49edb586 | ||
|
|
f4a9cc9cda | ||
|
|
8817663371 | ||
|
|
19b8b92f49 | ||
|
|
f584b49cd4 | ||
|
|
13eefdd50c | ||
|
|
3cd9031eef | ||
|
|
94c901a233 | ||
|
|
1819c31267 |
@@ -19,6 +19,11 @@ export default class Chat extends BaseClientObject {
|
|||||||
* 实例化方法
|
* 实例化方法
|
||||||
* ================================================
|
* ================================================
|
||||||
*/
|
*/
|
||||||
|
static getForInvokeOnlyById(client: LingChairClient, id: string) {
|
||||||
|
return new Chat(client, {
|
||||||
|
id
|
||||||
|
} as ChatBean)
|
||||||
|
}
|
||||||
static async getById(client: LingChairClient, id: string) {
|
static async getById(client: LingChairClient, id: string) {
|
||||||
try {
|
try {
|
||||||
return await this.getByIdOrThrow(client, id)
|
return await this.getByIdOrThrow(client, id)
|
||||||
|
|||||||
@@ -131,33 +131,37 @@ export default class LingChairClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 客户端上线
|
* 进行身份验证以接受客户端事件
|
||||||
*
|
*
|
||||||
* 使用验证方式优先级: 访问 > 刷新 > 账号密码
|
* 使用验证方式优先级: 访问 > 刷新 > 账号密码
|
||||||
*
|
*
|
||||||
* 不会逐一尝试
|
* 若传递了账号密码, 则同时缓存新的访问令牌和刷新令牌
|
||||||
|
*
|
||||||
|
* 如只传递两个令牌的其一, 按照优先级并在成功验证后赋值
|
||||||
|
*
|
||||||
|
* 多个验证方式不会逐一尝试
|
||||||
*/
|
*/
|
||||||
async authOrThrow(args: {
|
async authOrThrow(args: {
|
||||||
refresh_token?: string,
|
refresh_token?: string
|
||||||
access_token?: string,
|
access_token?: string
|
||||||
account?: string,
|
account?: string
|
||||||
password?: string,
|
password?: string
|
||||||
|
ignore_all_empty?: boolean
|
||||||
}) {
|
}) {
|
||||||
if ((!args.access_token && !args.refresh_token) && (!args.account && !args.password))
|
if ((!args.access_token && !args.refresh_token) && (!args.account && !args.password) && !args.ignore_all_empty)
|
||||||
throw new Error('Access/Refresh token or account & password required')
|
throw new Error('Access/Refresh token or account & password required, or ignore_all_empty=true')
|
||||||
|
|
||||||
this.auth_cache = args
|
this.auth_cache = args
|
||||||
|
|
||||||
this.refresh_token = args.refresh_token
|
|
||||||
|
|
||||||
let access_token = args.access_token
|
let access_token = args.access_token
|
||||||
if (!access_token && args.refresh_token) {
|
if (!access_token && args.refresh_token) {
|
||||||
const re = await this.invoke('User.refreshAccessToken', {
|
const re = await this.invoke('User.refreshAccessToken', {
|
||||||
refresh_token: args.refresh_token,
|
refresh_token: args.refresh_token,
|
||||||
})
|
})
|
||||||
if (re.code == 200)
|
if (re.code == 200) {
|
||||||
access_token = re.data!.access_token as string | undefined
|
access_token = re.data!.access_token as string | undefined
|
||||||
else
|
this.refresh_token = args.refresh_token
|
||||||
|
} else
|
||||||
throw new CallbackError(re)
|
throw new CallbackError(re)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,9 +170,10 @@ export default class LingChairClient {
|
|||||||
account: args.account,
|
account: args.account,
|
||||||
password: crypto.createHash('sha256').update(args.password).digest('hex'),
|
password: crypto.createHash('sha256').update(args.password).digest('hex'),
|
||||||
})
|
})
|
||||||
if (re.code == 200)
|
if (re.code == 200) {
|
||||||
access_token = re.data!.access_token as string | undefined
|
access_token = re.data!.access_token as string | undefined
|
||||||
else
|
this.refresh_token = re.data!.refresh_token as string
|
||||||
|
} else
|
||||||
throw new CallbackError(re)
|
throw new CallbackError(re)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,15 @@ export default class User extends BaseClientObject {
|
|||||||
* 实例化方法
|
* 实例化方法
|
||||||
* ================================================
|
* ================================================
|
||||||
*/
|
*/
|
||||||
|
static getForInvokeOnlyById(client: LingChairClient, id: string) {
|
||||||
|
return new User(client, {
|
||||||
|
id
|
||||||
|
} as UserBean)
|
||||||
|
}
|
||||||
static async getById(client: LingChairClient, id: string) {
|
static async getById(client: LingChairClient, id: string) {
|
||||||
try {
|
try {
|
||||||
return await this.getByIdOrThrow(client, id)
|
return await this.getByIdOrThrow(client, id)
|
||||||
} catch(_) {
|
} catch (_) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export default class UserMySelf extends User {
|
|||||||
token: this.client.access_token
|
token: this.client.access_token
|
||||||
})
|
})
|
||||||
if (re.code == 200)
|
if (re.code == 200)
|
||||||
return re.data!.recent_chats as ChatBean[]
|
return (re.data!.favourite_chats || re.data!.contacts_list) as ChatBean[]
|
||||||
throw new CallbackError(re)
|
throw new CallbackError(re)
|
||||||
}
|
}
|
||||||
async getMyFavouriteChats() {
|
async getMyFavouriteChats() {
|
||||||
|
|||||||
23
client/ClientCache.ts
Normal file
23
client/ClientCache.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Chat, User } from "lingchair-client-protocol"
|
||||||
|
import getClient from "./getClient"
|
||||||
|
|
||||||
|
type CouldCached = User | Chat | null
|
||||||
|
export default class ClientCache {
|
||||||
|
static caches: { [key: string]: CouldCached } = {}
|
||||||
|
|
||||||
|
static async getUser(id: string) {
|
||||||
|
const k = 'user_' + id
|
||||||
|
if (this.caches[k] != null)
|
||||||
|
return this.caches[k] as User | null
|
||||||
|
this.caches[k] = await User.getById(getClient(), id)
|
||||||
|
return this.caches[k]
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getChat(id: string) {
|
||||||
|
const k = 'chat_' + id
|
||||||
|
if (this.caches[k] != null)
|
||||||
|
return this.caches[k] as Chat | null
|
||||||
|
this.caches[k] = await Chat.getById(getClient(), id)
|
||||||
|
return this.caches[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// @ts-types="npm:@types/crypto-js"
|
|
||||||
import * as CryptoJS from 'crypto-js'
|
|
||||||
|
|
||||||
const dataIsEmpty = !localStorage.tws_data || localStorage.tws_data == ''
|
|
||||||
|
|
||||||
const aes = {
|
|
||||||
enc: (data: string, key: string) => CryptoJS.AES.encrypt(data, key).toString(),
|
|
||||||
dec: (data: string, key: string) => CryptoJS.AES.decrypt(data, key).toString(CryptoJS.enc.Utf8),
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = location.host + '_TWS_姐姐'
|
|
||||||
|
|
||||||
if (dataIsEmpty) localStorage.tws_data = aes.enc('{}', key)
|
|
||||||
|
|
||||||
let _dec = aes.dec(localStorage.tws_data, key)
|
|
||||||
if (_dec == '') _dec = '{}'
|
|
||||||
|
|
||||||
const _data_cached = JSON.parse(_dec)
|
|
||||||
|
|
||||||
// 類型定義
|
|
||||||
declare global {
|
|
||||||
interface Window {
|
|
||||||
data: {
|
|
||||||
refresh_token?: string
|
|
||||||
split_sizes: number[]
|
|
||||||
apply(): void
|
|
||||||
access_token?: string
|
|
||||||
device_id: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore: 忽略...
|
|
||||||
// deno-lint-ignore no-window
|
|
||||||
(window.data == null) && (window.data = new Proxy({
|
|
||||||
apply() {}
|
|
||||||
}, {
|
|
||||||
get(_obj, k) {
|
|
||||||
if (k == '_cached') return _data_cached
|
|
||||||
if (k == 'apply') return () => localStorage.tws_data = aes.enc(JSON.stringify(_data_cached), key)
|
|
||||||
return _data_cached[k]
|
|
||||||
},
|
|
||||||
set(_obj, k, v) {
|
|
||||||
if (k == '_cached') return false
|
|
||||||
_data_cached[k] = v
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
// deno-lint-ignore no-window
|
|
||||||
export default window.data
|
|
||||||
71
client/data.ts
Normal file
71
client/data.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import crypto from 'node:crypto'
|
||||||
|
|
||||||
|
const dataIsEmpty = !localStorage.tws_data || localStorage.tws_data == ''
|
||||||
|
|
||||||
|
class Aes {
|
||||||
|
static randomIv() {
|
||||||
|
return crypto.randomBytes(12)
|
||||||
|
}
|
||||||
|
static normalizeKey(key: string, keyLength = 32) {
|
||||||
|
const hash = crypto.createHash('sha256')
|
||||||
|
hash.update(key)
|
||||||
|
const keyBuffer = hash.digest()
|
||||||
|
return keyLength ? keyBuffer.subarray(0, keyLength) : keyBuffer
|
||||||
|
}
|
||||||
|
static encrypt(data: string, key: string) {
|
||||||
|
const iv = this.randomIv()
|
||||||
|
return Buffer.concat([iv, crypto.createCipheriv("aes-256-gcm", this.normalizeKey(key), iv).update(data)]).toString('hex')
|
||||||
|
}
|
||||||
|
static decrypt(data: string, key: string) {
|
||||||
|
const buffer = Buffer.from(data, 'hex')
|
||||||
|
const iv = buffer.subarray(0, 12)
|
||||||
|
return crypto.createDecipheriv("aes-256-gcm", this.normalizeKey(key), iv).update(buffer.subarray(12)).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尽可能防止被窃取, 虽然理论上还是会被窃取
|
||||||
|
const key = crypto.createHash('sha256').update(location.host + '_TWS_姐姐_' + navigator.userAgent).digest().toString('base64')
|
||||||
|
|
||||||
|
if (dataIsEmpty) localStorage.tws_data = Aes.encrypt('{}', key)
|
||||||
|
|
||||||
|
let _data_cached
|
||||||
|
try {
|
||||||
|
_data_cached = JSON.parse(Aes.decrypt(localStorage.tws_data, key))
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("数据解密失败, 使用空数据...", e)
|
||||||
|
_data_cached = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
type IData = {
|
||||||
|
refresh_token?: string
|
||||||
|
split_sizes: number[]
|
||||||
|
apply(): void
|
||||||
|
access_token?: string
|
||||||
|
device_id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
data?: IData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new Proxy({} as IData, {
|
||||||
|
get(_obj, k) {
|
||||||
|
if (k == '_cached') return _data_cached
|
||||||
|
if (k == 'apply') return () => localStorage.tws_data = Aes.encrypt(JSON.stringify(_data_cached), key)
|
||||||
|
return _data_cached[k]
|
||||||
|
},
|
||||||
|
set(_obj, k, v) {
|
||||||
|
if (k == '_cached') return false
|
||||||
|
_data_cached[k] = v
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (new URL(location.href).searchParams.get('export_data') == 'true') {
|
||||||
|
window.data = data
|
||||||
|
console.warn("警告: 将 data 暴露到 window 有可能会导致令牌泄露!")
|
||||||
|
}
|
||||||
|
|
||||||
|
export default data
|
||||||
8
client/env.d.ts
vendored
Normal file
8
client/env.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/// <reference types="mdui/jsx.zh-cn.d.ts" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare const __APP_VERSION__: string
|
||||||
|
declare const __GIT_HASH__: string
|
||||||
|
declare const __GIT_HASH_FULL__: string
|
||||||
|
declare const __GIT_BRANCH__: string
|
||||||
|
declare const __BUILD_TIME__: string
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { LingChairClient } from 'lingchair-client-protocol'
|
import { LingChairClient } from 'lingchair-client-protocol'
|
||||||
import data from "./Data.ts"
|
import data from "./data.ts"
|
||||||
import { UAParser } from 'ua-parser-js'
|
import { UAParser } from 'ua-parser-js'
|
||||||
import { randomUUID } from 'lingchair-internal-shared'
|
import { randomUUID } from 'lingchair-internal-shared'
|
||||||
|
import performAuth from './performAuth.ts'
|
||||||
|
|
||||||
if (!data.device_id) {
|
if (!data.device_id) {
|
||||||
const ua = new UAParser(navigator.userAgent)
|
const ua = new UAParser(navigator.userAgent)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import 'mdui/mdui.css'
|
|||||||
import 'mdui'
|
import 'mdui'
|
||||||
import { breakpoint } from "mdui"
|
import { breakpoint } from "mdui"
|
||||||
|
|
||||||
import './mdui.d.ts'
|
import './env.d.ts'
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
@@ -16,6 +16,14 @@ import './ui/chat-elements/chat-text-container.ts'
|
|||||||
import './ui/chat-elements/chat-quote.ts'
|
import './ui/chat-elements/chat-quote.ts'
|
||||||
import Main from "./ui/Main.tsx"
|
import Main from "./ui/Main.tsx"
|
||||||
|
|
||||||
|
import performAuth from './performAuth.ts'
|
||||||
|
|
||||||
|
try {
|
||||||
|
await performAuth({})
|
||||||
|
} catch (e) {
|
||||||
|
console.log("验证失败", e)
|
||||||
|
}
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(React.createElement(Main))
|
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(React.createElement(Main))
|
||||||
|
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
|
|||||||
1
client/mdui.d.ts
vendored
1
client/mdui.d.ts
vendored
@@ -1 +0,0 @@
|
|||||||
/// <reference types="mdui/jsx.zh-cn.d.ts" />
|
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "lingchair-client",
|
"name": "lingchair-client",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"version": "0.1.0-alpha",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "npx vite build",
|
"build": "npx vite build",
|
||||||
"build-watch": "npx vite --watch build"
|
"build-watch": "npx vite --watch build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crypto-browserify": "3.12.1",
|
|
||||||
"crypto-js": "4.2.0",
|
|
||||||
"dompurify": "3.2.7",
|
"dompurify": "3.2.7",
|
||||||
"lingchair-internal-shared": "*",
|
"lingchair-internal-shared": "*",
|
||||||
"marked": "16.3.0",
|
"marked": "16.3.0",
|
||||||
@@ -18,7 +17,8 @@
|
|||||||
"react-router": "7.10.1",
|
"react-router": "7.10.1",
|
||||||
"socket.io-client": "4.8.1",
|
"socket.io-client": "4.8.1",
|
||||||
"split.js": "1.3.2",
|
"split.js": "1.3.2",
|
||||||
"ua-parser-js": "2.0.6"
|
"ua-parser-js": "2.0.6",
|
||||||
|
"use-context-selector": "2.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rollup/wasm-node": "4.48.0",
|
"@rollup/wasm-node": "4.48.0",
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
"@types/react-dom": "18.3.1",
|
"@types/react-dom": "18.3.1",
|
||||||
"@vitejs/plugin-react": "4.7.0",
|
"@vitejs/plugin-react": "4.7.0",
|
||||||
"chalk": "5.4.1",
|
"chalk": "5.4.1",
|
||||||
"vite": "7.0.6",
|
"vite": "7.2.6",
|
||||||
"vite-plugin-node-polyfills": "^0.24.0"
|
"vite-plugin-node-polyfills": "^0.24.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import data from "./Data.ts";
|
import data from "./data.ts"
|
||||||
import getClient from "./getClient.ts"
|
import getClient from "./getClient.ts"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 尝试进行验证
|
* 进行身份验证以接受客户端事件
|
||||||
*
|
*
|
||||||
* 成功后自动保存到本地
|
* 使用验证方式优先级: 访问 > 刷新 > 账号密码
|
||||||
*
|
*
|
||||||
* 优先级: 账号密码 > 提供刷新令牌 > 储存的刷新令牌
|
* 若传递了账号密码, 则同时缓存新的访问令牌和刷新令牌
|
||||||
*
|
*
|
||||||
* 不会逐一尝试
|
* 如只传递两个令牌的其一, 按照优先级并在成功验证后赋值
|
||||||
|
*
|
||||||
|
* 多个验证方式不会逐一尝试
|
||||||
*/
|
*/
|
||||||
export default async function performAuth(args: {
|
export default async function performAuth(args: {
|
||||||
refresh_token?: string
|
refresh_token?: string
|
||||||
@@ -21,7 +23,7 @@ export default async function performAuth(args: {
|
|||||||
password: args.password,
|
password: args.password,
|
||||||
})
|
})
|
||||||
else {
|
else {
|
||||||
await getClient().authOrThrow({ refresh_token: args.refresh_token ? args.refresh_token : data.refresh_token })
|
await getClient().authOrThrow({ refresh_token: args.refresh_token ? args.refresh_token : data.refresh_token, ignore_all_empty: true })
|
||||||
}
|
}
|
||||||
data.refresh_token = getClient().getCachedRefreshToken()
|
data.refresh_token = getClient().getCachedRefreshToken()
|
||||||
data.access_token = getClient().getCachedAccessToken()
|
data.access_token = getClient().getCachedAccessToken()
|
||||||
|
|||||||
@@ -67,3 +67,7 @@ html {
|
|||||||
.gutter.gutter-horizontal {
|
.gutter.gutter-horizontal {
|
||||||
cursor: col-resize;
|
cursor: col-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgb(var(--mdui-color-primary));
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export default function Avatar({
|
|||||||
avatarRef,
|
avatarRef,
|
||||||
...props
|
...props
|
||||||
}: Args) {
|
}: Args) {
|
||||||
if (src != null)
|
if (src != null && src != '')
|
||||||
return <mdui-avatar ref={avatarRef} {...props} src={src} />
|
return <mdui-avatar ref={avatarRef} {...props} src={src} />
|
||||||
else if (text != null)
|
else if (text != null && text != '')
|
||||||
return <mdui-avatar ref={avatarRef} {...props}>
|
return <mdui-avatar ref={avatarRef} {...props}>
|
||||||
{
|
{
|
||||||
text.substring(0, 1)
|
text.substring(0, 1)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import useAsyncEffect from "../utils/useAsyncEffect.ts"
|
|||||||
import Avatar from "./Avatar.tsx"
|
import Avatar from "./Avatar.tsx"
|
||||||
import getClient from "../getClient.ts"
|
import getClient from "../getClient.ts"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
|
import sleep from "../utils/sleep.ts"
|
||||||
|
|
||||||
interface Args extends React.HTMLAttributes<HTMLElement> {
|
interface Args extends React.HTMLAttributes<HTMLElement> {
|
||||||
avatarRef?: React.LegacyRef<HTMLElement>
|
avatarRef?: React.LegacyRef<HTMLElement>
|
||||||
@@ -21,6 +22,7 @@ export default function AvatarMySelf({
|
|||||||
})
|
})
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
|
await sleep(200)
|
||||||
const mySelf = await UserMySelf.getMySelfOrThrow(getClient())
|
const mySelf = await UserMySelf.getMySelfOrThrow(getClient())
|
||||||
setArgs({
|
setArgs({
|
||||||
text: mySelf.getNickName(),
|
text: mySelf.getNickName(),
|
||||||
@@ -28,5 +30,5 @@ export default function AvatarMySelf({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
return <Avatar avatarRef={avatarRef} {...props} {...args}></Avatar>
|
return <Avatar avatarRef={avatarRef} {...props} text={args.text} src={args.src}></Avatar>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,26 @@ import useEventListener from "../utils/useEventListener.ts"
|
|||||||
import AvatarMySelf from "./AvatarMySelf.tsx"
|
import AvatarMySelf from "./AvatarMySelf.tsx"
|
||||||
import MainSharedContext from './MainSharedContext.ts'
|
import MainSharedContext from './MainSharedContext.ts'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { BrowserRouter, Outlet, Route, Routes } from "react-router"
|
import { BrowserRouter, Link, Outlet, Route, Routes } from "react-router"
|
||||||
import LoginDialog from "./main-page/LoginDialog.tsx"
|
import LoginDialog from "./main-page/LoginDialog.tsx"
|
||||||
import useAsyncEffect from "../utils/useAsyncEffect.ts"
|
import useAsyncEffect from "../utils/useAsyncEffect.ts"
|
||||||
import performAuth from "../performAuth.ts"
|
import performAuth from "../performAuth.ts"
|
||||||
import { CallbackError } from "lingchair-client-protocol"
|
import { CallbackError, Chat, UserMySelf } from "lingchair-client-protocol"
|
||||||
import showCircleProgressDialog from "./showCircleProgressDialog.ts"
|
import showCircleProgressDialog from "./showCircleProgressDialog.ts"
|
||||||
import RegisterDialog from "./main-page/RegisterDialog.tsx"
|
import RegisterDialog from "./main-page/RegisterDialog.tsx"
|
||||||
import sleep from "../utils/sleep.ts"
|
import sleep from "../utils/sleep.ts"
|
||||||
|
import { $, NavigationDrawer } from "mdui"
|
||||||
|
import getClient from "../getClient.ts"
|
||||||
|
import showSnackbar from "../utils/showSnackbar.ts"
|
||||||
|
import AllChatsList from "./main-page/AllChatsList.tsx"
|
||||||
|
import FavouriteChatsList from "./main-page/FavouriteChatsList.tsx"
|
||||||
|
import AddFavourtieChatDialog from "./main-page/AddFavourtieChatDialog.tsx"
|
||||||
|
import RecentChatsList from "./main-page/RecentChatsList.tsx"
|
||||||
|
import ChatInfoDialog from "./routers/ChatInfoDialog.tsx"
|
||||||
|
|
||||||
export default function Main() {
|
export default function Main() {
|
||||||
|
const [myProfileCache, setMyProfileCache] = React.useState<UserMySelf>()
|
||||||
|
|
||||||
// 多页面切换
|
// 多页面切换
|
||||||
const navigationRef = React.useRef<HTMLElement>()
|
const navigationRef = React.useRef<HTMLElement>()
|
||||||
const [currentShowPage, setCurrentShowPage] = React.useState('Recents')
|
const [currentShowPage, setCurrentShowPage] = React.useState('Recents')
|
||||||
@@ -21,21 +31,60 @@ export default function Main() {
|
|||||||
setCurrentShowPage((event.target as HTMLElementWithValue).value)
|
setCurrentShowPage((event.target as HTMLElementWithValue).value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const drawerRef = React.useRef<NavigationDrawer>()
|
||||||
|
React.useEffect(() => {
|
||||||
|
$(drawerRef.current!.shadowRoot).append(`
|
||||||
|
<style>
|
||||||
|
.panel {
|
||||||
|
width: 17.5rem !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [showLoginDialog, setShowLoginDialog] = React.useState(false)
|
const [showLoginDialog, setShowLoginDialog] = React.useState(false)
|
||||||
const [showRegisterDialog, setShowRegisterDialog] = React.useState(false)
|
const [showRegisterDialog, setShowRegisterDialog] = React.useState(false)
|
||||||
|
const [showAddFavourtieChatDialog, setShowAddFavourtieChatDialog] = React.useState(false)
|
||||||
|
|
||||||
|
const [currentSelectedChatId, setCurrentSelectedChatId] = React.useState('')
|
||||||
|
|
||||||
|
const [favouriteChats, setFavouriteChats] = React.useState<Chat[]>([])
|
||||||
|
|
||||||
const sharedContext = {
|
const sharedContext = {
|
||||||
ui_functions: React.useRef({
|
functions_lazy: React.useRef({
|
||||||
|
updateFavouriteChats: () => { },
|
||||||
|
updateRecentChats: () => { },
|
||||||
|
updateAllChats: () => { },
|
||||||
}),
|
}),
|
||||||
|
favouriteChats,
|
||||||
|
setFavouriteChats,
|
||||||
|
|
||||||
setShowLoginDialog,
|
setShowLoginDialog,
|
||||||
setShowRegisterDialog,
|
setShowRegisterDialog,
|
||||||
|
setShowAddFavourtieChatDialog,
|
||||||
|
|
||||||
|
currentSelectedChatId,
|
||||||
|
setCurrentSelectedChatId,
|
||||||
|
|
||||||
|
myProfileCache,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
const waitingForAuth = showCircleProgressDialog("验证中...")
|
const waitingForAuth = showCircleProgressDialog("加载中...")
|
||||||
try {
|
try {
|
||||||
await performAuth({})
|
await performAuth({})
|
||||||
|
|
||||||
|
try {
|
||||||
|
setMyProfileCache(await UserMySelf.getMySelfOrThrow(getClient()))
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof CallbackError)
|
||||||
|
showSnackbar({
|
||||||
|
message: '获取资料失败: ' + e.message
|
||||||
|
})
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof CallbackError)
|
if (e instanceof CallbackError)
|
||||||
if (e.code == 401 || e.code == 400)
|
if (e.code == 401 || e.code == 400)
|
||||||
@@ -46,6 +95,13 @@ export default function Main() {
|
|||||||
waitingForAuth.open = false
|
waitingForAuth.open = false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const subRoutes = <>
|
||||||
|
<Route path="/info">
|
||||||
|
<Route path="chat" element={<ChatInfoDialog />} />
|
||||||
|
<Route path="user" element={<ChatInfoDialog />} />
|
||||||
|
</Route>
|
||||||
|
</>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MainSharedContext.Provider value={sharedContext}>
|
<MainSharedContext.Provider value={sharedContext}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
@@ -64,28 +120,47 @@ export default function Main() {
|
|||||||
}
|
}
|
||||||
<LoginDialog open={showLoginDialog} />
|
<LoginDialog open={showLoginDialog} />
|
||||||
<RegisterDialog open={showRegisterDialog} />
|
<RegisterDialog open={showRegisterDialog} />
|
||||||
|
<AddFavourtieChatDialog open={showAddFavourtieChatDialog} />
|
||||||
|
<mdui-navigation-drawer ref={drawerRef} modal close-on-esc close-on-overlay-click>
|
||||||
|
<mdui-list style={{
|
||||||
|
padding: '10px',
|
||||||
|
}}>
|
||||||
|
<mdui-list-item rounded>
|
||||||
|
<span>{myProfileCache?.getNickName()}</span>
|
||||||
|
<AvatarMySelf slot="icon" />
|
||||||
|
</mdui-list-item>
|
||||||
|
<mdui-list-item rounded icon="manage_accounts">账号设置</mdui-list-item>
|
||||||
|
<mdui-divider style={{
|
||||||
|
margin: '10px',
|
||||||
|
}}></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,
|
||||||
|
}}></div>
|
||||||
|
<span style={{
|
||||||
|
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 />
|
||||||
|
在 Codeberg 上<a href="https://codeberg.org/CrescentLeaf/LingChair">查看源代码</a>
|
||||||
|
</span>
|
||||||
|
</mdui-navigation-drawer>
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Default: 侧边列表提供列表切换
|
* Default: 侧边列表提供列表切换
|
||||||
*/
|
*/
|
||||||
!isMobileUI() ?
|
!isMobileUI() ?
|
||||||
<mdui-navigation-rail ref={navigationRef} contained value="Recents">
|
<mdui-navigation-rail ref={navigationRef} contained value="Recents">
|
||||||
<mdui-button-icon slot="top">
|
<mdui-button-icon slot="top" icon="menu" onClick={() => drawerRef.current!.open = true}></mdui-button-icon>
|
||||||
<AvatarMySelf />
|
|
||||||
</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="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="Contacts"></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="chat--outlined" active-icon="chat--filled" value="AllChats"></mdui-navigation-rail-item>
|
<mdui-navigation-rail-item icon="chat--outlined" active-icon="chat--filled" value="AllChats"></mdui-navigation-rail-item>
|
||||||
|
|
||||||
|
|
||||||
<mdui-dropdown trigger="hover" slot="bottom">
|
|
||||||
<mdui-button-icon icon="add" slot="trigger"></mdui-button-icon>
|
|
||||||
<mdui-menu>
|
|
||||||
<mdui-menu-item icon="person_add">添加收藏对话</mdui-menu-item>
|
|
||||||
<mdui-menu-item icon="group_add">创建群组</mdui-menu-item>
|
|
||||||
</mdui-menu>
|
|
||||||
</mdui-dropdown>
|
|
||||||
</mdui-navigation-rail>
|
</mdui-navigation-rail>
|
||||||
/**
|
/**
|
||||||
* Mobile: 底部导航栏提供列表切换
|
* Mobile: 底部导航栏提供列表切换
|
||||||
@@ -97,27 +172,17 @@ export default function Main() {
|
|||||||
marginLeft: '15px',
|
marginLeft: '15px',
|
||||||
top: '0px',
|
top: '0px',
|
||||||
}}>
|
}}>
|
||||||
|
<mdui-button-icon icon="menu" onClick={() => drawerRef.current!.open = true}></mdui-button-icon>
|
||||||
<mdui-top-app-bar-title>{
|
<mdui-top-app-bar-title>{
|
||||||
({
|
({
|
||||||
Recents: "最近对话",
|
Recents: "最近对话",
|
||||||
Contacts: "收藏对话",
|
Favourites: "收藏对话",
|
||||||
AllChats: "所有对话",
|
AllChats: "所有对话",
|
||||||
})['Recents']
|
})[currentShowPage]
|
||||||
}</mdui-top-app-bar-title>
|
}</mdui-top-app-bar-title>
|
||||||
<div style={{
|
<div style={{
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
}}></div>
|
}}></div>
|
||||||
<mdui-dropdown trigger="hover">
|
|
||||||
<mdui-button-icon icon="add" slot="trigger"></mdui-button-icon>
|
|
||||||
<mdui-menu>
|
|
||||||
<mdui-menu-item icon="person_add">添加收藏对话</mdui-menu-item>
|
|
||||||
<mdui-menu-item icon="group_add">创建群组</mdui-menu-item>
|
|
||||||
</mdui-menu>
|
|
||||||
</mdui-dropdown>
|
|
||||||
<mdui-button-icon icon="settings"></mdui-button-icon>
|
|
||||||
<mdui-button-icon>
|
|
||||||
<AvatarMySelf />
|
|
||||||
</mdui-button-icon>
|
|
||||||
</mdui-top-app-bar>
|
</mdui-top-app-bar>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -130,7 +195,15 @@ export default function Main() {
|
|||||||
height: 'calc(100% - 80px - 67px)',
|
height: 'calc(100% - 80px - 67px)',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
} : {}} id="SideBar">
|
} : {}} 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>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -143,12 +216,13 @@ export default function Main() {
|
|||||||
bottom: '0',
|
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="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="Contacts">收藏对话</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-item icon="chat--outlined" active-icon="chat--filled" value="AllChats">全部对话</mdui-navigation-bar-item>
|
||||||
</mdui-navigation-bar>
|
</mdui-navigation-bar>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)}>
|
)}>
|
||||||
|
{subRoutes}
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import { createContext } from 'react'
|
import { Chat, UserMySelf } from "lingchair-client-protocol"
|
||||||
|
import { createContext } from "use-context-selector"
|
||||||
type shared = {
|
|
||||||
ui_functions: React.MutableRefObject<{
|
|
||||||
|
|
||||||
|
type Shared = {
|
||||||
|
functions_lazy: React.MutableRefObject<{
|
||||||
|
updateFavouriteChats: () => void
|
||||||
|
updateRecentChats: () => void
|
||||||
|
updateAllChats: () => void
|
||||||
}>
|
}>
|
||||||
|
favouriteChats: Chat[]
|
||||||
|
setFavouriteChats: React.Dispatch<React.SetStateAction<Chat[]>>
|
||||||
setShowLoginDialog: React.Dispatch<React.SetStateAction<boolean>>
|
setShowLoginDialog: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
setShowRegisterDialog: React.Dispatch<React.SetStateAction<boolean>>
|
setShowRegisterDialog: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setShowAddFavourtieChatDialog: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
setCurrentSelectedChatId: React.Dispatch<React.SetStateAction<string>>
|
||||||
|
myProfileCache?: UserMySelf
|
||||||
|
currentSelectedChatId: string
|
||||||
}
|
}
|
||||||
const MainSharedContext = createContext({} as shared)
|
const MainSharedContext = createContext({} as Shared)
|
||||||
|
|
||||||
export default MainSharedContext
|
export default MainSharedContext
|
||||||
|
|
||||||
|
export type { Shared }
|
||||||
|
|||||||
46
client/ui/main-page/AddFavourtieChatDialog.tsx
Normal file
46
client/ui/main-page/AddFavourtieChatDialog.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from 'react'
|
||||||
|
import { Button, Dialog, snackbar, TextField } from "mdui"
|
||||||
|
import { data } 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'
|
||||||
|
|
||||||
|
export default function AddFavourtieChatDialog({ ...props }: { open: boolean } & React.HTMLAttributes<Dialog>) {
|
||||||
|
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
||||||
|
myProfileCache: context.myProfileCache,
|
||||||
|
setShowAddFavourtieChatDialog: context.setShowAddFavourtieChatDialog,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const dialogRef = React.useRef<Dialog>()
|
||||||
|
useEventListener(dialogRef, 'closed', () => shared.setShowAddFavourtieChatDialog(false))
|
||||||
|
|
||||||
|
const inputTargetRef = React.useRef<TextField>(null)
|
||||||
|
|
||||||
|
async function addFavouriteChat() {
|
||||||
|
try {
|
||||||
|
shared.myProfileCache!.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="添加收藏对话" {...props} ref={dialogRef}>
|
||||||
|
<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={() => shared.setShowAddFavourtieChatDialog(false)}>取消</mdui-button>
|
||||||
|
<mdui-button slot="action" variant="text" onClick={() => addFavouriteChat()}>添加</mdui-button>
|
||||||
|
</mdui-dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
80
client/ui/main-page/AllChatsList.tsx
Normal file
80
client/ui/main-page/AllChatsList.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { TextField } from "mdui"
|
||||||
|
import React from "react"
|
||||||
|
import AllChatsListItem from "./AllChatsListItem.tsx"
|
||||||
|
import useEventListener from "../../utils/useEventListener.ts"
|
||||||
|
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||||
|
import { CallbackError, Chat, UserMySelf } from "lingchair-client-protocol"
|
||||||
|
import getClient from "../../getClient.ts"
|
||||||
|
import showSnackbar from "../../utils/showSnackbar.ts"
|
||||||
|
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||||
|
import { useContextSelector } from "use-context-selector"
|
||||||
|
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
||||||
|
|
||||||
|
export default function AllChatsList({ ...props }: React.HTMLAttributes<HTMLElement>) {
|
||||||
|
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
||||||
|
myProfileCache: context.myProfileCache,
|
||||||
|
functions_lazy: context.functions_lazy,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const searchRef = React.useRef<HTMLElement>(null)
|
||||||
|
const [searchText, setSearchText] = React.useState('')
|
||||||
|
const [allChatsList, setAllChatsList] = React.useState<Chat[]>([])
|
||||||
|
|
||||||
|
useEventListener(searchRef, 'input', (e) => {
|
||||||
|
setSearchText((e.target as unknown as TextField).value)
|
||||||
|
})
|
||||||
|
|
||||||
|
useAsyncEffect(async () => {
|
||||||
|
async function updateAllChats() {
|
||||||
|
try {
|
||||||
|
setAllChatsList(await shared.myProfileCache!.getMyAllChatsOrThrow())
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof CallbackError)
|
||||||
|
if (e.code != 401 && e.code != 400)
|
||||||
|
showSnackbar({
|
||||||
|
message: '获取所有对话失败: ' + e.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateAllChats()
|
||||||
|
|
||||||
|
shared.functions_lazy.current.updateAllChats = updateAllChats
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return <mdui-list style={{
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingRight: '10px',
|
||||||
|
paddingLeft: '10px',
|
||||||
|
paddingTop: '0',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
...props?.style,
|
||||||
|
}} {...props}>
|
||||||
|
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
|
||||||
|
paddingTop: '12px',
|
||||||
|
paddingBottom: '13px',
|
||||||
|
position: 'sticky',
|
||||||
|
top: '0',
|
||||||
|
backgroundColor: 'rgb(var(--mdui-color-background))',
|
||||||
|
zIndex: '10',
|
||||||
|
}}></mdui-text-field>
|
||||||
|
{
|
||||||
|
allChatsList.filter((chat) =>
|
||||||
|
searchText == '' ||
|
||||||
|
chat.getTitle().includes(searchText) ||
|
||||||
|
chat.getId().includes(searchText)
|
||||||
|
).map((v) =>
|
||||||
|
<AllChatsListItem
|
||||||
|
active={isMobileUI() ? false : currentChatId == v.getId()}
|
||||||
|
key={v.getId()}
|
||||||
|
onClick={() => {
|
||||||
|
openChatInfoDialog(v)
|
||||||
|
}}
|
||||||
|
chat={v} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</mdui-list>
|
||||||
|
}
|
||||||
29
client/ui/main-page/AllChatsListItem.tsx
Normal file
29
client/ui/main-page/AllChatsListItem.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { $ } from "mdui/jq"
|
||||||
|
import Avatar from "../Avatar.tsx"
|
||||||
|
import React from 'react'
|
||||||
|
import { Chat } from "lingchair-client-protocol"
|
||||||
|
import getClient from "../../getClient.ts"
|
||||||
|
|
||||||
|
interface Args extends React.HTMLAttributes<HTMLElement> {
|
||||||
|
chat: Chat
|
||||||
|
active?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AllChatsListItem({ chat, active, ...prop }: Args) {
|
||||||
|
const title = chat.getTitle()
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mdui-list-item active={active} ref={ref} rounded style={{
|
||||||
|
marginTop: '3px',
|
||||||
|
marginBottom: '3px',
|
||||||
|
width: '100%',
|
||||||
|
}} {...prop as any}>
|
||||||
|
<span style={{
|
||||||
|
width: "100%",
|
||||||
|
}}>{title}</span>
|
||||||
|
<Avatar src={getClient().getUrlForFileByHash(chat.getAvatarFileHash() as string)} text={title} slot="icon" />
|
||||||
|
</mdui-list-item>
|
||||||
|
)
|
||||||
|
}
|
||||||
170
client/ui/main-page/FavouriteChatsList.tsx
Normal file
170
client/ui/main-page/FavouriteChatsList.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import React from "react"
|
||||||
|
import FavouriteChatsListItem from "./FavouriteChatsListItem.tsx"
|
||||||
|
import { dialog, TextField } from "mdui"
|
||||||
|
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||||
|
import useEventListener from "../../utils/useEventListener.ts"
|
||||||
|
import { CallbackError, Chat, UserMySelf } from "lingchair-client-protocol"
|
||||||
|
import showSnackbar from "../../utils/showSnackbar.ts"
|
||||||
|
import getClient from "../../getClient.ts"
|
||||||
|
import { useContextSelector } from "use-context-selector"
|
||||||
|
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
||||||
|
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||||
|
|
||||||
|
export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HTMLElement>) {
|
||||||
|
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
||||||
|
myProfileCache: context.myProfileCache,
|
||||||
|
setShowAddFavourtieChatDialog: context.setShowAddFavourtieChatDialog,
|
||||||
|
functions_lazy: context.functions_lazy,
|
||||||
|
currentSelectedChatId: context.currentSelectedChatId,
|
||||||
|
values_lazy: context.values_lazy,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const searchRef = React.useRef<HTMLElement>(null)
|
||||||
|
const [isMultiSelecting, setIsMultiSelecting] = React.useState(false)
|
||||||
|
const [searchText, setSearchText] = React.useState('')
|
||||||
|
const [favouriteChatsList, setFavouriteChatsList] = React.useState<Chat[]>([])
|
||||||
|
const [checkedList, setCheckedList] = React.useState<{ [key: string]: boolean }>({})
|
||||||
|
|
||||||
|
useEventListener(searchRef, 'input', (e) => {
|
||||||
|
setSearchText((e.target as unknown as TextField).value)
|
||||||
|
})
|
||||||
|
|
||||||
|
useAsyncEffect(async () => {
|
||||||
|
async function updateFavouriteChats() {
|
||||||
|
try {
|
||||||
|
const ls = await shared.myProfileCache!.getMyFavouriteChatsOrThrow()
|
||||||
|
setFavouriteChatsList(ls)
|
||||||
|
shared.favourite_chats
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof CallbackError)
|
||||||
|
if (e.code != 401 && e.code != 400)
|
||||||
|
showSnackbar({
|
||||||
|
message: '获取收藏对话失败: ' + e.message
|
||||||
|
})
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateFavouriteChats()
|
||||||
|
|
||||||
|
shared.functions_lazy.current.updateFavouriteChats = updateFavouriteChats
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
}
|
||||||
|
}, [shared.myProfileCache])
|
||||||
|
|
||||||
|
return <mdui-list style={{
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingLeft: '10px',
|
||||||
|
paddingRight: '10px',
|
||||||
|
paddingTop: '0',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
...props?.style,
|
||||||
|
}} {...props}>
|
||||||
|
<div style={{
|
||||||
|
position: 'sticky',
|
||||||
|
top: '0',
|
||||||
|
backgroundColor: 'rgb(var(--mdui-color-background))',
|
||||||
|
zIndex: '10',
|
||||||
|
}}>
|
||||||
|
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
|
||||||
|
paddingTop: '12px',
|
||||||
|
}}></mdui-text-field>
|
||||||
|
<mdui-list-item rounded style={{
|
||||||
|
marginTop: '13px',
|
||||||
|
width: '100%',
|
||||||
|
}} icon="person_add" onClick={() => shared.setShowAddFavourtieChatDialog(true)}>添加收藏</mdui-list-item>
|
||||||
|
<mdui-list-item rounded style={{
|
||||||
|
width: '100%',
|
||||||
|
}} icon="refresh" onClick={() => shared.functions_lazy.current.updateFavouriteChats()}>刷新列表</mdui-list-item>
|
||||||
|
<mdui-list-item rounded style={{
|
||||||
|
width: '100%',
|
||||||
|
}} icon={isMultiSelecting ? "done" : "edit"} onClick={() => {
|
||||||
|
if (isMultiSelecting)
|
||||||
|
setCheckedList({})
|
||||||
|
setIsMultiSelecting(!isMultiSelecting)
|
||||||
|
}}>{isMultiSelecting ? "关闭多选" : "多选模式"}</mdui-list-item>
|
||||||
|
{
|
||||||
|
isMultiSelecting && <>
|
||||||
|
<mdui-list-item rounded style={{
|
||||||
|
width: '100%',
|
||||||
|
}} icon="delete" onClick={() => dialog({
|
||||||
|
headline: "移除收藏对话",
|
||||||
|
description: "确定将所选对话从收藏中移除吗? 这不会导致对话被删除.",
|
||||||
|
closeOnEsc: true,
|
||||||
|
closeOnOverlayClick: true,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: "取消",
|
||||||
|
onClick: () => {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "确定",
|
||||||
|
onClick: async () => {
|
||||||
|
const ls = Object.keys(checkedList).filter((chatId) => checkedList[chatId] == true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
shared.myProfileCache!.removeFavouriteChatsOrThrow(ls)
|
||||||
|
|
||||||
|
setCheckedList({})
|
||||||
|
setIsMultiSelecting(false)
|
||||||
|
|
||||||
|
shared.functions_lazy.current.updateFavouriteChats()
|
||||||
|
|
||||||
|
showSnackbar({
|
||||||
|
message: "已删除所选",
|
||||||
|
action: "撤销操作",
|
||||||
|
onActionClick: async () => {
|
||||||
|
try {
|
||||||
|
shared.myProfileCache!.addFavouriteChatsOrThrow(ls)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof CallbackError)
|
||||||
|
showSnackbar({
|
||||||
|
message: '撤销删除收藏失败: ' + e.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
shared.functions_lazy.current.updateFavouriteChats()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof CallbackError)
|
||||||
|
showSnackbar({
|
||||||
|
message: '删除收藏对话失败: ' + e.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})}>删除所选</mdui-list-item>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
<div style={{
|
||||||
|
height: "10px",
|
||||||
|
}}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
favouriteChatsList.filter((chat) =>
|
||||||
|
searchText == '' ||
|
||||||
|
chat.getTitle().includes(searchText) ||
|
||||||
|
chat.getId().includes(searchText)
|
||||||
|
).map((v) =>
|
||||||
|
<FavouriteChatsListItem
|
||||||
|
active={isMultiSelecting ? checkedList[v.getId()] == true : (isMobileUI() ? false : shared.currentSelectedChatId == v.getId())}
|
||||||
|
onClick={() => {
|
||||||
|
if (isMultiSelecting)
|
||||||
|
setCheckedList({
|
||||||
|
...checkedList,
|
||||||
|
[v.getId()]: !checkedList[v.getId()],
|
||||||
|
})
|
||||||
|
else
|
||||||
|
openChatInfoDialog(v)
|
||||||
|
}}
|
||||||
|
key={v.getId()}
|
||||||
|
chat={v} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</mdui-list>
|
||||||
|
}
|
||||||
28
client/ui/main-page/FavouriteChatsListItem.tsx
Normal file
28
client/ui/main-page/FavouriteChatsListItem.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Chat } from "lingchair-client-protocol"
|
||||||
|
import Avatar from "../Avatar.tsx"
|
||||||
|
import React from 'react'
|
||||||
|
import getClient from "../../getClient.ts"
|
||||||
|
|
||||||
|
interface Args extends React.HTMLAttributes<HTMLElement> {
|
||||||
|
chat: Chat
|
||||||
|
active?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FavouriteChatsListItem({ chat, active, ...prop }: Args) {
|
||||||
|
const title = chat.getTitle()
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLElement>(null)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mdui-list-item active={active} ref={ref} rounded style={{
|
||||||
|
marginTop: '3px',
|
||||||
|
marginBottom: '3px',
|
||||||
|
width: '100%',
|
||||||
|
}} {...prop}>
|
||||||
|
<span style={{
|
||||||
|
width: "100%",
|
||||||
|
}}>{title}</span>
|
||||||
|
<Avatar src={getClient().getUrlForFileByHash(chat.getAvatarFileHash() as string)} text={title} slot="icon" />
|
||||||
|
</mdui-list-item>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,19 +1,26 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Button, Dialog, TextField } from "mdui"
|
import { Dialog, TextField } from "mdui"
|
||||||
|
|
||||||
import performAuth from '../../performAuth.ts'
|
import performAuth from '../../performAuth.ts'
|
||||||
import showSnackbar from '../../utils/showSnackbar.ts'
|
import showSnackbar from '../../utils/showSnackbar.ts'
|
||||||
import MainSharedContext from '../MainSharedContext.ts'
|
import MainSharedContext, { Shared } from '../MainSharedContext.ts'
|
||||||
|
import { useContextSelector } from 'use-context-selector'
|
||||||
|
import useEventListener from '../../utils/useEventListener.ts'
|
||||||
|
|
||||||
export default function LoginDialog({ ...props }: { open: boolean } & React.HTMLAttributes<Dialog>) {
|
export default function LoginDialog({ ...props }: { open: boolean } & React.HTMLAttributes<Dialog>) {
|
||||||
const shared = React.useContext(MainSharedContext)
|
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
||||||
|
setShowRegisterDialog: context.setShowRegisterDialog,
|
||||||
|
setShowLoginDialog: context.setShowLoginDialog
|
||||||
|
}))
|
||||||
|
|
||||||
|
const dialogRef = React.useRef<Dialog>()
|
||||||
|
useEventListener(dialogRef, 'closed', () => shared.setShowLoginDialog(false))
|
||||||
|
|
||||||
const loginDialogRef = React.useRef<Dialog>(null)
|
|
||||||
const loginInputAccountRef = React.useRef<TextField>(null)
|
const loginInputAccountRef = React.useRef<TextField>(null)
|
||||||
const loginInputPasswordRef = React.useRef<TextField>(null)
|
const loginInputPasswordRef = React.useRef<TextField>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mdui-dialog {...props} headline="登录" ref={loginDialogRef}>
|
<mdui-dialog {...props} headline="登录" ref={dialogRef}>
|
||||||
|
|
||||||
<mdui-text-field label="用户 ID / 用户名" ref={loginInputAccountRef}></mdui-text-field>
|
<mdui-text-field label="用户 ID / 用户名" ref={loginInputAccountRef}></mdui-text-field>
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
83
client/ui/main-page/RecentChatsList.tsx
Normal file
83
client/ui/main-page/RecentChatsList.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { TextField } from "mdui"
|
||||||
|
import RecentsListItem from "./RecentsListItem.tsx"
|
||||||
|
import React from "react"
|
||||||
|
import RecentChat from "lingchair-client-protocol/RecentChat.ts"
|
||||||
|
import { data } from "react-router"
|
||||||
|
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||||
|
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||||
|
import useEventListener from "../../utils/useEventListener.ts"
|
||||||
|
import { CallbackError } from "lingchair-client-protocol"
|
||||||
|
import { useContextSelector } from "use-context-selector"
|
||||||
|
import showSnackbar from "../../utils/showSnackbar.ts"
|
||||||
|
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
||||||
|
|
||||||
|
export default function RecentChatsList({ ...props }: React.HTMLAttributes<HTMLElement>) {
|
||||||
|
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
||||||
|
myProfileCache: context.myProfileCache,
|
||||||
|
functions_lazy: context.functions_lazy,
|
||||||
|
currentSelectedChatId: context.currentSelectedChatId,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const searchRef = React.useRef<HTMLElement>(null)
|
||||||
|
const [searchText, setSearchText] = React.useState('')
|
||||||
|
const [recentsList, setRecentsList] = React.useState<RecentChat[]>([])
|
||||||
|
|
||||||
|
useEventListener(searchRef, 'input', (e) => {
|
||||||
|
setSearchText((e.target as unknown as TextField).value)
|
||||||
|
})
|
||||||
|
|
||||||
|
useAsyncEffect(async () => {
|
||||||
|
async function updateRecents() {
|
||||||
|
try {
|
||||||
|
setRecentsList(await shared.myProfileCache!.getMyRecentChats())
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof CallbackError)
|
||||||
|
if (e.code != 401 && e.code != 400)
|
||||||
|
showSnackbar({
|
||||||
|
message: '获取最近对话失败: ' + e.message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateRecents()
|
||||||
|
|
||||||
|
shared.functions_lazy.current.updateRecentChats = updateRecents
|
||||||
|
|
||||||
|
const id = setInterval(() => updateRecents(), 15 * 1000)
|
||||||
|
return () => {
|
||||||
|
clearInterval(id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return <mdui-list style={{
|
||||||
|
overflowY: 'auto',
|
||||||
|
paddingRight: '10px',
|
||||||
|
paddingLeft: '10px',
|
||||||
|
paddingTop: '0',
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
...props?.style,
|
||||||
|
}} {...props}>
|
||||||
|
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
|
||||||
|
paddingTop: '12px',
|
||||||
|
marginBottom: '13px',
|
||||||
|
position: 'sticky',
|
||||||
|
top: '0',
|
||||||
|
backgroundColor: 'rgb(var(--mdui-color-background))',
|
||||||
|
zIndex: '10',
|
||||||
|
}}></mdui-text-field>
|
||||||
|
{
|
||||||
|
recentsList.filter((chat) =>
|
||||||
|
searchText == '' ||
|
||||||
|
chat.getTitle().includes(searchText) ||
|
||||||
|
chat.getId().includes(searchText) ||
|
||||||
|
chat.getContent().includes(searchText)
|
||||||
|
).map((v) =>
|
||||||
|
<RecentsListItem
|
||||||
|
active={isMobileUI() ? false : shared.currentSelectedChatId == v.getId()}
|
||||||
|
openChatFragment={() => openChatFragment(v.getId())}
|
||||||
|
key={v.getId()}
|
||||||
|
recentChat={v} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</mdui-list>
|
||||||
|
}
|
||||||
36
client/ui/main-page/RecentsListItem.tsx
Normal file
36
client/ui/main-page/RecentsListItem.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { $ } from "mdui/jq"
|
||||||
|
import Avatar from "../Avatar.tsx"
|
||||||
|
import React from 'react'
|
||||||
|
import getClient from "../../getClient.ts"
|
||||||
|
import RecentChat from "lingchair-client-protocol/RecentChat.ts"
|
||||||
|
|
||||||
|
interface Args extends React.HTMLAttributes<HTMLElement> {
|
||||||
|
recentChat: RecentChat
|
||||||
|
active?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RecentsListItem({ recentChat, active, ...props }: Args) {
|
||||||
|
const { id, title, avatar_file_hash, content } = recentChat.bean
|
||||||
|
|
||||||
|
const itemRef = React.useRef<HTMLElement>(null)
|
||||||
|
React.useEffect(() => {
|
||||||
|
$(itemRef.current!.shadowRoot).find('.headline').css('margin-top', '3px')
|
||||||
|
})
|
||||||
|
return (
|
||||||
|
<mdui-list-item rounded style={{
|
||||||
|
marginTop: '3px',
|
||||||
|
marginBottom: '3px',
|
||||||
|
}} active={active} ref={itemRef} {...props}>
|
||||||
|
{title}
|
||||||
|
<Avatar src={getClient().getUrlForFileByHash(avatar_file_hash!)} text={title} slot="icon" />
|
||||||
|
<span slot="description"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
display: "inline-block",
|
||||||
|
whiteSpace: "nowrap", /* 禁止换行 */
|
||||||
|
overflow: "hidden", /* 隐藏溢出内容 */
|
||||||
|
textOverflow: "ellipsis", /* 显示省略号 */
|
||||||
|
}}>{content}</span>
|
||||||
|
</mdui-list-item>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,20 +1,27 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Button, Dialog, TextField } from "mdui"
|
import { Button, Dialog, TextField } from "mdui"
|
||||||
import MainSharedContext from '../MainSharedContext'
|
import MainSharedContext, { Shared } from '../MainSharedContext'
|
||||||
import showSnackbar from '../../utils/showSnackbar'
|
import showSnackbar from '../../utils/showSnackbar'
|
||||||
import showCircleProgressDialog from '../showCircleProgressDialog'
|
import showCircleProgressDialog from '../showCircleProgressDialog'
|
||||||
import getClient from '../../getClient'
|
import getClient from '../../getClient'
|
||||||
import performAuth from '../../performAuth'
|
import performAuth from '../../performAuth'
|
||||||
|
import { useContextSelector } from 'use-context-selector'
|
||||||
|
import useEventListener from '../../utils/useEventListener'
|
||||||
|
|
||||||
export default function RegisterDialog({ ...props }: { open: boolean } & React.HTMLAttributes<Dialog>) {
|
export default function RegisterDialog({ ...props }: { open: boolean } & React.HTMLAttributes<Dialog>) {
|
||||||
const shared = React.useContext(MainSharedContext)
|
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
||||||
|
setShowRegisterDialog: context.setShowRegisterDialog
|
||||||
|
}))
|
||||||
|
|
||||||
|
const dialogRef = React.useRef<Dialog>()
|
||||||
|
useEventListener(dialogRef, 'closed', () => shared.setShowRegisterDialog(false))
|
||||||
|
|
||||||
const registerInputUserNameRef = React.useRef<TextField>(null)
|
const registerInputUserNameRef = React.useRef<TextField>(null)
|
||||||
const registerInputNickNameRef = React.useRef<TextField>(null)
|
const registerInputNickNameRef = React.useRef<TextField>(null)
|
||||||
const registerInputPasswordRef = React.useRef<TextField>(null)
|
const registerInputPasswordRef = React.useRef<TextField>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mdui-dialog headline="注册" {...props}>
|
<mdui-dialog headline="注册" {...props} ref={dialogRef}>
|
||||||
|
|
||||||
<mdui-text-field label="用户名 (可选)" ref={registerInputUserNameRef}></mdui-text-field>
|
<mdui-text-field label="用户名 (可选)" ref={registerInputUserNameRef}></mdui-text-field>
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
144
client/ui/routers/ChatInfoDialog.tsx
Normal file
144
client/ui/routers/ChatInfoDialog.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { dialog, Dialog } from "mdui"
|
||||||
|
import Avatar from "../Avatar.tsx"
|
||||||
|
import { CallbackError, Chat } from 'lingchair-client-protocol'
|
||||||
|
import { data, useLocation, useNavigate, useSearchParams } from 'react-router'
|
||||||
|
import useAsyncEffect from '../../utils/useAsyncEffect.ts'
|
||||||
|
import { useContextSelector } from 'use-context-selector'
|
||||||
|
import MainSharedContext, { Shared } from '../MainSharedContext.ts'
|
||||||
|
import getClient from '../../getClient.ts'
|
||||||
|
import useEventListener from '../../utils/useEventListener.ts'
|
||||||
|
import showSnackbar from '../../utils/showSnackbar.ts'
|
||||||
|
|
||||||
|
export default function ChatInfoDialog({ ...props }: React.HTMLAttributes<HTMLElement>) {
|
||||||
|
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
||||||
|
myProfileCache: context.myProfileCache,
|
||||||
|
favouriteChats: context.favouriteChats,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const [chat, setChat] = React.useState<Chat>()
|
||||||
|
const [userId, setUserId] = React.useState<string>()
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
let currentLocation = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
function back() {
|
||||||
|
navigate(-1)
|
||||||
|
}
|
||||||
|
const dialogRef = React.useRef<Dialog>()
|
||||||
|
useEventListener(dialogRef, 'overlay-click', () => back())
|
||||||
|
const id = searchParams.get('id')
|
||||||
|
|
||||||
|
const [favourited, setIsFavourited] = React.useState(false)
|
||||||
|
React.useEffect(() => {
|
||||||
|
setIsFavourited(shared.favouriteChats.map((v) => v.getId()).indexOf(chat?.getId() || '') != -1)
|
||||||
|
}, [chat, shared])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
console.log(currentLocation)
|
||||||
|
}, [currentLocation])
|
||||||
|
|
||||||
|
useAsyncEffect(async () => {
|
||||||
|
console.log(id, currentLocation.pathname)
|
||||||
|
try {
|
||||||
|
if (!currentLocation.pathname.startsWith('/info/')) {
|
||||||
|
dialogRef.current!.open = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id == null) {
|
||||||
|
dialogRef.current!.open = false
|
||||||
|
return back()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentLocation.pathname.startsWith('/info/user')) {
|
||||||
|
setChat(await Chat.getOrCreatePrivateChatOrThrow(getClient(), id))
|
||||||
|
setUserId(id)
|
||||||
|
} else
|
||||||
|
setChat(await Chat.getByIdOrThrow(getClient(), id))
|
||||||
|
dialogRef.current!.open = true
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof CallbackError)
|
||||||
|
showSnackbar({
|
||||||
|
message: '打开资料卡失败: ' + e.message
|
||||||
|
})
|
||||||
|
console.log(e)
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
}, [id, currentLocation])
|
||||||
|
|
||||||
|
if (!currentLocation.pathname.startsWith('/info/'))
|
||||||
|
return null
|
||||||
|
|
||||||
|
const avatarUrl = getClient().getUrlForFileByHash(chat?.getAvatarFileHash())!
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mdui-dialog ref={dialogRef}>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}>
|
||||||
|
<Avatar src={avatarUrl} text={chat?.getTitle()} style={{
|
||||||
|
width: '50px',
|
||||||
|
height: '50px',
|
||||||
|
}} onClick={() => avatarUrl && openImageViewer(avatarUrl)} />
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
marginLeft: '15px',
|
||||||
|
marginRight: '15px',
|
||||||
|
fontSize: '16.5px',
|
||||||
|
flexDirection: 'column',
|
||||||
|
}}>
|
||||||
|
<span style={{
|
||||||
|
fontSize: '16.5px'
|
||||||
|
}}>{chat?.getTitle()}</span>
|
||||||
|
<span style={{
|
||||||
|
fontSize: '10.5px',
|
||||||
|
marginTop: '3px',
|
||||||
|
color: 'rgb(var(--mdui-color-secondary))',
|
||||||
|
}}>({chat?.getType()}) ID: {chat?.getType() == 'private' ? userId : chat?.getId()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<mdui-divider style={{
|
||||||
|
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
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "确定",
|
||||||
|
onClick: () => {
|
||||||
|
; (async () => {
|
||||||
|
const re = await Client.invoke(favourited ? "User.removeContacts" : "User.addContacts", {
|
||||||
|
token: data.access_token,
|
||||||
|
targets: [
|
||||||
|
chat!.id
|
||||||
|
],
|
||||||
|
})
|
||||||
|
if (re.code != 200)
|
||||||
|
checkApiSuccessOrSncakbar(re, favourited ? "取消收藏失败" : "收藏失败")
|
||||||
|
EventBus.emit('ContactsList.updateContacts')
|
||||||
|
})()
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
})}>{favourited ? '取消收藏' : '收藏对话'}</mdui-list-item>
|
||||||
|
<mdui-list-item icon="chat" rounded onClick={() => {
|
||||||
|
chatInfoDialogRef.current!.open = false
|
||||||
|
openChatFragment(chat!.id)
|
||||||
|
}}>打开对话</mdui-list-item>
|
||||||
|
</mdui-list>
|
||||||
|
</mdui-dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,6 +2,36 @@ import { defineConfig } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
import config from '../server/config.ts'
|
import config from '../server/config.ts'
|
||||||
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
|
import { execSync } from 'child_process'
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
|
||||||
|
const gitHash = execSync('git rev-parse --short HEAD')
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
const gitFullHash = execSync('git rev-parse HEAD')
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
const gitBranch = execSync('git rev-parse --abbrev-ref HEAD')
|
||||||
|
.toString()
|
||||||
|
.trim()
|
||||||
|
const versionEnv = {
|
||||||
|
define: {
|
||||||
|
__APP_VERSION__: JSON.stringify(JSON.parse(await fs.readFile('package.json', 'utf-8')).version),
|
||||||
|
__GIT_HASH__: JSON.stringify(gitHash),
|
||||||
|
__GIT_HASH_FULL__: JSON.stringify(gitFullHash),
|
||||||
|
__GIT_BRANCH__: JSON.stringify(gitBranch),
|
||||||
|
__BUILD_TIME__: JSON.stringify(new Date().toLocaleString('zh-CN')),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitHashPlugin() {
|
||||||
|
return {
|
||||||
|
name: 'git-hash-plugin',
|
||||||
|
config() {
|
||||||
|
return versionEnv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -14,7 +44,8 @@ export default defineConfig({
|
|||||||
global: true,
|
global: true,
|
||||||
process: true,
|
process: true,
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
gitHashPlugin(),
|
||||||
],
|
],
|
||||||
build: {
|
build: {
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ export default async function createLingChairServer() {
|
|||||||
res.sendFile(path.resolve(file!.getFilePath()))
|
res.sendFile(path.resolve(file!.getFilePath()))
|
||||||
file.updateLastUsedTime()
|
file.updateLastUsedTime()
|
||||||
})
|
})
|
||||||
|
// For client-side router
|
||||||
|
app.get(/.*/, (req, res, next) => {
|
||||||
|
if (
|
||||||
|
req.path.startsWith('/uploaded_files/')
|
||||||
|
|| req.path === '/config.json'
|
||||||
|
) {
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendFile(path.resolve(config.data_path + '/page_compiled/index.html'))
|
||||||
|
})
|
||||||
|
|
||||||
await fs.mkdir(config.data_path + '/upload_cache', { recursive: true })
|
await fs.mkdir(config.data_path + '/upload_cache', { recursive: true })
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user