mirror of
https://github.com/AppFlowy-IO/AppFlowy-Web.git
synced 2026-03-13 10:00:26 +08:00
fix: page icon auth if needed (#192)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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:')) {
|
||||
|
||||
Reference in New Issue
Block a user