a tool for shared writing and social publishing

Compare changes

Choose any two refs to compare.

+2
actions/publishToPublication.ts
··· 784 root_entity, 785 "theme/background-image-repeat", 786 )?.[0]; 787 788 let theme: PubLeafletPublication.Theme = { 789 showPageBackground: showPageBackground ?? true, 790 }; 791 792 if (pageBackground) 793 theme.backgroundColor = ColorToRGBA(parseColor(`hsba(${pageBackground})`)); 794 if (cardBackground)
··· 784 root_entity, 785 "theme/background-image-repeat", 786 )?.[0]; 787 + let pageWidth = scan.eav(root_entity, "theme/page-width")?.[0]; 788 789 let theme: PubLeafletPublication.Theme = { 790 showPageBackground: showPageBackground ?? true, 791 }; 792 793 + if (pageWidth) theme.pageWidth = pageWidth.data.value; 794 if (pageBackground) 795 theme.backgroundColor = ColorToRGBA(parseColor(`hsba(${pageBackground})`)); 796 if (cardBackground)
+2 -2
app/[leaflet_id]/actions/PublishButton.tsx
··· 136 content: ( 137 <div> 138 {pub.doc ? "Updated! " : "Published! "} 139 - <SpeedyLink className="underline font-bold" href={docUrl}> 140 - See Post 141 </SpeedyLink> 142 </div> 143 ),
··· 136 content: ( 137 <div> 138 {pub.doc ? "Updated! " : "Published! "} 139 + <SpeedyLink className="underline" href={docUrl}> 140 + See Published Post 141 </SpeedyLink> 142 </div> 143 ),
+6 -1
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 202 isSubpage: boolean | undefined; 203 data: PostPageData; 204 profile: ProfileViewDetailed; 205 - preferences: { showComments?: boolean }; 206 quotesCount: number | undefined; 207 commentsCount: number | undefined; 208 }) => { ··· 213 quotesCount={props.quotesCount || 0} 214 commentsCount={props.commentsCount || 0} 215 showComments={props.preferences.showComments} 216 pageId={props.pageId} 217 /> 218 {!props.isSubpage && (
··· 202 isSubpage: boolean | undefined; 203 data: PostPageData; 204 profile: ProfileViewDetailed; 205 + preferences: { 206 + showComments?: boolean; 207 + showMentions?: boolean; 208 + showPrevNext?: boolean; 209 + }; 210 quotesCount: number | undefined; 211 commentsCount: number | undefined; 212 }) => { ··· 217 quotesCount={props.quotesCount || 0} 218 commentsCount={props.commentsCount || 0} 219 showComments={props.preferences.showComments} 220 + showMentions={props.preferences.showMentions} 221 pageId={props.pageId} 222 /> 223 {!props.isSubpage && (
+4 -1
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 51 }, []); 52 53 return ( 54 - <div id={"commentsDrawer"} className="flex flex-col gap-2 relative"> 55 <div className="w-full flex justify-between text-secondary font-bold"> 56 Comments 57 <button
··· 51 }, []); 52 53 return ( 54 + <div 55 + id={"commentsDrawer"} 56 + className="flex flex-col gap-2 relative text-sm text-secondary" 57 + > 58 <div className="w-full flex justify-between text-secondary font-bold"> 59 Comments 60 <button
+2 -1
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 9 import { decodeQuotePosition } from "../quotePosition"; 10 11 export const InteractionDrawer = (props: { 12 document_uri: string; 13 quotesAndMentions: { uri: string; link?: string }[]; 14 comments: Comment[]; ··· 38 <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))]"> 39 <div 40 id="interaction-drawer" 41 - 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] " 42 > 43 {drawer.drawer === "quotes" ? ( 44 <Quotes {...props} quotesAndMentions={filteredQuotesAndMentions} />
··· 9 import { decodeQuotePosition } from "../quotePosition"; 10 11 export const InteractionDrawer = (props: { 12 + showPageBackground: boolean | undefined; 13 document_uri: string; 14 quotesAndMentions: { uri: string; link?: string }[]; 15 comments: Comment[]; ··· 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))]"> 40 <div 41 id="interaction-drawer" 42 + className={`opaque-container h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll -ml-[1px] ${props.showPageBackground ? "rounded-l-none! rounded-r-lg!" : "rounded-lg! sm:mx-2"}`} 43 > 44 {drawer.drawer === "quotes" ? ( 45 <Quotes {...props} quotesAndMentions={filteredQuotesAndMentions} />
+68 -44
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 108 commentsCount: number; 109 className?: string; 110 showComments?: boolean; 111 pageId?: string; 112 }) => { 113 const data = useContext(PostPageContext); ··· 131 <div className={`flex gap-2 text-tertiary text-sm ${props.className}`}> 132 {tagCount > 0 && <TagPopover tags={tags} tagCount={tagCount} />} 133 134 - {props.quotesCount > 0 && ( 135 <button 136 className="flex w-fit gap-2 items-center" 137 onClick={() => { ··· 168 commentsCount: number; 169 className?: string; 170 showComments?: boolean; 171 pageId?: string; 172 }) => { 173 const data = useContext(PostPageContext); ··· 189 const tags = (data?.data as any)?.tags as string[] | undefined; 190 const tagCount = tags?.length || 0; 191 192 let subscribed = 193 identity?.atp_did && 194 publication?.publication_subscriptions && ··· 229 <TagList tags={tags} className="mb-3" /> 230 </> 231 )} 232 <hr className="border-border-light mb-3 " /> 233 <div className="flex gap-2 justify-between"> 234 - <div className="flex gap-2"> 235 - {props.quotesCount > 0 && ( 236 - <button 237 - className="flex w-fit gap-2 items-center px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 238 - onClick={() => { 239 - if (!drawerOpen || drawer !== "quotes") 240 - openInteractionDrawer("quotes", document_uri, props.pageId); 241 - else setInteractionState(document_uri, { drawerOpen: false }); 242 - }} 243 - onMouseEnter={handleQuotePrefetch} 244 - onTouchStart={handleQuotePrefetch} 245 - aria-label="Post quotes" 246 - > 247 - <QuoteTiny aria-hidden /> {props.quotesCount}{" "} 248 - <span 249 - aria-hidden 250 - >{`Mention${props.quotesCount === 1 ? "" : "s"}`}</span> 251 - </button> 252 - )} 253 - {props.showComments === false ? null : ( 254 - <button 255 - className="flex gap-2 items-center w-fit px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 256 - onClick={() => { 257 - if ( 258 - !drawerOpen || 259 - drawer !== "comments" || 260 - pageId !== props.pageId 261 - ) 262 - openInteractionDrawer("comments", document_uri, props.pageId); 263 - else setInteractionState(document_uri, { drawerOpen: false }); 264 - }} 265 - aria-label="Post comments" 266 - > 267 - <CommentTiny aria-hidden />{" "} 268 - {props.commentsCount > 0 ? ( 269 - <span aria-hidden> 270 - {`${props.commentsCount} Comment${props.commentsCount === 1 ? "" : "s"}`} 271 - </span> 272 - ) : ( 273 - "Comment" 274 )} 275 - </button> 276 - )} 277 - </div> 278 <EditButton document={data} /> 279 {subscribed && publication && ( 280 <ManageSubscription
··· 108 commentsCount: number; 109 className?: string; 110 showComments?: boolean; 111 + showMentions?: boolean; 112 pageId?: string; 113 }) => { 114 const data = useContext(PostPageContext); ··· 132 <div className={`flex gap-2 text-tertiary text-sm ${props.className}`}> 133 {tagCount > 0 && <TagPopover tags={tags} tagCount={tagCount} />} 134 135 + {props.quotesCount === 0 || props.showMentions === false ? null : ( 136 <button 137 className="flex w-fit gap-2 items-center" 138 onClick={() => { ··· 169 commentsCount: number; 170 className?: string; 171 showComments?: boolean; 172 + showMentions?: boolean; 173 pageId?: string; 174 }) => { 175 const data = useContext(PostPageContext); ··· 191 const tags = (data?.data as any)?.tags as string[] | undefined; 192 const tagCount = tags?.length || 0; 193 194 + let noInteractions = !props.showComments && !props.showMentions; 195 + 196 let subscribed = 197 identity?.atp_did && 198 publication?.publication_subscriptions && ··· 233 <TagList tags={tags} className="mb-3" /> 234 </> 235 )} 236 + 237 <hr className="border-border-light mb-3 " /> 238 + 239 <div className="flex gap-2 justify-between"> 240 + {noInteractions ? ( 241 + <div /> 242 + ) : ( 243 + <> 244 + <div className="flex gap-2"> 245 + {props.quotesCount === 0 || 246 + props.showMentions === false ? null : ( 247 + <button 248 + className="flex w-fit gap-2 items-center px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 249 + onClick={() => { 250 + if (!drawerOpen || drawer !== "quotes") 251 + openInteractionDrawer( 252 + "quotes", 253 + document_uri, 254 + props.pageId, 255 + ); 256 + else 257 + setInteractionState(document_uri, { drawerOpen: false }); 258 + }} 259 + onMouseEnter={handleQuotePrefetch} 260 + onTouchStart={handleQuotePrefetch} 261 + aria-label="Post quotes" 262 + > 263 + <QuoteTiny aria-hidden /> {props.quotesCount}{" "} 264 + <span 265 + aria-hidden 266 + >{`Mention${props.quotesCount === 1 ? "" : "s"}`}</span> 267 + </button> 268 )} 269 + {props.showComments === false ? null : ( 270 + <button 271 + className="flex gap-2 items-center w-fit px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 272 + onClick={() => { 273 + if ( 274 + !drawerOpen || 275 + drawer !== "comments" || 276 + pageId !== props.pageId 277 + ) 278 + openInteractionDrawer( 279 + "comments", 280 + document_uri, 281 + props.pageId, 282 + ); 283 + else 284 + setInteractionState(document_uri, { drawerOpen: false }); 285 + }} 286 + aria-label="Post comments" 287 + > 288 + <CommentTiny aria-hidden />{" "} 289 + {props.commentsCount > 0 ? ( 290 + <span aria-hidden> 291 + {`${props.commentsCount} Comment${props.commentsCount === 1 ? "" : "s"}`} 292 + </span> 293 + ) : ( 294 + "Comment" 295 + )} 296 + </button> 297 + )} 298 + </div> 299 + </> 300 + )} 301 + 302 <EditButton document={data} /> 303 {subscribed && publication && ( 304 <ManageSubscription
+7 -2
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 14 ExpandedInteractions, 15 getCommentCount, 16 getQuoteCount, 17 - Interactions, 18 } from "./Interactions/Interactions"; 19 import { PostContent } from "./PostContent"; 20 import { PostHeader } from "./PostHeader/PostHeader"; ··· 25 import { decodeQuotePosition } from "./quotePosition"; 26 import { PollData } from "./fetchPollData"; 27 import { SharedPageProps } from "./PostPages"; 28 29 export function LinearDocumentPage({ 30 blocks, ··· 56 57 const isSubpage = !!pageId; 58 59 return ( 60 <> 61 <PageWrapper ··· 83 did={did} 84 prerenderedCodeBlocks={prerenderedCodeBlocks} 85 /> 86 - 87 <ExpandedInteractions 88 pageId={pageId} 89 showComments={preferences.showComments} 90 commentsCount={getCommentCount(document, pageId) || 0} 91 quotesCount={getQuoteCount(document, pageId) || 0} 92 />
··· 14 ExpandedInteractions, 15 getCommentCount, 16 getQuoteCount, 17 } from "./Interactions/Interactions"; 18 import { PostContent } from "./PostContent"; 19 import { PostHeader } from "./PostHeader/PostHeader"; ··· 24 import { decodeQuotePosition } from "./quotePosition"; 25 import { PollData } from "./fetchPollData"; 26 import { SharedPageProps } from "./PostPages"; 27 + import { PostPrevNextButtons } from "./PostPrevNextButtons"; 28 29 export function LinearDocumentPage({ 30 blocks, ··· 56 57 const isSubpage = !!pageId; 58 59 + console.log("prev/next?: " + preferences.showPrevNext); 60 + 61 return ( 62 <> 63 <PageWrapper ··· 85 did={did} 86 prerenderedCodeBlocks={prerenderedCodeBlocks} 87 /> 88 + <PostPrevNextButtons 89 + showPrevNext={preferences.showPrevNext && !isSubpage} 90 + /> 91 <ExpandedInteractions 92 pageId={pageId} 93 showComments={preferences.showComments} 94 + showMentions={preferences.showMentions} 95 commentsCount={getCommentCount(document, pageId) || 0} 96 quotesCount={getQuoteCount(document, pageId) || 0} 97 />
+2 -1
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 23 export function PostHeader(props: { 24 data: PostPageData; 25 profile: ProfileViewDetailed; 26 - preferences: { showComments?: boolean }; 27 }) { 28 let { identity } = useIdentityData(); 29 let document = props.data; ··· 91 </div> 92 <Interactions 93 showComments={props.preferences.showComments} 94 quotesCount={getQuoteCount(document) || 0} 95 commentsCount={getCommentCount(document) || 0} 96 />
··· 23 export function PostHeader(props: { 24 data: PostPageData; 25 profile: ProfileViewDetailed; 26 + preferences: { showComments?: boolean; showMentions?: boolean }; 27 }) { 28 let { identity } = useIdentityData(); 29 let document = props.data; ··· 91 </div> 92 <Interactions 93 showComments={props.preferences.showComments} 94 + showMentions={props.preferences.showMentions} 95 quotesCount={getQuoteCount(document) || 0} 96 commentsCount={getCommentCount(document) || 0} 97 />
+22 -4
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 147 document: PostPageData; 148 did: string; 149 profile: ProfileViewDetailed; 150 - preferences: { showComments?: boolean }; 151 pubRecord?: PubLeafletPublication.Record; 152 theme?: PubLeafletPublication.Theme | null; 153 prerenderedCodeBlocks?: Map<string, string>; ··· 206 did: string; 207 prerenderedCodeBlocks?: Map<string, string>; 208 bskyPostData: AppBskyFeedDefs.PostView[]; 209 - preferences: { showComments?: boolean }; 210 pollData: PollData[]; 211 }) { 212 let drawer = useDrawerOpen(document_uri); ··· 261 262 {drawer && !drawer.pageId && ( 263 <InteractionDrawer 264 document_uri={document.uri} 265 comments={ 266 pubRecord?.preferences?.showComments === false 267 ? [] 268 : document.comments_on_documents 269 } 270 - quotesAndMentions={quotesAndMentions} 271 did={did} 272 /> 273 )} ··· 347 /> 348 {drawer && drawer.pageId === page.id && ( 349 <InteractionDrawer 350 pageId={page.id} 351 document_uri={document.uri} 352 comments={ ··· 354 ? [] 355 : document.comments_on_documents 356 } 357 - quotesAndMentions={quotesAndMentions} 358 did={did} 359 /> 360 )}
··· 147 document: PostPageData; 148 did: string; 149 profile: ProfileViewDetailed; 150 + preferences: { 151 + showComments?: boolean; 152 + showMentions?: boolean; 153 + showPrevNext?: boolean; 154 + }; 155 pubRecord?: PubLeafletPublication.Record; 156 theme?: PubLeafletPublication.Theme | null; 157 prerenderedCodeBlocks?: Map<string, string>; ··· 210 did: string; 211 prerenderedCodeBlocks?: Map<string, string>; 212 bskyPostData: AppBskyFeedDefs.PostView[]; 213 + preferences: { 214 + showComments?: boolean; 215 + showMentions?: boolean; 216 + showPrevNext?: boolean; 217 + }; 218 pollData: PollData[]; 219 }) { 220 let drawer = useDrawerOpen(document_uri); ··· 269 270 {drawer && !drawer.pageId && ( 271 <InteractionDrawer 272 + showPageBackground={pubRecord?.theme?.showPageBackground} 273 document_uri={document.uri} 274 comments={ 275 pubRecord?.preferences?.showComments === false 276 ? [] 277 : document.comments_on_documents 278 } 279 + quotesAndMentions={ 280 + pubRecord?.preferences?.showMentions === false 281 + ? [] 282 + : quotesAndMentions 283 + } 284 did={did} 285 /> 286 )} ··· 360 /> 361 {drawer && drawer.pageId === page.id && ( 362 <InteractionDrawer 363 + showPageBackground={pubRecord?.theme?.showPageBackground} 364 pageId={page.id} 365 document_uri={document.uri} 366 comments={ ··· 368 ? [] 369 : document.comments_on_documents 370 } 371 + quotesAndMentions={ 372 + pubRecord?.preferences?.showMentions === false 373 + ? [] 374 + : quotesAndMentions 375 + } 376 did={did} 377 /> 378 )}
+58
app/lish/[did]/[publication]/[rkey]/PostPrevNextButtons.tsx
···
··· 1 + "use client"; 2 + import { PubLeafletDocument } from "lexicons/api"; 3 + import { usePublicationData } from "../dashboard/PublicationSWRProvider"; 4 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 5 + import { AtUri } from "@atproto/api"; 6 + import { useParams } from "next/navigation"; 7 + import { getPostPageData } from "./getPostPageData"; 8 + import { PostPageContext } from "./PostPageContext"; 9 + import { useContext } from "react"; 10 + import { SpeedyLink } from "components/SpeedyLink"; 11 + import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 12 + 13 + export const PostPrevNextButtons = (props: { 14 + showPrevNext: boolean | undefined; 15 + }) => { 16 + let postData = useContext(PostPageContext); 17 + let pub = postData?.documents_in_publications[0]?.publications; 18 + 19 + if (!props.showPrevNext || !pub || !postData) return; 20 + 21 + function getPostLink(uri: string) { 22 + return pub && uri 23 + ? `${getPublicationURL(pub)}/${new AtUri(uri).rkey}` 24 + : "leaflet.pub/not-found"; 25 + } 26 + let prevPost = postData?.prevNext?.prev; 27 + let nextPost = postData?.prevNext?.next; 28 + 29 + return ( 30 + <div className="flex flex-col gap-1 w-full px-3 sm:px-4 pb-2 pt-2"> 31 + {/*<hr className="border-border-light" />*/} 32 + <div className="flex justify-between w-full gap-8 "> 33 + {nextPost ? ( 34 + <SpeedyLink 35 + href={getPostLink(nextPost.uri)} 36 + className="flex gap-1 items-center truncate min-w-0 basis-1/2" 37 + > 38 + <ArrowRightTiny className="rotate-180 shrink-0" /> 39 + <div className="min-w-0 truncate">{nextPost.title}</div> 40 + </SpeedyLink> 41 + ) : ( 42 + <div /> 43 + )} 44 + {prevPost ? ( 45 + <SpeedyLink 46 + href={getPostLink(prevPost.uri)} 47 + className="flex gap-1 items-center truncate min-w-0 basis-1/2 justify-end" 48 + > 49 + <div className="min-w-0 truncate">{prevPost.title}</div> 50 + <ArrowRightTiny className="shrink-0" /> 51 + </SpeedyLink> 52 + ) : ( 53 + <div /> 54 + )} 55 + </div> 56 + </div> 57 + ); 58 + };
+3 -2
app/lish/[did]/[publication]/[rkey]/QuoteHandler.tsx
··· 186 <BlueskyLinkTiny className="shrink-0" /> 187 Bluesky 188 </a> 189 - <Separator classname="h-4" /> 190 <button 191 id="copy-quote-link" 192 className="flex gap-1 items-center hover:font-bold px-1" ··· 211 </button> 212 {pubRecord?.preferences?.showComments !== false && identity?.atp_did && ( 213 <> 214 - <Separator classname="h-4" /> 215 <button 216 className="flex gap-1 items-center hover:font-bold px-1" 217 onClick={() => {
··· 186 <BlueskyLinkTiny className="shrink-0" /> 187 Bluesky 188 </a> 189 + <Separator classname="h-4!" /> 190 <button 191 id="copy-quote-link" 192 className="flex gap-1 items-center hover:font-bold px-1" ··· 211 </button> 212 {pubRecord?.preferences?.showComments !== false && identity?.atp_did && ( 213 <> 214 + <Separator classname="h-4! " /> 215 + 216 <button 217 className="flex gap-1 items-center hover:font-bold px-1" 218 onClick={() => {
+58 -1
app/lish/[did]/[publication]/[rkey]/getPostPageData.ts
··· 10 data, 11 uri, 12 comments_on_documents(*, bsky_profiles(*)), 13 - documents_in_publications(publications(*, publication_subscriptions(*))), 14 document_mentions_in_bsky(*), 15 leaflets_in_publications(*) 16 `, ··· 51 ?.record as PubLeafletPublication.Record 52 )?.theme || (document?.data as PubLeafletDocument.Record)?.theme; 53 54 return { 55 ...document, 56 quotesAndMentions, 57 theme, 58 }; 59 } 60
··· 10 data, 11 uri, 12 comments_on_documents(*, bsky_profiles(*)), 13 + documents_in_publications(publications(*, 14 + documents_in_publications(documents(uri, data)), 15 + publication_subscriptions(*)) 16 + ), 17 document_mentions_in_bsky(*), 18 leaflets_in_publications(*) 19 `, ··· 54 ?.record as PubLeafletPublication.Record 55 )?.theme || (document?.data as PubLeafletDocument.Record)?.theme; 56 57 + // Calculate prev/next documents from the fetched publication documents 58 + let prevNext: 59 + | { 60 + prev?: { uri: string; title: string }; 61 + next?: { uri: string; title: string }; 62 + } 63 + | undefined; 64 + 65 + const currentPublishedAt = (document.data as PubLeafletDocument.Record) 66 + ?.publishedAt; 67 + const allDocs = 68 + document.documents_in_publications[0]?.publications 69 + ?.documents_in_publications; 70 + 71 + if (currentPublishedAt && allDocs) { 72 + // Filter and sort documents by publishedAt 73 + const sortedDocs = allDocs 74 + .map((dip) => ({ 75 + uri: dip?.documents?.uri, 76 + title: (dip?.documents?.data as PubLeafletDocument.Record).title, 77 + publishedAt: (dip?.documents?.data as PubLeafletDocument.Record) 78 + .publishedAt, 79 + })) 80 + .filter((doc) => doc.publishedAt) // Only include docs with publishedAt 81 + .sort( 82 + (a, b) => 83 + new Date(a.publishedAt!).getTime() - 84 + new Date(b.publishedAt!).getTime(), 85 + ); 86 + 87 + // Find current document index 88 + const currentIndex = sortedDocs.findIndex((doc) => doc.uri === uri); 89 + 90 + if (currentIndex !== -1) { 91 + prevNext = { 92 + prev: 93 + currentIndex > 0 94 + ? { 95 + uri: sortedDocs[currentIndex - 1].uri || "", 96 + title: sortedDocs[currentIndex - 1].title, 97 + } 98 + : undefined, 99 + next: 100 + currentIndex < sortedDocs.length - 1 101 + ? { 102 + uri: sortedDocs[currentIndex + 1].uri || "", 103 + title: sortedDocs[currentIndex + 1].title, 104 + } 105 + : undefined, 106 + }; 107 + } 108 + } 109 + 110 return { 111 ...document, 112 quotesAndMentions, 113 theme, 114 + prevNext, 115 }; 116 } 117
+1
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 140 commentsCount={comments} 141 tags={tags} 142 showComments={pubRecord?.preferences?.showComments} 143 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 144 /> 145 </div>
··· 140 commentsCount={comments} 141 tags={tags} 142 showComments={pubRecord?.preferences?.showComments} 143 + showMentions={pubRecord?.preferences?.showMentions} 144 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 145 /> 146 </div>
+31 -25
app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx
··· 22 ? true 23 : record.preferences.showComments, 24 ); 25 - let [showMentions, setShowMentions] = useState(true); 26 - let [showPrevNext, setShowPrevNext] = useState(true); 27 28 let toast = useToaster(); 29 return ( 30 <form 31 onSubmit={async (e) => { 32 - // if (!pubData) return; 33 - // e.preventDefault(); 34 - // props.setLoading(true); 35 - // let data = await updatePublication({ 36 - // uri: pubData.uri, 37 - // name: nameValue, 38 - // description: descriptionValue, 39 - // iconFile: iconFile, 40 - // preferences: { 41 - // showInDiscover: showInDiscover, 42 - // showComments: showComments, 43 - // }, 44 - // }); 45 - // toast({ type: "success", content: "Posts Updated!" }); 46 - // props.setLoading(false); 47 - // mutate("publication-data"); 48 }} 49 className="text-primary flex flex-col" 50 > ··· 57 Post Options 58 </PubSettingsHeader> 59 <h4 className="mb-1">Layout</h4> 60 - {/*<div>Max Post Width</div>*/} 61 <Toggle 62 toggle={showPrevNext} 63 onToggle={() => { 64 setShowPrevNext(!showPrevNext); 65 }} 66 > 67 - <div className="flex flex-col justify-start"> 68 - <div className="font-bold">Show Prev/Next Buttons</div> 69 - <div className="text-tertiary text-sm leading-tight"> 70 - Show buttons that navigate to the previous and next posts 71 - </div> 72 - </div> 73 </Toggle> 74 <hr className="my-2 border-border-light" /> 75 <h4 className="mb-1">Interactions</h4>
··· 22 ? true 23 : record.preferences.showComments, 24 ); 25 + let [showMentions, setShowMentions] = useState( 26 + record?.preferences?.showMentions === undefined 27 + ? true 28 + : record.preferences.showMentions, 29 + ); 30 + let [showPrevNext, setShowPrevNext] = useState( 31 + record?.preferences?.showPrevNext === undefined 32 + ? true 33 + : record.preferences.showPrevNext, 34 + ); 35 36 let toast = useToaster(); 37 return ( 38 <form 39 onSubmit={async (e) => { 40 + if (!pubData) return; 41 + e.preventDefault(); 42 + props.setLoading(true); 43 + let data = await updatePublication({ 44 + name: record.name, 45 + uri: pubData.uri, 46 + preferences: { 47 + showInDiscover: 48 + record?.preferences?.showInDiscover === undefined 49 + ? true 50 + : record.preferences.showInDiscover, 51 + showComments: showComments, 52 + showMentions: showMentions, 53 + showPrevNext: showPrevNext, 54 + }, 55 + }); 56 + toast({ type: "success", content: <strong>Posts Updated!</strong> }); 57 + console.log(record.preferences?.showPrevNext); 58 + props.setLoading(false); 59 + mutate("publication-data"); 60 }} 61 className="text-primary flex flex-col" 62 > ··· 69 Post Options 70 </PubSettingsHeader> 71 <h4 className="mb-1">Layout</h4> 72 <Toggle 73 toggle={showPrevNext} 74 onToggle={() => { 75 setShowPrevNext(!showPrevNext); 76 }} 77 > 78 + <div className="font-bold">Show Prev/Next Buttons</div> 79 </Toggle> 80 <hr className="my-2 border-border-light" /> 81 <h4 className="mb-1">Interactions</h4>
+2 -2
app/lish/[did]/[publication]/dashboard/settings/PublicationSettings.tsx
··· 103 Theme and Layout 104 <ArrowRightTiny /> 105 </button> 106 - {/*<button 107 className={menuItemClassName} 108 type="button" 109 onClick={() => props.setState("post-options")} 110 > 111 Post Options 112 <ArrowRightTiny /> 113 - </button>*/} 114 </div> 115 ); 116 };
··· 103 Theme and Layout 104 <ArrowRightTiny /> 105 </button> 106 + <button 107 className={menuItemClassName} 108 type="button" 109 onClick={() => props.setState("post-options")} 110 > 111 Post Options 112 <ArrowRightTiny /> 113 + </button> 114 </div> 115 ); 116 };
+1
app/lish/[did]/[publication]/page.tsx
··· 172 tags={tags} 173 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 174 showComments={record?.preferences?.showComments} 175 /> 176 </div> 177 </div>
··· 172 tags={tags} 173 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 174 showComments={record?.preferences?.showComments} 175 + showMentions={record?.preferences?.showMentions} 176 /> 177 </div> 178 </div>
+9 -2
app/lish/createPub/CreatePubForm.tsx
··· 53 description: descriptionValue, 54 iconFile: logoFile, 55 subdomain: domainValue, 56 - preferences: { showInDiscover, showComments: true }, 57 }); 58 59 if (!result.success) { ··· 68 setTimeout(() => { 69 setFormState("normal"); 70 if (result.publication) 71 - router.push(`${getBasePublicationURL(result.publication)}/dashboard`); 72 }, 500); 73 }} 74 >
··· 53 description: descriptionValue, 54 iconFile: logoFile, 55 subdomain: domainValue, 56 + preferences: { 57 + showInDiscover, 58 + showComments: true, 59 + showMentions: true, 60 + showPrevNext: false, 61 + }, 62 }); 63 64 if (!result.success) { ··· 73 setTimeout(() => { 74 setFormState("normal"); 75 if (result.publication) 76 + router.push( 77 + `${getBasePublicationURL(result.publication)}/dashboard`, 78 + ); 79 }, 500); 80 }} 81 >
+19 -14
app/lish/createPub/UpdatePubForm.tsx
··· 21 import { Checkbox } from "components/Checkbox"; 22 import type { GetDomainConfigResponseBody } from "@vercel/sdk/esm/models/getdomainconfigop"; 23 import { PubSettingsHeader } from "../[did]/[publication]/dashboard/settings/PublicationSettings"; 24 25 export const EditPubForm = (props: { 26 backToMenuAction: () => void; ··· 43 ? true 44 : record.preferences.showComments, 45 ); 46 let [descriptionValue, setDescriptionValue] = useState( 47 record?.description || "", 48 ); ··· 74 preferences: { 75 showInDiscover: showInDiscover, 76 showComments: showComments, 77 }, 78 }); 79 toast({ type: "success", content: "Updated!" }); ··· 90 General Settings 91 </PubSettingsHeader> 92 <div className="flex flex-col gap-3 w-[1000px] max-w-full pb-2"> 93 - <div className="flex items-center justify-between gap-2 "> 94 <p className="pl-0.5 pb-0.5 text-tertiary italic text-sm font-bold"> 95 Logo <span className="font-normal">(optional)</span> 96 </p> ··· 160 <CustomDomainForm /> 161 <hr className="border-border-light" /> 162 163 - <Checkbox 164 - checked={showInDiscover} 165 - onChange={(e) => setShowInDiscover(e.target.checked)} 166 > 167 - <div className=" pt-0.5 flex flex-col text-sm italic text-tertiary "> 168 <p className="font-bold"> 169 Show In{" "} 170 <a href="/discover" target="_blank"> ··· 179 page. You can change this at any time! 180 </p> 181 </div> 182 - </Checkbox> 183 184 - <Checkbox 185 - checked={showComments} 186 - onChange={(e) => setShowComments(e.target.checked)} 187 - > 188 - <div className=" pt-0.5 flex flex-col text-sm italic text-tertiary "> 189 - <p className="font-bold">Show comments on posts</p> 190 - </div> 191 - </Checkbox> 192 </div> 193 </form> 194 );
··· 21 import { Checkbox } from "components/Checkbox"; 22 import type { GetDomainConfigResponseBody } from "@vercel/sdk/esm/models/getdomainconfigop"; 23 import { PubSettingsHeader } from "../[did]/[publication]/dashboard/settings/PublicationSettings"; 24 + import { Toggle } from "components/Toggle"; 25 26 export const EditPubForm = (props: { 27 backToMenuAction: () => void; ··· 44 ? true 45 : record.preferences.showComments, 46 ); 47 + let showMentions = 48 + record?.preferences?.showMentions === undefined 49 + ? true 50 + : record.preferences.showMentions; 51 + let showPrevNext = 52 + record?.preferences?.showPrevNext === undefined 53 + ? true 54 + : record.preferences.showPrevNext; 55 + 56 let [descriptionValue, setDescriptionValue] = useState( 57 record?.description || "", 58 ); ··· 84 preferences: { 85 showInDiscover: showInDiscover, 86 showComments: showComments, 87 + showMentions: showMentions, 88 + showPrevNext: showPrevNext, 89 }, 90 }); 91 toast({ type: "success", content: "Updated!" }); ··· 102 General Settings 103 </PubSettingsHeader> 104 <div className="flex flex-col gap-3 w-[1000px] max-w-full pb-2"> 105 + <div className="flex items-center justify-between gap-2 mt-2 "> 106 <p className="pl-0.5 pb-0.5 text-tertiary italic text-sm font-bold"> 107 Logo <span className="font-normal">(optional)</span> 108 </p> ··· 172 <CustomDomainForm /> 173 <hr className="border-border-light" /> 174 175 + <Toggle 176 + toggle={showInDiscover} 177 + onToggle={() => setShowInDiscover(!showInDiscover)} 178 > 179 + <div className=" pt-0.5 flex flex-col text-sm text-tertiary "> 180 <p className="font-bold"> 181 Show In{" "} 182 <a href="/discover" target="_blank"> ··· 191 page. You can change this at any time! 192 </p> 193 </div> 194 + </Toggle> 195 196 + 197 </div> 198 </form> 199 );
+2 -2
app/lish/createPub/updatePublication.ts
··· 25 }: { 26 uri: string; 27 name: string; 28 - description: string; 29 - iconFile: File | null; 30 preferences?: Omit<PubLeafletPublication.Preferences, "$type">; 31 }): Promise<UpdatePublicationResult> { 32 let identity = await getIdentityData();
··· 25 }: { 26 uri: string; 27 name: string; 28 + description?: string; 29 + iconFile?: File | null; 30 preferences?: Omit<PubLeafletPublication.Preferences, "$type">; 31 }): Promise<UpdatePublicationResult> { 32 let identity = await getIdentityData();
+6 -3
components/Canvas.tsx
··· 170 171 let pubRecord = pub.publications.record as PubLeafletPublication.Record; 172 let showComments = pubRecord.preferences?.showComments; 173 174 return ( 175 <div className="flex flex-row gap-3 items-center absolute top-6 right-3 sm:top-4 sm:right-4 bg-bg-page border-border-light rounded-md px-2 py-1 h-fit z-20"> ··· 178 <CommentTiny className="text-border" /> โ€” 179 </div> 180 )} 181 - <div className="flex gap-1 text-tertiary items-center"> 182 - <QuoteTiny className="text-border" /> โ€” 183 - </div> 184 185 {!props.isSubpage && ( 186 <>
··· 170 171 let pubRecord = pub.publications.record as PubLeafletPublication.Record; 172 let showComments = pubRecord.preferences?.showComments; 173 + let showMentions = pubRecord.preferences?.showMentions; 174 175 return ( 176 <div className="flex flex-row gap-3 items-center absolute top-6 right-3 sm:top-4 sm:right-4 bg-bg-page border-border-light rounded-md px-2 py-1 h-fit z-20"> ··· 179 <CommentTiny className="text-border" /> โ€” 180 </div> 181 )} 182 + {showComments && ( 183 + <div className="flex gap-1 text-tertiary items-center"> 184 + <QuoteTiny className="text-border" /> โ€” 185 + </div> 186 + )} 187 188 {!props.isSubpage && ( 189 <>
+4 -2
components/InteractionsPreview.tsx
··· 14 tags?: string[]; 15 postUrl: string; 16 showComments: boolean | undefined; 17 share?: boolean; 18 }) => { 19 let smoker = useSmoker(); 20 let interactionsAvailable = 21 - props.quotesCount > 0 || 22 (props.showComments !== false && props.commentsCount > 0); 23 24 const tagsCount = props.tags?.length || 0; ··· 36 </> 37 )} 38 39 - {props.quotesCount === 0 ? null : ( 40 <SpeedyLink 41 aria-label="Post quotes" 42 href={`${props.postUrl}?interactionDrawer=quotes`}
··· 14 tags?: string[]; 15 postUrl: string; 16 showComments: boolean | undefined; 17 + showMentions: boolean | undefined; 18 + 19 share?: boolean; 20 }) => { 21 let smoker = useSmoker(); 22 let interactionsAvailable = 23 + (props.quotesCount > 0 && props.showMentions !== false) || 24 (props.showComments !== false && props.commentsCount > 0); 25 26 const tagsCount = props.tags?.length || 0; ··· 38 </> 39 )} 40 41 + {props.showMentions === false || props.quotesCount === 0 ? null : ( 42 <SpeedyLink 43 aria-label="Post quotes" 44 href={`${props.postUrl}?interactionDrawer=quotes`}
+5 -3
components/Pages/PublicationMetadata.tsx
··· 121 <Separator classname="h-4!" /> 122 </> 123 )} 124 - <div className="flex gap-1 items-center"> 125 - <QuoteTiny />โ€” 126 - </div> 127 {pubRecord?.preferences?.showComments && ( 128 <div className="flex gap-1 items-center"> 129 <CommentTiny />โ€”
··· 121 <Separator classname="h-4!" /> 122 </> 123 )} 124 + {pubRecord?.preferences?.showMentions && ( 125 + <div className="flex gap-1 items-center"> 126 + <QuoteTiny />โ€” 127 + </div> 128 + )} 129 {pubRecord?.preferences?.showComments && ( 130 <div className="flex gap-1 items-center"> 131 <CommentTiny />โ€”
+1
components/PostListing.tsx
··· 97 commentsCount={comments} 98 tags={tags} 99 showComments={pubRecord?.preferences?.showComments} 100 share 101 /> 102 </div>
··· 97 commentsCount={comments} 98 tags={tags} 99 showComments={pubRecord?.preferences?.showComments} 100 + showMentions={pubRecord?.preferences?.showMentions} 101 share 102 /> 103 </div>
+7 -7
components/ThemeManager/PublicationThemeProvider.tsx
··· 2 import { useMemo, useState } from "react"; 3 import { parseColor } from "react-aria-components"; 4 import { useEntity } from "src/replicache"; 5 - import { getColorContrast } from "./themeUtils"; 6 import { useColorAttribute, colorToString } from "./useColorAttribute"; 7 import { BaseThemeProvider, CardBorderHiddenContext } from "./ThemeProvider"; 8 import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; ··· 174 let newAccentContrast; 175 let sortedAccents = [newTheme.accent1, newTheme.accent2].sort((a, b) => { 176 return ( 177 - getColorContrast( 178 colorToString(b, "rgb"), 179 colorToString( 180 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 181 "rgb", 182 ), 183 ) - 184 - getColorContrast( 185 colorToString(a, "rgb"), 186 colorToString( 187 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, ··· 191 ); 192 }); 193 if ( 194 - getColorContrast( 195 colorToString(sortedAccents[0], "rgb"), 196 colorToString(newTheme.primary, "rgb"), 197 - ) < 30 && 198 - getColorContrast( 199 colorToString(sortedAccents[1], "rgb"), 200 colorToString( 201 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 202 "rgb", 203 ), 204 - ) > 12 205 ) { 206 newAccentContrast = sortedAccents[1]; 207 } else newAccentContrast = sortedAccents[0];
··· 2 import { useMemo, useState } from "react"; 3 import { parseColor } from "react-aria-components"; 4 import { useEntity } from "src/replicache"; 5 + import { getColorDifference } from "./themeUtils"; 6 import { useColorAttribute, colorToString } from "./useColorAttribute"; 7 import { BaseThemeProvider, CardBorderHiddenContext } from "./ThemeProvider"; 8 import { PubLeafletPublication, PubLeafletThemeColor } from "lexicons/api"; ··· 174 let newAccentContrast; 175 let sortedAccents = [newTheme.accent1, newTheme.accent2].sort((a, b) => { 176 return ( 177 + getColorDifference( 178 colorToString(b, "rgb"), 179 colorToString( 180 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 181 "rgb", 182 ), 183 ) - 184 + getColorDifference( 185 colorToString(a, "rgb"), 186 colorToString( 187 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, ··· 191 ); 192 }); 193 if ( 194 + getColorDifference( 195 colorToString(sortedAccents[0], "rgb"), 196 colorToString(newTheme.primary, "rgb"), 197 + ) < 0.15 && 198 + getColorDifference( 199 colorToString(sortedAccents[1], "rgb"), 200 colorToString( 201 showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 202 "rgb", 203 ), 204 + ) > 0.08 205 ) { 206 newAccentContrast = sortedAccents[1]; 207 } else newAccentContrast = sortedAccents[0];
+9 -9
components/ThemeManager/ThemeProvider.tsx
··· 22 PublicationThemeProvider, 23 } from "./PublicationThemeProvider"; 24 import { PubLeafletPublication } from "lexicons/api"; 25 - import { getColorContrast } from "./themeUtils"; 26 27 // define a function to set an Aria Color to a CSS Variable in RGB 28 function setCSSVariableToColor( ··· 140 //sorting the accents by contrast on background 141 let sortedAccents = [accent1, accent2].sort((a, b) => { 142 return ( 143 - getColorContrast( 144 colorToString(b, "rgb"), 145 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 146 ) - 147 - getColorContrast( 148 colorToString(a, "rgb"), 149 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 150 ) ··· 156 // then use the not contrasty option 157 158 if ( 159 - getColorContrast( 160 colorToString(sortedAccents[0], "rgb"), 161 colorToString(primary, "rgb"), 162 - ) < 30 && 163 - getColorContrast( 164 colorToString(sortedAccents[1], "rgb"), 165 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 166 - ) > 12 167 ) { 168 accentContrast = sortedAccents[1]; 169 } else accentContrast = sortedAccents[0]; ··· 286 bgPage && accent1 && accent2 287 ? [accent1, accent2].sort((a, b) => { 288 return ( 289 - getColorContrast( 290 colorToString(b, "rgb"), 291 colorToString(bgPage, "rgb"), 292 ) - 293 - getColorContrast( 294 colorToString(a, "rgb"), 295 colorToString(bgPage, "rgb"), 296 )
··· 22 PublicationThemeProvider, 23 } from "./PublicationThemeProvider"; 24 import { PubLeafletPublication } from "lexicons/api"; 25 + import { getColorDifference } from "./themeUtils"; 26 27 // define a function to set an Aria Color to a CSS Variable in RGB 28 function setCSSVariableToColor( ··· 140 //sorting the accents by contrast on background 141 let sortedAccents = [accent1, accent2].sort((a, b) => { 142 return ( 143 + getColorDifference( 144 colorToString(b, "rgb"), 145 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 146 ) - 147 + getColorDifference( 148 colorToString(a, "rgb"), 149 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 150 ) ··· 156 // then use the not contrasty option 157 158 if ( 159 + getColorDifference( 160 colorToString(sortedAccents[0], "rgb"), 161 colorToString(primary, "rgb"), 162 + ) < 0.15 && 163 + getColorDifference( 164 colorToString(sortedAccents[1], "rgb"), 165 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 166 + ) > 0.08 167 ) { 168 accentContrast = sortedAccents[1]; 169 } else accentContrast = sortedAccents[0]; ··· 286 bgPage && accent1 && accent2 287 ? [accent1, accent2].sort((a, b) => { 288 return ( 289 + getColorDifference( 290 colorToString(b, "rgb"), 291 colorToString(bgPage, "rgb"), 292 ) - 293 + getColorDifference( 294 colorToString(a, "rgb"), 295 colorToString(bgPage, "rgb"), 296 )
+2 -3
components/ThemeManager/ThemeSetter.tsx
··· 1 "use client"; 2 import { Popover } from "components/Popover"; 3 - import { theme } from "../../tailwind.config"; 4 5 import { Color } from "react-aria-components"; 6 ··· 166 setOpenPicker={(pickers) => setOpenPicker(pickers)} 167 /> 168 <SectionArrow 169 - fill={theme.colors["accent-2"]} 170 - stroke={theme.colors["accent-1"]} 171 className="ml-2" 172 /> 173 </div>
··· 1 "use client"; 2 import { Popover } from "components/Popover"; 3 4 import { Color } from "react-aria-components"; 5 ··· 165 setOpenPicker={(pickers) => setOpenPicker(pickers)} 166 /> 167 <SectionArrow 168 + fill="rgb(var(--accent-2))" 169 + stroke="rgb(var(--accent-1))" 170 className="ml-2" 171 /> 172 </div>
+4 -3
components/ThemeManager/themeUtils.ts
··· 1 - import { parse, contrastLstar, ColorSpace, sRGB } from "colorjs.io/fn"; 2 3 // define the color defaults for everything 4 export const ThemeDefaults = { ··· 17 }; 18 19 // used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast 20 - export function getColorContrast(color1: string, color2: string) { 21 ColorSpace.register(sRGB); 22 23 let parsedColor1 = parse(`rgb(${color1})`); 24 let parsedColor2 = parse(`rgb(${color2})`); 25 26 - return contrastLstar(parsedColor1, parsedColor2); 27 }
··· 1 + import { parse, ColorSpace, sRGB, distance, OKLab } from "colorjs.io/fn"; 2 3 // define the color defaults for everything 4 export const ThemeDefaults = { ··· 17 }; 18 19 // used to calculate the contrast between page and accent1, accent2, and determin which is higher contrast 20 + export function getColorDifference(color1: string, color2: string) { 21 ColorSpace.register(sRGB); 22 + ColorSpace.register(OKLab); 23 24 let parsedColor1 = parse(`rgb(${color1})`); 25 let parsedColor2 = parse(`rgb(${color2})`); 26 27 + return distance(parsedColor1, parsedColor2, "oklab"); 28 }
+8
lexicons/api/lexicons.ts
··· 1810 type: 'boolean', 1811 default: true, 1812 }, 1813 }, 1814 }, 1815 theme: {
··· 1810 type: 'boolean', 1811 default: true, 1812 }, 1813 + showMentions: { 1814 + type: 'boolean', 1815 + default: true, 1816 + }, 1817 + showPrevNext: { 1818 + type: 'boolean', 1819 + default: false, 1820 + }, 1821 }, 1822 }, 1823 theme: {
+2
lexicons/api/types/pub/leaflet/publication.ts
··· 37 $type?: 'pub.leaflet.publication#preferences' 38 showInDiscover: boolean 39 showComments: boolean 40 } 41 42 const hashPreferences = 'preferences'
··· 37 $type?: 'pub.leaflet.publication#preferences' 38 showInDiscover: boolean 39 showComments: boolean 40 + showMentions: boolean 41 + showPrevNext: boolean 42 } 43 44 const hashPreferences = 'preferences'
+8
lexicons/pub/leaflet/publication.json
··· 51 "showComments": { 52 "type": "boolean", 53 "default": true 54 } 55 } 56 },
··· 51 "showComments": { 52 "type": "boolean", 53 "default": true 54 + }, 55 + "showMentions": { 56 + "type": "boolean", 57 + "default": true 58 + }, 59 + "showPrevNext": { 60 + "type": "boolean", 61 + "default": false 62 } 63 } 64 },
+2
lexicons/src/publication.ts
··· 27 properties: { 28 showInDiscover: { type: "boolean", default: true }, 29 showComments: { type: "boolean", default: true }, 30 }, 31 }, 32 theme: {
··· 27 properties: { 28 showInDiscover: { type: "boolean", default: true }, 29 showComments: { type: "boolean", default: true }, 30 + showMentions: { type: "boolean", default: true }, 31 + showPrevNext: { type: "boolean", default: false }, 32 }, 33 }, 34 theme: {