@@ -144,7 +145,7 @@ function AudioLoadingState({ title }: { title: string }) {
diff --git a/surfsense_web/components/tool-ui/image/index.tsx b/surfsense_web/components/tool-ui/image/index.tsx
index f872e293f..42725d258 100644
--- a/surfsense_web/components/tool-ui/image/index.tsx
+++ b/surfsense_web/components/tool-ui/image/index.tsx
@@ -1,11 +1,12 @@
"use client";
-import { ExternalLinkIcon, ImageIcon, Loader2 } from "lucide-react";
+import { ExternalLinkIcon, ImageIcon } from "lucide-react";
import NextImage from "next/image";
import { Component, type ReactNode, useState } from "react";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { Card } from "@/components/ui/card";
+import { Spinner } from "@/components/ui/spinner";
import { cn } from "@/lib/utils";
/**
@@ -184,7 +185,7 @@ export function ImageLoading({ title = "Loading image..." }: { title?: string })
diff --git a/surfsense_web/components/tool-ui/media-card/index.tsx b/surfsense_web/components/tool-ui/media-card/index.tsx
index d4fe0c7c0..c7c8cfdf2 100644
--- a/surfsense_web/components/tool-ui/media-card/index.tsx
+++ b/surfsense_web/components/tool-ui/media-card/index.tsx
@@ -1,12 +1,13 @@
"use client";
-import { ExternalLinkIcon, Globe, ImageIcon, LinkIcon, Loader2 } from "lucide-react";
+import { ExternalLinkIcon, Globe, ImageIcon, LinkIcon } from "lucide-react";
import Image from "next/image";
import { Component, type ReactNode } from "react";
import { z } from "zod";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
+import { Spinner } from "@/components/ui/spinner";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
@@ -299,18 +300,17 @@ export function MediaCard({
{/* Response Actions */}
{responseActions && responseActions.length > 0 && (
- e.stopPropagation()}
- onKeyDown={(e) => e.stopPropagation()}
- >
+
{responseActions.map((action) => (
@@ -337,7 +337,7 @@ export function MediaCardLoading({ title = "Loading preview..." }: { title?: str
return (
-
+
diff --git a/surfsense_web/components/tool-ui/write-todos.tsx b/surfsense_web/components/tool-ui/write-todos.tsx
index a5da31e9e..9b959bd33 100644
--- a/surfsense_web/components/tool-ui/write-todos.tsx
+++ b/surfsense_web/components/tool-ui/write-todos.tsx
@@ -2,7 +2,6 @@
import { makeAssistantToolUI, useAssistantState } from "@assistant-ui/react";
import { useAtomValue, useSetAtom } from "jotai";
-import { Loader2 } from "lucide-react";
import { useEffect, useMemo } from "react";
import { z } from "zod";
import {
@@ -11,6 +10,7 @@ import {
registerPlanOwner,
updatePlanStateAtom,
} from "@/atoms/chat/plan-state.atom";
+import { Spinner } from "@/components/ui/spinner";
import { Plan, PlanErrorBoundary, parseSerializablePlan, TodoStatusSchema } from "./plan";
// ============================================================================
@@ -46,7 +46,7 @@ function WriteTodosLoading() {
return (
diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json
index 59b9cf56a..f23359bf2 100644
--- a/surfsense_web/messages/en.json
+++ b/surfsense_web/messages/en.json
@@ -321,10 +321,10 @@
"columns": "Columns",
"confirm_delete": "Confirm Delete",
"confirm_delete_desc": "Are you sure you want to delete {count} document(s)? This action cannot be undone.",
- "uploading": "Uploading...",
+ "uploading": "Uploading",
"upload_success": "Document uploaded successfully",
"upload_failed": "Failed to upload document",
- "loading": "Loading documents...",
+ "loading": "Loading documents",
"error_loading": "Error loading documents",
"retry": "Retry",
"no_documents": "No documents found",
@@ -391,8 +391,8 @@
"selected_files": "Selected Files ({count})",
"total_size": "Total size",
"clear_all": "Clear all",
- "uploading_files": "Uploading files...",
- "uploading": "Uploading...",
+ "uploading_files": "Uploading files",
+ "uploading": "Uploading",
"upload_button": "Upload {count} {count, plural, one {file} other {files}}",
"upload_initiated": "Upload Task Initiated",
"upload_initiated_desc": "Files Uploading Initiated",
From 66a3c877efd90e3a0bee6276022283a8e22b2ef0 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sun, 25 Jan 2026 16:15:25 +0530
Subject: [PATCH 10/12] feat: replace unified loading approach with global
loading provider and refactor loading handling across components
- Introduced GlobalLoadingProvider to manage a consistent loading overlay across the application.
- Replaced existing loading implementations with useGlobalLoadingEffect hook for better control and to prevent animation resets.
- Updated components such as LoginPage, AuthCallbackPage, and DashboardLayout to utilize the new global loading mechanism.
- Removed UnifiedLoadingScreen component to streamline loading management and enhance user experience.
---
surfsense_web/app/(home)/layout.tsx | 6 +-
surfsense_web/app/(home)/login/page.tsx | 12 +-
surfsense_web/app/auth/callback/loading.tsx | 21 ++--
surfsense_web/app/auth/callback/page.tsx | 8 +-
.../[search_space_id]/client-layout.tsx | 26 +++--
surfsense_web/app/dashboard/layout.tsx | 9 +-
surfsense_web/app/dashboard/loading.tsx | 21 ++--
surfsense_web/app/dashboard/page.tsx | 13 ++-
surfsense_web/app/layout.tsx | 5 +-
surfsense_web/atoms/ui/loading.atoms.ts | 31 ++++++
surfsense_web/components/TokenHandler.tsx | 10 +-
.../components/providers/ElectricProvider.tsx | 16 +--
.../providers/GlobalLoadingProvider.tsx | 80 ++++++++++++++
.../components/ui/unified-loading-screen.tsx | 72 ------------
surfsense_web/hooks/use-global-loading.ts | 104 ++++++++++++++++++
15 files changed, 294 insertions(+), 140 deletions(-)
create mode 100644 surfsense_web/atoms/ui/loading.atoms.ts
create mode 100644 surfsense_web/components/providers/GlobalLoadingProvider.tsx
delete mode 100644 surfsense_web/components/ui/unified-loading-screen.tsx
create mode 100644 surfsense_web/hooks/use-global-loading.ts
diff --git a/surfsense_web/app/(home)/layout.tsx b/surfsense_web/app/(home)/layout.tsx
index 9488ee875..f1ceffac0 100644
--- a/surfsense_web/app/(home)/layout.tsx
+++ b/surfsense_web/app/(home)/layout.tsx
@@ -1,14 +1,18 @@
"use client";
+import { usePathname } from "next/navigation";
import { FooterNew } from "@/components/homepage/footer-new";
import { Navbar } from "@/components/homepage/navbar";
export default function HomePageLayout({ children }: { children: React.ReactNode }) {
+ const pathname = usePathname();
+ const isAuthPage = pathname === "/login" || pathname === "/register";
+
return (
{children}
-
+ {!isAuthPage && }
);
}
diff --git a/surfsense_web/app/(home)/login/page.tsx b/surfsense_web/app/(home)/login/page.tsx
index 0026fdcd0..0dc9c445f 100644
--- a/surfsense_web/app/(home)/login/page.tsx
+++ b/surfsense_web/app/(home)/login/page.tsx
@@ -6,7 +6,7 @@ import { useTranslations } from "next-intl";
import { Suspense, useEffect, useState } from "react";
import { toast } from "sonner";
import { Logo } from "@/components/Logo";
-import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
+import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors";
import { AUTH_TYPE } from "@/lib/env-config";
import { AmbientBackground } from "./AmbientBackground";
@@ -92,9 +92,12 @@ function LoginContent() {
setIsLoading(false);
}, [searchParams, t, tCommon]);
- // Show loading state while determining auth type
+ // Use global loading screen for auth type determination - spinner animation won't reset
+ useGlobalLoadingEffect(isLoading, tCommon("loading"), "login");
+
+ // Show nothing while loading - the GlobalLoadingProvider handles the loading UI
if (isLoading) {
- return
;
+ return null;
}
if (authType === "GOOGLE") {
@@ -176,8 +179,9 @@ function LoginContent() {
}
export default function LoginPage() {
+ // Suspense fallback returns null - the GlobalLoadingProvider handles the loading UI
return (
-
}>
+
);
diff --git a/surfsense_web/app/auth/callback/loading.tsx b/surfsense_web/app/auth/callback/loading.tsx
index 676ca3632..0c94e1ee0 100644
--- a/surfsense_web/app/auth/callback/loading.tsx
+++ b/surfsense_web/app/auth/callback/loading.tsx
@@ -1,21 +1,14 @@
"use client";
import { useTranslations } from "next-intl";
-import { Spinner } from "@/components/ui/spinner";
+import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
export default function AuthCallbackLoading() {
const t = useTranslations("auth");
- return (
-
-
-
-
-
-
- {t("processing_authentication")}
-
-
-
- );
-}
+ // Use global loading - spinner animation won't reset when page transitions
+ useGlobalLoadingEffect(true, t("processing_authentication"), "default");
+
+ // Return null - the GlobalLoadingProvider handles the loading UI
+ return null;
+}
diff --git a/surfsense_web/app/auth/callback/page.tsx b/surfsense_web/app/auth/callback/page.tsx
index 920581654..4050eefb6 100644
--- a/surfsense_web/app/auth/callback/page.tsx
+++ b/surfsense_web/app/auth/callback/page.tsx
@@ -1,15 +1,13 @@
"use client";
import { Suspense } from "react";
-import { useTranslations } from "next-intl";
-import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
import TokenHandler from "@/components/TokenHandler";
export default function AuthCallbackPage() {
- const t = useTranslations("auth");
-
+ // Suspense fallback returns null - the GlobalLoadingProvider handles the loading UI
+ // TokenHandler uses useGlobalLoadingEffect to show the loading screen
return (
-
}>
+
- );
+ isAutoConfiguring);
+
+ // Use global loading screen - spinner animation won't reset
+ useGlobalLoadingEffect(
+ shouldShowLoading,
+ isAutoConfiguring ? t("setting_up_ai") : t("checking_llm_prefs"),
+ "default"
+ );
+
+ if (shouldShowLoading) {
+ return null;
}
if (error && !hasCheckedOnboarding && !isOnboardingPage) {
diff --git a/surfsense_web/app/dashboard/layout.tsx b/surfsense_web/app/dashboard/layout.tsx
index 8ffef229c..6a548ea51 100644
--- a/surfsense_web/app/dashboard/layout.tsx
+++ b/surfsense_web/app/dashboard/layout.tsx
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
-import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
+import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getBearerToken, redirectToLogin } from "@/lib/auth-utils";
interface DashboardLayoutProps {
@@ -13,6 +13,9 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
const t = useTranslations("dashboard");
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
+ // Use the global loading screen - spinner animation won't reset
+ useGlobalLoadingEffect(isCheckingAuth, t("checking_auth"), "default");
+
useEffect(() => {
// Check if user is authenticated
const token = getBearerToken();
@@ -24,9 +27,9 @@ export default function DashboardLayout({ children }: DashboardLayoutProps) {
setIsCheckingAuth(false);
}, []);
- // Show loading screen while checking authentication
+ // Return null while loading - the global provider handles the loading UI
if (isCheckingAuth) {
- return ;
+ return null;
}
return (
diff --git a/surfsense_web/app/dashboard/loading.tsx b/surfsense_web/app/dashboard/loading.tsx
index b18c5dd75..2eee93651 100644
--- a/surfsense_web/app/dashboard/loading.tsx
+++ b/surfsense_web/app/dashboard/loading.tsx
@@ -1,21 +1,14 @@
"use client";
import { useTranslations } from "next-intl";
-import { Spinner } from "@/components/ui/spinner";
+import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
export default function DashboardLoading() {
const t = useTranslations("common");
- return (
-
-
-
-
-
-
- {t("loading")}
-
-
-
- );
-}
+ // Use global loading - spinner animation won't reset when page transitions
+ useGlobalLoadingEffect(true, t("loading"), "default");
+
+ // Return null - the GlobalLoadingProvider handles the loading UI
+ return null;
+}
diff --git a/surfsense_web/app/dashboard/page.tsx b/surfsense_web/app/dashboard/page.tsx
index 440f37123..504d172c3 100644
--- a/surfsense_web/app/dashboard/page.tsx
+++ b/surfsense_web/app/dashboard/page.tsx
@@ -18,7 +18,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
-import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
+import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
function ErrorScreen({ message }: { message: string }) {
const t = useTranslations("dashboard");
@@ -102,11 +102,16 @@ export default function DashboardPage() {
}
}, [isLoading, searchSpaces, router]);
- if (isLoading) return ;
+ // Show loading while fetching or while we have spaces and are about to redirect
+ const shouldShowLoading = isLoading || searchSpaces.length > 0;
+
+ // Use global loading screen - spinner animation won't reset
+ useGlobalLoadingEffect(shouldShowLoading, t("fetching_spaces"), "default");
+
if (error) return ;
- if (searchSpaces.length > 0) {
- return ;
+ if (shouldShowLoading) {
+ return null;
}
return (
diff --git a/surfsense_web/app/layout.tsx b/surfsense_web/app/layout.tsx
index 2f9921629..3722dc26e 100644
--- a/surfsense_web/app/layout.tsx
+++ b/surfsense_web/app/layout.tsx
@@ -3,6 +3,7 @@ import "./globals.css";
import { RootProvider } from "fumadocs-ui/provider/next";
import { Roboto } from "next/font/google";
import { ElectricProvider } from "@/components/providers/ElectricProvider";
+import { GlobalLoadingProvider } from "@/components/providers/GlobalLoadingProvider";
import { I18nProvider } from "@/components/providers/I18nProvider";
import { PostHogProvider } from "@/components/providers/PostHogProvider";
import { ThemeProvider } from "@/components/theme/theme-provider";
@@ -104,7 +105,9 @@ export default function RootLayout({
>
- {children}
+
+ {children}
+
diff --git a/surfsense_web/atoms/ui/loading.atoms.ts b/surfsense_web/atoms/ui/loading.atoms.ts
new file mode 100644
index 000000000..6c6cac1a2
--- /dev/null
+++ b/surfsense_web/atoms/ui/loading.atoms.ts
@@ -0,0 +1,31 @@
+import { atom } from "jotai";
+
+interface GlobalLoadingState {
+ isLoading: boolean;
+ message?: string;
+ variant: "login" | "default";
+}
+
+export const globalLoadingAtom = atom({
+ isLoading: false,
+ message: undefined,
+ variant: "default",
+});
+
+// Helper atom for showing global loading
+export const showGlobalLoadingAtom = atom(
+ null,
+ (
+ get,
+ set,
+ { message, variant = "default" }: { message?: string; variant?: "login" | "default" }
+ ) => {
+ set(globalLoadingAtom, { isLoading: true, message, variant });
+ }
+);
+
+// Helper atom for hiding global loading
+export const hideGlobalLoadingAtom = atom(null, (get, set) => {
+ set(globalLoadingAtom, { isLoading: false, message: undefined, variant: "default" });
+});
+
diff --git a/surfsense_web/components/TokenHandler.tsx b/surfsense_web/components/TokenHandler.tsx
index 3f98451ef..35408c1b2 100644
--- a/surfsense_web/components/TokenHandler.tsx
+++ b/surfsense_web/components/TokenHandler.tsx
@@ -3,7 +3,7 @@
import { useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
-import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
+import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getAndClearRedirectPath, setBearerToken } from "@/lib/auth-utils";
import { trackLoginSuccess } from "@/lib/posthog/events";
@@ -30,6 +30,9 @@ const TokenHandler = ({
const t = useTranslations("auth");
const searchParams = useSearchParams();
+ // Always show loading for this component - spinner animation won't reset
+ useGlobalLoadingEffect(true, t("processing_authentication"), "default");
+
useEffect(() => {
// Only run on client-side
if (typeof window === "undefined") return;
@@ -69,9 +72,8 @@ const TokenHandler = ({
}
}, [searchParams, tokenParamName, storageKey, redirectPath]);
- return (
-
- );
+ // Return null - the global provider handles the loading UI
+ return null;
};
export default TokenHandler;
diff --git a/surfsense_web/components/providers/ElectricProvider.tsx b/surfsense_web/components/providers/ElectricProvider.tsx
index 68263b036..07d736c64 100644
--- a/surfsense_web/components/providers/ElectricProvider.tsx
+++ b/surfsense_web/components/providers/ElectricProvider.tsx
@@ -4,7 +4,7 @@ import { useAtomValue } from "jotai";
import { useTranslations } from "next-intl";
import { useEffect, useRef, useState } from "react";
import { currentUserAtom } from "@/atoms/user/user-query.atoms";
-import { UnifiedLoadingScreen } from "@/components/ui/unified-loading-screen";
+import { useGlobalLoadingEffect } from "@/hooks/use-global-loading";
import { getBearerToken } from "@/lib/auth-utils";
import {
cleanupElectric,
@@ -113,19 +113,21 @@ export function ElectricProvider({ children }: ElectricProviderProps) {
// This prevents showing loading screen for unauthenticated users on homepage
const hasToken = typeof window !== "undefined" && !!getBearerToken();
+ // Determine if we should show loading
+ const shouldShowLoading = hasToken && isUserLoaded && !!user?.id && !electricClient && !error;
+
+ // Use global loading hook with ownership tracking - prevents flash during transitions
+ useGlobalLoadingEffect(shouldShowLoading, t("initializing"), "default");
+
// For non-authenticated pages (like landing page), render immediately with null context
// Also render immediately if user query failed (e.g., token expired)
if (!hasToken || !isUserLoaded || !user?.id || isUserError) {
return {children};
}
- // Show loading state while initializing for authenticated users
+ // Return children with null context while initializing - the global provider handles the loading UI
if (!electricClient && !error) {
- return (
-
-
-
- );
+ return {children};
}
// If there's an error, still render but warn
diff --git a/surfsense_web/components/providers/GlobalLoadingProvider.tsx b/surfsense_web/components/providers/GlobalLoadingProvider.tsx
new file mode 100644
index 000000000..fe816b316
--- /dev/null
+++ b/surfsense_web/components/providers/GlobalLoadingProvider.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { useAtomValue } from "jotai";
+import { useEffect, useState } from "react";
+import { createPortal } from "react-dom";
+import { globalLoadingAtom } from "@/atoms/ui/loading.atoms";
+import { Logo } from "@/components/Logo";
+import { Spinner } from "@/components/ui/spinner";
+import { AmbientBackground } from "@/app/(home)/login/AmbientBackground";
+import { cn } from "@/lib/utils";
+
+/**
+ * GlobalLoadingProvider renders a persistent loading overlay.
+ * The spinner is ALWAYS in the DOM to prevent animation reset when
+ * loading states change between different pages/components.
+ *
+ * Visibility is controlled via CSS opacity/pointer-events, NOT mounting/unmounting.
+ */
+export function GlobalLoadingProvider({ children }: { children: React.ReactNode }) {
+ const [mounted, setMounted] = useState(false);
+ const { isLoading, message, variant } = useAtomValue(globalLoadingAtom);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ // The overlay is ALWAYS rendered, but visibility is controlled by CSS
+ // This prevents the spinner animation from resetting
+ const loadingOverlay = (
+
+ {variant === "login" ? (
+
+
+
+
+
+
+ {/* Spinner is always mounted, animation never resets */}
+
+
+
+ {message}
+
+
+
+
+ ) : (
+
+
+
+ {/* Spinner is always mounted, animation never resets */}
+
+
+
+ {message}
+
+
+
+ )}
+
+ );
+
+ // Render inline during SSR/before hydration, use portal after mounting
+ // This prevents the white flash during initial render
+ return (
+ <>
+ {children}
+ {mounted ? createPortal(loadingOverlay, document.body) : loadingOverlay}
+ >
+ );
+}
+
diff --git a/surfsense_web/components/ui/unified-loading-screen.tsx b/surfsense_web/components/ui/unified-loading-screen.tsx
deleted file mode 100644
index 7ed6b83d9..000000000
--- a/surfsense_web/components/ui/unified-loading-screen.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-"use client";
-
-import { useEffect, useState } from "react";
-import { createPortal } from "react-dom";
-import { Logo } from "@/components/Logo";
-import { Spinner } from "@/components/ui/spinner";
-import { AmbientBackground } from "@/app/(home)/login/AmbientBackground";
-
-interface UnifiedLoadingScreenProps {
- /** Optional message to display below the spinner */
- message?: string;
- /** Visual style variant */
- variant?: "login" | "default";
-}
-
-export function UnifiedLoadingScreen({
- message,
- variant = "default",
-}: UnifiedLoadingScreenProps) {
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => {
- setMounted(true);
- }, []);
-
- // Fixed-size container to prevent layout shifts
- const spinnerContainer = (
-
-
-
- );
-
- const content = variant === "login" ? (
-
-
-
-
-
- {spinnerContainer}
- {message && (
-
- {message}
-
- )}
-
-
-
- ) : (
-
-
- {spinnerContainer}
- {message && (
-
- {message}
-
- )}
-
-
- );
-
- // Render inline during SSR, use portal after mounting
- // This prevents the black flash during initial render
- if (!mounted) {
- return content;
- }
-
- return createPortal(content, document.body);
-}
-
diff --git a/surfsense_web/hooks/use-global-loading.ts b/surfsense_web/hooks/use-global-loading.ts
new file mode 100644
index 000000000..baaa1f089
--- /dev/null
+++ b/surfsense_web/hooks/use-global-loading.ts
@@ -0,0 +1,104 @@
+"use client";
+
+import { useAtom } from "jotai";
+import { useCallback, useEffect, useRef } from "react";
+import { globalLoadingAtom } from "@/atoms/ui/loading.atoms";
+
+// Global counter to generate unique IDs for each loading request
+let loadingIdCounter = 0;
+
+// Track the current active loading ID globally
+let currentLoadingId: number | null = null;
+
+// Pending hide timeout - allows new loading requests to take over before hiding
+let pendingHideTimeout: ReturnType | null = null;
+
+/**
+ * Hook to control the global loading screen.
+ * The spinner is always mounted in the DOM to prevent animation reset.
+ */
+export function useGlobalLoading() {
+ const [loading, setLoading] = useAtom(globalLoadingAtom);
+
+ const show = useCallback(
+ (message?: string, variant: "login" | "default" = "default") => {
+ // Cancel any pending hide - new loading request takes over
+ if (pendingHideTimeout) {
+ clearTimeout(pendingHideTimeout);
+ pendingHideTimeout = null;
+ }
+
+ const id = ++loadingIdCounter;
+ currentLoadingId = id;
+ setLoading({ isLoading: true, message, variant });
+ return id;
+ },
+ [setLoading]
+ );
+
+ const hide = useCallback(
+ (id?: number) => {
+ // Only hide if this is the current loading, or if no ID provided (force hide)
+ if (id === undefined || id === currentLoadingId) {
+ // Use a small delay to allow React to flush pending mounts
+ // This prevents flash when transitioning between loading states
+ if (pendingHideTimeout) {
+ clearTimeout(pendingHideTimeout);
+ }
+
+ pendingHideTimeout = setTimeout(() => {
+ // Double-check we're still the current loading after the delay
+ if (id === undefined || id === currentLoadingId) {
+ currentLoadingId = null;
+ setLoading({ isLoading: false, message: undefined, variant: "default" });
+ }
+ pendingHideTimeout = null;
+ }, 50); // Small delay to allow next component to mount and show loading
+ }
+ },
+ [setLoading]
+ );
+
+ return { show, hide, isLoading: loading.isLoading };
+}
+
+/**
+ * Hook that automatically shows/hides the global loading screen based on a condition.
+ * Useful for components that show loading on mount and hide on unmount.
+ *
+ * Uses ownership tracking to prevent flashes when multiple components
+ * transition loading states (e.g., layout → page).
+ *
+ * @param shouldShow - Whether the loading screen should be visible
+ * @param message - Optional message to display
+ * @param variant - Visual style variant ("login" or "default")
+ */
+export function useGlobalLoadingEffect(
+ shouldShow: boolean,
+ message?: string,
+ variant: "login" | "default" = "default"
+) {
+ const { show, hide } = useGlobalLoading();
+ const loadingIdRef = useRef(null);
+
+ useEffect(() => {
+ if (shouldShow) {
+ // Show loading and store the ID
+ loadingIdRef.current = show(message, variant);
+ } else if (loadingIdRef.current !== null) {
+ // Only hide if we were the ones showing loading
+ hide(loadingIdRef.current);
+ loadingIdRef.current = null;
+ }
+ }, [shouldShow, message, variant, show, hide]);
+
+ // Cleanup on unmount - only hide if we're still the active loading
+ useEffect(() => {
+ return () => {
+ if (loadingIdRef.current !== null) {
+ hide(loadingIdRef.current);
+ loadingIdRef.current = null;
+ }
+ };
+ }, [hide]);
+}
From 03e38ab17898b6001ce902c9ad771ff684496e46 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sun, 25 Jan 2026 16:16:09 +0530
Subject: [PATCH 11/12] chore: ran frontend linting
---
.../[search_space_id]/client-layout.tsx | 4 +-
surfsense_web/atoms/ui/loading.atoms.ts | 1 -
.../components/bookstack-connect-form.tsx | 1 -
surfsense_web/components/homepage/navbar.tsx | 8 ++-
.../ui/dialogs/CreateSearchSpaceDialog.tsx | 19 ++++++-
.../layout/ui/sidebar/ChatListItem.tsx | 9 ++-
.../layout/ui/sidebar/SidebarUserProfile.tsx | 56 ++++++++-----------
.../new-chat/source-detail-panel.tsx | 16 ++----
.../providers/GlobalLoadingProvider.tsx | 5 +-
.../settings/general-settings-manager.tsx | 9 +--
.../components/settings/llm-role-manager.tsx | 10 +---
11 files changed, 68 insertions(+), 70 deletions(-)
diff --git a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx
index b2af3a28a..e6730d8d1 100644
--- a/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx
+++ b/surfsense_web/app/dashboard/[search_space_id]/client-layout.tsx
@@ -148,10 +148,10 @@ export function DashboardClientLayout({
// Determine if we should show loading
const shouldShowLoading =
- ((!hasCheckedOnboarding &&
+ (!hasCheckedOnboarding &&
(loading || accessLoading || globalConfigsLoading) &&
!isOnboardingPage) ||
- isAutoConfiguring);
+ isAutoConfiguring;
// Use global loading screen - spinner animation won't reset
useGlobalLoadingEffect(
diff --git a/surfsense_web/atoms/ui/loading.atoms.ts b/surfsense_web/atoms/ui/loading.atoms.ts
index 6c6cac1a2..f10d9247b 100644
--- a/surfsense_web/atoms/ui/loading.atoms.ts
+++ b/surfsense_web/atoms/ui/loading.atoms.ts
@@ -28,4 +28,3 @@ export const showGlobalLoadingAtom = atom(
export const hideGlobalLoadingAtom = atom(null, (get, set) => {
set(globalLoadingAtom, { isLoading: false, message: undefined, variant: "default" });
});
-
diff --git a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx
index 789e23787..65edb095c 100644
--- a/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx
+++ b/surfsense_web/components/assistant-ui/connector-popup/connect-forms/components/bookstack-connect-form.tsx
@@ -296,7 +296,6 @@ export const BookStackConnectForm: FC = ({ onSubmit, isSubmitt
)}
-
);
};
diff --git a/surfsense_web/components/homepage/navbar.tsx b/surfsense_web/components/homepage/navbar.tsx
index 2a8820bd6..c83d3556a 100644
--- a/surfsense_web/components/homepage/navbar.tsx
+++ b/surfsense_web/components/homepage/navbar.tsx
@@ -1,5 +1,11 @@
"use client";
-import { IconBrandDiscord, IconBrandGithub, IconBrandReddit, IconMenu2, IconX } from "@tabler/icons-react";
+import {
+ IconBrandDiscord,
+ IconBrandGithub,
+ IconBrandReddit,
+ IconMenu2,
+ IconX,
+} from "@tabler/icons-react";
import { AnimatePresence, motion } from "motion/react";
import Link from "next/link";
import { useEffect, useState } from "react";
diff --git a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx
index c0f92b440..63a430004 100644
--- a/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx
+++ b/surfsense_web/components/layout/ui/dialogs/CreateSearchSpaceDialog.tsx
@@ -107,7 +107,12 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
{t("name_label")}
-
+
@@ -126,7 +131,11 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
-
+
@@ -143,7 +152,11 @@ export function CreateSearchSpaceDialog({ open, onOpenChange }: CreateSearchSpac
>
{tCommon("cancel")}
-
)}
diff --git a/surfsense_web/components/providers/GlobalLoadingProvider.tsx b/surfsense_web/components/providers/GlobalLoadingProvider.tsx
index fe816b316..e72f2b876 100644
--- a/surfsense_web/components/providers/GlobalLoadingProvider.tsx
+++ b/surfsense_web/components/providers/GlobalLoadingProvider.tsx
@@ -30,8 +30,8 @@ export function GlobalLoadingProvider({ children }: { children: React.ReactNode
);
}
-
diff --git a/surfsense_web/components/settings/general-settings-manager.tsx b/surfsense_web/components/settings/general-settings-manager.tsx
index fb82a8fdf..b2f93402a 100644
--- a/surfsense_web/components/settings/general-settings-manager.tsx
+++ b/surfsense_web/components/settings/general-settings-manager.tsx
@@ -111,8 +111,8 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager
- Update your search space name and description. These details help identify and organize your
- workspace.
+ Update your search space name and description. These details help identify and organize
+ your workspace.
@@ -144,9 +144,7 @@ export function GeneralSettingsManager({ searchSpaceId }: GeneralSettingsManager
);
}
-
diff --git a/surfsense_web/components/settings/llm-role-manager.tsx b/surfsense_web/components/settings/llm-role-manager.tsx
index 6e600defe..649507d77 100644
--- a/surfsense_web/components/settings/llm-role-manager.tsx
+++ b/surfsense_web/components/settings/llm-role-manager.tsx
@@ -1,15 +1,7 @@
"use client";
import { useAtomValue } from "jotai";
-import {
- AlertCircle,
- Bot,
- CheckCircle,
- FileText,
- RefreshCw,
- RotateCcw,
- Save,
-} from "lucide-react";
+import { AlertCircle, Bot, CheckCircle, FileText, RefreshCw, RotateCcw, Save } from "lucide-react";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { toast } from "sonner";
From 86f2e798a9bd679345f0f973067db1ab2ec54ddc Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sun, 25 Jan 2026 16:28:34 +0530
Subject: [PATCH 12/12] fix: added spinner in login and register page
---
.../app/(home)/login/LocalLoginForm.tsx | 20 ++++++++++---------
surfsense_web/app/(home)/register/page.tsx | 19 +++++++++---------
.../components/homepage/hero-section.tsx | 2 +-
surfsense_web/messages/en.json | 1 +
surfsense_web/messages/zh.json | 1 +
5 files changed, 24 insertions(+), 19 deletions(-)
diff --git a/surfsense_web/app/(home)/login/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx
index 62d2a2a66..bd9c2c1b4 100644
--- a/surfsense_web/app/(home)/login/LocalLoginForm.tsx
+++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx
@@ -12,6 +12,7 @@ import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-err
import { AUTH_TYPE } from "@/lib/env-config";
import { ValidationError } from "@/lib/error";
import { trackLoginAttempt, trackLoginFailure, trackLoginSuccess } from "@/lib/posthog/events";
+import { Spinner } from "@/components/ui/spinner";
export function LocalLoginForm() {
const t = useTranslations("auth");
@@ -42,9 +43,6 @@ export function LocalLoginForm() {
// Track login attempt
trackLoginAttempt("local");
- // Show loading toast
- const loadingToast = toast.loading(tCommon("loading"));
-
try {
const data = await login({
username,
@@ -62,8 +60,7 @@ export function LocalLoginForm() {
// Success toast
toast.success(t("login_success"), {
- id: loadingToast,
- description: "Redirecting to dashboard...",
+ description: "Redirecting to dashboard",
duration: 2000,
});
@@ -76,7 +73,6 @@ export function LocalLoginForm() {
trackLoginFailure("local", err.message);
setError({ title: err.name, message: err.message });
toast.error(err.name, {
- id: loadingToast,
description: err.message,
duration: 6000,
});
@@ -106,7 +102,6 @@ export function LocalLoginForm() {
// Show error toast with conditional retry action
const toastOptions: any = {
- id: loadingToast,
description: errorDetails.description,
duration: 6000,
};
@@ -244,9 +239,16 @@ export function LocalLoginForm() {
- {isLoggingIn ? tCommon("loading") : t("sign_in")}
+ {isLoggingIn ? (
+ <>
+
+ {t("signing_in")}
+ >
+ ) : (
+ t("sign_in")
+ )}
diff --git a/surfsense_web/app/(home)/register/page.tsx b/surfsense_web/app/(home)/register/page.tsx
index 243ad4c60..f8170ff63 100644
--- a/surfsense_web/app/(home)/register/page.tsx
+++ b/surfsense_web/app/(home)/register/page.tsx
@@ -18,6 +18,7 @@ import {
trackRegistrationSuccess,
} from "@/lib/posthog/events";
import { AmbientBackground } from "../login/AmbientBackground";
+import { Spinner } from "@/components/ui/spinner";
export default function RegisterPage() {
const t = useTranslations("auth");
@@ -60,9 +61,6 @@ export default function RegisterPage() {
// Track registration attempt
trackRegistrationAttempt();
- // Show loading toast
- const loadingToast = toast.loading(t("creating_account"));
-
try {
await register({
email,
@@ -77,7 +75,6 @@ export default function RegisterPage() {
// Success toast
toast.success(t("register_success"), {
- id: loadingToast,
description: t("redirecting_login"),
duration: 2000,
});
@@ -95,7 +92,6 @@ export default function RegisterPage() {
trackRegistrationFailure("Registration disabled");
setError({ title: "Registration is disabled", message: friendlyMessage });
toast.error("Registration is disabled", {
- id: loadingToast,
description: friendlyMessage,
duration: 6000,
});
@@ -109,7 +105,6 @@ export default function RegisterPage() {
trackRegistrationFailure(err.message);
setError({ title: err.name, message: err.message });
toast.error(err.name, {
- id: loadingToast,
description: err.message,
duration: 6000,
});
@@ -137,7 +132,6 @@ export default function RegisterPage() {
// Show error toast with conditional retry action
const toastOptions: any = {
- id: loadingToast,
description: errorDetails.description,
duration: 6000,
};
@@ -295,9 +289,16 @@ export default function RegisterPage() {
- {isRegistering ? t("creating_account_btn") : t("register")}
+ {isRegistering ? (
+ <>
+
+ {t("creating_account_btn")}
+ >
+ ) : (
+ t("register")
+ )}
diff --git a/surfsense_web/components/homepage/hero-section.tsx b/surfsense_web/components/homepage/hero-section.tsx
index 0ff61ac28..7ccdd850c 100644
--- a/surfsense_web/components/homepage/hero-section.tsx
+++ b/surfsense_web/components/homepage/hero-section.tsx
@@ -184,7 +184,7 @@ function GetStartedButton() {
return (
Get Started
diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json
index f23359bf2..f14c73ddc 100644
--- a/surfsense_web/messages/en.json
+++ b/surfsense_web/messages/en.json
@@ -46,6 +46,7 @@
"hide_password": "Hide password",
"remember_me": "Remember Me",
"sign_in": "Sign In",
+ "signing_in": "Signing in",
"sign_up": "Sign Up",
"sign_in_with": "Sign in with {provider}",
"dont_have_account": "Don't have an account?",
diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json
index 3c4b6cf34..6838b0f52 100644
--- a/surfsense_web/messages/zh.json
+++ b/surfsense_web/messages/zh.json
@@ -46,6 +46,7 @@
"hide_password": "隐藏密码",
"remember_me": "记住我",
"sign_in": "登录",
+ "signing_in": "正在登录",
"sign_up": "注册",
"sign_in_with": "使用 {provider} 登录",
"dont_have_account": "还没有账户?",