a tool for shared writing and social publishing

Compare changes

Choose any two refs to compare.

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