2025-04-28 22:46:48 +00:00
|
|
|
<script lang="ts">
|
2025-06-03 17:41:38 +00:00
|
|
|
import {onMount, onDestroy} from "svelte"
|
2026-02-17 21:45:00 +00:00
|
|
|
import {displayUrl, once} from "@welshman/lib"
|
2026-09-22 15:48:04 +00:00
|
|
|
import {getBlob, makeBlossomAuthEvent, tagSpec, tagValue} from "@welshman/util"
|
2025-09-10 22:26:39 +00:00
|
|
|
import LinkRound from "@assets/icons/link-round.svg?dataurl"
|
2025-06-05 20:37:50 +00:00
|
|
|
import Icon from "@lib/components/Icon.svelte"
|
2026-07-28 16:16:26 +00:00
|
|
|
import {user} from "@app/core"
|
2026-09-22 15:48:04 +00:00
|
|
|
import {decryptUrl, getUrlTags} from "@app/content"
|
2025-04-28 22:46:48 +00:00
|
|
|
|
2025-04-29 19:30:01 +00:00
|
|
|
const {value, event, ...props} = $props()
|
2025-04-28 22:46:48 +00:00
|
|
|
|
|
|
|
|
const url = value.url.toString()
|
2026-09-08 21:38:50 +00:00
|
|
|
const meta = getUrlTags(url, event)
|
2025-06-03 17:41:38 +00:00
|
|
|
|
2025-12-04 22:37:59 +00:00
|
|
|
// Fallback to filename if hash was omitted from the message for interoperability
|
2026-07-28 16:16:26 +00:00
|
|
|
const hash = tagValue(tagSpec("x"), meta) || url.split(/[\/\.]/).slice(-2)[0]
|
2026-09-22 15:48:04 +00:00
|
|
|
const controller = new AbortController()
|
2026-02-25 22:52:05 +00:00
|
|
|
const fileName =
|
2026-07-28 16:16:26 +00:00
|
|
|
tagValue(tagSpec("filename"), meta) ||
|
|
|
|
|
tagValue(tagSpec("name"), meta) ||
|
2026-02-25 22:52:05 +00:00
|
|
|
decodeURIComponent(new URL(url).pathname.split("/").filter(Boolean).at(-1) || "image")
|
|
|
|
|
|
|
|
|
|
const revokeSrc = () => {
|
|
|
|
|
if (src.startsWith("blob:")) {
|
|
|
|
|
URL.revokeObjectURL(src)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const setBlobSrc = (data: Blob | Uint8Array<ArrayBuffer>, type?: string) => {
|
|
|
|
|
revokeSrc()
|
|
|
|
|
src = URL.createObjectURL(new File([data], fileName, type ? {type} : undefined))
|
|
|
|
|
}
|
2025-06-03 17:41:38 +00:00
|
|
|
|
2026-02-17 21:45:00 +00:00
|
|
|
const onError = once(async () => {
|
2025-09-29 21:24:42 +00:00
|
|
|
// If the image failed to load, try authenticating
|
2026-07-28 16:16:26 +00:00
|
|
|
if (hash) {
|
2025-09-29 21:24:42 +00:00
|
|
|
const server = new URL(url).origin
|
|
|
|
|
const template = makeBlossomAuthEvent({action: "get", server, hashes: [hash]})
|
2026-07-28 16:16:26 +00:00
|
|
|
const authEvent = await $user.signer.sign(template)
|
2025-09-29 21:24:42 +00:00
|
|
|
const res = await getBlob(server, hash, {authEvent})
|
|
|
|
|
|
|
|
|
|
if (res.status === 200) {
|
2026-02-25 22:52:05 +00:00
|
|
|
const blob = await res.blob()
|
|
|
|
|
setBlobSrc(blob, blob.type || undefined)
|
2025-09-29 21:24:42 +00:00
|
|
|
} else {
|
|
|
|
|
hasError = true
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
hasError = true
|
|
|
|
|
}
|
2026-02-17 21:45:00 +00:00
|
|
|
})
|
2025-06-05 20:37:50 +00:00
|
|
|
|
|
|
|
|
let hasError = $state(false)
|
2025-09-29 21:24:42 +00:00
|
|
|
let src = $state("")
|
2025-06-03 17:41:38 +00:00
|
|
|
|
|
|
|
|
onMount(async () => {
|
2026-09-22 15:48:04 +00:00
|
|
|
try {
|
|
|
|
|
src = await decryptUrl(url, event, controller.signal)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (!controller.signal.aborted) {
|
|
|
|
|
console.error(error)
|
2026-09-15 17:01:55 +00:00
|
|
|
hasError = true
|
2025-06-03 17:41:38 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
onDestroy(() => {
|
2026-09-22 15:48:04 +00:00
|
|
|
controller.abort()
|
2026-02-25 22:52:05 +00:00
|
|
|
revokeSrc()
|
2025-06-03 17:41:38 +00:00
|
|
|
})
|
2025-04-28 22:46:48 +00:00
|
|
|
</script>
|
|
|
|
|
|
2025-06-05 20:37:50 +00:00
|
|
|
{#if hasError}
|
|
|
|
|
<a href={url} class="link-content whitespace-nowrap">
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon icon={LinkRound} size={3} class="inline-block" />
|
2025-06-05 20:37:50 +00:00
|
|
|
{displayUrl(url)}
|
|
|
|
|
</a>
|
2025-09-29 21:24:42 +00:00
|
|
|
{:else if src}
|
2025-06-05 20:37:50 +00:00
|
|
|
<img alt="" {src} onerror={onError} {...props} />
|
|
|
|
|
{/if}
|