feat(frontend): a finished action answers on the button that was pressed instead of a toast

This commit is contained in:
hh committed 2026-10-19 06:07:54 +00:00
1 parent e228d999dd
commit bca0b34636
15 files changed
+179 -73

No files matched your search

+11 -3
View File
@@ -17,6 +17,7 @@
import { formatBytes, mediaKindLabel } from "#lib/format/media.js"; import { formatBytes, mediaKindLabel } from "#lib/format/media.js";
import { t } from "#lib/i18n/locale.svelte.js"; import { t } from "#lib/i18n/locale.svelte.js";
import { poster } from "#lib/media/poster.js"; import { poster } from "#lib/media/poster.js";
import { createFlash } from "#lib/stores/flash.svelte.js";
import { shareUi } from "#lib/stores/shares.svelte.js"; import { shareUi } from "#lib/stores/shares.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js"; import { toasts } from "#lib/stores/toasts.svelte.js";
@@ -49,6 +50,7 @@
let loading = $state(false); let loading = $state(false);
let saving = $state(false); let saving = $state(false);
let token = 0; let token = 0;
const queued = createFlash<number>();
const mime = $derived(result?.state === "ready" ? (result.mime ?? "") : ""); const mime = $derived(result?.state === "ready" ? (result.mime ?? "") : "");
const isImage = $derived(mime.startsWith("image/") || kind === "photo"); const isImage = $derived(mime.startsWith("image/") || kind === "photo");
@@ -140,9 +142,10 @@
if (messageId === null) { if (messageId === null) {
return; return;
} }
const target = messageId;
try { try {
await fetchMedia(chatId, messageId); await fetchMedia(chatId, target);
toasts.success(t("media.queued")); queued.show(target);
} catch { } catch {
toasts.error(t("media.queueFailed")); toasts.error(t("media.queueFailed"));
} }
@@ -312,7 +315,12 @@
<div class="media-message"> <div class="media-message">
<p>{t("media.notDownloaded")}</p> <p>{t("media.notDownloaded")}</p>
<Button fluid onclick={queueFetch} variant="primary"> <Button fluid onclick={queueFetch} variant="primary">
{t("media.downloadToArchive")} {#if queued.current === messageId}
<Icon name="check" />
{t("chat.media.queued")}
{:else}
{t("media.downloadToArchive")}
{/if}
</Button> </Button>
</div> </div>
{:else if result?.state === "missing"} {:else if result?.state === "missing"}
+8 -4
View File
@@ -6,6 +6,7 @@
import { accounts } from "#lib/stores/accounts.svelte.js"; import { accounts } from "#lib/stores/accounts.svelte.js";
import { chats } from "#lib/stores/chats.svelte.js"; import { chats } from "#lib/stores/chats.svelte.js";
import { discover } from "#lib/stores/discover.svelte.js"; import { discover } from "#lib/stores/discover.svelte.js";
import { createFlash } from "#lib/stores/flash.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js"; import { toasts } from "#lib/stores/toasts.svelte.js";
interface Props { interface Props {
@@ -15,6 +16,7 @@
let { chatId }: Props = $props(); let { chatId }: Props = $props();
let busy = $state(false); let busy = $state(false);
const started = createFlash<true>();
const item = $derived(discover.get(chatId)); const item = $derived(discover.get(chatId));
const tracked = $derived(item?.tracked ?? false); const tracked = $derived(item?.tracked ?? false);
@@ -34,7 +36,7 @@
busy = true; busy = true;
try { try {
discover.set(await trackChat(chatId, true)); discover.set(await trackChat(chatId, true));
toasts.success(t("track.started")); started.show(true);
chats.refresh(); chats.refresh();
} catch { } catch {
toasts.error(t("track.failed")); toasts.error(t("track.failed"));
@@ -50,7 +52,7 @@
busy = true; busy = true;
try { try {
await enqueueBackfill(chatId, true, true); await enqueueBackfill(chatId, true, true);
toasts.success(t("jobs.backfillStarted")); started.show(true);
} catch { } catch {
toasts.error(t("jobs.backfillFailed")); toasts.error(t("jobs.backfillFailed"));
} finally { } finally {
@@ -67,8 +69,10 @@
<div class="actions"> <div class="actions">
{#if tracked} {#if tracked}
<Button loading={busy} onclick={backfill} pill variant="secondary"> <Button loading={busy} onclick={backfill} pill variant="secondary">
<Icon name="cloud-download" /> <Icon name={started.current ? "check" : "cloud-download"} />
<span>{t("track.loadHistory")}</span> <span
>{started.current ? t("track.loading") : t("track.loadHistory")}</span
>
</Button> </Button>
{:else} {:else}
<Button loading={busy} onclick={track} pill variant="primary"> <Button loading={busy} onclick={track} pill variant="primary">
@@ -11,6 +11,7 @@
import { t } from "#lib/i18n/locale.svelte.js"; import { t } from "#lib/i18n/locale.svelte.js";
import { createChatPicker } from "#lib/stores/chat-picker.svelte.js"; import { createChatPicker } from "#lib/stores/chat-picker.svelte.js";
import { chats } from "#lib/stores/chats.svelte.js"; import { chats } from "#lib/stores/chats.svelte.js";
import { createFlash } from "#lib/stores/flash.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js"; import { toasts } from "#lib/stores/toasts.svelte.js";
import { page } from "$app/state"; import { page } from "$app/state";
@@ -24,6 +25,7 @@
let starting = $state(false); let starting = $state(false);
let syncing = $state(false); let syncing = $state(false);
let syncingContacts = $state(false); let syncingContacts = $state(false);
const started = createFlash<"backfill" | "sync" | "contacts">();
const picker = createChatPicker(); const picker = createChatPicker();
const availableChats = $derived(picker.results); const availableChats = $derived(picker.results);
@@ -58,7 +60,7 @@
starting = true; starting = true;
try { try {
await enqueueBackfill(selected, media, true); await enqueueBackfill(selected, media, true);
toasts.success(t("jobs.backfillStarted")); started.show("backfill");
version += 1; version += 1;
} catch { } catch {
toasts.error(t("jobs.backfillFailed")); toasts.error(t("jobs.backfillFailed"));
@@ -74,7 +76,7 @@
syncing = true; syncing = true;
try { try {
await syncDialogs(); await syncDialogs();
toasts.success(t("jobs.syncStarted")); started.show("sync");
version += 1; version += 1;
} catch { } catch {
toasts.error(t("jobs.syncFailed")); toasts.error(t("jobs.syncFailed"));
@@ -90,7 +92,7 @@
syncingContacts = true; syncingContacts = true;
try { try {
await syncContacts(); await syncContacts();
toasts.success(t("jobs.contactsStarted")); started.show("contacts");
version += 1; version += 1;
} catch { } catch {
toasts.error(t("jobs.contactsFailed")); toasts.error(t("jobs.contactsFailed"));
@@ -110,8 +112,13 @@
<p class="hint">{t("jobs.dialogsHint")}</p> <p class="hint">{t("jobs.dialogsHint")}</p>
<div class="action"> <div class="action">
<Button fluid loading={syncing} onclick={sync} variant="secondary"> <Button fluid loading={syncing} onclick={sync} variant="secondary">
<Icon name="reload" /> {#if started.current === "sync"}
<span>{t("jobs.syncDialogs")}</span> <Icon name="check" />
<span>{t("jobs.status.pending")}</span>
{:else}
<Icon name="reload" />
<span>{t("jobs.syncDialogs")}</span>
{/if}
</Button> </Button>
</div> </div>
<p class="hint">{t("jobs.contactsHint")}</p> <p class="hint">{t("jobs.contactsHint")}</p>
@@ -122,8 +129,13 @@
onclick={syncContactList} onclick={syncContactList}
variant="secondary" variant="secondary"
> >
<Icon name="user" /> {#if started.current === "contacts"}
<span>{t("jobs.syncContacts")}</span> <Icon name="check" />
<span>{t("jobs.status.pending")}</span>
{:else}
<Icon name="user" />
<span>{t("jobs.syncContacts")}</span>
{/if}
</Button> </Button>
</div> </div>
</section> </section>
@@ -188,8 +200,13 @@
onclick={start} onclick={start}
variant="primary" variant="primary"
> >
<Icon name="cloud-download" /> {#if started.current === "backfill"}
<span>{t("jobs.startBackfill")}</span> <Icon name="check" />
<span>{t("jobs.status.pending")}</span>
{:else}
<Icon name="cloud-download" />
<span>{t("jobs.startBackfill")}</span>
{/if}
</Button> </Button>
</div> </div>
</section> </section>
@@ -11,6 +11,7 @@
import Spinner from "#lib/components/ui/Spinner.svelte"; import Spinner from "#lib/components/ui/Spinner.svelte";
import { t } from "#lib/i18n/locale.svelte.js"; import { t } from "#lib/i18n/locale.svelte.js";
import { poster } from "#lib/media/poster.js"; import { poster } from "#lib/media/poster.js";
import { createFlash } from "#lib/stores/flash.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js"; import { toasts } from "#lib/stores/toasts.svelte.js";
interface Props { interface Props {
@@ -24,7 +25,17 @@
let loaded = $state(false); let loaded = $state(false);
let inline = $state<InlineMedia | null>(null); let inline = $state<InlineMedia | null>(null);
let queuing = $state(false); let queuing = $state(false);
const queued = createFlash<true>();
const queueIcon = $derived.by(() => {
if (queuing) {
return "timer";
}
return queued.current ? "check" : "download";
});
const queueLabel = $derived(
queued.current ? t("chat.media.queued") : t("media.downloadToArchive")
);
const ready = $derived(inline?.state === "ready" ? inline : null); const ready = $derived(inline?.state === "ready" ? inline : null);
const isVideo = $derived(ready ? visualKind(ready.kind) === "video" : false); const isVideo = $derived(ready ? visualKind(ready.kind) === "video" : false);
@@ -40,7 +51,7 @@
queuing = true; queuing = true;
try { try {
await fetchMedia(chatId, media.message_id); await fetchMedia(chatId, media.message_id);
toasts.success(t("media.queued")); queued.show(true);
} catch { } catch {
toasts.error(t("media.queueFailed")); toasts.error(t("media.queueFailed"));
} finally { } finally {
@@ -66,8 +77,14 @@
<img alt={t("media.attachment")} src={ready.url}> <img alt={t("media.attachment")} src={ready.url}>
</button> </button>
{:else if inline?.state === "not-downloaded"} {:else if inline?.state === "not-downloaded"}
<button class="tile placeholder" onclick={queue} type="button"> <button
<Icon name={queuing ? "timer" : "download"} size="1.5rem" /> aria-label={queueLabel}
class="tile placeholder"
onclick={queue}
title={queueLabel}
type="button"
>
<Icon name={queueIcon} size="1.5rem" />
</button> </button>
{:else if loaded} {:else if loaded}
<div class="tile placeholder"><Icon name="photo" size="1.5rem" /></div> <div class="tile placeholder"><Icon name="photo" size="1.5rem" /></div>
@@ -55,9 +55,8 @@
loginId = ""; loginId = "";
await accounts.load(); await accounts.load();
if (state.account) { if (state.account) {
accounts.select(state.account.account_id); accounts.switchTo(state.account.account_id);
} }
toasts.success(t("login.added"));
open = false; open = false;
} }
@@ -19,6 +19,7 @@
import { formatShareLimits } from "#lib/format/shares.js"; import { formatShareLimits } from "#lib/format/shares.js";
import { t } from "#lib/i18n/locale.svelte.js"; import { t } from "#lib/i18n/locale.svelte.js";
import type { MessageKey } from "#lib/i18n/messages.js"; import type { MessageKey } from "#lib/i18n/messages.js";
import { createFlash } from "#lib/stores/flash.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js"; import { toasts } from "#lib/stores/toasts.svelte.js";
interface Props { interface Props {
@@ -72,6 +73,8 @@
let maxDownloads = $state<number | null>(null); let maxDownloads = $state<number | null>(null);
let showQr = $state(false); let showQr = $state(false);
let token = 0; let token = 0;
const saved = createFlash<number>();
const revoked = createFlash<ShareSubject>();
const settings = $derived<ShareSettings>({ const settings = $derived<ShareSettings>({
expiresInSeconds: expiresIn === KEEP ? null : expiresIn, expiresInSeconds: expiresIn === KEEP ? null : expiresIn,
@@ -125,7 +128,6 @@
busy = true; busy = true;
try { try {
adopt(await createShare(subject, settings)); adopt(await createShare(subject, settings));
toasts.success(t("shares.opened"));
} catch (error) { } catch (error) {
fail(error, "shares.openFailed"); fail(error, "shares.openFailed");
} finally { } finally {
@@ -139,8 +141,9 @@
} }
busy = true; busy = true;
try { try {
adopt(await updateShare(share.id, settings)); const next = await updateShare(share.id, settings);
toasts.success(t("shares.updated")); adopt(next);
saved.show(next.id);
} catch (error) { } catch (error) {
fail(error, "shares.updateFailed"); fail(error, "shares.updateFailed");
} finally { } finally {
@@ -149,15 +152,15 @@
} }
async function revoke() { async function revoke() {
if (!share || busy) { if (!(share && subject) || busy) {
return; return;
} }
const target = subject;
busy = true; busy = true;
try { try {
await revokeShare(share.id); await revokeShare(share.id);
adopt(null); adopt(null);
toasts.success(t("shares.revokedToast")); revoked.show(target);
open = false;
} catch (error) { } catch (error) {
fail(error, "shares.revokeFailed"); fail(error, "shares.revokeFailed");
} finally { } finally {
@@ -314,12 +317,22 @@
{#if !loading} {#if !loading}
<div class="dialog-actions"> <div class="dialog-actions">
{#if share} {#if subject && revoked.current === subject}
<p class="done">
<Icon name="check" size="1.25rem" />
{t("shares.accessRevoked")}
</p>
{:else if share}
<Button loading={busy} onclick={revoke} pill variant="danger"> <Button loading={busy} onclick={revoke} pill variant="danger">
{t("shares.revoke")} {t("shares.revoke")}
</Button> </Button>
<Button loading={busy} onclick={applyLimits} pill> <Button loading={busy} onclick={applyLimits} pill>
{t("common.save")} {#if saved.current === share.id}
<Icon name="check" />
{t("shares.saved")}
{:else}
{t("common.save")}
{/if}
</Button> </Button>
{:else} {:else}
<Button fluid loading={busy} onclick={publish} pill> <Button fluid loading={busy} onclick={publish} pill>
@@ -333,6 +346,20 @@
</Dialog.Root> </Dialog.Root>
<style lang="scss"> <style lang="scss">
.done {
display: flex;
flex: 1;
gap: 0.375rem;
align-items: center;
justify-content: center;
height: 3rem;
margin: 0;
font-weight: var(--font-weight-medium);
color: var(--color-text-green);
}
.center { .center {
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { SvelteSet } from "svelte/reactivity";
import { import {
deleteShare, deleteShare,
listShares, listShares,
@@ -11,6 +12,7 @@
import ShareLinkField from "#lib/components/shares/ShareLinkField.svelte"; import ShareLinkField from "#lib/components/shares/ShareLinkField.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 CopyItem from "#lib/components/ui/CopyItem.svelte";
import EmptyState from "#lib/components/ui/EmptyState.svelte"; import EmptyState from "#lib/components/ui/EmptyState.svelte";
import Icon from "#lib/components/ui/Icon.svelte"; import Icon from "#lib/components/ui/Icon.svelte";
import Spinner from "#lib/components/ui/Spinner.svelte"; import Spinner from "#lib/components/ui/Spinner.svelte";
@@ -19,6 +21,7 @@
import { formatDownloads, formatExpiry } from "#lib/format/shares.js"; import { formatDownloads, formatExpiry } from "#lib/format/shares.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 { createFlash } from "#lib/stores/flash.svelte.js";
import { shareUi } from "#lib/stores/shares.svelte.js"; import { shareUi } from "#lib/stores/shares.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js"; import { toasts } from "#lib/stores/toasts.svelte.js";
import { ui } from "#lib/stores/ui.svelte.js"; import { ui } from "#lib/stores/ui.svelte.js";
@@ -31,10 +34,14 @@
let filter = $state<Filter>("active"); let filter = $state<Filter>("active");
let expanded = $state<number | null>(null); let expanded = $state<number | null>(null);
let token = 0; let token = 0;
const kept = new SvelteSet<number>();
const done = createFlash<number>();
const visible = $derived( const visible = $derived(
filter === "active" filter === "active"
? items.filter((item) => shareState(item) === "active") ? items.filter(
(item) => kept.has(item.id) || shareState(item) === "active"
)
: items : items
); );
const activeCount = $derived( const activeCount = $derived(
@@ -50,6 +57,7 @@
const rows = await listShares(activeOnly); const rows = await listShares(activeOnly);
if (current === token) { if (current === token) {
items = rows; items = rows;
kept.clear();
} }
} catch { } catch {
if (current === token) { if (current === token) {
@@ -79,15 +87,11 @@
ui.closePanel(); ui.closePanel();
} }
async function copy(share: FileShare) {
await navigator.clipboard.writeText(shareUrl(share));
toasts.success(t("shares.copied"));
}
async function revoke(share: FileShare) { async function revoke(share: FileShare) {
try { try {
replace(await revokeShare(share.id)); replace(await revokeShare(share.id));
toasts.success(t("shares.revokedToast")); kept.add(share.id);
done.show(share.id);
} catch { } catch {
toasts.error(t("shares.revokeFailed")); toasts.error(t("shares.revokeFailed"));
} }
@@ -96,7 +100,7 @@
async function reissue(share: FileShare) { async function reissue(share: FileShare) {
try { try {
replace(await reissueShare(share.id)); replace(await reissueShare(share.id));
toasts.success(t("shares.reissued")); done.show(share.id);
} catch { } catch {
toasts.error(t("shares.reissueFailed")); toasts.error(t("shares.reissueFailed"));
} }
@@ -205,9 +209,12 @@
</button> </button>
{/snippet} {/snippet}
{#snippet menu()} {#snippet menu()}
<ContextMenuItem icon="copy" onselect={() => copy(share)}> <CopyItem
{t("shares.copy")} done={t("shares.copied")}
</ContextMenuItem> icon="copy"
label={t("shares.copy")}
text={() => shareUrl(share)}
/>
{#if share.message_id !== null || share.peer_id !== null} {#if share.message_id !== null || share.peer_id !== null}
<ContextMenuItem icon="reply" onselect={() => jump(share)}> <ContextMenuItem icon="reply" onselect={() => jump(share)}>
{t("ui.jumpToMessage")} {t("ui.jumpToMessage")}
@@ -247,7 +254,14 @@
<Icon name="reply" size="1rem" />{t("shares.toMessage")} <Icon name="reply" size="1rem" />{t("shares.toMessage")}
</button> </button>
{/if} {/if}
{#if state === "active"} {#if done.current === share.id}
<span class="done">
<Icon name="check" size="1rem" />
{state === "active"
? t("shares.reissued")
: t("shares.accessRevoked")}
</span>
{:else if state === "active"}
<button <button
class="pill danger" class="pill danger"
onclick={() => revoke(share)} onclick={() => revoke(share)}
@@ -461,6 +475,18 @@
} }
} }
.done {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.3125rem 0;
border: 1px solid transparent;
font-size: 0.8125rem;
color: var(--color-text-green);
}
.footnote { .footnote {
margin: 0; margin: 0;
padding: 0 1rem 1rem; padding: 0 1rem 1rem;
@@ -18,6 +18,7 @@
import { peerName } from "#lib/format/peer.js"; import { peerName } from "#lib/format/peer.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 { createFlash } from "#lib/stores/flash.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js"; import { toasts } from "#lib/stores/toasts.svelte.js";
interface Group { interface Group {
@@ -45,6 +46,7 @@
let viewerItems = $state<StoryView[]>([]); let viewerItems = $state<StoryView[]>([]);
let viewerPeerId = $state(0); let viewerPeerId = $state(0);
let backfilling = $state<number | null>(null); let backfilling = $state<number | null>(null);
const started = createFlash<number>();
async function load() { async function load() {
const current = token; const current = token;
@@ -150,7 +152,7 @@
backfilling = peerId; backfilling = peerId;
try { try {
await enqueueStoriesBackfill(peerId); await enqueueStoriesBackfill(peerId);
toasts.success(t("stories.backfillStarted")); started.show(peerId);
} catch { } catch {
toasts.error(t("stories.backfillFailed")); toasts.error(t("stories.backfillFailed"));
} finally { } finally {
@@ -178,6 +180,10 @@
{/if} {/if}
{:else} {:else}
{#each groups as group (group.peerId)} {#each groups as group (group.peerId)}
{@const fresh = started.current === group.peerId}
{@const backfillLabel = fresh
? t("stories.backfillStarted")
: t("stories.loadOlderStories")}
<section class="group"> <section class="group">
<header class="group-head"> <header class="group-head">
<button <button
@@ -200,14 +206,15 @@
</span> </span>
</button> </button>
<Button <Button
aria-label={t("stories.loadOlderStories")} aria-label={backfillLabel}
loading={backfilling === group.peerId} loading={backfilling === group.peerId}
onclick={() => backfill(group.peerId)} onclick={() => backfill(group.peerId)}
round round
smaller smaller
title={backfillLabel}
variant="translucent" variant="translucent"
> >
<Icon name="cloud-download" /> <Icon name={fresh ? "check" : "cloud-download"} />
</Button> </Button>
</header> </header>
{#if expanded[group.peerId]} {#if expanded[group.peerId]}
@@ -11,6 +11,7 @@
import Spinner from "#lib/components/ui/Spinner.svelte"; import Spinner from "#lib/components/ui/Spinner.svelte";
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 { createFlash } from "#lib/stores/flash.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js"; import { toasts } from "#lib/stores/toasts.svelte.js";
import { page } from "$app/state"; import { page } from "$app/state";
@@ -27,6 +28,7 @@
); );
let backfilling = $state(false); let backfilling = $state(false);
const started = createFlash<number>();
let items = $state<StoryView[]>([]); let items = $state<StoryView[]>([]);
let loading = $state(false); let loading = $state(false);
@@ -109,7 +111,7 @@
backfilling = true; backfilling = true;
try { try {
await enqueueStoriesBackfill(id); await enqueueStoriesBackfill(id);
toasts.success(t("stories.backfillStarted")); started.show(id);
} catch { } catch {
toasts.error(t("stories.backfillFailed")); toasts.error(t("stories.backfillFailed"));
} finally { } finally {
@@ -137,7 +139,11 @@
smaller smaller
variant="secondary" variant="secondary"
> >
<Icon name="cloud-download" />{t("stories.loadOlder")} {#if started.current === peerId}
<Icon name="check" />{t("stories.backfillStarted")}
{:else}
<Icon name="cloud-download" />{t("stories.loadOlder")}
{/if}
</Button> </Button>
<Button <Button
aria-label={t("ui.refresh")} aria-label={t("ui.refresh")}
-12
View File
@@ -10,10 +10,6 @@ export const jobs = {
en: "A full backfill rereads the whole chat history from the very beginning. The button in the chat header only loads messages newer than the last saved one.", en: "A full backfill rereads the whole chat history from the very beginning. The button in the chat header only loads messages newer than the last saved one.",
ru: "Полный бэкфилл перечитывает всю историю чата с самого начала. Кнопка в шапке чата догружает только сообщения новее последнего сохранённого.", ru: "Полный бэкфилл перечитывает всю историю чата с самого начала. Кнопка в шапке чата догружает только сообщения новее последнего сохранённого.",
}, },
"jobs.backfillStarted": {
en: "Full backfill started",
ru: "Полный бэкфилл запущен",
},
"jobs.chat": { en: "chat {id}", ru: "чат {id}" }, "jobs.chat": { en: "chat {id}", ru: "чат {id}" },
"jobs.contactsFailed": { "jobs.contactsFailed": {
en: "Could not sync contacts", en: "Could not sync contacts",
@@ -23,10 +19,6 @@ export const jobs = {
en: "Contacts are synced separately, then search finds people you haven't chatted with yet.", en: "Contacts are synced separately, then search finds people you haven't chatted with yet.",
ru: "Контакты подтягиваются отдельно, тогда в поиске появятся люди, с которыми ещё не было переписки.", ru: "Контакты подтягиваются отдельно, тогда в поиске появятся люди, с которыми ещё не было переписки.",
}, },
"jobs.contactsStarted": {
en: "Contact sync started",
ru: "Синхронизация контактов запущена",
},
"jobs.dialogs": { en: "Chats", ru: "Диалоги" }, "jobs.dialogs": { en: "Chats", ru: "Диалоги" },
"jobs.dialogsHint": { "jobs.dialogsHint": {
en: "Sync pulls in every chat of the account, so old chats show up and can be backfilled.", en: "Sync pulls in every chat of the account, so old chats show up and can be backfilled.",
@@ -76,9 +68,5 @@ export const jobs = {
en: "Could not sync chats", en: "Could not sync chats",
ru: "Не удалось синхронизировать диалоги", ru: "Не удалось синхронизировать диалоги",
}, },
"jobs.syncStarted": {
en: "Chat sync started",
ru: "Синхронизация диалогов запущена",
},
"jobs.title": { en: "Jobs", ru: "Задачи" }, "jobs.title": { en: "Jobs", ru: "Задачи" },
} satisfies Messages; } satisfies Messages;
-1
View File
@@ -1,7 +1,6 @@
import type { Messages } from "#lib/i18n/types.js"; import type { Messages } from "#lib/i18n/types.js";
export const login = { export const login = {
"login.added": { en: "Account added", ru: "Аккаунт добавлен" },
"login.byPhone": { "login.byPhone": {
en: "Log in by phone number", en: "Log in by phone number",
ru: "Войти по номеру телефона", ru: "Войти по номеру телефона",
-4
View File
@@ -27,10 +27,6 @@ export const media = {
en: "Play voice message", en: "Play voice message",
ru: "Воспроизвести голосовое", ru: "Воспроизвести голосовое",
}, },
"media.queued": {
en: "Download queued",
ru: "Скачивание поставлено в очередь",
},
"media.queueFailed": { "media.queueFailed": {
en: "Could not queue the download", en: "Could not queue the download",
ru: "Не удалось поставить в очередь", ru: "Не удалось поставить в очередь",
+2 -6
View File
@@ -1,6 +1,7 @@
import type { Messages } from "#lib/i18n/types.js"; import type { Messages } from "#lib/i18n/types.js";
export const shares = { export const shares = {
"shares.accessRevoked": { en: "Access revoked", ru: "Доступ отозван" },
"shares.checkFailed": { "shares.checkFailed": {
en: "Could not check the link", en: "Could not check the link",
ru: "Не удалось проверить ссылку", ru: "Не удалось проверить ссылку",
@@ -79,7 +80,6 @@ export const shares = {
"shares.noChat": { en: "No chat", ru: "Без чата" }, "shares.noChat": { en: "No chat", ru: "Без чата" },
"shares.noExpiry": { en: "No expiry", ru: "Бессрочно" }, "shares.noExpiry": { en: "No expiry", ru: "Бессрочно" },
"shares.noLimit": { en: "No limit", ru: "Без лимита" }, "shares.noLimit": { en: "No limit", ru: "Без лимита" },
"shares.opened": { en: "Link sharing is on", ru: "Доступ по ссылке открыт" },
"shares.openFailed": { "shares.openFailed": {
en: "Could not share the file", en: "Could not share the file",
ru: "Не удалось открыть доступ", ru: "Не удалось открыть доступ",
@@ -115,11 +115,11 @@ export const shares = {
}, },
"shares.revoke": { en: "Revoke", ru: "Отозвать" }, "shares.revoke": { en: "Revoke", ru: "Отозвать" },
"shares.revoked": { en: "revoked", ru: "отозвана" }, "shares.revoked": { en: "revoked", ru: "отозвана" },
"shares.revokedToast": { en: "Access revoked", ru: "Доступ отозван" },
"shares.revokeFailed": { "shares.revokeFailed": {
en: "Could not revoke access", en: "Could not revoke access",
ru: "Не удалось отозвать доступ", ru: "Не удалось отозвать доступ",
}, },
"shares.saved": { en: "Saved", ru: "Сохранено" },
"shares.state.active": { en: "Active", ru: "Открыт" }, "shares.state.active": { en: "Active", ru: "Открыт" },
"shares.state.exhausted": { en: "Limit reached", ru: "Лимит исчерпан" }, "shares.state.exhausted": { en: "Limit reached", ru: "Лимит исчерпан" },
"shares.state.expired": { en: "Expired", ru: "Истёк" }, "shares.state.expired": { en: "Expired", ru: "Истёк" },
@@ -128,10 +128,6 @@ export const shares = {
"shares.tab.all": { en: "All", ru: "Все" }, "shares.tab.all": { en: "All", ru: "Все" },
"shares.toMessage": { en: "Go to message", ru: "К сообщению" }, "shares.toMessage": { en: "Go to message", ru: "К сообщению" },
"shares.until": { en: "until {date}", ru: "до {date}" }, "shares.until": { en: "until {date}", ru: "до {date}" },
"shares.updated": {
en: "Link settings updated",
ru: "Настройки ссылки обновлены",
},
"shares.updateFailed": { "shares.updateFailed": {
en: "Could not update the link", en: "Could not update the link",
ru: "Не удалось обновить ссылку", ru: "Не удалось обновить ссылку",
+1 -4
View File
@@ -11,11 +11,8 @@ export const track = {
ru: "Включите отслеживание, чтобы собирать сообщения, медиа и статистику по этому чату.", ru: "Включите отслеживание, чтобы собирать сообщения, медиа и статистику по этому чату.",
}, },
"track.loadHistory": { en: "Load history", ru: "Загрузить историю" }, "track.loadHistory": { en: "Load history", ru: "Загрузить историю" },
"track.loading": { en: "History is loading", ru: "История загружается" },
"track.start": { en: "Start tracking", ru: "Начать отслеживать" }, "track.start": { en: "Start tracking", ru: "Начать отслеживать" },
"track.started": {
en: "Tracking is on, loading the history",
ru: "Отслеживание включено, история загружается",
},
"track.thisChat": { en: "This chat", ru: "Этот чат" }, "track.thisChat": { en: "This chat", ru: "Этот чат" },
"track.tracked": { "track.tracked": {
en: "{title} is tracked, new messages and stats are collected automatically.", en: "{title} is tracked, new messages and stats are collected automatically.",
+19
View File
@@ -0,0 +1,19 @@
const FLASH_MS = 2000;
export function createFlash<T>() {
let current = $state<T | null>(null);
let timer: ReturnType<typeof setTimeout> | undefined;
return {
get current(): T | null {
return current;
},
show(value: T) {
clearTimeout(timer);
current = value;
timer = setTimeout(() => {
current = null;
}, FLASH_MS);
},
};
}