332 lines
7.9 KiB
Svelte
332 lines
7.9 KiB
Svelte
<script lang="ts">
|
|
import { enqueueBackfill, getPeer } from "#lib/api/endpoints.js";
|
|
import type { PeerView } from "#lib/api/types.js";
|
|
import Avatar from "#lib/components/ui/Avatar.svelte";
|
|
import Button from "#lib/components/ui/Button.svelte";
|
|
import ContextMenu from "#lib/components/ui/ContextMenu.svelte";
|
|
import ContextMenuItem from "#lib/components/ui/ContextMenuItem.svelte";
|
|
import Icon from "#lib/components/ui/Icon.svelte";
|
|
import { peerName } from "#lib/format/peer.js";
|
|
import { formatPresence, isOnline } from "#lib/format/presence.js";
|
|
import { t } from "#lib/i18n/locale.svelte.js";
|
|
import { accounts } from "#lib/stores/accounts.svelte.js";
|
|
import { chats } from "#lib/stores/chats.svelte.js";
|
|
import { discover } from "#lib/stores/discover.svelte.js";
|
|
import { withoutMessage } from "#lib/stores/feed.svelte.js";
|
|
import { policyUi } from "#lib/stores/policy.svelte.js";
|
|
import { presence } from "#lib/stores/presence.svelte.js";
|
|
import { toasts } from "#lib/stores/toasts.svelte.js";
|
|
import { ui } from "#lib/stores/ui.svelte.js";
|
|
import { goto } from "$app/navigation";
|
|
import { page } from "$app/state";
|
|
|
|
interface Props {
|
|
chatId: number;
|
|
}
|
|
|
|
let { chatId }: Props = $props();
|
|
|
|
const isDm = $derived(chatId > 0);
|
|
const chat = $derived(chats.byId(chatId));
|
|
const discovered = $derived(discover.get(chatId));
|
|
const DONE_DURATION = 2500;
|
|
|
|
let peer = $state<PeerView | null>(null);
|
|
let backfilling = $state(false);
|
|
let backfilled = $state(false);
|
|
|
|
async function backfill() {
|
|
if (backfilling) {
|
|
return;
|
|
}
|
|
backfilling = true;
|
|
try {
|
|
await enqueueBackfill(chatId, true);
|
|
backfilled = true;
|
|
setTimeout(() => {
|
|
backfilled = false;
|
|
}, DONE_DURATION);
|
|
} catch {
|
|
toasts.error(t("chat.backfill.failed"));
|
|
} finally {
|
|
backfilling = false;
|
|
}
|
|
}
|
|
|
|
function back() {
|
|
if (page.state.inApp) {
|
|
history.back();
|
|
return;
|
|
}
|
|
const target = new URL(page.url.href);
|
|
target.pathname = "/app";
|
|
goto(withoutMessage(target));
|
|
}
|
|
|
|
$effect(() => {
|
|
if (accounts.selectedId === null) {
|
|
return;
|
|
}
|
|
discover.ensure(chatId).catch(() => undefined);
|
|
});
|
|
|
|
$effect(() => {
|
|
if (accounts.selectedId === null || !isDm) {
|
|
peer = null;
|
|
return;
|
|
}
|
|
let active = true;
|
|
peer = null;
|
|
getPeer(chatId)
|
|
.then((result) => {
|
|
if (active) {
|
|
peer = result;
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (active) {
|
|
peer = null;
|
|
}
|
|
});
|
|
return () => {
|
|
active = false;
|
|
};
|
|
});
|
|
|
|
$effect(() => {
|
|
presence.follow(accounts.selectedId, isDm ? chatId : null);
|
|
return () => presence.follow(null, null);
|
|
});
|
|
|
|
const fallbackTitle = $derived(
|
|
chat?.title ??
|
|
discovered?.title ??
|
|
(isDm
|
|
? t("chat.deletedAccount")
|
|
: t("chat.fallbackTitle", { id: chatId }))
|
|
);
|
|
const title = $derived(isDm && peer ? peerName(peer) : fallbackTitle);
|
|
const subtitle = $derived.by(() => {
|
|
if (isDm) {
|
|
if (presence.sample) {
|
|
return formatPresence(presence.sample, presence.now);
|
|
}
|
|
if (peer?.username) {
|
|
return `@${peer.username}`;
|
|
}
|
|
return peer?.phone ?? `ID ${chatId}`;
|
|
}
|
|
const count = chat?.message_count ?? discovered?.message_count ?? 0;
|
|
if (count > 0) {
|
|
return t("chat.messages", { count });
|
|
}
|
|
return discovered?.kind === "channel"
|
|
? t("chat.kind.channel")
|
|
: t("chat.kind.group");
|
|
});
|
|
const avatarKind = $derived(isDm ? "peer" : "chat");
|
|
const hasAvatar = $derived(
|
|
chat?.has_avatar ?? Boolean(peer?.has_avatar || discovered?.has_avatar)
|
|
);
|
|
</script>
|
|
|
|
<svelte:head>
|
|
<title>{title} · Beavergram</title>
|
|
</svelte:head>
|
|
|
|
<header class="chat-header">
|
|
<div class="back">
|
|
<Button
|
|
aria-label={t("common.back")}
|
|
onclick={back}
|
|
round
|
|
smaller
|
|
variant="translucent"
|
|
>
|
|
<Icon name="arrow-left" />
|
|
</Button>
|
|
</div>
|
|
<ContextMenu>
|
|
{#snippet children({
|
|
props,
|
|
})}
|
|
<button
|
|
{...props}
|
|
aria-label={t("chat.openProfile")}
|
|
class="peek"
|
|
onclick={() => ui.openPanel("profile")}
|
|
type="button"
|
|
>
|
|
<Avatar
|
|
avatar={{ id: chatId, kind: avatarKind }}
|
|
colorKey={chatId}
|
|
deleted={peer?.is_deleted_account ?? false}
|
|
{hasAvatar}
|
|
name={title}
|
|
size={2.5}
|
|
/>
|
|
<div class="info">
|
|
<h2 class="title">{title}</h2>
|
|
<span
|
|
class={[
|
|
"subtitle",
|
|
{ online: isDm && isOnline(presence.sample, presence.now) },
|
|
]}
|
|
>
|
|
{subtitle}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
{/snippet}
|
|
{#snippet menu()}
|
|
<ContextMenuItem icon="info" onselect={() => ui.openPanel("profile")}
|
|
>{t("chat.openProfile")}</ContextMenuItem
|
|
>
|
|
<ContextMenuItem icon="recent" onselect={() => ui.openPanel("changes")}
|
|
>{t("feed.changes")}</ContextMenuItem
|
|
>
|
|
<ContextMenuItem icon="stats" onselect={() => ui.openPanel("presence")}
|
|
>{t("chat.analytics")}</ContextMenuItem
|
|
>
|
|
<ContextMenuItem
|
|
icon={backfilled ? "check" : "cloud-download"}
|
|
onselect={backfill}
|
|
>{backfilled
|
|
? t("chat.backfill.started")
|
|
: t("chat.backfill")}</ContextMenuItem
|
|
>
|
|
<ContextMenuItem
|
|
icon="play-story"
|
|
onselect={() => ui.openPanel("stories")}
|
|
>{t("settings.stories")}</ContextMenuItem
|
|
>
|
|
<ContextMenuItem
|
|
icon="settings"
|
|
onselect={() => policyUi.edit({ chatId }, title)}
|
|
>{t("settings.policy")}</ContextMenuItem
|
|
>
|
|
{/snippet}
|
|
</ContextMenu>
|
|
<div class="actions">
|
|
<Button
|
|
aria-label={t("chat.search")}
|
|
onclick={() => ui.openPanel("search")}
|
|
round
|
|
smaller
|
|
variant="translucent"
|
|
>
|
|
<Icon name="search" />
|
|
</Button>
|
|
<Button
|
|
aria-label={t("feed.changes")}
|
|
class="wide-only"
|
|
onclick={() => ui.openPanel("changes")}
|
|
round
|
|
smaller
|
|
variant="translucent"
|
|
>
|
|
<Icon name="recent" />
|
|
</Button>
|
|
<Button
|
|
aria-label={t("chat.analytics")}
|
|
onclick={() => ui.openPanel("presence")}
|
|
round
|
|
smaller
|
|
variant="translucent"
|
|
>
|
|
<Icon name="stats" />
|
|
</Button>
|
|
<Button
|
|
aria-label={backfilled ? t("chat.backfill.started") : t("chat.backfill")}
|
|
class="wide-only"
|
|
loading={backfilling}
|
|
onclick={backfill}
|
|
round
|
|
smaller
|
|
title={backfilled ? t("chat.backfill.started") : t("chat.backfill")}
|
|
variant="translucent"
|
|
>
|
|
<Icon name={backfilled ? "check" : "cloud-download"} />
|
|
</Button>
|
|
</div>
|
|
</header>
|
|
|
|
<style lang="scss">
|
|
.chat-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.625rem;
|
|
|
|
height: var(--header-height);
|
|
padding: 0 1rem;
|
|
border-bottom: 1px solid var(--color-borders);
|
|
|
|
background-color: var(--color-background);
|
|
}
|
|
|
|
.back {
|
|
display: none;
|
|
margin-left: -0.25rem;
|
|
|
|
@media (max-width: 600px) {
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.peek {
|
|
display: flex;
|
|
flex: 1;
|
|
align-items: center;
|
|
gap: 0.625rem;
|
|
|
|
min-width: 0;
|
|
padding: 0;
|
|
border: 0;
|
|
|
|
text-align: left;
|
|
cursor: pointer;
|
|
background: transparent;
|
|
}
|
|
|
|
.info {
|
|
overflow: hidden;
|
|
min-width: 0;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
margin-left: auto;
|
|
|
|
@media (max-width: 600px) {
|
|
:global(.wide-only) {
|
|
display: none;
|
|
}
|
|
}
|
|
}
|
|
|
|
.title {
|
|
overflow: hidden;
|
|
margin: 0;
|
|
font-size: 1rem;
|
|
font-weight: var(--font-weight-medium);
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.subtitle {
|
|
overflow: hidden;
|
|
display: block;
|
|
|
|
font-size: 0.8125rem;
|
|
color: var(--color-text-secondary);
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
|
|
&.online {
|
|
color: var(--color-primary);
|
|
}
|
|
}
|
|
</style>
|