diff --git a/src/app/components/Modal500.tsx b/src/app/components/Modal500.tsx
index d421b62..59844cc 100644
--- a/src/app/components/Modal500.tsx
+++ b/src/app/components/Modal500.tsx
@@ -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 (
}>
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(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 = (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 (
{
if (matchPath(LOGIN_PATH, pathname)) {
@@ -123,87 +124,90 @@ export function AuthLayout() {
discoveryState.status === AsyncStatus.Success ? discoveryState.data.response : [];
return (
-
-
-
-
-
-
- Paarrot
+ <>
+
+
+
+
+
+
+
+ Paarrot
+
+
+
+
+
+ Homeserver
+
+
+
+ {discoveryState.status === AsyncStatus.Loading && (
+
+ )}
+ {discoveryState.status === AsyncStatus.Error && (
+
+ )}
+ {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && (
+
+ )}
+ {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && (
+
+ )}
+ {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && (
+
+
+ (
+
+ )}
+ error={() => (
+
+ )}
+ >
+ {(specVersions) => (
+
+ (
+
+ )}
+ error={() => (
+
+ )}
+ >
+ {(authFlows) => (
+
+
+
+ )}
+
+
+ )}
+
+
+
+ )}
-
-
-
-
- Homeserver
-
-
-
- {discoveryState.status === AsyncStatus.Loading && (
-
- )}
- {discoveryState.status === AsyncStatus.Error && (
-
- )}
- {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_PROMPT && (
-
- )}
- {autoDiscoveryError?.action === AutoDiscoveryAction.FAIL_ERROR && (
-
- )}
- {discoveryState.status === AsyncStatus.Success && autoDiscoveryInfo && (
-
-
- (
-
- )}
- error={() => (
-
- )}
- >
- {(specVersions) => (
-
- (
-
- )}
- error={() => (
-
- )}
- >
- {(authFlows) => (
-
-
-
- )}
-
-
- )}
-
-
-
- )}
+
-
-
-
+
+ >
);
}
diff --git a/src/app/utils/gifMetadata.ts b/src/app/utils/gifMetadata.ts
index 0895272..814a728 100644
--- a/src/app/utils/gifMetadata.ts
+++ b/src/app/utils/gifMetadata.ts
@@ -185,10 +185,7 @@ function createPaarrotExtension(color: string): Uint8Array {
export function extractColorFromGIF(imageData: ArrayBuffer | Uint8Array): string | undefined {
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
- console.log('[extractColorFromGIF] Input size:', data.length, 'bytes');
-
if (!isGIF(data)) {
- console.error('[extractColorFromGIF] Not a GIF file');
return undefined;
}
@@ -196,19 +193,16 @@ export function extractColorFromGIF(imageData: ArrayBuffer | Uint8Array): string
const startOffset = 13 + gctSize; // Header(6) + LSD(7) + GCT
const extensions = findAppExtensions(data, startOffset);
- console.log('[extractColorFromGIF] Found', extensions.length, 'application extensions');
for (const ext of extensions) {
if (ext.appId.startsWith('PAARROT')) {
if (ext.dataBlocks.length > 0) {
const color = new TextDecoder().decode(ext.dataBlocks[0]);
- console.log('[extractColorFromGIF] Found color:', color);
return color;
}
}
}
- console.log('[extractColorFromGIF] No color found');
return undefined;
}
@@ -218,10 +212,7 @@ export function extractColorFromGIF(imageData: ArrayBuffer | Uint8Array): string
export function embedColorInGIF(imageData: ArrayBuffer | Uint8Array, color: string): Uint8Array | null {
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
- console.log('[embedColorInGIF] Input size:', data.length, 'bytes');
-
if (!isGIF(data)) {
- console.error('[embedColorInGIF] Not a GIF file');
return null;
}
@@ -229,7 +220,6 @@ export function embedColorInGIF(imageData: ArrayBuffer | Uint8Array, color: stri
const insertOffset = 13 + gctSize; // After Header + LSD + GCT
const extensions = findAppExtensions(data, insertOffset);
- console.log('[embedColorInGIF] Found', extensions.length, 'application extensions');
// Find existing PAARROT extension
let existingExt: { offset: number; length: number } | null = null;
@@ -243,14 +233,10 @@ export function embedColorInGIF(imageData: ArrayBuffer | Uint8Array, color: stri
// Create new extension
const newExtension = createPaarrotExtension(color);
- console.log('[embedColorInGIF] Created extension, size:', newExtension.length, 'bytes');
- console.log('[embedColorInGIF] Color being embedded:', color);
-
let newData: Uint8Array;
if (existingExt) {
// Replace existing extension
- console.log('[embedColorInGIF] Replacing existing extension at offset', existingExt.offset);
newData = new Uint8Array(data.length - existingExt.length + newExtension.length);
newData.set(data.slice(0, existingExt.offset), 0);
newData.set(newExtension, existingExt.offset);
@@ -260,15 +246,12 @@ export function embedColorInGIF(imageData: ArrayBuffer | Uint8Array, color: stri
);
} else {
// Insert after GCT (before any other blocks)
- console.log('[embedColorInGIF] Inserting new extension at offset', insertOffset);
newData = new Uint8Array(data.length + newExtension.length);
newData.set(data.slice(0, insertOffset), 0);
newData.set(newExtension, insertOffset);
newData.set(data.slice(insertOffset), insertOffset + newExtension.length);
}
- console.log('[embedColorInGIF] Output size:', newData.length, 'bytes');
-
return newData;
}
diff --git a/src/app/utils/jpegMetadata.ts b/src/app/utils/jpegMetadata.ts
index a5e642d..757ba32 100644
--- a/src/app/utils/jpegMetadata.ts
+++ b/src/app/utils/jpegMetadata.ts
@@ -145,15 +145,11 @@ function isPaarrotXMP(segmentData: Uint8Array): boolean {
export function extractColorFromJPEG(imageData: ArrayBuffer | Uint8Array): string | undefined {
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
- console.log('[extractColorFromJPEG] Input size:', data.length, 'bytes');
-
if (!isJPEG(data)) {
- console.error('[extractColorFromJPEG] Not a JPEG file');
return undefined;
}
const segments = parseSegments(data);
- console.log('[extractColorFromJPEG] Found', segments.length, 'segments');
for (const segment of segments) {
// APP1 marker (0xE1) is used for XMP
@@ -172,7 +168,6 @@ export function extractColorFromJPEG(imageData: ArrayBuffer | Uint8Array): strin
const xmpString = new TextDecoder().decode(segment.data.slice(XMP_NAMESPACE_BYTES.length));
const color = extractColorFromXMP(xmpString);
if (color) {
- console.log('[extractColorFromJPEG] Found color:', color);
return color;
}
}
@@ -180,7 +175,6 @@ export function extractColorFromJPEG(imageData: ArrayBuffer | Uint8Array): strin
}
}
- console.log('[extractColorFromJPEG] No color found');
return undefined;
}
@@ -190,15 +184,11 @@ export function extractColorFromJPEG(imageData: ArrayBuffer | Uint8Array): strin
export function embedColorInJPEG(imageData: ArrayBuffer | Uint8Array, color: string): Uint8Array | null {
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
- console.log('[embedColorInJPEG] Input size:', data.length, 'bytes');
-
if (!isJPEG(data)) {
- console.error('[embedColorInJPEG] Not a JPEG file');
return null;
}
const segments = parseSegments(data);
- console.log('[embedColorInJPEG] Found', segments.length, 'segments');
// Find existing paarrot XMP segment
let existingSegment: { offset: number; length: number } | null = null;
@@ -225,14 +215,10 @@ export function embedColorInJPEG(imageData: ArrayBuffer | Uint8Array, color: str
app1Segment.set(writeUint16BE(segmentLength), 2);
app1Segment.set(segmentData, 4);
- console.log('[embedColorInJPEG] Created APP1 segment, size:', app1Segment.length, 'bytes');
- console.log('[embedColorInJPEG] Color being embedded:', color);
-
let newData: Uint8Array;
if (existingSegment) {
// Replace existing segment
- console.log('[embedColorInJPEG] Replacing existing segment at offset', existingSegment.offset);
newData = new Uint8Array(data.length - existingSegment.length + app1Segment.length);
newData.set(data.slice(0, existingSegment.offset), 0);
newData.set(app1Segment, existingSegment.offset);
@@ -242,15 +228,12 @@ export function embedColorInJPEG(imageData: ArrayBuffer | Uint8Array, color: str
);
} else {
// Insert after SOI marker (at offset 2)
- console.log('[embedColorInJPEG] Inserting new segment after SOI');
newData = new Uint8Array(data.length + app1Segment.length);
newData.set(JPEG_SOI, 0);
newData.set(app1Segment, 2);
newData.set(data.slice(2), 2 + app1Segment.length);
}
- console.log('[embedColorInJPEG] Output size:', newData.length, 'bytes');
-
return newData;
}
diff --git a/src/app/utils/pngMetadata.ts b/src/app/utils/pngMetadata.ts
index cb3ee35..ea28870 100644
--- a/src/app/utils/pngMetadata.ts
+++ b/src/app/utils/pngMetadata.ts
@@ -141,31 +141,24 @@ function parseTextChunk(data: Uint8Array): { key: string; value: string } | null
export function extractColorFromPNG(imageData: ArrayBuffer | Uint8Array): string | undefined {
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
- console.log('[extractColorFromPNG] Input size:', data.length, 'bytes');
-
// Verify PNG signature
for (let i = 0; i < 8; i++) {
if (data[i] !== PNG_SIGNATURE[i]) {
- console.error('[extractColorFromPNG] Not a PNG! Byte', i, 'is', data[i], 'expected', PNG_SIGNATURE[i]);
return undefined; // Not a PNG
}
}
const chunks = parseChunks(data);
- console.log('[extractColorFromPNG] Found', chunks.length, 'chunks:', chunks.map(c => c.type).join(', '));
for (const chunk of chunks) {
if (chunk.type === 'tEXt') {
const text = parseTextChunk(chunk.data);
- console.log('[extractColorFromPNG] tEXt chunk found, key:', text?.key);
if (text && text.key === PAARROT_COLOR_KEY) {
- console.log('[extractColorFromPNG] Found color:', text.value);
return text.value;
}
}
}
- console.log('[extractColorFromPNG] No color found');
return undefined;
}
@@ -178,22 +171,14 @@ export function extractColorFromPNG(imageData: ArrayBuffer | Uint8Array): string
export function embedColorInPNG(imageData: ArrayBuffer | Uint8Array, color: string): Uint8Array | null {
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
- console.log('[embedColorInPNG] Input size:', data.length, 'bytes');
- console.log('[embedColorInPNG] First 8 bytes:', Array.from(data.slice(0, 8)));
- console.log('[embedColorInPNG] PNG signature expected:', Array.from(PNG_SIGNATURE));
-
// Verify PNG signature
for (let i = 0; i < 8; i++) {
if (data[i] !== PNG_SIGNATURE[i]) {
- console.error('[embedColorInPNG] Not a PNG! Byte', i, 'is', data[i], 'expected', PNG_SIGNATURE[i]);
return null; // Not a PNG
}
}
- console.log('[embedColorInPNG] PNG signature verified OK');
-
const chunks = parseChunks(data);
- console.log('[embedColorInPNG] Found', chunks.length, 'chunks:', chunks.map(c => c.type).join(', '));
// Find IHDR chunk (always first) and any existing paarrot tEXt chunk
let insertOffset = 8; // After signature
@@ -212,15 +197,12 @@ export function embedColorInPNG(imageData: ArrayBuffer | Uint8Array, color: stri
// Create new tEXt chunk with color
const colorChunk = createTextChunk(PAARROT_COLOR_KEY, color);
- console.log('[embedColorInPNG] Created color chunk, size:', colorChunk.length, 'bytes');
- console.log('[embedColorInPNG] Color being embedded:', color);
// Build new PNG data
let newData: Uint8Array;
if (existingPaarrotChunk) {
// Replace existing chunk
- console.log('[embedColorInPNG] Replacing existing chunk at offset', existingPaarrotChunk.offset);
newData = new Uint8Array(data.length - existingPaarrotChunk.length + colorChunk.length);
newData.set(data.slice(0, existingPaarrotChunk.offset), 0);
newData.set(colorChunk, existingPaarrotChunk.offset);
@@ -230,15 +212,12 @@ export function embedColorInPNG(imageData: ArrayBuffer | Uint8Array, color: stri
);
} else {
// Insert new chunk after IHDR
- console.log('[embedColorInPNG] Inserting new chunk at offset', insertOffset);
newData = new Uint8Array(data.length + colorChunk.length);
newData.set(data.slice(0, insertOffset), 0);
newData.set(colorChunk, insertOffset);
newData.set(data.slice(insertOffset), insertOffset + colorChunk.length);
}
- console.log('[embedColorInPNG] Output size:', newData.length, 'bytes');
-
return newData;
}
diff --git a/src/app/utils/webpMetadata.ts b/src/app/utils/webpMetadata.ts
index bd9b25b..434d3af 100644
--- a/src/app/utils/webpMetadata.ts
+++ b/src/app/utils/webpMetadata.ts
@@ -123,28 +123,22 @@ function parseChunks(data: Uint8Array): Array<{ id: string; data: Uint8Array; of
export function extractColorFromWebP(imageData: ArrayBuffer | Uint8Array): string | undefined {
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
- console.log('[extractColorFromWebP] Input size:', data.length, 'bytes');
-
if (!isWebP(data)) {
- console.error('[extractColorFromWebP] Not a WebP file');
return undefined;
}
const chunks = parseChunks(data);
- console.log('[extractColorFromWebP] Found', chunks.length, 'chunks:', chunks.map(c => c.id).join(', '));
for (const chunk of chunks) {
if (chunk.id === 'XMP ') {
const xmpString = new TextDecoder().decode(chunk.data);
const color = extractColorFromXMP(xmpString);
if (color) {
- console.log('[extractColorFromWebP] Found color:', color);
return color;
}
}
}
- console.log('[extractColorFromWebP] No color found');
return undefined;
}
@@ -157,15 +151,11 @@ export function extractColorFromWebP(imageData: ArrayBuffer | Uint8Array): strin
export function embedColorInWebP(imageData: ArrayBuffer | Uint8Array, color: string): Uint8Array | null {
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
- console.log('[embedColorInWebP] Input size:', data.length, 'bytes');
-
if (!isWebP(data)) {
- console.error('[embedColorInWebP] Not a WebP file');
return null;
}
const chunks = parseChunks(data);
- console.log('[embedColorInWebP] Found', chunks.length, 'chunks:', chunks.map(c => c.id).join(', '));
// Find existing XMP chunk with our color
let existingXMPChunk: { offset: number; length: number } | null = null;
@@ -192,14 +182,10 @@ export function embedColorInWebP(imageData: ArrayBuffer | Uint8Array, color: str
xmpChunk.set(xmpBytes, 8);
// Padding byte is already 0 from Uint8Array initialization
- console.log('[embedColorInWebP] Created XMP chunk, size:', xmpChunk.length, 'bytes');
- console.log('[embedColorInWebP] Color being embedded:', color);
-
let newData: Uint8Array;
if (existingXMPChunk) {
// Replace existing chunk
- console.log('[embedColorInWebP] Replacing existing XMP chunk at offset', existingXMPChunk.offset);
newData = new Uint8Array(data.length - existingXMPChunk.length + xmpChunk.length);
newData.set(data.slice(0, existingXMPChunk.offset), 0);
newData.set(xmpChunk, existingXMPChunk.offset);
@@ -209,7 +195,6 @@ export function embedColorInWebP(imageData: ArrayBuffer | Uint8Array, color: str
);
} else {
// Insert new chunk at end (before any trailing data)
- console.log('[embedColorInWebP] Inserting new XMP chunk at end');
newData = new Uint8Array(data.length + xmpChunk.length);
newData.set(data, 0);
newData.set(xmpChunk, data.length);
@@ -219,8 +204,6 @@ export function embedColorInWebP(imageData: ArrayBuffer | Uint8Array, color: str
const newFileSize = newData.length - 8; // RIFF size excludes first 8 bytes
newData.set(writeUint32LE(newFileSize), 4);
- console.log('[embedColorInWebP] Output size:', newData.length, 'bytes');
-
return newData;
}