fix(composer): re-anchor timeline scrollTop when overlay composer height changes via prop-driven layout effect
This commit is contained in:
parent
41a9af19e3
commit
de2354f1da
2 changed files with 47 additions and 20 deletions
|
|
@ -222,6 +222,14 @@ type RoomTimelineProps = {
|
|||
eventId?: string;
|
||||
roomInputRef: RefObject<HTMLElement>;
|
||||
editor: Editor;
|
||||
// Pixel height of the overlay composer painted by RoomView. The CSS var
|
||||
// applied on the chat surface paints the bottom padding visually, but
|
||||
// when this number transitions 0 → N at mount the scroll-content grows
|
||||
// without a corresponding scrollTop bump — the latest message ends up
|
||||
// hidden behind the composer. We use this prop in a layout effect to
|
||||
// re-anchor the scroll to the bottom whenever it changes and the user
|
||||
// was already at the bottom.
|
||||
bottomOverlayHeight?: number;
|
||||
};
|
||||
|
||||
const PAGINATION_LIMIT = 80;
|
||||
|
|
@ -471,7 +479,13 @@ const isChannelsModeHidden = (room: Room, event: MatrixEvent, isBridged: boolean
|
|||
return false;
|
||||
};
|
||||
|
||||
export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
|
||||
export function RoomTimeline({
|
||||
room,
|
||||
eventId,
|
||||
roomInputRef,
|
||||
editor,
|
||||
bottomOverlayHeight = 0,
|
||||
}: RoomTimelineProps) {
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||
|
|
@ -868,6 +882,20 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
useCallback(() => roomInputRef.current, [roomInputRef])
|
||||
);
|
||||
|
||||
// Re-anchor to bottom when the overlay-composer height changes. At mount
|
||||
// the composer measures async (RoomView's `useEffect` → `setComposerHeight`
|
||||
// → CSS var → our paddingBottom grows), which adds N pixels to scrollHeight
|
||||
// without bumping scrollTop, leaving the latest message hidden behind the
|
||||
// composer. A layout effect on the prop catches every transition (initial
|
||||
// 0 → N and subsequent multi-line/reply-preview growth) deterministically,
|
||||
// unlike a ResizeObserver dispatch which can coalesce the mount and the
|
||||
// resize into a single skipped first firing.
|
||||
useLayoutEffect(() => {
|
||||
if (!atBottomRef.current) return;
|
||||
const el = getScrollElement();
|
||||
if (el) scrollToBottom(el);
|
||||
}, [bottomOverlayHeight, getScrollElement]);
|
||||
|
||||
// Stay at bottom when scroll container resizes (e.g. Android keyboard open/close)
|
||||
useResizeObserver(
|
||||
useMemo(() => {
|
||||
|
|
@ -2059,14 +2087,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
|||
justifyContent="End"
|
||||
style={{
|
||||
minHeight: '100%',
|
||||
// Bottom padding adds the composer's live height (set as the
|
||||
// `--vojo-composer-height` var on the chat surface by RoomView)
|
||||
// so the latest message sits flush above the overlaid composer
|
||||
// instead of disappearing behind it. Falls back to 0 when the
|
||||
// var is unset (e.g. thread-drawer view doesn't mount the
|
||||
// overlay composer).
|
||||
// Bottom padding reserves room for the overlay composer painted
|
||||
// by RoomView. Driven by the same number passed in via
|
||||
// `bottomOverlayHeight` to keep React state and CSS layout in
|
||||
// sync — a layout effect (`bottomOverlayHeight` dep, above)
|
||||
// re-anchors the scroll on every transition so the latest
|
||||
// message stays flush above the composer.
|
||||
paddingTop: config.space.S600,
|
||||
paddingBottom: `calc(${config.space.S600} + var(--vojo-composer-height, 0px))`,
|
||||
paddingBottom: `calc(${config.space.S600} + ${bottomOverlayHeight}px)`,
|
||||
}}
|
||||
>
|
||||
{!canPaginateBack && rangeAtStart && getItems().length > 0 && (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Box, Text, color, config, toRem } from 'folds';
|
||||
import { EventType } from 'matrix-js-sdk';
|
||||
import { ReactEditor } from 'slate-react';
|
||||
|
|
@ -127,18 +127,16 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
|||
|
||||
// Composer renders as a true overlay (absolute, bottom-stuck) above the
|
||||
// timeline so messages can scroll behind it — WhatsApp / Telegram pattern.
|
||||
// The CSS variable lets RoomTimeline.tsx pad its scroll-content bottom by
|
||||
// the exact composer height, so the latest message sits flush above the
|
||||
// composer at rest and older messages slide out from underneath as the
|
||||
// user scrolls up.
|
||||
const pageStyle = {
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
position: 'relative',
|
||||
'--vojo-composer-height': `${composerHeight}px`,
|
||||
} as CSSProperties;
|
||||
|
||||
// RoomTimeline takes `bottomOverlayHeight` as a prop and both (a) reserves
|
||||
// bottom scroll-padding for the overlay so the latest message sits flush
|
||||
// above it at rest and (b) re-anchors scrollTop in a layout effect when
|
||||
// the height transitions (e.g. composer measures async after mount, multi-
|
||||
// line typing, reply-preview opens).
|
||||
return (
|
||||
<Page ref={roomViewRef} style={pageStyle}>
|
||||
<Page
|
||||
ref={roomViewRef}
|
||||
style={{ backgroundColor: color.SurfaceVariant.Container, position: 'relative' }}
|
||||
>
|
||||
<Box grow="Yes" direction="Column">
|
||||
<RoomTimeline
|
||||
key={roomId}
|
||||
|
|
@ -146,6 +144,7 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
|||
eventId={eventId}
|
||||
roomInputRef={roomInputRef}
|
||||
editor={editor}
|
||||
bottomOverlayHeight={composerHeight}
|
||||
/>
|
||||
<RoomViewTyping room={room} />
|
||||
</Box>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue