mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-29 15:26:05 +02:00
fix: prevent update browser toasts getting stuck in a cycle
This commit is contained in:
+45
-105
@@ -48,24 +48,26 @@ export default function Home() {
|
||||
useState<BrowserProfile | null>(null);
|
||||
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
|
||||
useState<BrowserProfile | null>(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<BrowserProfile[]>(
|
||||
"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<BrowserProfile[]>(
|
||||
"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<boolean>(
|
||||
"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<boolean>(
|
||||
"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<string>("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<string>("smart_open_url", {
|
||||
@@ -270,40 +264,33 @@ export default function Home() {
|
||||
|
||||
const runningProfilesRef = useRef<Set<string>>(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<boolean>("check_browser_status", {
|
||||
profile,
|
||||
});
|
||||
|
||||
try {
|
||||
const isRunning = await invoke<boolean>("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 (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 gap-8 sm:p-12 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center w-full max-w-3xl">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle>Profiles</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<GoGear className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Settings</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<GoPlus className="h-4 w-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Create a new profile</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-8 text-center">
|
||||
<div className="animate-pulse">Loading...</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 gap-8 sm:p-12 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-8 row-start-2 items-center w-full max-w-3xl">
|
||||
|
||||
@@ -134,7 +134,7 @@ export function useAppUpdateNotifications() {
|
||||
{
|
||||
id: "app-update",
|
||||
duration: Number.POSITIVE_INFINITY, // Persistent until user action
|
||||
position: "top-right",
|
||||
position: "top-left",
|
||||
},
|
||||
);
|
||||
}, [
|
||||
|
||||
@@ -16,33 +16,63 @@ interface UpdateNotification {
|
||||
is_rolling_release: boolean;
|
||||
}
|
||||
|
||||
export function useUpdateNotifications() {
|
||||
export function useUpdateNotifications(
|
||||
onProfilesUpdated?: () => Promise<void>,
|
||||
) {
|
||||
const [notifications, setNotifications] = useState<UpdateNotification[]>([]);
|
||||
const [updatingBrowsers, setUpdatingBrowsers] = useState<Set<string>>(
|
||||
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<string>
|
||||
>(new Set());
|
||||
|
||||
const checkForUpdates = useCallback(async () => {
|
||||
if (!isClient) return; // Only run on client side
|
||||
|
||||
try {
|
||||
const updates = await invoke<UpdateNotification[]>(
|
||||
"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,
|
||||
|
||||
+45
-4
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user