mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-07-15 10:15:33 +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
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>
|
68
src/routes/(app)/chats/[server]/[chatId]/ChatHeader.svelte
Normal file
68
src/routes/(app)/chats/[server]/[chatId]/ChatHeader.svelte
Normal 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>
|
23
src/routes/(app)/chats/[server]/[chatId]/ChatHistory.svelte
Normal file
23
src/routes/(app)/chats/[server]/[chatId]/ChatHistory.svelte
Normal 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>
|
74
src/routes/(app)/chats/[server]/[chatId]/ChatInput.svelte
Normal file
74
src/routes/(app)/chats/[server]/[chatId]/ChatInput.svelte
Normal 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>
|
47
src/routes/(app)/chats/[server]/[chatId]/ChatMessage.svelte
Normal file
47
src/routes/(app)/chats/[server]/[chatId]/ChatMessage.svelte
Normal 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>
|
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
Add a link
Reference in a new issue