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 bannerUrl = useOtherUserBanner(userId, avatarMxc); // Now returns blob URL directly
|
||||||
const profileStyle = useOtherUserProfileStyle(userId, avatarMxc);
|
const profileStyle = useOtherUserProfileStyle(userId, avatarMxc);
|
||||||
|
|
||||||
|
console.log('[UserHero]', {
|
||||||
|
userId,
|
||||||
|
avatarMxc,
|
||||||
|
bannerUrl,
|
||||||
|
profileStyle,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
direction="Column"
|
direction="Column"
|
||||||
@@ -125,6 +132,13 @@ export function UserHeroName({ displayName, userId, avatarMxc }: UserHeroNamePro
|
|||||||
const username = getMxIdLocalPart(userId);
|
const username = getMxIdLocalPart(userId);
|
||||||
const userColor = useOtherUserColor(userId, avatarMxc);
|
const userColor = useOtherUserColor(userId, avatarMxc);
|
||||||
|
|
||||||
|
console.log('[UserHeroName]', {
|
||||||
|
userId,
|
||||||
|
avatarMxc,
|
||||||
|
userColor,
|
||||||
|
fallbackColor: colorMXID(userId),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box grow="Yes" direction="Column" gap="100">
|
<Box grow="Yes" direction="Column" gap="100">
|
||||||
<Box alignItems="Baseline" gap="200" wrap="Wrap">
|
<Box alignItems="Baseline" gap="200" wrap="Wrap">
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import { DirectCreateSearchParams } from '../../pages/paths';
|
|||||||
import { getContrastingTextColor, stripAlphaFromColor, getTextShadowColor } from '../../utils/common';
|
import { getContrastingTextColor, stripAlphaFromColor, getTextShadowColor } from '../../utils/common';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { BreakWord, LineClamp3 } from '../../styles/Text.css';
|
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 = {
|
type UserRoomProfileProps = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@@ -64,6 +65,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||||||
|
|
||||||
const presence = useUserPresence(userId);
|
const presence = useUserPresence(userId);
|
||||||
const profileStyle = useOtherUserProfileStyle(userId, avatarMxc);
|
const profileStyle = useOtherUserProfileStyle(userId, avatarMxc);
|
||||||
|
const userColor = useOtherUserColor(userId, avatarMxc);
|
||||||
|
|
||||||
// Build gradient CSS if configured
|
// Build gradient CSS if configured
|
||||||
const gradientStyle = profileStyle.gradient
|
const gradientStyle = profileStyle.gradient
|
||||||
@@ -87,9 +89,25 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||||||
(canKickUser && membership === Membership.Join) ||
|
(canKickUser && membership === Membership.Join) ||
|
||||||
(canBanUser && membership !== Membership.Ban);
|
(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 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 (
|
return (
|
||||||
<Box direction="Column">
|
<Box direction="Column">
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import { useUserProfileStyle } from '../../../hooks/useUserProfileStyle';
|
|||||||
import { useUserPresence } from '../../../hooks/useUserPresence';
|
import { useUserPresence } from '../../../hooks/useUserPresence';
|
||||||
import { AvatarPresence, PresenceBadge } from '../../../components/presence';
|
import { AvatarPresence, PresenceBadge } from '../../../components/presence';
|
||||||
import { BreakWord, LineClamp3 } from '../../../styles/Text.css';
|
import { BreakWord, LineClamp3 } from '../../../styles/Text.css';
|
||||||
import colorMXID from '../../../../util/colorMXID';
|
import colorMXID, { getColorMXIDValue } from '../../../../util/colorMXID';
|
||||||
import {
|
import {
|
||||||
extractMetadataFromImage,
|
extractMetadataFromImage,
|
||||||
embedMetadataInImage,
|
embedMetadataInImage,
|
||||||
@@ -574,7 +574,7 @@ function ProfileBanner({ avatarRef, nameRef }: { avatarRef: React.RefObject<HTML
|
|||||||
: profileStyle.avatarBorderColor;
|
: profileStyle.avatarBorderColor;
|
||||||
|
|
||||||
const previewPillBgColor = previewBorderColor ? stripAlphaFromColor(previewBorderColor) : undefined;
|
const previewPillBgColor = previewBorderColor ? stripAlphaFromColor(previewBorderColor) : undefined;
|
||||||
const previewProfileColor = userColor || colorMXID(userId);
|
const previewProfileColor = userColor || getColorMXIDValue(userId);
|
||||||
|
|
||||||
const previewTextShadow = `0 1px 4px ${getTextShadowColor(previewProfileColor)}`;
|
const previewTextShadow = `0 1px 4px ${getTextShadowColor(previewProfileColor)}`;
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export function useUserBanner(): [
|
|||||||
|
|
||||||
const profile = await mx.getProfileInfo(userId);
|
const profile = await mx.getProfileInfo(userId);
|
||||||
const avatarUrl = profile.avatar_url;
|
const avatarUrl = profile.avatar_url;
|
||||||
|
console.log('[useUserBanner.loadBanner] Loading banner from avatar:', avatarUrl);
|
||||||
|
|
||||||
if (!avatarUrl) {
|
if (!avatarUrl) {
|
||||||
setBanner(undefined);
|
setBanner(undefined);
|
||||||
@@ -90,6 +91,7 @@ export function useUserBanner(): [
|
|||||||
// Always use current session's token to avoid stale tokens during account switches
|
// Always use current session's token to avoid stale tokens during account switches
|
||||||
const accessToken = getCurrentAccessToken();
|
const accessToken = getCurrentAccessToken();
|
||||||
const metadata = await fetchAndExtractMetadata(httpUrl, useAuthentication ? accessToken : null);
|
const metadata = await fetchAndExtractMetadata(httpUrl, useAuthentication ? accessToken : null);
|
||||||
|
console.log('[useUserBanner.loadBanner] Extracted banner:', metadata.banner);
|
||||||
setBanner(metadata.banner);
|
setBanner(metadata.banner);
|
||||||
} catch {
|
} catch {
|
||||||
setBanner(undefined);
|
setBanner(undefined);
|
||||||
@@ -104,7 +106,8 @@ export function useUserBanner(): [
|
|||||||
if (!userId) return undefined;
|
if (!userId) return undefined;
|
||||||
|
|
||||||
const user = mx.getUser(userId);
|
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();
|
loadBanner();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -143,12 +146,16 @@ export function useUserBanner(): [
|
|||||||
|
|
||||||
// Get existing metadata to preserve
|
// Get existing metadata to preserve
|
||||||
const existingMetadata = extractMetadataFromImage(avatarData);
|
const existingMetadata = extractMetadataFromImage(avatarData);
|
||||||
|
console.log('[updateBanner] Existing metadata:', existingMetadata);
|
||||||
|
|
||||||
// Modify image metadata with new banner, preserving color
|
// Modify image metadata with new banner, preserving color
|
||||||
const newMetadata: ImageMetadata = {
|
const newMetadata: ImageMetadata = {
|
||||||
color: existingMetadata.color,
|
color: existingMetadata.color,
|
||||||
banner: newBanner,
|
banner: newBanner,
|
||||||
|
avatarBorderColor: existingMetadata.avatarBorderColor,
|
||||||
|
gradient: existingMetadata.gradient,
|
||||||
};
|
};
|
||||||
|
console.log('[updateBanner] New metadata to embed:', newMetadata);
|
||||||
|
|
||||||
const newAvatarData = embedMetadataInImage(avatarData, newMetadata);
|
const newAvatarData = embedMetadataInImage(avatarData, newMetadata);
|
||||||
|
|
||||||
@@ -158,10 +165,13 @@ export function useUserBanner(): [
|
|||||||
|
|
||||||
// Verify the banner was embedded correctly before uploading
|
// Verify the banner was embedded correctly before uploading
|
||||||
const verifyMetadata = extractMetadataFromImage(newAvatarData);
|
const verifyMetadata = extractMetadataFromImage(newAvatarData);
|
||||||
|
console.log('[updateBanner] Verification - extracted metadata from new avatar:', verifyMetadata);
|
||||||
|
|
||||||
if (newBanner && verifyMetadata.banner !== newBanner) {
|
if (newBanner && verifyMetadata.banner !== newBanner) {
|
||||||
|
console.error('[updateBanner] Banner verification FAILED!', 'Expected:', newBanner, 'Got:', verifyMetadata.banner);
|
||||||
throw new Error('Banner verification failed');
|
throw new Error('Banner verification failed');
|
||||||
}
|
}
|
||||||
|
console.log('[updateBanner] Banner verification passed');
|
||||||
|
|
||||||
// Upload modified avatar with correct MIME type
|
// Upload modified avatar with correct MIME type
|
||||||
const mimeType = getMimeType(format);
|
const mimeType = getMimeType(format);
|
||||||
@@ -172,21 +182,26 @@ export function useUserBanner(): [
|
|||||||
name: `avatar.${extension}`,
|
name: `avatar.${extension}`,
|
||||||
type: mimeType,
|
type: mimeType,
|
||||||
});
|
});
|
||||||
|
console.log('[updateBanner] Avatar uploaded successfully, MXC:', uploadResponse.content_uri);
|
||||||
|
|
||||||
// Update profile with new avatar
|
// Update profile with new avatar
|
||||||
try {
|
try {
|
||||||
|
console.log('[updateBanner] Setting avatar URL to:', uploadResponse.content_uri);
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
mx.setAvatarUrl(uploadResponse.content_uri),
|
mx.setAvatarUrl(uploadResponse.content_uri),
|
||||||
new Promise((_, reject) => {
|
new Promise((_, reject) => {
|
||||||
setTimeout(() => reject(new Error('setAvatarUrl timeout')), 30000);
|
setTimeout(() => reject(new Error('setAvatarUrl timeout')), 30000);
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
|
console.log('[updateBanner] setAvatarUrl completed');
|
||||||
|
|
||||||
// Manually sync user object to ensure event listeners are triggered
|
// Manually sync user object to ensure event listeners are triggered
|
||||||
const user = mx.getUser(userId);
|
const user = mx.getUser(userId);
|
||||||
if (user && user.avatarUrl !== uploadResponse.content_uri) {
|
if (user && user.avatarUrl !== uploadResponse.content_uri) {
|
||||||
|
console.log('[updateBanner] Manually syncing user avatar URL');
|
||||||
user.setAvatarUrl(uploadResponse.content_uri);
|
user.setAvatarUrl(uploadResponse.content_uri);
|
||||||
}
|
}
|
||||||
|
console.log('[updateBanner] Banner update complete');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to update avatar URL: ${error instanceof Error ? error.message : 'Unknown 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;
|
let blobUrl: string | undefined;
|
||||||
|
|
||||||
if (!avatarMxc) {
|
if (!avatarMxc) {
|
||||||
|
console.log('[useOtherUserBanner] No avatarMxc provided for', userId);
|
||||||
setBannerBlobUrl(undefined);
|
setBannerBlobUrl(undefined);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadBanner = async () => {
|
const loadBanner = async () => {
|
||||||
|
console.log('[useOtherUserBanner] Loading banner for', userId, 'avatarMxc:', avatarMxc);
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const cacheKey = `${userId}:${avatarMxc}`;
|
const cacheKey = `${userId}:${avatarMxc}`;
|
||||||
const cached = userBannerCache.get(cacheKey);
|
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) {
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
||||||
// Use cached banner MXC
|
// Use cached banner MXC
|
||||||
|
console.log('[useOtherUserBanner] Using cached bannerMxc:', cached.bannerMxc);
|
||||||
bannerMxc = cached.bannerMxc;
|
bannerMxc = cached.bannerMxc;
|
||||||
} else {
|
} else {
|
||||||
// Fetch banner MXC from avatar metadata
|
// Fetch banner MXC from avatar metadata
|
||||||
const httpUrl = mxcUrlToHttp(mx, avatarMxc, useAuthentication);
|
const httpUrl = mxcUrlToHttp(mx, avatarMxc, useAuthentication);
|
||||||
|
console.log('[useOtherUserBanner] Fetching metadata from:', httpUrl);
|
||||||
|
|
||||||
if (!httpUrl) {
|
if (!httpUrl) {
|
||||||
|
console.log('[useOtherUserBanner] Failed to get HTTP URL');
|
||||||
setBannerBlobUrl(undefined);
|
setBannerBlobUrl(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -243,6 +265,8 @@ export function useOtherUserBanner(userId: string, avatarMxc: string | undefined
|
|||||||
const accessToken = getCurrentAccessToken();
|
const accessToken = getCurrentAccessToken();
|
||||||
const metadata = await fetchAndExtractMetadata(httpUrl, useAuthentication ? accessToken : null);
|
const metadata = await fetchAndExtractMetadata(httpUrl, useAuthentication ? accessToken : null);
|
||||||
|
|
||||||
|
console.log('[useOtherUserBanner] Extracted metadata:', metadata);
|
||||||
|
|
||||||
bannerMxc = metadata.banner;
|
bannerMxc = metadata.banner;
|
||||||
|
|
||||||
// Cache the banner MXC (not the blob URL)
|
// Cache the banner MXC (not the blob URL)
|
||||||
@@ -250,17 +274,23 @@ export function useOtherUserBanner(userId: string, avatarMxc: string | undefined
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!bannerMxc) {
|
if (!bannerMxc) {
|
||||||
|
console.log('[useOtherUserBanner] No banner MXC found');
|
||||||
setBannerBlobUrl(undefined);
|
setBannerBlobUrl(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[useOtherUserBanner] Fetching banner image from MXC:', bannerMxc);
|
||||||
|
|
||||||
// Fetch the banner image data with authentication
|
// Fetch the banner image data with authentication
|
||||||
const bannerHttpUrl = mxcUrlToHttp(mx, bannerMxc, useAuthentication);
|
const bannerHttpUrl = mxcUrlToHttp(mx, bannerMxc, useAuthentication);
|
||||||
if (!bannerHttpUrl) {
|
if (!bannerHttpUrl) {
|
||||||
|
console.log('[useOtherUserBanner] Failed to convert banner MXC to HTTP URL');
|
||||||
setBannerBlobUrl(undefined);
|
setBannerBlobUrl(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[useOtherUserBanner] Banner HTTP URL:', bannerHttpUrl);
|
||||||
|
|
||||||
// Always use current session's token to avoid stale tokens during account switches
|
// Always use current session's token to avoid stale tokens during account switches
|
||||||
const accessToken = getCurrentAccessToken();
|
const accessToken = getCurrentAccessToken();
|
||||||
const headers: HeadersInit = {};
|
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 {
|
export function useOtherUserColor(userId: string, avatarMxc: string | undefined): string | undefined {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
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(() => {
|
useEffect(() => {
|
||||||
if (!avatarMxc) {
|
if (!avatarMxc) {
|
||||||
|
console.log('[useOtherUserColor] No avatarMxc provided for', userId);
|
||||||
setColor(undefined);
|
setColor(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -272,13 +281,16 @@ export function useOtherUserColor(userId: string, avatarMxc: string | undefined)
|
|||||||
const cacheKey = `${userId}:${avatarMxc}`;
|
const cacheKey = `${userId}:${avatarMxc}`;
|
||||||
const cached = userColorCache.get(cacheKey);
|
const cached = userColorCache.get(cacheKey);
|
||||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
||||||
|
console.log('[useOtherUserColor] Using cached color for', userId, ':', cached.color);
|
||||||
setColor(cached.color);
|
setColor(cached.color);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadColor = async () => {
|
const loadColor = async () => {
|
||||||
const httpUrl = mxcUrlToHttp(mx, avatarMxc, useAuthentication);
|
const httpUrl = mxcUrlToHttp(mx, avatarMxc, useAuthentication);
|
||||||
|
console.log('[useOtherUserColor] Loading color for', userId, 'from', httpUrl);
|
||||||
if (!httpUrl) {
|
if (!httpUrl) {
|
||||||
|
console.log('[useOtherUserColor] Failed to get HTTP URL for', avatarMxc);
|
||||||
setColor(undefined);
|
setColor(undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -287,6 +299,8 @@ export function useOtherUserColor(userId: string, avatarMxc: string | undefined)
|
|||||||
const accessToken = getCurrentAccessToken();
|
const accessToken = getCurrentAccessToken();
|
||||||
const extractedColor = await fetchAndExtractColor(httpUrl, useAuthentication ? accessToken : null);
|
const extractedColor = await fetchAndExtractColor(httpUrl, useAuthentication ? accessToken : null);
|
||||||
|
|
||||||
|
console.log('[useOtherUserColor] Extracted color for', userId, ':', extractedColor);
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
userColorCache.set(cacheKey, { color: extractedColor, timestamp: Date.now() });
|
userColorCache.set(cacheKey, { color: extractedColor, timestamp: Date.now() });
|
||||||
|
|
||||||
|
|||||||
@@ -212,22 +212,29 @@ export function useOtherUserProfileStyle(userId: string, avatarMxc: string | und
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!avatarMxc) {
|
if (!avatarMxc) {
|
||||||
|
console.log('[useOtherUserProfileStyle] No avatarMxc provided for', userId);
|
||||||
setStyle({});
|
setStyle({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadStyle = async () => {
|
const loadStyle = async () => {
|
||||||
|
console.log('[useOtherUserProfileStyle] Loading style for', userId, 'avatarMxc:', avatarMxc);
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
const cacheKey = `${userId}:${avatarMxc}`;
|
const cacheKey = `${userId}:${avatarMxc}`;
|
||||||
const cached = userStyleCache.get(cacheKey);
|
const cached = userStyleCache.get(cacheKey);
|
||||||
|
|
||||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
||||||
|
console.log('[useOtherUserProfileStyle] Using cached style:', cached.style);
|
||||||
setStyle(cached.style);
|
setStyle(cached.style);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const httpUrl = mxcUrlToHttp(mx, avatarMxc, useAuthentication);
|
const httpUrl = mxcUrlToHttp(mx, avatarMxc, useAuthentication);
|
||||||
|
console.log('[useOtherUserProfileStyle] Fetching metadata from:', httpUrl);
|
||||||
|
|
||||||
if (!httpUrl) {
|
if (!httpUrl) {
|
||||||
|
console.log('[useOtherUserProfileStyle] Failed to get HTTP URL');
|
||||||
setStyle({});
|
setStyle({});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -236,11 +243,15 @@ export function useOtherUserProfileStyle(userId: string, avatarMxc: string | und
|
|||||||
const accessToken = getCurrentAccessToken();
|
const accessToken = getCurrentAccessToken();
|
||||||
const metadata = await fetchAndExtractMetadata(httpUrl, useAuthentication ? accessToken : null);
|
const metadata = await fetchAndExtractMetadata(httpUrl, useAuthentication ? accessToken : null);
|
||||||
|
|
||||||
|
console.log('[useOtherUserProfileStyle] Extracted metadata:', metadata);
|
||||||
|
|
||||||
const styleData: ProfileStyleData = {
|
const styleData: ProfileStyleData = {
|
||||||
avatarBorderColor: metadata.avatarBorderColor,
|
avatarBorderColor: metadata.avatarBorderColor,
|
||||||
gradient: metadata.gradient,
|
gradient: metadata.gradient,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[useOtherUserProfileStyle] Final styleData:', styleData);
|
||||||
|
|
||||||
// Cache the result
|
// Cache the result
|
||||||
userStyleCache.set(cacheKey, { style: styleData, timestamp: Date.now() });
|
userStyleCache.set(cacheKey, { style: styleData, timestamp: Date.now() });
|
||||||
setStyle(styleData);
|
setStyle(styleData);
|
||||||
|
|||||||
@@ -170,6 +170,8 @@ export function detectImageFormat(data: ArrayBuffer | Uint8Array): ImageFormat {
|
|||||||
export function extractColorFromImage(imageData: ArrayBuffer | Uint8Array): string | undefined {
|
export function extractColorFromImage(imageData: ArrayBuffer | Uint8Array): string | undefined {
|
||||||
const format = detectImageFormat(imageData);
|
const format = detectImageFormat(imageData);
|
||||||
|
|
||||||
|
console.log('[extractColorFromImage] Detected format:', format);
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case 'png':
|
case 'png':
|
||||||
return extractColorFromPNG(imageData);
|
return extractColorFromPNG(imageData);
|
||||||
@@ -211,14 +213,19 @@ export function extractBannerFromImage(imageData: ArrayBuffer | Uint8Array): str
|
|||||||
export function extractMetadataFromImage(imageData: ArrayBuffer | Uint8Array): ImageMetadata {
|
export function extractMetadataFromImage(imageData: ArrayBuffer | Uint8Array): ImageMetadata {
|
||||||
const format = detectImageFormat(imageData);
|
const format = detectImageFormat(imageData);
|
||||||
|
|
||||||
|
console.log('[extractMetadataFromImage] Detected format:', format);
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case 'png':
|
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
|
// For other formats, extract color only for now
|
||||||
case 'webp':
|
case 'webp':
|
||||||
case 'jpeg':
|
case 'jpeg':
|
||||||
case 'gif': {
|
case 'gif': {
|
||||||
const color = extractColorFromImage(imageData);
|
const color = extractColorFromImage(imageData);
|
||||||
|
console.log('[extractMetadataFromImage] Non-PNG color:', color);
|
||||||
return color ? { color } : {};
|
return color ? { color } : {};
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -344,6 +351,10 @@ export function getExtension(format: ImageFormat): string {
|
|||||||
const avatarMetadataCache = new LRUCache<string, { metadata: ImageMetadata; timestamp: number }>(100);
|
const avatarMetadataCache = new LRUCache<string, { metadata: ImageMetadata; timestamp: number }>(100);
|
||||||
const METADATA_CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
|
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)
|
* Fetch and extract color from an image URL (supports all formats)
|
||||||
* @param url - HTTP URL to fetch the image from
|
* @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
|
// Check if full metadata is cached
|
||||||
const cached = avatarMetadataCache.get(url);
|
const cached = avatarMetadataCache.get(url);
|
||||||
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
|
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;
|
return cached.metadata.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Check if fetch already in progress
|
||||||
const response = await fetch(url, {
|
const inflightKey = `${url}:${accessToken || ''}`;
|
||||||
method: 'GET',
|
const existingFetch = inflightColorFetches.get(inflightKey);
|
||||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
if (existingFetch) {
|
||||||
});
|
console.log('[fetchAndExtractColor] Reusing in-flight fetch for', url);
|
||||||
if (!response.ok) return undefined;
|
return existingFetch;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Check cache first
|
||||||
const cached = avatarMetadataCache.get(url);
|
const cached = avatarMetadataCache.get(url);
|
||||||
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
|
if (cached && Date.now() - cached.timestamp < METADATA_CACHE_TTL_MS) {
|
||||||
|
console.log('[fetchAndExtractMetadata] Using cached metadata for', url, ':', cached.metadata);
|
||||||
return cached.metadata;
|
return cached.metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Check if fetch already in progress
|
||||||
const response = await fetch(url, {
|
const inflightKey = `${url}:${accessToken || ''}`;
|
||||||
method: 'GET',
|
const existingFetch = inflightMetadataFetches.get(inflightKey);
|
||||||
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
|
if (existingFetch) {
|
||||||
});
|
console.log('[fetchAndExtractMetadata] Reusing in-flight fetch for', url);
|
||||||
if (!response.ok) return {};
|
return existingFetch;
|
||||||
|
|
||||||
const data = await response.arrayBuffer();
|
|
||||||
const metadata = extractMetadataFromImage(data);
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
avatarMetadataCache.set(url, {
|
|
||||||
metadata,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
return metadata;
|
|
||||||
} catch {
|
|
||||||
return {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// Verify PNG signature
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
if (data[i] !== PNG_SIGNATURE[i]) {
|
if (data[i] !== PNG_SIGNATURE[i]) {
|
||||||
|
console.warn('[extractColorFromPNG] Not a valid PNG');
|
||||||
return undefined; // Not a PNG
|
return undefined; // Not a PNG
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunks = parseChunks(data);
|
const chunks = parseChunks(data);
|
||||||
|
console.log('[extractColorFromPNG] Found', chunks.length, 'chunks');
|
||||||
|
|
||||||
|
let textChunksFound = 0;
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
if (chunk.type === 'tEXt') {
|
if (chunk.type === 'tEXt') {
|
||||||
|
textChunksFound++;
|
||||||
const text = parseTextChunk(chunk.data);
|
const text = parseTextChunk(chunk.data);
|
||||||
if (text && text.key === PAARROT_COLOR_KEY) {
|
if (text) {
|
||||||
return text.value;
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,16 +223,21 @@ export function extractMetadataFromPNG(imageData: ArrayBuffer | Uint8Array): Ima
|
|||||||
// Verify PNG signature
|
// Verify PNG signature
|
||||||
for (let i = 0; i < 8; i++) {
|
for (let i = 0; i < 8; i++) {
|
||||||
if (data[i] !== PNG_SIGNATURE[i]) {
|
if (data[i] !== PNG_SIGNATURE[i]) {
|
||||||
|
console.warn('[extractMetadataFromPNG] Not a valid PNG');
|
||||||
return metadata; // Not a PNG
|
return metadata; // Not a PNG
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const chunks = parseChunks(data);
|
const chunks = parseChunks(data);
|
||||||
|
console.log('[extractMetadataFromPNG] Found', chunks.length, 'chunks');
|
||||||
|
|
||||||
|
let textChunksFound = 0;
|
||||||
for (const chunk of chunks) {
|
for (const chunk of chunks) {
|
||||||
if (chunk.type === 'tEXt') {
|
if (chunk.type === 'tEXt') {
|
||||||
|
textChunksFound++;
|
||||||
const text = parseTextChunk(chunk.data);
|
const text = parseTextChunk(chunk.data);
|
||||||
if (text) {
|
if (text) {
|
||||||
|
console.log('[extractMetadataFromPNG] Found tEXt chunk:', text.key, '=', text.value);
|
||||||
if (text.key === PAARROT_COLOR_KEY) {
|
if (text.key === PAARROT_COLOR_KEY) {
|
||||||
metadata.color = text.value;
|
metadata.color = text.value;
|
||||||
} else if (text.key === PAARROT_BANNER_KEY) {
|
} 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;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,69 @@
|
|||||||
// https://github.com/cloudrac3r/cadencegq/blob/master/pug/mxid.pug
|
// 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) {
|
function hashCode(str) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
let i;
|
let i;
|
||||||
@@ -22,6 +86,33 @@ export function cssColorMXID(userId) {
|
|||||||
return `--mx-uc-${colorNumber + 1}`;
|
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) {
|
export default function colorMXID(userId) {
|
||||||
return `var(${cssColorMXID(userId)})`;
|
return `var(${cssColorMXID(userId)})`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user