import React, { type MouseEventHandler, type ReactNode } from 'react'; import classNames from 'classnames'; import { Avatar, Box, Icon, Icons, type IconSrc, as } from 'folds'; import { type Room } from 'matrix-js-sdk'; import * as css from './Channel.css'; import { CHANNEL_AVATAR_PX } from './Channel.css'; import { UserAvatar } from '../../user-avatar'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { getMemberAvatarMxc } from '../../../utils/room'; import { mxcUrlToHttp } from '../../../utils/matrix'; // MessageBase recipe `space` variant for channel rows. '300' tightens the // vertical rhythm vs the '400' Stream uses for bubble rows — channel // rows have no bubble border, so they don't need as much breathing room. export const CHANNEL_MESSAGE_SPACING = '300' as const; export type ChannelLayoutProps = { // Avatar slot — pass `undefined` for collapsed rows (adjacent same-user) // so the slot stays a fixed-width spacer and the body column aligns // across all rows in the cluster. avatar?: ReactNode; // Username + time inline. `undefined` on collapsed rows. header?: ReactNode; reactions?: ReactNode; threadSummary?: ReactNode; // Forwarded onto the row root as `data-own="true"|"false"`. Channels // 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; // `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; }; // Channels timeline message row primitive. No rail. No bubble. Avatar // + body two-column flex; body holds header (name + time), content, // thread-summary, reactions in vertical flow. export const ChannelLayout = as<'div', ChannelLayoutProps>( ( { className, avatar, header, reactions, threadSummary, isOwn, headerInBubble, onContextMenu, children, ...props }, ref ) => (
{avatar}
{!headerInBubble && header &&
{header}
}
{headerInBubble && header && (
{header}
)} {children} {headerInBubble && threadSummary && ( // Inside-bubble footer: stretches via negative margins to the // bubble's inner border edge, paints a 1px top divider, and // hosts the existing thread-summary button as a flush // full-width chip. Reads as one continuous card with a // section break instead of two stacked pills.
{threadSummary}
)}
{!headerInBubble && threadSummary && (
{threadSummary}
)} {reactions &&
{reactions}
}
) ); export type ChannelDayDividerProps = { label: ReactNode; }; // Section break between days in the channels timeline. Horizontal line // + centered uppercase label, spans full row including the avatar gutter // so it reads as a structural separator. export const ChannelDayDivider = as<'div', ChannelDayDividerProps>( ({ className, label, ...props }, ref) => (
{label}
) ); export type ChannelEventContentProps = { iconSrc: IconSrc; content: ReactNode; }; // Sysline (membership change / room.create / pinned events / etc) row // for channels. Single-line, indented past the avatar slot so it visually // belongs to the body column, no rail/timestamp chrome. export function ChannelEventContent({ iconSrc, content }: ChannelEventContentProps) { return (
{content}
); } export type ChannelMessageAvatarProps = { room: Room; senderId: string; senderDisplayName: string; }; // Resolves the sender avatar (with auth + thumbnail) and renders the // folds `` + `` combination. Lifted out of `Message` // so the `useMediaAuthentication` / `useMatrixClient` hook calls only run // when channel layout is selected (Stream rows don't need an avatar). export function ChannelMessageAvatar({ room, senderId, senderDisplayName, }: ChannelMessageAvatarProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const avatarMxc = getMemberAvatarMxc(room, senderId); const avatarUrl = avatarMxc ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined : undefined; return ( } /> ); }