Compare commits
21 Commits
faec599822
...
4bcc6e4347
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bcc6e4347 | ||
|
|
9395104c20 | ||
|
|
f063c4d165 | ||
|
|
b3b077fa9d | ||
|
|
88123e1edb | ||
|
|
0106311a2a | ||
|
|
f5f2d5743f | ||
|
|
4e38ad8e20 | ||
|
|
41362a591c | ||
|
|
1b36a45252 | ||
|
|
38db2e1310 | ||
|
|
9a3e87d89c | ||
|
|
954b5d3430 | ||
|
|
6dfe59c5a8 | ||
|
|
b741cbf9ba | ||
|
|
d5fbc490ea | ||
|
|
276ce5cae8 | ||
|
|
3a9312654e | ||
|
|
0a10009613 | ||
|
|
8759b660f5 | ||
|
|
ae3b9c8226 |
@@ -15,7 +15,15 @@ export type CallMethod =
|
|||||||
|
|
||||||
"Chat.getInfo" |
|
"Chat.getInfo" |
|
||||||
"Chat.sendMessage" |
|
"Chat.sendMessage" |
|
||||||
"Chat.getMessageHistory"
|
"Chat.getMessageHistory" |
|
||||||
|
|
||||||
|
"Chat.uploadFile"
|
||||||
|
|
||||||
|
export const CallableMethodBeforeAuth = [
|
||||||
|
"User.auth",
|
||||||
|
"User.register",
|
||||||
|
"User.login",
|
||||||
|
]
|
||||||
|
|
||||||
export type ClientEvent =
|
export type ClientEvent =
|
||||||
"Client.onMessage"
|
"Client.onMessage"
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
import { io, Socket } from 'socket.io-client'
|
import { io, Socket } from 'socket.io-client'
|
||||||
import { CallMethod, ClientEvent } from './ApiDeclare.ts'
|
import { CallMethod, ClientEvent, CallableMethodBeforeAuth } from './ApiDeclare.ts'
|
||||||
import ApiCallbackMessage from './ApiCallbackMessage.ts'
|
import ApiCallbackMessage from './ApiCallbackMessage.ts'
|
||||||
import User from "./client_data/User.ts"
|
import User from "./client_data/User.ts"
|
||||||
import data from "../Data.ts"
|
import data from "../Data.ts"
|
||||||
import { checkApiSuccessOrSncakbar } from "../ui/snackbar.ts"
|
import { checkApiSuccessOrSncakbar } from "../ui/snackbar.ts"
|
||||||
|
import randomUUID from "../randomUUID.ts"
|
||||||
|
|
||||||
type UnknownObject = { [key: string]: unknown }
|
type UnknownObject = { [key: string]: unknown }
|
||||||
|
|
||||||
class Client {
|
class Client {
|
||||||
|
static sessionId = randomUUID()
|
||||||
static myUserProfile?: User
|
static myUserProfile?: User
|
||||||
static socket?: Socket
|
static socket?: Socket
|
||||||
static events: { [key: string]: (data: UnknownObject) => UnknownObject | void } = {}
|
static events: { [key: string]: (data: UnknownObject) => UnknownObject | void } = {}
|
||||||
|
static connected = false
|
||||||
static connect() {
|
static connect() {
|
||||||
if (data.device_id == null)
|
if (data.device_id == null)
|
||||||
data.device_id = crypto.randomUUID()
|
data.device_id = randomUUID()
|
||||||
this.socket?.disconnect()
|
this.socket?.disconnect()
|
||||||
this.socket && delete this.socket
|
this.socket && delete this.socket
|
||||||
this.socket = io({
|
this.socket = io({
|
||||||
transports: ['websocket'],
|
transports: ['websocket'],
|
||||||
auth: {
|
auth: {
|
||||||
device_id: data.device_id
|
device_id: data.device_id,
|
||||||
|
session_id: this.sessionId,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
this.socket!.on("connect", async () => {
|
this.socket!.on("connect", async () => {
|
||||||
@@ -28,6 +32,10 @@ class Client {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
if (re.code != 200)
|
if (re.code != 200)
|
||||||
checkApiSuccessOrSncakbar(re, "重連失敗")
|
checkApiSuccessOrSncakbar(re, "重連失敗")
|
||||||
|
this.connected = true
|
||||||
|
})
|
||||||
|
this.socket!.on("disconnect", () => {
|
||||||
|
this.connected = false
|
||||||
})
|
})
|
||||||
this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => {
|
this.socket!.on("The_White_Silk", (name: string, data: UnknownObject, callback: (ret: UnknownObject) => void) => {
|
||||||
try {
|
try {
|
||||||
@@ -40,8 +48,7 @@ class Client {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
static invoke(method: CallMethod, args: UnknownObject = {}, timeout: number = 5000): Promise<ApiCallbackMessage> {
|
static invoke(method: CallMethod, args: UnknownObject = {}, timeout: number = 5000): Promise<ApiCallbackMessage> {
|
||||||
if (this.socket == null) {
|
if (this.socket == null || (!this.connected && !CallableMethodBeforeAuth.includes(method))) {
|
||||||
console.warn("客戶端未初始化, 等待初始化后再請求......")
|
|
||||||
return new Promise((reslove) => {
|
return new Promise((reslove) => {
|
||||||
setTimeout(async () => reslove(await this.invoke(method, args, timeout)), 500)
|
setTimeout(async () => reslove(await this.invoke(method, args, timeout)), 500)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"crypto-js": "npm:crypto-js@4.2.0",
|
"crypto-js": "npm:crypto-js@4.2.0",
|
||||||
"socket.io-client": "npm:socket.io-client@4.8.1",
|
"socket.io-client": "npm:socket.io-client@4.8.1",
|
||||||
"marked": "npm:marked@16.3.0",
|
"marked": "npm:marked@16.3.0",
|
||||||
"dompurify": "npm:dompurify@3.2.7"
|
"dompurify": "npm:dompurify@3.2.7",
|
||||||
|
"pinch-zoom-element": "npm:pinch-zoom-element@1.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
<title>TheWhiteSilk</title>
|
<title>TheWhiteSilk</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="./style.css" />
|
<link rel="stylesheet" href="./style.css" />
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.slim.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/gh/GoogleChromeLabs/pinch-zoom@1.1.1/dist/pinch-zoom-min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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'
|
||||||
|
|
||||||
import './ui/custom-elements/chat-image.js'
|
import './ui/custom-elements/chat-image.ts'
|
||||||
|
|
||||||
const urlParams = new URL(location.href).searchParams
|
const urlParams = new URL(location.href).searchParams
|
||||||
|
|
||||||
|
|||||||
34
client/randomUUID.ts
Normal file
34
client/randomUUID.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// https://www.xiabingbao.com/post/crypto/js-crypto-randomuuid-qxcuqj.html
|
||||||
|
// 在此表示感謝
|
||||||
|
|
||||||
|
export default function randomUUID() {
|
||||||
|
// crypto - 只支持在安全的上下文使用
|
||||||
|
if (typeof crypto === 'object') {
|
||||||
|
if (typeof crypto.randomUUID === 'function') {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID
|
||||||
|
return crypto.randomUUID()
|
||||||
|
}
|
||||||
|
if (typeof crypto.getRandomValues === 'function' && typeof Uint8Array === 'function') {
|
||||||
|
// https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid
|
||||||
|
const callback = (c: string) => {
|
||||||
|
const num = Number(c)
|
||||||
|
return (num ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (num / 4)))).toString(16)
|
||||||
|
};
|
||||||
|
return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 隨機數 - fallback
|
||||||
|
let timestamp = new Date().getTime()
|
||||||
|
let perforNow = (typeof performance !== 'undefined' && performance.now && performance.now() * 1000) || 0
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
let random = Math.random() * 16
|
||||||
|
if (timestamp > 0) {
|
||||||
|
random = (timestamp + random) % 16 | 0
|
||||||
|
timestamp = Math.floor(timestamp / 16)
|
||||||
|
} else {
|
||||||
|
random = (perforNow + random) % 16 | 0
|
||||||
|
perforNow = Math.floor(perforNow / 16)
|
||||||
|
}
|
||||||
|
return (c === 'x' ? random : (random & 0x3) | 0x8).toString(16)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import Avatar from "./Avatar.tsx"
|
|||||||
|
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { Dialog, NavigationBar, TextField } from "mdui"
|
import { Dialog, NavigationBar, TextField } from "mdui"
|
||||||
import Split from 'split.js'
|
|
||||||
import 'mdui/jsx.zh-cn.d.ts'
|
import 'mdui/jsx.zh-cn.d.ts'
|
||||||
import { checkApiSuccessOrSncakbar } from "./snackbar.ts"
|
import { checkApiSuccessOrSncakbar } from "./snackbar.ts"
|
||||||
|
|
||||||
@@ -18,8 +17,8 @@ import UserProfileDialog from "./dialog/UserProfileDialog.tsx"
|
|||||||
import ContactsList from "./main/ContactsList.tsx"
|
import ContactsList from "./main/ContactsList.tsx"
|
||||||
import RecentsList from "./main/RecentsList.tsx"
|
import RecentsList from "./main/RecentsList.tsx"
|
||||||
import useAsyncEffect from "./useAsyncEffect.ts"
|
import useAsyncEffect from "./useAsyncEffect.ts"
|
||||||
import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx";
|
import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx"
|
||||||
import Chat from "../api/client_data/Chat.ts";
|
import Chat from "../api/client_data/Chat.ts"
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
namespace React {
|
namespace React {
|
||||||
@@ -52,6 +51,10 @@ export default function AppMobile() {
|
|||||||
const registerInputPasswordRef = React.useRef<TextField>(null)
|
const registerInputPasswordRef = React.useRef<TextField>(null)
|
||||||
|
|
||||||
const userProfileDialogRef = React.useRef<Dialog>(null)
|
const userProfileDialogRef = React.useRef<Dialog>(null)
|
||||||
|
const openMyUserProfileDialogButtonRef = React.useRef<HTMLElement>(null)
|
||||||
|
useEventListener(openMyUserProfileDialogButtonRef, 'click', (_event) => {
|
||||||
|
userProfileDialogRef.current!.open = true
|
||||||
|
})
|
||||||
|
|
||||||
const chatInfoDialogRef = React.useRef<Dialog>(null)
|
const chatInfoDialogRef = React.useRef<Dialog>(null)
|
||||||
const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat)
|
const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat)
|
||||||
@@ -64,13 +67,13 @@ export default function AppMobile() {
|
|||||||
|
|
||||||
const chatFragmentDialogRef = React.useRef<Dialog>(null)
|
const chatFragmentDialogRef = React.useRef<Dialog>(null)
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const shadow = chatFragmentDialogRef.current!.shadowRoot
|
const shadow = chatFragmentDialogRef.current!.shadowRoot as ShadowRoot
|
||||||
const panel = shadow.querySelector(".panel")
|
const panel = shadow.querySelector(".panel") as HTMLElement
|
||||||
panel.style.padding = '0'
|
panel.style.padding = '0'
|
||||||
panel.style.color = 'inherit'
|
panel.style.color = 'inherit'
|
||||||
panel.style.backgroundColor = 'rgb(var(--mdui-color-background))'
|
panel.style.backgroundColor = 'rgb(var(--mdui-color-background))'
|
||||||
panel.style.setProperty('--mdui-color-background', 'inherit')
|
panel.style.setProperty('--mdui-color-background', 'inherit')
|
||||||
const body = shadow.querySelector(".body")
|
const body = shadow.querySelector(".body") as HTMLElement
|
||||||
body.style.height = '100%'
|
body.style.height = '100%'
|
||||||
body.style.display = 'flex'
|
body.style.display = 'flex'
|
||||||
})
|
})
|
||||||
@@ -91,6 +94,7 @@ export default function AppMobile() {
|
|||||||
<div style={{
|
<div style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
|
flexDirection: 'column',
|
||||||
width: 'var(--whitesilk-window-width)',
|
width: 'var(--whitesilk-window-width)',
|
||||||
height: 'var(--whitesilk-window-height)',
|
height: 'var(--whitesilk-window-height)',
|
||||||
}}>
|
}}>
|
||||||
@@ -100,7 +104,7 @@ export default function AppMobile() {
|
|||||||
}
|
}
|
||||||
<div id="ChatFragment" style={{
|
<div id="ChatFragment" style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
heght: '100%',
|
height: '100%',
|
||||||
}}>
|
}}>
|
||||||
<ChatFragment
|
<ChatFragment
|
||||||
showReturnButton={true}
|
showReturnButton={true}
|
||||||
@@ -135,16 +139,33 @@ export default function AppMobile() {
|
|||||||
}}
|
}}
|
||||||
chat={chatInfo} />
|
chat={chatInfo} />
|
||||||
|
|
||||||
<mdui-navigation-bar scroll-target="#SideBar" label-visibility="selected" value="Recents" ref={navigationBarRef}>
|
<mdui-top-app-bar style={{
|
||||||
<mdui-navigation-bar-item icon="watch_later--outlined" active-icon="watch_later--filled" value="Recents">最近</mdui-navigation-bar-item>
|
position: 'sticky',
|
||||||
<mdui-navigation-bar-item icon="contacts--outlined" active-icon="contacts--filled" value="Contacts">聯絡人</mdui-navigation-bar-item>
|
marginTop: '3px',
|
||||||
</mdui-navigation-bar>
|
marginRight: '6px',
|
||||||
|
marginLeft: '15px',
|
||||||
|
top: '0px',
|
||||||
|
}}>
|
||||||
|
<mdui-top-app-bar-title>{
|
||||||
|
({
|
||||||
|
Recents: "最近對話",
|
||||||
|
Contacts: "聯絡人"
|
||||||
|
})[navigationItemSelected]
|
||||||
|
}</mdui-top-app-bar-title>
|
||||||
|
<div style={{
|
||||||
|
flexGrow: 1,
|
||||||
|
}}></div>
|
||||||
|
<mdui-button-icon icon="settings"></mdui-button-icon>
|
||||||
|
<mdui-button-icon>
|
||||||
|
<Avatar src={myUserProfileCache?.avatar} text={myUserProfileCache?.nickname} avatarRef={openMyUserProfileDialogButtonRef} />
|
||||||
|
</mdui-button-icon>
|
||||||
|
</mdui-top-app-bar>
|
||||||
{
|
{
|
||||||
// 侧边列表
|
// 侧边列表
|
||||||
}
|
}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
height: 'calc(100% - 80px)',
|
height: 'calc(100% - 80px - 67px)',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}} id="SideBar">
|
}} id="SideBar">
|
||||||
{
|
{
|
||||||
@@ -167,6 +188,13 @@ export default function AppMobile() {
|
|||||||
display={navigationItemSelected == "Contacts"} />
|
display={navigationItemSelected == "Contacts"} />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<mdui-navigation-bar label-visibility="selected" value="Recents" ref={navigationBarRef} style={{
|
||||||
|
position: 'sticky',
|
||||||
|
bottom: '0',
|
||||||
|
}}>
|
||||||
|
<mdui-navigation-bar-item icon="watch_later--outlined" active-icon="watch_later--filled" value="Recents">最近</mdui-navigation-bar-item>
|
||||||
|
<mdui-navigation-bar-item icon="contacts--outlined" active-icon="contacts--filled" value="Contacts">聯絡人</mdui-navigation-bar-item>
|
||||||
|
</mdui-navigation-bar>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Tab, TextField } from "mdui"
|
import { Tab, TextField } from "mdui"
|
||||||
|
import { $ } from "mdui/jq"
|
||||||
import useEventListener from "../useEventListener.ts"
|
import useEventListener from "../useEventListener.ts"
|
||||||
import Element_Message from "./Message.tsx"
|
import Element_Message from "./Message.tsx"
|
||||||
import MessageContainer from "./MessageContainer.tsx"
|
import MessageContainer from "./MessageContainer.tsx"
|
||||||
@@ -8,10 +9,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 +27,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 ``
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -41,11 +45,11 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
title: '加載中...'
|
title: '加載中...'
|
||||||
} as Chat)
|
} as Chat)
|
||||||
|
|
||||||
const [tabItemSelected, setTabItemSelected] = React.useState('Chat')
|
const [tabItemSelected, setTabItemSelected] = React.useState('None')
|
||||||
const tabRef = React.useRef<Tab>(null)
|
const tabRef = React.useRef<Tab>(null)
|
||||||
const chatPanelRef = React.useRef<HTMLElement>(null)
|
const chatPanelRef = React.useRef<HTMLElement>(null)
|
||||||
useEventListener(tabRef, 'change', () => {
|
useEventListener(tabRef, 'change', () => {
|
||||||
setTabItemSelected(tabRef.current?.value || "Chat")
|
tabRef.current != null && setTabItemSelected(tabRef.current!.value as string)
|
||||||
})
|
})
|
||||||
|
|
||||||
useAsyncEffect(async () => {
|
useAsyncEffect(async () => {
|
||||||
@@ -54,10 +58,18 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
target: target,
|
target: target,
|
||||||
})
|
})
|
||||||
if (re.code != 200)
|
if (re.code != 200)
|
||||||
return checkApiSuccessOrSncakbar(re, "對話錯誤")
|
return target != '' && checkApiSuccessOrSncakbar(re, "對話錯誤")
|
||||||
setChatInfo(re.data as Chat)
|
setChatInfo(re.data as Chat)
|
||||||
|
|
||||||
loadMore()
|
await loadMore()
|
||||||
|
|
||||||
|
setTabItemSelected("Chat")
|
||||||
|
setTimeout(() => {
|
||||||
|
chatPanelRef.current!.scrollTo({
|
||||||
|
top: 10000000000,
|
||||||
|
behavior: "smooth",
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
}, [target])
|
}, [target])
|
||||||
|
|
||||||
const page = React.useRef(0)
|
const page = React.useRef(0)
|
||||||
@@ -75,13 +87,10 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
setTimeout(() => setShowNoMoreMessagesTip(false), 1000)
|
setTimeout(() => setShowNoMoreMessagesTip(false), 1000)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setMessagesList(returnMsgs.concat(messagesList))
|
|
||||||
|
|
||||||
if (page.current == 0)
|
const oldest = messagesList[0]
|
||||||
setTimeout(() => chatPanelRef.current!.scrollTo({
|
setMessagesList(returnMsgs.concat(messagesList))
|
||||||
top: 10000000000,
|
setTimeout(() => chatPanelRef.current!.scrollTo({ top: $(`#chat_${target}_message_${oldest.id}`).get(0).offsetTop, behavior: 'smooth' }), 100)
|
||||||
behavior: "smooth",
|
|
||||||
}), 100)
|
|
||||||
|
|
||||||
page.current++
|
page.current++
|
||||||
}
|
}
|
||||||
@@ -95,7 +104,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
const { chat, msg } = (data as OnMessageData)
|
const { chat, msg } = (data as OnMessageData)
|
||||||
if (target == chat) {
|
if (target == chat) {
|
||||||
setMessagesList(messagesList.concat([msg]))
|
setMessagesList(messagesList.concat([msg]))
|
||||||
if ((chatPanelRef.current!.scrollHeight - chatPanelRef.current!.scrollTop - chatPanelRef.current!.clientHeight) < 80)
|
if ((chatPanelRef.current!.scrollHeight - chatPanelRef.current!.scrollTop - chatPanelRef.current!.clientHeight) < 130)
|
||||||
setTimeout(() => chatPanelRef.current!.scrollTo({
|
setTimeout(() => chatPanelRef.current!.scrollTo({
|
||||||
top: 10000000000,
|
top: 10000000000,
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
@@ -111,18 +120,74 @@ 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)
|
||||||
|
|
||||||
async function sendMessage() {
|
const [isMessageSending, setIsMessageSending] = React.useState(false)
|
||||||
const text = inputRef.current!.value
|
|
||||||
|
|
||||||
const re = await Client.invoke("Chat.sendMessage", {
|
const cachedFiles = React.useRef({} as { [fileName: string]: ArrayBuffer })
|
||||||
token: data.access_token,
|
const cachedFileNamesCount = React.useRef({} as { [fileName: string]: number })
|
||||||
target,
|
async function sendMessage() {
|
||||||
text,
|
try {
|
||||||
}, 5000)
|
let text = inputRef.current!.value
|
||||||
if (checkApiSuccessOrSncakbar(re, "發送失敗")) return
|
if (text.trim() == '') return
|
||||||
inputRef.current!.value = ''
|
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)
|
||||||
|
if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上傳失敗`)) return
|
||||||
|
text = text.replaceAll('(' + fileName + ')', '(' + re.data!.file_path as string + ')')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const re = await Client.invoke("Chat.sendMessage", {
|
||||||
|
token: data.access_token,
|
||||||
|
target,
|
||||||
|
text,
|
||||||
|
}, 5000)
|
||||||
|
if (checkApiSuccessOrSncakbar(re, "發送失敗")) return
|
||||||
|
inputRef.current!.value = ''
|
||||||
|
cachedFiles.current = {}
|
||||||
|
} catch (e) {
|
||||||
|
snackbar({
|
||||||
|
message: '發送失敗: ' + (e as Error).message,
|
||||||
|
placement: 'top',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setIsMessageSending(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attachFileInputRef = React.useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
function insertText(text: string) {
|
||||||
|
const input = inputRef.current!.shadowRoot!.querySelector('[part=input]') as HTMLTextAreaElement
|
||||||
|
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) {
|
||||||
|
let name = name_
|
||||||
|
while (cachedFiles.current[name] != null) {
|
||||||
|
name = name_ + '_' + cachedFileNamesCount.current[name]
|
||||||
|
cachedFileNamesCount.current[name]++
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedFiles.current[name] = await data.arrayBuffer()
|
||||||
|
cachedFileNamesCount.current[name] = 1
|
||||||
|
if (type.startsWith('image/'))
|
||||||
|
insertText(``)
|
||||||
|
else
|
||||||
|
insertText(``)
|
||||||
|
}
|
||||||
|
useEventListener(attachFileInputRef, 'change', (_e) => {
|
||||||
|
const files = attachFileInputRef.current!.files as unknown as File[]
|
||||||
|
if (files?.length == 0) return
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
addFile(file.type, file.name, file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -131,7 +196,7 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
}} {...props}>
|
}} {...props}>
|
||||||
<mdui-tabs ref={tabRef} value="Chat" style={{
|
<mdui-tabs ref={tabRef} value={tabItemSelected} style={{
|
||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
@@ -141,13 +206,14 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
showReturnButton && <mdui-button-icon icon="arrow_back" onClick={onReturnButtonClicked} style={{
|
showReturnButton && <mdui-button-icon icon="arrow_back" onClick={onReturnButtonClicked} style={{
|
||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
marginLeft: '5px',
|
marginLeft: '5px',
|
||||||
marginRight: '15px',
|
marginRight: '5px',
|
||||||
}}></mdui-button-icon>
|
}}></mdui-button-icon>
|
||||||
}
|
}
|
||||||
<mdui-tab value="Chat">{
|
<mdui-tab value="Chat">{
|
||||||
chatInfo.title
|
chatInfo.title
|
||||||
}</mdui-tab>
|
}</mdui-tab>
|
||||||
<mdui-tab value="Settings">設定</mdui-tab>
|
<mdui-tab value="Settings">設定</mdui-tab>
|
||||||
|
<mdui-tab value="None" style={{ display: 'none' }}></mdui-tab>
|
||||||
|
|
||||||
<mdui-tab-panel slot="panel" value="Chat" ref={chatPanelRef} style={{
|
<mdui-tab-panel slot="panel" value="Chat" ref={chatPanelRef} style={{
|
||||||
display: tabItemSelected == "Chat" ? "flex" : "none",
|
display: tabItemSelected == "Chat" ? "flex" : "none",
|
||||||
@@ -193,15 +259,16 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
messagesList.map((msg) =>
|
messagesList.map((msg) =>
|
||||||
<Element_Message
|
<Element_Message
|
||||||
key={msg.id}
|
key={msg.id}
|
||||||
|
id={`chat_${target}_message_${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>
|
||||||
)
|
)
|
||||||
@@ -221,31 +288,72 @@ 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) {
|
function getFileNameOrRandom(urlString: string) {
|
||||||
const files = e.dataTransfer.files
|
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()
|
||||||
|
}} onPaste={(event) => {
|
||||||
|
for (const item of event.clipboardData.items) {
|
||||||
|
if (item.kind == 'file') {
|
||||||
|
event.preventDefault()
|
||||||
|
const file = item.getAsFile() as File
|
||||||
|
addFile(item.type, file.name, file)
|
||||||
|
}
|
||||||
|
}
|
||||||
}} style={{
|
}} style={{
|
||||||
marginRight: '10px',
|
marginRight: '10px',
|
||||||
marginTop: '3px',
|
marginTop: '3px',
|
||||||
marginBottom: '3px',
|
marginBottom: '3px',
|
||||||
}}></mdui-text-field>
|
}}></mdui-text-field>
|
||||||
<mdui-button-icon slot="end-icon" icon="more_vert" style={{
|
<mdui-button-icon slot="end-icon" icon="attach_file" style={{
|
||||||
marginRight: '6px',
|
marginRight: '6px',
|
||||||
|
}} onClick={() => {
|
||||||
|
attachFileInputRef.current!.click()
|
||||||
}}></mdui-button-icon>
|
}}></mdui-button-icon>
|
||||||
<mdui-button-icon icon="send" style={{
|
<mdui-button-icon icon="send" style={{
|
||||||
marginRight: '7px',
|
marginRight: '7px',
|
||||||
}} onClick={() => sendMessage()}></mdui-button-icon>
|
}} onClick={() => sendMessage()} loading={isMessageSending}></mdui-button-icon>
|
||||||
|
<div style={{
|
||||||
|
display: 'none'
|
||||||
|
}}>
|
||||||
|
<input accept="*/*" type="file" name="選擇附加文檔" multiple ref={attachFileInputRef}></input>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mdui-tab-panel>
|
</mdui-tab-panel>
|
||||||
<mdui-tab-panel slot="panel" value="Settings" style={{
|
<mdui-tab-panel slot="panel" value="Settings" style={{
|
||||||
@@ -255,6 +363,8 @@ export default function ChatFragment({ target, showReturnButton, onReturnButtonC
|
|||||||
}}>
|
}}>
|
||||||
Work in progress...
|
Work in progress...
|
||||||
</mdui-tab-panel>
|
</mdui-tab-panel>
|
||||||
|
<mdui-tab-panel slot="panel" value="None">
|
||||||
|
</mdui-tab-panel>
|
||||||
</mdui-tabs>
|
</mdui-tabs>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,16 +1,22 @@
|
|||||||
function openImageViewer(src) {
|
import { $ } from 'mdui/jq'
|
||||||
|
|
||||||
|
import 'pinch-zoom-element'
|
||||||
|
import { snackbar } from "../snackbar.ts";
|
||||||
|
|
||||||
|
function openImageViewer(src: string) {
|
||||||
$('#image-viewer-dialog-inner').empty()
|
$('#image-viewer-dialog-inner').empty()
|
||||||
|
|
||||||
const e = new Image()
|
const e = new Image()
|
||||||
|
e.onload = () => ($('#image-viewer-dialog-inner').get(0) as any).scaleTo(0.1, {
|
||||||
|
// Transform origin. Can be a number, or string percent, eg "50%"
|
||||||
|
originX: '50%',
|
||||||
|
originY: '50%',
|
||||||
|
// Should the transform origin be relative to the container, or content?
|
||||||
|
relativeTo: 'container',
|
||||||
|
})
|
||||||
e.src = src
|
e.src = src
|
||||||
$('#image-viewer-dialog-inner').append(e)
|
$('#image-viewer-dialog-inner').append(e)
|
||||||
|
$('#image-viewer-dialog').attr('open', 'true')
|
||||||
e.onload = () => $('#image-viewer-dialog-inner').get(0).setTransform({
|
|
||||||
scale: 0.6,
|
|
||||||
x: $(window).width() / 2 - (e.width / 4),
|
|
||||||
y: $(window).height() / 2 - (e.height / 3),
|
|
||||||
})
|
|
||||||
$('#image-viewer-dialog').get(0).open = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('chat-image', class extends HTMLElement {
|
customElements.define('chat-image', class extends HTMLElement {
|
||||||
@@ -21,36 +27,24 @@ customElements.define('chat-image', class extends HTMLElement {
|
|||||||
const e = new Image()
|
const e = new Image()
|
||||||
e.style.maxWidth = "100%"
|
e.style.maxWidth = "100%"
|
||||||
e.style.maxHeight = "90%"
|
e.style.maxHeight = "90%"
|
||||||
e.style.marginTop = "13px"
|
e.style.marginTop = '5px'
|
||||||
|
e.style.marginBottom = '5px'
|
||||||
e.style.borderRadius = "var(--mdui-shape-corner-medium)"
|
e.style.borderRadius = "var(--mdui-shape-corner-medium)"
|
||||||
e.src = $(this).attr('src')
|
e.alt = $(this).attr('alt') || ""
|
||||||
e.alt = $(this).attr('alt')
|
|
||||||
e.onerror = () => {
|
e.onerror = () => {
|
||||||
const bak = $(this).html()
|
const src = $(this).attr('src')
|
||||||
$(this).html(`<br/><mdui-icon name="broken_image" style="font-size: 2rem;"></mdui-icon>`)
|
$(this).html(`<mdui-icon name="broken_image" style="font-size: 2rem;"></mdui-icon>`)
|
||||||
$(this).attr('alt', '無法加載圖像')
|
$(this).attr('alt', '無法加載圖像')
|
||||||
$(this).on('click', () => dialog({
|
$(this).on('click', () => {
|
||||||
headline: "圖片無法載入",
|
snackbar({
|
||||||
description: "您是否需要重新加載?",
|
message: `圖片 (${src}) 無法加載!`,
|
||||||
actions: [
|
placement: 'top'
|
||||||
{
|
})
|
||||||
text: "重載",
|
})
|
||||||
onClick: () => {
|
|
||||||
$(this).html(bak)
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "取消",
|
|
||||||
onClick: () => {
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
e.src = $(this).attr('src') as string
|
||||||
e.onclick = () => {
|
e.onclick = () => {
|
||||||
openImageViewer($(this).attr('src'))
|
openImageViewer($(this).attr('src') as string)
|
||||||
}
|
}
|
||||||
this.appendChild(e)
|
this.appendChild(e)
|
||||||
}
|
}
|
||||||
@@ -81,11 +75,11 @@ document.body.appendChild(new DOMParser().parseFromString(`
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<mdui-button-icon icon="open_in_new"
|
<mdui-button-icon icon="open_in_new"
|
||||||
onclick="window.open($('#image-viewer-dialog-inner > *').attr('src'), '_blank')">
|
onclick="window.open(document.querySelector('#image-viewer-dialog-inner > *').src, '_blank')">
|
||||||
</mdui-button-icon>
|
</mdui-button-icon>
|
||||||
<mdui-button-icon icon="close" onclick="this.parentNode.open = false">
|
<mdui-button-icon icon="close" onclick="this.parentNode.open = false">
|
||||||
</mdui-button-icon>
|
</mdui-button-icon>
|
||||||
<pinch-zoom id="image-viewer-dialog-inner" style="width: var(--whitesilk-window-width); height: var(--whitesilk-window-height);">
|
<pinch-zoom id="image-viewer-dialog-inner" style="width: var(--whitesilk-window-width); height: var(--whitesilk-window-height);">
|
||||||
</pinch-zoom>
|
</pinch-zoom>
|
||||||
</mdui-dialog>
|
</mdui-dialog>
|
||||||
`, 'text/html').body.firstChild)
|
`, 'text/html').body.firstChild as Node)
|
||||||
@@ -30,6 +30,7 @@ export default function RecentsList({
|
|||||||
return <mdui-list style={{
|
return <mdui-list style={{
|
||||||
overflowY: 'auto',
|
overflowY: 'auto',
|
||||||
paddingRight: '10px',
|
paddingRight: '10px',
|
||||||
|
paddingLeft: '10px',
|
||||||
display: display ? undefined : 'none',
|
display: display ? undefined : 'none',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -37,7 +38,6 @@ export default function RecentsList({
|
|||||||
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
|
<mdui-text-field icon="search" type="search" clearable ref={searchRef} variant="outlined" placeholder="搜索..." style={{
|
||||||
marginTop: '5px',
|
marginTop: '5px',
|
||||||
marginBottom: '13px',
|
marginBottom: '13px',
|
||||||
paddingLeft: '10px',
|
|
||||||
}}></mdui-text-field>
|
}}></mdui-text-field>
|
||||||
{
|
{
|
||||||
recentsList.filter((chat) =>
|
recentsList.filter((chat) =>
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ import BaseApi from "./BaseApi.ts"
|
|||||||
import DataWrongError from "./DataWrongError.ts"
|
import DataWrongError from "./DataWrongError.ts"
|
||||||
import chalk from "chalk"
|
import chalk from "chalk"
|
||||||
|
|
||||||
|
function stringifyNotIncludeArrayBuffer(value: any) {
|
||||||
|
return JSON.stringify(value, (_k, v) => {
|
||||||
|
if (v?.type == 'Buffer') {
|
||||||
|
return {
|
||||||
|
type: 'Buffer',
|
||||||
|
data: '[...binary data omitted...]'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default class ApiManager {
|
export default class ApiManager {
|
||||||
static httpServer: HttpServerLike
|
static httpServer: HttpServerLike
|
||||||
static socketIoServer: SocketIo.Server
|
static socketIoServer: SocketIo.Server
|
||||||
@@ -36,6 +48,9 @@ export default class ApiManager {
|
|||||||
static checkUserIsOnline(userId: string) {
|
static checkUserIsOnline(userId: string) {
|
||||||
return this.getUserClientSockets(userId) != null
|
return this.getUserClientSockets(userId) != null
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* 獲取用戶所有的客戶端表 (格式遵循 設備ID_當前Session)
|
||||||
|
*/
|
||||||
static getUserClientSockets(userId: string) {
|
static getUserClientSockets(userId: string) {
|
||||||
return this.clients[userId]
|
return this.clients[userId]
|
||||||
}
|
}
|
||||||
@@ -48,10 +63,12 @@ export default class ApiManager {
|
|||||||
const ip = socket.conn.remoteAddress
|
const ip = socket.conn.remoteAddress
|
||||||
|
|
||||||
const deviceId = socket.handshake.auth.device_id as string
|
const deviceId = socket.handshake.auth.device_id as string
|
||||||
|
const sessionId = socket.handshake.auth.session_id as string
|
||||||
|
|
||||||
const clientInfo = {
|
const clientInfo = {
|
||||||
userId: '',
|
userId: '',
|
||||||
deviceId,
|
deviceId,
|
||||||
|
sessionId,
|
||||||
ip,
|
ip,
|
||||||
socket,
|
socket,
|
||||||
}
|
}
|
||||||
@@ -61,14 +78,14 @@ export default class ApiManager {
|
|||||||
console.log(chalk.yellow('[斷]') + ` ${ip} disconnected`)
|
console.log(chalk.yellow('[斷]') + ` ${ip} disconnected`)
|
||||||
else {
|
else {
|
||||||
console.log(chalk.green('[斷]') + ` ${ip} disconnected`)
|
console.log(chalk.green('[斷]') + ` ${ip} disconnected`)
|
||||||
delete this.clients[clientInfo.userId][deviceId]
|
delete this.clients[clientInfo.userId][deviceId + '_' + sessionId]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
console.log(chalk.yellow('[連]') + ` ${ip} connected`)
|
console.log(chalk.yellow('[連]') + ` ${ip} connected`)
|
||||||
|
|
||||||
socket.on("The_White_Silk", async (name: string, args: { [key: string]: unknown }, callback_: (ret: ApiCallbackMessage) => void) => {
|
socket.on("The_White_Silk", async (name: string, args: { [key: string]: unknown }, callback_: (ret: ApiCallbackMessage) => void) => {
|
||||||
function callback(ret: ApiCallbackMessage) {
|
function callback(ret: ApiCallbackMessage) {
|
||||||
console.log(chalk.blue('[發]') + ` ${ip} <- ${ret.code == 200 ? chalk.green(ret.msg) : chalk.red(ret.msg)} [${ret.code}]${ret.data ? (' <extras: ' + JSON.stringify(ret.data) + '>') : ''}`)
|
console.log(chalk.blue('[發]') + ` ${ip} <- ${ret.code == 200 ? chalk.green(ret.msg) : chalk.red(ret.msg)} [${ret.code}]${ret.data ? (' <extras: ' + stringifyNotIncludeArrayBuffer(ret.data) + '>') : ''}`)
|
||||||
return callback_(ret)
|
return callback_(ret)
|
||||||
}
|
}
|
||||||
async function checkIsPromiseAndAwait(value: Promise<unknown> | unknown) {
|
async function checkIsPromiseAndAwait(value: Promise<unknown> | unknown) {
|
||||||
@@ -81,7 +98,7 @@ export default class ApiManager {
|
|||||||
msg: "Invalid request.",
|
msg: "Invalid request.",
|
||||||
code: 400
|
code: 400
|
||||||
})
|
})
|
||||||
console.log(chalk.red('[收]') + ` ${ip} -> ${chalk.yellow(name)} <args: ${JSON.stringify(args)}>`)
|
console.log(chalk.red('[收]') + ` ${ip} -> ${chalk.yellow(name)} <args: ${stringifyNotIncludeArrayBuffer(args)}>`)
|
||||||
|
|
||||||
return callback(await checkIsPromiseAndAwait(this.event_listeners[name]?.(args, clientInfo)) || {
|
return callback(await checkIsPromiseAndAwait(this.event_listeners[name]?.(args, clientInfo)) || {
|
||||||
code: 501,
|
code: 501,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer"
|
||||||
import Chat from "../data/Chat.ts";
|
import Chat from "../data/Chat.ts"
|
||||||
import ChatPrivate from "../data/ChatPrivate.ts"
|
|
||||||
import FileManager from "../data/FileManager.ts"
|
import FileManager from "../data/FileManager.ts"
|
||||||
import MessagesManager from "../data/MessagesManager.ts"
|
import MessagesManager from "../data/MessagesManager.ts"
|
||||||
import User from "../data/User.ts"
|
import User from "../data/User.ts"
|
||||||
@@ -182,13 +181,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()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default class UserApi extends BaseApi {
|
|||||||
msg: "參數缺失",
|
msg: "參數缺失",
|
||||||
code: 400,
|
code: 400,
|
||||||
}
|
}
|
||||||
const { deviceId, ip, socket } = clientInfo
|
const { deviceId, ip, socket, sessionId } = clientInfo
|
||||||
try {
|
try {
|
||||||
const access_token = TokenManager.decode(args.access_token as string)
|
const access_token = TokenManager.decode(args.access_token as string)
|
||||||
|
|
||||||
@@ -38,9 +38,9 @@ export default class UserApi extends BaseApi {
|
|||||||
clientInfo.userId = access_token.author
|
clientInfo.userId = access_token.author
|
||||||
console.log(chalk.green('[驗]') + ` ${access_token.author} authed on Client ${deviceId} (ip = ${ip})`)
|
console.log(chalk.green('[驗]') + ` ${access_token.author} authed on Client ${deviceId} (ip = ${ip})`)
|
||||||
if (ApiManager.clients[clientInfo.userId] == null) ApiManager.clients[clientInfo.userId] = {
|
if (ApiManager.clients[clientInfo.userId] == null) ApiManager.clients[clientInfo.userId] = {
|
||||||
[deviceId]: socket
|
[deviceId + '_' + sessionId]: socket
|
||||||
}
|
}
|
||||||
else ApiManager.clients[clientInfo.userId][deviceId] = socket
|
else ApiManager.clients[clientInfo.userId][deviceId + '_' + sessionId] = socket
|
||||||
|
|
||||||
return {
|
return {
|
||||||
msg: "成功",
|
msg: "成功",
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const httpServer: HttpServerLike = (
|
|||||||
http.createServer(app)
|
http.createServer(app)
|
||||||
)
|
)
|
||||||
const io = new SocketIo.Server(httpServer, {
|
const io = new SocketIo.Server(httpServer, {
|
||||||
maxHttpBufferSize: 1e9,
|
maxHttpBufferSize: 1e114514,
|
||||||
})
|
})
|
||||||
|
|
||||||
ApiManager.initServer(httpServer, io)
|
ApiManager.initServer(httpServer, io)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as SocketIo from "socket.io"
|
|||||||
type EventCallbackFunction = (args: { [key: string]: unknown }, clientInfo: {
|
type EventCallbackFunction = (args: { [key: string]: unknown }, clientInfo: {
|
||||||
userId: string
|
userId: string
|
||||||
deviceId: string
|
deviceId: string
|
||||||
|
sessionId: string
|
||||||
ip: string
|
ip: string
|
||||||
socket: SocketIo.Socket<SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, any>
|
socket: SocketIo.Socket<SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, SocketIo.DefaultEventsMap, any>
|
||||||
}) => ApiCallbackMessage | Promise<ApiCallbackMessage>
|
}) => ApiCallbackMessage | Promise<ApiCallbackMessage>
|
||||||
|
|||||||
Reference in New Issue
Block a user