refactor(frontend): saved and transcription results show where they were asked, not in a toast
This commit is contained in:
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 ReplyHeader from "#lib/components/ReplyHeader.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 ContextMenu from "#lib/components/ui/ContextMenu.svelte";
|
||||
import ContextMenuItem from "#lib/components/ui/ContextMenuItem.svelte";
|
||||
@@ -333,13 +334,11 @@
|
||||
>
|
||||
{/if}
|
||||
{#if savedUi.available && !savedUi.isSavedChat(message.chat_id)}
|
||||
<ContextMenuItem
|
||||
icon="forward"
|
||||
onselect={() => savedUi.forward(message.chat_id, message.message_id)}
|
||||
>{message.deleted_at || message.is_self_destruct
|
||||
? t("saved.copy")
|
||||
: t("saved.forward")}</ContextMenuItem
|
||||
>
|
||||
<SavedItem
|
||||
chatId={message.chat_id}
|
||||
copy={Boolean(message.deleted_at || message.is_self_destruct)}
|
||||
messageId={message.message_id}
|
||||
/>
|
||||
{/if}
|
||||
<CopyItem
|
||||
done={t("chat.menu.linkCopied")}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
let text = $state("");
|
||||
let sending = $state(false);
|
||||
let failure = $state<string | null>(null);
|
||||
let file = $state<File | null>(null);
|
||||
let caption = $state("");
|
||||
let voice = $state(false);
|
||||
@@ -49,11 +50,13 @@
|
||||
return;
|
||||
}
|
||||
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 = "";
|
||||
requestAnimationFrame(fit);
|
||||
}
|
||||
sending = false;
|
||||
}
|
||||
|
||||
async function sendFile() {
|
||||
@@ -61,13 +64,14 @@
|
||||
return;
|
||||
}
|
||||
sending = true;
|
||||
const sent = await savedUi.send({
|
||||
const result = await savedUi.send({
|
||||
file,
|
||||
text: caption.trim(),
|
||||
voice: isAudio && voice,
|
||||
});
|
||||
sending = false;
|
||||
if (sent) {
|
||||
failure = result.ok ? null : t(result.label);
|
||||
if (result.ok) {
|
||||
file = null;
|
||||
caption = "";
|
||||
}
|
||||
@@ -97,6 +101,7 @@
|
||||
|
||||
function onOpenChange(open: boolean) {
|
||||
if (!(open || sending)) {
|
||||
failure = null;
|
||||
if (!text) {
|
||||
text = caption;
|
||||
}
|
||||
@@ -108,6 +113,9 @@
|
||||
</script>
|
||||
|
||||
<div class="SavedComposer">
|
||||
{#if failure && !file}
|
||||
<p class="failure">{failure}</p>
|
||||
{/if}
|
||||
<div class="field">
|
||||
<textarea
|
||||
aria-label={t("saved.message")}
|
||||
@@ -189,6 +197,9 @@
|
||||
</span>
|
||||
</button>
|
||||
{/if}
|
||||
{#if failure}
|
||||
<p class="failure">{failure}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="dialog-actions">
|
||||
<Dialog.Close>
|
||||
@@ -212,6 +223,7 @@
|
||||
.SavedComposer {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
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 {
|
||||
display: flex;
|
||||
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>
|
||||
{#if stt.open}
|
||||
<div class="transcript">{stt.text}</div>
|
||||
{:else if stt.failure}
|
||||
<div class="transcript failed">{stt.failure}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -178,6 +180,11 @@
|
||||
color: var(--color-text);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.failed {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
|
||||
.RoundVideo {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -157,6 +157,8 @@
|
||||
</div>
|
||||
{#if stt.open}
|
||||
<div class="transcript">{stt.text}</div>
|
||||
{:else if stt.failure}
|
||||
<div class="transcript failed">{stt.failure}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -223,6 +225,11 @@
|
||||
color: var(--color-text);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.failed {
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
|
||||
.toggle {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { requestTranscription } from "#lib/api/media.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import type { MessageKey } from "#lib/i18n/messages.js";
|
||||
import { toasts } from "#lib/stores/toasts.svelte.js";
|
||||
|
||||
const REFUSALS: Record<string, MessageKey> = {
|
||||
empty: "media.transcribeEmpty",
|
||||
@@ -14,6 +13,7 @@ export class Transcript {
|
||||
readonly #chatId: () => number;
|
||||
readonly #messageId: () => number;
|
||||
readonly #initial: () => string | null;
|
||||
failure = $state<string | null>(null);
|
||||
fetched = $state<string | null>(null);
|
||||
pending = $state(false);
|
||||
shown = $state(false);
|
||||
@@ -45,6 +45,7 @@ export class Transcript {
|
||||
return;
|
||||
}
|
||||
this.pending = true;
|
||||
this.failure = null;
|
||||
try {
|
||||
const outcome = await requestTranscription(
|
||||
this.#chatId(),
|
||||
@@ -54,10 +55,10 @@ export class Transcript {
|
||||
this.fetched = outcome.text;
|
||||
this.shown = true;
|
||||
} else {
|
||||
toasts.error(t(REFUSALS[outcome.error] ?? "media.transcribeFailed"));
|
||||
this.failure = t(REFUSALS[outcome.error] ?? "media.transcribeFailed");
|
||||
}
|
||||
} catch {
|
||||
toasts.error(t("media.transcribeFailed"));
|
||||
this.failure = t("media.transcribeFailed");
|
||||
} finally {
|
||||
this.pending = false;
|
||||
}
|
||||
|
||||
@@ -4,25 +4,29 @@ import {
|
||||
type SavedResult,
|
||||
sendToSaved,
|
||||
} from "#lib/api/endpoints.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import type { MessageKey } from "#lib/i18n/messages.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> = {
|
||||
media_missing: "saved.mediaMissing",
|
||||
};
|
||||
|
||||
function report(result: SavedResult): boolean {
|
||||
if (result.status === "done") {
|
||||
return true;
|
||||
}
|
||||
function outcome(result: SavedResult): SavedOutcome {
|
||||
if (result.status === "pending") {
|
||||
toasts.show(t("saved.pending"));
|
||||
return true;
|
||||
return { label: "saved.pending", ok: true };
|
||||
}
|
||||
toasts.error(t(REFUSALS[result.error ?? ""] ?? "saved.failed"));
|
||||
return false;
|
||||
if (result.status === "failed") {
|
||||
return { label: REFUSALS[result.error ?? ""] ?? "saved.failed", ok: false };
|
||||
}
|
||||
return {
|
||||
label: result.mode === "copied" ? "saved.copied" : "saved.forwarded",
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
|
||||
export const savedUi = {
|
||||
@@ -31,16 +35,11 @@ export const savedUi = {
|
||||
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 {
|
||||
const result = await forwardToSaved(chatId, messageId);
|
||||
if (report(result) && result.status === "done") {
|
||||
toasts.show(
|
||||
t(result.mode === "copied" ? "saved.copied" : "saved.forwarded")
|
||||
);
|
||||
}
|
||||
return outcome(await forwardToSaved(chatId, messageId));
|
||||
} 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;
|
||||
},
|
||||
|
||||
async send(draft: SavedDraft): Promise<boolean> {
|
||||
async send(draft: SavedDraft): Promise<SavedOutcome> {
|
||||
try {
|
||||
return report(await sendToSaved(draft));
|
||||
return outcome(await sendToSaved(draft));
|
||||
} catch {
|
||||
toasts.error(t("saved.failed"));
|
||||
return false;
|
||||
return { label: "saved.failed", ok: false };
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in new issue
Block a user