Compare commits
7 Commits
9e3c1c554f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d2779922 | ||
|
|
a9dbb9655b | ||
|
|
2aa9425334 | ||
|
|
ec527bafc6 | ||
|
|
44ada8206d | ||
|
|
3044cabcaa | ||
|
|
7e6cbbdce4 |
@@ -1,5 +1,5 @@
|
||||
import { Chat, User, UserMySelf } from "lingchair-client-protocol"
|
||||
import getClient from "./getClient"
|
||||
import getClient from "./getClient.ts"
|
||||
|
||||
type CouldCached = User | Chat | null
|
||||
export default class ClientCache {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { LingChairClient } from 'lingchair-client-protocol'
|
||||
import data from "./data.ts"
|
||||
import { UAParser } from 'ua-parser-js'
|
||||
import { randomUUID } from 'lingchair-internal-shared'
|
||||
import performAuth from './performAuth.ts'
|
||||
|
||||
if (!data.device_id) {
|
||||
const ua = new UAParser(navigator.userAgent)
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CallbackError, Chat, UserMySelf } from "lingchair-client-protocol"
|
||||
import showCircleProgressDialog from "./showCircleProgressDialog.ts"
|
||||
import RegisterDialog from "./main-page/RegisterDialog.tsx"
|
||||
import sleep from "../utils/sleep.ts"
|
||||
import { $, dialog, NavigationDrawer } from "mdui"
|
||||
import { $, NavigationDrawer } from "mdui"
|
||||
import getClient from "../getClient.ts"
|
||||
import showSnackbar from "../utils/showSnackbar.ts"
|
||||
import AllChatsList from "./main-page/AllChatsList.tsx"
|
||||
@@ -135,7 +135,7 @@ function Root() {
|
||||
}}></mdui-divider>
|
||||
<mdui-list-item rounded icon="settings">客户端设置</mdui-list-item>
|
||||
<mdui-list-item rounded icon="person_add" onClick={() => AppStateRef.current!.openAddFavouriteChat()}>添加收藏对话</mdui-list-item>
|
||||
<mdui-list-item rounded icon="group_add">创建新的群组</mdui-list-item>
|
||||
<mdui-list-item rounded icon="group_add" onClick={() => AppStateRef.current!.openCreateGroup()}>创建新的群组</mdui-list-item>
|
||||
</mdui-list>
|
||||
<div style={{
|
||||
flexGrow: 1,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Chat, UserMySelf } from "lingchair-client-protocol"
|
||||
import { Chat } from "lingchair-client-protocol"
|
||||
import { createContext } from "use-context-selector"
|
||||
import { SharedState } from "./MainSharedReducer"
|
||||
import { SharedState } from "./MainSharedReducer.ts"
|
||||
|
||||
type Shared = {
|
||||
functions_lazy: React.MutableRefObject<{
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Chat, UserMySelf } from "lingchair-client-protocol"
|
||||
import { Chat } from "lingchair-client-protocol"
|
||||
|
||||
export interface SharedState {
|
||||
favouriteChats: Chat[]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import EffectOnly from "./EffectOnly"
|
||||
import showCircleProgressDialog from "./showCircleProgressDialog"
|
||||
import EffectOnly from "./EffectOnly.tsx"
|
||||
import showCircleProgressDialog from "./showCircleProgressDialog.ts"
|
||||
|
||||
export default function ProgressDialogFallback({ text }: { text: string }) {
|
||||
return <EffectOnly effect={() => {
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import { Button, Dialog, snackbar, TextField } from "mdui"
|
||||
import { data, useNavigate } from 'react-router'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import MainSharedContext, { Shared } from '../MainSharedContext'
|
||||
import showSnackbar from '../../utils/showSnackbar'
|
||||
import { Dialog, TextField } from "mdui"
|
||||
import showSnackbar from '../../utils/showSnackbar.ts'
|
||||
import { CallbackError } from 'lingchair-client-protocol'
|
||||
import useEventListener from '../../utils/useEventListener'
|
||||
import ClientCache from '../../ClientCache'
|
||||
import AppStateContext from './AppStateContext'
|
||||
import useEventListener from '../../utils/useEventListener.ts'
|
||||
import ClientCache from '../../ClientCache.ts'
|
||||
|
||||
export default function AddFavourtieChatDialog({ useRef }: { useRef: React.MutableRefObject<Dialog | undefined> }) {
|
||||
const inputTargetRef = React.useRef<TextField>(null)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { Chat, User } from 'lingchair-client-protocol'
|
||||
import { Dialog } from 'mdui'
|
||||
import * as React from 'react'
|
||||
|
||||
type AppState = {
|
||||
openChatInfo: (chat: Chat | string) => void,
|
||||
openUserInfo: (user: Chat | User | string) => void,
|
||||
openEditMyProfile: () => void,
|
||||
openAddFavouriteChat: () => void,
|
||||
openChat: (chat: string | Chat, inDialog?: boolean) => void,
|
||||
closeChat: () => void,
|
||||
openChatInfo: (chat: Chat | string) => void
|
||||
openUserInfo: (user: Chat | User | string) => void
|
||||
openEditMyProfile: () => void
|
||||
openAddFavouriteChat: () => void
|
||||
openCreateGroup: () => void
|
||||
openChat: (chat: string | Chat, inDialog?: boolean) => void
|
||||
closeChat: () => void
|
||||
}
|
||||
|
||||
const AppStateContext = React.createContext<AppState>({
|
||||
@@ -16,6 +16,7 @@ const AppStateContext = React.createContext<AppState>({
|
||||
openUserInfo: () => {},
|
||||
openEditMyProfile: () => {},
|
||||
openAddFavouriteChat: () => {},
|
||||
openCreateGroup: () => {},
|
||||
openChat: () => {},
|
||||
closeChat: () => {},
|
||||
})
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { $, Dialog } from "mdui"
|
||||
import AppStateContext, { AppState } from "./AppStateContext"
|
||||
import AppStateContext, { AppState } from "./AppStateContext.ts"
|
||||
import { Chat, User } from "lingchair-client-protocol"
|
||||
import getClient from "../../getClient"
|
||||
import UserOrChatInfoDialog from "./UserOrChatInfoDialog"
|
||||
import useEffectRef from "../../utils/useEffectRef"
|
||||
import EditMyProfileDialog from "./EditMyProfileDialog"
|
||||
import AddFavourtieChatDialog from "./AddFavourtieChatDialog"
|
||||
import getClient from "../../getClient.ts"
|
||||
import UserOrChatInfoDialog from "./UserOrChatInfoDialog.tsx"
|
||||
import useEffectRef from "../../utils/useEffectRef.ts"
|
||||
import EditMyProfileDialog from "./EditMyProfileDialog.tsx"
|
||||
import AddFavourtieChatDialog from "./AddFavourtieChatDialog.tsx"
|
||||
import * as React from 'react'
|
||||
import { useContextSelector } from "use-context-selector"
|
||||
import MainSharedContext, { Shared } from "../MainSharedContext"
|
||||
import ChatFragmentDialog from "./ChatFragmentDialog"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
||||
import ClientCache from "../../ClientCache"
|
||||
import isMobileUI from "../../utils/isMobileUI"
|
||||
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
||||
import ChatFragmentDialog from "./ChatFragmentDialog.tsx"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||
import ClientCache from "../../ClientCache.ts"
|
||||
import CreateGroupDialog from "./CreateGroupDialog.tsx"
|
||||
|
||||
const config = await fetch('/config.json').then((re) => re.json())
|
||||
|
||||
@@ -34,6 +34,7 @@ export default function DialogContextWrapper({ children, useRef }: { children: R
|
||||
|
||||
const editMyProfileDialogRef = React.useRef<Dialog>()
|
||||
const addFavouriteChatDialogRef = React.useRef<Dialog>()
|
||||
const createGroupDialogRef = React.useRef<Dialog>()
|
||||
|
||||
const setCurrentSelectedChatId = useContextSelector(
|
||||
MainSharedContext,
|
||||
@@ -67,6 +68,9 @@ export default function DialogContextWrapper({ children, useRef }: { children: R
|
||||
static openAddFavouriteChat() {
|
||||
addFavouriteChatDialogRef.current!.open = true
|
||||
}
|
||||
static openCreateGroup() {
|
||||
createGroupDialogRef.current!.open = true
|
||||
}
|
||||
static async openChat(chat: string | Chat, inDialog?: boolean) {
|
||||
if (chat instanceof Chat) chat = chat.getId()
|
||||
|
||||
@@ -88,6 +92,7 @@ export default function DialogContextWrapper({ children, useRef }: { children: R
|
||||
<UserOrChatInfoDialog chat={userOrChatInfoDialogState[userOrChatInfoDialogState.length - 1] || lastUserOrChatInfoDialogStateRef.current} useRef={userOrChatInfoDialogRef} />
|
||||
<EditMyProfileDialog useRef={editMyProfileDialogRef} />
|
||||
<AddFavourtieChatDialog useRef={addFavouriteChatDialogRef} />
|
||||
<CreateGroupDialog useRef={createGroupDialogRef} />
|
||||
{children}
|
||||
</AppStateContext.Provider>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dialog } from "mdui"
|
||||
import * as React from 'react'
|
||||
import LazyChatFragment from "../chat-fragment/LazyChatFragment"
|
||||
import useEventListener from "../../utils/useEventListener"
|
||||
import LazyChatFragment from "../chat-fragment/LazyChatFragment.tsx"
|
||||
import useEventListener from "../../utils/useEventListener.ts"
|
||||
|
||||
export default function ChatFragmentDialog({ chatId, useRef }: { chatId: string, useRef: React.MutableRefObject<Dialog | undefined> }) {
|
||||
useEventListener(useRef, 'open', () => {
|
||||
|
||||
41
client/ui/app-state/CreateGroupDialog.tsx
Normal file
41
client/ui/app-state/CreateGroupDialog.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as React from 'react'
|
||||
import { Dialog, TextField } from "mdui"
|
||||
import showSnackbar from '../../utils/showSnackbar'
|
||||
import { CallbackError, Chat } from 'lingchair-client-protocol'
|
||||
import useEventListener from '../../utils/useEventListener.ts'
|
||||
import getClient from '../../getClient.ts'
|
||||
|
||||
export default function CreateGroupDialog({ useRef }: { useRef: React.MutableRefObject<Dialog | undefined> }) {
|
||||
const inputTargetRef = React.useRef<TextField>(null)
|
||||
|
||||
useEventListener(useRef, 'closed', () => {
|
||||
inputTargetRef.current!.value = ''
|
||||
})
|
||||
|
||||
async function createGroup() {
|
||||
try {
|
||||
await Chat.createGroupOrThrow(getClient(), inputTargetRef.current!.value)
|
||||
inputTargetRef.current!.value = ''
|
||||
showSnackbar({
|
||||
message: '创建成功!'
|
||||
})
|
||||
useRef.current!.open = false
|
||||
} catch (e) {
|
||||
if (e instanceof CallbackError)
|
||||
showSnackbar({
|
||||
message: '创建群组失败: ' + e.message
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<mdui-dialog close-on-overlay-click close-on-esc headline="创建群组" ref={useRef}>
|
||||
<mdui-text-field clearable label="群组标题" ref={inputTargetRef} onKeyDown={(event: KeyboardEvent) => {
|
||||
if (event.key == 'Enter')
|
||||
createGroup()
|
||||
}}></mdui-text-field>
|
||||
<mdui-button slot="action" variant="text" onClick={() => useRef.current!.open = false}>取消</mdui-button>
|
||||
<mdui-button slot="action" variant="text" onClick={() => createGroup()}>创建</mdui-button>
|
||||
</mdui-dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { CallbackError, UserMySelf } from "lingchair-client-protocol"
|
||||
import ClientCache from "../../ClientCache"
|
||||
import AvatarMySelf from "../AvatarMySelf"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
||||
import getClient from "../../getClient"
|
||||
import { useNavigate } from "react-router"
|
||||
import showSnackbar from "../../utils/showSnackbar"
|
||||
import useEventListener from "../../utils/useEventListener"
|
||||
import ClientCache from "../../ClientCache.ts"
|
||||
import AvatarMySelf from "../AvatarMySelf.tsx"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||
import getClient from "../../getClient.ts"
|
||||
import showSnackbar from "../../utils/showSnackbar.ts"
|
||||
import useEventListener from "../../utils/useEventListener.ts"
|
||||
import { Dialog, TextField } from "mdui"
|
||||
import * as React from 'react'
|
||||
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
import { Dialog, dialog } from "mdui"
|
||||
import { useLoaderData, useNavigate } from "react-router"
|
||||
import { CallbackError, Chat } from "lingchair-client-protocol"
|
||||
import showSnackbar from "../../utils/showSnackbar"
|
||||
import Avatar from "../Avatar"
|
||||
import showSnackbar from "../../utils/showSnackbar.ts"
|
||||
import Avatar from "../Avatar.tsx"
|
||||
import { useContextSelector } from "use-context-selector"
|
||||
import MainSharedContext, { Shared } from "../MainSharedContext"
|
||||
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
||||
import * as React from 'react'
|
||||
import ClientCache from "../../ClientCache"
|
||||
import getClient from "../../getClient"
|
||||
import isMobileUI from "../../utils/isMobileUI"
|
||||
import useEffectRef from "../../utils/useEffectRef"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
||||
import AppStateContext from "./AppStateContext"
|
||||
import ClientCache from "../../ClientCache.ts"
|
||||
import getClient from "../../getClient.ts"
|
||||
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||
import AppStateContext from "./AppStateContext.ts"
|
||||
|
||||
export default function UserOrChatInfoDialog({ chat, useRef }: { chat?: Chat, useRef: React.MutableRefObject<Dialog | undefined> }) {
|
||||
const favouriteChats = useContextSelector(
|
||||
MainSharedContext,
|
||||
(context: Shared) => context.state.favouriteChats
|
||||
)
|
||||
const setCurrentSelectedChatId = useContextSelector(
|
||||
MainSharedContext,
|
||||
(context: Shared) => context.setCurrentSelectedChatId
|
||||
)
|
||||
|
||||
const AppState = React.useContext(AppStateContext)
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ customElements.define('chat-text', class extends HTMLElement {
|
||||
|
||||
// 避免不同的消息类型之间的换行符导致显示异常
|
||||
if (isFirstElementInParent)
|
||||
this.span.textContent = this.textContent.trimStart()
|
||||
this.span.textContent = (this.textContent || '').trimStart()
|
||||
else if (isLastElementInParent)
|
||||
this.span.textContent = this.textContent.trimEnd()
|
||||
this.span.textContent = (this.textContent || '').trimEnd()
|
||||
else
|
||||
this.span.textContent = this.textContent
|
||||
this.span.style.textDecoration = $(this).attr('underline') ? 'underline' : ''
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { $, Tab, TextField } from "mdui"
|
||||
import useEventListener from "../../utils/useEventListener"
|
||||
import useEffectRef from "../../utils/useEffectRef"
|
||||
import isMobileUI from "../../utils/isMobileUI"
|
||||
import useEventListener from "../../utils/useEventListener.ts"
|
||||
import useEffectRef from "../../utils/useEffectRef.ts"
|
||||
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||
import { Chat } from "lingchair-client-protocol"
|
||||
import Preference from "../preference/Preference"
|
||||
import PreferenceHeader from "../preference/PreferenceHeader"
|
||||
import PreferenceLayout from "../preference/PreferenceLayout"
|
||||
import PreferenceUpdater from "../preference/PreferenceUpdater"
|
||||
import SwitchPreference from "../preference/SwitchPreference"
|
||||
import TextFieldPreference from "../preference/TextFieldPreference"
|
||||
import Preference from "../preference/Preference.tsx"
|
||||
import PreferenceHeader from "../preference/PreferenceHeader.tsx"
|
||||
import PreferenceLayout from "../preference/PreferenceLayout.tsx"
|
||||
import PreferenceUpdater from "../preference/PreferenceUpdater.tsx"
|
||||
import SwitchPreference from "../preference/SwitchPreference.tsx"
|
||||
import TextFieldPreference from "../preference/TextFieldPreference.tsx"
|
||||
import * as React from 'react'
|
||||
import ChatMessageContainer from "./ChatMessageContainer"
|
||||
import AppStateContext from "../app-state/AppStateContext"
|
||||
import ChatPanel, { ChatPanelRef } from "./ChatPanel"
|
||||
import AppStateContext from "../app-state/AppStateContext.ts"
|
||||
import ChatPanel, { ChatPanelRef } from "./ChatPanel.tsx"
|
||||
|
||||
interface MduiTabFitSizeArgs extends React.HTMLAttributes<HTMLElement & Tab> {
|
||||
value: string
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ChatParserTransformers, Message } from "lingchair-client-protocol"
|
||||
import isMobileUI from "../../utils/isMobileUI"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
||||
import ClientCache from "../../ClientCache"
|
||||
import getClient from "../../getClient"
|
||||
import Avatar from "../Avatar"
|
||||
import AppStateContext from "../app-state/AppStateContext"
|
||||
import { $, dialog, Dropdown } from "mdui"
|
||||
import useEventListener from "../../utils/useEventListener"
|
||||
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||
import ClientCache from "../../ClientCache.ts"
|
||||
import getClient from "../../getClient.ts"
|
||||
import Avatar from "../Avatar.tsx"
|
||||
import AppStateContext from "../app-state/AppStateContext.ts"
|
||||
import { Dropdown } from "mdui"
|
||||
import useEventListener from "../../utils/useEventListener.ts"
|
||||
import DOMPurify from 'dompurify'
|
||||
import * as React from 'react'
|
||||
import ChatMentionElement from "../chat-elements/chat-mention"
|
||||
import ChatMentionElement from "../chat-elements/chat-mention.ts"
|
||||
|
||||
function escapeHTML(str: string) {
|
||||
const div = document.createElement('div')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Chat, Message } from 'lingchair-client-protocol'
|
||||
import { Message } from 'lingchair-client-protocol'
|
||||
import * as React from 'react'
|
||||
import ChatMessage from './ChatMessage'
|
||||
import ChatMessage from './ChatMessage.tsx'
|
||||
import { dialog } from 'mdui'
|
||||
|
||||
export default function ChatMessageContainer({ messages }: { messages: Message[] }) {
|
||||
return (
|
||||
@@ -53,7 +54,51 @@ export default function ChatMessageContainer({ messages }: { messages: Message[]
|
||||
</div>
|
||||
</mdui-tooltip>
|
||||
}
|
||||
<ChatMessage message={msg} noUserDisplay={lastUser == user && !shouldShowTime} />
|
||||
<ChatMessage
|
||||
message={msg}
|
||||
noUserDisplay={lastUser == user && !shouldShowTime}
|
||||
avatarMenuItems={[
|
||||
<mdui-menu-item icon="info" onClick={async () => {
|
||||
const user = await msg.getUser().then((re) => re?.bean) || {}
|
||||
dialog({
|
||||
headline: "Info",
|
||||
body: `<span style="word-break: break-word;">${Object.keys(user)
|
||||
// @ts-ignore 懒
|
||||
.map((k) => `${k} = ${user[k]}`)
|
||||
.join('<br><br>')}<span>`,
|
||||
closeOnEsc: true,
|
||||
closeOnOverlayClick: true,
|
||||
actions: [
|
||||
{
|
||||
text: "关闭",
|
||||
onClick: () => {
|
||||
return true
|
||||
},
|
||||
}
|
||||
]
|
||||
}).addEventListener('click', (e) => e.stopPropagation())
|
||||
}}>JSON</mdui-menu-item>
|
||||
]}
|
||||
messageMenuItems={[
|
||||
<mdui-menu-item icon="info" onClick={() => dialog({
|
||||
headline: "Info",
|
||||
body: `<span style="word-break: break-word;">${Object.keys(msg.bean)
|
||||
// @ts-ignore 懒
|
||||
.map((k) => `${k} = ${msg.bean[k]}`)
|
||||
.join('<br><br>')}<span>`,
|
||||
closeOnEsc: true,
|
||||
closeOnOverlayClick: true,
|
||||
actions: [
|
||||
{
|
||||
text: "关闭",
|
||||
onClick: () => {
|
||||
return true
|
||||
},
|
||||
}
|
||||
]
|
||||
}).addEventListener('click', (e) => e.stopPropagation())}>Info</mdui-menu-item>
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Chat, Message } from "lingchair-client-protocol"
|
||||
import ChatMessageContainer from "./ChatMessageContainer"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
||||
import ChatMessageContainer from "./ChatMessageContainer.tsx"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||
import * as React from 'react'
|
||||
|
||||
function ChatPanelInner({ chat }: { chat: Chat }, ref: React.ForwardedRef<any>) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Chat } from "lingchair-client-protocol"
|
||||
import getClient from "../../getClient"
|
||||
import ChatFragment from "./ChatFragment"
|
||||
import getClient from "../../getClient.ts"
|
||||
import ChatFragment from "./ChatFragment.tsx"
|
||||
import * as React from 'react'
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
||||
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||
|
||||
export default function LazyChatFragment({ chatId, openedInDialog }: { chatId: string, openedInDialog: boolean }) {
|
||||
const [child, setChild] = React.useState<React.ReactNode>()
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useContextSelector } from "use-context-selector"
|
||||
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
||||
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||
import ClientCache from "../../ClientCache.ts"
|
||||
import { useNavigate } from "react-router"
|
||||
import AppStateContext from "../app-state/AppStateContext.ts"
|
||||
|
||||
export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HTMLElement>) {
|
||||
|
||||
@@ -2,7 +2,6 @@ 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"
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from 'react'
|
||||
import { Button, Dialog, TextField } from "mdui"
|
||||
import MainSharedContext, { Shared } from '../MainSharedContext'
|
||||
import showSnackbar from '../../utils/showSnackbar'
|
||||
import showCircleProgressDialog from '../showCircleProgressDialog'
|
||||
import getClient from '../../getClient'
|
||||
import performAuth from '../../performAuth'
|
||||
import { Dialog, TextField } from "mdui"
|
||||
import MainSharedContext, { Shared } from '../MainSharedContext.ts'
|
||||
import showSnackbar from '../../utils/showSnackbar.ts'
|
||||
import showCircleProgressDialog from '../showCircleProgressDialog.ts'
|
||||
import getClient from '../../getClient.ts'
|
||||
import performAuth from '../../performAuth.ts'
|
||||
import { useContextSelector } from 'use-context-selector'
|
||||
import useEventListener from '../../utils/useEventListener'
|
||||
import useEventListener from '../../utils/useEventListener.ts'
|
||||
|
||||
export default function RegisterDialog({ ...props }: { open: boolean } & React.HTMLAttributes<Dialog>) {
|
||||
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import data from "../data"
|
||||
import data from "../data.ts"
|
||||
|
||||
const searchParams = new URL(location.href).searchParams
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Dialog } from "mdui"
|
||||
import { BlockerFunction, useBlocker, useNavigate } from "react-router"
|
||||
import * as React from 'react'
|
||||
|
||||
export default function useEffectRef<T = undefined>(effect: (ref: React.MutableRefObject<T | undefined>) => void | (() => void), deps?: React.DependencyList) {
|
||||
|
||||
48
readme.md
48
readme.md
@@ -6,31 +6,27 @@
|
||||
|
||||
铃之椅, 一个普通的即时通讯项目——简单, 轻量, 纯粹, 时而天真
|
||||
|
||||
*目前还没有发布正式版本, 仍在积极开发中*
|
||||
***仍在开发阶段, 随时都可能有破坏性变更!***
|
||||
|
||||
项目代号: TheWhiteSilk
|
||||
|
||||
### 基本功能
|
||||
### 目前的功能
|
||||
|
||||
<details>
|
||||
<summary>客户端</summary>
|
||||
<summary>新客户端</summary>
|
||||
|
||||
*: 重构中
|
||||
|
||||
- 消息
|
||||
- [x] 收发消息
|
||||
- [x] 富文本 (based on Marked)
|
||||
- [x] 图片
|
||||
- [x] 视频
|
||||
- [x] 文件
|
||||
- [ ] 测试其他 Markdown 语法的可用性
|
||||
- [ ] *收发消息
|
||||
- [x] 富文本
|
||||
- [ ] 撤回消息
|
||||
- [ ] 修改消息
|
||||
|
||||
- 对话
|
||||
- [x] 最近对话
|
||||
- [x] 添加对话
|
||||
- [x] 添加收藏对话
|
||||
- [x] 添加用户
|
||||
- [x] 添加群组
|
||||
- [ ] 群组管理
|
||||
- [ ] *群组管理
|
||||
|
||||
- 帐号
|
||||
- [x] 登录注册
|
||||
@@ -64,46 +60,48 @@
|
||||
- [x] 登录注册
|
||||
- [x] 资料编辑
|
||||
- [ ] 帐号管理
|
||||
- [ ] 重设密码
|
||||
- [x] 重设密码 (不够好!)
|
||||
- [ ] 绑定邮箱
|
||||
|
||||
</details>
|
||||
|
||||
### 快速上手
|
||||
### 部署
|
||||
|
||||
```bash
|
||||
git clone https://codeberg.org/CrescentLeaf/LingChair
|
||||
cd LingChair
|
||||
# 编译前端
|
||||
deno task build
|
||||
# 运行服务
|
||||
deno task server
|
||||
npm run install-dependencies
|
||||
npm run build-client
|
||||
npm run server
|
||||
```
|
||||
|
||||
#### 配置
|
||||
|
||||
[thewhitesilk_config.json 是什么?](./server/config.ts)
|
||||
thewhitesilk_config.json [请见此处](./server/config.ts)
|
||||
|
||||
### 使用的项目 / 技术栈
|
||||
### 使用到的项目 / 库
|
||||
|
||||
本项目由 Deno 强力驱动
|
||||
由于 Deno 存在的严重问题, 已重新迁移到 Node.js
|
||||
|
||||
*当然, 由于没有使用 Deno Api, 只有 Node Api, 因此理论上 Node.js 也能运行, 但需要另外安装依赖*
|
||||
- 客户端协议
|
||||
- crypto-browserify
|
||||
|
||||
- 前端
|
||||
- 编译
|
||||
- vite
|
||||
- vite-plugin-babel
|
||||
- react
|
||||
- socket.io-client
|
||||
- mdui
|
||||
- split.js
|
||||
- react-json-view
|
||||
- ua-parser-js
|
||||
- pinch-zoom
|
||||
- use-context-selector
|
||||
- dompurify
|
||||
- marked
|
||||
|
||||
- 后端
|
||||
- express
|
||||
- express-fileupload
|
||||
- socket.io
|
||||
- chalk
|
||||
- file-type
|
||||
|
||||
@@ -140,7 +140,12 @@ export default class UserApi extends BaseApi {
|
||||
})
|
||||
// 注冊
|
||||
this.registerEvent("User.register", (args, { deviceId }) => {
|
||||
if (this.checkArgsMissing(args, ['nickname', 'password'])) return {
|
||||
// 判断密码是否为空已经没有意义
|
||||
// 因为空字符的哈希值不为空
|
||||
// 后续会修缮关于注册的机制
|
||||
// 比如限制 IP, 需要邮箱
|
||||
// 虽然我知道这没有什么意义
|
||||
if (this.checkArgsEmpty(args, ['nickname'])) return {
|
||||
msg: "参数缺失",
|
||||
code: 400,
|
||||
}
|
||||
@@ -318,11 +323,11 @@ export default class UserApi extends BaseApi {
|
||||
const user = User.findById(token.author) as User
|
||||
const recentChats = user.getRecentChats()
|
||||
const recentChatsList: any[] = []
|
||||
for (const [chatId, content] of recentChats) {
|
||||
const chat = Chat.findById(chatId)
|
||||
for (const {chat_id, content} of recentChats) {
|
||||
const chat = Chat.findById(chat_id)
|
||||
recentChatsList.push({
|
||||
content,
|
||||
id: chatId,
|
||||
id: chat_id,
|
||||
title: chat?.getTitle(user) || "未知",
|
||||
avatar_file_hash: chat?.getAvatarFileHash(user) ? chat?.getAvatarFileHash(user) : undefined
|
||||
})
|
||||
|
||||
10
server/data/RecentChatBean.ts
Normal file
10
server/data/RecentChatBean.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default class RecentChatBean {
|
||||
declare count: number
|
||||
/** 最近对话所关联的用户 */
|
||||
declare user_id: string
|
||||
declare chat_id: string
|
||||
declare content: string
|
||||
declare updated_time: number
|
||||
|
||||
[key: string]: unknown
|
||||
}
|
||||
@@ -11,11 +11,10 @@ import UserBean from './UserBean.ts'
|
||||
import FileManager from './FileManager.ts'
|
||||
import { SQLInputValue } from "node:sqlite"
|
||||
import ChatPrivate from "./ChatPrivate.ts"
|
||||
import Chat from "./Chat.ts"
|
||||
import ChatBean from "./ChatBean.ts"
|
||||
import MapJson from "../MapJson.ts"
|
||||
import DataWrongError from '../api/DataWrongError.ts'
|
||||
import UserChatLinker from "./UserChatLinker.ts";
|
||||
import UserChatLinker from "./UserChatLinker.ts"
|
||||
import UserFavouriteChatLinker from "./UserFavouriteChatLinker.ts"
|
||||
import UserRecentChatLinker from "./UserRecentChatLinker.ts"
|
||||
|
||||
type UserBeanKey = keyof UserBean
|
||||
|
||||
@@ -38,8 +37,6 @@ export default class User {
|
||||
/* 用户名 */ username TEXT,
|
||||
/* 昵称 */ nickname TEXT NOT NULL,
|
||||
/* 头像, 可选 */ avatar_file_hash TEXT,
|
||||
/* 对话列表 */ favourite_chats TEXT NOT NULL,
|
||||
/* 最近对话 */ recent_chats TEXT NOT NULL,
|
||||
/* 设置 */ settings TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
@@ -69,18 +66,14 @@ export default class User {
|
||||
username,
|
||||
nickname,
|
||||
avatar_file_hash,
|
||||
favourite_chats,
|
||||
recent_chats,
|
||||
settings
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`).run(
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?);`).run(
|
||||
crypto.randomUUID(),
|
||||
password,
|
||||
Date.now(),
|
||||
userName,
|
||||
nickName,
|
||||
null,
|
||||
'[]',
|
||||
JSON.stringify(new Map(), MapJson.replacer),
|
||||
"{}"
|
||||
).lastInsertRowid
|
||||
)[0]
|
||||
@@ -136,37 +129,25 @@ export default class User {
|
||||
this.setAttr("username", userName)
|
||||
}
|
||||
updateRecentChat(chatId: string, content: string) {
|
||||
const map = JSON.parse(this.bean.recent_chats, MapJson.reviver) as Map<string, string>
|
||||
map.delete(chatId)
|
||||
map.set(chatId, content)
|
||||
this.setAttr("recent_chats", JSON.stringify(map, MapJson.replacer))
|
||||
UserRecentChatLinker.updateOrAddRecentChat(this.bean.id, chatId, content)
|
||||
}
|
||||
getRecentChats(): Map<string, string> {
|
||||
try {
|
||||
return JSON.parse(this.bean.recent_chats, MapJson.reviver)
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(`警告: 最近对话列表解析失敗: ${(e as Error).message}`))
|
||||
return new Map()
|
||||
}
|
||||
getRecentChats() {
|
||||
return UserRecentChatLinker.getUserRecentChatBeans(this.bean.id)
|
||||
}
|
||||
|
||||
getFavouriteChats() {
|
||||
return UserFavouriteChatLinker.getUserFavouriteChats(this.bean.id)
|
||||
}
|
||||
addFavouriteChats(chatIds: string[]) {
|
||||
chatIds.forEach((v) => UserFavouriteChatLinker.linkUserAndChat(this.bean.id, v))
|
||||
}
|
||||
removeFavouriteChats(chatIds: string[]) {
|
||||
chatIds.forEach((v) => UserFavouriteChatLinker.unlinkUserAndChat(this.bean.id, v))
|
||||
}
|
||||
addFavouriteChat(chatId: string) {
|
||||
const ls = this.getFavouriteChats()
|
||||
if (ls.indexOf(chatId) != -1 || ChatPrivate.getChatIdByUsersId(this.bean.id, this.bean.id) == chatId) return
|
||||
ls.push(chatId)
|
||||
this.setAttr("favourite_chats", JSON.stringify(ls))
|
||||
}
|
||||
removeFavouriteChats(contacts: string[]) {
|
||||
const ls = this.getFavouriteChats().filter((v) => !contacts.includes(v))
|
||||
this.setAttr("favourite_chats", JSON.stringify(ls))
|
||||
}
|
||||
getFavouriteChats() {
|
||||
try {
|
||||
return [...(JSON.parse(this.bean.favourite_chats) as string[]), ChatPrivate.findOrCreateForPrivate(this, this).bean.id]
|
||||
} catch (e) {
|
||||
console.log(chalk.yellow(`警告: 收藏对话解析失败: ${(e as Error).message}`))
|
||||
return []
|
||||
}
|
||||
this.addFavouriteChats([chatId])
|
||||
}
|
||||
|
||||
getAllChatsList() {
|
||||
return UserChatLinker.getUserChats(this.bean.id)
|
||||
}
|
||||
|
||||
57
server/data/UserFavouriteChatLinker.ts
Normal file
57
server/data/UserFavouriteChatLinker.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { DatabaseSync } from "node:sqlite"
|
||||
import path from 'node:path'
|
||||
|
||||
import config from "../config.ts"
|
||||
import { SQLInputValue } from "node:sqlite"
|
||||
|
||||
export default class UserFavouriteChatLinker {
|
||||
static database: DatabaseSync = this.init()
|
||||
|
||||
private static init(): DatabaseSync {
|
||||
const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, 'UserFavouriteChatLinker.db'))
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS UserFavouriteChatLinker (
|
||||
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/* 用戶 ID */ user_id TEXT NOT NULL,
|
||||
/* Chat ID */ chat_id TEXT NOT NULL
|
||||
);
|
||||
`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_user_id ON UserFavouriteChatLinker(user_id);`)
|
||||
return db
|
||||
}
|
||||
|
||||
/**
|
||||
* 若用户和对话未关联, 则进行关联
|
||||
*/
|
||||
static linkUserAndChat(userId: string, chatId: string) {
|
||||
if (!this.checkUserIsLinkedToChat(userId, chatId))
|
||||
this.database.prepare(`INSERT INTO UserFavouriteChatLinker (
|
||||
user_id,
|
||||
chat_id
|
||||
) VALUES (?, ?);`).run(
|
||||
userId,
|
||||
chatId
|
||||
)
|
||||
}
|
||||
/**
|
||||
* 解除用户和对话的关联
|
||||
*/
|
||||
static unlinkUserAndChat(userId: string, chatId: string) {
|
||||
this.database.prepare(`DELETE FROM UserFavouriteChatLinker WHERE user_id = ? AND chat_id = ?`).run(userId, chatId)
|
||||
}
|
||||
/**
|
||||
* 检测用户和对话的关联
|
||||
*/
|
||||
static checkUserIsLinkedToChat(userId: string, chatId: string) {
|
||||
return this.findAllByCondition('user_id = ? AND chat_id = ?', userId, chatId).length != 0
|
||||
}
|
||||
/**
|
||||
* 获取用户所有关联的对话
|
||||
*/
|
||||
static getUserFavouriteChats(userId: string) {
|
||||
return this.findAllByCondition('user_id = ?', userId).map((v) => v.chat_id) as string[]
|
||||
}
|
||||
protected static findAllByCondition(condition: string, ...args: SQLInputValue[]) {
|
||||
return this.database.prepare(`SELECT * FROM UserFavouriteChatLinker WHERE ${condition}`).all(...args)
|
||||
}
|
||||
}
|
||||
71
server/data/UserRecentChatLinker.ts
Normal file
71
server/data/UserRecentChatLinker.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { DatabaseSync } from "node:sqlite"
|
||||
import path from 'node:path'
|
||||
|
||||
import RecentChatBean from './RecentChatBean.ts'
|
||||
import config from "../config.ts"
|
||||
import { SQLInputValue } from "node:sqlite"
|
||||
|
||||
export default class UserRecentChatLinker {
|
||||
static database: DatabaseSync = this.init()
|
||||
|
||||
private static init(): DatabaseSync {
|
||||
const db: DatabaseSync = new DatabaseSync(path.join(config.data_path, 'UserRecentChatLinker.db'))
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS UserRecentChatLinker (
|
||||
/* 序号 */ count INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
/* 用戶 ID */ user_id TEXT NOT NULL,
|
||||
/* Chat ID */ chat_id TEXT NOT NULL,
|
||||
/* Last Message Content */ content TEXT NOT NULL,
|
||||
/* Last Update Time */ updated_time INT8 NOT NULL
|
||||
);
|
||||
`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_user_id ON UserRecentChatLinker(user_id);`)
|
||||
return db
|
||||
}
|
||||
|
||||
/**
|
||||
* 若用户和对话未关联, 则进行关联
|
||||
*/
|
||||
static updateOrAddRecentChat(userId: string, chatId: string, content: string) {
|
||||
if (!this.checkUserIsLinkedToChat(userId, chatId))
|
||||
this.database.prepare(`INSERT INTO UserRecentChatLinker (
|
||||
user_id,
|
||||
chat_id,
|
||||
content,
|
||||
updated_time
|
||||
) VALUES (?, ?, ?, ?);`).run(
|
||||
userId,
|
||||
chatId,
|
||||
content,
|
||||
Date.now()
|
||||
)
|
||||
else
|
||||
this.database.prepare('UPDATE UserRecentChatLinker SET content = ?, updated_time = ? WHERE count = ?').run(
|
||||
content,
|
||||
Date.now(),
|
||||
/* 既然已经 bind 了, 那么就不需要判断了? */
|
||||
this.findAllByCondition('user_id = ? AND chat_id = ?', userId, chatId)[0].count
|
||||
)
|
||||
}
|
||||
/**
|
||||
* 解除用户和对话的关联
|
||||
*/
|
||||
static removeRecentChat(userId: string, chatId: string) {
|
||||
this.database.prepare(`DELETE FROM UserRecentChatLinker WHERE user_id = ? AND chat_id = ?`).run(userId, chatId)
|
||||
}
|
||||
/**
|
||||
* 检测用户和对话的关联
|
||||
*/
|
||||
static checkUserIsLinkedToChat(userId: string, chatId: string) {
|
||||
return this.findAllByCondition('user_id = ? AND chat_id = ?', userId, chatId).length != 0
|
||||
}
|
||||
/**
|
||||
* 获取用户所有关联的对话
|
||||
*/
|
||||
static getUserRecentChatBeans(userId: string) {
|
||||
return this.findAllByCondition('user_id = ? ORDER BY updated_time DESC', userId) as unknown as RecentChatBean[]
|
||||
}
|
||||
protected static findAllByCondition(condition: string, ...args: SQLInputValue[]) {
|
||||
return this.database.prepare(`SELECT * FROM UserRecentChatLinker WHERE ${condition}`).all(...args)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user