From 75239c865d0717bd4afc8e3af4bde93724d2e328 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Thu, 29 May 2025 01:56:04 +0800 Subject: [PATCH] refactor: Update @blah-im/core to 0.10.0 and refactor message manager - Move MessageManager to src/lib/blah/chat with sectioning logic - Remove old messageManager from connection - Add message sectioning utility - Update apiCall to support query params - Update chat exports and types --- package.json | 2 +- pnpm-lock.yaml | 10 ++-- src/lib/blah/chat/index.ts | 1 + src/lib/blah/chat/manager.svelte.ts | 58 +++++++++++++++++++ src/lib/blah/chat/sectioning.ts | 41 +++++++++++++ src/lib/blah/connection/connection.ts | 21 ++++--- src/lib/blah/connection/index.ts | 1 - .../blah/connection/messageManager.svelte.ts | 35 ----------- src/lib/chat.ts | 6 -- 9 files changed, 119 insertions(+), 56 deletions(-) create mode 100644 src/lib/blah/chat/index.ts create mode 100644 src/lib/blah/chat/manager.svelte.ts create mode 100644 src/lib/blah/chat/sectioning.ts delete mode 100644 src/lib/blah/connection/messageManager.svelte.ts diff --git a/package.json b/package.json index 4403c77..a88deb0 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "vitest": "^3.1.4" }, "dependencies": { - "@blah-im/core": "^0.9.0", + "@blah-im/core": "^0.10.0", "@zeabur/svelte-adapter": "^1.0.0", "bits-ui": "^1.5.3", "canonicalize": "^2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bab0afe..c8323eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@blah-im/core': - specifier: ^0.9.0 - version: 0.9.0 + specifier: ^0.10.0 + version: 0.10.0 '@zeabur/svelte-adapter': specifier: ^1.0.0 version: 1.0.0(@sveltejs/kit@2.21.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.33.0)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1)))(svelte@5.33.0)(vite@6.3.5(jiti@2.4.2)(lightningcss@1.30.1))) @@ -133,8 +133,8 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@blah-im/core@0.9.0': - resolution: {integrity: sha512-DJ3TjBNuPTsnyk9FSM2v3+mLHOX9W0JEKYPvMYyhHFfjAJTuXEZV2TmPWP9legGnm3bUCJFHrFec+ew9hMwuTw==} + '@blah-im/core@0.10.0': + resolution: {integrity: sha512-AQTOi7LOdP3KW6tBpIuz9s/z59mpXY5O9dmeH8gQqMxX2S9gfcKCqq+38Kf847WJtrX08+8ehykA2EI42qtxmw==} '@esbuild/aix-ppc64@0.19.12': resolution: {integrity: sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==} @@ -2104,7 +2104,7 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 - '@blah-im/core@0.9.0': + '@blah-im/core@0.10.0': dependencies: zod: 3.25.20 diff --git a/src/lib/blah/chat/index.ts b/src/lib/blah/chat/index.ts new file mode 100644 index 0000000..0cfe582 --- /dev/null +++ b/src/lib/blah/chat/index.ts @@ -0,0 +1 @@ +export * from './manager.svelte'; diff --git a/src/lib/blah/chat/manager.svelte.ts b/src/lib/blah/chat/manager.svelte.ts new file mode 100644 index 0000000..5e08ac1 --- /dev/null +++ b/src/lib/blah/chat/manager.svelte.ts @@ -0,0 +1,58 @@ +import type { BlahRichText } from '@blah-im/core/richText'; +import type { BlahSignedPayload } from '@blah-im/core/crypto'; + +import type { BlahChatServerConnection } from '../connection'; +import type { BlahMessage } from '../structures'; +import { sectionMessages, type MessageSection } from './sectioning'; +import { messageFromBlah } from '$lib/types'; + +export class MessageManager { + connection: BlahChatServerConnection; + roomID: string; + rawMessages: BlahSignedPayload[] = $state([]); + sectionedMessages: MessageSection[] = $derived( + sectionMessages(this.rawMessages.map(messageFromBlah)) + ); + #currentMessageID: string | null = $state(null); + + get currentMessageID(): string | null { + return this.#currentMessageID; + } + + set currentMessageID(id: string | null) { + this.currentMessageID = id; + this.fetchRoomHistory(id); + } + + constructor( + connection: BlahChatServerConnection, + roomID: string, + currentMessageID: string | null + ) { + this.connection = connection; + this.roomID = roomID; + this.currentMessageID = currentMessageID; + + $effect(() => this.listen()); + this.fetchRoomHistory(); + } + + async sendMessage(message: BlahRichText): Promise { + if (!this.connection.identity) throw new Error('Must send message with a keypair'); + const payload: BlahMessage = { room: this.roomID, rich_text: message, typ: 'chat' }; + await this.connection.apiCall('POST', `/room/${payload.room}/msg`, payload); + } + + async fetchRoomHistory(skipToken: string | null = null) { + const { items }: { items: BlahSignedPayload[] } = await this.connection.apiCall( + 'GET', + [`/room/${this.roomID}/msg`, { skip_token: skipToken ?? this.currentMessageID }] + ); + this.rawMessages = items; + } + + listen() { + return this.connection.subscribe((m) => this.rawMessages.push(m), { roomID: this.roomID }) + .unsubscribe; + } +} diff --git a/src/lib/blah/chat/sectioning.ts b/src/lib/blah/chat/sectioning.ts new file mode 100644 index 0000000..f33230b --- /dev/null +++ b/src/lib/blah/chat/sectioning.ts @@ -0,0 +1,41 @@ +import type { Message, User } from '$lib/types'; + +const MAX_MESSAGES_PER_SECTION = 10; +const SHOW_TIME_AFTER_SILENCE = 30 * 60 * 1000; + +export type MessageSection = { + sender?: User; + messages: Message[]; + date?: Date; +}; + +export function sectionMessages(messages: Message[]): MessageSection[] { + const sections: MessageSection[] = []; + + let lastMessage: Message | undefined = messages[0]; + let currentSection: MessageSection = { + messages: [], + sender: lastMessage?.sender, + date: lastMessage?.date + }; + + for (const message of messages) { + const reachesMaxMessages = currentSection.messages.length >= MAX_MESSAGES_PER_SECTION; + const senderChanged = message.sender.id !== lastMessage.sender.id; + const silentForTooLong = + message.date.getTime() - lastMessage.date.getTime() > SHOW_TIME_AFTER_SILENCE; + if (reachesMaxMessages || senderChanged || silentForTooLong) { + if (currentSection.messages.length > 0) { + sections.push(currentSection); + } + currentSection = { messages: [], sender: message.sender }; + if (silentForTooLong) currentSection.date = message.date; + } + currentSection.messages.push(message); + lastMessage = message; + } + + sections.push(currentSection); + + return sections; +} diff --git a/src/lib/blah/connection/connection.ts b/src/lib/blah/connection/connection.ts index db17f4e..6add45e 100644 --- a/src/lib/blah/connection/connection.ts +++ b/src/lib/blah/connection/connection.ts @@ -1,12 +1,9 @@ import { version } from '$app/environment'; -import type { BlahRichText } from '@blah-im/core/richText'; -import type { BlahKeyPair, BlahSignedPayload, SignOptions } from '@blah-im/core/crypto'; +import type { BlahSignedPayload, SignOptions } from '@blah-im/core/crypto'; import { blahUserUnregisteredResponseSchema, type BlahAuth, type BlahMessage, - type BlahRoomInfo, - type BlahUserJoinMessage, type BlahUserRegisterRequest } from '../structures'; import { BlahError } from './error'; @@ -54,7 +51,7 @@ export class BlahChatServerConnection { return { Authorization: JSON.stringify(signedAuthPayload) }; } - private async fetchWithAuthHeader(url: string, init?: RequestInit): Promise { + private async fetchWithAuthHeader(url: string | URL, init?: RequestInit): Promise { const authHeader = await this.generateAuthHeader(); return fetch(url, { ...init, @@ -84,19 +81,27 @@ export class BlahChatServerConnection { public async apiCall( method: 'POST' | 'GET', - path: `/${string}`, + path: `/${string}` | [`/${string}`, { [query: string]: string | null | undefined }], payload?: P, signOptions?: Omit ): Promise { if (payload && !this.identity) throw new Error('Must make authorized API call with an identity'); + const url = new URL(typeof path === 'string' ? path : path[0], this.endpoint_); + const query = typeof path === 'string' ? undefined : path[1]; + if (query) { + for (const [key, value] of Object.entries(query)) { + if (value) url.searchParams.append(key, value); + } + } + let response: Response; if (method === 'GET') { if (this.identity) { - response = await this.fetchWithAuthHeader(`${this.endpoint_}${path}`); + response = await this.fetchWithAuthHeader(url); } else { - response = await fetch(`${this.endpoint_}${path}`, { + response = await fetch(url, { headers: BlahChatServerConnection.commonHeaders }); } diff --git a/src/lib/blah/connection/index.ts b/src/lib/blah/connection/index.ts index 9b94810..48a7ba9 100644 --- a/src/lib/blah/connection/index.ts +++ b/src/lib/blah/connection/index.ts @@ -1,4 +1,3 @@ export * from './connection'; export * from './error'; -export * from './messageManager.svelte'; export * from './roomManager.svelte'; diff --git a/src/lib/blah/connection/messageManager.svelte.ts b/src/lib/blah/connection/messageManager.svelte.ts deleted file mode 100644 index 8b357dd..0000000 --- a/src/lib/blah/connection/messageManager.svelte.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { BlahRichText } from '@blah-im/core/richText'; -import type { BlahSignedPayload } from '@blah-im/core/crypto'; - -import type { BlahChatServerConnection } from './connection'; -import type { BlahMessage } from '../structures'; - -export class MessageManager { - connection: BlahChatServerConnection; - roomID: string; - messages: BlahSignedPayload[] = $state([]); - - constructor(connection: BlahChatServerConnection, roomID: string) { - this.connection = connection; - this.roomID = roomID; - } - - async sendMessage(message: BlahRichText): Promise { - if (!this.connection.identity) throw new Error('Must send message with a keypair'); - const payload: BlahMessage = { room: this.roomID, rich_text: message, typ: 'chat' }; - await this.connection.apiCall('POST', `/room/${payload.room}/item`, payload); - } - - async fetchRoomHistory() { - const { items }: { items: BlahSignedPayload[] } = await this.connection.apiCall( - 'GET', - `/room/${this.roomID}/item` - ); - this.messages = items; - } - - listen() { - return this.connection.subscribe((m) => this.messages.push(m), { roomID: this.roomID }) - .unsubscribe; - } -} diff --git a/src/lib/chat.ts b/src/lib/chat.ts index 914e3ad..23b88a0 100644 --- a/src/lib/chat.ts +++ b/src/lib/chat.ts @@ -7,12 +7,6 @@ import { BlahError } from './blah/connection/error'; const MAX_MESSAGES_PER_SECTION = 10; const SHOW_TIME_AFTER_SILENCE = 30 * 60 * 1000; -export type MessageSection = { - sender?: User; - messages: Message[]; - date?: Date; -}; - export function useChat( server: BlahChatServerConnection, chatId: string