Compare commits

...

10 Commits

Author SHA1 Message Date
CrescentLeaf
edf35b7dd0 ui: 阻止 附件 點擊導致消息菜單彈出 2025-10-02 21:28:11 +08:00
CrescentLeaf
de886dcfcc ui: 修正 chat-file 為 超鏈接 2025-10-02 21:27:52 +08:00
CrescentLeaf
67f019713a ui: 修正 消息右鍵菜單
* 修正打開狀態
* 避免不必要的狀態變更
2025-10-02 20:54:39 +08:00
CrescentLeaf
2af396a2b8 chore: 修正一個不合理的 uri-list 轉換成功判斷 2025-10-02 20:51:07 +08:00
CrescentLeaf
20f12c97c1 ui: 微調 時間顯示 文字 2025-10-02 20:49:00 +08:00
CrescentLeaf
15c4bcd48e fix: 消息發送失敗時, 沒有取消 加載狀態 2025-10-02 18:49:19 +08:00
CrescentLeaf
e429bbbcdb feat: 瀏覽器下載文件時, 支持設定爲原始文件名
* 基於 Content-Disposition
2025-10-02 18:42:29 +08:00
CrescentLeaf
020fd63c97 feat: 聊天文件 2025-10-02 18:37:25 +08:00
CrescentLeaf
2771503b6f fix: typo 2025-10-02 18:30:58 +08:00
CrescentLeaf
65458cf491 chore: make lint happy 2025-10-02 18:18:29 +08:00
7 changed files with 56 additions and 13 deletions

View File

@@ -8,6 +8,7 @@ import ReactDOM from 'react-dom/client'
import './ui/custom-elements/chat-image.ts'
import './ui/custom-elements/chat-video.ts'
import './ui/custom-elements/chat-file.ts'
const urlParams = new URL(location.href).searchParams

View File

