From ae3f63d37f6eaeefb782e8787192831bf1a6ed12 Mon Sep 17 00:00:00 2001 From: David <33458145+davidmcpowell@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:55:37 +0100 Subject: [PATCH] Merge pull request #27209 from overleaf/dp-collaborator-colour Adapt online user and chat user colors based on luminance GitOrigin-RevId: 1b0c843147ee3dc585866bc491a7c7613cb00e70 --- .../ide-redesign/components/chat/message.tsx | 19 +++++--- .../online-users/online-users-widget.tsx | 15 ++++++- .../web/frontend/js/shared/utils/colors.ts | 45 +++++++++++++++++++ .../pages/editor/online-users.scss | 8 ++++ 4 files changed, 78 insertions(+), 9 deletions(-) diff --git a/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx b/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx index 9a4ffe3a1b..6822db39da 100644 --- a/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/chat/message.tsx @@ -1,15 +1,14 @@ import { MessageProps } from '@/features/chat/components/message' import { User } from '../../../../../../types/user' -import { getHueForUserId } from '@/shared/utils/colors' +import { + getBackgroundColorForUserId, + hslStringToLuminance, +} from '@/shared/utils/colors' import MessageContent from '@/features/chat/components/message-content' import classNames from 'classnames' import MaterialIcon from '@/shared/components/material-icon' import { t } from 'i18next' -function hue(user?: User) { - return user ? getHueForUserId(user.id) : 0 -} - function getAvatarStyle(user?: User) { if (!user?.id) { // Deleted user @@ -20,9 +19,15 @@ function getAvatarStyle(user?: User) { } } + const backgroundColor = getBackgroundColorForUserId(user.id) + return { - borderColor: `hsl(${hue(user)}, 85%, 40%)`, - backgroundColor: `hsl(${hue(user)}, 85%, 40%`, + borderColor: backgroundColor, + backgroundColor, + color: + hslStringToLuminance(backgroundColor) < 0.5 + ? 'var(--content-primary-dark)' + : 'var(--content-primary)', } } diff --git a/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx b/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx index 07aaa647a9..2d30297e51 100644 --- a/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/online-users/online-users-widget.tsx @@ -7,7 +7,11 @@ import { DropdownToggle, } from '@/features/ui/components/bootstrap-5/dropdown-menu' import OLTooltip from '@/features/ui/components/ol/ol-tooltip' -import { getBackgroundColorForUserId } from '@/shared/utils/colors' +import { + getBackgroundColorForUserId, + hslStringToLuminance, +} from '@/shared/utils/colors' +import classNames from 'classnames' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' @@ -86,9 +90,16 @@ const OnlineUserWidget = ({ const OnlineUserCircle = ({ user }: { user: OnlineUser }) => { const backgroundColor = getBackgroundColorForUserId(user.user_id) + const luminance = hslStringToLuminance(backgroundColor) const [character] = [...user.name] return ( - + = 0.5, + })} + style={{ backgroundColor }} + > {character} ) diff --git a/services/web/frontend/js/shared/utils/colors.ts b/services/web/frontend/js/shared/utils/colors.ts index 346a52d03a..3c43de4119 100644 --- a/services/web/frontend/js/shared/utils/colors.ts +++ b/services/web/frontend/js/shared/utils/colors.ts @@ -34,6 +34,51 @@ export function getBackgroundColorForUserId(userId?: string) { return `hsl(${getHueForUserId(userId)}, 70%, 50%)` } +export function hslStringToLuminance(hslString: string): number { + // First extract the individual components from the HSL string + const hslSplit = hslString.slice(4).split(')')[0].split(',') + + const h = Number(hslSplit[0]) + const s = Number(hslSplit[1].slice(0, -1)) / 100 + const l = Number(hslSplit[2].slice(0, -1)) / 100 + + // Then we need to convert HSL to RGB + const c = (1 - Math.abs(2 * l - 1)) * s + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)) + const m = l - c / 2 + let r = 0 + let g = 0 + let b = 0 + if (h >= 0 && h < 60) { + r = c + m + g = x + m + b = m + } else if (h >= 60 && h < 120) { + r = x + m + g = c + m + b = m + } else if (h >= 120 && h < 180) { + r = m + g = c + m + b = x + m + } else if (h >= 180 && h < 240) { + r = m + g = x + m + b = c + m + } else if (h >= 240 && h < 300) { + r = x + m + g = m + b = c + m + } else if (h >= 300 && h < 360) { + r = c + m + g = m + b = x + m + } + + // Finally we calculate the luminance + return 0.2126 * r + 0.7152 * g + 0.0722 * b +} + const cachedHues = new Map() export function getHueForId(id: string) { diff --git a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss index a37152854d..0ae3eff278 100644 --- a/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss +++ b/services/web/frontend/stylesheets/bootstrap-5/pages/editor/online-users.scss @@ -124,4 +124,12 @@ box-sizing: border-box; display: inline-block; } + + .online-user-circle-light-font { + color: var(--content-primary-dark); + } + + .online-user-circle-dark-font { + color: var(--content-primary); + } }