diff --git a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx index fbe42825..fbe3dddd 100644 --- a/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx @@ -16,7 +16,7 @@ import { import { AppBskyFeedDefs } from "@atproto/api"; import { TextBlock } from "./TextBlock"; import { useDocument } from "contexts/DocumentContext"; -import { openPage, useOpenPages } from "../PostPages"; +import { openPage, useOpenPages } from "../postPageState"; import { openInteractionDrawer, setInteractionState, @@ -38,7 +38,6 @@ export function PublishedPageLinkBlock(props: { isCanvas?: boolean; pages?: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; }) { - //switch to use actually state let openPages = useOpenPages(); let isOpen = openPages.some((p) => p.type === "doc" && p.id === props.pageId); return ( @@ -209,9 +208,7 @@ const Interactions = (props: { pageId: string; parentPageId?: string }) => { let comments = allComments.filter( (c) => (c.record as PubLeafletComment.Record)?.onPage === props.pageId, ).length; - let quotes = mentions.filter((q) => - q.link.includes(props.pageId), - ).length; + let quotes = mentions.filter((q) => q.link.includes(props.pageId)).length; let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); diff --git a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx index f2812cb8..83c79789 100644 --- a/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx @@ -5,7 +5,7 @@ import { PageWrapper } from "components/Pages/Page"; import { useDrawerOpen } from "./Interactions/InteractionDrawer"; import { DotLoader } from "components/utils/DotLoader"; import { QuoteTiny } from "components/Icons/QuoteTiny"; -import { openPage } from "./PostPages"; +import { openPage } from "./postPageState"; import { BskyPostContent } from "./BskyPostContent"; import { QuotesLink, diff --git a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx index 850c6168..a6ce9f07 100644 --- a/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx +++ b/app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx @@ -8,7 +8,7 @@ import { QuoteTiny } from "components/Icons/QuoteTiny"; import { Separator } from "components/Layout"; import { useLocalizedDate } from "src/hooks/useLocalizedDate"; import { useHasPageLoaded } from "components/InitialPageLoadProvider"; -import { OpenPage, openPage } from "./PostPages"; +import { OpenPage, openPage } from "./postPageState"; import { ThreadLink, QuotesLink } from "./PostLinks"; import { BlueskyLinkTiny } from "components/Icons/BlueskyLinkTiny"; import { Avatar } from "components/Avatar"; diff --git a/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx b/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx index 08fa53ab..c418d27d 100644 --- a/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx +++ b/app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx @@ -18,7 +18,7 @@ import { useActiveHighlightState } from "../useHighlight"; import { PostContent } from "../PostContent"; import { ProfileViewBasic } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; import { flushSync } from "react-dom"; -import { openPage } from "../PostPages"; +import { openPage } from "../postPageState"; import useSWR, { mutate } from "swr"; import { DotLoader } from "components/utils/DotLoader"; import { CommentTiny } from "components/Icons/CommentTiny"; diff --git a/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx b/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx index 5762dea8..25169896 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostLinks.tsx @@ -1,7 +1,7 @@ "use client"; import { AppBskyFeedDefs } from "@atproto/api"; import { preload } from "swr"; -import { openPage, OpenPage } from "./PostPages"; +import { openPage, OpenPage } from "./postPageState"; type ThreadViewPost = AppBskyFeedDefs.ThreadViewPost; type NotFoundPost = AppBskyFeedDefs.NotFoundPost; diff --git a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx index ccf5bea2..f622d03e 100644 --- a/app/lish/[did]/[publication]/[rkey]/PostPages.tsx +++ b/app/lish/[did]/[publication]/[rkey]/PostPages.tsx @@ -10,7 +10,6 @@ import { useDocument } from "contexts/DocumentContext"; import { PostPageData } from "./getPostPageData"; import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; import { AppBskyFeedDefs } from "@atproto/api"; -import { create } from "zustand/react"; import { InteractionDrawer, useDrawerOpen, @@ -18,150 +17,27 @@ import { import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout"; import { PageOptionButton } from "components/Pages/PageOptions"; import { CloseTiny } from "components/Icons/CloseTiny"; -import { Fragment, useEffect } from "react"; -import { flushSync } from "react-dom"; -import { scrollIntoView } from "src/utils/scrollIntoView"; -import { useParams, useSearchParams } from "next/navigation"; -import { decodeQuotePosition } from "./quotePosition"; +import { Fragment } from "react"; import { PollData } from "./fetchPollData"; import { LinearDocumentPage } from "./LinearDocumentPage"; import { CanvasPage } from "./CanvasPage"; import { ThreadPage as ThreadPageComponent } from "./ThreadPage"; import { BlueskyQuotesPage } from "./BlueskyQuotesPage"; import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; - -// Page types -export type DocPage = { type: "doc"; id: string }; -export type ThreadPage = { type: "thread"; uri: string }; -export type QuotesPage = { type: "quotes"; uri: string }; -export type OpenPage = DocPage | ThreadPage | QuotesPage; - -// Get a stable key for a page -const getPageKey = (page: OpenPage): string => { - if (page.type === "doc") return page.id; - if (page.type === "quotes") return `quotes:${page.uri}`; - return `thread:${page.uri}`; -}; - -const usePostPageUIState = create(() => ({ - pages: [] as OpenPage[], - initialized: false, -})); - -export const useOpenPages = (): OpenPage[] => { - const { quote } = useParams(); - const state = usePostPageUIState((s) => s); - const searchParams = useSearchParams(); - const pageParam = searchParams.get("page"); - - if (!state.initialized) { - // Check for page search param first (for comment links) - if (pageParam) { - return [{ type: "doc", id: pageParam }]; - } - // Then check for quote param - if (quote) { - const decodedQuote = decodeQuotePosition(quote as string); - if (decodedQuote?.pageId) { - return [{ type: "doc", id: decodedQuote.pageId }]; - } - } - } - - return state.pages; -}; - -export const useInitializeOpenPages = () => { - const { quote } = useParams(); - const searchParams = useSearchParams(); - const pageParam = searchParams.get("page"); - - useEffect(() => { - const state = usePostPageUIState.getState(); - if (!state.initialized) { - // Check for page search param first (for comment links) - if (pageParam) { - usePostPageUIState.setState({ - pages: [{ type: "doc", id: pageParam }], - initialized: true, - }); - return; - } - // Then check for quote param - if (quote) { - const decodedQuote = decodeQuotePosition(quote as string); - if (decodedQuote?.pageId) { - usePostPageUIState.setState({ - pages: [{ type: "doc", id: decodedQuote.pageId }], - initialized: true, - }); - return; - } - } - // Mark as initialized even if no pageId found - usePostPageUIState.setState({ initialized: true }); - } - }, [quote, pageParam]); -}; - -export const openPage = ( - parent: OpenPage | undefined, - page: OpenPage, - options?: { scrollIntoView?: boolean }, -) => { - const pageKey = getPageKey(page); - const parentKey = parent ? getPageKey(parent) : undefined; - - // Check if the page is already open - const currentState = usePostPageUIState.getState(); - const existingPageIndex = currentState.pages.findIndex( - (p) => getPageKey(p) === pageKey, - ); - - // If page is already open, just scroll to it - if (existingPageIndex !== -1) { - if (options?.scrollIntoView !== false) { - scrollIntoView(`post-page-${pageKey}`); - } - return; - } - - flushSync(() => { - usePostPageUIState.setState((state) => { - let parentPosition = state.pages.findIndex( - (s) => getPageKey(s) === parentKey, - ); - // Close any pages after the parent and add the new page - return { - pages: - parentPosition === -1 - ? [page] - : [...state.pages.slice(0, parentPosition + 1), page], - initialized: true, - }; - }); - }); - - if (options?.scrollIntoView !== false) { - // Use requestAnimationFrame to ensure the DOM has been painted before scrolling - requestAnimationFrame(() => { - scrollIntoView(`post-page-${pageKey}`); - }); - } -}; - -export const closePage = (page: OpenPage) => { - const pageKey = getPageKey(page); - usePostPageUIState.setState((state) => { - let parentPosition = state.pages.findIndex( - (s) => getPageKey(s) === pageKey, - ); - return { - pages: state.pages.slice(0, parentPosition), - initialized: true, - }; - }); -}; +import { + type OpenPage, + type DocPage, + type ThreadPage, + type QuotesPage, + getPageKey, + useOpenPages, + useInitializeOpenPages, + openPage, + closePage, +} from "./postPageState"; + +export type { DocPage, ThreadPage, QuotesPage, OpenPage }; +export { getPageKey, useOpenPages, useInitializeOpenPages, openPage, closePage }; // Shared props type for both page components export type SharedPageProps = { @@ -305,9 +181,7 @@ export function PostPages({ : document.comments_on_documents } quotesAndMentions={ - preferences.showMentions === false - ? [] - : quotesAndMentions + preferences.showMentions === false ? [] : quotesAndMentions } did={did} /> @@ -401,9 +275,7 @@ export function PostPages({ : document.comments_on_documents } quotesAndMentions={ - preferences.showMentions === false - ? [] - : quotesAndMentions + preferences.showMentions === false ? [] : quotesAndMentions } did={did} /> diff --git a/app/lish/[did]/[publication]/[rkey]/postPageState.ts b/app/lish/[did]/[publication]/[rkey]/postPageState.ts new file mode 100644 index 00000000..7553b9cd --- /dev/null +++ b/app/lish/[did]/[publication]/[rkey]/postPageState.ts @@ -0,0 +1,139 @@ +import { create } from "zustand"; +import { flushSync } from "react-dom"; +import { scrollIntoView } from "src/utils/scrollIntoView"; +import { useParams, useSearchParams } from "next/navigation"; +import { decodeQuotePosition } from "./quotePosition"; +import { useEffect } from "react"; + +// Page types +export type DocPage = { type: "doc"; id: string }; +export type ThreadPage = { type: "thread"; uri: string }; +export type QuotesPage = { type: "quotes"; uri: string }; +export type OpenPage = DocPage | ThreadPage | QuotesPage; + +// Get a stable key for a page +export const getPageKey = (page: OpenPage): string => { + if (page.type === "doc") return page.id; + if (page.type === "quotes") return `quotes:${page.uri}`; + return `thread:${page.uri}`; +}; + +const usePostPageUIState = create(() => ({ + pages: [] as OpenPage[], + initialized: false, +})); + +export const useOpenPages = (): OpenPage[] => { + const { quote } = useParams(); + const state = usePostPageUIState((s) => s); + const searchParams = useSearchParams(); + const pageParam = searchParams.get("page"); + + if (!state.initialized) { + // Check for page search param first (for comment links) + if (pageParam) { + return [{ type: "doc", id: pageParam }]; + } + // Then check for quote param + if (quote) { + const decodedQuote = decodeQuotePosition(quote as string); + if (decodedQuote?.pageId) { + return [{ type: "doc", id: decodedQuote.pageId }]; + } + } + } + + return state.pages; +}; + +export const useInitializeOpenPages = () => { + const { quote } = useParams(); + const searchParams = useSearchParams(); + const pageParam = searchParams.get("page"); + + useEffect(() => { + const state = usePostPageUIState.getState(); + if (!state.initialized) { + // Check for page search param first (for comment links) + if (pageParam) { + usePostPageUIState.setState({ + pages: [{ type: "doc", id: pageParam }], + initialized: true, + }); + return; + } + // Then check for quote param + if (quote) { + const decodedQuote = decodeQuotePosition(quote as string); + if (decodedQuote?.pageId) { + usePostPageUIState.setState({ + pages: [{ type: "doc", id: decodedQuote.pageId }], + initialized: true, + }); + return; + } + } + // Mark as initialized even if no pageId found + usePostPageUIState.setState({ initialized: true }); + } + }, [quote, pageParam]); +}; + +export const openPage = ( + parent: OpenPage | undefined, + page: OpenPage, + options?: { scrollIntoView?: boolean }, +) => { + const pageKey = getPageKey(page); + const parentKey = parent ? getPageKey(parent) : undefined; + + // Check if the page is already open + const currentState = usePostPageUIState.getState(); + const existingPageIndex = currentState.pages.findIndex( + (p) => getPageKey(p) === pageKey, + ); + + // If page is already open, just scroll to it + if (existingPageIndex !== -1) { + if (options?.scrollIntoView !== false) { + scrollIntoView(`post-page-${pageKey}`); + } + return; + } + + flushSync(() => { + usePostPageUIState.setState((state) => { + let parentPosition = state.pages.findIndex( + (s) => getPageKey(s) === parentKey, + ); + // Close any pages after the parent and add the new page + return { + pages: + parentPosition === -1 + ? [page] + : [...state.pages.slice(0, parentPosition + 1), page], + initialized: true, + }; + }); + }); + + if (options?.scrollIntoView !== false) { + // Use requestAnimationFrame to ensure the DOM has been painted before scrolling + requestAnimationFrame(() => { + scrollIntoView(`post-page-${pageKey}`); + }); + } +}; + +export const closePage = (page: OpenPage) => { + const pageKey = getPageKey(page); + usePostPageUIState.setState((state) => { + let parentPosition = state.pages.findIndex( + (s) => getPageKey(s) === pageKey, + ); + return { + pages: state.pages.slice(0, parentPosition), + initialized: true, + }; + }); +};