From 67ee8f4bb7bb1fe8c66453613cde6971b1367634 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 8 Apr 2026 00:01:30 -0700 Subject: [PATCH 1/3] fix: replace window.location with router.push for client-side navigation --- surfsense_web/components/UserDropdown.tsx | 10 ++-------- .../components/layout/ui/tabs/DocumentTabContent.tsx | 4 +++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/surfsense_web/components/UserDropdown.tsx b/surfsense_web/components/UserDropdown.tsx index 53fd61b20..0ffb172bf 100644 --- a/surfsense_web/components/UserDropdown.tsx +++ b/surfsense_web/components/UserDropdown.tsx @@ -40,19 +40,13 @@ export function UserDropdown({ await logout(); - router.push("/"); + router.push(getLoginPath()); router.refresh(); - if (typeof window !== "undefined") { - window.location.href = getLoginPath(); - } } catch (error) { console.error("Error during logout:", error); await logout(); - router.push("/"); + router.push(getLoginPath()); router.refresh(); - if (typeof window !== "undefined") { - window.location.href = getLoginPath(); - } } }; diff --git a/surfsense_web/components/layout/ui/tabs/DocumentTabContent.tsx b/surfsense_web/components/layout/ui/tabs/DocumentTabContent.tsx index 97c5b7cd9..026f3afc3 100644 --- a/surfsense_web/components/layout/ui/tabs/DocumentTabContent.tsx +++ b/surfsense_web/components/layout/ui/tabs/DocumentTabContent.tsx @@ -1,6 +1,7 @@ "use client"; import { Download, FileQuestionMark, FileText, Loader2, PenLine, RefreshCw } from "lucide-react"; +import { useRouter } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { PlateEditor } from "@/components/editor/plate-editor"; @@ -60,6 +61,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen const markdownRef = useRef(""); const initialLoadDone = useRef(false); const changeCountRef = useRef(0); + const router = useRouter(); const isLargeDocument = (doc?.content_size_bytes ?? 0) > LARGE_DOCUMENT_THRESHOLD; @@ -190,7 +192,7 @@ export function DocumentTabContent({ documentId, searchSpaceId, title }: Documen variant="outline" size="sm" className="mt-1 gap-1.5" - onClick={() => window.location.reload()} + onClick={() => router.refresh()} > Retry From 90ed853a2624014353d8bc52fdd8ffcfd0082b99 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:36:24 -0700 Subject: [PATCH 2/3] perf: remove unnecessary useMemo wrapping simple boolean expressions Replace useMemo calls that wrap trivial boolean checks with plain const expressions. The memo overhead exceeds the cost of these simple permission checks and mode comparisons. Fixes #1052 --- .../components/new-chat/chat-share-button.tsx | 9 +++------ .../components/new-chat/model-selector.tsx | 12 ++++-------- .../public-chat-snapshots-manager.tsx | 14 ++++---------- .../settings/image-model-manager.tsx | 14 ++++---------- .../settings/model-config-manager.tsx | 18 +++--------------- 5 files changed, 18 insertions(+), 49 deletions(-) diff --git a/surfsense_web/components/new-chat/chat-share-button.tsx b/surfsense_web/components/new-chat/chat-share-button.tsx index 4fc35aba1..2f16ab860 100644 --- a/surfsense_web/components/new-chat/chat-share-button.tsx +++ b/surfsense_web/components/new-chat/chat-share-button.tsx @@ -4,7 +4,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useAtomValue, useSetAtom } from "jotai"; import { Earth, User, Users } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useState } from "react"; import { toast } from "sonner"; import { currentThreadAtom, setThreadVisibilityAtom } from "@/atoms/chat/current-thread.atom"; import { myAccessAtom } from "@/atoms/members/members-query.atoms"; @@ -63,11 +63,8 @@ export function ChatShareButton({ thread, onVisibilityChange, className }: ChatS // Permission check for public sharing const { data: access } = useAtomValue(myAccessAtom); - const canCreatePublicLink = useMemo(() => { - if (!access) return false; - if (access.is_owner) return true; - return access.permissions?.includes("public_sharing:create") ?? false; - }, [access]); + const canCreatePublicLink = + !!access && (access.is_owner || (access.permissions?.includes("public_sharing:create") ?? false)); // Query to check if thread has public snapshots const { data: snapshotsData } = useQuery({ diff --git a/surfsense_web/components/new-chat/model-selector.tsx b/surfsense_web/components/new-chat/model-selector.tsx index 46b4a2c3a..ec5bf6760 100644 --- a/surfsense_web/components/new-chat/model-selector.tsx +++ b/surfsense_web/components/new-chat/model-selector.tsx @@ -121,9 +121,8 @@ export function ModelSelector({ return llmUserConfigs?.find((c) => c.id === agentLlmId) ?? null; }, [preferences, llmGlobalConfigs, llmUserConfigs]); - const isLLMAutoMode = useMemo(() => { - return currentLLMConfig && "is_auto_mode" in currentLLMConfig && currentLLMConfig.is_auto_mode; - }, [currentLLMConfig]); + const isLLMAutoMode = + currentLLMConfig && "is_auto_mode" in currentLLMConfig && currentLLMConfig.is_auto_mode; // ─── Image current config ─── const currentImageConfig = useMemo(() => { @@ -135,11 +134,8 @@ export function ModelSelector({ return imageUserConfigs?.find((c) => c.id === id) ?? null; }, [preferences, imageGlobalConfigs, imageUserConfigs]); - const isImageAutoMode = useMemo(() => { - return ( - currentImageConfig && "is_auto_mode" in currentImageConfig && currentImageConfig.is_auto_mode - ); - }, [currentImageConfig]); + const isImageAutoMode = + currentImageConfig && "is_auto_mode" in currentImageConfig && currentImageConfig.is_auto_mode; // ─── Vision current config ─── const currentVisionConfig = useMemo(() => { diff --git a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx index 24d801409..7f8cd1b01 100644 --- a/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx +++ b/surfsense_web/components/public-chat-snapshots/public-chat-snapshots-manager.tsx @@ -43,17 +43,11 @@ export function PublicChatSnapshotsManager({ // Permissions const { data: access } = useAtomValue(myAccessAtom); - const canView = useMemo(() => { - if (!access) return false; - if (access.is_owner) return true; - return access.permissions?.includes("public_sharing:view") ?? false; - }, [access]); + const canView = + !!access && (access.is_owner || (access.permissions?.includes("public_sharing:view") ?? false)); - const canDelete = useMemo(() => { - if (!access) return false; - if (access.is_owner) return true; - return access.permissions?.includes("public_sharing:delete") ?? false; - }, [access]); + const canDelete = + !!access && (access.is_owner || (access.permissions?.includes("public_sharing:delete") ?? false)); // Mutations const { mutateAsync: deleteSnapshot } = useAtomValue(deletePublicChatSnapshotMutationAtom); diff --git a/surfsense_web/components/settings/image-model-manager.tsx b/surfsense_web/components/settings/image-model-manager.tsx index 23162b629..977e42efc 100644 --- a/surfsense_web/components/settings/image-model-manager.tsx +++ b/surfsense_web/components/settings/image-model-manager.tsx @@ -78,16 +78,10 @@ export function ImageModelManager({ searchSpaceId }: ImageModelManagerProps) { }, [members]); const { data: access } = useAtomValue(myAccessAtom); - const canCreate = useMemo(() => { - if (!access) return false; - if (access.is_owner) return true; - return access.permissions?.includes("image_generations:create") ?? false; - }, [access]); - const canDelete = useMemo(() => { - if (!access) return false; - if (access.is_owner) return true; - return access.permissions?.includes("image_generations:delete") ?? false; - }, [access]); + const canCreate = + !!access && (access.is_owner || (access.permissions?.includes("image_generations:create") ?? false)); + const canDelete = + !!access && (access.is_owner || (access.permissions?.includes("image_generations:delete") ?? false)); const canUpdate = canCreate; const isReadOnly = !canCreate && !canDelete; diff --git a/surfsense_web/components/settings/model-config-manager.tsx b/surfsense_web/components/settings/model-config-manager.tsx index db4f169f8..82464a0ed 100644 --- a/surfsense_web/components/settings/model-config-manager.tsx +++ b/surfsense_web/components/settings/model-config-manager.tsx @@ -89,21 +89,9 @@ export function ModelConfigManager({ searchSpaceId }: ModelConfigManagerProps) { // Permissions const { data: access } = useAtomValue(myAccessAtom); - const canCreate = useMemo(() => { - if (!access) return false; - if (access.is_owner) return true; - return access.permissions?.includes("llm_configs:create") ?? false; - }, [access]); - const canUpdate = useMemo(() => { - if (!access) return false; - if (access.is_owner) return true; - return access.permissions?.includes("llm_configs:update") ?? false; - }, [access]); - const canDelete = useMemo(() => { - if (!access) return false; - if (access.is_owner) return true; - return access.permissions?.includes("llm_configs:delete") ?? false; - }, [access]); + const canCreate = !!access && (access.is_owner || (access.permissions?.includes("llm_configs:create") ?? false)); + const canUpdate = !!access && (access.is_owner || (access.permissions?.includes("llm_configs:update") ?? false)); + const canDelete = !!access && (access.is_owner || (access.permissions?.includes("llm_configs:delete") ?? false)); const isReadOnly = !canCreate && !canUpdate && !canDelete; // Local state From 833ea18800d6a5e8790b113d3b56c0b62bb8f38c Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:39:14 -0700 Subject: [PATCH 3/3] fix: add cancelAnimationFrame cleanup in animated-tabs useEffect Store the requestAnimationFrame ID and cancel it on unmount to prevent updateActiveIndicator from running on an unmounted component. Fixes #1093 --- surfsense_web/components/ui/animated-tabs.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/surfsense_web/components/ui/animated-tabs.tsx b/surfsense_web/components/ui/animated-tabs.tsx index 58c988a7c..7bc12821f 100644 --- a/surfsense_web/components/ui/animated-tabs.tsx +++ b/surfsense_web/components/ui/animated-tabs.tsx @@ -310,7 +310,8 @@ const TabsList = forwardRef< }, [updateActiveIndicator]); useEffect(() => { - requestAnimationFrame(updateActiveIndicator); + const id = requestAnimationFrame(updateActiveIndicator); + return () => cancelAnimationFrame(id); }, [updateActiveIndicator]); const scrollTabToCenter = useCallback((index: number) => {