Skip to content
Open
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
8 changes: 4 additions & 4 deletions screenpipe-app-tauri/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,12 @@ export default function RootLayout({

return (
<html lang="en" suppressHydrationWarning>
<Providers>
<body className={inter.className}>
<body className={inter.className}>
<Providers>
{children}
<Toaster />
</body>
</Providers>
</Providers>
</body>
</html>
);
}
145 changes: 110 additions & 35 deletions screenpipe-app-tauri/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
"use client";

import { getStore, useSettings } from "@/lib/hooks/use-settings";
// Zustand stores - modern state management
import {
useSettingsZustand,
awaitZustandHydration,
getZustandStore,
resetZustandStore,
} from "@/lib/hooks/use-settings-zustand";
import {
useProfilesZustand,
} from "@/lib/hooks/use-profiles-zustand";

import React, { useEffect, useState } from "react";
import NotificationHandler from "@/components/notification-handler";
import Header from "@/components/header";
import { useToast } from "@/components/ui/use-toast";
import Onboarding from "@/components/onboarding";
import { useOnboarding } from "@/lib/hooks/use-onboarding";
import { OnboardingProvider } from "@/lib/hooks/use-onboarding";
import { ChangelogDialog } from "@/components/changelog-dialog";
import { BreakingChangesInstructionsDialog } from "@/components/breaking-changes-instructions-dialog";
import { useChangelogDialog } from "@/lib/hooks/use-changelog-dialog";
Expand All @@ -17,7 +26,6 @@ import { useSettingsDialog } from "@/lib/hooks/use-settings-dialog";
import { PipeStore } from "@/components/pipe-store";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useProfiles } from "@/lib/hooks/use-profiles";
import { relaunch } from "@tauri-apps/plugin-process";
import { PipeApi } from "@/lib/api";
import localforage from "localforage";
Expand All @@ -26,24 +34,60 @@ import { LoginDialog } from "../components/login-dialog";
import { ModelDownloadTracker } from "../components/model-download-tracker";

export default function Home() {
const { settings, updateSettings, loadUser, reloadStore } = useSettings();
const { setActiveProfile } = useProfiles();
// Migrate to Zustand with selective subscriptions
const settings = useSettingsZustand((state) => state.settings);
const updateSettings = useSettingsZustand((state) => state.updateSettings);
const loadUser = useSettingsZustand((state) => state.loadUser);
const reloadStore = useSettingsZustand((state) => state.reloadStore);
const isHydrated = useSettingsZustand((state) => state.isHydrated);

const setActiveProfile = useProfilesZustand((state) => state.setActiveProfile);
const { toast } = useToast();
const { showOnboarding, setShowOnboarding } = useOnboarding();
const { setShowChangelogDialog } = useChangelogDialog();
const { open: openStatusDialog } = useStatusDialog();
const { setIsOpen: setSettingsOpen } = useSettingsDialog();
const isProcessingRef = React.useRef(false);
const [shouldShowOnboarding, setShouldShowOnboarding] = React.useState<boolean | null>(null);

// Trigger client-side hydration
useEffect(() => {
if (typeof window !== 'undefined' && !isHydrated) {
// Trigger hydration for both stores on client-side
Promise.all([
useSettingsZustand.getState()._hydrate(),
useProfilesZustand.getState()._hydrate(),
]).catch(error => {
console.error('Failed to hydrate stores:', error);
});
}
}, [isHydrated]);

// Handle hydration and initial loading with Zustand
useEffect(() => {
if (!isHydrated) return; // Wait for Zustand hydration

// Set onboarding state based on settings
setShouldShowOnboarding(settings.isFirstTimeUser);

// Load user if token exists
if (settings.user?.token) {
loadUser(settings.user.token);
}
}, [settings.user.token]);
}, [isHydrated, settings.isFirstTimeUser, settings.user?.token, loadUser]);

// Create setShowOnboarding function - now using Zustand
const setShowOnboarding = React.useCallback(async (show: boolean) => {
try {
await updateSettings({ isFirstTimeUser: show });
setShouldShowOnboarding(show);
} catch (error) {
console.error('Failed to update onboarding settings:', error);
}
}, [updateSettings]);

