mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 13:35:42 +09:00
fix(app): keep the chat scroller pinned to the end while it resizes (#1860)
This commit is contained in:
1 parent
a2099fb0e2
commit
2af0405317
36 files changed
+171
-191
No files matched your search
@@ -308,8 +308,8 @@ export const Autocomplete = observer(
|
||||
const scrollChildIntoView = useCallback((node: HTMLElement | null, margin = 32) => {
|
||||
if (!node) return;
|
||||
const scroller = scrollerRef.current as ScrollerWithScrollableElement | null;
|
||||
if (scroller && typeof scroller.scrollIntoViewNode === 'function') {
|
||||
scroller.scrollIntoViewNode({node, padding: margin});
|
||||
if (scroller && typeof scroller.revealElement === 'function') {
|
||||
scroller.revealElement({node, padding: margin});
|
||||
return;
|
||||
}
|
||||
let scrollerEl: HTMLElement | null = null;
|
||||
|
||||
@@ -64,9 +64,9 @@ export const AutocompleteGif = observer(
|
||||
if (!scrollerRef.current) {
|
||||
return;
|
||||
}
|
||||
scrollerRef.current.scrollIntoViewNode({
|
||||
scrollerRef.current.revealElement({
|
||||
node: selectedElement,
|
||||
shouldScrollToStart: false,
|
||||
preferStartEdge: false,
|
||||
padding: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -597,7 +597,7 @@ export const ChannelAttachmentArea = observer(({channelId}: {channelId: string})
|
||||
const getScrollElement = useCallback(() => {
|
||||
const scroller = scrollerRef.current;
|
||||
if (scroller == null) return null;
|
||||
return scroller.getScrollerNode();
|
||||
return scroller.getViewportElement();
|
||||
}, []);
|
||||
useDragAutoScroll({active: isDragging, axis: 'horizontal', getScrollElement});
|
||||
channelIdRef.current = channelId;
|
||||
|
||||
@@ -374,7 +374,7 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe
|
||||
[flushScrollRangeUpdate],
|
||||
);
|
||||
const scheduleRangeUpdateFromScroller = useCallback(() => {
|
||||
const scrollerState = scrollerRef.current?.getScrollerState();
|
||||
const scrollerState = scrollerRef.current?.readViewportMetrics();
|
||||
if (!scrollerState) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -254,7 +254,7 @@ export const Messages = observer(function Messages({
|
||||
}, [channel.id, state]);
|
||||
const onMessageEdit = useCallback(
|
||||
(targetNode: HTMLElement) => {
|
||||
const scrollerNode = scrollManager.ref.current?.getScrollerNode();
|
||||
const scrollerNode = scrollManager.ref.current?.getViewportElement();
|
||||
if (!scrollerNode) return;
|
||||
if (scrollManager.pinIsAtBottomNow()) {
|
||||
return;
|
||||
@@ -272,7 +272,7 @@ export const Messages = observer(function Messages({
|
||||
const isAbove = targetRect.top < scrollerRect.top;
|
||||
const isBelow = targetRect.bottom > scrollerRect.bottom;
|
||||
if (isAbove || isBelow) {
|
||||
scrollManager.ref.current?.scrollIntoViewNode({
|
||||
scrollManager.ref.current?.revealElement({
|
||||
node: targetNode,
|
||||
padding: 80,
|
||||
animate: false,
|
||||
@@ -369,7 +369,7 @@ export const Messages = observer(function Messages({
|
||||
const onFocusBottommostMessage = (payload?: unknown) => {
|
||||
const data = (payload ?? {}) as {channelId?: string};
|
||||
if (!data.channelId || data.channelId !== channel.id) return;
|
||||
const scroller = scrollManager.ref.current?.getScrollerNode();
|
||||
const scroller = scrollManager.ref.current?.getViewportElement();
|
||||
const scrollerInner = scrollerInnerRef.current;
|
||||
if (!scroller || !scrollerInner) return;
|
||||
const messageElements = scrollerInner.querySelectorAll<HTMLElement>('[data-message-id]');
|
||||
|
||||
@@ -206,7 +206,7 @@ export const ChannelSearchResults = observer(
|
||||
setSortMode(cachedSortMode);
|
||||
}, [contextId, searchContext?.lastSearchSortMode]);
|
||||
const getScrollPosition = useCallback((): number => {
|
||||
const node = scrollerRef.current?.getScrollerNode();
|
||||
const node = scrollerRef.current?.getViewportElement();
|
||||
return node ? node.scrollTop : 0;
|
||||
}, []);
|
||||
const updateScrollPosition = useCallback(
|
||||
@@ -827,7 +827,7 @@ export const ChannelSearchResults = observer(
|
||||
clearChannelSearchHighlight();
|
||||
return;
|
||||
}
|
||||
const scrollerNode = scrollerRef.current?.getScrollerNode();
|
||||
const scrollerNode = scrollerRef.current?.getViewportElement();
|
||||
if (!scrollerNode) return;
|
||||
const timer = setTimeout(() => {
|
||||
const terms = tokenizeSearchQuery(searchQuery);
|
||||
|
||||
@@ -187,7 +187,7 @@ export const EmojiPicker = observer(
|
||||
const handleCategoryClick = (category: string) => {
|
||||
const element = categoryRefs.current.get(category);
|
||||
if (element) {
|
||||
scrollerRef.current?.scrollIntoViewNode({node: element, shouldScrollToStart: true});
|
||||
scrollerRef.current?.revealElement({node: element, preferStartEdge: true});
|
||||
}
|
||||
};
|
||||
const handleHover = (emoji: FlatEmoji | null, row?: number, column?: number) => {
|
||||
|
||||
@@ -223,7 +223,7 @@ export const MobileEmojiPicker = observer(
|
||||
const handleCategoryClick = (category: string) => {
|
||||
const element = categoryRefs.current.get(category);
|
||||
if (element) {
|
||||
scrollerRef.current?.scrollIntoViewNode({node: element, shouldScrollToStart: true});
|
||||
scrollerRef.current?.revealElement({node: element, preferStartEdge: true});
|
||||
}
|
||||
};
|
||||
const handleHover = (emoji: FlatEmoji | null) => {
|
||||
|
||||
@@ -173,7 +173,7 @@ export const MobileStickersPicker = observer(
|
||||
const handleCategoryClick = (category: string) => {
|
||||
const element = categoryRefs.current.get(category);
|
||||
if (element) {
|
||||
scrollerRef.current?.scrollIntoViewNode({node: element, shouldScrollToStart: true});
|
||||
scrollerRef.current?.revealElement({node: element, preferStartEdge: true});
|
||||
}
|
||||
};
|
||||
const handleHover = (sticker: GuildSticker | null) => {
|
||||
|
||||
@@ -172,7 +172,7 @@ export const StickersPicker = observer(
|
||||
const handleCategoryClick = (category: string) => {
|
||||
const element = categoryRefs.current.get(category);
|
||||
if (element) {
|
||||
scrollerRef.current?.scrollIntoViewNode({node: element, shouldScrollToStart: true});
|
||||
scrollerRef.current?.revealElement({node: element, preferStartEdge: true});
|
||||
}
|
||||
};
|
||||
const handleHover = (sticker: GuildSticker | null, row?: number, column?: number) => {
|
||||
|
||||
+1
-1
@@ -367,7 +367,7 @@ export const ChannelSearchBottomSheet: React.FC<ChannelSearchBottomSheetProps> =
|
||||
clearChannelSearchHighlight();
|
||||
return;
|
||||
}
|
||||
const container = scrollerRef.current?.getScrollerNode();
|
||||
const container = scrollerRef.current?.getViewportElement();
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
import {MasonryListComputer} from '@app/features/channel/components/MasonryListComputer';
|
||||
import {bench, describe} from 'vitest';
|
||||
|
||||
const ITEM_COUNT = 500;
|
||||
const VIEWPORT_WIDTH = 520;
|
||||
const VIEWPORT_HEIGHT = 420;
|
||||
const COLUMNS = 3;
|
||||
const ITEM_GUTTER = 8;
|
||||
const ITEM_KEYS = Array.from({length: ITEM_COUNT}, (_, index) => `gif-${index}`);
|
||||
const ITEM_HEIGHTS = Array.from({length: ITEM_COUNT}, (_, index) => 90 + ((index * 37) % 180));
|
||||
const SCROLL_OFFSETS = Array.from({length: 1_000}, (_, index) => (index * 173) % 30_000);
|
||||
|
||||
function configureComputer(computer: MasonryListComputer, version: number): void {
|
||||
computer.mergeProps({
|
||||
sections: [ITEM_COUNT],
|
||||
columns: COLUMNS,
|
||||
itemGutter: ITEM_GUTTER,
|
||||
bufferWidth: VIEWPORT_WIDTH,
|
||||
padding: {left: 12, right: 12, top: 0, bottom: 0},
|
||||
version,
|
||||
getItemKey: (_section, itemIndex) => ITEM_KEYS[itemIndex] ?? null,
|
||||
getItemHeight: (_section, itemIndex) => ITEM_HEIGHTS[itemIndex] ?? 0,
|
||||
getSectionHeight: () => 0,
|
||||
});
|
||||
}
|
||||
|
||||
describe('MasonryListComputer benchmarks', () => {
|
||||
bench('computes full masonry layout for 500 GIFs', () => {
|
||||
const computer = new MasonryListComputer();
|
||||
configureComputer(computer, 1);
|
||||
computer.computeVisibleSections(0, VIEWPORT_HEIGHT);
|
||||
computer.getState();
|
||||
});
|
||||
|
||||
bench('computes 1k visible windows over a stable GIF layout', () => {
|
||||
const computer = new MasonryListComputer();
|
||||
configureComputer(computer, 1);
|
||||
computer.computeVisibleSections(0, VIEWPORT_HEIGHT);
|
||||
for (const scrollTop of SCROLL_OFFSETS) {
|
||||
computer.computeVisibleSections(scrollTop, scrollTop + VIEWPORT_HEIGHT + 480);
|
||||
}
|
||||
computer.getState();
|
||||
});
|
||||
});
|
||||
@@ -60,7 +60,7 @@ export function useScrollerViewport(scrollerRef: React.RefObject<ScrollerHandle
|
||||
[scheduleViewportSize],
|
||||
);
|
||||
useEffect(() => {
|
||||
const state = scrollerRef.current?.getScrollerState();
|
||||
const state = scrollerRef.current?.readViewportMetrics();
|
||||
if (!state || state.offsetWidth === 0 || state.offsetHeight === 0) return;
|
||||
scheduleViewportSize({width: state.offsetWidth, height: state.offsetHeight});
|
||||
}, [scrollerRef, scheduleViewportSize]);
|
||||
|
||||
Reference in new issue
Block a user