From 8b0960d3661643aaefbc17ea0d66d557f251c7df Mon Sep 17 00:00:00 2001 From: Litruv Date: Sat, 21 Feb 2026 17:50:49 +1100 Subject: [PATCH] electron --- src/app/components/Modal500.tsx | 20 ++- src/app/components/user-avatar/UserAvatar.tsx | 34 +++- src/app/pages/auth/AuthLayout.tsx | 162 +++++++++--------- src/app/utils/gifMetadata.ts | 17 -- src/app/utils/jpegMetadata.ts | 17 -- src/app/utils/pngMetadata.ts | 21 --- src/app/utils/webpMetadata.ts | 17 -- 7 files changed, 133 insertions(+), 155 deletions(-) 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 Logo - Paarrot + <> + + + + +
+ + Paarrot Logo + 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; }