fix: page icon auth if needed (#192)

This commit is contained in:
Nathan.fooo
2025-12-05 13:53:38 +08:00
committed by GitHub
parent 33a46b0f6e
commit bb210457a6
2 changed files with 59 additions and 6 deletions

View File

@@ -8,6 +8,7 @@ import { ReactComponent as CalendarSvg } from '@/assets/icons/calendar.svg';
import { ReactComponent as GridSvg } from '@/assets/icons/grid.svg';
import { ReactComponent as DocumentSvg } from '@/assets/icons/page.svg';
import { cn } from '@/lib/utils';
import { getImageUrl, revokeBlobUrl } from '@/utils/authenticated-image';
import { renderColor } from '@/utils/color';
import { getIcon, isFlagEmoji } from '@/utils/emoji';
@@ -24,6 +25,7 @@ function PageIcon({
iconSize?: number;
}) {
const [iconContent, setIconContent] = React.useState<string | undefined>(undefined);
const [imgSrc, setImgSrc] = React.useState<string | undefined>(undefined);
const emoji = useMemo(() => {
if (view.icon && view.icon.ty === ViewIconType.Emoji && view.icon.value) {
@@ -33,17 +35,36 @@ function PageIcon({
return null;
}, [view]);
const img = useMemo(() => {
useEffect(() => {
let currentBlobUrl: string | undefined;
if (view.icon && view.icon.ty === ViewIconType.URL && view.icon.value) {
void getImageUrl(view.icon.value).then((url) => {
currentBlobUrl = url;
setImgSrc(url);
});
} else {
setImgSrc(undefined);
}
return () => {
if (currentBlobUrl) {
revokeBlobUrl(currentBlobUrl);
}
};
}, [view.icon]);
const img = useMemo(() => {
if (imgSrc) {
return (
<span className={cn('h-full w-full p-[2px]', className)}>
<img className={'h-full w-full'} src={view.icon.value} alt='icon' />
<img className={'h-full w-full'} src={imgSrc} alt='icon' />
</span>
);
}
return null;
}, [className, view.icon]);
}, [className, imgSrc]);
const isFlag = useMemo(() => {
return emoji ? isFlagEmoji(emoji) : false;

View File

@@ -81,10 +81,42 @@ export async function getImageUrl(url: string | undefined): Promise<string> {
}
/**
* Cleans up a blob URL created by fetchAuthenticatedImage
* Should be called when the component unmounts or the URL is no longer needed
* Cleans up a blob URL created by fetchAuthenticatedImage.
*
* @param url - The blob URL to revoke
* ## Why this is needed
*
* When `fetchAuthenticatedImage` fetches an image with auth headers, it creates
* a Blob URL using `URL.createObjectURL()`. This URL holds a reference to the
* binary image data in browser memory.
*
* The browser keeps this data alive as long as the Blob URL exists - even if:
* - The `<img>` element is removed from the DOM
* - The React component unmounts
* - The URL is no longer referenced anywhere in code
*
* Without calling `revokeBlobUrl`, each authenticated image fetch causes a
* memory leak. For example, browsing 100 pages with 1MB icon images would
* accumulate ~100MB in memory that is never freed until page reload.
*
* ## Usage
*
* Call this function in a useEffect cleanup when the component unmounts
* or when the image URL changes:
*
* ```tsx
* useEffect(() => {
* let blobUrl: string | undefined;
* getImageUrl(url).then((result) => {
* blobUrl = result;
* setImgSrc(result);
* });
* return () => {
* if (blobUrl) revokeBlobUrl(blobUrl);
* };
* }, [url]);
* ```
*
* @param url - The blob URL to revoke. Safe to call with non-blob URLs (no-op).
*/
export function revokeBlobUrl(url: string): void {
if (url && url.startsWith('blob:')) {