a tool for shared writing and social publishing

implement subpage interaction drawers and buttons

+107 -31
+11 -2
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 23 23 uri: string; 24 24 bsky_profiles: { record: Json } | null; 25 25 }; 26 - export function Comments(props: { document_uri: string; comments: Comment[] }) { 26 + export function Comments(props: { 27 + document_uri: string; 28 + comments: Comment[]; 29 + pageId?: string; 30 + }) { 27 31 let { identity } = useIdentityData(); 28 32 let { localComments } = useInteractionState(props.document_uri); 29 33 let comments = useMemo(() => { 30 - return [...localComments, ...props.comments]; 34 + return [ 35 + ...localComments.filter( 36 + (c) => (c.record as any)?.onPage === props.pageId, 37 + ), 38 + ...props.comments, 39 + ]; 31 40 }, [props.comments, localComments]); 32 41 let pathname = usePathname(); 33 42 let redirectRoute = useMemo(() => {
+28 -7
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 1 1 "use client"; 2 2 import { Media } from "components/Media"; 3 3 import { Quotes } from "./Quotes"; 4 - import { useInteractionState } from "./Interactions"; 4 + import { InteractionState, useInteractionState } from "./Interactions"; 5 5 import { Json } from "supabase/database.types"; 6 6 import { Comment, Comments } from "./Comments"; 7 7 import { useSearchParams } from "next/navigation"; 8 8 import { SandwichSpacer } from "components/LeafletLayout"; 9 + import { decodeQuotePosition } from "../quotePosition"; 9 10 10 11 export const InteractionDrawer = (props: { 11 12 document_uri: string; 12 13 quotes: { link: string; bsky_posts: { post_view: Json } | null }[]; 13 14 comments: Comment[]; 14 15 did: string; 16 + pageId?: string; 15 17 }) => { 16 18 let drawer = useDrawerOpen(props.document_uri); 17 19 if (!drawer) return null; 20 + 21 + // Filter comments and quotes based on pageId 22 + const filteredComments = props.comments.filter( 23 + (c) => (c.record as any)?.onPage === props.pageId, 24 + ); 25 + 26 + const filteredQuotes = props.pageId 27 + ? props.quotes.filter((q) => q.link.includes(props.pageId!)) 28 + : props.quotes.filter((q) => { 29 + const url = new URL(q.link); 30 + const quoteParam = url.pathname.split("/l-quote/")[1]; 31 + if (!quoteParam) return null; 32 + const quotePosition = decodeQuotePosition(quoteParam); 33 + return !quotePosition?.pageId; 34 + }); 35 + 18 36 return ( 19 37 <> 20 38 <SandwichSpacer noWidth /> 21 - <div className="snap-center h-full flex z-10 shrink-0 w-[calc(var(--page-width-units)-6px)] sm:w-[calc(var(--page-width-units)*.75)]"> 39 + <div className="snap-center h-full flex z-10 shrink-0 w-[calc(var(--page-width-units)-6px)] sm:w-[calc(var(--page-width-units))]"> 22 40 <div 23 41 id="interaction-drawer" 24 42 className="opaque-container rounded-l-none! rounded-r-lg! h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll -ml-[1px] " 25 43 > 26 - {drawer === "quotes" ? ( 27 - <Quotes {...props} /> 44 + {drawer.drawer === "quotes" ? ( 45 + <Quotes {...props} quotes={filteredQuotes} /> 28 46 ) : ( 29 47 <Comments 30 48 document_uri={props.document_uri} 31 - comments={props.comments} 49 + comments={filteredComments} 50 + pageId={props.pageId} 32 51 /> 33 52 )} 34 53 </div> ··· 40 59 export const useDrawerOpen = (uri: string) => { 41 60 let params = useSearchParams(); 42 61 let interactionDrawerSearchParam = params.get("interactionDrawer"); 43 - let { drawerOpen: open, drawer } = useInteractionState(uri); 62 + let { drawerOpen: open, drawer, pageId } = useInteractionState(uri); 44 63 if (open === false || (open === undefined && !interactionDrawerSearchParam)) 45 64 return null; 46 - return drawer || interactionDrawerSearchParam; 65 + drawer = 66 + drawer || (interactionDrawerSearchParam as InteractionState["drawer"]); 67 + return { drawer, pageId }; 47 68 };
+7 -4
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 10 10 import { PostPageContext } from "../PostPageContext"; 11 11 import { scrollIntoView } from "src/utils/scrollIntoView"; 12 12 13 - type InteractionState = { 13 + export type InteractionState = { 14 14 drawerOpen: undefined | boolean; 15 + pageId?: string; 15 16 drawer: undefined | "comments" | "quotes"; 16 17 localComments: Comment[]; 17 18 commentBox: { quote: QuotePosition | null }; ··· 84 85 export function openInteractionDrawer( 85 86 drawer: "comments" | "quotes", 86 87 document_uri: string, 88 + pageId?: string, 87 89 ) { 88 90 flushSync(() => { 89 - setInteractionState(document_uri, { drawerOpen: true, drawer }); 91 + setInteractionState(document_uri, { drawerOpen: true, drawer, pageId }); 90 92 }); 91 93 scrollIntoView("interaction-drawer"); 92 94 } ··· 97 99 compact?: boolean; 98 100 className?: string; 99 101 showComments?: boolean; 102 + pageId?: string; 100 103 }) => { 101 104 const data = useContext(PostPageContext); 102 105 const document_uri = data?.uri; ··· 113 116 className={`flex gap-1 items-center ${!props.compact && "px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline"}`} 114 117 onClick={() => { 115 118 if (!drawerOpen || drawer !== "quotes") 116 - openInteractionDrawer("quotes", document_uri); 119 + openInteractionDrawer("quotes", document_uri, props.pageId); 117 120 else setInteractionState(document_uri, { drawerOpen: false }); 118 121 }} 119 122 > ··· 130 133 className={`flex gap-1 items-center ${!props.compact && "px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline"}`} 131 134 onClick={() => { 132 135 if (!drawerOpen || drawer !== "comments") 133 - openInteractionDrawer("comments", document_uri); 136 + openInteractionDrawer("comments", document_uri, props.pageId); 134 137 else setInteractionState(document_uri, { drawerOpen: false }); 135 138 }} 136 139 >
+51 -15
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 1 1 "use client"; 2 2 import { 3 + PubLeafletComment, 3 4 PubLeafletDocument, 4 5 PubLeafletPagesLinearDocument, 5 6 PubLeafletPublication, ··· 119 120 preferences: { showComments?: boolean }; 120 121 }) { 121 122 let { identity } = useIdentityData(); 122 - let drawerOpen = useDrawerOpen(document_uri); 123 + let drawer = useDrawerOpen(document_uri); 123 124 useInitializeOpenPages(); 124 125 let pages = useOpenPages(); 125 126 if (!document || !document.documents_in_publications[0].publications) 126 127 return null; 127 128 128 129 let hasPageBackground = !!pubRecord.theme?.showPageBackground; 129 - let fullPageScroll = !hasPageBackground && !drawerOpen && pages.length === 0; 130 + let fullPageScroll = !hasPageBackground && !drawer && pages.length === 0; 130 131 let record = document.data as PubLeafletDocument.Record; 131 132 return ( 132 133 <> ··· 135 136 fullPageScroll={fullPageScroll} 136 137 cardBorderHidden={!hasPageBackground} 137 138 id={"post-page"} 138 - drawerOpen={!!drawerOpen} 139 + drawerOpen={!!drawer && !drawer.pageId} 139 140 > 140 141 <PostHeader 141 142 data={document} ··· 152 153 <Interactions 153 154 showComments={preferences.showComments} 154 155 quotesCount={document.document_mentions_in_bsky.length} 155 - commentsCount={document.comments_on_documents.length} 156 + commentsCount={ 157 + document.comments_on_documents.filter( 158 + (c) => !(c.record as PubLeafletComment.Record)?.onPage, 159 + ).length 160 + } 156 161 /> 157 162 <hr className="border-border-light mb-4 mt-4 sm:mx-4 mx-3" /> 158 163 <div className="pb-6 sm:px-4 px-3"> ··· 183 188 </div> 184 189 </PageWrapper> 185 190 186 - <InteractionDrawer 187 - document_uri={document.uri} 188 - comments={ 189 - pubRecord.preferences?.showComments === false 190 - ? [] 191 - : document.comments_on_documents 192 - } 193 - quotes={document.document_mentions_in_bsky} 194 - did={did} 195 - /> 191 + {drawer && !drawer.pageId && ( 192 + <InteractionDrawer 193 + document_uri={document.uri} 194 + comments={ 195 + pubRecord.preferences?.showComments === false 196 + ? [] 197 + : document.comments_on_documents 198 + } 199 + quotes={document.document_mentions_in_bsky} 200 + did={did} 201 + /> 202 + )} 196 203 197 204 {pages.map((p) => { 198 205 let page = record.pages.find( ··· 207 214 cardBorderHidden={!hasPageBackground} 208 215 id={`post-page-${p}`} 209 216 fullPageScroll={false} 210 - drawerOpen={!!drawerOpen} 217 + drawerOpen={!!drawer && drawer.pageId === page.id} 211 218 pageOptions={ 212 219 <PageOptions 213 220 onClick={() => closePage(page?.id!)} ··· 223 230 did={did} 224 231 prerenderedCodeBlocks={prerenderedCodeBlocks} 225 232 /> 233 + <Interactions 234 + pageId={page.id} 235 + showComments={preferences.showComments} 236 + quotesCount={ 237 + document.document_mentions_in_bsky.filter((q) => 238 + q.link.includes(page.id!), 239 + ).length 240 + } 241 + commentsCount={ 242 + document.comments_on_documents.filter( 243 + (c) => 244 + (c.record as PubLeafletComment.Record)?.onPage === 245 + page.id, 246 + ).length 247 + } 248 + /> 226 249 </PageWrapper> 250 + {drawer && drawer.pageId === page.id && ( 251 + <InteractionDrawer 252 + pageId={page.id} 253 + document_uri={document.uri} 254 + comments={ 255 + pubRecord.preferences?.showComments === false 256 + ? [] 257 + : document.comments_on_documents 258 + } 259 + quotes={document.document_mentions_in_bsky} 260 + did={did} 261 + /> 262 + )} 227 263 </Fragment> 228 264 ); 229 265 })}
+1
app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx
··· 212 212 setInteractionState(document_uri, { 213 213 drawer: "comments", 214 214 drawerOpen: true, 215 + pageId: position.pageId, 215 216 commentBox: { quote: position }, 216 217 }), 217 218 );
+3 -1
lexicons/api/lexicons.ts
··· 1325 1325 ref: 'lex:pub.leaflet.richtext.facet', 1326 1326 }, 1327 1327 }, 1328 + onPage: { 1329 + type: 'string', 1330 + }, 1328 1331 attachment: { 1329 1332 type: 'union', 1330 1333 refs: ['lex:pub.leaflet.comment#linearDocumentQuote'], ··· 1541 1544 }, 1542 1545 base_path: { 1543 1546 type: 'string', 1544 - format: 'uri', 1545 1547 }, 1546 1548 description: { 1547 1549 type: 'string',
+1
lexicons/api/types/pub/leaflet/comment.ts
··· 19 19 reply?: ReplyRef 20 20 plaintext: string 21 21 facets?: PubLeafletRichtextFacet.Main[] 22 + onPage?: string 22 23 attachment?: $Typed<LinearDocumentQuote> | { $type: string } 23 24 [k: string]: unknown 24 25 }
+3
lexicons/pub/leaflet/comment.json
··· 38 38 "ref": "pub.leaflet.richtext.facet" 39 39 } 40 40 }, 41 + "onPage": { 42 + "type": "string" 43 + }, 41 44 "attachment": { 42 45 "type": "union", 43 46 "refs": [
+1 -2
lexicons/pub/leaflet/publication.json
··· 17 17 "maxLength": 2000 18 18 }, 19 19 "base_path": { 20 - "type": "string", 21 - "format": "uri" 20 + "type": "string" 22 21 }, 23 22 "description": { 24 23 "type": "string",
+1
lexicons/src/comment.ts
··· 23 23 type: "array", 24 24 items: { type: "ref", ref: PubLeafletRichTextFacet.id }, 25 25 }, 26 + onPage: { type: "string" }, 26 27 attachment: { type: "union", refs: ["#linearDocumentQuote"] }, 27 28 }, 28 29 },