a tool for shared writing and social publishing

refactored the footer a bit

+162 -351
+36 -206
app/lish/Subscribe.tsx
··· 23 23 import { useSearchParams } from "next/navigation"; 24 24 import LoginForm from "app/login/LoginForm"; 25 25 import { RSSSmall } from "components/Icons/RSSSmall"; 26 - import { SpeedyLink } from "components/SpeedyLink"; 27 - 28 - type State = 29 - | { state: "email" } 30 - | { state: "code"; token: string } 31 - | { state: "success" }; 32 - export const SubscribeButton = (props: { 33 - compact?: boolean; 34 - publication: string; 35 - }) => { 36 - let { identity, mutate } = useIdentityData(); 37 - let [emailInputValue, setEmailInputValue] = useState(""); 38 - let [codeInputValue, setCodeInputValue] = useState(""); 39 - let [state, setState] = useState<State>({ state: "email" }); 40 - 41 - if (state.state === "email") { 42 - return ( 43 - <div className="flex gap-2"> 44 - <div className="flex relative w-full max-w-sm"> 45 - <Input 46 - type="email" 47 - className="input-with-border pr-[104px]! py-1! grow w-full" 48 - placeholder={ 49 - props.compact ? "subscribe with email..." : "email here..." 50 - } 51 - disabled={!!identity?.email} 52 - value={identity?.email ? identity.email : emailInputValue} 53 - onChange={(e) => { 54 - setEmailInputValue(e.currentTarget.value); 55 - }} 56 - /> 57 - <ButtonPrimary 58 - compact 59 - className="absolute right-1 top-1 outline-0!" 60 - onClick={async () => { 61 - if (identity?.email) { 62 - await subscribeToPublicationWithEmail(props.publication); 63 - //optimistically could add! 64 - await mutate(); 65 - return; 66 - } 67 - let tokenID = await requestAuthEmailToken(emailInputValue); 68 - setState({ state: "code", token: tokenID }); 69 - }} 70 - > 71 - {props.compact ? ( 72 - <ArrowRightTiny className="w-4 h-6" /> 73 - ) : ( 74 - "Subscribe" 75 - )} 76 - </ButtonPrimary> 77 - </div> 78 - {/* <ShareButton /> */} 79 - </div> 80 - ); 81 - } 82 - if (state.state === "code") { 83 - return ( 84 - <div 85 - className="w-full flex flex-col justify-center place-items-center p-4 rounded-md" 86 - style={{ 87 - background: 88 - "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 85%)", 89 - }} 90 - > 91 - <div className="flex flex-col leading-snug text-secondary"> 92 - <div>Please enter the code we sent to </div> 93 - <div className="italic font-bold">{emailInputValue}</div> 94 - </div> 95 - 96 - <ConfirmCodeInput 97 - publication={props.publication} 98 - token={state.token} 99 - codeInputValue={codeInputValue} 100 - setCodeInputValue={setCodeInputValue} 101 - setState={setState} 102 - /> 103 - 104 - <button 105 - className="text-accent-contrast text-sm mt-1" 106 - onClick={() => { 107 - setState({ state: "email" }); 108 - }} 109 - > 110 - Re-enter Email 111 - </button> 112 - </div> 113 - ); 114 - } 115 - 116 - if (state.state === "success") { 117 - return ( 118 - <div 119 - className={`w-full flex flex-col gap-2 justify-center place-items-center p-4 rounded-md text-secondary ${props.compact ? "py-1 animate-bounce" : "p-4"}`} 120 - style={{ 121 - background: 122 - "color-mix(in oklab, rgb(var(--accent-contrast)), rgb(var(--bg-page)) 85%)", 123 - }} 124 - > 125 - <div className="flex gap-2 leading-snug font-bold italic"> 126 - <div>You're subscribed!</div> 127 - {/* <ShareButton /> */} 128 - </div> 129 - </div> 130 - ); 131 - } 132 - }; 133 - 134 - export const ShareButton = () => { 135 - return ( 136 - <button className="text-accent-contrast"> 137 - <ShareSmall /> 138 - </button> 139 - ); 140 - }; 141 - 142 - const ConfirmCodeInput = (props: { 143 - codeInputValue: string; 144 - token: string; 145 - setCodeInputValue: (value: string) => void; 146 - setState: (state: State) => void; 147 - publication: string; 148 - }) => { 149 - let { mutate } = useIdentityData(); 150 - return ( 151 - <div className="relative w-fit mt-2"> 152 - <Input 153 - type="text" 154 - pattern="[0-9]" 155 - className="input-with-border pr-[88px]! py-1! max-w-[156px]" 156 - placeholder="000000" 157 - value={props.codeInputValue} 158 - onChange={(e) => { 159 - props.setCodeInputValue(e.currentTarget.value); 160 - }} 161 - /> 162 - <ButtonPrimary 163 - compact 164 - className="absolute right-1 top-1 outline-0!" 165 - onClick={async () => { 166 - console.log( 167 - await confirmEmailAuthToken(props.token, props.codeInputValue), 168 - ); 169 - 170 - await subscribeToPublicationWithEmail(props.publication); 171 - //optimistically could add! 172 - await mutate(); 173 - props.setState({ state: "success" }); 174 - return; 175 - }} 176 - > 177 - Confirm 178 - </ButtonPrimary> 179 - </div> 180 - ); 181 - }; 182 26 183 27 export const SubscribeWithBluesky = (props: { 184 - isPost?: boolean; 185 28 pubName: string; 186 29 pub_uri: string; 187 30 base_url: string; ··· 208 51 } 209 52 return ( 210 53 <div className="flex flex-col gap-2 text-center justify-center"> 211 - {props.isPost && ( 212 - <div className="text-sm text-tertiary font-bold"> 213 - Get updates from {props.pubName}! 214 - </div> 215 - )} 216 54 <div className="flex flex-row gap-2 place-self-center"> 217 55 <BlueskySubscribeButton 218 56 pub_uri={props.pub_uri} ··· 231 69 ); 232 70 }; 233 71 234 - const ManageSubscription = (props: { 235 - isPost?: boolean; 236 - pubName: string; 72 + export const ManageSubscription = (props: { 237 73 pub_uri: string; 238 74 subscribers: { identity: string }[]; 239 75 base_url: string; ··· 248 84 }); 249 85 }, null); 250 86 return ( 251 - <div 252 - className={`flex ${props.isPost ? "flex-col " : "gap-2"} justify-center text-center`} 87 + <Popover 88 + trigger={ 89 + <div className="text-accent-contrast text-sm">Manage Subscription</div> 90 + } 253 91 > 254 - <div className="font-bold text-tertiary text-sm"> 255 - You&apos;re Subscribed{props.isPost ? ` to ` : "!"} 256 - {props.isPost && ( 257 - <SpeedyLink href={props.base_url} className="text-accent-contrast"> 258 - {props.pubName} 259 - </SpeedyLink> 260 - )} 261 - </div> 262 - <Popover 263 - trigger={<div className="text-accent-contrast text-sm">Manage</div>} 264 - > 265 - <div className="max-w-sm flex flex-col gap-1"> 266 - <h4>Update Options</h4> 92 + <div className="max-w-sm flex flex-col gap-1"> 93 + <h4>Update Options</h4> 267 94 268 - {!hasFeed && ( 269 - <a 270 - href="https://bsky.app/profile/leaflet.pub/feed/subscribedPublications" 271 - target="_blank" 272 - className=" place-self-center" 273 - > 274 - <ButtonPrimary fullWidth compact className="!px-4"> 275 - View Bluesky Custom Feed 276 - </ButtonPrimary> 277 - </a> 278 - )} 279 - 95 + {!hasFeed && ( 280 96 <a 281 - href={`${props.base_url}/rss`} 282 - className="flex" 97 + href="https://bsky.app/profile/leaflet.pub/feed/subscribedPublications" 283 98 target="_blank" 284 - aria-label="Subscribe to RSS" 99 + className=" place-self-center" 285 100 > 286 - <ButtonPrimary fullWidth compact> 287 - Get RSS 101 + <ButtonPrimary fullWidth compact className="!px-4"> 102 + View Bluesky Custom Feed 288 103 </ButtonPrimary> 289 104 </a> 105 + )} 290 106 291 - <hr className="border-border-light my-1" /> 107 + <a 108 + href={`${props.base_url}/rss`} 109 + className="flex" 110 + target="_blank" 111 + aria-label="Subscribe to RSS" 112 + > 113 + <ButtonPrimary fullWidth compact> 114 + Get RSS 115 + </ButtonPrimary> 116 + </a> 292 117 293 - <form action={unsubscribe}> 294 - <button className="font-bold text-accent-contrast w-max place-self-center"> 295 - {unsubscribePending ? <DotLoader /> : "Unsubscribe"} 296 - </button> 297 - </form> 298 - </div>{" "} 299 - </Popover> 300 - </div> 118 + <hr className="border-border-light my-1" /> 119 + 120 + <form action={unsubscribe}> 121 + <button className="font-bold text-accent-contrast w-max place-self-center"> 122 + {unsubscribePending ? <DotLoader /> : "Unsubscribe"} 123 + </button> 124 + </form> 125 + </div> 126 + </Popover> 301 127 ); 302 128 }; 303 129 ··· 430 256 </Dialog.Root> 431 257 ); 432 258 }; 259 + 260 + export const SubscribeOnPost = () => { 261 + return <div></div>; 262 + };
+123 -43
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 13 13 import { Tag } from "components/Tags"; 14 14 import { Popover } from "components/Popover"; 15 15 import { PostPageData } from "../getPostPageData"; 16 - import { PubLeafletComment } from "lexicons/api"; 16 + import { PubLeafletComment, PubLeafletPublication } from "lexicons/api"; 17 17 import { prefetchQuotesData } from "./Quotes"; 18 + import { useIdentityData } from "components/IdentityProvider"; 19 + import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; 20 + import { EditTiny } from "components/Icons/EditTiny"; 21 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 22 + import { PubListing } from "app/(home-pages)/discover/PubListing"; 23 + import { PubIcon } from "components/ActionBar/Publications"; 18 24 19 25 export type InteractionState = { 20 26 drawerOpen: undefined | boolean; ··· 108 114 }) => { 109 115 const data = useContext(PostPageContext); 110 116 const document_uri = data?.uri; 117 + let { identity } = useIdentityData(); 111 118 if (!document_uri) 112 119 throw new Error("document_uri not available in PostPageContext"); 113 120 ··· 121 128 122 129 const tags = (data?.data as any)?.tags as string[] | undefined; 123 130 const tagCount = tags?.length || 0; 131 + 124 132 return ( 125 133 <div className={`flex gap-2 text-tertiary text-sm ${props.className}`}> 126 134 {tagCount > 0 && <TagPopover tags={tags} tagCount={tagCount} />} ··· 165 173 pageId?: string; 166 174 }) => { 167 175 const data = useContext(PostPageContext); 176 + let { identity } = useIdentityData(); 177 + 168 178 const document_uri = data?.uri; 169 179 if (!document_uri) 170 180 throw new Error("document_uri not available in PostPageContext"); ··· 176 186 prefetchQuotesData(data.quotesAndMentions); 177 187 } 178 188 }; 189 + let publication = data?.documents_in_publications[0]?.publications; 179 190 180 191 const tags = (data?.data as any)?.tags as string[] | undefined; 181 192 const tagCount = tags?.length || 0; 193 + 194 + let subscribed = 195 + identity?.atp_did && 196 + publication?.publication_subscriptions && 197 + publication?.publication_subscriptions.find( 198 + (s) => s.identity === identity.atp_did, 199 + ); 200 + 201 + let isAuthor = 202 + identity && 203 + identity.atp_did === 204 + data.documents_in_publications[0]?.publications?.identity_did && 205 + data.leaflets_in_publications[0]; 206 + 182 207 return ( 183 208 <div 184 - className={`gap-2 text-tertiary px-3 sm:px-4 flex flex-col ${props.className}`} 209 + className={`text-tertiary px-3 sm:px-4 flex flex-col ${props.className}`} 185 210 > 211 + {!subscribed && !isAuthor && publication && publication.record && ( 212 + <div className="text-center flex flex-col accent-container rounded-md mb-3"> 213 + <div className="flex flex-col py-4"> 214 + <div className="leading-snug flex flex-col pb-2 text-sm"> 215 + <div className="font-bold">Subscribe to {publication.name}</div>{" "} 216 + to get updates in Reader, RSS, or via Bluesky Feed 217 + </div> 218 + <SubscribeWithBluesky 219 + pubName={publication.name} 220 + pub_uri={publication.uri} 221 + base_url={ 222 + (publication.record as PubLeafletPublication.Record) 223 + .base_path || "" 224 + } 225 + subscribers={publication?.publication_subscriptions} 226 + /> 227 + </div> 228 + </div> 229 + )} 186 230 {tagCount > 0 && ( 187 231 <> 188 - <hr className="border-border-light mb-1 " /> 189 - <TagList tags={tags} /> 190 - <hr className="border-border-light mt-1 " /> 232 + <hr className="border-border-light mb-3" /> 233 + 234 + <TagList tags={tags} className="mb-3" /> 191 235 </> 192 236 )} 193 - 194 - {props.quotesCount > 0 && ( 195 - <button 196 - className="flex w-fit gap-2 items-center px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 197 - onClick={() => { 198 - if (!drawerOpen || drawer !== "quotes") 199 - openInteractionDrawer("quotes", document_uri, props.pageId); 200 - else setInteractionState(document_uri, { drawerOpen: false }); 201 - }} 202 - onMouseEnter={handleQuotePrefetch} 203 - onTouchStart={handleQuotePrefetch} 204 - aria-label="Post quotes" 205 - > 206 - <QuoteTiny aria-hidden /> {props.quotesCount}{" "} 207 - <span 208 - aria-hidden 209 - >{`Quote${props.quotesCount === 1 ? "" : "s"}`}</span> 210 - </button> 211 - )} 212 - {props.showComments === false ? null : ( 213 - <button 214 - className="flex gap-2 items-center w-fit px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 215 - onClick={() => { 216 - if (!drawerOpen || drawer !== "comments" || pageId !== props.pageId) 217 - openInteractionDrawer("comments", document_uri, props.pageId); 218 - else setInteractionState(document_uri, { drawerOpen: false }); 219 - }} 220 - aria-label="Post comments" 221 - > 222 - <CommentTiny aria-hidden />{" "} 223 - {props.commentsCount > 0 ? ( 224 - <span aria-hidden> 225 - {`${props.commentsCount} Comment${props.commentsCount === 1 ? "" : "s"}`} 226 - </span> 227 - ) : ( 228 - "Comment" 237 + <hr className="border-border-light mb-3 " /> 238 + <div className="flex gap-2 justify-between"> 239 + <div className="flex gap-2"> 240 + {props.quotesCount > 0 && ( 241 + <button 242 + className="flex w-fit gap-2 items-center px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 243 + onClick={() => { 244 + if (!drawerOpen || drawer !== "quotes") 245 + openInteractionDrawer("quotes", document_uri, props.pageId); 246 + else setInteractionState(document_uri, { drawerOpen: false }); 247 + }} 248 + onMouseEnter={handleQuotePrefetch} 249 + onTouchStart={handleQuotePrefetch} 250 + aria-label="Post quotes" 251 + > 252 + <QuoteTiny aria-hidden /> {props.quotesCount}{" "} 253 + <span 254 + aria-hidden 255 + >{`Quote${props.quotesCount === 1 ? "" : "s"}`}</span> 256 + </button> 257 + )} 258 + {props.showComments === false ? null : ( 259 + <button 260 + className="flex gap-2 items-center w-fit px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 261 + onClick={() => { 262 + if ( 263 + !drawerOpen || 264 + drawer !== "comments" || 265 + pageId !== props.pageId 266 + ) 267 + openInteractionDrawer("comments", document_uri, props.pageId); 268 + else setInteractionState(document_uri, { drawerOpen: false }); 269 + }} 270 + aria-label="Post comments" 271 + > 272 + <CommentTiny aria-hidden />{" "} 273 + {props.commentsCount > 0 ? ( 274 + <span aria-hidden> 275 + {`${props.commentsCount} Comment${props.commentsCount === 1 ? "" : "s"}`} 276 + </span> 277 + ) : ( 278 + "Comment" 279 + )} 280 + </button> 229 281 )} 230 - </button> 231 - )} 282 + </div> 283 + <EditButton document={data} /> 284 + {subscribed && publication && ( 285 + <ManageSubscription 286 + base_url={getPublicationURL(publication)} 287 + pub_uri={publication.uri} 288 + subscribers={publication.publication_subscriptions} 289 + /> 290 + )} 291 + </div> 232 292 </div> 233 293 ); 234 294 }; ··· 298 358 (c) => !(c.record as PubLeafletComment.Record)?.onPage, 299 359 ).length; 300 360 } 361 + 362 + const EditButton = (props: { document: PostPageData }) => { 363 + let { identity } = useIdentityData(); 364 + if (!props.document) return; 365 + if ( 366 + identity && 367 + identity.atp_did === 368 + props.document.documents_in_publications[0]?.publications?.identity_did && 369 + props.document.leaflets_in_publications[0] 370 + ) 371 + return ( 372 + <a 373 + href={`https://leaflet.pub/${props.document.leaflets_in_publications[0]?.leaflet}`} 374 + className="flex gap-2 items-center hover:!no-underline selected-outline px-2 py-0.5 bg-accent-1 text-accent-2 font-bold w-fit rounded-lg !border-accent-1 !outline-accent-1" 375 + > 376 + <EditTiny /> Edit Post 377 + </a> 378 + ); 379 + return; 380 + };
+2 -38
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 48 48 fullPageScroll, 49 49 hasPageBackground, 50 50 } = props; 51 - let { identity } = useIdentityData(); 52 51 let drawer = useDrawerOpen(document_uri); 53 52 54 53 if (!document) return null; ··· 85 84 did={did} 86 85 prerenderedCodeBlocks={prerenderedCodeBlocks} 87 86 /> 87 + 88 88 <ExpandedInteractions 89 89 pageId={pageId} 90 90 showComments={preferences.showComments} 91 91 commentsCount={getCommentCount(document, pageId) || 0} 92 92 quotesCount={getQuoteCount(document, pageId) || 0} 93 93 /> 94 - {!isSubpage && ( 95 - <> 96 - <div className="sm:px-4 px-3"> 97 - {identity && 98 - identity.atp_did === 99 - document.documents_in_publications[0]?.publications 100 - ?.identity_did && 101 - document.leaflets_in_publications[0] ? ( 102 - <a 103 - href={`https://leaflet.pub/${document.leaflets_in_publications[0]?.leaflet}`} 104 - className="flex gap-2 items-center hover:!no-underline selected-outline px-2 py-0.5 bg-accent-1 text-accent-2 font-bold w-fit rounded-lg !border-accent-1 !outline-accent-1 mx-auto" 105 - > 106 - <EditTiny /> Edit Post 107 - </a> 108 - ) : ( 109 - document.documents_in_publications[0]?.publications && ( 110 - <SubscribeWithBluesky 111 - isPost 112 - base_url={getPublicationURL( 113 - document.documents_in_publications[0].publications, 114 - )} 115 - pub_uri={ 116 - document.documents_in_publications[0].publications.uri 117 - } 118 - subscribers={ 119 - document.documents_in_publications[0].publications 120 - .publication_subscriptions 121 - } 122 - pubName={ 123 - document.documents_in_publications[0].publications.name 124 - } 125 - /> 126 - ) 127 - )} 128 - </div> 129 - </> 130 - )} 94 + {!hasPageBackground && <div className={`spacer h-8 w-full`} />} 131 95 </PageWrapper> 132 96 </> 133 97 );
+1 -1
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 59 59 return ( 60 60 <div 61 61 //The postContent class is important for QuoteHandler 62 - className={`postContent flex flex-col sm:px-4 px-3 sm:pt-3 pt-2 pb-1 sm:pb-6 ${className}`} 62 + className={`postContent flex flex-col sm:px-4 px-3 sm:pt-3 pt-2 pb-1 sm:pb-4 ${className}`} 63 63 > 64 64 {blocks.map((b, index) => { 65 65 return (
-63
app/lish/[did]/[publication]/[rkey]/PostFooter.tsx
··· 1 - import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs"; 2 - import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 3 - import { SubscribeWithBluesky } from "app/lish/Subscribe"; 4 - import { EditTiny } from "components/Icons/EditTiny"; 5 - import { useIdentityData } from "components/IdentityProvider"; 6 - import { PubLeafletComment } from "lexicons/api"; 7 - import { PostPageData } from "./getPostPageData"; 8 - import { ExpandedInteractions } from "./Interactions/Interactions"; 9 - import { decodeQuotePosition } from "./quotePosition"; 10 - 11 - export const PostFooter = (props: { 12 - data: PostPageData; 13 - profile: ProfileViewDetailed; 14 - preferences: { showComments?: boolean }; 15 - }) => { 16 - let { identity } = useIdentityData(); 17 - if (!props.data || !props.data.documents_in_publications[0].publications) 18 - return; 19 - return ( 20 - <div className="flex flex-col px-3 sm:px-4"> 21 - <ExpandedInteractions 22 - showComments={props.preferences.showComments} 23 - quotesCount={ 24 - props.data.document_mentions_in_bsky.filter((q) => { 25 - const url = new URL(q.link); 26 - const quoteParam = url.pathname.split("/l-quote/")[1]; 27 - if (!quoteParam) return null; 28 - const quotePosition = decodeQuotePosition(quoteParam); 29 - return !quotePosition?.pageId; 30 - }).length 31 - } 32 - commentsCount={ 33 - props.data.comments_on_documents.filter( 34 - (c) => !(c.record as PubLeafletComment.Record)?.onPage, 35 - ).length 36 - } 37 - /> 38 - {identity && 39 - identity.atp_did === 40 - props.data.documents_in_publications[0]?.publications?.identity_did ? ( 41 - <a 42 - href={`https://leaflet.pub/${props.data.leaflets_in_publications[0]?.leaflet}`} 43 - className="flex gap-2 items-center hover:!no-underline selected-outline px-2 py-0.5 bg-accent-1 text-accent-2 font-bold w-fit rounded-lg !border-accent-1 !outline-accent-1 mx-auto" 44 - > 45 - <EditTiny /> Edit Post 46 - </a> 47 - ) : ( 48 - <SubscribeWithBluesky 49 - isPost 50 - base_url={getPublicationURL( 51 - props.data.documents_in_publications[0].publications, 52 - )} 53 - pub_uri={props.data.documents_in_publications[0].publications.uri} 54 - subscribers={ 55 - props.data.documents_in_publications[0].publications 56 - .publication_subscriptions 57 - } 58 - pubName={props.data.documents_in_publications[0].publications.name} 59 - /> 60 - )} 61 - </div> 62 - ); 63 - };