Compare commits
12 Commits
326d62a8bd
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05d2779922 | ||
|
|
a9dbb9655b | ||
|
|
2aa9425334 | ||
|
|
ec527bafc6 | ||
|
|
44ada8206d | ||
|
|
3044cabcaa | ||
|
|
7e6cbbdce4 | ||
|
|
9e3c1c554f | ||
|
|
01ece27e75 | ||
|
|
da505305a3 | ||
|
|
200f5fd0aa | ||
|
|
d35ce7a255 |
90
client-protocol/ChatAttachment.ts
Normal file
90
client-protocol/ChatAttachment.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { ApiCallbackMessage } from 'lingchair-internal-shared'
|
||||||
|
import BaseClientObject from './BaseClientObject.ts'
|
||||||
|
import CallbackError from './CallbackError.ts'
|
||||||
|
import LingChairClient from './LingChairClient.ts'
|
||||||
|
|
||||||
|
export default class ChatAttachment extends BaseClientObject {
|
||||||
|
declare file_hash: string
|
||||||
|
declare file_name: string
|
||||||
|
constructor(client: LingChairClient, {
|
||||||
|
file_hash,
|
||||||
|
file_name
|
||||||
|
}: {
|
||||||
|
file_hash: string,
|
||||||
|
file_name: string
|
||||||
|
}) {
|
||||||
|
super(client)
|
||||||
|
this.file_name = file_name
|
||||||
|
this.file_hash = file_hash
|
||||||
|
}
|
||||||
|
async blob() {
|
||||||
|
try {
|
||||||
|
return await this.blobOrThrow()
|
||||||
|
} catch (_) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fetch(init?: RequestInit) {
|
||||||
|
const url = this.client.getUrlForFileByHash(this.file_hash)
|
||||||
|
return fetch(url!, init)
|
||||||
|
}
|
||||||
|
async blobOrThrow() {
|
||||||
|
const re = await this.fetch()
|
||||||
|
const blob = await re.blob()
|
||||||
|
if (!re.ok) throw new CallbackError({
|
||||||
|
msg: await blob.text(),
|
||||||
|
code: re.status,
|
||||||
|
} as ApiCallbackMessage)
|
||||||
|
return blob
|
||||||
|
}
|
||||||
|
async getMimeType() {
|
||||||
|
try {
|
||||||
|
return await this.getMimeTypeOrThrow()
|
||||||
|
} catch (_) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getMimeTypeOrThrow() {
|
||||||
|
const re = await this.fetch({
|
||||||
|
method: 'HEAD'
|
||||||
|
})
|
||||||
|
if (re.ok) {
|
||||||
|
const t = re.headers.get('content-type')
|
||||||
|
if (t)
|
||||||
|
return t
|
||||||
|
throw new Error("Unable to get Content-Type")
|
||||||
|
}
|
||||||
|
throw new CallbackError({
|
||||||
|
msg: await re.text(),
|
||||||
|
code: re.status,
|
||||||
|
} as ApiCallbackMessage)
|
||||||
|
}
|
||||||
|
async getLength() {
|
||||||
|
try {
|
||||||
|
return await this.getLengthOrThrow()
|
||||||
|
} catch (_) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async getLengthOrThrow() {
|
||||||
|
const re = await this.fetch({
|
||||||
|
method: 'HEAD'
|
||||||
|
})
|
||||||
|
if (re.ok) {
|
||||||
|
const contentLength = re.headers.get('content-length')
|
||||||
|
if (contentLength)
|
||||||
|
return parseInt(contentLength)
|
||||||
|
throw new Error("Unable to get Content-Length")
|
||||||
|
}
|
||||||
|
throw new CallbackError({
|
||||||
|
msg: await re.text(),
|
||||||
|
code: re.status,
|
||||||
|
} as ApiCallbackMessage)
|
||||||
|
}
|
||||||
|
getFileHash() {
|
||||||
|
return this.file_hash
|
||||||
|
}
|
||||||
|
getFileName() {
|
||||||
|
return this.file_name
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,7 @@ import MessageBean from "./bean/MessageBean.ts"
|
|||||||
import LingChairClient from "./LingChairClient.ts"
|
import LingChairClient from "./LingChairClient.ts"
|
||||||
import Chat from "./Chat.ts"
|
import Chat from "./Chat.ts"
|
||||||
import User from "./User.ts"
|
import User from "./User.ts"
|
||||||
import CallbackError from "./CallbackError.ts"
|
import ChatAttachment from "./ChatAttachment.ts"
|
||||||
import ApiCallbackMessage from "./ApiCallbackMessage.ts"
|
|
||||||
|
|
||||||
import * as marked from 'marked'
|
import * as marked from 'marked'
|
||||||
|
|
||||||
@@ -37,93 +36,18 @@ export class ChatMention extends BaseClientObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileType = 'Video' | 'Image' | 'File'
|
type ChatMentionType = 'ChatMention' | 'UserMention'
|
||||||
type MentionType = 'ChatMention' | 'UserMention'
|
type ChatFileType = 'Video' | 'Image' | 'File'
|
||||||
|
|
||||||
export class ChatAttachment extends BaseClientObject {
|
type ChatParserTransformers = {
|
||||||
declare file_hash: string
|
attachment?: ({ text, fileType, attachment }: { text: string, fileType: ChatFileType, attachment: ChatAttachment }) => string,
|
||||||
declare file_name: string
|
mention?: ({ text, mentionType, mention }: { text: string, mentionType: ChatMentionType, mention: ChatMention }) => string,
|
||||||
constructor(client: LingChairClient, {
|
}
|
||||||
file_hash,
|
|
||||||
file_name
|
export type {
|
||||||
}: {
|
ChatMentionType,
|
||||||
file_hash: string,
|
ChatFileType,
|
||||||
file_name: string
|
ChatParserTransformers,
|
||||||
}) {
|
|
||||||
super(client)
|
|
||||||
this.file_name = file_name
|
|
||||||
this.file_hash = file_hash
|
|
||||||
}
|
|
||||||
async blob() {
|
|
||||||
try {
|
|
||||||
return await this.blobOrThrow()
|
|
||||||
} catch (_) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fetch(init?: RequestInit) {
|
|
||||||
const url = this.client.getUrlForFileByHash(this.file_hash)
|
|
||||||
return fetch(url!, init)
|
|
||||||
}
|
|
||||||
async blobOrThrow() {
|
|
||||||
const re = await this.fetch()
|
|
||||||
const blob = await re.blob()
|
|
||||||
if (!re.ok) throw new CallbackError({
|
|
||||||
msg: await blob.text(),
|
|
||||||
code: re.status,
|
|
||||||
} as ApiCallbackMessage)
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
async getMimeType() {
|
|
||||||
try {
|
|
||||||
return await this.getMimeTypeOrThrow()
|
|
||||||
} catch (_) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async getMimeTypeOrThrow() {
|
|
||||||
const re = await this.fetch({
|
|
||||||
method: 'HEAD'
|
|
||||||
})
|
|
||||||
if (re.ok) {
|
|
||||||
const t = re.headers.get('content-type')
|
|
||||||
if (t)
|
|
||||||
return t
|
|
||||||
throw new Error("Unable to get Content-Type")
|
|
||||||
}
|
|
||||||
throw new CallbackError({
|
|
||||||
msg: await re.text(),
|
|
||||||
code: re.status,
|
|
||||||
} as ApiCallbackMessage)
|
|
||||||
}
|
|
||||||
async getLength() {
|
|
||||||
try {
|
|
||||||
return await this.getLengthOrThrow()
|
|
||||||
} catch (_) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async getLengthOrThrow() {
|
|
||||||
const re = await this.fetch({
|
|
||||||
method: 'HEAD'
|
|
||||||
})
|
|
||||||
if (re.ok) {
|
|
||||||
const contentLength = re.headers.get('content-length')
|
|
||||||
if (contentLength)
|
|
||||||
return parseInt(contentLength)
|
|
||||||
throw new Error("Unable to get Content-Length")
|
|
||||||
}
|
|
||||||
throw new CallbackError({
|
|
||||||
msg: await re.text(),
|
|
||||||
code: re.status,
|
|
||||||
} as ApiCallbackMessage)
|
|
||||||
}
|
|
||||||
getFileHash() {
|
|
||||||
return this.file_hash
|
|
||||||
}
|
|
||||||
getFileName() {
|
|
||||||
return this.file_name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class Message extends BaseClientObject {
|
export default class Message extends BaseClientObject {
|
||||||
@@ -152,10 +76,7 @@ export default class Message extends BaseClientObject {
|
|||||||
parseWithTransformers({
|
parseWithTransformers({
|
||||||
attachment,
|
attachment,
|
||||||
mention,
|
mention,
|
||||||
}: {
|
}: ChatParserTransformers) {
|
||||||
attachment?: ({ text, fileType, attachment }: { text: string, fileType: FileType, attachment: ChatAttachment }) => string,
|
|
||||||
mention?: ({ text, mentionType, mention }: { text: string, mentionType: MentionType, mention: ChatMention }) => string,
|
|
||||||
}) {
|
|
||||||
return new marked.Marked({
|
return new marked.Marked({
|
||||||
async: false,
|
async: false,
|
||||||
extensions: [
|
extensions: [
|
||||||
@@ -178,8 +99,8 @@ export default class Message extends BaseClientObject {
|
|||||||
{
|
{
|
||||||
name: 'image',
|
name: 'image',
|
||||||
renderer: ({ text, href }) => {
|
renderer: ({ text, href }) => {
|
||||||
const mentionType = /^(UserMention|ChatMention)=.*/.exec(text)?.[1] as MentionType
|
const mentionType = /^(UserMention|ChatMention)=.*/.exec(text)?.[1] as ChatMentionType
|
||||||
const fileType = (/^(Video|File|Image)=.*/.exec(text)?.[1] || 'Image') as FileType
|
const fileType = (/^(Video|File|Image)=.*/.exec(text)?.[1] || 'Image') as ChatFileType
|
||||||
|
|
||||||
if (fileType != null && /tws:\/\/file\?hash=[A-Za-z0-9]+$/.test(href)) {
|
if (fileType != null && /tws:\/\/file\?hash=[A-Za-z0-9]+$/.test(href)) {
|
||||||
const file_hash = /^tws:\/\/file\?hash=(.*)/.exec(href)?.[1]!
|
const file_hash = /^tws:\/\/file\?hash=(.*)/.exec(href)?.[1]!
|
||||||
|
|||||||
@@ -7,10 +7,16 @@ import GroupSettingsBean from "./bean/GroupSettingsBean.ts"
|
|||||||
import JoinRequestBean from "./bean/JoinRequestBean.ts"
|
import JoinRequestBean from "./bean/JoinRequestBean.ts"
|
||||||
import MessageBean from "./bean/MessageBean.ts"
|
import MessageBean from "./bean/MessageBean.ts"
|
||||||
import RecentChatBean from "./bean/RecentChatBean.ts"
|
import RecentChatBean from "./bean/RecentChatBean.ts"
|
||||||
import Message, { ChatAttachment, ChatMention } from "./Message.ts"
|
import Message, {
|
||||||
|
ChatMention,
|
||||||
|
ChatParserTransformers,
|
||||||
|
ChatMentionType,
|
||||||
|
ChatFileType,
|
||||||
|
} from "./Message.ts"
|
||||||
|
|
||||||
import LingChairClient from "./LingChairClient.ts"
|
import LingChairClient from "./LingChairClient.ts"
|
||||||
import CallbackError from "./CallbackError.ts"
|
import CallbackError from "./CallbackError.ts"
|
||||||
|
import ChatAttachment from "./ChatAttachment.ts"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
LingChairClient,
|
LingChairClient,
|
||||||
@@ -19,6 +25,7 @@ export {
|
|||||||
Chat,
|
Chat,
|
||||||
User,
|
User,
|
||||||
UserMySelf,
|
UserMySelf,
|
||||||
|
|
||||||
Message,
|
Message,
|
||||||
ChatAttachment,
|
ChatAttachment,
|
||||||
ChatMention,
|
ChatMention,
|
||||||
@@ -29,4 +36,10 @@ export {
|
|||||||
RecentChatBean,
|
RecentChatBean,
|
||||||
JoinRequestBean,
|
JoinRequestBean,
|
||||||
}
|
}
|
||||||
export type { GroupSettingsBean }
|
export type {
|
||||||
|
ChatParserTransformers,
|
||||||
|
ChatMentionType,
|
||||||
|
ChatFileType,
|
||||||
|
|
||||||
|
GroupSettingsBean,
|
||||||
|
}
|
||||||
|
|||||||
5
client-protocol/type/ChatParserTransformers.ts
Normal file
5
client-protocol/type/ChatParserTransformers.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import ChatAttachment from '../ChatAttachment.ts'
|
||||||
|
import { ChatMention } from '../Message.ts'
|
||||||
|
import ChatFileType from './ChatFileType.ts'
|
||||||
|
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Chat, User, UserMySelf } from "lingchair-client-protocol"
|
import { Chat, User, UserMySelf } from "lingchair-client-protocol"
|
||||||
import getClient from "./getClient"
|
import getClient from "./getClient.ts"
|
||||||
|
|
||||||
type CouldCached = User | Chat | null
|
type CouldCached = User | Chat | null
|
||||||
export default class ClientCache {
|
export default class ClientCache {
|
||||||
|
|||||||
3
client/env.d.ts
vendored
3
client/env.d.ts
vendored
@@ -6,8 +6,9 @@ declare global {
|
|||||||
namespace React {
|
namespace React {
|
||||||
namespace JSX {
|
namespace JSX {
|
||||||
interface IntrinsicElements {
|
interface IntrinsicElements {
|
||||||
'input-element': {
|
'mdui-patched-textarea': {
|
||||||
'value'?: string
|
'value'?: string
|
||||||
|
insertHtml: (html: string) => void
|
||||||
} & React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>
|
} & React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ 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 { $, dialog, NavigationDrawer } from "mdui"
|
import { $, NavigationDrawer } from "mdui"
|
||||||
import getClient from "../getClient.ts"
|
import getClient from "../getClient.ts"
|
||||||
import showSnackbar from "../utils/showSnackbar.ts"
|
import showSnackbar from "../utils/showSnackbar.ts"
|
||||||
import AllChatsList from "./main-page/AllChatsList.tsx"
|
import AllChatsList from "./main-page/AllChatsList.tsx"
|
||||||
@@ -135,7 +135,7 @@ function Root() {
|
|||||||
}}></mdui-divider>
|
}}></mdui-divider>
|
||||||
<mdui-list-item rounded icon="settings">客户端设置</mdui-list-item>
|
<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="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>
|
</mdui-list>
|
||||||
<div style={{
|
<div style={{
|
||||||
flexGrow: 1,
|
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 { createContext } from "use-context-selector"
|
||||||
import { SharedState } from "./MainSharedReducer"
|
import { SharedState } from "./MainSharedReducer.ts"
|
||||||
|
|
||||||
type Shared = {
|
type Shared = {
|
||||||
functions_lazy: React.MutableRefObject<{
|
functions_lazy: React.MutableRefObject<{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Chat, UserMySelf } from "lingchair-client-protocol"
|
import { Chat } from "lingchair-client-protocol"
|
||||||
|
|
||||||
export interface SharedState {
|
export interface SharedState {
|
||||||
favouriteChats: Chat[]
|
favouriteChats: Chat[]
|
||||||
|
|||||||
@@ -60,6 +60,9 @@ export default class MduiPatchedTextAreaElement extends HTMLElement {
|
|||||||
this._lastValue = this.value || ''
|
this._lastValue = this.value || ''
|
||||||
this.dispatchEvent(new Event('change', { bubbles: true }))
|
this.dispatchEvent(new Event('change', { bubbles: true }))
|
||||||
}
|
}
|
||||||
|
// 消除 <br> 对 placeholder 的影响
|
||||||
|
if (this.value == '')
|
||||||
|
this.value = ''
|
||||||
})
|
})
|
||||||
this.inputDiv.addEventListener('paste', (e: ClipboardEvent) => {
|
this.inputDiv.addEventListener('paste', (e: ClipboardEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -101,6 +104,11 @@ export default class MduiPatchedTextAreaElement extends HTMLElement {
|
|||||||
set value(v) {
|
set value(v) {
|
||||||
this.inputDiv && (this.inputDiv.textContent = v)
|
this.inputDiv && (this.inputDiv.textContent = v)
|
||||||
}
|
}
|
||||||
|
insertHtml(html: string) {
|
||||||
|
this.inputDiv?.focus()
|
||||||
|
|
||||||
|
document.execCommand('insertHTML', false, html)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('mdui-patched-textarea', MduiPatchedTextAreaElement)
|
customElements.define('mdui-patched-textarea', MduiPatchedTextAreaElement)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import EffectOnly from "./EffectOnly"
|
import EffectOnly from "./EffectOnly.tsx"
|
||||||
import showCircleProgressDialog from "./showCircleProgressDialog"
|
import showCircleProgressDialog from "./showCircleProgressDialog.ts"
|
||||||
|
|
||||||
export default function ProgressDialogFallback({ text }: { text: string }) {
|
export default function ProgressDialogFallback({ text }: { text: string }) {
|
||||||
return <EffectOnly effect={() => {
|
return <EffectOnly effect={() => {
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Button, Dialog, snackbar, TextField } from "mdui"
|
import { Dialog, TextField } from "mdui"
|
||||||
import { data, useNavigate } from 'react-router'
|
import showSnackbar from '../../utils/showSnackbar.ts'
|
||||||
import { useContextSelector } from 'use-context-selector'
|
|
||||||
import MainSharedContext, { Shared } from '../MainSharedContext'
|
|
||||||
import showSnackbar from '../../utils/showSnackbar'
|
|
||||||
import { CallbackError } from 'lingchair-client-protocol'
|
import { CallbackError } from 'lingchair-client-protocol'
|
||||||
import useEventListener from '../../utils/useEventListener'
|
import useEventListener from '../../utils/useEventListener.ts'
|
||||||
import ClientCache from '../../ClientCache'
|
import ClientCache from '../../ClientCache.ts'
|
||||||
import AppStateContext from './AppStateContext'
|
|
||||||
|
|
||||||
export default function AddFavourtieChatDialog({ useRef }: { useRef: React.MutableRefObject<Dialog | undefined> }) {
|
export default function AddFavourtieChatDialog({ useRef }: { useRef: React.MutableRefObject<Dialog | undefined> }) {
|
||||||
const inputTargetRef = React.useRef<TextField>(null)
|
const inputTargetRef = React.useRef<TextField>(null)
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Chat, User } from 'lingchair-client-protocol'
|
import { Chat, User } from 'lingchair-client-protocol'
|
||||||
import { Dialog } from 'mdui'
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
type AppState = {
|
type AppState = {
|
||||||
openChatInfo: (chat: Chat | string) => void,
|
openChatInfo: (chat: Chat | string) => void
|
||||||
openUserInfo: (user: Chat | User | string) => void,
|
openUserInfo: (user: Chat | User | string) => void
|
||||||
openEditMyProfile: () => void,
|
openEditMyProfile: () => void
|
||||||
openAddFavouriteChat: () => void,
|
openAddFavouriteChat: () => void
|
||||||
openChat: (chat: string | Chat, inDialog?: boolean) => void,
|
openCreateGroup: () => void
|
||||||
closeChat: () => void,
|
openChat: (chat: string | Chat, inDialog?: boolean) => void
|
||||||
|
closeChat: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppStateContext = React.createContext<AppState>({
|
const AppStateContext = React.createContext<AppState>({
|
||||||
@@ -16,6 +16,7 @@ const AppStateContext = React.createContext<AppState>({
|
|||||||
openUserInfo: () => {},
|
openUserInfo: () => {},
|
||||||
openEditMyProfile: () => {},
|
openEditMyProfile: () => {},
|
||||||
openAddFavouriteChat: () => {},
|
openAddFavouriteChat: () => {},
|
||||||
|
openCreateGroup: () => {},
|
||||||
openChat: () => {},
|
openChat: () => {},
|
||||||
closeChat: () => {},
|
closeChat: () => {},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { $, Dialog } from "mdui"
|
import { $, Dialog } from "mdui"
|
||||||
import AppStateContext, { AppState } from "./AppStateContext"
|
import AppStateContext, { AppState } from "./AppStateContext.ts"
|
||||||
import { Chat, User } from "lingchair-client-protocol"
|
import { Chat, User } from "lingchair-client-protocol"
|
||||||
import getClient from "../../getClient"
|
import getClient from "../../getClient.ts"
|
||||||
import UserOrChatInfoDialog from "./UserOrChatInfoDialog"
|
import UserOrChatInfoDialog from "./UserOrChatInfoDialog.tsx"
|
||||||
import useEffectRef from "../../utils/useEffectRef"
|
import useEffectRef from "../../utils/useEffectRef.ts"
|
||||||
import EditMyProfileDialog from "./EditMyProfileDialog"
|
import EditMyProfileDialog from "./EditMyProfileDialog.tsx"
|
||||||
import AddFavourtieChatDialog from "./AddFavourtieChatDialog"
|
import AddFavourtieChatDialog from "./AddFavourtieChatDialog.tsx"
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { useContextSelector } from "use-context-selector"
|
import { useContextSelector } from "use-context-selector"
|
||||||
import MainSharedContext, { Shared } from "../MainSharedContext"
|
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
||||||
import ChatFragmentDialog from "./ChatFragmentDialog"
|
import ChatFragmentDialog from "./ChatFragmentDialog.tsx"
|
||||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||||
import ClientCache from "../../ClientCache"
|
import ClientCache from "../../ClientCache.ts"
|
||||||
import isMobileUI from "../../utils/isMobileUI"
|
import CreateGroupDialog from "./CreateGroupDialog.tsx"
|
||||||
|
|
||||||
const config = await fetch('/config.json').then((re) => re.json())
|
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 editMyProfileDialogRef = React.useRef<Dialog>()
|
||||||
const addFavouriteChatDialogRef = React.useRef<Dialog>()
|
const addFavouriteChatDialogRef = React.useRef<Dialog>()
|
||||||
|
const createGroupDialogRef = React.useRef<Dialog>()
|
||||||
|
|
||||||
const setCurrentSelectedChatId = useContextSelector(
|
const setCurrentSelectedChatId = useContextSelector(
|
||||||
MainSharedContext,
|
MainSharedContext,
|
||||||
@@ -67,6 +68,9 @@ export default function DialogContextWrapper({ children, useRef }: { children: R
|
|||||||
static openAddFavouriteChat() {
|
static openAddFavouriteChat() {
|
||||||
addFavouriteChatDialogRef.current!.open = true
|
addFavouriteChatDialogRef.current!.open = true
|
||||||
}
|
}
|
||||||
|
static openCreateGroup() {
|
||||||
|
createGroupDialogRef.current!.open = true
|
||||||
|
}
|
||||||
static async openChat(chat: string | Chat, inDialog?: boolean) {
|
static async openChat(chat: string | Chat, inDialog?: boolean) {
|
||||||
if (chat instanceof Chat) chat = chat.getId()
|
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} />
|
<UserOrChatInfoDialog chat={userOrChatInfoDialogState[userOrChatInfoDialogState.length - 1] || lastUserOrChatInfoDialogStateRef.current} useRef={userOrChatInfoDialogRef} />
|
||||||
<EditMyProfileDialog useRef={editMyProfileDialogRef} />
|
<EditMyProfileDialog useRef={editMyProfileDialogRef} />
|
||||||
<AddFavourtieChatDialog useRef={addFavouriteChatDialogRef} />
|
<AddFavourtieChatDialog useRef={addFavouriteChatDialogRef} />
|
||||||
|
<CreateGroupDialog useRef={createGroupDialogRef} />
|
||||||
{children}
|
{children}
|
||||||
</AppStateContext.Provider>
|
</AppStateContext.Provider>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Dialog } from "mdui"
|
import { Dialog } from "mdui"
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import LazyChatFragment from "../chat-fragment/LazyChatFragment"
|
import LazyChatFragment from "../chat-fragment/LazyChatFragment.tsx"
|
||||||
import useEventListener from "../../utils/useEventListener"
|
import useEventListener from "../../utils/useEventListener.ts"
|
||||||
|
|
||||||
export default function ChatFragmentDialog({ chatId, useRef }: { chatId: string, useRef: React.MutableRefObject<Dialog | undefined> }) {
|
export default function ChatFragmentDialog({ chatId, useRef }: { chatId: string, useRef: React.MutableRefObject<Dialog | undefined> }) {
|
||||||
useEventListener(useRef, 'open', () => {
|
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 { CallbackError, UserMySelf } from "lingchair-client-protocol"
|
||||||
import ClientCache from "../../ClientCache"
|
import ClientCache from "../../ClientCache.ts"
|
||||||
import AvatarMySelf from "../AvatarMySelf"
|
import AvatarMySelf from "../AvatarMySelf.tsx"
|
||||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||||
import getClient from "../../getClient"
|
import getClient from "../../getClient.ts"
|
||||||
import { useNavigate } from "react-router"
|
import showSnackbar from "../../utils/showSnackbar.ts"
|
||||||
import showSnackbar from "../../utils/showSnackbar"
|
import useEventListener from "../../utils/useEventListener.ts"
|
||||||
import useEventListener from "../../utils/useEventListener"
|
|
||||||
import { Dialog, TextField } from "mdui"
|
import { Dialog, TextField } from "mdui"
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,22 @@
|
|||||||
import { Dialog, dialog } from "mdui"
|
import { Dialog, dialog } from "mdui"
|
||||||
import { useLoaderData, useNavigate } from "react-router"
|
|
||||||
import { CallbackError, Chat } from "lingchair-client-protocol"
|
import { CallbackError, Chat } from "lingchair-client-protocol"
|
||||||
import showSnackbar from "../../utils/showSnackbar"
|
import showSnackbar from "../../utils/showSnackbar.ts"
|
||||||
import Avatar from "../Avatar"
|
import Avatar from "../Avatar.tsx"
|
||||||
import { useContextSelector } from "use-context-selector"
|
import { useContextSelector } from "use-context-selector"
|
||||||
import MainSharedContext, { Shared } from "../MainSharedContext"
|
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ClientCache from "../../ClientCache"
|
import ClientCache from "../../ClientCache.ts"
|
||||||
import getClient from "../../getClient"
|
import getClient from "../../getClient.ts"
|
||||||
import isMobileUI from "../../utils/isMobileUI"
|
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||||
import useEffectRef from "../../utils/useEffectRef"
|
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
import AppStateContext from "./AppStateContext.ts"
|
||||||
import AppStateContext from "./AppStateContext"
|
|
||||||
|
|
||||||
export default function UserOrChatInfoDialog({ chat, useRef }: { chat?: Chat, useRef: React.MutableRefObject<Dialog | undefined> }) {
|
export default function UserOrChatInfoDialog({ chat, useRef }: { chat?: Chat, useRef: React.MutableRefObject<Dialog | undefined> }) {
|
||||||
const favouriteChats = useContextSelector(
|
const favouriteChats = useContextSelector(
|
||||||
MainSharedContext,
|
MainSharedContext,
|
||||||
(context: Shared) => context.state.favouriteChats
|
(context: Shared) => context.state.favouriteChats
|
||||||
)
|
)
|
||||||
const setCurrentSelectedChatId = useContextSelector(
|
|
||||||
MainSharedContext,
|
|
||||||
(context: Shared) => context.setCurrentSelectedChatId
|
|
||||||
)
|
|
||||||
|
|
||||||
const AppState = React.useContext(AppStateContext)
|
const AppState = React.useContext(AppStateContext)
|
||||||
|
|
||||||
const [isMySelf, setIsMySelf] = React.useState(false)
|
const [isMySelf, setIsMySelf] = React.useState(false)
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ customElements.define('chat-text', class extends HTMLElement {
|
|||||||
|
|
||||||
// 避免不同的消息类型之间的换行符导致显示异常
|
// 避免不同的消息类型之间的换行符导致显示异常
|
||||||
if (isFirstElementInParent)
|
if (isFirstElementInParent)
|
||||||
this.span.textContent = this.textContent.trimStart()
|
this.span.textContent = (this.textContent || '').trimStart()
|
||||||
else if (isLastElementInParent)
|
else if (isLastElementInParent)
|
||||||
this.span.textContent = this.textContent.trimEnd()
|
this.span.textContent = (this.textContent || '').trimEnd()
|
||||||
else
|
else
|
||||||
this.span.textContent = this.textContent
|
this.span.textContent = this.textContent
|
||||||
this.span.style.textDecoration = $(this).attr('underline') ? 'underline' : ''
|
this.span.style.textDecoration = $(this).attr('underline') ? 'underline' : ''
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { $, Tab, TextField } from "mdui"
|
import { $, Tab, TextField } from "mdui"
|
||||||
import useEventListener from "../../utils/useEventListener"
|
import useEventListener from "../../utils/useEventListener.ts"
|
||||||
import useEffectRef from "../../utils/useEffectRef"
|
import useEffectRef from "../../utils/useEffectRef.ts"
|
||||||
import isMobileUI from "../../utils/isMobileUI"
|
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||||
import { Chat } from "lingchair-client-protocol"
|
import { Chat } from "lingchair-client-protocol"
|
||||||
import Preference from "../preference/Preference"
|
import Preference from "../preference/Preference.tsx"
|
||||||
import PreferenceHeader from "../preference/PreferenceHeader"
|
import PreferenceHeader from "../preference/PreferenceHeader.tsx"
|
||||||
import PreferenceLayout from "../preference/PreferenceLayout"
|
import PreferenceLayout from "../preference/PreferenceLayout.tsx"
|
||||||
import PreferenceUpdater from "../preference/PreferenceUpdater"
|
import PreferenceUpdater from "../preference/PreferenceUpdater.tsx"
|
||||||
import SwitchPreference from "../preference/SwitchPreference"
|
import SwitchPreference from "../preference/SwitchPreference.tsx"
|
||||||
import TextFieldPreference from "../preference/TextFieldPreference"
|
import TextFieldPreference from "../preference/TextFieldPreference.tsx"
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ChatMessageContainer from "./ChatMessageContainer"
|
import ChatMessageContainer from "./ChatMessageContainer"
|
||||||
import AppStateContext from "../app-state/AppStateContext"
|
import AppStateContext from "../app-state/AppStateContext.ts"
|
||||||
import ChatPanel, { ChatPanelRef } from "./ChatPanel"
|
import ChatPanel, { ChatPanelRef } from "./ChatPanel.tsx"
|
||||||
|
|
||||||
interface MduiTabFitSizeArgs extends React.HTMLAttributes<HTMLElement & Tab> {
|
interface MduiTabFitSizeArgs extends React.HTMLAttributes<HTMLElement & Tab> {
|
||||||
value: string
|
value: string
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Message } from "lingchair-client-protocol"
|
import { ChatParserTransformers, Message } from "lingchair-client-protocol"
|
||||||
import isMobileUI from "../../utils/isMobileUI"
|
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||||
import ClientCache from "../../ClientCache"
|
import ClientCache from "../../ClientCache.ts"
|
||||||
import getClient from "../../getClient"
|
import getClient from "../../getClient.ts"
|
||||||
import Avatar from "../Avatar"
|
import Avatar from "../Avatar.tsx"
|
||||||
import AppStateContext from "../app-state/AppStateContext"
|
import AppStateContext from "../app-state/AppStateContext.ts"
|
||||||
import { $, dialog, Dropdown } from "mdui"
|
import { Dropdown } from "mdui"
|
||||||
import useEventListener from "../../utils/useEventListener"
|
import useEventListener from "../../utils/useEventListener.ts"
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ChatMentionElement from "../chat-elements/chat-mention"
|
import ChatMentionElement from "../chat-elements/chat-mention.ts"
|
||||||
|
|
||||||
function escapeHTML(str: string) {
|
function escapeHTML(str: string) {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
@@ -94,9 +94,30 @@ const sanitizeConfig = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const transformers: ChatParserTransformers = {
|
||||||
|
attachment({ fileType, attachment }) {
|
||||||
|
const url = getClient().getUrlForFileByHash(attachment.getFileHash())
|
||||||
|
return ({
|
||||||
|
Image: `<chat-image src="${url}" alt="${attachment.getFileName()}"></chat-image>`,
|
||||||
|
Video: `<chat-video src="${url}"></chat-video>`,
|
||||||
|
File: `<chat-file href="${url}" name="${attachment.getFileName()}"></chat-file>`,
|
||||||
|
})?.[fileType]
|
||||||
|
},
|
||||||
|
mention({ mentionType, mention }) {
|
||||||
|
switch (mentionType) {
|
||||||
|
case "UserMention":
|
||||||
|
return `<chat-mention user-id="${mention.user_id}" text="${mention.text}">[对话提及]</chat-mention>`
|
||||||
|
case "ChatMention":
|
||||||
|
return `<chat-mention chat-id="${mention.chat_id}" text="${mention.text}">[对话提及]</chat-mention>`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
export default function ChatMessage({ message, noUserDisplay, avatarMenuItems, messageMenuItems }: { message: Message, noUserDisplay?: boolean, avatarMenuItems?: globalThis.React.JSX.IntrinsicElements['mdui-menu-item'][], messageMenuItems?: globalThis.React.JSX.IntrinsicElements['mdui-menu-item'][] }) {
|
export default function ChatMessage({ message, noUserDisplay, avatarMenuItems, messageMenuItems }: { message: Message, noUserDisplay?: boolean, avatarMenuItems?: globalThis.React.JSX.IntrinsicElements['mdui-menu-item'][], messageMenuItems?: globalThis.React.JSX.IntrinsicElements['mdui-menu-item'][] }) {
|
||||||
const AppState = React.useContext(AppStateContext)
|
const AppState = React.useContext(AppStateContext)
|
||||||
|
|
||||||
|
const [show, setShown] = React.useState(false)
|
||||||
|
|
||||||
const [isAtRight, setAtRight] = React.useState(false)
|
const [isAtRight, setAtRight] = React.useState(false)
|
||||||
|
|
||||||
const messageDropDownRef = React.useRef<Dropdown>()
|
const messageDropDownRef = React.useRef<Dropdown>()
|
||||||
@@ -111,35 +132,19 @@ export default function ChatMessage({ message, noUserDisplay, avatarMenuItems, m
|
|||||||
setAvatarDropDownOpen(false)
|
setAvatarDropDownOpen(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
const [nickName, setNickName] = React.useState('')
|
const [nickName, setNickName] = React.useState(message.getUserId()! || 'System')
|
||||||
const [avatarUrl, setAvatarUrl] = React.useState('')
|
const [avatarUrl, setAvatarUrl] = React.useState('')
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
const user = await ClientCache.getUser(message.getUserId()!)
|
const user = await ClientCache.getUser(message.getUserId()!)
|
||||||
setAtRight(await ClientCache.getMySelf().then((re) => re?.getId()) == user?.getId())
|
setAtRight(await ClientCache.getMySelf().then((re) => re?.getId()) == user?.getId())
|
||||||
setNickName(user?.getNickName() || '')
|
setNickName(user?.getNickName() || '')
|
||||||
setAvatarUrl(getClient().getUrlForFileByHash(user?.getAvatarFileHash() || '') || '')
|
setAvatarUrl(getClient().getUrlForFileByHash(user?.getAvatarFileHash() || '') || '')
|
||||||
|
setShown(true)
|
||||||
}, [message])
|
}, [message])
|
||||||
|
|
||||||
const messageInnerRef = React.useRef<HTMLSpanElement>(null)
|
const messageInnerRef = React.useRef<HTMLSpanElement>(null)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
messageInnerRef.current!.innerHTML = prettyFlatParsedMessage(DOMPurify.sanitize(message.parseWithTransformers({
|
messageInnerRef.current!.innerHTML = prettyFlatParsedMessage(DOMPurify.sanitize(message.parseWithTransformers(transformers), sanitizeConfig))
|
||||||
attachment({ fileType, attachment }) {
|
|
||||||
const url = getClient().getUrlForFileByHash(attachment.getFileHash())
|
|
||||||
return ({
|
|
||||||
Image: `<chat-image src="${url}" alt="${attachment.getFileName()}"></chat-image>`,
|
|
||||||
Video: `<chat-video src="${url}"></chat-video>`,
|
|
||||||
File: `<chat-file href="${url}" name="${attachment.getFileName()}"></chat-file>`,
|
|
||||||
})?.[fileType]
|
|
||||||
},
|
|
||||||
mention({ mentionType, mention }) {
|
|
||||||
switch (mentionType) {
|
|
||||||
case "UserMention":
|
|
||||||
return `<chat-mention user-id="${mention.user_id}" text="${mention.text}">[对话提及]</chat-mention>`
|
|
||||||
case "ChatMention":
|
|
||||||
return `<chat-mention chat-id="${mention.chat_id}" text="${mention.text}">[对话提及]</chat-mention>`
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}), sanitizeConfig))
|
|
||||||
|
|
||||||
// 没有办法的办法 (笑)
|
// 没有办法的办法 (笑)
|
||||||
// 姐姐, 谁让您不是 React 组件呢
|
// 姐姐, 谁让您不是 React 组件呢
|
||||||
@@ -150,7 +155,11 @@ export default function ChatMessage({ message, noUserDisplay, avatarMenuItems, m
|
|||||||
})
|
})
|
||||||
}, [message])
|
}, [message])
|
||||||
|
|
||||||
return (
|
return <>
|
||||||
|
<div style={{
|
||||||
|
display: show ? 'none' : undefined,
|
||||||
|
padding: '5px',
|
||||||
|
}}>加载中...</div>
|
||||||
<div
|
<div
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
onContextMenu={(e) => {
|
onContextMenu={(e) => {
|
||||||
@@ -165,12 +174,12 @@ export default function ChatMessage({ message, noUserDisplay, avatarMenuItems, m
|
|||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
display: show ? 'flex' : 'none',
|
||||||
justifyContent: isAtRight ? "flex-end" : "flex-start",
|
justifyContent: isAtRight ? "flex-end" : "flex-start",
|
||||||
flexDirection: "column"
|
flexDirection: "column"
|
||||||
}}>
|
}}>
|
||||||
{
|
{
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: noUserDisplay ? "none" : "flex",
|
display: noUserDisplay ? "none" : "flex",
|
||||||
justifyContent: isAtRight ? "flex-end" : "flex-start",
|
justifyContent: isAtRight ? "flex-end" : "flex-start",
|
||||||
@@ -258,5 +267,5 @@ export default function ChatMessage({ message, noUserDisplay, avatarMenuItems, m
|
|||||||
</mdui-card>
|
</mdui-card>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Chat, Message } from 'lingchair-client-protocol'
|
import { Message } from 'lingchair-client-protocol'
|
||||||
import * as React from 'react'
|
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[] }) {
|
export default function ChatMessageContainer({ messages }: { messages: Message[] }) {
|
||||||
return (
|
return (
|
||||||
@@ -53,7 +54,51 @@ export default function ChatMessageContainer({ messages }: { messages: Message[]
|
|||||||
</div>
|
</div>
|
||||||
</mdui-tooltip>
|
</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 { Chat, Message } from "lingchair-client-protocol"
|
||||||
import ChatMessageContainer from "./ChatMessageContainer"
|
import ChatMessageContainer from "./ChatMessageContainer.tsx"
|
||||||
import useAsyncEffect from "../../utils/useAsyncEffect"
|
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
|
|
||||||
function ChatPanelInner({ chat }: { chat: Chat }, ref: React.ForwardedRef<any>) {
|
function ChatPanelInner({ chat }: { chat: Chat }, ref: React.ForwardedRef<any>) {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Chat } from "lingchair-client-protocol"
|
import { Chat } from "lingchair-client-protocol"
|
||||||
import getClient from "../../getClient"
|
import getClient from "../../getClient.ts"
|
||||||
import ChatFragment from "./ChatFragment"
|
import ChatFragment from "./ChatFragment.tsx"
|
||||||
import * as React from 'react'
|
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 }) {
|
export default function LazyChatFragment({ chatId, openedInDialog }: { chatId: string, openedInDialog: boolean }) {
|
||||||
const [child, setChild] = React.useState<React.ReactNode>()
|
const [child, setChild] = React.useState<React.ReactNode>()
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { useContextSelector } from "use-context-selector"
|
|||||||
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
import MainSharedContext, { Shared } from "../MainSharedContext.ts"
|
||||||
import isMobileUI from "../../utils/isMobileUI.ts"
|
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||||
import ClientCache from "../../ClientCache.ts"
|
import ClientCache from "../../ClientCache.ts"
|
||||||
import { useNavigate } from "react-router"
|
|
||||||
import AppStateContext from "../app-state/AppStateContext.ts"
|
import AppStateContext from "../app-state/AppStateContext.ts"
|
||||||
|
|
||||||
export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HTMLElement>) {
|
export default function FavouriteChatsList({ ...props }: React.HTMLAttributes<HTMLElement>) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { TextField } from "mdui"
|
|||||||
import RecentsListItem from "./RecentsListItem.tsx"
|
import RecentsListItem from "./RecentsListItem.tsx"
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import RecentChat from "lingchair-client-protocol/RecentChat.ts"
|
import RecentChat from "lingchair-client-protocol/RecentChat.ts"
|
||||||
import { data } from "react-router"
|
|
||||||
import isMobileUI from "../../utils/isMobileUI.ts"
|
import isMobileUI from "../../utils/isMobileUI.ts"
|
||||||
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
import useAsyncEffect from "../../utils/useAsyncEffect.ts"
|
||||||
import useEventListener from "../../utils/useEventListener.ts"
|
import useEventListener from "../../utils/useEventListener.ts"
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Button, Dialog, TextField } from "mdui"
|
import { Dialog, TextField } from "mdui"
|
||||||
import MainSharedContext, { Shared } from '../MainSharedContext'
|
import MainSharedContext, { Shared } from '../MainSharedContext.ts'
|
||||||
import showSnackbar from '../../utils/showSnackbar'
|
import showSnackbar from '../../utils/showSnackbar.ts'
|
||||||
import showCircleProgressDialog from '../showCircleProgressDialog'
|
import showCircleProgressDialog from '../showCircleProgressDialog.ts'
|
||||||
import getClient from '../../getClient'
|
import getClient from '../../getClient.ts'
|
||||||
import performAuth from '../../performAuth'
|
import performAuth from '../../performAuth.ts'
|
||||||
import { useContextSelector } from 'use-context-selector'
|
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>) {
|
export default function RegisterDialog({ ...props }: { open: boolean } & React.HTMLAttributes<Dialog>) {
|
||||||
const shared = useContextSelector(MainSharedContext, (context: Shared) => ({
|
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
|
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'
|
import * as React from 'react'
|
||||||
|
|
||||||
export default function useEffectRef<T = undefined>(effect: (ref: React.MutableRefObject<T | undefined>) => void | (() => void), deps?: React.DependencyList) {
|
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>
|
<details>
|
||||||
<summary>客户端</summary>
|
<summary>新客户端</summary>
|
||||||
|
|
||||||
|
*: 重构中
|
||||||
|
|
||||||
- 消息
|
- 消息
|
||||||
- [x] 收发消息
|
- [ ] *收发消息
|
||||||
- [x] 富文本 (based on Marked)
|
- [x] 富文本
|
||||||
- [x] 图片
|
|
||||||
- [x] 视频
|
|
||||||
- [x] 文件
|
|
||||||
- [ ] 测试其他 Markdown 语法的可用性
|
|
||||||
- [ ] 撤回消息
|
- [ ] 撤回消息
|
||||||
- [ ] 修改消息
|
- [ ] 修改消息
|
||||||
|
|
||||||
- 对话
|
- 对话
|
||||||
- [x] 最近对话
|
- [x] 最近对话
|
||||||
- [x] 添加对话
|
- [x] 添加收藏对话
|
||||||
- [x] 添加用户
|
- [x] 添加用户
|
||||||
- [x] 添加群组
|
- [x] 添加群组
|
||||||
- [ ] 群组管理
|
- [ ] *群组管理
|
||||||
|
|
||||||
- 帐号
|
- 帐号
|
||||||
- [x] 登录注册
|
- [x] 登录注册
|
||||||
@@ -64,46 +60,48 @@
|
|||||||
- [x] 登录注册
|
- [x] 登录注册
|
||||||
- [x] 资料编辑
|
- [x] 资料编辑
|
||||||
- [ ] 帐号管理
|
- [ ] 帐号管理
|
||||||
- [ ] 重设密码
|
- [x] 重设密码 (不够好!)
|
||||||
- [ ] 绑定邮箱
|
- [ ] 绑定邮箱
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### 快速上手
|
### 部署
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://codeberg.org/CrescentLeaf/LingChair
|
git clone https://codeberg.org/CrescentLeaf/LingChair
|
||||||
cd LingChair
|
cd LingChair
|
||||||
# 编译前端
|
npm run install-dependencies
|
||||||
deno task build
|
npm run build-client
|
||||||
# 运行服务
|
npm run server
|
||||||
deno task 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
|
||||||
- vite-plugin-babel
|
|
||||||
- react
|
- react
|
||||||
- socket.io-client
|
- socket.io-client
|
||||||
- mdui
|
- mdui
|
||||||
- split.js
|
- split.js
|
||||||
- react-json-view
|
- ua-parser-js
|
||||||
|
- pinch-zoom
|
||||||
|
- use-context-selector
|
||||||
- dompurify
|
- dompurify
|
||||||
- marked
|
- marked
|
||||||
|
|
||||||
- 后端
|
- 后端
|
||||||
- express
|
- express
|
||||||
|
- express-fileupload
|
||||||
- socket.io
|
- socket.io
|
||||||
- chalk
|
- chalk
|
||||||
- file-type
|
- file-type
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export default class ChatApi extends BaseApi {
|
|||||||
code: 200,
|
code: 200,
|
||||||
msg: "成功",
|
msg: "成功",
|
||||||
data: {
|
data: {
|
||||||
messages: MessagesManager.getInstanceForChat(chat)[args.page ? 'getMessagesWithPage' : 'getMessagesWithOffset'](args.limit as number | undefined, (args.page ? args.page : args.offset) as number).reverse(),
|
messages: MessagesManager.getInstanceForChat(chat)[args.page ? 'getMessagesWithPage' : 'getMessagesWithOffset'](args.limit as number | undefined, (args.page ? args.page : args.offset) as number),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -140,7 +140,12 @@ export default class UserApi extends BaseApi {
|
|||||||
})
|
})
|
||||||
// 注冊
|
// 注冊
|
||||||
this.registerEvent("User.register", (args, { deviceId }) => {
|
this.registerEvent("User.register", (args, { deviceId }) => {
|
||||||
if (this.checkArgsMissing(args, ['nickname', 'password'])) return {
|
// 判断密码是否为空已经没有意义
|
||||||
|
// 因为空字符的哈希值不为空
|
||||||
|
// 后续会修缮关于注册的机制
|
||||||
|
// 比如限制 IP, 需要邮箱
|
||||||
|
// 虽然我知道这没有什么意义
|
||||||
|
if (this.checkArgsEmpty(args, ['nickname'])) return {
|
||||||
msg: "参数缺失",
|
msg: "参数缺失",
|
||||||
code: 400,
|
code: 400,
|
||||||
}
|
}
|
||||||
@@ -318,11 +323,11 @@ export default class UserApi extends BaseApi {
|
|||||||
const user = User.findById(token.author) as User
|
const user = User.findById(token.author) as User
|
||||||
const recentChats = user.getRecentChats()
|
const recentChats = user.getRecentChats()
|
||||||
const recentChatsList: any[] = []
|
const recentChatsList: any[] = []
|
||||||
for (const [chatId, content] of recentChats) {
|
for (const {chat_id, content} of recentChats) {
|
||||||
const chat = Chat.findById(chatId)
|
const chat = Chat.findById(chat_id)
|
||||||
recentChatsList.push({
|
recentChatsList.push({
|
||||||
content,
|
content,
|
||||||
id: chatId,
|
id: chat_id,
|
||||||
title: chat?.getTitle(user) || "未知",
|
title: chat?.getTitle(user) || "未知",
|
||||||
avatar_file_hash: chat?.getAvatarFileHash(user) ? chat?.getAvatarFileHash(user) : undefined
|
avatar_file_hash: chat?.getAvatarFileHash(user) ? chat?.getAvatarFileHash(user) : undefined
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -59,8 +59,13 @@ export default class Chat {
|
|||||||
return new Chat(beans[0])
|
return new Chat(beans[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对话创建基本方法
|
||||||
|
* @param chatName 对话别名, 供查询
|
||||||
|
* @param type 对话类型
|
||||||
|
*/
|
||||||
static create(chatName: string | undefined, type: ChatType) {
|
static create(chatName: string | undefined, type: ChatType) {
|
||||||
if (this.findAllChatBeansByCondition('id = ?', chatName || null).length > 0)
|
if (this.findAllChatBeansByCondition('name = ?', chatName || null).length > 0)
|
||||||
throw new DataWrongError(`对话名称 ${chatName} 已被使用`)
|
throw new DataWrongError(`对话名称 ${chatName} 已被使用`)
|
||||||
const chat = new Chat(
|
const chat = new Chat(
|
||||||
Chat.findAllChatBeansByCondition(
|
Chat.findAllChatBeansByCondition(
|
||||||
@@ -123,6 +128,11 @@ export default class Chat {
|
|||||||
* ======================================================
|
* ======================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加加入请求
|
||||||
|
* @param userId
|
||||||
|
* @param reason
|
||||||
|
*/
|
||||||
addJoinRequest(userId: string, reason?: string) {
|
addJoinRequest(userId: string, reason?: string) {
|
||||||
if (this.findAllJoinRequestsByCondition('user_id = ?', userId).length == 0)
|
if (this.findAllJoinRequestsByCondition('user_id = ?', userId).length == 0)
|
||||||
Chat.database.prepare(`INSERT INTO ${this.getJoinRequestsTableName()} (
|
Chat.database.prepare(`INSERT INTO ${this.getJoinRequestsTableName()} (
|
||||||
@@ -149,6 +159,11 @@ export default class Chat {
|
|||||||
* ======================================================
|
* ======================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加对话管理员
|
||||||
|
* @param userId
|
||||||
|
* @param permission
|
||||||
|
*/
|
||||||
addAdmin(userId: string, permission: string[] | string) {
|
addAdmin(userId: string, permission: string[] | string) {
|
||||||
if (!this.checkUserIsAdmin(userId))
|
if (!this.checkUserIsAdmin(userId))
|
||||||
Chat.database.prepare(`INSERT INTO ${this.getAdminsTableName()} (
|
Chat.database.prepare(`INSERT INTO ${this.getAdminsTableName()} (
|
||||||
@@ -185,6 +200,9 @@ export default class Chat {
|
|||||||
* ======================================================
|
* ======================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取对话成员
|
||||||
|
*/
|
||||||
getMembersList() {
|
getMembersList() {
|
||||||
return UserChatLinker.getChatMembers(this.bean.id)
|
return UserChatLinker.getChatMembers(this.bean.id)
|
||||||
}
|
}
|
||||||
@@ -201,6 +219,10 @@ export default class Chat {
|
|||||||
* ======================================================
|
* ======================================================
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从**私聊**中获取对方用户
|
||||||
|
* @param userMySelf
|
||||||
|
*/
|
||||||
getAnotherUserForPrivate(userMySelf: User) {
|
getAnotherUserForPrivate(userMySelf: User) {
|
||||||
const members = this.getMembersList()
|
const members = this.getMembersList()
|
||||||
const user_a_id = members[0]
|
const user_a_id = members[0]
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ class GroupSettings {
|
|||||||
this.settings = JSON.parse(chat.bean.settings)
|
this.settings = JSON.parse(chat.bean.settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 覆盖群组设定
|
||||||
|
* @param bean 要覆盖的设定, 不需要覆盖的不需要填入
|
||||||
|
*/
|
||||||
update(bean: GroupSettingsBean) {
|
update(bean: GroupSettingsBean) {
|
||||||
const updateValue = (key: string) => {
|
const updateValue = (key: string) => {
|
||||||
if (key in bean)
|
if (key in bean)
|
||||||
@@ -26,6 +30,9 @@ class GroupSettings {
|
|||||||
|
|
||||||
this.apply()
|
this.apply()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 应用更改
|
||||||
|
*/
|
||||||
apply() {
|
apply() {
|
||||||
this.chat.setAttr('settings', JSON.stringify(this.settings))
|
this.chat.setAttr('settings', JSON.stringify(this.settings))
|
||||||
}
|
}
|
||||||
@@ -36,10 +43,19 @@ export default class ChatGroup extends Chat {
|
|||||||
return new GroupSettings(this)
|
return new GroupSettings(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保是群组类型后, 转换成群组对话
|
||||||
|
* 唯一的作用可能是修改群组设定
|
||||||
|
* @param chat
|
||||||
|
*/
|
||||||
static fromChat(chat: Chat) {
|
static fromChat(chat: Chat) {
|
||||||
return new ChatGroup(chat.bean)
|
return new ChatGroup(chat.bean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新的群组
|
||||||
|
* @param group_name 群组名称
|
||||||
|
*/
|
||||||
static createGroup(group_name?: string) {
|
static createGroup(group_name?: string) {
|
||||||
return this.create(group_name, 'group')
|
return this.create(group_name, 'group')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import Chat from "./Chat.ts"
|
|||||||
import User from "./User.ts"
|
import User from "./User.ts"
|
||||||
|
|
||||||
export default class ChatPrivate extends Chat {
|
export default class ChatPrivate extends Chat {
|
||||||
|
/**
|
||||||
|
* 确保是私聊类型后, 转换成私聊对话
|
||||||
|
* 实际上没啥用, 因为实例方法都在 Chat
|
||||||
|
* 未来可能会移除
|
||||||
|
* @param chat
|
||||||
|
*/
|
||||||
static fromChat(chat: Chat) {
|
static fromChat(chat: Chat) {
|
||||||
return new ChatPrivate(chat.bean)
|
return new ChatPrivate(chat.bean)
|
||||||
}
|
}
|
||||||
@@ -11,6 +17,11 @@ export default class ChatPrivate extends Chat {
|
|||||||
return 'priv_' + [userIdA, userIdB].sort().join('__').replaceAll('-', '_')
|
return 'priv_' + [userIdA, userIdB].sort().join('__').replaceAll('-', '_')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为两个用户创建对话 (无需注意顺序)
|
||||||
|
* @param userA
|
||||||
|
* @param userB
|
||||||
|
*/
|
||||||
static createForPrivate(userA: User, userB: User) {
|
static createForPrivate(userA: User, userB: User) {
|
||||||
const chat = this.create(undefined, 'private')
|
const chat = this.create(undefined, 'private')
|
||||||
chat.setAttr('id', this.getChatIdByUsersId(userA.bean.id, userB.bean.id))
|
chat.setAttr('id', this.getChatIdByUsersId(userA.bean.id, userB.bean.id))
|
||||||
@@ -19,11 +30,21 @@ export default class ChatPrivate extends Chat {
|
|||||||
userB.bean.id
|
userB.bean.id
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 寻找两个用户间的对话 (无需注意顺序)
|
||||||
|
* @param userA
|
||||||
|
* @param userB
|
||||||
|
*/
|
||||||
static findByUsersForPrivate(userA: User, userB: User) {
|
static findByUsersForPrivate(userA: User, userB: User) {
|
||||||
const chat = this.findById(this.getChatIdByUsersId(userA.bean.id, userB.bean.id))
|
const chat = this.findById(this.getChatIdByUsersId(userA.bean.id, userB.bean.id))
|
||||||
if (chat)
|
if (chat)
|
||||||
return this.fromChat(chat as Chat)
|
return this.fromChat(chat as Chat)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 寻找两个用户间的对话, 若无则创建 (无需注意顺序)
|
||||||
|
* @param userA
|
||||||
|
* @param userB
|
||||||
|
*/
|
||||||
static findOrCreateForPrivate(userA: User, userB: User) {
|
static findOrCreateForPrivate(userA: User, userB: User) {
|
||||||
let a = this.findByUsersForPrivate(userA, userB)
|
let a = this.findByUsersForPrivate(userA, userB)
|
||||||
if (a == null) {
|
if (a == null) {
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ class File {
|
|||||||
getName() {
|
getName() {
|
||||||
return this.bean.name
|
return this.bean.name
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 获取文件的相对路径
|
||||||
|
*/
|
||||||
getFilePath() {
|
getFilePath() {
|
||||||
const hash = this.bean.hash
|
const hash = this.bean.hash
|
||||||
return path.join(
|
return path.join(
|
||||||
@@ -90,6 +93,12 @@ export default class FileManager {
|
|||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文件 (与 HTTP API 对接)
|
||||||
|
* @param fileName 文件名
|
||||||
|
* @param data 文件二进制数据
|
||||||
|
* @param chatId 所属的对话
|
||||||
|
*/
|
||||||
static async uploadFile(fileName: string, data: Buffer, chatId?: string) {
|
static async uploadFile(fileName: string, data: Buffer, chatId?: string) {
|
||||||
const hash = crypto.createHash('sha256').update(data).digest('hex')
|
const hash = crypto.createHash('sha256').update(data).digest('hex')
|
||||||
const file = FileManager.findByHash(hash)
|
const file = FileManager.findByHash(hash)
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ export default class MessagesManager {
|
|||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为对话获取实例
|
||||||
|
* @param chat 对话
|
||||||
|
*/
|
||||||
static getInstanceForChat(chat: Chat) {
|
static getInstanceForChat(chat: Chat) {
|
||||||
return new MessagesManager(chat)
|
return new MessagesManager(chat)
|
||||||
}
|
}
|
||||||
@@ -36,6 +40,9 @@ export default class MessagesManager {
|
|||||||
protected getTableName() {
|
protected getTableName() {
|
||||||
return `messages_${this.chat.bean.id}`.replaceAll('-', '_')
|
return `messages_${this.chat.bean.id}`.replaceAll('-', '_')
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 添加一条消息
|
||||||
|
*/
|
||||||
addMessage({
|
addMessage({
|
||||||
text,
|
text,
|
||||||
user_id,
|
user_id,
|
||||||
@@ -55,19 +62,58 @@ export default class MessagesManager {
|
|||||||
time || Date.now()
|
time || Date.now()
|
||||||
).lastInsertRowid
|
).lastInsertRowid
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 添加一条无用户信息的系统消息
|
||||||
|
*/
|
||||||
addSystemMessage(text: string) {
|
addSystemMessage(text: string) {
|
||||||
this.addMessage({
|
this.addMessage({
|
||||||
text
|
text
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
getMessagesWithOffset(limit: number | undefined | null, offset: number = 0) {
|
/**
|
||||||
|
* 从最新消息开始偏移某些量向**前**获取 n 条消息 (顺序: 从新到旧)
|
||||||
|
* @param limit 获取消息的数量
|
||||||
|
* @param offset 偏移量
|
||||||
|
*/
|
||||||
|
getMessagesWithOffset(limit: number | undefined | null, offset: number = 0): MessageBean[] {
|
||||||
const ls = MessagesManager.database.prepare(`SELECT * FROM ${this.getTableName()} ORDER BY id DESC LIMIT ? OFFSET ?;`).all(limit || PageFetchMaxLimit, offset) as unknown as MessageBean[]
|
const ls = MessagesManager.database.prepare(`SELECT * FROM ${this.getTableName()} ORDER BY id DESC LIMIT ? OFFSET ?;`).all(limit || PageFetchMaxLimit, offset) as unknown as MessageBean[]
|
||||||
return ls.map((v) => ({
|
return ls.map((v) => ({
|
||||||
...v,
|
...v,
|
||||||
chat_id: this.chat.bean.id,
|
chat_id: this.chat.bean.id,
|
||||||
}))
|
})).reverse()
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 从最新消息开始偏移某些量向**前**获取第 n 页消息 (顺序: 从新到旧)
|
||||||
|
* @param limit 获取消息的数量
|
||||||
|
* @param page 页数
|
||||||
|
*/
|
||||||
getMessagesWithPage(limit: number | undefined | null, page: number = 0) {
|
getMessagesWithPage(limit: number | undefined | null, page: number = 0) {
|
||||||
return this.getMessagesWithOffset(limit, (limit || PageFetchMaxLimit) * page)
|
return this.getMessagesWithOffset(limit, (limit || PageFetchMaxLimit) * page)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 获取最新的消息的 ID
|
||||||
|
*/
|
||||||
|
getNewestMessageId() {
|
||||||
|
return MessagesManager.database.prepare(`SELECT id FROM ${this.getTableName()} ORDER BY id DESC LIMIT 1;`).all()[0].id as number | undefined
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 从某消息开始获取包括其在内往**前**的 n 条消息 (顺序: 从新到旧)
|
||||||
|
* @param limit 获取消息的数量
|
||||||
|
* @param msg_id 从哪条开始? (-1 = 最新)
|
||||||
|
*/
|
||||||
|
getMessagesEndWith(limit: number | undefined | null, msg_id: number) {
|
||||||
|
const newestMessageId = this.getNewestMessageId()
|
||||||
|
const offset = (msg_id == -1 || newestMessageId == null) ? 0 : (newestMessageId - msg_id)
|
||||||
|
return this.getMessagesWithOffset(limit, offset)
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 从某消息开始获取包括其在内往**后**的 n 条消息 (顺序: 从新到旧)
|
||||||
|
* @param limit 获取消息的数量
|
||||||
|
* @param msg_id 从哪条开始? (-1 = 最新)
|
||||||
|
*/
|
||||||
|
getMessagesBeginWith(limit: number | undefined | null, msg_id: number) {
|
||||||
|
const newestMessageId = this.getNewestMessageId()
|
||||||
|
const offset = (msg_id == -1 || newestMessageId == null) ? 0 : (newestMessageId - msg_id)
|
||||||
|
return this.getMessagesWithOffset(limit, offset)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 FileManager from './FileManager.ts'
|
||||||
import { SQLInputValue } from "node:sqlite"
|
import { SQLInputValue } from "node:sqlite"
|
||||||
import ChatPrivate from "./ChatPrivate.ts"
|
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 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
|
type UserBeanKey = keyof UserBean
|
||||||
|
|
||||||
@@ -38,8 +37,6 @@ export default class User {
|
|||||||
/* 用户名 */ username TEXT,
|
/* 用户名 */ username TEXT,
|
||||||
/* 昵称 */ nickname TEXT NOT NULL,
|
/* 昵称 */ nickname TEXT NOT NULL,
|
||||||
/* 头像, 可选 */ avatar_file_hash TEXT,
|
/* 头像, 可选 */ avatar_file_hash TEXT,
|
||||||
/* 对话列表 */ favourite_chats TEXT NOT NULL,
|
|
||||||
/* 最近对话 */ recent_chats TEXT NOT NULL,
|
|
||||||
/* 设置 */ settings TEXT NOT NULL
|
/* 设置 */ settings TEXT NOT NULL
|
||||||
);
|
);
|
||||||
`)
|
`)
|
||||||
@@ -49,6 +46,13 @@ export default class User {
|
|||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户名是否存在, 不存在则创建用户, 否则报错
|
||||||
|
* @param userName
|
||||||
|
* @param password
|
||||||
|
* @param nickName
|
||||||
|
* @param avatar
|
||||||
|
*/
|
||||||
static create(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
|
static create(userName: string | null, password: string, nickName: string, avatar: Buffer | null) {
|
||||||
if (userName && User.findAllBeansByCondition('username = ?', userName).length > 0)
|
if (userName && User.findAllBeansByCondition('username = ?', userName).length > 0)
|
||||||
throw new DataWrongError(`用户名 ${userName} 已存在`)
|
throw new DataWrongError(`用户名 ${userName} 已存在`)
|
||||||
@@ -62,18 +66,14 @@ export default class User {
|
|||||||
username,
|
username,
|
||||||
nickname,
|
nickname,
|
||||||
avatar_file_hash,
|
avatar_file_hash,
|
||||||
favourite_chats,
|
|
||||||
recent_chats,
|
|
||||||
settings
|
settings
|
||||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`).run(
|
) VALUES (?, ?, ?, ?, ?, ?, ?);`).run(
|
||||||
crypto.randomUUID(),
|
crypto.randomUUID(),
|
||||||
password,
|
password,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
userName,
|
userName,
|
||||||
nickName,
|
nickName,
|
||||||
null,
|
null,
|
||||||
'[]',
|
|
||||||
JSON.stringify(new Map(), MapJson.replacer),
|
|
||||||
"{}"
|
"{}"
|
||||||
).lastInsertRowid
|
).lastInsertRowid
|
||||||
)[0]
|
)[0]
|
||||||
@@ -102,6 +102,10 @@ export default class User {
|
|||||||
console.error(chalk.red(`警告: 查询 username = ${userName} 时, 查询到多个相同用户名的用户`))
|
console.error(chalk.red(`警告: 查询 username = ${userName} 时, 查询到多个相同用户名的用户`))
|
||||||
return new User(beans[0])
|
return new User(beans[0])
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 通过用户名或 ID 获取某个用户, 用户名优先
|
||||||
|
* @param account 用户名或用户 ID
|
||||||
|
*/
|
||||||
static findByAccount(account: string) {
|
static findByAccount(account: string) {
|
||||||
return User.findByUserName(account) || User.findById(account)
|
return User.findByUserName(account) || User.findById(account)
|
||||||
}
|
}
|
||||||
@@ -125,37 +129,25 @@ export default class User {
|
|||||||
this.setAttr("username", userName)
|
this.setAttr("username", userName)
|
||||||
}
|
}
|
||||||
updateRecentChat(chatId: string, content: string) {
|
updateRecentChat(chatId: string, content: string) {
|
||||||
const map = JSON.parse(this.bean.recent_chats, MapJson.reviver) as Map<string, string>
|
UserRecentChatLinker.updateOrAddRecentChat(this.bean.id, chatId, content)
|
||||||
map.delete(chatId)
|
|
||||||
map.set(chatId, content)
|
|
||||||
this.setAttr("recent_chats", JSON.stringify(map, MapJson.replacer))
|
|
||||||
}
|
}
|
||||||
getRecentChats(): Map<string, string> {
|
getRecentChats() {
|
||||||
try {
|
return UserRecentChatLinker.getUserRecentChatBeans(this.bean.id)
|
||||||
return JSON.parse(this.bean.recent_chats, MapJson.reviver)
|
}
|
||||||
} catch (e) {
|
|
||||||
console.log(chalk.yellow(`警告: 最近对话列表解析失敗: ${(e as Error).message}`))
|
getFavouriteChats() {
|
||||||
return new Map()
|
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) {
|
addFavouriteChat(chatId: string) {
|
||||||
const ls = this.getFavouriteChats()
|
this.addFavouriteChats([chatId])
|
||||||
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 []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllChatsList() {
|
getAllChatsList() {
|
||||||
return UserChatLinker.getUserChats(this.bean.id)
|
return UserChatLinker.getUserChats(this.bean.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ export default class UserChatLinker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 對用戶和對話建立關聯
|
* 若用户和对话未关联, 则进行关联
|
||||||
* 自動檢測是否已關聯, 保證不會重複
|
|
||||||
*/
|
*/
|
||||||
static linkUserAndChat(userId: string, chatId: string) {
|
static linkUserAndChat(userId: string, chatId: string) {
|
||||||
if (!this.checkUserIsLinkedToChat(userId, chatId))
|
if (!this.checkUserIsLinkedToChat(userId, chatId))
|
||||||
@@ -34,15 +33,27 @@ export default class UserChatLinker {
|
|||||||
chatId
|
chatId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 解除用户和对话的关联
|
||||||
|
*/
|
||||||
static unlinkUserAndChat(userId: string, chatId: string) {
|
static unlinkUserAndChat(userId: string, chatId: string) {
|
||||||
this.database.prepare(`DELETE FROM UserChatLinker WHERE user_id = ? AND chat_id = ?`).run(userId, chatId)
|
this.database.prepare(`DELETE FROM UserChatLinker WHERE user_id = ? AND chat_id = ?`).run(userId, chatId)
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 检测用户和对话的关联
|
||||||
|
*/
|
||||||
static checkUserIsLinkedToChat(userId: string, chatId: string) {
|
static checkUserIsLinkedToChat(userId: string, chatId: string) {
|
||||||
return this.findAllByCondition('user_id = ? AND chat_id = ?', userId, chatId).length != 0
|
return this.findAllByCondition('user_id = ? AND chat_id = ?', userId, chatId).length != 0
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 获取用户所有关联的对话
|
||||||
|
*/
|
||||||
static getUserChats(userId: string) {
|
static getUserChats(userId: string) {
|
||||||
return this.findAllByCondition('user_id = ?', userId).map((v) => v.chat_id) as string[]
|
return this.findAllByCondition('user_id = ?', userId).map((v) => v.chat_id) as string[]
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 获取对话所有关联的用户
|
||||||
|
*/
|
||||||
static getChatMembers(chatId: string) {
|
static getChatMembers(chatId: string) {
|
||||||
return this.findAllByCondition('chat_id = ?', chatId).map((v) => v.user_id) as string[]
|
return this.findAllByCondition('chat_id = ?', chatId).map((v) => v.user_id) as string[]
|
||||||
}
|
}
|
||||||
|
|||||||
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