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

@ -109,7 +109,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
return null;
}}
element={
<>
<AuthRouteThemeManager>
<ClientRoot>
<ClientInitStorageAtom>
<ClientRoomsNotificationPreferences>
@ -132,8 +132,7 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
</ClientRoomsNotificationPreferences>
</ClientInitStorageAtom>
</ClientRoot>
<AuthRouteThemeManager />
</>
</AuthRouteThemeManager>
}
>
<Route

View file

@ -1,8 +1,13 @@
import { useEffect } from 'react';
import React, { ReactNode, useEffect } from 'react';
import { configClass, varsClass } from 'folds';
import { DarkTheme, LightTheme, ThemeKind, useSystemThemeKind, useThemes } from '../hooks/useTheme';
import { useSetting } from '../state/hooks/settings';
import { settingsAtom } from '../state/settings';
import {
DarkTheme,
LightTheme,
ThemeContextProvider,
ThemeKind,
useActiveTheme,
useSystemThemeKind,
} from '../hooks/useTheme';
export function UnAuthRouteThemeManager() {
const systemThemeKind = useSystemThemeKind();
@ -21,38 +26,15 @@ export function UnAuthRouteThemeManager() {
return null;
}
export function AuthRouteThemeManager() {
const systemThemeKind = useSystemThemeKind();
const themes = useThemes();
const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const [themeId] = useSetting(settingsAtom, 'themeId');
const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
export function AuthRouteThemeManager({ children }: { children: ReactNode }) {
const activeTheme = useActiveTheme();
// apply normal theme if system theme is disabled
useEffect(() => {
if (!systemTheme) {
document.body.className = '';
document.body.classList.add(configClass, varsClass);
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
document.body.className = '';
document.body.classList.add(configClass, varsClass);
document.body.classList.add(...selectedTheme.classNames);
}
}, [systemTheme, themes, themeId]);
document.body.classList.add(...activeTheme.classNames);
}, [activeTheme]);
// apply preferred system theme if system theme is enabled
useEffect(() => {
if (systemTheme) {
document.body.className = '';
document.body.classList.add(configClass, varsClass);
const selectedTheme =
systemThemeKind === ThemeKind.Dark
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
document.body.classList.add(...selectedTheme.classNames);
}
}, [systemTheme, systemThemeKind, themes, lightThemeId, darkThemeId]);
return null;
return <ThemeContextProvider value={activeTheme}>{children}</ThemeContextProvider>;
}

View file

@ -1,7 +1,7 @@
import React, { ReactNode } from 'react';
import { useParams } from 'react-router-dom';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import { RoomProvider } from '../../../hooks/useRoom';
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
import { useDirectRooms } from './useDirectRooms';
@ -20,7 +20,7 @@ export function DirectRouteRoomProvider({ children }: { children: ReactNode }) {
return (
<RoomProvider key={room.roomId} value={room}>
{children}
<IsDirectRoomProvider value>{children}</IsDirectRoomProvider>
</RoomProvider>
);
}

View file

@ -1,7 +1,7 @@
import React, { ReactNode } from 'react';
import { useParams } from 'react-router-dom';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import { RoomProvider } from '../../../hooks/useRoom';
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
import { useHomeRooms } from './useHomeRooms';
@ -28,7 +28,7 @@ export function HomeRouteRoomProvider({ children }: { children: ReactNode }) {
return (
<RoomProvider key={room.roomId} value={room}>
{children}
<IsDirectRoomProvider value={false}>{children}</IsDirectRoomProvider>
</RoomProvider>
);
}

View file

