From 5d4d518af87d6551e2bbfd0470bc0f2bea5a54cc Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Thu, 30 Apr 2026 20:23:44 +1000 Subject: [PATCH] feat: Add detailed logging for user profile components and image metadata extraction; enhance color handling utilities --- src/app/components/user-profile/UserHero.tsx | 14 ++ .../user-profile/UserRoomProfile.tsx | 24 +++- src/app/features/settings/account/Profile.tsx | 4 +- src/app/hooks/useUserBanner.ts | 32 ++++- src/app/hooks/useUserColor.ts | 16 ++- src/app/hooks/useUserProfileStyle.ts | 11 ++ src/app/utils/imageMetadata.ts | 135 +++++++++++++----- src/app/utils/pngMetadata.ts | 20 ++- src/util/colorMXID.js | 91 ++++++++++++ 9 files changed, 299 insertions(+), 48 deletions(-) diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 62fbc1a..f193ad7 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -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 ( diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 4384a4e..ac27019 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -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 ( diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index 829e012..bca8c93 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -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 { + 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 = {}; diff --git a/src/app/hooks/useUserColor.ts b/src/app/hooks/useUserColor.ts index 1da70e8..083c515 100644 --- a/src/app/hooks/useUserColor.ts +++ b/src/app/hooks/useUserColor.ts @@ -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(); + + // 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(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() }); diff --git a/src/app/hooks/useUserProfileStyle.ts b/src/app/hooks/useUserProfileStyle.ts index ea49679..e82249e 100644 --- a/src/app/hooks/useUserProfileStyle.ts +++ b/src/app/hooks/useUserProfileStyle.ts @@ -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); diff --git a/src/app/utils/imageMetadata.ts b/src/app/utils/imageMetadata.ts index 1823291..9611c7f 100644 --- a/src/app/utils/imageMetadata.ts +++ b/src/app/utils/imageMetadata.ts @@ -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(100); const METADATA_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes +// In-flight fetch tracking to prevent duplicate parallel requests +const inflightColorFetches = new Map>(); +const inflightMetadataFetches = new Map>(); + /** * 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; } diff --git a/src/app/utils/pngMetadata.ts b/src/app/utils/pngMetadata.ts index 39310d9..32a9f4e 100644 --- a/src/app/utils/pngMetadata.ts +++ b/src/app/utils/pngMetadata.ts @@ -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; } diff --git a/src/util/colorMXID.js b/src/util/colorMXID.js index 95600d2..710e54a 100644 --- a/src/util/colorMXID.js +++ b/src/util/colorMXID.js @@ -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)})`; }