This commit is contained in:
2026-02-21 17:50:49 +11:00
parent 6c741577ce
commit 8b0960d366
7 changed files with 133 additions and 155 deletions

View File

@@ -8,13 +8,31 @@ type Modal500Props = {
children: ReactNode;
};
export function Modal500({ requestClose, children }: Modal500Props) {
const handleClickOutside = (e: MouseEvent | TouchEvent | FocusEvent) => {
const target = e.target as HTMLElement;
// Don't close if clicking on a popout menu, overlay, or other portaled content
// Check for common portal container elements and popout menus
if (
target.closest('[role="menu"]') ||
target.closest('[role="dialog"]') ||
target.closest('[data-floating-ui-portal]') ||
target.closest('.folds-overlay') ||
target.closest('.popout')
) {
return false;
}
return true;
};
return (
<Overlay open backdrop={<OverlayBackdrop />}>
<OverlayCenter>
<FocusTrap
focusTrapOptions={{
initialFocus: false,
clickOutsideDeactivates: true,
clickOutsideDeactivates: handleClickOutside,
onDeactivate: requestClose,
escapeDeactivates: stopPropagation,
}}

View File

@@ -1,5 +1,5 @@
import { AvatarFallback, AvatarImage, color } from 'folds';
import React, { ReactEventHandler, ReactNode, useState } from 'react';
import React, { ReactEventHandler, ReactNode, useEffect, useRef, useState } from 'react';
import classNames from 'classnames';
import * as css from './UserAvatar.css';
import colorMXID from '../../../util/colorMXID';
@@ -7,6 +7,13 @@ import { useAuthenticatedMediaUrl } from '../../hooks/useAuthenticatedMediaUrl';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { cacheAvatar, isAvatarCached, pruneAvatarCache } from '../../utils/avatarCache';
// Check if image is already in browser cache
function isImageInBrowserCache(url: string): boolean {
const img = new Image();
img.src = url;
return img.complete && img.naturalWidth > 0;
}
type UserAvatarProps = {
className?: string;
userId: string;
@@ -16,9 +23,28 @@ type UserAvatarProps = {
};
export function UserAvatar({ className, userId, src, alt, renderFallback }: UserAvatarProps) {
const [error, setError] = useState(false);
const [loaded, setLoaded] = useState(() => isAvatarCached(src));
const useAuthentication = useMediaAuthentication();
const authenticatedSrc = useAuthenticatedMediaUrl(src, useAuthentication);
// Check both our cache and browser's native cache
const [loaded, setLoaded] = useState(() => {
if (isAvatarCached(src)) return true;
if (authenticatedSrc && isImageInBrowserCache(authenticatedSrc)) {
cacheAvatar(src);
return true;
}
return false;
});
const imgRef = useRef<HTMLImageElement>(null);
// Also check on mount if image is already complete (browser cached)
useEffect(() => {
if (!loaded && imgRef.current?.complete && imgRef.current?.naturalWidth > 0) {
setLoaded(true);
cacheAvatar(src);
}
}, [authenticatedSrc, loaded, src]);
const handleLoad: ReactEventHandler<HTMLImageElement> = (evt) => {
evt.currentTarget.setAttribute('data-image-loaded', 'true');
@@ -54,7 +80,8 @@ export function UserAvatar({ className, userId, src, alt, renderFallback }: User
);
}
// Loading state - render fallback with image overlay
// Loading state - render image with hidden fallback behind it
// Image loads invisibly, then we show it once complete
return (
<div style={{ position: 'relative', width: '100%', height: '100%' }}>
<AvatarFallback
@@ -69,6 +96,7 @@ export function UserAvatar({ className, userId, src, alt, renderFallback }: User
{renderFallback()}
</AvatarFallback>
<AvatarImage
ref={imgRef}
className={classNames(css.UserAvatar, className)}
style={{ position: 'relative', zIndex: 1 }}
src={authenticatedSrc}