fix(app): keep the chat scroller pinned to the end while it resizes (#1860)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-24 12:33:06 +02:00
1 parent a2099fb0e2
commit 2af0405317
36 files changed
+171 -191

No files matched your search

@@ -392,7 +392,7 @@ export const MobileSectionNav: React.FC<MobileSectionNavProps> = observer(
const scrollerRef = useRef<ScrollerHandle | null>(null);
useEffect(() => {
if (!activeSectionId) return;
const node = scrollerRef.current?.getScrollerNode();
const node = scrollerRef.current?.getViewportElement();
if (!node) return;
const activeButton = node.querySelector(`[data-section-id="${activeSectionId}"]`);
if (activeButton instanceof HTMLElement) {
@@ -125,7 +125,7 @@ export const SettingsModalDesktopScroll: React.FC<SettingsModalDesktopScrollProp
if (!scrollerRef) {
return;
}
const node = internalRef.current?.getScrollerNode() ?? null;
const node = internalRef.current?.getViewportElement() ?? null;
if (typeof scrollerRef === 'function') {
scrollerRef(node);
} else {
@@ -734,7 +734,7 @@ export const UnreadChannelsContent = observer(function UnreadChannelsContent() {
const target = event.target as Element | null;
if (target instanceof HTMLTextAreaElement || target instanceof HTMLInputElement) return;
if (target instanceof HTMLElement && target.isContentEditable) return;
const scrollerNode = scrollerRef.current?.getScrollerNode() ?? null;
const scrollerNode = scrollerRef.current?.getViewportElement() ?? null;
if (!scrollerNode) return;
if (target && !scrollerNode.contains(target) && document.activeElement !== document.body) return;
const sections = Array.from(scrollerNode.querySelectorAll<HTMLElement>('[data-inbox-channel-section]'));
@@ -69,7 +69,7 @@ const AuthLayoutContent = observer(function AuthLayoutContent({children}: {child
};
}, []);
useEffect(() => {
scrollerRef.current?.scrollToTop();
scrollerRef.current?.jumpToStartEdge();
}, [location.pathname]);
const splashScale = useMemo(() => {
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],
);
const getChannelScrollContainer = useCallback(() => scrollerRef.current?.getScrollerNode() ?? null, [scrollerRef]);
const getChannelScrollContainer = useCallback(() => scrollerRef.current?.getViewportElement() ?? null, [scrollerRef]);
useDragAutoScroll({active: isDraggingAnything, getScrollElement: getChannelScrollContainer});
const handleChannelDrop = useCallback(
(item: DragItem, result: DropResult) => {
@@ -306,15 +306,15 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
}, [guild.id]);
const handleResize = useCallback((_entry: ResizeObserverEntry, _type: 'container' | 'content') => {
if (stickToBottomRef.current && scrollerRef.current) {
scrollerRef.current.scrollToBottom({animate: false});
scrollerRef.current.jumpToEndEdge({animate: false});
}
}, []);
useEffect(() => {
const guildDimensions = Dimension.getGuildDimensions(guild.id);
const guildDimensions = Dimension.guildDimensionsFor(guild.id);
if (guildDimensions.scrollTo) {
const element = document.querySelector(`[data-channel-id="${guildDimensions.scrollTo}"]`);
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);
} 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 mentionCount = ReadStates.getMentionCount(channelId);
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 unreadBadgesLevel = channel
? UserGuildSettings.resolvedUnreadBadgesLevel({
@@ -360,7 +361,9 @@ export const ChannelListContent = observer(({guild, scrollY}: {guild: Guild; scr
for (const group of channelGroups) {
const isCollapsed = group.category ? (collapsedCategories?.has(group.category.id) ?? false) : false;
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 filteredVoiceChannels: Array<Channel>;
if (hideMutedChannels) {
@@ -651,7 +651,7 @@ export const FavoritesChannelListContent = observer(() => {
const getScrollElement = useCallback(() => {
const scroller = scrollerRef.current;
if (scroller == null) return null;
return scroller.getScrollerNode();
return scroller.getViewportElement();
}, []);
useDragAutoScroll({active: isDraggingFavorite, getScrollElement});
const favorites = Favorites.sortedChannels;
@@ -164,7 +164,7 @@ function resolveScrollerNode(scroller: ScrollerHandle | null): HTMLElement | nul
if (scroller == null) {
return null;
}
return scroller.getScrollerNode();
return scroller.getViewportElement();
}
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 {
const container = scroller.getScrollerNode();
const container = scroller.getViewportElement();
if (container == null) return false;
const containerRect = container.getBoundingClientRect();
const targetRect = targetNode.getBoundingClientRect();
@@ -451,7 +451,7 @@ export const FloatingUnreadIndicators = observer(
if (scrollTarget(edge.id, direction)) {
scheduleRefresh(FloatingUnreadRefreshScope.GEOMETRY);
if (scroller == null) return;
const scrollNode = scroller.getScrollerNode();
const scrollNode = scroller.getViewportElement();
if (scrollNode == null) return;
const ownerWindow = resolveOwnerWindow(scrollNode);
cancelOwnedAnimationFrame(targetAlignmentFrameRef.current);
@@ -467,7 +467,7 @@ export const FloatingUnreadIndicators = observer(
}
scheduleRefresh(FloatingUnreadRefreshScope.SCROLL);
if (scroller == null) return;
const scrollNode = scroller.getScrollerNode();
const scrollNode = scroller.getViewportElement();
if (scrollNode == null) return;
const ownerWindow = resolveOwnerWindow(scrollNode);
cancelOwnedSettleTimer(settleTimerRef.current);
@@ -184,7 +184,7 @@ function resolveGuildFolderId(folderId: number | null): number {
function resolveGuildListScrollerNode(scroller: ScrollerHandle | null): HTMLElement | null {
if (scroller == null) return null;
return scroller.getScrollerNode();
return scroller.getViewportElement();
}
type GuildNavigationRow =
@@ -1583,7 +1583,7 @@ const GuildList = observer(() => {
const visibleDMListRows = useFrameBatchedDMListRows(targetDMListRows);
const shouldRenderGuildListItems = hasUnavailableGuilds || organizedItems.length > 0;
const initialGuildListScrollTop = useMemo(() => {
const liveScrollTop = untracked(() => Dimension.getGuildListDimensions().scrollTop);
const liveScrollTop = untracked(() => Dimension.currentGuildListDimensions().scrollTop);
if (liveScrollTop > 0) {
return liveScrollTop;
}
@@ -1859,7 +1859,7 @@ const GuildList = observer(() => {
const node = scrollTargetRegistry.getTargetNode(targetId);
const scroller = scrollRef.current;
if (node == null || scroller == null) return false;
scroller.scrollIntoViewNode({
scroller.revealElement({
node,
alignment,
animate: Accessibility.useSmoothScrolling,
@@ -467,7 +467,7 @@ export const GuildRailSkeleton: React.FC<GuildRailSkeletonProps> = ({
const [mountState] = useState(() => {
const rememberedLayout = getRememberedSkeletonGuildRailLayout();
const remScale = getRemScaleForDocument(document);
const liveScrollTop = Math.max(0, Dimension.getGuildListDimensions().scrollTop);
const liveScrollTop = Math.max(0, Dimension.currentGuildListDimensions().scrollTop);
let scrollTop = liveScrollTop;
if (liveScrollTop === 0 && rememberedLayout != null) {
scrollTop = rememberedLayout.scrollTopPx * remScale;
@@ -72,7 +72,7 @@ export function WhatsNewModal({entry}: WhatsNewModalProps) {
ModalCommands.pop();
}, [entry.id]);
const updateScrollHintVisibility = useCallback(() => {
const scrollerNode = scrollerRef.current?.getScrollerNode();
const scrollerNode = scrollerRef.current?.getViewportElement();
if (!scrollerNode) {
setIsScrollHintVisible(false);
return;
@@ -89,7 +89,7 @@ export function WhatsNewModal({entry}: WhatsNewModalProps) {
updateScrollHintVisibility();
}, [updateScrollHintVisibility]);
const handleScrollToBottom = useCallback(() => {
scrollerRef.current?.scrollToBottom({animate: true});
scrollerRef.current?.jumpToEndEdge({animate: true});
}, []);
useEffect(() => {
updateScrollHintVisibility();
@@ -265,7 +265,7 @@ function resolveScrollNode(scrollerRef: React.RefObject<ScrollerHandle | null>):
if (scroller == null) {
return null;
}
return scroller.getScrollerNode();
return scroller.getViewportElement();
}
interface ResolveNavigationRowNodeQuery {
@@ -311,7 +311,7 @@ function scrollNavigationRowIntoView({
if (node == null) {
return false;
}
scroller.scrollIntoViewNode({node, alignment: resolveNavigationScrollerAlignment(align)});
scroller.revealElement({node, alignment: resolveNavigationScrollerAlignment(align)});
return true;
}