@ -53,8 +53,8 @@ import {
Reply,
Time,
Username,
UsernameBold,
} from '../../../components/message';
import colorMXID from '../../../../util/colorMXID';
import {
factoryRenderLinkifyWithMention,
getReactCustomHtmlParser,
@ -84,6 +84,16 @@ import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize';
import { BackRouteHandler } from '../../../components/BackRouteHandler';
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { usePowerLevels, usePowerLevelsAPI } from '../../../hooks/usePowerLevels';
import {
getTagIconSrc,
useAccessibleTagColors,
usePowerLevelTags,
} from '../../../hooks/usePowerLevelTags';
import { useTheme } from '../../../hooks/useTheme';
import { PowerIcon } from '../../../components/power';
import colorMXID from '../../../../util/colorMXID';
import { mDirectAtom } from '../../../state/mDirectList';
type RoomNotificationsGroup = {
roomId: string;
@ -194,6 +204,7 @@ type RoomNotificationsGroupProps = {
urlPreview?: boolean;
hideActivity: boolean;
onOpen: (roomId: string, eventId: string) => void;
legacyUsernameColor?: boolean;
};
function RoomNotificationsGroupComp({
room,
@ -202,10 +213,18 @@ function RoomNotificationsGroupComp({
urlPreview,
hideActivity,
onOpen,
legacyUsernameColor,
}: RoomNotificationsGroupProps) {
const mx = useMatrixClient();
const useAuthentication = useMediaAuthentication();
const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const powerLevels = usePowerLevels(room);
const { getPowerLevel } = usePowerLevelsAPI(powerLevels);
const [powerLevelTags, getPowerLevelTag] = usePowerLevelTags(room, powerLevels);
const theme = useTheme();
const accessibleTagColors = useAccessibleTagColors(theme.kind, powerLevelTags);
const mentionClickHandler = useMentionClickHandler(room.roomId);
const spoilerClickHandler = useSpoilerClickHandler();
@ -424,6 +443,17 @@ function RoomNotificationsGroupComp({
const threadRootId =
relation?.rel_type === RelationType.Thread ? relation.event_id : undefined;
const senderPowerLevel = getPowerLevel(event.sender);
const powerLevelTag = getPowerLevelTag(senderPowerLevel);
const tagColor = powerLevelTag?.color
? accessibleTagColors?.get(powerLevelTag.color)
: undefined;
const tagIconSrc = powerLevelTag?.icon
? getTagIconSrc(mx, useAuthentication, powerLevelTag.icon)
: undefined;
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
return (
<SequenceCard
key={notification.event.event_id}
@ -458,11 +488,14 @@ function RoomNotificationsGroupComp({
>
<Box gap="300" justifyContent="SpaceBetween" alignItems="Center" grow="Yes">
<Box gap="200" alignItems="Baseline">
<Username style={{ color: colorMXID(event.sender) }}>
<Text as="span" truncate>
<b>{displayName}</b>
</Text>
</Username>
<Box alignItems="Center" gap="200">
<Username style={{ color: usernameColor }}>
<Text as="span" truncate>
<UsernameBold>{displayName}</UsernameBold>
</Text>
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
<Time ts={event.origin_server_ts} />
</Box>
<Box shrink="No" gap="200" alignItems="Center">
@ -482,6 +515,10 @@ function RoomNotificationsGroupComp({
replyEventId={replyEventId}
threadRootId={threadRootId}
onClick={handleOpenClick}
getPowerLevel={getPowerLevel}
getPowerLevelTag={getPowerLevelTag}
accessibleTagColors={accessibleTagColors}
legacyUsernameColor={legacyUsernameColor}
/>
)}
{renderMatrixEvent(event.type, false, event, displayName, getContent)}
@ -511,7 +548,9 @@ export function Notifications() {
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad');
const [urlPreview] = useSetting(settingsAtom, 'urlPreview');
const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor');
const screenSize = useScreenSizeContext();
const mDirects = useAtomValue(mDirectAtom);
const { navigateRoom } = useRoomNavigate();
const [searchParams, setSearchParams] = useSearchParams();
@ -671,6 +710,9 @@ export function Notifications() {
urlPreview={urlPreview}
hideActivity={hideActivity}
onOpen={navigateRoom}
legacyUsernameColor={
legacyUsernameColor || mDirects.has(groupRoom.roomId)
}
/>
</VirtualTile>
);

View file

@ -2,7 +2,7 @@ import React, { ReactNode } from 'react';
import { useParams } from 'react-router-dom';
import { useAtomValue } from 'jotai';
import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
import { RoomProvider } from '../../../hooks/useRoom';
import { IsDirectRoomProvider, RoomProvider } from '../../../hooks/useRoom';
import { useMatrixClient } from '../../../hooks/useMatrixClient';
import { JoinBeforeNavigate } from '../../../features/join-before-navigate';
import { useSpace } from '../../../hooks/useSpace';
@ -10,11 +10,13 @@ import { getAllParents } from '../../../utils/room';
import { roomToParentsAtom } from '../../../state/room/roomToParents';
import { allRoomsAtom } from '../../../state/room-list/roomList';
import { useSearchParamsViaServers } from '../../../hooks/router/useSearchParamsViaServers';
import { mDirectAtom } from '../../../state/mDirectList';
export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
const mx = useMatrixClient();
const space = useSpace();
const roomToParents = useAtomValue(roomToParentsAtom);
const mDirects = useAtomValue(mDirectAtom);
const allRooms = useAtomValue(allRoomsAtom);
const { roomIdOrAlias, eventId } = useParams();
@ -39,7 +41,7 @@ export function SpaceRouteRoomProvider({ children }: { children: ReactNode }) {
return (
<RoomProvider key={room.roomId} value={room}>
{children}
<IsDirectRoomProvider value={mDirects.has(room.roomId)}>{children}</IsDirectRoomProvider>
</RoomProvider>
);
}