diff --git a/src/lib/api/coves/types.ts b/src/lib/api/coves/types.ts index 04897e82..6d73163b 100644 --- a/src/lib/api/coves/types.ts +++ b/src/lib/api/coves/types.ts @@ -310,6 +310,8 @@ export interface EmbedImageAspectRatio { export interface EmbedImage { image: string + thumb?: string + fullsize?: string alt?: string aspectRatio?: EmbedImageAspectRatio } @@ -340,12 +342,12 @@ export interface ExternalEmbedExternal { } export interface ExternalEmbed { - $type: 'social.coves.embed.external#view' + $type: 'social.coves.embed.external' | 'social.coves.embed.external#view' external: ExternalEmbedExternal } export interface VideoEmbed { - $type: 'social.coves.embed.video#view' + $type: 'social.coves.embed.video' | 'social.coves.embed.video#view' video: string thumbnail?: string alt?: string @@ -353,7 +355,10 @@ export interface VideoEmbed { } export interface RecordEmbed { - $type: 'social.coves.embed.record#view' + $type: + | 'social.coves.embed.post' + | 'social.coves.embed.record' + | 'social.coves.embed.record#view' post: StrongRef resolved?: unknown } diff --git a/src/lib/app/markdown/renderers/MdImage.svelte b/src/lib/app/markdown/renderers/MdImage.svelte index 542e5115..380767aa 100644 --- a/src/lib/app/markdown/renderers/MdImage.svelte +++ b/src/lib/app/markdown/renderers/MdImage.svelte @@ -1,12 +1,13 @@ -
- {#if loaded} - {#if type == 'video' || type == 'embed' || type == 'iframe'} - +{#if href} +
+ {#if loaded} + {#if type == 'video' || type == 'embed' || type == 'iframe'} + + {:else} + + {/if} {:else} {/if} - {:else} - - {/if} -
+
+{/if} diff --git a/src/lib/feature/community/CommunityItemBig.svelte b/src/lib/feature/community/CommunityItemBig.svelte index f73d82f9..24105fca 100644 --- a/src/lib/feature/community/CommunityItemBig.svelte +++ b/src/lib/feature/community/CommunityItemBig.svelte @@ -11,9 +11,9 @@ import { Button, modal, toast } from 'mono-svelte' import type { Snippet } from 'svelte' import { Check, Icon, InformationCircle, Plus } from 'svelte-hero-icons/dist' - import { optimizeImageURL } from '../post' import CommunityCard from './CommunityCard.svelte' import { communityDisplayName, communityIdentifier } from './helpers' + import { withPreset } from '$lib/feature/post/image-proxy' interface Props { community: CommunityView @@ -27,6 +27,7 @@ } let banner = $derived(getBanner(community)) + let bannerError = $state(false) {#snippet communityInfo()} @@ -48,9 +49,10 @@ class="-m-4 mask-b-from-25% relative h-24" style="min-width: calc(100% + calc(var(--spacing) * 8));" > - {#if banner} + {#if banner && !bannerError} (bannerError = true)} alt="" class="object-cover min-h-full min-w-full" /> diff --git a/src/lib/feature/post/PostLink.svelte b/src/lib/feature/post/PostLink.svelte index 146a68cd..9cfc0ada 100644 --- a/src/lib/feature/post/PostLink.svelte +++ b/src/lib/feature/post/PostLink.svelte @@ -3,7 +3,7 @@ import { parseURL } from '$lib/ui/form/Link.svelte' import { Material } from 'mono-svelte' import { ArrowTopRightOnSquare, Icon, Link } from 'svelte-hero-icons/dist' - import { optimizeImageURL } from './helpers' + import { withPreset } from './image-proxy' interface Props { url: string @@ -14,6 +14,7 @@ let { url, thumbnail_url, embed_title, view = 'cozy' }: Props = $props() + let imgError = $state(false) let richURL = $derived(parseURL(url)) @@ -21,7 +22,7 @@ @component For embed-type posts. Displays embed card or a compact link. --> -{#if embed_title && view == 'cozy'} +{#if (embed_title || thumbnail_url) && view == 'cozy'} {/if} -

{embed_title}

+ {#if embed_title} +

{embed_title}

+ {/if} - {#if thumbnail_url} + {#if thumbnail_url && !imgError}
- - {#each ['webp'] as format} - - {/each} - - + (imgError = true)} + class="" + width={600} + height={400} + alt="" + /> { expect(bestImageURL(undefined)).toBe('') }) - it('returns optimized URL for image embed', () => { + it('returns image URL for image embed', () => { const result = bestImageURL(imageEmbed) - expect(result).toContain('cdn.example.com/pic.jpg') - expect(result).toContain('format=webp') + expect(result).toBe('https://cdn.example.com/pic.jpg') + }) + + it('returns thumb by default when thumb field is present', () => { + const embedWithThumb = { + $type: 'social.coves.embed.images#view' as const, + images: [ + { + image: 'https://cdn.example.com/pic.jpg', + thumb: 'https://cdn.example.com/pic-thumb.jpg', + alt: 'A photo', + }, + ], + } satisfies import('$lib/api/coves/types').ImageEmbed + // default variant (no 3rd arg) should use 'thumb' + expect(bestImageURL(embedWithThumb)).toBe( + 'https://cdn.example.com/pic-thumb.jpg', + ) + }) + + it('returns thumb field when available for image embed', () => { + const embedWithThumb: ImageEmbed = { + $type: 'social.coves.embed.images#view', + images: [ + { + image: 'https://cdn.example.com/pic.jpg', + thumb: 'https://cdn.example.com/pic-thumb.jpg', + alt: 'A photo', + }, + ], + } + expect(bestImageURL(embedWithThumb, true, 'thumb')).toBe( + 'https://cdn.example.com/pic-thumb.jpg', + ) + }) + + it('returns fullsize field when variant is fullsize', () => { + const embedWithFullsize: ImageEmbed = { + $type: 'social.coves.embed.images#view', + images: [ + { + image: 'https://cdn.example.com/pic.jpg', + fullsize: 'https://cdn.example.com/pic-full.jpg', + alt: 'A photo', + }, + ], + } + expect(bestImageURL(embedWithFullsize, true, 'fullsize')).toBe( + 'https://cdn.example.com/pic-full.jpg', + ) }) it('returns thumbnail for external embed when thumbnail=true', () => { const result = bestImageURL(externalEmbed, true) - expect(result).toContain('cdn.example.com/thumb.jpg') + expect(result).toBe('https://cdn.example.com/thumb.jpg') }) it('returns external URI when no thumb and thumbnail=false', () => { @@ -159,7 +207,7 @@ describe('bestImageURL', () => { it('returns thumbnail for video embed', () => { const result = bestImageURL(videoEmbed) - expect(result).toContain('cdn.example.com/video-thumb.jpg') + expect(result).toBe('https://cdn.example.com/video-thumb.jpg') }) it('returns empty string for video embed with no thumbnail', () => { @@ -173,6 +221,22 @@ describe('bestImageURL', () => { it('returns empty string for record embed', () => { expect(bestImageURL(recordEmbed)).toBe('') }) + + it('returns empty string for image embed with empty images (type-cast edge case)', () => { + const emptyImages = { + $type: 'social.coves.embed.images#view' as const, + images: [] as unknown as [ + import('$lib/api/coves/types').EmbedImage, + ...import('$lib/api/coves/types').EmbedImage[], + ], + } + expect(bestImageURL(emptyImages)).toBe('') + }) + + it('returns external URI (not thumb) when thumbnail=false', () => { + const result = bestImageURL(externalEmbed, false) + expect(result).toBe('https://example.com/article') + }) }) // --------------------------------------------------------------------------- @@ -180,36 +244,33 @@ describe('bestImageURL', () => { // --------------------------------------------------------------------------- describe('optimizeImageURL', () => { - it('adds format and thumbnail params', () => { + it('swaps preset in proxy URLs', () => { const result = optimizeImageURL( - 'https://cdn.example.com/pic.jpg', - 512, - 'webp', + 'https://cdn.example.com/img/content_preview/plain/did:plc:abc/bafyreig1', + 'content_full', + ) + expect(result).toBe( + 'https://cdn.example.com/img/content_full/plain/did:plc:abc/bafyreig1', ) - expect(result).toContain('format=webp') - expect(result).toContain('thumbnail=512') - }) - - it('returns original string for invalid URLs', () => { - expect(optimizeImageURL('not-a-url')).toBe('not-a-url') }) - it('respects null format param', () => { - const result = optimizeImageURL( + it('returns non-proxy URLs unchanged', () => { + expect(optimizeImageURL('https://cdn.example.com/pic.jpg')).toBe( 'https://cdn.example.com/pic.jpg', - 256, - null, ) - expect(result).not.toContain('format=') }) - it('does not overwrite existing thumbnail param', () => { - const result = optimizeImageURL( - 'https://cdn.example.com/pic.jpg?thumbnail=128', - 1024, + it('returns non-URL strings unchanged', () => { + expect(optimizeImageURL('not-a-url')).toBe('not-a-url') + }) + + it('uses content_preview as default preset', () => { + const proxyUrl = + 'https://cdn.example.com/img/avatar/plain/did:plc:abc/bafyreig1' + const result = optimizeImageURL(proxyUrl) + expect(result).toBe( + 'https://cdn.example.com/img/content_preview/plain/did:plc:abc/bafyreig1', ) - expect(result).toContain('thumbnail=128') - expect(result).not.toContain('thumbnail=1024') }) }) diff --git a/src/lib/feature/post/helpers.ts b/src/lib/feature/post/helpers.ts index 55255bed..c01b5b70 100644 --- a/src/lib/feature/post/helpers.ts +++ b/src/lib/feature/post/helpers.ts @@ -8,71 +8,67 @@ import { parseAtUri } from '$lib/api/coves/types' import { canParseUrl, communitySlug, - findClosestNumber, isImage, isVideo, } from '$lib/app/util.svelte' +import { + type ImagePreset, + type ImageVariant, + imageUrl, + withPreset, +} from './image-proxy' -// Algorithm to determine the best image URL to use from a Coves PostView's embed +/** + * Returns the best image URL for a post embed. + * + * @param embed - The post embed to extract an image URL from. + * @param thumbnail - For external embeds only: when true, prefer the external + * embed's dedicated thumbnail over its URI. Has no effect on other embed types. + * @param variant - For image embeds only: selects the 'thumb' (smaller) or + * 'fullsize' proxy variant. Has no effect on other embed types. + */ export const bestImageURL = ( embed: PostEmbed | undefined, thumbnail: boolean = true, - width: number = 1024, - format: 'avif' | 'webp' | null = 'webp', + variant: ImageVariant = 'thumb', ): string => { if (!embed) return '' switch (embed.$type) { case 'social.coves.embed.images#view': { const img = embed.images[0] - if (img?.image) return optimizeImageURL(img.image, width, format) + if (img) return imageUrl(img, variant) return '' } + case 'social.coves.embed.external': case 'social.coves.embed.external#view': { - if (embed.external.thumb && thumbnail) - return optimizeImageURL(embed.external.thumb, width, format) + if (embed.external.thumb && thumbnail) return embed.external.thumb return embed.external.uri ?? '' } + case 'social.coves.embed.video': case 'social.coves.embed.video#view': { - if (embed.thumbnail) - return optimizeImageURL(embed.thumbnail, width, format) + if (embed.thumbnail) return embed.thumbnail return '' } - default: + case 'social.coves.embed.post': + case 'social.coves.embed.record': + case 'social.coves.embed.record#view': return '' + default: { + const _exhaustive: never = embed + return '' + } } } +/** + * @deprecated Use `withPreset` from `./image-proxy` directly. + */ export const optimizeImageURL = ( - urlStr: string, - width: number = 1024, - format: 'avif' | 'webp' | null = 'webp', + url: string, + preset: ImagePreset = 'content_preview', ): string => { - try { - let url: URL - try { - url = new URL(urlStr) - } catch { - return urlStr - } - - if (format) url.searchParams.set('format', format) - - if (width > 0 && !url.searchParams.has('thumbnail')) { - url.searchParams.set( - 'thumbnail', - findClosestNumber( - [128, 196, 256, 512, 728, 1024, 1536], - width, - ).toString(), - ) - } - - return url.toString() - } catch (e) { - console.error(e) - return urlStr - } + return withPreset(url, preset) } const YOUTUBE_REGEX = @@ -104,8 +100,10 @@ export function mediaType(embed?: PostEmbed): MediaType { switch (embed.$type) { case 'social.coves.embed.images#view': return 'image' + case 'social.coves.embed.video': case 'social.coves.embed.video#view': return 'iframe' + case 'social.coves.embed.external': case 'social.coves.embed.external#view': { const uri = embed.external.uri if (!uri) return 'none' @@ -122,10 +120,14 @@ export function mediaType(embed?: PostEmbed): MediaType { if (canParseUrl(uri)) return 'embed' return 'none' } + case 'social.coves.embed.post': + case 'social.coves.embed.record': case 'social.coves.embed.record#view': return 'embed' - default: + default: { + const _exhaustive: never = embed return 'none' + } } } @@ -144,14 +146,20 @@ export function extractEmbedUrl(embed?: PostEmbed): string | undefined { switch (embed.$type) { case 'social.coves.embed.images#view': return embed.images[0]?.image + case 'social.coves.embed.external': case 'social.coves.embed.external#view': return embed.external.uri + case 'social.coves.embed.video': case 'social.coves.embed.video#view': return embed.video + case 'social.coves.embed.post': + case 'social.coves.embed.record': case 'social.coves.embed.record#view': return undefined - default: + default: { + const _exhaustive: never = embed return undefined + } } } @@ -162,14 +170,24 @@ export function extractEmbedThumbnail(embed?: PostEmbed): string | undefined { if (!embed) return undefined switch (embed.$type) { - case 'social.coves.embed.images#view': - return embed.images[0]?.image + case 'social.coves.embed.images#view': { + const img = embed.images[0] + return img ? imageUrl(img, 'thumb') : undefined + } + case 'social.coves.embed.external': case 'social.coves.embed.external#view': return embed.external.thumb + case 'social.coves.embed.video': case 'social.coves.embed.video#view': return embed.thumbnail - default: + case 'social.coves.embed.post': + case 'social.coves.embed.record': + case 'social.coves.embed.record#view': + return undefined + default: { + const _exhaustive: never = embed return undefined + } } } @@ -180,10 +198,20 @@ export function extractEmbedTitle(embed?: PostEmbed): string | undefined { if (!embed) return undefined switch (embed.$type) { + case 'social.coves.embed.external': case 'social.coves.embed.external#view': return embed.external.title - default: + case 'social.coves.embed.images#view': + case 'social.coves.embed.video': + case 'social.coves.embed.video#view': + case 'social.coves.embed.post': + case 'social.coves.embed.record': + case 'social.coves.embed.record#view': return undefined + default: { + const _exhaustive: never = embed + return undefined + } } } @@ -196,10 +224,19 @@ export function extractEmbedAlt(embed?: PostEmbed): string | undefined { switch (embed.$type) { case 'social.coves.embed.images#view': return embed.images[0]?.alt + case 'social.coves.embed.video': case 'social.coves.embed.video#view': return embed.alt - default: + case 'social.coves.embed.external': + case 'social.coves.embed.external#view': + case 'social.coves.embed.post': + case 'social.coves.embed.record': + case 'social.coves.embed.record#view': return undefined + default: { + const _exhaustive: never = embed + return undefined + } } } diff --git a/src/lib/feature/post/image-proxy.test.ts b/src/lib/feature/post/image-proxy.test.ts new file mode 100644 index 00000000..cc486c1c --- /dev/null +++ b/src/lib/feature/post/image-proxy.test.ts @@ -0,0 +1,142 @@ +import { describe, it, expect } from 'vitest' +import type { EmbedImage } from '$lib/api/coves/types' +import { parseProxyUrl, withPreset, imageUrl } from './image-proxy' + +describe('parseProxyUrl', () => { + it('parses a relative proxy URL', () => { + const result = parseProxyUrl( + '/img/avatar_small/plain/did:plc:abc123/bafkrei456', + ) + expect(result).toEqual({ + preset: 'avatar_small', + did: 'did:plc:abc123', + cid: 'bafkrei456', + }) + }) + + it('parses a full proxy URL with base domain', () => { + const result = parseProxyUrl( + 'https://coves.social/img/content_full/plain/did:plc:xyz/bafyrei789', + ) + expect(result).toEqual({ + preset: 'content_full', + did: 'did:plc:xyz', + cid: 'bafyrei789', + }) + }) + + it('returns null for non-proxy URLs', () => { + expect(parseProxyUrl('https://example.com/image.jpg')).toBeNull() + expect(parseProxyUrl('')).toBeNull() + expect(parseProxyUrl('not-a-url')).toBeNull() + }) + + it('returns null for PDS blob URLs', () => { + expect( + parseProxyUrl( + 'https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=abc&cid=xyz', + ), + ).toBeNull() + }) + + it('parses a proxy URL with did:web: DID method', () => { + const result = parseProxyUrl( + 'https://coves.social/img/avatar/plain/did:web:example.com/bafkrei456', + ) + expect(result).toEqual({ + preset: 'avatar', + did: 'did:web:example.com', + cid: 'bafkrei456', + }) + }) +}) + +describe('withPreset', () => { + it('swaps preset on a relative proxy URL', () => { + expect( + withPreset('/img/avatar_small/plain/did:plc:abc/bafkrei123', 'avatar'), + ).toBe('/img/avatar/plain/did:plc:abc/bafkrei123') + }) + + it('swaps preset on a full proxy URL', () => { + expect( + withPreset( + 'https://coves.social/img/content_preview/plain/did:plc:xyz/bafyrei456', + 'content_full', + ), + ).toBe('https://coves.social/img/content_full/plain/did:plc:xyz/bafyrei456') + }) + + it('returns non-proxy URLs unchanged', () => { + const pdsUrl = + 'https://pds.example.com/xrpc/com.atproto.sync.getBlob?did=abc&cid=xyz' + expect(withPreset(pdsUrl, 'content_full')).toBe(pdsUrl) + }) + + it('returns empty string unchanged', () => { + expect(withPreset('', 'avatar')).toBe('') + }) + + it('strips query strings when swapping preset', () => { + const url = + 'https://coves.social/img/avatar/plain/did:plc:abc/bafkrei123?format=webp' + const result = withPreset(url, 'content_full') + expect(result).toBe( + 'https://coves.social/img/content_full/plain/did:plc:abc/bafkrei123', + ) + }) +}) + +describe('imageUrl', () => { + it('returns thumb when available for thumb variant', () => { + const img: EmbedImage = { + image: 'https://original.com/pic.jpg', + thumb: 'https://proxy.com/img/content_preview/plain/did:plc:a/baf1', + fullsize: 'https://proxy.com/img/content_full/plain/did:plc:a/baf1', + } + expect(imageUrl(img, 'thumb')).toBe(img.thumb) + }) + + it('returns fullsize when available for fullsize variant', () => { + const img: EmbedImage = { + image: 'https://original.com/pic.jpg', + thumb: 'https://proxy.com/img/content_preview/plain/did:plc:a/baf1', + fullsize: 'https://proxy.com/img/content_full/plain/did:plc:a/baf1', + } + expect(imageUrl(img, 'fullsize')).toBe(img.fullsize) + }) + + it('falls back to image when thumb is undefined', () => { + const img: EmbedImage = { image: 'https://original.com/pic.jpg' } + expect(imageUrl(img, 'thumb')).toBe(img.image) + }) + + it('falls back to image when fullsize is undefined', () => { + const img: EmbedImage = { image: 'https://original.com/pic.jpg' } + expect(imageUrl(img, 'fullsize')).toBe(img.image) + }) + + it('defaults to thumb variant', () => { + const img: EmbedImage = { + image: 'https://original.com/pic.jpg', + thumb: 'https://proxy.com/thumb', + } + expect(imageUrl(img)).toBe(img.thumb) + }) + + it('falls back to image when thumb is empty string', () => { + const img: EmbedImage = { + image: 'https://original.com/pic.jpg', + thumb: '', + } + expect(imageUrl(img, 'thumb')).toBe(img.image) + }) + + it('falls back to image when fullsize is empty string', () => { + const img: EmbedImage = { + image: 'https://original.com/pic.jpg', + fullsize: '', + } + expect(imageUrl(img, 'fullsize')).toBe(img.image) + }) +}) diff --git a/src/lib/feature/post/image-proxy.ts b/src/lib/feature/post/image-proxy.ts new file mode 100644 index 00000000..ab551e48 --- /dev/null +++ b/src/lib/feature/post/image-proxy.ts @@ -0,0 +1,64 @@ +import type { EmbedImage } from '$lib/api/coves/types' + +/** + * Available image proxy presets matching the Coves backend. + * The proxy serves at /img/{preset}/plain/{did}/{cid}. + */ +export type ImagePreset = + | 'avatar' + | 'avatar_small' + | 'banner' + | 'content_preview' + | 'content_full' + | 'embed_thumbnail' + +const PROXY_URL_REGEX = /\/img\/([a-z_]+)\/plain\/(did:[^/]+)\/([^/?#]+)/ + +export interface ParsedProxyUrl { + preset: ImagePreset | (string & {}) + did: string + cid: string +} + +/** + * Parses a Coves image proxy URL into its components. + * Returns null if the URL is not a proxy URL. + */ +export function parseProxyUrl(url: string): ParsedProxyUrl | null { + const match = url.match(PROXY_URL_REGEX) + if (!match) return null + return { preset: match[1], did: match[2], cid: match[3] } +} + +/** + * Constructs a proxy URL with a different preset. + * If the input is already a proxy URL, swaps the preset. + * If the input is not a proxy URL, returns it unmodified. + */ +export function withPreset(url: string, preset: ImagePreset): string { + const parsed = parseProxyUrl(url) + if (!parsed) { + if (import.meta.env.DEV && url) { + console.warn(`[image-proxy] withPreset called with non-proxy URL: ${url}`) + } + return url + } + + const baseEnd = url.indexOf('/img/') + const base = baseEnd >= 0 ? url.substring(0, baseEnd) : '' + return `${base}/img/${preset}/plain/${parsed.did}/${parsed.cid}` +} + +export type ImageVariant = 'thumb' | 'fullsize' + +/** + * Returns the best URL for an EmbedImage given a display variant. + * Prefers thumb/fullsize when present and non-empty, falls back to image. + */ +export function imageUrl( + image: EmbedImage, + variant: ImageVariant = 'thumb', +): string { + if (variant === 'fullsize') return image.fullsize || image.image + return image.thumb || image.image +} diff --git a/src/lib/feature/post/index.ts b/src/lib/feature/post/index.ts index cb4adb01..7a395334 100644 --- a/src/lib/feature/post/index.ts +++ b/src/lib/feature/post/index.ts @@ -19,3 +19,4 @@ export { } from './PostMeta.svelte' export * from './helpers' +export * from './image-proxy' diff --git a/src/lib/feature/post/media/PostIframe.svelte b/src/lib/feature/post/media/PostIframe.svelte index a4bc228f..6fb2a842 100644 --- a/src/lib/feature/post/media/PostIframe.svelte +++ b/src/lib/feature/post/media/PostIframe.svelte @@ -8,7 +8,8 @@ PuzzlePiece, VideoCamera, } from 'svelte-hero-icons/dist' - import { type IframeType, optimizeImageURL } from '../helpers' + import { type IframeType } from '../helpers' + import { withPreset } from '../image-proxy' const youtubeDomain = (place: 'youtube' | 'invidious' | 'piped') => { switch (place) { @@ -116,6 +117,7 @@ class: clazz, }: Props = $props() + let thumbError = $state(false) let data = $derived(typeData(type)) let embedUrl = $derived(urlToEmbed(url)) @@ -146,9 +148,10 @@
- {#if thumbnail} + {#if thumbnail && !thumbError} (thumbError = true)} class="absolute top-0 left-0 -z-10 w-full object-cover h-full mask-b-from-0 brightness-75" alt="" /> diff --git a/src/lib/feature/post/media/PostImage.svelte b/src/lib/feature/post/media/PostImage.svelte index 49f6a91f..2665f155 100644 --- a/src/lib/feature/post/media/PostImage.svelte +++ b/src/lib/feature/post/media/PostImage.svelte @@ -19,7 +19,7 @@ }) let altText = $derived(extractEmbedAlt(embed)) - let fullImageUrl = $derived(bestImageURL(embed, false, -1, null)) + let fullImageUrl = $derived(bestImageURL(embed, false, 'fullsize')) @@ -41,33 +41,19 @@ - {#each ['webp'] as format} - - {/each} + + {altText (imageLoaded = true)} + onerror={() => (imageLoaded = true)} /> diff --git a/src/lib/feature/post/media/PostMediaCompact.svelte b/src/lib/feature/post/media/PostMediaCompact.svelte index 9c401fd7..59c5172c 100644 --- a/src/lib/feature/post/media/PostMediaCompact.svelte +++ b/src/lib/feature/post/media/PostMediaCompact.svelte @@ -57,7 +57,8 @@ ? $t('aria.postDecor.openImage', { default: altText ?? 'Image' }) : $t('aria.postDecor.openPost', { default: 'Post' })} onclick={() => { - if (type === 'image' && embed) showImage(bestImageURL(embed, false, -1)) + if (type === 'image' && embed) + showImage(bestImageURL(embed, false, 'fullsize')) }} role={type === 'image' ? 'button' : 'presentation'} tabindex={type === 'image' ? 0 : -1} @@ -71,40 +72,16 @@ > {#if hasThumbnail} {@const useThumbnail = !!thumbnailUrl && type !== 'image'} - - {#each ['webp'] as format} - - {/each} - {altText - + {altText {#if type !== 'image'}
diff --git a/src/lib/ui/generic/Avatar.svelte b/src/lib/ui/generic/Avatar.svelte index 1e811e2b..7ec1c336 100644 --- a/src/lib/ui/generic/Avatar.svelte +++ b/src/lib/ui/generic/Avatar.svelte @@ -1,40 +1,15 @@ -{#if url && optimizedURLs[0] != undefined} +{#if url && !imgError} (imgError = true)} alt="" {width} {title}