feat(channels): rebuild thread drawer and channel rows as chat-style bubble cards that merge thread-summary into the bubble footer
This commit is contained in:
parent
3f873a5041
commit
c9ea22d2d4
6 changed files with 219 additions and 13 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { style } from '@vanilla-extract/css';
|
import { globalStyle, style } from '@vanilla-extract/css';
|
||||||
import { color, config, toRem } from 'folds';
|
import { color, config, toRem } from 'folds';
|
||||||
|
|
||||||
// 36px circular avatar — a notch above folds `Avatar size="200"` (32px)
|
// 36px circular avatar — a notch above folds `Avatar size="200"` (32px)
|
||||||
|
|
@ -121,3 +121,111 @@ export const ChannelSyslineBody = style({
|
||||||
minWidth: 0,
|
minWidth: 0,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bubble chrome applied when `ChannelLayout` is invoked with
|
||||||
|
// `headerInBubble` (thread drawer and channels main timeline pass it).
|
||||||
|
// Mirrors `StreamBubble` from the DM timeline so a channel row reads
|
||||||
|
// like a chat-bubble cluster: dark `Surface.Container` card with an
|
||||||
|
// asymmetric notch corner per `data-own`, sized `fit-content` so short
|
||||||
|
// bubbles shrink-wrap instead of stretching across the column.
|
||||||
|
// Reactions and the thread-summary card live as siblings of the body
|
||||||
|
// in `ChannelLayout`, so they stay OUTSIDE the bubble — identical
|
||||||
|
// composition to Stream. The `[data-bubble="true"]` row marker keeps
|
||||||
|
// the un-bubbled channel/sysline layout (pre-redesign callers) opt-in
|
||||||
|
// rather than forcing the look on every consumer.
|
||||||
|
globalStyle(`${ChannelRow}[data-bubble="true"] ${ChannelMessageBody}`, {
|
||||||
|
backgroundColor: color.Surface.Container,
|
||||||
|
color: color.SurfaceVariant.OnContainer,
|
||||||
|
border: `1px solid ${color.Surface.ContainerLine}`,
|
||||||
|
paddingTop: config.space.S200,
|
||||||
|
paddingBottom: config.space.S200,
|
||||||
|
paddingLeft: toRem(15),
|
||||||
|
paddingRight: toRem(15),
|
||||||
|
display: 'inline-block',
|
||||||
|
width: 'fit-content',
|
||||||
|
maxWidth: '100%',
|
||||||
|
minWidth: 0,
|
||||||
|
position: 'relative',
|
||||||
|
zIndex: 1,
|
||||||
|
// Clips the thread-summary footer's hover bg against the bubble's
|
||||||
|
// rounded BR/BL corners — without it the rectangular hover paint
|
||||||
|
// punches past the curve. No outflow content lives inside the bubble
|
||||||
|
// (option bar, reactions are siblings on the row) so clipping is
|
||||||
|
// safe.
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Asymmetric corner per `data-own` — own messages flatten TOP-LEFT
|
||||||
|
// (4px), incoming messages flatten BOTTOM-LEFT. Same pattern as
|
||||||
|
// `StreamBubble.own`/`StreamBubble.others`.
|
||||||
|
globalStyle(
|
||||||
|
`${ChannelRow}[data-bubble="true"][data-own="true"] ${ChannelMessageBody}`,
|
||||||
|
{
|
||||||
|
borderRadius: `${toRem(4)} ${config.radii.R500} ${config.radii.R500} ${config.radii.R500}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
globalStyle(
|
||||||
|
`${ChannelRow}[data-bubble="true"][data-own="false"] ${ChannelMessageBody}`,
|
||||||
|
{
|
||||||
|
borderRadius: `${config.radii.R500} ${config.radii.R500} ${config.radii.R500} ${toRem(4)}`,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Small gap so the in-bubble header (username + time) doesn't sit flush
|
||||||
|
// against the first line of message text. Matches `StreamBubbleHeader`'s
|
||||||
|
// 2px gap.
|
||||||
|
globalStyle(
|
||||||
|
`${ChannelRow}[data-bubble="true"] ${ChannelHeader}[data-in-bubble="true"]`,
|
||||||
|
{
|
||||||
|
marginBottom: toRem(2),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Thread-summary footer rendered INSIDE the bubble (rather than as a
|
||||||
|
// separate pill below). Negative L/R margin (matches the bubble's
|
||||||
|
// `paddingLeft/Right: 15px`) stretches the wrapper to the bubble's
|
||||||
|
// inner border edge so the 1px top rule reads as a section divider
|
||||||
|
// spanning the whole bubble. Negative `marginBottom` cancels the
|
||||||
|
// bubble's S200 bottom pad so the footer flushes against the bubble's
|
||||||
|
// rounded bottom edge — bubble + summary read as one card with a
|
||||||
|
// horizontal rule splitting them.
|
||||||
|
//
|
||||||
|
// The footer body keeps no own border/radius — it inherits the bubble's
|
||||||
|
// bottom corners via clipping (`ChannelMessageBody` itself doesn't
|
||||||
|
// `overflow: hidden`, but the rounded bottom of the bubble visually
|
||||||
|
// caps the footer anyway because the divider line never reaches the
|
||||||
|
// curved corner pixel).
|
||||||
|
export const ChannelBubbleThreadSummary = style({
|
||||||
|
marginTop: config.space.S200,
|
||||||
|
marginLeft: toRem(-15),
|
||||||
|
marginRight: toRem(-15),
|
||||||
|
marginBottom: `calc(-1 * ${config.space.S200})`,
|
||||||
|
borderTop: `1px solid ${color.Surface.ContainerLine}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Footer button — strip the original ThreadSummaryCard pill chrome
|
||||||
|
// (own bg, radius, padding, max-width) so it reads as a flush bubble
|
||||||
|
// footer. Click target expands to the full footer width. Hover paints
|
||||||
|
// a subtle `SurfaceVariant.Container` shade that contrasts against
|
||||||
|
// the bubble's `Surface.Container` bg, signalling tappable footer
|
||||||
|
// without the pill silhouette returning.
|
||||||
|
globalStyle(`${ChannelBubbleThreadSummary} > button`, {
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 'none',
|
||||||
|
borderRadius: 0,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
padding: `${config.space.S200} ${toRem(15)}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
globalStyle(`${ChannelBubbleThreadSummary} > button:hover`, {
|
||||||
|
backgroundColor: color.SurfaceVariant.Container,
|
||||||
|
});
|
||||||
|
|
||||||
|
globalStyle(`${ChannelBubbleThreadSummary} > button:focus-visible`, {
|
||||||
|
// Inset the focus ring slightly so it doesn't punch through the
|
||||||
|
// bubble's rounded bottom corners on the BR/BL when the row is
|
||||||
|
// either own or incoming.
|
||||||
|
outlineOffset: toRem(-2),
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,16 @@ export type ChannelLayoutProps = {
|
||||||
header?: ReactNode;
|
header?: ReactNode;
|
||||||
reactions?: ReactNode;
|
reactions?: ReactNode;
|
||||||
threadSummary?: 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;
|
||||||
|
// When true, the header is rendered INSIDE the message-body slot
|
||||||
|
// (above content) instead of as a sibling row above the body. Thread
|
||||||
|
// drawer flips this on so the bubble wraps the username + time the
|
||||||
|
// same way `StreamBubble` does in DM chat. Channels main timeline
|
||||||
|
// keeps this false — name + time stay above an unbordered body.
|
||||||
|
headerInBubble?: boolean;
|
||||||
onContextMenu?: MouseEventHandler<HTMLDivElement>;
|
onContextMenu?: MouseEventHandler<HTMLDivElement>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -33,20 +43,50 @@ export type ChannelLayoutProps = {
|
||||||
// thread-summary, reactions in vertical flow.
|
// thread-summary, reactions in vertical flow.
|
||||||
export const ChannelLayout = as<'div', ChannelLayoutProps>(
|
export const ChannelLayout = as<'div', ChannelLayoutProps>(
|
||||||
(
|
(
|
||||||
{ className, avatar, header, reactions, threadSummary, onContextMenu, children, ...props },
|
{
|
||||||
|
className,
|
||||||
|
avatar,
|
||||||
|
header,
|
||||||
|
reactions,
|
||||||
|
threadSummary,
|
||||||
|
isOwn,
|
||||||
|
headerInBubble,
|
||||||
|
onContextMenu,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
},
|
||||||
ref
|
ref
|
||||||
) => (
|
) => (
|
||||||
<div
|
<div
|
||||||
className={classNames(css.ChannelRow, className)}
|
className={classNames(css.ChannelRow, className)}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
|
data-own={isOwn ? 'true' : 'false'}
|
||||||
|
data-bubble={headerInBubble ? 'true' : undefined}
|
||||||
{...props}
|
{...props}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<div className={css.ChannelAvatarSlot}>{avatar}</div>
|
<div className={css.ChannelAvatarSlot}>{avatar}</div>
|
||||||
<div className={css.ChannelBody}>
|
<div className={css.ChannelBody}>
|
||||||
{header && <div className={css.ChannelHeader}>{header}</div>}
|
{!headerInBubble && header && <div className={css.ChannelHeader}>{header}</div>}
|
||||||
<div className={css.ChannelMessageBody}>{children}</div>
|
<div className={css.ChannelMessageBody}>
|
||||||
{threadSummary && <div className={css.ChannelThreadSummary}>{threadSummary}</div>}
|
{headerInBubble && header && (
|
||||||
|
<div className={css.ChannelHeader} data-in-bubble="true">
|
||||||
|
{header}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{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.
|
||||||
|
<div className={css.ChannelBubbleThreadSummary}>{threadSummary}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!headerInBubble && threadSummary && (
|
||||||
|
<div className={css.ChannelThreadSummary}>{threadSummary}</div>
|
||||||
|
)}
|
||||||
{reactions && <div className={css.ChannelReactions}>{reactions}</div>}
|
{reactions && <div className={css.ChannelReactions}>{reactions}</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -541,6 +541,11 @@ export function RoomTimeline({
|
||||||
// Channel layout too — visually consistent with native channels — only
|
// Channel layout too — visually consistent with native channels — only
|
||||||
// the thread plus / cards differ (bridge has no thread semantic).
|
// the thread plus / cards differ (bridge has no thread semantic).
|
||||||
const messageLayout: 'stream' | 'channel' = channelsMode ? 'channel' : 'stream';
|
const messageLayout: 'stream' | 'channel' = channelsMode ? 'channel' : 'stream';
|
||||||
|
// Channels main timeline shares the thread-drawer bubble silhouette:
|
||||||
|
// dark `Surface.Container` card with the username + time INSIDE the
|
||||||
|
// bubble. Stream layout (DM/Bots) keeps its native bubble header so
|
||||||
|
// the flag is gated on channels-mode only.
|
||||||
|
const channelHeaderInBubble = channelsMode;
|
||||||
// M2: when the thread drawer is open, the channel composer is
|
// M2: when the thread drawer is open, the channel composer is
|
||||||
// unmounted by RoomView (single-Slate-at-a-time pattern). Clicking
|
// unmounted by RoomView (single-Slate-at-a-time pattern). Clicking
|
||||||
// the channel timeline's «Reply» menu would write a reply chip into
|
// the channel timeline's «Reply» menu would write a reply chip into
|
||||||
|
|
@ -1342,6 +1347,7 @@ export function RoomTimeline({
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
>
|
>
|
||||||
{mEvent.isRedacted() ? (
|
{mEvent.isRedacted() ? (
|
||||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||||
|
|
@ -1452,6 +1458,7 @@ export function RoomTimeline({
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
if (mEvent.isRedacted()) return <RedactedContent />;
|
if (mEvent.isRedacted()) return <RedactedContent />;
|
||||||
|
|
@ -1573,6 +1580,7 @@ export function RoomTimeline({
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
>
|
>
|
||||||
{mEvent.isRedacted() ? (
|
{mEvent.isRedacted() ? (
|
||||||
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
<RedactedContent reason={mEvent.getUnsigned().redacted_because?.content.reason} />
|
||||||
|
|
@ -1637,6 +1645,7 @@ export function RoomTimeline({
|
||||||
railStart={streamRailStart}
|
railStart={streamRailStart}
|
||||||
railEnd={streamRailEnd}
|
railEnd={streamRailEnd}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
iconSrc={iconSrc}
|
iconSrc={iconSrc}
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
|
@ -1686,6 +1695,7 @@ export function RoomTimeline({
|
||||||
railStart={streamRailStart}
|
railStart={streamRailStart}
|
||||||
railEnd={streamRailEnd}
|
railEnd={streamRailEnd}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
iconSrc={Icons.Hash}
|
iconSrc={Icons.Hash}
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
|
@ -1736,6 +1746,7 @@ export function RoomTimeline({
|
||||||
railStart={streamRailStart}
|
railStart={streamRailStart}
|
||||||
railEnd={streamRailEnd}
|
railEnd={streamRailEnd}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
iconSrc={Icons.Hash}
|
iconSrc={Icons.Hash}
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
|
@ -1786,6 +1797,7 @@ export function RoomTimeline({
|
||||||
railStart={streamRailStart}
|
railStart={streamRailStart}
|
||||||
railEnd={streamRailEnd}
|
railEnd={streamRailEnd}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
iconSrc={Icons.Hash}
|
iconSrc={Icons.Hash}
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
|
@ -1844,6 +1856,7 @@ export function RoomTimeline({
|
||||||
railStart={streamRailStart}
|
railStart={streamRailStart}
|
||||||
railEnd={streamRailEnd}
|
railEnd={streamRailEnd}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
iconSrc={callJoined ? Icons.Phone : Icons.PhoneDown}
|
iconSrc={callJoined ? Icons.Phone : Icons.PhoneDown}
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
|
@ -1891,6 +1904,7 @@ export function RoomTimeline({
|
||||||
railStart={streamRailStart}
|
railStart={streamRailStart}
|
||||||
railEnd={streamRailEnd}
|
railEnd={streamRailEnd}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
iconSrc={Icons.Code}
|
iconSrc={Icons.Code}
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
|
@ -1940,6 +1954,7 @@ export function RoomTimeline({
|
||||||
railStart={streamRailStart}
|
railStart={streamRailStart}
|
||||||
railEnd={streamRailEnd}
|
railEnd={streamRailEnd}
|
||||||
layout={messageLayout}
|
layout={messageLayout}
|
||||||
|
channelHeaderInBubble={channelHeaderInBubble}
|
||||||
iconSrc={Icons.Code}
|
iconSrc={Icons.Code}
|
||||||
content={
|
content={
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,11 @@ export const ThreadDrawer = style({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
backgroundColor: color.Surface.Container,
|
// Match the chat surface (`RoomView.tsx` paints
|
||||||
|
// `color.SurfaceVariant.Container` as its Page bg). Bubbles inside
|
||||||
|
// the drawer are `color.Surface.Container` so the «darker card on
|
||||||
|
// lighter surface» contrast reads identically to the main timeline.
|
||||||
|
backgroundColor: color.SurfaceVariant.Container,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
borderTopLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
|
borderTopLeftRadius: toRem(VOJO_HORSESHOE_RADIUS_PX),
|
||||||
|
|
@ -120,7 +124,8 @@ export const ThreadDrawerMobile = style({
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
backgroundColor: color.Surface.Container,
|
// Same chat-surface tone as the desktop drawer above.
|
||||||
|
backgroundColor: color.SurfaceVariant.Container,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
paddingTop: 'var(--vojo-safe-top, 0px)',
|
paddingTop: 'var(--vojo-safe-top, 0px)',
|
||||||
});
|
});
|
||||||
|
|
@ -151,13 +156,20 @@ export const ThreadDrawerScroll = style({
|
||||||
// only has this container's top padding above it — S400 wasn't enough,
|
// only has this container's top padding above it — S400 wasn't enough,
|
||||||
// so the bar clipped at the scroll viewport's top edge on hover. S700
|
// so the bar clipped at the scroll viewport's top edge on hover. S700
|
||||||
// gives the bar 32px of clearance, enough to render the ~28-32px hover
|
// gives the bar 32px of clearance, enough to render the ~28-32px hover
|
||||||
// bar fully on the root. Side / bottom padding stays S400 to preserve
|
// bar fully on the root.
|
||||||
// the original visual density.
|
//
|
||||||
|
// Left/right padding zeroed: `ChannelRow` already provides an S400
|
||||||
|
// (16px) horizontal gutter per row, and stacking two S400 paddings
|
||||||
|
// pushed the avatar 32px from the drawer edge — a perceptibly off
|
||||||
|
// «double-gutter» look in the narrow side pane. With this gutter
|
||||||
|
// removed, the avatar sits at row-padding (16px) from the drawer's
|
||||||
|
// rounded edge, matching the channels main-timeline rhythm.
|
||||||
export const ThreadDrawerContent = style({
|
export const ThreadDrawerContent = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: config.space.S400,
|
gap: config.space.S400,
|
||||||
padding: `${config.space.S700} ${config.space.S400} ${config.space.S400}`,
|
paddingTop: config.space.S700,
|
||||||
|
paddingBottom: config.space.S400,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ThreadDivider = style({
|
export const ThreadDivider = style({
|
||||||
|
|
@ -212,11 +224,23 @@ export const ThreadCounterText = style({
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Thread composer wrap — geometry mirrors the personal-chat composer
|
||||||
|
// in `RoomView.tsx`: a 12px outer pad on the sides + bottom (matches
|
||||||
|
// the horseshoe void gap) frames a card whose visual chrome is owned
|
||||||
|
// by `ChatComposer` from `RoomView.css.ts`. The class itself is
|
||||||
|
// reapplied to the inner wrap in `ThreadDrawer.tsx` so the same
|
||||||
|
// `globalStyle` rules (`Surface.Container` bg, 32px radius, dark
|
||||||
|
// touch-hover gate) reach the Editor inside.
|
||||||
export const ThreadComposer = style({
|
export const ThreadComposer = style({
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
padding: `0 ${config.space.S400} ${config.space.S400}`,
|
padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} ${toRem(VOJO_HORSESHOE_GAP_PX)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Bubble chrome itself lives in `Channel.css.ts` and applies via the
|
||||||
|
// row's `data-bubble="true"` marker (set by `ChannelLayout` when
|
||||||
|
// `headerInBubble` is enabled). Both the thread drawer and the
|
||||||
|
// channels main timeline opt in, so the look is shared.
|
||||||
|
|
||||||
export const ThreadEmptyState = style({
|
export const ThreadEmptyState = style({
|
||||||
padding: `${config.space.S500} ${config.space.S300}`,
|
padding: `${config.space.S500} ${config.space.S300}`,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { HTMLReactParserOptions } from 'html-react-parser';
|
||||||
import { Opts as LinkifyOpts } from 'linkifyjs';
|
import { Opts as LinkifyOpts } from 'linkifyjs';
|
||||||
|
|
||||||
import * as css from './ThreadDrawer.css';
|
import * as css from './ThreadDrawer.css';
|
||||||
|
import { ChatComposer } from './RoomView.css';
|
||||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||||
import { useResizeObserver } from '../../hooks/useResizeObserver';
|
import { useResizeObserver } from '../../hooks/useResizeObserver';
|
||||||
|
|
@ -1091,6 +1092,7 @@ export function ThreadDrawer({
|
||||||
// to `[roomId, rootId]` so the chip surfaces there.
|
// to `[roomId, rootId]` so the chip surfaces there.
|
||||||
hideMainReplyAffordance={false}
|
hideMainReplyAffordance={false}
|
||||||
layout="channel"
|
layout="channel"
|
||||||
|
channelHeaderInBubble
|
||||||
>
|
>
|
||||||
{(() => {
|
{(() => {
|
||||||
if (mEvent.isRedacted()) {
|
if (mEvent.isRedacted()) {
|
||||||
|
|
@ -1307,7 +1309,7 @@ export function ThreadDrawer({
|
||||||
{renderBody()}
|
{renderBody()}
|
||||||
</Scroll>
|
</Scroll>
|
||||||
</div>
|
</div>
|
||||||
<div className={css.ThreadComposer}>
|
<div className={`${css.ThreadComposer} ${ChatComposer}`}>
|
||||||
{canMessage ? (
|
{canMessage ? (
|
||||||
<RoomInput
|
<RoomInput
|
||||||
room={room}
|
room={room}
|
||||||
|
|
|
||||||
|
|
@ -705,6 +705,12 @@ export type MessageProps = {
|
||||||
// without bubble (Slack/Discord-style channels timeline). Default
|
// without bubble (Slack/Discord-style channels timeline). Default
|
||||||
// `'stream'` so non-channels callers don't have to opt in.
|
// `'stream'` so non-channels callers don't have to opt in.
|
||||||
layout?: 'stream' | 'channel';
|
layout?: 'stream' | 'channel';
|
||||||
|
// Opt-in for thread-drawer rendering: forces `ChannelLayout` to
|
||||||
|
// render the username + time header INSIDE the bubble slot (mirrors
|
||||||
|
// `StreamBubble`'s in-bubble header), so the thread reads like a
|
||||||
|
// chat-bubble cluster instead of avatar-name-then-body channel rows.
|
||||||
|
// Channels main timeline keeps this false.
|
||||||
|
channelHeaderInBubble?: boolean;
|
||||||
};
|
};
|
||||||
export const Message = as<'div', MessageProps>(
|
export const Message = as<'div', MessageProps>(
|
||||||
(
|
(
|
||||||
|
|
@ -739,6 +745,7 @@ export const Message = as<'div', MessageProps>(
|
||||||
msgType,
|
msgType,
|
||||||
threadSummary,
|
threadSummary,
|
||||||
layout = 'stream',
|
layout = 'stream',
|
||||||
|
channelHeaderInBubble,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
|
|
@ -1140,6 +1147,8 @@ export const Message = as<'div', MessageProps>(
|
||||||
)}
|
)}
|
||||||
{layout === 'channel' ? (
|
{layout === 'channel' ? (
|
||||||
<ChannelLayout
|
<ChannelLayout
|
||||||
|
isOwn={isOwnMessage}
|
||||||
|
headerInBubble={channelHeaderInBubble}
|
||||||
avatar={
|
avatar={
|
||||||
!collapse ? (
|
!collapse ? (
|
||||||
<ChannelMessageAvatar
|
<ChannelMessageAvatar
|
||||||
|
|
@ -1159,7 +1168,15 @@ export const Message = as<'div', MessageProps>(
|
||||||
onContextMenu={onUserClick}
|
onContextMenu={onUserClick}
|
||||||
onClick={onUsernameClick}
|
onClick={onUsernameClick}
|
||||||
>
|
>
|
||||||
<Text as="span" size="T400" truncate>
|
{/* In-bubble (thread) uses Stream's compact T200 size
|
||||||
|
so the bubble header matches the DM-chat rhythm.
|
||||||
|
Channels main timeline keeps T400 for the prominent
|
||||||
|
avatar-and-name row above an unbordered body. */}
|
||||||
|
<Text
|
||||||
|
as="span"
|
||||||
|
size={channelHeaderInBubble ? 'T200' : 'T400'}
|
||||||
|
truncate
|
||||||
|
>
|
||||||
<UsernameBold>
|
<UsernameBold>
|
||||||
{isOwnMessage ? t('Direct.message_me_label') : senderDisplayName}
|
{isOwnMessage ? t('Direct.message_me_label') : senderDisplayName}
|
||||||
</UsernameBold>
|
</UsernameBold>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue