a tool for shared writing and social publishing

init the recommend button, some stuff is broken tho

+174 -53
+6 -1
.claude/settings.local.json
··· 3 3 "allow": [ 4 4 "mcp__acp__Edit", 5 5 "mcp__acp__Write", 6 - "mcp__acp__Bash" 6 + "mcp__acp__Bash", 7 + "mcp__primitive__say_hello", 8 + "mcp__primitive__pending_delegations", 9 + "mcp__primitive__claim_delegation", 10 + "mcp__primitive__tasks_update", 11 + "mcp__primitive__contexts_update" 7 12 ] 8 13 } 9 14 }
+1
actions/getIdentityData.ts
··· 42 42 .eq("confirmed", true) 43 43 .single() 44 44 : null; 45 + console.log(auth_res); 45 46 if (!auth_res?.data?.identities) return null; 46 47 if (auth_res.data.identities.atp_did) { 47 48 //I should create a relationship table so I can do this in the above query
+15 -12
app/(home-pages)/p/[didOrHandle]/getProfilePosts.ts
··· 26 26 `*, 27 27 comments_on_documents(count), 28 28 document_mentions_in_bsky(count), 29 + recommends_on_documents(count), 29 30 documents_in_publications(publications(*))`, 30 31 ) 31 32 .like("uri", `at://${did}/%`) ··· 39 40 ); 40 41 } 41 42 42 - let [{ data: rawDocs }, { data: rawPubs }, { data: profile }] = await Promise.all([ 43 - query, 44 - supabaseServerClient 45 - .from("publications") 46 - .select("*") 47 - .eq("identity_did", did), 48 - supabaseServerClient 49 - .from("bsky_profiles") 50 - .select("handle") 51 - .eq("did", did) 52 - .single(), 53 - ]); 43 + let [{ data: rawDocs }, { data: rawPubs }, { data: profile }] = 44 + await Promise.all([ 45 + query, 46 + supabaseServerClient 47 + .from("publications") 48 + .select("*") 49 + .eq("identity_did", did), 50 + supabaseServerClient 51 + .from("bsky_profiles") 52 + .select("handle") 53 + .eq("did", did) 54 + .single(), 55 + ]); 54 56 55 57 // Deduplicate records that may exist under both pub.leaflet and site.standard namespaces 56 58 const docs = deduplicateByUriOrdered(rawDocs || []); ··· 82 84 sort_date: doc.sort_date, 83 85 comments_on_documents: doc.comments_on_documents, 84 86 document_mentions_in_bsky: doc.document_mentions_in_bsky, 87 + recommends_on_documents: doc.recommends_on_documents, 85 88 }, 86 89 }; 87 90
+3
app/(home-pages)/reader/getReaderFeed.ts
··· 32 32 `*, 33 33 comments_on_documents(count), 34 34 document_mentions_in_bsky(count), 35 + recommends_on_documents(count), 35 36 documents_in_publications!inner(publications!inner(*, publication_subscriptions!inner(*)))`, 36 37 ) 37 38 .eq( ··· 76 77 documents: { 77 78 comments_on_documents: post.comments_on_documents, 78 79 document_mentions_in_bsky: post.document_mentions_in_bsky, 80 + recommends_on_documents: post.recommends_on_documents, 79 81 data: normalizedData, 80 82 uri: post.uri, 81 83 sort_date: post.sort_date, ··· 112 114 sort_date: string; 113 115 comments_on_documents: { count: number }[] | undefined; 114 116 document_mentions_in_bsky: { count: number }[] | undefined; 117 + recommends_on_documents: { count: number }[] | undefined; 115 118 }; 116 119 };
+2
app/(home-pages)/tag/[tag]/getDocumentsByTag.ts
··· 21 21 `*, 22 22 comments_on_documents(count), 23 23 document_mentions_in_bsky(count), 24 + recommends_on_documents(count), 24 25 documents_in_publications(publications(*))`, 25 26 ) 26 27 .contains("data->tags", `["${tag}"]`) ··· 67 68 documents: { 68 69 comments_on_documents: doc.comments_on_documents, 69 70 document_mentions_in_bsky: doc.document_mentions_in_bsky, 71 + recommends_on_documents: doc.recommends_on_documents, 70 72 data: normalizedData, 71 73 uri: doc.uri, 72 74 sort_date: doc.sort_date,
+7 -5
app/api/oauth/[route]/route.ts
··· 89 89 // Trigger migration if identity needs it 90 90 const metadata = identity?.metadata as Record<string, unknown> | null; 91 91 if (metadata?.needsStandardSiteMigration) { 92 - await inngest.send({ 93 - name: "user/migrate-to-standard", 94 - data: { did: session.did }, 95 - }); 92 + if (process.env.NODE_ENV === "production") 93 + await inngest.send({ 94 + name: "user/migrate-to-standard", 95 + data: { did: session.did }, 96 + }); 96 97 } 97 98 98 99 let { data: token } = await supabaseServerClient ··· 104 105 }) 105 106 .select() 106 107 .single(); 107 - 108 + console.log({ token }); 108 109 if (token) await setAuthToken(token.id); 109 110 110 111 // Process successful authentication here ··· 113 114 console.log("User authenticated as:", session.did); 114 115 return handleAction(s.action, redirectPath); 115 116 } catch (e) { 117 + console.log(e); 116 118 redirect(redirectPath); 117 119 } 118 120 }
+4 -1
app/api/rpc/[command]/get_publication_data.ts
··· 40 40 documents_in_publications(documents( 41 41 *, 42 42 comments_on_documents(count), 43 - document_mentions_in_bsky(count) 43 + document_mentions_in_bsky(count), 44 + recommends_on_documents(count) 44 45 )), 45 46 publication_subscriptions(*, identities(bsky_profiles(*))), 46 47 publication_domains(*), ··· 87 88 data: dip.documents.data, 88 89 commentsCount: dip.documents.comments_on_documents[0]?.count || 0, 89 90 mentionsCount: dip.documents.document_mentions_in_bsky[0]?.count || 0, 91 + recommendsCount: 92 + dip.documents.recommends_on_documents?.[0]?.count || 0, 90 93 }; 91 94 }) 92 95 .filter((d): d is NonNullable<typeof d> => d !== null);
+6
app/lish/[did]/[publication]/[rkey]/CanvasPage.tsx
··· 71 71 preferences={preferences} 72 72 commentsCount={getCommentCount(document.comments_on_documents, pageId)} 73 73 quotesCount={getQuoteCount(document.quotesAndMentions, pageId)} 74 + recommendsCount={document.recommendsCount} 75 + hasRecommended={document.hasRecommended} 74 76 /> 75 77 <CanvasContent 76 78 blocks={blocks} ··· 209 211 }; 210 212 quotesCount: number | undefined; 211 213 commentsCount: number | undefined; 214 + recommendsCount: number; 215 + hasRecommended: boolean; 212 216 }) => { 213 217 let isMobile = useIsMobile(); 214 218 return ( ··· 216 220 <Interactions 217 221 quotesCount={props.quotesCount || 0} 218 222 commentsCount={props.commentsCount || 0} 223 + recommendsCount={props.recommendsCount} 224 + hasRecommended={props.hasRecommended} 219 225 showComments={props.preferences.showComments !== false} 220 226 showMentions={props.preferences.showMentions !== false} 221 227 pageId={props.pageId}
+37 -7
app/lish/[did]/[publication]/[rkey]/Interactions/Interactions.tsx
··· 18 18 import { ManageSubscription, SubscribeWithBluesky } from "app/lish/Subscribe"; 19 19 import { EditTiny } from "components/Icons/EditTiny"; 20 20 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 21 + import { RecommendButton } from "components/RecommendButton"; 21 22 22 23 export type InteractionState = { 23 24 drawerOpen: undefined | boolean; ··· 105 106 export const Interactions = (props: { 106 107 quotesCount: number; 107 108 commentsCount: number; 109 + recommendsCount: number; 110 + hasRecommended: boolean; 108 111 className?: string; 109 112 showComments: boolean; 110 113 showMentions: boolean; 111 114 pageId?: string; 112 115 }) => { 113 - const { uri: document_uri, quotesAndMentions, normalizedDocument } = useDocument(); 116 + const { 117 + uri: document_uri, 118 + quotesAndMentions, 119 + normalizedDocument, 120 + } = useDocument(); 114 121 let { identity } = useIdentityData(); 115 122 116 123 let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); ··· 128 135 <div className={`flex gap-2 text-tertiary text-sm ${props.className}`}> 129 136 {tagCount > 0 && <TagPopover tags={tags} tagCount={tagCount} />} 130 137 138 + <RecommendButton 139 + documentUri={document_uri} 140 + recommendsCount={props.recommendsCount} 141 + hasRecommended={props.hasRecommended} 142 + /> 143 + 131 144 {props.quotesCount === 0 || props.showMentions === false ? null : ( 132 145 <button 133 146 className="flex w-fit gap-2 items-center" ··· 163 176 export const ExpandedInteractions = (props: { 164 177 quotesCount: number; 165 178 commentsCount: number; 179 + recommendsCount: number; 180 + hasRecommended: boolean; 166 181 className?: string; 167 182 showComments: boolean; 168 183 showMentions: boolean; 169 184 pageId?: string; 170 185 }) => { 171 - const { uri: document_uri, quotesAndMentions, normalizedDocument, publication, leafletId } = useDocument(); 186 + const { 187 + uri: document_uri, 188 + quotesAndMentions, 189 + normalizedDocument, 190 + publication, 191 + leafletId, 192 + } = useDocument(); 172 193 let { identity } = useIdentityData(); 173 194 174 195 let { drawerOpen, drawer, pageId } = useInteractionState(document_uri); ··· 192 213 ); 193 214 194 215 let isAuthor = 195 - identity && 196 - identity.atp_did === publication?.identity_did && 197 - leafletId; 216 + identity && identity.atp_did === publication?.identity_did && leafletId; 198 217 199 218 return ( 200 219 <div ··· 216 235 ) : ( 217 236 <> 218 237 <div className="flex gap-2"> 238 + <RecommendButton 239 + documentUri={document_uri} 240 + recommendsCount={props.recommendsCount} 241 + hasRecommended={props.hasRecommended} 242 + /> 219 243 {props.quotesCount === 0 || !props.showMentions ? null : ( 220 244 <button 221 245 className="flex w-fit gap-2 items-center px-1 py-0.5 border border-border-light rounded-lg trasparent-outline selected-outline" ··· 313 337 </div> 314 338 ); 315 339 }; 316 - export function getQuoteCount(quotesAndMentions: { uri: string; link?: string }[], pageId?: string) { 340 + export function getQuoteCount( 341 + quotesAndMentions: { uri: string; link?: string }[], 342 + pageId?: string, 343 + ) { 317 344 return getQuoteCountFromArray(quotesAndMentions, pageId); 318 345 } 319 346 ··· 338 365 } 339 366 } 340 367 341 - export function getCommentCount(comments: CommentOnDocument[], pageId?: string) { 368 + export function getCommentCount( 369 + comments: CommentOnDocument[], 370 + pageId?: string, 371 + ) { 342 372 if (pageId) 343 373 return comments.filter( 344 374 (c) => (c.record as PubLeafletComment.Record)?.onPage === pageId,
+5 -1
app/lish/[did]/[publication]/[rkey]/LinearDocumentPage.tsx
··· 87 87 pageId={pageId} 88 88 showComments={preferences.showComments !== false} 89 89 showMentions={preferences.showMentions !== false} 90 - commentsCount={getCommentCount(document.comments_on_documents, pageId) || 0} 90 + commentsCount={ 91 + getCommentCount(document.comments_on_documents, pageId) || 0 92 + } 91 93 quotesCount={getQuoteCount(document.quotesAndMentions, pageId) || 0} 94 + recommendsCount={document.recommendsCount} 95 + hasRecommended={document.hasRecommended} 92 96 /> 93 97 {!hasPageBackground && <div className={`spacer h-8 w-full`} />} 94 98 </PageWrapper>
+5 -1
app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader.tsx
··· 88 88 showComments={props.preferences.showComments !== false} 89 89 showMentions={props.preferences.showMentions !== false} 90 90 quotesCount={getQuoteCount(document?.quotesAndMentions || []) || 0} 91 - commentsCount={getCommentCount(document?.comments_on_documents || []) || 0} 91 + commentsCount={ 92 + getCommentCount(document?.comments_on_documents || []) || 0 93 + } 94 + recommendsCount={document?.recommendsCount || 0} 95 + hasRecommended={document?.hasRecommended || false} 92 96 /> 93 97 </> 94 98 }
+48 -12
app/lish/[did]/[publication]/[rkey]/getPostPageData.ts
··· 8 8 } from "src/utils/normalizeRecords"; 9 9 import { PubLeafletPublication, SiteStandardPublication } from "lexicons/api"; 10 10 import { documentUriFilter } from "src/utils/uriHelpers"; 11 + import { getIdentityData } from "actions/getIdentityData"; 11 12 12 13 export async function getPostPageData(did: string, rkey: string) { 14 + const identity = await getIdentityData(); 15 + const currentUserDid = identity?.atp_did; 16 + 13 17 let { data: documents } = await supabaseServerClient 14 18 .from("documents") 15 19 .select( ··· 22 26 publication_subscriptions(*)) 23 27 ), 24 28 document_mentions_in_bsky(*), 25 - leaflets_in_publications(*) 29 + leaflets_in_publications(*), 30 + recommends_on_documents(count) 26 31 `, 27 32 ) 28 33 .or(documentUriFilter(did, rkey)) ··· 32 37 33 38 if (!document) return null; 34 39 40 + // Check if current user has recommended this document 41 + let hasRecommended = false; 42 + if (currentUserDid) { 43 + const { data: userRecommend } = await supabaseServerClient 44 + .from("recommends_on_documents") 45 + .select("uri") 46 + .eq("document", document.uri) 47 + .eq("recommender_did", currentUserDid) 48 + .limit(1); 49 + hasRecommended = (userRecommend?.length ?? 0) > 0; 50 + } 51 + 35 52 // Normalize the document record - this is the primary way consumers should access document data 36 - const normalizedDocument = normalizeDocumentRecord(document.data, document.uri); 53 + const normalizedDocument = normalizeDocumentRecord( 54 + document.data, 55 + document.uri, 56 + ); 37 57 if (!normalizedDocument) return null; 38 58 39 59 // Normalize the publication record - this is the primary way consumers should access publication data 40 60 const normalizedPublication = normalizePublicationRecord( 41 - document.documents_in_publications[0]?.publications?.record 61 + document.documents_in_publications[0]?.publications?.record, 42 62 ); 43 63 44 64 // Fetch constellation backlinks for mentions ··· 83 103 // Filter and sort documents by publishedAt 84 104 const sortedDocs = allDocs 85 105 .map((dip) => { 86 - const normalizedData = normalizeDocumentRecord(dip?.documents?.data, dip?.documents?.uri); 106 + const normalizedData = normalizeDocumentRecord( 107 + dip?.documents?.data, 108 + dip?.documents?.uri, 109 + ); 87 110 return { 88 111 uri: dip?.documents?.uri, 89 112 title: normalizedData?.title, ··· 98 121 ); 99 122 100 123 // Find current document index 101 - const currentIndex = sortedDocs.findIndex((doc) => doc.uri === document.uri); 124 + const currentIndex = sortedDocs.findIndex( 125 + (doc) => doc.uri === document.uri, 126 + ); 102 127 103 128 if (currentIndex !== -1) { 104 129 prevNext = { ··· 122 147 123 148 // Build explicit publication context for consumers 124 149 const rawPub = document.documents_in_publications[0]?.publications; 125 - const publication = rawPub ? { 126 - uri: rawPub.uri, 127 - name: rawPub.name, 128 - identity_did: rawPub.identity_did, 129 - record: rawPub.record as PubLeafletPublication.Record | SiteStandardPublication.Record | null, 130 - publication_subscriptions: rawPub.publication_subscriptions || [], 131 - } : null; 150 + const publication = rawPub 151 + ? { 152 + uri: rawPub.uri, 153 + name: rawPub.name, 154 + identity_did: rawPub.identity_did, 155 + record: rawPub.record as 156 + | PubLeafletPublication.Record 157 + | SiteStandardPublication.Record 158 + | null, 159 + publication_subscriptions: rawPub.publication_subscriptions || [], 160 + } 161 + : null; 162 + 163 + // Get recommends count from the aggregated query result 164 + const recommendsCount = document.recommends_on_documents?.[0]?.count ?? 0; 132 165 133 166 return { 134 167 ...document, ··· 143 176 comments: document.comments_on_documents, 144 177 mentions: document.document_mentions_in_bsky, 145 178 leafletId: document.leaflets_in_publications[0]?.leaflet || null, 179 + // Recommends data 180 + recommendsCount, 181 + hasRecommended, 146 182 }; 147 183 } 148 184
+8 -8
app/lish/[did]/[publication]/dashboard/PublishedPostsLists.tsx
··· 60 60 61 61 function PublishedPostItem(props: { 62 62 doc: PublishedDocument; 63 - publication: NonNullable<NonNullable<ReturnType<typeof usePublicationData>["data"]>["publication"]>; 63 + publication: NonNullable< 64 + NonNullable<ReturnType<typeof usePublicationData>["data"]>["publication"] 65 + >; 64 66 pubRecord: ReturnType<typeof useNormalizedPublicationRecord>; 65 67 showPageBackground: boolean; 66 68 }) { ··· 94 96 <div className="flex justify-start align-top flex-row gap-1"> 95 97 {leaflet && leaflet.permission_tokens && ( 96 98 <> 97 - <SpeedyLink 98 - className="pt-[6px]" 99 - href={`/${leaflet.leaflet}`} 100 - > 99 + <SpeedyLink className="pt-[6px]" href={`/${leaflet.leaflet}`}> 101 100 <EditTiny /> 102 101 </SpeedyLink> 103 102 ··· 129 128 </div> 130 129 131 130 {doc.record.description ? ( 132 - <p className="italic text-secondary"> 133 - {doc.record.description} 134 - </p> 131 + <p className="italic text-secondary">{doc.record.description}</p> 135 132 ) : null} 136 133 <div className="text-sm text-tertiary flex gap-3 justify-between sm:justify-start items-center pt-3"> 137 134 {doc.record.publishedAt ? ( ··· 140 137 <InteractionPreview 141 138 quotesCount={doc.mentionsCount} 142 139 commentsCount={doc.commentsCount} 140 + recommendsCount={doc.recommendsCount} 141 + hasRecommended={false} 142 + documentUri={doc.uri} 143 143 tags={doc.record.tags || []} 144 144 showComments={pubRecord?.preferences?.showComments !== false} 145 145 showMentions={pubRecord?.preferences?.showMentions !== false}
+10 -2
app/lish/[did]/[publication]/page.tsx
··· 38 38 documents_in_publications(documents( 39 39 *, 40 40 comments_on_documents(count), 41 - document_mentions_in_bsky(count) 41 + document_mentions_in_bsky(count), 42 + recommends_on_documents(count) 42 43 )) 43 44 `, 44 45 ) ··· 119 120 }) 120 121 .map((doc) => { 121 122 if (!doc.documents) return null; 122 - const doc_record = normalizeDocumentRecord(doc.documents.data); 123 + const doc_record = normalizeDocumentRecord( 124 + doc.documents.data, 125 + ); 123 126 if (!doc_record) return null; 124 127 let uri = new AtUri(doc.documents.uri); 125 128 let quotes = ··· 128 131 record?.preferences?.showComments === false 129 132 ? 0 130 133 : doc.documents.comments_on_documents[0].count || 0; 134 + let recommends = 135 + doc.documents.recommends_on_documents?.[0]?.count || 0; 131 136 let tags = doc_record.tags || []; 132 137 133 138 return ( ··· 164 169 <InteractionPreview 165 170 quotesCount={quotes} 166 171 commentsCount={comments} 172 + recommendsCount={recommends} 173 + hasRecommended={false} 174 + documentUri={doc.documents.uri} 167 175 tags={tags} 168 176 postUrl={`${getPublicationURL(publication)}/${uri.rkey}`} 169 177 showComments={
+11 -3
components/InteractionsPreview.tsx
··· 7 7 import { Popover } from "./Popover"; 8 8 import { TagTiny } from "./Icons/TagTiny"; 9 9 import { SpeedyLink } from "./SpeedyLink"; 10 + import { RecommendButton } from "./RecommendButton"; 10 11 11 12 export const InteractionPreview = (props: { 12 13 quotesCount: number; 13 14 commentsCount: number; 15 + recommendsCount: number; 16 + hasRecommended: boolean; 17 + documentUri: string; 14 18 tags?: string[]; 15 19 postUrl: string; 16 20 showComments: boolean; ··· 26 30 const tagsCount = props.tags?.length || 0; 27 31 28 32 return ( 29 - <div 30 - className={`flex gap-2 text-tertiary text-sm items-center self-start`} 31 - > 33 + <div className={`flex gap-2 text-tertiary text-sm items-center`}> 32 34 {tagsCount === 0 ? null : ( 33 35 <> 34 36 <TagPopover tags={props.tags!} /> ··· 37 39 ) : null} 38 40 </> 39 41 )} 42 + 43 + <RecommendButton 44 + documentUri={props.documentUri} 45 + recommendsCount={props.recommendsCount} 46 + hasRecommended={props.hasRecommended} 47 + /> 40 48 41 49 {!props.showMentions || props.quotesCount === 0 ? null : ( 42 50 <SpeedyLink
+4
components/PostListing.tsx
··· 53 53 pubRecord?.preferences?.showComments === false 54 54 ? 0 55 55 : props.documents.comments_on_documents?.[0]?.count || 0; 56 + let recommends = props.documents.recommends_on_documents?.[0]?.count || 0; 56 57 let tags = (postRecord?.tags as string[] | undefined) || []; 57 58 58 59 // For standalone posts, link directly to the document ··· 103 104 postUrl={postHref} 104 105 quotesCount={quotes} 105 106 commentsCount={comments} 107 + recommendsCount={recommends} 108 + hasRecommended={false} 109 + documentUri={props.documents.uri} 106 110 tags={tags} 107 111 showComments={pubRecord?.preferences?.showComments !== false} 108 112 showMentions={pubRecord?.preferences?.showMentions !== false}
+2
contexts/DocumentContext.tsx
··· 21 21 | "comments" 22 22 | "mentions" 23 23 | "leafletId" 24 + | "recommendsCount" 25 + | "hasRecommended" 24 26 >; 25 27 26 28 const DocumentContext = createContext<DocumentContextValue | null>(null);