a tool for shared writing and social publishing

Merge branch 'main' of https://github.com/hyperlink-academy/minilink into feature/backdate

+231 -178
+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 -4
app/globals.css
··· 309 309 .ProseMirror .didMention.ProseMirror-selectednode { 310 310 @apply text-accent-contrast; 311 311 @apply px-0.5; 312 - @apply -mx-[3px]; /* extra px to account for the border*/ 313 - @apply -my-px; /*to account for the border*/ 314 312 @apply rounded-[4px]; 315 313 @apply box-decoration-clone; 316 314 background-color: rgba(var(--accent-contrast), 0.2); ··· 321 319 @apply cursor-pointer; 322 320 @apply text-accent-contrast; 323 321 @apply px-0.5; 324 - @apply -mx-[3px]; 325 - @apply -my-px; /*to account for the border*/ 326 322 @apply rounded-[4px]; 327 323 @apply box-decoration-clone; 328 324 background-color: rgba(var(--accent-contrast), 0.2); 329 325 border: 1px solid transparent; 326 + display: inline; 327 + white-space: normal; 330 328 } 331 329 332 330 .multiselected:focus-within .selection-highlight {
+2 -2
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 216 216 <Interactions 217 217 quotesCount={props.quotesCount || 0} 218 218 commentsCount={props.commentsCount || 0} 219 - showComments={props.preferences.showComments} 220 - showMentions={props.preferences.showMentions} 219 + showComments={props.preferences.showComments !== false} 220 + showMentions={props.preferences.showMentions !== false} 221 221 pageId={props.pageId} 222 222 /> 223 223 {!props.isSubpage && (
+10 -2
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 119 119 }) => { 120 120 const did = props.comment.bsky_profiles?.did; 121 121 122 + let timeAgoDate = timeAgo(props.record.createdAt); 123 + const formattedDate = useLocalizedDate(props.record.createdAt, { 124 + year: "numeric", 125 + month: "long", 126 + day: "2-digit", 127 + }); 128 + 122 129 return ( 123 130 <div id={props.comment.uri} className="comment"> 124 131 <div className="flex gap-2"> 125 - {did && ( 132 + {did ? ( 126 133 <ProfilePopover 127 134 didOrHandle={did} 128 135 trigger={ ··· 131 138 </div> 132 139 } 133 140 /> 134 - )} 141 + ) : null} 142 + <div className="text-sm text-tertiary">{timeAgoDate}</div> 135 143 </div> 136 144 {props.record.attachment && 137 145 PubLeafletComment.isLinearDocumentQuote(props.record.attachment) && (
+2 -2
app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 36 36 return ( 37 37 <> 38 38 <SandwichSpacer noWidth /> 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 + <div className="snap-center h-full flex z-10 shrink-0 sm:max-w-prose sm:w-full w-[calc(100vw-12px)]"> 40 40 <div 41 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"}`} 42 + className={`opaque-container h-full w-full px-3 sm:px-4 pt-2 sm:pt-3 pb-6 overflow-scroll ${props.showPageBackground ? "rounded-l-none! rounded-r-lg! -ml-[1px]" : "rounded-lg! sm:ml-4"}`} 43 43 > 44 44 {drawer.drawer === "quotes" ? ( 45 45 <Quotes {...props} quotesAndMentions={filteredQuotesAndMentions} />
+6 -23
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 107 107 quotesCount: number; 108 108 commentsCount: number; 109 109 className?: string; 110 - showComments?: boolean; 111 - showMentions?: boolean; 110 + showComments: boolean; 111 + showMentions: boolean; 112 112 pageId?: string; 113 113 }) => { 114 114 const data = useContext(PostPageContext); ··· 168 168 quotesCount: number; 169 169 commentsCount: number; 170 170 className?: string; 171 - showComments?: boolean; 172 - showMentions?: boolean; 171 + showComments: boolean; 172 + showMentions: boolean; 173 173 pageId?: string; 174 174 }) => { 175 175 const data = useContext(PostPageContext); ··· 210 210 <div 211 211 className={`text-tertiary px-3 sm:px-4 flex flex-col ${props.className}`} 212 212 > 213 - {!subscribed && !isAuthor && publication && publication.record && ( 214 - <div className="text-center flex flex-col accent-container rounded-md mb-3"> 215 - <div className="flex flex-col py-4"> 216 - <div className="leading-snug flex flex-col pb-2 text-sm"> 217 - <div className="font-bold">Subscribe to {publication.name}</div>{" "} 218 - to get updates in Reader, RSS, or via Bluesky Feed 219 - </div> 220 - <SubscribeWithBluesky 221 - pubName={publication.name} 222 - pub_uri={publication.uri} 223 - base_url={getPublicationURL(publication)} 224 - subscribers={publication?.publication_subscriptions} 225 - /> 226 - </div> 227 - </div> 228 - )} 229 213 {tagCount > 0 && ( 230 214 <> 231 215 <hr className="border-border-light mb-3" /> ··· 242 226 ) : ( 243 227 <> 244 228 <div className="flex gap-2"> 245 - {props.quotesCount === 0 || 246 - props.showMentions === false ? null : ( 229 + {props.quotesCount === 0 || !props.showMentions ? null : ( 247 230 <button 248 231 className="flex w-fit gap-2 items-center px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 249 232 onClick={() => { ··· 266 249 >{`Mention${props.quotesCount === 1 ? "" : "s"}`}</span> 267 250 </button> 268 251 )} 269 - {props.showComments === false ? null : ( 252 + {!props.showComments ? null : ( 270 253 <button 271 254 className="flex gap-2 items-center w-fit px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" 272 255 onClick={() => {
+5 -5
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 25 25 import { PollData } from "./fetchPollData"; 26 26 import { SharedPageProps } from "./PostPages"; 27 27 import { PostPrevNextButtons } from "./PostPrevNextButtons"; 28 + import { PostSubscribe } from "./PostSubscribe"; 28 29 29 30 export function LinearDocumentPage({ 30 31 blocks, ··· 56 57 57 58 const isSubpage = !!pageId; 58 59 59 - console.log("prev/next?: " + preferences.showPrevNext); 60 - 61 60 return ( 62 61 <> 63 62 <PageWrapper ··· 85 84 did={did} 86 85 prerenderedCodeBlocks={prerenderedCodeBlocks} 87 86 /> 87 + <PostSubscribe /> 88 88 <PostPrevNextButtons 89 - showPrevNext={preferences.showPrevNext && !isSubpage} 89 + showPrevNext={preferences.showPrevNext !== false && !isSubpage} 90 90 /> 91 91 <ExpandedInteractions 92 92 pageId={pageId} 93 - showComments={preferences.showComments} 94 - showMentions={preferences.showMentions} 93 + showComments={preferences.showComments !== false} 94 + showMentions={preferences.showMentions !== false} 95 95 commentsCount={getCommentCount(document, pageId) || 0} 96 96 quotesCount={getQuoteCount(document, pageId) || 0} 97 97 />
+2 -2
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 90 90 ) : null} 91 91 </div> 92 92 <Interactions 93 - showComments={props.preferences.showComments} 94 - showMentions={props.preferences.showMentions} 93 + showComments={props.preferences.showComments !== false} 94 + showMentions={props.preferences.showMentions !== false} 95 95 quotesCount={getQuoteCount(document) || 0} 96 96 commentsCount={getCommentCount(document) || 0} 97 97 />
+1 -3
app/lish/[did]/[publication]/[rkey]/PostPrevNextButtons.tsx
··· 10 10 import { SpeedyLink } from "components/SpeedyLink"; 11 11 import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 12 12 13 - export const PostPrevNextButtons = (props: { 14 - showPrevNext: boolean | undefined; 15 - }) => { 13 + export const PostPrevNextButtons = (props: { showPrevNext: boolean }) => { 16 14 let postData = useContext(PostPageContext); 17 15 let pub = postData?.documents_in_publications[0]?.publications; 18 16
+45
app/lish/[did]/[publication]/[rkey]/PostSubscribe.tsx
··· 1 + "use client"; 2 + import { useContext } from "react"; 3 + import { PostPageContext } from "./PostPageContext"; 4 + import { useIdentityData } from "components/IdentityProvider"; 5 + import { SubscribeWithBluesky } from "app/lish/Subscribe"; 6 + import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 7 + 8 + export const PostSubscribe = () => { 9 + const data = useContext(PostPageContext); 10 + let { identity } = useIdentityData(); 11 + 12 + let publication = data?.documents_in_publications[0]?.publications; 13 + 14 + let subscribed = 15 + identity?.atp_did && 16 + publication?.publication_subscriptions && 17 + publication?.publication_subscriptions.find( 18 + (s) => s.identity === identity.atp_did, 19 + ); 20 + 21 + let isAuthor = 22 + identity && 23 + identity.atp_did === 24 + data?.documents_in_publications[0]?.publications?.identity_did && 25 + data?.leaflets_in_publications[0]; 26 + 27 + if (!subscribed && !isAuthor && publication && publication.record) 28 + return ( 29 + <div className="text-center flex flex-col accent-container rounded-md mb-3 mx-3 sm:mx-4"> 30 + <div className="flex flex-col py-4"> 31 + <div className="leading-snug flex flex-col pb-2 "> 32 + <div className="font-bold">Subscribe to {publication.name}</div> to 33 + get updates in Reader, RSS, or via Bluesky Feed 34 + </div> 35 + <SubscribeWithBluesky 36 + pubName={publication.name} 37 + pub_uri={publication.uri} 38 + base_url={getPublicationURL(publication)} 39 + subscribers={publication?.publication_subscriptions} 40 + /> 41 + </div> 42 + </div> 43 + ); 44 + else return; 45 + };
+6 -2
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 139 139 quotesCount={quotes} 140 140 commentsCount={comments} 141 141 tags={tags} 142 - showComments={pubRecord?.preferences?.showComments} 143 - showMentions={pubRecord?.preferences?.showMentions} 142 + showComments={ 143 + pubRecord?.preferences?.showComments !== false 144 + } 145 + showMentions={ 146 + pubRecord?.preferences?.showMentions !== false 147 + } 144 148 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 145 149 /> 146 150 </div>
-1
app/lish/[did]/[publication]/dashboard/settings/PostOptions.tsx
··· 54 54 }, 55 55 }); 56 56 toast({ type: "success", content: <strong>Posts Updated!</strong> }); 57 - console.log(record.preferences?.showPrevNext); 58 57 props.setLoading(false); 59 58 mutate("publication-data"); 60 59 }}
+6 -2
app/lish/[did]/[publication]/page.tsx
··· 171 171 commentsCount={comments} 172 172 tags={tags} 173 173 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 174 - showComments={record?.preferences?.showComments} 175 - showMentions={record?.preferences?.showMentions} 174 + showComments={ 175 + record?.preferences?.showComments !== false 176 + } 177 + showMentions={ 178 + record?.preferences?.showMentions !== false 179 + } 176 180 /> 177 181 </div> 178 182 </div>
+1 -1
app/lish/createPub/CreatePubForm.tsx
··· 57 57 showInDiscover, 58 58 showComments: true, 59 59 showMentions: true, 60 - showPrevNext: false, 60 + showPrevNext: true, 61 61 }, 62 62 }); 63 63
+2 -1
components/Blocks/Block.tsx
··· 32 32 import { HorizontalRule } from "./HorizontalRule"; 33 33 import { deepEquals } from "src/utils/deepEquals"; 34 34 import { isTextBlock } from "src/utils/isTextBlock"; 35 + import { focusPage } from "src/utils/focusPage"; 35 36 36 37 export type Block = { 37 38 factID: string; ··· 62 63 // Block handles all block level events like 63 64 // mouse events, keyboard events and longPress, and setting AreYouSure state 64 65 // and shared styling like padding and flex for list layouting 65 - 66 + let { rep } = useReplicache(); 66 67 let mouseHandlers = useBlockMouseHandlers(props); 67 68 let handleDrop = useHandleDrop({ 68 69 parent: props.parent,
+12
components/Blocks/useBlockMouseHandlers.ts
··· 8 8 import { getBlocksWithType } from "src/hooks/queries/useBlocks"; 9 9 import { focusBlock } from "src/utils/focusBlock"; 10 10 import { useIsMobile } from "src/hooks/isMobile"; 11 + import { scrollIntoViewIfNeeded } from "src/utils/scrollIntoViewIfNeeded"; 12 + import { elementId } from "src/utils/elementId"; 11 13 12 14 let debounce: number | null = null; 13 15 export function useBlockMouseHandlers(props: Block) { ··· 39 41 parent: props.parent, 40 42 }); 41 43 useUIState.getState().setSelectedBlock(props); 44 + 45 + // scroll to the page containing the block, if offscreen 46 + let parentPage = elementId.page(props.parent).container; 47 + setTimeout(() => { 48 + scrollIntoViewIfNeeded( 49 + document.getElementById(parentPage), 50 + false, 51 + "smooth", 52 + ); 53 + }, 50); 42 54 } 43 55 }, 44 56 [props, entity_set.permissions.write, isMobile],
+2 -2
components/Canvas.tsx
··· 169 169 if (!pub || !pub.publications) return null; 170 170 171 171 let pubRecord = pub.publications.record as PubLeafletPublication.Record; 172 - let showComments = pubRecord.preferences?.showComments; 173 - let showMentions = pubRecord.preferences?.showMentions; 172 + let showComments = pubRecord.preferences?.showComments !== false; 173 + let showMentions = pubRecord.preferences?.showMentions !== false; 174 174 175 175 return ( 176 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">
+5 -5
components/InteractionsPreview.tsx
··· 13 13 commentsCount: number; 14 14 tags?: string[]; 15 15 postUrl: string; 16 - showComments: boolean | undefined; 17 - showMentions: boolean | undefined; 16 + showComments: boolean; 17 + showMentions: boolean; 18 18 19 19 share?: boolean; 20 20 }) => { 21 21 let smoker = useSmoker(); 22 22 let interactionsAvailable = 23 - (props.quotesCount > 0 && props.showMentions !== false) || 23 + (props.quotesCount > 0 && props.showMentions) || 24 24 (props.showComments !== false && props.commentsCount > 0); 25 25 26 26 const tagsCount = props.tags?.length || 0; ··· 38 38 </> 39 39 )} 40 40 41 - {props.showMentions === false || props.quotesCount === 0 ? null : ( 41 + {!props.showMentions || props.quotesCount === 0 ? null : ( 42 42 <SpeedyLink 43 43 aria-label="Post quotes" 44 44 href={`${props.postUrl}?interactionDrawer=quotes`} ··· 47 47 <QuoteTiny /> {props.quotesCount} 48 48 </SpeedyLink> 49 49 )} 50 - {props.showComments === false || props.commentsCount === 0 ? null : ( 50 + {!props.showComments || props.commentsCount === 0 ? null : ( 51 51 <SpeedyLink 52 52 aria-label="Post comments" 53 53 href={`${props.postUrl}?interactionDrawer=comments`}
+6 -3
components/Pages/PublicationMetadata.tsx
··· 123 123 {tags && ( 124 124 <> 125 125 <AddTags /> 126 - <Separator classname="h-4!" /> 126 + {pubRecord?.preferences?.showMentions !== false || 127 + pubRecord?.preferences?.showComments !== false ? ( 128 + <Separator classname="h-4!" /> 129 + ) : null} 127 130 </> 128 131 )} 129 - {pubRecord?.preferences?.showMentions && ( 132 + {pubRecord?.preferences?.showMentions !== false && ( 130 133 <div className="flex gap-1 items-center"> 131 134 <QuoteTiny />— 132 135 </div> 133 136 )} 134 - {pubRecord?.preferences?.showComments && ( 137 + {pubRecord?.preferences?.showComments !== false && ( 135 138 <div className="flex gap-1 items-center"> 136 139 <CommentTiny />— 137 140 </div>
+2 -2
components/PostListing.tsx
··· 96 96 quotesCount={quotes} 97 97 commentsCount={comments} 98 98 tags={tags} 99 - showComments={pubRecord?.preferences?.showComments} 100 - showMentions={pubRecord?.preferences?.showMentions} 99 + showComments={pubRecord?.preferences?.showComments !== false} 100 + showMentions={pubRecord?.preferences?.showMentions !== false} 101 101 share 102 102 /> 103 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 };
+2 -2
components/ThemeManager/Pickers/PageWidthSetter.tsx
··· 89 89 <div 90 90 className={`w-full cursor-pointer ${selectedPreset === "default" ? "text-[#595959]" : "text-[#969696]"}`} 91 91 > 92 - default (624px) 92 + default ({defaultPreset}px) 93 93 </div> 94 94 </Radio> 95 95 </label> ··· 111 111 <div 112 112 className={`w-full cursor-pointer ${selectedPreset === "wide" ? "text-[#595959]" : "text-[#969696]"}`} 113 113 > 114 - wide (756px) 114 + wide ({widePreset}px) 115 115 </div> 116 116 </Radio> 117 117 </label>
+2 -2
components/ThemeManager/PubPickers/PubPageWidthSetter.tsx
··· 76 76 <div 77 77 className={`w-full cursor-pointer ${selectedPreset === "default" ? "text-[#595959]" : "text-[#969696]"}`} 78 78 > 79 - default (624px) 79 + default ({defaultPreset}px) 80 80 </div> 81 81 </Radio> 82 82 </label> ··· 98 98 <div 99 99 className={`w-full cursor-pointer ${selectedPreset === "wide" ? "text-[#595959]" : "text-[#969696]"}`} 100 100 > 101 - wide (756px) 101 + wide ({widePreset}px) 102 102 </div> 103 103 </Radio> 104 104 </label>
+1 -35
components/ThemeManager/PublicationThemeProvider.tsx
··· 171 171 ...localOverrides, 172 172 showPageBackground, 173 173 }; 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, 188 - "rgb", 189 - ), 190 - ) 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]; 174 + 208 175 return { 209 176 ...newTheme, 210 - accentContrast: newAccentContrast, 211 177 }; 212 178 }, [pubTheme, localOverrides, showPageBackground]); 213 179 return {
+10 -10
components/ThemeManager/ThemeProvider.tsx
··· 134 134 // pageBg should inherit from leafletBg 135 135 const bgPage = 136 136 !showPageBackground && !hasBackgroundImage ? bgLeaflet : bgPageProp; 137 - // set accent contrast to the accent color that has the highest contrast with the page background 137 + 138 138 let accentContrast; 139 - 140 - //sorting the accents by contrast on background 141 139 let sortedAccents = [accent1, accent2].sort((a, b) => { 140 + // sort accents by contrast against the background 142 141 return ( 143 142 getColorDifference( 144 143 colorToString(b, "rgb"), ··· 150 149 ) 151 150 ); 152 151 }); 153 - 154 - // if the contrast-y accent is too similar to the primary text color, 155 - // and the not contrast-y option is different from the backgrond, 156 - // then use the not contrasty option 157 - 158 152 if ( 153 + // if the contrast-y accent is too similar to text color 159 154 getColorDifference( 160 155 colorToString(sortedAccents[0], "rgb"), 161 156 colorToString(primary, "rgb"), 162 157 ) < 0.15 && 158 + // and if the other accent is different enough from the background 163 159 getColorDifference( 164 160 colorToString(sortedAccents[1], "rgb"), 165 161 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 166 - ) > 0.08 162 + ) > 0.31 167 163 ) { 164 + //then choose the less contrast-y accent 168 165 accentContrast = sortedAccents[1]; 169 - } else accentContrast = sortedAccents[0]; 166 + } else { 167 + // otherwise, choose the more contrast-y option 168 + accentContrast = sortedAccents[0]; 169 + } 170 170 171 171 useEffect(() => { 172 172 if (local) return;
+1 -1
lexicons/api/lexicons.ts
··· 1816 1816 }, 1817 1817 showPrevNext: { 1818 1818 type: 'boolean', 1819 - default: false, 1819 + default: true, 1820 1820 }, 1821 1821 }, 1822 1822 },
+1 -1
lexicons/pub/leaflet/publication.json
··· 58 58 }, 59 59 "showPrevNext": { 60 60 "type": "boolean", 61 - "default": false 61 + "default": true 62 62 } 63 63 } 64 64 },
+1 -1
lexicons/src/publication.ts
··· 28 28 showInDiscover: { type: "boolean", default: true }, 29 29 showComments: { type: "boolean", default: true }, 30 30 showMentions: { type: "boolean", default: true }, 31 - showPrevNext: { type: "boolean", default: false }, 31 + showPrevNext: { type: "boolean", default: true }, 32 32 }, 33 33 }, 34 34 theme: {
+6
src/utils/timeAgo.ts
··· 6 6 const diffMinutes = Math.floor(diffSeconds / 60); 7 7 const diffHours = Math.floor(diffMinutes / 60); 8 8 const diffDays = Math.floor(diffHours / 24); 9 + const diffWeeks = Math.floor(diffDays / 7); 10 + const diffMonths = Math.floor(diffDays / 30); 9 11 const diffYears = Math.floor(diffDays / 365); 10 12 11 13 if (diffYears > 0) { 12 14 return `${diffYears} year${diffYears === 1 ? "" : "s"} ago`; 15 + } else if (diffMonths > 0) { 16 + return `${diffMonths} month${diffMonths === 1 ? "" : "s"} ago`; 17 + } else if (diffWeeks > 0) { 18 + return `${diffWeeks} week${diffWeeks === 1 ? "" : "s"} ago`; 13 19 } else if (diffDays > 0) { 14 20 return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; 15 21 } else if (diffHours > 0) {