Change username color in chat with power level color (#2282)

* add active theme context

* add chroma js library

* add hook for accessible tag color

* disable reply user color - temporary

* render user color based on tag in room timeline

* remove default tag icons

* move accessible color function to plugins

* render user power color in reply

* increase username weight in timeline

* add default color for member power level tag

* show red slash in power color badge with no color

* show power level color in room input reply

* show power level username color in notifications

* show power level color in notification reply

* show power level color in message search

* render power level color in room pin menu

* add toggle for legacy username colors

* drop over saturation from member default color

* change border color of power color badge

* show legacy username color in direct rooms
This commit is contained in:
Ajay Bura 2025-03-23 22:09:29 +11:00 committed by GitHub
parent 7d54eef95b
commit 08e975cd8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 463 additions and 91 deletions

View file

@ -2,7 +2,6 @@ import { Box, Icon, Icons, Text, as, color, toRem } from 'folds';
import { EventTimelineSet, Room } from 'matrix-js-sdk';
import React, { MouseEventHandler, ReactNode, useCallback, useMemo } from 'react';
import classNames from 'classnames';
import colorMXID from '../../../util/colorMXID';
import { getMemberDisplayName, trimReplyFromBody } from '../../utils/room';
import { getMxIdLocalPart } from '../../utils/matrix';
import { LinePlaceholder } from './placeholder';
@ -11,6 +10,8 @@ import * as css from './Reply.css';
import { MessageBadEncryptedContent, MessageDeletedContent, MessageFailedContent } from './content';
import { scaleSystemEmoji } from '../../plugins/react-custom-html-parser';
import { useRoomEvent } from '../../hooks/useRoomEvent';
import { GetPowerLevelTag } from '../../hooks/usePowerLevelTags';
import colorMXID from '../../../util/colorMXID';
type ReplyLayoutProps = {
userColor?: string;
@ -49,10 +50,28 @@ type ReplyProps = {
replyEventId: string;
threadRootId?: string | undefined;
onClick?: MouseEventHandler | undefined;
getPowerLevel?: (userId: string) => number;
getPowerLevelTag?: GetPowerLevelTag;
accessibleTagColors?: Map<string, string>;
legacyUsernameColor?: boolean;
};
export const Reply = as<'div', ReplyProps>(
({ room, timelineSet, replyEventId, threadRootId, onClick, ...props }, ref) => {
(
{
room,
timelineSet,
replyEventId,
threadRootId,
onClick,
getPowerLevel,
getPowerLevelTag,
accessibleTagColors,
legacyUsernameColor,
...props
},
ref
) => {
const placeholderWidth = useMemo(() => randomNumberBetween(40, 400), []);
const getFromLocalTimeline = useCallback(
() => timelineSet?.findEventById(replyEventId),
@ -62,6 +81,11 @@ export const Reply = as<'div', ReplyProps>(
const { body } = replyEvent?.getContent() ?? {};
const sender = replyEvent?.getSender();
const senderPL = sender && getPowerLevel?.(sender);
const powerTag = typeof senderPL === 'number' ? getPowerLevelTag?.(senderPL) : undefined;
const tagColor = powerTag?.color ? accessibleTagColors?.get(powerTag.color) : undefined;
const usernameColor = legacyUsernameColor ? colorMXID(sender ?? replyEventId) : tagColor;
const fallbackBody = replyEvent?.isRedacted() ? (
<MessageDeletedContent />
@ -79,7 +103,7 @@ export const Reply = as<'div', ReplyProps>(
)}
<ReplyLayout
as="button"
userColor={sender ? colorMXID(sender) : undefined}
userColor={usernameColor}
username={
sender && (
<Text size="T300" truncate>

View file

@ -24,6 +24,10 @@ export const Username = as<'span'>(({ as: AsUsername = 'span', className, ...pro
<AsUsername className={classNames(css.Username, className)} {...props} ref={ref} />
));
export const UsernameBold = as<'b'>(({ as: AsUsernameBold = 'b', className, ...props }, ref) => (
<AsUsernameBold className={classNames(css.UsernameBold, className)} {...props} ref={ref} />
));
export const MessageTextBody = as<'div', css.MessageTextBodyVariants & { notice?: boolean }>(
({ as: asComp = 'div', className, preWrap, jumboEmoji, emote, notice, ...props }, ref) => (
<Text

View file

@ -157,6 +157,10 @@ export const Username = style({
},
});
export const UsernameBold = style({
fontWeight: 550,
});
export const MessageTextBody = recipe({
base: {
wordBreak: 'break-word',

View file

@ -9,7 +9,7 @@ type PowerColorBadgeProps = {
export const PowerColorBadge = as<'span', PowerColorBadgeProps>(
({ as: AsPowerColorBadge = 'span', color, className, style, ...props }, ref) => (
<AsPowerColorBadge
className={classNames(css.PowerColorBadge, className)}
className={classNames(css.PowerColorBadge, { [css.PowerColorBadgeNone]: !color }, className)}
style={{
backgroundColor: color,
...style,

View file

@ -3,13 +3,30 @@ import { recipe, RecipeVariants } from '@vanilla-extract/recipes';
import { color, config, DefaultReset, toRem } from 'folds';
export const PowerColorBadge = style({
display: 'inline-block',
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
width: toRem(16),
height: toRem(16),
backgroundColor: color.Surface.OnContainer,
borderRadius: config.radii.Pill,
border: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
border: `${config.borderWidth.B300} solid ${color.Secondary.ContainerLine}`,
position: 'relative',
});
export const PowerColorBadgeNone = style({
selectors: {
'&::before': {
content: '',
display: 'inline-block',
width: '100%',
height: config.borderWidth.B300,
backgroundColor: color.Critical.Main,
position: 'absolute',
transform: `rotateZ(-45deg)`,
},
},
});
const PowerIconSize = createVar();