a tool for shared writing and social publishing

cleaned up naming

+214 -225
+1 -1
app/lish/[did]/[publication]/[rkey]/Blocks/PublishBskyPostBlock.tsx
··· 113 113 {post.replyCount != null && post.replyCount > 0 && ( 114 114 <> 115 115 <ThreadLink 116 - threadUri={post.uri} 116 + postUri={post.uri} 117 117 parent={parent} 118 118 className="flex items-center gap-1 hover:text-accent-contrast" 119 119 onClick={(e) => e.stopPropagation()}
+8 -12
app/lish/[did]/[publication]/[rkey]/BlueskyQuotesPage.tsx
··· 87 87 const parent = { type: "quotes" as const, uri: quotesUri }; 88 88 89 89 return ( 90 - <div 91 - className="flex gap-2 relative py-2 px-2 hover:bg-bg-page rounded cursor-pointer" 92 - onClick={() => openPage(parent, { type: "thread", uri: post.uri })} 93 - > 94 - <BskyPostContent 95 - post={post} 96 - parent={parent} 97 - showEmbed={true} 98 - showBlueskyLink={true} 99 - onEmbedClick={(e) => e.stopPropagation()} 100 - /> 101 - </div> 90 + <BskyPostContent 91 + post={post} 92 + parent={parent} 93 + showEmbed={true} 94 + showBlueskyLink={true} 95 + onEmbedClick={(e) => e.stopPropagation()} 96 + className="relative py-2 px-2 hover:bg-bg-page rounded cursor-pointer" 97 + /> 102 98 ); 103 99 }
+139 -106
app/lish/[did]/[publication]/[rkey]/BskyPostContent.tsx
··· 8 8 import { Separator } from "components/Layout"; 9 9 import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 10 10 import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 11 - import { OpenPage } from "./PostPages"; 11 + import { OpenPage, openPage } from "./PostPages"; 12 12 import { ThreadLink, QuotesLink } from "./PostLinks"; 13 13 import { BlueskyLinkTiny } from "components/Icons/BlueskyLinkTiny"; 14 14 import { Avatar } from "components/Avatar"; ··· 25 25 showEmbed?: boolean; 26 26 showBlueskyLink?: boolean; 27 27 onEmbedClick?: (e: React.MouseEvent) => void; 28 - quoteCountOnClick?: (e: React.MouseEvent) => void; 29 - replyCountOnClick?: (e: React.MouseEvent) => void; 28 + quoteEnabled?: boolean; 29 + replyEnabled?: boolean; 30 + replyOnClick?: (e: React.MouseEvent) => void; 31 + replyLine?: { 32 + onToggle: (e: React.MouseEvent) => void; 33 + }; 30 34 }) { 31 35 const { 32 36 post, ··· 35 39 showEmbed = true, 36 40 showBlueskyLink = true, 37 41 onEmbedClick, 38 - quoteCountOnClick, 39 - replyCountOnClick, 42 + quoteEnabled, 43 + replyEnabled, 44 + replyOnClick, 45 + replyLine, 40 46 } = props; 41 47 42 48 const record = post.record as AppBskyFeedPost.Record; ··· 44 50 const url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; 45 51 46 52 return ( 47 - <> 48 - <Avatar 49 - src={post.author.avatar} 50 - displayName={post.author.displayName} 51 - size={props.avatarSize ? props.avatarSize : "medium"} 52 - /> 53 + <div className="bskyPost relative flex flex-col"> 54 + {replyLine && ( 55 + <div className="replyLine absolute top-0 bottom-0 shrink-0 w-6 bg-test"> 56 + <button 57 + onClick={(e) => { 58 + e.preventDefault(); 59 + e.stopPropagation(); 60 + replyLine.onToggle(e); 61 + console.log("clicked"); 62 + }} 63 + className="w-full h-full flex justify-center" 64 + aria-label="Toggle replies" 65 + > 66 + <div className="w-0.5 h-full bg-border-light" /> 67 + </button> 68 + </div> 69 + )} 70 + <button 71 + className={`flex gap-2 text-left ${props.className}`} 72 + onClick={() => { 73 + openPage(parent, { type: "thread", uri: post.uri }); 74 + console.log("do this"); 75 + }} 76 + > 77 + <Avatar 78 + src={post.author.avatar} 79 + displayName={post.author.displayName} 80 + size={props.avatarSize ? props.avatarSize : "medium"} 81 + /> 53 82 54 - <div className={`flex flex-col grow min-w-0 ${props.className}`}> 55 - <div 56 - className={`flex justify-between items-center gap-2 leading-tight `} 57 - > 58 - <div className="flex gap-2 items-center"> 59 - <div className="font-bold text-secondary"> 60 - {post.author.displayName} 83 + <div className={`bskyPostTextContent flex flex-col grow min-w-0 mt-1 `}> 84 + <div 85 + className={`flex justify-between items-center gap-2 leading-tight `} 86 + > 87 + <div className="flex gap-2 items-center"> 88 + <div className="font-bold text-secondary"> 89 + {post.author.displayName} 90 + </div> 91 + <ProfilePopover 92 + trigger={ 93 + <div className="text-sm text-tertiary hover:underline"> 94 + @{post.author.handle} 95 + </div> 96 + } 97 + didOrHandle={post.author.handle} 98 + /> 61 99 </div> 62 - <ProfilePopover 63 - trigger={ 64 - <div className="text-sm text-tertiary hover:underline"> 65 - @{post.author.handle} 66 - </div> 67 - } 68 - didOrHandle={post.author.handle} 69 - /> 100 + <div className="text-sm text-tertiary"> 101 + {timeAgo(record.createdAt, { compact: true })} 102 + </div> 70 103 </div> 71 - <div className="text-sm text-tertiary"> 72 - {timeAgo(record.createdAt, { compact: true })} 104 + 105 + <div 106 + className={`flex flex-col gap-2 ${avatarSize === "large" ? "mt-0.5" : "mt-1"}`} 107 + > 108 + <div className="text-sm text-secondary"> 109 + <BlueskyRichText record={record} /> 110 + </div> 111 + {showEmbed && post.embed && ( 112 + <div onClick={onEmbedClick}> 113 + <BlueskyEmbed 114 + embed={post.embed} 115 + postUrl={url} 116 + className="text-sm" 117 + /> 118 + </div> 119 + )} 73 120 </div> 74 - </div> 75 121 76 - <div 77 - className={`flex flex-col gap-2 ${avatarSize === "large" ? "mt-0.5" : "mt-1"}`} 78 - > 79 - <div className="text-sm text-secondary"> 80 - <BlueskyRichText record={record} /> 81 - </div> 82 - {showEmbed && post.embed && ( 83 - <div onClick={onEmbedClick}> 84 - <BlueskyEmbed 85 - embed={post.embed} 86 - postUrl={url} 87 - className="text-sm" 88 - /> 122 + <div className={`flex gap-2 items-center justify-between mt-2`}> 123 + <PostCounts 124 + post={post} 125 + parent={parent} 126 + replyEnabled={replyEnabled} 127 + replyOnClick={replyOnClick} 128 + quoteEnabled={quoteEnabled} 129 + showBlueskyLink={showBlueskyLink} 130 + url={url} 131 + /> 132 + <div className="flex gap-3 items-center"> 133 + {showBlueskyLink && ( 134 + <> 135 + <a className="text-tertiary" target="_blank" href={url}> 136 + <BlueskyLinkTiny /> 137 + </a> 138 + </> 139 + )} 89 140 </div> 90 - )} 91 - </div> 92 - 93 - <div className={`flex gap-2 items-center justify-between mt-2`}> 94 - <PostCounts 95 - post={post} 96 - parent={parent} 97 - replyCountOnClick={replyCountOnClick} 98 - quoteCountOnClick={quoteCountOnClick} 99 - showBlueskyLink={showBlueskyLink} 100 - url={url} 101 - /> 102 - <div className="flex gap-3 items-center"> 103 - {showBlueskyLink && ( 104 - <> 105 - <a className="text-tertiary" target="_blank" href={url}> 106 - <BlueskyLinkTiny /> 107 - </a> 108 - </> 109 - )} 110 141 </div> 111 142 </div> 112 - </div> 113 - </> 143 + </button> 144 + </div> 114 145 ); 115 146 } 116 147 117 148 function PostCounts(props: { 118 149 post: PostView; 119 150 parent?: OpenPage; 120 - quoteCountOnClick?: (e: React.MouseEvent) => void; 121 - replyCountOnClick?: (e: React.MouseEvent) => void; 151 + quoteEnabled?: boolean; 152 + replyEnabled?: boolean; 153 + replyOnClick?: (e: React.MouseEvent) => void; 122 154 showBlueskyLink: boolean; 123 155 url: string; 124 156 }) { 157 + const replyContent = props.post.replyCount != null && 158 + props.post.replyCount > 0 && ( 159 + <div className="postRepliesCount flex items-center gap-1 text-tertiary text-xs"> 160 + <CommentTiny /> 161 + {props.post.replyCount} 162 + </div> 163 + ); 164 + 165 + const quoteContent = props.post.quoteCount != null && 166 + props.post.quoteCount > 0 && ( 167 + <div className="postQuoteCount flex items-center gap-1 text-tertiary text-xs"> 168 + <QuoteTiny /> 169 + {props.post.quoteCount} 170 + </div> 171 + ); 172 + 125 173 return ( 126 174 <div className="postCounts flex gap-2 items-center"> 127 - {props.post.replyCount != null && props.post.replyCount > 0 && ( 128 - <> 129 - {props.replyCountOnClick ? ( 130 - <ThreadLink 131 - threadUri={props.post.uri} 132 - parent={parent} 133 - className="relative postRepliesLink flex items-center gap-1 text-tertiary text-xs hover:text-accent-contrast" 134 - onClick={props.replyCountOnClick} 135 - > 136 - <CommentTiny /> 137 - {props.post.replyCount} 138 - </ThreadLink> 139 - ) : ( 140 - <div className="postRepliesCount flex items-center gap-1 text-tertiary text-xs"> 141 - <CommentTiny /> 142 - {props.post.replyCount} 143 - </div> 144 - )} 145 - </> 146 - )} 147 - {props.post.quoteCount != null && props.post.quoteCount > 0 && ( 148 - <> 149 - {props.quoteCountOnClick ? ( 150 - <QuotesLink 151 - postUri={props.post.uri} 152 - parent={parent} 153 - className="relative flex items-center gap-1 text-tertiary text-xs hover:text-accent-contrast" 154 - onClick={props.quoteCountOnClick} 155 - > 156 - <QuoteTiny /> 157 - {props.post.quoteCount} 158 - </QuotesLink> 159 - ) : ( 160 - <div className="postQuoteCount flex items-center gap-1 text-tertiary text-xs"> 161 - <QuoteTiny /> 162 - {props.post.quoteCount} 163 - </div> 164 - )} 165 - </> 166 - )} 175 + {replyContent && 176 + (props.replyEnabled ? ( 177 + <ThreadLink 178 + postUri={props.post.uri} 179 + parent={props.parent} 180 + className="relative postRepliesLink hover:text-accent-contrast" 181 + onClick={props.replyOnClick} 182 + > 183 + {replyContent} 184 + </ThreadLink> 185 + ) : ( 186 + replyContent 187 + ))} 188 + {quoteContent && 189 + (props.quoteEnabled ? ( 190 + <QuotesLink 191 + postUri={props.post.uri} 192 + parent={props.parent} 193 + className="relative hover:text-accent-contrast" 194 + > 195 + {quoteContent} 196 + </QuotesLink> 197 + ) : ( 198 + quoteContent 199 + ))} 167 200 </div> 168 201 ); 169 202 }
+9 -20
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
··· 136 136 137 137 const parent = { type: "thread" as const, uri: q.uri }; 138 138 return ( 139 - <button 140 - className="flex gap-2 text-left" 141 - onClick={() => { 142 - openPage(undefined, { type: "thread", uri: q.uri }); 143 - }} 144 - > 145 - <BskyPostContent 146 - key={`mention-${index}`} 147 - post={post} 148 - parent={parent} 149 - showBlueskyLink={true} 150 - showEmbed={true} 151 - avatarSize="large" 152 - quoteCountOnClick={(e) => { 153 - e.stopPropagation(); 154 - e.preventDefault(); 155 - openPage(undefined, { type: "quotes", uri: q.uri }); 156 - }} 157 - /> 158 - </button> 139 + <BskyPostContent 140 + key={`mention-${index}`} 141 + post={post} 142 + parent={parent} 143 + showBlueskyLink={true} 144 + showEmbed={true} 145 + avatarSize="large" 146 + quoteEnabled={true} 147 + /> 159 148 ); 160 149 })} 161 150 </div>
+6 -4
app/lish/[did]/[publication]/[rkey]/PostLinks.tsx
··· 55 55 56 56 // Link component for opening thread pages with prefetching 57 57 export function ThreadLink(props: { 58 - threadUri: string; 58 + postUri: string; 59 59 parent?: OpenPage; 60 60 children: React.ReactNode; 61 61 className?: string; 62 62 onClick?: (e: React.MouseEvent) => void; 63 63 }) { 64 - const { threadUri, parent, children, className, onClick } = props; 64 + const { postUri, parent, children, className, onClick } = props; 65 65 66 66 const handleClick = (e: React.MouseEvent) => { 67 + e.stopPropagation(); 67 68 onClick?.(e); 68 69 if (e.defaultPrevented) return; 69 - openPage(parent, { type: "thread", uri: threadUri }); 70 + openPage(parent, { type: "thread", uri: postUri }); 70 71 }; 71 72 72 73 const handlePrefetch = () => { 73 - prefetchThread(threadUri); 74 + prefetchThread(postUri); 74 75 }; 75 76 76 77 return ( ··· 96 97 const { postUri, parent, children, className, onClick } = props; 97 98 98 99 const handleClick = (e: React.MouseEvent) => { 100 + e.stopPropagation(); 99 101 onClick?.(e); 100 102 if (e.defaultPrevented) return; 101 103 openPage(parent, { type: "quotes", uri: postUri });
+1 -1
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 297 297 <Fragment key={pageKey}> 298 298 <SandwichSpacer /> 299 299 <ThreadPageComponent 300 - threadUri={openPage.uri} 300 + parentUri={openPage.uri} 301 301 pageId={pageKey} 302 302 hasPageBackground={hasPageBackground} 303 303 pageOptions={
+50 -81
app/lish/[did]/[publication]/[rkey]/ThreadPage.tsx
··· 25 25 type ThreadType = ThreadViewPost | NotFoundPost | BlockedPost; 26 26 27 27 export function ThreadPage(props: { 28 - threadUri: string; 28 + parentUri: string; 29 29 pageId: string; 30 30 pageOptions?: React.ReactNode; 31 31 hasPageBackground: boolean; 32 32 }) { 33 - const { threadUri, pageId, pageOptions } = props; 34 - const drawer = useDrawerOpen(threadUri); 33 + const { parentUri: parentUri, pageId, pageOptions } = props; 34 + const drawer = useDrawerOpen(parentUri); 35 35 36 36 const { 37 37 data: thread, 38 38 isLoading, 39 39 error, 40 - } = useSWR(threadUri ? getThreadKey(threadUri) : null, () => 41 - fetchThread(threadUri), 40 + } = useSWR(parentUri ? getThreadKey(parentUri) : null, () => 41 + fetchThread(parentUri), 42 42 ); 43 43 44 44 return ( ··· 60 60 Failed to load thread 61 61 </div> 62 62 ) : thread ? ( 63 - <ThreadContent thread={thread} threadUri={threadUri} /> 63 + <ThreadContent thread={thread} parentUri={parentUri} /> 64 64 ) : null} 65 65 </div> 66 66 </PageWrapper> 67 67 ); 68 68 } 69 69 70 - function ThreadContent(props: { thread: ThreadType; threadUri: string }) { 71 - const { thread, threadUri } = props; 70 + function ThreadContent(props: { thread: ThreadType; parentUri: string }) { 71 + const { thread, parentUri: parentUri } = props; 72 72 const mainPostRef = useRef<HTMLDivElement>(null); 73 73 74 74 // Scroll the main post into view when the thread loads ··· 114 114 post={parent} 115 115 isMainPost={false} 116 116 showReplyLine={index < parents.length - 1 || true} 117 - threadUri={threadUri} 117 + parentUri={parentUri} 118 118 /> 119 119 </div> 120 120 ))} ··· 125 125 post={thread} 126 126 isMainPost={true} 127 127 showReplyLine={false} 128 - threadUri={threadUri} 128 + parentUri={parentUri} 129 129 /> 130 130 </div> 131 131 ··· 134 134 <div className="threadReplies flex flex-col mt-2 pt-2 border-t border-border-light"> 135 135 <Replies 136 136 replies={thread.replies as any[]} 137 - threadUri={threadUri} 137 + parentUri={parentUri} 138 138 depth={0} 139 139 parentAuthorDid={thread.post.author.did} 140 140 /> ··· 148 148 post: ThreadViewPost; 149 149 isMainPost: boolean; 150 150 showReplyLine: boolean; 151 - threadUri: string; 151 + parentUri: string; 152 152 }) { 153 - const { post, isMainPost, showReplyLine, threadUri } = props; 153 + const { post, isMainPost, showReplyLine, parentUri } = props; 154 154 const postView = post.post; 155 - const parent = { type: "thread" as const, uri: threadUri }; 155 + const parent = { type: "thread" as const, uri: parentUri }; 156 156 157 157 return ( 158 158 <div className="threadPost flex gap-2 relative"> ··· 165 165 parent={parent} 166 166 showBlueskyLink={true} 167 167 showEmbed={true} 168 - quoteCountOnClick={(e) => { 169 - e.stopPropagation(); 170 - e.preventDefault(); 171 - openPage(parent, { type: "quotes", uri: postView.uri }); 172 - }} 173 - replyCountOnClick={(e) => { 174 - e.stopPropagation(); 175 - e.preventDefault(); 176 - }} 168 + quoteEnabled 169 + replyEnabled 177 170 /> 178 171 </div> 179 172 ); ··· 181 174 182 175 function Replies(props: { 183 176 replies: (ThreadViewPost | NotFoundPost | BlockedPost)[]; 184 - threadUri: string; 185 177 depth: number; 186 178 parentAuthorDid?: string; 187 - parentUri?: string; 179 + parentUri: string; 188 180 }) { 189 - const { replies, threadUri, depth, parentAuthorDid, parentUri } = props; 181 + const { replies, depth, parentAuthorDid, parentUri } = props; 190 182 const collapsedThreads = useThreadState((s) => s.collapsedThreads); 191 183 const toggleCollapsed = useThreadState((s) => s.toggleCollapsed); 192 184 ··· 241 233 <ReplyPost 242 234 post={reply} 243 235 isLast={index === replies.length - 1 && !hasReplies} 244 - threadUri={threadUri} 245 - toggleCollapsed={(e) => { 246 - e.stopPropagation(); 247 - e.preventDefault(); 248 - if (parentUri) toggleCollapsed(parentUri); 249 - console.log("collapse?"); 250 - }} 236 + parentUri={parentUri} 237 + toggleCollapsed={(uri) => toggleCollapsed(uri)} 251 238 isCollapsed={isCollapsed} 252 239 depth={props.depth} 253 240 /> ··· 255 242 })} 256 243 {parentUri && depth > 0 && replies.length > 3 && ( 257 244 <ThreadLink 258 - threadUri={parentUri} 259 - parent={{ type: "thread", uri: threadUri }} 245 + postUri={parentUri} 246 + parent={{ type: "thread", uri: parentUri }} 260 247 className="flex justify-start text-sm text-accent-contrast h-fit hover:underline" 261 248 > 262 249 <div className="mx-[19px] w-0.5 h-[24px] bg-border-light" /> ··· 271 258 const ReplyPost = (props: { 272 259 post: ThreadViewPost; 273 260 isLast: boolean; 274 - threadUri: string; 275 - toggleCollapsed: (e: React.MouseEvent) => void; 261 + parentUri: string; 262 + toggleCollapsed: (uri: string) => void; 276 263 isCollapsed: boolean; 277 264 depth: number; 278 265 }) => { 279 - const { post, threadUri } = props; 266 + const { post, parentUri } = props; 280 267 const postView = post.post; 281 - const parent = { type: "thread" as const, uri: threadUri }; 282 268 283 269 const hasReplies = props.post.replies && props.post.replies.length > 0; 284 270 285 - // was in the middle of trying to get the right set of comments to close when this line is clicked 286 - // then i really need to style the parent and grandparent threads, hide some of the content unless its the main post 287 - // the thread line on them is also weird 288 271 return ( 289 272 <div className="threadReply relative flex flex-col"> 290 - {props.depth > 0 && ( 291 - <button 292 - onClick={(e) => { 293 - props.toggleCollapsed(e); 294 - }} 295 - className="replyThreadLine absolute top-0 bottom-0 left-1 z-0 cursor-pointer shrink-0 " 296 - aria-label={"Toggle replies"} 297 - > 298 - <div className="mx-[15px] w-0.5 h-full bg-border-light" /> 299 - </button> 300 - )} 301 - 302 - <button 303 - className="replyThreadPost flex gap-2 text-left relative py-2 px-2 rounded cursor-pointer" 304 - onClick={() => { 305 - openPage(parent, { type: "thread", uri: postView.uri }); 273 + <BskyPostContent 274 + post={postView} 275 + parent={{ type: "thread", uri: parentUri }} 276 + showEmbed={false} 277 + showBlueskyLink={false} 278 + replyLine={ 279 + props.depth > 0 280 + ? { 281 + onToggle: () => { 282 + props.toggleCollapsed(props.parentUri); 283 + console.log("click click"); 284 + }, 285 + } 286 + : undefined 287 + } 288 + quoteEnabled 289 + replyEnabled 290 + replyOnClick={(e) => { 291 + e.preventDefault(); 292 + props.toggleCollapsed(post.post.uri); 306 293 }} 307 - > 308 - <BskyPostContent 309 - post={postView} 310 - parent={parent} 311 - showEmbed={false} 312 - showBlueskyLink={false} 313 - quoteCountOnClick={(e) => { 314 - e.preventDefault(); 315 - e.stopPropagation(); 316 - openPage(parent, { type: "quotes", uri: postView.uri }); 317 - }} 318 - replyCountOnClick={(e) => { 319 - e.preventDefault(); 320 - e.stopPropagation(); 321 - props.toggleCollapsed(); 322 - }} 323 - onEmbedClick={(e) => e.stopPropagation()} 324 - className="text-sm z-10" 325 - /> 326 - </button> 294 + onEmbedClick={(e) => e.stopPropagation()} 295 + className="text-sm z-10" 296 + /> 327 297 {hasReplies && props.depth < 3 && ( 328 298 <div className="ml-[28px] flex"> 329 299 {!props.isCollapsed && ( ··· 331 301 <Replies 332 302 parentUri={postView.uri} 333 303 replies={props.post.replies as any[]} 334 - threadUri={threadUri} 335 304 depth={props.depth + 1} 336 305 parentAuthorDid={props.post.post.author.did} 337 306 /> ··· 342 311 343 312 {hasReplies && props.depth >= 3 && ( 344 313 <ThreadLink 345 - threadUri={props.post.post.uri} 346 - parent={{ type: "thread", uri: threadUri }} 314 + postUri={props.post.post.uri} 315 + parent={{ type: "thread", uri: parentUri }} 347 316 className="text-left ml-10 text-sm text-accent-contrast hover:underline" 348 317 > 349 318 View more replies