refactor(frontend): saved and transcription results show where they were asked, not in a toast

This commit is contained in:
hh committed 2026-10-28 01:56:19 +00:00
1 parent 58b4346166
commit fb869e767b
7 files changed
+127 -36

No files matched your search

@@ -12,6 +12,7 @@
import Reactions from "#lib/components/Reactions.svelte"; import Reactions from "#lib/components/Reactions.svelte";
import ReplyHeader from "#lib/components/ReplyHeader.svelte"; import ReplyHeader from "#lib/components/ReplyHeader.svelte";
import MessageReadItem from "#lib/components/read/MessageReadItem.svelte"; import MessageReadItem from "#lib/components/read/MessageReadItem.svelte";
import SavedItem from "#lib/components/SavedItem.svelte";
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";
@@ -333,13 +334,11 @@
> >
{/if} {/if}
{#if savedUi.available && !savedUi.isSavedChat(message.chat_id)} {#if savedUi.available && !savedUi.isSavedChat(message.chat_id)}
<ContextMenuItem <SavedItem
icon="forward" chatId={message.chat_id}
onselect={() => savedUi.forward(message.chat_id, message.message_id)} copy={Boolean(message.deleted_at || message.is_self_destruct)}
>{message.deleted_at || message.is_self_destruct messageId={message.message_id}
? t("saved.copy") />
: t("saved.forward")}</ContextMenuItem
>
{/if} {/if}
<CopyItem <CopyItem
done={t("chat.menu.linkCopied")} done={t("chat.menu.linkCopied")}
@@ -11,6 +11,7 @@
let text = $state(""); let text = $state("");
let sending = $state(false); let sending = $state(false);
let failure = $state<string | null>(null);
let file = $state<File | null>(null); let file = $state<File | null>(null);
let caption = $state(""); let caption = $state("");
let voice = $state(false); let voice = $state(false);
@@ -49,11 +50,13 @@
return; return;
} }
sending = true; sending = true;
if (await savedUi.send({ text: message })) { const result = await savedUi.send({ text: message });
sending = false;
failure = result.ok ? null : t(result.label);
if (result.ok) {
text = ""; text = "";
requestAnimationFrame(fit); requestAnimationFrame(fit);
} }
sending = false;
} }
async function sendFile() { async function sendFile() {
@@ -61,13 +64,14 @@
return; return;
} }
sending = true; sending = true;
const sent = await savedUi.send({ const result = await savedUi.send({
file, file,
text: caption.trim(), text: caption.trim(),
voice: isAudio && voice, voice: isAudio && voice,
}); });
sending = false; sending = false;
if (sent) { failure = result.ok ? null : t(result.label);
if (result.ok) {
file = null; file = null;
caption = ""; caption = "";
} }
@@ -97,6 +101,7 @@
function onOpenChange(open: boolean) { function onOpenChange(open: boolean) {
if (!(open || sending)) { if (!(open || sending)) {
failure = null;
if (!text) { if (!text) {
text = caption; text = caption;
} }
@@ -108,6 +113,9 @@
</script> </script>
<div class="SavedComposer"> <div class="SavedComposer">
{#if failure && !file}
<p class="failure">{failure}</p>
{/if}
<div class="field"> <div class="field">
<textarea <textarea
aria-label={t("saved.message")} aria-label={t("saved.message")}
@@ -189,6 +197,9 @@
</span> </span>
</button> </button>
{/if} {/if}
{#if failure}
<p class="failure">{failure}</p>
{/if}
</div> </div>
<div class="dialog-actions"> <div class="dialog-actions">
<Dialog.Close> <Dialog.Close>
@@ -212,6 +223,7 @@
.SavedComposer { .SavedComposer {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
flex-wrap: wrap;
align-items: flex-end; align-items: flex-end;
gap: 0.5rem; gap: 0.5rem;
@@ -225,6 +237,20 @@
} }
} }
.failure {
flex-basis: 100%;
margin: 0;
padding: 0 0.25rem;
font-size: 0.875rem;
color: var(--color-error);
}
.dialog-body .failure {
margin-top: 0.75rem;
padding: 0;
}
.field { .field {
display: flex; display: flex;
flex: 1; flex: 1;
@@ -0,0 +1,53 @@
<script lang="ts">
import { ContextMenu } from "bits-ui";
import { menuCloser } from "#lib/components/ui/ContextMenu.svelte";
import Icon from "#lib/components/ui/Icon.svelte";
import Spinner from "#lib/components/ui/Spinner.svelte";
import { t } from "#lib/i18n/locale.svelte.js";
import { type SavedOutcome, savedUi } from "#lib/stores/saved.svelte.js";
interface Props {
chatId: number;
copy: boolean;
messageId: number;
}
const { chatId, copy, messageId }: Props = $props();
const DONE_DELAY = 900;
const FAILED_DELAY = 2400;
const close = menuCloser();
let busy = $state(false);
let result = $state<SavedOutcome | null>(null);
const icon = $derived.by(() => {
if (result === null) {
return "forward";
}
return result.ok ? "check" : "warning";
});
async function send() {
if (busy || result !== null) {
return;
}
busy = true;
result = await savedUi.forward(chatId, messageId);
busy = false;
setTimeout(close, result.ok ? DONE_DELAY : FAILED_DELAY);
}
</script>
<ContextMenu.Item class="bg-menu-item" closeOnSelect={false} onSelect={send}>
{#if busy}
<Spinner color="gray" size="1.125rem" />
{:else}
<Icon name={icon} size="1.125rem" />
{/if}
{#if result}
{t(result.label)}
{:else}
{copy ? t("saved.copy") : t("saved.forward")}
{/if}
</ContextMenu.Item>
@@ -134,6 +134,8 @@
</button> </button>
{#if stt.open} {#if stt.open}
<div class="transcript">{stt.text}</div> <div class="transcript">{stt.text}</div>
{:else if stt.failure}
<div class="transcript failed">{stt.failure}</div>
{/if} {/if}
</div> </div>
@@ -178,6 +180,11 @@
color: var(--color-text); color: var(--color-text);
white-space: pre-wrap; white-space: pre-wrap;
} }
.failed {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.RoundVideo { .RoundVideo {
cursor: pointer; cursor: pointer;
@@ -157,6 +157,8 @@
</div> </div>
{#if stt.open} {#if stt.open}
<div class="transcript">{stt.text}</div> <div class="transcript">{stt.text}</div>
{:else if stt.failure}
<div class="transcript failed">{stt.failure}</div>
{/if} {/if}
</div> </div>
@@ -223,6 +225,11 @@
color: var(--color-text); color: var(--color-text);
white-space: pre-wrap; white-space: pre-wrap;
} }
.failed {
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.toggle { .toggle {
cursor: pointer; cursor: pointer;
+4 -3
View File
@@ -1,7 +1,6 @@
import { requestTranscription } from "#lib/api/media.js"; import { requestTranscription } from "#lib/api/media.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 { toasts } from "#lib/stores/toasts.svelte.js";
const REFUSALS: Record<string, MessageKey> = { const REFUSALS: Record<string, MessageKey> = {
empty: "media.transcribeEmpty", empty: "media.transcribeEmpty",
@@ -14,6 +13,7 @@ export class Transcript {
readonly #chatId: () => number; readonly #chatId: () => number;
readonly #messageId: () => number; readonly #messageId: () => number;
readonly #initial: () => string | null; readonly #initial: () => string | null;
failure = $state<string | null>(null);
fetched = $state<string | null>(null); fetched = $state<string | null>(null);
pending = $state(false); pending = $state(false);
shown = $state(false); shown = $state(false);
@@ -45,6 +45,7 @@ export class Transcript {
return; return;
} }
this.pending = true; this.pending = true;
this.failure = null;
try { try {
const outcome = await requestTranscription( const outcome = await requestTranscription(
this.#chatId(), this.#chatId(),
@@ -54,10 +55,10 @@ export class Transcript {
this.fetched = outcome.text; this.fetched = outcome.text;
this.shown = true; this.shown = true;
} else { } else {
toasts.error(t(REFUSALS[outcome.error] ?? "media.transcribeFailed")); this.failure = t(REFUSALS[outcome.error] ?? "media.transcribeFailed");
} }
} catch { } catch {
toasts.error(t("media.transcribeFailed")); this.failure = t("media.transcribeFailed");
} finally { } finally {
this.pending = false; this.pending = false;
} }
+20 -22
View File
@@ -4,25 +4,29 @@ import {
type SavedResult, type SavedResult,
sendToSaved, sendToSaved,
} from "#lib/api/endpoints.js"; } from "#lib/api/endpoints.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 { accounts } from "#lib/stores/accounts.svelte.js"; import { accounts } from "#lib/stores/accounts.svelte.js";
import { toasts } from "#lib/stores/toasts.svelte.js";
export interface SavedOutcome {
label: MessageKey;
ok: boolean;
}
const REFUSALS: Record<string, MessageKey> = { const REFUSALS: Record<string, MessageKey> = {
media_missing: "saved.mediaMissing", media_missing: "saved.mediaMissing",
}; };
function report(result: SavedResult): boolean { function outcome(result: SavedResult): SavedOutcome {
if (result.status === "done") {
return true;
}
if (result.status === "pending") { if (result.status === "pending") {
toasts.show(t("saved.pending")); return { label: "saved.pending", ok: true };
return true;
} }
toasts.error(t(REFUSALS[result.error ?? ""] ?? "saved.failed")); if (result.status === "failed") {
return false; return { label: REFUSALS[result.error ?? ""] ?? "saved.failed", ok: false };
}
return {
label: result.mode === "copied" ? "saved.copied" : "saved.forwarded",
ok: true,
};
} }
export const savedUi = { export const savedUi = {
@@ -31,16 +35,11 @@ export const savedUi = {
return account !== null && !account.is_bot && account.tg_user_id !== null; return account !== null && !account.is_bot && account.tg_user_id !== null;
}, },
async forward(chatId: number, messageId: number): Promise<void> { async forward(chatId: number, messageId: number): Promise<SavedOutcome> {
try { try {
const result = await forwardToSaved(chatId, messageId); return outcome(await forwardToSaved(chatId, messageId));
if (report(result) && result.status === "done") {
toasts.show(
t(result.mode === "copied" ? "saved.copied" : "saved.forwarded")
);
}
} catch { } catch {
toasts.error(t("saved.failed")); return { label: "saved.failed", ok: false };
} }
}, },
@@ -48,12 +47,11 @@ export const savedUi = {
return this.available && accounts.selected?.tg_user_id === chatId; return this.available && accounts.selected?.tg_user_id === chatId;
}, },
async send(draft: SavedDraft): Promise<boolean> { async send(draft: SavedDraft): Promise<SavedOutcome> {
try { try {
return report(await sendToSaved(draft)); return outcome(await sendToSaved(draft));
} catch { } catch {
toasts.error(t("saved.failed")); return { label: "saved.failed", ok: false };
return false;
} }
}, },
}; };