feat(room): extend the centred message band, date pill and composer width to group and channel rooms, matching 1:1
This commit is contained in:
parent
4b7ad11620
commit
2581ff8137
6 changed files with 42 additions and 126 deletions
|
|
@ -16,10 +16,12 @@ export const ChannelRow = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
gap: ChannelAvatarGap,
|
gap: ChannelAvatarGap,
|
||||||
// Span the full pane edge-to-edge so the hover highlight runs the whole
|
// Span the full message-column width so the hover highlight runs edge-to-edge
|
||||||
// width like Discord: cancel MessageBase's S400/S200 horizontal padding with
|
// like Discord: cancel MessageBase's S400/S200 horizontal padding with negative
|
||||||
// negative margins, then re-add the 16px avatar gutter as paddingLeft (so the
|
// margins, then re-add the 16px avatar gutter as paddingLeft (so the avatar's
|
||||||
// avatar's left edge lands exactly 16px from the screen edge — Discord cozy).
|
// left edge lands 16px from the column edge — Discord cozy). NB: the column is
|
||||||
|
// the centred BubbleTimelineBand, so that edge is the band's content edge, not
|
||||||
|
// the screen edge (the band adds 12px native / 40px desktop outside this).
|
||||||
marginLeft: `calc(-1 * ${config.space.S400})`,
|
marginLeft: `calc(-1 * ${config.space.S400})`,
|
||||||
marginRight: `calc(-1 * ${config.space.S200})`,
|
marginRight: `calc(-1 * ${config.space.S200})`,
|
||||||
paddingLeft: ChannelEdgePad,
|
paddingLeft: ChannelEdgePad,
|
||||||
|
|
@ -82,35 +84,6 @@ export const ChannelThreadSummary = style({
|
||||||
marginTop: config.space.S100,
|
marginTop: config.space.S100,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Horizontal line + centered label. Spans the full row width including
|
|
||||||
// the avatar slot so the line reads as a section break, not a per-message
|
|
||||||
// chrome element.
|
|
||||||
export const ChannelDayDividerRoot = style({
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: config.space.S300,
|
|
||||||
paddingLeft: config.space.S400,
|
|
||||||
paddingRight: config.space.S400,
|
|
||||||
paddingTop: config.space.S400,
|
|
||||||
paddingBottom: config.space.S400,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ChannelDayDividerLine = style({
|
|
||||||
flex: 1,
|
|
||||||
height: '1px',
|
|
||||||
backgroundColor: color.SurfaceVariant.ContainerLine,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ChannelDayDividerLabel = style({
|
|
||||||
fontSize: toRem(11),
|
|
||||||
fontWeight: 600,
|
|
||||||
letterSpacing: '0.06em',
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
color: color.SurfaceVariant.OnContainer,
|
|
||||||
opacity: 0.7,
|
|
||||||
flexShrink: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sysline (membership / room.create / pinned-events). Compact single
|
// Sysline (membership / room.create / pinned-events). Compact single
|
||||||
// row aligned with the body gutter — the sysline sits where messages'
|
// row aligned with the body gutter — the sysline sits where messages'
|
||||||
// body would, indented past the avatar slot, so the column reads
|
// body would, indented past the avatar slot, so the column reads
|
||||||
|
|
|
||||||
|
|
@ -98,29 +98,6 @@ export const ChannelLayout = as<'div', ChannelLayoutProps>(
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
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) => (
|
|
||||||
<div
|
|
||||||
className={classNames(css.ChannelDayDividerRoot, className)}
|
|
||||||
role="separator"
|
|
||||||
aria-label={typeof label === 'string' ? label : undefined}
|
|
||||||
{...props}
|
|
||||||
ref={ref}
|
|
||||||
>
|
|
||||||
<span className={css.ChannelDayDividerLine} aria-hidden />
|
|
||||||
<span className={css.ChannelDayDividerLabel}>{label}</span>
|
|
||||||
<span className={css.ChannelDayDividerLine} aria-hidden />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
export type ChannelEventContentProps = {
|
export type ChannelEventContentProps = {
|
||||||
iconSrc: IconSrc;
|
iconSrc: IconSrc;
|
||||||
content: ReactNode;
|
content: ReactNode;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import {
|
||||||
VOJO_STICKY_DATE_TOP_PX,
|
VOJO_STICKY_DATE_TOP_PX,
|
||||||
} from '../../styles/horseshoe';
|
} from '../../styles/horseshoe';
|
||||||
|
|
||||||
// Bubble (1:1 DM) timeline band — centre the message column in the same band
|
// Timeline band (every room class — 1:1 bubble, group, channel) — centre the
|
||||||
|
// message column in the same band
|
||||||
// the AI-bot chat uses (ThreadDrawerContentAssistant), so on wide web viewports
|
// the AI-bot chat uses (ThreadDrawerContentAssistant), so on wide web viewports
|
||||||
// the chat is centred instead of spreading edge-to-edge. Inert on mobile (band
|
// the chat is centred instead of spreading edge-to-edge. Inert on mobile (band
|
||||||
// > viewport). The composer mirrors this via ComposerBubbleBand; both read the
|
// > viewport). The composer mirrors this via ComposerBubbleBand; both read the
|
||||||
|
|
@ -32,7 +33,8 @@ export const BubbleTimelineBand = style({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Day capsule for the bubble (1:1 DM) timeline («Среда, 4 июня» / «Сегодня») —
|
// Day capsule for the timeline («Среда, 4 июня» / «Сегодня»), shared by every
|
||||||
|
// room class (1:1 bubble, group, channel) —
|
||||||
// a single dark-blue pill in the message-input tone (Surface.Container, the same
|
// a single dark-blue pill in the message-input tone (Surface.Container, the same
|
||||||
// token the composer card paints with), centred, with generous rounding. No
|
// token the composer card paints with), centred, with generous rounding. No
|
||||||
// border, no echo.
|
// border, no echo.
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,6 @@ import {
|
||||||
MSticker,
|
MSticker,
|
||||||
ImageContent,
|
ImageContent,
|
||||||
EventContent,
|
EventContent,
|
||||||
CHANNEL_MESSAGE_SPACING,
|
|
||||||
ChannelDayDivider,
|
|
||||||
} from '../../components/message';
|
} from '../../components/message';
|
||||||
import {
|
import {
|
||||||
factoryRenderLinkifyWithMention,
|
factoryRenderLinkifyWithMention,
|
||||||
|
|
@ -1302,7 +1300,8 @@ export function RoomTimeline({
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
// Sticky day capsules (bubble layout only). Each day boundary renders a REAL
|
// Sticky day capsules (every room class — 1:1 bubble, group, channel). Each
|
||||||
|
// day boundary renders a REAL
|
||||||
// capsule that is a CSS `position: sticky` element (see RoomTimeline.css
|
// capsule that is a CSS `position: sticky` element (see RoomTimeline.css
|
||||||
// `[data-sticky-dates='on'] BubbleDayCapsuleRow`). The browser pins it on the
|
// `[data-sticky-dates='on'] BubbleDayCapsuleRow`). The browser pins it on the
|
||||||
// compositor while you scroll through a day, then lets it settle back into its
|
// compositor while you scroll through a day, then lets it settle back into its
|
||||||
|
|
@ -1319,7 +1318,6 @@ export function RoomTimeline({
|
||||||
// `offsetTop` is the in-flow position (sticky doesn't change it), so the
|
// `offsetTop` is the in-flow position (sticky doesn't change it), so the
|
||||||
// front detection and the virtual paginator's offsetTop maths stay in agreement.
|
// front detection and the virtual paginator's offsetTop maths stay in agreement.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messageLayout === 'channel') return undefined;
|
|
||||||
const scrollEl = getScrollElement();
|
const scrollEl = getScrollElement();
|
||||||
if (!scrollEl) return undefined;
|
if (!scrollEl) return undefined;
|
||||||
|
|
||||||
|
|
@ -1370,7 +1368,7 @@ export function RoomTimeline({
|
||||||
delete scrollEl.dataset.stickyDates;
|
delete scrollEl.dataset.stickyDates;
|
||||||
showAll();
|
showAll();
|
||||||
};
|
};
|
||||||
}, [getScrollElement, messageLayout]);
|
}, [getScrollElement]);
|
||||||
|
|
||||||
// Group `m.call.member` (StateEvent.GroupCallMemberPrefix) events into one
|
// Group `m.call.member` (StateEvent.GroupCallMemberPrefix) events into one
|
||||||
// aggregate bubble per CALL SESSION. Each session is delimited by «joined
|
// aggregate bubble per CALL SESSION. Each session is delimited by «joined
|
||||||
|
|
@ -2635,18 +2633,13 @@ export function RoomTimeline({
|
||||||
return timeDayMonYear(mEvent.getTs());
|
return timeDayMonYear(mEvent.getTs());
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const renderDayDivider = () =>
|
const renderDayDivider = () => (
|
||||||
messageLayout === 'channel' ? (
|
// Every room class (1:1 bubble, group, channel) draws the same centred,
|
||||||
<MessageBase space={CHANNEL_MESSAGE_SPACING}>
|
// single dark-blue date pill. The row is the real `position: sticky`
|
||||||
<ChannelDayDivider label={dayLabel} />
|
// element — `data-day-divider` is the hook the scroll effect uses to engage
|
||||||
</MessageBase>
|
// stickiness and pick the front pill when several pile up. No MessageBase
|
||||||
) : (
|
// wrapper: the row must be a direct child of the timeline column so its
|
||||||
// Bubble (1:1 DM): a centred, single dark-blue date pill. The row is the
|
// sticky containing block is the whole day, not a one-row box.
|
||||||
// real `position: sticky` element — `data-day-divider` is the hook the
|
|
||||||
// scroll effect uses to engage stickiness and pick the front pill when
|
|
||||||
// several pile up. No MessageBase wrapper: the row must be a direct child
|
|
||||||
// of the timeline column so its sticky containing block is the whole day,
|
|
||||||
// not a one-row box.
|
|
||||||
<div className={css.BubbleDayCapsuleRow} data-day-divider="true">
|
<div className={css.BubbleDayCapsuleRow} data-day-divider="true">
|
||||||
<span className={css.BubbleDayCapsule} role="separator">
|
<span className={css.BubbleDayCapsule} role="separator">
|
||||||
{dayLabel}
|
{dayLabel}
|
||||||
|
|
@ -2673,7 +2666,7 @@ export function RoomTimeline({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box grow="Yes" style={{ position: 'relative' }}>
|
<Box grow="Yes" style={{ position: 'relative' }}>
|
||||||
{/* Bubble (1:1 DM) day dates are the inline capsules themselves, made
|
{/* Day dates are the inline capsules themselves (every room class), made
|
||||||
sticky via real CSS `position: sticky` (engaged by the effect above) —
|
sticky via real CSS `position: sticky` (engaged by the effect above) —
|
||||||
no separate floating pill. */}
|
no separate floating pill. */}
|
||||||
{unreadFloatShown && (
|
{unreadFloatShown && (
|
||||||
|
|
@ -2703,8 +2696,8 @@ export function RoomTimeline({
|
||||||
<Box
|
<Box
|
||||||
direction="Column"
|
direction="Column"
|
||||||
justifyContent="End"
|
justifyContent="End"
|
||||||
// Bubble (1:1 DM) layout: centre the message column in the AI-chat band.
|
// Every room class centres its message column in the AI-chat band.
|
||||||
className={messageLayout === 'stream' ? css.BubbleTimelineBand : undefined}
|
className={css.BubbleTimelineBand}
|
||||||
style={{
|
style={{
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
// Bottom padding reserves room for the overlay composer painted
|
// Bottom padding reserves room for the overlay composer painted
|
||||||
|
|
|
||||||
|
|
@ -24,22 +24,11 @@ import {
|
||||||
// same Android-WebView stuck-:hover suppression.
|
// same Android-WebView stuck-:hover suppression.
|
||||||
export const ChatComposer = style({});
|
export const ChatComposer = style({});
|
||||||
|
|
||||||
// Desktop web only: constrain the composer card to ~3/4 of the chat pane
|
// Composer band (every room class) — fit the input form to the same ~960px
|
||||||
// width and centre it, instead of spanning edge-to-edge (user point 15).
|
// centred band as the timeline (BubbleTimelineBand) + the AI-bot chat
|
||||||
// Applied conditionally in RoomView.tsx via `useScreenSizeContext` so native
|
// (ThreadComposerAssistant), so on wide web the composer width matches the
|
||||||
// / mobile / tablet keep the full-width card the user is happy with.
|
// messages. Horizontal gutter matches BubbleTimelineBand / the bot chat: 12px on
|
||||||
export const ComposerDesktopClamp = style({
|
// native, 40px on desktop, so the composer stays aligned with the message column.
|
||||||
maxWidth: '75%',
|
|
||||||
marginLeft: 'auto',
|
|
||||||
marginRight: 'auto',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bubble (1:1 DM) composer band — fit the input form to the same ~960px centred
|
|
||||||
// band as the bubble timeline + the AI-bot chat (ThreadComposerAssistant), so on
|
|
||||||
// wide web the composer width matches the messages. Horizontal gutter matches
|
|
||||||
// BubbleTimelineBand / the bot chat: 12px on native, 40px on desktop, so the
|
|
||||||
// composer stays aligned with the message column. Replaces ComposerDesktopClamp
|
|
||||||
// for 1:1 DMs.
|
|
||||||
export const ComposerBubbleBand = style({
|
export const ComposerBubbleBand = style({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxWidth: toRem(VOJO_BUBBLE_BAND_PX),
|
maxWidth: toRem(VOJO_BUBBLE_BAND_PX),
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||||
import { Box, Text, color, config, toRem } from 'folds';
|
import { Box, Text, color, config } from 'folds';
|
||||||
import { EventType } from 'matrix-js-sdk';
|
import { EventType } from 'matrix-js-sdk';
|
||||||
import { ReactEditor } from 'slate-react';
|
import { ReactEditor } from 'slate-react';
|
||||||
import { isKeyHotkey } from 'is-hotkey';
|
import { isKeyHotkey } from 'is-hotkey';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
|
||||||
import { useStateEvent } from '../../hooks/useStateEvent';
|
import { useStateEvent } from '../../hooks/useStateEvent';
|
||||||
import { StateEvent } from '../../../types/matrix/room';
|
import { StateEvent } from '../../../types/matrix/room';
|
||||||
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
|
||||||
|
|
@ -19,9 +18,8 @@ import { useKeyDown } from '../../hooks/useKeyDown';
|
||||||
import { editableActiveElement } from '../../utils/dom';
|
import { editableActiveElement } from '../../utils/dom';
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useIsOneOnOne, useRoom } from '../../hooks/useRoom';
|
import { useRoom } from '../../hooks/useRoom';
|
||||||
import { useChannelsMode, useThreadDrawerOpen } from '../../hooks/useChannelsMode';
|
import { useThreadDrawerOpen } from '../../hooks/useChannelsMode';
|
||||||
import { VOJO_HORSESHOE_GAP_PX } from '../../styles/horseshoe';
|
|
||||||
import * as css from './RoomView.css';
|
import * as css from './RoomView.css';
|
||||||
|
|
||||||
const FN_KEYS_REGEX = /^F\d+$/;
|
const FN_KEYS_REGEX = /^F\d+$/;
|
||||||
|
|
@ -91,16 +89,6 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||||
// `TimelineRenderingType.Thread` context.
|
// `TimelineRenderingType.Thread` context.
|
||||||
const threadDrawerOpen = useThreadDrawerOpen();
|
const threadDrawerOpen = useThreadDrawerOpen();
|
||||||
|
|
||||||
// Desktop web: centre the composer at ~3/4 width (user point 15). Native /
|
|
||||||
// mobile / tablet keep the full-width card.
|
|
||||||
const isDesktop = useScreenSizeContext() === ScreenSize.Desktop;
|
|
||||||
|
|
||||||
// 1:1 DM (bubble layout): fit the composer to the same centred ~960px band as
|
|
||||||
// the bubble timeline + the AI-bot chat, instead of the 75% desktop clamp.
|
|
||||||
const isOneOnOne = useIsOneOnOne();
|
|
||||||
const channelsMode = useChannelsMode();
|
|
||||||
const isBubble = isOneOnOne && !channelsMode;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = composerWrapRef.current;
|
const el = composerWrapRef.current;
|
||||||
if (!el) {
|
if (!el) {
|
||||||
|
|
@ -183,17 +171,11 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||||
onFocusCapture={() => setComposerHidden(false)}
|
onFocusCapture={() => setComposerHidden(false)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
// Every room class fits its composer to the same centred ~960px band
|
||||||
css.ChatComposer,
|
// as the timeline (BubbleTimelineBand) and the AI-bot chat, so on wide
|
||||||
isBubble ? css.ComposerBubbleBand : isDesktop && css.ComposerDesktopClamp
|
// web the input lines up with the message column. The band owns its own
|
||||||
)}
|
// responsive horizontal padding, so no inline padding here.
|
||||||
style={
|
className={classNames(css.ChatComposer, css.ComposerBubbleBand)}
|
||||||
// ComposerBubbleBand owns its own (responsive) horizontal padding;
|
|
||||||
// the non-bubble path keeps the fixed horseshoe-void inline padding.
|
|
||||||
isBubble
|
|
||||||
? undefined
|
|
||||||
: { padding: `0 ${toRem(VOJO_HORSESHOE_GAP_PX)} ${toRem(VOJO_HORSESHOE_GAP_PX)}` }
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{tombstoneEvent ? (
|
{tombstoneEvent ? (
|
||||||
<RoomTombstone
|
<RoomTombstone
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue