fix(composer): re-anchor timeline scrollTop when overlay composer height changes via prop-driven layout effect
This commit is contained in:
parent
7aaba05e46
commit
2bce6a791d
2 changed files with 47 additions and 20 deletions
|
|
@ -222,6 +222,14 @@ type RoomTimelineProps = {
|
||||||
eventId?: string;
|
eventId?: string;
|
||||||
roomInputRef: RefObject<HTMLElement>;
|
roomInputRef: RefObject<HTMLElement>;
|
||||||
editor: Editor;
|
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;
|
const PAGINATION_LIMIT = 80;
|
||||||
|
|
@ -471,7 +479,13 @@ const isChannelsModeHidden = (room: Room, event: MatrixEvent, isBridged: boolean
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimelineProps) {
|
export function RoomTimeline({
|
||||||
|
room,
|
||||||
|
eventId,
|
||||||
|
roomInputRef,
|
||||||
|
editor,
|
||||||
|
bottomOverlayHeight = 0,
|
||||||
|
}: RoomTimelineProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
|
||||||
|
|
@ -868,6 +882,20 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
useCallback(() => roomInputRef.current, [roomInputRef])
|
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)
|
// Stay at bottom when scroll container resizes (e.g. Android keyboard open/close)
|
||||||
useResizeObserver(
|
useResizeObserver(
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
|
|
@ -2059,14 +2087,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
|
||||||
justifyContent="End"
|
justifyContent="End"
|
||||||
style={{
|
style={{
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
// Bottom padding adds the composer's live height (set as the
|
// Bottom padding reserves room for the overlay composer painted
|
||||||
// `--vojo-composer-height` var on the chat surface by RoomView)
|
// by RoomView. Driven by the same number passed in via
|
||||||
// so the latest message sits flush above the overlaid composer
|
// `bottomOverlayHeight` to keep React state and CSS layout in
|
||||||
// instead of disappearing behind it. Falls back to 0 when the
|
// sync — a layout effect (`bottomOverlayHeight` dep, above)
|
||||||
// var is unset (e.g. thread-drawer view doesn't mount the
|
// re-anchors the scroll on every transition so the latest
|
||||||
// overlay composer).
|
// message stays flush above the composer.
|
||||||
paddingTop: config.space.S600,
|
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 && (
|
{!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 { Box, Text, color, config, toRem } from 'folds';
|
||||||
import { EventType } from 'matrix-js-sdk';
|
import { EventType } from 'matrix-js-sdk';
|
||||||
import { ReactEditor } from 'slate-react';
|
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
|
// Composer renders as a true overlay (absolute, bottom-stuck) above the
|
||||||
// timeline so messages can scroll behind it — WhatsApp / Telegram pattern.
|
// timeline so messages can scroll behind it — WhatsApp / Telegram pattern.
|
||||||
// The CSS variable lets RoomTimeline.tsx pad its scroll-content bottom by
|
// RoomTimeline takes `bottomOverlayHeight` as a prop and both (a) reserves
|
||||||
// the exact composer height, so the latest message sits flush above the
|
// bottom scroll-padding for the overlay so the latest message sits flush
|
||||||
// composer at rest and older messages slide out from underneath as the
|
// above it at rest and (b) re-anchors scrollTop in a layout effect when
|
||||||
// user scrolls up.
|
// the height transitions (e.g. composer measures async after mount, multi-
|
||||||
const pageStyle = {
|
// line typing, reply-preview opens).
|
||||||
backgroundColor: color.SurfaceVariant.Container,
|
|
||||||
position: 'relative',
|
|
||||||
'--vojo-composer-height': `${composerHeight}px`,
|
|
||||||
} as CSSProperties;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page ref={roomViewRef} style={pageStyle}>
|
<Page
|
||||||
|
ref={roomViewRef}
|
||||||
|
style={{ backgroundColor: color.SurfaceVariant.Container, position: 'relative' }}
|
||||||
|
>
|
||||||
<Box grow="Yes" direction="Column">
|
<Box grow="Yes" direction="Column">
|
||||||
<RoomTimeline
|
<RoomTimeline
|
||||||
key={roomId}
|
key={roomId}
|
||||||
|
|
@ -146,6 +144,7 @@ export function RoomView({ eventId }: { eventId?: string }) {
|
||||||
eventId={eventId}
|
eventId={eventId}
|
||||||
roomInputRef={roomInputRef}
|
roomInputRef={roomInputRef}
|
||||||
editor={editor}
|
editor={editor}
|
||||||
|
bottomOverlayHeight={composerHeight}
|
||||||
/>
|
/>
|
||||||
<RoomViewTyping room={room} />
|
<RoomViewTyping room={room} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue