BigBlocks

Social Feed

A chronological feed of on-chain BSocial posts with infinite scroll, channel filtering, and composable like buttons

Installation

bunx shadcn@latest add https://registry.bigblocks.dev/r/social-feed.json

Usage

The simplest usage fetches from https://api.1sat.app on mount and renders an infinite-scroll feed.

import { SocialFeed } from "@/components/blocks/social-feed"
 
export default function FeedPage() {
  return (
    <SocialFeed
      onPostClick={(post) => router.push(`/post/${post.txid}`)}
      onAuthorClick={(post) => router.push(`/user/${post.signers?.[0]?.bapId}`)}
    />
  )
}

Channel Filtering

Filter by BSocial channel to show only posts from a specific topic or community.

<SocialFeed channel="general" />
 
<SocialFeed channel="bitcoin" limit={10} />

Pass a query string to filter posts by content.

<SocialFeed query="bitcoin" />

Composing with LikeButton

Provide renderLikeButton to add on-chain likes to each post card. The post object is passed so you can wire up the correct txid and initial state.

import { SocialFeed } from "@/components/blocks/social-feed"
import { LikeButton } from "@/components/blocks/like-button"
 
export default function FeedWithLikes() {
  return (
    <SocialFeed
      renderLikeButton={(post) => (
        <LikeButton
          txid={post.txid}
          count={post.likes ?? 0}
          variant="text"
          onLike={async (txid) => {
            const result = await broadcastLike(txid)
            return { txid: result.txid }
          }}
          onUnlike={async (txid) => {
            const result = await broadcastUnlike(txid)
            return { txid: result.txid }
          }}
        />
      )}
    />
  )
}

Custom Post Cards

Replace the default PostCardUI entirely using renderPostCard. The second argument supplies pre-computed default props so you can spread them selectively.

import { SocialFeed } from "@/components/blocks/social-feed"
import type { SocialPost, PostCardUIProps } from "@/components/blocks/social-feed"
 
export default function CustomFeed() {
  return (
    <SocialFeed
      renderPostCard={(post: SocialPost, defaultProps: PostCardUIProps) => (
        <article className="p-4 border-b">
          <p className="text-sm text-muted-foreground">{post.txid}</p>
          <p>{post.content}</p>
        </article>
      )}
    />
  )
}

Manual Pagination

Disable infinite scroll to show an explicit "Load more" button instead.

<SocialFeed infiniteScroll={false} />

Custom API URL

Point the feed at your own 1sat-stack instance.

<SocialFeed apiUrl="https://your-api.example.com" />

Hook: useSocialFeed

Use useSocialFeed when you want full control over how posts are rendered, or when you need the raw feed state to combine with other data sources.

import { useSocialFeed, type SocialPost } from "@/components/blocks/social-feed"
 
export function CustomFeedView() {
  const { posts, isLoading, isLoadingMore, error, hasMore, loadMore, refresh } =
    useSocialFeed({
      channel: "general",
      limit: 10,
      apiUrl: "https://api.1sat.app",
    })
 
  if (isLoading) return <p>Loading...</p>
  if (error) return <p>Error: {error.message}</p>
 
  return (
    <div>
      {posts.map((post: SocialPost) => (
        <div key={post.txid}>
          <p>{post.content}</p>
        </div>
      ))}
      {hasMore && (
        <button onClick={loadMore} disabled={isLoadingMore}>
          {isLoadingMore ? "Loading..." : "Load more"}
        </button>
      )}
    </div>
  )
}

API Reference

SocialFeed

PropTypeDefaultDescription
channelstringChannel to filter posts by
querystringSearch query string
limitnumber20Number of posts per page
apiUrlstring"https://api.1sat.app"Base URL for the 1sat-stack API
autoFetchbooleantrueWhether to fetch on mount
classNamestringCSS classes for the outer container
onPostClick(post: SocialPost) => voidCalled when a post card is clicked
onAuthorClick(post: SocialPost) => voidCalled when an author is clicked
onReplyClick(post: SocialPost) => voidCalled when the reply button is clicked
infiniteScrollbooleantrueUse IntersectionObserver for infinite scroll
renderLikeButton(post: SocialPost) => React.ReactNodeRender function for a custom like button per post
renderPostCard(post: SocialPost, defaultProps: PostCardUIProps) => React.ReactNodeRender function to replace the default post card entirely

SocialPost

interface SocialPost {
  /** Transaction ID of the post */
  txid: string
  /** Post content text */
  content: string
  /** Timestamp in seconds since epoch */
  timestamp: number
  /** App name from MAP protocol */
  app: string
  /** Action type (post, reply, etc.) */
  type: string
  /** Channel the post belongs to, if any */
  channel?: string
  /** Signer/author information from AIP */
  signers?: PostSigner[]
  /** Resolved author profile (populated client-side) */
  author?: AuthorProfile
  /** Number of likes on this post */
  likes?: number
  /** Number of replies to this post */
  replies?: number
  /** Media outpoint if post has embedded media */
  mediaOutpoint?: string
}

PostSigner

interface PostSigner {
  /** Algorithm identifier (e.g. "BITCOIN_ECDSA") */
  algorithm: string
  /** Bitcoin address of the signer */
  address: string
  /** BAP identity ID, if present */
  bapId?: string
}

AuthorProfile

interface AuthorProfile {
  /** BAP identity ID */
  bapId: string
  /** Display name */
  name?: string
  /** Profile image URL or on-chain reference */
  avatar?: string
  /** Bitcoin address */
  address?: string
}

useSocialFeed Options

OptionTypeDefaultDescription
channelstringChannel to filter posts by
querystringSearch query string
limitnumber20Posts per page
apiUrlstring"https://api.1sat.app"API base URL
autoFetchbooleantrueWhether to fetch on mount

useSocialFeed Return

PropertyTypeDescription
postsSocialPost[]Array of fetched posts
isLoadingbooleanWhether the initial fetch is in progress
isLoadingMorebooleanWhether a "load more" request is in progress
errorError | nullError from the most recent fetch
hasMorebooleanWhether more posts are available
loadMore() => Promise<void>Fetch the next page
refresh() => Promise<void>Re-fetch from the beginning