diff --git a/client/index.ts b/client/index.ts
index b385836..35ffd15 100644
--- a/client/index.ts
+++ b/client/index.ts
@@ -1,7 +1,6 @@
import 'mdui/mdui.css'
import 'mdui'
-import { $ } from "mdui/jq"
-import { breakpoint, Dialog } from "mdui"
+import { breakpoint } from "mdui"
import * as React from 'react'
import ReactDOM from 'react-dom/client'
@@ -10,6 +9,7 @@ import './ui/custom-elements/chat-image.ts'
import './ui/custom-elements/chat-video.ts'
import './ui/custom-elements/chat-file.ts'
import './ui/custom-elements/chat-text.ts'
+import './ui/custom-elements/chat-mention.ts'
import './ui/custom-elements/chat-text-container.ts'
import App from './ui/App.tsx'
diff --git a/client/ui/chat/ChatFragment.tsx b/client/ui/chat/ChatFragment.tsx
index f40fb6e..9f17fa1 100644
--- a/client/ui/chat/ChatFragment.tsx
+++ b/client/ui/chat/ChatFragment.tsx
@@ -46,15 +46,17 @@ const sanitizeConfig = {
"chat-file",
'chat-text',
"chat-link",
+ 'chat-mention',
],
ALLOWED_ATTR: [
'underline',
'em',
-
'src',
'alt',
'href',
'name',
+ 'user-id',
+ 'chat-id',
],
}
@@ -71,16 +73,23 @@ const markedInstance = new marked.Marked({
return `${escapeHTML(text)}`
},
image({ text, href }) {
- const type = /^(Video|File)=.*/.exec(text)?.[1] || 'Image'
- if (/tws:\/\/file\?hash=[A-Za-z0-9]+$/.test(href)) {
+ const type = /^(Video|File|UserMention|ChatMention)=.*/.exec(text)?.[1]
+ const fileType = /^(Video|File)=.*/.exec(text)?.[1] || 'Image'
+ if (fileType != null && /tws:\/\/file\?hash=[A-Za-z0-9]+$/.test(href)) {
const url = getUrlForFileByHash(/^tws:\/\/file\?hash=(.*)/.exec(href)?.[1])
return ({
Image: ``,
Video: ``,
File: ``,
- })?.[type] || ``
- }
- return ``
+ })?.[fileType] || ``
+ } else
+ switch (type) {
+ case "UserMention":
+ return ``
+ case "ChatMention":
+ return ``
+ }
+ return `(不支持的附件语法: )`
},
}
})
@@ -191,26 +200,28 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
let i = 1
let i2 = 0
const sendingFilesSnackbarId = setInterval(() => {
- sendingFilesSnackbar.textContent = `上传第 ${i2}/${Object.keys(cachedFiles.current).length} 文件到 [${chatInfo.title}]... (${i}s)`
+ const len = Object.keys(cachedFiles.current).length
+ sendingFilesSnackbar.textContent = i2 == len ? `发送消息到 [${chatInfo.title}]... (${i}s)` : `上传第 ${i2}/${len} 文件到 [${chatInfo.title}]... (${i}s)`
i++
}, 1000)
+ function endSendingSnack() {
+ clearTimeout(sendingFilesSnackbarId)
+ sendingFilesSnackbar.open = false
+ }
try {
let text = inputRef.current!.value
if (text.trim() == '') return
setIsMessageSending(true)
for (const fileName of Object.keys(cachedFiles.current)) {
if (text.indexOf(fileName) != -1) {
- /* const re = await Client.invoke("Chat.uploadFile", {
- token: data.access_token,
- file_name: fileName,
- target,
- data: cachedFiles.current[fileName],
- }, 5000) */
const re = await Client.uploadFileLikeApi(
fileName,
cachedFiles.current[fileName]
)
- if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上传失败`)) return setIsMessageSending(false)
+ if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上传失败`)) {
+ endSendingSnack()
+ return setIsMessageSending(false)
+ }
text = text.replaceAll('(' + fileName + ')', '(tws://file?hash=' + re.data!.file_hash as string + ')')
i2++
}
@@ -221,7 +232,10 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
target,
text,
}, 5000)
- if (checkApiSuccessOrSncakbar(re, "发送失败")) return setIsMessageSending(false)
+ if (checkApiSuccessOrSncakbar(re, "发送失败")) {
+ endSendingSnack()
+ return setIsMessageSending(false)
+ }
inputRef.current!.value = ''
cachedFiles.current = {}
} catch (e) {
@@ -231,8 +245,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
})
}
setIsMessageSending(false)
- clearTimeout(sendingFilesSnackbarId)
- sendingFilesSnackbar.open = false
+ endSendingSnack()
}
const attachFileInputRef = React.useRef(null)
@@ -376,6 +389,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
if (!chatInfo.is_member) return
const scrollTop = (e.target as HTMLDivElement).scrollTop
if (scrollTop == 0 && !showLoadingMoreMessagesTip) {
+ setShowNoMoreMessagesTip(false)
setShowLoadingMoreMessagesTip(true)
await loadMore()
setShowLoadingMoreMessagesTip(false)
@@ -413,13 +427,21 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
(() => {
let date = new Date(0)
return messagesList.map((msg) => {
- const rendeText = DOMPurify.sanitize(markedInstance.parse(msg.text) as string, sanitizeConfig)
const lastDate = date
date = new Date(msg.time)
- const msgElement = msg.user_id == null ? {msg.text} : : v.innerHTML))
diff --git a/client/ui/custom-elements/chat-mention.ts b/client/ui/custom-elements/chat-mention.ts
index e69de29..97c6865 100644
--- a/client/ui/custom-elements/chat-mention.ts
+++ b/client/ui/custom-elements/chat-mention.ts
@@ -0,0 +1,62 @@
+import { $ } from 'mdui'
+import DataCaches from "../../api/DataCaches.ts"
+import { snackbar } from "../snackbar.ts"
+
+customElements.define('chat-mention', class extends HTMLElement {
+ declare span: HTMLSpanElement
+ static observedAttributes = ['user-id']
+ constructor() {
+ super()
+
+ this.attachShadow({ mode: 'open' })
+ }
+ connectedCallback() {
+ const shadow = this.shadowRoot as ShadowRoot
+
+ this.span = document.createElement('span')
+ this.span.style.whiteSpace = 'pre-wrap'
+ this.span.style.fontSynthesis = 'style weight'
+ this.span.style.color = 'rgb(var(--mdui-color-primary))'
+ shadow.appendChild(this.span)
+
+ this.update()
+ }
+ attributeChangedCallback(_name: string, _oldValue: unknown, _newValue: unknown) {
+ this.update()
+ }
+ async update() {
+ if (this.span == null) return
+
+ const userId = $(this).attr('user-id')
+ const chatId = $(this).attr('chat-id')
+ const text = $(this).attr('text')
+ this.span.style.fontStyle = ''
+ if (chatId) {
+ const chat = await DataCaches.getChatInfo(chatId)
+ this.span.textContent = chat?.title
+ this.span.onclick = () => {
+ // deno-lint-ignore no-window
+ window.openChatInfoDialog(chat)
+ }
+ } else if (userId) {
+ const user = await DataCaches.getUserProfile(userId)
+ this.span.textContent = user?.nickname
+ this.span.onclick = () => {
+ // deno-lint-ignore no-window
+ window.openUserInfoDialog(user)
+ }
+ }
+
+ text && (this.span.textContent = text)
+ if (!(userId || chatId)) {
+ this.span.textContent = "无效的提及"
+ this.span.style.fontStyle = 'italic'
+ this.span.onclick = () => {
+ snackbar({
+ message: "该提及没有指定用户或者对话!",
+ placement: 'top',
+ })
+ }
+ }
+ }
+})