flotilla/src/app/components/ContentLinkBlock.svelte

133 lines
4.3 KiB
Svelte
Raw Normal View History

2026-06-23 23:30:54 +00:00
<script module lang="ts">
import {postJson, simpleCache} from "@welshman/lib"
import {dufflepud} from "@app/env"
// Cache previews by url so the same link isn't re-fetched across renders/instances.
const loadPreview = simpleCache(async ([url]: [string]) => {
const json = await postJson(dufflepud("link/preview"), {url})
if (!json?.title && !json?.image) {
throw new Error("Failed to load link preview")
}
return json
})
</script>
2024-09-24 21:12:38 +00:00
<script lang="ts">
2026-06-23 23:30:54 +00:00
import {call, ellipsize, displayUrl} from "@welshman/lib"
import {isRelayUrl} from "@welshman/util"
import {Capacitor} from "@capacitor/core"
2025-02-04 03:28:29 +00:00
import {preventDefault, stopPropagation} from "@lib/html"
2024-09-24 21:12:38 +00:00
import Link from "@lib/components/Link.svelte"
import Spinner from "@lib/components/Spinner.svelte"
2024-10-17 23:03:50 +00:00
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
import ContentLinkUrl from "@app/components/ContentLinkUrl.svelte"
2025-04-28 22:46:48 +00:00
import ContentLinkBlockImage from "@app/components/ContentLinkBlockImage.svelte"
2026-06-08 21:14:40 +00:00
import {pushModal} from "@app/modal"
2026-06-23 23:30:54 +00:00
import {PLATFORM_URL, THUMBNAIL_URL} from "@app/env"
import {
getUrlContentType,
AUDIO_CONTENT_TYPES,
IMAGE_CONTENT_TYPES,
VIDEO_CONTENT_TYPES,
} from "@app/content"
2026-07-28 16:16:26 +00:00
import {isRoomId} from "@app/rooms"
2024-09-24 21:12:38 +00:00
2025-04-28 22:46:48 +00:00
const {value, event} = $props()
2024-09-24 21:12:38 +00:00
let hideImage = $state(false)
2024-09-24 21:12:38 +00:00
const url = value.url.toString()
const isRoomOrRelay = isRoomId(url) || isRelayUrl(url)
const [href, external] = call(() => {
if (url.startsWith(PLATFORM_URL)) return [url.replace(PLATFORM_URL, ""), false]
return [url, true]
})
2024-09-24 21:12:38 +00:00
const fileType = getUrlContentType(url, event)
const isAudio =
Boolean(url.match(/\.(mp3|m4a|wav|ogg|oga|opus|flac)$/)) ||
AUDIO_CONTENT_TYPES.includes(fileType)
const isVideo = Boolean(url.match(/\.(mov|webm|mp4)$/)) || VIDEO_CONTENT_TYPES.includes(fileType)
const isImage =
Boolean(url.match(/\.(jpe?g|png|gif|webp)$/)) || IMAGE_CONTENT_TYPES.includes(fileType)
const getVideoPoster = (videoUrl: string): string | undefined => {
if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) {
return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}`
}
return undefined
}
const onError = () => {
hideImage = true
}
const expand = () =>
pushModal(ContentLinkDetail, {value, event}, {fullscreen: true, label: "Content preview"})
2024-09-24 21:12:38 +00:00
</script>
{#if isRoomOrRelay}
2026-07-06 19:55:03 +00:00
<ContentLinkUrl {url} class="link-content whitespace-nowrap" />
{:else if isAudio}
<audio controls src={url} preload="metadata" class="my-2 w-full max-w-xl"></audio>
{:else if isVideo}
<Link {external} {href} class="my-2 block">
<video
controls
src={url}
poster={getVideoPoster(url)}
preload="metadata"
class="max-h-96 rounded-2xl object-contain object-center">
<track kind="captions" />
</video>
</Link>
{:else if isImage}
<Link {external} {href} class="my-2 block">
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-2xl" />
</button>
</Link>
{:else}
{#await loadPreview(url)}
<Link {external} {href} class="my-2 block">
<div
class="border border-solid flex max-w-xl flex-col overflow-hidden leading-normal rounded-2xl"
style="border-color: var(--line)">
<div class="flex flex-col gap-2 p-4">
<Spinner>
<span class="overflow-hidden text-ellipsis whitespace-nowrap">{displayUrl(url)}</span>
</Spinner>
</div>
</div>
</Link>
{:then preview}
<Link {external} {href} class="my-2 block">
<div
class="border border-solid flex max-w-xl flex-col overflow-hidden leading-normal rounded-2xl"
style="border-color: var(--line)">
{#if preview.image && !hideImage}
<img
alt=""
onerror={onError}
src={preview.image}
class="bg-surface max-h-72 object-contain object-center" />
{/if}
<div class="flex flex-col gap-2 p-4">
<strong class="overflow-hidden text-ellipsis whitespace-nowrap"
>{preview.title || displayUrl(url)}</strong>
<p>{ellipsize(preview.description, 140)}</p>
</div>
</div>
</Link>
{:catch}
<ContentLinkUrl {url} class="link-content whitespace-nowrap" />
{/await}
{/if}