diff --git a/src/app/components/message/layout/Channel.css.ts b/src/app/components/message/layout/Channel.css.ts index 81387520..2958b567 100644 --- a/src/app/components/message/layout/Channel.css.ts +++ b/src/app/components/message/layout/Channel.css.ts @@ -1,29 +1,36 @@ import { globalStyle, style } from '@vanilla-extract/css'; import { color, config, toRem } from 'folds'; -// 36px circular avatar — a notch above folds `Avatar size="200"` (32px) -// for visual weight matching the channels mockup. Consumers override -// the folds preset via inline style; the shared `CHANNEL_AVATAR_PX` -// constant keeps the CSS slot width and the inline override in sync. -export const CHANNEL_AVATAR_PX = 36; +// 40px circular avatar — Discord's cozy-mode avatar size. Consumers override +// the folds preset via inline style; the shared `CHANNEL_AVATAR_PX` constant +// keeps the CSS slot width and the inline override in sync. +export const CHANNEL_AVATAR_PX = 40; const ChannelAvatarWidth = toRem(CHANNEL_AVATAR_PX); +// Discord cozy-mode geometry: avatar 16px from the list edge, 16px gap to the +// content, so the message column starts at 16 + 40 + 16 = 72px. +const ChannelEdgePad = toRem(16); +const ChannelAvatarGap = toRem(16); + export const ChannelRow = style({ display: 'flex', alignItems: 'flex-start', - gap: config.space.S300, - // S200 (8px) sits inside MessageBase's S400 (16px) left pad, so the - // avatar's left edge lands ~24px from the screen edge — tighter than - // the previous 32px (S400+S400) without crowding the bubble cluster. - // Mirror on the right for symmetric breathing room. - paddingLeft: config.space.S200, - paddingRight: config.space.S200, - paddingTop: config.space.S100, - paddingBottom: config.space.S100, + gap: ChannelAvatarGap, + // Span the full pane edge-to-edge so the hover highlight runs the whole + // width like Discord: cancel MessageBase's S400/S200 horizontal padding with + // negative margins, then re-add the 16px avatar gutter as paddingLeft (so the + // avatar's left edge lands exactly 16px from the screen edge — Discord cozy). + marginLeft: `calc(-1 * ${config.space.S400})`, + marginRight: `calc(-1 * ${config.space.S200})`, + paddingLeft: ChannelEdgePad, + paddingRight: ChannelEdgePad, + // Tight vertical rhythm (Discord stacks lines on line-height); group + // separation comes from MessageBase's `space` marginTop on the run head. + paddingTop: toRem(2), + paddingBottom: toRem(2), minWidth: 0, - // Hover bg subtle so adjacent rows still read as distinct units even - // without bubble borders. `@media (hover: hover)` keeps this inert on - // touch where there's no pointer to follow. + // Hover bg subtle so adjacent rows still read as distinct units. `@media + // (hover: hover)` keeps this inert on touch where there's no pointer. '@media': { '(hover: hover) and (pointer: fine)': { selectors: { @@ -109,10 +116,11 @@ export const ChannelDayDividerLabel = style({ // body would, indented past the avatar slot, so the column reads // continuous. export const ChannelSysline = style({ - // Indent past the avatar gutter so the sysline body aligns with the - // body column of the surrounding message rows (avatar + gap + row pad). - // Tracks `ChannelRow.paddingLeft/Right` (S200) in lockstep. - paddingLeft: `calc(${ChannelAvatarWidth} + ${config.space.S300} + ${config.space.S200})`, + // Indent past the avatar gutter so the sysline body aligns with the message + // body column (72px). The sysline sits inside MessageBase's S400 (16px) left + // pad (it has no edge-to-edge negative margin), so paddingLeft = avatar (40) + // + gap (16) = 56 lands the content at 16 + 56 = 72px. + paddingLeft: `calc(${ChannelAvatarWidth} + ${ChannelAvatarGap})`, paddingRight: config.space.S200, paddingTop: config.space.S100, paddingBottom: config.space.S100, diff --git a/src/app/components/message/layout/Channel.tsx b/src/app/components/message/layout/Channel.tsx index a72996a2..98b17c0a 100644 --- a/src/app/components/message/layout/Channel.tsx +++ b/src/app/components/message/layout/Channel.tsx @@ -29,14 +29,13 @@ export type ChannelLayoutProps = { // main timeline doesn't style off it; the thread-drawer bubble CSS // reads it to mirror `StreamBubble`'s own-vs-incoming notch corner. isOwn?: boolean; - // When true, the header is rendered INSIDE the message-body slot - // (above content) instead of as a sibling row above the body, and the - // body element gets the `data-bubble="true"` marker so `Channel.css.ts` - // paints it as a chat bubble (same silhouette as `StreamBubble`). All - // current timeline callers (channels main, group rooms, thread drawer) - // pass `true`. The flag stays as an opt-in so consumers that render - // un-bubbled rows (`ChannelEventContent` / future picker previews) can - // still get the avatar-name-then-body shape without the bubble. + // `true` (thread drawer): the header (name + time) renders INSIDE the body + // slot above the content, and the body is a chat bubble — the compact + // in-bubble look. + // `false` (Discord-style main timeline): the header renders as a sibling row + // ABOVE the body (next to the avatar) and the body is plain text (no bubble) + // for everyone — Discord groups don't bubble messages. See `data-bubble` + // below and `Channel.css.ts`. headerInBubble?: boolean; onContextMenu?: MouseEventHandler; }; @@ -64,6 +63,9 @@ export const ChannelLayout = as<'div', ChannelLayoutProps>( className={classNames(css.ChannelRow, className)} onContextMenu={onContextMenu} data-own={isOwn ? 'true' : 'false'} + // Bubbles only in the compact in-bubble mode (thread drawer). The Discord + // header-above main timeline renders ALL messages as plain text (no + // bubbles) — own and peer alike. data-bubble={headerInBubble ? 'true' : undefined} {...props} ref={ref} diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index b72e21ed..f58e643b 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -540,10 +540,11 @@ export function RoomTimeline({ // this flag is purely visual. const channelStyleLayout = channelsMode || !isOneOnOne; const messageLayout: 'stream' | 'channel' = channelStyleLayout ? 'channel' : 'stream'; - // Bubble silhouette with the username + time INSIDE the bubble (mirrors - // the thread-drawer look). Stream layout keeps its native in-bubble - // header anyway, so the flag is only meaningful on the channel branch. - const channelHeaderInBubble = channelStyleLayout; + // Discord-style channel rows: avatar + the username + time on a header line + // ABOVE the message body (not inside the bubble). `false` puts the header + // above and lets ChannelLayout bubble only the peer body (own = plain text); + // the thread drawer passes `true` separately for its compact in-bubble look. + const channelHeaderInBubble = false; // M2: when the thread drawer is open, the channel composer is // unmounted by RoomView (single-Slate-at-a-time pattern). Clicking // the channel timeline's «Reply» menu would write a reply chip into diff --git a/src/app/features/room/message/CallMessage.tsx b/src/app/features/room/message/CallMessage.tsx index 823b5a27..dfb80bde 100644 --- a/src/app/features/room/message/CallMessage.tsx +++ b/src/app/features/room/message/CallMessage.tsx @@ -200,9 +200,8 @@ export function CallMessage({ <> - - {isOwnMessage ? t('Direct.message_me_label') : senderName} - + {/* Own events show the user's own nick too (not a «me» label). */} + {senderName}