feat(frontend): exact time on the message time, deletion and edit times in the meta, note marker, copy link answered in the menu, media menu keeps the message items, date pill opens the calendar, capture gaps between messages
This commit is contained in:
1 parent
848d2aca1a
commit
171d8d922d
8 files changed
+479
-131
No files matched your search
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { ContextMenu as Menu } from "bits-ui";
|
||||
import type { Snippet } from "svelte";
|
||||
import { visible } from "#lib/actions/visible.js";
|
||||
import { downloadMedia } from "#lib/api/download.js";
|
||||
import { fetchMedia } from "#lib/api/endpoints.js";
|
||||
@@ -14,24 +16,25 @@
|
||||
import TgsSticker from "#lib/components/media/TgsSticker.svelte";
|
||||
import VideoNote from "#lib/components/media/VideoNote.svelte";
|
||||
import VoiceMessage from "#lib/components/media/VoiceMessage.svelte";
|
||||
import MessageReadItem from "#lib/components/read/MessageReadItem.svelte";
|
||||
import ContextMenu from "#lib/components/ui/ContextMenu.svelte";
|
||||
import ContextMenuItem from "#lib/components/ui/ContextMenuItem.svelte";
|
||||
import Icon from "#lib/components/ui/Icon.svelte";
|
||||
import Spinner from "#lib/components/ui/Spinner.svelte";
|
||||
import { formatBytes, mediaKindLabel } from "#lib/format/media.js";
|
||||
import { t } from "#lib/i18n/locale.svelte.js";
|
||||
import { poster } from "#lib/media/poster.js";
|
||||
import { shareUi } from "#lib/stores/shares.svelte.js";
|
||||
import { toasts } from "#lib/stores/toasts.svelte.js";
|
||||
import { ui } from "#lib/stores/ui.svelte.js";
|
||||
|
||||
interface Props {
|
||||
items?: Snippet;
|
||||
message: MessageView;
|
||||
onopen: () => void;
|
||||
onversions?: () => void;
|
||||
own: boolean;
|
||||
}
|
||||
|
||||
let { message, onopen, own }: Props = $props();
|
||||
let { items, message, onopen, onversions, own }: Props = $props();
|
||||
|
||||
const POLL_TRIES = 5;
|
||||
const POLL_DELAY = 3000;
|
||||
@@ -39,6 +42,7 @@
|
||||
let loaded = $state(false);
|
||||
let media = $state<InlineMedia | null>(null);
|
||||
let queuing = $state(false);
|
||||
let queued = $state(false);
|
||||
let saving = $state(false);
|
||||
|
||||
const ref = $derived(message.media[0] ?? null);
|
||||
@@ -71,7 +75,10 @@
|
||||
const storedId = $derived(
|
||||
ready?.mediaId ?? (asFile ? ref?.id : null) ?? null
|
||||
);
|
||||
const fileName = $derived(ref?.file_name ?? mediaKindLabel(kind) ?? "Файл");
|
||||
const fileName = $derived(
|
||||
ref?.file_name ?? mediaKindLabel(kind) ?? t("chat.media.file")
|
||||
);
|
||||
const waiting = $derived(queuing || queued);
|
||||
|
||||
function delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
@@ -101,16 +108,16 @@
|
||||
}
|
||||
|
||||
async function queue() {
|
||||
if (queuing) {
|
||||
if (waiting) {
|
||||
return;
|
||||
}
|
||||
queuing = true;
|
||||
try {
|
||||
await fetchMedia(message.chat_id, message.message_id);
|
||||
toasts.success("Скачивание поставлено в очередь");
|
||||
queued = true;
|
||||
poll();
|
||||
} catch {
|
||||
toasts.error("Не удалось поставить в очередь");
|
||||
toasts.error(t("chat.media.queueFailed"));
|
||||
} finally {
|
||||
queuing = false;
|
||||
}
|
||||
@@ -124,7 +131,7 @@
|
||||
try {
|
||||
await downloadMedia(storedId);
|
||||
} catch {
|
||||
toasts.error("Не удалось скачать файл");
|
||||
toasts.error(t("chat.media.downloadFailed"));
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
@@ -145,7 +152,7 @@
|
||||
{#if message.is_self_destruct}
|
||||
<button class="media-chip self-destruct" onclick={onopen} type="button">
|
||||
<Icon name="timer" size="1.25rem" />
|
||||
<span>Самоуничтожающееся медиа</span>
|
||||
<span>{t("chat.media.selfDestruct")}</span>
|
||||
</button>
|
||||
{:else if asFile && ref}
|
||||
<FileChip media={ref} {own} />
|
||||
@@ -165,11 +172,11 @@
|
||||
<AudioFile {own} title={fileName} url={ready.url} />
|
||||
{:else if ready && isImage}
|
||||
<button class="media-thumb" onclick={onopen} type="button">
|
||||
<img alt="attachment" src={ready.url}>
|
||||
<img alt={t("chat.media.photo")} src={ready.url}>
|
||||
</button>
|
||||
{:else if ready && isStaticSticker}
|
||||
<button class="media-sticker" onclick={onopen} type="button">
|
||||
<img alt="sticker" src={ready.url}>
|
||||
<img alt={fileName} src={ready.url}>
|
||||
</button>
|
||||
{:else if ready && isVideoSticker}
|
||||
<video
|
||||
@@ -205,46 +212,59 @@
|
||||
</button>
|
||||
{:else if media?.state === "not-downloaded" && vk !== "other"}
|
||||
<button class="media-placeholder" onclick={queue} type="button">
|
||||
<Icon name={queuing ? "timer" : "download"} size="1.5rem" />
|
||||
<span>{vk === "video" ? "Видео" : "Фото"}</span>
|
||||
<small>{queuing ? "В очереди" : "Нажмите, чтобы скачать"}</small>
|
||||
<Icon name={waiting ? "timer" : "download"} size="1.5rem" />
|
||||
<span
|
||||
>{vk === "video"
|
||||
? t("chat.media.video")
|
||||
: t("chat.media.photo")}</span
|
||||
>
|
||||
<small
|
||||
>{waiting
|
||||
? t("chat.media.queued")
|
||||
: t("chat.media.tapToDownload")}</small
|
||||
>
|
||||
</button>
|
||||
{:else if media?.state === "not-downloaded"}
|
||||
<button class="media-chip" onclick={queue} type="button">
|
||||
<Icon name={queuing ? "timer" : "download"} size="1.25rem" />
|
||||
<span>{queuing ? "В очереди" : `Скачать ${label}`}</span>
|
||||
<Icon name={waiting ? "timer" : "download"} size="1.25rem" />
|
||||
<span
|
||||
>{waiting
|
||||
? t("chat.media.queued")
|
||||
: t("chat.media.download", { kind: label })}</span
|
||||
>
|
||||
</button>
|
||||
{:else}
|
||||
<button class="media-chip" onclick={onopen} type="button">
|
||||
<Icon name="photo" size="1.25rem" />
|
||||
<span>Медиа</span>
|
||||
<span>{t("chat.media")}</span>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet menu()}
|
||||
{#if own}
|
||||
<MessageReadItem {message} />
|
||||
{/if}
|
||||
<ContextMenuItem icon="open-in-new-tab" onselect={onopen}
|
||||
>Открыть на весь экран</ContextMenuItem
|
||||
>
|
||||
<ContextMenuItem
|
||||
icon="recent"
|
||||
onselect={() => ui.openMessagePanel("versions", message.message_id)}
|
||||
>Версии медиа</ContextMenuItem
|
||||
>{t("chat.media.fullscreen")}</ContextMenuItem
|
||||
>
|
||||
{#if onversions}
|
||||
<ContextMenuItem icon="recent" onselect={onversions}
|
||||
>{t("chat.media.versions")}</ContextMenuItem
|
||||
>
|
||||
{/if}
|
||||
{#if storedId === null}
|
||||
<ContextMenuItem icon="cloud-download" onselect={queue}>
|
||||
Скачать в архив
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem icon="cloud-download" onselect={queue}
|
||||
>{t("chat.media.queue")}</ContextMenuItem
|
||||
>
|
||||
{:else}
|
||||
<ContextMenuItem icon="download" onselect={save}>
|
||||
Скачать файл
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem icon="allow-share" onselect={share}>
|
||||
Доступ по ссылке
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem icon="download" onselect={save}
|
||||
>{t("chat.media.save")}</ContextMenuItem
|
||||
>
|
||||
<ContextMenuItem icon="allow-share" onselect={share}
|
||||
>{t("chat.media.share")}</ContextMenuItem
|
||||
>
|
||||
{/if}
|
||||
{#if items}
|
||||
<Menu.Separator class="bg-menu-separator" />
|
||||
{@render items()}
|
||||
{/if}
|
||||
{/snippet}
|
||||
</ContextMenu>
|
||||
|
||||
Reference in new issue
Block a user