From 4a32e32114d0df4979216f4578df55abd4e7a09c Mon Sep 17 00:00:00 2001 From: h Date: Mon, 19 Oct 2026 03:49:58 +0000 Subject: [PATCH] feat(frontend): online dots on private chats, deleted last message struck through, back from a chat goes through history --- frontend/src/lib/components/ChatHeader.svelte | 22 +++++- frontend/src/lib/components/ChatList.svelte | 16 +++++ .../src/lib/components/ChatListItem.svelte | 67 +++++++++++++++++-- frontend/src/lib/stores/chats.svelte.ts | 25 +++++++ 4 files changed, 120 insertions(+), 10 deletions(-) diff --git a/frontend/src/lib/components/ChatHeader.svelte b/frontend/src/lib/components/ChatHeader.svelte index 98b0665..0b9d7b1 100644 --- a/frontend/src/lib/components/ChatHeader.svelte +++ b/frontend/src/lib/components/ChatHeader.svelte @@ -29,8 +29,11 @@ const isDm = $derived(chatId > 0); const chat = $derived(chats.byId(chatId)); const discovered = $derived(discover.get(chatId)); + const DONE_DURATION = 2500; + let peer = $state(null); let backfilling = $state(false); + let backfilled = $state(false); async function backfill() { if (backfilling) { @@ -39,7 +42,10 @@ backfilling = true; try { await enqueueBackfill(chatId, true); - toasts.success(t("chat.backfill.started")); + backfilled = true; + setTimeout(() => { + backfilled = false; + }, DONE_DURATION); } catch { toasts.error(t("chat.backfill.failed")); } finally { @@ -48,6 +54,10 @@ } function back() { + if (page.state.inApp) { + history.back(); + return; + } const target = new URL(page.url.href); target.pathname = "/app"; goto(withoutMessage(target)); @@ -216,14 +226,15 @@ @@ -287,8 +298,13 @@ } .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); diff --git a/frontend/src/lib/components/ChatList.svelte b/frontend/src/lib/components/ChatList.svelte index 1cfe381..f40268b 100644 --- a/frontend/src/lib/components/ChatList.svelte +++ b/frontend/src/lib/components/ChatList.svelte @@ -76,6 +76,22 @@ } }); + $effect(() => { + const id = activeChatId; + untrack(() => { + const index = list.findIndex((chat) => chat.chat_id === id); + if (index === -1 || viewport === null) { + return; + } + const top = index * rowHeight; + if (top < viewport.scrollTop) { + viewport.scrollTop = top; + } else if (top + rowHeight > viewport.scrollTop + viewport.clientHeight) { + viewport.scrollTop = top + rowHeight - viewport.clientHeight; + } + }); + }); + function measure(el: HTMLElement) { ({ scrollTop } = el); if (el.scrollTop + el.clientHeight >= el.scrollHeight - SCROLL_THRESHOLD) { diff --git a/frontend/src/lib/components/ChatListItem.svelte b/frontend/src/lib/components/ChatListItem.svelte index f8e7aa9..e7d2a63 100644 --- a/frontend/src/lib/components/ChatListItem.svelte +++ b/frontend/src/lib/components/ChatListItem.svelte @@ -4,9 +4,11 @@ import Avatar from "#lib/components/ui/Avatar.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 { formatListDate } from "#lib/format/datetime.js"; import { t } from "#lib/i18n/locale.svelte.js"; import { accounts } from "#lib/stores/accounts.svelte.js"; + import { online } from "#lib/stores/online.svelte.js"; import { peers } from "#lib/stores/peers.svelte.js"; import { policyUi } from "#lib/stores/policy.svelte.js"; import { type RightPanel, ui } from "#lib/stores/ui.svelte.js"; @@ -31,6 +33,14 @@ ? t("chat.deletedAccount") : t("chat.fallbackTitle", { id: chat.chat_id })) ); + const person = $derived(chat.chat_id > 0 && !chat.is_bot); + const isOnline = $derived(person && online.isOnline(chat.chat_id)); + + $effect(() => { + if (person) { + online.watch(chat.chat_id); + } + }); const avatarKind = $derived(chat.chat_id > 0 ? "peer" : "chat"); const ownId = $derived(accounts.selected?.tg_user_id ?? null); const showSender = $derived( @@ -74,12 +84,14 @@ class:selected use:ripple > - + + +

{title}

@@ -90,7 +102,13 @@ {#if senderPrefix} {senderPrefix} {/if} - {preview} + {#if chat.last_deleted} + {preview} + {:else} + {preview} + {/if}
@@ -147,11 +165,15 @@ @media (hover: hover) { &:hover { + --online-ring: var(--color-chat-hover); + background-color: var(--color-chat-hover); } } &.selected { + --online-ring: var(--color-chat-active); + background-color: var(--color-chat-active); .title, @@ -163,6 +185,37 @@ } } + .avatar { + position: relative; + display: flex; + flex-shrink: 0; + + &.online::after { + content: ""; + + position: absolute; + right: 0.0625rem; + bottom: 0.0625rem; + + width: 0.875rem; + height: 0.875rem; + border: 2px solid var(--online-ring, var(--color-background)); + border-radius: 50%; + + background-color: var(--color-green); + } + } + + .deleted-mark { + display: inline-flex; + margin-inline-end: 0.1875rem; + vertical-align: -0.1875rem; + } + + .deleted-text { + text-decoration: line-through; + } + .info { overflow: hidden; flex: 1; diff --git a/frontend/src/lib/stores/chats.svelte.ts b/frontend/src/lib/stores/chats.svelte.ts index fb50b6e..4babc15 100644 --- a/frontend/src/lib/stores/chats.svelte.ts +++ b/frontend/src/lib/stores/chats.svelte.ts @@ -14,6 +14,7 @@ import { folders } from "#lib/stores/folders.svelte.js"; const POLL_INTERVAL = 1500; const POLL_MAX = 12; const PAGE_SIZE = 40; +const CHANNEL_IDS = -1_000_000_000_000; const ALL = "all"; const EMPTY: Chat[] = []; @@ -159,7 +160,29 @@ function createChats() { } } + function applyDelete(chatId: number | null, ids: number[]) { + for (const bucket of Object.values(buckets)) { + for (const chat of bucket.list) { + const inScope = + chatId === null + ? chat.chat_id > CHANNEL_IDS + : chat.chat_id === chatId; + if ( + inScope && + chat.last_message_id !== null && + ids.includes(chat.last_message_id) + ) { + chat.last_deleted = true; + } + } + } + } + function applyEvent(event: LiveEvent) { + if (event.type === "delete") { + applyDelete(event.chat_id, event.message_ids); + return; + } if (event.type !== "message") { return; } @@ -174,6 +197,8 @@ function createChats() { } seenChat = true; existing.last_date = message.date; + existing.last_deleted = message.deleted_at !== null; + existing.last_message_id = message.message_id; existing.last_sender_id = message.sender_id; existing.last_text = message.text; existing.message_count += 1;