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();
+ });
+});