Something went wrong. Try again.
A Bsky-like frontend using the atprotocol natively.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207import { lazy, Suspense, useMemo } from 'react';import { BrowserRouter, Routes, Route, Navigate, useSearchParams, useLocation, useParams } from 'react-router-dom';import { AuthProvider } from './auth/AuthProvider';import { useAuth } from './auth/useAuth';import { PostViewProvider } from './settings/PostViewProvider';import { ProfileViewProvider } from './settings/ProfileViewProvider';import { ThemeProvider } from './settings/ThemeProvider';import { useThemeMode } from './settings/useThemeMode';import { CustomColorsProvider } from './settings/CustomColorsProvider';import { PostViewerProvider } from './components/PostViewerProvider';import { ProfileViewerProvider } from './components/ProfileViewerProvider';import { ReplyStyleProvider } from './components/ReplyStyleProvider';import { VideoAutoplayProvider } from './components/VideoAutoplayProvider';import { VideoVolumeProvider } from './settings/VideoVolumeProvider';import { ImageProxyProvider } from './settings/ImageProxyProvider';import { PostLabelsProvider } from './settings/PostLabelsProvider';import { AppViewUrlsProvider } from './settings/AppViewUrlsProvider';import { NotificationCountProvider } from './components/NotificationCountProvider';import { NavigationMemoryProvider } from './components/NavigationMemoryProvider';import { useNavigationMemory } from './components/useNavigationMemory';import BookmarkProvider from './components/BookmarkProvider';import MuteBlockProvider from './components/MuteBlockProvider';import { useProfileViewMode } from './settings/useProfileViewMode';import { usePostViewMode } from './settings/usePostViewMode';import { useAndroidBackButton } from './hooks/useAndroidBackButton';import Layout from './components/Layout';import Login from './components/Login';import Feed from './components/Feed';import PostThread from './components/PostThread';import ProfilePage from './components/ProfilePage';import SearchPage from './components/SearchPage';import NotificationsPage from './components/NotificationsPage';import SavedPage from './components/SavedPage';import FeedViewerPage from './components/FeedViewerPage';import PostViewerModal from './components/PostViewerModal';import PostViewerSide from './components/PostViewerSide';import ProfileSidePanel from './components/ProfileSidePanel';import DrawerAutoOpener from './components/DrawerAutoOpener';import LoadingScreen from './components/LoadingScreen';
const SettingsPage = lazy(() => import('./settings/SettingsPage'));
function ProfilePostRedirect() { const { actor, rkey } = useParams<{ actor: string; rkey: string }>(); return <Navigate to={`/post/${actor}/${rkey}`} replace />;}
function PostThreadRoute() { const { handle, rkey, uri } = useParams<{ handle?: string; rkey?: string; uri?: string }>(); const key = handle && rkey ? `${handle}/${rkey}` : uri; return <PostThread key={key} />;}
function ProfilePageWrapper() { const { actor } = useParams<{ actor?: string }>(); return <ProfilePage key={actor ?? '__self__'} />;}
function MainContentRoutes() { const location = useLocation(); const { mainContentPath, mainContentSearch } = useNavigationMemory(); const { mode: profileViewMode } = useProfileViewMode();
const { mode: postViewMode } = usePostViewMode();
const isDrawerUrl = useMemo(() => { const { pathname } = location; if (pathname.startsWith('/post/') && postViewMode !== 'page') return true; const profileMatch = pathname.match(/^\/profile\/[^/]+$/); if (profileMatch) { return profileViewMode === 'side'; } return false; }, [location, postViewMode, profileViewMode]);
const effectiveLocation = isDrawerUrl ? { pathname: mainContentPath, search: mainContentSearch, hash: '', state: null, key: 'default' } : location;
return ( <Routes location={effectiveLocation}> <Route path="/" element={<Feed />} /> <Route path="/search" element={<SearchPage />} /> <Route path="/notifications" element={<NotificationsPage />} /> <Route path="/saved" element={<SavedPage />} /> <Route path="/feed/:did/:feedname" element={<FeedViewerPage />} /> <Route path="/post/:handle/:rkey" element={<PostThreadRoute />} /> <Route path="/post/:uri" element={<PostThreadRoute />} /> <Route path="/profile" element={<ProfilePageWrapper />} /> <Route path="/profile/:actor" element={<ProfilePageWrapper />} /> <Route path="/profile/:actor/post/:rkey" element={<ProfilePostRedirect />} /> <Route path="/settings/*" element={ <Suspense fallback={<div className="settings-loading">Loading settings…</div>}> <SettingsPage /> </Suspense> } /> <Route path="*" element={<Navigate to="/" replace />} /> </Routes> );}
function AuthenticatedRoutes() { const { session, loading } = useAuth();
if (loading) return <LoadingScreen />;
if (!session) { try { const currentPath = window.location.pathname + window.location.search + window.location.hash; if (currentPath !== '/' && currentPath !== '/login') { sessionStorage.setItem('foxsky_oauth_redirect', currentPath); } } catch {} return <Navigate to="/login" replace />; }
return ( <NavigationMemoryProvider> <MuteBlockProvider> <BookmarkProvider> <Layout> <DrawerAutoOpener /> <MainContentRoutes /> </Layout> <PostViewerModal /> <PostViewerSide /> <ProfileSidePanel /> </BookmarkProvider> </MuteBlockProvider> </NavigationMemoryProvider> );}
function AppRoutes() { const { session, loading } = useAuth(); const [searchParams] = useSearchParams();
if (loading) return <LoadingScreen />;
const addingAccount = searchParams.get('add_account') === '1';
return ( <Routes> <Route path="/login" element={ session && !addingAccount ? <Navigate to="/" replace /> : <Login /> } /> <Route path="/*" element={<AuthenticatedRoutes />} /> </Routes> );}
function ThemeColorsWrapper({ children }: { children: React.ReactNode }) { const { resolvedTheme } = useThemeMode(); return ( <CustomColorsProvider resolvedTheme={resolvedTheme}> {children} </CustomColorsProvider> );}
export default function App() { useAndroidBackButton();
return ( <BrowserRouter> <ThemeProvider> <ThemeColorsWrapper> <AuthProvider> <PostViewProvider> <ProfileViewProvider> <PostViewerProvider> <ProfileViewerProvider> <ReplyStyleProvider> <VideoAutoplayProvider> <VideoVolumeProvider> <ImageProxyProvider> <AppViewUrlsProvider> <PostLabelsProvider> <NotificationCountProvider> <AppRoutes /> </NotificationCountProvider> </PostLabelsProvider> </AppViewUrlsProvider> </ImageProxyProvider> </VideoVolumeProvider> </VideoAutoplayProvider> </ReplyStyleProvider> </ProfileViewerProvider> </PostViewerProvider> </ProfileViewProvider> </PostViewProvider> </AuthProvider> </ThemeColorsWrapper> </ThemeProvider> </BrowserRouter> );}