From 10133c257cfbf8ad47aa3baf99b7c40f04f231aa Mon Sep 17 00:00:00 2001 From: Jared Pereira Date: Fri, 13 Feb 2026 15:47:22 -0500 Subject: [PATCH] Add proper reader experience and rework nav (#269) * moved around some stuff for our navigation * little tweak * messing around with navigation on mobile * Added a page title to the home layout * added pagetitle to looseleaf and pub, added shortcut if you only have one pub or looseleaf * some icons * quick little adjustments * moved action buttons on mobile to page title area, publist commented out for now * added the publicaions nav to the mobile footer * add states for logged out * stray console log eradication * some shifting around of things * update the homeempty layout * move publicationbuttons into separate compoennt, added state to handle jsut one pub or looseleaf * rename small prop in PubIcon to tiny * adding tabs to Reader * oops forgot the new files * updated postListing design * add a drawer for comments and mentions in reader * new files * remove features not in scope * adjust width of ActionButon in desktop * added indication that post is on non-leaflet page is applicable * repostion interaction drawer close button * small fixes per feedback * add inngest function to track bsky post likes * don't sync bsky likes of brid.gy posts * add bsky auth view all permissions * add aud to bsky view permissions * add recommend notification * fix post links to standard site blogs * don't getpreferences on subscribe * add recommend column to documents * check permissions on retractions * don't show block options if no write permissions * handle pasting bluesky posts * fix: copy post link button * add indexed col to documents and update inngest * fix type error * add hot feed * adding profile button to nav, removing things where necessary * Remove accidentally committed files from bbc6c135 Remove scratch/, scripts/, database.sql, appview/test.ts, CoordDebugger.tsx, search_loose_leafs.ts, supabase migration, and tmp_standard-site-migration.md that were unintentionally included in the previous commit. Co-Authored-By: Claude Opus 4.6 * added profile settings to nav * WIP suspense reader pages * add loading.tsx for route level suspense boundary * render what's hot for logged out users and don't render subs * highlight what's hot tab if default * persist nav state on reader/home * remove .claude/settings.local.json * wire up interactions * remove publications and fix external url logic * don't keep previous data in interactions sidebar in feed * fix empty subs state * fix share links in postlisting * remove help text * fix cover image positioning --------- Co-authored-by: celine Co-authored-by: Henrique Dias Co-authored-by: Claude Opus 4.6 --- app/(home-pages)/discover/SortButtons.tsx | 97 ------ .../discover/SortedPublicationList.tsx | 195 ------------ app/(home-pages)/discover/getPublications.ts | 133 -------- app/(home-pages)/discover/page.tsx | 53 ---- .../home/Actions/AccountSettings.tsx | 122 +------- app/(home-pages)/home/Actions/Actions.tsx | 8 +- .../home/Actions/CreateNewButton.tsx | 3 +- app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx | 134 ++++---- .../home/HomeEmpty/WelcomeToLeafletIllo.tsx | 4 +- app/(home-pages)/home/HomeLayout.tsx | 13 +- app/(home-pages)/layout.tsx | 3 + .../looseleafs/LooseleafsLayout.tsx | 1 + .../p/[didOrHandle]/PostsContent.tsx | 2 +- .../p/[didOrHandle]/ProfileHeader.tsx | 6 +- .../p/[didOrHandle]/ProfileLayout.tsx | 4 +- .../p/[didOrHandle]/ProfileTabs.tsx | 2 +- .../[didOrHandle]}/PubListing.tsx | 30 +- .../comments/CommentsContent.tsx | 2 +- app/(home-pages)/p/[didOrHandle]/layout.tsx | 5 +- .../subscriptions/SubscriptionsContent.tsx | 4 +- app/(home-pages)/page.tsx | 31 ++ app/(home-pages)/reader/FeedSkeleton.tsx | 13 + app/(home-pages)/reader/GlobalContent.tsx | 49 +++ .../{ReaderContent.tsx => InboxContent.tsx} | 61 ++-- .../reader/InteractionDrawers.tsx | 108 +++++++ app/(home-pages)/reader/NewContent.tsx | 93 ++++++ app/(home-pages)/reader/PreviewDrawer.tsx | 0 .../reader/ReaderMentionsContent.tsx | 72 +++++ .../reader/SubscriptionsContent.tsx | 105 ------- app/(home-pages)/reader/enrichPost.ts | 88 ++++++ app/(home-pages)/reader/getHotFeed.ts | 100 ++++++ app/(home-pages)/reader/getNewFeed.ts | 54 ++++ app/(home-pages)/reader/getReaderFeed.ts | 51 +-- app/(home-pages)/reader/getSubscriptions.ts | 10 +- app/(home-pages)/reader/hot/page.tsx | 7 + app/(home-pages)/reader/layout.tsx | 79 +++++ app/(home-pages)/reader/loading.tsx | 4 + app/(home-pages)/reader/new/page.tsx | 7 + app/(home-pages)/reader/page.tsx | 45 +-- app/[leaflet_id]/Footer.tsx | 22 +- .../publish/BskyPostEditorProsemirror.tsx | 2 - .../[command]/get_document_interactions.ts | 90 ++++++ app/api/rpc/[command]/get_hot_feed.ts | 17 + app/api/rpc/[command]/route.ts | 4 + app/api/update-nav-state/route.ts | 16 + app/lish/Subscribe.tsx | 56 +++- .../[rkey]/Interactions/Comments/index.tsx | 32 +- .../[rkey]/Interactions/InteractionDrawer.tsx | 52 +++- .../[rkey]/Interactions/Quotes.tsx | 10 +- .../[did]/[publication]/[rkey]/ThreadPage.tsx | 1 - .../[publication]/[rkey]/getPostPageData.ts | 4 - .../[did]/[publication]/dashboard/Actions.tsx | 1 + .../[publication]/dashboard/DraftList.tsx | 10 +- .../dashboard/NewDraftButton.tsx | 26 +- .../dashboard/PublicationDashboard.tsx | 1 + .../dashboard/PublicationSWRProvider.tsx | 6 +- .../settings/PublicationSettings.tsx | 1 + app/route.ts | 11 - components/ActionBar/ActionButton.tsx | 20 +- components/ActionBar/DesktopNavigation.tsx | 66 ++++ components/ActionBar/Footer.tsx | 2 +- components/ActionBar/MobileNavigation.tsx | 63 ++++ components/ActionBar/Navigation.tsx | 179 ----------- components/ActionBar/NavigationButtons.tsx | 101 ++++++ components/ActionBar/ProfileButton.tsx | 61 ++++ .../ActionBar/PublicationNavigation.tsx | 148 +++++++++ components/ActionBar/Publications.tsx | 29 +- components/Blocks/BaseTextareaBlock.tsx | 1 - components/Icons/HomeTiny.tsx | 21 ++ components/Icons/LooseleafTiny.tsx | 19 ++ components/Icons/MoreOptionsVerticalTiny.tsx | 1 + components/Icons/NotificationSmall.tsx | 37 +-- components/Icons/RSSTiny.tsx | 18 ++ components/Icons/ReaderSmall.tsx | 7 +- components/Icons/ShareTiny.tsx | 19 ++ components/Icons/TagSmall.tsx | 19 ++ components/Icons/WriterSmall.tsx | 19 ++ components/InteractionsPreview.tsx | 2 +- components/LoginButton.tsx | 6 +- components/NavStateTracker.tsx | 27 ++ components/PageHeader.tsx | 2 +- components/PageLayouts/DashboardLayout.tsx | 49 ++- components/Pages/useHasBackgroundImage.ts | 5 + components/Popover/index.tsx | 2 +- components/PostListing.tsx | 294 +++++++++++++----- components/Tab.tsx | 4 + components/Tags.tsx | 1 - components/ThemeManager/ThemeProvider.tsx | 41 ++- src/replicache/mutations.ts | 1 - src/useSelectedPostState.ts | 16 + src/utils/useRecordFromDid.ts | 12 + 91 files changed, 2070 insertions(+), 1382 deletions(-) delete mode 100644 app/(home-pages)/discover/SortButtons.tsx delete mode 100644 app/(home-pages)/discover/SortedPublicationList.tsx delete mode 100644 app/(home-pages)/discover/getPublications.ts delete mode 100644 app/(home-pages)/discover/page.tsx rename app/(home-pages)/{discover => p/[didOrHandle]}/PubListing.tsx (71%) create mode 100644 app/(home-pages)/page.tsx create mode 100644 app/(home-pages)/reader/FeedSkeleton.tsx create mode 100644 app/(home-pages)/reader/GlobalContent.tsx rename app/(home-pages)/reader/{ReaderContent.tsx => InboxContent.tsx} (61%) create mode 100644 app/(home-pages)/reader/InteractionDrawers.tsx create mode 100644 app/(home-pages)/reader/NewContent.tsx create mode 100644 app/(home-pages)/reader/PreviewDrawer.tsx create mode 100644 app/(home-pages)/reader/ReaderMentionsContent.tsx delete mode 100644 app/(home-pages)/reader/SubscriptionsContent.tsx create mode 100644 app/(home-pages)/reader/enrichPost.ts create mode 100644 app/(home-pages)/reader/getHotFeed.ts create mode 100644 app/(home-pages)/reader/getNewFeed.ts create mode 100644 app/(home-pages)/reader/hot/page.tsx create mode 100644 app/(home-pages)/reader/layout.tsx create mode 100644 app/(home-pages)/reader/loading.tsx create mode 100644 app/(home-pages)/reader/new/page.tsx create mode 100644 app/api/rpc/[command]/get_document_interactions.ts create mode 100644 app/api/rpc/[command]/get_hot_feed.ts create mode 100644 app/api/update-nav-state/route.ts delete mode 100644 app/route.ts create mode 100644 components/ActionBar/DesktopNavigation.tsx create mode 100644 components/ActionBar/MobileNavigation.tsx delete mode 100644 components/ActionBar/Navigation.tsx create mode 100644 components/ActionBar/NavigationButtons.tsx create mode 100644 components/ActionBar/ProfileButton.tsx create mode 100644 components/ActionBar/PublicationNavigation.tsx create mode 100644 components/Icons/HomeTiny.tsx create mode 100644 components/Icons/LooseleafTiny.tsx create mode 100644 components/Icons/RSSTiny.tsx create mode 100644 components/Icons/ShareTiny.tsx create mode 100644 components/Icons/TagSmall.tsx create mode 100644 components/Icons/WriterSmall.tsx create mode 100644 components/NavStateTracker.tsx create mode 100644 components/Pages/useHasBackgroundImage.ts create mode 100644 src/useSelectedPostState.ts create mode 100644 src/utils/useRecordFromDid.ts 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)/home/Actions/AccountSettings.tsx b/app/(home-pages)/home/Actions/AccountSettings.tsx index 67df4f37..c3cb0043 100644 --- a/app/(home-pages)/home/Actions/AccountSettings.tsx +++ b/app/(home-pages)/home/Actions/AccountSettings.tsx @@ -1,134 +1,24 @@ "use client"; import { ActionButton } from "components/ActionBar/ActionButton"; -import { mutate } from "swr"; -import { AccountSmall } from "components/Icons/AccountSmall"; -import { LogoutSmall } from "components/Icons/LogoutSmall"; import { Popover } from "components/Popover"; -import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; -import { SpeedyLink } from "components/SpeedyLink"; -import { GoBackSmall } from "components/Icons/GoBackSmall"; -import { useState } from "react"; import { ThemeSetterContent } from "components/ThemeManager/ThemeSetter"; import { useIsMobile } from "src/hooks/isMobile"; +import { PaintSmall } from "components/Icons/PaintSmall"; -export const AccountSettings = (props: { entityID: string }) => { - let [state, setState] = useState<"menu" | "general" | "theme">("menu"); +export const AccountTheme = (props: { entityID: string }) => { let isMobile = useIsMobile(); return ( setState("menu")} side={isMobile ? "top" : "right"} align={isMobile ? "center" : "start"} - className={`max-w-xs w-[1000px] ${state === "theme" && "bg-white!"}`} - trigger={ label="Settings" />} + className={`w-xs bg-white!`} + arrowFill="bg-white" + trigger={ label="Theme" />} > - {state === "general" ? ( - setState("menu")} /> - ) : state === "theme" ? ( - setState("menu")} - /> - ) : ( - - )} - - ); -}; - -const SettingsMenu = (props: { - state: "menu" | "general" | "theme"; - setState: (s: typeof props.state) => void; -}) => { - let menuItemClassName = - "menuItem -mx-[8px] text-left flex items-center justify-between hover:no-underline!"; - - return ( -
- - - -
- ); -}; - -const GeneralSettings = (props: { backToMenu: () => void }) => { - return ( -
- props.backToMenu()} - /> - - -
- ); -}; -const AccountThemeSettings = (props: { - entityID: string; - backToMenu: () => void; -}) => { - return ( -
- props.backToMenu()} - /> -
- ); -}; -export const AccountSettingsHeader = (props: { - state: "menu" | "general" | "theme"; - backToMenuAction?: () => void; -}) => { - return ( -
- {props.state === "menu" - ? "Settings" - : props.state === "general" - ? "General" - : props.state === "theme" - ? "Account Theme" - : ""} - {props.backToMenuAction && ( - - )} -
+ ); }; diff --git a/app/(home-pages)/home/Actions/Actions.tsx b/app/(home-pages)/home/Actions/Actions.tsx index 20852f4d..050f7f16 100644 --- a/app/(home-pages)/home/Actions/Actions.tsx +++ b/app/(home-pages)/home/Actions/Actions.tsx @@ -2,7 +2,7 @@ import { ThemePopover } from "components/ThemeManager/ThemeSetter"; import { CreateNewLeafletButton } from "./CreateNewButton"; import { HelpButton } from "app/[leaflet_id]/actions/HelpButton"; -import { AccountSettings } from "./AccountSettings"; +import { AccountTheme } from "./AccountSettings"; import { useIdentityData } from "components/IdentityProvider"; import { useReplicache } from "src/replicache"; import { LoginActionButton } from "components/LoginButton"; @@ -13,11 +13,7 @@ export const Actions = () => { return ( <> - {identity ? ( - - ) : ( - - )} + {identity && } ); }; diff --git a/app/(home-pages)/home/Actions/CreateNewButton.tsx b/app/(home-pages)/home/Actions/CreateNewButton.tsx index 417c5dcd..454ae7ca 100644 --- a/app/(home-pages)/home/Actions/CreateNewButton.tsx +++ b/app/(home-pages)/home/Actions/CreateNewButton.tsx @@ -26,8 +26,9 @@ export const CreateNewLeafletButton = (props: {}) => { + icon= label="New" + smallOnMobile /> } > diff --git a/app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx b/app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx index ecabbabc..517e2580 100644 --- a/app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx +++ b/app/(home-pages)/home/HomeEmpty/HomeEmpty.tsx @@ -1,107 +1,83 @@ "use client"; - import { PubListEmptyIllo } from "components/ActionBar/Publications"; -import { ButtonPrimary } from "components/Buttons"; -import { AddSmall } from "components/Icons/AddSmall"; -import { Link } from "react-aria-components"; -import { DiscoverIllo } from "./DiscoverIllo"; +import { ButtonPrimary, ButtonSecondary } from "components/Buttons"; import { WelcomeToLeafletIllo } from "./WelcomeToLeafletIllo"; -import { DiscoverSmall } from "components/Icons/DiscoverSmall"; -import { PublishSmall } from "components/Icons/PublishSmall"; import { createNewLeaflet } from "actions/createNewLeaflet"; import { useIsMobile } from "src/hooks/isMobile"; +import { SpeedyLink } from "components/SpeedyLink"; export function HomeEmptyState() { - let isMobile = useIsMobile(); return ( -
-
-
- -
-
-

Leaflet

- {/*

A platform for social publishing.

*/} -
- Write and share delightful documents! -
- { - let openNewLeaflet = (id: string) => { - if (isMobile) { - window.location.href = `/${id}?focusFirstBlock`; - } else { - window.open(`/${id}?focusFirstBlock`, "_blank"); - } - }; - - let id = await createNewLeaflet({ - pageType: "doc", - redirectUser: false, - }); - openNewLeaflet(id); - }} - > - Write a Doc! - -
-
-
-
-
or
-
-
- +
- -
- Right now docs and publications are separate. Soon you'll be able to add - docs to pubs! +
+
+
or
+
+
); } -export const PublicationBanner = (props: { small?: boolean }) => { +let bannerStyles = + "flex flex-row sm:flex-col py-4 px-4 sm:items-center items-start gap-4"; + +const PublicationBanner = () => { return ( -
- {props.small ? ( - - ) : ( +
+
- )} -
- - Start a Publication - {" "} - and blog in the Atmosphere +
+

Create a Publication!

+
+ You can decide to share or publish it later. +
+ + Create a Publication + +
); }; -export const DiscoverBanner = (props: { small?: boolean }) => { +const DocBanner = () => { + let isMobile = useIsMobile(); + return ( -
- {props.small ? ( - - ) : ( -
- +
+
+ +
+ +
+

Just write something

+
+ You can decide to share or publish it later.
- )} -
- - Explore Publications - {" "} - on art, tech, games, music & more! + { + let openNewLeaflet = (id: string) => { + if (isMobile) { + window.location.href = `/${id}?focusFirstBlock`; + } else { + window.open(`/${id}?focusFirstBlock`, "_blank"); + } + }; + + let id = await createNewLeaflet({ + pageType: "doc", + redirectUser: false, + }); + openNewLeaflet(id); + }} + > + New Doc! +
); diff --git a/app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx b/app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx index 289d8355..d88c55a5 100644 --- a/app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx +++ b/app/(home-pages)/home/HomeEmpty/WelcomeToLeafletIllo.tsx @@ -3,8 +3,8 @@ import { theme } from "tailwind.config"; export const WelcomeToLeafletIllo = () => { return ( ); }; @@ -170,10 +167,6 @@ export function HomeLeafletList(props: { showPreview />
- - {leaflets.filter((l) => !!l.token.leaflets_in_publications).length === - 0 && } - ); } @@ -204,7 +197,7 @@ export function LeafletList(props: { className={` leafletList w-full - ${display === "grid" ? "grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-4 gap-x-4 sm:gap-x-6 sm:gap-y-5 grow" : "flex flex-col gap-2 pt-2"} `} + ${display === "grid" ? "grid auto-rows-max md:grid-cols-4 sm:grid-cols-3 grid-cols-2 gap-y-4 gap-x-4 sm:gap-x-6 sm:gap-y-5 grow" : "flex flex-col gap-2"} `} > {props.leaflets.map(({ token: leaflet, added_at, archived }, index) => ( + {props.children} ); @@ -34,6 +36,7 @@ export default async function HomePagesLayout(props: { > + {props.children} diff --git a/app/(home-pages)/looseleafs/LooseleafsLayout.tsx b/app/(home-pages)/looseleafs/LooseleafsLayout.tsx index 5f0c12b3..c389d6e4 100644 --- a/app/(home-pages)/looseleafs/LooseleafsLayout.tsx +++ b/app/(home-pages)/looseleafs/LooseleafsLayout.tsx @@ -47,6 +47,7 @@ export const LooseleafsLayout = (props: { ), }, }} + pageTitle="Looseleafs" /> ); }; diff --git a/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx b/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx index eb841838..7bd67c3c 100644 --- a/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx +++ b/app/(home-pages)/p/[didOrHandle]/PostsContent.tsx @@ -68,7 +68,7 @@ export const ProfilePostsContent = (props: { } return ( -
+
{allPosts.map((post) => ( ))} diff --git a/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx index 3fb2b0fd..f99934c7 100644 --- a/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx +++ b/app/(home-pages)/p/[didOrHandle]/ProfileHeader.tsx @@ -42,8 +42,6 @@ export const ProfileHeader = (props: { @{props.profile.handle}
); - console.log(props.profile); - return (
-
+
{props.publications.map((p) => ( 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)/p/[didOrHandle]/ProfileTabs.tsx b/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx index c5b9e3a2..bb058749 100644 --- a/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx +++ b/app/(home-pages)/p/[didOrHandle]/ProfileTabs.tsx @@ -41,7 +41,7 @@ export const ProfileTabs = (props: { didOrHandle: string }) => { const bgColor = cardBorderHidden ? "var(--bg-leaflet)" : "var(--bg-page)"; return ( -
+
{ +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]/layout.tsx b/app/(home-pages)/p/[didOrHandle]/layout.tsx index 4108fe28..eb9f3531 100644 --- a/app/(home-pages)/p/[didOrHandle]/layout.tsx +++ b/app/(home-pages)/p/[didOrHandle]/layout.tsx @@ -82,6 +82,7 @@ export default async function ProfilePageLayout(props: { id="profile" defaultTab="default" currentPage="profile" + profileDid={did} actions={null} tabs={{ default: { @@ -93,9 +94,7 @@ export default async function ProfilePageLayout(props: { publications={publications || []} /> -
- {props.children} -
+ <>{props.children} ), }, diff --git a/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx b/app/(home-pages)/p/[didOrHandle]/subscriptions/SubscriptionsContent.tsx index 9e5e4ca8..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, @@ -82,7 +82,7 @@ export const ProfileSubscriptionsContent = (props: { return (
-
+
{allSubscriptions.map((sub) => ( ))} diff --git a/app/(home-pages)/page.tsx b/app/(home-pages)/page.tsx new file mode 100644 index 00000000..036e9562 --- /dev/null +++ b/app/(home-pages)/page.tsx @@ -0,0 +1,31 @@ +import { cookies } from "next/headers"; +import ReaderLayout from "./reader/layout"; +import ReaderPage from "./reader/page"; +import HomePage from "./home/page"; + +export default async function RootPage() { + const cookieStore = await cookies(); + const hasAuth = + cookieStore.has("auth_token") || + cookieStore.has("external_auth_token"); + + if (!hasAuth) { + return ( + + + + ); + } + + const navState = cookieStore.get("nav-state")?.value; + + if (navState === "reader") { + return ( + + + + ); + } + + return ; +} diff --git a/app/(home-pages)/reader/FeedSkeleton.tsx b/app/(home-pages)/reader/FeedSkeleton.tsx new file mode 100644 index 00000000..2541b869 --- /dev/null +++ b/app/(home-pages)/reader/FeedSkeleton.tsx @@ -0,0 +1,13 @@ +export function FeedSkeleton() { + return ( +
+ {[...Array(3)].map((_, i) => ( +
+
+
+
+
+ ))} +
+ ); +} diff --git a/app/(home-pages)/reader/GlobalContent.tsx b/app/(home-pages)/reader/GlobalContent.tsx new file mode 100644 index 00000000..53cf2462 --- /dev/null +++ b/app/(home-pages)/reader/GlobalContent.tsx @@ -0,0 +1,49 @@ +"use client"; +import { use } from "react"; +import useSWR from "swr"; +import { callRPC } from "app/api/rpc/client"; +import { PostListing } from "components/PostListing"; +import type { Post } from "./getReaderFeed"; +import { + DesktopInteractionPreviewDrawer, + MobileInteractionPreviewDrawer, +} from "./InteractionDrawers"; + +export const GlobalContent = (props: { + promise: Promise<{ posts: Post[] }>; +}) => { + const initialData = use(props.promise); + + const { data } = useSWR( + "hot_feed", + async () => { + const res = await callRPC("get_hot_feed", {}); + return res as unknown as { posts: Post[] }; + }, + { + fallbackData: { posts: initialData.posts }, + }, + ); + + const posts = data?.posts ?? []; + + if (posts.length === 0) { + return ( +
+ Nothing trending right now. Check back soon! +
+ ); + } + + return ( +
+
+ {posts.map((p) => ( + + ))} +
+ + +
+ ); +}; diff --git a/app/(home-pages)/reader/ReaderContent.tsx b/app/(home-pages)/reader/InboxContent.tsx similarity index 61% rename from app/(home-pages)/reader/ReaderContent.tsx rename to app/(home-pages)/reader/InboxContent.tsx index e73b3df9..791d4827 100644 --- a/app/(home-pages)/reader/ReaderContent.tsx +++ b/app/(home-pages)/reader/InboxContent.tsx @@ -1,4 +1,5 @@ "use client"; +import { use } from "react"; import { ButtonPrimary } from "components/Buttons"; import { DiscoverSmall } from "components/Icons/DiscoverSmall"; import type { Cursor, Post } from "./getReaderFeed"; @@ -7,11 +8,17 @@ import { getReaderFeed } from "./getReaderFeed"; import { useEffect, useRef } from "react"; import Link from "next/link"; import { PostListing } from "components/PostListing"; +import { useHasBackgroundImage } from "components/Pages/useHasBackgroundImage"; +import { + DesktopInteractionPreviewDrawer, + MobileInteractionPreviewDrawer, +} from "./InteractionDrawers"; -export const ReaderContent = (props: { - posts: Post[]; - nextCursor: Cursor | null; +export const InboxContent = (props: { + promise: Promise<{ posts: Post[]; nextCursor: Cursor | null }>; }) => { + const { posts, nextCursor } = use(props.promise); + const getKey = ( pageIndex: number, previousPageData: { @@ -33,7 +40,7 @@ export const ReaderContent = (props: { getKey, ([_, cursor]) => getReaderFeed(cursor), { - fallbackData: [{ posts: props.posts, nextCursor: props.nextCursor }], + fallbackData: [{ posts, nextCursor }], revalidateFirstPage: false, }, ); @@ -63,24 +70,36 @@ export const ReaderContent = (props: { const allPosts = data ? data.flatMap((page) => page.posts) : []; + const sortedPosts = allPosts.sort( + (a, b) => + new Date(b.documents.data?.publishedAt || 0).getTime() - + new Date(a.documents.data?.publishedAt || 0).getTime(), + ); + if (allPosts.length === 0 && !isValidating) return ; + let hasBackgroundImage = useHasBackgroundImage(); + return ( -
- {allPosts.map((p) => ( - - ))} - {/* Trigger element for loading more posts */} -