a tool for shared writing and social publishing
0

Configure Feed

Select the types of activity you want to include in your feed.

standard site posts open in interaction drawer on posts

celine (Jun 1, 2026, 12:46 AM EDT) fc10476a bf56508f

+325 -138
+1 -1
app/(app)/lish/[did]/[publication]/PublicationPostItem.tsx
··· 129 129 <img 130 130 src={props.coverImageSrc} 131 131 alt={props.coverImageAlt || props.title || ""} 132 - className="w-full aspect-square object-cover rounded" 132 + className="w-full h-full aspect-square object-cover rounded" 133 133 /> 134 134 </div> 135 135 )}
+72 -7
app/(app)/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer.tsx
··· 14 14 import { DoubleArrowRightTiny } from "components/Icons/DoubleArrowRightTiny"; 15 15 import { ToggleGroup } from "components/ToggleGroup"; 16 16 import { useDocument } from "contexts/DocumentContext"; 17 - import { useEffect, useMemo, useRef } from "react"; 17 + import { useEffect, useMemo, useRef, useState } from "react"; 18 18 import { DrawerThread, DrawerThreadContext } from "./drawerThreadContext"; 19 19 import { useDrawerOpen } from "./useDrawerOpen"; 20 20 import { ThreadView } from "../ThreadPage"; 21 + import { StandardSitePostDrawerView } from "./StandardSitePostDrawerView"; 22 + import { useDocumentDiscussionData } from "./useDocumentDiscussionData"; 21 23 22 24 export const InteractionDrawer = (props: { 23 25 showPageBackground: boolean | undefined; ··· 37 39 }), 38 40 [props.document_uri], 39 41 ); 42 + 43 + // The innermost thread/quotes view opened within the drawer, if any. When 44 + // present it replaces the comments/mentions tabs. 45 + const activeThread = threadStack[threadStack.length - 1]; 46 + 47 + // A standard-site-post thread shows another post's own discussion. It's always 48 + // at the root of the stack (Bluesky threads opened from its mentions become 49 + // the active thread instead), so its comments/mentions toggle lives in the 50 + // drawer header in place of a Back button. Its data is fetched here too (SWR 51 + // dedupes with the view below) to drive that toggle. 52 + const sspUri = 53 + activeThread?.type === "standardSitePost" ? activeThread.uri : null; 54 + const ssp = useDocumentDiscussionData(sspUri ?? "", !!sspUri); 55 + const [sspTab, setSspTab] = useState<"comments" | "quotes">("comments"); 56 + useEffect(() => { 57 + setSspTab("comments"); 58 + }, [sspUri]); 59 + 40 60 // Reset the drawer's scroll to the top whenever we navigate between views, so 41 61 // a pushed thread (or a back navigation) doesn't start scrolled partway down. 42 62 const scrollRef = useRef<HTMLDivElement>(null); ··· 45 65 }, [threadStack.length]); 46 66 if (!drawer) return null; 47 67 68 + const sspCommentsAvailable = ssp.showComments && ssp.comments.length > 0; 69 + const sspMentionsAvailable = 70 + ssp.showMentions && ssp.quotesAndMentions.length > 0; 71 + const sspBothAvailable = sspCommentsAvailable && sspMentionsAvailable; 72 + let sspActiveTab: "comments" | "quotes" = sspTab; 73 + if (sspActiveTab === "comments" && !sspCommentsAvailable) 74 + sspActiveTab = "quotes"; 75 + if (sspActiveTab === "quotes" && !sspMentionsAvailable) 76 + sspActiveTab = "comments"; 77 + 48 78 const filteredQuotesAndMentions = props.quotesAndMentions.filter((q) => { 49 79 if (!q.link) return !props.pageId; // Direct mentions without quote context go to main page 50 80 const url = new URL(q.link); ··· 66 96 if (activeTab === "comments" && !commentsAvailable) activeTab = "quotes"; 67 97 if (activeTab === "quotes" && !mentionsAvailable) activeTab = "comments"; 68 98 69 - // The innermost thread/quotes view opened within the drawer, if any. When 70 - // present it replaces the comments/mentions tabs. 71 - const activeThread = threadStack[threadStack.length - 1]; 72 - 73 99 return ( 74 100 <> 75 101 <SandwichSpacer noWidth /> ··· 81 107 > 82 108 <div className="w-full flex items-center gap-2 mb-3"> 83 109 <div className="flex-1 min-w-0"> 84 - {activeThread ? ( 110 + {sspUri ? ( 111 + sspBothAvailable ? ( 112 + <ToggleGroup 113 + fullWidth 114 + value={sspActiveTab} 115 + onChange={(value, e) => { 116 + e?.preventDefault(); 117 + setSspTab(value); 118 + }} 119 + options={[ 120 + { 121 + value: "comments", 122 + label: 123 + ssp.comments.length > 0 124 + ? `Comments (${ssp.comments.length})` 125 + : "Comments", 126 + }, 127 + { 128 + value: "quotes", 129 + label: ( 130 + <div> 131 + Bluesky{" "} 132 + <span className="hidden sm:inline">Mentions</span>{" "} 133 + {ssp.quotesAndMentions.length > 0 && 134 + `(${ssp.quotesAndMentions.length})`} 135 + </div> 136 + ), 137 + }, 138 + ]} 139 + /> 140 + ) : ( 141 + <h4> 142 + {sspActiveTab === "quotes" 143 + ? `Bluesky Mentions${ssp.quotesAndMentions.length > 0 ? ` (${ssp.quotesAndMentions.length})` : ""}` 144 + : `Comments${ssp.comments.length > 0 ? ` (${ssp.comments.length})` : ""}`} 145 + </h4> 146 + ) 147 + ) : activeThread ? ( 85 148 <div className="flex items-center gap-2"> 86 149 {threadStack.length >= 2 && ( 87 150 <button ··· 144 207 </button> 145 208 </div> 146 209 <DrawerThreadContext.Provider value={drawerNav}> 147 - {activeThread ? ( 210 + {sspUri ? ( 211 + <StandardSitePostDrawerView uri={sspUri} tab={sspActiveTab} /> 212 + ) : activeThread ? ( 148 213 <ThreadView 149 214 parentUri={activeThread.uri} 150 215 initialTab={
+4 -3
app/(app)/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
··· 249 249 }} 250 250 > 251 251 <div className="flex gap-3 items-stretch italic rounded-md pt-1 "> 252 - <div className="relative flex"> 253 - <QuoteTiny className="shrink-0 text-tertiary bg-bg-page absolute -left-2 top-3" /> 254 - <div className="w-0.5 grow border border-border mx-auto my-2" /> 252 + <div className="flex flex-col w-4"> 253 + <div className="shrink-0 h-1 w-0.5 border border-border mx-auto mt-2" /> 254 + <QuoteTiny className="shrink-0 text-tertiary " /> 255 + <div className="w-0.5 grow border border-border mx-auto mb-2" /> 255 256 </div> 256 257 <PostContent 257 258 pollData={[]}
+64
app/(app)/lish/[did]/[publication]/[rkey]/Interactions/StandardSitePostDrawerView.tsx
··· 1 + "use client"; 2 + import { DotLoader } from "components/utils/DotLoader"; 3 + import { StandardSitePostItem } from "components/Blocks/StandardSitePostBlock/StandardSitePostItem"; 4 + import { DocumentProvider } from "contexts/DocumentContext"; 5 + import { LeafletContentProvider } from "contexts/LeafletContentContext"; 6 + import { CommentsDrawerContent } from "./Comments"; 7 + import { MentionsDrawerContent } from "./Quotes"; 8 + import { useDocumentDiscussionData } from "./useDocumentDiscussionData"; 9 + 10 + // Renders a referenced post's discussion inside the interaction drawer: the 11 + // post itself (as a StandardSitePostItem) at the top of the stack, then its 12 + // comments or Bluesky mentions for the active tab. The tab is driven by the 13 + // drawer header's toggle (InteractionDrawer), which shares the same fetch via 14 + // SWR — this view is otherwise self-contained. Pushed onto the drawer's thread 15 + // stack when a standard-site-post block's interactions are clicked from within 16 + // a published post body, mirroring how a Bluesky post opens its thread. 17 + export function StandardSitePostDrawerView(props: { 18 + uri: string; 19 + tab: "comments" | "quotes"; 20 + }) { 21 + const { 22 + isLoading, 23 + data, 24 + did, 25 + pages, 26 + documentContextValue, 27 + comments, 28 + quotesAndMentions, 29 + } = useDocumentDiscussionData(props.uri, true); 30 + 31 + return ( 32 + <div className="standardSitePostDiscussion flex flex-col"> 33 + <div className="standardSitePostBlock block-border overflow-hidden bg-bg-page mb-3"> 34 + <StandardSitePostItem uri={props.uri} hideInteractions /> 35 + </div> 36 + 37 + {!data && isLoading ? ( 38 + <div className="flex items-center justify-center gap-1 text-tertiary italic text-sm py-8"> 39 + <span>loading</span> 40 + <DotLoader /> 41 + </div> 42 + ) : documentContextValue ? ( 43 + <LeafletContentProvider value={{ pages }}> 44 + <DocumentProvider value={documentContextValue}> 45 + {props.tab === "comments" ? ( 46 + <CommentsDrawerContent 47 + document_uri={props.uri} 48 + comments={comments} 49 + /> 50 + ) : ( 51 + <> 52 + <hr className="border-border-light mt-3 mb-6" /> 53 + <MentionsDrawerContent 54 + did={did} 55 + quotesAndMentions={quotesAndMentions} 56 + /> 57 + </> 58 + )} 59 + </DocumentProvider> 60 + </LeafletContentProvider> 61 + ) : null} 62 + </div> 63 + ); 64 + }
+7 -2
app/(app)/lish/[did]/[publication]/[rkey]/Interactions/drawerThreadContext.tsx
··· 4 4 import { openDrawerThread } from "./Interactions"; 5 5 6 6 // A thread or quotes view that can be shown inside the interaction drawer. 7 + // `standardSitePost` shows a referenced post's own discussion (the post itself 8 + // plus its comments / Bluesky mentions) rather than a Bluesky thread. 7 9 export type DrawerThread = 8 10 | { type: "thread"; uri: string } 9 - | { type: "quotes"; uri: string }; 11 + | { type: "quotes"; uri: string } 12 + | { type: "standardSitePost"; uri: string }; 10 13 11 14 type DrawerThreadNav = { 12 15 push: (thread: DrawerThread) => void; ··· 24 27 const drawerNav = useContext(DrawerThreadContext); 25 28 return (parent: OpenPage | undefined, thread: DrawerThread) => { 26 29 if (drawerNav) drawerNav.push(thread); 27 - else openPage(parent, thread); 30 + // standardSitePost only exists inside the drawer; it has no page form, so 31 + // it's never reached here without a drawer-aware provider in scope. 32 + else if (thread.type !== "standardSitePost") openPage(parent, thread); 28 33 }; 29 34 } 30 35
+75
app/(app)/lish/[did]/[publication]/[rkey]/Interactions/useDocumentDiscussionData.ts
··· 1 + "use client"; 2 + import useSWR from "swr"; 3 + import { AtUri } from "@atproto/api"; 4 + import { callRPC } from "app/api/rpc/client"; 5 + import type { DocumentContextValue } from "contexts/DocumentContext"; 6 + import { 7 + getDocumentPages, 8 + type NormalizedDocument, 9 + type NormalizedPublication, 10 + } from "src/utils/normalizeRecords"; 11 + import type { Comment } from "./Comments"; 12 + 13 + export type DocumentInteractionsData = { 14 + comments: Comment[]; 15 + quotesAndMentions: { uri: string; link?: string }[]; 16 + document: NormalizedDocument | null; 17 + publication: NormalizedPublication | null; 18 + }; 19 + 20 + // Fetches a document's comments and Bluesky mentions and builds the Document / 21 + // LeafletContent context values that the shared drawer content (Comments / 22 + // Quotes) reads off `useDocument`. Used by the DiscussionModal and the 23 + // standard-site-post drawer view, which both render another document's 24 + // discussion outside of that document's own post page. 25 + export function useDocumentDiscussionData( 26 + document_uri: string, 27 + enabled: boolean, 28 + ) { 29 + const swr = useSWR(enabled ? ["doc_interactions", document_uri] : null, () => 30 + callRPC("get_document_interactions", { document_uri }), 31 + ); 32 + const data = swr.data as unknown as DocumentInteractionsData | undefined; 33 + 34 + let did = ""; 35 + try { 36 + did = new AtUri(document_uri).host; 37 + } catch { 38 + did = ""; 39 + } 40 + 41 + const documentRecord = data?.document ?? null; 42 + const pages = documentRecord ? (getDocumentPages(documentRecord) ?? []) : []; 43 + 44 + // The drawer content only reads uri / normalizedDocument / normalizedPublication 45 + // off the document context; the rest is filled with sensible defaults. 46 + const documentContextValue: DocumentContextValue | null = documentRecord 47 + ? ({ 48 + uri: document_uri, 49 + normalizedDocument: documentRecord, 50 + normalizedPublication: data?.publication ?? null, 51 + theme: null, 52 + prevNext: null, 53 + quotesAndMentions: data?.quotesAndMentions ?? [], 54 + publication: null, 55 + commentsCount: data?.comments.length ?? 0, 56 + mentions: [], 57 + leafletId: null, 58 + recommendsCount: 0, 59 + } as unknown as DocumentContextValue) 60 + : null; 61 + 62 + const prefs = data?.publication?.preferences; 63 + 64 + return { 65 + isLoading: swr.isLoading, 66 + data, 67 + did, 68 + pages, 69 + documentContextValue, 70 + comments: (data?.comments ?? []) as Comment[], 71 + quotesAndMentions: data?.quotesAndMentions ?? [], 72 + showComments: prefs?.showComments !== false, 73 + showMentions: prefs?.showMentions !== false, 74 + }; 75 + }
+18 -12
app/(app)/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 248 248 </div> 249 249 ); 250 250 } 251 + // default to "medium" to match the draft (StandardSitePostBlock), 252 + // since the publish step omits size when it hasn't been explicitly set 251 253 let size: "large" | "medium" | "small" = 252 254 b.block.size === "large" 253 255 ? "large" 254 - : b.block.size === "medium" 255 - ? "medium" 256 - : "small"; 256 + : b.block.size === "small" 257 + ? "small" 258 + : "medium"; 257 259 return ( 258 260 <div className={className} {...blockProps}> 259 - <WithStandardSitePostPublicationTheme 260 - post={post} 261 - enabled={b.block.showPublicationTheme !== false} 262 - > 263 - <StandardSitePostItemView 261 + <div className="standardSitePostBlock block-border overflow-hidden w-full"> 262 + <WithStandardSitePostPublicationTheme 264 263 post={post} 265 - size={size} 266 - currentPublicationUri={currentPublicationUri} 267 - /> 268 - </WithStandardSitePostPublicationTheme> 264 + enabled={b.block.showPublicationTheme !== false} 265 + > 266 + <div className="bg-bg-page"> 267 + <StandardSitePostItemView 268 + post={post} 269 + size={size} 270 + currentPublicationUri={currentPublicationUri} 271 + /> 272 + </div> 273 + </WithStandardSitePostPublicationTheme> 274 + </div> 269 275 </div> 270 276 ); 271 277 }
+1 -1
components/Blocks/BlueskyPostBlock/BlueskyEmbed.tsx
··· 29 29 case AppBskyEmbedImages.isView(props.embed): 30 30 let imageEmbed = props.embed; 31 31 return ( 32 - <div className="imageEmbed flex flex-wrap rounded-md w-full overflow-hidden"> 32 + <div className="imageEmbed flex flex-wrap rounded-lg w-full overflow-hidden"> 33 33 {imageEmbed.images.map( 34 34 ( 35 35 image: {
+18 -4
components/Blocks/StandardSitePostBlock/StandardSitePostItem.tsx
··· 26 26 uri, 27 27 size = "medium", 28 28 currentPublicationUri, 29 + pageWidth, 30 + hideInteractions, 29 31 }: { 30 32 uri: string; 31 33 size?: StandardSitePostSize; 32 34 currentPublicationUri?: string | null; 35 + pageWidth?: number; 36 + hideInteractions?: boolean; 33 37 }) { 34 38 const { data, isLoading } = useStandardSitePost(uri); 35 39 const { rootEntity } = useReplicache(); 36 - const pageWidth = useEntity(rootEntity, "theme/page-width")?.data.value; 40 + const postPageWidth = useEntity(rootEntity, "theme/page-width")?.data.value; 37 41 38 42 if (isLoading) { 39 43 return ( 40 - <StandardSitePostItemPlaceholder size={size} pageWidth={pageWidth} /> 44 + <StandardSitePostItemPlaceholder 45 + size={size} 46 + pageWidth={pageWidth ? pageWidth : postPageWidth} 47 + /> 41 48 ); 42 49 } 43 50 ··· 49 56 <StandardSitePostItemView 50 57 post={data} 51 58 size={size} 59 + pageWidth={pageWidth} 52 60 currentPublicationUri={currentPublicationUri} 61 + hideInteractions={hideInteractions} 53 62 /> 54 63 ); 55 64 } ··· 155 164 post, 156 165 size = "medium", 157 166 currentPublicationUri, 167 + hideInteractions, 168 + pageWidth: pageWidthProp, 158 169 }: { 159 170 post: StandardSitePostData; 160 171 size?: StandardSitePostSize; 161 172 currentPublicationUri?: string | null; 173 + hideInteractions?: boolean; 174 + pageWidth?: number; 162 175 }) { 163 176 const docUrl = getDocumentURL( 164 177 post.record, ··· 188 201 : undefined; 189 202 190 203 const { rootEntity } = useReplicache(); 191 - const pageWidth = useEntity(rootEntity, "theme/page-width")?.data.value; 204 + const themePageWidth = useEntity(rootEntity, "theme/page-width")?.data.value; 205 + const pageWidth = pageWidthProp ?? themePageWidth; 192 206 193 207 const publicationPrefs = post.publication?.record?.preferences; 194 208 const showComments = publicationPrefs?.showComments !== false; ··· 196 210 const showRecommends = publicationPrefs?.showRecommends !== false; 197 211 const commentsCount = showComments ? post.commentsCount : 0; 198 212 199 - const interactions = ( 213 + const interactions = hideInteractions ? undefined : ( 200 214 <InteractionPreview 201 215 quotesCount={post.mentionsCount} 202 216 commentsCount={commentsCount}
+52 -105
components/DiscussionModal.tsx
··· 1 1 "use client"; 2 2 import { useEffect, useState } from "react"; 3 - import useSWR from "swr"; 4 - import { AtUri } from "@atproto/api"; 5 3 import { Modal } from "./Modal"; 6 4 import { ToggleGroup } from "./ToggleGroup"; 7 5 import { SpeedyLink } from "./SpeedyLink"; 8 - import { ExternalLinkTiny } from "./Icons/ExternalLinkTiny"; 9 6 import { DotLoader } from "./utils/DotLoader"; 10 - import { callRPC } from "app/api/rpc/client"; 11 - import { 12 - DocumentProvider, 13 - type DocumentContextValue, 14 - } from "contexts/DocumentContext"; 7 + import { DocumentProvider } from "contexts/DocumentContext"; 15 8 import { LeafletContentProvider } from "contexts/LeafletContentContext"; 16 - import { 17 - getDocumentPages, 18 - type NormalizedDocument, 19 - type NormalizedPublication, 20 - } from "src/utils/normalizeRecords"; 21 - import { 22 - CommentsDrawerContent, 23 - type Comment, 24 - } from "app/(app)/lish/[did]/[publication]/[rkey]/Interactions/Comments"; 9 + import { CommentsDrawerContent } from "app/(app)/lish/[did]/[publication]/[rkey]/Interactions/Comments"; 25 10 import { MentionsDrawerContent } from "app/(app)/lish/[did]/[publication]/[rkey]/Interactions/Quotes"; 26 11 import { decodeQuotePosition } from "app/(app)/lish/[did]/[publication]/[rkey]/quotePosition"; 12 + import { useDocumentDiscussionData } from "app/(app)/lish/[did]/[publication]/[rkey]/Interactions/useDocumentDiscussionData"; 27 13 import { GoToArrow } from "./Icons/GoToArrow"; 28 14 import { ButtonPrimary } from "./Buttons"; 29 - 30 - type InteractionsData = { 31 - comments: Comment[]; 32 - quotesAndMentions: { uri: string; link?: string }[]; 33 - document: NormalizedDocument | null; 34 - publication: NormalizedPublication | null; 35 - }; 15 + import { StandardSitePostItem } from "./Blocks/StandardSitePostBlock/StandardSitePostItem"; 36 16 37 17 // A self-contained modal that renders a post's comments and Bluesky mentions 38 18 // with a toggle between them. Used from post listings, the reader feed, and ··· 52 32 showMentions: boolean; 53 33 pageId?: string; 54 34 }) { 55 - const did = new AtUri(props.document_uri).host; 56 - 57 35 const commentsAvailable = props.showComments && props.commentsCount > 0; 58 36 const mentionsAvailable = props.showMentions && props.quotesCount > 0; 59 37 const bothAvailable = commentsAvailable && mentionsAvailable; ··· 66 44 if (props.open) setTab(commentsAvailable ? "comments" : "quotes"); 67 45 }, [props.open]); 68 46 69 - const swr = useSWR( 70 - props.open ? ["doc_interactions", props.document_uri] : null, 71 - () => 72 - callRPC("get_document_interactions", { 73 - document_uri: props.document_uri, 74 - }), 75 - ); 76 - const data = swr.data as unknown as InteractionsData | undefined; 77 - const isLoading = swr.isLoading; 47 + const { isLoading, data, did, pages, documentContextValue, comments } = 48 + useDocumentDiscussionData(props.document_uri, props.open); 78 49 79 50 // Restrict mentions to the page this modal is about (mirrors InteractionDrawer). 80 51 const quotesAndMentions = (data?.quotesAndMentions ?? []).filter((q) => { ··· 86 57 return quotePosition?.pageId === props.pageId; 87 58 }); 88 59 89 - const documentRecord = data?.document as 90 - | NormalizedDocument 91 - | null 92 - | undefined; 93 - const pages = documentRecord ? getDocumentPages(documentRecord) ?? [] : []; 94 - 95 - // The drawer content only reads uri / normalizedDocument / normalizedPublication 96 - // off the document context; the rest is filled with sensible defaults. 97 - const documentContextValue: DocumentContextValue | null = documentRecord 98 - ? ({ 99 - uri: props.document_uri, 100 - normalizedDocument: documentRecord, 101 - normalizedPublication: (data?.publication ?? 102 - null) as NormalizedPublication | null, 103 - theme: null, 104 - prevNext: null, 105 - quotesAndMentions: data?.quotesAndMentions ?? [], 106 - publication: null, 107 - commentsCount: data?.comments.length ?? props.commentsCount, 108 - mentions: [], 109 - leafletId: null, 110 - recommendsCount: 0, 111 - } as unknown as DocumentContextValue) 112 - : null; 113 - 114 60 return ( 115 61 <Modal 116 62 open={props.open} 117 63 onOpenChange={props.onOpenChange} 118 - className="p-0! pb-4 gap-0 sm:w-lg max-w-full" 64 + className="px-3! pt-0! pb-4 gap-0 sm:w-lg max-w-full relative bg-[var(--color-bg-light)]!" 119 65 > 120 - <div className="flex flex-col px-3 pt-2 -mb-1 "> 121 - <div className="flex items-center justify-between gap-3"> 122 - <div className="text-sm text-tertiary "> 123 - {bothAvailable 124 - ? "Discussion on" 125 - : commentsAvailable 126 - ? "Comments on" 127 - : "Bluesky Mentions about"} 128 - </div> 66 + <div className="standardSitePostBlock block-border overflow-hidden w-full bg-bg-page my-3"> 67 + <StandardSitePostItem 68 + pageWidth={448} 69 + uri={props.document_uri} 70 + size="small" 71 + hideInteractions 72 + /> 73 + </div> 74 + <div className="discussionModalStickyHeader sticky top-0 z-10 bg-[var(--color-bg-light)]! -mx-3"> 75 + <div className="flex items-center justify-between gap-3 pt-3 pb-2 px-3"> 76 + {bothAvailable ? ( 77 + <ToggleGroup 78 + fullWidth 79 + value={tab} 80 + onChange={(value) => setTab(value)} 81 + options={[ 82 + { 83 + value: "comments", 84 + label: 85 + props.commentsCount > 0 86 + ? `Comments (${props.commentsCount})` 87 + : "Comments", 88 + }, 89 + { 90 + value: "quotes", 91 + label: ( 92 + <div> 93 + Bluesky <span className="hidden sm:inline">Mentions</span>{" "} 94 + {props.quotesCount > 0 && `(${props.quotesCount})`} 95 + </div> 96 + ), 97 + }, 98 + ]} 99 + /> 100 + ) : ( 101 + <div className="font-bold text-tertiary text-sm! "> 102 + {commentsAvailable 103 + ? `Comments (${props.commentsCount})` 104 + : `Bluesky Mentions (${props.quotesCount})`} 105 + </div> 106 + )}{" "} 129 107 <SpeedyLink 130 108 href={props.postUrl} 131 109 className="shrink-0 hover:no-underline!" 132 110 > 133 - <ButtonPrimary compact className="text-sm"> 111 + <ButtonPrimary className="text-sm py-[3px]! rounded-lg!"> 134 112 Full Post <GoToArrow /> 135 113 </ButtonPrimary> 136 114 </SpeedyLink> 137 115 </div> 138 - <h3 className="line-clamp-1 min-w-0 text-primary"> 139 - {props.title || "Post"} 140 - </h3> 116 + <hr className="border-border-light" /> 141 117 </div> 142 - 143 - {bothAvailable && ( 144 - <div className="sticky top-0 z-10 px-3 pt-3"> 145 - <ToggleGroup 146 - fullWidth 147 - value={tab} 148 - onChange={(value) => setTab(value)} 149 - options={[ 150 - { 151 - value: "comments", 152 - label: 153 - props.commentsCount > 0 154 - ? `Comments (${props.commentsCount})` 155 - : "Comments", 156 - }, 157 - { 158 - value: "quotes", 159 - label: ( 160 - <div> 161 - Bluesky <span className="hidden sm:inline">Mentions</span>{" "} 162 - {props.quotesCount > 0 && `(${props.quotesCount})`} 163 - </div> 164 - ), 165 - }, 166 - ]} 167 - /> 168 - </div> 169 - )} 170 - 171 - <div className="px-3 pb-3 pt-4"> 118 + <div className="pb-3 pt-4"> 172 119 {!data && isLoading ? ( 173 120 <div className="flex items-center justify-center gap-1 text-tertiary italic text-sm py-8"> 174 121 <span>loading</span> ··· 180 127 {tab === "comments" ? ( 181 128 <CommentsDrawerContent 182 129 document_uri={props.document_uri} 183 - comments={(data?.comments ?? []) as Comment[]} 130 + comments={comments} 184 131 pageId={props.pageId} 185 132 /> 186 133 ) : (
+13 -3
components/InteractionsPreview.tsx
··· 1 1 "use client"; 2 - import { useState } from "react"; 2 + import { useContext, useState } from "react"; 3 3 import { Separator } from "./Layout"; 4 4 import { CommentTiny } from "./Icons/CommentTiny"; 5 5 import { useSmoker } from "./Toast"; ··· 8 8 import { TagTiny } from "./Icons/TagTiny"; 9 9 import { RecommendButton } from "./RecommendButton"; 10 10 import { DiscussionModal } from "./DiscussionModal"; 11 + import { DrawerThreadContext } from "app/(app)/lish/[did]/[publication]/[rkey]/Interactions/drawerThreadContext"; 11 12 12 13 export const InteractionPreview = (props: { 13 14 quotesCount: number; ··· 24 25 share?: boolean; 25 26 }) => { 26 27 let smoker = useSmoker(); 28 + // Inside a published post body a DrawerThreadContext is in scope; there we 29 + // open this post's discussion in the interaction drawer (like a Bluesky post's 30 + // thread) instead of the standalone modal used in listings/feeds. 31 + let drawerNav = useContext(DrawerThreadContext); 27 32 let [discussionsOpen, setDiscussionsOpen] = useState(false); 28 33 let commentsAvailable = props.showComments !== false && props.commentsCount > 0; 29 34 let mentionsAvailable = props.showMentions && props.quotesCount > 0; ··· 49 54 onClick={(e) => { 50 55 e.preventDefault(); 51 56 e.stopPropagation(); 52 - setDiscussionsOpen(true); 57 + if (drawerNav) 58 + drawerNav.push({ 59 + type: "standardSitePost", 60 + uri: props.documentUri, 61 + }); 62 + else setDiscussionsOpen(true); 53 63 }} 54 64 className="relative flex flex-row gap-1 text-sm items-center hover:text-accent-contrast text-tertiary" 55 65 > 56 66 <CommentTiny /> {props.commentsCount + props.quotesCount} 57 67 </button> 58 68 )} 59 - {discussionsAvailable && ( 69 + {discussionsAvailable && !drawerNav && ( 60 70 <DiscussionModal 61 71 open={discussionsOpen} 62 72 onOpenChange={setDiscussionsOpen}