a tool for shared writing and social publishing

fixed a bunch of small things

+125 -108
+1 -20
app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx
··· 1 1 import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 2 2 import { AppBskyFeedDefs, AppBskyFeedPost } from "@atproto/api"; 3 - import { Separator } from "components/Layout"; 4 - import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 5 - import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 6 - import { CommentTiny } from "components/Icons/CommentTiny"; 7 - import { QuoteTiny } from "components/Icons/QuoteTiny"; 8 - import { ThreadLink, QuotesLink } from "../PostLinks"; 9 - import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 10 - import { 11 - BlueskyEmbed, 12 - PostNotAvailable, 13 - } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; 14 - import { BlueskyRichText } from "components/Blocks/BlueskyPostBlock/BlueskyRichText"; 15 - import { openPage } from "../PostPages"; 3 + import { PostNotAvailable } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; 16 4 import { BskyPostContent } from "../BskyPostContent"; 17 5 18 6 export const PubBlueskyPostBlock = (props: { ··· 21 9 pageId?: string; 22 10 }) => { 23 11 let post = props.post; 24 - 25 - const handleOpenThread = () => { 26 - openPage(props.pageId ? { type: "doc", id: props.pageId } : undefined, { 27 - type: "thread", 28 - uri: post.uri, 29 - }); 30 - }; 31 12 32 13 switch (true) { 33 14 case AppBskyFeedDefs.isBlockedPost(post) ||
+1 -2
app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx
··· 98 98 showBlueskyLink={true} 99 99 quoteEnabled 100 100 replyEnabled 101 - onEmbedClick={(e) => e.stopPropagation()} 102 - className="relative rounded cursor-pointer text-sm" 101 + className="relative rounded text-sm" 103 102 /> 104 103 ); 105 104 }
+35 -47
app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx
··· 25 25 showEmbed?: boolean; 26 26 compactEmbed?: boolean; 27 27 showBlueskyLink?: boolean; 28 - onEmbedClick?: (e: React.MouseEvent) => void; 29 28 quoteEnabled?: boolean; 30 29 replyEnabled?: boolean; 31 30 replyOnClick?: (e: React.MouseEvent) => void; 32 - replyLine?: { 33 - onToggle: (e: React.MouseEvent) => void; 34 - }; 35 31 }) { 36 32 const { 37 33 post, ··· 40 36 showEmbed = true, 41 37 compactEmbed = false, 42 38 showBlueskyLink = true, 43 - onEmbedClick, 44 39 quoteEnabled, 45 40 replyEnabled, 46 41 replyOnClick, 47 - replyLine, 48 42 } = props; 49 43 50 44 const record = post.record as AppBskyFeedPost.Record; ··· 52 46 const url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; 53 47 54 48 return ( 55 - // pointer events non so that is there is a replyLine, it can be clicked even though its underneath the postContent (buttons here have pointer-events-auto applied to make them clickable) 56 - <div className="bskyPost relative flex flex-col w-full pointer-events-none"> 57 - <div className={`flex gap-2 text-left w-full ${props.className}`}> 58 - <div className="flex flex-col items-start shrink-0 w-fit"> 49 + <div className={`bskyPost relative flex flex-col w-full `}> 50 + <button 51 + className="absolute inset-0" 52 + onClick={() => { 53 + openPage(parent, { type: "thread", uri: post.uri }); 54 + }} 55 + /> 56 + 57 + <div 58 + className={`flex gap-2 text-left w-full pointer-events-none ${props.className}`} 59 + > 60 + <div className="flex flex-col items-start shrink-0 w-fit pointer-events-auto"> 59 61 <Avatar 60 62 src={post.author.avatar} 61 63 displayName={post.author.displayName} 62 64 size={avatarSize ? avatarSize : "medium"} 63 65 /> 64 66 </div> 65 - <div 66 - className={`flex flex-col min-w-0 w-full z-0 ${props.replyLine ? "mt-2" : ""}`} 67 - > 68 - <button 69 - className={`bskyPostTextContent flex flex-col grow text-left w-full pointer-events-auto ${props.avatarSize === "small" ? "mt-0.5" : props.avatarSize === "large" ? "mt-2" : "mt-1"}`} 70 - onClick={() => { 71 - openPage(parent, { type: "thread", uri: post.uri }); 72 - }} 67 + <div className={`flex flex-col min-w-0 w-full mb-2`}> 68 + <div 69 + className={`bskyPostTextContent flex flex-col grow text-left w-full ${props.avatarSize === "small" ? "mt-0.5" : props.avatarSize === "large" ? "mt-2" : "mt-1"}`} 73 70 > 74 71 <PostInfo 75 72 displayName={post.author.displayName} ··· 82 79 <BlueskyRichText record={record} /> 83 80 </div> 84 81 {showEmbed && post.embed && ( 85 - <div onClick={onEmbedClick}> 82 + <div 83 + className="pointer-events-auto relative" 84 + onClick={(e) => e.stopPropagation()} 85 + > 86 86 <BlueskyEmbed 87 + parent={parent} 87 88 embed={post.embed} 88 89 compact={compactEmbed} 89 90 postUrl={url} ··· 92 93 </div> 93 94 )} 94 95 </div> 95 - </button> 96 + </div> 96 97 {props.showBlueskyLink || 97 98 (props.post.quoteCount && props.post.quoteCount > 0) || 98 99 (props.post.replyCount && props.post.replyCount > 0) ? ( ··· 137 138 quoteEnabled?: boolean; 138 139 replyEnabled?: boolean; 139 140 replyOnClick?: (e: React.MouseEvent) => void; 140 - replyLine?: { 141 - onToggle: (e: React.MouseEvent) => void; 142 - }; 143 141 }) { 144 - const { post, parent, quoteEnabled, replyEnabled, replyOnClick, replyLine } = 145 - props; 142 + const { post, parent, quoteEnabled, replyEnabled, replyOnClick } = props; 146 143 147 144 const record = post.record as AppBskyFeedPost.Record; 148 145 const postId = post.uri.split("/")[4]; ··· 150 147 151 148 return ( 152 149 <div className="bskyPost relative flex flex-col w-full"> 150 + <button 151 + className="absolute inset-0 " 152 + onClick={() => { 153 + openPage(parent, { type: "thread", uri: post.uri }); 154 + }} 155 + /> 153 156 <div className={`flex gap-2 text-left w-full ${props.className}`}> 154 - <div className="flex flex-col items-start shrink-0 w-fit"> 155 - <Avatar 156 - src={post.author.avatar} 157 - displayName={post.author.displayName} 158 - size="small" 159 - /> 160 - {replyLine && ( 161 - <button 162 - onClick={(e) => { 163 - replyLine.onToggle(e); 164 - }} 165 - className="relative w-full grow flex" 166 - aria-label="Toggle replies" 167 - > 168 - <div className="w-0.5 h-full bg-border-light mx-auto" /> 169 - </button> 170 - )} 171 - </div> 172 - <div 173 - className={`flex flex-col min-w-0 w-full z-0 ${replyLine ? "mb-2" : ""}`} 174 - > 157 + <Avatar 158 + src={post.author.avatar} 159 + displayName={post.author.displayName} 160 + size="small" 161 + /> 162 + <div className={`flex flex-col min-w-0 w-full`}> 175 163 <button 176 164 className="bskyPostTextContent flex flex-col grow mt-0.5 text-left text-xs text-tertiary" 177 165 onClick={() => { ··· 225 213 <div className={`font-bold text-secondary truncate`}> 226 214 {displayName} 227 215 </div> 228 - <div className="truncate items-end flex"> 216 + <div className="truncate items-end flex pointer-events-auto"> 229 217 <ProfilePopover 230 218 trigger={ 231 219 <div
+14 -4
app/lish/[did]/[publication]/[rkey]/DocumentPageRenderer.tsx
··· 15 15 import { extractCodeBlocks } from "./extractCodeBlocks"; 16 16 import { LeafletLayout } from "components/LeafletLayout"; 17 17 import { fetchPollData } from "./fetchPollData"; 18 - import { getDocumentPages, hasLeafletContent } from "src/utils/normalizeRecords"; 18 + import { 19 + getDocumentPages, 20 + hasLeafletContent, 21 + } from "src/utils/normalizeRecords"; 19 22 import { DocumentProvider } from "contexts/DocumentContext"; 20 23 import { LeafletContentProvider } from "contexts/LeafletContentContext"; 21 24 ··· 118 121 return ( 119 122 <DocumentProvider value={document}> 120 123 <LeafletContentProvider value={{ pages }}> 121 - <PublicationThemeProvider theme={document.theme} pub_creator={pub_creator} isStandalone={isStandalone}> 122 - <PublicationBackgroundProvider theme={document.theme} pub_creator={pub_creator}> 124 + <PublicationThemeProvider 125 + theme={document.theme} 126 + pub_creator={pub_creator} 127 + isStandalone={isStandalone} 128 + > 129 + <PublicationBackgroundProvider 130 + theme={document.theme} 131 + pub_creator={pub_creator} 132 + > 123 133 <LeafletLayout> 124 134 <PostPages 125 135 document_uri={document.uri} ··· 127 137 pubRecord={pubRecord} 128 138 profile={JSON.parse(JSON.stringify(profile.data))} 129 139 document={document} 130 - bskyPostData={bskyPostData} 140 + bskyPostData={JSON.parse(JSON.stringify(bskyPostData))} 131 141 did={did} 132 142 prerenderedCodeBlocks={prerenderedCodeBlocks} 133 143 pollData={pollData}
-2
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 28 28 import { PubCodeBlock } from "./Blocks/PubCodeBlock"; 29 29 import { AppBskyFeedDefs } from "@atproto/api"; 30 30 import { PubBlueskyPostBlock } from "./Blocks/PublishBskyPostBlock"; 31 - import { openPage } from "./PostPages"; 32 - import { PageLinkBlock } from "components/Blocks/PageLinkBlock"; 33 31 import { PublishedPageLinkBlock } from "./Blocks/PublishedPageBlock"; 34 32 import { PublishedPollBlock } from "./Blocks/PublishedPollBlock"; 35 33 import { PollData } from "./fetchPollData";
+19 -1
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 111 111 const pageKey = getPageKey(page); 112 112 const parentKey = parent ? getPageKey(parent) : undefined; 113 113 114 + // Check if the page is already open 115 + const currentState = usePostPageUIState.getState(); 116 + const existingPageIndex = currentState.pages.findIndex( 117 + (p) => getPageKey(p) === pageKey, 118 + ); 119 + 120 + // If page is already open, just scroll to it 121 + if (existingPageIndex !== -1) { 122 + if (options?.scrollIntoView !== false) { 123 + scrollIntoView(`post-page-${pageKey}`); 124 + } 125 + return; 126 + } 127 + 114 128 flushSync(() => { 115 129 usePostPageUIState.setState((state) => { 116 130 let parentPosition = state.pages.findIndex( 117 131 (s) => getPageKey(s) === parentKey, 118 132 ); 133 + // Close any pages after the parent and add the new page 119 134 return { 120 135 pages: 121 136 parentPosition === -1 ··· 127 142 }); 128 143 129 144 if (options?.scrollIntoView !== false) { 130 - scrollIntoView(`post-page-${pageKey}`); 145 + // Use requestAnimationFrame to ensure the DOM has been painted before scrolling 146 + requestAnimationFrame(() => { 147 + scrollIntoView(`post-page-${pageKey}`); 148 + }); 131 149 } 132 150 }; 133 151
+29 -26
app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx
··· 130 130 131 131 {/* Replies */} 132 132 {post.replies && post.replies.length > 0 && ( 133 - <div className="threadReplies flex flex-col mt-4 pt-1 border-t border-border-light w-full"> 133 + <div className="threadReplies flex flex-col mt-4 pt-4 border-t border-border-light w-full"> 134 134 <Replies 135 135 replies={post.replies as any[]} 136 136 pageUri={post.post.uri} ··· 170 170 } 171 171 172 172 return ( 173 - <div className="threadGrandparentPost flex gap-2 relative w-full pl-1"> 174 - <div className="absolute top-0 bottom-0 left-1 w-5 "> 173 + <div className="threadGrandparentPost flex gap-2 relative w-full pl-[6px] pb-2"> 174 + <div className="absolute top-0 bottom-0 left-[6px] w-5 "> 175 175 <div className="bg-border-light w-[2px] h-full mx-auto" /> 176 176 </div> 177 177 <CompactBskyPostContent ··· 211 211 : replies; 212 212 213 213 return ( 214 - <div className="replies flex flex-col gap-0 pt-2 pb-1 pointer-events-none"> 214 + <div className="replies flex flex-col gap-0 w-full"> 215 215 {sortedReplies.map((reply, index) => { 216 216 if (AppBskyFeedDefs.isNotFoundPost(reply)) { 217 217 return ( ··· 285 285 const hasReplies = props.post.replies && props.post.replies.length > 0; 286 286 287 287 return ( 288 - <div className="flex h-fit"> 288 + <div className="flex h-fit relative"> 289 289 {props.depth > 0 && ( 290 - <button 291 - className="replyLine relative w-6 h-auto -mr-6 pointer-events-auto" 292 - onClick={() => { 293 - props.toggleCollapsed(parentPostUri); 294 - }} 295 - > 296 - <div className="bg-border-light w-[2px] h-full mx-auto" /> 297 - </button> 290 + <> 291 + <div className="absolute replyLine top-0 bottom-0 left-0 w-6 pointer-events-none "> 292 + <div className="bg-border-light w-[2px] h-full mx-auto" /> 293 + </div> 294 + <button 295 + className="absolute top-0 bottom-0 left-0 w-6 z-10" 296 + onClick={(e) => { 297 + e.preventDefault(); 298 + e.stopPropagation(); 299 + 300 + props.toggleCollapsed(parentPostUri); 301 + console.log("reply clicked"); 302 + }} 303 + /> 304 + </> 298 305 )} 299 306 <div 300 - className={`reply relative flex flex-col w-full ${props.depth === 0 && "mb-2"} ${props.depth > 0 && "pointer-events-none"}`} 307 + className={`reply relative flex flex-col w-full ${props.depth === 0 && "mb-3"}`} 301 308 > 302 309 <BskyPostContent 303 310 post={postView} ··· 309 316 replyOnClick={(e) => { 310 317 e.preventDefault(); 311 318 props.toggleCollapsed(post.post.uri); 312 - console.log(post.post.uri); 313 319 }} 314 - onEmbedClick={(e) => e.stopPropagation()} 315 320 className="text-sm" 316 321 /> 317 322 {hasReplies && props.depth < 3 && ( 318 - <div className="ml-[28px] flex pointer-events-none"> 323 + <div className="ml-[28px] flex grow "> 319 324 {!props.isCollapsed && ( 320 - <div className="grow pointer-events-none"> 321 - <Replies 322 - pageUri={pageUri} 323 - parentPostUri={post.post.uri} 324 - replies={props.post.replies as any[]} 325 - depth={props.depth + 1} 326 - parentAuthorDid={props.post.post.author.did} 327 - /> 328 - </div> 325 + <Replies 326 + pageUri={pageUri} 327 + parentPostUri={post.post.uri} 328 + replies={props.post.replies as any[]} 329 + depth={props.depth + 1} 330 + parentAuthorDid={props.post.post.author.did} 331 + /> 329 332 )} 330 333 </div> 331 334 )}
+15 -2
components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx
··· 11 11 AppBskyLabelerDefs, 12 12 } from "@atproto/api"; 13 13 import { Avatar } from "components/Avatar"; 14 + import { 15 + OpenPage, 16 + openPage, 17 + } from "app/lish/[did]/[publication]/[rkey]/PostPages"; 14 18 15 19 export const BlueskyEmbed = (props: { 16 20 embed: Exclude<AppBskyFeedDefs.PostView["embed"], undefined>; 17 21 postUrl?: string; 18 22 className?: string; 19 23 compact?: boolean; 24 + parent?: OpenPage; 20 25 }) => { 21 26 // check this file from bluesky for ref 22 27 // https://github.com/bluesky-social/social-app/blob/main/bskyembed/src/components/embed.tsx ··· 158 163 text = (record.value as AppBskyFeedPost.Record).text; 159 164 } 160 165 return ( 161 - <div className="bskyPostEmbed w-full flex gap-2 items-start relative overflow-hidden p-2! text-xs block-border"> 166 + <button 167 + className={`bskyPostEmbed text-left w-full flex gap-2 items-start relative overflow-hidden p-2! text-xs block-border hover:border-accent-contrast! `} 168 + onClick={(e) => { 169 + e.preventDefault(); 170 + e.stopPropagation(); 171 + 172 + openPage(props.parent, { type: "thread", uri: record.uri }); 173 + }} 174 + > 162 175 <Avatar 163 176 src={record.author?.avatar} 164 177 displayName={record.author?.displayName} ··· 192 205 : null}*/} 193 206 </div> 194 207 </div> 195 - </div> 208 + </button> 196 209 ); 197 210 } 198 211
+2 -2
components/Blocks/BlueskyPostBlock/index.tsx
··· 6 6 import { elementId } from "src/utils/elementId"; 7 7 import { focusBlock } from "src/utils/focusBlock"; 8 8 import { AppBskyFeedDefs, AppBskyFeedPost, RichText } from "@atproto/api"; 9 - import { BlueskyEmbed, PostNotAvailable } from "./BlueskyEmbed"; 9 + import { PostNotAvailable } from "./BlueskyEmbed"; 10 10 import { BlueskyPostEmpty } from "./BlueskyEmpty"; 11 - import { BlueskyRichText } from "./BlueskyRichText"; 11 + 12 12 import { Separator } from "components/Layout"; 13 13 import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 14 14 import { CommentTiny } from "components/Icons/CommentTiny";
+1 -1
components/Pages/Page.tsx
··· 113 113 } 114 114 ${cardBorderHidden && "sm:h-[calc(100%+48px)] h-[calc(100%+20px)] sm:-my-6 -my-3 sm:pt-6 pt-3"} 115 115 ${props.fullPageScroll && "max-w-full "} 116 - ${props.pageType === "doc" && !props.fullPageScroll ? (props.fixedWidth ? "sm:max-w-prose max-w-[var(--page-width-units)]" : "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]") : ""} 116 + ${props.pageType === "doc" && !props.fullPageScroll ? (props.fixedWidth ? "w-[10000px] sm:max-w-prose max-w-[var(--page-width-units)]" : "w-[10000px] sm:mx-0 max-w-[var(--page-width-units)]") : ""} 117 117 ${ 118 118 props.pageType === "canvas" && 119 119 !props.fullPageScroll &&
+8 -1
src/utils/scrollIntoView.ts
··· 6 6 threshold: number = 0.9, 7 7 ) { 8 8 const element = document.getElementById(elementId); 9 - scrollIntoViewIfNeeded(element, false, "smooth"); 9 + // Use double requestAnimationFrame to ensure the element is fully painted 10 + // before attempting to scroll. This fixes smooth scrolling when opening 11 + // pages from within other pages. 12 + requestAnimationFrame(() => { 13 + requestAnimationFrame(() => { 14 + scrollIntoViewIfNeeded(element, false, "smooth"); 15 + }); 16 + }); 10 17 }