feat: 支持發送文件
* 目前還只能拖拽到輸入框
This commit is contained in:
@@ -15,7 +15,9 @@ export type CallMethod =
|
|||||||
|
|
||||||
"Chat.getInfo" |
|
"Chat.getInfo" |
|
||||||
"Chat.sendMessage" |
|
"Chat.sendMessage" |
|
||||||
"Chat.getMessageHistory"
|
"Chat.getMessageHistory" |
|
||||||
|
|
||||||
|
"Chat.uploadFile"
|
||||||
|
|
||||||
export type ClientEvent =
|
export type ClientEvent =
|
||||||
"Client.onMessage"
|
"Client.onMessage"
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import Client from "../../api/Client.ts"
|
|||||||
import Message from "../../api/client_data/Message.ts"
|
import Message from "../../api/client_data/Message.ts"
|
||||||
import Chat from "../../api/client_data/Chat.ts"
|
import Chat from "../../api/client_data/Chat.ts"
|
||||||
import data from "../../Data.ts"
|
import data from "../../Data.ts"
|
||||||
import { checkApiSuccessOrSncakbar } from "../snackbar.ts"
|
import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts"
|
||||||
import useAsyncEffect from "../useAsyncEffect.ts"
|
import useAsyncEffect from "../useAsyncEffect.ts"
|
||||||
import * as marked from 'marked'
|
import * as marked from 'marked'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
|
import randomUUID from "../../randomUUID.ts"
|
||||||
|
|
||||||
interface Args extends React.HTMLAttributes<HTMLElement> {
|
interface Args extends React.HTMLAttributes<HTMLElement> {
|
||||||
target: string
|
target: string
|
||||||
@@ -25,12 +26,14 @@ const markedInstance = new marked.Marked({
|
|||||||
const text = this.parser.parseInline(tokens)
|
const text = this.parser.parseInline(tokens)
|
||||||
return `<span>${text}</span>`
|
return `<span>${text}</span>`
|
||||||
},
|
},
|
||||||
paragraph({ tokens, depth: _depth }) {
|
paragraph({ tokens }) {
|
||||||
const text = this.parser.parseInline(tokens)
|
const text = this.parser.parseInline(tokens)
|
||||||
return `<span>${text}</span>`
|
return `<span>${text}</span>`
|
||||||
},
|
},
|
||||||
image({ title, href }) {
|
image({ text, href }) {
|
||||||
return `<chat-image src="${href}"></chat-image>`
|
if (/uploaded_files\/[A-Za-z0-9]+$/.test(href))
|
||||||
|
return `<chat-image src="${href}" alt="${text}"></chat-image>`
|
||||||
|
return ``
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -85,12 +88,6 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
}
|
}
|
||||||
setMessagesList(returnMsgs.concat(messagesList))
|
setMessagesList(returnMsgs.concat(messagesList))
|
||||||
|
|
||||||
if (page.current == 0)
|
|
||||||
setTimeout(() => chatPanelRef.current!.scrollTo({
|
|
||||||
top: 10000000000,
|
|
||||||
behavior: "smooth",
|
|
||||||
}), 100)
|
|
||||||
|
|
||||||
page.current++
|
page.current++
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,8 +116,21 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
const [showLoadingMoreMessagesTip, setShowLoadingMoreMessagesTip] = React.useState(false)
|
const [showLoadingMoreMessagesTip, setShowLoadingMoreMessagesTip] = React.useState(false)
|
||||||
const [showNoMoreMessagesTip, setShowNoMoreMessagesTip] = React.useState(false)
|
const [showNoMoreMessagesTip, setShowNoMoreMessagesTip] = React.useState(false)
|
||||||
|
|
||||||
|
const cachedFiles = React.useRef({} as { [fileName: string]: ArrayBuffer })
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const text = inputRef.current!.value
|
let text = inputRef.current!.value
|
||||||
|
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)
|
||||||
|
if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上傳失敗`)) return
|
||||||
|
text = text.replaceAll(fileName, re.data!.file_path as string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const re = await Client.invoke("Chat.sendMessage", {
|
const re = await Client.invoke("Chat.sendMessage", {
|
||||||
token: data.access_token,
|
token: data.access_token,
|
||||||
@@ -129,6 +139,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
}, 5000)
|
}, 5000)
|
||||||
if (checkApiSuccessOrSncakbar(re, "發送失敗")) return
|
if (checkApiSuccessOrSncakbar(re, "發送失敗")) return
|
||||||
inputRef.current!.value = ''
|
inputRef.current!.value = ''
|
||||||
|
cachedFiles.current = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -203,14 +214,14 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
<Element_Message
|
<Element_Message
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
userId={msg.user_id}>
|
userId={msg.user_id}>
|
||||||
<div dangerouslySetInnerHTML={{
|
<div dangerouslySetInnerHTML={{
|
||||||
__html: DOMPurify.sanitize(markedInstance.parse(msg.text) as string, {
|
__html: DOMPurify.sanitize(markedInstance.parse(msg.text) as string, {
|
||||||
ALLOWED_TAGS: [
|
ALLOWED_TAGS: [
|
||||||
"chat-image",
|
"chat-image",
|
||||||
"span",
|
"span",
|
||||||
"chat-link",
|
"chat-link",
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
}}></div>
|
}}></div>
|
||||||
</Element_Message>
|
</Element_Message>
|
||||||
)
|
)
|
||||||
@@ -230,18 +241,55 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
paddingRight: '4px',
|
paddingRight: '4px',
|
||||||
backgroundColor: 'rgb(var(--mdui-color-surface))',
|
backgroundColor: 'rgb(var(--mdui-color-surface))',
|
||||||
}} onDrop={(e) => {
|
}} onDrop={(e) => {
|
||||||
if (e.dataTransfer.files) {
|
const input = inputRef.current!.shadowRoot!.querySelector('[part=input]') as HTMLTextAreaElement
|
||||||
const files = e.dataTransfer.files
|
function insertText(text: string) {
|
||||||
|
inputRef.current!.value = input.value!.substring(0, input.selectionStart as number) + text + input.value!.substring(input.selectionEnd as number, input.value.length)
|
||||||
|
}
|
||||||
|
async function addFile(type: string, name: string, data: Blob | Response) {
|
||||||
|
cachedFiles.current![name] = await data.arrayBuffer()
|
||||||
|
if (type.startsWith('image/'))
|
||||||
|
insertText(``)
|
||||||
|
else
|
||||||
|
insertText(``)
|
||||||
|
}
|
||||||
|
function getFileNameOrRandom(urlString: string) {
|
||||||
|
const url = new URL(urlString)
|
||||||
|
let filename = url.pathname.substring(url.pathname.lastIndexOf('/') + 1).trim()
|
||||||
|
if (filename == '')
|
||||||
|
filename = 'file_' + randomUUID()
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
if (e.dataTransfer.items.length > 0) {
|
||||||
// 基于当前的实现, 浏览器不会读取文件的字节流来确定其媒体类型, 其根据文件扩展名进行假设
|
// 基于当前的实现, 浏览器不会读取文件的字节流来确定其媒体类型, 其根据文件扩展名进行假设
|
||||||
// https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/type
|
// https://developer.mozilla.org/zh-CN/docs/Web/API/Blob/type
|
||||||
for (const file of files) {
|
for (const item of e.dataTransfer.items) {
|
||||||
if (file.type.startsWith("image/")) {
|
if (item.type == 'text/uri-list') {
|
||||||
|
item.getAsString(async (url) => {
|
||||||
|
try {
|
||||||
|
// 即便是 no-cors 還是殘廢, 因此暫時沒有什麽想法
|
||||||
|
const re = await fetch(url)
|
||||||
|
const type = re.headers.get("Content-Type")
|
||||||
|
if (type?.startsWith("image/"))
|
||||||
|
addFile(type as string, getFileNameOrRandom(url), re)
|
||||||
|
} catch (e) {
|
||||||
|
snackbar({
|
||||||
|
message: '無法解析連結: ' + (e as Error).message,
|
||||||
|
placement: 'top',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (item.kind == 'file') {
|
||||||
|
e.preventDefault()
|
||||||
|
const file = item.getAsFile() as File
|
||||||
|
addFile(item.type, file.name, file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<mdui-text-field variant="outlined" placeholder="喵呜~" autosize ref={inputRef as any} max-rows={6} onKeyDown={(event) => {
|
<mdui-text-field variant="outlined" placeholder="喵呜~" autosize ref={inputRef as any} max-rows={6} onChange={() => {
|
||||||
|
if (inputRef.current?.value.trim() == '')
|
||||||
|
cachedFiles.current = {}
|
||||||
|
}} onKeyDown={(event) => {
|
||||||
if (event.ctrlKey && event.key == 'Enter')
|
if (event.ctrlKey && event.key == 'Enter')
|
||||||
sendMessage()
|
sendMessage()
|
||||||
}} style={{
|
}} style={{
|
||||||
|
|||||||
@@ -182,13 +182,13 @@ export default class ChatApi extends BaseApi {
|
|||||||
msg: "用戶無權訪問該對話",
|
msg: "用戶無權訪問該對話",
|
||||||
}
|
}
|
||||||
|
|
||||||
const file = await FileManager.uploadFile(args.file_name as string, args.data as Buffer<ArrayBufferLike>)
|
const file = await FileManager.uploadFile(args.file_name as string, args.data as Buffer<ArrayBufferLike>, args.target as string)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: 200,
|
code: 200,
|
||||||
msg: "成功",
|
msg: "成功",
|
||||||
data: {
|
data: {
|
||||||
messages: MessagesManager.getInstanceForChat(chat).getMessagesWithPage(15, args.page as number),
|
file_path: 'uploaded_files/' + file.getHash()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user