import React, { ReactNode, useImperativeHandle, useRef } from 'react'; import classNames from 'classnames'; import { as } from 'folds'; import * as css from './layout.css'; import { useStreamLayoutDebug } from './streamDebug'; import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; // Stream rows use a fixed `S400` gap so the rail-bridge offsets in // layout.css.ts (StreamRailBridgeY = S400) always match the gap between rows. // All three Stream call sites — RoomTimeline.tsx StreamDayDivider wrapper plus // Message.tsx Message / Event MessageBase — share this single constant. export const STREAM_MESSAGE_SPACING = '400' as const; // Stream layout — DM redesign (docs/plans/dm_1x1_redesign.md §6.5b). // // Visual structure: // ┌──┬─────────────────────────────────────────────────┐ // │ │ bubble (asymmetric radius) │ // │● │ ┌──────────────────────────────────────────┐ │ // │ │ │ time · sender · e2ee chip (header line) │ │ // │ │ │ message body │ │ // │ │ └──────────────────────────────────────────┘ │ // └──┴─────────────────────────────────────────────────┘ // rail+dot bubble (time absolutely-positioned to the // LEFT of the bubble, on the rail-time column) // // Read-state lives entirely on the dot now (own = Primary violet, opacity // 0.3 → 1.0 when peer reads; incoming = author hash, opacity 1.0 → 0.3 once // I read it). The legacy WhatsApp checkmark `` is intentionally // not rendered in Stream — the dot already encodes that signal. // // Geometry constants live in layout.css.ts (StreamRailWidth, StreamDotSize). // `time` and `header` are caller-controlled ReactNodes so Message.tsx keeps // ownership of the timestamp formatting / sender Username component / e2ee // indicators it already builds. export type StreamLayoutProps = { time?: ReactNode; dotColor: string; dotOpacity: number; isOwn?: boolean; compact?: boolean; header?: ReactNode; railStart?: boolean; railEnd?: boolean; }; // Stream day divider — used by RoomTimeline.tsx in place of the legacy // TimelineDivider for DM rooms. Sits as a regular row in the timeline so the // rail flows through it: rail-column has the date label (small mono uppercase), // a slightly larger Fleet-soft dot anchors on the rail, and the rest of the // row is a faint hairline. Matches stream-v2-dawn.jsx::DawnPhoneV3 line 73-78. export type StreamDayDividerProps = { label: ReactNode; }; export const StreamDayDivider = as<'div', StreamDayDividerProps>( ({ className, label, ...props }, ref) => { // Reads screen size internally instead of taking a prop so RoomTimeline // doesn't have to thread it through. The day-divider must use the SAME // `compact` value as message rows above and below or the rail and label // would mis-align horizontally on desktop. const compact = useScreenSizeContext() === ScreenSize.Mobile; return (
{/* Rail segment under the row — same recipe as message rows so the rail flows continuously through the day boundary. */}
{label}
); } ); export const StreamLayout = as<'div', StreamLayoutProps>( ( { className, time, dotColor, dotOpacity, isOwn, compact, header, railStart, railEnd, children, ...props }, ref ) => { const rootRef = useRef(null); const timeRef = useRef(null); const railRef = useRef(null); const dotRef = useRef(null); const bubbleRef = useRef(null); const headerRef = useRef(null); useImperativeHandle(ref, () => rootRef.current as HTMLDivElement); // Debug helper is dev-only and behind a localStorage opt-in (see // streamDebug.ts). After P3c every timeline row goes through StreamLayout, // so `active` is unconditionally true here. useStreamLayoutDebug( 'message', { root: rootRef, timeColumn: timeRef, rail: railRef, dot: dotRef, content: bubbleRef, header: headerRef, }, true ); return (
{time} {header && (
{header}
)} {children}
); } );