Compare commits
5 Commits
d00dfab898
...
e1039703d1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1039703d1 | ||
|
|
ace3f8c4f9 | ||
|
|
30c09d0613 | ||
|
|
dec9068cc8 | ||
|
|
19cfd84e7d |
@@ -1,7 +1,6 @@
|
|||||||
import 'mdui/mdui.css'
|
import 'mdui/mdui.css'
|
||||||
import 'mdui'
|
import 'mdui'
|
||||||
import { $ } from "mdui/jq"
|
import { breakpoint } from "mdui"
|
||||||
import { breakpoint, Dialog } from "mdui"
|
|
||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
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-video.ts'
|
||||||
import './ui/custom-elements/chat-file.ts'
|
import './ui/custom-elements/chat-file.ts'
|
||||||
import './ui/custom-elements/chat-text.ts'
|
import './ui/custom-elements/chat-text.ts'
|
||||||
|
import './ui/custom-elements/chat-mention.ts'
|
||||||
import './ui/custom-elements/chat-text-container.ts'
|
import './ui/custom-elements/chat-text-container.ts'
|
||||||
|
|
||||||
import App from './ui/App.tsx'
|
import App from './ui/App.tsx'
|
||||||
@@ -17,6 +17,16 @@ import AppMobile from './ui/AppMobile.tsx'
|
|||||||
import isMobileUI from "./ui/isMobileUI.ts"
|
import isMobileUI from "./ui/isMobileUI.ts"
|
||||||
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(React.createElement(isMobileUI() ? AppMobile : App, null))
|
ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(React.createElement(isMobileUI() ? AppMobile : App, null))
|
||||||
|
|
||||||
|
import User from "./api/client_data/User.ts"
|
||||||
|
import Chat from "./api/client_data/Chat.ts"
|
||||||
|
// TODO: 无奈之举 以后会找更好的办法
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
openUserInfoDialog: (user: User | string) => Promise<void>
|
||||||
|
openChatInfoDialog: (chat: Chat) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onResize = () => {
|
const onResize = () => {
|
||||||
document.body.style.setProperty('--whitesilk-widget-message-maxwidth', breakpoint().down('md') ? "80%" : "70%")
|
document.body.style.setProperty('--whitesilk-widget-message-maxwidth', breakpoint().down('md') ? "80%" : "70%")
|
||||||
// deno-lint-ignore no-window
|
// deno-lint-ignore no-window
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openUserInfoDialog(user: User | string) {
|
async function openUserInfoDialog(user: User | string) {
|
||||||
if (user instanceof User) {
|
if (typeof user == 'object') {
|
||||||
setUserInfo(user)
|
setUserInfo(user)
|
||||||
} else {
|
} else {
|
||||||
setUserInfo(await DataCaches.getUserProfile(user))
|
setUserInfo(await DataCaches.getUserProfile(user))
|
||||||
@@ -120,6 +120,10 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
userProfileDialogRef.current!.open = true
|
userProfileDialogRef.current!.open = true
|
||||||
}
|
}
|
||||||
|
// deno-lint-ignore no-window
|
||||||
|
window.openUserInfoDialog = openUserInfoDialog
|
||||||
|
// deno-lint-ignore no-window
|
||||||
|
window.openChatInfoDialog = openChatInfoDialog
|
||||||
|
|
||||||
if ('Notification' in window) {
|
if ('Notification' in window) {
|
||||||
Notification.requestPermission()
|
Notification.requestPermission()
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export default function AppMobile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function openUserInfoDialog(user: User | string) {
|
async function openUserInfoDialog(user: User | string) {
|
||||||
if (user instanceof User) {
|
if (typeof user == 'object') {
|
||||||
setUserInfo(user)
|
setUserInfo(user)
|
||||||
} else {
|
} else {
|
||||||
setUserInfo(await DataCaches.getUserProfile(user))
|
setUserInfo(await DataCaches.getUserProfile(user))
|
||||||
@@ -120,6 +120,10 @@ export default function AppMobile() {
|
|||||||
}
|
}
|
||||||
userProfileDialogRef.current!.open = true
|
userProfileDialogRef.current!.open = true
|
||||||
}
|
}
|
||||||
|
// deno-lint-ignore no-window
|
||||||
|
window.openUserInfoDialog = openUserInfoDialog
|
||||||
|
// deno-lint-ignore no-window
|
||||||
|
window.openChatInfoDialog = openChatInfoDialog
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
|||||||
@@ -46,15 +46,17 @@ const sanitizeConfig = {
|
|||||||
"chat-file",
|
"chat-file",
|
||||||
'chat-text',
|
'chat-text',
|
||||||
"chat-link",
|
"chat-link",
|
||||||
|
'chat-mention',
|
||||||
],
|
],
|
||||||
ALLOWED_ATTR: [
|
ALLOWED_ATTR: [
|
||||||
'underline',
|
'underline',
|
||||||
'em',
|
'em',
|
||||||
|
|
||||||
'src',
|
'src',
|
||||||
'alt',
|
'alt',
|
||||||
'href',
|
'href',
|
||||||
'name',
|
'name',
|
||||||
|
'user-id',
|
||||||
|
'chat-id',
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,16 +73,23 @@ const markedInstance = new marked.Marked({
|
|||||||
return `<chat-text>${escapeHTML(text)}</chat-text>`
|
return `<chat-text>${escapeHTML(text)}</chat-text>`
|
||||||
},
|
},
|
||||||
image({ text, href }) {
|
image({ text, href }) {
|
||||||
const type = /^(Video|File)=.*/.exec(text)?.[1] || 'Image'
|
const type = /^(Video|File|UserMention|ChatMention)=.*/.exec(text)?.[1]
|
||||||
if (/tws:\/\/file\?hash=[A-Za-z0-9]+$/.test(href)) {
|
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])
|
const url = getUrlForFileByHash(/^tws:\/\/file\?hash=(.*)/.exec(href)?.[1])
|
||||||
return ({
|
return ({
|
||||||
Image: `<chat-image src="${url}" alt="${escapeHTML(text)}"></chat-image>`,
|
Image: `<chat-image src="${url}" alt="${escapeHTML(text)}"></chat-image>`,
|
||||||
Video: `<chat-video src="${url}"></chat-video>`,
|
Video: `<chat-video src="${url}"></chat-video>`,
|
||||||
File: `<chat-file href="${url}" name="${escapeHTML(/^Video|File=(.*)/.exec(text)?.[1] || 'Unnamed file')}"></chat-file>`,
|
File: `<chat-file href="${url}" name="${escapeHTML(/^Video|File=(.*)/.exec(text)?.[1] || 'Unnamed file')}"></chat-file>`,
|
||||||
})?.[type] || ``
|
})?.[fileType] || ``
|
||||||
}
|
} else
|
||||||
return ``
|
switch (type) {
|
||||||
|
case "UserMention":
|
||||||
|
return `<chat-mention user-id="${escapeHTML(/^tws:\/\/user\?id=(.*)/.exec(href)?.[1] || '')}" text="${escapeHTML(/^UserMention=(.*)/.exec(text)?.[1] || '')}"></chat-mention>`
|
||||||
|
case "ChatMention":
|
||||||
|
return `<chat-mention chat-id="${escapeHTML(/^tws:\/\/chat\?id=(.*)/.exec(href)?.[1] || '')}" text="${escapeHTML(/^ChatMention=(.*)/.exec(text)?.[1] || '')}"></chat-mention>`
|
||||||
|
}
|
||||||
|
return `<chat-text em="true">(不支持的附件语法: )</chat-text>`
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -191,26 +200,28 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
let i = 1
|
let i = 1
|
||||||
let i2 = 0
|
let i2 = 0
|
||||||
const sendingFilesSnackbarId = setInterval(() => {
|
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++
|
i++
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
function endSendingSnack() {
|
||||||
|
clearTimeout(sendingFilesSnackbarId)
|
||||||
|
sendingFilesSnackbar.open = false
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let text = inputRef.current!.value
|
let text = inputRef.current!.value
|
||||||
if (text.trim() == '') return
|
if (text.trim() == '') return
|
||||||
setIsMessageSending(true)
|
setIsMessageSending(true)
|
||||||
for (const fileName of Object.keys(cachedFiles.current)) {
|
for (const fileName of Object.keys(cachedFiles.current)) {
|
||||||
if (text.indexOf(fileName) != -1) {
|
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(
|
const re = await Client.uploadFileLikeApi(
|
||||||
fileName,
|
fileName,
|
||||||
cachedFiles.current[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 + ')')
|
text = text.replaceAll('(' + fileName + ')', '(tws://file?hash=' + re.data!.file_hash as string + ')')
|
||||||
i2++
|
i2++
|
||||||
}
|
}
|
||||||
@@ -221,7 +232,10 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
target,
|
target,
|
||||||
text,
|
text,
|
||||||
}, 5000)
|
}, 5000)
|
||||||
if (checkApiSuccessOrSncakbar(re, "发送失败")) return setIsMessageSending(false)
|
if (checkApiSuccessOrSncakbar(re, "发送失败")) {
|
||||||
|
endSendingSnack()
|
||||||
|
return setIsMessageSending(false)
|
||||||
|
}
|
||||||
inputRef.current!.value = ''
|
inputRef.current!.value = ''
|
||||||
cachedFiles.current = {}
|
cachedFiles.current = {}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -231,8 +245,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
setIsMessageSending(false)
|
setIsMessageSending(false)
|
||||||
clearTimeout(sendingFilesSnackbarId)
|
endSendingSnack()
|
||||||
sendingFilesSnackbar.open = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachFileInputRef = React.useRef<HTMLInputElement>(null)
|
const attachFileInputRef = React.useRef<HTMLInputElement>(null)
|
||||||
@@ -376,6 +389,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
if (!chatInfo.is_member) return
|
if (!chatInfo.is_member) return
|
||||||
const scrollTop = (e.target as HTMLDivElement).scrollTop
|
const scrollTop = (e.target as HTMLDivElement).scrollTop
|
||||||
if (scrollTop == 0 && !showLoadingMoreMessagesTip) {
|
if (scrollTop == 0 && !showLoadingMoreMessagesTip) {
|
||||||
|
setShowNoMoreMessagesTip(false)
|
||||||
setShowLoadingMoreMessagesTip(true)
|
setShowLoadingMoreMessagesTip(true)
|
||||||
await loadMore()
|
await loadMore()
|
||||||
setShowLoadingMoreMessagesTip(false)
|
setShowLoadingMoreMessagesTip(false)
|
||||||
@@ -413,13 +427,21 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
(() => {
|
(() => {
|
||||||
let date = new Date(0)
|
let date = new Date(0)
|
||||||
return messagesList.map((msg) => {
|
return messagesList.map((msg) => {
|
||||||
const rendeText = DOMPurify.sanitize(markedInstance.parse(msg.text) as string, sanitizeConfig)
|
|
||||||
const lastDate = date
|
const lastDate = date
|
||||||
date = new Date(msg.time)
|
date = new Date(msg.time)
|
||||||
|
|
||||||
const msgElement = msg.user_id == null ? <SystemMessage>{msg.text}</SystemMessage> : <Element_Message
|
const msgElement = msg.user_id == null ? <SystemMessage><div dangerouslySetInnerHTML={{
|
||||||
|
__html: DOMPurify.sanitize(markedInstance.parse(msg.text) as string, {
|
||||||
|
ALLOWED_ATTR: [
|
||||||
|
...sanitizeConfig.ALLOWED_ATTR,
|
||||||
|
],
|
||||||
|
ALLOWED_TAGS: [
|
||||||
|
...sanitizeConfig.ALLOWED_TAGS,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}} /></SystemMessage> : <Element_Message
|
||||||
rawData={msg.text}
|
rawData={msg.text}
|
||||||
renderHTML={rendeText}
|
renderHTML={DOMPurify.sanitize(markedInstance.parse(msg.text) as string, sanitizeConfig)}
|
||||||
message={msg}
|
message={msg}
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
|
|||||||
@@ -27,11 +27,17 @@ function prettyFlatParsedMessage(html: string) {
|
|||||||
let ret = ''
|
let ret = ''
|
||||||
// 第一个元素时, 不会被聚合在一起
|
// 第一个元素时, 不会被聚合在一起
|
||||||
let lastElementType = ''
|
let lastElementType = ''
|
||||||
|
const textElementTags = [
|
||||||
|
'chat-text',
|
||||||
|
'chat-mention',
|
||||||
|
]
|
||||||
function checkContinuousElement(tagName: string) {
|
function checkContinuousElement(tagName: string) {
|
||||||
if (lastElementType != tagName) {
|
if (lastElementType != tagName) {
|
||||||
if (lastElementType == 'chat-text')
|
console.log(lastElementType, ls.map((v) => v.innerHTML))
|
||||||
ret += `<chat-text-container>${ls.map((v) => v.outerHTML).join('')}</chat-text-container>`
|
if (textElementTags.indexOf(lastElementType) != -1) {
|
||||||
else
|
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('')
|
ret += ls.map((v) => v.outerHTML).join('')
|
||||||
ls = []
|
ls = []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -71,10 +71,9 @@ ApiManager.initEvents()
|
|||||||
ApiManager.initAllApis()
|
ApiManager.initAllApis()
|
||||||
|
|
||||||
httpServer.listen(config.server.listen)
|
httpServer.listen(config.server.listen)
|
||||||
console.log(chalk.green("API & Web 服务已启动"))
|
console.log(chalk.green(`API & Web 服务已启动, 端口为 ${config.server.listen.port}`))
|
||||||
function help() {
|
function help() {
|
||||||
console.log(chalk.yellow("===== LingChair Server ====="))
|
console.log(chalk.yellow("输入 b 或者执行 deno task build 以编译前端"))
|
||||||
console.log(chalk.yellow("b - 重新编译前端"))
|
|
||||||
}
|
}
|
||||||
help()
|
help()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user