@@ -36,8 +36,8 @@ const markedInstance = new marked.Marked({
if (/uploaded_files\/[A-Za-z0-9]+$/.test(href)) {
return ({
Image: `<chat-image src="${href}" alt="${text}"></chat-image>`,
Video: `<chat-video src="${href}" alt="${text}"></chat-video>`,
File: `<chat-file src="${href}" alt="${text}"></chat-file>`,
Video: `<chat-video src="${href}"></chat-video>`,
File: `<chat-file href="${href}" name="${/^Video|File=(.*)/.exec(text)?.[1] || 'Unnamed file'}"></chat-file>`,
})?.[type] || ``
}
return ``
@@ -144,7 +144,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
target,
data: cachedFiles.current[fileName],
}, 5000)
if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上傳失敗`)) return
if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上傳失敗`)) return setIsMessageSending(false)
text = text.replaceAll('(' + fileName + ')', '(' + re.data!.file_path as string + ')')
}
}
@@ -154,7 +154,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
target,
text,
}, 5000)
if (checkApiSuccessOrSncakbar(re, "發送失敗")) return
if (checkApiSuccessOrSncakbar(re, "發送失敗")) return setIsMessageSending(false)
inputRef.current!.value = ''
cachedFiles.current = {}
} catch (e) {
@@ -280,6 +280,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
'src',
'alt',
'href',
'name',
],
}).replaceAll('\n', '<br>')
const lastDate = date
@@ -299,11 +300,14 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
{
(date.getMinutes() != lastDate.getMinutes() || date.getDate() != lastDate.getDate() || date.getMonth() != lastDate.getMonth() || date.getFullYear() != lastDate.getFullYear())
&& <mdui-tooltip content={`${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`}>
<div>
<div style={{
fontSize: '87%',
marginTop: '10px',
}}>
{
(date.getFullYear() != lastDate.getFullYear() ? `${date.getFullYear()}` : '')
+ (date.getMonth() != lastDate.getMonth() ? `${date.getMonth() + 1}` : '')
+ (date.getDate() != lastDate.getDate() ? `${date.getDate()}` : '')
+ `${date.getMonth() + 1}`
+ `${date.getDate()}`
+ ` ${date.getHours()}:${date.getMinutes()}`
}
</div>
@@ -349,7 +353,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
// 即便是 no-cors 還是殘廢, 因此暫時沒有什麽想法
const re = await fetch(url)
const type = re.headers.get("Content-Type")
if (type?.startsWith("image/"))
if (type && re.ok)
addFile(type as string, getFileNameOrRandom(url), re)
} catch (e) {
snackbar({

View File

@@ -33,6 +33,9 @@ export default function Message({ userId, rawData, renderHTML, message, ...props
const dropDownRef = React.useRef<Dropdown>(null)
const messageJsonDialogRef = React.useRef<Dialog>(null)
useEventListener(messageJsonDialogRef, 'click', (e) => {
e.stopPropagation()
})
const [isDropDownOpen, setDropDownOpen] = React.useState(false)
@@ -116,7 +119,10 @@ export default function Message({ userId, rawData, renderHTML, message, ...props
dangerouslySetInnerHTML={{
__html: renderHTML
}} />
<mdui-menu onClick={(e) => e.stopPropagation()}>
<mdui-menu onClick={(e) => {
e.stopPropagation()
setDropDownOpen(false)
}}>
<mdui-menu-item icon="content_copy" onClick={() => copyToClipboard($(dropDownRef.current as HTMLElement).find('#msg').text())}></mdui-menu-item>
<mdui-menu-item icon="content_copy" onClick={() => copyToClipboard(rawData)}></mdui-menu-item>
<mdui-menu-item icon="info" onClick={() => messageJsonDialogRef.current.open = true}></mdui-menu-item>

View File

@@ -1,7 +1,7 @@
export default function copyToClipboard(text: string) {
if (navigator.clipboard)
return navigator.clipboard.writeText(text)
return new Promise((res rej) => {
return new Promise((res, rej) => {
if (document.hasFocus()) {
const a = document.createElement("textarea")
document.body.appendChild(a)
@@ -12,7 +12,7 @@ export default function copyToClipboard(text: string) {
a.select()
document.execCommand("copy", true)
document.body.removeChild(a)
res()
res(null)
} else {
rej()
}

View File

@@ -0,0 +1,30 @@
import { $ } from 'mdui/jq'
customElements.define('chat-file', class extends HTMLElement {
constructor() {
super()
}
connectedCallback() {
this.style.display = 'block'
const e = new DOMParser().parseFromString(`
<a style="width: 100%;height: 100%;">
<mdui-card variant="outlined" clickable style="display: flex;align-items: center;">
<mdui-icon name="insert_drive_file" style="margin: 13px;font-size: 34px;"></mdui-icon>
<span style="margin-right: 13px;"></span>
</mdui-card>
</a>`, 'text/html').body.firstChild as HTMLElement
$(e).find('span').text($(this).attr("name"))
const href = $(this).attr('href')
$(e).attr('href', href)
$(e).attr('target', '_blank')
$(e).attr('download', href)
e.style.textDecoration = 'none'
e.style.color = 'inherit'
// deno-lint-ignore no-window
e.onclick = (e) => {
e.stopPropagation()
window.open(href, '_blank')
}
this.appendChild(e)
}
})

View File

@@ -6,12 +6,12 @@ customElements.define('chat-video', class extends HTMLElement {
}
connectedCallback() {
this.style.display = 'block'
const e = new DOMParser().parseFromString(`<video controls>視頻無法播放</video>`, 'text/html').body.firstChild as Node
const e = new DOMParser().parseFromString(`<video controls>視頻無法播放</video>`, 'text/html').body.firstChild as HTMLVideoElement
e.style.width = "100%"
e.style.height = "100%"
e.style.borderRadius = "var(--mdui-shape-corner-medium)"
e.alt = $(this).attr('alt') || ""
e.src = $(this).attr('src') as string
e.onclick = (e) => e.stopPropagation()
this.appendChild(e)
}
})

View File

@@ -45,6 +45,8 @@ app.get('/uploaded_files/:hash', (req, res) => {
}
}
const fileName = encodeURIComponent(file!.getName()?.replaceAll('"', ''))
res.setHeader('Content-Disposition', `inline; filename="${fileName}"`)
res.setHeader('Content-Type', file!.getMime())
res.sendFile(path.resolve(file!.getFilePath()))
})