a tool for shared writing and social publishing

added interactions and metadata to leaflet and posted canvases

+167 -39
+65 -2
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 10 10 import { PageWrapper } from "components/Pages/Page"; 11 11 import { Block } from "./PostContent"; 12 12 import { CanvasBackgroundPattern } from "components/Canvas"; 13 + import { 14 + getCommentCount, 15 + getQuoteCount, 16 + Interactions, 17 + } from "./Interactions/Interactions"; 18 + import { Separator } from "components/Layout"; 19 + import { Popover } from "components/Popover"; 20 + import { InfoSmall } from "components/Icons/InfoSmall"; 21 + import { PostHeader } from "./PostHeader/PostHeader"; 22 + import { useDrawerOpen } from "./Interactions/InteractionDrawer"; 13 23 14 24 export function CanvasPage({ 15 25 document, ··· 29 39 document_uri: string; 30 40 document: PostPageData; 31 41 blocks: PubLeafletPagesCanvas.Block[]; 32 - profile?: ProfileViewDetailed; 42 + profile: ProfileViewDetailed; 33 43 pubRecord: PubLeafletPublication.Record; 34 44 did: string; 35 45 prerenderedCodeBlocks?: Map<string, string>; ··· 41 51 pages: (PubLeafletPagesLinearDocument.Main | PubLeafletPagesCanvas.Main)[]; 42 52 }) { 43 53 let hasPageBackground = !!pubRecord.theme?.showPageBackground; 54 + let isSubpage = !!pageId; 55 + let drawer = useDrawerOpen(document_uri); 44 56 45 57 return ( 46 58 <PageWrapper ··· 48 60 fullPageScroll={fullPageScroll} 49 61 cardBorderHidden={!hasPageBackground} 50 62 id={pageId ? `post-page-${pageId}` : "post-page"} 51 - drawerOpen={false} 63 + drawerOpen={ 64 + !!drawer && (pageId ? drawer.pageId === pageId : !drawer.pageId) 65 + } 52 66 pageOptions={pageOptions} 53 67 > 68 + <CanvasMetadata 69 + pageId={pageId} 70 + isSubpage={isSubpage} 71 + data={document} 72 + profile={profile} 73 + preferences={preferences} 74 + commentsCount={getCommentCount(document, pageId)} 75 + quotesCount={getQuoteCount(document, pageId)} 76 + /> 54 77 <CanvasContent 55 78 blocks={blocks} 56 79 did={did} ··· 90 113 className="relative h-full w-[1272px]" 91 114 > 92 115 <CanvasBackground /> 116 + 93 117 {blocks 94 118 .sort((a, b) => { 95 119 if (a.y === b.y) { ··· 167 191 </div> 168 192 ); 169 193 } 194 + 195 + const CanvasMetadata = (props: { 196 + pageId: string | undefined; 197 + isSubpage: boolean | undefined; 198 + data: PostPageData; 199 + profile: ProfileViewDetailed; 200 + preferences: { showComments?: boolean }; 201 + quotesCount: number | undefined; 202 + commentsCount: number | undefined; 203 + }) => { 204 + return ( 205 + <div className="flex flex-row gap-3 items-center absolute sm:top-4 sm:right-4 bg-bg-page border-border-light rounded-md px-2 py-1 h-fit z-20"> 206 + <Interactions 207 + quotesCount={props.quotesCount || 0} 208 + commentsCount={props.commentsCount || 0} 209 + compact 210 + showComments={props.preferences.showComments} 211 + pageId={props.pageId} 212 + /> 213 + {!props.isSubpage && ( 214 + <> 215 + <Separator classname="h-5" /> 216 + <Popover 217 + side="left" 218 + align="start" 219 + className="flex flex-col gap-2 p-0! max-w-sm w-[1000px]" 220 + trigger={<InfoSmall />} 221 + > 222 + <PostHeader 223 + data={props.data} 224 + profile={props.profile} 225 + preferences={props.preferences} 226 + /> 227 + </Popover> 228 + </> 229 + )} 230 + </div> 231 + ); 232 + }; 170 233 171 234 const CanvasBackground = () => { 172 235 return (
+32 -1
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 5 5 import type { Json } from "supabase/database.types"; 6 6 import { create } from "zustand"; 7 7 import type { Comment } from "./Comments"; 8 - import { QuotePosition } from "../quotePosition"; 8 + import { decodeQuotePosition, QuotePosition } from "../quotePosition"; 9 9 import { useContext } from "react"; 10 10 import { PostPageContext } from "../PostPageContext"; 11 11 import { scrollIntoView } from "src/utils/scrollIntoView"; 12 + import { PostPageData } from "../getPostPageData"; 13 + import { PubLeafletComment } from "lexicons/api"; 12 14 13 15 export type InteractionState = { 14 16 drawerOpen: undefined | boolean; ··· 149 151 </div> 150 152 ); 151 153 }; 154 + 155 + export function getCommentCount(document: PostPageData, pageId?: string) { 156 + if (!document) return; 157 + 158 + if (pageId) 159 + return document.document_mentions_in_bsky.filter((q) => 160 + q.link.includes(pageId), 161 + ).length; 162 + else 163 + return document.document_mentions_in_bsky.filter((q) => { 164 + const url = new URL(q.link); 165 + const quoteParam = url.pathname.split("/l-quote/")[1]; 166 + if (!quoteParam) return null; 167 + const quotePosition = decodeQuotePosition(quoteParam); 168 + return !quotePosition?.pageId; 169 + }).length; 170 + } 171 + 172 + export function getQuoteCount(document: PostPageData, pageId?: string) { 173 + if (!document) return; 174 + if (pageId) 175 + return document.comments_on_documents.filter( 176 + (c) => (c.record as PubLeafletComment.Record)?.onPage === pageId, 177 + ).length; 178 + else 179 + return document.comments_on_documents.filter( 180 + (c) => !(c.record as PubLeafletComment.Record)?.onPage, 181 + ).length; 182 + }
+7 -24
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 10 10 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 11 11 import { SubscribeWithBluesky } from "app/lish/Subscribe"; 12 12 import { EditTiny } from "components/Icons/EditTiny"; 13 - import { Interactions } from "./Interactions/Interactions"; 13 + import { 14 + getCommentCount, 15 + getQuoteCount, 16 + Interactions, 17 + } from "./Interactions/Interactions"; 14 18 import { PostContent } from "./PostContent"; 15 19 import { PostHeader } from "./PostHeader/PostHeader"; 16 20 import { useIdentityData } from "components/IdentityProvider"; ··· 87 91 <Interactions 88 92 pageId={pageId} 89 93 showComments={preferences.showComments} 90 - quotesCount={ 91 - pageId 92 - ? document.document_mentions_in_bsky.filter((q) => 93 - q.link.includes(pageId), 94 - ).length 95 - : document.document_mentions_in_bsky.filter((q) => { 96 - const url = new URL(q.link); 97 - const quoteParam = url.pathname.split("/l-quote/")[1]; 98 - if (!quoteParam) return null; 99 - const quotePosition = decodeQuotePosition(quoteParam); 100 - return !quotePosition?.pageId; 101 - }).length 102 - } 103 - commentsCount={ 104 - pageId 105 - ? document.comments_on_documents.filter( 106 - (c) => 107 - (c.record as PubLeafletComment.Record)?.onPage === pageId, 108 - ).length 109 - : document.comments_on_documents.filter( 110 - (c) => !(c.record as PubLeafletComment.Record)?.onPage, 111 - ).length 112 - } 94 + commentsCount={getCommentCount(document, pageId) || 0} 95 + quotesCount={getQuoteCount(document, pageId) || 0} 113 96 /> 114 97 {!isSubpage && ( 115 98 <>
+1
app/lish/[did]/[publication]/[rkey]/PostPages.tsx
··· 183 183 blocks={(page as PubLeafletPagesCanvas.Main).blocks} 184 184 did={did} 185 185 preferences={preferences} 186 + profile={profile} 186 187 pubRecord={pubRecord} 187 188 prerenderedCodeBlocks={prerenderedCodeBlocks} 188 189 bskyPostData={bskyPostData}
+56 -2
components/Canvas.tsx
··· 14 14 import { TooltipButton } from "./Buttons"; 15 15 import { useBlockKeyboardHandlers } from "./Blocks/useBlockKeyboardHandlers"; 16 16 import { AddSmall } from "./Icons/AddSmall"; 17 + import { InfoSmall } from "./Icons/InfoSmall"; 18 + import { Popover } from "./Popover"; 19 + import { Separator } from "./Layout"; 20 + import { CommentTiny } from "./Icons/CommentTiny"; 21 + import { QuoteTiny } from "./Icons/QuoteTiny"; 22 + import { PublicationMetadata } from "./Pages/PublicationMetadata"; 23 + import { useLeafletPublicationData } from "./PageSWRDataProvider"; 24 + import { 25 + PubLeafletPublication, 26 + PubLeafletPublicationRecord, 27 + } from "lexicons/api"; 17 28 18 - export function Canvas(props: { entityID: string; preview?: boolean }) { 29 + export function Canvas(props: { 30 + entityID: string; 31 + preview?: boolean; 32 + first?: boolean; 33 + }) { 19 34 let entity_set = useEntitySetContext(); 20 35 let ref = useRef<HTMLDivElement>(null); 21 36 useEffect(() => { ··· 55 70 `} 56 71 > 57 72 <AddCanvasBlockButton entityID={props.entityID} entity_set={entity_set} /> 73 + 74 + <CanvasMetadata isSubpage={props.first} /> 75 + 58 76 <CanvasContent {...props} /> 59 77 </div> 60 78 ); ··· 132 150 ); 133 151 } 134 152 153 + const CanvasMetadata = (props: { isSubpage: boolean | undefined }) => { 154 + let { data: pub } = useLeafletPublicationData(); 155 + if (!pub || !pub.publications) return null; 156 + 157 + let pubRecord = pub.publications.record as PubLeafletPublication.Record; 158 + let showComments = pubRecord.preferences?.showComments; 159 + 160 + return ( 161 + <div className="flex flex-row gap-3 items-center absolute sm:top-4 sm:right-4 bg-bg-page border-border-light rounded-md px-2 py-1 h-fit z-20"> 162 + {/*ONLY IF SHOW COMMENTS PREF IS ON*/} 163 + {showComments && ( 164 + <div className="flex gap-1 text-tertiary items-center"> 165 + <CommentTiny className="text-border" /> — 166 + </div> 167 + )} 168 + <div className="flex gap-1 text-tertiary items-center"> 169 + <QuoteTiny className="text-border" /> — 170 + </div> 171 + 172 + {!props.isSubpage && ( 173 + <> 174 + <Separator classname="h-5" /> 175 + <Popover 176 + side="left" 177 + align="start" 178 + className="flex flex-col gap-2 p-0! max-w-sm w-[1000px]" 179 + trigger={<InfoSmall />} 180 + > 181 + <PublicationMetadata /> 182 + </Popover> 183 + </> 184 + )} 185 + </div> 186 + ); 187 + }; 188 + 135 189 const AddCanvasBlockButton = (props: { 136 190 entityID: string; 137 191 entity_set: { set: string }; ··· 142 196 143 197 if (!permissions.write) return null; 144 198 return ( 145 - <div className="absolute right-2 sm:top-4 sm:right-4 bottom-2 sm:bottom-auto z-10 flex flex-col gap-1 justify-center"> 199 + <div className="absolute right-2 sm:bottom-4 sm:right-4 bottom-2 sm:top-auto z-10 flex flex-col gap-1 justify-center"> 146 200 <TooltipButton 147 201 side="left" 148 202 open={blocks.length === 0 ? true : undefined}
+1 -1
components/Input.tsx
··· 100 100 JSX.IntrinsicElements["textarea"], 101 101 ) => { 102 102 let { label, textarea, ...inputProps } = props; 103 - let style = `appearance-none w-full font-normal bg-transparent text-base text-primary focus:outline-0 ${props.className} outline-hidden resize-none`; 103 + let style = `appearance-none w-full font-normal not-italic bg-transparent text-base text-primary focus:outline-0 ${props.className} outline-hidden resize-none`; 104 104 return ( 105 105 <label className=" input-with-border flex flex-col gap-px text-sm text-tertiary font-bold italic leading-tight py-1! px-[6px]!"> 106 106 {props.label}
+4 -4
components/Pages/Page.tsx
··· 61 61 > 62 62 {props.first && ( 63 63 <> 64 - <PublicationMetadata cardBorderHidden={!!cardBorderHidden} /> 64 + <PublicationMetadata /> 65 65 </> 66 66 )} 67 - <PageContent entityID={props.entityID} /> 67 + <PageContent entityID={props.entityID} first={props.first} /> 68 68 </PageWrapper> 69 69 <DesktopPageFooter pageID={props.entityID} /> 70 70 </CardThemeProvider> ··· 134 134 ); 135 135 }; 136 136 137 - const PageContent = (props: { entityID: string }) => { 137 + const PageContent = (props: { entityID: string; first?: boolean }) => { 138 138 let pageType = useEntity(props.entityID, "page/type")?.data.value || "doc"; 139 139 if (pageType === "doc") return <DocContent entityID={props.entityID} />; 140 - return <Canvas entityID={props.entityID} />; 140 + return <Canvas entityID={props.entityID} first={props.first} />; 141 141 }; 142 142 143 143 const DocContent = (props: { entityID: string }) => {
+1 -5
components/Pages/PublicationMetadata.tsx
··· 13 13 import { useSubscribe } from "src/replicache/useSubscribe"; 14 14 import { useEntitySetContext } from "components/EntitySetProvider"; 15 15 import { timeAgo } from "src/utils/timeAgo"; 16 - export const PublicationMetadata = ({ 17 - cardBorderHidden, 18 - }: { 19 - cardBorderHidden: boolean; 20 - }) => { 16 + export const PublicationMetadata = () => { 21 17 let { rep } = useReplicache(); 22 18 let { data: pub } = useLeafletPublicationData(); 23 19 let title = useSubscribe(rep, (tx) => tx.get<string>("publication_title"));