Files
llink/js/desktop/src/App.tsx
T
ClaudeandArjun Patel af06a250fe Add first-run onboarding wizard for recording mechanics
Introduce a minimal, skippable first-run wizard that teaches the record
key. On first authenticated launch it renders as a full-screen takeover
(gated above PusherProvider) so the stream/compose keyboard handlers are
not mounted to compete for the same keys.

Steps: welcome, hold ` to record, tap V to switch audio/video, tap ` to
toggle recording, mic/camera permission priming, and a recap. The lessons
detect the real gestures and never block — Esc or the Skip control
dismisses at any point.

Reuses existing pieces: VideoAudioToggle, useMediaDevices().requestLabels
for permission priming, KeyHint, and isTypingTarget. Extracts the shared
HOLD_THRESHOLD_MS into lib/constants and reuses it in the compose overlay.

Resolves #259

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_01A9AHySdLb1zfZAEm6SuaQP
2026-06-21 11:35:01 -07:00

153 lines
4.3 KiB
TypeScript

import { useEffect } from 'react';
import { Routes, Route, useNavigate } from 'react-router-dom';
import { RouterShell } from '@/lib/router-shell';
import { TooltipProvider } from '@/components/ui/tooltip';
import { useAuthStore } from '@/stores/auth-store';
import { LoginPage } from '@/features/auth/login-page';
import { QueryClientProvider } from '@tanstack/react-query';
import SettingsPage from '@/features/settings-page';
import AudioVideoSettingsPage from '@/features/settings/audio-video-settings-page';
import NetworkSelector from '@/features/network-selector';
import NetworkRoot from '@/features/network-root';
import ParticleViewResolver from '@/features/particles/particle-view-resolver';
import Layout from '@/features/layout';
import { logError } from '@/lib/errors';
import NetworkSettingsPage from '@/features/network-settings';
import { Toaster } from '@/components/ui/sonner';
import { PusherProvider } from '@/lib/pusher-provider';
import { createQueryClient } from '@/lib/query-client';
import {
RouteErrorBoundary,
TopLevelErrorBoundary,
} from '@/components/app-error-boundary';
import { SoundEffectsProvider } from '@/lib/sound-effects/sound-effects-provider';
import { platform } from '@/lib/platform';
import { InAppAutoplayCard } from '@/components/in-app-autoplay-card';
import { useOnboardingStore } from '@/stores/onboarding-store';
import { OnboardingOverlay } from '@/features/onboarding/onboarding-overlay';
const queryClient = createQueryClient();
const App = () => {
const status = useAuthStore((s) => s.status);
const restoreSession = useAuthStore((s) => s.restoreSession);
const hasCompletedOnboarding = useOnboardingStore(
(s) => s.hasCompletedOnboarding,
);
const markOnboardingComplete = useOnboardingStore((s) => s.markComplete);
useEffect(() => {
restoreSession();
}, [restoreSession]);
if (status === 'idle' || status === 'restoring') {
return (
<div className="flex min-h-screen items-center justify-center">
<p className="text-muted-foreground text-sm">Loading...</p>
</div>
);
}
if (status !== 'authenticated') {
return <LoginPage />;
}
if (!hasCompletedOnboarding) {
return <OnboardingOverlay onComplete={markOnboardingComplete} />;
}
return (
<PusherProvider>
<AuthenticatedApp />
</PusherProvider>
);
};
function DeepLinkNavigationListener() {
const navigate = useNavigate();
useEffect(() => {
platform.deepLink
.getPending()
.then((path) => {
if (path) navigate(path);
})
.catch((err) => {
logError(err, { scope: 'deepLink.getPending' });
});
return platform.deepLink.onNavigate((path) => navigate(path));
}, [navigate]);
return null;
}
function AutoplayNavigationListener() {
const navigate = useNavigate();
useEffect(() => {
return platform.autoplay.onNavigate((data) => {
navigate(`/${data.networkId}/${data.streamId}`);
});
}, [navigate]);
return null;
}
function AuthenticatedApp() {
return (
<RouterShell>
<AutoplayNavigationListener />
<DeepLinkNavigationListener />
<InAppAutoplayCard />
<RouteErrorBoundary>
<Routes>
<Route path="settings" element={<SettingsPage />} />
<Route
path="settings/audio-video"
element={<AudioVideoSettingsPage />}
/>
<Route path="/">
<Route
index
element={
<Layout>
<NetworkSelector />
</Layout>
}
/>
<Route path=":networkId">
<Route
index
element={
<Layout>
<NetworkRoot />
</Layout>
}
/>
<Route path="settings" element={<NetworkSettingsPage />} />
<Route path="*" element={<ParticleViewResolver />} />
</Route>
</Route>
</Routes>
</RouteErrorBoundary>
</RouterShell>
);
}
const AppWithProviders = () => (
<TopLevelErrorBoundary>
<TooltipProvider>
<QueryClientProvider client={queryClient}>
<SoundEffectsProvider>
<App />
<Toaster />
</SoundEffectsProvider>
</QueryClientProvider>
</TooltipProvider>
</TopLevelErrorBoundary>
);
export default AppWithProviders;