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 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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Reference in new issue
Block a user