Something went wrong. Try again.
(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
margin.at
extension web atproto comments
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417import { useStore } from "@nanostores/react";import { Suspense, useEffect, useState } from "react";import { I18nextProvider } from "react-i18next";import i18n from "../i18n";import type { UserProfile } from "../types";
declare global { interface Window { __MARGIN_USER__?: UserProfile | null; }}
import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate, useParams,} from "react-router-dom";import { checkSession } from "../api/client";
import MobileNav from "../components/navigation/MobileNav";import RightSidebar from "../components/navigation/RightSidebar";import Sidebar from "../components/navigation/Sidebar";import { $user } from "../store/auth";import { analytics } from "../lib/analytics";
import AdminModeration from "./core/AdminModeration";import Discover from "./core/Discover";import Feed from "./core/Feed";import New from "./core/New";import Notifications from "./core/Notifications";import Search from "./core/Search";import Settings from "./core/Settings";import Collections from "./collections/Collections";import CollectionDetail from "./collections/CollectionDetail";import AnnotationDetail from "./content/AnnotationDetail";import UrlPage from "./content/UrlPage";import UserUrlPage from "./content/UserUrlPage";import Profile from "./profile/Profile";import ReadingRoom from "./reading-room/ReadingRoom";import ReadingRoomNote from "./reading-room/ReadingRoomNote";import ReadingRoomSettings from "./core/ReadingRoomSettings";
const PAGE_TITLES: Record<string, string> = { "/home": "Home — Margin", "/bookmarks": "Bookmarks — Margin", "/highlights": "Highlights — Margin", "/annotations": "Annotations — Margin", "/discover": "Discover — Margin", "/search": "Search — Margin", "/notifications": "Notifications — Margin", "/new": "New Annotation — Margin", "/settings": "Settings — Margin", "/collections": "Collections — Margin", "/admin/moderation": "Admin — Margin",};
function AuthGuard({ children }: { children: React.ReactNode }) { const user = useStore($user); const [checked, setChecked] = useState(() => "__MARGIN_USER__" in window);
useEffect(() => { if (!checked) { const unsub = $user.subscribe(() => setChecked(true)); const t = setTimeout(() => setChecked(true), 3000); return () => { unsub(); clearTimeout(t); }; } }, [checked]);
useEffect(() => { if (checked && !user) { window.location.href = "/login"; } }, [checked, user]);
if (!checked || !user) return null; return <>{children}</>;}
function CollectionDetailRoute() { const { handle, rkey } = useParams<{ handle: string; rkey: string }>(); return <CollectionDetail handle={handle} rkey={rkey} />;}
function AnnotationDetailRoute() { const { handle, rkey, type } = useParams<{ handle: string; rkey: string; type: string; }>(); return <AnnotationDetail handle={handle} rkey={rkey} type={type} />;}
function AtAnnotationRoute() { const { did, rkey } = useParams<{ did: string; rkey: string }>(); return <AnnotationDetail did={did} rkey={rkey} />;}
function AtCollectionAnnotationRoute() { const { did, collection, rkey } = useParams<{ did: string; collection: string; rkey: string; }>(); return <AnnotationDetail did={did} collection={collection} rkey={rkey} />;}
function UriAnnotationRoute() { const { uri } = useParams<{ uri: string }>(); return <AnnotationDetail uri={uri ? decodeURIComponent(uri) : undefined} />;}
function ProfileRoute() { const { did } = useParams<{ did: string }>(); if (!did) return <Navigate to="/home" replace />; return <Profile did={did} />;}
function UrlRoute() { const params = useParams(); const urlPath = params["*"]; return <UrlPage urlPath={urlPath} />;}
function UserUrlRoute() { const params = useParams(); return <UserUrlPage handle={params.handle} urlPath={params["*"]} />;}
function AppLayout() { const location = useLocation(); const navigate = useNavigate(); const searchParams = new URLSearchParams(location.search);
const rrHandle = (window as unknown as { __READING_ROOM_HANDLE__?: string }) .__READING_ROOM_HANDLE__; const rrIsNote = (window as unknown as { __READING_ROOM_IS_NOTE__?: boolean }) .__READING_ROOM_IS_NOTE__; const isReadingRoom = location.pathname.startsWith("/reading-room/") || !!rrHandle;
useEffect(() => { if (isReadingRoom) return; document.title = PAGE_TITLES[location.pathname] ?? "Margin"; }, [location.pathname, isReadingRoom]);
useEffect(() => { if (searchParams.get("logged_in") !== "true") return; const user = $user.get(); analytics.capture("login_success", { handle: user?.handle ?? "", pds: undefined, }); const url = new URL(window.location.href); url.searchParams.delete("logged_in"); window.history.replaceState({}, "", url.toString()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.search]);
useEffect(() => { const SERVER_PATHS = [ "/login", "/about", "/privacy", "/terms", "/brand", "/imprint", "/pro", "/auth/", "/api/", "/og-image", ]; const handleClick = (e: MouseEvent) => { const a = (e.target as Element).closest("a"); if (!a) return; if (a.hasAttribute("target") || a.hasAttribute("download")) return; const href = a.getAttribute("href"); if (!href || !href.startsWith("/")) return; if (href === "/" || SERVER_PATHS.some((p: string) => href.startsWith(p))) return; e.preventDefault(); navigate(href); }; document.addEventListener("click", handleClick); return () => document.removeEventListener("click", handleClick); }, [navigate]);
return ( <div className={`min-h-screen flex ${isReadingRoom ? "" : "bg-surface-100 dark:bg-surface-900"}`} > {isReadingRoom && ( <div className="flex-1 min-w-0"> {rrHandle ? ( rrIsNote ? ( <ReadingRoomNote handle={rrHandle} /> ) : ( <ReadingRoom handle={rrHandle} /> ) ) : ( <Routes> <Route path="/reading-room/:handle/note" element={<ReadingRoomNote />} /> <Route path="/reading-room/:handle" element={<ReadingRoom />} /> </Routes> )} </div> )}
{!isReadingRoom && ( <> <Sidebar currentPath={location.pathname} onNavigate={navigate} />
<div className="flex-1 min-w-0 transition-all duration-200"> <div className="flex w-full max-w-[1800px] mx-auto"> <main className="flex-1 w-full min-w-0 p-2 md:py-3 md:px-0"> <div className="bg-white dark:bg-surface-800 rounded-2xl min-h-[calc(100vh-16px)] md:min-h-[calc(100vh-24px)] py-6 px-4 md:px-6 lg:px-8 pb-28 md:pb-6"> <Routes> <Route path="/home" element={ <Feed key="home" initialType="all" initialTag={searchParams.get("tag") ?? undefined} /> } /> <Route path="/bookmarks" element={ <Feed key="bookmarks" initialType="all" motivation="bookmarking" showTabs={false} /> } /> <Route path="/highlights" element={ <Feed key="highlights" initialType="all" motivation="highlighting" showTabs={false} /> } /> <Route path="/annotations" element={ <Feed key="annotations" initialType="all" motivation="commenting" showTabs={false} /> } /> <Route path="/discover" element={<Discover />} /> <Route path="/search" element={ <Search key={searchParams.get("q") ?? ""} initialQuery={searchParams.get("q") ?? undefined} /> } /> <Route path="/notifications" element={ <AuthGuard> <Notifications /> </AuthGuard> } /> <Route path="/new" element={ <AuthGuard> <New initialUrl={searchParams.get("url") ?? undefined} initialSelectorJson={ searchParams.get("selector") ?? undefined } initialQuote={ searchParams.get("quote") ?? undefined } /> </AuthGuard> } /> <Route path="/settings" element={<Settings />} /> <Route path="/settings/reading-room" element={ <AuthGuard> <ReadingRoomSettings /> </AuthGuard> } /> <Route path="/admin/moderation" element={ <AuthGuard> <AdminModeration /> </AuthGuard> } /> <Route path="/collections" element={<Collections />} /> <Route path="/collections/:rkey" element={<CollectionDetail />} /> <Route path="/:handle/collection/:rkey" element={<CollectionDetailRoute />} /> <Route path="/:handle/note/:rkey" element={<AnnotationDetailRoute />} /> <Route path="/:handle/annotation/:rkey" element={<AnnotationDetailRoute />} /> <Route path="/:handle/highlight/:rkey" element={<AnnotationDetailRoute />} /> <Route path="/:handle/bookmark/:rkey" element={<AnnotationDetailRoute />} /> <Route path="/annotation/:uri" element={<UriAnnotationRoute />} /> <Route path="/at/:did/:rkey" element={<AtAnnotationRoute />} /> <Route path="/at/:did/:collection/:rkey" element={<AtCollectionAnnotationRoute />} /> <Route path="/url/*" element={<UrlRoute />} /> <Route path="/:handle/url/*" element={<UserUrlRoute />} /> <Route path="/profile/:did" element={<ProfileRoute />} /> <Route path="/profile" element={ <AuthGuard> <ProfileSelfRedirect /> </AuthGuard> } /> <Route path="*" element={<Navigate to="/home" replace />} /> </Routes> </div> </main>
<RightSidebar onNavigate={navigate} /> </div> </div>
<MobileNav currentPath={location.pathname} onNavigate={navigate} /> </> )} </div> );}
function ProfileSelfRedirect() { const user = useStore($user); if (!user) return null; return <Navigate to={`/profile/${user.did}`} replace />;}
export default function AppShell() { useState(() => { const ssrUser = window.__MARGIN_USER__; if (ssrUser !== undefined) { $user.set(ssrUser); } });
useEffect(() => { checkSession().then((user) => { if (user !== undefined) { $user.set(user); } }); }, []);
return ( <I18nextProvider i18n={i18n}> <Suspense fallback={null}> <BrowserRouter> <AppLayout /> </BrowserRouter> </Suspense> </I18nextProvider> );}