feat: Add detailed logging for user profile components and image metadata extraction; enhance color handling utilities

This commit is contained in:
2026-04-30 20:23:44 +10:00
parent cad4852878
commit 5d4d518af8
9 changed files with 299 additions and 48 deletions

View File

@@ -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">

View File

@@ -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">

View File

@@ -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)}`;

View File

@@ -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 = {};

View File

@@ -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() });

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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)})`;
}