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,151 @@
<script lang="ts">
import { untrack } from "svelte";
import { page } from "$app/state";
import { getMessageLinks } from "$lib/api/endpoints";
import type { LinkView } from "$lib/api/types";
import EmptyState from "$lib/components/ui/EmptyState.svelte";
import Icon from "$lib/components/ui/Icon.svelte";
import Spinner from "$lib/components/ui/Spinner.svelte";
import { accounts } from "$lib/stores/accounts.svelte";
import { ui } from "$lib/stores/ui.svelte";
const WWW_PREFIX = /^www\./;
const chatId = $derived(
page.params.chatId ? Number(page.params.chatId) : null
);
const messageId = $derived(ui.panelMessageId);
let items = $state<LinkView[]>([]);
let loading = $state(false);
let token = 0;
function host(url: string): string {
try {
return new URL(url).hostname.replace(WWW_PREFIX, "");
} catch {
return url;
}
}
$effect(() => {
const cid = chatId;
const mid = messageId;
if (cid === null || mid === null || accounts.selectedId === null) {
return;
}
untrack(() => {
token++;
const current = token;
loading = true;
items = [];
getMessageLinks(cid, mid)
.then((rows) => {
if (current === token) {
items = rows;
}
})
.finally(() => {
if (current === token) {
loading = false;
}
});
});
});
</script>
{#if chatId === null || messageId === null}
<EmptyState title="Ссылки" description="Сообщение не выбрано" />
{:else if loading && items.length === 0}
<div class="center"><Spinner /></div>
{:else if items.length === 0}
<EmptyState title="Нет ссылок" />
{:else}
<ul class="list">
{#each items as item (item.position)}
<li>
<span class="link-icon"><Icon name="link" /></span>
<span class="meta">
{#if item.web_title}
<span class="title">{item.web_title}</span>
{/if}
<a class="url" href={item.url} target="_blank" rel="noopener">
{item.url}
</a>
{#if item.web_site_name || item.web_description}
<span class="sub">
{item.web_site_name ?? host(item.url)}
{#if item.web_description}
· {item.web_description}
{/if}
</span>
{/if}
</span>
</li>
{/each}
</ul>
{/if}
<style lang="scss">
.center {
display: flex;
justify-content: center;
padding: 2rem 0;
}
.list {
display: flex;
flex-direction: column;
margin: 0;
padding: 0.5rem;
list-style: none;
}
.list > li {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 0.5rem;
border-radius: 0.5rem;
}
.link-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
color: var(--color-white);
background-color: var(--color-primary);
}
.meta {
display: flex;
flex-direction: column;
gap: 0.125rem;
min-width: 0;
}
.title {
overflow: hidden;
font-size: 0.9375rem;
color: var(--color-text);
text-overflow: ellipsis;
white-space: nowrap;
}
.url {
overflow-wrap: anywhere;
font-size: 0.8125rem;
color: var(--color-primary);
}
.sub {
font-size: 0.8125rem;
color: var(--color-text-secondary);
}
</style>