feat: enhance user profile customization with avatar border color and gradient options
- Added support for setting avatar border color and gradient in user profile settings. - Introduced new hooks for managing user profile styles and fetching metadata from avatars. - Implemented an AngleSelector component for selecting gradient direction. - Updated RoomNavItem to display parent space information for direct messages. - Improved caching mechanism for user banners and profile styles. - Refactored image metadata handling to include new profile style attributes.
This commit is contained in:
@@ -18,9 +18,28 @@ import { embedColorInWebP, extractColorFromWebP, removeColorFromWebP } from './w
|
||||
import { embedColorInJPEG, extractColorFromJPEG, removeColorFromJPEG } from './jpegMetadata';
|
||||
import { embedColorInGIF, extractColorFromGIF, removeColorFromGIF } from './gifMetadata';
|
||||
|
||||
/**
|
||||
* Gradient configuration for profile styling
|
||||
* All colors support RGBA format (e.g., #FF550080 for 50% opacity)
|
||||
*/
|
||||
export type ProfileGradient = {
|
||||
/** CSS gradient direction (e.g., "to bottom", "45deg", "to bottom right") */
|
||||
direction: string;
|
||||
/** Start color in hex format with optional alpha (#RRGGBB or #RRGGBBAA) */
|
||||
startColor: string;
|
||||
/** End color in hex format with optional alpha (#RRGGBB or #RRGGBBAA) */
|
||||
stopColor: string;
|
||||
};
|
||||
|
||||
export type ImageMetadata = {
|
||||
/** Profile name color */
|
||||
color?: string;
|
||||
/** Banner MXC URL */
|
||||
banner?: string;
|
||||
/** Avatar border color with optional transparency (#RRGGBB or #RRGGBBAA) */
|
||||
avatarBorderColor?: string;
|
||||
/** Profile card gradient */
|
||||
gradient?: ProfileGradient;
|
||||
};
|
||||
|
||||
/** PNG signature bytes */
|
||||
|
||||
@@ -17,6 +17,12 @@ export const PAARROT_COLOR_KEY = 'paarrot:color';
|
||||
/** Key used to store banner URL in PNG metadata */
|
||||
export const PAARROT_BANNER_KEY = 'paarrot:banner';
|
||||
|
||||
/** Key used to store avatar border color in PNG metadata */
|
||||
export const PAARROT_BORDER_COLOR_KEY = 'paarrot:borderColor';
|
||||
|
||||
/** Key used to store profile gradient (JSON) in PNG metadata */
|
||||
export const PAARROT_GRADIENT_KEY = 'paarrot:gradient';
|
||||
|
||||
let crc32Table: Uint32Array | null = null;
|
||||
function getCRC32Table(): Uint32Array {
|
||||
if (crc32Table) return crc32Table;
|
||||
@@ -194,14 +200,16 @@ export function extractBannerFromPNG(imageData: ArrayBuffer | Uint8Array): strin
|
||||
return undefined;
|
||||
}
|
||||
|
||||
import type { ImageMetadata, ProfileGradient } from './imageMetadata';
|
||||
|
||||
/**
|
||||
* Extract all paarrot metadata from PNG image data
|
||||
* @param imageData - Raw PNG image data as ArrayBuffer or Uint8Array
|
||||
* @returns Object with color and banner if found
|
||||
* @returns Object with all metadata fields if found
|
||||
*/
|
||||
export function extractMetadataFromPNG(imageData: ArrayBuffer | Uint8Array): { color?: string; banner?: string } {
|
||||
export function extractMetadataFromPNG(imageData: ArrayBuffer | Uint8Array): ImageMetadata {
|
||||
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
|
||||
const metadata: { color?: string; banner?: string } = {};
|
||||
const metadata: ImageMetadata = {};
|
||||
|
||||
// Verify PNG signature
|
||||
for (let i = 0; i < 8; i++) {
|
||||
@@ -220,6 +228,14 @@ export function extractMetadataFromPNG(imageData: ArrayBuffer | Uint8Array): { c
|
||||
metadata.color = text.value;
|
||||
} else if (text.key === PAARROT_BANNER_KEY) {
|
||||
metadata.banner = text.value;
|
||||
} else if (text.key === PAARROT_BORDER_COLOR_KEY) {
|
||||
metadata.avatarBorderColor = text.value;
|
||||
} else if (text.key === PAARROT_GRADIENT_KEY) {
|
||||
try {
|
||||
metadata.gradient = JSON.parse(text.value) as ProfileGradient;
|
||||
} catch {
|
||||
// Invalid JSON, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -287,16 +303,19 @@ export function embedColorInPNG(imageData: ArrayBuffer | Uint8Array, color: stri
|
||||
return newData;
|
||||
}
|
||||
|
||||
/** All paarrot metadata keys */
|
||||
const PAARROT_KEYS = [PAARROT_COLOR_KEY, PAARROT_BANNER_KEY, PAARROT_BORDER_COLOR_KEY, PAARROT_GRADIENT_KEY];
|
||||
|
||||
/**
|
||||
* Embed paarrot metadata (color and/or banner) into PNG image data
|
||||
* Embed paarrot metadata into PNG image data
|
||||
* Preserves existing metadata that is not being updated
|
||||
* @param imageData - Raw PNG image data as ArrayBuffer or Uint8Array
|
||||
* @param metadata - Object with color and/or banner to embed
|
||||
* @param metadata - Object with metadata fields to embed
|
||||
* @returns New PNG data with embedded metadata, or null if not a valid PNG
|
||||
*/
|
||||
export function embedMetadataInPNG(
|
||||
imageData: ArrayBuffer | Uint8Array,
|
||||
metadata: { color?: string; banner?: string }
|
||||
metadata: ImageMetadata
|
||||
): Uint8Array | null {
|
||||
const data = imageData instanceof Uint8Array ? imageData : new Uint8Array(imageData);
|
||||
|
||||
@@ -313,9 +332,11 @@ export function embedMetadataInPNG(
|
||||
const existing = extractMetadataFromPNG(data);
|
||||
|
||||
// Merge with new metadata
|
||||
const finalMetadata = {
|
||||
const finalMetadata: ImageMetadata = {
|
||||
color: metadata.color !== undefined ? metadata.color : existing.color,
|
||||
banner: metadata.banner !== undefined ? metadata.banner : existing.banner,
|
||||
avatarBorderColor: metadata.avatarBorderColor !== undefined ? metadata.avatarBorderColor : existing.avatarBorderColor,
|
||||
gradient: metadata.gradient !== undefined ? metadata.gradient : existing.gradient,
|
||||
};
|
||||
|
||||
// Find IHDR chunk and existing paarrot chunks
|
||||
@@ -327,7 +348,7 @@ export function embedMetadataInPNG(
|
||||
insertOffset = chunk.offset + chunk.length;
|
||||
} else if (chunk.type === 'tEXt') {
|
||||
const text = parseTextChunk(chunk.data);
|
||||
if (text && (text.key === PAARROT_COLOR_KEY || text.key === PAARROT_BANNER_KEY)) {
|
||||
if (text && PAARROT_KEYS.includes(text.key)) {
|
||||
existingChunks.push({ key: text.key, offset: chunk.offset, length: chunk.length });
|
||||
}
|
||||
}
|
||||
@@ -341,6 +362,12 @@ export function embedMetadataInPNG(
|
||||
if (finalMetadata.banner) {
|
||||
newChunks.push(createTextChunk(PAARROT_BANNER_KEY, finalMetadata.banner));
|
||||
}
|
||||
if (finalMetadata.avatarBorderColor) {
|
||||
newChunks.push(createTextChunk(PAARROT_BORDER_COLOR_KEY, finalMetadata.avatarBorderColor));
|
||||
}
|
||||
if (finalMetadata.gradient) {
|
||||
newChunks.push(createTextChunk(PAARROT_GRADIENT_KEY, JSON.stringify(finalMetadata.gradient)));
|
||||
}
|
||||
|
||||
// Calculate new size
|
||||
const removedSize = existingChunks.reduce((sum, chunk) => sum + chunk.length, 0);
|
||||
|
||||
Reference in New Issue
Block a user