diff --git a/src/app/page.tsx b/src/app/page.tsx index 7cb4f0c..6626bd5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -48,24 +48,26 @@ export default function Home() { useState(null); const [currentProfileForVersionChange, setCurrentProfileForVersionChange] = useState(null); - const [isClient, setIsClient] = useState(false); - // Auto-update functionality - only initialize on client - const updateNotifications = useUpdateNotifications(); - const { checkForUpdates, isUpdating } = updateNotifications; - - // App auto-update functionality - const appUpdateNotifications = useAppUpdateNotifications(); - const { checkForAppUpdatesManual } = appUpdateNotifications; - - // Ensure we're on the client side to prevent hydration mismatches - useEffect(() => { - setIsClient(true); + // Simple profiles loader without updates check (for use as callback) + const loadProfiles = useCallback(async () => { + try { + const profileList = await invoke( + "list_browser_profiles", + ); + setProfiles(profileList); + } catch (err: unknown) { + console.error("Failed to load profiles:", err); + setError(`Failed to load profiles: ${JSON.stringify(err)}`); + } }, []); - const loadProfiles = useCallback(async () => { - if (!isClient) return; // Only run on client side + // Auto-update functionality - pass loadProfiles to refresh profiles after updates + const updateNotifications = useUpdateNotifications(loadProfiles); + const { checkForUpdates, isUpdating } = updateNotifications; + // Profiles loader with update check (for initial load and manual refresh) + const loadProfilesWithUpdateCheck = useCallback(async () => { try { const profileList = await invoke( "list_browser_profiles", @@ -78,12 +80,12 @@ export default function Home() { console.error("Failed to load profiles:", err); setError(`Failed to load profiles: ${JSON.stringify(err)}`); } - }, [checkForUpdates, isClient]); + }, [checkForUpdates]); + + useAppUpdateNotifications(); useEffect(() => { - if (!isClient) return; // Only run on client side - - void loadProfiles(); + void loadProfilesWithUpdateCheck(); // Check for startup default browser prompt void checkStartupPrompt(); @@ -105,11 +107,9 @@ export default function Home() { return () => { clearInterval(updateInterval); }; - }, [loadProfiles, checkForUpdates, isClient]); + }, [loadProfilesWithUpdateCheck, checkForUpdates]); const checkStartupPrompt = async () => { - if (!isClient) return; // Only run on client side - try { const shouldShow = await invoke( "should_show_settings_on_startup", @@ -123,8 +123,6 @@ export default function Home() { }; const checkStartupUrls = async () => { - if (!isClient) return; // Only run on client side - try { const hasStartupUrl = await invoke( "check_and_handle_startup_url", @@ -138,8 +136,6 @@ export default function Home() { }; const listenForUrlEvents = async () => { - if (!isClient) return; // Only run on client side - try { // Listen for URL open events from the deep link handler (when app is already running) await listen("url-open-request", (event) => { @@ -173,8 +169,6 @@ export default function Home() { }; const handleUrlOpen = async (url: string) => { - if (!isClient) return; // Only run on client side - try { // Use smart profile selection const result = await invoke("smart_open_url", { @@ -270,40 +264,33 @@ export default function Home() { const runningProfilesRef = useRef>(new Set()); - const checkBrowserStatus = useCallback( - async (profile: BrowserProfile) => { - if (!isClient) return; // Only run on client side + const checkBrowserStatus = useCallback(async (profile: BrowserProfile) => { + try { + const isRunning = await invoke("check_browser_status", { + profile, + }); - try { - const isRunning = await invoke("check_browser_status", { - profile, + const currentRunning = runningProfilesRef.current.has(profile.name); + + if (isRunning !== currentRunning) { + setRunningProfiles((prev) => { + const next = new Set(prev); + if (isRunning) { + next.add(profile.name); + } else { + next.delete(profile.name); + } + runningProfilesRef.current = next; + return next; }); - - const currentRunning = runningProfilesRef.current.has(profile.name); - - if (isRunning !== currentRunning) { - setRunningProfiles((prev) => { - const next = new Set(prev); - if (isRunning) { - next.add(profile.name); - } else { - next.delete(profile.name); - } - runningProfilesRef.current = next; - return next; - }); - } - } catch (err) { - console.error("Failed to check browser status:", err); } - }, - [isClient], - ); + } catch (err) { + console.error("Failed to check browser status:", err); + } + }, []); const launchProfile = useCallback( async (profile: BrowserProfile) => { - if (!isClient) return; // Only run on client side - setError(null); // Check if browser is disabled due to ongoing update @@ -337,11 +324,11 @@ export default function Home() { setError(`Failed to launch browser: ${JSON.stringify(err)}`); } }, - [loadProfiles, checkBrowserStatus, isUpdating, isClient], + [loadProfiles, checkBrowserStatus, isUpdating], ); useEffect(() => { - if (profiles.length === 0 || !isClient) return; + if (profiles.length === 0) return; const interval = setInterval(() => { for (const profile of profiles) { @@ -352,7 +339,7 @@ export default function Home() { return () => { clearInterval(interval); }; - }, [profiles, checkBrowserStatus, isClient]); + }, [profiles, checkBrowserStatus]); useEffect(() => { runningProfilesRef.current = runningProfiles; @@ -408,53 +395,6 @@ export default function Home() { [loadProfiles], ); - // Don't render anything until we're on the client side to prevent hydration issues - if (!isClient) { - return ( -
-
- - -
- Profiles -
- - - - - Settings - - - - - - Create a new profile - -
-
-
- -
Loading...
-
-
-
-
- ); - } - return (
diff --git a/src/hooks/use-app-update-notifications.tsx b/src/hooks/use-app-update-notifications.tsx index 0badb2d..61e97bb 100644 --- a/src/hooks/use-app-update-notifications.tsx +++ b/src/hooks/use-app-update-notifications.tsx @@ -134,7 +134,7 @@ export function useAppUpdateNotifications() { { id: "app-update", duration: Number.POSITIVE_INFINITY, // Persistent until user action - position: "top-right", + position: "top-left", }, ); }, [ diff --git a/src/hooks/use-update-notifications.tsx b/src/hooks/use-update-notifications.tsx index 1ba313b..946ffff 100644 --- a/src/hooks/use-update-notifications.tsx +++ b/src/hooks/use-update-notifications.tsx @@ -16,33 +16,63 @@ interface UpdateNotification { is_rolling_release: boolean; } -export function useUpdateNotifications() { +export function useUpdateNotifications( + onProfilesUpdated?: () => Promise, +) { const [notifications, setNotifications] = useState([]); const [updatingBrowsers, setUpdatingBrowsers] = useState>( new Set(), ); - const [isClient, setIsClient] = useState(false); - - // Ensure we're on the client side to prevent hydration mismatches - useEffect(() => { - setIsClient(true); - }, []); + const [dismissedNotifications, setDismissedNotifications] = useState< + Set + >(new Set()); const checkForUpdates = useCallback(async () => { - if (!isClient) return; // Only run on client side - try { const updates = await invoke( "check_for_browser_updates", ); - setNotifications(updates); + + // Filter out dismissed notifications unless they're for a newer version + const filteredUpdates = updates.filter((notification) => { + // Check if this exact notification was dismissed + if (dismissedNotifications.has(notification.id)) { + return false; + } + + // Check if we dismissed an older version for this browser + const dismissedForBrowser = Array.from(dismissedNotifications).find( + (dismissedId) => { + const parts = dismissedId.split("_"); + if (parts.length >= 2) { + const browser = parts[0]; + return browser === notification.browser; + } + return false; + }, + ); + + if (dismissedForBrowser) { + // Extract the dismissed version to compare + const dismissedParts = dismissedForBrowser.split("_to_"); + if (dismissedParts.length === 2) { + const dismissedToVersion = dismissedParts[1]; + // Only show if this is a newer version than what was dismissed + return notification.new_version !== dismissedToVersion; + } + } + + return true; + }); + + setNotifications(filteredUpdates); // Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately // to avoid circular dependencies } catch (error) { console.error("Failed to check for updates:", error); } - }, [isClient]); + }, [dismissedNotifications]); const handleUpdate = useCallback( async (browser: string, newVersion: string) => { @@ -118,6 +148,11 @@ export function useUpdateNotifications() { duration: 5000, }); } + + // Trigger profile refresh to update UI with new versions + if (onProfilesUpdated) { + void onProfilesUpdated(); + } } catch (downloadError) { console.error("Failed to download browser:", downloadError); @@ -159,28 +194,28 @@ export function useUpdateNotifications() { }); } }, - [notifications, checkForUpdates], + [notifications, checkForUpdates, onProfilesUpdated], ); const handleDismiss = useCallback( async (notificationId: string) => { - if (!isClient) return; // Only run on client side - try { toast.dismiss(notificationId); await invoke("dismiss_update_notification", { notificationId }); + + // Track this notification as dismissed to prevent showing it again + setDismissedNotifications((prev) => new Set(prev).add(notificationId)); + await checkForUpdates(); } catch (error) { console.error("Failed to dismiss notification:", error); } }, - [checkForUpdates, isClient], + [checkForUpdates], ); // Separate effect to show toasts when notifications change useEffect(() => { - if (!isClient) return; - for (const notification of notifications) { const isUpdating = updatingBrowsers.has(notification.browser); @@ -202,7 +237,7 @@ export function useUpdateNotifications() { }, ); } - }, [notifications, updatingBrowsers, handleUpdate, handleDismiss, isClient]); + }, [notifications, updatingBrowsers, handleUpdate, handleDismiss]); return { notifications, diff --git a/src/lib/toast-utils.ts b/src/lib/toast-utils.ts index e1534b9..da3c650 100644 --- a/src/lib/toast-utils.ts +++ b/src/lib/toast-utils.ts @@ -24,7 +24,12 @@ export interface ErrorToastProps extends BaseToastProps { export interface DownloadToastProps extends BaseToastProps { type: "download"; - stage?: "downloading" | "extracting" | "verifying" | "completed"; + stage?: + | "downloading" + | "extracting" + | "verifying" + | "completed" + | "downloading (twilight rolling release)"; progress?: { percentage: number; speed?: string; @@ -46,13 +51,20 @@ export interface FetchingToastProps extends BaseToastProps { browserName?: string; } +export interface TwilightUpdateToastProps extends BaseToastProps { + type: "twilight-update"; + browserName?: string; + hasUpdate?: boolean; +} + export type ToastProps = | LoadingToastProps | SuccessToastProps | ErrorToastProps | DownloadToastProps | VersionUpdateToastProps - | FetchingToastProps; + | FetchingToastProps + | TwilightUpdateToastProps; // Unified toast function export function showToast(props: ToastProps & { id?: string }) { @@ -81,6 +93,9 @@ export function showToast(props: ToastProps & { id?: string }) { case "version-update": duration = 15000; break; + case "twilight-update": + duration = 10000; + break; case "success": duration = 3000; break; @@ -149,7 +164,12 @@ export function showLoadingToast( export function showDownloadToast( browserName: string, version: string, - stage: "downloading" | "extracting" | "verifying" | "completed", + stage: + | "downloading" + | "extracting" + | "verifying" + | "completed" + | "downloading (twilight rolling release)", progress?: { percentage: number; speed?: string; eta?: string }, options?: { suppressCompletionToast?: boolean }, ) { @@ -160,7 +180,9 @@ export function showDownloadToast( ? `Downloading ${browserName} ${version}` : stage === "extracting" ? `Extracting ${browserName} ${version}` - : `Verifying ${browserName} ${version}`; + : stage === "downloading (twilight rolling release)" + ? `Downloading ${browserName} ${version}` + : `Verifying ${browserName} ${version}`; // Don't show completion toast if suppressed (for auto-update scenarios) if (stage === "completed" && options?.suppressCompletionToast) { @@ -245,6 +267,25 @@ export function showErrorToast( }); } +export function showTwilightUpdateToast( + browserName: string, + options?: { + id?: string; + description?: string; + hasUpdate?: boolean; + duration?: number; + }, +) { + return showToast({ + type: "twilight-update", + title: options?.hasUpdate + ? `${browserName} twilight update available` + : `Checking for ${browserName} twilight updates...`, + browserName, + ...options, + }); +} + // Generic helper for dismissing toasts export function dismissToast(id: string) { sonnerToast.dismiss(id);