diff --git a/app/(home-pages)/discover/SortButtons.tsx b/app/(home-pages)/discover/SortButtons.tsx deleted file mode 100644 index 3b90228d..00000000 --- a/app/(home-pages)/discover/SortButtons.tsx +++ /dev/null @@ -1,97 +0,0 @@ -"use client"; -import Link from "next/link"; -import { useState } from "react"; -import { theme } from "tailwind.config"; - -export default function SortButtons(props: { order: string }) { - const [selected, setSelected] = useState<"recentlyUpdated" | "popular">( - "recentlyUpdated", - ); - - return ( -
- - - Recently Updated - - - - - Popular - -
- ); -} - -const SortButton = (props: { - children: React.ReactNode; - selected: boolean; -}) => { - return ( -
- - {props.selected && ( - <> -
- -
-
- -
-
- -
- - )} -
- ); -}; - -const GlitterBig = () => { - return ( - - - - ); -}; - -const GlitterSmall = () => { - return ( - - - - ); -}; diff --git a/app/(home-pages)/discover/SortedPublicationList.tsx b/app/(home-pages)/discover/SortedPublicationList.tsx deleted file mode 100644 index d72a3e14..00000000 --- a/app/(home-pages)/discover/SortedPublicationList.tsx +++ /dev/null @@ -1,195 +0,0 @@ -"use client"; -import Link from "next/link"; -import { useState, useEffect, useRef } from "react"; -import { theme } from "tailwind.config"; -import { PubListing } from "./PubListing"; -import useSWRInfinite from "swr/infinite"; -import { getPublications, type Cursor, type Publication } from "./getPublications"; - -export function SortedPublicationList(props: { - publications: Publication[]; - order: string; - nextCursor: Cursor | null; -}) { - let [order, setOrder] = useState(props.order); - - const getKey = ( - pageIndex: number, - previousPageData: { publications: Publication[]; nextCursor: Cursor | null } | null, - ) => { - // Reached the end - if (previousPageData && !previousPageData.nextCursor) return null; - - // First page, we don't have previousPageData - if (pageIndex === 0) return ["discover-publications", order, null] as const; - - // Add the cursor to the key - return ["discover-publications", order, previousPageData?.nextCursor] as const; - }; - - const { data, error, size, setSize, isValidating } = useSWRInfinite( - getKey, - ([_, orderValue, cursor]) => { - const orderParam = orderValue === "popular" ? "popular" : "recentlyUpdated"; - return getPublications(orderParam, cursor); - }, - { - fallbackData: order === props.order - ? [{ publications: props.publications, nextCursor: props.nextCursor }] - : undefined, - revalidateFirstPage: false, - }, - ); - - const loadMoreRef = useRef(null); - - // Set up intersection observer to load more when trigger element is visible - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && !isValidating) { - const hasMore = data && data[data.length - 1]?.nextCursor; - if (hasMore) { - setSize(size + 1); - } - } - }, - { threshold: 0.1 }, - ); - - if (loadMoreRef.current) { - observer.observe(loadMoreRef.current); - } - - return () => observer.disconnect(); - }, [data, size, setSize, isValidating]); - - const allPublications = data ? data.flatMap((page) => page.publications) : []; - - return ( -
- { - const url = new URL(window.location.href); - url.searchParams.set("order", o); - window.history.pushState({}, "", url); - setOrder(o); - }} - /> -
- {allPublications.map((pub) => ( - - ))} - {/* Trigger element for loading more publications */} - -
- ); -} - -export default function SortButtons(props: { - order: string; - setOrder: (order: string) => void; -}) { - const [selected, setSelected] = useState<"recentlyUpdated" | "popular">( - "recentlyUpdated", - ); - - return ( -
- props.setOrder("recentlyUpdated")} - > - Recently Updated - - - props.setOrder("popular")} - > - Popular - -
- ); -} - -const SortButton = (props: { - children: React.ReactNode; - onClick: () => void; - selected: boolean; -}) => { - return ( -
- - {props.selected && ( - <> -
- -
-
- -
-
- -
- - )} -
- ); -}; - -const GlitterBig = () => { - return ( - - - - ); -}; - -const GlitterSmall = () => { - return ( - - - - ); -}; diff --git a/app/(home-pages)/discover/getPublications.ts b/app/(home-pages)/discover/getPublications.ts deleted file mode 100644 index aa633c1a..00000000 --- a/app/(home-pages)/discover/getPublications.ts +++ /dev/null @@ -1,133 +0,0 @@ -"use server"; - -import { supabaseServerClient } from "supabase/serverClient"; -import { - normalizePublicationRow, - hasValidPublication, -} from "src/utils/normalizeRecords"; -import { deduplicateByUri } from "src/utils/deduplicateRecords"; - -export type Cursor = { - sort_date?: string; - count?: number; - uri: string; -}; - -export type Publication = Awaited< - ReturnType ->["publications"][number]; - -export async function getPublications( - order: "recentlyUpdated" | "popular" = "recentlyUpdated", - cursor?: Cursor | null, -): Promise<{ publications: any[]; nextCursor: Cursor | null }> { - const limit = 25; - - // Fetch all publications with their most recent document - let { data: publications, error } = await supabaseServerClient - .from("publications") - .select( - "*, documents_in_publications(*, documents(*)), publication_subscriptions(count)", - ) - .or( - "record->preferences->showInDiscover.is.null,record->preferences->>showInDiscover.eq.true", - ) - .order("documents(sort_date)", { - referencedTable: "documents_in_publications", - ascending: false, - }) - .limit(1, { referencedTable: "documents_in_publications" }); - - if (error) { - console.error("Error fetching publications:", error); - return { publications: [], nextCursor: null }; - } - - // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces - const dedupedPublications = deduplicateByUri(publications || []); - - // Filter out publications without documents - const allPubs = dedupedPublications.filter( - (pub) => pub.documents_in_publications.length > 0, - ); - - // Sort on the server - allPubs.sort((a, b) => { - if (order === "popular") { - const aCount = a.publication_subscriptions[0]?.count || 0; - const bCount = b.publication_subscriptions[0]?.count || 0; - if (bCount !== aCount) { - return bCount - aCount; - } - // Secondary sort by uri for stability - return b.uri.localeCompare(a.uri); - } else { - // recentlyUpdated - const aDate = new Date( - a.documents_in_publications[0]?.documents?.sort_date || 0, - ).getTime(); - const bDate = new Date( - b.documents_in_publications[0]?.documents?.sort_date || 0, - ).getTime(); - if (bDate !== aDate) { - return bDate - aDate; - } - // Secondary sort by uri for stability - return b.uri.localeCompare(a.uri); - } - }); - - // Find cursor position and slice - let startIndex = 0; - if (cursor) { - startIndex = allPubs.findIndex((pub) => { - if (order === "popular") { - const pubCount = pub.publication_subscriptions[0]?.count || 0; - // Find first pub after cursor - return ( - pubCount < (cursor.count || 0) || - (pubCount === cursor.count && pub.uri < cursor.uri) - ); - } else { - const pubDate = pub.documents_in_publications[0]?.documents?.sort_date || ""; - // Find first pub after cursor - return ( - pubDate < (cursor.sort_date || "") || - (pubDate === cursor.sort_date && pub.uri < cursor.uri) - ); - } - }); - // If not found, we're at the end - if (startIndex === -1) { - return { publications: [], nextCursor: null }; - } - } - - // Get the page - const page = allPubs.slice(startIndex, startIndex + limit); - - // Normalize publication records - const normalizedPage = page - .map(normalizePublicationRow) - .filter(hasValidPublication); - - // Create next cursor based on last item in normalizedPage - const lastItem = normalizedPage[normalizedPage.length - 1]; - const nextCursor = - normalizedPage.length > 0 && startIndex + limit < allPubs.length - ? order === "recentlyUpdated" - ? { - sort_date: lastItem.documents_in_publications[0]?.documents?.sort_date, - uri: lastItem.uri, - } - : { - count: lastItem.publication_subscriptions[0]?.count || 0, - uri: lastItem.uri, - } - : null; - - return { - publications: normalizedPage, - nextCursor, - }; -} diff --git a/app/(home-pages)/discover/page.tsx b/app/(home-pages)/discover/page.tsx deleted file mode 100644 index facf7c12..00000000 --- a/app/(home-pages)/discover/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import Link from "next/link"; -import { SortedPublicationList } from "./SortedPublicationList"; -import { Metadata } from "next"; -import { DashboardLayout } from "components/PageLayouts/DashboardLayout"; -import { getPublications } from "./getPublications"; - -export const metadata: Metadata = { - title: "Leaflet Discover", - description: "Explore publications on Leaflet ✨ Or make your own!", -}; - -export default async function Discover(props: { - searchParams: Promise<{ [key: string]: string | string[] | undefined }>; -}) { - let order = ((await props.searchParams).order as string) || "recentlyUpdated"; - - return ( - , - }, - }} - /> - ); -} - -const DiscoverContent = async (props: { order: string }) => { - const orderValue = props.order === "popular" ? "popular" : "recentlyUpdated"; - let { publications, nextCursor } = await getPublications(orderValue); - - return ( -
-
-

Discover

-

- Explore publications on Leaflet ✨ Or{" "} - make your own! -

-
- -
- ); -}; diff --git a/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx index a298dec3..15e598d0 100644 --- a/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx +++ b/app/(home-pages)/p/[didOrHandle]/ProfileLayout.tsx @@ -11,10 +11,10 @@ export function ProfileLayout(props: { children: React.ReactNode }) { ${ cardBorderHidden ? "" - : "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page" + : "overflow-y-scroll h-full border border-border-light rounded-lg bg-bg-page px-3 sm:px-4" } max-w-prose mx-auto w-full - flex flex-col + flex flex-col pb-3 text-center `} > diff --git a/app/(home-pages)/discover/PubListing.tsx b/app/(home-pages)/p/[didOrHandle]/PubListing.tsx similarity index 71% rename from app/(home-pages)/discover/PubListing.tsx rename to app/(home-pages)/p/[didOrHandle]/PubListing.tsx index a899c0f4..10364400 100644 --- a/app/(home-pages)/discover/PubListing.tsx +++ b/app/(home-pages)/p/[didOrHandle]/PubListing.tsx @@ -1,7 +1,8 @@ "use client"; import { AtUri } from "@atproto/syntax"; import { PublicationSubscription } from "app/(home-pages)/reader/getSubscriptions"; -import { SubscribeWithBluesky } from "app/lish/Subscribe"; +import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; +import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; import { PubIcon } from "components/ActionBar/Publications"; import { Separator } from "components/Layout"; import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; @@ -9,11 +10,7 @@ import { BaseThemeProvider } from "components/ThemeManager/ThemeProvider"; import { blobRefToSrc } from "src/utils/blobRefToSrc"; import { timeAgo } from "src/utils/timeAgo"; -export const PubListing = ( - props: PublicationSubscription & { - resizeHeight?: boolean; - }, -) => { +export const PubListing = (props: PublicationSubscription) => { let record = props.record; let theme = usePubTheme(record?.theme); let backgroundImage = record?.theme?.backgroundImage?.image?.ref @@ -28,8 +25,7 @@ export const PubListing = ( if (!record) return null; return ( - +
); }; diff --git a/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx b/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx index b7a9a350..a2a3f474 100644 --- a/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx +++ b/app/(home-pages)/p/[didOrHandle]/comments/CommentsContent.tsx @@ -85,7 +85,7 @@ export const ProfileCommentsContent = (props: { } return ( -
+
{allComments.map((comment) => ( ))} diff --git a/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx b/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx index 0284b0f5..49818ecd 100644 --- a/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx +++ b/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from "react"; import useSWRInfinite from "swr/infinite"; -import { PubListing } from "app/(home-pages)/discover/PubListing"; +import { PubListing } from "app/(home-pages)/p/[didOrHandle]/PubListing"; import { getSubscriptions, type PublicationSubscription, diff --git a/app/(home-pages)/reader/SubscriptionsContent.tsx b/app/(home-pages)/reader/SubscriptionsContent.tsx deleted file mode 100644 index 9645822f..00000000 --- a/app/(home-pages)/reader/SubscriptionsContent.tsx +++ /dev/null @@ -1,105 +0,0 @@ -"use client"; -import { PubListing } from "app/(home-pages)/discover/PubListing"; -import { ButtonPrimary } from "components/Buttons"; -import { DiscoverSmall } from "components/Icons/DiscoverSmall"; -import { Json } from "supabase/database.types"; -import { PublicationSubscription, getSubscriptions } from "./getSubscriptions"; -import useSWRInfinite from "swr/infinite"; -import { useEffect, useRef } from "react"; -import { Cursor } from "./getReaderFeed"; -import Link from "next/link"; - -export const SubscriptionsContent = (props: { - publications: PublicationSubscription[]; - nextCursor: Cursor | null; -}) => { - const getKey = ( - pageIndex: number, - previousPageData: { - subscriptions: PublicationSubscription[]; - nextCursor: Cursor | null; - } | null, - ) => { - // Reached the end - if (previousPageData && !previousPageData.nextCursor) return null; - - // First page, we don't have previousPageData - if (pageIndex === 0) return ["subscriptions", null] as const; - - // Add the cursor to the key - return ["subscriptions", previousPageData?.nextCursor] as const; - }; - - const { data, error, size, setSize, isValidating } = useSWRInfinite( - getKey, - ([_, cursor]) => getSubscriptions(null, cursor), - { - fallbackData: [ - { subscriptions: props.publications, nextCursor: props.nextCursor }, - ], - revalidateFirstPage: false, - }, - ); - - const loadMoreRef = useRef(null); - - // Set up intersection observer to load more when trigger element is visible - useEffect(() => { - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && !isValidating) { - const hasMore = data && data[data.length - 1]?.nextCursor; - if (hasMore) { - setSize(size + 1); - } - } - }, - { threshold: 0.1 }, - ); - - if (loadMoreRef.current) { - observer.observe(loadMoreRef.current); - } - - return () => observer.disconnect(); - }, [data, size, setSize, isValidating]); - - const allPublications = data - ? data.flatMap((page) => page.subscriptions) - : []; - - if (allPublications.length === 0 && !isValidating) - return ; - - return ( -
-
- {allPublications?.map((p, index) => )} -
- {/* Trigger element for loading more subscriptions */} - - ); -}; - -export const SubscriptionsEmpty = () => { - return ( -
- You haven't subscribed to any publications yet! - - - Discover Publications - - -
- ); -}; diff --git a/app/(home-pages)/reader/getSubscriptions.ts b/app/(home-pages)/reader/getSubscriptions.ts index d1b3e6ff..b474c19a 100644 --- a/app/(home-pages)/reader/getSubscriptions.ts +++ b/app/(home-pages)/reader/getSubscriptions.ts @@ -29,7 +29,9 @@ export async function getSubscriptions( let query = supabaseServerClient .from("publication_subscriptions") - .select(`*, publications(*, documents_in_publications(*, documents(*)))`) + .select( + `*, publications(*, publication_subscriptions(*), documents_in_publications(*, documents(*)))`, + ) .order(`created_at`, { ascending: false }) .order(`uri`, { ascending: false }) .order("documents(sort_date)", { @@ -51,7 +53,7 @@ export async function getSubscriptions( await Promise.all( pubs?.map(async (pub) => { const normalizedRecord = normalizePublicationRecord( - pub.publications?.record + pub.publications?.record, ); if (!normalizedRecord) return null; let id = await idResolver.did.resolve(pub.publications?.identity_did!); @@ -62,10 +64,9 @@ export async function getSubscriptions( ? { handle: `@${id.alsoKnownAs[0].slice(5)}` } : undefined, } as PublicationSubscription; - }) || [] + }) || [], ) ).filter((sub): sub is PublicationSubscription => sub !== null); - const nextCursor = pubs && pubs.length > 0 ? { @@ -83,6 +84,7 @@ export async function getSubscriptions( export type PublicationSubscription = { authorProfile?: { handle: string }; record: NormalizedPublication; + publication_subscriptions: { identity: string }[]; uri: string; documents_in_publications: { documents: { data?: Json; sort_date: string } | null; diff --git a/app/lish/Subscribe.tsx b/app/lish/Subscribe.tsx index eb2e1411..7c443496 100644 --- a/app/lish/Subscribe.tsx +++ b/app/lish/Subscribe.tsx @@ -24,8 +24,10 @@ import { useSearchParams } from "next/navigation"; import LoginForm from "app/login/LoginForm"; import { RSSSmall } from "components/Icons/RSSSmall"; import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; +import { RSSTiny } from "components/Icons/RSSTiny"; export const SubscribeWithBluesky = (props: { + compact?: boolean; pubName: string; pub_uri: string; base_url: string; @@ -36,8 +38,12 @@ export const SubscribeWithBluesky = (props: { let [successModalOpen, setSuccessModalOpen] = useState( !!searchParams.has("showSubscribeSuccess"), ); + let [localSubscribeState, setLocalSubscribeState] = useState< + "subscribed" | "unsubscribed" + >("subscribed"); let subscribed = identity?.atp_did && + localSubscribeState !== "unsubscribed" && props.subscribers.find((s) => s.identity === identity.atp_did); if (successModalOpen) @@ -48,12 +54,19 @@ export const SubscribeWithBluesky = (props: { /> ); if (subscribed) { - return ; + return ( + setLocalSubscribeState("unsubscribed")} + /> + ); } return (
setLocalSubscribeState("subscribed")} + compact={props.compact} pub_uri={props.pub_uri} setSuccessModalOpen={setSuccessModalOpen} /> @@ -63,7 +76,11 @@ export const SubscribeWithBluesky = (props: { target="_blank" aria-label="Subscribe to RSS" > - + {props.compact ? ( + + ) : ( + + )}
@@ -74,6 +91,8 @@ export const ManageSubscription = (props: { pub_uri: string; subscribers: { identity: string }[]; base_url: string; + compact?: boolean; + onUnsubscribe?: () => void; }) => { let toaster = useToaster(); let [hasFeed] = useState(false); @@ -83,16 +102,21 @@ export const ManageSubscription = (props: { content: "You unsubscribed.", type: "success", }); + props.onUnsubscribe?.(); }, null); return ( +
Manage Subscription
} > -
+

Update Options

{!hasFeed && ( @@ -102,7 +126,7 @@ export const ManageSubscription = (props: { className=" place-self-center" > - View Bluesky Custom Feed + Bluesky Custom Feed )} @@ -121,8 +145,12 @@ export const ManageSubscription = (props: {
-
@@ -133,6 +161,8 @@ export const ManageSubscription = (props: { let BlueskySubscribeButton = (props: { pub_uri: string; setSuccessModalOpen: (open: boolean) => void; + compact?: boolean; + setLocalSubscribeState: () => void; }) => { let { identity } = useIdentityData(); let toaster = useToaster(); @@ -155,6 +185,7 @@ let BlueskySubscribeButton = (props: { props.setSuccessModalOpen(true); } toaster({ content:
You're Subscribed!
, type: "success" }); + props.setLocalSubscribeState(); }, null); let [isClient, setIsClient] = useState(false); @@ -166,8 +197,12 @@ let BlueskySubscribeButton = (props: { return ( + Subscribe with Bluesky } @@ -190,7 +225,10 @@ let BlueskySubscribeButton = (props: { action={subscribe} className="place-self-center flex flex-row gap-1" > - + {subscribePending ? ( ) : ( diff --git a/components/ActionBar/ProfileButton.tsx b/components/ActionBar/ProfileButton.tsx index 7c3b5f7a..218101c2 100644 --- a/components/ActionBar/ProfileButton.tsx +++ b/components/ActionBar/ProfileButton.tsx @@ -40,7 +40,7 @@ export const ProfileButton = () => { > {record && ( <> - + {}}>View Profile diff --git a/components/Icons/RSSTiny.tsx b/components/Icons/RSSTiny.tsx new file mode 100644 index 00000000..9e0c24ab --- /dev/null +++ b/components/Icons/RSSTiny.tsx @@ -0,0 +1,18 @@ +import { Props } from "./Props"; +export const RSSTiny = (props: Props) => { + return ( + + + + ); +}; diff --git a/components/Popover/index.tsx b/components/Popover/index.tsx index fb987d3d..98b44eef 100644 --- a/components/Popover/index.tsx +++ b/components/Popover/index.tsx @@ -42,7 +42,7 @@ export const Popover = (props: {