mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 00:31:08 +00:00
feat: chat list (joined)
This commit is contained in:
parent
48c5ed4687
commit
9e899bbb27
10 changed files with 162 additions and 126 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
68
src/lib/chatList.ts
Normal 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;
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,115 +1,22 @@
|
|||
<script>
|
||||
import { browser } from '$app/environment';
|
||||
import { useChatList } from '$lib/chatList';
|
||||
import { chatServerConnectionPool } from '$lib/chatServers';
|
||||
import ChatListHeader from './ChatListHeader.svelte';
|
||||
import ChatListItem from './ChatListItem.svelte';
|
||||
|
||||
const chatList = browser ? useChatList(chatServerConnectionPool.chatList) : null;
|
||||
</script>
|
||||
|
||||
<div class="flex h-[100dvh] flex-col justify-stretch">
|
||||
<ChatListHeader />
|
||||
<div class="min-h-0 flex-1 touch-pan-y overflow-y-auto">
|
||||
<ul>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: 'room-1',
|
||||
name: 'Blah IM Interest Group',
|
||||
lastMessage: {
|
||||
sender: { id: '1', name: 'septs' },
|
||||
content: '窄带通信吧,asn1 + bzip2 效果还是可以的',
|
||||
date: new Date('2024-08-29T02:11Z')
|
||||
},
|
||||
unreadCount: 3
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: 'room-2',
|
||||
name: 'Laoself Chat',
|
||||
lastMessage: {
|
||||
sender: { id: '10', name: 'Richard Luo 🐱' },
|
||||
content: '如果durov没事 那你是不是又可以拖延症复发了(',
|
||||
date: new Date('2024-08-29T02:11Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: 'room-3',
|
||||
name: 'Ubuntu中文',
|
||||
lastMessage: {
|
||||
sender: { id: '15', name: 'chen jianhao' },
|
||||
content:
|
||||
'就什么也没安装,昨晚好好的,就打开了pycharm而已,写完代码直接按设置好的快捷键alt+s 关机 重启就不行了',
|
||||
date: new Date('2024-08-29T02:27Z')
|
||||
},
|
||||
unreadCount: 827469
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '1',
|
||||
name: 'septs',
|
||||
lastMessage: {
|
||||
sender: { id: '1', name: 'septs' },
|
||||
content: '验证 checksum 是否正确的代价还是可以接受的',
|
||||
date: new Date('2024-08-28T02:54Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '2',
|
||||
name: 'oxa',
|
||||
lastMessage: {
|
||||
sender: { id: '2', name: 'oxa' },
|
||||
content: '但似乎现在大家都讨厌 pgp ,觉得太复杂',
|
||||
date: new Date('2024-08-28T02:37Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '3',
|
||||
name: 'omo',
|
||||
lastMessage: {
|
||||
sender: { id: '3', name: 'omo' },
|
||||
content: '我對 revalidate 的理解是不經過 cache 直接重拉一遍',
|
||||
date: new Date('2024-08-28T02:11Z')
|
||||
},
|
||||
unreadCount: 8
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '4',
|
||||
name: 'Inno Aiolos',
|
||||
lastMessage: {
|
||||
sender: { id: '4', name: 'Inno Aiolos' },
|
||||
content: '至少得把信息分发给所有广播自己是这个public key的destination',
|
||||
date: new Date('2024-07-28T02:11Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '5',
|
||||
name: 'Gary です',
|
||||
lastMessage: {
|
||||
sender: { id: '5', name: 'Gary です' },
|
||||
content: '没必要8,长毛象那样挺麻烦的',
|
||||
date: new Date('2023-07-28T02:11Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '6',
|
||||
name: 'Chtholly Nota Seniorious',
|
||||
lastMessage: {
|
||||
sender: { id: '6', name: 'Chtholly Nota Seniorious' },
|
||||
content: '遥遥领先!\n隔壁 nostr 最开始没有注意到这个问题,然后被狂灌置顶 spam',
|
||||
date: new Date('2022-07-28T02:11Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if $chatList}
|
||||
{#each $chatList as chat}
|
||||
<ChatListItem {chat} />
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
<li
|
||||
class="relative after:absolute after:bottom-0 after:end-0 after:start-14 after:border-t-[0.5px] after:border-ss-secondary"
|
||||
>
|
||||
<a href="/chats/{chat.id}" class="flex h-20 cursor-default items-center gap-2 px-2">
|
||||
<a
|
||||
href="/chats/{urlSafeEndpoint}/{chat.id}"
|
||||
class="flex h-20 cursor-default items-center gap-2 px-2"
|
||||
>
|
||||
<div class="size-10">
|
||||
<AvatarBeam size={40} name={chat.name} />
|
||||
</div>
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
<div class="flex h-full w-full flex-col items-center justify-center">
|
||||
{#if server}
|
||||
{@const { info, messages, sendMessage } = useChat(server, roomId)}
|
||||
<ChatPage {info} {messages} on:sendMessage={sendMessage} />
|
||||
<ChatPage {info} {messages} on:sendMessage={(e) => sendMessage(e.detail)} />
|
||||
{:else}
|
||||
<ServiceMessage>
|
||||
To view this chat, you need to connect to chat server
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
</script>
|
||||
|
||||
<form
|
||||
class="flex items-end gap-2 border-t border-ss-secondary bg-sb-primary p-2 shadow-sm"
|
||||
class="flex w-full items-end gap-2 border-t border-ss-secondary bg-sb-primary p-2 shadow-sm"
|
||||
bind:this={form}
|
||||
on:submit|preventDefault={submit}
|
||||
>
|
||||
|
|
|
@ -13,13 +13,13 @@
|
|||
export let info: Readable<Chat>;
|
||||
export let messages: Readable<Message[]>;
|
||||
|
||||
type $$Events = {
|
||||
sendMessage: BlahRichText;
|
||||
};
|
||||
interface $$Events {
|
||||
sendMessage: CustomEvent<BlahRichText>;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ChatHeader info={$info} outsideUnreadCount={263723} />
|
||||
<BgPattern class="flex-1" pattern="charlieBrown">
|
||||
<BgPattern class="w-full flex-1" pattern="charlieBrown">
|
||||
<ChatHistory messages={$messages} mySenderId={$currentKeyPair?.id} />
|
||||
</BgPattern>
|
||||
<ChatInput on:sendMessage />
|
||||
|
|
Loading…
Add table
Reference in a new issue