a tool for shared writing and social publishing
0
fork

Configure Feed

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

add bsky post embeds

+274 -13
+13 -10
actions/publishToPublication.ts
··· 17 17 PubLeafletBlocksCode, 18 18 PubLeafletBlocksMath, 19 19 PubLeafletBlocksHorizontalRule, 20 + PubLeafletBlocksBskyPost, 20 21 } from "lexicons/api"; 21 22 import { Block } from "components/Blocks/Block"; 22 23 import { TID } from "@atproto/common"; ··· 227 228 let facets = YJSFragmentToFacets(nodes[0]); 228 229 return [stringValue, facets] as const; 229 230 }; 230 - if ( 231 - b.type !== "text" && 232 - b.type !== "heading" && 233 - b.type !== "image" && 234 - b.type !== "link" && 235 - b.type !== "code" && 236 - b.type !== "math" && 237 - b.type !== "horizontal-rule" 238 - ) 239 - return; 240 231 232 + if (b.type === "bluesky-post") { 233 + let [post] = scan.eav(b.value, "block/bluesky-post"); 234 + if (!post || !post.data.value.post) return; 235 + let block: $Typed<PubLeafletBlocksBskyPost.Main> = { 236 + $type: ids.PubLeafletBlocksBskyPost, 237 + postRef: { 238 + uri: post.data.value.post.uri, 239 + cid: post.data.value.post.cid, 240 + }, 241 + }; 242 + return block; 243 + } 241 244 if (b.type === "horizontal-rule") { 242 245 let block: $Typed<PubLeafletBlocksHorizontalRule.Main> = { 243 246 $type: ids.PubLeafletBlocksHorizontalRule,
+1
app/lish/[did]/[publication]/[rkey]/Interactions/Quotes.tsx
··· 103 103 > 104 104 <div className="italic"> 105 105 <PostContent 106 + bskyPostData={[]} 106 107 blocks={content || []} 107 108 did={props.did} 108 109 preview
+18
app/lish/[did]/[publication]/[rkey]/PostContent.tsx
··· 10 10 PubLeafletPagesLinearDocument, 11 11 PubLeafletBlocksHorizontalRule, 12 12 PubLeafletBlocksBlockquote, 13 + PubLeafletBlocksBskyPost, 13 14 } from "lexicons/api"; 14 15 import { blobRefToSrc } from "src/utils/blobRefToSrc"; 15 16 import { TextBlock } from "./TextBlock"; ··· 20 21 import Katex from "katex"; 21 22 import { StaticMathBlock } from "./StaticMathBlock"; 22 23 import { PubCodeBlock } from "./PubCodeBlock"; 24 + import { AppBskyFeedDefs } from "@atproto/api"; 25 + import { PubBlueskyPostBlock } from "./PublishBskyPostBlock"; 23 26 24 27 export function PostContent({ 25 28 blocks, 26 29 did, 27 30 preview, 28 31 prerenderedCodeBlocks, 32 + bskyPostData, 29 33 }: { 30 34 blocks: PubLeafletPagesLinearDocument.Block[]; 31 35 did: string; 32 36 preview?: boolean; 33 37 prerenderedCodeBlocks?: Map<string, string>; 38 + bskyPostData: AppBskyFeedDefs.PostView[]; 34 39 }) { 35 40 return ( 36 41 <div id="post-content" className="postContent flex flex-col"> 37 42 {blocks.map((b, index) => { 38 43 return ( 39 44 <Block 45 + bskyPostData={bskyPostData} 40 46 block={b} 41 47 did={did} 42 48 key={index} ··· 59 65 preview, 60 66 previousBlock, 61 67 prerenderedCodeBlocks, 68 + bskyPostData, 62 69 }: { 63 70 preview?: boolean; 64 71 index: number[]; ··· 67 74 isList?: boolean; 68 75 previousBlock?: PubLeafletPagesLinearDocument.Block; 69 76 prerenderedCodeBlocks?: Map<string, string>; 77 + bskyPostData: AppBskyFeedDefs.PostView[]; 70 78 }) => { 71 79 let b = block; 72 80 let blockProps = { ··· 97 105 `; 98 106 99 107 switch (true) { 108 + case PubLeafletBlocksBskyPost.isMain(b.block): { 109 + let uri = b.block.postRef.uri; 110 + let post = bskyPostData.find((p) => p.uri === uri); 111 + if (!post) return <div>no prefetched post rip</div>; 112 + return <PubBlueskyPostBlock post={post} />; 113 + } 100 114 case PubLeafletBlocksHorizontalRule.isMain(b.block): { 101 115 return <hr className="my-2 w-full border-border-light" />; 102 116 } ··· 105 119 <ul className="-ml-[1px] sm:ml-[9px] pb-2"> 106 120 {b.block.children.map((child, i) => ( 107 121 <ListItem 122 + bskyPostData={bskyPostData} 108 123 index={[...index, i]} 109 124 item={child} 110 125 did={did} ··· 263 278 item: PubLeafletBlocksUnorderedList.ListItem; 264 279 did: string; 265 280 className?: string; 281 + bskyPostData: AppBskyFeedDefs.PostView[]; 266 282 }) { 267 283 let children = props.item.children?.length ? ( 268 284 <ul className="-ml-[7px] sm:ml-[7px]"> 269 285 {props.item.children.map((child, index) => ( 270 286 <ListItem 287 + bskyPostData={props.bskyPostData} 271 288 index={[...props.index, index]} 272 289 item={child} 273 290 did={props.did} ··· 285 302 /> 286 303 <div className="flex flex-col w-full"> 287 304 <Block 305 + bskyPostData={props.bskyPostData} 288 306 block={{ block: props.item.content }} 289 307 did={props.did} 290 308 isList
+4
app/lish/[did]/[publication]/[rkey]/PostPage.tsx
··· 12 12 import { PostContent } from "./PostContent"; 13 13 import { PostHeader } from "./PostHeader/PostHeader"; 14 14 import { useIdentityData } from "components/IdentityProvider"; 15 + import { AppBskyFeedDefs } from "@atproto/api"; 15 16 16 17 export function PostPage({ 17 18 document, ··· 21 22 profile, 22 23 pubRecord, 23 24 prerenderedCodeBlocks, 25 + bskyPostData, 24 26 }: { 25 27 document: PostPageData; 26 28 blocks: PubLeafletPagesLinearDocument.Block[]; ··· 29 31 pubRecord: PubLeafletPublication.Record; 30 32 did: string; 31 33 prerenderedCodeBlocks?: Map<string, string>; 34 + bskyPostData: AppBskyFeedDefs.PostView[]; 32 35 }) { 33 36 let { identity } = useIdentityData(); 34 37 let { drawerOpen } = useInteractionState(); ··· 61 64 > 62 65 <PostHeader data={document} profile={profile} name={name} /> 63 66 <PostContent 67 + bskyPostData={bskyPostData} 64 68 blocks={blocks} 65 69 did={did} 66 70 prerenderedCodeBlocks={prerenderedCodeBlocks}
+125
app/lish/[did]/[publication]/[rkey]/PublishBskyPostBlock.tsx
··· 1 + import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 2 + import { useEntitySetContext } from "components/EntitySetProvider"; 3 + import { useEffect, useState } from "react"; 4 + import { useEntity } from "src/replicache"; 5 + import { useUIState } from "src/useUIState"; 6 + import { elementId } from "src/utils/elementId"; 7 + import { focusBlock } from "src/utils/focusBlock"; 8 + import { AppBskyFeedDefs, AppBskyFeedPost, RichText } from "@atproto/api"; 9 + import { Separator } from "components/Layout"; 10 + import { useInitialPageLoad } from "components/InitialPageLoadProvider"; 11 + import { BlueskyTiny } from "components/Icons/BlueskyTiny"; 12 + import { CommentTiny } from "components/Icons/CommentTiny"; 13 + import { 14 + BlueskyEmbed, 15 + PostNotAvailable, 16 + } from "components/Blocks/BlueskyPostBlock/BlueskyEmbed"; 17 + import { BlueskyRichText } from "components/Blocks/BlueskyPostBlock/BlueskyRichText"; 18 + 19 + export const PubBlueskyPostBlock = ({ post }: { post: PostView }) => { 20 + switch (true) { 21 + case AppBskyFeedDefs.isBlockedPost(post) || 22 + AppBskyFeedDefs.isBlockedAuthor(post) || 23 + AppBskyFeedDefs.isNotFoundPost(post): 24 + return ( 25 + <div className={`w-full`}> 26 + <PostNotAvailable /> 27 + </div> 28 + ); 29 + 30 + case AppBskyFeedDefs.validatePostView(post).success: 31 + let record = post.record as AppBskyFeedDefs.PostView["record"]; 32 + let facets = record.facets; 33 + 34 + // silliness to get the text and timestamp from the record with proper types 35 + let text: string | null = null; 36 + let timestamp: string | undefined = undefined; 37 + if (AppBskyFeedPost.isRecord(record)) { 38 + text = (record as AppBskyFeedPost.Record).text; 39 + timestamp = (record as AppBskyFeedPost.Record).createdAt; 40 + } 41 + 42 + //getting the url to the post 43 + let postId = post.uri.split("/")[4]; 44 + let url = `https://bsky.app/profile/${post.author.handle}/post/${postId}`; 45 + 46 + let datetimeFormatted = new Date( 47 + timestamp ? timestamp : "", 48 + ).toLocaleString("en-US", { 49 + month: "short", 50 + day: "numeric", 51 + year: "numeric", 52 + hour: "numeric", 53 + minute: "numeric", 54 + hour12: true, 55 + }); 56 + return ( 57 + <div 58 + className={` 59 + block-border 60 + mb-2 61 + flex flex-col gap-2 relative w-full overflow-hidden group/blueskyPostBlock sm:p-3 p-2 text-sm text-secondary bg-bg-page 62 + `} 63 + > 64 + {post.author && record && ( 65 + <> 66 + <div className="bskyAuthor w-full flex items-center gap-2"> 67 + <img 68 + src={post.author?.avatar} 69 + alt={`${post.author?.displayName}'s avatar`} 70 + className="shink-0 w-8 h-8 rounded-full border border-border-light" 71 + /> 72 + <div className="grow flex flex-col gap-0.5 leading-tight"> 73 + <div className=" font-bold text-secondary"> 74 + {post.author?.displayName} 75 + </div> 76 + <a 77 + className="text-xs text-tertiary hover:underline" 78 + target="_blank" 79 + href={`https://bsky.app/profile/${post.author?.handle}`} 80 + > 81 + @{post.author?.handle} 82 + </a> 83 + </div> 84 + </div> 85 + 86 + <div className="flex flex-col gap-2 "> 87 + <div> 88 + <pre className="whitespace-pre-wrap"> 89 + {BlueskyRichText({ 90 + record: record as AppBskyFeedPost.Record | null, 91 + })} 92 + </pre> 93 + </div> 94 + {post.embed && ( 95 + <BlueskyEmbed embed={post.embed} postUrl={url} /> 96 + )} 97 + </div> 98 + </> 99 + )} 100 + <div className="w-full flex gap-2 items-center justify-between"> 101 + <div className="text-xs text-tertiary">{datetimeFormatted}</div> 102 + <div className="flex gap-2 items-center"> 103 + {post.replyCount && post.replyCount > 0 && ( 104 + <> 105 + <a 106 + className="flex items-center gap-1 hover:no-underline" 107 + target="_blank" 108 + href={url} 109 + > 110 + {post.replyCount} 111 + <CommentTiny /> 112 + </a> 113 + <Separator classname="h-4" /> 114 + </> 115 + )} 116 + 117 + <a className="" target="_blank" href={url}> 118 + <BlueskyTiny /> 119 + </a> 120 + </div> 121 + </div> 122 + </div> 123 + ); 124 + } 125 + };
+29 -2
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 2 2 import { AtUri } from "@atproto/syntax"; 3 3 import { ids } from "lexicons/api/lexicons"; 4 4 import { 5 + PubLeafletBlocksBskyPost, 5 6 PubLeafletDocument, 6 7 PubLeafletPagesLinearDocument, 7 8 PubLeafletPublication, 8 9 } from "lexicons/api"; 9 10 import { Metadata } from "next"; 10 - import { AtpAgent } from "@atproto/api"; 11 + import { AtpAgent, Agent, AtpBaseClient } from "@atproto/api"; 11 12 import { QuoteHandler } from "./QuoteHandler"; 12 13 import { InteractionDrawer } from "./Interactions/InteractionDrawer"; 13 14 import { ··· 19 20 import { PostPage } from "./PostPage"; 20 21 import { PageLayout } from "./PageLayout"; 21 22 import { extractCodeBlocks } from "./extractCodeBlocks"; 23 + import { getIdentityData } from "actions/getIdentityData"; 24 + import { createOauthClient } from "src/atproto-oauth"; 22 25 23 26 export async function generateMetadata(props: { 24 27 params: Promise<{ publication: string; did: string; rkey: string }>; ··· 61 64 </p> 62 65 </div> 63 66 ); 64 - let agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 67 + let identity = await getIdentityData(); 68 + let agent; 69 + if (identity?.atp_did) { 70 + const oauthClient = await createOauthClient(); 71 + let credentialSession = await oauthClient.restore(identity.atp_did); 72 + agent = new Agent(credentialSession); 73 + } else agent = new AtpAgent({ service: "https://public.api.bsky.app" }); 65 74 let [document, profile] = await Promise.all([ 66 75 getPostPageData( 67 76 AtUri.make( ··· 83 92 </div> 84 93 ); 85 94 let record = document.data as PubLeafletDocument.Record; 95 + let bskyPosts = record.pages.flatMap((p) => { 96 + let page = p as PubLeafletPagesLinearDocument.Main; 97 + return page.blocks?.filter( 98 + (b) => b.block.$type === ids.PubLeafletBlocksBskyPost, 99 + ); 100 + }); 101 + let bskyPostData = 102 + bskyPosts.length > 0 103 + ? await agent.getPosts({ 104 + uris: bskyPosts 105 + .map((p) => { 106 + let block = p?.block as PubLeafletBlocksBskyPost.Main; 107 + return block.postRef.uri; 108 + }) 109 + .slice(0, 24), 110 + }) 111 + : { data: { posts: [] } }; 86 112 let firstPage = record.pages[0]; 87 113 let blocks: PubLeafletPagesLinearDocument.Block[] = []; 88 114 if (PubLeafletPagesLinearDocument.isMain(firstPage)) { ··· 128 154 pubRecord={pubRecord} 129 155 profile={profile.data} 130 156 document={document} 157 + bskyPostData={bskyPostData.data.posts} 131 158 did={did} 132 159 blocks={blocks} 133 160 name={decodeURIComponent((await props.params).publication)}
+2
lexicons/api/index.ts
··· 8 8 import * as PubLeafletDocument from './types/pub/leaflet/document' 9 9 import * as PubLeafletPublication from './types/pub/leaflet/publication' 10 10 import * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote' 11 + import * as PubLeafletBlocksBskyPost from './types/pub/leaflet/blocks/bskyPost' 11 12 import * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code' 12 13 import * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header' 13 14 import * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule' ··· 39 40 export * as PubLeafletDocument from './types/pub/leaflet/document' 40 41 export * as PubLeafletPublication from './types/pub/leaflet/publication' 41 42 export * as PubLeafletBlocksBlockquote from './types/pub/leaflet/blocks/blockquote' 43 + export * as PubLeafletBlocksBskyPost from './types/pub/leaflet/blocks/bskyPost' 42 44 export * as PubLeafletBlocksCode from './types/pub/leaflet/blocks/code' 43 45 export * as PubLeafletBlocksHeader from './types/pub/leaflet/blocks/header' 44 46 export * as PubLeafletBlocksHorizontalRule from './types/pub/leaflet/blocks/horizontalRule'
+18
lexicons/api/lexicons.ts
··· 183 183 }, 184 184 }, 185 185 }, 186 + PubLeafletBlocksBskyPost: { 187 + lexicon: 1, 188 + id: 'pub.leaflet.blocks.bskyPost', 189 + defs: { 190 + main: { 191 + type: 'object', 192 + required: ['postRef'], 193 + properties: { 194 + postRef: { 195 + type: 'ref', 196 + ref: 'lex:com.atproto.repo.strongRef', 197 + }, 198 + }, 199 + }, 200 + }, 201 + }, 186 202 PubLeafletBlocksCode: { 187 203 lexicon: 1, 188 204 id: 'pub.leaflet.blocks.code', ··· 437 453 'lex:pub.leaflet.blocks.math', 438 454 'lex:pub.leaflet.blocks.code', 439 455 'lex:pub.leaflet.blocks.horizontalRule', 456 + 'lex:pub.leaflet.blocks.bskyPost', 440 457 ], 441 458 }, 442 459 alignment: { ··· 1685 1702 PubLeafletDocument: 'pub.leaflet.document', 1686 1703 PubLeafletPublication: 'pub.leaflet.publication', 1687 1704 PubLeafletBlocksBlockquote: 'pub.leaflet.blocks.blockquote', 1705 + PubLeafletBlocksBskyPost: 'pub.leaflet.blocks.bskyPost', 1688 1706 PubLeafletBlocksCode: 'pub.leaflet.blocks.code', 1689 1707 PubLeafletBlocksHeader: 'pub.leaflet.blocks.header', 1690 1708 PubLeafletBlocksHorizontalRule: 'pub.leaflet.blocks.horizontalRule',
+27
lexicons/api/types/pub/leaflet/blocks/bskyPost.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../lexicons' 7 + import { $Typed, is$typed as _is$typed, OmitKey } from '../../../../util' 8 + import type * as ComAtprotoRepoStrongRef from '../../../com/atproto/repo/strongRef' 9 + 10 + const is$typed = _is$typed, 11 + validate = _validate 12 + const id = 'pub.leaflet.blocks.bskyPost' 13 + 14 + export interface Main { 15 + $type?: 'pub.leaflet.blocks.bskyPost' 16 + postRef: ComAtprotoRepoStrongRef.Main 17 + } 18 + 19 + const hashMain = 'main' 20 + 21 + export function isMain<V>(v: V) { 22 + return is$typed(v, id, hashMain) 23 + } 24 + 25 + export function validateMain<V>(v: V) { 26 + return validate<Main & V>(v, id, hashMain) 27 + }
+2
lexicons/api/types/pub/leaflet/pages/linearDocument.ts
··· 14 14 import type * as PubLeafletBlocksMath from '../blocks/math' 15 15 import type * as PubLeafletBlocksCode from '../blocks/code' 16 16 import type * as PubLeafletBlocksHorizontalRule from '../blocks/horizontalRule' 17 + import type * as PubLeafletBlocksBskyPost from '../blocks/bskyPost' 17 18 18 19 const is$typed = _is$typed, 19 20 validate = _validate ··· 46 47 | $Typed<PubLeafletBlocksMath.Main> 47 48 | $Typed<PubLeafletBlocksCode.Main> 48 49 | $Typed<PubLeafletBlocksHorizontalRule.Main> 50 + | $Typed<PubLeafletBlocksBskyPost.Main> 49 51 | { $type: string } 50 52 alignment?: 51 53 | 'lex:pub.leaflet.pages.linearDocument#textAlignLeft'
+18
lexicons/pub/leaflet/blocks/bskyPost.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.blocks.bskyPost", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": [ 8 + "postRef" 9 + ], 10 + "properties": { 11 + "postRef": { 12 + "type": "ref", 13 + "ref": "com.atproto.repo.strongRef" 14 + } 15 + } 16 + } 17 + } 18 + }
+2 -1
lexicons/pub/leaflet/pages/linearDocument.json
··· 31 31 "pub.leaflet.blocks.website", 32 32 "pub.leaflet.blocks.math", 33 33 "pub.leaflet.blocks.code", 34 - "pub.leaflet.blocks.horizontalRule" 34 + "pub.leaflet.blocks.horizontalRule", 35 + "pub.leaflet.blocks.bskyPost" 35 36 ] 36 37 }, 37 38 "alignment": {
+15
lexicons/src/blocks.ts
··· 19 19 }, 20 20 }; 21 21 22 + export const PubLeafletBlocksBskyPost: LexiconDoc = { 23 + lexicon: 1, 24 + id: "pub.leaflet.blocks.bskyPost", 25 + defs: { 26 + main: { 27 + type: "object", 28 + required: ["postRef"], 29 + properties: { 30 + postRef: { type: "ref", ref: "com.atproto.repo.strongRef" }, 31 + }, 32 + }, 33 + }, 34 + }; 35 + 22 36 export const PubLeafletBlocksBlockQuote: LexiconDoc = { 23 37 lexicon: 1, 24 38 id: "pub.leaflet.blocks.blockquote", ··· 201 215 PubLeafletBlocksMath, 202 216 PubLeafletBlocksCode, 203 217 PubLeafletBlocksHorizontalRule, 218 + PubLeafletBlocksBskyPost, 204 219 ]; 205 220 export const BlockUnion: LexRefUnion = { 206 221 type: "union",