From ce41960fcd7b2a0dc5ccf968627ea06f3e905144 Mon Sep 17 00:00:00 2001 From: Hampus Date: Wed, 19 Aug 2026 17:38:00 +0200 Subject: [PATCH] perf(app): load embed author and footer icons through the image cache (#1756) --- .../embeds/channel_embed/EmbedParts.tsx | 7 ++-- .../messaging/hooks/useCachedImageLoaded.ts | 26 +++++++++++++++ .../messaging/utils/ImageCacheUtils.test.ts | 32 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 fluxer_app/src/features/messaging/hooks/useCachedImageLoaded.ts create mode 100644 fluxer_app/src/features/messaging/utils/ImageCacheUtils.test.ts diff --git a/fluxer_app/src/features/channel/components/embeds/channel_embed/EmbedParts.tsx b/fluxer_app/src/features/channel/components/embeds/channel_embed/EmbedParts.tsx index 85ce45b88..5df915f43 100644 --- a/fluxer_app/src/features/channel/components/embeds/channel_embed/EmbedParts.tsx +++ b/fluxer_app/src/features/channel/components/embeds/channel_embed/EmbedParts.tsx @@ -5,6 +5,7 @@ import styles from '@app/features/channel/components/embeds/ChannelEmbed.module. import {EmbedLink} from '@app/features/channel/components/embeds/channel_embed/EmbedLink'; import {SafeMarkdown} from '@app/features/messaging/components/markdown'; import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes'; +import {useCachedImageLoaded} from '@app/features/messaging/hooks/useCachedImageLoaded'; import {buildMediaProxyURL} from '@app/features/messaging/utils/MediaProxyUtils'; import * as DateUtils from '@app/features/user/utils/DateFormatting'; import type {EmbedAuthor, EmbedField, EmbedFooter} from '@fluxer/schema/src/domains/message/EmbedSchemas'; @@ -32,10 +33,11 @@ export const EmbedAuthorComponent: FC<{author?: EmbedAuthor}> = observer(({autho const iconSrc = author?.proxy_icon_url ? buildMediaProxyURL(author.proxy_icon_url, animatedMediaPlaybackAllowed ? {} : {format: 'webp', animated: false}) : undefined; + const iconLoaded = useCachedImageLoaded(iconSrc); if (!author) return null; return (
- {iconSrc && ( + {iconSrc && iconLoaded && ( - {iconSrc && ( + {iconSrc && iconLoaded && ( (() => + resolved !== null && ImageCacheUtils.hasImage(resolved) ? resolved : null, + ); + useEffect(() => { + if (resolved === null) { + return; + } + let active = true; + const markReady = (): void => { + if (active) setReadySrc(resolved); + }; + const cleanup = ImageCacheUtils.loadImage(resolved, markReady, markReady); + return () => { + active = false; + cleanup(); + }; + }, [resolved]); + return resolved !== null && readySrc === resolved; +} diff --git a/fluxer_app/src/features/messaging/utils/ImageCacheUtils.test.ts b/fluxer_app/src/features/messaging/utils/ImageCacheUtils.test.ts new file mode 100644 index 000000000..1eb0e3946 --- /dev/null +++ b/fluxer_app/src/features/messaging/utils/ImageCacheUtils.test.ts @@ -0,0 +1,32 @@ +// @vitest-environment happy-dom +// SPDX-License-Identifier: AGPL-3.0-or-later + +import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils'; +import {afterEach, describe, expect, it, vi} from 'vitest'; + +afterEach(() => { + ImageCacheUtils._clearForTests(); +}); + +describe('loadImage rejection path', () => { + it('reports an unusable source through onError so callers can fall back', () => { + const onLoad = vi.fn(); + const onError = vi.fn(); + ImageCacheUtils.loadImage(null, onLoad, onError); + expect(onLoad).not.toHaveBeenCalled(); + expect(onError).toHaveBeenCalledTimes(1); + }); + + it('reports an oversized source through onError rather than queueing it', () => { + const onLoad = vi.fn(); + const onError = vi.fn(); + ImageCacheUtils.loadImage(`https://cdn.test/${'a'.repeat(32 * 1024)}.png`, onLoad, onError); + expect(onLoad).not.toHaveBeenCalled(); + expect(onError).toHaveBeenCalledTimes(1); + }); + + it('returns a no-op disposer when it rejects, so cleanup stays safe', () => { + const dispose = ImageCacheUtils.loadImage(null, vi.fn(), vi.fn()); + expect(() => dispose()).not.toThrow(); + }); +});