feat(message): paint non-own bubbles via --vojo-peer-bubble-bg in Stream + Channel layouts and Stream rail/day-divider via --vojo-timeline-rail

This commit is contained in:
heaven 2026-05-16 13:13:28 +03:00
parent c78984a6d8
commit 45c69317ff
7 changed files with 51 additions and 3 deletions

View file

@ -169,6 +169,10 @@ globalStyle(
`${ChannelRow}[data-bubble="true"][data-own="false"] ${ChannelMessageBody}`,
{
borderRadius: `${config.radii.R500} ${config.radii.R500} ${config.radii.R500} ${toRem(4)}`,
// Peer (not-own) bubble bg — matches Stream layout's `peerBg`
// variant. Covers channels main timeline AND thread drawer
// (both pass `headerInBubble`, so `data-bubble="true"` fires).
backgroundColor: 'var(--vojo-peer-bubble-bg)',
}
);

View file

@ -32,6 +32,10 @@ export type StreamLayoutProps = {
dotColor: string;
dotOpacity: number;
isOwn?: boolean;
// Peer (not-own) bubble bg — caller passes `!isOwn` so every
// «чужое» сообщение reshades to `--vojo-peer-bubble-bg`. Applies
// in 1-1 DMs, groups, channels alike. No effect for own messages.
peerBg?: boolean;
compact?: boolean;
header?: ReactNode;
railStart?: boolean;
@ -100,6 +104,7 @@ export const StreamLayout = as<'div', StreamLayoutProps>(
dotColor,
dotOpacity,
isOwn,
peerBg,
compact,
header,
railStart,
@ -169,6 +174,7 @@ export const StreamLayout = as<'div', StreamLayoutProps>(
className={css.StreamBubble({
own: !!isOwn,
compact: !!compact,
peerBg: !!peerBg,
mediaMode: !!mediaMode,
})}
ref={bubbleRef}

View file

@ -283,7 +283,7 @@ export const StreamRail = style({
left: '50%',
transform: 'translateX(-50%)',
width: StreamRailLineWidth,
background: color.Surface.Container,
background: 'var(--vojo-timeline-rail)',
pointerEvents: 'none',
zIndex: 0,
});
@ -452,6 +452,14 @@ export const StreamBubble = recipe({
paddingRight: toRem(15),
},
},
// Peer (not-own) bubble bg — differentiation between «я» and
// «не я» across every room class. Media rows neutralize this via
// the `peerBg + mediaMode` compound below (order-independent).
peerBg: {
true: {
backgroundColor: 'var(--vojo-peer-bubble-bg)',
},
},
// Image messages: bubble becomes a transparent shell so the
// StreamMediaImage child supplies the visible chrome instead.
// `display: block, width: 100%` (NOT fit-content) so the bubble has a
@ -471,9 +479,21 @@ export const StreamBubble = recipe({
},
},
},
// Compound overrides — emitted after all variant classes, so they
// win the cascade regardless of variant declaration order. Keeps
// peer image / video bubbles transparent (the StreamMediaImage
// child supplies the chrome) even though `peerBg` would otherwise
// paint `--vojo-peer-bubble-bg` underneath.
compoundVariants: [
{
variants: { peerBg: true, mediaMode: true },
style: { backgroundColor: 'transparent' },
},
],
defaultVariants: {
own: false,
compact: false,
peerBg: false,
mediaMode: false,
},
});
@ -631,7 +651,7 @@ export const StreamDayLineWrap = style({
export const StreamDayLineSegment = style({
flex: 1,
height: 1,
background: color.Surface.Container,
background: 'var(--vojo-timeline-rail)',
minWidth: toRem(8),
});

View file

@ -109,6 +109,7 @@ export function CallMessage({
const senderId = aggregate.anchorSenderId;
const isOwnMessage = !!mx.getUserId() && senderId === mx.getUserId();
const peerBg = !isOwnMessage;
const senderName =
getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId) || senderId;
@ -220,6 +221,7 @@ export function CallMessage({
dotColor={dot.color}
dotOpacity={dot.opacity}
isOwn={isOwnMessage}
peerBg={peerBg}
compact={isMobile}
railStart={streamRailStart}
railEnd={streamRailEnd}

View file

@ -761,7 +761,7 @@ export const Message = as<'div', MessageProps>(
const [menuAnchor, setMenuAnchor] = useState<RectCords>();
const [emojiBoardAnchor, setEmojiBoardAnchor] = useState<RectCords>();
const isOwnMessage = senderId === mx.getUserId();
const isOwnMessage = !!mx.getUserId() && senderId === mx.getUserId();
const senderDisplayName =
getMemberDisplayName(room, senderId) ?? getMxIdLocalPart(senderId) ?? senderId;
@ -774,6 +774,7 @@ export const Message = as<'div', MessageProps>(
const dot = useDotColor(room, mEvent, true, hideReadReceipts);
const screenSize = useScreenSizeContext();
const isMobile = screenSize === ScreenSize.Mobile;
const peerBg = !isOwnMessage;
// msgType comes from the parent — RoomTimeline reads
// `mEvent.getContent().msgtype` synchronously and re-evaluates inside
@ -1200,6 +1201,7 @@ export const Message = as<'div', MessageProps>(
dotColor={dot.color}
dotOpacity={dot.opacity}
isOwn={isOwnMessage}
peerBg={peerBg}
compact={isMobile}
railStart={streamRailStart}
railEnd={streamRailEnd}

View file

@ -58,6 +58,7 @@ export function SyslineMessage({
const senderId = mEvent.getSender() ?? '';
const isOwnMessage = !!mx.getUserId() && senderId === mx.getUserId();
const peerBg = !isOwnMessage;
const senderName =
getMemberDisplayName(room, senderId) || getMxIdLocalPart(senderId) || senderId;
@ -112,6 +113,7 @@ export function SyslineMessage({
dotColor={dot.color}
dotOpacity={dot.opacity}
isOwn={isOwnMessage}
peerBg={peerBg}
compact={isMobile}
railStart={streamRailStart}
railEnd={streamRailEnd}

View file

@ -21,6 +21,15 @@
default is the light-theme value; `.dark-theme` overrides below. */
--vojo-horseshoe-void: #d6d6e3;
/* Peer (not-own) bubble bg every «чужое» message reshades to
this, in 1-1 DMs, groups, channels alike. Light: slight off-white
step from #ffffff. Dark override below. */
--vojo-peer-bubble-bg: #f5f5fa;
/* Stream timeline rail (vertical line through dots) + day-divider
horizontal segments. Light: hairline cool-grey; dark overrides
below. */
--vojo-timeline-rail: #e8e8f2;
--font-emoji: 'Twemoji_DISABLED';
--font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
}
@ -39,6 +48,9 @@
--vojo-horseshoe-void: #090909;
--vojo-peer-bubble-bg: #090909;
--vojo-timeline-rail: #000000;
--font-secondary: 'InterVariable', var(--font-emoji), sans-serif;
}