240 lines
6.5 KiB
Svelte
240 lines
6.5 KiB
Svelte
<script lang="ts">
|
|
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";
|
|
import SharedMedia from "#lib/components/profile/SharedMedia.svelte";
|
|
import StoriesArchive from "#lib/components/stories/StoriesArchive.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 { formatPhone, 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: 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"];
|
|
const FILE_KINDS = ["document", "audio", "voice"];
|
|
|
|
const chatId = $derived(
|
|
page.params.chatId ? Number(page.params.chatId) : null
|
|
);
|
|
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 (isPerson) {
|
|
if (peer?.username) {
|
|
return `@${peer.username}`;
|
|
}
|
|
return peer?.phone ? formatPhone(peer.phone) : `ID ${targetId}`;
|
|
}
|
|
const count = chat?.message_count ?? 0;
|
|
return count > 0 ? t("profile.messages", { count }) : t("profile.group");
|
|
});
|
|
const hasAvatar = $derived(
|
|
isPerson ? Boolean(peer?.has_avatar) : (chat?.has_avatar ?? false)
|
|
);
|
|
|
|
$effect(() => {
|
|
if (accounts.selectedId === null || targetId === null || !isPerson) {
|
|
overview = null;
|
|
return;
|
|
}
|
|
const peerId = targetId;
|
|
let active = true;
|
|
overview = null;
|
|
getPeerOverview(peerId)
|
|
.then((result) => {
|
|
if (active) {
|
|
overview = result;
|
|
}
|
|
})
|
|
.catch(() => undefined);
|
|
return () => {
|
|
active = false;
|
|
};
|
|
});
|
|
</script>
|
|
|
|
{#if chatId === null || targetId === null}
|
|
<EmptyState description={t("profile.empty")} title={t("profile.title")} />
|
|
{:else}
|
|
<div class="profile">
|
|
<header class="hero">
|
|
<Avatar
|
|
avatar={{ id: targetId, kind: isPerson ? "peer" : "chat" }}
|
|
colorKey={targetId}
|
|
deleted={peer?.is_deleted_account ?? false}
|
|
{hasAvatar}
|
|
name={title}
|
|
size={5}
|
|
/>
|
|
<h2 class="name">{title}</h2>
|
|
<span class="sub">{subtitle}</span>
|
|
</header>
|
|
|
|
<nav class="tabs">
|
|
{#each tabs as item (item.id)}
|
|
<button
|
|
aria-label={t(item.label)}
|
|
aria-pressed={tab === item.id}
|
|
class="tab"
|
|
onclick={() => {
|
|
picked = { tab: item.id, target: targetId };
|
|
}}
|
|
title={t(item.label)}
|
|
type="button"
|
|
class:active={tab === item.id}
|
|
>
|
|
<Icon name={item.icon} />
|
|
</button>
|
|
{/each}
|
|
</nav>
|
|
|
|
<div class="tab-body">
|
|
{#if tab === "info"}
|
|
<ProfileInfo
|
|
{chatId}
|
|
id={targetId}
|
|
name={title}
|
|
onstories={() => {
|
|
picked = { tab: "stories", target: targetId };
|
|
}}
|
|
{overview}
|
|
/>
|
|
{:else if tab === "media"}
|
|
<SharedMedia chatId={targetId} kinds={MEDIA_KINDS} layout="grid" />
|
|
{:else if tab === "files"}
|
|
<SharedMedia chatId={targetId} kinds={FILE_KINDS} layout="list" />
|
|
{:else if tab === "links"}
|
|
<SharedLinks chatId={targetId} />
|
|
{:else if tab === "stories"}
|
|
<StoriesArchive chatId={targetId} />
|
|
{:else if tab === "calendar"}
|
|
<ChatCalendar chatId={targetId} />
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<style lang="scss">
|
|
.profile {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-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 {
|
|
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;
|
|
justify-content: center;
|
|
|
|
height: 3rem;
|
|
border: 0;
|
|
border-bottom: 2px solid transparent;
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
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 {
|
|
flex: 1;
|
|
}
|
|
</style>
|