"use client"; import { buildOauthLoginUrl } from "src/utils/customDomain"; import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; import { Popover } from "components/Popover"; import Link from "next/link"; import { useState } from "react"; import { encodeActionToSearchParam } from "app/api/oauth/[route]/afterSignInActions"; import { subscribeToPublication } from "actions/publications/subscribeToPublication"; import { isOAuthSessionError, OAuthErrorMessage } from "components/OAuthError"; import { useToaster } from "components/Toast"; import { DotLoader } from "components/utils/DotLoader"; import type { OAuthSessionError } from "src/atproto-oauth"; import { HandleSearchInput } from "components/HandleSearchInput"; import { Avatar } from "components/Avatar"; import { useIdentityData, refreshIdentityData, } from "components/IdentityProvider"; import { useRecordFromDid } from "src/utils/useRecordFromDid"; import { LinkIdentityModal } from "./LinkIdentityModal"; import { RSSTiny } from "components/Icons/RSSTiny"; import { Tooltip } from "components/Tooltip"; import { SubscribeButtonModeMenu } from "./SubscribeButton"; import { RecommendedPublications } from "./RecommendedPublications"; import { useSubscribeSuccessData } from "./useSubscribeSuccessData"; import type { SubscriptionSource } from "src/subscriptionSource"; const apps = [ { name: "Leaflet", logo: "https://leaflet.pub/logos/leaflet.svg" }, { name: "Bluesky", logo: "https://leaflet.pub/logos/bluesky.svg" }, { name: "Blacksky", logo: "https://leaflet.pub/logos/blacksky.svg" }, { name: "Eurosky", logo: "https://leaflet.pub/logos/eurosky.svg" }, { name: "Tangled", logo: "https://leaflet.pub/logos/tangled.svg" }, { name: "Semble", logo: "https://leaflet.pub/logos/semble.svg" }, { name: "Surf", logo: "https://leaflet.pub/logos/surf.svg" }, { name: "Spark", logo: "https://leaflet.pub/logos/spark.svg" }, { name: "Pckt", logo: "https://leaflet.pub/logos/pckt.svg" }, { name: "pdsls", logo: "https://leaflet.pub/logos/pdsls.svg" }, { name: "plyr.fm", logo: "https://leaflet.pub/logos/plyr.fm.svg" }, { name: "Popfeed", logo: "https://leaflet.pub/logos/popfeed.svg" }, { name: "Roomy", logo: "https://leaflet.pub/logos/roomy.svg" }, { name: "Sill", logo: "https://leaflet.pub/logos/sill.svg" }, { name: "Offprint", logo: "https://leaflet.pub/logos/offprint.svg" }, { name: "Margin", logo: "https://leaflet.pub/logos/margin.svg" }, { name: "Anisota", logo: "https://leaflet.pub/logos/anisota.svg" }, { name: "Blento", logo: "https://leaflet.pub/logos/blento.svg" }, { name: "Cartridge", logo: "https://leaflet.pub/logos/cartridge.svg" }, { name: "Graze", logo: "https://leaflet.pub/logos/graze.svg" }, ]; // True when this document is running inside an iframe. Reading `window.top` // across origins throws a SecurityError, which itself only happens when we're // framed by a different origin — so treat that as being framed too. const isInIframe = () => { if (typeof window === "undefined") return false; try { return window.self !== window.top; } catch { return true; } }; export const SubscribeWithHandle = (props: { autoFocus?: boolean; compact?: boolean; publicationUri: string; publicationUrl?: string; source?: SubscriptionSource; onSubscribed?: () => void; onAtSuccess?: () => void; leading?: React.ReactNode; user: { loggedIn: boolean; email: string | undefined; handle: string | undefined; }; }) => { let toaster = useToaster(); let { identity } = useIdentityData(); let { data: record } = useRecordFromDid(identity?.atp_did); let [loading, setLoading] = useState(false); let [subscribing, setSubscribing] = useState(false); let [oauthError, setOauthError] = useState(null); // When an email-only user subscribes via the atproto flow, we surface a // confirmation modal first ("link Bluesky to your account?") so they can't // accidentally orphan their email account. let [pendingLinkHandle, setPendingLinkHandle] = useState(null); const viewerEmail = identity?.email; const viewerAtpDid = identity?.atp_did; const needsLinkConfirmation = !!viewerEmail && !viewerAtpDid; const redirectToOauthForSubscribe = (handle: string, link: boolean) => { let action = encodeActionToSearchParam({ action: "subscribe", publication: props.publicationUri, // The subscribe completes after the OAuth redirect, so stamp the // originating page into the source now. ...(props.source ? { source: { url: window.location.href, ...props.source } } : {}), }); let inIframe = isInIframe(); let url = new URL(window.location.href); if (inIframe) { url.searchParams.set("showSubscribeSuccess", "true"); url.searchParams.set("subscribed_pub", props.publicationUri); } let loginUrl = buildOauthLoginUrl({ handle, redirect: url.toString(), action, link, autoMerge: link, }); if (inIframe) { window.open(loginUrl, "_blank", "noopener,noreferrer"); // The iframe stays put while the user completes login in the new tab — // clear the pending spinner so the embedded form isn't stuck loading. setLoading(false); return; } window.location.href = loginUrl; }; // Without a handle there's no atproto identity to one-click subscribe with, so // fall through to the logged-out HandleSearchInput form (same as a logged-out user). if (props.user.loggedIn && props.user.handle) { let tooltipLabel = props.user.handle ? `@${props.user.handle}` : null; let avatar = ( ); const subscribeAtproto = async () => { if (subscribing) return; setSubscribing(true); setOauthError(null); let result = await subscribeToPublication( props.publicationUri, window.location.href, props.source, ); if (!result.success) { if (isOAuthSessionError(result.error)) setOauthError(result.error); setSubscribing(false); return; } if (props.onAtSuccess) { props.onAtSuccess(); } else { toaster({ content:
You're Subscribed!
, type: "success", }); } props.onSubscribed?.(); // onSubscribed only flips this instance's local state; the subscription // now on the identity is what every other SubscribeButton on the page // reads, and on a published page nothing else will refetch it. refreshIdentityData(); setSubscribing(false); }; let subscribeButton = ( {subscribing ? ( ) : ( <> {avatar}
Subscribe
{!props.compact && ( as {props.user.handle} )}
)}
); return (
{props.leading && (
{props.leading}
)} {props.compact && tooltipLabel ? ( {tooltipLabel} ) : ( subscribeButton )} {props.compact && ( )}
{!props.compact && ( )}
{oauthError && ( )}
); } else return (
{ let trimmed = handle.trim(); if (!trimmed) return; if (needsLinkConfirmation) { setPendingLinkHandle(trimmed); return; } setLoading(true); redirectToOauthForSubscribe(trimmed, false); }} action=
Subscribe
/> {props.publicationUrl && ( )}
{needsLinkConfirmation && ( { if (!open) setPendingLinkHandle(null); }} signedInAs={viewerEmail!} linkingIdentity={`@${pendingLinkHandle ?? ""}`} confirmButtonLabel="Link Bluesky" confirming={loading} onConfirm={() => { if (!pendingLinkHandle) return; setLoading(true); redirectToOauthForSubscribe(pendingLinkHandle, true); }} /> )}
); }; export const LinkHandle = (props: { compact?: boolean }) => { let [loading, setLoading] = useState(false); return (

Link your Atmosphere account

to comment, recommend, and see what your friends are reading
{ let trimmed = handle.trim(); if (!trimmed) return; setLoading(true); window.location.href = buildOauthLoginUrl({ handle: trimmed, redirect: window.location.href, }); }} action={
Link
} />
); }; export const AtSubscribeSuccess = (props: { publicationUri?: string }) => { let { loading, publicationName, listings } = useSubscribeSuccessData( props.publicationUri, ); if (loading) return (
); return (

{publicationName ? `You've subscribed to ${publicationName}!` : "You've Subscribed!"}

You'll receive new posts in the
Leaflet Reader
or any standard.site enabled reader!

Other ways to follow

Get the RSS Feed Pin Custom Feed in Bluesky
); }; export const AtmosphericHandleInfo = (props: { trigger?: React.ReactNode }) => { return ( What's the Atmosphere? ) } >
The Atmosphere is a growing ecosystem of social apps, like Leaflet and Bluesky.
One account gets you into all of them.
Apps on the Atmosphere!
{[...apps, ...apps].map((app, i) => ( ))}
Sign up via Bluesky!
); }; const AtApp = (props: { logo: string; name: string }) => { return (
{props.name} {props.name}
); };