feat: Implement inertial horizontal scrolling for carousel components and enhance text selection handling

This commit is contained in:
2026-05-17 16:22:57 +10:00
parent b9b0dce40b
commit af02b215f4
6 changed files with 424 additions and 11 deletions

View File

@@ -52,7 +52,7 @@ import { eventWithShortcode, factoryEventSentBy, getMxIdLocalPart } from '../../
import { useMatrixClient } from '../../hooks/useMatrixClient';
import { useVirtualPaginator, ItemRange } from '../../hooks/useVirtualPaginator';
import { useAlive } from '../../hooks/useAlive';
import { autoScrollToBottom, editableActiveElement } from '../../utils/dom';
import { autoScrollToBottom, editableActiveElement, isScrolledToBottom } from '../../utils/dom';
import {
DefaultPlaceholder,
CompactPlaceholder,
@@ -110,6 +110,7 @@ import { useDocumentFocusChange } from '../../hooks/useDocumentFocusChange';
import { RenderMessageContent } from '../../components/RenderMessageContent';
import { Image } from '../../components/media';
import { ImageViewer } from '../../components/image-viewer';
import { useInertialHorizontalScroll } from '../../hooks/useInertialHorizontalScroll';
import { roomToParentsAtom } from '../../state/room/roomToParents';
import { useRoomUnread } from '../../state/hooks/unread';
import { roomToUnreadAtom } from '../../state/room/roomToUnread';
@@ -651,7 +652,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
useCallback(
(mEvt: MatrixEvent) => {
const isOwnMessage = mEvt.getSender() === mx.getUserId();
const shouldStickToBottom = atBottomRef.current || isOwnMessage;
const isBottomPinnedEvent =
mEvt.getType() === MessageEvent.RoomMessage || mEvt.getType() === MessageEvent.Sticker;
const shouldStickToBottom = atBottomRef.current || (isOwnMessage && isBottomPinnedEvent);
// Always update timeline with new message
setTimeline((ct) => ({
@@ -676,10 +679,12 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
setUnreadInfo(getRoomUnreadInfo(room));
}
// Preserve the pre-update bottom state so a newly added message keeps the view pinned.
scrollToBottomRef.current.count += 1;
scrollToBottomRef.current.smooth = !isOwnMessage;
scrollToBottomRef.current.force = shouldStickToBottom;
// Preserve the pre-update bottom state so newly added chat messages keep the view pinned.
if (shouldStickToBottom) {
scrollToBottomRef.current.count += 1;
scrollToBottomRef.current.smooth = !isOwnMessage;
scrollToBottomRef.current.force = true;
}
// Show unread message preview for incoming messages when not at bottom
if (!isOwnMessage && !atBottomRef.current) {
@@ -753,7 +758,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const scrollElement = getScrollElement();
if (!editorBaseEntry || !scrollElement) return;
if (atBottomRef.current) {
if (isScrolledToBottom(scrollElement)) {
autoScrollToBottom(scrollElement, false, true);
}
};
@@ -774,7 +779,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const previousHeight = timelineContentHeightRef.current;
timelineContentHeightRef.current = nextHeight;
if (previousHeight === 0 || nextHeight <= previousHeight || !atBottomRef.current) {
if (previousHeight === 0 || nextHeight <= previousHeight || !isScrolledToBottom(scrollElement)) {
return;
}
@@ -2023,7 +2028,12 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
ref={timelineContentRef}
direction="Column"
justifyContent="End"
style={{ minHeight: '100%', padding: `${config.space.S600} 0` }}
style={{
minHeight: '100%',
padding: `${config.space.S600} 0`,
userSelect: 'none',
WebkitUserSelect: 'none',
}}
>
{!canPaginateBack && rangeAtStart && getItems().length > 0 && (
<div