1
0
This commit is contained in:
2024-05-19 19:20:22 +03:00
parent 8dedbf208c
commit 9b27b59b69
20 changed files with 127 additions and 71 deletions

41
src/lib/stores/cache/channels.ts vendored Normal file
View File

@@ -0,0 +1,41 @@
import { getChannelById } from "$lib/api/channel";
import { isErrorResponse, type Channel } from "$lib/types";
import { get, writable, type Writable } from "svelte/store";
const channelsCache: Writable<Map<number, Channel>> = writable(new Map<number, Channel>());
const runningCaches = new Set();
export function addChannelToCache(channel: Channel) {
channelsCache.update((channels) => channels.set(channel.id, channel));
}
export async function getCachedChannel(channelId: number): Promise<Channel | null> {
const cached = get(channelsCache).get(channelId);
if (cached)
return cached;
if (runningCaches.has(channelId))
return await new Promise((resolve) => {
channelsCache.subscribe((channels) => {
const channel = channels.get(channelId);
if (channel) {
runningCaches.delete(channelId);
resolve(channel);
}
});
});
runningCaches.add(channelId);
const response = await getChannelById(channelId);
if (isErrorResponse(response))
return null;
const channel = response as Channel;
addChannelToCache(channel);
return channel;
}

View File

@@ -9,15 +9,15 @@ export function isErrorResponse(data: unknown): data is ErrorResponse {
export type Token = {
token: string;
userId: number;
createdAt: Date;
expiresAt: Date;
createdAt: string;
expiresAt: string;
}
export type User = {
id: number;
username: string;
avatar: string | null;
createdAt: Date;
createdAt: string;
};
export type Message = {
@@ -25,12 +25,12 @@ export type Message = {
channelId: number;
authorId: number;
content: string;
createdAt: Date;
createdAt: string;
};
export type Channel = {
id: number;
name: string;
lastMessageId: number | null;
createdAt: Date;
createdAt: string;
};

2
src/params/integer.ts Normal file
View File

@@ -0,0 +1,2 @@
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => { return /^\d+$/.test(param); };

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import * as Avatar from '$lib/components/ui/avatar';
import ScrollArea from '$lib/components/ui/scroll-area/scroll-area.svelte';
import { ScrollArea } from '$lib/components/ui/scroll-area';
import type { Message as MessageType } from '$lib/types';
import MessageArea from './message-area.svelte';
import Message from './message.svelte';

View File

@@ -1,10 +1,18 @@
<script lang="ts">
import { getMessageById } from '$lib/api/message';
import * as Avatar from '$lib/components/ui/avatar';
import { isErrorResponse, type Channel, type Message } from '$lib/types';
import { cn } from '$lib/utils';
export let username: string;
export let lastMessage: string;
export let avatarUrl: string | undefined | null;
export let channel: Channel;
let lastMessage: Message | null = null;
if (channel.lastMessageId) {
getMessageById(channel.lastMessageId).then((message) => {
if (!isErrorResponse(message)) lastMessage = message;
});
}
export let selected: boolean = false;
export let onClick: () => void = () => {};
@@ -14,12 +22,15 @@
<button on:click={onClick} class={cn('flex w-full space-x-4 rounded-xl p-4', className)}>
<div>
<Avatar.Root class="h-12 w-12">
<Avatar.Image src={avatarUrl} />
<Avatar.Fallback>{username[0].toUpperCase()}</Avatar.Fallback>
<Avatar.Image src="/default-avatar.png" />
<Avatar.Fallback>{channel.name[0].toUpperCase()}</Avatar.Fallback>
</Avatar.Root>
</div>
<div class="flex flex-col overflow-auto">
<span class="overflow-hidden text-ellipsis text-left text-xl font-bold">{username}</span>
<span class="overflow-hidden text-ellipsis text-left text-sm">{lastMessage}</span>
<span class="overflow-hidden text-ellipsis text-left text-xl font-bold">{channel.name}</span
>
<span class="overflow-hidden text-ellipsis text-left text-sm"
>{lastMessage?.content || ''}</span
>
</div>
</button>

View File

@@ -0,0 +1,30 @@
<script lang="ts">
import { writable, type Writable } from 'svelte/store';
import ChannelListItem from './channel-list-item.svelte';
import { goto } from '$app/navigation';
import type { Channel } from '$lib/types';
export let channels: Channel[] = [];
export let defaultSelected: number | undefined = undefined;
const selectedChannel: Writable<number | undefined> = writable(defaultSelected);
export function select(channel: Channel) {
selectedChannel.set(channel.id);
goto(`/channels/${channel.id}`);
}
export function deselect() {
selectedChannel.set(undefined);
}
</script>
<div class="flex flex-col gap-2">
{#each channels as channel}
<ChannelListItem
{channel}
selected={$selectedChannel == channel.id}
onClick={() => select(channel)}
/>
{/each}
</div>

View File

@@ -11,7 +11,7 @@
<div>
<ScrollArea class="flex h-[calc(100vh-4rem)] flex-grow rounded-md">
<div class="p-2 pr-3">
<slot name="chats"></slot>
<slot name="channels"></slot>
</div>
</ScrollArea>
</div>

View File

@@ -10,17 +10,18 @@
import LogOut from 'lucide-svelte/icons/log-out';
import type { ComponentType } from 'svelte';
import type { MenuItem } from './(components)/sidebar-header.svelte';
import ChatList from './(components)/chat-list.svelte';
import ChannelList from './(components)/channel-list.svelte';
import { user } from '$lib/stores/user';
import { number } from 'zod';
export let data: LayoutData;
let chatList: ChatList | undefined;
let channelList: ChannelList | undefined;
function onKeyUp(event: KeyboardEvent) {
if (event.key === 'Escape') {
chatList?.deselect();
goto('/chats');
channelList?.deselect();
goto('/channels');
}
}
@@ -40,6 +41,8 @@
}
}
];
$: channelId = parseInt($page.params.channel_id);
</script>
<svelte:window on:keyup={onKeyUp} />
@@ -51,11 +54,11 @@
<SidebarHeader {menuItems} />
</div>
<div slot="chats">
<ChatList
chats={data.chats}
defaultSelected={$page.params.username}
bind:this={chatList}
<div slot="channels">
<ChannelList
channels={data.channels}
defaultSelected={channelId}
bind:this={channelList}
/>
</div>
</Sidebar>

