mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 08:41:08 +00:00
refactor: extract logic from chat page
This commit is contained in:
parent
20fdc2203d
commit
5954928834
9 changed files with 203 additions and 104 deletions
|
@ -1,5 +1,7 @@
|
||||||
import { version } from '$app/environment';
|
import { version } from '$app/environment';
|
||||||
import type { BlahRichText } from '$lib/richText';
|
import type { BlahRichText } from '$lib/richText';
|
||||||
|
import { messageFromBlah, type Chat, type Message } from '$lib/types';
|
||||||
|
import { readable, type Readable } from 'svelte/store';
|
||||||
import type { BlahKeyPair, BlahSignedPayload } from '../crypto';
|
import type { BlahKeyPair, BlahSignedPayload } from '../crypto';
|
||||||
import type { BlahAuth, BlahMessage, BlahRoomInfo, BlahUserJoinMessage } from '../structures';
|
import type { BlahAuth, BlahMessage, BlahRoomInfo, BlahUserJoinMessage } from '../structures';
|
||||||
import { BlahError } from './error';
|
import { BlahError } from './error';
|
||||||
|
@ -11,14 +13,14 @@ export class BlahChatServerConnection {
|
||||||
private static commonHeaders = { 'x-blah-client': `Weblah/${version}` };
|
private static commonHeaders = { 'x-blah-client': `Weblah/${version}` };
|
||||||
|
|
||||||
private endpoint: string;
|
private endpoint: string;
|
||||||
private keypair?: BlahKeyPair;
|
private keypair: BlahKeyPair | null;
|
||||||
|
|
||||||
private webSocket: WebSocket | null = null;
|
private webSocket: WebSocket | null = null;
|
||||||
private messageListeners: Map<string, Set<(message: BlahSignedPayload<BlahMessage>) => void>> =
|
private messageListeners: Map<string, Set<(message: BlahSignedPayload<BlahMessage>) => void>> =
|
||||||
new Map();
|
new Map();
|
||||||
private webSocketRetryTimeout: number | null = null;
|
private webSocketRetryTimeout: number | null = null;
|
||||||
|
|
||||||
constructor(endpoint: string, keypair?: BlahKeyPair) {
|
constructor(endpoint: string, keypair: BlahKeyPair | null = null) {
|
||||||
this.endpoint = endpoint;
|
this.endpoint = endpoint;
|
||||||
this.keypair = keypair;
|
this.keypair = keypair;
|
||||||
}
|
}
|
||||||
|
@ -147,11 +149,29 @@ export class BlahChatServerConnection {
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
if (!this.webSocket) this.webSocket = this.createWebSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.webSocketRetryTimeout) clearTimeout(this.webSocketRetryTimeout);
|
||||||
|
this.webSocket?.close();
|
||||||
|
this.webSocket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
changeKeyPair(keypair: BlahKeyPair | null) {
|
||||||
|
this.keypair = keypair;
|
||||||
|
if (this.webSocket) {
|
||||||
|
this.disconnect();
|
||||||
|
this.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
subscribeRoom(
|
subscribeRoom(
|
||||||
roomId: string,
|
roomId: string,
|
||||||
onNewMessage: (message: BlahSignedPayload<BlahMessage>) => void
|
onNewMessage: (message: BlahSignedPayload<BlahMessage>) => void
|
||||||
): { unsubscribe: () => void } {
|
): { unsubscribe: () => void } {
|
||||||
if (!this.webSocket) this.webSocket = this.createWebSocket();
|
if (!this.webSocket) throw new Error('Must connect to WebSocket before subscribing to rooms');
|
||||||
|
|
||||||
const listeners = this.messageListeners.get(roomId) ?? new Set();
|
const listeners = this.messageListeners.get(roomId) ?? new Set();
|
||||||
listeners.add(onNewMessage);
|
listeners.add(onNewMessage);
|
||||||
|
@ -164,12 +184,43 @@ export class BlahChatServerConnection {
|
||||||
if (listeners.size === 0) {
|
if (listeners.size === 0) {
|
||||||
this.messageListeners.delete(roomId);
|
this.messageListeners.delete(roomId);
|
||||||
}
|
}
|
||||||
if (this.messageListeners.size === 0) {
|
|
||||||
if (this.webSocketRetryTimeout) clearTimeout(this.webSocketRetryTimeout);
|
|
||||||
this.webSocket?.close();
|
|
||||||
this.webSocket = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chat(chatId: string): {
|
||||||
|
info: Readable<Chat>;
|
||||||
|
messages: Readable<Message[]>;
|
||||||
|
sendMessage: (brt: BlahRichText) => Promise<void>;
|
||||||
|
} {
|
||||||
|
const info = readable<Chat>(
|
||||||
|
{ server: this.endpoint, id: chatId, name: '', type: 'group' },
|
||||||
|
(set) => {
|
||||||
|
this.fetchRoomInfo(chatId).then((room) => {
|
||||||
|
set({ server: this.endpoint, id: chatId, name: room.title, type: 'group' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const messages = readable<Message[]>([], (set, update) => {
|
||||||
|
this.fetchRoomHistory(chatId).then((history) =>
|
||||||
|
update((messages) => [
|
||||||
|
...history.map(messageFromBlah).toSorted((a, b) => a.date.getTime() - b.date.getTime()),
|
||||||
|
...messages
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { unsubscribe } = this.subscribeRoom(chatId, (message) => {
|
||||||
|
update((messages) => [...messages, messageFromBlah(message)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendMessage = async (brt: BlahRichText) => {
|
||||||
|
await this.sendMessage(chatId, brt);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { info, messages, sendMessage };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
63
src/lib/chatServers.ts
Normal file
63
src/lib/chatServers.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import { persisted } from 'svelte-persisted-store';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { BlahChatServerConnection } from './blah/connection/chatServer';
|
||||||
|
import { BlahKeyPair, type EncodedBlahKeyPair } from './blah/crypto';
|
||||||
|
import { currentKeyPair } from './keystore';
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
chatServers.subscribe(this.onChatServersChange.bind(this));
|
||||||
|
currentKeyPair.subscribe(this.onKeyPairChange.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
connectAll(keypair?: BlahKeyPair) {
|
||||||
|
for (const endpoint of get(chatServers)) {
|
||||||
|
const connection = new BlahChatServerConnection(endpoint, keypair);
|
||||||
|
this.connections.set(endpoint, connection);
|
||||||
|
connection.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
disconnectAll() {
|
||||||
|
for (const connection of this.connections.values()) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
this.connections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onKeyPairChange(encodedKeyPair: EncodedBlahKeyPair) {
|
||||||
|
this.keypair = await BlahKeyPair.fromEncoded(encodedKeyPair);
|
||||||
|
for (const connection of this.connections.values()) {
|
||||||
|
connection.changeKeyPair(this.keypair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onChatServersChange(newChatServers: string[]) {
|
||||||
|
// Disconnect from chat servers that are no longer in the list
|
||||||
|
for (const [endpoint, connection] of this.connections.entries()) {
|
||||||
|
if (!newChatServers.includes(endpoint)) {
|
||||||
|
connection.disconnect();
|
||||||
|
this.connections.delete(endpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getConnection(endpoint: string): BlahChatServerConnection | null {
|
||||||
|
return this.connections.get(endpoint) ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chatServerConnectionPool = new ChatServerConnectionPool();
|
|
@ -1,68 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import { page } from '$app/stores';
|
|
||||||
import BgPattern from '$lib/components/BgPattern.svelte';
|
|
||||||
import { messageFromBlah, type Chat, type Message } from '$lib/types';
|
|
||||||
import { onDestroy } from 'svelte';
|
|
||||||
import ChatHeader from './ChatHeader.svelte';
|
|
||||||
import ChatHistory from './ChatHistory.svelte';
|
|
||||||
import ChatInput from './ChatInput.svelte';
|
|
||||||
import { BlahChatServerConnection } from '$lib/blah/connection/chatServer';
|
|
||||||
import { currentKeyPair } from '$lib/keystore';
|
|
||||||
import { BlahKeyPair, type EncodedBlahKeyPair } from '$lib/blah/crypto';
|
|
||||||
import { browser } from '$app/environment';
|
|
||||||
|
|
||||||
const roomId = $page.params.chatId;
|
|
||||||
|
|
||||||
let chat: Chat = {
|
|
||||||
id: roomId,
|
|
||||||
name: '',
|
|
||||||
type: 'group'
|
|
||||||
};
|
|
||||||
let messages: Message[] = [];
|
|
||||||
let server: BlahChatServerConnection;
|
|
||||||
let unsubscribe: () => void = () => {};
|
|
||||||
|
|
||||||
async function initConnection(encodedKeyPair?: EncodedBlahKeyPair) {
|
|
||||||
messages = [];
|
|
||||||
const keyPair = encodedKeyPair ? await BlahKeyPair.fromEncoded(encodedKeyPair) : undefined;
|
|
||||||
server = new BlahChatServerConnection('https://blah.oxa.li/api', keyPair);
|
|
||||||
unsubscribe();
|
|
||||||
unsubscribe = server.subscribeRoom(roomId, (message) => {
|
|
||||||
messages = [...messages, messageFromBlah(message)];
|
|
||||||
}).unsubscribe;
|
|
||||||
return server;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadChatInfo(server: BlahChatServerConnection) {
|
|
||||||
const room = await server.fetchRoomInfo(roomId);
|
|
||||||
chat = {
|
|
||||||
id: roomId,
|
|
||||||
name: room.title,
|
|
||||||
type: 'group'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadChatHistory(server: BlahChatServerConnection) {
|
|
||||||
const history = await server.fetchRoomHistory(roomId);
|
|
||||||
messages = [
|
|
||||||
...history.map(messageFromBlah).toSorted((a, b) => a.date.getTime() - b.date.getTime()),
|
|
||||||
...messages
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadChat(server: BlahChatServerConnection) {
|
|
||||||
return await Promise.allSettled([loadChatInfo(server), loadChatHistory(server)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (browser) initConnection($currentKeyPair).then((server) => loadChat(server));
|
|
||||||
|
|
||||||
onDestroy(() => unsubscribe());
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex h-full w-full flex-col justify-stretch">
|
|
||||||
<ChatHeader {chat} outsideUnreadCount={263723} />
|
|
||||||
<BgPattern class="flex-1" pattern="charlieBrown">
|
|
||||||
<ChatHistory {messages} mySenderId={$currentKeyPair?.id} />
|
|
||||||
</BgPattern>
|
|
||||||
<ChatInput {roomId} {server} />
|
|
||||||
</div>
|
|
45
src/routes/(app)/chats/[server]/[chatId]/+page.svelte
Normal file
45
src/routes/(app)/chats/[server]/[chatId]/+page.svelte
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import BgPattern from '$lib/components/BgPattern.svelte';
|
||||||
|
import { messageFromBlah, type Chat, type Message } from '$lib/types';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import ChatHeader from './ChatHeader.svelte';
|
||||||
|
import ChatHistory from './ChatHistory.svelte';
|
||||||
|
import ChatInput from './ChatInput.svelte';
|
||||||
|
import { BlahChatServerConnection } from '$lib/blah/connection/chatServer';
|
||||||
|
import { currentKeyPair } from '$lib/keystore';
|
||||||
|
import { BlahKeyPair, type EncodedBlahKeyPair } from '$lib/blah/crypto';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { chatServerConnectionPool } from '$lib/chatServers';
|
||||||
|
import ServiceMessage from '$lib/components/ServiceMessage.svelte';
|
||||||
|
import ChatPage from './ChatPage.svelte';
|
||||||
|
|
||||||
|
$: roomId = $page.params.chatId;
|
||||||
|
|
||||||
|
let serverEndpoint: string = '';
|
||||||
|
$: {
|
||||||
|
const endpointString = decodeURIComponent($page.params.server);
|
||||||
|
serverEndpoint = endpointString.startsWith('http')
|
||||||
|
? endpointString
|
||||||
|
: `https://${endpointString}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let server: BlahChatServerConnection | null;
|
||||||
|
$: {
|
||||||
|
if (browser) {
|
||||||
|
server = chatServerConnectionPool.getConnection(serverEndpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex h-full w-full flex-col items-center justify-center">
|
||||||
|
{#if server}
|
||||||
|
{@const { info, messages, sendMessage } = server.chat(roomId)}
|
||||||
|
<ChatPage {info} {messages} on:sendMessage={sendMessage} />
|
||||||
|
{:else}
|
||||||
|
<ServiceMessage>
|
||||||
|
To view this chat, you need to connect to chat server
|
||||||
|
<span class="font-semibold">{serverEndpoint}</span>.
|
||||||
|
</ServiceMessage>
|
||||||
|
{/if}
|
||||||
|
</div>
|
|
@ -4,7 +4,7 @@
|
||||||
import type { Chat } from '$lib/types';
|
import type { Chat } from '$lib/types';
|
||||||
import { AvatarBeam } from 'svelte-boring-avatars';
|
import { AvatarBeam } from 'svelte-boring-avatars';
|
||||||
|
|
||||||
export let chat: Chat;
|
export let info: Chat;
|
||||||
export let outsideUnreadCount = 0;
|
export let outsideUnreadCount = 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -28,10 +28,10 @@
|
||||||
<span class="sr-only">Back</span>
|
<span class="sr-only">Back</span>
|
||||||
</Button>
|
</Button>
|
||||||
<div class="flex flex-1 flex-col justify-center text-center sm:order-2 sm:text-start">
|
<div class="flex flex-1 flex-col justify-center text-center sm:order-2 sm:text-start">
|
||||||
<h3 class="truncate text-sm font-semibold">{chat.name}</h3>
|
<h3 class="truncate text-sm font-semibold">{info.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="sm:order-1">
|
<div class="sm:order-1">
|
||||||
<AvatarBeam size={30} name={chat.name} />
|
<AvatarBeam size={30} name={info.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="absolute inset-y-0 start-0 hidden w-2 cursor-default sm:block" href="/">
|
<a class="absolute inset-y-0 start-0 hidden w-2 cursor-default sm:block" href="/">
|
|
@ -1,19 +1,16 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { BlahChatServerConnection } from '$lib/blah/connection/chatServer';
|
|
||||||
import { BlahError } from '$lib/blah/connection/error';
|
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import RichTextInput from '$lib/components/RichTextInput.svelte';
|
import RichTextInput from '$lib/components/RichTextInput.svelte';
|
||||||
import { deltaToBlahRichText } from '$lib/richText';
|
import { deltaToBlahRichText, type BlahRichText } from '$lib/richText';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
import type { Delta, Editor } from 'typewriter-editor';
|
import type { Delta, Editor } from 'typewriter-editor';
|
||||||
|
|
||||||
export let roomId: string;
|
|
||||||
export let server: BlahChatServerConnection | undefined;
|
|
||||||
|
|
||||||
let editor: Editor | undefined;
|
let editor: Editor | undefined;
|
||||||
let delta: Delta;
|
let delta: Delta;
|
||||||
let plainText: string = '';
|
let plainText: string = '';
|
||||||
let form: HTMLFormElement | null = null;
|
let form: HTMLFormElement | null = null;
|
||||||
let sendDisabled = false;
|
|
||||||
|
const dispatch = createEventDispatcher<{ sendMessage: BlahRichText }>();
|
||||||
|
|
||||||
function onKeyboardSubmit() {
|
function onKeyboardSubmit() {
|
||||||
editor?.select(null);
|
editor?.select(null);
|
||||||
|
@ -21,27 +18,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!server || plainText.trim() === '') return;
|
if (plainText.trim() === '') return;
|
||||||
|
|
||||||
const brt = deltaToBlahRichText(delta);
|
const brt = deltaToBlahRichText(delta);
|
||||||
sendDisabled = true;
|
dispatch('sendMessage', brt);
|
||||||
try {
|
|
||||||
await server.sendMessage(roomId, brt);
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
if (e instanceof BlahError && e.statusCode === 403) {
|
|
||||||
// TODO: Actual error handling
|
|
||||||
await server.joinRoom(roomId);
|
|
||||||
await server.sendMessage(roomId, brt);
|
|
||||||
} else {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sendDisabled = false;
|
|
||||||
plainText = '';
|
plainText = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
$: sendDisabled = !server;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
|
@ -75,7 +58,7 @@
|
||||||
keyboardSubmitMethod="enter"
|
keyboardSubmitMethod="enter"
|
||||||
on:keyboardSubmit={onKeyboardSubmit}
|
on:keyboardSubmit={onKeyboardSubmit}
|
||||||
/>
|
/>
|
||||||
<Button class="p-1.5" variant="primary" type="submit" disabled={sendDisabled}>
|
<Button class="p-1.5" variant="primary" type="submit">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
25
src/routes/(app)/chats/[server]/[chatId]/ChatPage.svelte
Normal file
25
src/routes/(app)/chats/[server]/[chatId]/ChatPage.svelte
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { Readable } from 'svelte/store';
|
||||||
|
|
||||||
|
import type { Chat, Message } from '$lib/types';
|
||||||
|
import BgPattern from '$lib/components/BgPattern.svelte';
|
||||||
|
import { currentKeyPair } from '$lib/keystore';
|
||||||
|
|
||||||
|
import ChatHeader from './ChatHeader.svelte';
|
||||||
|
import ChatHistory from './ChatHistory.svelte';
|
||||||
|
import ChatInput from './ChatInput.svelte';
|
||||||
|
import type { BlahRichText } from '$lib/richText';
|
||||||
|
|
||||||
|
export let info: Readable<Chat>;
|
||||||
|
export let messages: Readable<Message[]>;
|
||||||
|
|
||||||
|
type $$Events = {
|
||||||
|
sendMessage: BlahRichText;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ChatHeader info={$info} outsideUnreadCount={263723} />
|
||||||
|
<BgPattern class="flex-1" pattern="charlieBrown">
|
||||||
|
<ChatHistory messages={$messages} mySenderId={$currentKeyPair?.id} />
|
||||||
|
</BgPattern>
|
||||||
|
<ChatInput on:sendMessage />
|
Loading…
Add table
Reference in a new issue