From a549773eb272107256353cb4724d3f0fab4197f5 Mon Sep 17 00:00:00 2001 From: CrescentLeaf Date: Sat, 6 Dec 2025 00:18:10 +0800 Subject: [PATCH] =?UTF-8?q?TODO:=20=E6=8E=A8=E7=BF=BB=E6=95=B4=E4=B8=AA?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E9=87=8D=E6=96=B0=E5=BB=BA=E7=AB=8B=E6=A0=B9?= =?UTF-8?q?=E5=9F=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client-protocol/LingChairClient.ts | 26 + client-protocol/RecentChat.ts | 13 + client-protocol/UserMySelf.ts | 32 + client-protocol/bean/RecentChatBean.ts | 2 +- client/EventBus.ts | 12 - client/MapJson.ts | 20 - client/api/ApiCallbackMessage.ts | 3 - client/api/ApiDeclare.ts | 1 - client/api/Client.ts | 205 ----- client/api/DataCaches.ts | 36 - client/api/client_data/Chat.ts | 14 - client/api/client_data/ChatType.ts | 3 - client/api/client_data/GroupSettings.ts | 14 - client/api/client_data/JoinRequest.ts | 8 - client/api/client_data/Message.ts | 6 - client/api/client_data/RecentChat.ts | 5 - client/api/client_data/User.ts | 6 - client/deno.jsonc | 4 +- client/escapeHtml.ts | 5 - client/getClient.ts | 18 + client/getUrlForFileByHash.ts | 3 - client/icon.ico | Bin 15529 -> 0 bytes client/index.html | 5 +- client/index.ts | 54 -- client/init.ts | 31 + client/performAuth.ts | 26 + client/sw.ts | 30 - client/test.html | 40 - client/ui/App.tsx | 281 ------- client/ui/AppMobile.tsx | 275 ------- client/ui/AvatarMySelf.tsx | 31 + client/ui/Main.tsx | 103 +++ client/ui/MainSharedContext.ts | 5 + client/ui/TextFieldCustom.tsx | 18 - .../chat-file.ts | 0 .../chat-image.ts | 36 +- .../chat-mention.ts | 17 +- .../chat-quote.ts | 0 .../chat-text-container.ts | 0 .../chat-text.ts | 0 .../chat-video.ts | 0 client/ui/chat/ChatFragment.tsx | 730 ------------------ client/ui/chat/GroupMembersList.tsx | 81 -- client/ui/chat/GroupMembersListItem.tsx | 84 -- client/ui/chat/JoinRequestsList.tsx | 106 --- client/ui/chat/JoinRequestsListItem.tsx | 40 - client/ui/chat/Message.tsx | 222 ------ client/ui/chat/MessageContainer.tsx | 17 - client/ui/chat/SystemMessage.tsx | 24 - client/ui/copyToClipboard.ts | 20 - client/ui/dialog/AddContactDialog.tsx | 47 -- client/ui/dialog/ChatInfoDialog.tsx | 111 --- client/ui/dialog/CreateGroupDialog.tsx | 54 -- client/ui/dialog/LoginDialog.tsx | 55 -- client/ui/dialog/MyProfileDialog.tsx | 198 ----- client/ui/dialog/RegisterDialog.tsx | 67 -- client/ui/isMobileUI.ts | 3 - client/ui/main/AllChatsList.tsx | 86 --- client/ui/main/AllChatsListItem.tsx | 29 - client/ui/main/ContactsList.tsx | 171 ---- client/ui/main/ContactsListItem.tsx | 27 - client/ui/main/RecentsList.tsx | 86 --- client/ui/main/RecentsListItem.tsx | 37 - client/ui/openImageViewer.ts | 17 - client/ui/preference/Preference.tsx | 16 - client/ui/preference/PreferenceHeader.tsx | 5 - client/ui/preference/PreferenceLayout.tsx | 8 - client/ui/preference/PreferenceStore.ts | 27 - client/ui/preference/PreferenceUpdater.ts | 6 - client/ui/preference/SelectPreference.tsx | 43 -- client/ui/preference/SwitchPreference.tsx | 30 - client/ui/preference/TextFieldPreference.tsx | 37 - client/utils/isMobileUI.ts | 5 + client/utils/openImageViewer.ts | 52 ++ client/{ => utils}/randomUUID.ts | 0 .../{ui/snackbar.ts => utils/showSnackbar.ts} | 16 +- client/{ui => utils}/useAsyncEffect.ts | 0 client/{ui => utils}/useEventListener.ts | 0 client/vite.config.ts | 3 + 79 files changed, 359 insertions(+), 3589 deletions(-) create mode 100644 client-protocol/RecentChat.ts delete mode 100644 client/EventBus.ts delete mode 100644 client/MapJson.ts delete mode 100644 client/api/ApiCallbackMessage.ts delete mode 100644 client/api/ApiDeclare.ts delete mode 100644 client/api/Client.ts delete mode 100644 client/api/DataCaches.ts delete mode 100644 client/api/client_data/Chat.ts delete mode 100644 client/api/client_data/ChatType.ts delete mode 100644 client/api/client_data/GroupSettings.ts delete mode 100644 client/api/client_data/JoinRequest.ts delete mode 100644 client/api/client_data/Message.ts delete mode 100644 client/api/client_data/RecentChat.ts delete mode 100644 client/api/client_data/User.ts delete mode 100644 client/escapeHtml.ts create mode 100644 client/getClient.ts delete mode 100644 client/getUrlForFileByHash.ts delete mode 100644 client/icon.ico delete mode 100644 client/index.ts create mode 100644 client/init.ts create mode 100644 client/performAuth.ts delete mode 100644 client/sw.ts delete mode 100644 client/test.html delete mode 100644 client/ui/App.tsx delete mode 100644 client/ui/AppMobile.tsx create mode 100644 client/ui/AvatarMySelf.tsx create mode 100644 client/ui/Main.tsx create mode 100644 client/ui/MainSharedContext.ts delete mode 100644 client/ui/TextFieldCustom.tsx rename client/ui/{custom-elements => chat-elements}/chat-file.ts (100%) rename client/ui/{custom-elements => chat-elements}/chat-image.ts (58%) rename client/ui/{custom-elements => chat-elements}/chat-mention.ts (78%) rename client/ui/{custom-elements => chat-elements}/chat-quote.ts (100%) rename client/ui/{custom-elements => chat-elements}/chat-text-container.ts (100%) rename client/ui/{custom-elements => chat-elements}/chat-text.ts (100%) rename client/ui/{custom-elements => chat-elements}/chat-video.ts (100%) delete mode 100644 client/ui/chat/ChatFragment.tsx delete mode 100644 client/ui/chat/GroupMembersList.tsx delete mode 100644 client/ui/chat/GroupMembersListItem.tsx delete mode 100644 client/ui/chat/JoinRequestsList.tsx delete mode 100644 client/ui/chat/JoinRequestsListItem.tsx delete mode 100644 client/ui/chat/Message.tsx delete mode 100644 client/ui/chat/MessageContainer.tsx delete mode 100644 client/ui/chat/SystemMessage.tsx delete mode 100644 client/ui/copyToClipboard.ts delete mode 100644 client/ui/dialog/AddContactDialog.tsx delete mode 100644 client/ui/dialog/ChatInfoDialog.tsx delete mode 100644 client/ui/dialog/CreateGroupDialog.tsx delete mode 100644 client/ui/dialog/LoginDialog.tsx delete mode 100644 client/ui/dialog/MyProfileDialog.tsx delete mode 100644 client/ui/dialog/RegisterDialog.tsx delete mode 100644 client/ui/isMobileUI.ts delete mode 100644 client/ui/main/AllChatsList.tsx delete mode 100644 client/ui/main/AllChatsListItem.tsx delete mode 100644 client/ui/main/ContactsList.tsx delete mode 100644 client/ui/main/ContactsListItem.tsx delete mode 100644 client/ui/main/RecentsList.tsx delete mode 100644 client/ui/main/RecentsListItem.tsx delete mode 100644 client/ui/openImageViewer.ts delete mode 100644 client/ui/preference/Preference.tsx delete mode 100644 client/ui/preference/PreferenceHeader.tsx delete mode 100644 client/ui/preference/PreferenceLayout.tsx delete mode 100644 client/ui/preference/PreferenceStore.ts delete mode 100644 client/ui/preference/PreferenceUpdater.ts delete mode 100644 client/ui/preference/SelectPreference.tsx delete mode 100644 client/ui/preference/SwitchPreference.tsx delete mode 100644 client/ui/preference/TextFieldPreference.tsx create mode 100644 client/utils/isMobileUI.ts create mode 100644 client/utils/openImageViewer.ts rename client/{ => utils}/randomUUID.ts (100%) rename client/{ui/snackbar.ts => utils/showSnackbar.ts} (84%) rename client/{ui => utils}/useAsyncEffect.ts (100%) rename client/{ui => utils}/useEventListener.ts (100%) diff --git a/client-protocol/LingChairClient.ts b/client-protocol/LingChairClient.ts index 4c6d51c..69fac7c 100644 --- a/client-protocol/LingChairClient.ts +++ b/client-protocol/LingChairClient.ts @@ -121,6 +121,25 @@ export default class LingChairClient { }) }) } + /** + * 建议在 auth 返回 true 时调用 + */ + getCachedAccessToken() { + return this.access_token + } + /** + * 建议在 auth 返回 true 时调用 + */ + getCachedRefreshToken() { + return this.refresh_token + } + /** + * 客户端上线 + * + * 使用验证方式优先级: 访问 > 刷新 > 账号密码 + * + * 不会逐一尝试 + */ async auth(args: { refresh_token?: string, access_token?: string, @@ -134,6 +153,13 @@ export default class LingChairClient { return false } } + /** + * 客户端上线 + * + * 使用验证方式优先级: 访问 > 刷新 > 账号密码 + * + * 不会逐一尝试 + */ async authOrThrow(args: { refresh_token?: string, access_token?: string, diff --git a/client-protocol/RecentChat.ts b/client-protocol/RecentChat.ts new file mode 100644 index 0000000..ed0f04e --- /dev/null +++ b/client-protocol/RecentChat.ts @@ -0,0 +1,13 @@ +import RecentChatBean from "./bean/RecentChatBean.ts" +import Chat from "./Chat.ts" +import LingChairClient from "./LingChairClient.ts" + +export default class RecentChat extends Chat { + declare bean: RecentChatBean + constructor(client: LingChairClient, bean: RecentChatBean) { + super(client, bean) + } + getContent() { + return this.bean.content + } +} diff --git a/client-protocol/UserMySelf.ts b/client-protocol/UserMySelf.ts index 770ea58..13afacc 100644 --- a/client-protocol/UserMySelf.ts +++ b/client-protocol/UserMySelf.ts @@ -1,5 +1,7 @@ import CallbackError from "./CallbackError.ts" +import Chat from "./Chat.ts" import LingChairClient from "./LingChairClient.ts" +import RecentChat from "./RecentChat.ts" import User from "./User.ts" import ChatBean from "./bean/ChatBean.ts" import RecentChatBean from "./bean/RecentChatBean.ts" @@ -157,6 +159,16 @@ export default class UserMySelf extends User { return re.data!.recent_chats as ChatBean[] throw new CallbackError(re) } + async getMyFavouriteChats() { + try { + return await this.getMyFavouriteChatsOrThrow() + } catch (_) { + return [] + } + } + async getMyFavouriteChatsOrThrow() { + return (await this.getMyFavouriteChatBeansOrThrow()).map((bean) => new Chat(this.client, bean)) + } /* * ================================================ * 最近对话 @@ -177,6 +189,16 @@ export default class UserMySelf extends User { return re.data!.recent_chats as RecentChatBean[] throw new CallbackError(re) } + async getMyRecentChats() { + try { + return await this.getMyRecentChatsOrThrow() + } catch (_) { + return [] + } + } + async getMyRecentChatsOrThrow() { + return (await this.getMyRecentChatBeansOrThrow()).map((bean) => new RecentChat(this.client, bean)) + } /* * ================================================ * 所有对话 @@ -197,4 +219,14 @@ export default class UserMySelf extends User { return re.data!.all_chats as ChatBean[] throw new CallbackError(re) } + async getMyAllChats() { + try { + return await this.getMyAllChatsOrThrow() + } catch (_) { + return [] + } + } + async getMyAllChatsOrThrow() { + return (await this.getMyAllChatBeansOrThrow()).map((bean) => new Chat(this.client, bean)) + } } diff --git a/client-protocol/bean/RecentChatBean.ts b/client-protocol/bean/RecentChatBean.ts index 2d6d455..ec93e92 100644 --- a/client-protocol/bean/RecentChatBean.ts +++ b/client-protocol/bean/RecentChatBean.ts @@ -1,5 +1,5 @@ import ChatBean from "./ChatBean.ts" -export default class RecentChat extends ChatBean { +export default class RecentChatBean extends ChatBean { declare content: string } diff --git a/client/EventBus.ts b/client/EventBus.ts deleted file mode 100644 index 1db9873..0000000 --- a/client/EventBus.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default class EventBus { - static events: { [key: string]: () => void } = {} - static on(eventName: string, func: () => void) { - this.events[eventName] = func - } - static off(eventName: string) { - delete this.events[eventName] - } - static emit(eventName: string) { - this.events[eventName]() - } -} \ No newline at end of file diff --git a/client/MapJson.ts b/client/MapJson.ts deleted file mode 100644 index be64c19..0000000 --- a/client/MapJson.ts +++ /dev/null @@ -1,20 +0,0 @@ -// https://stackoverflow.com/questions/29085197/how-do-you-json-stringify-an-es6-map - -export default class MapJson { - static replacer(key: unknown, value: unknown) { - if (value instanceof Map) { - return { - dataType: 'Map', - value: Array.from(value.entries()), // or with spread: value: [...value] - } - } else { - return value - } - } - static reviver(key: unknown, value: any) { - if (value?.dataType === 'Map') { - return new Map(value.value) - } - return value - } -} \ No newline at end of file diff --git a/client/api/ApiCallbackMessage.ts b/client/api/ApiCallbackMessage.ts deleted file mode 100644 index c5f98ee..0000000 --- a/client/api/ApiCallbackMessage.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { ApiCallbackMessage } from 'lingchair-internal-shared' - -export type { ApiCallbackMessage as default } diff --git a/client/api/ApiDeclare.ts b/client/api/ApiDeclare.ts deleted file mode 100644 index e9868c2..0000000 --- a/client/api/ApiDeclare.ts +++ /dev/null @@ -1 +0,0 @@ -export * from 'lingchair-internal-shared' diff --git a/client/api/Client.ts b/client/api/Client.ts deleted file mode 100644 index 23e75f5..0000000 --- a/client/api/Client.ts +++ /dev/null @@ -1,205 +0,0 @@ -import { io, Socket } from 'socket.io-client' -import { CallMethod, ClientEvent, CallableMethodBeforeAuth } from './ApiDeclare.ts' -import ApiCallbackMessage from './ApiCallbackMessage.ts' -import User from "./client_data/User.ts" -import data from "../Data.ts" -import { checkApiSuccessOrSncakbar, snackbar } from "../ui/snackbar.ts" -import randomUUID from "../randomUUID.ts" - -class Client { - static sessionId = randomUUID() - static myUserProfile?: User - static socket?: Socket - static events: { [key: string]: ((data: unknown) => void)[] } = {} - static connected = false - static connect() { - if (data.device_id == null) - data.device_id = randomUUID() - this.socket?.disconnect() - this.socket && delete this.socket - this.socket = io({ - transports: ['websocket', 'polling', 'webtransport'], - auth: { - device_id: data.device_id, - session_id: this.sessionId, - }, - }) - this.socket!.on("connect", () => { - const auth = async () => { - this.connected = true - const s = snackbar({ - message: '重新验证中...', - placement: 'top', - autoCloseDelay: 0, - }) - let i = 1 - const id = setInterval(() => { - s.textContent = `重新验证中... (${i}s)` - i++ - }, 1000) - const re = await this.auth(data.access_token as string, 6000) - if (re.code != 200) { - if (re.code == -1) { - auth() - } else if (re.code != 401 && re.code != 400) { - const s2 = checkApiSuccessOrSncakbar(re, "重新验证失败") - s2!.autoCloseDelay = 0 - s2!.action = "重试" - s2!.addEventListener('action-click', () => { - auth() - }) - this.socket!.once("disconnect", () => { - s2!.open = false - }) - } - } - clearTimeout(id) - s.open = false - } - auth() - }) - this.socket!.on("disconnect", () => { - this.connected = false - const s = snackbar({ - message: '重新连接服务器中...', - placement: 'top', - autoCloseDelay: 0, - }) - let i = 1 - const id = setInterval(() => { - s.textContent = `重新连接服务器中... (${i}s)` - i++ - this.socket!.connect() - }, 1000) - this.socket!.once('connect', () => { - s.open = false - clearTimeout(id) - }) - }) - this.socket!.on("The_White_Silk", (name: string, data: unknown, callback: (ret: unknown) => void) => { - try { - if (name == null || data == null) return - this.events[name]?.forEach((v) => v(data)) - } catch (e) { - console.error(e) - } - }) - } - static invoke(method: CallMethod, args: object = {}, timeout: number = 10000, refreshAndRetryLimit: number = 3, forceRefreshAndRetry: boolean = false): Promise { - // 在 未初始化 / 未建立连接且调用非可调用接口 的时候进行延迟 - if (this.socket == null || (!this.connected && !CallableMethodBeforeAuth.includes(method))) { - return new Promise((reslove) => { - setTimeout(async () => reslove(await this.invoke(method, args, timeout)), 500) - }) - } - // 反之, 返回 Promise - return new Promise((resolve) => { - this.socket!.timeout(timeout).emit("The_White_Silk", method, args, async (err: Error, res: ApiCallbackMessage) => { - // 错误处理 - if (err) return resolve({ - code: -1, - msg: err.message.indexOf("timed out") != -1 ? "请求超时" : err.message, - }) - // 在特殊的方法之中, 不予进行: 令牌刷新并重试 - // 附带 retry 次数限制 - if ( - ( - forceRefreshAndRetry || - ( - !CallableMethodBeforeAuth.includes(method) - && res.code == 401 - ) - ) && refreshAndRetryLimit > 0 - ) { - const token = await this.refreshAccessToken() - if (token) { - data.access_token = token - data.apply() - resolve(await this.invoke(method, { - ...args, - [method == "User.auth" ? "access_token" : "token"]: token, - }, timeout, refreshAndRetryLimit - 1)) - } else - resolve(res) - } else - resolve(res) - }) - }) - } - static async refreshAccessToken() { - const re = await this.invoke("User.refreshAccessToken", { - refresh_token: data.refresh_token - }) - return re.data?.access_token as string - } - static async auth(token: string, timeout?: number) { - const re = await this.invoke("User.auth", { - access_token: token - }, timeout, 1, true) - if (re.code == 200) { - // 灵车: 你应该先 connected = true 再调用 - await this.updateCachedProfile() - document.cookie = 'token=' + token - document.cookie = 'device_id=' + data.device_id - } - return re - } - static async uploadFileLikeApi(fileName: string, fileData: ArrayBuffer | Blob | Response, chatId?: string) { - const form = new FormData() - form.append("file", - fileData instanceof ArrayBuffer - ? new File([fileData], fileName, { type: 'application/octet-stream' }) - : ( - fileData instanceof Blob ? fileData : - new File([await fileData.arrayBuffer()], fileName, { type: 'application/octet-stream' }) - ) - ) - form.append('file_name', fileName) - chatId && form.append('chat_id', chatId) - const re = await fetch('./upload_file', { - method: 'POST', - headers: { - "Token": data.access_token, - "Device-Id": data.device_id, - } as HeadersInit, - body: form, - credentials: 'omit', - }) - const text = await (await re.blob()).text() - let json - try { - json = JSON.parse(text) - } catch(_) {} - return { - ...(json == null ? { - msg: text - } : json), - code: re.status, - } as ApiCallbackMessage - } - static async uploadFile(fileName: string, fileData: ArrayBuffer | Blob | Response, chatId?: string) { - const re = await this.uploadFileLikeApi(fileName, fileData, chatId) - if (re.code != 200) throw new Error(re.msg) - return re.data!.hash as string - } - static async updateCachedProfile() { - this.myUserProfile = (await Client.invoke("User.getMyInfo", { - token: data.access_token - })).data as unknown as User - } - static on(eventName: ClientEvent, func: (data: unknown) => void) { - if (this.events[eventName] == null) - this.events[eventName] = [] - if (this.events[eventName].indexOf(func) == -1) - this.events[eventName].push(func) - } - static off(eventName: ClientEvent, func: (data: unknown) => void) { - if (this.events[eventName] == null) - this.events[eventName] = [] - const index = this.events[eventName].indexOf(func) - if (index != -1) - this.events[eventName].splice(index, 1) - } -} - -export default Client diff --git a/client/api/DataCaches.ts b/client/api/DataCaches.ts deleted file mode 100644 index 325e883..0000000 --- a/client/api/DataCaches.ts +++ /dev/null @@ -1,36 +0,0 @@ -import data from "../Data.ts" -import Client from "./Client.ts" -import Chat from "./client_data/Chat.ts" -import User from "./client_data/User.ts" - -export default class DataCaches { - static userProfiles: { [key: string]: User} = {} - static async getUserProfile(userId: string): Promise { - if (this.userProfiles[userId]) return this.userProfiles[userId] - const re = await Client.invoke("User.getInfo", { - token: data.access_token, - target: userId - }) - if (re.code != 200) return { - id: '', - nickname: "", - } - return this.userProfiles[userId] = (re.data as unknown as User) - } - static chatInfos: { [key: string]: Chat} = {} - static async getChatInfo(chatId: string): Promise { - if (this.chatInfos[chatId]) return this.chatInfos[chatId] - const re = await Client.invoke('Chat.getInfo', { - token: data.access_token, - target: chatId, - }) - if (re.code != 200) return { - id: '', - title: '', - type: '' as any, - is_admin: false, - is_member: false, - } - return this.chatInfos[chatId] = (re.data as unknown as Chat) - } -} \ No newline at end of file diff --git a/client/api/client_data/Chat.ts b/client/api/client_data/Chat.ts deleted file mode 100644 index 2607314..0000000 --- a/client/api/client_data/Chat.ts +++ /dev/null @@ -1,14 +0,0 @@ -import ChatType from "./ChatType.ts" - -export default class Chat { - declare type: ChatType - declare id: string - declare title: string - declare avatar_file_hash?: string - declare settings?: { [key: string]: unknown } - - declare is_member: boolean - declare is_admin: boolean - - [key: string]: unknown -} diff --git a/client/api/client_data/ChatType.ts b/client/api/client_data/ChatType.ts deleted file mode 100644 index 0ac05ed..0000000 --- a/client/api/client_data/ChatType.ts +++ /dev/null @@ -1,3 +0,0 @@ -type ChatType = 'private' | 'group' - -export default ChatType diff --git a/client/api/client_data/GroupSettings.ts b/client/api/client_data/GroupSettings.ts deleted file mode 100644 index dc7ab45..0000000 --- a/client/api/client_data/GroupSettings.ts +++ /dev/null @@ -1,14 +0,0 @@ -interface GroupSettings { - allow_new_member_join?: boolean - allow_new_member_from_invitation?: boolean - new_member_join_method?: 'disabled' | 'allowed_by_admin' | 'answered_and_allowed_by_admin' - answered_and_allowed_by_admin_question?: string - - // 下面两个比较特殊, 由服务端给予 - group_title: string - group_name: string - - [key: string]: unknown -} - -export default GroupSettings diff --git a/client/api/client_data/JoinRequest.ts b/client/api/client_data/JoinRequest.ts deleted file mode 100644 index 5d3dfe7..0000000 --- a/client/api/client_data/JoinRequest.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default class JoinRequest { - declare user_id: string - declare title: string - declare avatar?: string - declare reason?: string - - [key: string]: unknown -} diff --git a/client/api/client_data/Message.ts b/client/api/client_data/Message.ts deleted file mode 100644 index cc843a1..0000000 --- a/client/api/client_data/Message.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default class Message { - declare id: number - declare text: string - declare user_id: string - declare time: string -} diff --git a/client/api/client_data/RecentChat.ts b/client/api/client_data/RecentChat.ts deleted file mode 100644 index 553ea04..0000000 --- a/client/api/client_data/RecentChat.ts +++ /dev/null @@ -1,5 +0,0 @@ -import Chat from "./Chat.ts" - -export default class RecentChat extends Chat { - declare content: string -} diff --git a/client/api/client_data/User.ts b/client/api/client_data/User.ts deleted file mode 100644 index 4a41263..0000000 --- a/client/api/client_data/User.ts +++ /dev/null @@ -1,6 +0,0 @@ -export default class User { - declare id: string - declare username?: string - declare nickname: string - declare avatar_file_hash?: string -} diff --git a/client/deno.jsonc b/client/deno.jsonc index 32311c4..a0dc39b 100644 --- a/client/deno.jsonc +++ b/client/deno.jsonc @@ -35,7 +35,9 @@ "marked": "npm:marked@16.3.0", "dompurify": "npm:dompurify@3.2.7", "pinch-zoom-element": "npm:pinch-zoom-element@1.1.1", + "ua-parser-js": "npm:ua-parser-js@2.0.6", - "lingchair-internal-shared": "../internal-shared/mod.ts" + "lingchair-internal-shared": "../internal-shared/mod.ts", + "lingchair-client-protocol": "../mod.ts" } } diff --git a/client/escapeHtml.ts b/client/escapeHtml.ts deleted file mode 100644 index df1f2f8..0000000 --- a/client/escapeHtml.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default function escapeHTML(str: string) { - const div = document.createElement('div') - div.textContent = str - return div.innerHTML -} \ No newline at end of file diff --git a/client/getClient.ts b/client/getClient.ts new file mode 100644 index 0000000..50ea688 --- /dev/null +++ b/client/getClient.ts @@ -0,0 +1,18 @@ +import { LingChairClient } from 'lingchair-client-protocol' +import data from "./Data.ts" +import { UAParser } from 'ua-parser-js' +import randomUUID from "./utils/randomUUID.ts" + +if (!data.device_id) { + const ua = new UAParser(navigator.userAgent) + data.device_id = `LingChair_Web_${ua.getOS() || 'unknown-os'}-${ua.getDevice().type || 'unknown_device'}-${randomUUID()}` +} +const client = new LingChairClient({ + server_url: '', + device_id: data.device_id, + auto_fresh_token: true, +}) + +export default function getClient() { + return client +} diff --git a/client/getUrlForFileByHash.ts b/client/getUrlForFileByHash.ts deleted file mode 100644 index 4a1db3d..0000000 --- a/client/getUrlForFileByHash.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default function getUrlForFileByHash(file_hash?: string, defaultUrl?: string) { - return file_hash ? "uploaded_files/" + file_hash: defaultUrl -} diff --git a/client/icon.ico b/client/icon.ico deleted file mode 100644 index d8ec26a4169dbdd1b04bb6252ed299d1f5863d4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15529 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 Gg#Qm0dQ4~l diff --git a/client/index.html b/client/index.html index 93aba72..b20d9a1 100644 --- a/client/index.html +++ b/client/index.html @@ -18,10 +18,7 @@
- - + \ No newline at end of file diff --git a/client/index.ts b/client/index.ts deleted file mode 100644 index 02af9d2..0000000 --- a/client/index.ts +++ /dev/null @@ -1,54 +0,0 @@ -import 'mdui/mdui.css' -import 'mdui' -import { breakpoint } from "mdui" - -import * as React from 'react' -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' -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-quote.ts' - -import App from './ui/App.tsx' -import AppMobile from './ui/AppMobile.tsx' -import isMobileUI from "./ui/isMobileUI.ts" -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 - openChatInfoDialog: (chat: Chat) => void - } -} - -const onResize = () => { - document.body.style.setProperty('--whitesilk-widget-message-maxwidth', breakpoint().down('md') ? "80%" : "70%") - // deno-lint-ignore no-window - document.body.style.setProperty('--whitesilk-window-width', window.innerWidth + 'px') - // deno-lint-ignore no-window - document.body.style.setProperty('--whitesilk-window-height', window.innerHeight + 'px') -} -// deno-lint-ignore no-window no-window-prefix -window.addEventListener('resize', onResize) -onResize() - -// @ts-ignore 工作正常, 这里是获取为 URL 以便于构建 -import sw from './sw.ts?worker&url' - -if ("serviceWorker" in navigator) - try { - navigator.serviceWorker - .register(sw as URL) - } catch (e) { - console.error(e) - } - -const config = await fetch('config.json').then((re) => re.json()) -config.title && (document.title = config.title) diff --git a/client/init.ts b/client/init.ts new file mode 100644 index 0000000..75c4ef2 --- /dev/null +++ b/client/init.ts @@ -0,0 +1,31 @@ +import 'mdui/mdui.css' +import 'mdui' +import { breakpoint } from "mdui" + +import * as React from 'react' +import ReactDOM from 'react-dom/client' + +import './ui/chat-elements/chat-image.ts' +import './ui/chat-elements/chat-video.ts' +import './ui/chat-elements/chat-file.ts' +import './ui/chat-elements/chat-text.ts' +import './ui/chat-elements/chat-mention.ts' +import './ui/chat-elements/chat-text-container.ts' +import './ui/chat-elements/chat-quote.ts' +import Main from "./ui/Main.tsx" + +ReactDOM.createRoot(document.getElementById('app') as HTMLElement).render(React.createElement(Main)) + +const onResize = () => { + document.body.style.setProperty('--whitesilk-widget-message-maxwidth', breakpoint().down('md') ? "80%" : "70%") + // deno-lint-ignore no-window + document.body.style.setProperty('--whitesilk-window-width', window.innerWidth + 'px') + // deno-lint-ignore no-window + document.body.style.setProperty('--whitesilk-window-height', window.innerHeight + 'px') +} +// deno-lint-ignore no-window no-window-prefix +window.addEventListener('resize', onResize) +onResize() + +const config = await fetch('config.json').then((re) => re.json()) +config.title && (document.title = config.title) diff --git a/client/performAuth.ts b/client/performAuth.ts new file mode 100644 index 0000000..a513687 --- /dev/null +++ b/client/performAuth.ts @@ -0,0 +1,26 @@ +import data from "./Data.ts"; +import getClient from "./getClient.ts" + +/** + * 客户端上线 + * + * 优先级: 账号密码 > 提供刷新令牌 > 储存的刷新令牌 + * + * 不会逐一尝试 + */ +export default async function performAuth(args: { + refresh_token?: string + account?: string + password?: string +}) { + if (args.account && args.password) + await getClient().authOrThrow({ + account: args.account, + password: args.password, + }) + else { + await getClient().authOrThrow({ refresh_token: args.refresh_token ? args.refresh_token : data.refresh_token }) + } + data.refresh_token = getClient().getCachedRefreshToken() + data.access_token = getClient().getCachedAccessToken() +} diff --git a/client/sw.ts b/client/sw.ts deleted file mode 100644 index 9e30143..0000000 --- a/client/sw.ts +++ /dev/null @@ -1,30 +0,0 @@ -interface FetchEvent extends Event { - waitUntil: (p: Promise) => void - request: Request - respondWith: (r: Response | Promise) => void -} - -// 上传文件的代理与缓存 -self.addEventListener("fetch", (e) => { - const event = e as FetchEvent - if (event.request.method != "GET" || event.request.url.indexOf("/uploaded_files/") == -1) return - - event.respondWith( - (async () => { - const cache = await caches.open("LingChair-UploadedFile-Cache") - const cachedResponse = await cache.match(event.request) - if (cachedResponse) { - event.waitUntil(cache.add(event.request)) - return cachedResponse - } - return fetch({ - ...event.request, - headers: { - ...event.request.headers, - // 目前还不能获取 token - // localsotrage 在这里不可用 - } - }) - })() - ) -}) diff --git a/client/test.html b/client/test.html deleted file mode 100644 index 379c99f..0000000 --- a/client/test.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - TheWhiteSilk Debugger - - - - Send - -
- -
- - - - \ No newline at end of file diff --git a/client/ui/App.tsx b/client/ui/App.tsx deleted file mode 100644 index 3f36443..0000000 --- a/client/ui/App.tsx +++ /dev/null @@ -1,281 +0,0 @@ -import Client from "../api/Client.ts" -import data from "../Data.ts" -import ChatFragment from "./chat/ChatFragment.tsx" -import useEventListener from './useEventListener.ts' -import User from "../api/client_data/User.ts" -import Avatar from "./Avatar.tsx" - -import * as React from 'react' -import { Dialog, NavigationRail, TextField } from "mdui" -import Split from 'split.js' -import 'mdui/jsx.zh-cn.d.ts' -import { checkApiSuccessOrSncakbar } from "./snackbar.ts" - -import RegisterDialog from "./dialog/RegisterDialog.tsx" -import LoginDialog from "./dialog/LoginDialog.tsx" -import MyProfileDialog from "./dialog/MyProfileDialog.tsx" -import ContactsList from "./main/ContactsList.tsx" -import RecentsList from "./main/RecentsList.tsx" -import useAsyncEffect from "./useAsyncEffect.ts" -import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx" -import Chat from "../api/client_data/Chat.ts" -import AddContactDialog from './dialog/AddContactDialog.tsx' -import CreateGroupDialog from './dialog/CreateGroupDialog.tsx' -import DataCaches from "../api/DataCaches.ts" -import getUrlForFileByHash from "../getUrlForFileByHash.ts" -import Message from "../api/client_data/Message.ts" -import EventBus from "../EventBus.ts" -import AllChatsList from "./main/AllChatsList.tsx"; - -declare global { - namespace React { - namespace JSX { - interface IntrinsicAttributes { - id?: string - slot?: string - } - } - } -} - -export default function App() { - const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents') - - const navigationRailRef = React.useRef(null) - useEventListener(navigationRailRef, 'change', (event) => { - setNavigationItemSelected((event.target as HTMLElement as NavigationRail).value as string) - }) - - const loginDialogRef = React.useRef(null) - const loginInputAccountRef = React.useRef(null) - const loginInputPasswordRef = React.useRef(null) - - const registerDialogRef = React.useRef(null) - const registerInputUserNameRef = React.useRef(null) - const registerInputNickNameRef = React.useRef(null) - const registerInputPasswordRef = React.useRef(null) - - const myProfileDialogRef = React.useRef(null) - const openMyProfileDialogButtonRef = React.useRef(null) - useEventListener(openMyProfileDialogButtonRef, 'click', (_event) => { - myProfileDialogRef.current!.open = true - }) - - const addContactDialogRef = React.useRef(null) - const createGroupDialogRef = React.useRef(null) - - const chatInfoDialogRef = React.useRef(null) - const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat) - - const [myUserProfileCache, setMyUserProfileCache] = React.useState(null as unknown as User) - - const [isShowChatFragment, setIsShowChatFragment] = React.useState(false) - - const [currentChatId, setCurrentChatId] = React.useState('') - - const [sharedFavouriteChats, setSharedFavouriteChats] = React.useState([]) - - useAsyncEffect(async () => { - const split = Split(['#SideBar', '#ChatFragment'], { - sizes: data.split_sizes ? data.split_sizes : [25, 75], - minSize: [200, 400], - gutterSize: 2, - onDragEnd: function () { - data.split_sizes = split.getSizes() - data.apply() - } - }) - - Client.connect() - const re = await Client.auth(data.access_token || "") - if (re.code == 401) - loginDialogRef.current!.open = true - else if (re.code != 200) { - if (checkApiSuccessOrSncakbar(re, "验证失败")) return - } else if (re.code == 200) { - setMyUserProfileCache(Client.myUserProfile as User) - } - }) - - function openChatInfoDialog(chat: Chat) { - setChatInfo(chat) - chatInfoDialogRef.current!.open = true - } - - function openChatFragment(chatId: string) { - setCurrentChatId(chatId) - setIsShowChatFragment(true) - } - - async function openUserInfoDialog(user: User | string) { - const re = await Client.invoke("Chat.getIdForPrivate", { - token: data.access_token, - target: typeof user == 'object' ? user.id : user, - }) - if (re.code != 200) { - checkApiSuccessOrSncakbar(re, '获取对话失败') - return - } - - openChatInfoDialog(re.data as Chat) - /* if (typeof user == 'object') { - setUserInfo(user) - } else { - setUserInfo(await DataCaches.getUserProfile(user)) - - } - userProfileDialogRef.current!.open = true */ - } - // deno-lint-ignore no-window - window.openUserInfoDialog = openUserInfoDialog - // deno-lint-ignore no-window - window.openChatInfoDialog = openChatInfoDialog - - if ('Notification' in window) { - Notification.requestPermission() - React.useEffect(() => { - interface OnMessageData { - chat: string - msg: Message - } - async function onMessage(_event: unknown) { - EventBus.emit('RecentsList.updateRecents') - - const event = _event as OnMessageData - if (currentChatId != event.chat) { - const chat = await DataCaches.getChatInfo(event.chat) - const user = await DataCaches.getUserProfile(event.msg.user_id) - const notification = new Notification(`${user.nickname} (对话: ${chat.title})`, { - icon: getUrlForFileByHash(chat.avatar_file_hash), - body: event.msg.text, - }) - notification.addEventListener('click', () => { - setCurrentChatId(chat.id) - setIsShowChatFragment(true) - notification.close() - }) - } - } - Client.on('Client.onMessage', onMessage) - return () => { - Client.off('Client.onMessage', onMessage) - } - }, [currentChatId]) - } - - return ( -
- - - - - - - - - - - - - - - - - - - - - - { - EventBus.emit('RecentsList.updateRecents') - EventBus.emit('ContactsList.updateContacts') - EventBus.emit('AllChatsList.updateAllChats') - }}> - - - - addContactDialogRef.current!.open = true}>添加收藏对话 - createGroupDialogRef.current!.open = true}>创建群组 - - - - - { - // 侧边列表 - } - - { - // 聊天页面 - } -
- { - !isShowChatFragment &&
- 选择以开始对话...... -
- } - { - isShowChatFragment && - } -
-
- ) -} \ No newline at end of file diff --git a/client/ui/AppMobile.tsx b/client/ui/AppMobile.tsx deleted file mode 100644 index d583913..0000000 --- a/client/ui/AppMobile.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import Client from "../api/Client.ts" -import data from "../Data.ts" -import ChatFragment from "./chat/ChatFragment.tsx" -import useEventListener from './useEventListener.ts' -import User from "../api/client_data/User.ts" -import Avatar from "./Avatar.tsx" - -import * as React from 'react' -import { Dialog, NavigationBar, TextField } from "mdui" -import 'mdui/jsx.zh-cn.d.ts' -import { checkApiSuccessOrSncakbar } from "./snackbar.ts" - -import RegisterDialog from "./dialog/RegisterDialog.tsx" -import LoginDialog from "./dialog/LoginDialog.tsx" -import MyProfileDialog from "./dialog/MyProfileDialog.tsx" -import ContactsList from "./main/ContactsList.tsx" -import RecentsList from "./main/RecentsList.tsx" -import useAsyncEffect from "./useAsyncEffect.ts" -import ChatInfoDialog from "./dialog/ChatInfoDialog.tsx" -import Chat from "../api/client_data/Chat.ts" -import AddContactDialog from './dialog/AddContactDialog.tsx' -import CreateGroupDialog from './dialog/CreateGroupDialog.tsx' -import getUrlForFileByHash from "../getUrlForFileByHash.ts" -import AllChatsList from "./main/AllChatsList.tsx"; -import EventBus from "../EventBus.ts"; - -declare global { - namespace React { - namespace JSX { - interface IntrinsicAttributes { - id?: string - slot?: string - } - } - } -} - -export default function AppMobile() { - const [navigationItemSelected, setNavigationItemSelected] = React.useState('Recents') - - const navigationBarRef = React.useRef(null) - useEventListener(navigationBarRef, 'change', (event) => { - setNavigationItemSelected((event.target as HTMLElement as NavigationBar).value as string) - }) - - const loginDialogRef = React.useRef(null) - const loginInputAccountRef = React.useRef(null) - const loginInputPasswordRef = React.useRef(null) - - const registerDialogRef = React.useRef(null) - const registerInputUserNameRef = React.useRef(null) - const registerInputNickNameRef = React.useRef(null) - const registerInputPasswordRef = React.useRef(null) - - const myProfileDialogRef = React.useRef(null) - const openMyProfileDialogButtonRef = React.useRef(null) - useEventListener(openMyProfileDialogButtonRef, 'click', (_event) => { - myProfileDialogRef.current!.open = true - }) - - const addContactDialogRef = React.useRef(null) - const createGroupDialogRef = React.useRef(null) - - const chatInfoDialogRef = React.useRef(null) - const [chatInfo, setChatInfo] = React.useState(null as unknown as Chat) - - const [myUserProfileCache, setMyUserProfileCache] = React.useState(null as unknown as User) - - const [isShowChatFragment, setIsShowChatFragment] = React.useState(false) - - const [currentChatId, setCurrentChatId] = React.useState('') - - const [sharedFavouriteChats, setSharedFavouriteChats] = React.useState([]) - - const chatFragmentDialogRef = React.useRef(null) - React.useEffect(() => { - const shadow = chatFragmentDialogRef.current!.shadowRoot as ShadowRoot - const panel = shadow.querySelector(".panel") as HTMLElement - panel.style.padding = '0' - panel.style.color = 'inherit' - panel.style.backgroundColor = 'rgb(var(--mdui-color-background))' - panel.style.setProperty('--mdui-color-background', 'inherit') - const body = shadow.querySelector(".body") as HTMLElement - body.style.height = '100%' - body.style.display = 'flex' - }) - - useAsyncEffect(async () => { - Client.connect() - const re = await Client.auth(data.access_token || "") - if (re.code == 401) - loginDialogRef.current!.open = true - else if (re.code != 200) { - if (checkApiSuccessOrSncakbar(re, "验证失败")) return - } else if (re.code == 200) { - setMyUserProfileCache(Client.myUserProfile as User) - } - }) - - function openChatInfoDialog(chat: Chat) { - setChatInfo(chat) - chatInfoDialogRef.current!.open = true - } - - function openChatFragment(chatId: string) { - setCurrentChatId(chatId) - setIsShowChatFragment(true) - } - - async function openUserInfoDialog(user: User | string) { - const re = await Client.invoke("Chat.getIdForPrivate", { - token: data.access_token, - target: typeof user == 'object' ? user.id : user, - }) - if (re.code != 200) { - checkApiSuccessOrSncakbar(re, '获取对话失败') - return - } - - openChatInfoDialog(re.data as Chat) - /* if (typeof user == 'object') { - setUserInfo(user) - } else { - setUserInfo(await DataCaches.getUserProfile(user)) - - } - userProfileDialogRef.current!.open = true */ - } - // deno-lint-ignore no-window - window.openUserInfoDialog = openUserInfoDialog - // deno-lint-ignore no-window - window.openChatInfoDialog = openChatInfoDialog - - return ( -
- - { - // 聊天页面 - } -
- setIsShowChatFragment(false)} - key={currentChatId} - openChatInfoDialog={openChatInfoDialog} - target={currentChatId} /> -
-
- - - - - - { - setCurrentChatId(id) - setIsShowChatFragment(true) - }} - chat={chatInfo} /> - - - - - - - - - { - ({ - Recents: "最近对话", - Contacts: "收藏对话", - AllChats: "所有对话", - })[navigationItemSelected] - } -
- { - EventBus.emit('RecentsList.updateRecents') - EventBus.emit('ContactsList.updateContacts') - EventBus.emit('AllChatsList.updateAllChats') - }} style={{ - margin: "0", - }}> - - - - addContactDialogRef.current!.open = true}>添加收藏对话 - createGroupDialogRef.current!.open = true}>创建群组 - - - - - - -
- { - // 侧边列表 - } - - - 最近对话 - 收藏对话 - 全部对话 - -
- ) -} \ No newline at end of file diff --git a/client/ui/AvatarMySelf.tsx b/client/ui/AvatarMySelf.tsx new file mode 100644 index 0000000..52110fc --- /dev/null +++ b/client/ui/AvatarMySelf.tsx @@ -0,0 +1,31 @@ +import { UserMySelf } from "lingchair-client-protocol" +import useAsyncEffect from "../utils/useAsyncEffect.ts" +import Avatar from "./Avatar.tsx" +import getClient from "../getClient.ts" + +interface Args extends React.HTMLAttributes { + avatarRef?: React.LegacyRef +} +export default function AvatarMySelf({ + avatarRef, + ...props +}: Args) { + if (!avatarRef) avatarRef = React.useRef(null) + const [args, setArgs] = React.useState<{ + text: string, + src: string, + }>({ + text: '', + src: '', + }) + + useAsyncEffect(async () => { + const mySelf = await UserMySelf.getMySelfOrThrow(getClient()) + setArgs({ + text: mySelf.getNickName(), + src: getClient().getUrlForFileByHash(mySelf.getAvatarFileHash(), '')! + }) + }) + + return +} \ No newline at end of file diff --git a/client/ui/Main.tsx b/client/ui/Main.tsx new file mode 100644 index 0000000..d8931e9 --- /dev/null +++ b/client/ui/Main.tsx @@ -0,0 +1,103 @@ +import isMobileUI from "../utils/isMobileUI.ts" +import AvatarMySelf from "./AvatarMySelf.tsx" +import MainSharedContext from './MainSharedContext.ts' + +export default function Main() { + const sharedContext = { + openChatFragment: React.useRef() + } + return ( + +
+ { + /** + * Default: 侧边列表提供列表切换 + */ + !isMobileUI() ? + + + + + + + + + + + + + + 添加收藏对话 + 创建群组 + + + + /** + * Mobile: 底部导航栏提供列表切换 + */ + : + { + ({ + Recents: "最近对话", + Contacts: "收藏对话", + AllChats: "所有对话", + })['Recents'] + } +
+ + + + 添加收藏对话 + 创建群组 + + + + + + +
+ } + { + /** + * Mobile: 指定高度的容器 + * Default: 侧边列表 + */ + + } + { + /** + * Mobile: 底部导航栏提供列表切换 + * Default: 侧边列表提供列表切换 + */ + isMobileUI() && + 最近对话 + 收藏对话 + 全部对话 + + } +
+
+ ) +} \ No newline at end of file diff --git a/client/ui/MainSharedContext.ts b/client/ui/MainSharedContext.ts new file mode 100644 index 0000000..6edbde0 --- /dev/null +++ b/client/ui/MainSharedContext.ts @@ -0,0 +1,5 @@ +import { createContext } from 'react' + +const MainSharedContext = createContext({}) + +export default MainSharedContext diff --git a/client/ui/TextFieldCustom.tsx b/client/ui/TextFieldCustom.tsx deleted file mode 100644 index 79615a4..0000000 --- a/client/ui/TextFieldCustom.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import * as React from 'react' -import { $, TextField } from "mdui" - -interface Args extends React.HTMLAttributes { - -} - -export default function TextFieldCustom({ ...prop }: Args) { - // deno-lint-ignore no-explicit-any - const textField = React.useRef(null) - - React.useEffect(() => { - const shadow = (textField.current as TextField).shadowRoot - // $(shadow).find('textarea') - }) - - return -} \ No newline at end of file diff --git a/client/ui/custom-elements/chat-file.ts b/client/ui/chat-elements/chat-file.ts similarity index 100% rename from client/ui/custom-elements/chat-file.ts rename to client/ui/chat-elements/chat-file.ts diff --git a/client/ui/custom-elements/chat-image.ts b/client/ui/chat-elements/chat-image.ts similarity index 58% rename from client/ui/custom-elements/chat-image.ts rename to client/ui/chat-elements/chat-image.ts index b1e6aa2..c9aff3e 100644 --- a/client/ui/custom-elements/chat-image.ts +++ b/client/ui/chat-elements/chat-image.ts @@ -1,4 +1,4 @@ -import openImageViewer from "../openImageViewer.ts" +import openImageViewer from "../../utils/openImageViewer.ts" import { $ } from 'mdui/jq' @@ -56,37 +56,3 @@ customElements.define('chat-image', class extends HTMLElement { this.update() } }) - -document.body.appendChild(new DOMParser().parseFromString(` - - - - - - - - - -`, 'text/html').body.firstChild as Node) diff --git a/client/ui/custom-elements/chat-mention.ts b/client/ui/chat-elements/chat-mention.ts similarity index 78% rename from client/ui/custom-elements/chat-mention.ts rename to client/ui/chat-elements/chat-mention.ts index 93f546b..c0de054 100644 --- a/client/ui/custom-elements/chat-mention.ts +++ b/client/ui/chat-elements/chat-mention.ts @@ -1,7 +1,5 @@ import { $ } from 'mdui' -import DataCaches from "../../api/DataCaches.ts" -import { snackbar } from "../snackbar.ts" - +import showSnackbar from "../../utils/showSnackbar.ts"; customElements.define('chat-mention', class extends HTMLElement { declare link: HTMLAnchorElement static observedAttributes = ['user-id'] @@ -32,20 +30,18 @@ customElements.define('chat-mention', class extends HTMLElement { const text = $(this).attr('text') this.link.style.fontStyle = '' if (chatId) { - const chat = await DataCaches.getChatInfo(chatId) - this.link.textContent = chat?.title + this.link.onclick = (e) => { e.stopPropagation() // deno-lint-ignore no-window - window.openChatInfoDialog(chat) + } } else if (userId) { - const user = await DataCaches.getUserProfile(userId) - this.link.textContent = user?.nickname + this.link.onclick = (e) => { e.stopPropagation() // deno-lint-ignore no-window - window.openUserInfoDialog(user) + } } @@ -55,9 +51,8 @@ customElements.define('chat-mention', class extends HTMLElement { this.link.style.fontStyle = 'italic' this.link.onclick = (e) => { e.stopPropagation() - snackbar({ + showSnackbar({ message: "该提及没有指定用户或者对话!", - placement: 'top', }) } } diff --git a/client/ui/custom-elements/chat-quote.ts b/client/ui/chat-elements/chat-quote.ts similarity index 100% rename from client/ui/custom-elements/chat-quote.ts rename to client/ui/chat-elements/chat-quote.ts diff --git a/client/ui/custom-elements/chat-text-container.ts b/client/ui/chat-elements/chat-text-container.ts similarity index 100% rename from client/ui/custom-elements/chat-text-container.ts rename to client/ui/chat-elements/chat-text-container.ts diff --git a/client/ui/custom-elements/chat-text.ts b/client/ui/chat-elements/chat-text.ts similarity index 100% rename from client/ui/custom-elements/chat-text.ts rename to client/ui/chat-elements/chat-text.ts diff --git a/client/ui/custom-elements/chat-video.ts b/client/ui/chat-elements/chat-video.ts similarity index 100% rename from client/ui/custom-elements/chat-video.ts rename to client/ui/chat-elements/chat-video.ts diff --git a/client/ui/chat/ChatFragment.tsx b/client/ui/chat/ChatFragment.tsx deleted file mode 100644 index d8b65dc..0000000 --- a/client/ui/chat/ChatFragment.tsx +++ /dev/null @@ -1,730 +0,0 @@ -import { Tab, Tabs, TextField } from "mdui" -import { $ } from "mdui/jq" -import useEventListener from "../useEventListener.ts" -import Element_Message from "./Message.tsx" -import MessageContainer from "./MessageContainer.tsx" - -import * as React from 'react' -import Client from "../../api/Client.ts" -import Message from "../../api/client_data/Message.ts" -import Chat from "../../api/client_data/Chat.ts" -import data from "../../Data.ts" -import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts" -import useAsyncEffect from "../useAsyncEffect.ts" -import * as marked from 'marked' -import DOMPurify from 'dompurify' -import randomUUID from "../../randomUUID.ts" -import EventBus from "../../EventBus.ts" -import User from "../../api/client_data/User.ts" - -import PreferenceLayout from '../preference/PreferenceLayout.tsx' -import PreferenceHeader from '../preference/PreferenceHeader.tsx' -import PreferenceStore from '../preference/PreferenceStore.ts' -import SwitchPreference from '../preference/SwitchPreference.tsx' -import SelectPreference from '../preference/SelectPreference.tsx' -import TextFieldPreference from '../preference/TextFieldPreference.tsx' -import Preference from '../preference/Preference.tsx' -import GroupSettings from "../../api/client_data/GroupSettings.ts" -import PreferenceUpdater from "../preference/PreferenceUpdater.ts" -import SystemMessage from "./SystemMessage.tsx" -import JoinRequestsList from "./JoinRequestsList.tsx" -import getUrlForFileByHash from "../../getUrlForFileByHash.ts" -import escapeHTML from "../../escapeHtml.ts" -import GroupMembersList from "./GroupMembersList.tsx" -import isMobileUI from "../isMobileUI.ts" - -interface Args extends React.HTMLAttributes { - target: string - showReturnButton?: boolean - openChatInfoDialog: (chat: Chat) => void - onReturnButtonClicked?: () => void - openUserInfoDialog: (user: User | string) => void -} - -const sanitizeConfig = { - ALLOWED_TAGS: [ - "chat-image", - "chat-video", - "chat-file", - 'chat-text', - "chat-link", - 'chat-mention', - 'chat-quote', - ], - ALLOWED_ATTR: [ - 'underline', - 'em', - 'src', - 'alt', - 'href', - 'name', - 'user-id', - 'chat-id', - ], -} - -const markedInstance = new marked.Marked({ - renderer: { - blockquote({ text }) { - return `${escapeHTML(text)}` - }, - text({ text }) { - return `${escapeHTML(text)}` - }, - em({ text }) { - return `${escapeHTML(text)}` - }, - heading({ tokens, depth: _depth }) { - const text = this.parser.parseInline(tokens) - return `${escapeHTML(text)}` - }, - image({ text, 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: ``, - })?.[fileType] || `` - } else - switch (type) { - case "UserMention": - return `PH` - case "ChatMention": - return `PH` - } - return `${escapeHTML(`[无效数据 (<${text}>=${href})]`)}` - }, - } -}) - -interface MduiTabFitSizeArgs extends React.HTMLAttributes { - value: string -} -function MduiTabFitSize({ children, ...props }: MduiTabFitSizeArgs) { - return - {children} - -} - -export default function ChatFragment({ target, showReturnButton, onReturnButtonClicked, openChatInfoDialog, openUserInfoDialog, ...props }: Args) { - const [messagesList, setMessagesList] = React.useState([] as Message[]) - const [chatInfo, setChatInfo] = React.useState({ - title: '加载中...', - is_member: true, - is_admin: true, - } as Chat) - - const [tabItemSelected, setTabItemSelected] = React.useState('None') - const tabRef = React.useRef(null) - const chatPanelRef = React.useRef(null) - useEventListener(tabRef, 'change', () => { - tabRef.current != null && setTabItemSelected(tabRef.current!.value as string) - }) - - const containerTabRef = React.useRef(null) - React.useEffect(() => { - $(containerTabRef.current!.shadowRoot).append(``) - $(tabRef.current!.shadowRoot).append(``) - ; (!isMobileUI()) && $(tabRef.current!.shadowRoot).append(``) - }, [target]) - - async function getChatInfoAndInit() { - setMessagesList([]) - page.current = 0 - const re = await Client.invoke('Chat.getInfo', { - token: data.access_token, - target: target, - }) - if (re.code != 200) - return target != '' && checkApiSuccessOrSncakbar(re, "获取对话信息失败") - const chatInfo = re.data as Chat - setChatInfo(chatInfo) - - if (chatInfo.is_member) - await loadMore() - - setTabItemSelected(chatInfo.is_member ? "Chat" : "RequestJoin") - if (re.data!.type == 'group') { - groupPreferenceStore.setState(chatInfo.settings as GroupSettings) - } - setTimeout(() => { - chatPanelRef.current!.scrollTo({ - top: 10000000000, - behavior: "smooth", - }) - }, 500) - } - useAsyncEffect(getChatInfoAndInit, [target]) - - const page = React.useRef(0) - async function loadMore() { - const re = await Client.invoke("Chat.getMessageHistory", { - token: data.access_token, - target, - page: page.current, - }) - - if (checkApiSuccessOrSncakbar(re, "拉取对话记录失败")) - return - const returnMsgs = (re.data!.messages as Message[]).reverse() - page.current++ - if (returnMsgs.length == 0) { - setShowNoMoreMessagesTip(true) - setTimeout(() => setShowNoMoreMessagesTip(false), 1000) - return - } - - const oldest = messagesList[0] - setMessagesList(returnMsgs.concat(messagesList)) - oldest && setTimeout(() => chatPanelRef.current!.scrollTo({ top: $(`#chat_${target}_message_${oldest.id}`).get(0).offsetTop }), 200) - } - - React.useEffect(() => { - interface OnMessageData { - chat: string - msg: Message - } - function callback(data: unknown) { - const { chat, msg } = (data as OnMessageData) - if (target == chat) { - setMessagesList(messagesList.concat([msg])) - if ((chatPanelRef.current!.scrollHeight - chatPanelRef.current!.scrollTop - chatPanelRef.current!.clientHeight) < 130) - setTimeout(() => chatPanelRef.current!.scrollTo({ - top: 10000000000, - behavior: "smooth", - }), 100) - } - } - - Client.on('Client.onMessage', callback) - return () => { - Client.off('Client.onMessage', callback) - } - }) - - const inputRef = React.useRef(null) - const [showLoadingMoreMessagesTip, setShowLoadingMoreMessagesTip] = React.useState(false) - const [showNoMoreMessagesTip, setShowNoMoreMessagesTip] = React.useState(false) - - const [isMessageSending, setIsMessageSending] = React.useState(false) - - const cachedFiles = React.useRef({} as { [fileName: string]: ArrayBuffer }) - const cachedFileNamesCount = React.useRef({} as { [fileName: string]: number }) - async function sendMessage() { - let text = inputRef.current!.value - if (text.trim() == '') return - const sendingFilesSnackbar = snackbar({ - message: `发送消息到 [${chatInfo.title}]...`, - placement: 'top', - autoCloseDelay: 0, - }) - let i = 1 - let i2 = 0 - const sendingFilesSnackbarId = setInterval(() => { - const len = Object.keys(cachedFiles.current).filter((fileName) => text.indexOf(fileName)).length - sendingFilesSnackbar.textContent = i2 == len ? `发送消息到 [${chatInfo.title}]... (${i}s)` : `上传第 ${i2}/${len} 文件到 [${chatInfo.title}]... (${i}s)` - i++ - }, 1000) - function endSendingSnack() { - clearTimeout(sendingFilesSnackbarId) - sendingFilesSnackbar.open = false - } - Client.socket?.once('disconnect', () => endSendingSnack()) - try { - setIsMessageSending(true) - for (const fileName of Object.keys(cachedFiles.current)) { - if (text.indexOf(fileName) != -1) { - const re = await Client.uploadFileLikeApi( - fileName, - cachedFiles.current[fileName] - ) - if (checkApiSuccessOrSncakbar(re, `文件[${fileName}] 上传失败`)) { - endSendingSnack() - return setIsMessageSending(false) - } - text = text.replaceAll('(' + fileName + ')', '(tws://file?hash=' + re.data!.file_hash as string + ')') - i2++ - } - } - - const re = await Client.invoke("Chat.sendMessage", { - token: data.access_token, - target, - text, - }, 5000) - if (checkApiSuccessOrSncakbar(re, "发送失败")) { - endSendingSnack() - return setIsMessageSending(false) - } - inputRef.current!.value = '' - cachedFiles.current = {} - } catch (e) { - snackbar({ - message: '发送失败: ' + (e as Error).message, - placement: 'top', - }) - } - setIsMessageSending(false) - endSendingSnack() - } - - const attachFileInputRef = React.useRef(null) - const uploadChatAvatarRef = React.useRef(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(`![图片](${name})`) - else if (type.startsWith('video/')) - insertText(`![Video=${name}](${name})`) - else - insertText(`![File=${name}](${name})`) - } - 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) - } - attachFileInputRef.current!.value = '' - }) - useEventListener(uploadChatAvatarRef, 'change', async (_e) => { - const file = uploadChatAvatarRef.current!.files?.[0] as File - if (file == null) return - - let re = await Client.uploadFileLikeApi( - 'avatar', - file - ) - if (checkApiSuccessOrSncakbar(re, "上传失败")) return - const hash = re.data!.file_hash - re = await Client.invoke("Chat.setAvatar", { - token: data.access_token, - target: target, - file_hash: hash, - }) - uploadChatAvatarRef.current!.value = '' - - if (checkApiSuccessOrSncakbar(re, "修改失败")) return - snackbar({ - message: "修改成功 (刷新页面以更新)", - placement: "top", - }) - }) - - const groupPreferenceStore = new PreferenceStore() - groupPreferenceStore.setOnUpdate(async (value, oldvalue) => { - const re = await Client.invoke("Chat.updateSettings", { - token: data.access_token, - target, - settings: value, - }) - if (checkApiSuccessOrSncakbar(re, "更新设定失败")) return groupPreferenceStore.setState(oldvalue) - }) - - return ( -
- - { - showReturnButton && - } - - { - chatInfo.is_member ? <> - {chatInfo.title} - {chatInfo.type == 'group' && chatInfo.is_admin && 加入请求} - {chatInfo.type == 'group' && 群组成员} - - : {chatInfo.title} - } - {chatInfo.type == 'group' && 设置} - - -
- { - page.current = 0 - getChatInfoAndInit() - }} style={{ - alignSelf: 'center', - marginLeft: '5px', - marginRight: '5px', - }}> - openChatInfoDialog(chatInfo)} style={{ - alignSelf: 'center', - marginLeft: '5px', - marginRight: '5px', - }}> - - -
- { - const re = await Client.invoke("Chat.sendJoinRequest", { - token: data.access_token, - target: target, - }) - if (re.code != 200) - return checkApiSuccessOrSncakbar(re, "发送加入请求失败") - - snackbar({ - message: '发送成功!', - placement: 'top', - }) - }}>请求加入对话 -
-
- { - if (!chatInfo.is_member) return - const scrollTop = (e.target as HTMLDivElement).scrollTop - if (scrollTop == 0 && !showLoadingMoreMessagesTip) { - setShowNoMoreMessagesTip(false) - setShowLoadingMoreMessagesTip(true) - await loadMore() - setShowLoadingMoreMessagesTip(false) - } - }}> -
-
- - 加载中... -
-
- 沒有更多消息啦~ -
-
- - { - (() => { - let date = new Date(0) - let user: string - function timeAddZeroPrefix(t: number) { - if (t >= 0 && t < 10) - return '0' + t - return t + '' - } - return messagesList.map((msg) => { - const lastDate = date - const lastUser = user - date = new Date(msg.time) - user = msg.user_id - - const shouldShowTime = msg.user_id != null && - (date.getMinutes() != lastDate.getMinutes() || date.getDate() != lastDate.getDate() || date.getMonth() != lastDate.getMonth() || date.getFullYear() != lastDate.getFullYear()) - - const msgElement = msg.user_id == null ?
: - - return ( - <> - { - shouldShowTime - && -
- { - (date.getFullYear() != lastDate.getFullYear() ? `${date.getFullYear()}年` : '') - + `${date.getMonth() + 1}月` - + `${date.getDate()}日` - + ` ${timeAddZeroPrefix(date.getHours())}:${timeAddZeroPrefix(date.getMinutes())}` - } -
-
- } - { - msgElement - } - - ) - }) - })() - } - - { - // 输入框 - } -
{ - 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 - for (const item of e.dataTransfer.items) { - 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 && re.ok) - 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) - } - } - } - }}> - { - if (inputRef.current?.value.trim() == '') - cachedFiles.current = {} - }} onKeyDown={(event) => { - if (event.ctrlKey && event.key == 'Enter') - 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={{ - marginRight: '10px', - marginTop: '3px', - marginBottom: '3px', - }}> - { - attachFileInputRef.current!.click() - }}> - sendMessage()} loading={isMessageSending}> -
- -
-
- - { - chatInfo.type == 'group' && - - - } - { - chatInfo.type == 'group' && - {chatInfo.is_admin && } - - } - -
- -
- { - chatInfo.type == 'group' && - - - { - uploadChatAvatarRef.current!.click() - }} /> - - - - - {/* - - { - groupPreferenceStore.state.new_member_join_method == 'answered_and_allowed_by_admin' - && - } */} - - - } - { - chatInfo.type == 'private' && ( -
- 未制作 -
- ) - } -
- -
- -
-
- -
- ) -} diff --git a/client/ui/chat/GroupMembersList.tsx b/client/ui/chat/GroupMembersList.tsx deleted file mode 100644 index 21a21f2..0000000 --- a/client/ui/chat/GroupMembersList.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { TextField } from "mdui" -import useEventListener from "../useEventListener.ts" -import React from "react" -import useAsyncEffect from "../useAsyncEffect.ts" -import Client from "../../api/Client.ts" -import { checkApiSuccessOrSncakbar } from "../snackbar.ts" -import data from "../../Data.ts" -import EventBus from "../../EventBus.ts" -import GroupMembersListItem from "./GroupMembersListItem.tsx" -import User from "../../api/client_data/User.ts" -import Chat from "../../api/client_data/Chat.ts" - -interface Args extends React.HTMLAttributes { - chat: Chat -} - -export default function GroupMembersList({ - chat, - ...props -}: Args) { - const target = chat.id - const searchRef = React.useRef(null) - const [searchText, setSearchText] = React.useState('') - const [groupMembers, setGroupMembers] = React.useState([]) - - useEventListener(searchRef, 'input', (e) => { - setSearchText((e.target as unknown as TextField).value) - }) - - React.useEffect(() => { - async function updateMembers() { - const re = await Client.invoke("Chat.getMembers", { - token: data.access_token, - target: target, - }) - if (re.code != 200) - return checkApiSuccessOrSncakbar(re, "获取群组成员列表失败") - - setGroupMembers(re.data!.members as User[]) - } - updateMembers() - EventBus.on('GroupMembersList.updateMembers', () => updateMembers()) - const id = setInterval(() => updateMembers(), 15 * 1000) - return () => { - clearInterval(id) - EventBus.off('GroupMembersList.updateMembers') - } - }, [target]) - - return - - - EventBus.emit('GroupMembersList.updateMembers')}>刷新 - - { - groupMembers.filter((user) => - searchText == '' || - user.nickname.includes(searchText) || - user.username?.includes(searchText) || - user.id.includes(searchText) - ).map((v) => - - ) - } - -} \ No newline at end of file diff --git a/client/ui/chat/GroupMembersListItem.tsx b/client/ui/chat/GroupMembersListItem.tsx deleted file mode 100644 index 6f759da..0000000 --- a/client/ui/chat/GroupMembersListItem.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { $, dialog } from "mdui" -import Avatar from "../Avatar.tsx" -import React from 'react' -import User from "../../api/client_data/User.ts" -import getUrlForFileByHash from "../../getUrlForFileByHash.ts" -import Client from "../../api/Client.ts" -import data from "../../Data.ts" -import Chat from "../../api/client_data/Chat.ts" -import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts" -import EventBus from "../../EventBus.ts" - -interface Args extends React.HTMLAttributes { - user: User - chat: Chat -} - -export default function GroupMembersListItem({ user, chat }: Args) { - const { id, nickname, avatar_file_hash } = user - - const itemRef = React.useRef(null) - return ( - { - // deno-lint-ignore no-window - window.openUserInfoDialog(user) - }}> - {nickname} - -
- { - e.stopPropagation() - dialog({ - headline: "移除群组成员", - description: `确定要移除 ${nickname} 吗?`, - closeOnEsc: true, - closeOnOverlayClick: true, - actions: [ - { - text: "取消", - onClick: () => { - return true - }, - }, - { - text: "确定", - onClick: () => { - ; (async () => { - const re = await Client.invoke("Chat.removeMembers", { - token: data.access_token, - chat_id: chat.id, - user_ids: [ - id - ], - }) - if (re.code != 200) - checkApiSuccessOrSncakbar(re, "移除群组成员失败") - EventBus.emit('GroupMembersList.updateMembers') - snackbar({ - message: `已移除 ${nickname}`, - placement: "top", - /* action: "撤销操作", - onActionClick: async () => { - const re = await Client.invoke("User.addContacts", { - token: data.access_token, - targets: ls, - }) - if (re.code != 200) - checkApiSuccessOrSncakbar(re, "恢复所选收藏失败") - EventBus.emit('ContactsList.updateContacts') - } */ - }) - })() - return true - }, - } - ], - }) - }}> -
-
- ) -} diff --git a/client/ui/chat/JoinRequestsList.tsx b/client/ui/chat/JoinRequestsList.tsx deleted file mode 100644 index f34dc82..0000000 --- a/client/ui/chat/JoinRequestsList.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { TextField } from "mdui" -import useEventListener from "../useEventListener.ts" -import React from "react" -import Client from "../../api/Client.ts" -import { checkApiSuccessOrSncakbar } from "../snackbar.ts" -import data from "../../Data.ts" -import EventBus from "../../EventBus.ts" -import JoinRequest from "../../api/client_data/JoinRequest.ts" -import JoinRequestsListItem from "./JoinRequestsListItem.tsx" -import Chat from "../../api/client_data/Chat.ts" - -interface Args extends React.HTMLAttributes { - chat: Chat -} - -export default function GroupMembersList({ - chat, - ...props -}: Args) { - const target = chat.id - const searchRef = React.useRef(null) - const [searchText, setSearchText] = React.useState('') - const [updateJoinRequests, setUpdateJoinRequests] = React.useState([]) - - useEventListener(searchRef, 'input', (e) => { - setSearchText((e.target as unknown as TextField).value) - }) - - React.useEffect(() => { - async function updateJoinRequests() { - const re = await Client.invoke("Chat.getJoinRequests", { - token: data.access_token, - target: target, - }) - if (re.code != 200) - return checkApiSuccessOrSncakbar(re, "获取加入请求列表失败") - - setUpdateJoinRequests(re.data!.join_requests as JoinRequest[]) - } - updateJoinRequests() - EventBus.on('JoinRequestsList.updateJoinRequests', () => updateJoinRequests()) - const id = setInterval(() => updateJoinRequests(), 15 * 1000) - return () => { - clearInterval(id) - EventBus.off('JoinRequestsList.updateJoinRequests') - } - }, [target]) - - async function removeJoinRequest(userId: string) { - const re = await Client.invoke("Chat.processJoinRequest", { - token: data.access_token, - chat_id: target, - user_id: userId, - action: 'remove', - }) - if (re.code != 200) - return checkApiSuccessOrSncakbar(re, "删除加入请求失败") - - EventBus.emit('JoinRequestsList.updateJoinRequests') - } - async function acceptJoinRequest(userId: string) { - const re = await Client.invoke("Chat.processJoinRequest", { - token: data.access_token, - chat_id: target, - user_id: userId, - action: 'accept', - }) - if (re.code != 200) - return checkApiSuccessOrSncakbar(re, "通过加入请求失败") - - EventBus.emit('JoinRequestsList.updateJoinRequests') - } - - return - - - EventBus.emit('JoinRequestsList.updateJoinRequests')}>刷新 - - { - updateJoinRequests.filter((joinRequest) => - searchText == '' || - joinRequest.title.includes(searchText) || - joinRequest.reason?.includes(searchText) || - joinRequest.user_id.includes(searchText) - ).map((v) => - - ) - } - -} \ No newline at end of file diff --git a/client/ui/chat/JoinRequestsListItem.tsx b/client/ui/chat/JoinRequestsListItem.tsx deleted file mode 100644 index c7c8f59..0000000 --- a/client/ui/chat/JoinRequestsListItem.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { $ } from "mdui/jq" -import Avatar from "../Avatar.tsx" -import React from 'react' -import JoinRequest from "../../api/client_data/JoinRequest.ts" - -interface Args extends React.HTMLAttributes { - joinRequest: JoinRequest - acceptJoinRequest: (userId: string) => any - removeJoinRequest: (userId: string) => any -} - -export default function JoinRequestsListItem({ joinRequest, acceptJoinRequest, removeJoinRequest }: Args) { - const { user_id, title, avatar, reason } = joinRequest - - const itemRef = React.useRef(null) - React.useEffect(() => { - $(itemRef.current!.shadowRoot).find('.headline').css('margin-top', '3px') - }) - return ( - - {title} - - 请求原因: {reason || "无"} -
- acceptJoinRequest(user_id)}> - removeJoinRequest(user_id)}> -
-
- ) -} diff --git a/client/ui/chat/Message.tsx b/client/ui/chat/Message.tsx deleted file mode 100644 index 7a27c45..0000000 --- a/client/ui/chat/Message.tsx +++ /dev/null @@ -1,222 +0,0 @@ -import { Dropdown, Dialog, dialog } from "mdui" -import { $ } from "mdui/jq" -import Client from "../../api/Client.ts" -import Data_Message from "../../api/client_data/Message.ts" -import DataCaches from "../../api/DataCaches.ts" -import Avatar from "../Avatar.tsx" -import copyToClipboard from "../copyToClipboard.ts" -import useAsyncEffect from "../useAsyncEffect.ts" -import useEventListener from "../useEventListener.ts" -import React from "react" -import isMobileUI from "../isMobileUI.ts" -import User from "../../api/client_data/User.ts" -import getUrlForFileByHash from "../../getUrlForFileByHash.ts" -import escapeHTML from "../../escapeHtml.ts" - -function prettyFlatParsedMessage(html: string) { - const elements = new DOMParser().parseFromString(html, 'text/html').body.children - // 纯文本直接处理 - if (elements.length == 0) - return `${escapeHTML(html)}` - let ls: Element[] = [] - let ret = '' - // 第一个元素时, 不会被聚合在一起 - let lastElementType = '' - const textElementTags = [ - 'chat-text', - 'chat-mention', - 'chat-quote', - ] - function checkContinuousElement(tagName: string) { - /* console.log('shangyige ', lastElementType) - console.log("dangqian", tagName) - console.log("上一个元素的类型和当前不一致?", lastElementType != tagName) - console.log("上一个元素的类型和这个元素的类型都属于文本类型", (textElementTags.indexOf(lastElementType) != -1 && textElementTags.indexOf(tagName) != -1)) */ - // 如果上一个元素的类型和当前不一致, 或者上一个元素的类型和这个元素的类型都属于文本类型 (亦或者到最后一步时) 执行 - if ((lastElementType != tagName || (textElementTags.indexOf(lastElementType) != -1 && textElementTags.indexOf(tagName) != -1)) || tagName == 'LAST_CHICKEN') { - /* console.log(tagName, '进入') */ - // 如果上一个元素类型为文本类型, 且当前不是文本类型时, 用文本包裹 - if (textElementTags.indexOf(lastElementType) != -1) { - // 当前的文本类型不应该和上一个分离, 滚出去 - if (textElementTags.indexOf(tagName) != -1) return - /* console.log(tagName, '文字和被') */ - // 由于 chat-mention 不是用内部元素实现的, 因此在这个元素的生成中必须放置占位字符串 - // 尽管显示上占位字符串不会显示, 但是在这里依然是会被处理的, 因为本身还是 innerHTML - - // 当文本非空时, 将文字合并在一起 - if (ls.map((v) => v.innerHTML).join('').trim() != '') - ret += `${ls.map((v) => v.outerHTML).join('')}` - } else - // 非文本类型元素, 各自成块 - ret += ls.map((v) => v.outerHTML).join('') - ls = [] - } - } - for (const e of elements) { - // 当出现非文本元素时, 将文本聚合在一起 - // 如果是其他类型, 虽然也执行聚合, 但是不会有外层包裹 - /* console.log("当前", e, "内容", e.innerHTML) */ - checkContinuousElement(e.nodeName.toLowerCase()) - ls.push(e) - lastElementType = e.nodeName.toLowerCase() - } - // 最后将剩余的转换 - checkContinuousElement('LAST_CHICKEN') - return ret -} - -interface Args extends React.HTMLAttributes { - userId: string - noUserDisplay?: boolean - rawData: string - renderHTML: string - message: Data_Message - openUserInfoDialog: (user: User | string) => void -} - -export default function Message({ userId, rawData, renderHTML, message, openUserInfoDialog, noUserDisplay, ...props }: Args) { - const isAtRight = Client.myUserProfile?.id == userId - - const [nickName, setNickName] = React.useState("") - const [avatarUrl, setAvatarUrl] = React.useState("") - - useAsyncEffect(async () => { - const user = await DataCaches.getUserProfile(userId) - setNickName(user.nickname) - setAvatarUrl(getUrlForFileByHash(user?.avatar_file_hash)) - }, [userId]) - - const dropDownRef = React.useRef(null) - useEventListener(dropDownRef, 'closed', () => { - setDropDownOpen(false) - }) - - const [isDropDownOpen, setDropDownOpen] = React.useState(false) - - /* const [isUsingFullDisplay, setIsUsingFullDisplay] = React.useState(false) */ - - /* React.useEffect(() => { - const text = $(dropDownRef.current as HTMLElement).find('#msg').text().trim() - setIsUsingFullDisplay(text == '' || ( - rawData.split("tws:\/\/file\?hash=").length == 2 - && /\<\/chat\-(file|image|video)\>(\<\/span\>)?$/.test(renderHTML.trim()) - )) - }, [renderHTML]) */ - - return ( -
{ - if (isMobileUI()) return - e.preventDefault() - setDropDownOpen(!isDropDownOpen) - }} - onClick={(e) => { - if (!isMobileUI()) return - e.preventDefault() - setDropDownOpen(!isDropDownOpen) - }} - style={{ - width: "100%", - display: "flex", - justifyContent: isAtRight ? "flex-end" : "flex-start", - flexDirection: "column" - }} - {...props}> -
- { - // 发送者昵称(左) - isAtRight && - {nickName} - - } - { - // 发送者头像 - } - { - e.stopPropagation() - openUserInfoDialog(userId) - }} /> - { - // 发送者昵称(右) - !isAtRight && - {nickName} - - } -
- - - - { - e.stopPropagation() - setDropDownOpen(false) - }}> - copyToClipboard($(dropDownRef.current as HTMLElement).find('#msg').text().trim())}>复制文字 - copyToClipboard(rawData)}>复制原文 - dialog({ - headline: "原始数据", - body: `${Object.keys(message) - // @ts-ignore 懒 - .map((k) => `${k} = ${message[k]}`) - .join('

')}`, - closeOnEsc: true, - closeOnOverlayClick: true, - actions: [ - { - text: "关闭", - onClick: () => { - return true - }, - } - ] - }).addEventListener('click', (e) => e.stopPropagation())}>原始数据
-
-
-
- -
- ) -} diff --git a/client/ui/chat/MessageContainer.tsx b/client/ui/chat/MessageContainer.tsx deleted file mode 100644 index b97223a..0000000 --- a/client/ui/chat/MessageContainer.tsx +++ /dev/null @@ -1,17 +0,0 @@ -interface Args extends React.HTMLAttributes {} - -export default function MessageContainer({ children, style, ...props }: Args) { - return ( -
- {children} -
- ) -} diff --git a/client/ui/chat/SystemMessage.tsx b/client/ui/chat/SystemMessage.tsx deleted file mode 100644 index ea87fd6..0000000 --- a/client/ui/chat/SystemMessage.tsx +++ /dev/null @@ -1,24 +0,0 @@ - -export default function SystemMessage({ children }: React.HTMLAttributes) { - return ( -
- - {children} - -
- ) -} diff --git a/client/ui/copyToClipboard.ts b/client/ui/copyToClipboard.ts deleted file mode 100644 index 2cb4a7b..0000000 --- a/client/ui/copyToClipboard.ts +++ /dev/null @@ -1,20 +0,0 @@ -export default function copyToClipboard(text: string) { - if (!("via" in window) && navigator.clipboard) - return navigator.clipboard.writeText(text) - return new Promise((res, rej) => { - if (document.hasFocus()) { - const a = document.createElement("textarea") - document.body.appendChild(a) - a.style.position = "fixed" - a.style.clip = "rect(0 0 0 0)" - a.style.top = "10px" - a.value = text - a.select() - document.execCommand("cut", true) - document.body.removeChild(a) - res(null) - } else { - rej() - } - }) -} diff --git a/client/ui/dialog/AddContactDialog.tsx b/client/ui/dialog/AddContactDialog.tsx deleted file mode 100644 index b290f00..0000000 --- a/client/ui/dialog/AddContactDialog.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import * as React from 'react' -import { Button, Dialog, TextField } from "mdui" -import useEventListener from "../useEventListener.ts" -import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts" -import Client from "../../api/Client.ts" - -import * as CryptoJS from 'crypto-js' -import data from "../../Data.ts" -import EventBus from "../../EventBus.ts" - -interface Refs { - addContactDialogRef: React.MutableRefObject -} - -export default function AddContactDialog({ - addContactDialogRef, -}: Refs) { - const inputTargetRef = React.useRef(null) - - async function addContact() { - const re = await Client.invoke("User.addContacts", { - targets: [inputTargetRef.current!.value], - token: data.access_token, - }) - - if (checkApiSuccessOrSncakbar(re, "添加失敗")) return - snackbar({ - message: re.msg, - placement: "top", - }) - EventBus.emit('ContactsList.updateContacts') - - inputTargetRef.current!.value = '' - addContactDialogRef.current!.open = false - } - - return ( - - { - if (event.key == 'Enter') - addContact() - }}> - addContactDialogRef.current!.open = false}>取消 - addContact()}>添加 - - ) -} \ No newline at end of file diff --git a/client/ui/dialog/ChatInfoDialog.tsx b/client/ui/dialog/ChatInfoDialog.tsx deleted file mode 100644 index 120d9d0..0000000 --- a/client/ui/dialog/ChatInfoDialog.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import React from 'react' -import Chat from "../../api/client_data/Chat.ts" -import useAsyncEffect from "../useAsyncEffect.ts" -import Client from "../../api/Client.ts" -import data from "../../Data.ts" -import { dialog, Dialog } from "mdui" -import Avatar from "../Avatar.tsx" -import { checkApiSuccessOrSncakbar } from "../snackbar.ts" -import User from "../../api/client_data/User.ts" -import getUrlForFileByHash from "../../getUrlForFileByHash.ts" -import openImageViewer from "../openImageViewer.ts" -import EventBus from "../../EventBus.ts" - -interface Args extends React.HTMLAttributes { - chat?: Chat - openChatFragment: (id: string) => void - chatInfoDialogRef: React.MutableRefObject - sharedFavouriteChats: Chat[] -} - -export default function ChatInfoDialog({ chat, chatInfoDialogRef, openChatFragment, sharedFavouriteChats }: Args) { - const [favourited, setIsFavourited] = React.useState(false) - - React.useEffect(() => { - setIsFavourited(sharedFavouriteChats.map((v) => v.id).indexOf(chat?.id || '') != -1) - }) - - const [userId, setUserId] = React.useState(null) - useAsyncEffect(async () => { - if (chat?.type == 'private') { - const re = await Client.invoke("Chat.getAnotherUserIdFromPrivate", { - token: data.access_token, - target: chat.id, - }) - if (re.code != 200) - return checkApiSuccessOrSncakbar(re, '获取用户失败') - setUserId(re.data!.user_id as string) - } - }, [chat, sharedFavouriteChats]) - - const avatarUrl = getUrlForFileByHash(chat?.avatar_file_hash as string) - - return ( - -
- avatarUrl && openImageViewer(avatarUrl)} /> -
- {chat?.title} - ({chat?.type}) ID: {chat?.type == 'private' ? userId : chat?.id} -
-
- - - - dialog({ - headline: favourited ? "取消收藏对话" : "收藏对话", - description: favourited ? "确定从收藏对话列表中移除吗? (虽然这不会导致聊天记录丢失)" : "确定要添加到收藏对话列表吗?", - actions: [ - { - text: "取消", - onClick: () => { - return true - }, - }, - { - text: "确定", - onClick: () => { - ; (async () => { - const re = await Client.invoke(favourited ? "User.removeContacts" : "User.addContacts", { - token: data.access_token, - targets: [ - chat!.id - ], - }) - if (re.code != 200) - checkApiSuccessOrSncakbar(re, favourited ? "取消收藏失败" : "收藏失败") - EventBus.emit('ContactsList.updateContacts') - })() - return true - }, - } - ], - })}>{favourited ? '取消收藏' : '收藏对话'} - { - chatInfoDialogRef.current!.open = false - openChatFragment(chat!.id) - }}>打开对话 - -
- ) -} diff --git a/client/ui/dialog/CreateGroupDialog.tsx b/client/ui/dialog/CreateGroupDialog.tsx deleted file mode 100644 index 908ed73..0000000 --- a/client/ui/dialog/CreateGroupDialog.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import * as React from 'react' -import { Button, Dialog, TextField } from "mdui" -import useEventListener from "../useEventListener.ts" -import { checkApiSuccessOrSncakbar, snackbar } from "../snackbar.ts" -import Client from "../../api/Client.ts" - -import * as CryptoJS from 'crypto-js' -import data from "../../Data.ts" -import EventBus from "../../EventBus.ts" - -interface Refs { - createGroupDialogRef: React.MutableRefObject -} - -export default function CreateGroupDialog({ - createGroupDialogRef, -}: Refs) { - const inputGroupTitleRef = React.useRef(null) - const inputGroupNameRef = React.useRef(null) - - async function createGroup() { - const re = await Client.invoke("Chat.createGroup", { - title: inputGroupTitleRef.current!.value, - name: inputGroupNameRef.current!.value, - token: data.access_token, - }) - - if (checkApiSuccessOrSncakbar(re, "添加失敗")) return - snackbar({ - message: "创建成功!", - placement: "top", - }) - EventBus.emit('ContactsList.updateContacts') - - inputGroupTitleRef.current!.value = '' - inputGroupNameRef.current!.value = '' - createGroupDialogRef.current!.open = false - } - - return ( - - { - if (event.key == 'Enter') - inputGroupNameRef.current!.click() - }}> - { - if (event.key == 'Enter') - createGroup() - }}> - createGroupDialogRef.current!.open = false}>取消 - createGroup()}>创建 - - ) -} \ No newline at end of file diff --git a/client/ui/dialog/LoginDialog.tsx b/client/ui/dialog/LoginDialog.tsx deleted file mode 100644 index dd1ea4d..0000000 --- a/client/ui/dialog/LoginDialog.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from 'react' -import { Button, Dialog, TextField } from "mdui" -import useEventListener from "../useEventListener.ts" -import { checkApiSuccessOrSncakbar } from "../snackbar.ts" -import Client from "../../api/Client.ts" - -import * as CryptoJS from 'crypto-js' -import data from "../../Data.ts"; - -interface Refs { - loginInputAccountRef: React.MutableRefObject - loginInputPasswordRef: React.MutableRefObject - loginDialogRef: React.MutableRefObject - registerDialogRef: React.MutableRefObject -} - -export default function LoginDialog({ - loginInputAccountRef, - loginInputPasswordRef, - loginDialogRef, - registerDialogRef -}: Refs) { - const loginButtonRef = React.useRef