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 d8ec26a..0000000 Binary files a/client/icon.ico and /dev/null differ 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