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
153 lines
4.3 KiB
TypeScript
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;
|