Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Skeleton } from "@/components/ui/skeleton";
import { useLoadMoreSentinel } from "@/shared/hooks/use-load-more-sentinel";
import { cn } from "@/lib/utils";
import type { FileObjectDTO } from "@/shared/api/file.types";

Expand Down Expand Up @@ -221,34 +222,13 @@ export function SidebarList({
const loadMoreRef = React.useRef<HTMLDivElement | null>(null);
const selectedFileIDSet = React.useMemo(() => new Set(selectedFileIDs), [selectedFileIDs]);

React.useEffect(() => {
if (!hasMore || loading || loadingMore) {
return;
}

const sentinel = loadMoreRef.current;
const viewport = scrollAreaRef.current;
if (!sentinel || !viewport) {
return;
}

const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
if (entry?.isIntersecting) {
onLoadMore();
}
},
{
root: viewport,
rootMargin: "0px 0px 200px 0px",
threshold: 0.01,
},
);

observer.observe(sentinel);
return () => observer.disconnect();
}, [hasMore, loading, loadingMore, onLoadMore]);
useLoadMoreSentinel({
enabled: hasMore && !loading && !loadingMore,
rootMargin: "0px 0px 200px 0px",
rootRef: scrollAreaRef,
targetRef: loadMoreRef,
onLoadMore,
});

