diff --git a/src/lib/blah/connection/chatServer.ts b/src/lib/blah/connection/chatServer.ts index 5da8a13..3e329f8 100644 --- a/src/lib/blah/connection/chatServer.ts +++ b/src/lib/blah/connection/chatServer.ts @@ -95,6 +95,23 @@ export class BlahChatServerConnection { await this.apiCall('POST', `/room/${id}/admin`, payload); } + private async fetchRoomList(filter: 'joined' | 'public'): Promise { + const { rooms }: { rooms: BlahRoomInfo[] } = await this.apiCall( + 'GET', + `/room?filter=${filter}` + ); + return rooms; + } + + async fetchJoinedRooms(): Promise { + if (!this.keypair) return []; + return await this.fetchRoomList('joined'); + } + + async discoverRooms(): Promise { + return await this.fetchRoomList('public'); + } + async sendMessage(room: string, message: BlahRichText): Promise { 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) => 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) => void): { unsubscribe: () => void; } { - if (!this.webSocket) - throw new Error('Must connect to WebSocket before subscribing to messages'); - this.serverListeners.add(onNewMessage); return { diff --git a/src/lib/blah/structures/roomInfo.ts b/src/lib/blah/structures/roomInfo.ts index a56f01e..9061585 100644 --- a/src/lib/blah/structures/roomInfo.ts +++ b/src/lib/blah/structures/roomInfo.ts @@ -1,3 +1,8 @@ +import type { BlahSignedPayload } from '../crypto'; +import type { BlahMessage } from './message'; + export type BlahRoomInfo = { + ruuid: string; title: string; + last_chat?: BlahSignedPayload; }; diff --git a/src/lib/chatList.ts b/src/lib/chatList.ts new file mode 100644 index 0000000..3685c98 --- /dev/null +++ b/src/lib/chatList.ts @@ -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; + + constructor() { + this.chatList = writable([]); + } + + 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, 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 { + return readable([], (set) => { + const unsubscribe = manager.chatList.subscribe(set); + return unsubscribe; + }); +} diff --git a/src/lib/chatServers.ts b/src/lib/chatServers.ts index 1761a5d..e13b73c 100644 --- a/src/lib/chatServers.ts +++ b/src/lib/chatServers.ts @@ -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('weblah-chat-servers', ['https://blah.oxa.li/api']); class ChatServerConnectionPool { private connections: Map = 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); } } } diff --git a/src/lib/types/chat.ts b/src/lib/types/chat.ts index 8f14463..e5697c6 100644 --- a/src/lib/types/chat.ts +++ b/src/lib/types/chat.ts @@ -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 + }; +} diff --git a/src/routes/(app)/ChatList.svelte b/src/routes/(app)/ChatList.svelte index 3d52eeb..452365b 100644 --- a/src/routes/(app)/ChatList.svelte +++ b/src/routes/(app)/ChatList.svelte @@ -1,115 +1,22 @@
    - - - - - - - - - + {#if $chatList} + {#each $chatList as chat} + + {/each} + {/if}
diff --git a/src/routes/(app)/ChatListItem.svelte b/src/routes/(app)/ChatListItem.svelte index b58b4af..f97c991 100644 --- a/src/routes/(app)/ChatListItem.svelte +++ b/src/routes/(app)/ChatListItem.svelte @@ -7,12 +7,21 @@ import { blahRichTextToPlainText } from '$lib/richText'; export let chat: Chat; + + let urlSafeEndpoint: string; + $: { + const url = new URL(chat.server); + urlSafeEndpoint = encodeURIComponent(url.hostname + url.pathname); + }
  • - +
    diff --git a/src/routes/(app)/chats/[server]/[chatId]/+page.svelte b/src/routes/(app)/chats/[server]/[chatId]/+page.svelte index dc843cd..3a5396d 100644 --- a/src/routes/(app)/chats/[server]/[chatId]/+page.svelte +++ b/src/routes/(app)/chats/[server]/[chatId]/+page.svelte @@ -28,7 +28,7 @@
    {#if server} {@const { info, messages, sendMessage } = useChat(server, roomId)} - + sendMessage(e.detail)} /> {:else} To view this chat, you need to connect to chat server diff --git a/src/routes/(app)/chats/[server]/[chatId]/ChatInput.svelte b/src/routes/(app)/chats/[server]/[chatId]/ChatInput.svelte index 144c099..f59c846 100644 --- a/src/routes/(app)/chats/[server]/[chatId]/ChatInput.svelte +++ b/src/routes/(app)/chats/[server]/[chatId]/ChatInput.svelte @@ -28,7 +28,7 @@
    diff --git a/src/routes/(app)/chats/[server]/[chatId]/ChatPage.svelte b/src/routes/(app)/chats/[server]/[chatId]/ChatPage.svelte index 3cd4006..dbca17d 100644 --- a/src/routes/(app)/chats/[server]/[chatId]/ChatPage.svelte +++ b/src/routes/(app)/chats/[server]/[chatId]/ChatPage.svelte @@ -13,13 +13,13 @@ export let info: Readable; export let messages: Readable; - type $$Events = { - sendMessage: BlahRichText; - }; + interface $$Events { + sendMessage: CustomEvent; + } - +