fix(frontend): emptied chat keeps its storage head, shared-only chat explains why nothing can be bulk moved, back from a chat returns to the same place in the list, cleanup wording agrees in number

This commit is contained in:
hh committed 2026-10-14 05:15:27 +02:00
1 parent 2836dcb3a9
commit b8a38092eb
3 files changed
+94 -38

No files matched your search

@@ -1,5 +1,5 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { onDestroy, onMount, tick } from "svelte";
import { SvelteSet } from "svelte/reactivity";
import { mediaUrl } from "#lib/api/client.js";
import { downloadMedia } from "#lib/api/download.js";
@@ -61,7 +61,7 @@
let summary = $state<StorageSummary | null>(null);
let files = $state<StorageFile[]>([]);
let heavyChats = $state<StorageChat[]>([]);
let scopeRevision = $state(0);
let scope = $state<ReturnType<typeof ScopeFiles>>();
let group = $state<StorageGroupName | null>(null);
let loading = $state(false);
let failed = $state(false);
@@ -75,6 +75,7 @@
let viewerIndex = $state(0);
const selected = new SvelteSet<number>();
let token = 0;
let scroller: HTMLElement | null = null;
let undoTimer: ReturnType<typeof setTimeout> | undefined;
let undoShownAt = 0;
@@ -130,10 +131,10 @@
}
}
async function loadPage(current: number): Promise<number> {
async function loadPage(current: number, limit: number): Promise<number> {
if (listing === "chats") {
const batch = await getStorageChats(group, {
limit: PAGE,
limit,
offset: heavyChats.length,
});
if (current === token) {
@@ -144,7 +145,7 @@
}
const batch = await getStorageFiles(
group,
{ limit: PAGE, offset: files.length },
{ limit, offset: files.length },
chatId
);
if (current === token) {
@@ -154,7 +155,7 @@
return batch.length;
}
async function loadMore() {
async function loadMore(limit = PAGE) {
if (loading || done) {
return;
}
@@ -162,9 +163,9 @@
loading = true;
failed = false;
try {
const loaded = await loadPage(current);
const loaded = await loadPage(current, limit);
if (current === token) {
done = loaded < PAGE;
done = loaded < limit;
}
} catch {
if (current === token) {
@@ -177,15 +178,34 @@
}
}
function reload() {
function reload(limit = PAGE) {
token += 1;
files = [];
heavyChats = [];
done = false;
loading = false;
selected.clear();
loadSummary();
loadMore();
return Promise.all([loadSummary(), loadMore(limit)]);
}
async function resume() {
const from = chatId === null ? storageUi.takeResume() : null;
await reload(Math.ceil(Math.max(1, from?.count ?? 0) / PAGE) * PAGE);
if (from && scroller) {
await tick();
scroller.scrollTop = from.top;
}
}
function openChat(id: number) {
storageUi.openChat(id, {
count: heavyChats.length,
top: scroller?.scrollTop ?? 0,
});
}
function hold(node: HTMLElement) {
scroller = node.parentElement;
}
function toggle(mediaId: number) {
@@ -226,7 +246,7 @@
}
files = files.filter((file) => !moved.has(file.media_id));
selected.clear();
scopeRevision += 1;
scope?.refresh();
showUndo(outcome, trashed);
if (outcome.blocked.length > 0) {
problem = "Файл занят в сторис или аватаре и остался на диске";
@@ -250,7 +270,7 @@
files = [...files, ...restored.files].sort(
(a, b) => b.file_size - a.file_size
);
scopeRevision += 1;
scope?.refresh();
await loadSummary();
} catch {
problem = "Не удалось вернуть файлы";
@@ -301,16 +321,16 @@
}
}
onMount(reload);
onMount(resume);
onDestroy(() => clearTimeout(undoTimer));
</script>
<div class="storage">
<div class="storage" {@attach hold}>
{#if summary}
{#if summary.files > 0}
{#if summary.files > 0 || chatId !== null}
<section class="summary">
<div class="hero">
<span class="total">{formatBytes(summary.bytes)}</span>
<span class="total">{formatBytes(summary.bytes) || "0 B"}</span>
<span class="count">
{filesLabel(summary.files)}
{#if chatId !== null}
@@ -322,7 +342,7 @@
{/if}
</span>
</div>
<div class="stack">
<div class="stack" class:bare={groups.length === 0}>
{#each groups as item (item.group)}
<span
title="{GROUP_LABELS[item.group]}: {formatBytes(item.bytes)}"
@@ -392,16 +412,15 @@
{/each}
</div>
{:else}
{#key scopeRevision}
<ScopeFiles
label="Все файлы чата"
onchange={() => {
hideUndo();
reload();
}}
target={{ chatId }}
/>
{/key}
<ScopeFiles
label="Все файлы чата"
onchange={() => {
hideUndo();
reload();
}}
target={{ chatId }}
bind:this={scope}
/>
{/if}
{/if}
@@ -423,15 +442,12 @@
<StorageChatRow
{chat}
hasAvatar={chats.byId(chat.chat_id)?.has_avatar ?? false}
onopen={() => storageUi.openChat(chat.chat_id)}
onopen={() => openChat(chat.chat_id)}
title={chatTitle(chat.chat_id)}
weight={heaviestChat > 0 ? chat.bytes / heaviestChat : 0}
>
{#snippet menu()}
<ContextMenuItem
icon="data"
onselect={() => storageUi.openChat(chat.chat_id)}
>
<ContextMenuItem icon="data" onselect={() => openChat(chat.chat_id)}>
Файлы чата
</ContextMenuItem>
<ContextMenuItem icon="reply" onselect={() => visit(chat)}>
@@ -491,12 +507,14 @@
{:else if failed}
<div class="state">
<p>Не удалось загрузить хранилище.</p>
<Button onclick={reload} smaller variant="text">Повторить</Button>
<Button onclick={() => reload()} smaller variant="text">
Повторить
</Button>
</div>
{:else if shown === 0}
<div class="state"><p>{emptiness}</p></div>
{:else if !done}
<button class="more" onclick={loadMore} type="button">
<button class="more" onclick={() => loadMore()} type="button">
Показать ещё
</button>
{/if}
@@ -599,6 +617,11 @@
height: 0.625rem;
margin-top: 0.875rem;
&.bare {
border-radius: 0.3125rem;
background-color: rgba(var(--color-text-secondary-rgb), 0.16);
}
span {
min-width: 0.1875rem;
border-radius: 2px;