if (!loading && items.length === 0) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ import { DeleteFilesOption } from "@/shared/components/delete-files-option"
import { useDialogSnapshot } from "@/shared/hooks/use-dialog-snapshot"
import { useSettingsChatPreferences } from "@/features/settings/hooks/use-settings-chat-preferences"
import { useLayoutActiveConversation } from "@/features/layouts/hooks/use-layout-active-conversation"
import { useMobileSidebarNavigation } from "@/features/layouts/hooks/use-mobile-sidebar-navigation"
import { useSidebarConversationNavigation } from "@/features/layouts/hooks/use-sidebar-conversation-navigation"
import { SidebarConversationItem } from "@/features/layouts/components/navigation/sidebar-conversation-item"
import type {
SidebarConversationDeleteTarget,
Expand Down Expand Up @@ -593,7 +593,7 @@ export function NavProjects() {
const resolveErrorMessage = useLocalizedErrorMessage()
const { isMobile, setOpenMobile } = useSidebar()
const router = useRouter()
const onNavigate = useMobileSidebarNavigation()
const onNavigate = useSidebarConversationNavigation()
const pathname = usePathname()
const searchParams = useSearchParams()
const activeRecentProjectID = searchParams.get("project") ?? ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { useDialogSnapshot } from "@/shared/hooks/use-dialog-snapshot"
import { useSettingsChatPreferences } from "@/features/settings/hooks/use-settings-chat-preferences"
import { useLayoutActiveConversation } from "@/features/layouts/hooks/use-layout-active-conversation"
import { useLayoutSidebarListFlip } from "@/features/layouts/hooks/use-layout-sidebar-list-flip"
import { useMobileSidebarNavigation } from "@/features/layouts/hooks/use-mobile-sidebar-navigation"
import { useSidebarConversationNavigation } from "@/features/layouts/hooks/use-sidebar-conversation-navigation"
import type {
SidebarConversationDeleteTarget,
SidebarConversationRenameTarget,
Expand All @@ -55,7 +55,7 @@ const RECENTS_OPEN_STORAGE_KEY = "deeix.sidebar.recents.open"

export function NavRecents() {
const t = useTranslations("recent")
const onNavigate = useMobileSidebarNavigation()
const onNavigate = useSidebarConversationNavigation()
const router = useRouter()
const activeConversationID = useLayoutActiveConversation()
const { deleteFilesByDefault } = useSettingsChatPreferences()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import { useDialogSnapshot } from "@/shared/hooks/use-dialog-snapshot"
import { useSettingsChatPreferences } from "@/features/settings/hooks/use-settings-chat-preferences"
import { useLayoutActiveConversation } from "@/features/layouts/hooks/use-layout-active-conversation"
import { useLayoutSidebarListFlip } from "@/features/layouts/hooks/use-layout-sidebar-list-flip"
import { useMobileSidebarNavigation } from "@/features/layouts/hooks/use-mobile-sidebar-navigation"
import { useSidebarConversationNavigation } from "@/features/layouts/hooks/use-sidebar-conversation-navigation"
import { SIDEBAR_OVERFLOW_ROW_TRANSITION } from "@/features/layouts/model/sidebar-motion"
import type {
SidebarConversationDeleteTarget,
Expand Down Expand Up @@ -72,7 +72,7 @@ export function NavStarred() {
const t = useTranslations("recent")
const { isMobile, setOpenMobile } = useSidebar()
const router = useRouter()
const onNavigate = useMobileSidebarNavigation()
const onNavigate = useSidebarConversationNavigation()
const activeConversationID = useLayoutActiveConversation()
const { deleteFilesByDefault } = useSettingsChatPreferences()

Expand Down
20 changes: 0 additions & 20 deletions frontend/features/layouts/hooks/use-mobile-sidebar-navigation.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"use client"

import * as React from "react"
import { useRouter } from "next/navigation"

import { useSidebar } from "@/components/ui/sidebar"

function shouldUseNativeNavigation(event: React.MouseEvent<HTMLAnchorElement>) {
const target = event.currentTarget.getAttribute("target")
return event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey ||
Boolean(target && target !== "_self")
}

export function useSidebarConversationNavigation() {
const router = useRouter()
const { isMobile, setOpenMobile } = useSidebar()

return React.useCallback((href: string, event: React.MouseEvent<HTMLAnchorElement>) => {
if (shouldUseNativeNavigation(event)) {
return
}

event.preventDefault()
router.push(href)
if (isMobile) {
setOpenMobile(false)
}
}, [isMobile, router, setOpenMobile])
}
65 changes: 62 additions & 3 deletions frontend/shared/hooks/use-load-more-sentinel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,25 @@ import * as React from "react";
type UseLoadMoreSentinelOptions = {
enabled: boolean;
rootMargin?: string;
rootRef?: React.RefObject<HTMLElement | null>;
targetRef: React.RefObject<Element | null>;
onLoadMore: () => void | Promise<void>;
};

function rootMarginToPixels(rootMargin: string): number {
const values = rootMargin.trim().split(/\s+/).filter(Boolean);
const value = values.length >= 3 ? values[2] : values[0] ?? "";
const match = value.match(/^(-?\d+(?:\.\d+)?)px$/);
if (!match) {
return 120;
}
return Number(match[1]) || 0;
}

export function useLoadMoreSentinel({
enabled,
rootMargin = "120px",
rootRef,
targetRef,
onLoadMore,
}: UseLoadMoreSentinelOptions) {
Expand All @@ -31,7 +43,33 @@ export function useLoadMoreSentinel({
return;
}

const root = target.parentElement?.closest<HTMLElement>("[data-sidebar-scroll-root='true']") ?? null;
const root = rootRef?.current ?? target.parentElement?.closest<HTMLElement>("[data-sidebar-scroll-root='true']") ?? null;
const marginPx = rootMarginToPixels(rootMargin);
let animationFrame: number | null = null;
let disposed = false;

const isNearLoadMorePoint = () => {
if (root) {
return root.scrollHeight - root.scrollTop - root.clientHeight <= marginPx;
}
return target.getBoundingClientRect().top <= window.innerHeight + marginPx;
};

const check = () => {
animationFrame = null;
if (disposed || !isNearLoadMorePoint()) {
return;
}
void onLoadMoreRef.current();
};

const scheduleCheck = () => {
if (animationFrame !== null) {
return;
}
animationFrame = window.requestAnimationFrame(check);
};

const observer = new IntersectionObserver(
(entries) => {
const entry = entries[0];
Expand All @@ -43,6 +81,27 @@ export function useLoadMoreSentinel({
);

observer.observe(target);
return () => observer.disconnect();
}, [enabled, rootMargin, targetRef]);
const scrollTarget: HTMLElement | Window = root ?? window;
scrollTarget.addEventListener("scroll", scheduleCheck, { passive: true });
window.addEventListener("resize", scheduleCheck);

const resizeObserver = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(scheduleCheck);
resizeObserver?.observe(target);
if (root) {
resizeObserver?.observe(root);
}

scheduleCheck();

return () => {
disposed = true;
observer.disconnect();
scrollTarget.removeEventListener("scroll", scheduleCheck);
window.removeEventListener("resize", scheduleCheck);
resizeObserver?.disconnect();
if (animationFrame !== null) {
window.cancelAnimationFrame(animationFrame);
}
};
}, [enabled, rootMargin, rootRef, targetRef]);
}
Loading