feat: Add detailed logging for user profile components and image metadata extraction; enhance color handling utilities
This commit is contained in:
@@ -37,6 +37,13 @@ export function UserHero({ userId, avatarUrl, avatarMxc, presence }: UserHeroPro
|
||||
const bannerUrl = useOtherUserBanner(userId, avatarMxc); // Now returns blob URL directly
|
||||
const profileStyle = useOtherUserProfileStyle(userId, avatarMxc);
|
||||
|
||||
console.log('[UserHero]', {
|
||||
userId,
|
||||
avatarMxc,
|
||||
bannerUrl,
|
||||
profileStyle,
|
||||
});
|
||||
|
||||
return (
|
||||
<Box
|
||||
direction="Column"
|
||||
@@ -125,6 +132,13 @@ export function UserHeroName({ displayName, userId, avatarMxc }: UserHeroNamePro
|
||||
const username = getMxIdLocalPart(userId);
|
||||
const userColor = useOtherUserColor(userId, avatarMxc);
|
||||
|
||||
console.log('[UserHeroName]', {
|
||||
userId,
|
||||
avatarMxc,
|
||||
userColor,
|
||||
fallbackColor: colorMXID(userId),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box grow="Yes" direction="Column" gap="100">
|
||||
<Box alignItems="Baseline" gap="200" wrap="Wrap">
|
||||
|
||||
@@ -26,7 +26,8 @@ import { DirectCreateSearchParams } from '../../pages/paths';
|
||||
import { getContrastingTextColor, stripAlphaFromColor, getTextShadowColor } from '../../utils/common';
|
||||
import classNames from 'classnames';
|
||||
import { BreakWord, LineClamp3 } from '../../styles/Text.css';
|
||||
import colorMXID from '../../../util/colorMXID';
|
||||
import colorMXID, { getColorMXIDValue } from '../../../util/colorMXID';
|
||||
import { useOtherUserColor } from '../../hooks/useUserColor';
|
||||
|
||||
type UserRoomProfileProps = {
|
||||
userId: string;
|
||||
@@ -64,6 +65,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
||||
|
||||
const presence = useUserPresence(userId);
|
||||
const profileStyle = useOtherUserProfileStyle(userId, avatarMxc);
|
||||
const userColor = useOtherUserColor(userId, avatarMxc);
|
||||
|
||||
// Build gradient CSS if configured
|
||||
const gradientStyle = profileStyle.gradient
|
||||
@@ -87,9 +89,25 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
||||
(canKickUser && membership === Membership.Join) ||
|
||||
(canBanUser && membership !== Membership.Ban);
|
||||
|
||||
const profileColor = profileStyle.avatarBorderColor ? stripAlphaFromColor(profileStyle.avatarBorderColor) : colorMXID(userId);
|
||||
// Use userColor for text, or fall back to colorMXID
|
||||
const profileColor = userColor ? userColor : getColorMXIDValue(userId);
|
||||
const profileTextShadow = `0 1px 4px ${getTextShadowColor(profileColor)}`;
|
||||
const pillBgColor = profileStyle.avatarBorderColor ? stripAlphaFromColor(profileStyle.avatarBorderColor) : undefined;
|
||||
|
||||
// Use avatarBorderColor for pill background only if it has visible alpha (not fully transparent)
|
||||
const hasVisibleBorder = profileStyle.avatarBorderColor && !profileStyle.avatarBorderColor.endsWith('00');
|
||||
const pillBgColor = hasVisibleBorder ? stripAlphaFromColor(profileStyle.avatarBorderColor!) : undefined;
|
||||
|
||||
console.log('[UserRoomProfile]', {
|
||||
userId,
|
||||
avatarMxc,
|
||||
userColor,
|
||||
profileStyle,
|
||||
profileColor,
|
||||
profileTextShadow,
|
||||
hasVisibleBorder,
|
||||
pillBgColor,
|
||||
fallbackColorMXID: getColorMXIDValue(userId),
|
||||
});
|
||||
|
||||
return (
|
||||
<Box direction="Column">
|
||||
|
||||
@@ -55,7 +55,7 @@ import { useUserProfileStyle } from '../../../hooks/useUserProfileStyle';
|
||||
import { useUserPresence } from '../../../hooks/useUserPresence';
|
||||
import { AvatarPresence, PresenceBadge } from '../../../components/presence';
|
||||
import { BreakWord, LineClamp3 } from '../../../styles/Text.css';
|
||||
import colorMXID from '../../../../util/colorMXID';
|
||||
import colorMXID, { getColorMXIDValue } from '../../../../util/colorMXID';
|
||||
import {
|
||||
extractMetadataFromImage,
|
||||
embedMetadataInImage,
|
||||
@@ -574,7 +574,7 @@ function ProfileBanner({ avatarRef, nameRef }: { avatarRef: React.RefObject<HTML
|
||||
: profileStyle.avatarBorderColor;
|
||||
|
||||
const previewPillBgColor = previewBorderColor ? stripAlphaFromColor(previewBorderColor) : undefined;
|
||||
const previewProfileColor = userColor || colorMXID(userId);
|
||||
const previewProfileColor = userColor || getColorMXIDValue(userId);
|
||||
|
||||
const previewTextShadow = `0 1px 4px ${getTextShadowColor(previewProfileColor)}`;
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ export function useUserBanner(): [
|
||||
|
||||
const profile = await mx.getProfileInfo(userId);
|
||||
const avatarUrl = profile.avatar_url;
|
||||
console.log('[useUserBanner.loadBanner] Loading banner from avatar:', avatarUrl);
|
||||
|
||||
if (!avatarUrl) {
|
||||
setBanner(undefined);
|
||||
@@ -90,6 +91,7 @@ export function useUserBanner(): [
|
||||
// Always use current session's token to avoid stale tokens during account switches
|
||||
const accessToken = getCurrentAccessToken();
|
||||
const metadata = await fetchAndExtractMetadata(httpUrl, useAuthentication ? accessToken : null);
|
||||
console.log('[useUserBanner.loadBanner] Extracted banner:', metadata.banner);
|
||||
setBanner(metadata.banner);
|
||||
} catch {
|
||||
setBanner(undefined);
|
||||
@@ -104,7 +106,8 @@ export function useUserBanner(): [
|
||||
if (!userId) return undefined;
|
||||
|
||||
const user = mx.getUser(userId);
|
||||
const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = () => {
|
||||
const onAvatarChange: UserEventHandlerMap[UserEvent.AvatarUrl] = (newAvatarUrl) => {
|
||||
console.log('[useUserBanner] Avatar changed event fired, new URL:', newAvatarUrl);
|
||||
loadBanner();
|
||||
};
|
||||
|
||||
@@ -143,12 +146,16 @@ export function useUserBanner(): [
|
||||
|
||||
// Get existing metadata to preserve
|
||||
const existingMetadata = extractMetadataFromImage(avatarData);
|
||||
console.log('[updateBanner] Existing metadata:', existingMetadata);
|
||||
|
||||
// Modify image metadata with new banner, preserving color
|
||||
const newMetadata: ImageMetadata = {
|
||||
color: existingMetadata.color,
|
||||
banner: newBanner,
|
||||
avatarBorderColor: existingMetadata.avatarBorderColor,
|
||||
gradient: existingMetadata.gradient,
|
||||
};
|
||||
console.log('[updateBanner] New metadata to embed:', newMetadata);
|
||||
|
||||
const newAvatarData = embedMetadataInImage(avatarData, newMetadata);
|
||||
|
||||
@@ -158,10 +165,13 @@ export function useUserBanner(): [
|
||||
|
||||
// Verify the banner was embedded correctly before uploading
|
||||
const verifyMetadata = extractMetadataFromImage(newAvatarData);
|
||||
console.log('[updateBanner] Verification - extracted metadata from new avatar:', verifyMetadata);
|
||||
|
||||
if (newBanner && verifyMetadata.banner !== newBanner) {
|
||||
console.error('[updateBanner] Banner verification FAILED!', 'Expected:', newBanner, 'Got:', verifyMetadata.banner);
|
||||
throw new Error('Banner verification failed');
|
||||
}
|
||||
console.log('[updateBanner] Banner verification passed');
|
||||
|
||||
// Upload modified avatar with correct MIME type
|
||||
const mimeType = getMimeType(format);
|
||||
@@ -172,21 +182,26 @@ export function useUserBanner(): [
|
||||
name: `avatar.${extension}`,
|
||||
type: mimeType,
|
||||
});
|
||||
console.log('[updateBanner] Avatar uploaded successfully, MXC:', uploadResponse.content_uri);
|
||||
|
||||
// Update profile with new avatar
|
||||
try {
|
||||
console.log('[updateBanner] Setting avatar URL to:', uploadResponse.content_uri);
|
||||
await Promise.race([
|
||||
mx.setAvatarUrl(uploadResponse.content_uri),
|
||||
new Promise((_, reject) => {
|
||||
setTimeout(() => reject(new Error('setAvatarUrl timeout')), 30000);
|
||||
})
|
||||
]);
|
||||
console.log('[updateBanner] setAvatarUrl completed');
|
||||
|
||||
// Manually sync user object to ensure event listeners are triggered
|
||||
const user = mx.getUser(userId);
|
||||
if (user && user.avatarUrl !== uploadResponse.content_uri) {
|
||||
console.log('[updateBanner] Manually syncing user avatar URL');
|
||||
user.setAvatarUrl(uploadResponse.content_uri);
|
||||
}
|
||||
console.log('[updateBanner] Banner update complete');
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update avatar URL: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
@@ -217,11 +232,14 @@ export function useOtherUserBanner(userId: string, avatarMxc: string | undefined
|
||||
let blobUrl: string | undefined;
|
||||
|
||||
if (!avatarMxc) {
|
||||
console.log('[useOtherUserBanner] No avatarMxc provided for', userId);
|
||||
setBannerBlobUrl(undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const loadBanner = async () => {
|
||||
console.log('[useOtherUserBanner] Loading banner for', userId, 'avatarMxc:', avatarMxc);
|
||||
|
||||
// Check cache first
|
||||
const cacheKey = `${userId}:${avatarMxc}`;
|
||||
const cached = userBannerCache.get(cacheKey);
|
||||
@@ -230,11 +248,15 @@ export function useOtherUserBanner(userId: string, avatarMxc: string | undefined
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
||||
// Use cached banner MXC
|
||||
console.log('[useOtherUserBanner] Using cached bannerMxc:', cached.bannerMxc);
|
||||
bannerMxc = cached.bannerMxc;
|
||||
} else {
|
||||
// Fetch banner MXC from avatar metadata
|
||||
const httpUrl = mxcUrlToHttp(mx, avatarMxc, useAuthentication);
|
||||
console.log('[useOtherUserBanner] Fetching metadata from:', httpUrl);
|
||||
|
||||
if (!httpUrl) {
|
||||
console.log('[useOtherUserBanner] Failed to get HTTP URL');
|
||||
setBannerBlobUrl(undefined);
|
||||
return;
|
||||
}
|
||||
@@ -243,6 +265,8 @@ export function useOtherUserBanner(userId: string, avatarMxc: string | undefined
|
||||
const accessToken = getCurrentAccessToken();
|
||||
const metadata = await fetchAndExtractMetadata(httpUrl, useAuthentication ? accessToken : null);
|
||||
|
||||
console.log('[useOtherUserBanner] Extracted metadata:', metadata);
|
||||
|
||||
bannerMxc = metadata.banner;
|
||||
|
||||
// Cache the banner MXC (not the blob URL)
|
||||
@@ -250,17 +274,23 @@ export function useOtherUserBanner(userId: string, avatarMxc: string | undefined
|
||||
}
|
||||
|
||||
if (!bannerMxc) {
|
||||
console.log('[useOtherUserBanner] No banner MXC found');
|
||||
setBannerBlobUrl(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[useOtherUserBanner] Fetching banner image from MXC:', bannerMxc);
|
||||
|
||||
// Fetch the banner image data with authentication
|
||||
const bannerHttpUrl = mxcUrlToHttp(mx, bannerMxc, useAuthentication);
|
||||
if (!bannerHttpUrl) {
|
||||
console.log('[useOtherUserBanner] Failed to convert banner MXC to HTTP URL');
|
||||
setBannerBlobUrl(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[useOtherUserBanner] Banner HTTP URL:', bannerHttpUrl);
|
||||
|
||||
// Always use current session's token to avoid stale tokens during account switches
|
||||
const accessToken = getCurrentAccessToken();
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
@@ -260,10 +260,19 @@ const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
export function useOtherUserColor(userId: string, avatarMxc: string | undefined): string | undefined {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [color, setColor] = useState<string | undefined>();
|
||||
|
||||
// Check cache synchronously BEFORE initializing state to avoid flash
|
||||
const cacheKey = avatarMxc ? `${userId}:${avatarMxc}` : '';
|
||||
const cachedEntry = cacheKey ? userColorCache.get(cacheKey) : undefined;
|
||||
const cachedColor = cachedEntry && Date.now() - cachedEntry.timestamp < CACHE_TTL_MS
|
||||
? cachedEntry.color
|
||||
: undefined;
|
||||
|
||||
const [color, setColor] = useState<string | undefined>(cachedColor);
|
||||
|
||||
useEffect(() => {
|
||||
if (!avatarMxc) {
|
||||
console.log('[useOtherUserColor] No avatarMxc provided for', userId);
|
||||
setColor(undefined);
|
||||
return;
|
||||
}
|
||||
@@ -272,13 +281,16 @@ export function useOtherUserColor(userId: string, avatarMxc: string | undefined)
|
||||
const cacheKey = `${userId}:${avatarMxc}`;
|
||||
const cached = userColorCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
||||
console.log('[useOtherUserColor] Using cached color for', userId, ':', cached.color);
|
||||
setColor(cached.color);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadColor = async () => {
|
||||
const httpUrl = mxcUrlToHttp(mx, avatarMxc, useAuthentication);
|
||||
console.log('[useOtherUserColor] Loading color for', userId, 'from', httpUrl);
|
||||
if (!httpUrl) {
|
||||
console.log('[useOtherUserColor] Failed to get HTTP URL for', avatarMxc);
|
||||
setColor(undefined);
|
||||
return;
|
||||
}
|
||||
@@ -287,6 +299,8 @@ export function useOtherUserColor(userId: string, avatarMxc: string | undefined)
|
||||
const accessToken = getCurrentAccessToken();
|
||||
const extractedColor = await fetchAndExtractColor(httpUrl, useAuthentication ? accessToken : null);
|
||||
|
||||
console.log('[useOtherUserColor] Extracted color for', userId, ':', extractedColor);
|
||||
|
||||
// Cache the result
|
||||
userColorCache.set(cacheKey, { color: extractedColor, timestamp: Date.now() });
|
||||
|
||||
|
||||
@@ -212,22 +212,29 @@ export function useOtherUserProfileStyle(userId: string, avatarMxc: string | und
|
||||
|
||||
useEffect(() => {
|
||||
if (!avatarMxc) {
|
||||
console.log('[useOtherUserProfileStyle] No avatarMxc provided for', userId);
|
||||
setStyle({});
|
||||
return;
|
||||
}
|
||||
|
||||
const loadStyle = async () => {
|
||||
console.log('[useOtherUserProfileStyle] Loading style for', userId, 'avatarMxc:', avatarMxc);
|
||||
|
||||
// Check cache first
|
||||
const cacheKey = `${userId}:${avatarMxc}`;
|
||||
const cached = userStyleCache.get(cacheKey);
|
||||
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
||||
console.log('[useOtherUserProfileStyle] Using cached style:', cached.style);
|
||||
setStyle(cached.style);
|
||||
return;
|
||||
}
|
||||
|
||||
const httpUrl = mxcUrlToHttp(mx, avatarMxc, useAuthentication);
|
||||
console.log('[useOtherUserProfileStyle] Fetching metadata from:', httpUrl);
|
||||
|
||||
if (!httpUrl) {
|
||||
console.log('[useOtherUserProfileStyle] Failed to get HTTP URL');
|
||||
setStyle({});
|
||||
return;
|
||||
}
|
||||
@@ -236,11 +243,15 @@ export function useOtherUserProfileStyle(userId: string, avatarMxc: string | und
|
||||
const accessToken = getCurrentAccessToken();
|
||||
const metadata = await fetchAndExtractMetadata(httpUrl, useAuthentication ? accessToken : null);
|
||||
|
||||
console.log('[useOtherUserProfileStyle] Extracted metadata:', metadata);
|
||||
|
||||
const styleData: ProfileStyleData = {
|
||||
avatarBorderColor: metadata.avatarBorderColor,
|
||||
gradient: metadata.gradient,
|
||||
};
|
||||
|
||||
console.log('[useOtherUserProfileStyle] Final styleData:', styleData);
|
||||
|
||||
// Cache the result
|
||||
userStyleCache.set(cacheKey, { style: styleData, timestamp: Date.now() });
|
||||
setStyle(styleData);
|
||||
|
||||
@@ -170,6 +170,8 @@ export function detectImageFormat(data: ArrayBuffer | Uint8Array): ImageFormat {
|
||||
export function extractColorFromImage(imageData: ArrayBuffer | Uint8Array): string | undefined {
|
||||
const format = detectImageFormat(imageData);
|
||||
|
||||
console.log('[extractColorFromImage] Detected format:', format);
|
||||
|
||||
switch (format) {
|
||||
case 'png':
|
||||
return extractColorFromPNG(imageData);
|
||||
@@ -211,14 +213,19 @@ export function extractBannerFromImage(imageData: ArrayBuffer | Uint8Array): str
|
||||
export function extractMetadataFromImage(imageData: ArrayBuffer | Uint8Array): ImageMetadata {
|
||||
const format = detectImageFormat(imageData);
|
||||
|
||||
console.log('[extractMetadataFromImage] Detected format:', format);
|
||||
|
||||
switch (format) {
|
||||
case 'png':
|
||||
return extractMetadataFromPNG(imageData);
|
||||
const pngMetadata = extractMetadataFromPNG(imageData);
|
||||
console.log('[extractMetadataFromImage] PNG metadata:', pngMetadata);
|
||||
return pngMetadata;
|
||||
// For other formats, extract color only for now
|
||||
case 'webp':
|
||||
case 'jpeg':
|
||||
case 'gif': {
|
||||
const color = extractColorFromImage(imageData);
|
||||
console.log('[extractMetadataFromImage] Non-PNG color:', color);
|
||||
return color ? { color } : {};
|
||||
}
|
||||
default:
|
||||
@@ -344,6 +351,10 @@ export function getExtension(format: ImageFormat): string {
|
||||
const avatarMetadataCache = new LRUCache<string, { metadata: ImageMetadata; timestamp: number }>(100);
|
||||
const METADATA_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
// In-flight fetch tracking to prevent duplicate parallel requests
|
||||
const inflightColorFetches = new Map<string, Promise<string | undefined>>();
|
||||
const inflightMetadataFetches = new Map<string, Promise<ImageMetadata>>();
|
||||
|
||||
/**
|
||||
* Fetch and extract color from an image URL (supports all formats)
|
||||
* @param url - HTTP URL to fetch the image from
|
||||
@@ -354,29 +365,51 @@ export async function fetchAndExtractColor(url: string, accessToken?: string | n
|
||||
// Check if full metadata is cached
|
||||
const cached = avatarMetadataCache.get(url);
|
||||
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
|
||||
console.log('[fetchAndExtractColor] Using cached color for', url, ':', cached.metadata.color);
|
||||
return cached.metadata.color;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||
});
|
||||
if (!response.ok) return undefined;
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
const color = extractColorFromImage(data);
|
||||
|
||||
// Cache the color result as part of metadata
|
||||
avatarMetadataCache.set(url, {
|
||||
metadata: { color },
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return color;
|
||||
} catch {
|
||||
return undefined;
|
||||
// Check if fetch already in progress
|
||||
const inflightKey = `${url}:${accessToken || ''}`;
|
||||
const existingFetch = inflightColorFetches.get(inflightKey);
|
||||
if (existingFetch) {
|
||||
console.log('[fetchAndExtractColor] Reusing in-flight fetch for', url);
|
||||
return existingFetch;
|
||||
}
|
||||
|
||||
console.log('[fetchAndExtractColor] Fetching and extracting color from', url);
|
||||
|
||||
const fetchPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||
});
|
||||
|
||||
console.log('[fetchAndExtractColor] Fetch response status:', response.ok, response.status);
|
||||
|
||||
if (!response.ok) return undefined;
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
console.log('[fetchAndExtractColor] Downloaded', data.byteLength, 'bytes');
|
||||
|
||||
const color = extractColorFromImage(data);
|
||||
console.log('[fetchAndExtractColor] Extracted color:', color);
|
||||
|
||||
// Don't cache here - fetchAndExtractMetadata will cache full metadata
|
||||
// This avoids cache collision where color-only fetch overwrites full metadata
|
||||
|
||||
return color;
|
||||
} catch (error) {
|
||||
console.error('[fetchAndExtractColor] Error fetching/extracting color:', error);
|
||||
return undefined;
|
||||
} finally {
|
||||
inflightColorFetches.delete(inflightKey);
|
||||
}
|
||||
})();
|
||||
|
||||
inflightColorFetches.set(inflightKey, fetchPromise);
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,27 +422,51 @@ export async function fetchAndExtractMetadata(url: string, accessToken?: string
|
||||
// Check cache first
|
||||
const cached = avatarMetadataCache.get(url);
|
||||
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
|
||||
console.log('[fetchAndExtractMetadata] Using cached metadata for', url, ':', cached.metadata);
|
||||
return cached.metadata;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||
});
|
||||
if (!response.ok) return {};
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
const metadata = extractMetadataFromImage(data);
|
||||
|
||||
// Cache the result
|
||||
avatarMetadataCache.set(url, {
|
||||
metadata,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return metadata;
|
||||
} catch {
|
||||
return {};
|
||||
// Check if fetch already in progress
|
||||
const inflightKey = `${url}:${accessToken || ''}`;
|
||||
const existingFetch = inflightMetadataFetches.get(inflightKey);
|
||||
if (existingFetch) {
|
||||
console.log('[fetchAndExtractMetadata] Reusing in-flight fetch for', url);
|
||||
return existingFetch;
|
||||
}
|
||||
|
||||
console.log('[fetchAndExtractMetadata] Fetching metadata from', url);
|
||||
|
||||
const fetchPromise = (async () => {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
||||
});
|
||||
|
||||
console.log('[fetchAndExtractMetadata] Fetch response status:', response.ok, response.status);
|
||||
|
||||
if (!response.ok) return {};
|
||||
|
||||
const data = await response.arrayBuffer();
|
||||
console.log('[fetchAndExtractMetadata] Downloaded', data.byteLength, 'bytes');
|
||||
|
||||
const metadata = extractMetadataFromImage(data);
|
||||
console.log('[fetchAndExtractMetadata] Extracted metadata:', metadata);
|
||||
|
||||
// Cache the result
|
||||
avatarMetadataCache.set(url, {
|
||||
metadata,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return metadata;
|
||||
} catch {
|
||||
return {};
|
||||
} finally {
|
||||
inflightMetadataFetches.delete(inflightKey);
|
||||
}
|
||||
})();
|
||||
|
||||
inflightMetadataFetches.set(inflightKey, fetchPromise);
|
||||
return fetchPromise;
|
||||
}
|
||||
|
||||
@@ -153,21 +153,30 @@ export function extractColorFromPNG(imageData: ArrayBuffer | Uint8Array): string
|
||||
// Verify PNG signature
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (data[i] !== PNG_SIGNATURE[i]) {
|
||||
console.warn('[extractColorFromPNG] Not a valid PNG');
|
||||
return undefined; // Not a PNG
|
||||
}
|
||||
}
|
||||
|
||||
const chunks = parseChunks(data);
|
||||
console.log('[extractColorFromPNG] Found', chunks.length, 'chunks');
|
||||
|
||||
let textChunksFound = 0;
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.type === 'tEXt') {
|
||||
textChunksFound++;
|
||||
const text = parseTextChunk(chunk.data);
|
||||
if (text && text.key === PAARROT_COLOR_KEY) {
|
||||
return text.value;
|
||||
if (text) {
|
||||
console.log('[extractColorFromPNG] Found tEXt chunk:', text.key, '=', text.value);
|
||||
if (text.key === PAARROT_COLOR_KEY) {
|
||||
console.log('[extractColorFromPNG] Found paarrot:color =', text.value);
|
||||
return text.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[extractColorFromPNG] No paarrot:color found (found', textChunksFound, 'tEXt chunks)');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -214,16 +223,21 @@ export function extractMetadataFromPNG(imageData: ArrayBuffer | Uint8Array): Ima
|
||||
// Verify PNG signature
|
||||
for (let i = 0; i < 8; i++) {
|
||||
if (data[i] !== PNG_SIGNATURE[i]) {
|
||||
console.warn('[extractMetadataFromPNG] Not a valid PNG');
|
||||
return metadata; // Not a PNG
|
||||
}
|
||||
}
|
||||
|
||||
const chunks = parseChunks(data);
|
||||
console.log('[extractMetadataFromPNG] Found', chunks.length, 'chunks');
|
||||
|
||||
let textChunksFound = 0;
|
||||
for (const chunk of chunks) {
|
||||
if (chunk.type === 'tEXt') {
|
||||
textChunksFound++;
|
||||
const text = parseTextChunk(chunk.data);
|
||||
if (text) {
|
||||
console.log('[extractMetadataFromPNG] Found tEXt chunk:', text.key, '=', text.value);
|
||||
if (text.key === PAARROT_COLOR_KEY) {
|
||||
metadata.color = text.value;
|
||||
} else if (text.key === PAARROT_BANNER_KEY) {
|
||||
@@ -241,6 +255,8 @@ export function extractMetadataFromPNG(imageData: ArrayBuffer | Uint8Array): Ima
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[extractMetadataFromPNG] Final metadata extracted:', metadata, 'found', textChunksFound, 'tEXt chunks');
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,69 @@
|
||||
// https://github.com/cloudrac3r/cadencegq/blob/master/pug/mxid.pug
|
||||
|
||||
/**
|
||||
* Convert HSL to hex color
|
||||
* @param {number} h - Hue (0-360)
|
||||
* @param {number} s - Saturation (0-100)
|
||||
* @param {number} l - Lightness (0-100)
|
||||
* @returns {string} Hex color string
|
||||
*/
|
||||
function hslToHex(h, s, l) {
|
||||
const sNorm = s / 100;
|
||||
const lNorm = l / 100;
|
||||
|
||||
const c = (1 - Math.abs(2 * lNorm - 1)) * sNorm;
|
||||
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
|
||||
const m = lNorm - c / 2;
|
||||
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
|
||||
if (h >= 0 && h < 60) {
|
||||
r = c; g = x; b = 0;
|
||||
} else if (h >= 60 && h < 120) {
|
||||
r = x; g = c; b = 0;
|
||||
} else if (h >= 120 && h < 180) {
|
||||
r = 0; g = c; b = x;
|
||||
} else if (h >= 180 && h < 240) {
|
||||
r = 0; g = x; b = c;
|
||||
} else if (h >= 240 && h < 300) {
|
||||
r = x; g = 0; b = c;
|
||||
} else if (h >= 300 && h < 360) {
|
||||
r = c; g = 0; b = x;
|
||||
}
|
||||
|
||||
const rHex = Math.round((r + m) * 255).toString(16).padStart(2, '0');
|
||||
const gHex = Math.round((g + m) * 255).toString(16).padStart(2, '0');
|
||||
const bHex = Math.round((b + m) * 255).toString(16).padStart(2, '0');
|
||||
|
||||
return `#${rHex}${gHex}${bHex}`.toUpperCase();
|
||||
}
|
||||
|
||||
// Light theme colors: hsl(h, 100%, l)
|
||||
const LIGHT_COLORS = [
|
||||
{ h: 208, s: 100, l: 45 }, // --mx-uc-1
|
||||
{ h: 302, s: 100, l: 30 }, // --mx-uc-2
|
||||
{ h: 163, s: 100, l: 30 }, // --mx-uc-3
|
||||
{ h: 343, s: 100, l: 45 }, // --mx-uc-4
|
||||
{ h: 24, s: 100, l: 45 }, // --mx-uc-5
|
||||
{ h: 181, s: 100, l: 30 }, // --mx-uc-6
|
||||
{ h: 242, s: 100, l: 45 }, // --mx-uc-7
|
||||
{ h: 94, s: 100, l: 35 }, // --mx-uc-8
|
||||
];
|
||||
|
||||
// Dark theme colors: hsl(h, 100%, l)
|
||||
const DARK_COLORS = [
|
||||
{ h: 208, s: 100, l: 75 }, // --mx-uc-1
|
||||
{ h: 301, s: 100, l: 80 }, // --mx-uc-2
|
||||
{ h: 163, s: 100, l: 70 }, // --mx-uc-3
|
||||
{ h: 343, s: 100, l: 75 }, // --mx-uc-4
|
||||
{ h: 24, s: 100, l: 70 }, // --mx-uc-5
|
||||
{ h: 181, s: 100, l: 60 }, // --mx-uc-6
|
||||
{ h: 243, s: 100, l: 80 }, // --mx-uc-7
|
||||
{ h: 94, s: 100, l: 80 }, // --mx-uc-8
|
||||
];
|
||||
|
||||
function hashCode(str) {
|
||||
let hash = 0;
|
||||
let i;
|
||||
@@ -22,6 +86,33 @@ export function cssColorMXID(userId) {
|
||||
return `--mx-uc-${colorNumber + 1}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual hex color value for a user ID
|
||||
* @param {string} userId - Matrix user ID
|
||||
* @param {boolean} isDark - Whether to use dark theme colors (default: auto-detect)
|
||||
* @returns {string} Hex color string
|
||||
*/
|
||||
export function getColorMXIDValue(userId, isDark) {
|
||||
const colorNumber = hashCode(userId) % 8;
|
||||
|
||||
// Auto-detect theme if not specified
|
||||
let useDark = isDark;
|
||||
if (typeof isDark === 'undefined') {
|
||||
const bodyClass = document.body.className;
|
||||
useDark = bodyClass.includes('dark-theme') ||
|
||||
bodyClass.includes('butter-theme') ||
|
||||
bodyClass.includes('discord-theme') ||
|
||||
bodyClass.includes('discord-darker-theme') ||
|
||||
bodyClass.includes('twilight-theme') ||
|
||||
bodyClass.includes('mocha-theme');
|
||||
}
|
||||
|
||||
const colorSet = useDark ? DARK_COLORS : LIGHT_COLORS;
|
||||
const color = colorSet[colorNumber];
|
||||
|
||||
return hslToHex(color.h, color.s, color.l);
|
||||
}
|
||||
|
||||
export default function colorMXID(userId) {
|
||||
return `var(${cssColorMXID(userId)})`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user