electron
This commit is contained in:
@@ -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,
|
||||
}}
|
||||
|
||||
@@ -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,10 +23,29 @@ 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');
|
||||
setLoaded(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}
|
||||
|
||||
@@ -30,6 +30,7 @@ import { AuthFlowsLoader } from '../../components/AuthFlowsLoader';
|
||||
import { AuthFlowsProvider } from '../../hooks/useAuthFlows';
|
||||
import { AuthServerProvider } from '../../hooks/useAuthServer';
|
||||
import { tryDecodeURIComponent } from '../../utils/dom';
|
||||
import { TitleBar } from '../../components/title-bar';
|
||||
|
||||
const currentAuthPath = (pathname: string): string => {
|
||||
if (matchPath(LOGIN_PATH, pathname)) {
|
||||
@@ -123,6 +124,8 @@ export function AuthLayout() {
|
||||
discoveryState.status === AsyncStatus.Success ? discoveryState.data.response : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<TitleBar />
|
||||
<Scroll variant="Background" visibility="Hover" size="300" hideTrack>
|
||||
<Box
|
||||
className={classNames(css.AuthLayout, PatternsCss.BackgroundDotPattern)}
|
||||
@@ -205,5 +208,6 @@ export function AuthLayout() {
|
||||
<AuthFooter />
|
||||
</Box>
|
||||
</Scroll>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user