useEffect(() => {
const getAudioDevices = async () => {
const store = await getStore();
const store = await getZustandStore();
const devices = (await store.get("audioDevices")) as string[];
return devices;
};
Expand All @@ -58,7 +102,7 @@ export default function Home() {
if (url.includes("api_key=")) {
const apiKey = parsedUrl.searchParams.get("api_key");
if (apiKey) {
updateSettings({ user: { token: apiKey } });
await updateSettings({ user: { token: apiKey } });
toast({
title: "logged in!",
description: "you have been logged in",
Expand Down Expand Up @@ -113,7 +157,9 @@ export default function Home() {

listen<string>("switch-profile", async (event) => {
const profile = event.payload;
setActiveProfile(profile);
await setActiveProfile(profile);
resetZustandStore(); // Use Zustand store reset
await reloadStore();

toast({
title: "profile switched",
Expand Down Expand Up @@ -200,20 +246,33 @@ export default function Home() {
});
if (deepLinkUnsubscribe) deepLinkUnsubscribe();
};
}, [setSettingsOpen]);
}, [setSettingsOpen, updateSettings, toast, setShowChangelogDialog, setShowOnboarding, openStatusDialog, setActiveProfile, reloadStore]);

useEffect(() => {
const checkScreenPermissionRestart = async () => {
const restartPending = await localforage.getItem(
"screenPermissionRestartPending"
);
if (restartPending) {
setShowOnboarding(true);
if (!isHydrated) return; // Wait for Zustand hydration

try {
const restartPending = await localforage.getItem(
"screenPermissionRestartPending"
);

if (restartPending) {
// Clear the flag first to prevent infinite loop
await localforage.removeItem("screenPermissionRestartPending");

// Only show onboarding if user is still first time user
if (settings.isFirstTimeUser) {
setShowOnboarding(true);
}
}
} catch (error) {
console.error('Failed to check screen permission restart:', error);
}
};

checkScreenPermissionRestart();
}, [setShowOnboarding]);
}, [isHydrated, setShowOnboarding, settings.isFirstTimeUser]);

useEffect(() => {
const unlisten = listen("cli-login", async (event) => {
Expand All @@ -224,25 +283,41 @@ export default function Home() {
return () => {
unlisten.then((unlistenFn) => unlistenFn());
};
}, []);
}, [reloadStore]);

// Show loading until settings are hydrated AND we know the onboarding state
if (!isHydrated || shouldShowOnboarding === null) {
return (
<div className="flex flex-col items-center justify-center flex-1 max-w-screen-2xl mx-auto relative min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4"></div>
<p className="text-gray-600">Loading...</p>
</div>
</div>
);
}

return (
<div className="flex flex-col items-center flex-1 max-w-screen-2xl mx-auto relative">
<LoginDialog />
<ModelDownloadTracker />
<NotificationHandler />
{showOnboarding ? (
<Onboarding />
) : (
<>
<ChangelogDialog />
{/* <BreakingChangesInstructionsDialog /> */}
<Header />
<div className=" w-full">
<PipeStore />
</div>
</>
)}
</div>
<OnboardingProvider value={{ showOnboarding: shouldShowOnboarding, setShowOnboarding }}>
<div className="flex flex-col items-center flex-1 max-w-screen-2xl mx-auto relative">
<LoginDialog />
<ModelDownloadTracker />
<NotificationHandler />
<div suppressHydrationWarning>
{shouldShowOnboarding ? (
<Onboarding />
) : (
<>
<ChangelogDialog />
{/* <BreakingChangesInstructionsDialog /> */}
<Header />
<div className=" w-full">
<PipeStore />
</div>
</>
)}
</div>
</div>
</OnboardingProvider>
);
}
114 changes: 88 additions & 26 deletions screenpipe-app-tauri/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,99 @@
"use client";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import { useEffect } from "react";
import { useEffect, useState, useCallback } from "react";
import { ChangelogDialogProvider } from "@/lib/hooks/use-changelog-dialog";
import { forwardRef } from "react";
import { store as SettingsStore, useSettings } from "@/lib/hooks/use-settings";
import { profilesStore as ProfilesStore } from "@/lib/hooks/use-profiles";

export const Providers = forwardRef<
HTMLDivElement,
{ children: React.ReactNode }
>(({ children }, ref) => {
import React from "react";
// Modern Zustand stores
import {
useSettingsZustand,
awaitZustandHydration
} from "@/lib/hooks/use-settings-zustand";
import {
useProfilesZustand,
} from "@/lib/hooks/use-profiles-zustand";

// Separate analytics initialization to prevent unnecessary re-renders
const useAnalyticsInitialization = (analyticsEnabled: boolean) => {
const [initialized, setInitialized] = useState(false);

useEffect(() => {
if (typeof window !== "undefined") {
const isDebug = process.env.TAURI_ENV_DEBUG === "true";
if (isDebug) return;
posthog.init("phc_Bt8GoTBPgkCpDrbaIZzJIEYt0CrJjhBiuLaBck1clce", {
api_host: "https://eu.i.posthog.com",
person_profiles: "identified_only",
capture_pageview: false,
});
if (typeof window === "undefined") return;

const isDebug = process.env.TAURI_ENV_DEBUG === "true";
if (isDebug) return;

// Only initialize once
if (initialized) return;

let cancelled = false;
let timeoutId: NodeJS.Timeout;

(async () => {
try {
// Add timeout to prevent infinite waiting
const hydrationPromise = awaitZustandHydration();
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error('Hydration timeout')), 5000);
});

await Promise.race([hydrationPromise, timeoutPromise]);

if (cancelled) return;

if (analyticsEnabled) {
posthog.init("phc_Bt8GoTBPgkCpDrbaIZzJIEYt0CrJjhBiuLaBck1clce", {
api_host: "https://eu.i.posthog.com",
person_profiles: "identified_only",
capture_pageview: false,
});
} else {
posthog.opt_out_capturing();
}
setInitialized(true);
} catch (error) {
console.error('Failed to wait for settings hydration in analytics setup:', error);
// Still set initialized to prevent hanging
setInitialized(true);
}
})();

return () => {
cancelled = true;
if (timeoutId) clearTimeout(timeoutId);
};
}, [analyticsEnabled, initialized]);

// Handle analytics preference changes after initialization
useEffect(() => {
if (!initialized) return;

if (analyticsEnabled) {
posthog.opt_in_capturing();
} else {
posthog.opt_out_capturing();
}
}, []);
}, [analyticsEnabled, initialized]);
};

// Memoized inner provider to prevent unnecessary re-renders
const ProviderInner = React.memo(({ children }: { children: React.ReactNode }) => {
// Use Zustand with selective subscription for analytics
const analyticsEnabled = useSettingsZustand((state) => state.settings.analyticsEnabled);

// Initialize analytics with the hook
useAnalyticsInitialization(analyticsEnabled);

return (
<SettingsStore.Provider>
<ProfilesStore.Provider>
<ChangelogDialogProvider>
<PostHogProvider client={posthog}>{children}</PostHogProvider>
</ChangelogDialogProvider>
</ProfilesStore.Provider>
</SettingsStore.Provider>
<ChangelogDialogProvider>
<PostHogProvider client={posthog}>{children}</PostHogProvider>
</ChangelogDialogProvider>
);
});

Providers.displayName = "Providers";
ProviderInner.displayName = 'ProviderInner';

export const Providers = ({ children }: { children: React.ReactNode }) => {
// Zustand doesn't need provider wrappers - stores are global
return <ProviderInner>{children}</ProviderInner>;
};
Binary file modified screenpipe-app-tauri/bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion screenpipe-app-tauri/components/cli-command-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { CodeBlock } from "./ui/codeblock";
import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard";
import { useToast } from "@/components/ui/use-toast";
import { IconCode } from "./ui/icons";
import { Settings } from "@/lib/hooks/use-settings";
import { Settings } from "@/lib/types/settings";
import { getCliPath } from "@/lib/utils";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
import { platform } from "@tauri-apps/plugin-os";
Expand Down
5 changes: 3 additions & 2 deletions screenpipe-app-tauri/components/dev-mode-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { CodeBlock } from "@/components/ui/codeblock";
import { platform } from "@tauri-apps/plugin-os";
import { Label } from "./ui/label";
import { Switch } from "./ui/switch";
import { useSettings } from "@/lib/hooks/use-settings";
import { useSettingsZustand } from "@/lib/hooks/use-settings-zustand";
import { invoke } from "@tauri-apps/api/core";
import { useToast } from "./ui/use-toast";
import {
Expand Down Expand Up @@ -96,7 +96,8 @@ const getDebuggingCommands = (os: string | null, dataDir: string) => {
};

export const DevModeSettings = ({ localDataDir }: { localDataDir: string }) => {
const { settings, updateSettings } = useSettings();
const settings = useSettingsZustand((state) => state.settings);
const updateSettings = useSettingsZustand((state) => state.updateSettings);
const handleDevModeToggle = async (checked: boolean) => {
try {
updateSettings({ devMode: checked });
Expand Down
2 changes: 1 addition & 1 deletion screenpipe-app-tauri/components/log-file-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { FileText, Copy, AppWindow, Loader, X, Upload } from "lucide-react";
import { readTextFile } from "@tauri-apps/plugin-fs";
import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard";
import { cn } from "@/lib/utils";
import { useSettings } from "@/lib/hooks/use-settings";
import { useSettingsZustand } from "@/lib/hooks/use-settings-zustand";
import {
Dialog,
DialogHeader,
Expand Down
Loading
Loading