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

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