diff --git a/.changeset/add_color_heros.md b/.changeset/add_color_heros.md new file mode 100644 index 000000000..52525613e --- /dev/null +++ b/.changeset/add_color_heros.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add background styling to user profile cards diff --git a/src/app/components/user-profile/CreatorChip.tsx b/src/app/components/user-profile/CreatorChip.tsx index ab4b4e82d..1e5bf84dc 100644 --- a/src/app/components/user-profile/CreatorChip.tsx +++ b/src/app/components/user-profile/CreatorChip.tsx @@ -16,8 +16,19 @@ import { useOpenSpaceSettings } from '$state/hooks/spaceSettings'; import { SpaceSettingsPage } from '$state/spaceSettings'; import { RoomSettingsPage } from '$state/roomSettings'; import { PowerColorBadge, PowerIcon } from '$components/power'; +import * as css from './styles.css'; -export function CreatorChip() { +export function CreatorChip({ + backgroundColor, + innerColor, + cardColor, + textColor, +}: { + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; +}) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const room = useRoom(); @@ -53,10 +64,12 @@ export function CreatorChip() { }} > -
+
{ @@ -80,8 +93,6 @@ export function CreatorChip() { } > : undefined} onClick={open} aria-pressed={!!cords} + className={css.UserHeroChip} + style={{ + backgroundColor: cardColor, + borderColor: backgroundColor, + color: textColor, + }} > {tag.name} diff --git a/src/app/components/user-profile/PowerChip.tsx b/src/app/components/user-profile/PowerChip.tsx index 5b9669847..58a54bd19 100644 --- a/src/app/components/user-profile/PowerChip.tsx +++ b/src/app/components/user-profile/PowerChip.tsx @@ -45,6 +45,7 @@ import { useMemberPowerCompare } from '$hooks/useMemberPowerCompare'; import { CutoutCard } from '$components/cutout-card'; import { PowerColorBadge, PowerIcon } from '$components/power'; import { EventType } from '$types/matrix-sdk'; +import * as css from './styles.css'; type SelfDemoteAlertProps = { power: number; @@ -146,7 +147,19 @@ function SharedPowerAlert({ power, onCancel, onChange }: SharedPowerAlertProps) ); } -export function PowerChip({ userId }: { userId: string }) { +export function PowerChip({ + userId, + backgroundColor, + innerColor, + cardColor, + textColor, +}: { + userId: string; + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; +}) { const mx = useMatrixClient(); const room = useRoom(); const space = useSpaceOptionally(); @@ -241,7 +254,11 @@ export function PowerChip({ userId }: { userId: string }) { {error && ( @@ -270,6 +287,8 @@ export function PowerChip({ userId }: { userId: string }) { radii="300" aria-disabled={changing || !canChangePowers || !canAssignPower} aria-pressed={selected} + className={css.UserHeroMenuItem} + style={{ backgroundColor: cardColor, color: textColor }} before={} after={ powerTagIconSrc ? ( @@ -287,13 +306,15 @@ export function PowerChip({ userId }: { userId: string }) { ); })} - -
+ +
{ if (room.isSpaceRoom()) { openSpaceSettings( @@ -319,8 +340,14 @@ export function PowerChip({ userId }: { userId: string }) { } > diff --git a/src/app/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx index 586ff9d50..c57532dd6 100644 --- a/src/app/components/user-profile/UserChips.tsx +++ b/src/app/components/user-profile/UserChips.tsx @@ -42,11 +42,21 @@ import { useNickname, useSetNickname } from '$hooks/useNickname'; import { CutoutCard } from '$components/cutout-card'; import { SettingTile } from '$components/setting-tile'; import { RoomAvatar, RoomIcon } from '$components/room-avatar'; -import { getMxIdServer } from '$utils/mxIdHelper'; - -export function ServerChip({ server }: { server: string }) { - const mx = useMatrixClient(); - const myServer = getMxIdServer(mx.getSafeUserId()); +import * as css from './styles.css'; + +export function ServerChip({ + server, + innerColor, + cardColor, + textColor, + backgroundColor, +}: { + server: string; + innerColor?: string; + cardColor?: string; + textColor?: string; + backgroundColor?: string; +}) { const navigate = useNavigate(); const closeProfile = useCloseUserRoomProfile(); const [copied, setCopied] = useTimeoutToggle(); @@ -77,9 +87,14 @@ export function ServerChip({ server }: { server: string }) { }} > -
+
Copy Server Explore Community
-
+
Open in Browser @@ -124,7 +154,6 @@ export function ServerChip({ server }: { server: string }) { } > {server} @@ -144,7 +179,19 @@ export function ServerChip({ server }: { server: string }) { ); } -export function ShareChip({ userId }: { userId: string }) { +export function ShareChip({ + userId, + innerColor, + cardColor, + textColor, + backgroundColor, +}: { + userId: string; + innerColor?: string; + cardColor?: string; + textColor?: string; + backgroundColor?: string; +}) { const [cords, setCords] = useState(); const [copied, setCopied] = useTimeoutToggle(); @@ -173,12 +220,16 @@ export function ShareChip({ userId }: { userId: string }) { }} > -
+
{ copyToClipboard(userId); setCopied(); @@ -188,10 +239,14 @@ export function ShareChip({ userId }: { userId: string }) { Copy User ID { copyToClipboard(getMatrixToUser(userId)); setCopied(); @@ -206,7 +261,7 @@ export function ShareChip({ userId }: { userId: string }) { } > Share @@ -232,7 +293,19 @@ type MutualRoomsData = { directs: Room[]; }; -export function MutualRoomsChip({ userId }: { userId: string }) { +export function MutualRoomsChip({ + userId, + backgroundColor, + innerColor, + cardColor, + textColor, +}: { + userId: string; + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; +}) { const mx = useMatrixClient(); const mutualRoomSupported = useMutualRoomsSupport(); const mutualRoomsState = useMutualRooms(userId); @@ -298,7 +371,12 @@ export function MutualRoomsChip({ userId }: { userId: string }) { fill="None" size="300" radii="300" - style={{ paddingLeft: config.space.S100 }} + className={css.UserHeroMenuItem} + style={{ + paddingLeft: config.space.S100, + backgroundColor: cardColor, + color: textColor, + }} onClick={() => { if (room.isSpaceRoom()) { navigateSpace(roomId); @@ -325,12 +403,17 @@ export function MutualRoomsChip({ userId }: { userId: string }) { )} /> ) : ( - + )} } > - + {room.name} @@ -360,6 +443,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) { display: 'flex', maxWidth: toRem(200), maxHeight: '80vh', + backgroundColor: innerColor, }} > @@ -367,7 +451,7 @@ export function MutualRoomsChip({ userId }: { userId: string }) { {mutual.spaces.length > 0 && ( @@ -402,7 +486,6 @@ export function MutualRoomsChip({ userId }: { userId: string }) { } > } disabled={ @@ -410,8 +493,14 @@ export function MutualRoomsChip({ userId }: { userId: string }) { } onClick={open} aria-pressed={!!cords} + className={css.UserHeroChip} + style={{ + backgroundColor: cardColor, + borderColor: backgroundColor, + color: textColor, + }} > - + {mutualRoomsState.status === AsyncStatus.Success && `${mutualRoomsState.data.length} Mutual Rooms`} {mutualRoomsState.status === AsyncStatus.Loading && 'Mutual Rooms'} @@ -438,7 +527,19 @@ export function IgnoredUserAlert() { ); } -export function OptionsChip({ userId }: { userId: string }) { +export function OptionsChip({ + userId, + backgroundColor, + innerColor, + cardColor, + textColor, +}: { + userId: string; + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; +}) { const mx = useMatrixClient(); const [cords, setCords] = useState(); const [editingNick, setEditingNick] = useState(false); @@ -503,12 +604,12 @@ export function OptionsChip({ userId }: { userId: string }) { }} > -
+
{editingNick ? ( Nickname Save @@ -543,10 +649,15 @@ export function OptionsChip({ userId }: { userId: string }) { radii="300" variant="Critical" fill="None" + className={css.UserHeroMenuItem} onClick={() => { setNickname(userId, undefined); close(); }} + style={{ + backgroundColor: cardColor, + color: textColor, + }} > Clear @@ -561,6 +672,11 @@ export function OptionsChip({ userId }: { userId: string }) { radii="300" before={} onClick={() => setEditingNick(true)} + className={css.UserHeroMenuItem} + style={{ + backgroundColor: cardColor, + color: textColor, + }} > {currentNick ? 'Edit Nickname' : 'Set Nickname'} @@ -574,6 +690,8 @@ export function OptionsChip({ userId }: { userId: string }) { toggleIgnore(); close(); }} + className={css.UserHeroMenuItem} + style={{ backgroundColor: cardColor }} before={ ignoring ? ( @@ -583,14 +701,26 @@ export function OptionsChip({ userId }: { userId: string }) { } disabled={ignoring} > - {ignored ? 'Unblock User' : 'Block User'} + + {ignored ? 'Unblock User' : 'Block User'} +
} > - + {ignoring ? ( ) : ( diff --git a/src/app/components/user-profile/UserHero.tsx b/src/app/components/user-profile/UserHero.tsx index 34cf7635a..f284296b2 100644 --- a/src/app/components/user-profile/UserHero.tsx +++ b/src/app/components/user-profile/UserHero.tsx @@ -2,6 +2,7 @@ import { useMemo, useState } from 'react'; import { Avatar, Box, + color as standardColors, Icon, Icons, Modal, @@ -28,6 +29,8 @@ import { ImageViewer } from '$components/image-viewer'; import { AvatarPresence, PresenceBadge } from '$components/presence'; import { UserAvatar } from '$components/user-avatar'; import { ClientSideHoverFreeze } from '$components/ClientSideHoverFreeze'; +import { useUserProfile } from '$hooks/useUserProfile'; +import { shadeColor, areColorsTooSimilar } from '$utils/shadeColor'; import * as css from './styles.css'; type UserHeroProps = { @@ -69,8 +72,19 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs const status = presence?.status; const isExpandable = (status?.length ?? 0) > 70; + const fetchedProfile = useUserProfile(userId); + const backgroundColor = fetchedProfile.heroColor ?? standardColors.Surface.Container; + const fetchedBrightness = fetchedProfile?.heroBrightness; + const isBackgroundDark = fetchedBrightness ? fetchedBrightness === 'dark' : undefined; + const cardColor = + shadeColor(backgroundColor, isBackgroundDark ? -80 : 80) ?? standardColors.Background.Container; + const textColor = + ((fetchedBrightness === 'dark' || areColorsTooSimilar('#000000', cardColor)) && '#FFFFFF') || + ((fetchedBrightness === 'light' || areColorsTooSimilar('#FFFFFF', cardColor)) && '#000000') || + undefined; + return ( - +
@@ -195,14 +211,14 @@ export function UserHero({ userId, avatarUrl, bannerUrl, presence, autoplayGifs type UserHeroNameProps = { displayName?: string; userId: string; + customHeroCards?: boolean; }; -export function UserHeroName({ displayName, userId }: UserHeroNameProps) { +export function UserHeroName({ displayName, userId, customHeroCards }: UserHeroNameProps) { const username = getMxIdLocalPart(userId); const nick = useNickname(userId); // Sable username color and fonts - const { color, font } = useSableCosmetics(userId, useRoom()); - + const { color, font } = useSableCosmetics(userId, useRoom(), customHeroCards); const shownName = nick ?? displayName ?? username ?? userId; return ( diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 15eba1aae..f1d2ebca7 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -1,4 +1,16 @@ -import { Box, Button, config, Icon, Icons, Menu, MenuItem, Scroll, Text, toRem } from 'folds'; +import { + Box, + Button, + color, + config, + Icon, + Icons, + Menu, + MenuItem, + Scroll, + Text, + toRem, +} from 'folds'; import type { SyntheticEvent } from 'react'; import { useCallback, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; @@ -39,12 +51,14 @@ import { useSetting } from '$state/hooks/settings'; import { useSettingsLinkBaseUrl } from '$features/settings/useSettingsLinkBaseUrl'; import { getMxIdServer } from '$utils/mxIdHelper'; import { TextViewerContent } from '$components/text-viewer'; +import { areColorsTooSimilar, shadeColor } from '$utils/shadeColor'; import { CreatorChip } from './CreatorChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { PowerChip } from './PowerChip'; import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips'; import { UserHero, UserHeroName } from './UserHero'; import { KnownMembership } from '$types/matrix-sdk'; +import * as css from './styles.css'; const KNOWN_KEYS = new Set([ 'moe.sable.app.bio', @@ -65,6 +79,10 @@ type UserExtendedSectionProps = { profile: UserProfile; htmlReactParserOptions: HTMLReactParserOptions; linkifyOpts: LinkifyOpts; + backgroundColor?: string; + innerColor?: string; + cardColor?: string; + textColor?: string; }; const renderValue = (val: unknown) => { @@ -78,6 +96,10 @@ function UserExtendedSection({ profile, htmlReactParserOptions, linkifyOpts, + backgroundColor, + innerColor, + cardColor, + textColor, }: Readonly) { const [showMisc, setShowMisc] = useState(false); const [miscDataIndex, setMiscDataIndex] = useState(-1); @@ -163,13 +185,24 @@ function UserExtendedSection({ return null; } return ( - + handleMiscSelector(-1)} > @@ -181,8 +214,7 @@ function UserExtendedSection({ size="300" radii="300" fill="None" - variant="Secondary" - style={{ justifyContent: 'Center' }} + style={{ justifyContent: 'Center', backgroundColor: cardColor, color: textColor }} onClick={() => handleMiscSelector(index)} > {key} @@ -190,7 +222,7 @@ function UserExtendedSection({ ))} ); - }, [miscDataIndex, showMisc, unknownFields]); + }, [cardColor, innerColor, miscDataIndex, showMisc, textColor, unknownFields]); const miscHeader = useMemo( () => ( @@ -205,6 +237,7 @@ function UserExtendedSection({ justifyContent: 'flex-start', width: 'fit-content', textAlign: 'center', + color: textColor, }} > @@ -216,11 +249,10 @@ function UserExtendedSection({ {showMisc && miscSelector} ), - [miscSelector, miscDataIndex, selectedUnknownField, showMisc, unknownFields] + [miscSelector, miscDataIndex, selectedUnknownField, showMisc, unknownFields, textColor] ); - return ( - + {(pronouns || localTime) && ( {pronouns && ( @@ -258,14 +290,23 @@ function UserExtendedSection({ visibility="Always" size="300" style={{ - backgroundColor: 'var(--sable-bg-container)', + backgroundColor: cardColor, borderRadius: config.radii.R400, + borderColor: backgroundColor, + borderStyle: 'solid', + borderWidth: '1px', maxHeight: '200px', marginTop: config.space.S0, overflowY: 'auto', }} > - + {unknownFields.length > 1 && ( @@ -315,16 +357,23 @@ function UserExtendedSection({ {miscHeader} {unknownFields.length > 1 && ( )} - + + - - + + - + {userId !== myUserId && ( @@ -483,13 +574,62 @@ export function UserRoomProfile({ userId, initialProfile }: Readonly - {server && } - - {creator ? : } - {userId !== myUserId && } - {userId !== myUserId && } + {server && ( + + )} + + {creator ? ( + + ) : ( + + )} + {userId !== myUserId && ( + + )} + {userId !== myUserId && ( + + )} {ignored && } diff --git a/src/app/components/user-profile/styles.css.ts b/src/app/components/user-profile/styles.css.ts index 10b0c9e51..3ff2f6c10 100644 --- a/src/app/components/user-profile/styles.css.ts +++ b/src/app/components/user-profile/styles.css.ts @@ -78,3 +78,19 @@ export const UserHeroAvatarImg = style({ }, }, }); +export const UserHeroChip = style({ + borderStyle: 'solid', + borderWidth: '1px', + ':hover': { + filter: 'brightness(0.8)', + transform: 'translateY(-1px)', + }, +}); +export const UserHeroMenuItem = style({ + borderStyle: 'hidden', + borderWidth: '1px', + ':hover': { + filter: 'brightness(0.8)', + transform: 'translateY(-1px)', + }, +}); diff --git a/src/app/features/settings/account/Profile.tsx b/src/app/features/settings/account/Profile.tsx index fcc2b18ec..dfc3873c4 100644 --- a/src/app/features/settings/account/Profile.tsx +++ b/src/app/features/settings/account/Profile.tsx @@ -628,6 +628,44 @@ function ProfileExtended({ profile, userId }: Readonly) { }} /> + + + handleSaveField('chat.commet.profile_color_scheme', { + color, + brightness: profile?.heroColorScheme?.brightness, + }) + } + /> + + handleSaveField('chat.commet.profile_color_scheme', { + color: profile?.heroColorScheme?.color, + brightness: profile?.heroColorScheme?.brightness === 'dark' ? 'light' : 'dark', + }) + } + > + + + {profile?.heroColorScheme?.brightness === 'dark' ? 'Dark Mode' : 'Light Mode'} + + + + + {extendedFields.length > 0 && extendedFields.map(([key, value]) => { diff --git a/src/app/features/settings/cosmetics/Cosmetics.tsx b/src/app/features/settings/cosmetics/Cosmetics.tsx index 1681774a2..6b4192b49 100644 --- a/src/app/features/settings/cosmetics/Cosmetics.tsx +++ b/src/app/features/settings/cosmetics/Cosmetics.tsx @@ -18,7 +18,7 @@ import FocusTrap from 'focus-trap-react'; import { PageContent } from '$components/page'; import { SequenceCard } from '$components/sequence-card'; import { useSetting } from '$state/hooks/settings'; -import type { JumboEmojiSize } from '$state/settings'; +import type { JumboEmojiSize, RenderUserCardsMode } from '$state/settings'; import { settingsAtom } from '$state/settings'; import { SettingTile } from '$components/setting-tile'; import { stopPropagation } from '$utils/keyboard'; @@ -104,6 +104,82 @@ function SelectJumboEmojiSize() { ); } +const profileCardRenderItems: { id: RenderUserCardsMode; name: string }[] = [ + { id: 'both', name: 'Light & dark' }, + { id: 'light', name: 'Light only' }, + { id: 'dark', name: 'Dark only' }, + { id: 'none', name: 'Off' }, +]; + +function SelectRenderCustomProfileCards() { + const [menuCords, setMenuCords] = useState(); + const [renderUserCardsMode, setRenderUserCardsMode] = useSetting(settingsAtom, 'renderUserCards'); + + const handleMenu: MouseEventHandler = (evt) => { + setMenuCords(evt.currentTarget.getBoundingClientRect()); + }; + + const handleSelect = (mode: RenderUserCardsMode) => { + setRenderUserCardsMode(mode); + setMenuCords(undefined); + }; + + const currentLabel = + profileCardRenderItems.find((i) => i.id === renderUserCardsMode)?.name ?? 'Light & dark'; + + return ( + <> + + setMenuCords(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => + evt.key === 'ArrowDown' || evt.key === 'ArrowRight', + isKeyBackward: (evt: KeyboardEvent) => + evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', + escapeDeactivates: stopPropagation, + }} + > + + + {profileCardRenderItems.map((item) => ( + handleSelect(item.id)} + > + {item.name} + + ))} + + + + } + /> + + ); +} + function JumboEmoji() { return ( @@ -214,6 +290,14 @@ function IdentityCosmetics() { after={} /> + + } + /> + { if (!room || !userId) return { color: undefined, font: undefined }; - let finalColor = profile.resolvedColor; + let finalColor = isUserHero ? profile.heroNameColor : profile.resolvedColor; if (!finalColor) { const memberPowerTag = getPowerTag(userId); finalColor = memberPowerTag?.color ? accessibleTagColors?.get(memberPowerTag.color) : undefined; } - - return { + const resolvedCosmetics: IContent = { color: finalColor, font: profile.resolvedFont, }; - }, [room, userId, profile.resolvedColor, profile.resolvedFont, getPowerTag, accessibleTagColors]); + + return resolvedCosmetics; + }, [ + room, + userId, + isUserHero, + profile.heroNameColor, + profile.resolvedColor, + profile.resolvedFont, + getPowerTag, + accessibleTagColors, + ]); } diff --git a/src/app/hooks/useUserProfile.ts b/src/app/hooks/useUserProfile.ts index 633d8d38a..e7494b640 100644 --- a/src/app/hooks/useUserProfile.ts +++ b/src/app/hooks/useUserProfile.ts @@ -7,8 +7,9 @@ import { EventTimeline, EventType } from '$types/matrix-sdk'; import colorMXID from '$utils/colorMXID'; import { profilesCacheAtom } from '$state/userRoomProfile'; import { useSetting } from '$state/hooks/settings'; -import { settingsAtom } from '$state/settings'; +import { settingsAtom, shouldApplyUserHeroCards } from '$state/settings'; import type { MSC1767Text } from '$types/matrix/common'; +import { areColorsTooSimilar, shadeColor } from '$utils/shadeColor'; import type { PronounSet } from '$utils/pronouns'; import { useMatrixClient } from './useMatrixClient'; import { ThemeKind, useActiveTheme } from './useTheme'; @@ -31,6 +32,7 @@ export type UserProfile = { nameColor?: string; nameColorDark?: string; nameColorLight?: string; + heroColorScheme?: Record; isCat?: boolean; hasCats?: boolean; extended?: Record; @@ -53,6 +55,7 @@ const normalizeInfo = (info: Record): UserProfile => { 'moe.sable.app.name_color', 'moe.sable.app.name_color_dark_theme', 'moe.sable.app.name_color_light_theme', + 'chat.commet.profile_color_scheme', 'kitty.meow.has_cats', 'kitty.meow.is_cat', ]); @@ -78,6 +81,7 @@ const normalizeInfo = (info: Record): UserProfile => { nameColor: info['moe.sable.app.name_color'] as string | undefined, nameColorDark: info['moe.sable.app.name_color_dark_theme'] as string | undefined, nameColorLight: info['moe.sable.app.name_color_light_theme'] as string | undefined, + heroColorScheme: info['chat.commet.profile_color_scheme'] as Record | undefined, isCat: info['kitty.meow.is_cat'] === true, hasCats: info['kitty.meow.has_cats'] === true, extended, @@ -102,12 +106,16 @@ export const useUserProfile = ( resolvedColor?: string; resolvedFont?: string; resolvedPronouns?: PronounSet[]; + heroColor?: string; + heroNameColor?: string; + heroBrightness?: string; } => { const mx = useMatrixClient(); const [legacyUsernameColor] = useSetting(settingsAtom, 'legacyUsernameColor'); const [renderGlobalColors] = useSetting(settingsAtom, 'renderGlobalNameColors'); const [renderRoomColors] = useSetting(settingsAtom, 'renderRoomColors'); const [renderRoomFonts] = useSetting(settingsAtom, 'renderRoomFonts'); + const [renderUserCardsMode] = useSetting(settingsAtom, 'renderUserCards'); const themeKind = useActiveTheme().kind; const userSelector = useMemo(() => selectAtom(profilesCacheAtom, (db) => db[userId]), [userId]); @@ -184,7 +192,6 @@ export const useUserProfile = ( Array.isArray(localFontEvent) ? localFontEvent[0] : localFontEvent )?.getContent()?.font; } - const localPronounEvent = state?.getStateEvents( CustomStateEvent.RoomCosmeticsPronouns as string, userId @@ -221,6 +228,7 @@ export const useUserProfile = ( )?.getContent()?.pronouns; } } + const validGlobalVal = isValidHex(data?.nameColor); const validGlobalValDark = isValidHex(data?.nameColorDark); const validGlobalValLight = isValidHex(data?.nameColorLight); @@ -262,12 +270,31 @@ export const useUserProfile = ( const resolvedPronouns = localPronouns || spacePronouns || data?.pronouns; + const rawHeroBrightness = data?.heroColorScheme?.brightness; + const heroCardsAllowed = shouldApplyUserHeroCards(renderUserCardsMode, rawHeroBrightness); + const validHeroColor = heroCardsAllowed ? isValidHex(data?.heroColorScheme?.color) : undefined; + const heroBrightness = heroCardsAllowed ? rawHeroBrightness : undefined; + const testUserHeroColor = shadeColor(validHeroColor, heroBrightness === 'dark' ? -80 : 80); + + const heroNameColor = heroCardsAllowed + ? ((renderGlobalColors || userId === mx.getUserId()) && + heroBrightness === 'light' && + !areColorsTooSimilar(testUserHeroColor, validGlobalValLight) && + validGlobalValLight) || + (heroBrightness === 'dark' && + !areColorsTooSimilar(testUserHeroColor, validGlobalValDark) && + validGlobalValDark) || + resolvedColor + : resolvedColor; return { ...data, resolvedColor, resolvedFont, resolvedPronouns, pronouns: resolvedPronouns, + heroColor: validHeroColor, + heroBrightness, + heroNameColor, }; }, [ cached, @@ -278,6 +305,7 @@ export const useUserProfile = ( renderRoomColors, renderRoomFonts, renderGlobalColors, + renderUserCardsMode, themeKind, legacyUsernameColor, ]); diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index d71ca4f00..0f0c9c628 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -23,6 +23,19 @@ export enum CaptionPosition { } export type JumboEmojiSize = 'none' | 'extraSmall' | 'small' | 'normal' | 'large' | 'extraLarge'; +/** Custom profile card hero colors: which brightness schemes to honor. */ +export type RenderUserCardsMode = 'both' | 'light' | 'dark' | 'none'; + +export function shouldApplyUserHeroCards( + mode: RenderUserCardsMode, + brightness: string | undefined +): boolean { + if (mode === 'none') return false; + if (mode === 'both') return true; + if (brightness !== 'light' && brightness !== 'dark') return false; + return brightness === mode; +} + export interface Settings { themeId?: string; useSystemTheme: boolean; @@ -82,6 +95,7 @@ export interface Settings { showPronouns: boolean; parsePronouns: boolean; renderGlobalNameColors: boolean; + renderUserCards: RenderUserCardsMode; filterPronounsBasedOnLanguage?: boolean; filterPronounsLanguages?: string[]; renderRoomColors: boolean; @@ -183,6 +197,7 @@ const defaultSettings: Settings = { showPronouns: true, parsePronouns: true, renderGlobalNameColors: true, + renderUserCards: 'both', renderRoomColors: true, renderRoomFonts: true, captionPosition: CaptionPosition.Below, @@ -234,6 +249,17 @@ export const getSettings = () => { } delete parsed.monochromeMode; + if (typeof parsed.renderUserCards === 'boolean') { + parsed.renderUserCards = parsed.renderUserCards ? 'both' : 'none'; + } else if ( + parsed.renderUserCards !== 'both' && + parsed.renderUserCards !== 'light' && + parsed.renderUserCards !== 'dark' && + parsed.renderUserCards !== 'none' + ) { + parsed.renderUserCards = 'both'; + } + return { ...defaultSettings, ...(parsed as Settings), diff --git a/src/app/utils/shadeColor.ts b/src/app/utils/shadeColor.ts new file mode 100644 index 000000000..725c3e96c --- /dev/null +++ b/src/app/utils/shadeColor.ts @@ -0,0 +1,35 @@ +export function shadeColor(initialColor?: string, percent?: number) { + if (!initialColor || initialColor[0] !== '#' || initialColor.length !== 7 || !percent) + return undefined; + const ratio = 1 + percent / 100; + + // Get hex value, convert it to number, multiply it by the desired amount, then clamp it + let R = Math.min(parseInt(initialColor.substring(1, 3), 16) * ratio, 255); + let G = Math.min(parseInt(initialColor.substring(3, 5), 16) * ratio, 255); + let B = Math.min(parseInt(initialColor.substring(5, 7), 16) * ratio, 255); + + if (R <= 8 && G <= 8 && B <= 8 && percent > 0) { + R = R <= 8 ? Math.max(178 - R, 0) : R; + G = G <= 8 ? Math.max(178 - G, 0) : G; + B = B <= 8 ? Math.max(178 - B, 0) : B; + } + + const RR = Math.floor(R).toString(16).padStart(2, '0'); + const GG = Math.floor(G).toString(16).padStart(2, '0'); + const BB = Math.floor(B).toString(16).padStart(2, '0'); + + return `#${RR}${GG}${BB}`; +} + +export function areColorsTooSimilar(colorA?: string, colorB?: string) { + if (!colorA || !colorB) return false; + + const aR = parseInt(colorA.substring(1, 3), 16); + const aG = parseInt(colorA.substring(3, 5), 16); + const aB = parseInt(colorA.substring(5, 7), 16); + const bR = parseInt(colorB.substring(1, 3), 16); + const bG = parseInt(colorB.substring(3, 5), 16); + const bB = parseInt(colorB.substring(5, 7), 16); + + return Math.abs(aR - bR) < 32 && Math.abs(aG - bG) < 32 && Math.abs(aB - bB) < 32; +}