Files
LingChair/client/ui/chat/Message.tsx
CrescentLeaf e90e1911e8 断章取义
2025-11-23 14:16:26 +08:00

221 lines
9.6 KiB
TypeScript

import { Dropdown, Dialog, dialog } from "mdui"
import { $ } from "mdui/jq"
import Client from "../../api/Client.ts"
import Data_Message from "../../api/client_data/Message.ts"
import DataCaches from "../../api/DataCaches.ts"
import Avatar from "../Avatar.tsx"
import copyToClipboard from "../copyToClipboard.ts"
import useAsyncEffect from "../useAsyncEffect.ts"
import useEventListener from "../useEventListener.ts"
import React from "react"
import isMobileUI from "../isMobileUI.ts"
import User from "../../api/client_data/User.ts"
import getUrlForFileByHash from "../../getUrlForFileByHash.ts"
import escapeHTML from "../../escapeHtml.ts"
interface Args extends React.HTMLAttributes<HTMLElement> {
userId: string
rawData: string
renderHTML: string
message: Data_Message
openUserInfoDialog: (user: User | string) => void
}
function prettyFlatParsedMessage(html: string) {
const elements = new DOMParser().parseFromString(html, 'text/html').body.children
// 纯文本直接处理
if (elements.length == 0)
return `<chat-text-container><chat-text>${escapeHTML(html)}</chat-text></chat-text-container>`
let ls: Element[] = []
let ret = ''
// 第一个元素时, 不会被聚合在一起
let lastElementType = ''
const textElementTags = [
'chat-text',
'chat-mention',
]
function checkContinuousElement(tagName: string) {
/* console.log('shangyige ', lastElementType)
console.log("dangqian", tagName)
console.log("上一个元素的类型和当前不一致?", lastElementType != tagName)
console.log("上一个元素的类型和这个元素的类型都属于文本类型", (textElementTags.indexOf(lastElementType) != -1 && textElementTags.indexOf(tagName) != -1)) */
// 如果上一个元素的类型和当前不一致, 或者上一个元素的类型和这个元素的类型都属于文本类型 (亦或者到最后一步时) 执行
if ((lastElementType != tagName || (textElementTags.indexOf(lastElementType) != -1 && textElementTags.indexOf(tagName) != -1)) || tagName == 'LAST_CHICKEN') {
/* console.log(tagName, '进入') */
// 如果上一个元素类型为文本类型, 且当前不是文本类型时, 用文本包裹
if (textElementTags.indexOf(lastElementType) != -1) {
// 当前的文本类型不应该和上一个分离, 滚出去
if (textElementTags.indexOf(tagName) != -1) return
/* console.log(tagName, '文字和被') */
// 由于 chat-mention 不是用内部元素实现的, 因此在这个元素的生成中必须放置占位字符串
// 尽管显示上占位字符串不会显示, 但是在这里依然是会被处理的, 因为本身还是 innerHTML
// 当文本非空时, 将文字合并在一起
if (ls.map((v) => v.innerHTML).join('').trim() != '')
ret += `<chat-text-container>${ls.map((v) => v.outerHTML).join('')}</chat-text-container>`
} else
// 非文本类型元素, 各自成块
ret += ls.map((v) => v.outerHTML).join('')
ls = []
}
}
for (const e of elements) {
// 当出现非文本元素时, 将文本聚合在一起
// 如果是其他类型, 虽然也执行聚合, 但是不会有外层包裹
/* console.log("当前", e, "内容", e.innerHTML) */
checkContinuousElement(e.nodeName.toLowerCase())
ls.push(e)
lastElementType = e.nodeName.toLowerCase()
}
// 最后将剩余的转换
checkContinuousElement('LAST_CHICKEN')
return ret
}
export default function Message({ userId, rawData, renderHTML, message, openUserInfoDialog, ...props }: Args) {
const isAtRight = Client.myUserProfile?.id == userId
const [nickName, setNickName] = React.useState("")
const [avatarUrl, setAvatarUrl] = React.useState<string | undefined>("")
useAsyncEffect(async () => {
const user = await DataCaches.getUserProfile(userId)
setNickName(user.nickname)
setAvatarUrl(getUrlForFileByHash(user?.avatar_file_hash))
}, [userId])
const dropDownRef = React.useRef<Dropdown>(null)
useEventListener(dropDownRef, 'closed', () => {
setDropDownOpen(false)
})
const [isDropDownOpen, setDropDownOpen] = React.useState(false)
/* const [isUsingFullDisplay, setIsUsingFullDisplay] = React.useState(false) */
/* React.useEffect(() => {
const text = $(dropDownRef.current as HTMLElement).find('#msg').text().trim()
setIsUsingFullDisplay(text == '' || (
rawData.split("tws:\/\/file\?hash=").length == 2
&& /\<\/chat\-(file|image|video)\>(\<\/span\>)?$/.test(renderHTML.trim())
))
}, [renderHTML]) */
return (
<div
slot="trigger"
onContextMenu={(e) => {
if (isMobileUI()) return
e.preventDefault()
setDropDownOpen(!isDropDownOpen)
}}
onClick={(e) => {
if (!isMobileUI()) return
e.preventDefault()
setDropDownOpen(!isDropDownOpen)
}}
style={{
width: "100%",
display: "flex",
justifyContent: isAtRight ? "flex-end" : "flex-start",
flexDirection: "column"
}}
{...props}>
<div
style={{
display: "flex",
justifyContent: isAtRight ? "flex-end" : "flex-start",
}}>
{
// 发送者昵称(左)
isAtRight && <span
style={{
alignSelf: "center",
fontSize: "90%"
}}>
{nickName}
</span>
}
{
// 发送者头像
}
<Avatar
src={avatarUrl}
text={nickName}
style={{
width: "43px",
height: "43px",
margin: "11px"
}}
onClick={(e) => {
e.stopPropagation()
openUserInfoDialog(userId)
}} />
{
// 发送者昵称(右)
!isAtRight && <span
style={{
alignSelf: "center",
fontSize: "90%"
}}>
{nickName}
</span>
}
</div>
<mdui-card
variant="elevated"
style={{
maxWidth: 'var(--whitesilk-widget-message-maxwidth)', // (window.matchMedia('(pointer: fine)') && "50%") || (window.matchMedia('(pointer: coarse)') && "77%"),
minWidth: "0%",
[isAtRight ? "marginRight" : "marginLeft"]: "55px",
marginTop: "-5px",
alignSelf: isAtRight ? "flex-end" : "flex-start",
// boxShadow: isUsingFullDisplay ? 'inherit' : 'var(--mdui-elevation-level1)',
// padding: isUsingFullDisplay ? undefined : "13px",
// paddingTop: isUsingFullDisplay ? undefined : "14px",
// backgroundColor: isUsingFullDisplay ? "inherit" : undefined
}}>
<mdui-dropdown trigger="manual" ref={dropDownRef} open={isDropDownOpen}>
<span
slot="trigger"
id="msg"
style={{
fontSize: "94%",
wordBreak: 'break-word',
display: 'flex',
flexDirection: 'column',
}}
dangerouslySetInnerHTML={{
__html: prettyFlatParsedMessage(renderHTML)
}} />
<mdui-menu onClick={(e) => {
e.stopPropagation()
setDropDownOpen(false)
}}>
<mdui-menu-item icon="content_copy" onClick={() => copyToClipboard($(dropDownRef.current as HTMLElement).find('#msg').text().trim())}></mdui-menu-item>
<mdui-menu-item icon="content_copy" onClick={() => copyToClipboard(rawData)}></mdui-menu-item>
<mdui-menu-item icon="info" onClick={() => dialog({
headline: "原始数据",
body: `<span style="word-break: break-word;">${Object.keys(message)
// @ts-ignore 懒
.map((k) => `${k} = ${message[k]}`)
.join('<br><br>')}<span>`,
closeOnEsc: true,
closeOnOverlayClick: true,
actions: [
{
text: "关闭",
onClick: () => {
return true
},
}
]
}).addEventListener('click', (e) => e.stopPropagation())}></mdui-menu-item>
</mdui-menu>
</mdui-dropdown>
</mdui-card>
</div>
)
}