feat: chat list (joined)

This commit is contained in:
Shibo Lyu 2024-09-04 05:28:10 +08:00
parent 48c5ed4687
commit 9e899bbb27
10 changed files with 162 additions and 126 deletions

View file

@ -95,6 +95,23 @@ export class BlahChatServerConnection {
await this.apiCall('POST', `/room/${id}/admin`, payload);
}
private async fetchRoomList(filter: 'joined' | 'public'): Promise<BlahRoomInfo[]> {
const { rooms }: { rooms: BlahRoomInfo[] } = await this.apiCall(
'GET',
`/room?filter=${filter}`
);
return rooms;
}
async fetchJoinedRooms(): Promise<BlahRoomInfo[]> {
if (!this.keypair) return [];
return await this.fetchRoomList('joined');
}
async discoverRooms(): Promise<BlahRoomInfo[]> {
return await this.fetchRoomList('public');
}
async sendMessage(room: string, message: BlahRichText): Promise<void> {
if (!this.keypair) throw new Error('Must send message with a keypair');
const payload: BlahMessage = { room, rich_text: message, typ: 'chat' };
@ -153,7 +170,7 @@ export class BlahChatServerConnection {
}
connect() {
if (!this.webSocket) this.webSocket = this.createWebSocket();
if (!this.webSocket && this.keypair) this.webSocket = this.createWebSocket();
}
disconnect() {
@ -174,8 +191,6 @@ export class BlahChatServerConnection {
roomId: string,
onNewMessage: (message: BlahSignedPayload<BlahMessage>) => void
): { unsubscribe: () => void } {
if (!this.webSocket) throw new Error('Must connect to WebSocket before subscribing to rooms');
const listeners = this.roomListeners.get(roomId) ?? new Set();
listeners.add(onNewMessage);
this.roomListeners.set(roomId, listeners);
@ -194,9 +209,6 @@ export class BlahChatServerConnection {
subscribe(onNewMessage: (message: BlahSignedPayload<BlahMessage>) => void): {
unsubscribe: () => void;
} {
if (!this.webSocket)
throw new Error('Must connect to WebSocket before subscribing to messages');
this.serverListeners.add(onNewMessage);
return {

View file

@ -1,3 +1,8 @@
import type { BlahSignedPayload } from '../crypto';
import type { BlahMessage } from './message';
export type BlahRoomInfo = {
ruuid: string;
title: string;
last_chat?: BlahSignedPayload<BlahMessage>;
};

68
src/lib/chatList.ts Normal file
View file

@ -0,0 +1,68 @@
import { readable, writable, type Readable, type Writable } from 'svelte/store';
import { chatFromBlah, type Chat } from './types';
import type { BlahMessage, BlahRoomInfo } from './blah/structures';
import type { BlahSignedPayload } from './blah/crypto';
export class ChatListManager {
chatList: Writable<Chat[]>;
constructor() {
this.chatList = writable<Chat[]>([]);
}
private sortChats(chatList: Chat[]) {
chatList.sort(
(a, b) =>
(b.lastMessage?.date ?? new Date(1970, 0, 1)).getTime() ??
-(a.lastMessage?.date ?? new Date(1970, 0, 1)).getTime()
);
}
ingestChats(chats: BlahRoomInfo[], serverEndpoint: string) {
this.chatList.update((chatList) => {
for (const chat of chats) {
const newChat = chatFromBlah(chat, serverEndpoint);
const existing = chatList.find((c) => c.id === chat.ruuid);
if (existing) {
existing.name = newChat.name;
existing.lastMessage = newChat.lastMessage ?? existing.lastMessage;
} else {
chatList.push(newChat);
console.log('new chat added to list', newChat);
}
}
this.sortChats(chatList);
return chatList;
});
}
ingestMessage(message: BlahSignedPayload<BlahMessage>, serverEndpoint: string) {
this.chatList.update((chatList) => {
const chat = chatList.find((c) => c.id === message.signee.payload.room);
if (chat) {
const newChat = chatFromBlah(
{ ruuid: chat.id, title: chat.name, last_chat: message },
serverEndpoint
);
chat.lastMessage = newChat.lastMessage ?? chat.lastMessage;
}
this.sortChats(chatList);
return chatList;
});
}
leaveChatServers(serverEndpoints: string[]) {
this.chatList.update((chatList) => {
return chatList.filter((chat) => serverEndpoints.includes(chat.server));
});
}
}
export function useChatList(manager: ChatListManager): Readable<Chat[]> {
return readable<Chat[]>([], (set) => {
const unsubscribe = manager.chatList.subscribe(set);
return unsubscribe;
});
}

View file

@ -3,23 +3,44 @@ import { get } from 'svelte/store';
import { BlahChatServerConnection } from './blah/connection/chatServer';
import { BlahKeyPair, type EncodedBlahKeyPair } from './blah/crypto';
import { currentKeyPair } from './keystore';
import { ChatListManager } from './chatList';
import { browser } from '$app/environment';
export const chatServers = persisted<string[]>('weblah-chat-servers', ['https://blah.oxa.li/api']);
class ChatServerConnectionPool {
private connections: Map<string, BlahChatServerConnection> = new Map();
private keypair: BlahKeyPair | null = null;
chatList: ChatListManager = new ChatListManager();
constructor() {
chatServers.subscribe(this.onChatServersChange.bind(this));
currentKeyPair.subscribe(this.onKeyPairChange.bind(this));
if (browser) {
chatServers.subscribe(this.onChatServersChange.bind(this));
currentKeyPair.subscribe(this.onKeyPairChange.bind(this));
}
}
connectAll(keypair?: BlahKeyPair) {
private createAndConnect(endpoint: string) {
const connection = new BlahChatServerConnection(endpoint, this.keypair);
this.connections.set(endpoint, connection);
connection.connect();
this.setupChatList(connection);
}
private fetchJoinedRooms(connection: BlahChatServerConnection) {
connection
.fetchJoinedRooms()
.then((rooms) => this.chatList.ingestChats(rooms, connection.endpoint));
}
private setupChatList(connection: BlahChatServerConnection) {
this.fetchJoinedRooms(connection);
connection.subscribe((message) => this.chatList.ingestMessage(message, connection.endpoint));
}
connectAll() {
for (const endpoint of get(chatServers)) {
const connection = new BlahChatServerConnection(endpoint, keypair);
this.connections.set(endpoint, connection);
connection.connect();
this.createAndConnect(endpoint);
}
}
disconnectAll() {
@ -33,24 +54,27 @@ class ChatServerConnectionPool {
this.keypair = await BlahKeyPair.fromEncoded(encodedKeyPair);
for (const connection of this.connections.values()) {
connection.changeKeyPair(this.keypair);
connection.connect();
this.fetchJoinedRooms(connection);
}
}
private async onChatServersChange(newChatServers: string[]) {
// Disconnect from chat servers that are no longer in the list
const disconnectedEndpoints: string[] = [];
for (const [endpoint, connection] of this.connections.entries()) {
if (!newChatServers.includes(endpoint)) {
connection.disconnect();
this.connections.delete(endpoint);
disconnectedEndpoints.push(endpoint);
}
}
this.chatList.leaveChatServers(disconnectedEndpoints);
// Connect to chat servers that are in the list but not yet connected
for (const endpoint of newChatServers) {
if (!this.connections.has(endpoint)) {
const connection = new BlahChatServerConnection(endpoint, this.keypair);
this.connections.set(endpoint, connection);
connection.connect();
this.createAndConnect(endpoint);
}
}
}

View file

@ -1,4 +1,5 @@
import type { Message } from './message';
import type { BlahRoomInfo } from '$lib/blah/structures';
import { messageFromBlah, type Message } from './message';
export type Chat = {
server: string;
@ -9,3 +10,13 @@ export type Chat = {
lastMessage?: Message;
unreadCount?: number;
};
export function chatFromBlah(room: BlahRoomInfo, serverEndpoint: string): Chat {
return {
server: serverEndpoint,
id: room.ruuid,
name: room.title,
type: 'group',
lastMessage: room.last_chat ? messageFromBlah(room.last_chat) : undefined
};
}