mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 05:25:40 +09:00
perf(app): load embed author and footer icons through the image cache (#1756)
This commit is contained in:
1 parent
2083eaddd6
commit
ce41960fcd
3 files changed
+63
-2
No files matched your search
@@ -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 {EmbedLink} from '@app/features/channel/components/embeds/channel_embed/EmbedLink';
|
||||||
import {SafeMarkdown} from '@app/features/messaging/components/markdown';
|
import {SafeMarkdown} from '@app/features/messaging/components/markdown';
|
||||||
import {MarkdownContext} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
|
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 {buildMediaProxyURL} from '@app/features/messaging/utils/MediaProxyUtils';
|
||||||
import * as DateUtils from '@app/features/user/utils/DateFormatting';
|
import * as DateUtils from '@app/features/user/utils/DateFormatting';
|
||||||
import type {EmbedAuthor, EmbedField, EmbedFooter} from '@fluxer/schema/src/domains/message/EmbedSchemas';
|
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
|
const iconSrc = author?.proxy_icon_url
|
||||||
? buildMediaProxyURL(author.proxy_icon_url, animatedMediaPlaybackAllowed ? {} : {format: 'webp', animated: false})
|
? buildMediaProxyURL(author.proxy_icon_url, animatedMediaPlaybackAllowed ? {} : {format: 'webp', animated: false})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const iconLoaded = useCachedImageLoaded(iconSrc);
|
||||||
if (!author) return null;
|
if (!author) return null;
|
||||||
return (
|
return (
|
||||||
<div className={styles.embedAuthor} data-flx="channel.embeds.embed.embed-author-component.embed-author">
|
<div className={styles.embedAuthor} data-flx="channel.embeds.embed.embed-author-component.embed-author">
|
||||||
{iconSrc && (
|
{iconSrc && iconLoaded && (
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className={styles.embedAuthorIcon}
|
className={styles.embedAuthorIcon}
|
||||||
@@ -181,13 +183,14 @@ export const EmbedFooterComponent: FC<{
|
|||||||
const iconSrc = footer?.proxy_icon_url
|
const iconSrc = footer?.proxy_icon_url
|
||||||
? buildMediaProxyURL(footer.proxy_icon_url, animatedMediaPlaybackAllowed ? {} : {format: 'webp', animated: false})
|
? buildMediaProxyURL(footer.proxy_icon_url, animatedMediaPlaybackAllowed ? {} : {format: 'webp', animated: false})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const iconLoaded = useCachedImageLoaded(iconSrc);
|
||||||
if (!(footer || formattedTimestamp)) return null;
|
if (!(footer || formattedTimestamp)) return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.embedFooter, footer?.proxy_icon_url && styles.hasThumbnail)}
|
className={clsx(styles.embedFooter, footer?.proxy_icon_url && styles.hasThumbnail)}
|
||||||
data-flx="channel.embeds.embed.embed-footer-component.embed-footer"
|
data-flx="channel.embeds.embed.embed-footer-component.embed-footer"
|
||||||
>
|
>
|
||||||
{iconSrc && (
|
{iconSrc && iconLoaded && (
|
||||||
<img
|
<img
|
||||||
alt=""
|
alt=""
|
||||||
className={styles.embedFooterIcon}
|
className={styles.embedFooterIcon}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
||||||
|
import {useEffect, useState} from 'react';
|
||||||
|
|
||||||
|
export function useCachedImageLoaded(src: string | null | undefined): boolean {
|
||||||
|
const resolved = src ?? null;
|
||||||
|
const [readySrc, setReadySrc] = useState<string | null>(() =>
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user