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
This commit is contained in:
Shibo Lyu 2025-05-29 01:56:04 +08:00
parent e410dc915f
commit 75239c865d
9 changed files with 119 additions and 56 deletions

View file

@ -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",

10
pnpm-lock.yaml generated
View file

@ -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

View file

@ -0,0 +1 @@
export * from './manager.svelte';

View file

@ -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<BlahMessage>[] = $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<void> {
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<BlahMessage>[] } = 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;
}
}

View file

@ -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;
}

View file

@ -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<Response> {
private async fetchWithAuthHeader(url: string | URL, init?: RequestInit): Promise<Response> {
const authHeader = await this.generateAuthHeader();
return fetch(url, {
...init,
@ -84,19 +81,27 @@ export class BlahChatServerConnection {
public async apiCall<P, R>(
method: 'POST' | 'GET',
path: `/${string}`,
path: `/${string}` | [`/${string}`, { [query: string]: string | null | undefined }],
payload?: P,
signOptions?: Omit<SignOptions, 'identityKeyID'>
): Promise<R> {
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
});
}

View file

@ -1,4 +1,3 @@
export * from './connection';
export * from './error';
export * from './messageManager.svelte';
export * from './roomManager.svelte';

View file

@ -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<BlahMessage>[] = $state([]);
constructor(connection: BlahChatServerConnection, roomID: string) {
this.connection = connection;
this.roomID = roomID;
}
async sendMessage(message: BlahRichText): Promise<void> {
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<BlahMessage>[] } = 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;
}
}

View file

@ -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