feat(timeline): use Channel avatar+bubble layout for every non-1:1 room and shift 1:1 Stream rail ~4px left on desktop
This commit is contained in:
parent
2a24ee60ff
commit
1665cb185f
6 changed files with 58 additions and 37 deletions
|
|
@ -66,10 +66,11 @@ Router in `Router.tsx`. Each top-level tab (`/direct/`, `/space/...`, `/explore/
|
|||
→ RoomTimeline + RoomViewTyping + RoomInput
|
||||
```
|
||||
|
||||
After P3c the Stream layout is the only timeline layout. There is no DM-vs-non-DM render gate. The classification that used to drive Stream now collapses into a single **member-count** check, mirroring Element-Web's tier-2 pattern (`room.getInvitedAndJoinedMemberCount() === 2`):
|
||||
After P3c the timeline picks between two layouts via a single **member-count** check, mirroring Element-Web's tier-2 pattern (`room.getInvitedAndJoinedMemberCount() === 2`). There is no DM-vs-non-DM render gate — the classification is purely participant-count + channels-route:
|
||||
|
||||
- 1:1 rooms (member-count = 2) get peer-style header chrome (peer avatar fallback in `useRoomAvatar(room, isOneOnOne)`), the `DmCallButton`, and unconditionally hide membership/nick/avatar syslines.
|
||||
- Group rooms (member-count > 2) get the room-style header (no peer fallback), no `DmCallButton`, and respect the `hideMembershipEvents` / `hideNickAvatarEvents` user settings for syslines.
|
||||
- 1:1 rooms (member-count = 2) get peer-style header chrome (peer avatar fallback in `useRoomAvatar(room, isOneOnOne)`), the `DmCallButton`, the **Stream** timeline layout (rail + dot + bubble), and unconditionally hide membership/nick/avatar syslines.
|
||||
- Group rooms (member-count > 2) get the room-style header (no peer fallback), no `DmCallButton`, the **Channel** timeline layout (avatar + in-bubble header + bubble — same silhouette as channels), and respect the `hideMembershipEvents` / `hideNickAvatarEvents` user settings for syslines.
|
||||
- Rooms under `/channels/` always get the Channel layout regardless of member count — `channelsMode` short-circuits the 1:1 check and additionally enables channels-only filtering (thread surfacing, RTC/edit hiding). See `RoomTimeline.tsx::channelStyleLayout`.
|
||||
- Bridged Telegram puppet rooms automatically classify correctly because the gate is server-side authoritative.
|
||||
|
||||
`useAutoDirectSync` (commit 84eeac9) still round-trips `m.direct` on join — **interop only**, so other Matrix clients (Element, FluffyChat) still categorize the same room as a DM. Vojo no longer reads `m.direct` for UI classification; the `mDirectAtom` is kept alive for `useDirectRooms` ordering and other read-only consumers but its truth is no longer load-bearing for the layout.
|
||||
|
|
@ -81,12 +82,12 @@ Use `useIsOneOnOne()` from `hooks/useRoom.ts` whenever you need the 1:1 vs group
|
|||
| Dir | Purpose |
|
||||
|-----|---------|
|
||||
| `room/` | Core room view — **RoomTimeline.tsx** (~1700 LOC after P3c collapse), **RoomInput.tsx** (~691 LOC), **RoomViewHeader.tsx** (thin wrapper after P4) → **RoomViewHeaderDm.tsx** (Dawn header for every room class; 1:1 chrome via avatar fallback + peer-profile-sheet, group chrome via `N members` line; phone button three-gated per §6.8b; search/pinned/invite/leave moved into the `…` menu), MembersDrawer (suppressed for 1:1 in `Room.tsx`), MessageEditor, RoomTombstone, RoomViewTyping, CallChatView, CommandAutocomplete |
|
||||
| `room/message/` | `Message.tsx` (~1170 LOC after P3c) — renders Stream layout unconditionally for every room (1:1 DM, group DM, non-DM, bridged). No layout switch, no `isStream` gate. Renders edit/delete/react menu, mention/hashtag links, reactions viewer. The legacy `Compact`/`Bubble` layouts and `MessageLayout` enum are gone; `Modern.tsx` survives only as a card-preview layout for pin-menu / message-search / inbox. |
|
||||
| `room/message/` | `Message.tsx` (~1170 LOC after P3c) — branches between **Stream** (1:1 rooms) and **Channel** (groups + channels) layouts on the `layout` prop driven by `RoomTimeline.tsx::channelStyleLayout`. Renders edit/delete/react menu, mention/hashtag links, reactions viewer. The legacy `Compact`/`Bubble` layouts and `MessageLayout` enum are gone; `Modern.tsx` survives only as a card-preview layout for pin-menu / message-search / inbox. |
|
||||
| `room-nav/` | `RoomNavItem.tsx` (~435 LOC) — list-row component, used by Home/Direct/Spaces. Carries call-room behaviour (`useCallSession`, `useCallMembers`, `useCallStart`) |
|
||||
| `room-settings/` | Room-specific settings page |
|
||||
| `common-settings/` | Shared settings: general, members, permissions, emojis-stickers, developer-tools |
|
||||
| `space-settings/` | Space-specific settings |
|
||||
| `settings/` | User settings (general, account, notifications, devices, emojis, about, dev-tools). `MessageLayout` / `messageSpacing` / `legacyUsernameColor` were removed in P3c — Stream is now the only layout, and user-settings cleanup migration drops orphan persisted fields on first load. `hideMembershipEvents` / `hideNickAvatarEvents` survive — they still gate the group-room syslines. **Logout lives here only.** |
|
||||
| `settings/` | User settings (general, account, notifications, devices, emojis, about, dev-tools). `MessageLayout` / `messageSpacing` / `legacyUsernameColor` were removed in P3c — layout is no longer user-configurable (Stream/Channel pick automatically on member count), and the cleanup migration drops orphan persisted fields on first load. `hideMembershipEvents` / `hideNickAvatarEvents` survive — they still gate the group-room syslines. **Logout lives here only.** |
|
||||
| `lobby/` | Space/room lobby view |
|
||||
| `search/` | Global search |
|
||||
| `message-search/` | In-room message search |
|
||||
|
|
|
|||
|
|
@ -12,8 +12,12 @@ export const ChannelRow = style({
|
|||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: config.space.S300,
|
||||
paddingLeft: config.space.S400,
|
||||
paddingRight: config.space.S400,
|
||||
// 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,
|
||||
minWidth: 0,
|
||||
|
|
@ -105,8 +109,11 @@ export const ChannelDayDividerLabel = style({
|
|||
// body would, indented past the avatar slot, so the column reads
|
||||
// continuous.
|
||||
export const ChannelSysline = style({
|
||||
paddingLeft: `calc(${ChannelAvatarWidth} + ${config.space.S300} + ${config.space.S400})`,
|
||||
paddingRight: config.space.S400,
|
||||
// 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})`,
|
||||
paddingRight: config.space.S200,
|
||||
paddingTop: config.space.S100,
|
||||
paddingBottom: config.space.S100,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
|
|
|
|||
|
|
@ -30,10 +30,13 @@ export type ChannelLayoutProps = {
|
|||
// 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.
|
||||
// (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.
|
||||
headerInBubble?: boolean;
|
||||
onContextMenu?: MouseEventHandler<HTMLDivElement>;
|
||||
};
|
||||
|
|
@ -143,7 +146,11 @@ export type ChannelMessageAvatarProps = {
|
|||
// folds `<Avatar>` + `<UserAvatar>` 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) {
|
||||
export function ChannelMessageAvatar({
|
||||
room,
|
||||
senderId,
|
||||
senderDisplayName,
|
||||
}: ChannelMessageAvatarProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const avatarMxc = getMemberAvatarMxc(room, senderId);
|
||||
|
|
|
|||
|
|
@ -168,15 +168,18 @@ export const UsernameBold = style({
|
|||
// loosen on mobile without disturbing the screen-edge anchor (which the
|
||||
// user dialled in earlier and asked to keep).
|
||||
//
|
||||
// Mobile: pad = S100 (minimal screen-edge anchor); gap = 2 × S100 = S200
|
||||
// (the user asked to double the inter-element gap on native).
|
||||
// Desktop: pad = S500 (≈ 0.5 cm PageNav clearance, unchanged); gap =
|
||||
// Mobile: pad = S100 (minimal screen-edge anchor — already at the limit,
|
||||
// dropping further would push timestamp glyphs flush against the screen
|
||||
// edge); gap = 2 × S100 = S200 (the user asked to double the inter-element
|
||||
// gap on native).
|
||||
// Desktop: pad = S400 (16px ≈ 0.42 cm — shifted ~4px / 1mm closer to the
|
||||
// PageNav per user request, the column still clears the nav rail); gap =
|
||||
// S500 / 1.1 ≈ 18.2 px (the user asked to shrink the desktop inter-element
|
||||
// gap by 1.1× — keeps the layout tighter without dropping a whole token).
|
||||
const StreamRowPadVar = createVar();
|
||||
const StreamRowGapVar = createVar();
|
||||
const StreamRowPadMobile = config.space.S100;
|
||||
const StreamRowPadDesktop = config.space.S500;
|
||||
const StreamRowPadDesktop = config.space.S400;
|
||||
const StreamRowGapMobile = config.space.S200;
|
||||
const StreamRowGapDesktop = `calc(${config.space.S500} / 1.1)`;
|
||||
|
||||
|
|
|
|||
|
|
@ -530,16 +530,19 @@ export function RoomTimeline({
|
|||
// visible row in channels-mode is safe (`useVirtualPaginator` keeps
|
||||
// the rendered window bounded; SDK Room maxListeners is 100).
|
||||
const showThreadSummary = channelsMode && !isBridged;
|
||||
// M3: channels timeline uses avatar-first ChannelLayout (no rail, no
|
||||
// bubble). DM/Bots stay on Stream rail. Bridged Telegram channels use
|
||||
// Channel layout too — visually consistent with native channels — only
|
||||
// the thread plus / cards differ (bridge has no thread semantic).
|
||||
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;
|
||||
// Avatar-first ChannelLayout is route-agnostic for any non-1:1 room:
|
||||
// group DMs in /direct/, group child rooms under /space/, and every
|
||||
// channel under /channels/. 1:1 DMs and 1:1 bot control rooms keep
|
||||
// the Stream rail — the dot/time chrome only carries meaning when there
|
||||
// are exactly two participants. Channels-mode-specific behaviour
|
||||
// (thread surfacing, RTC/edit hiding) stays gated on `channelsMode` —
|
||||
// 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;
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -702,16 +702,16 @@ export type MessageProps = {
|
|||
// whether to mount the card based on `channelsMode && !isBridged` —
|
||||
// outside channels-mode this stays `undefined` and the slot collapses.
|
||||
threadSummary?: React.ReactNode;
|
||||
// M3: choose timeline visual. `'stream'` = rail + dot + bubble (current
|
||||
// DM/Bots layout). `'channel'` = avatar + name + time inline + body
|
||||
// without bubble (Slack/Discord-style channels timeline). Default
|
||||
// `'stream'` so non-channels callers don't have to opt in.
|
||||
// Choose timeline visual. `'stream'` = rail + dot + bubble — used for
|
||||
// 1:1 DMs and 1:1 bot control rooms. `'channel'` = avatar + bubble with
|
||||
// an in-bubble username/time header — used for every non-1:1 room
|
||||
// (group DMs, group rooms under /space/, channels). Default `'stream'`
|
||||
// so card-preview callers (pin menu, message search, inbox) get the
|
||||
// legacy DM look.
|
||||
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.
|
||||
// Forwarded to `ChannelLayout.headerInBubble`. Every current timeline
|
||||
// caller passes `true` alongside `layout='channel'`; kept as a prop so
|
||||
// un-bubbled `ChannelLayout` consumers stay possible.
|
||||
channelHeaderInBubble?: boolean;
|
||||
};
|
||||
export const Message = as<'div', MessageProps>(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue