Fall back to an inline link when a link preview fails

This commit is contained in:
Coracle-Bot 2026-09-08 22:36:23 +00:00
parent 78fe1e11f4
commit 21aa5b165e
3 changed files with 49 additions and 47 deletions

View file

@ -940,8 +940,7 @@ Acceptance:
- A standalone url whose preview resolves shows a card with title, description, - A standalone url whose preview resolves shows a card with title, description,
and image after a brief loading state. and image after a brief loading state.
- A standalone url with nothing usable shows a card explaining the preview - A standalone url with nothing usable falls back to the compact inline link.
couldn't be loaded, naming the url.
- The same url embedded in a sentence renders as a compact inline link with no - The same url embedded in a sentence renders as a compact inline link with no
card. card.

View file

@ -231,7 +231,7 @@ test("US-063 preview a shared link", async ({seed, as}) => {
const previewImage = 'img[src="https://images.test/preview.png"]' const previewImage = 'img[src="https://images.test/preview.png"]'
await expect(card.locator(".spinner")).toBeVisible() await expect(card.locator(".spinner")).toBeVisible()
await expect(failed.getByText(`Unable to load a preview for ${unreadable}`)).toBeVisible() await expect(failed.getByRole("link", {name: "example.test/unreadable"})).toBeVisible()
servePreview() servePreview()

View file

@ -52,6 +52,11 @@
Boolean(url.match(/\.(mp3|m4a|wav|ogg|oga|opus|flac)$/)) || Boolean(url.match(/\.(mp3|m4a|wav|ogg|oga|opus|flac)$/)) ||
AUDIO_CONTENT_TYPES.includes(fileType) 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 => { const getVideoPoster = (videoUrl: string): string | undefined => {
if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) { if (Capacitor.getPlatform() === "android" && THUMBNAIL_URL) {
return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}` return `${THUMBNAIL_URL}/thumbnail?url=${encodeURIComponent(videoUrl)}`
@ -71,50 +76,48 @@
<ContentLinkUrl {url} class="link-content whitespace-nowrap" /> <ContentLinkUrl {url} class="link-content whitespace-nowrap" />
{:else if isAudio} {:else if isAudio}
<audio controls src={url} preload="metadata" class="my-2 w-full max-w-xl"></audio> <audio controls src={url} preload="metadata" class="my-2 w-full max-w-xl"></audio>
{:else} {:else if isVideo}
<Link {external} {href} class="my-2 block"> <Link {external} {href} class="my-2 block">
{#if url.match(/\.(mov|webm|mp4)$/) || VIDEO_CONTENT_TYPES.includes(fileType)} <video
<video controls
controls src={url}
src={url} poster={getVideoPoster(url)}
poster={getVideoPoster(url)} preload="metadata"
preload="metadata" class="max-h-96 rounded-2xl object-contain object-center">
class="max-h-96 rounded-2xl object-contain object-center"> <track kind="captions" />
<track kind="captions" /> </video>
</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>
</div>
</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}
</Link> </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)}
<div class="flex justify-center items-center my-12 w-full">
<Spinner />
</div>
{: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} {/if}