From da505305a30044189d97f34667c3793ae595f198 Mon Sep 17 00:00:00 2001 From: CrescentLeaf Date: Fri, 23 Jan 2026 22:32:28 +0800 Subject: [PATCH] chore(client-protocol): export some class & types --- client-protocol/ChatAttachment.ts | 90 +++++++++++++++ client-protocol/Message.ts | 109 +++--------------- client-protocol/main.ts | 17 ++- .../type/ChatParserTransformers.ts | 5 + client/ui/chat-fragment/ChatMessage.tsx | 42 +++---- 5 files changed, 147 insertions(+), 116 deletions(-) create mode 100644 client-protocol/ChatAttachment.ts create mode 100644 client-protocol/type/ChatParserTransformers.ts diff --git a/client-protocol/ChatAttachment.ts b/client-protocol/ChatAttachment.ts new file mode 100644 index 0000000..c895723 --- /dev/null +++ b/client-protocol/ChatAttachment.ts @@ -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 + } +} \ No newline at end of file diff --git a/client-protocol/Message.ts b/client-protocol/Message.ts index 2012f24..32122ea 100644 --- a/client-protocol/Message.ts +++ b/client-protocol/Message.ts @@ -3,8 +3,7 @@ import MessageBean from "./bean/MessageBean.ts" import LingChairClient from "./LingChairClient.ts" import Chat from "./Chat.ts" import User from "./User.ts" -import CallbackError from "./CallbackError.ts" -import ApiCallbackMessage from "./ApiCallbackMessage.ts" +import ChatAttachment from "./ChatAttachment.ts" import * as marked from 'marked' @@ -37,93 +36,18 @@ export class ChatMention extends BaseClientObject { } } -type FileType = 'Video' | 'Image' | 'File' -type MentionType = 'ChatMention' | 'UserMention' +type ChatMentionType = 'ChatMention' | 'UserMention' +type ChatFileType = 'Video' | 'Image' | 'File' -export 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 - } +type ChatParserTransformers = { + attachment?: ({ text, fileType, attachment }: { text: string, fileType: ChatFileType, attachment: ChatAttachment }) => string, + mention?: ({ text, mentionType, mention }: { text: string, mentionType: ChatMentionType, mention: ChatMention }) => string, +} + +export type { + ChatMentionType, + ChatFileType, + ChatParserTransformers, } export default class Message extends BaseClientObject { @@ -152,10 +76,7 @@ export default class Message extends BaseClientObject { parseWithTransformers({ attachment, mention, - }: { - attachment?: ({ text, fileType, attachment }: { text: string, fileType: FileType, attachment: ChatAttachment }) => string, - mention?: ({ text, mentionType, mention }: { text: string, mentionType: MentionType, mention: ChatMention }) => string, - }) { + }: ChatParserTransformers) { return new marked.Marked({ async: false, extensions: [ @@ -178,8 +99,8 @@ export default class Message extends BaseClientObject { { name: 'image', renderer: ({ text, href }) => { - const mentionType = /^(UserMention|ChatMention)=.*/.exec(text)?.[1] as MentionType - const fileType = (/^(Video|File|Image)=.*/.exec(text)?.[1] || 'Image') as FileType + const mentionType = /^(UserMention|ChatMention)=.*/.exec(text)?.[1] as ChatMentionType + const fileType = (/^(Video|File|Image)=.*/.exec(text)?.[1] || 'Image') as ChatFileType if (fileType != null && /tws:\/\/file\?hash=[A-Za-z0-9]+$/.test(href)) { const file_hash = /^tws:\/\/file\?hash=(.*)/.exec(href)?.[1]! diff --git a/client-protocol/main.ts b/client-protocol/main.ts index 3bc5867..8a2c63e 100644 --- a/client-protocol/main.ts +++ b/client-protocol/main.ts @@ -7,10 +7,16 @@ import GroupSettingsBean from "./bean/GroupSettingsBean.ts" import JoinRequestBean from "./bean/JoinRequestBean.ts" import MessageBean from "./bean/MessageBean.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 CallbackError from "./CallbackError.ts" +import ChatAttachment from "./ChatAttachment.ts" export { LingChairClient, @@ -19,6 +25,7 @@ export { Chat, User, UserMySelf, + Message, ChatAttachment, ChatMention, @@ -29,4 +36,10 @@ export { RecentChatBean, JoinRequestBean, } -export type { GroupSettingsBean } +export type { + ChatParserTransformers, + ChatMentionType, + ChatFileType, + + GroupSettingsBean, +} diff --git a/client-protocol/type/ChatParserTransformers.ts b/client-protocol/type/ChatParserTransformers.ts new file mode 100644 index 0000000..23dbc6f --- /dev/null +++ b/client-protocol/type/ChatParserTransformers.ts @@ -0,0 +1,5 @@ +import ChatAttachment from '../ChatAttachment.ts' +import { ChatMention } from '../Message.ts' +import ChatFileType from './ChatFileType.ts' + + diff --git a/client/ui/chat-fragment/ChatMessage.tsx b/client/ui/chat-fragment/ChatMessage.tsx index c4d9104..bd10316 100644 --- a/client/ui/chat-fragment/ChatMessage.tsx +++ b/client/ui/chat-fragment/ChatMessage.tsx @@ -1,4 +1,4 @@ -import { Message } from "lingchair-client-protocol" +import { ChatParserTransformers, Message } from "lingchair-client-protocol" import isMobileUI from "../../utils/isMobileUI" import useAsyncEffect from "../../utils/useAsyncEffect" import ClientCache from "../../ClientCache" @@ -94,6 +94,25 @@ const sanitizeConfig = { ], } +const transformers: ChatParserTransformers = { + attachment({ fileType, attachment }) { + const url = getClient().getUrlForFileByHash(attachment.getFileHash()) + return ({ + Image: ``, + Video: ``, + File: ``, + })?.[fileType] + }, + mention({ mentionType, mention }) { + switch (mentionType) { + case "UserMention": + return `[对话提及]` + case "ChatMention": + return `[对话提及]` + } + }, +} + 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) @@ -122,24 +141,7 @@ export default function ChatMessage({ message, noUserDisplay, avatarMenuItems, m const messageInnerRef = React.useRef(null) React.useEffect(() => { - messageInnerRef.current!.innerHTML = prettyFlatParsedMessage(DOMPurify.sanitize(message.parseWithTransformers({ - attachment({ fileType, attachment }) { - const url = getClient().getUrlForFileByHash(attachment.getFileHash()) - return ({ - Image: ``, - Video: ``, - File: ``, - })?.[fileType] - }, - mention({ mentionType, mention }) { - switch (mentionType) { - case "UserMention": - return `[对话提及]` - case "ChatMention": - return `[对话提及]` - } - }, - }), sanitizeConfig)) + messageInnerRef.current!.innerHTML = prettyFlatParsedMessage(DOMPurify.sanitize(message.parseWithTransformers(transformers), sanitizeConfig)) // 没有办法的办法 (笑) // 姐姐, 谁让您不是 React 组件呢 @@ -170,7 +172,7 @@ export default function ChatMessage({ message, noUserDisplay, avatarMenuItems, m flexDirection: "column" }}> { -