feat(frontend): online dots on private chats, deleted last message struck through, back from a chat goes through history

This commit is contained in:
hh committed 2026-10-19 05:26:11 +00:00
1 parent 9c556ff8b9
commit 4a32e32114
4 files changed
+120 -10

No files matched your search

+19 -3
View File
@@ -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<PeerView | null>(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 @@
<Icon name="stats" />
</Button>
<Button
aria-label={t("chat.backfill")}
aria-label={backfilled ? t("chat.backfill.started") : t("chat.backfill")}
loading={backfilling}
onclick={backfill}
round
smaller
title={backfilled ? t("chat.backfill.started") : t("chat.backfill")}
variant="translucent"
>
<Icon name="cloud-download" />
<Icon name={backfilled ? "check" : "cloud-download"} />
</Button>
</div>
</header>
@@ -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);
@@ -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) {
@@ -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
>
<Avatar
avatar={{ id: chat.chat_id, kind: avatarKind }}
colorKey={chat.chat_id}
hasAvatar={chat.has_avatar}
name={title}
/>
<span class="avatar" class:online={isOnline}>
<Avatar
avatar={{ id: chat.chat_id, kind: avatarKind }}
colorKey={chat.chat_id}
hasAvatar={chat.has_avatar}
name={title}
/>
</span>
<div class="info">
<div class="info-row">
<h3 class="title">{title}</h3>
@@ -90,7 +102,13 @@
{#if senderPrefix}
<span class="sender">{senderPrefix}</span>
{/if}
{preview}
{#if chat.last_deleted}
<span class="deleted-mark" title={t("chat.deletedPreview")}
><Icon name="delete" size="1rem" /></span
><s class="deleted-text">{preview}</s>
{:else}
{preview}
{/if}
</span>
</div>
</div>
@@ -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;