feat(frontend): online dots on private chats, deleted last message struck through, back from a chat goes through history
This commit is contained in:
1 parent
9c556ff8b9
commit
4a32e32114
4 files changed
+120
-10
No files matched your search
@@ -29,8 +29,11 @@
|
|||||||
const isDm = $derived(chatId > 0);
|
const isDm = $derived(chatId > 0);
|
||||||
const chat = $derived(chats.byId(chatId));
|
const chat = $derived(chats.byId(chatId));
|
||||||
const discovered = $derived(discover.get(chatId));
|
const discovered = $derived(discover.get(chatId));
|
||||||
|
const DONE_DURATION = 2500;
|
||||||
|
|
||||||
let peer = $state<PeerView | null>(null);
|
let peer = $state<PeerView | null>(null);
|
||||||
let backfilling = $state(false);
|
let backfilling = $state(false);
|
||||||
|
let backfilled = $state(false);
|
||||||
|
|
||||||
async function backfill() {
|
async function backfill() {
|
||||||
if (backfilling) {
|
if (backfilling) {
|
||||||
@@ -39,7 +42,10 @@
|
|||||||
backfilling = true;
|
backfilling = true;
|
||||||
try {
|
try {
|
||||||
await enqueueBackfill(chatId, true);
|
await enqueueBackfill(chatId, true);
|
||||||
toasts.success(t("chat.backfill.started"));
|
backfilled = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
backfilled = false;
|
||||||
|
}, DONE_DURATION);
|
||||||
} catch {
|
} catch {
|
||||||
toasts.error(t("chat.backfill.failed"));
|
toasts.error(t("chat.backfill.failed"));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -48,6 +54,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function back() {
|
function back() {
|
||||||
|
if (page.state.inApp) {
|
||||||
|
history.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const target = new URL(page.url.href);
|
const target = new URL(page.url.href);
|
||||||
target.pathname = "/app";
|
target.pathname = "/app";
|
||||||
goto(withoutMessage(target));
|
goto(withoutMessage(target));
|
||||||
@@ -216,14 +226,15 @@
|
|||||||
<Icon name="stats" />
|
<Icon name="stats" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("chat.backfill")}
|
aria-label={backfilled ? t("chat.backfill.started") : t("chat.backfill")}
|
||||||
loading={backfilling}
|
loading={backfilling}
|
||||||
onclick={backfill}
|
onclick={backfill}
|
||||||
round
|
round
|
||||||
smaller
|
smaller
|
||||||
|
title={backfilled ? t("chat.backfill.started") : t("chat.backfill")}
|
||||||
variant="translucent"
|
variant="translucent"
|
||||||
>
|
>
|
||||||
<Icon name="cloud-download" />
|
<Icon name={backfilled ? "check" : "cloud-download"} />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@@ -287,8 +298,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
|
overflow: hidden;
|
||||||
|
display: block;
|
||||||
|
|
||||||
font-size: 0.8125rem;
|
font-size: 0.8125rem;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&.online {
|
&.online {
|
||||||
color: var(--color-primary);
|
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) {
|
function measure(el: HTMLElement) {
|
||||||
({ scrollTop } = el);
|
({ scrollTop } = el);
|
||||||
if (el.scrollTop + el.clientHeight >= el.scrollHeight - SCROLL_THRESHOLD) {
|
if (el.scrollTop + el.clientHeight >= el.scrollHeight - SCROLL_THRESHOLD) {
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
import Avatar from "#lib/components/ui/Avatar.svelte";
|
import Avatar from "#lib/components/ui/Avatar.svelte";
|
||||||
import ContextMenu from "#lib/components/ui/ContextMenu.svelte";
|
import ContextMenu from "#lib/components/ui/ContextMenu.svelte";
|
||||||
import ContextMenuItem from "#lib/components/ui/ContextMenuItem.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 { formatListDate } from "#lib/format/datetime.js";
|
||||||
import { t } from "#lib/i18n/locale.svelte.js";
|
import { t } from "#lib/i18n/locale.svelte.js";
|
||||||
import { accounts } from "#lib/stores/accounts.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 { peers } from "#lib/stores/peers.svelte.js";
|
||||||
import { policyUi } from "#lib/stores/policy.svelte.js";
|
import { policyUi } from "#lib/stores/policy.svelte.js";
|
||||||
import { type RightPanel, ui } from "#lib/stores/ui.svelte.js";
|
import { type RightPanel, ui } from "#lib/stores/ui.svelte.js";
|
||||||
@@ -31,6 +33,14 @@
|
|||||||
? t("chat.deletedAccount")
|
? t("chat.deletedAccount")
|
||||||
: t("chat.fallbackTitle", { id: chat.chat_id }))
|
: 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 avatarKind = $derived(chat.chat_id > 0 ? "peer" : "chat");
|
||||||
const ownId = $derived(accounts.selected?.tg_user_id ?? null);
|
const ownId = $derived(accounts.selected?.tg_user_id ?? null);
|
||||||
const showSender = $derived(
|
const showSender = $derived(
|
||||||
@@ -74,12 +84,14 @@
|
|||||||
class:selected
|
class:selected
|
||||||
use:ripple
|
use:ripple
|
||||||
>
|
>
|
||||||
<Avatar
|
<span class="avatar" class:online={isOnline}>
|
||||||
avatar={{ id: chat.chat_id, kind: avatarKind }}
|
<Avatar
|
||||||
colorKey={chat.chat_id}
|
avatar={{ id: chat.chat_id, kind: avatarKind }}
|
||||||
hasAvatar={chat.has_avatar}
|
colorKey={chat.chat_id}
|
||||||
name={title}
|
hasAvatar={chat.has_avatar}
|
||||||
/>
|
name={title}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<h3 class="title">{title}</h3>
|
<h3 class="title">{title}</h3>
|
||||||
@@ -90,7 +102,13 @@
|
|||||||
{#if senderPrefix}
|
{#if senderPrefix}
|
||||||
<span class="sender">{senderPrefix}</span>
|
<span class="sender">{senderPrefix}</span>
|
||||||
{/if}
|
{/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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,11 +165,15 @@
|
|||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
&:hover {
|
&:hover {
|
||||||
|
--online-ring: var(--color-chat-hover);
|
||||||
|
|
||||||
background-color: var(--color-chat-hover);
|
background-color: var(--color-chat-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.selected {
|
&.selected {
|
||||||
|
--online-ring: var(--color-chat-active);
|
||||||
|
|
||||||
background-color: var(--color-chat-active);
|
background-color: var(--color-chat-active);
|
||||||
|
|
||||||
.title,
|
.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 {
|
.info {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { folders } from "#lib/stores/folders.svelte.js";
|
|||||||
const POLL_INTERVAL = 1500;
|
const POLL_INTERVAL = 1500;
|
||||||
const POLL_MAX = 12;
|
const POLL_MAX = 12;
|
||||||
const PAGE_SIZE = 40;
|
const PAGE_SIZE = 40;
|
||||||
|
const CHANNEL_IDS = -1_000_000_000_000;
|
||||||
const ALL = "all";
|
const ALL = "all";
|
||||||
const EMPTY: Chat[] = [];
|
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) {
|
function applyEvent(event: LiveEvent) {
|
||||||
|
if (event.type === "delete") {
|
||||||
|
applyDelete(event.chat_id, event.message_ids);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (event.type !== "message") {
|
if (event.type !== "message") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -174,6 +197,8 @@ function createChats() {
|
|||||||
}
|
}
|
||||||
seenChat = true;
|
seenChat = true;
|
||||||
existing.last_date = message.date;
|
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_sender_id = message.sender_id;
|
||||||
existing.last_text = message.text;
|
existing.last_text = message.text;
|
||||||
existing.message_count += 1;
|
existing.message_count += 1;
|
||||||
|
|||||||
Reference in new issue
Block a user