From ed12318e492d7f1e3cdfb9fdd7783e2974da5899 Mon Sep 17 00:00:00 2001 From: Matthias Steiner Date: Thu, 27 Aug 2026 10:32:43 +0200 Subject: [PATCH] feat(desktop): sender-derived background tints on message rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each message row gets a subtle, deterministic background tint (10% opacity hsla) computed from the sender's pubkey via a djb2-style hash → hue. Every sender gets a stable, unique colour across sessions; no profile changes or relay changes required. Three-touch, frontend only: - TimelineMessage.accentColor?: string — new optional field - formatTimelineMessages: pubkeyToAccentColor() helper populates it - MessageRow: absolutely-positioned overlay div (z-index: -10, inset-0) so the tint lives behind content but above the article background; memo comparator updated to include accentColor Follow-up PR will layer canonical colours from kind:0 profiles for agents that publish an accent_color field. Signed-off-by: Matthias Steiner --- .../features/messages/lib/formatTimelineMessages.ts | 11 +++++++++++ desktop/src/features/messages/types.ts | 2 ++ desktop/src/features/messages/ui/MessageRow.tsx | 8 ++++++++ 3 files changed, 21 insertions(+) diff --git a/desktop/src/features/messages/lib/formatTimelineMessages.ts b/desktop/src/features/messages/lib/formatTimelineMessages.ts index a24da67f700..dc02f357cab 100644 --- a/desktop/src/features/messages/lib/formatTimelineMessages.ts +++ b/desktop/src/features/messages/lib/formatTimelineMessages.ts @@ -49,6 +49,16 @@ import { truncatePubkey } from "@/shared/lib/pubkey"; const HEX_RE = /^[0-9a-f]+$/i; +function pubkeyToAccentColor(pubkey: string): string { + let hash = 0; + for (let i = 0; i < pubkey.length; i++) { + hash = (hash << 5) - hash + pubkey.charCodeAt(i); + hash |= 0; + } + const hue = Math.abs(hash) % 360; + return `hsla(${hue}, 65%, 55%, 0.10)`; +} + export function isTimelineContentEvent(event: RelayEvent) { return ( event.kind === KIND_STREAM_MESSAGE || @@ -508,6 +518,7 @@ export function formatTimelineMessages( rootId: thread.rootId, depth: getDepth(event), accent: currentPubkey === authorPubkey, + accentColor: authorPubkey ? pubkeyToAccentColor(authorPubkey) : undefined, pending: event.pending, edited: edit !== undefined, kind: event.kind, diff --git a/desktop/src/features/messages/types.ts b/desktop/src/features/messages/types.ts index ec656f22366..7d38bb33aa8 100644 --- a/desktop/src/features/messages/types.ts +++ b/desktop/src/features/messages/types.ts @@ -43,6 +43,8 @@ export type TimelineMessage = { rootId?: string | null; depth: number; accent?: boolean; + /** Deterministic background tint derived from the sender's pubkey. */ + accentColor?: string; pending?: boolean; edited?: boolean; highlighted?: boolean; diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index 0f1ab3c981d..b33f9d2cb65 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -904,6 +904,13 @@ export const MessageRow = React.memo( data-testid="message-row" onAnimationEnd={handleEntranceAnimationEnd} > + {message.accentColor ? ( +
+ ) : null} {isThreadReplyLayout ? ( <> {avatarGutterNode} @@ -942,6 +949,7 @@ export const MessageRow = React.memo( prev.message.ownerLabel === next.message.ownerLabel && prev.message.avatarUrl === next.message.avatarUrl && prev.message.accent === next.message.accent && + prev.message.accentColor === next.message.accentColor && // The header timestamp and hover gutter both derive from createdAt (the // old `time` prop was the same value pre-formatted; this row reads neither). prev.message.createdAt === next.message.createdAt &&