feat: add annotations, user profiles, watchers, stories, search and more

This commit is contained in:
hh committed 2026-06-01 17:15:09 +02:00
1 parent ed469ba8dd
commit 2465bcd184
47 files changed
+5009 -242

No files matched your search

@@ -0,0 +1,183 @@
<script lang="ts">
import { page } from "$app/state";
import { getPeer } from "$lib/api/endpoints";
import type { PeerView } from "$lib/api/types";
import ChatCalendar from "$lib/components/profile/ChatCalendar.svelte";
import ProfileInfo from "$lib/components/profile/ProfileInfo.svelte";
import SharedLinks from "$lib/components/profile/SharedLinks.svelte";
import SharedMedia from "$lib/components/profile/SharedMedia.svelte";
import Avatar from "$lib/components/ui/Avatar.svelte";
import EmptyState from "$lib/components/ui/EmptyState.svelte";
import Icon from "$lib/components/ui/Icon.svelte";
import { peerName } from "$lib/format/peer";
import { chats } from "$lib/stores/chats.svelte";
type Tab = "info" | "media" | "files" | "links" | "calendar";
const TABS: { id: Tab; icon: string; label: string }[] = [
{ id: "info", icon: "info", label: "Инфо" },
{ id: "media", icon: "photo", label: "Медиа" },
{ id: "files", icon: "document", label: "Файлы" },
{ id: "links", icon: "link", label: "Ссылки" },
{ id: "calendar", icon: "calendar", label: "Календарь" },
];
const MEDIA_KINDS = ["photo", "video", "animation", "video_note"];
const FILE_KINDS = ["document", "audio", "voice"];
const chatId = $derived(
page.params.chatId ? Number(page.params.chatId) : null
);
const isDm = $derived(chatId !== null && chatId > 0);
const chat = $derived(chatId === null ? null : chats.byId(chatId));
const avatarKind = $derived(isDm ? "peer" : "chat");
let peer = $state<PeerView | null>(null);
let tab = $state<Tab>("info");
const title = $derived(
isDm && peer
? peerName(peer)
: (chat?.title ?? (chatId === null ? "" : `Chat ${chatId}`))
);
const subtitle = $derived.by(() => {
if (isDm) {
if (peer?.username) {
return `@${peer.username}`;
}
return peer?.phone ? `+${peer.phone}` : `ID ${chatId}`;
}
const count = chat?.message_count ?? 0;
return count > 0 ? `${count} сообщений` : "группа";
});
const hasAvatar = $derived(chat?.has_avatar ?? Boolean(peer?.has_avatar));
$effect(() => {
if (chatId === null || !isDm) {
peer = null;
return;
}
let active = true;
peer = null;
getPeer(chatId)
.then((result) => {
if (active) {
peer = result;
}
})
.catch(() => undefined);
return () => {
active = false;
};
});
</script>
{#if chatId === null}
<EmptyState title="Профиль" description="Откройте чат" />
{:else}
<div class="profile">
<header class="hero">
<Avatar
name={title}
colorKey={chatId}
size={5}
avatar={{ kind: avatarKind, id: chatId }}
{hasAvatar}
deleted={peer?.is_deleted_account ?? false}
/>
<h2 class="name">{title}</h2>
<span class="sub">{subtitle}</span>
</header>
<nav class="tabs">
{#each TABS as item (item.id)}
<button
type="button"
class="tab"
class:active={tab === item.id}
onclick={() => {
tab = item.id;
}}
aria-label={item.label}
>
<Icon name={item.icon} />
</button>
{/each}
</nav>
<div class="tab-body">
{#if tab === "info"}
<ProfileInfo {chatId} {isDm} {peer} name={title} {avatarKind} />
{:else if tab === "media"}
<SharedMedia {chatId} kinds={MEDIA_KINDS} layout="grid" />
{:else if tab === "files"}
<SharedMedia {chatId} kinds={FILE_KINDS} layout="list" />
{:else if tab === "links"}
<SharedLinks {chatId} />
{:else if tab === "calendar"}
<ChatCalendar {chatId} />
{/if}
</div>
</div>
{/if}
<style lang="scss">
.profile {
display: flex;
flex-direction: column;
height: 100%;
}
.hero {
display: flex;
flex-direction: column;
flex-shrink: 0;
align-items: center;
gap: 0.25rem;
padding: 1.25rem 1rem 1rem;
}
.name {
margin: 0.5rem 0 0;
font-size: 1.25rem;
font-weight: var(--font-weight-medium);
text-align: center;
overflow-wrap: anywhere;
}
.sub {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.tabs {
display: flex;
flex-shrink: 0;
border-bottom: 1px solid var(--color-borders);
}
.tab {
display: flex;
flex: 1;
align-items: center;
justify-content: center;
height: 3rem;
border: 0;
border-bottom: 2px solid transparent;
color: var(--color-text-secondary);
cursor: pointer;
background: transparent;
&.active {
color: var(--color-primary);
border-bottom-color: var(--color-primary);
}
}
.tab-body {
overflow-y: auto;
flex: 1;
}
</style>