refactor: extract logic from chat page

This commit is contained in:
Shibo Lyu 2024-09-04 03:01:00 +08:00
parent 20fdc2203d
commit 5954928834
9 changed files with 203 additions and 104 deletions

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

View file

@ -0,0 +1,68 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import { formatUnreadCount } from '$lib/formatters';
import type { Chat } from '$lib/types';
import { AvatarBeam } from 'svelte-boring-avatars';
export let info: Chat;
export let outsideUnreadCount = 0;
</script>
<div
class="relative z-10 box-border flex min-h-[calc(3rem+1px)] w-full items-center gap-2 border-b border-ss-secondary bg-sb-primary p-2 shadow-sm"
>
<Button href="/" class="rounded-full sm:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="-me-0.5 -ms-1 size-5"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
{#if outsideUnreadCount}
<span class="text-xs text-sf-tertiary">{formatUnreadCount(outsideUnreadCount)}</span>
{/if}
<span class="sr-only">Back</span>
</Button>
<div class="flex flex-1 flex-col justify-center text-center sm:order-2 sm:text-start">
<h3 class="truncate text-sm font-semibold">{info.name}</h3>
</div>
<div class="sm:order-1">
<AvatarBeam size={30} name={info.id} />
</div>
<a class="absolute inset-y-0 start-0 hidden w-2 cursor-default sm:block" href="/">
<span class="sr-only">Back</span>
</a>
<Button class="order-3 hidden size-8 p-0 sm:inline-flex">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
<path
fill-rule="evenodd"
d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z"
clip-rule="evenodd"
/>
</svg>
<span class="sr-only">Search</span>
</Button>
<Button class="order-3 hidden size-8 p-0 sm:inline-flex">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM12.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0ZM18.75 12a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0Z"
/>
</svg>
<span class="sr-only">Options</span>
</Button>
</div>

View file

@ -0,0 +1,23 @@
<script lang="ts">
import { VList } from 'virtua/svelte';
import type { Message } from '$lib/types';
import ChatMessage from './ChatMessage.svelte';
import { tick } from 'svelte';
export let messages: Message[] = [];
export let mySenderId: string;
let ref: VList<Message> | undefined;
async function scrollToIndex(index: number, smooth = true) {
await tick();
ref?.scrollToIndex(index, { align: 'end', smooth });
}
$: scrollToIndex(messages.length - 1);
</script>
<VList data={messages} let:item={message} class="size-full pt-2" bind:this={ref}>
<ChatMessage {message} isMyself={mySenderId === message.sender.id} />
</VList>

View file

@ -0,0 +1,74 @@
<script lang="ts">
import Button from '$lib/components/Button.svelte';
import RichTextInput from '$lib/components/RichTextInput.svelte';
import { deltaToBlahRichText, type BlahRichText } from '$lib/richText';
import { createEventDispatcher } from 'svelte';
import type { Delta, Editor } from 'typewriter-editor';
let editor: Editor | undefined;
let delta: Delta;
let plainText: string = '';
let form: HTMLFormElement | null = null;
const dispatch = createEventDispatcher<{ sendMessage: BlahRichText }>();
function onKeyboardSubmit() {
editor?.select(null);
form?.requestSubmit();
}
async function submit() {
if (plainText.trim() === '') return;
const brt = deltaToBlahRichText(delta);
dispatch('sendMessage', brt);
plainText = '';
}
</script>
<form
class="flex items-end gap-2 border-t border-ss-secondary bg-sb-primary p-2 shadow-sm"
bind:this={form}
on:submit|preventDefault={submit}
>
<Button class="p-1.5">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13"
/>
</svg>
<span class="sr-only">Attach</span>
</Button>
<RichTextInput
bind:editor
bind:delta
bind:plainText
placeholder="Message"
class="max-h-40 flex-1"
keyboardSubmitMethod="enter"
on:keyboardSubmit={onKeyboardSubmit}
/>
<Button class="p-1.5" variant="primary" type="submit">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="z-10 size-5"
>
<path
d="M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z"
/>
</svg>
<span class="sr-only">Send</span>
</Button>
</form>

View file

@ -0,0 +1,47 @@
<script lang="ts">
import RichTextRenderer from '$lib/components/RichTextRenderer.svelte';
import { tw } from '$lib/tw';
import type { Message } from '$lib/types';
import { AvatarBeam } from 'svelte-boring-avatars';
export let message: Message;
export let isMyself: boolean;
</script>
<div class={tw('mb-2 flex items-end gap-2 px-2', isMyself && 'flex-row-reverse')}>
<div>
<AvatarBeam size={30} name={message.sender.name} />
</div>
<div
class={tw(
'relative flex-1',
isMyself
? '[--weblah-chat-bubble-bg:theme(colors.accent.100)] [--weblah-chat-bubble-stroke:theme(colors.accent.200)] dark:[--weblah-chat-bubble-bg:theme(colors.accent.900)] dark:[--weblah-chat-bubble-stroke:theme(colors.accent.950)]'
: '[--weblah-chat-bubble-bg:theme(colors.sb.primary)] [--weblah-chat-bubble-stroke:theme(colors.ss.secondary)]',
isMyself && 'text-end'
)}
>
<div
class={tw(
`relative inline-block max-w-[85%] rounded-2xl bg-[--weblah-chat-bubble-bg] shadow-sm ring-1 ring-[--weblah-chat-bubble-stroke]`,
// ::before: Fill of chat bubble tail
'before:absolute before:-bottom-[1px] before:box-content before:h-6 before:w-5 before:border-[--weblah-chat-bubble-bg] before:text-[--weblah-chat-bubble-stroke]',
isMyself
? 'before:-end-5 before:rounded-es-[16px_12px] before:border-s-[10px] before:drop-shadow-[1px_0]'
: `before:-start-5 before:rounded-ee-[16px_12px] before:border-e-[10px] before:drop-shadow-[-1px_0]`,
// ::after: Stroke of chat bubble tail
'after:absolute after:-bottom-[1px] after:-z-10 after:box-content after:h-6 after:w-5 after:text-[--weblah-chat-bubble-stroke]',
isMyself
? 'after:-end-5 after:rounded-es-[16px_12px] after:border-s-[10px] after:drop-shadow-[0_1px]'
: `after:-start-5 after:rounded-ee-[16px_12px] after:border-e-[10px] after:drop-shadow-[0_1px]`,
'sm:max-w-[70%] lg:max-w-[50%]',
isMyself && 'text-start'
)}
>
<RichTextRenderer
content={message.content}
class="z-10 select-text overflow-hidden rounded-2xl px-3 py-2"
/>
</div>
</div>
</div>

View 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 />