View File

@@ -0,0 +1,12 @@
import { getAllChannels } from '$lib/api/channel';
import type { Channel } from '$lib/types';
import type { LayoutLoad } from './$types';
export const load = (async () => {
const response = await getAllChannels();
const channels = response as Channel[]
return { channels };
}) satisfies LayoutLoad;

View File

@@ -1,5 +1,5 @@
<div class="flex h-full w-full items-center justify-center">
<div class="w-[20%] min-w-[300px] space-y-4 rounded-lg bg-secondary p-4">
<h1 class="text-center text-4xl font-bold">No chat selected</h1>
<h1 class="text-center text-4xl font-bold">No channel selected</h1>
</div>
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import type { PageData } from './$types';
import { page } from '$app/stores';
import Chat from '../(components)/(chat)/chat.svelte';
import Channel from '../(components)/(channel)/channel.svelte';
export let data: PageData;
@@ -9,5 +9,5 @@
</script>
<div class="flex h-screen w-full items-center justify-center">
<Chat {username} messages={data.messages} />
<Channel {username} messages={data.messages} />
</div>

View File

@@ -1,32 +0,0 @@
<script lang="ts">
import { writable, type Writable } from 'svelte/store';
import ChatListItem from './chat-list-item.svelte';
import { goto } from '$app/navigation';
import type { Channel } from '$lib/types';
export let chats: Channel[] = [];
export let defaultSelected: string | undefined = undefined;
const selectedChat: Writable<string | undefined> = writable(defaultSelected);
export function select(username: string) {
selectedChat.set(username);
goto(`/chats/${username}`);
}
export function deselect() {
selectedChat.set(undefined);
}
</script>
<div class="flex flex-col gap-2">
{#each chats as chat}
<ChatListItem
username={chat.username}
lastMessage={chat.lastMessage}
avatarUrl={chat.avatarUrl}
selected={$selectedChat == chat.username}
onClick={() => select(chat.username)}
/>
{/each}
</div>

View File

@@ -1,11 +0,0 @@
import type { LayoutLoad } from './$types';
const chats = [
{ username: 'lionarius', lastMessage: 'Привет', avatarUrl: '/default-avatar.png' },
{ username: 'tenebris', lastMessage: 'Как дела', avatarUrl: null },
{ username: 'staheys', lastMessage: 'Hey', avatarUrl: '/default-avatar.png' },
]
export const load = (async () => {
return { chats };
}) satisfies LayoutLoad;

View File

@@ -9,7 +9,7 @@ import { isErrorResponse } from '$lib/types';
export const load = (async ({ cookies }) => {
if (cookies.get('token'))
throw redirect(302, '/chats');
throw redirect(302, '/channels');
return { form: await superValidate(zod(loginFormSchema)) };
}) satisfies PageServerLoad;

View File

@@ -35,7 +35,7 @@
if (token) {
tokenStore.set(token.token);
window.location.href = '/chats';
window.location.href = '/channels';
}
}
}