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"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {isRelayUrl, tagSpec, tagValue} from "@welshman/util"
|
2026-04-08 16:07:11 +00:00
|
|
|
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"
|
2026-06-24 20:56:53 +00:00
|
|
|
import Spinner from "@lib/components/Spinner.svelte"
|
2024-10-17 23:03:50 +00:00
|
|
|
import ContentLinkDetail from "@app/components/ContentLinkDetail.svelte"
|
2026-04-04 11:18:29 +00:00
|
|
|
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"
|
2026-06-08 22:33:38 +00:00
|
|
|
import {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
|
|
|
|
2025-02-13 22:26:00 +00:00
|
|
|
let hideImage = $state(false)
|
|
|
|
|
|
2024-09-24 21:12:38 +00:00
|
|
|
const url = value.url.toString()
|
2026-04-04 11:18:29 +00:00
|
|
|
const isRoomOrRelay = isRoomId(url) || isRelayUrl(url)
|
2025-12-03 01:09:56 +00:00
|
|
|
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
|
|
|
|
2026-07-28 16:16:26 +00:00
|
|
|
const fileType = tagValue(tagSpec("file-type"), event.tags) || ""
|
2026-04-04 11:18:29 +00:00
|
|
|
|
2026-04-08 16:07:11 +00:00
|
|
|
const getVideoPoster = (videoUrl: string): string | undefined => {
|
|
|
|
|
if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) {
|
|
|
|
|
return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
2025-02-13 22:26:00 +00:00
|
|
|
const onError = () => {
|
|
|
|
|
hideImage = true
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-29 19:30:01 +00:00
|
|
|
const expand = () => pushModal(ContentLinkDetail, {value, event}, {fullscreen: true})
|
2024-09-24 21:12:38 +00:00
|
|
|
</script>
|
|
|
|
|
|
2026-04-04 11:18:29 +00:00
|
|
|
{#if isRoomOrRelay}
|
2026-07-06 19:55:03 +00:00
|
|
|
<ContentLinkUrl {url} class="link-content whitespace-nowrap" />
|
2026-04-04 11:18:29 +00:00
|
|
|
{:else}
|
|
|
|
|
<Link {external} {href} class="my-2 block">
|
2026-07-28 16:16:26 +00:00
|
|
|
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)}
|
|
|
|
|
<video
|
|
|
|
|
controls
|
|
|
|
|
src={url}
|
|
|
|
|
poster={getVideoPoster(url)}
|
|
|
|
|
preload="metadata"
|
|
|
|
|
class="max-h-96 rounded-2xl object-contain object-center">
|
|
|
|
|
<track kind="captions" />
|
|
|
|
|
</video>
|
|
|
|
|
{:else if url.match(/\.(jpe?g|png|gif|webp)$/) || IMAGE_CONTENT_TYPES.includes(fileType)}
|
|
|
|
|
<button type="button" onclick={stopPropagation(preventDefault(expand))}>
|
|
|
|
|
<ContentLinkBlockImage {value} {event} class="m-auto max-h-96 rounded-2xl" />
|
|
|
|
|
</button>
|
|
|
|
|
{:else}
|
|
|
|
|
{#await loadPreview(url)}
|
|
|
|
|
<div class="flex justify-center items-center my-12 w-full">
|
|
|
|
|
<Spinner />
|
|
|
|
|
</div>
|
|
|
|
|
{:then preview}
|
|
|
|
|
<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>
|
2026-04-04 11:18:29 +00:00
|
|
|
</div>
|
2026-07-28 16:16:26 +00:00
|
|
|
</div>
|
|
|
|
|
{:catch}
|
|
|
|
|
<p
|
|
|
|
|
class="border border-solid p-12 text-center leading-normal rounded-2xl"
|
|
|
|
|
style="border-color: var(--line)">
|
|
|
|
|
Unable to load a preview for {url}
|
|
|
|
|
</p>
|
|
|
|
|
{/await}
|
|
|
|
|
{/if}
|
2026-04-04 11:18:29 +00:00
|
|
|
</Link>
|
|
|
|
|
{/if}
|