mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +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
+169
-189
No files matched your search
@@ -392,7 +392,7 @@ export const MobileSectionNav: React.FC<MobileSectionNavProps> = observer(
|
|||||||
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
const scrollerRef = useRef<ScrollerHandle | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!activeSectionId) return;
|
if (!activeSectionId) return;
|
||||||
const node = scrollerRef.current?.getScrollerNode();
|
const node = scrollerRef.current?.getViewportElement();
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const activeButton = node.querySelector(`[data-section-id="${activeSectionId}"]`);
|
const activeButton = node.querySelector(`[data-section-id="${activeSectionId}"]`);
|
||||||
if (activeButton instanceof HTMLElement) {
|
if (activeButton instanceof HTMLElement) {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export const SettingsModalDesktopScroll: React.FC<SettingsModalDesktopScrollProp
|
|||||||
if (!scrollerRef) {
|
if (!scrollerRef) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const node = internalRef.current?.getScrollerNode() ?? null;
|
const node = internalRef.current?.getViewportElement() ?? null;
|
||||||
if (typeof scrollerRef === 'function') {
|
if (typeof scrollerRef === 'function') {
|
||||||
scrollerRef(node);
|
scrollerRef(node);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -734,7 +734,7 @@ export const UnreadChannelsContent = observer(function UnreadChannelsContent() {
|
|||||||
const target = event.target as Element | null;
|
const target = event.target as Element | null;
|
||||||
if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) return;
|
if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) return;
|
||||||
if (target instanceof HTMLElement && target.isContentEditable) return;
|
if (target instanceof HTMLElement && target.isContentEditable) return;
|
||||||
const scrollerNode = scrollerRef.current?.getScrollerNode() ?? null;
|
const scrollerNode = scrollerRef.current?.getViewportElement() ?? null;
|
||||||
if (!scrollerNode) return;
|
if (!scrollerNode) return;
|
||||||
if (target && !scrollerNode.contains(target) && document.activeElement !== document.body) return;
|
if (target && !scrollerNode.contains(target) && document.activeElement !== document.body) return;
|
||||||
const sections = Array.from(scrollerNode.querySelectorAll<HTMLElement>('[data-inbox-channel-section]'));
|
const sections = Array.from(scrollerNode.querySelectorAll<HTMLElement>('[data-inbox-channel-section]'));
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const AuthLayoutContent = observer(function AuthLayoutContent({children}: {child
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollerRef.current?.scrollToTop();
|
scrollerRef.current?.jumpToStartEdge();
|
||||||
}, [location.pathname]);
|
}, [location.pathname]);
|
||||||
const splashScale = useMemo(() => {
|
const splashScale = useMemo(() => {
|
||||||
if (!splashDimensions) return null;
|
if (!splashDimensions) return null;
|
||||||
|
|||||||
@@ -242,7 +242,7 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
|||||||
() => [channels.length, ReadStates.version, userGuildSettings, hideMutedChannels, showFadedUnreadOnMutedChannels],
|
() => [channels.length, ReadStates.version, userGuildSettings, hideMutedChannels, showFadedUnreadOnMutedChannels],
|
||||||
[channels.length, ReadStates.version, userGuildSettings, hideMutedChannels, showFadedUnreadOnMutedChannels],
|
[channels.length, ReadStates.version, userGuildSettings, hideMutedChannels, showFadedUnreadOnMutedChannels],
|
||||||
);
|
);
|
||||||
const getChannelScrollContainer = useCallback(() => scrollerRef.current?.getScrollerNode() ?? null, [scrollerRef]);
|
const getChannelScrollContainer = useCallback(() => scrollerRef.current?.getViewportElement() ?? null, [scrollerRef]);
|
||||||
useDragAutoScroll({active: isDraggingAnything, getScrollElement: getChannelScrollContainer});
|
useDragAutoScroll({active: isDraggingAnything, getScrollElement: getChannelScrollContainer});
|
||||||
const handleChannelDrop = useCallback(
|
const handleChannelDrop = useCallback(
|
||||||
(item: DragItem, result: DropResult) => {
|
(item: DragItem, result: DropResult) => {
|
||||||
@@ -306,15 +306,15 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
|||||||
}, [guild.id]);
|
}, [guild.id]);
|
||||||
const handleResize = useCallback((_entry: ResizeObserverEntry, _type: 'container' | 'content') => {
|
const handleResize = useCallback((_entry: ResizeObserverEntry, _type: 'container' | 'content') => {
|
||||||
if (stickToBottomRef.current && scrollerRef.current) {
|
if (stickToBottomRef.current && scrollerRef.current) {
|
||||||
scrollerRef.current.scrollToBottom({animate: false});
|
scrollerRef.current.jumpToEndEdge({animate: false});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const guildDimensions = Dimension.getGuildDimensions(guild.id);
|
const guildDimensions = Dimension.guildDimensionsFor(guild.id);
|
||||||
if (guildDimensions.scrollTo) {
|
if (guildDimensions.scrollTo) {
|
||||||
const element = document.querySelector(`[data-channel-id="${guildDimensions.scrollTo}"]`);
|
const element = document.querySelector(`[data-channel-id="${guildDimensions.scrollTo}"]`);
|
||||||
if (element && scrollerRef.current) {
|
if (element && scrollerRef.current) {
|
||||||
scrollerRef.current.scrollIntoViewNode({node: element as HTMLElement, shouldScrollToStart: false});
|
scrollerRef.current.revealElement({node: element as HTMLElement, preferStartEdge: false});
|
||||||
}
|
}
|
||||||
DimensionCommands.clearChannelListScrollTo(guild.id);
|
DimensionCommands.clearChannelListScrollTo(guild.id);
|
||||||
} else if (guildDimensions.scrollTop && guildDimensions.scrollTop > 0 && scrollerRef.current) {
|
} else if (guildDimensions.scrollTop && guildDimensions.scrollTop > 0 && scrollerRef.current) {
|
||||||
@@ -337,7 +337,8 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
|||||||
const unreadCount = ReadStates.getUnreadCount(channelId);
|
const unreadCount = ReadStates.getUnreadCount(channelId);
|
||||||
const mentionCount = ReadStates.getMentionCount(channelId);
|
const mentionCount = ReadStates.getMentionCount(channelId);
|
||||||
const isMuted =
|
const isMuted =
|
||||||
UserGuildSettings.isParentCategoryMuted(guild.id, channelId) || UserGuildSettings.isChannelDirectlyMuted(guild.id, channelId);
|
UserGuildSettings.isParentCategoryMuted(guild.id, channelId) ||
|
||||||
|
UserGuildSettings.isChannelDirectlyMuted(guild.id, channelId);
|
||||||
const channel = Channels.getChannel(channelId);
|
const channel = Channels.getChannel(channelId);
|
||||||
const unreadBadgesLevel = channel
|
const unreadBadgesLevel = channel
|
||||||
? UserGuildSettings.resolvedUnreadBadgesLevel({
|
? UserGuildSettings.resolvedUnreadBadgesLevel({
|
||||||
@@ -360,7 +361,9 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
|
|||||||
for (const group of channelGroups) {
|
for (const group of channelGroups) {
|
||||||
const isCollapsed = group.category ? (collapsedCategories?.has(group.category.id) ?? false) : false;
|
const isCollapsed = group.category ? (collapsedCategories?.has(group.category.id) ?? false) : false;
|
||||||
const isNullSpace = !group.category;
|
const isNullSpace = !group.category;
|
||||||
const isCategoryMuted = group.category ? UserGuildSettings.isChannelDirectlyMuted(guild.id, group.category.id) : false;
|
const isCategoryMuted = group.category
|
||||||
|
? UserGuildSettings.isChannelDirectlyMuted(guild.id, group.category.id)
|
||||||
|
: false;
|
||||||
let filteredTextChannels: Array<Channel>;
|
let filteredTextChannels: Array<Channel>;
|
||||||
let filteredVoiceChannels: Array<Channel>;
|
let filteredVoiceChannels: Array<Channel>;
|
||||||
if (hideMutedChannels) {
|
if (hideMutedChannels) {
|
||||||
|
|||||||
@@ -651,7 +651,7 @@ export const FavoritesChannelListContent = observer(() => {
|
|||||||
const getScrollElement = useCallback(() => {
|
const getScrollElement = useCallback(() => {
|
||||||
const scroller = scrollerRef.current;
|
const scroller = scrollerRef.current;
|
||||||
if (scroller == null) return null;
|
if (scroller == null) return null;
|
||||||
return scroller.getScrollerNode();
|
return scroller.getViewportElement();
|
||||||
}, []);
|
}, []);
|
||||||
useDragAutoScroll({active: isDraggingFavorite, getScrollElement});
|
useDragAutoScroll({active: isDraggingFavorite, getScrollElement});
|
||||||
const favorites = Favorites.sortedChannels;
|
const favorites = Favorites.sortedChannels;
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ function resolveScrollerNode(scroller: ScrollerHandle | null): HTMLElement | nul
|
|||||||
if (scroller == null) {
|
if (scroller == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return scroller.getScrollerNode();
|
return scroller.getViewportElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveOwnerWindow(container: HTMLElement): Window & typeof globalThis {
|
function resolveOwnerWindow(container: HTMLElement): Window & typeof globalThis {
|
||||||
@@ -221,7 +221,7 @@ function cancelOwnedSettleTimer(timer: OwnedSettleTimer | null): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function alignMountedTarget({animate, direction, scroller, targetNode}: AlignMountedTargetRequest): boolean {
|
function alignMountedTarget({animate, direction, scroller, targetNode}: AlignMountedTargetRequest): boolean {
|
||||||
const container = scroller.getScrollerNode();
|
const container = scroller.getViewportElement();
|
||||||
if (container == null) return false;
|
if (container == null) return false;
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
const targetRect = targetNode.getBoundingClientRect();
|
const targetRect = targetNode.getBoundingClientRect();
|
||||||
@@ -451,7 +451,7 @@ export const FloatingUnreadIndicators = observer(
|
|||||||
if (scrollTarget(edge.id, direction)) {
|
if (scrollTarget(edge.id, direction)) {
|
||||||
scheduleRefresh(FloatingUnreadRefreshScope.GEOMETRY);
|
scheduleRefresh(FloatingUnreadRefreshScope.GEOMETRY);
|
||||||
if (scroller == null) return;
|
if (scroller == null) return;
|
||||||
const scrollNode = scroller.getScrollerNode();
|
const scrollNode = scroller.getViewportElement();
|
||||||
if (scrollNode == null) return;
|
if (scrollNode == null) return;
|
||||||
const ownerWindow = resolveOwnerWindow(scrollNode);
|
const ownerWindow = resolveOwnerWindow(scrollNode);
|
||||||
cancelOwnedAnimationFrame(targetAlignmentFrameRef.current);
|
cancelOwnedAnimationFrame(targetAlignmentFrameRef.current);
|
||||||
@@ -467,7 +467,7 @@ export const FloatingUnreadIndicators = observer(
|
|||||||
}
|
}
|
||||||
scheduleRefresh(FloatingUnreadRefreshScope.SCROLL);
|
scheduleRefresh(FloatingUnreadRefreshScope.SCROLL);
|
||||||
if (scroller == null) return;
|
if (scroller == null) return;
|
||||||
const scrollNode = scroller.getScrollerNode();
|
const scrollNode = scroller.getViewportElement();
|
||||||
if (scrollNode == null) return;
|
if (scrollNode == null) return;
|
||||||
const ownerWindow = resolveOwnerWindow(scrollNode);
|
const ownerWindow = resolveOwnerWindow(scrollNode);
|
||||||
cancelOwnedSettleTimer(settleTimerRef.current);
|
cancelOwnedSettleTimer(settleTimerRef.current);
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ function resolveGuildFolderId(folderId: number | null): number {
|
|||||||
|
|
||||||
function resolveGuildListScrollerNode(scroller: ScrollerHandle | null): HTMLElement | null {
|
function resolveGuildListScrollerNode(scroller: ScrollerHandle | null): HTMLElement | null {
|
||||||
if (scroller == null) return null;
|
if (scroller == null) return null;
|
||||||
return scroller.getScrollerNode();
|
return scroller.getViewportElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
type GuildNavigationRow =
|
type GuildNavigationRow =
|
||||||
@@ -1583,7 +1583,7 @@ const GuildList = observer(() => {
|
|||||||
const visibleDMListRows = useFrameBatchedDMListRows(targetDMListRows);
|
const visibleDMListRows = useFrameBatchedDMListRows(targetDMListRows);
|
||||||
const shouldRenderGuildListItems = hasUnavailableGuilds || organizedItems.length > 0;
|
const shouldRenderGuildListItems = hasUnavailableGuilds || organizedItems.length > 0;
|
||||||
const initialGuildListScrollTop = useMemo(() => {
|
const initialGuildListScrollTop = useMemo(() => {
|
||||||
const liveScrollTop = untracked(() => Dimension.getGuildListDimensions().scrollTop);
|
const liveScrollTop = untracked(() => Dimension.currentGuildListDimensions().scrollTop);
|
||||||
if (liveScrollTop > 0) {
|
if (liveScrollTop > 0) {
|
||||||
return liveScrollTop;
|
return liveScrollTop;
|
||||||
}
|
}
|
||||||
@@ -1859,7 +1859,7 @@ const GuildList = observer(() => {
|
|||||||
const node = scrollTargetRegistry.getTargetNode(targetId);
|
const node = scrollTargetRegistry.getTargetNode(targetId);
|
||||||
const scroller = scrollRef.current;
|
const scroller = scrollRef.current;
|
||||||
if (node == null || scroller == null) return false;
|
if (node == null || scroller == null) return false;
|
||||||
scroller.scrollIntoViewNode({
|
scroller.revealElement({
|
||||||
node,
|
node,
|
||||||
alignment,
|
alignment,
|
||||||
animate: Accessibility.useSmoothScrolling,
|
animate: Accessibility.useSmoothScrolling,
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ export const GuildRailSkeleton: React.FC<GuildRailSkeletonProps> = ({
|
|||||||
const [mountState] = useState(() => {
|
const [mountState] = useState(() => {
|
||||||
const rememberedLayout = getRememberedSkeletonGuildRailLayout();
|
const rememberedLayout = getRememberedSkeletonGuildRailLayout();
|
||||||
const remScale = getRemScaleForDocument(document);
|
const remScale = getRemScaleForDocument(document);
|
||||||
const liveScrollTop = Math.max(0, Dimension.getGuildListDimensions().scrollTop);
|
const liveScrollTop = Math.max(0, Dimension.currentGuildListDimensions().scrollTop);
|
||||||
let scrollTop = liveScrollTop;
|
let scrollTop = liveScrollTop;
|
||||||
if (liveScrollTop === 0 && rememberedLayout != null) {
|
if (liveScrollTop === 0 && rememberedLayout != null) {
|
||||||
scrollTop = rememberedLayout.scrollTopPx * remScale;
|
scrollTop = rememberedLayout.scrollTopPx * remScale;
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function WhatsNewModal({entry}: WhatsNewModalProps) {
|
|||||||
ModalCommands.pop();
|
ModalCommands.pop();
|
||||||
}, [entry.id]);
|
}, [entry.id]);
|
||||||
const updateScrollHintVisibility = useCallback(() => {
|
const updateScrollHintVisibility = useCallback(() => {
|
||||||
const scrollerNode = scrollerRef.current?.getScrollerNode();
|
const scrollerNode = scrollerRef.current?.getViewportElement();
|
||||||
if (!scrollerNode) {
|
if (!scrollerNode) {
|
||||||
setIsScrollHintVisible(false);
|
setIsScrollHintVisible(false);
|
||||||
return;
|
return;
|
||||||
@@ -89,7 +89,7 @@ export function WhatsNewModal({entry}: WhatsNewModalProps) {
|
|||||||
updateScrollHintVisibility();
|
updateScrollHintVisibility();
|
||||||
}, [updateScrollHintVisibility]);
|
}, [updateScrollHintVisibility]);
|
||||||
const handleScrollToBottom = useCallback(() => {
|
const handleScrollToBottom = useCallback(() => {
|
||||||
scrollerRef.current?.scrollToBottom({animate: true});
|
scrollerRef.current?.jumpToEndEdge({animate: true});
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateScrollHintVisibility();
|
updateScrollHintVisibility();
|
||||||
|
|||||||
@@ -265,7 +265,7 @@ function resolveScrollNode(scrollerRef: React.RefObject<ScrollerHandle | null>):
|
|||||||
if (scroller == null) {
|
if (scroller == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return scroller.getScrollerNode();
|
return scroller.getViewportElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResolveNavigationRowNodeQuery {
|
interface ResolveNavigationRowNodeQuery {
|
||||||
@@ -311,7 +311,7 @@ function scrollNavigationRowIntoView({
|
|||||||
if (node == null) {
|
if (node == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
scroller.scrollIntoViewNode({node, alignment: resolveNavigationScrollerAlignment(align)});
|
scroller.revealElement({node, alignment: resolveNavigationScrollerAlignment(align)});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -308,8 +308,8 @@ export const Autocomplete = observer(
|
|||||||
const scrollChildIntoView = useCallback((node: HTMLElement | null, margin = 32) => {
|
const scrollChildIntoView = useCallback((node: HTMLElement | null, margin = 32) => {
|
||||||
if (!node) return;
|
if (!node) return;
|
||||||
const scroller = scrollerRef.current as ScrollerWithScrollableElement | null;
|
const scroller = scrollerRef.current as ScrollerWithScrollableElement | null;
|
||||||
if (scroller && typeof scroller.scrollIntoViewNode === 'function') {
|
if (scroller && typeof scroller.revealElement === 'function') {
|
||||||
scroller.scrollIntoViewNode({node, padding: margin});
|
scroller.revealElement({node, padding: margin});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let scrollerEl: HTMLElement | null = null;
|
let scrollerEl: HTMLElement | null = null;
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ export const AutocompleteGif = observer(
|
|||||||
if (!scrollerRef.current) {
|
if (!scrollerRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
scrollerRef.current.scrollIntoViewNode({
|
scrollerRef.current.revealElement({
|
||||||
node: selectedElement,
|
node: selectedElement,
|
||||||
shouldScrollToStart: false,
|
preferStartEdge: false,
|
||||||
padding: 0,
|
padding: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -597,7 +597,7 @@ export const ChannelAttachmentArea = observer(({channelId}: {channelId: string})
|
|||||||
const getScrollElement = useCallback(() => {
|
const getScrollElement = useCallback(() => {
|
||||||
const scroller = scrollerRef.current;
|
const scroller = scrollerRef.current;
|
||||||
if (scroller == null) return null;
|
if (scroller == null) return null;
|
||||||
return scroller.getScrollerNode();
|
return scroller.getViewportElement();
|
||||||
}, []);
|
}, []);
|
||||||
useDragAutoScroll({active: isDragging, axis: 'horizontal', getScrollElement});
|
useDragAutoScroll({active: isDragging, axis: 'horizontal', getScrollElement});
|
||||||
channelIdRef.current = channelId;
|
channelIdRef.current = channelId;
|
||||||
|
|||||||
@@ -374,7 +374,7 @@ const LazyMemberList = observer(function LazyMemberList({guild, channel}: LazyMe
|
|||||||
[flushScrollRangeUpdate],
|
[flushScrollRangeUpdate],
|
||||||
);
|
);
|
||||||
const scheduleRangeUpdateFromScroller = useCallback(() => {
|
const scheduleRangeUpdateFromScroller = useCallback(() => {
|
||||||
const scrollerState = scrollerRef.current?.getScrollerState();
|
const scrollerState = scrollerRef.current?.readViewportMetrics();
|
||||||
if (!scrollerState) {
|
if (!scrollerState) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export const Messages = observer(function Messages({
|
|||||||
}, [channel.id, state]);
|
}, [channel.id, state]);
|
||||||
const onMessageEdit = useCallback(
|
const onMessageEdit = useCallback(
|
||||||
(targetNode: HTMLElement) => {
|
(targetNode: HTMLElement) => {
|
||||||
const scrollerNode = scrollManager.ref.current?.getScrollerNode();
|
const scrollerNode = scrollManager.ref.current?.getViewportElement();
|
||||||
if (!scrollerNode) return;
|
if (!scrollerNode) return;
|
||||||
if (scrollManager.pinIsAtBottomNow()) {
|
if (scrollManager.pinIsAtBottomNow()) {
|
||||||
return;
|
return;
|
||||||
@@ -272,7 +272,7 @@ export const Messages = observer(function Messages({
|
|||||||
const isAbove = targetRect.top < scrollerRect.top;
|
const isAbove = targetRect.top < scrollerRect.top;
|
||||||
const isBelow = targetRect.bottom > scrollerRect.bottom;
|
const isBelow = targetRect.bottom > scrollerRect.bottom;
|
||||||
if (isAbove || isBelow) {
|
if (isAbove || isBelow) {
|
||||||
scrollManager.ref.current?.scrollIntoViewNode({
|
scrollManager.ref.current?.revealElement({
|
||||||
node: targetNode,
|
node: targetNode,
|
||||||
padding: 80,
|
padding: 80,
|
||||||
animate: false,
|
animate: false,
|
||||||
@@ -369,7 +369,7 @@ export const Messages = observer(function Messages({
|
|||||||
const onFocusBottommostMessage = (payload?: unknown) => {
|
const onFocusBottommostMessage = (payload?: unknown) => {
|
||||||
const data = (payload ?? {}) as {channelId?: string};
|
const data = (payload ?? {}) as {channelId?: string};
|
||||||
if (!data.channelId || data.channelId !== channel.id) return;
|
if (!data.channelId || data.channelId !== channel.id) return;
|
||||||
const scroller = scrollManager.ref.current?.getScrollerNode();
|
const scroller = scrollManager.ref.current?.getViewportElement();
|
||||||
const scrollerInner = scrollerInnerRef.current;
|
const scrollerInner = scrollerInnerRef.current;
|
||||||
if (!scroller || !scrollerInner) return;
|
if (!scroller || !scrollerInner) return;
|
||||||
const messageElements = scrollerInner.querySelectorAll<HTMLElement>('[data-message-id]');
|
const messageElements = scrollerInner.querySelectorAll<HTMLElement>('[data-message-id]');
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ export const ChannelSearchResults = observer(
|
|||||||
setSortMode(cachedSortMode);
|
setSortMode(cachedSortMode);
|
||||||
}, [contextId, searchContext?.lastSearchSortMode]);
|
}, [contextId, searchContext?.lastSearchSortMode]);
|
||||||
const getScrollPosition = useCallback((): number => {
|
const getScrollPosition = useCallback((): number => {
|
||||||
const node = scrollerRef.current?.getScrollerNode();
|
const node = scrollerRef.current?.getViewportElement();
|
||||||
return node ? node.scrollTop : 0;
|
return node ? node.scrollTop : 0;
|
||||||
}, []);
|
}, []);
|
||||||
const updateScrollPosition = useCallback(
|
const updateScrollPosition = useCallback(
|
||||||
@@ -827,7 +827,7 @@ export const ChannelSearchResults = observer(
|
|||||||
clearChannelSearchHighlight();
|
clearChannelSearchHighlight();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const scrollerNode = scrollerRef.current?.getScrollerNode();
|
const scrollerNode = scrollerRef.current?.getViewportElement();
|
||||||
if (!scrollerNode) return;
|
if (!scrollerNode) return;
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
const terms = tokenizeSearchQuery(searchQuery);
|
const terms = tokenizeSearchQuery(searchQuery);
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ export const EmojiPicker = observer(
|
|||||||
const handleCategoryClick = (category: string) => {
|
const handleCategoryClick = (category: string) => {
|
||||||
const element = categoryRefs.current.get(category);
|
const element = categoryRefs.current.get(category);
|
||||||
if (element) {
|
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) => {
|
const handleHover = (emoji: FlatEmoji | null, row?: number, column?: number) => {
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ export const MobileEmojiPicker = observer(
|
|||||||
const handleCategoryClick = (category: string) => {
|
const handleCategoryClick = (category: string) => {
|
||||||
const element = categoryRefs.current.get(category);
|
const element = categoryRefs.current.get(category);
|
||||||
if (element) {
|
if (element) {
|
||||||
scrollerRef.current?.scrollIntoViewNode({node: element, shouldScrollToStart: true});
|
scrollerRef.current?.revealElement({node: element, preferStartEdge: true});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleHover = (emoji: FlatEmoji | null) => {
|
const handleHover = (emoji: FlatEmoji | null) => {
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ export const MobileStickersPicker = observer(
|
|||||||
const handleCategoryClick = (category: string) => {
|
const handleCategoryClick = (category: string) => {
|
||||||
const element = categoryRefs.current.get(category);
|
const element = categoryRefs.current.get(category);
|
||||||
if (element) {
|
if (element) {
|
||||||
scrollerRef.current?.scrollIntoViewNode({node: element, shouldScrollToStart: true});
|
scrollerRef.current?.revealElement({node: element, preferStartEdge: true});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleHover = (sticker: GuildSticker | null) => {
|
const handleHover = (sticker: GuildSticker | null) => {
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ export const StickersPicker = observer(
|
|||||||
const handleCategoryClick = (category: string) => {
|
const handleCategoryClick = (category: string) => {
|
||||||
const element = categoryRefs.current.get(category);
|
const element = categoryRefs.current.get(category);
|
||||||
if (element) {
|
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) => {
|
const handleHover = (sticker: GuildSticker | null, row?: number, column?: number) => {
|
||||||
|
|||||||
+1
-1
@@ -367,7 +367,7 @@ export const ChannelSearchBottomSheet: React.FC<ChannelSearchBottomSheetProps> =
|
|||||||
clearChannelSearchHighlight();
|
clearChannelSearchHighlight();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const container = scrollerRef.current?.getScrollerNode();
|
const container = scrollerRef.current?.getViewportElement();
|
||||||
if (!container) {
|
if (!container) {
|
||||||
return;
|
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],
|
[scheduleViewportSize],
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const state = scrollerRef.current?.getScrollerState();
|
const state = scrollerRef.current?.readViewportMetrics();
|
||||||
if (!state || state.offsetWidth === 0 || state.offsetHeight === 0) return;
|
if (!state || state.offsetWidth === 0 || state.offsetHeight === 0) return;
|
||||||
scheduleViewportSize({width: state.offsetWidth, height: state.offsetHeight});
|
scheduleViewportSize({width: state.offsetWidth, height: state.offsetHeight});
|
||||||
}, [scrollerRef, scheduleViewportSize]);
|
}, [scrollerRef, scheduleViewportSize]);
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export const DiscoveryPage = observer(function DiscoveryPage() {
|
|||||||
const hasMore = guilds.length < Discovery.total;
|
const hasMore = guilds.length < Discovery.total;
|
||||||
const virtualizer = useVirtualizer({
|
const virtualizer = useVirtualizer({
|
||||||
count: rowCount,
|
count: rowCount,
|
||||||
getScrollElement: () => scrollerRef.current?.getScrollerNode() ?? null,
|
getScrollElement: () => scrollerRef.current?.getViewportElement() ?? null,
|
||||||
estimateSize: () => SKELETON_DISCOVERY_ESTIMATED_ROW_HEIGHT_PX,
|
estimateSize: () => SKELETON_DISCOVERY_ESTIMATED_ROW_HEIGHT_PX,
|
||||||
overscan: OVERSCAN_ROWS,
|
overscan: OVERSCAN_ROWS,
|
||||||
scrollMargin: gridOffsetTop,
|
scrollMargin: gridOffsetTop,
|
||||||
@@ -131,7 +131,7 @@ export const DiscoveryPage = observer(function DiscoveryPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setContainerWidth(node.getBoundingClientRect().width);
|
setContainerWidth(node.getBoundingClientRect().width);
|
||||||
setGridOffsetTop(resolveScrollOffsetTop(node, scrollerRef.current?.getScrollerNode() ?? null));
|
setGridOffsetTop(resolveScrollOffsetTop(node, scrollerRef.current?.getViewportElement() ?? null));
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resultsColumnNode) return;
|
if (!resultsColumnNode) return;
|
||||||
@@ -149,7 +149,7 @@ export const DiscoveryPage = observer(function DiscoveryPage() {
|
|||||||
}, [resultsColumnNode, searchActive]);
|
}, [resultsColumnNode, searchActive]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resultsColumnNode) return;
|
if (!resultsColumnNode) return;
|
||||||
setGridOffsetTop(resolveScrollOffsetTop(resultsColumnNode, scrollerRef.current?.getScrollerNode() ?? null));
|
setGridOffsetTop(resolveScrollOffsetTop(resultsColumnNode, scrollerRef.current?.getViewportElement() ?? null));
|
||||||
}, [resultsColumnNode, searchActive, containerWidth]);
|
}, [resultsColumnNode, searchActive, containerWidth]);
|
||||||
const scrollResultsToTop = useCallback(() => {
|
const scrollResultsToTop = useCallback(() => {
|
||||||
scrollerRef.current?.scrollTo({to: 0});
|
scrollerRef.current?.scrollTo({to: 0});
|
||||||
@@ -185,7 +185,7 @@ export const DiscoveryPage = observer(function DiscoveryPage() {
|
|||||||
if (searchActive || columns <= 0) {
|
if (searchActive || columns <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const viewportHeightPx = measureSkeletonHeightPx(scrollerRef.current?.getScrollerNode() ?? null);
|
const viewportHeightPx = measureSkeletonHeightPx(scrollerRef.current?.getViewportElement() ?? null);
|
||||||
reportSkeletonDiscoveryGrid(columns, Math.ceil(viewportHeightPx / SKELETON_DISCOVERY_ESTIMATED_ROW_HEIGHT_PX));
|
reportSkeletonDiscoveryGrid(columns, Math.ceil(viewportHeightPx / SKELETON_DISCOVERY_ESTIMATED_ROW_HEIGHT_PX));
|
||||||
}, `${searchActive}:${columns}:${containerWidth}`);
|
}, `${searchActive}:${columns}:${containerWidth}`);
|
||||||
const virtualRows = virtualizer.getVirtualItems();
|
const virtualRows = virtualizer.getVirtualItems();
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ interface MessageListKeyboardNavigationOptions {
|
|||||||
allowWhenInactive?: boolean;
|
allowWhenInactive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getScrollerNode = (value: ScrollerHandle | HTMLElement | null | undefined): HTMLElement | null => {
|
const getViewportElement = (value: ScrollerHandle | HTMLElement | null | undefined): HTMLElement | null => {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
if ('getScrollerNode' in value && typeof value.getScrollerNode === 'function') {
|
if ('getViewportElement' in value && typeof value.getViewportElement === 'function') {
|
||||||
return value.getScrollerNode();
|
return value.getViewportElement();
|
||||||
}
|
}
|
||||||
if (value instanceof HTMLElement) {
|
if (value instanceof HTMLElement) {
|
||||||
return value;
|
return value;
|
||||||
@@ -109,7 +109,7 @@ export function useMessageListKeyboardNavigation(options: MessageListKeyboardNav
|
|||||||
const getMessageElementsSnapshot = (): MessageNodesSnapshot => {
|
const getMessageElementsSnapshot = (): MessageNodesSnapshot => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const cache = messageNodesCache;
|
const cache = messageNodesCache;
|
||||||
const container = getScrollerNode(containerRef?.current ?? null);
|
const container = getViewportElement(containerRef?.current ?? null);
|
||||||
if (!container && containerRef) {
|
if (!container && containerRef) {
|
||||||
messageNodesCache = {
|
messageNodesCache = {
|
||||||
...EMPTY_MESSAGE_NODES_SNAPSHOT,
|
...EMPTY_MESSAGE_NODES_SNAPSHOT,
|
||||||
@@ -202,7 +202,7 @@ export function useMessageListKeyboardNavigation(options: MessageListKeyboardNav
|
|||||||
const isNavigationKey = delta !== 0;
|
const isNavigationKey = delta !== 0;
|
||||||
if (isNavigationKey && hasShortcutModifier(event)) return;
|
if (isNavigationKey && hasShortcutModifier(event)) return;
|
||||||
if (!isNavigationKey && event.key !== 'Escape') return;
|
if (!isNavigationKey && event.key !== 'Escape') return;
|
||||||
const container = getScrollerNode(containerRef?.current ?? null);
|
const container = getViewportElement(containerRef?.current ?? null);
|
||||||
if (container && !canHandleInsideContainer(container)) {
|
if (container && !canHandleInsideContainer(container)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ class Messages {
|
|||||||
ChannelMessages.forEach((messages) => channelIds.push(messages.channelId));
|
ChannelMessages.forEach((messages) => channelIds.push(messages.channelId));
|
||||||
for (const channelId of channelIds) {
|
for (const channelId of channelIds) {
|
||||||
this.clearMessages(channelId);
|
this.clearMessages(channelId);
|
||||||
Dimension.clearChannelDimensions(channelId);
|
Dimension.forgetChannelDimensions(channelId);
|
||||||
}
|
}
|
||||||
this.messageRefsByAuthor.clear();
|
this.messageRefsByAuthor.clear();
|
||||||
this.indexedAuthorsByChannel.clear();
|
this.indexedAuthorsByChannel.clear();
|
||||||
@@ -314,7 +314,7 @@ class Messages {
|
|||||||
didHydrateSelectedChannel = true;
|
didHydrateSelectedChannel = true;
|
||||||
} else {
|
} else {
|
||||||
this.clearMessages(messages.channelId);
|
this.clearMessages(messages.channelId);
|
||||||
Dimension.clearChannelDimensions(messages.channelId);
|
Dimension.forgetChannelDimensions(messages.channelId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (this.pendingFullHydration && !didHydrateSelectedChannel && selectedChannelId) {
|
if (this.pendingFullHydration && !didHydrateSelectedChannel && selectedChannelId) {
|
||||||
@@ -357,7 +357,7 @@ class Messages {
|
|||||||
const messages = ChannelMessages.getOrCreate(channelId);
|
const messages = ChannelMessages.getOrCreate(channelId);
|
||||||
this.commitMessages(messages.mutate({loadingMore: true, ready: false, error: false}));
|
this.commitMessages(messages.mutate({loadingMore: true, ready: false, error: false}));
|
||||||
if (forceScrollToBottom) {
|
if (forceScrollToBottom) {
|
||||||
Dimension.updateChannelDimensions(channelId, 1, 1, 0);
|
Dimension.recordChannelDimensions(channelId, 1, 1, 0);
|
||||||
MessageCommands.fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL);
|
MessageCommands.fetchMessages(channelId, null, null, MAX_MESSAGES_PER_CHANNEL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -402,7 +402,7 @@ class Messages {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!GatewayConnection.isConnected || messages.loadingMore || messages.ready) {
|
if (!GatewayConnection.isConnected || messages.loadingMore || messages.ready) {
|
||||||
if (messages.ready && Dimension.isAtBottom(channelId)) {
|
if (messages.ready && Dimension.channelPinnedToEnd(channelId)) {
|
||||||
this.commitMessages(messages.truncateTop(MAX_MESSAGES_PER_CHANNEL));
|
this.commitMessages(messages.truncateTop(MAX_MESSAGES_PER_CHANNEL));
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
}
|
}
|
||||||
@@ -450,7 +450,7 @@ class Messages {
|
|||||||
const channel = Channels.getChannel(channelId);
|
const channel = Channels.getChannel(channelId);
|
||||||
if (channel && channel.guildId === guildId) {
|
if (channel && channel.guildId === guildId) {
|
||||||
this.clearMessages(channelId);
|
this.clearMessages(channelId);
|
||||||
Dimension.clearChannelDimensions(channelId);
|
Dimension.forgetChannelDimensions(channelId);
|
||||||
didUpdate = true;
|
didUpdate = true;
|
||||||
if (channelId === selectedChannelId) {
|
if (channelId === selectedChannelId) {
|
||||||
selectedChannelAffected = true;
|
selectedChannelAffected = true;
|
||||||
@@ -593,7 +593,7 @@ class Messages {
|
|||||||
if (!existing?.ready) {
|
if (!existing?.ready) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const updated = existing.receiveMessage(action.message, Dimension.isAtBottom(action.channelId));
|
const updated = existing.receiveMessage(action.message, Dimension.channelPinnedToEnd(action.channelId));
|
||||||
this.commitMessages(updated);
|
this.commitMessages(updated);
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
return false;
|
return false;
|
||||||
@@ -725,7 +725,7 @@ class Messages {
|
|||||||
ChannelMessages.forEach(({channelId}) => {
|
ChannelMessages.forEach(({channelId}) => {
|
||||||
if (Channels.getChannel(channelId) == null) {
|
if (Channels.getChannel(channelId) == null) {
|
||||||
this.clearMessages(channelId);
|
this.clearMessages(channelId);
|
||||||
Dimension.clearChannelDimensions(channelId);
|
Dimension.forgetChannelDimensions(channelId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.notifyChange();
|
this.notifyChange();
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class AutoAck {
|
|||||||
if (!channelId) {
|
if (!channelId) {
|
||||||
return {windowId, channelId: null, isAtBottom: false, canAutoAck: false};
|
return {windowId, channelId: null, isAtBottom: false, canAutoAck: false};
|
||||||
}
|
}
|
||||||
const isAtBottom = Dimension.isAtBottom(channelId) ?? false;
|
const isAtBottom = Dimension.channelPinnedToEnd(channelId) ?? false;
|
||||||
const readState = ReadStates.getIfExists(channelId);
|
const readState = ReadStates.getIfExists(channelId);
|
||||||
const ackedManually = readState?.ackedManually ?? false;
|
const ackedManually = readState?.ackedManually ?? false;
|
||||||
const channel = Channels.getChannel(channelId);
|
const channel = Channels.getChannel(channelId);
|
||||||
|
|||||||
@@ -94,9 +94,9 @@ export class ScrollManager {
|
|||||||
if (props.messages.jumpTargetId != null) {
|
if (props.messages.jumpTargetId != null) {
|
||||||
this.pinIsAtBottom = false;
|
this.pinIsAtBottom = false;
|
||||||
} else {
|
} else {
|
||||||
const stored = Dimension.getChannelDimensions(props.channel.id);
|
const stored = Dimension.channelDimensionsFor(props.channel.id);
|
||||||
this.restoreHadSavedPosition = stored != null;
|
this.restoreHadSavedPosition = stored != null;
|
||||||
const isAtBottom = Dimension.isAtBottom(props.channel.id);
|
const isAtBottom = Dimension.channelPinnedToEnd(props.channel.id);
|
||||||
this.pinIsAtBottom = isAtBottom ?? true;
|
this.pinIsAtBottom = isAtBottom ?? true;
|
||||||
this.restorePendingInitialScrollTop = this.pinIsAtBottom ? null : (stored?.scrollTop ?? null);
|
this.restorePendingInitialScrollTop = this.pinIsAtBottom ? null : (stored?.scrollTop ?? null);
|
||||||
}
|
}
|
||||||
@@ -173,12 +173,12 @@ export class ScrollManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollGetDocument(): Document | undefined {
|
scrollGetDocument(): Document | undefined {
|
||||||
const node = this.ref.current?.getScrollerNode();
|
const node = this.ref.current?.getViewportElement();
|
||||||
return node?.ownerDocument;
|
return node?.ownerDocument;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollGetState(): ScrollerState {
|
scrollGetState(): ScrollerState {
|
||||||
return this.ref.current?.getScrollerState() ?? DEFAULT_SCROLLER_STATE;
|
return this.ref.current?.readViewportMetrics() ?? DEFAULT_SCROLLER_STATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private placeholderHeightGet(): number {
|
private placeholderHeightGet(): number {
|
||||||
@@ -186,7 +186,7 @@ export class ScrollManager {
|
|||||||
if (scrollerHandle == null) {
|
if (scrollerHandle == null) {
|
||||||
return this.props.placeholderHeight;
|
return this.props.placeholderHeight;
|
||||||
}
|
}
|
||||||
const scrollerNode = scrollerHandle.getScrollerNode();
|
const scrollerNode = scrollerHandle.getViewportElement();
|
||||||
if (scrollerNode == null) {
|
if (scrollerNode == null) {
|
||||||
return this.props.placeholderHeight;
|
return this.props.placeholderHeight;
|
||||||
}
|
}
|
||||||
@@ -249,7 +249,7 @@ export class ScrollManager {
|
|||||||
callback?: () => void,
|
callback?: () => void,
|
||||||
shouldContinue?: () => boolean,
|
shouldContinue?: () => boolean,
|
||||||
): void {
|
): void {
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if (!scrollerNode) {
|
if (!scrollerNode) {
|
||||||
callback?.();
|
callback?.();
|
||||||
return;
|
return;
|
||||||
@@ -263,7 +263,7 @@ export class ScrollManager {
|
|||||||
|
|
||||||
anchorGetData(messageId: string, scrollTop: number, clampTo?: number, layout?: ContainerLayout): AnchorData | null {
|
anchorGetData(messageId: string, scrollTop: number, clampTo?: number, layout?: ContainerLayout): AnchorData | null {
|
||||||
const element = this.layoutGetElementFromMessageId(messageId);
|
const element = this.layoutGetElementFromMessageId(messageId);
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if (!element || !scrollerNode) return null;
|
if (!element || !scrollerNode) return null;
|
||||||
const offsetHeight = element.offsetHeight;
|
const offsetHeight = element.offsetHeight;
|
||||||
const offsetTop = this.layoutGetOffsetTop(element, scrollerNode, layout);
|
const offsetTop = this.layoutGetOffsetTop(element, scrollerNode, layout);
|
||||||
@@ -299,7 +299,7 @@ export class ScrollManager {
|
|||||||
const {messages, hasUnreads, channel} = this.props;
|
const {messages, hasUnreads, channel} = this.props;
|
||||||
const state = this.scrollGetState();
|
const state = this.scrollGetState();
|
||||||
const {scrollTop, offsetHeight} = state;
|
const {scrollTop, offsetHeight} = state;
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
const layout = scrollerNode ? this.layoutGetContainerLayout(scrollerNode) : undefined;
|
const layout = scrollerNode ? this.layoutGetContainerLayout(scrollerNode) : undefined;
|
||||||
const buffer =
|
const buffer =
|
||||||
hasUnreads && scrollTop >= this.layoutGetNewMessageBarBuffer() ? this.layoutGetNewMessageBarBuffer() : 0;
|
hasUnreads && scrollTop >= this.layoutGetNewMessageBarBuffer() ? this.layoutGetNewMessageBarBuffer() : 0;
|
||||||
@@ -336,7 +336,7 @@ export class ScrollManager {
|
|||||||
anchorFindLoadMore(isBefore: boolean): AnchorData | null {
|
anchorFindLoadMore(isBefore: boolean): AnchorData | null {
|
||||||
const {messages} = this.props;
|
const {messages} = this.props;
|
||||||
const {scrollTop} = this.scrollGetState();
|
const {scrollTop} = this.scrollGetState();
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
const layout = scrollerNode ? this.layoutGetContainerLayout(scrollerNode) : undefined;
|
const layout = scrollerNode ? this.layoutGetContainerLayout(scrollerNode) : undefined;
|
||||||
const direction = isBefore ? 1 : -1;
|
const direction = isBefore ? 1 : -1;
|
||||||
const startIndex = isBefore ? 0 : messages.length - 1;
|
const startIndex = isBefore ? 0 : messages.length - 1;
|
||||||
@@ -366,7 +366,7 @@ export class ScrollManager {
|
|||||||
this.loadIsInProgress() ? null : this.anchorFetchFallback,
|
this.loadIsInProgress() ? null : this.anchorFetchFallback,
|
||||||
this.anchorAutomatic,
|
this.anchorAutomatic,
|
||||||
];
|
];
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if (!scrollerNode) return null;
|
if (!scrollerNode) return null;
|
||||||
const layout = this.layoutGetContainerLayout(scrollerNode);
|
const layout = this.layoutGetContainerLayout(scrollerNode);
|
||||||
for (const anchor of candidates) {
|
for (const anchor of candidates) {
|
||||||
@@ -418,7 +418,7 @@ export class ScrollManager {
|
|||||||
} else {
|
} else {
|
||||||
this.scrollMergeTo(fixedScrollTop, this.scrollHandle);
|
this.scrollMergeTo(fixedScrollTop, this.scrollHandle);
|
||||||
}
|
}
|
||||||
this.ref.current?.scrollIntoViewNode({
|
this.ref.current?.revealElement({
|
||||||
node,
|
node,
|
||||||
padding: 16 + this.props.additionalMessagePadding,
|
padding: 16 + this.props.additionalMessagePadding,
|
||||||
callback: this.scrollHandle,
|
callback: this.scrollHandle,
|
||||||
@@ -467,7 +467,7 @@ export class ScrollManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
anchorUpdateFetch(scrollTop: number, offsetHeight: number, scrollHeight: number, passLayout?: ContainerLayout): void {
|
anchorUpdateFetch(scrollTop: number, offsetHeight: number, scrollHeight: number, passLayout?: ContainerLayout): void {
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if ((!this.anchorFetch && !this.anchorFetchFallback) || !scrollerNode) return;
|
if ((!this.anchorFetch && !this.anchorFetchFallback) || !scrollerNode) return;
|
||||||
const region = this.scrollIsInPlaceholderRegion({scrollTop, offsetHeight, scrollHeight});
|
const region = this.scrollIsInPlaceholderRegion({scrollTop, offsetHeight, scrollHeight});
|
||||||
const clampTo = region !== ScrollRegion.None ? offsetHeight : undefined;
|
const clampTo = region !== ScrollRegion.None ? offsetHeight : undefined;
|
||||||
@@ -481,7 +481,7 @@ export class ScrollManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
anchorUpdateAutomatic(scrollTop: number, layout?: ContainerLayout): void {
|
anchorUpdateAutomatic(scrollTop: number, layout?: ContainerLayout): void {
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if (!this.anchorAutomatic || !scrollerNode) return;
|
if (!this.anchorAutomatic || !scrollerNode) return;
|
||||||
const anchorData = this.anchorGetData(this.anchorAutomatic.id, scrollTop, undefined, layout);
|
const anchorData = this.anchorGetData(this.anchorAutomatic.id, scrollTop, undefined, layout);
|
||||||
if (!anchorData) {
|
if (!anchorData) {
|
||||||
@@ -573,7 +573,7 @@ export class ScrollManager {
|
|||||||
|
|
||||||
private focusGetMessageIdInScroller(): string | null {
|
private focusGetMessageIdInScroller(): string | null {
|
||||||
const focusedElement = document.activeElement;
|
const focusedElement = document.activeElement;
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if (!focusedElement || !scrollerNode?.contains(focusedElement)) {
|
if (!focusedElement || !scrollerNode?.contains(focusedElement)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -697,7 +697,7 @@ export class ScrollManager {
|
|||||||
this.cacheScrollTop = state.scrollTop;
|
this.cacheScrollTop = state.scrollTop;
|
||||||
this.scrollFixPosition(state.offsetHeight, state.scrollHeight, isAtBottom);
|
this.scrollFixPosition(state.offsetHeight, state.scrollHeight, isAtBottom);
|
||||||
} else {
|
} else {
|
||||||
if (event && event.target !== this.ref.current?.getScrollerNode()) {
|
if (event && event.target !== this.ref.current?.getViewportElement()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.cacheScrollTop !== state.scrollTop) {
|
if (this.cacheScrollTop !== state.scrollTop) {
|
||||||
@@ -833,7 +833,7 @@ export class ScrollManager {
|
|||||||
const element = this.layoutGetElementFromMessageId(targetId);
|
const element = this.layoutGetElementFromMessageId(targetId);
|
||||||
if (element) {
|
if (element) {
|
||||||
const padding = this.jumpGetBreathingRoom();
|
const padding = this.jumpGetBreathingRoom();
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if (!scrollerNode) return;
|
if (!scrollerNode) return;
|
||||||
const targetScrollTop = this.layoutGetNodeAlignedScrollTop(element, 'center', padding, scrollerNode);
|
const targetScrollTop = this.layoutGetNodeAlignedScrollTop(element, 'center', padding, scrollerNode);
|
||||||
this.scrollMergeTo(targetScrollTop, () => this.jumpComplete(element, jumpToken));
|
this.scrollMergeTo(targetScrollTop, () => this.jumpComplete(element, jumpToken));
|
||||||
@@ -874,7 +874,7 @@ export class ScrollManager {
|
|||||||
const padding = suppressPadding ? 0 : this.jumpGetBreathingRoom();
|
const padding = suppressPadding ? 0 : this.jumpGetBreathingRoom();
|
||||||
if (newMessagesBar) {
|
if (newMessagesBar) {
|
||||||
if (orientation === 'middle') {
|
if (orientation === 'middle') {
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if (!scrollerNode) {
|
if (!scrollerNode) {
|
||||||
this.scrollTo(Number.MAX_SAFE_INTEGER, animate, onComplete);
|
this.scrollTo(Number.MAX_SAFE_INTEGER, animate, onComplete);
|
||||||
return;
|
return;
|
||||||
@@ -899,7 +899,7 @@ export class ScrollManager {
|
|||||||
|
|
||||||
scrollToBelowUnreadDivider(): void {
|
scrollToBelowUnreadDivider(): void {
|
||||||
const doc = this.scrollGetDocument();
|
const doc = this.scrollGetDocument();
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
const newMessagesBar = doc?.getElementById('new-messages-bar');
|
const newMessagesBar = doc?.getElementById('new-messages-bar');
|
||||||
if (!scrollerNode || !newMessagesBar) {
|
if (!scrollerNode || !newMessagesBar) {
|
||||||
this.scrollSetToBottom();
|
this.scrollSetToBottom();
|
||||||
@@ -963,7 +963,7 @@ export class ScrollManager {
|
|||||||
if (this.lifecycleIsDisposed) return;
|
if (this.lifecycleIsDisposed) return;
|
||||||
const {messages, channel} = this.props;
|
const {messages, channel} = this.props;
|
||||||
this.jumpStop(true);
|
this.jumpStop(true);
|
||||||
Dimension.updateChannelDimensions(channel.id, 1, 1, 0);
|
Dimension.recordChannelDimensions(channel.id, 1, 1, 0);
|
||||||
if (messages.hasMoreAfter) {
|
if (messages.hasMoreAfter) {
|
||||||
MessageCommands.jumpToPresent(channel.id, MAX_MESSAGES_PER_CHANNEL);
|
MessageCommands.jumpToPresent(channel.id, MAX_MESSAGES_PER_CHANNEL);
|
||||||
} else {
|
} else {
|
||||||
@@ -1045,7 +1045,7 @@ export class ScrollManager {
|
|||||||
|
|
||||||
layoutApplyShift(heightDelta: number): boolean {
|
layoutApplyShift(heightDelta: number): boolean {
|
||||||
if (this.lifecycleIsDisposed || !this.lifecycleIsInitialized()) return false;
|
if (this.lifecycleIsDisposed || !this.lifecycleIsInitialized()) return false;
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if (!scrollerNode) return false;
|
if (!scrollerNode) return false;
|
||||||
const action = resolveContainerResizeShift({
|
const action = resolveContainerResizeShift({
|
||||||
heightDelta,
|
heightDelta,
|
||||||
@@ -1079,19 +1079,19 @@ export class ScrollManager {
|
|||||||
const {channel} = this.props;
|
const {channel} = this.props;
|
||||||
const placeholderHeight = this.placeholderHeightGet();
|
const placeholderHeight = this.placeholderHeightGet();
|
||||||
if (this.pinIsAtBottomNow()) {
|
if (this.pinIsAtBottomNow()) {
|
||||||
Dimension.updateChannelDimensions(channel.id, 1, 1, 0, callback);
|
Dimension.recordChannelDimensions(channel.id, 1, 1, 0, callback);
|
||||||
} else {
|
} else {
|
||||||
const {scrollTop, scrollHeight, offsetHeight} = this.scrollGetState();
|
const {scrollTop, scrollHeight, offsetHeight} = this.scrollGetState();
|
||||||
const adjustedScrollTop = scrollTop - placeholderHeight;
|
const adjustedScrollTop = scrollTop - placeholderHeight;
|
||||||
const adjustedScrollHeight = scrollHeight - placeholderHeight;
|
const adjustedScrollHeight = scrollHeight - placeholderHeight;
|
||||||
Dimension.updateChannelDimensions(channel.id, adjustedScrollTop, adjustedScrollHeight, offsetHeight, callback);
|
Dimension.recordChannelDimensions(channel.id, adjustedScrollTop, adjustedScrollHeight, offsetHeight, callback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
focusOnMessage(messageId: string): void {
|
focusOnMessage(messageId: string): void {
|
||||||
const element = this.layoutGetElementFromMessageId(messageId);
|
const element = this.layoutGetElementFromMessageId(messageId);
|
||||||
if (!element) return;
|
if (!element) return;
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
if (!scrollerNode) return;
|
if (!scrollerNode) return;
|
||||||
const elementOffset = this.layoutGetOffsetTop(element, scrollerNode);
|
const elementOffset = this.layoutGetOffsetTop(element, scrollerNode);
|
||||||
const elementHeight = element.offsetHeight;
|
const elementHeight = element.offsetHeight;
|
||||||
@@ -1125,11 +1125,11 @@ export class ScrollManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scrollPageUp(animate = false): void {
|
scrollPageUp(animate = false): void {
|
||||||
this.ref.current?.scrollPageUp({animate});
|
this.ref.current?.pageBackward({animate});
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollPageDown(animate = false): void {
|
scrollPageDown(animate = false): void {
|
||||||
this.ref.current?.scrollPageDown({animate});
|
this.ref.current?.pageForward({animate});
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToMessage(messageId: string, animate = true): void {
|
scrollToMessage(messageId: string, animate = true): void {
|
||||||
@@ -1143,7 +1143,7 @@ export class ScrollManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const element = this.layoutGetElementFromMessageId(messageId);
|
const element = this.layoutGetElementFromMessageId(messageId);
|
||||||
const scrollerNode = scrollerHandle.getScrollerNode();
|
const scrollerNode = scrollerHandle.getViewportElement();
|
||||||
this.pinIsAtBottom = false;
|
this.pinIsAtBottom = false;
|
||||||
const jumpToken = this.jumpBegin(this.props.messages.jumpSequenceId);
|
const jumpToken = this.jumpBegin(this.props.messages.jumpSequenceId);
|
||||||
const shouldContinue = () => this.jumpIsCurrent(jumpToken);
|
const shouldContinue = () => this.jumpIsCurrent(jumpToken);
|
||||||
@@ -1174,7 +1174,7 @@ export class ScrollManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {scrollTop, offsetHeight, scrollHeight} = this.scrollGetState();
|
const {scrollTop, offsetHeight, scrollHeight} = this.scrollGetState();
|
||||||
const scrollerNode = this.ref.current?.getScrollerNode();
|
const scrollerNode = this.ref.current?.getViewportElement();
|
||||||
const layout = scrollerNode ? this.layoutGetContainerLayout(scrollerNode) : undefined;
|
const layout = scrollerNode ? this.layoutGetContainerLayout(scrollerNode) : undefined;
|
||||||
this.pinPreUpdateState = this.pinComputeState({scrollTop, offsetHeight, scrollHeight});
|
this.pinPreUpdateState = this.pinComputeState({scrollTop, offsetHeight, scrollHeight});
|
||||||
this.anchorUpdateFocus(focusId, scrollTop, offsetHeight, layout);
|
this.anchorUpdateFocus(focusId, scrollTop, offsetHeight, layout);
|
||||||
@@ -1247,7 +1247,7 @@ export class ScrollManager {
|
|||||||
if (focusId != null && prevFocusId !== focusId) {
|
if (focusId != null && prevFocusId !== focusId) {
|
||||||
const el = this.layoutGetElementFromMessageId(focusId);
|
const el = this.layoutGetElementFromMessageId(focusId);
|
||||||
if (el) {
|
if (el) {
|
||||||
this.ref.current?.scrollIntoViewNode({
|
this.ref.current?.revealElement({
|
||||||
node: el,
|
node: el,
|
||||||
padding: 16 + this.props.additionalMessagePadding,
|
padding: 16 + this.props.additionalMessagePadding,
|
||||||
callback: this.scrollHandle,
|
callback: this.scrollHandle,
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ class ReadStates {
|
|||||||
const decision = resolveReadStateIncomingMessageDecision({
|
const decision = resolveReadStateIncomingMessageDecision({
|
||||||
isCurrentUserAuthor: currentUser != null && action.message.author.id === currentUser.id,
|
isCurrentUserAuthor: currentUser != null && action.message.author.id === currentUser.id,
|
||||||
automaticAckEnabled: this.isAutomaticAckEnabled(action.channelId),
|
automaticAckEnabled: this.isAutomaticAckEnabled(action.channelId),
|
||||||
isAtBottom: Dimension.isAtBottom(action.channelId),
|
isAtBottom: Dimension.channelPinnedToEnd(action.channelId),
|
||||||
authorBlocked,
|
authorBlocked,
|
||||||
hadUnreadOrMentions,
|
hadUnreadOrMentions,
|
||||||
readStateKnown: state.readStateKnown,
|
readStateKnown: state.readStateKnown,
|
||||||
|
|||||||
@@ -341,7 +341,7 @@ export const QuickSwitcherBottomSheet: React.FC<QuickSwitcherBottomSheetProps> =
|
|||||||
shouldScrollToSelection.current = false;
|
shouldScrollToSelection.current = false;
|
||||||
const node = rowRefs.current[keyboardFocusIndex];
|
const node = rowRefs.current[keyboardFocusIndex];
|
||||||
if (node) {
|
if (node) {
|
||||||
scrollerRef.current?.scrollIntoViewNode({node: node as HTMLElement, padding: 32});
|
scrollerRef.current?.revealElement({node: node as HTMLElement, padding: 32});
|
||||||
}
|
}
|
||||||
}, [keyboardFocusIndex, activeTab]);
|
}, [keyboardFocusIndex, activeTab]);
|
||||||
const sections = useMemo(() => createSections(results), [results]);
|
const sections = useMemo(() => createSections(results), [results]);
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ const QuickSwitcherModalComponent: React.FC = observer(() => {
|
|||||||
shouldScrollToSelection.current = false;
|
shouldScrollToSelection.current = false;
|
||||||
const node = rowRefs.current[keyboardFocusIndex];
|
const node = rowRefs.current[keyboardFocusIndex];
|
||||||
if (node) {
|
if (node) {
|
||||||
scrollerRef.current?.scrollIntoViewNode({node: node as HTMLElement, padding: 32});
|
scrollerRef.current?.revealElement({node: node as HTMLElement, padding: 32});
|
||||||
}
|
}
|
||||||
}, [keyboardFocusIndex]);
|
}, [keyboardFocusIndex]);
|
||||||
const sections = useMemo(() => createSections(results), [results]);
|
const sections = useMemo(() => createSections(results), [results]);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
import {flushSync} from 'react-dom';
|
||||||
|
|
||||||
export interface ScrollerState {
|
export interface ScrollerState {
|
||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
@@ -39,42 +40,43 @@ export interface ScrollerState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ScrollerHandle {
|
export interface ScrollerHandle {
|
||||||
getScrollerNode(): HTMLElement | null;
|
getViewportElement(): HTMLElement | null;
|
||||||
getScrollerState(): ScrollerState;
|
readViewportMetrics(): ScrollerState;
|
||||||
cancelScroll(): void;
|
cancelScroll(): void;
|
||||||
scrollTo(options: {to: number; animate?: boolean; callback?: () => void}): void;
|
scrollTo(options: {to: number; animate?: boolean; callback?: () => void}): void;
|
||||||
mergeTo(options: {to: number; callback?: () => void}): void;
|
mergeTo(options: {to: number; callback?: () => void}): void;
|
||||||
scrollIntoViewRect(options: {
|
revealRange(options: {
|
||||||
start: number;
|
start: number;
|
||||||
end: number;
|
end: number;
|
||||||
shouldScrollToStart?: boolean;
|
preferStartEdge?: boolean;
|
||||||
alignment?: 'start' | 'center' | 'end' | 'nearest';
|
alignment?: 'start' | 'center' | 'end' | 'nearest';
|
||||||
padding?: number;
|
padding?: number;
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
callback?: () => void;
|
callback?: () => void;
|
||||||
}): void;
|
}): void;
|
||||||
scrollIntoViewNode(options: {
|
revealElement(options: {
|
||||||
node: HTMLElement;
|
node: HTMLElement;
|
||||||
shouldScrollToStart?: boolean;
|
preferStartEdge?: boolean;
|
||||||
alignment?: 'start' | 'center' | 'end' | 'nearest';
|
alignment?: 'start' | 'center' | 'end' | 'nearest';
|
||||||
padding?: number;
|
padding?: number;
|
||||||
animate?: boolean;
|
animate?: boolean;
|
||||||
callback?: () => void;
|
callback?: () => void;
|
||||||
}): void;
|
}): void;
|
||||||
scrollPageUp(options?: {animate?: boolean; callback?: () => void}): void;
|
pageBackward(options?: {animate?: boolean; callback?: () => void}): void;
|
||||||
scrollPageDown(options?: {animate?: boolean; callback?: () => void}): void;
|
pageForward(options?: {animate?: boolean; callback?: () => void}): void;
|
||||||
scrollToTop(options?: {animate?: boolean; callback?: () => void}): void;
|
jumpToStartEdge(options?: {animate?: boolean; callback?: () => void}): void;
|
||||||
scrollToBottom(options?: {animate?: boolean; callback?: () => void}): void;
|
jumpToEndEdge(options?: {animate?: boolean; callback?: () => void}): void;
|
||||||
isScrolledToTop(): boolean;
|
atStartEdge(): boolean;
|
||||||
isScrolledToBottom(): boolean;
|
atEndEdge(): boolean;
|
||||||
getDistanceFromTop(): number;
|
remainingToStartEdge(): number;
|
||||||
getDistanceFromBottom(): number;
|
remainingToEndEdge(): number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrollAxis = 'vertical' | 'horizontal';
|
type ScrollAxis = 'vertical' | 'horizontal';
|
||||||
type ScrollOverflow = 'scroll' | 'auto' | 'hidden';
|
type ScrollOverflow = 'scroll' | 'auto' | 'hidden';
|
||||||
type ScrollbarTrackMode = 'overlay' | 'reserve';
|
type ScrollbarTrackMode = 'overlay' | 'reserve';
|
||||||
const LINE_DELTA_PX = 16;
|
const LINE_DELTA_PX = 16;
|
||||||
|
const RESIZE_OBSERVE_OPTIONS = Object.freeze({box: 'border-box'} as const);
|
||||||
|
|
||||||
interface ScrollerProps
|
interface ScrollerProps
|
||||||
extends Omit<
|
extends Omit<
|
||||||
@@ -323,6 +325,13 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
},
|
},
|
||||||
[getMaxScroll],
|
[getMaxScroll],
|
||||||
);
|
);
|
||||||
|
const resolveScrollTarget = useCallback(
|
||||||
|
(node: HTMLDivElement, value: number): number => {
|
||||||
|
const max = getMaxScroll(node);
|
||||||
|
return value >= max ? max + 1 : Math.max(0, value);
|
||||||
|
},
|
||||||
|
[getMaxScroll],
|
||||||
|
);
|
||||||
const springScrollTo = useCallback(
|
const springScrollTo = useCallback(
|
||||||
(node: HTMLDivElement, target: number, callback?: () => void, config: SpringConfig = DEFAULT_SPRING) => {
|
(node: HTMLDivElement, target: number, callback?: () => void, config: SpringConfig = DEFAULT_SPRING) => {
|
||||||
cancelAnimation();
|
cancelAnimation();
|
||||||
@@ -368,20 +377,24 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
[cancelAnimation, clampToRange, getScrollPos, setScrollPos],
|
[cancelAnimation, clampToRange, getScrollPos, setScrollPos],
|
||||||
);
|
);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const containerEl = rootRef.current;
|
const containerEl = scrollRef.current;
|
||||||
const contentEl = contentRef.current;
|
const contentEl = contentRef.current;
|
||||||
if (!containerEl || !contentEl) return;
|
if (!containerEl || !contentEl) return;
|
||||||
const ownerWindow = containerEl.ownerDocument.defaultView ?? window;
|
const ownerWindow = containerEl.ownerDocument.defaultView ?? window;
|
||||||
const containerObserver = new ownerWindow.ResizeObserver((entries) => {
|
const containerObserver = new ownerWindow.ResizeObserver((entries) => {
|
||||||
scheduleThumbRefresh();
|
scheduleThumbRefresh();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
flushSync(() => {
|
||||||
onResizeRef.current?.(entry, 'container');
|
onResizeRef.current?.(entry, 'container');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const contentObserver = new ownerWindow.ResizeObserver((entries) => {
|
const contentObserver = new ownerWindow.ResizeObserver((entries) => {
|
||||||
scheduleThumbRefresh();
|
scheduleThumbRefresh();
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
|
flushSync(() => {
|
||||||
onResizeRef.current?.(entry, 'content');
|
onResizeRef.current?.(entry, 'content');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const childObserver = new ownerWindow.ResizeObserver((entries) => {
|
const childObserver = new ownerWindow.ResizeObserver((entries) => {
|
||||||
@@ -417,8 +430,8 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
refreshThumbState();
|
refreshThumbState();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
containerObserver.observe(containerEl);
|
containerObserver.observe(containerEl, RESIZE_OBSERVE_OPTIONS);
|
||||||
contentObserver.observe(contentEl);
|
contentObserver.observe(contentEl, RESIZE_OBSERVE_OPTIONS);
|
||||||
syncChildObservers();
|
syncChildObservers();
|
||||||
childListObserver.observe(contentEl, {childList: true, subtree: true});
|
childListObserver.observe(contentEl, {childList: true, subtree: true});
|
||||||
refreshThumbState();
|
refreshThumbState();
|
||||||
@@ -470,8 +483,8 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
useImperativeHandle(
|
useImperativeHandle(
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
() => ({
|
() => ({
|
||||||
getScrollerNode: () => scrollRef.current,
|
getViewportElement: () => scrollRef.current,
|
||||||
getScrollerState: () => getScrollState(scrollRef.current),
|
readViewportMetrics: () => getScrollState(scrollRef.current),
|
||||||
cancelScroll() {
|
cancelScroll() {
|
||||||
cancelAnimation();
|
cancelAnimation();
|
||||||
},
|
},
|
||||||
@@ -481,7 +494,7 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
callback?.();
|
callback?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const clampedTo = clampToRange(node, to);
|
const clampedTo = resolveScrollTarget(node, to);
|
||||||
if (!animate || !shouldAnimateScroll()) {
|
if (!animate || !shouldAnimateScroll()) {
|
||||||
cancelAnimation();
|
cancelAnimation();
|
||||||
setScrollPos(node, clampedTo);
|
setScrollPos(node, clampedTo);
|
||||||
@@ -496,11 +509,19 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
callback?.();
|
callback?.();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cancelAnimation();
|
const merged = clampToRange(node, to);
|
||||||
setScrollPos(node, clampToRange(node, to));
|
const anim = animRef.current;
|
||||||
|
if (anim == null) {
|
||||||
|
setScrollPos(node, merged);
|
||||||
|
callback?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const delta = merged - getScrollPos(node);
|
||||||
|
setScrollPos(node, merged);
|
||||||
|
anim.target += delta;
|
||||||
callback?.();
|
callback?.();
|
||||||
},
|
},
|
||||||
scrollIntoViewRect({start, end, shouldScrollToStart = false, alignment, padding = 0, animate = false, callback}) {
|
revealRange({start, end, preferStartEdge = false, alignment, padding = 0, animate = false, callback}) {
|
||||||
const node = scrollRef.current;
|
const node = scrollRef.current;
|
||||||
if (!node) {
|
if (!node) {
|
||||||
callback?.();
|
callback?.();
|
||||||
@@ -508,7 +529,7 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
}
|
}
|
||||||
const scrollPos = getScrollPos(node);
|
const scrollPos = getScrollPos(node);
|
||||||
const viewportSize = orientation === 'vertical' ? node.offsetHeight : node.offsetWidth;
|
const viewportSize = orientation === 'vertical' ? node.offsetHeight : node.offsetWidth;
|
||||||
const resolvedAlignment = alignment ?? (shouldScrollToStart ? 'start' : 'nearest');
|
const resolvedAlignment = alignment ?? (preferStartEdge ? 'start' : 'nearest');
|
||||||
if (resolvedAlignment === 'center') {
|
if (resolvedAlignment === 'center') {
|
||||||
const targetScroll = start - (viewportSize - (end - start)) / 2;
|
const targetScroll = start - (viewportSize - (end - start)) / 2;
|
||||||
this.scrollTo({to: targetScroll, animate, callback});
|
this.scrollTo({to: targetScroll, animate, callback});
|
||||||
@@ -537,14 +558,7 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
}
|
}
|
||||||
this.scrollTo({to: targetScroll, animate, callback});
|
this.scrollTo({to: targetScroll, animate, callback});
|
||||||
},
|
},
|
||||||
scrollIntoViewNode({
|
revealElement({node: targetNode, preferStartEdge = false, alignment, padding = 0, animate = false, callback}) {
|
||||||
node: targetNode,
|
|
||||||
shouldScrollToStart = false,
|
|
||||||
alignment,
|
|
||||||
padding = 0,
|
|
||||||
animate = false,
|
|
||||||
callback,
|
|
||||||
}) {
|
|
||||||
const container = scrollRef.current;
|
const container = scrollRef.current;
|
||||||
if (!container) {
|
if (!container) {
|
||||||
callback?.();
|
callback?.();
|
||||||
@@ -552,17 +566,17 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
}
|
}
|
||||||
const offset = measureElementOffset(targetNode, orientation, container);
|
const offset = measureElementOffset(targetNode, orientation, container);
|
||||||
const size = orientation === 'vertical' ? targetNode.offsetHeight : targetNode.offsetWidth;
|
const size = orientation === 'vertical' ? targetNode.offsetHeight : targetNode.offsetWidth;
|
||||||
this.scrollIntoViewRect({
|
this.revealRange({
|
||||||
start: offset,
|
start: offset,
|
||||||
end: offset + size,
|
end: offset + size,
|
||||||
shouldScrollToStart,
|
preferStartEdge,
|
||||||
alignment,
|
alignment,
|
||||||
padding,
|
padding,
|
||||||
animate,
|
animate,
|
||||||
callback,
|
callback,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
scrollPageUp({animate = false, callback} = {}) {
|
pageBackward({animate = false, callback} = {}) {
|
||||||
const node = scrollRef.current;
|
const node = scrollRef.current;
|
||||||
if (!node) {
|
if (!node) {
|
||||||
callback?.();
|
callback?.();
|
||||||
@@ -573,7 +587,7 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
const target = Math.max(0, scrollPos - 0.9 * viewportSize);
|
const target = Math.max(0, scrollPos - 0.9 * viewportSize);
|
||||||
this.scrollTo({to: target, animate, callback});
|
this.scrollTo({to: target, animate, callback});
|
||||||
},
|
},
|
||||||
scrollPageDown({animate = false, callback} = {}) {
|
pageForward({animate = false, callback} = {}) {
|
||||||
const node = scrollRef.current;
|
const node = scrollRef.current;
|
||||||
if (!node) {
|
if (!node) {
|
||||||
callback?.();
|
callback?.();
|
||||||
@@ -585,10 +599,10 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
const target = Math.min(maxScroll, scrollPos + 0.9 * viewportSize);
|
const target = Math.min(maxScroll, scrollPos + 0.9 * viewportSize);
|
||||||
this.scrollTo({to: target, animate, callback});
|
this.scrollTo({to: target, animate, callback});
|
||||||
},
|
},
|
||||||
scrollToTop({animate = false, callback} = {}) {
|
jumpToStartEdge({animate = false, callback} = {}) {
|
||||||
this.scrollTo({to: 0, animate, callback});
|
this.scrollTo({to: 0, animate, callback});
|
||||||
},
|
},
|
||||||
scrollToBottom({animate = false, callback} = {}) {
|
jumpToEndEdge({animate = false, callback} = {}) {
|
||||||
const node = scrollRef.current;
|
const node = scrollRef.current;
|
||||||
if (!node) {
|
if (!node) {
|
||||||
callback?.();
|
callback?.();
|
||||||
@@ -596,24 +610,24 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
}
|
}
|
||||||
this.scrollTo({to: getMaxScroll(node), animate, callback});
|
this.scrollTo({to: getMaxScroll(node), animate, callback});
|
||||||
},
|
},
|
||||||
isScrolledToTop() {
|
atStartEdge() {
|
||||||
const node = scrollRef.current;
|
const node = scrollRef.current;
|
||||||
if (!node) return false;
|
if (!node) return false;
|
||||||
return getScrollPos(node) === 0;
|
return getScrollPos(node) === 0;
|
||||||
},
|
},
|
||||||
isScrolledToBottom() {
|
atEndEdge() {
|
||||||
const node = scrollRef.current;
|
const node = scrollRef.current;
|
||||||
if (!node) return false;
|
if (!node) return false;
|
||||||
const scrollPos = getScrollPos(node);
|
const scrollPos = getScrollPos(node);
|
||||||
const maxScroll = getMaxScroll(node);
|
const maxScroll = getMaxScroll(node);
|
||||||
return scrollPos >= maxScroll;
|
return scrollPos >= maxScroll;
|
||||||
},
|
},
|
||||||
getDistanceFromTop() {
|
remainingToStartEdge() {
|
||||||
const node = scrollRef.current;
|
const node = scrollRef.current;
|
||||||
if (!node) return 0;
|
if (!node) return 0;
|
||||||
return Math.max(0, getScrollPos(node));
|
return Math.max(0, getScrollPos(node));
|
||||||
},
|
},
|
||||||
getDistanceFromBottom() {
|
remainingToEndEdge() {
|
||||||
const node = scrollRef.current;
|
const node = scrollRef.current;
|
||||||
if (!node) return 0;
|
if (!node) return 0;
|
||||||
const scrollPos = getScrollPos(node);
|
const scrollPos = getScrollPos(node);
|
||||||
@@ -621,7 +635,16 @@ export const Scroller = forwardRef<ScrollerHandle, ScrollerProps>(function Scrol
|
|||||||
return Math.max(0, maxScroll - scrollPos);
|
return Math.max(0, maxScroll - scrollPos);
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[orientation, cancelAnimation, clampToRange, getMaxScroll, getScrollPos, setScrollPos, springScrollTo],
|
[
|
||||||
|
orientation,
|
||||||
|
cancelAnimation,
|
||||||
|
clampToRange,
|
||||||
|
getMaxScroll,
|
||||||
|
getScrollPos,
|
||||||
|
resolveScrollTarget,
|
||||||
|
setScrollPos,
|
||||||
|
springScrollTo,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
const handleScroll = useCallback(
|
const handleScroll = useCallback(
|
||||||
(event: UIEvent<HTMLDivElement>) => {
|
(event: UIEvent<HTMLDivElement>) => {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function findScrollContainer(el: HTMLElement, selector: string | undefined): HTM
|
|||||||
function resolveScrollerNode(scrollerRef: RefObject<ScrollerHandle | null>): HTMLElement | null {
|
function resolveScrollerNode(scrollerRef: RefObject<ScrollerHandle | null>): HTMLElement | null {
|
||||||
const scroller = scrollerRef.current;
|
const scroller = scrollerRef.current;
|
||||||
if (scroller == null) return null;
|
if (scroller == null) return null;
|
||||||
return scroller.getScrollerNode();
|
return scroller.getViewportElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareListScrollAnchorPositions(left: ListScrollAnchorPosition, right: ListScrollAnchorPosition): number {
|
function compareListScrollAnchorPositions(left: ListScrollAnchorPosition, right: ListScrollAnchorPosition): number {
|
||||||
|
|||||||
@@ -36,14 +36,14 @@ class Dimension {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeObservable(this, {
|
makeObservable(this, {
|
||||||
updateChannelDimensions: action,
|
recordChannelDimensions: action,
|
||||||
updateGuildDimensions: action,
|
updateGuildDimensions: action,
|
||||||
updateGuildListDimensions: action,
|
updateGuildListDimensions: action,
|
||||||
clearChannelDimensions: action,
|
forgetChannelDimensions: action,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
percentageScrolled(channelId: string): number {
|
scrollProgressRatio(channelId: string): number {
|
||||||
const dimensions = this.channelDimensions.get(channelId);
|
const dimensions = this.channelDimensions.get(channelId);
|
||||||
if (dimensions != null) {
|
if (dimensions != null) {
|
||||||
const {scrollTop, scrollHeight} = dimensions;
|
const {scrollTop, scrollHeight} = dimensions;
|
||||||
@@ -52,11 +52,11 @@ class Dimension {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
getChannelDimensions(channelId: string): ChannelDimensions | undefined {
|
channelDimensionsFor(channelId: string): ChannelDimensions | undefined {
|
||||||
return this.channelDimensions.get(channelId);
|
return this.channelDimensions.get(channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuildDimensions(guildId: string): GuildDimensions {
|
guildDimensionsFor(guildId: string): GuildDimensions {
|
||||||
const existing = this.guildDimensions.get(guildId);
|
const existing = this.guildDimensions.get(guildId);
|
||||||
if (existing != null) {
|
if (existing != null) {
|
||||||
return existing;
|
return existing;
|
||||||
@@ -64,11 +64,11 @@ class Dimension {
|
|||||||
return createDefaultGuildDimensions(guildId);
|
return createDefaultGuildDimensions(guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGuildListDimensions(): GuildListDimensions {
|
currentGuildListDimensions(): GuildListDimensions {
|
||||||
return this.guildListDimensions.get();
|
return this.guildListDimensions.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
isAtBottom(channelId: string): boolean {
|
channelPinnedToEnd(channelId: string): boolean {
|
||||||
const dimensions = this.channelDimensions.get(channelId);
|
const dimensions = this.channelDimensions.get(channelId);
|
||||||
if (dimensions == null) {
|
if (dimensions == null) {
|
||||||
return true;
|
return true;
|
||||||
@@ -77,7 +77,7 @@ class Dimension {
|
|||||||
return scrollTop >= scrollHeight - offsetHeight - BOTTOM_TOLERANCE;
|
return scrollTop >= scrollHeight - offsetHeight - BOTTOM_TOLERANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChannelDimensions(
|
recordChannelDimensions(
|
||||||
channelId: string,
|
channelId: string,
|
||||||
scrollTop: number | null,
|
scrollTop: number | null,
|
||||||
scrollHeight: number | null,
|
scrollHeight: number | null,
|
||||||
@@ -129,7 +129,7 @@ class Dimension {
|
|||||||
this.guildListDimensions.set({scrollTop});
|
this.guildListDimensions.set({scrollTop});
|
||||||
}
|
}
|
||||||
|
|
||||||
clearChannelDimensions(channelId: string): void {
|
forgetChannelDimensions(channelId: string): void {
|
||||||
this.channelDimensions.delete(channelId);
|
this.channelDimensions.delete(channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +142,7 @@ class Dimension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleCallCreate(channelId: string): void {
|
handleCallCreate(channelId: string): void {
|
||||||
this.clearChannelDimensions(channelId);
|
this.forgetChannelDimensions(channelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export function useWindowFocusVideoControl({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
const node = scrollerRefRef.current.current?.getScrollerNode();
|
const node = scrollerRefRef.current.current?.getViewportElement();
|
||||||
if (document.activeElement instanceof HTMLElement && node?.contains(document.activeElement)) {
|
if (document.activeElement instanceof HTMLElement && node?.contains(document.activeElement)) {
|
||||||
const scrollTop = node.scrollTop;
|
const scrollTop = node.scrollTop;
|
||||||
document.activeElement.blur();
|
document.activeElement.blur();
|
||||||
|
|||||||
Reference in new issue
Block a user