a tool for shared writing and social publishing

extract out page ui state helpers

+161 -153
+2 -5
app/lish/[did]/[publication]/[rkey]/Blocks/PublishedPageBlock.tsx
··· 16 16 import { AppBskyFeedDefs } from "@atproto/api"; 17 17 import { TextBlock } from "./TextBlock"; 18 18 import { useDocument } from "contexts/DocumentContext"; 19 - import { openPage, useOpenPages } from "../PostPages"; 19 + import { openPage, useOpenPages } from "../postPageState"; 20 20 import { 21 21 openInteractionDrawer, 22 22 setInteractionState, ··· 38 38 isCanvas?: boolean; 39 39 pages?: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 40 40 }) { 41 - //switch to use actually state 42 41 let openPages = useOpenPages(); 43 42 let isOpen = openPages.some((p) => p.type === "doc" && p.id === props.pageId); 44 43 return ( ··· 209 208 let comments = allComments.filter( 210 209 (c) => (c.record as PubLeafletComment.Record)?.onPage === props.pageId, 211 210 ).length; 212 - let quotes = mentions.filter((q) => 213 - q.link.includes(props.pageId), 214 - ).length; 211 + let quotes = mentions.filter((q) => q.link.includes(props.pageId)).length; 215 212 216 213 let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); 217 214
+1 -1
app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx
··· 5 5 import { useDrawerOpen } from "./Interactions/InteractionDrawer"; 6 6 import { DotLoader } from "components/utils/DotLoader"; 7 7 import { QuoteTiny } from "components/Icons/QuoteTiny"; 8 - import { openPage } from "./PostPages"; 8 + import { openPage } from "./postPageState"; 9 9 import { BskyPostContent } from "./BskyPostContent"; 10 10 import { 11 11 QuotesLink,
+1 -1
app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx
··· 8 8 import { Separator } from "components/Layout"; 9 9 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 10 10 import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 11 - import { OpenPage, openPage } from "./PostPages"; 11 + import { OpenPage, openPage } from "./postPageState"; 12 12 import { ThreadLink, QuotesLink } from "./PostLinks"; 13 13 import { BlueskyLinkTiny } from "components/Icons/BlueskyLinkTiny"; 14 14 import { Avatar } from "components/Avatar";
+1 -1
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
··· 18 18 import { PostContent } from "../PostContent"; 19 19 import { ProfileViewBasic } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 20 20 import { flushSync } from "react-dom"; 21 - import { openPage } from "../PostPages"; 21 + import { openPage } from "../postPageState"; 22 22 import useSWR, { mutate } from "swr"; 23 23 import { DotLoader } from "components/utils/DotLoader"; 24 24 import { CommentTiny } from "components/Icons/CommentTiny";
+1 -1
app/lish/[did]/[publication]/[rkey]/PostLinks.tsx
··· 1 1 "use client"; 2 2 import { AppBskyFeedDefs } from "@atproto/api"; 3 3 import { preload } from "swr"; 4 - import { openPage, OpenPage } from "./PostPages"; 4 + import { openPage, OpenPage } from "./postPageState"; 5 5 6 6 type ThreadViewPost = AppBskyFeedDefs.ThreadViewPost; 7 7 type NotFoundPost = AppBskyFeedDefs.NotFoundPost;
+16 -144
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 10 10 import { PostPageData } from "./getPostPageData"; 11 11 import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 12 12 import { AppBskyFeedDefs } from "@atproto/api"; 13 - import { create } from "zustand/react"; 14 13 import { 15 14 InteractionDrawer, 16 15 useDrawerOpen, ··· 18 17 import { BookendSpacer, SandwichSpacer } from "components/LeafletLayout"; 19 18 import { PageOptionButton } from "components/Pages/PageOptions"; 20 19 import { CloseTiny } from "components/Icons/CloseTiny"; 21 - import { Fragment, useEffect } from "react"; 22 - import { flushSync } from "react-dom"; 23 - import { scrollIntoView } from "src/utils/scrollIntoView"; 24 - import { useParams, useSearchParams } from "next/navigation"; 25 - import { decodeQuotePosition } from "./quotePosition"; 20 + import { Fragment } from "react"; 26 21 import { PollData } from "./fetchPollData"; 27 22 import { LinearDocumentPage } from "./LinearDocumentPage"; 28 23 import { CanvasPage } from "./CanvasPage"; 29 24 import { ThreadPage as ThreadPageComponent } from "./ThreadPage"; 30 25 import { BlueskyQuotesPage } from "./BlueskyQuotesPage"; 31 26 import { useCardBorderHidden } from "components/Pages/useCardBorderHidden"; 32 - 33 - // Page types 34 - export type DocPage = { type: "doc"; id: string }; 35 - export type ThreadPage = { type: "thread"; uri: string }; 36 - export type QuotesPage = { type: "quotes"; uri: string }; 37 - export type OpenPage = DocPage | ThreadPage | QuotesPage; 38 - 39 - // Get a stable key for a page 40 - const getPageKey = (page: OpenPage): string => { 41 - if (page.type === "doc") return page.id; 42 - if (page.type === "quotes") return `quotes:${page.uri}`; 43 - return `thread:${page.uri}`; 44 - }; 45 - 46 - const usePostPageUIState = create(() => ({ 47 - pages: [] as OpenPage[], 48 - initialized: false, 49 - })); 50 - 51 - export const useOpenPages = (): OpenPage[] => { 52 - const { quote } = useParams(); 53 - const state = usePostPageUIState((s) => s); 54 - const searchParams = useSearchParams(); 55 - const pageParam = searchParams.get("page"); 56 - 57 - if (!state.initialized) { 58 - // Check for page search param first (for comment links) 59 - if (pageParam) { 60 - return [{ type: "doc", id: pageParam }]; 61 - } 62 - // Then check for quote param 63 - if (quote) { 64 - const decodedQuote = decodeQuotePosition(quote as string); 65 - if (decodedQuote?.pageId) { 66 - return [{ type: "doc", id: decodedQuote.pageId }]; 67 - } 68 - } 69 - } 70 - 71 - return state.pages; 72 - }; 73 - 74 - export const useInitializeOpenPages = () => { 75 - const { quote } = useParams(); 76 - const searchParams = useSearchParams(); 77 - const pageParam = searchParams.get("page"); 78 - 79 - useEffect(() => { 80 - const state = usePostPageUIState.getState(); 81 - if (!state.initialized) { 82 - // Check for page search param first (for comment links) 83 - if (pageParam) { 84 - usePostPageUIState.setState({ 85 - pages: [{ type: "doc", id: pageParam }], 86 - initialized: true, 87 - }); 88 - return; 89 - } 90 - // Then check for quote param 91 - if (quote) { 92 - const decodedQuote = decodeQuotePosition(quote as string); 93 - if (decodedQuote?.pageId) { 94 - usePostPageUIState.setState({ 95 - pages: [{ type: "doc", id: decodedQuote.pageId }], 96 - initialized: true, 97 - }); 98 - return; 99 - } 100 - } 101 - // Mark as initialized even if no pageId found 102 - usePostPageUIState.setState({ initialized: true }); 103 - } 104 - }, [quote, pageParam]); 105 - }; 106 - 107 - export const openPage = ( 108 - parent: OpenPage | undefined, 109 - page: OpenPage, 110 - options?: { scrollIntoView?: boolean }, 111 - ) => { 112 - const pageKey = getPageKey(page); 113 - const parentKey = parent ? getPageKey(parent) : undefined; 114 - 115 - // Check if the page is already open 116 - const currentState = usePostPageUIState.getState(); 117 - const existingPageIndex = currentState.pages.findIndex( 118 - (p) => getPageKey(p) === pageKey, 119 - ); 120 - 121 - // If page is already open, just scroll to it 122 - if (existingPageIndex !== -1) { 123 - if (options?.scrollIntoView !== false) { 124 - scrollIntoView(`post-page-${pageKey}`); 125 - } 126 - return; 127 - } 128 - 129 - flushSync(() => { 130 - usePostPageUIState.setState((state) => { 131 - let parentPosition = state.pages.findIndex( 132 - (s) => getPageKey(s) === parentKey, 133 - ); 134 - // Close any pages after the parent and add the new page 135 - return { 136 - pages: 137 - parentPosition === -1 138 - ? [page] 139 - : [...state.pages.slice(0, parentPosition + 1), page], 140 - initialized: true, 141 - }; 142 - }); 143 - }); 144 - 145 - if (options?.scrollIntoView !== false) { 146 - // Use requestAnimationFrame to ensure the DOM has been painted before scrolling 147 - requestAnimationFrame(() => { 148 - scrollIntoView(`post-page-${pageKey}`); 149 - }); 150 - } 151 - }; 27 + import { 28 + type OpenPage, 29 + type DocPage, 30 + type ThreadPage, 31 + type QuotesPage, 32 + getPageKey, 33 + useOpenPages, 34 + useInitializeOpenPages, 35 + openPage, 36 + closePage, 37 + } from "./postPageState"; 152 38 153 - export const closePage = (page: OpenPage) => { 154 - const pageKey = getPageKey(page); 155 - usePostPageUIState.setState((state) => { 156 - let parentPosition = state.pages.findIndex( 157 - (s) => getPageKey(s) === pageKey, 158 - ); 159 - return { 160 - pages: state.pages.slice(0, parentPosition), 161 - initialized: true, 162 - }; 163 - }); 164 - }; 39 + export type { DocPage, ThreadPage, QuotesPage, OpenPage }; 40 + export { getPageKey, useOpenPages, useInitializeOpenPages, openPage, closePage }; 165 41 166 42 // Shared props type for both page components 167 43 export type SharedPageProps = { ··· 305 181 : document.comments_on_documents 306 182 } 307 183 quotesAndMentions={ 308 - preferences.showMentions === false 309 - ? [] 310 - : quotesAndMentions 184 + preferences.showMentions === false ? [] : quotesAndMentions 311 185 } 312 186 did={did} 313 187 /> ··· 401 275 : document.comments_on_documents 402 276 } 403 277 quotesAndMentions={ 404 - preferences.showMentions === false 405 - ? [] 406 - : quotesAndMentions 278 + preferences.showMentions === false ? [] : quotesAndMentions 407 279 } 408 280 did={did} 409 281 />
+139
app/lish/[did]/[publication]/[rkey]/postPageState.ts
··· 1 + import { create } from "zustand"; 2 + import { flushSync } from "react-dom"; 3 + import { scrollIntoView } from "src/utils/scrollIntoView"; 4 + import { useParams, useSearchParams } from "next/navigation"; 5 + import { decodeQuotePosition } from "./quotePosition"; 6 + import { useEffect } from "react"; 7 + 8 + // Page types 9 + export type DocPage = { type: "doc"; id: string }; 10 + export type ThreadPage = { type: "thread"; uri: string }; 11 + export type QuotesPage = { type: "quotes"; uri: string }; 12 + export type OpenPage = DocPage | ThreadPage | QuotesPage; 13 + 14 + // Get a stable key for a page 15 + export const getPageKey = (page: OpenPage): string => { 16 + if (page.type === "doc") return page.id; 17 + if (page.type === "quotes") return `quotes:${page.uri}`; 18 + return `thread:${page.uri}`; 19 + }; 20 + 21 + const usePostPageUIState = create(() => ({ 22 + pages: [] as OpenPage[], 23 + initialized: false, 24 + })); 25 + 26 + export const useOpenPages = (): OpenPage[] => { 27 + const { quote } = useParams(); 28 + const state = usePostPageUIState((s) => s); 29 + const searchParams = useSearchParams(); 30 + const pageParam = searchParams.get("page"); 31 + 32 + if (!state.initialized) { 33 + // Check for page search param first (for comment links) 34 + if (pageParam) { 35 + return [{ type: "doc", id: pageParam }]; 36 + } 37 + // Then check for quote param 38 + if (quote) { 39 + const decodedQuote = decodeQuotePosition(quote as string); 40 + if (decodedQuote?.pageId) { 41 + return [{ type: "doc", id: decodedQuote.pageId }]; 42 + } 43 + } 44 + } 45 + 46 + return state.pages; 47 + }; 48 + 49 + export const useInitializeOpenPages = () => { 50 + const { quote } = useParams(); 51 + const searchParams = useSearchParams(); 52 + const pageParam = searchParams.get("page"); 53 + 54 + useEffect(() => { 55 + const state = usePostPageUIState.getState(); 56 + if (!state.initialized) { 57 + // Check for page search param first (for comment links) 58 + if (pageParam) { 59 + usePostPageUIState.setState({ 60 + pages: [{ type: "doc", id: pageParam }], 61 + initialized: true, 62 + }); 63 + return; 64 + } 65 + // Then check for quote param 66 + if (quote) { 67 + const decodedQuote = decodeQuotePosition(quote as string); 68 + if (decodedQuote?.pageId) { 69 + usePostPageUIState.setState({ 70 + pages: [{ type: "doc", id: decodedQuote.pageId }], 71 + initialized: true, 72 + }); 73 + return; 74 + } 75 + } 76 + // Mark as initialized even if no pageId found 77 + usePostPageUIState.setState({ initialized: true }); 78 + } 79 + }, [quote, pageParam]); 80 + }; 81 + 82 + export const openPage = ( 83 + parent: OpenPage | undefined, 84 + page: OpenPage, 85 + options?: { scrollIntoView?: boolean }, 86 + ) => { 87 + const pageKey = getPageKey(page); 88 + const parentKey = parent ? getPageKey(parent) : undefined; 89 + 90 + // Check if the page is already open 91 + const currentState = usePostPageUIState.getState(); 92 + const existingPageIndex = currentState.pages.findIndex( 93 + (p) => getPageKey(p) === pageKey, 94 + ); 95 + 96 + // If page is already open, just scroll to it 97 + if (existingPageIndex !== -1) { 98 + if (options?.scrollIntoView !== false) { 99 + scrollIntoView(`post-page-${pageKey}`); 100 + } 101 + return; 102 + } 103 + 104 + flushSync(() => { 105 + usePostPageUIState.setState((state) => { 106 + let parentPosition = state.pages.findIndex( 107 + (s) => getPageKey(s) === parentKey, 108 + ); 109 + // Close any pages after the parent and add the new page 110 + return { 111 + pages: 112 + parentPosition === -1 113 + ? [page] 114 + : [...state.pages.slice(0, parentPosition + 1), page], 115 + initialized: true, 116 + }; 117 + }); 118 + }); 119 + 120 + if (options?.scrollIntoView !== false) { 121 + // Use requestAnimationFrame to ensure the DOM has been painted before scrolling 122 + requestAnimationFrame(() => { 123 + scrollIntoView(`post-page-${pageKey}`); 124 + }); 125 + } 126 + }; 127 + 128 + export const closePage = (page: OpenPage) => { 129 + const pageKey = getPageKey(page); 130 + usePostPageUIState.setState((state) => { 131 + let parentPosition = state.pages.findIndex( 132 + (s) => getPageKey(s) === pageKey, 133 + ); 134 + return { 135 + pages: state.pages.slice(0, parentPosition), 136 + initialized: true, 137 + }; 138 + }); 139 + };