b
This commit is contained in:
41
src/lib/stores/cache/channels.ts
vendored
Normal file
41
src/lib/stores/cache/channels.ts
vendored
Normal 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;
|
||||
}
|
||||
@@ -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
2
src/params/integer.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import type { ParamMatcher } from '@sveltejs/kit';
|
||||
export const match: ParamMatcher = (param) => { return /^\d+$/.test(param); };
|
||||
@@ -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';
|
||||
@@ -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>
|
||||
30
src/routes/(auth)/channels/(components)/channel-list.svelte
Normal file
30
src/routes/(auth)/channels/(components)/channel-list.svelte
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
12
src/routes/(auth)/channels/+layout.ts
Normal file
12
src/routes/(auth)/channels/+layout.ts
Normal 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;
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
if (token) {
|
||||
tokenStore.set(token.token);
|
||||
|
||||
window.location.href = '/chats';
|
||||
window.location.href = '/channels';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user