feat(frontend): profile panel about one person with real changes, shared groups, joins and leaves and deleted messages

This commit is contained in:
hh committed 2026-10-19 02:57:49 +00:00
1 parent c8d485fe0e
commit 143408cd7f
12 files changed
+899 -244

No files matched your search

@@ -1,6 +1,6 @@
<script lang="ts">
import { getPeer } from "#lib/api/endpoints.js";
import type { PeerView } from "#lib/api/types.js";
import { getPeerOverview } from "#lib/api/endpoints.js";
import type { PeerOverview } from "#lib/api/types.js";
import ChatCalendar from "#lib/components/profile/ChatCalendar.svelte";
import ProfileInfo from "#lib/components/profile/ProfileInfo.svelte";
import SharedLinks from "#lib/components/profile/SharedLinks.svelte";
@@ -10,18 +10,23 @@
import EmptyState from "#lib/components/ui/EmptyState.svelte";
import Icon from "#lib/components/ui/Icon.svelte";
import { peerName } from "#lib/format/peer.js";
import { t } from "#lib/i18n/locale.svelte.js";
import type { MessageKey } from "#lib/i18n/messages.js";
import { accounts } from "#lib/stores/accounts.svelte.js";
import { chats } from "#lib/stores/chats.svelte.js";
import { peers } from "#lib/stores/peers.svelte.js";
import { ui } from "#lib/stores/ui.svelte.js";
import { page } from "$app/state";
type Tab = "info" | "media" | "files" | "links" | "stories" | "calendar";
const TABS: { id: Tab; icon: string; label: string }[] = [
{ icon: "info", id: "info", label: "Инфо" },
{ icon: "photo", id: "media", label: "Медиа" },
{ icon: "document", id: "files", label: "Файлы" },
{ icon: "link", id: "links", label: "Ссылки" },
{ icon: "play-story", id: "stories", label: "Сторис" },
{ icon: "calendar", id: "calendar", label: "Календарь" },
const TABS: { id: Tab; icon: string; label: MessageKey }[] = [
{ icon: "info", id: "info", label: "profile.tabInfo" },
{ icon: "photo", id: "media", label: "profile.tabMedia" },
{ icon: "document", id: "files", label: "profile.tabFiles" },
{ icon: "link", id: "links", label: "profile.tabLinks" },
{ icon: "play-story", id: "stories", label: "profile.stories" },
{ icon: "calendar", id: "calendar", label: "profile.tabCalendar" },
];
const MEDIA_KINDS = ["photo", "video", "animation", "video_note"];
@@ -30,41 +35,65 @@
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 targetId = $derived(
chatId === null ? null : (ui.personIn(chatId) ?? chatId)
);
const isPerson = $derived(targetId !== null && targetId > 0);
const chat = $derived(targetId === null ? undefined : chats.byId(targetId));
let overview = $state<PeerOverview | null>(null);
let picked = $state<{ target: number | null; tab: Tab }>({
tab: "info",
target: null,
});
const peer = $derived(
overview?.peer ?? (targetId === null ? undefined : peers.get(targetId))
);
const hasChat = $derived(targetId === chatId || Boolean(overview?.dm));
const tabs = $derived(
TABS.filter(
(item) => hasChat || item.id === "info" || item.id === "stories"
)
);
const tab = $derived(
picked.target === targetId && tabs.some((item) => item.id === picked.tab)
? picked.tab
: "info"
);
const title = $derived.by(() => {
if (isPerson && peer) {
return peerName(peer);
}
return chat?.title ?? t("profile.chat", { id: targetId ?? "" });
});
const subtitle = $derived.by(() => {
if (isDm) {
if (isPerson) {
if (peer?.username) {
return `@${peer.username}`;
}
return peer?.phone ? `+${peer.phone}` : `ID ${chatId}`;
return peer?.phone ? `+${peer.phone}` : `ID ${targetId}`;
}
const count = chat?.message_count ?? 0;
return count > 0 ? `${count} сообщений` : "группа";
return count > 0 ? t("profile.messages", { count }) : t("profile.group");
});
const hasAvatar = $derived(chat?.has_avatar ?? Boolean(peer?.has_avatar));
const hasAvatar = $derived(
isPerson ? Boolean(peer?.has_avatar) : (chat?.has_avatar ?? false)
);
$effect(() => {
if (chatId === null || !isDm) {
peer = null;
if (accounts.selectedId === null || targetId === null || !isPerson) {
overview = null;
return;
}
const peerId = targetId;
let active = true;
peer = null;
getPeer(chatId)
overview = null;
getPeerOverview(peerId)
.then((result) => {
if (active) {
peer = result;
overview = result;
}
})
.catch(() => undefined);
@@ -74,14 +103,14 @@
});
</script>
{#if chatId === null}
<EmptyState description="Откройте чат" title="Профиль" />
{#if chatId === null || targetId === null}
<EmptyState description={t("profile.empty")} title={t("profile.title")} />
{:else}
<div class="profile">
<header class="hero">
<Avatar
avatar={{ id: chatId, kind: avatarKind }}
colorKey={chatId}
avatar={{ id: targetId, kind: isPerson ? "peer" : "chat" }}
colorKey={targetId}
deleted={peer?.is_deleted_account ?? false}
{hasAvatar}
name={title}
@@ -92,13 +121,15 @@
</header>
<nav class="tabs">
{#each TABS as item (item.id)}
{#each tabs as item (item.id)}
<button
aria-label={item.label}
aria-label={t(item.label)}
aria-pressed={tab === item.id}
class="tab"
onclick={() => {
tab = item.id;
picked = { tab: item.id, target: targetId };
}}
title={t(item.label)}
type="button"
class:active={tab === item.id}
>
@@ -109,17 +140,25 @@
<div class="tab-body">
{#if tab === "info"}
<ProfileInfo {avatarKind} {chatId} {isDm} name={title} {peer} />
<ProfileInfo
{chatId}
id={targetId}
name={title}
onstories={() => {
picked = { tab: "stories", target: targetId };
}}
{overview}
/>
{:else if tab === "media"}
<SharedMedia {chatId} kinds={MEDIA_KINDS} layout="grid" />
<SharedMedia chatId={targetId} kinds={MEDIA_KINDS} layout="grid" />
{:else if tab === "files"}
<SharedMedia {chatId} kinds={FILE_KINDS} layout="list" />
<SharedMedia chatId={targetId} kinds={FILE_KINDS} layout="list" />
{:else if tab === "links"}
<SharedLinks {chatId} />
<SharedLinks chatId={targetId} />
{:else if tab === "stories"}
<StoriesArchive {chatId} />
<StoriesArchive chatId={targetId} />
{:else if tab === "calendar"}
<ChatCalendar {chatId} />
<ChatCalendar chatId={targetId} />
{/if}
</div>
</div>
@@ -129,7 +168,7 @@
.profile {
display: flex;
flex-direction: column;
height: 100%;
min-height: 100%;
}
.hero {
@@ -155,12 +194,21 @@
}
.tabs {
position: sticky;
top: 0;
z-index: 1;
display: flex;
flex-shrink: 0;
border-bottom: 1px solid var(--color-borders);
background-color: var(--color-background);
}
.tab {
cursor: pointer;
display: flex;
flex: 1;
align-items: center;
@@ -171,17 +219,21 @@
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);
}
&:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: -2px;
}
}
.tab-body {
overflow-y: auto;
flex: 1;
}
</style>