feat(frontend): a finished action answers on the button that was pressed instead of a toast
This commit is contained in:
1 parent
e228d999dd
commit
bca0b34636
15 files changed
+170
-64
No files matched your search
@@ -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">
|
||||||
|
{#if queued.current === messageId}
|
||||||
|
<Icon name="check" />
|
||||||
|
{t("chat.media.queued")}
|
||||||
|
{:else}
|
||||||
{t("media.downloadToArchive")}
|
{t("media.downloadToArchive")}
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{:else if result?.state === "missing"}
|
{:else if result?.state === "missing"}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
{#if started.current === "sync"}
|
||||||
|
<Icon name="check" />
|
||||||
|
<span>{t("jobs.status.pending")}</span>
|
||||||
|
{:else}
|
||||||
<Icon name="reload" />
|
<Icon name="reload" />
|
||||||
<span>{t("jobs.syncDialogs")}</span>
|
<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"
|
||||||
>
|
>
|
||||||
|
{#if started.current === "contacts"}
|
||||||
|
<Icon name="check" />
|
||||||
|
<span>{t("jobs.status.pending")}</span>
|
||||||
|
{:else}
|
||||||
<Icon name="user" />
|
<Icon name="user" />
|
||||||
<span>{t("jobs.syncContacts")}</span>
|
<span>{t("jobs.syncContacts")}</span>
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -188,8 +200,13 @@
|
|||||||
onclick={start}
|
onclick={start}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
>
|
>
|
||||||
|
{#if started.current === "backfill"}
|
||||||
|
<Icon name="check" />
|
||||||
|
<span>{t("jobs.status.pending")}</span>
|
||||||
|
{:else}
|
||||||
<Icon name="cloud-download" />
|
<Icon name="cloud-download" />
|
||||||
<span>{t("jobs.startBackfill")}</span>
|
<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>
|
||||||
|
{#if saved.current === share.id}
|
||||||
|
<Icon name="check" />
|
||||||
|
{t("shares.saved")}
|
||||||
|
{:else}
|
||||||
{t("common.save")}
|
{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"
|
||||||
>
|
>
|
||||||
|
{#if started.current === peerId}
|
||||||
|
<Icon name="check" />{t("stories.backfillStarted")}
|
||||||
|
{:else}
|
||||||
<Icon name="cloud-download" />{t("stories.loadOlder")}
|
<Icon name="cloud-download" />{t("stories.loadOlder")}
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
aria-label={t("ui.refresh")}
|
aria-label={t("ui.refresh")}
|
||||||
|
|||||||
@@ -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,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: "Войти по номеру телефона",
|
||||||
|
|||||||
@@ -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: "Не удалось поставить в очередь",
|
||||||
|
|||||||
@@ -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: "Не удалось обновить ссылку",
|
||||||
|
|||||||
@@ -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.",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user