Skip to content

bitsocialnet/bitsocial-react-hooks

Repository files navigation

CI Coverage License Commitizen friendly

React Hooks banner

Bitsocial React Hooks

React hooks for the Bitsocial protocol. Build decentralized, serverless social apps with React using a familiar hooks API — fetch feeds, comments, author profiles, manage accounts, publish content, and more, all without a central server.

This package is currently consumed directly from bitsocialnet/bitsocial-react-hooks and is used by 5chan and other Bitsocial clients.

Installation

yarn add https://github.com/bitsocialnet/bitsocial-react-hooks.git#<commit-hash>

Use a pinned commit hash (or tag) so installs are reproducible. The published build is self-contained ESM, so consumers should not need postinstall import-rewrite patches.

Development Setup

nvm install
nvm use
corepack enable
yarn install

Run corepack enable once per machine so plain yarn resolves to the pinned Yarn 4 release.


Table of Contents

Documentation Links

API Reference

Hooks

Accounts Hooks

useAccount(): Account | undefined
useAccountComment({commentIndex?: number, commentCid?: string}): Comment // get one own comment by index or cid
useAccountComments({filter?: AccountPublicationsFilter, commentCid?: string, commentIndices?: number[], communityAddress?: string, parentCid?: string, newerThan?: number, page?: number, pageSize?: number, sortType?: "new" | "old"}): {accountComments: Comment[]} // export or display list of own comments
useAccountVotes({filter?: AccountPublicationsFilter, vote?: number, commentCid?: string, communityAddress?: string, newerThan?: number, page?: number, pageSize?: number, sortType?: "new" | "old"}): {accountVotes: Vote[]}  // export or display list of own votes
useAccountVote({commentCid: string}): Vote // know if you already voted on some comment
useAccountEdits({filer: AccountPublicationsFilter}):  {accountEdits: AccountEdit[]}
useAccountCommunities(): {accountCommunities: {[communityAddress: string]: AccountCommunity}, onlyIfCached?: boolean}
useAccounts(): Account[]
useNotifications(): {notifications: Notification[], markAsRead: Function}

Comments Hooks

useComment({commentCid: string, onlyIfCached?: boolean, autoUpdate?: boolean}): Comment & {refresh: Function}
useReplies({comment: Comment, onlyIfCached?: boolean, sortType?: string, flat?: boolean, repliesPerPage?: number, filter?: CommentsFilter, accountComments?: {newerThan: number, append?: boolean}}): {replies: Comment[], hasMore: boolean, loadMore: function, reset: function, updatedReplies: Comment[], bufferedReplies: Comment[]}
useComments({commentCids: string[], onlyIfCached?: boolean, autoUpdate?: boolean}): {comments: Comment[], refresh: Function}
useEditedComment({comment: Comment}): {editedComment: Comment | undefined}
useValidateComment({comment: Comment, validateReplies?: boolean}): {valid: boolean}

Communities Hooks

useCommunity({community: {name?: string, publicKey?: string}, onlyIfCached?: boolean}): Community
useCommunities({communities?: CommunityIdentifier[], onlyIfCached?: boolean}): {communities: Communities[]}
useCommunityStats({community: {name?: string, publicKey?: string}, onlyIfCached?: boolean}): CommunityStats
useResolvedCommunityAddress({communityAddress: string, cache: boolean}): {resolvedAddress: string | undefined} // use {cache: false} when checking the user's own community address

Pass { publicKey, name } when you have both so pkc-js can fetch through the public key and resolve the name in the background. communityAddress, communityAddresses, and communityRefs are no longer accepted by these hooks.

Authors Hooks

useAuthor({authorAddress: string, commentCid: string}): {author: Author | undefined}
useAuthorAddress({comment: Comment}): {authorAddress: string | undefined, shortAuthorAddress: string | undefined, authorAddressChanged: boolean}
useAuthorComments({authorAddress: string, commentCid: string, filter?: CommentsFilter}): {authorComments: Comment[], hasMore: boolean, loadMore: Promise<void>}
useResolvedAuthorAddress({author?: Author, cache?: boolean}): {resolvedAddress: string | undefined, nameResolver: NameResolverInfo | undefined} // supports .eth/.bso aliases; use {cache: false} when checking the user's own author address
useAuthorAvatar({author?: Author}): {imageUrl: string | undefined}
setAuthorAvatarsWhitelistedTokenAddresses(tokenAddresses: string[])

Feeds Hooks

useFeed({communities?: CommunityIdentifier[], sortType?: string, postsPerPage?: number, filter?: CommentsFilter, newerThan?: number, accountComments?: {newerThan: number, append?: boolean}, modQueue: ['pendingApproval']}): {feed: Comment[], loadMore: function, hasMore: boolean, reset: function, updatedFeed: Comment[], bufferedFeed: Comment[], communityKeysWithNewerPosts: string[]}
useBufferedFeeds({feedsOptions: UseFeedOptions[]}) // preload or buffer feeds in the background, so they load faster when you call `useFeed`

useFeed().reset() clears the current feed and refreshes the latest community snapshots before rebuilding it.

Actions Hooks

useSubscribe({communityAddress: string}): {subscribed: boolean | undefined, subscribe: Function, unsubscribe: Function}
useBlock({address?: string, cid?: string}): {blocked: boolean | undefined, block: Function, unblock: Function}
usePublishComment(options: UsePublishCommentOptions): {index: number, abandonPublish: () => Promise<void>, ...UsePublishCommentResult}
usePublishVote(options: UsePublishVoteOptions): UsePublishVoteResult
usePublishCommentEdit(options: UsePublishCommentEditOptions): UsePublishCommentEditResult
usePublishCommentModeration(options: UsePublishCommentModerationOptions): UsePublishCommentModerationResult
usePublishCommunityEdit(options: UsePublishCommunityEditOptions): UsePublishCommunityEditResult
useCreateCommunity(options: CreateCommunityOptions): {createdCommunity: Community | undefined, createCommunity: Function}

States Hooks

useClientsStates({comment?: Comment, community?: Community}): {states, peers}
useCommunitiesStates({communities?: CommunityIdentifier[]}): {states, peers}

RPC Hooks

usePkcRpcSettings(): {pkcRpcSettings: {pkcOptions, challenges}, setPkcRpcSettings: Function}

Actions with no hooks implementations yet

createAccount(account: Account)
deleteAccount(accountName: string)
setAccount(account: Account)
setActiveAccount(accountName: string)
setAccountsOrder(accountNames: string[])
importAccount(serializedAccount: string)
exportAccount(accountName: string): string // don't allow undefined to prevent catastrophic bugs
deleteCommunity(communityAddress: string, accountName?: string)
deleteComment(commentCidOrAccountCommentIndex: string | number, accountName?: string): Promise<void>

Utility functions

setPkcJs(PKC) // swap the underlying protocol client implementation, e.g. for mocks or Electron
deleteDatabases() // delete all databases, including all caches and accounts data
deleteCaches() // delete the cached comments, cached communities and cached pages only, no accounts data

Recipes

Getting started

import { useComment, useAccount } from "@bitsocialnet/bitsocial-react-hooks";

const account = useAccount();
const comment = useComment({ commentCid });

Get the active account, if none exist in browser database, a default account is generated

const account = useAccount();

Create accounts and change active account

import {
  useAccount,
  useAccounts,
  createAccount,
  setActiveAccount,
} from "@bitsocialnet/bitsocial-react-hooks";

const account = useAccount();
const { accounts } = useAccounts();

// on first render
console.log(accounts.length); // 1
console.log(account.name); // 'Account 1'

await createAccount(); // create 'Account 2'
await createAccount(); // create 'Account 3'
await setActiveAccount("Account 3");

// on render after updates
console.log(accounts.length); // 3
console.log(account.name); // 'Account 3'

// you are now publishing from 'Account 3' because it is the active one
const { publishComment } = usePublishComment(publishCommentOptions);
await publishComment();

Get a post

const post = useComment({ commentCid });

// manual refresh is always available
await post.refresh();

// post.author.address should not be used directly, it needs to be verified asynchronously using useAuthorAddress
const { authorAddress, shortAuthorAddress } = useAuthorAddress({ comment: post });
// exception: when linking to an author profile page, /u/${comment.author.address}/c/${comment.cid} should be used, not useAuthorAddress({comment}).authorAddress

// use many times in a page without affecting performance
const post = useComment({ commentCid, onlyIfCached: true });

// disable background polling and refresh on demand
const post = useComment({ commentCid, autoUpdate: false });
await post.refresh();

// post.replies are not validated, to show replies
const { replies, hasMore, loadMore } = useReplies({ comment: post });

// only use the comment's preloaded replies plus any reply pages already cached in memory
// won't fetch missing reply pages; hasMore only reflects cached replies still available to load
const cachedReplies = useReplies({ comment: post, onlyIfCached: true });

// to show a preloaded reply without rerenders, validate manually
const { valid } = useValidateComment({ comment: post.replies.pages.best.comments[0] });
if (valid === false) {
  // don't show this reply, it's malicious
}
// won't cause any rerenders if true

Get a comment

const comment = useComment({ commentCid });
const { comments, refresh } = useComments({ commentCids: [commentCid1, commentCid2, commentCid3] });
await refresh();

// content
console.log(comment.content || comment.link || comment.title);

// comment.author.address should not be used directly, it needs to be verified asynchronously using useAuthorAddress
const { authorAddress, shortAuthorAddress } = useAuthorAddress({ comment });
// exception: when linking to an author profile page, /u/${comment.author.address}/c/${comment.cid} should be used, not useAuthorAddress({comment}).authorAddress

// use without affecting performance
const { comments } = useComments({ commentCids, onlyIfCached: true });

// disable background polling and refresh this list on demand
const frozenComments = useComments({ commentCids, autoUpdate: false });
await frozenComments.refresh();

Get author avatar

const comment = useComment({ commentCid });

// get the nft avatar image url of the comment author
const { imageUrl, state, error, chainProvider, metadataUrl } = useAuthorAvatar({
  author: comment.author,
});

// result
if (state === "succeeded") {
  console.log("Succeeded getting avatar image URL", imageUrl);
}
if (state === "failed") {
  console.log("Failed getting avatar image URL", error.message);
}

// pending
if (state === "fetching-owner") {
  console.log("Fetching NFT owner address from chain provider", chainProvider.urls);
}
if (state === "fetching-uri") {
  console.log("Fetching NFT URI from chain provider URL", chainProvider.urls);
}
if (state === "fetching-metadata") {
  console.log("Fetching NFT URI from", metadataUrl);
}

Get author profile page

// NOTE: you must have a comment cid from the author to load his profile page
// e.g. the page url would be /#/u/<authorAddress>/c/<commentCid>
const authorResult = useAuthor({ commentCid, authorAddress });
const { imageUrl } = useAuthorAvatar({ author: authorResult.author });
const { authorComments, lastCommentCid, hasMore, loadMore } = useAuthorComments({
  commentCid,
  authorAddress,
});

// result
if (authorResult.state === "succeeded") {
  console.log("Succeeded getting author", authorResult.author);
}
if (state === "failed") {
  console.log("Failed getting author", authorResult.error.message);
}

// listing the author comments with infinite scroll
import { Virtuoso } from "react-virtuoso";

<Virtuoso
  data={authorComments}
  itemContent={(index, comment) => <Comment index={index} comment={comment} />}
  useWindowScroll={true}
  components={{ Footer: hasMore ? () => <Loading /> : undefined }}
  endReached={loadMore}
  increaseViewportBy={{ bottom: 600, top: 600 }}
/>;

// it is recommended to always redirect the user to the last known comment cid
// in case they want to share the url with someone, the author's comments
// will load faster when using the last comment cid
import { useParams } from "react-router-dom";
const params = useParams();

useEffect(() => {
  if (lastCommentCid && params.comentCid !== lastCommentCid) {
    history.push(`/u/${params.authorAddress}/c/${lastCommentCid}`);
  }
}, [lastCommentCid]);

// search an author's comments
const createSearchFilter = (searchTerm) => ({
  filter: (comment) => comment.title?.includes(searchTerm) || comment.content?.includes(searchTerm),
  key: `includes-${searchTerm}`, // required key to cache the filter
});
const filter = createSearchFilter("bitcoin");
const { authorComments, lastCommentCid, hasMore, loadMore } = useAuthorComments({
  commentCid,
  authorAddress,
  filter,
});

Get a community

const community = useCommunity({ community: { name: communityAddress, publicKey: communityPublicKey } });
const communityStats = useCommunityStats({
  community: { name: communityAddress, publicKey: communityPublicKey },
});
const { communities } = useCommunities({
  communities: [
    { name: communityAddress, publicKey: communityPublicKey },
    { name: communityAddress2, publicKey: communityPublicKey2 },
    { name: communityAddress3, publicKey: communityPublicKey3 },
  ],
});

// use without affecting performance
const { communities: cachedCommunities } = useCommunities({
  communities: [
    { name: communityAddress, publicKey: communityPublicKey },
    { name: communityAddress2, publicKey: communityPublicKey2 },
    { name: communityAddress3, publicKey: communityPublicKey3 },
  ],
  onlyIfCached: true,
});

// community.posts are not validated, to show posts
const { feed, hasMore, loadMore } = useFeed({
  communities: [{ name: communityAddress, publicKey: communityPublicKey }],
});

// to show a preloaded post without rerenders, validate manually
const { valid } = useValidateComment({ comment: community.posts.pages.topAll.comments[0] });
if (valid === false) {
  // don't show this post, it's malicious
}
// won't cause any rerenders if true

Create a post or comment using callbacks

const onChallenge = async (challenges: Challenge[], comment: Comment) => {
  let challengeAnswers: string[]
  try {
    // ask the user to complete the challenges in a modal window
    challengeAnswers = await getChallengeAnswersFromUser(challenges)
  }
  catch (e) {
    // if he declines, throw error and don't get a challenge answer
  }
  if (challengeAnswers) {
    // if user declines, publishChallengeAnswers is not called, retry loop stops
    await comment.publishChallengeAnswers(challengeAnswers)
  }
}

const onChallengeVerification = (challengeVerification, comment) => {
  // if the challengeVerification fails, a new challenge request will be sent automatically
  // to break the loop, the user must decline to send a challenge answer
  // if the community owner sends more than 1 challenge for the same challenge request, subsequents will be ignored
  if (challengeVerification.challengeSuccess === true) {
    console.log('challenge success', {publishedCid: challengeVerification.publication.cid})
  }
  else if (challengeVerification.challengeSuccess === false) {
    console.error('challenge failed', {reason: challengeVerification.reason, errors: challengeVerification.errors})
  }
}

const onError = (error, comment) => console.error(error)

const publishCommentOptions = {
  content: 'hello',
  title: 'hello',
  communityAddress: '12D3KooW...',
  onChallenge,
  onChallengeVerification,
  onError
}

const {index, state, publishComment, abandonPublish} = usePublishComment(publishCommentOptions)

// create post
await publishComment()
// pending comment index
console.log(index)
// pending comment state
console.log(state)

// after publishComment is called, the account comment index gets defined
// it is recommended to immediately redirect the user to a page displaying
// the user's comment with a "pending" label
if (index !== undefined) {
  history.push(`/profile/c/${index}`)
  // on the "pending" comment page, you can get the pending comment by doing
  // const accountComment = useAccountComment({commentIndex: index})
  // after accountComment.cid gets defined, it means the comment was published successfully
  // it is recommended to immediately redirect to `/p/${accountComment.communityAddress}/c/${useAccountComment.cid}`
}

// if the user closes the challenge modal and wants to cancel publishing:
await abandonPublish()
// the pending local account comment is removed from accountComments
// this works even if called immediately from onChallenge before publishComment() resolves

// reply to a post or comment
const publishReplyOptions = {
  content: 'hello',
  parentCid: 'Qm...', // the cid of the comment to reply to
  communityAddress: '12D3KooW...',
  onChallenge,
  onChallengeVerification,
  onError
}
const {publishComment} = usePublishComment(publishReplyOptions)
await publishComment()

// when displaying replies, it is recommended to include the user's pending replies
// https://github.com/bitsocialnet/bitsocial-react-hooks/#get-replies-to-a-post-nested (nested)
// https://github.com/bitsocialnet/bitsocial-react-hooks/#get-replies-to-a-post-flattened-not-nested (not nested)

Create a post or comment using hooks

const publishCommentOptions = {
  content: "hello",
  title: "hello",
  communityAddress: "12D3KooW...",
};

const {
  index,
  state,
  publishComment,
  challenge,
  challengeVerification,
  publishChallengeAnswers,
  abandonPublish,
  error,
} = usePublishComment(publishCommentOptions);

if (challenge) {
  // display challenges to user and call publishChallengeAnswers(challengeAnswers)
}

if (challengeVerification) {
  // display challengeVerification.challengeSuccess to user
  // redirect to challengeVerification.publication.cid
}

if (error) {
  // display error to user
}

// if the user closes your challenge modal:
if (challenge && challengeModalClosedByUser) {
  await abandonPublish();
}

// after publishComment is called, the account comment index gets defined
// it is recommended to immediately redirect the user to a page displaying
// the user's comment with a "pending" label
if (index !== undefined) {
  history.push(`/profile/c/${index}`);
  // on the "pending" comment page, you can get the pending comment by doing
  // const accountComment = useAccountComment({commentIndex: index})
  // after accountComment.cid gets defined, it means the comment was published successfully
  // it is recommended to immediately redirect to `/p/${accountComment.communityAddress}/c/${useAccountComment.cid}`
}

// create post
await publishComment();

Create a post or comment anonymously (without account.signer or account.author)

const account = useAccount();
const signer = await account.pkc.createSigner();

const publishCommentOptions = {
  content: "hello",
  title: "hello",
  communityAddress: "12D3KooW...",
  // use a newly generated author address (optional)
  signer,
  // use a different display name (optional)
  author: {
    displayName: "Esteban",
    address: signer.address,
  },
};

const { publishComment } = usePublishComment(publishCommentOptions);
await publishComment();

Create a vote

const commentCid = "QmZVYzLChjKrYDVty6e5JokKffGDZivmEJz9318EYfp2ui";
const publishVoteOptions = {
  commentCid,
  vote: 1,
  communityAddress: "news.eth",
  onChallenge,
  onChallengeVerification,
  onError,
};
const { state, error, publishVote } = usePublishVote(publishVoteOptions);

await publishVote();
console.log(state);
console.log(error);

// display the user's vote
const { vote } = useAccountVote({ commentCid });

if (vote === 1) console.log("user voted 1");
if (vote === -1) console.log("user voted -1");
if (vote === 0) console.log("user voted 0");
if (vote === undefined) console.log(`user didn't vote yet`);

Create a comment edit

const publishCommentEditOptions = {
  commentCid: "QmZVYzLChjKrYDVty6e5JokKffGDZivmEJz9318EYfp2ui",
  content: "edited content",
  communityAddress: "news.eth",
  onChallenge,
  onChallengeVerification,
  onError,
};
const { state, error, publishCommentEdit } = usePublishCommentEdit(publishCommentEditOptions);

await publishCommentEdit();
console.log(state);
console.log(error);

// view the status of a comment edit instantly
let comment = useComment({ commentCid: publishCommentEditOptions.commentCid });
const { state: editedCommentState, editedComment } = useEditedComment({ comment });

// if the comment has a succeeded, failed or pending edit, use the edited comment
if (editedComment) {
  comment = editedComment;
}

let editLabel;
if (editedCommentState === "succeeded") {
  editLabel = { text: "EDITED", color: "green" };
}
if (editedCommentState === "pending") {
  editLabel = { text: "PENDING EDIT", color: "orange" };
}
if (editedCommentState === "failed") {
  editLabel = { text: "FAILED EDIT", color: "red" };
}

Create a comment moderation

const publishCommentModerationOptions = {
  commentCid: "QmZVYzLChjKrYDVty6e5JokKffGDZivmEJz9318EYfp2ui",
  communityAddress: "news.eth",
  commentModeration: { locked: true },
  onChallenge,
  onChallengeVerification,
  onError,
};
const { state, error, publishCommentModeration } = usePublishCommentModeration(
  publishCommentModerationOptions,
);

await publishCommentModeration();
console.log(state);
console.log(error);

// view the status of a comment moderation instantly
let comment = useComment({ commentCid: publishCommentModerationOptions.commentCid });
const { state: editedCommentState, editedComment } = useEditedComment({ comment });

// if the comment has a succeeded, failed or pending edit, use the edited comment
if (editedComment) {
  comment = editedComment;
}

let editLabel;
if (editedCommentState === "succeeded") {
  editLabel = { text: "EDITED", color: "green" };
}
if (editedCommentState === "pending") {
  editLabel = { text: "PENDING EDIT", color: "orange" };
}
if (editedCommentState === "failed") {
  editLabel = { text: "FAILED EDIT", color: "red" };
}

Delete a comment

You can remove comments from your local account database (local JSON export / IndexedDB state) in two ways. This only removes local account history entries; it does not delete already-published network comments.

1. Abandon a pending publish — if you just published and want to cancel before it propagates:

const { publishComment, abandonPublish } = usePublishComment(publishCommentOptions);

await publishComment();
// User changes mind — abandon the pending comment
await abandonPublish();
// Hook state returns to ready; the comment is removed from accountComments

2. Delete by index or CID — remove any of your comments (pending or published):

import { deleteComment, useAccountComments } from "@bitsocialnet/bitsocial-react-hooks";

// By account comment index (from usePublishComment or useAccountComment)
const { index, publishComment } = usePublishComment(publishCommentOptions);
await publishComment();
await deleteComment(index);

// By comment CID (from useAccountComments or useAccountComment)
const { accountComments } = useAccountComments();
const accountComment = accountComments[0];
await deleteComment(accountComment.cid);

Note: accountComment.index can change after deletions. If you delete a comment, indices of comments after it may shift. Prefer using commentCid when you need a stable identifier, or re-fetch accountComments after deletions.

Common cleanup pattern (remove failed UI clutter):

import { deleteComment, useAccountComments } from "@bitsocialnet/bitsocial-react-hooks";

const { accountComments } = useAccountComments();
const failedComments = accountComments.filter((comment) => comment.state === "failed");

for (const failedComment of failedComments) {
  // failed pending comments may not have a cid yet, so fallback to index
  await deleteComment(failedComment.cid || failedComment.index);
}

Subscribe to a community

let communityAddress = "news.eth";
communityAddress = "12D3KooWANwdyPERMQaCgiMnTT1t3Lr4XLFbK1z4ptFVhW2ozg1z";
communityAddress = "tech.eth";
const { subscribed, subscribe, unsubscribe } = useSubscribe({ communityAddress });
await subscribe();
console.log(subscribed); // true

// view subscriptions
const account = useAccount();
console.log(account.subscriptions); // ['news.eth', '12D3KooWANwdyPERMQaCgiMnTT1t3Lr4XLFbK1z4ptFVhW2ozg1z', 'tech.eth']

// unsubscribe
await unsubscribe();

// get a feed of subscriptions
const communities = account.subscriptions.map((communityAddress) => ({ name: communityAddress }));
const { feed, hasMore, loadMore } = useFeed({
  communities,
  sortType: "topAll",
});
console.log(feed);

Get feed

import {Virtuoso} from 'react-virtuoso'
const topAllCommunities = [
  {name: 'memes.eth', publicKey: '12D3KooWMemes...'},
  {publicKey: '12D3KooWNews...'},
  {publicKey: '12D3KooWTech...'},
]
const {feed, hasMore, loadMore} = useFeed({communities: topAllCommunities, sortType: 'topAll'})

<Virtuoso
  data={feed}
  itemContent={(index, post) => <Post index={index} post={post}/>}
  useWindowScroll={true}
  components={{Footer: hasMore ? () => <Loading/> : undefined}}
  endReached={loadMore}
  increaseViewportBy={{bottom: 600, top: 600}}
/>

// you probably will want to buffer some feeds in the background so they are already loaded
// when you need them
useBufferedFeeds({
  feedsOptions: [
    {communities: [{name: 'news.eth'}, {name: 'crypto.eth'}], sortType: 'new'},
    {communities: [{name: 'memes.eth', publicKey: '12D3KooWMemes...'}], sortType: 'topWeek'},
    {communities: [{publicKey: '12D3KooW...'}, {publicKey: '12D3KooW...'}, {publicKey: '12D3KooW...'}, {publicKey: '12D3KooW...'}], sortType: 'hot'}
  ]
})

// search a feed
const createSearchFilter = (searchTerm) => ({
  filter: (comment) => comment.title?.includes(searchTerm) || comment.content?.includes(searchTerm),
  key: `includes-${searchTerm}` // required key to cache the filter
})
const searchFilter = createSearchFilter('bitcoin')
const searchedCommunities = communityAddresses.map((communityAddress) => ({ name: communityAddress }))
const {feed, hasMore, loadMore} = useFeed({communities: searchedCommunities, filter: searchFilter})

// image only feed
const imageOnlyFilter = {
  filter: (comment) => getCommentLinkMediaType(comment?.link) === 'image',
  key: 'image-only' // required key to cache the filter
}
const {feed, hasMore, loadMore} = useFeed({
  communities: searchedCommunities,
  filter: imageOnlyFilter,
})

Get mod queue (pending approval)

import {Virtuoso} from 'react-virtuoso'
const {feed, hasMore, loadMore} = useFeed({
  communities: [{name: 'memes.eth'}, {publicKey: '12D3KooW...'}, {publicKey: '12D3KooW...'}],
  modQueue: ['pendingApproval']
})

<Virtuoso
  data={feed}
  itemContent={(index, post) => <Post index={index} post={post}/>}
  useWindowScroll={true}
  components={{Footer: hasMore ? () => <Loading/> : undefined}}
  endReached={loadMore}
  increaseViewportBy={{bottom: 600, top: 600}}
/>

Comments automatically drop out of this feed once they are no longer returned by the pending-approval mod-queue pages.

Approve a pending approval comment

const publishCommentModerationOptions = {
  commentCid: "QmZVYzLChjKrYDVty6e5JokKffGDZivmEJz9318EYfp2ui",
  communityAddress: "news.eth",
  commentModeration: { approved: true },
  onChallenge,
  onChallengeVerification,
  onError,
};
const { state, error, publishCommentModeration } = usePublishCommentModeration(
  publishCommentModerationOptions,
);

await publishCommentModeration();
console.log(state);
console.log(error);

Edit an account

import {useAccount, setAccount, useResolvedAuthorAddress} from '@bitsocialnet/bitsocial-react-hooks'
const account = useAccount() // or useAccount('Account 2') to use an account other than the active one

// `account.author.wallets` only auto-generates an `eth` wallet by default.
// `account.chainProviders` is the canonical chain config for wallets, NFT lookups, and other chain reads.
// `account.nameResolversChainProviders` optionally overrides only the RPCs used for `.eth` / `.bso` author-name resolution.
console.log(account.author.wallets.eth)

const author: {...account.author, displayName: 'John'}
const editedAccount = {
  ...account,
  author,
  chainProviders: {
    ...account.chainProviders,
    eth: { urls: ['https://ethereum-rpc.publicnode.com', 'viem', 'ethers.js'], chainId: 1 },
  },
  nameResolversChainProviders: {
    eth: { urls: ['https://ethereum-rpc.publicnode.com', 'viem'], chainId: 1 },
  },
}

await setAccount(editedAccount)

// check if the user has set their .eth or .bso author name properly, use {cache: false} or it won't update
const author = {...account.author, address: 'username.bso'} // or 'username.eth'
// authorAddress should equal to account.signer.address
const {resolvedAddress, state, error, chainProvider, nameResolver} = useResolvedAuthorAddress({author, cache: false})

// result
if (state === 'succeeded') {
  console.log('Succeeded resolving address', resolvedAddress)
}
if (state === 'failed') {
  console.log('Failed resolving address', error.message)
}

// pending
if (state === 'resolving' && nameResolver) {
  console.log(`Resolving ${nameResolver.nameSystem} address from ${nameResolver.providerLabel}`)
  console.log('Matching chain provider URLs', chainProvider?.urls)
}

Delete account

Note: deleting account is unrecoverable, warn the user to export/backup his account before deleting

import { deleteAccount } from "@bitsocialnet/bitsocial-react-hooks";

// delete active account
await deleteAccount();

// delete account by name
await deleteAccount("Account 2");

Get your own comments and votes

// all my own comments
const { accountComments } = useAccountComments();
for (const accountComment of accountComments) {
  // it is recommended to show a label in the UI if accountComment.state is 'pending' or 'failed'
  console.log("comment", accountComment.index, "is status", accountComment.state);
}
// `state` becomes `failed` as soon as a pending local publish records terminal failure (`publishingState === "failed"` and `state === "stopped"`) or a publish error, instead of waiting for the 20-minute fallback.
// note: accountComment.index can change after deletions; prefer commentCid for stable identifiers

// all my own votes
const { accountVotes } = useAccountVotes();

// my own comments in memes.eth
const communityAddress = "memes.eth";
const myCommentsInMemesEth = useAccountComments({ communityAddress });

// my own posts in memes.eth
const filter = useCallback(
  (comment) => comment.communityAddress === communityAddress && !comment.parentCid,
  [communityAddress],
);
const myPostsInMemesEth = useAccountComments({ filter });

// my own replies in a post with cid 'Qm...'
const postCid = "Qm...";
const filter = useCallback((comment) => comment.postCid === postCid, [postCid]);
const myCommentsInSomePost = useAccountComments({ filter });

// my own replies to a comment with cid 'Qm...'
const parentCommentCid = "Qm...";
const myRepliesToSomeComment = useAccountComments({ parentCid: parentCommentCid });

// recent own comments in memes.eth, newest first, one page at a time
const recentMyCommentsInMemesEth = useAccountComments({
  communityAddress,
  newerThan: 60 * 60 * 24 * 30,
  sortType: "new",
  page: 0,
  pageSize: 20,
});

// get one own comment directly by cid
const accountComment = useAccountComment({ commentCid: "Qm..." });

// get a specific set of own comments by account comment index
const replacementReplies = useAccountComments({ commentIndices: [5, 7, 9] });

// voted profile tab helpers
const recentUpvotes = useAccountVotes({
  vote: 1,
  newerThan: 60 * 60 * 24 * 30,
  sortType: "new",
  page: 0,
  pageSize: 20,
});

// know if you upvoted a comment already with cid 'Qm...'
const { vote } = useAccountVote({ commentCid: "Qm..." });
console.log(vote); // 1, -1 or 0

// my own pending posts in a feed
const { feed } = useFeed({
  communities: [{ name: communityAddress }],
  accountComments: { newerThan: Infinity, append: false },
});

// my own pending replies in a replies feed
const { replies } = useReplies({
  comment: post,
  accountComments: { newerThan: Infinity, append: false },
});

Determine if a comment is your own

const account = useAccount();
const comment = useComment({ commentCid });
const isMyOwnComment = account?.author.address === comment?.author.address;

Get account notifications

const { notifications, markAsRead } = useNotifications();
for (const notification of notifications) {
  console.log(notification);
}
await markAsRead();

const johnsNotifications = useNotifications({ accountName: "John" });
for (const notification of johnsNotifications.notifications) {
  console.log(notification);
}
await johnsNotifications.markAsRead();

// get the unread notification counts for all accounts
const { accounts } = useAccounts();
const accountsUnreadNotificationsCounts = accounts?.map(
  (account) => account.unreadNotificationCount,
);

Block an address (author, community or multisub)

const address: 'community-address.eth' // or 'author-address.eth' or '12D3KooW...'
const {blocked, unblock, block} = useBlock({address})

if (blocked) {
  console.log(`'${address}' is blocked`)
}
else {
  console.log(`'${address}' is not blocked`)
}

// to block
block()

// to unblock
unblock()

Block a cid (hide a comment)

const { blocked, unblock, block } = useBlock({ cid: "Qm..." });

if (blocked) {
  console.log(`'${cid}' is blocked`);
} else {
  console.log(`'${cid}' is not blocked`);
}

// to block
block();

// to unblock
unblock();

(Desktop only) Create a community

const createCommunityOptions = { title: "My community title" };
const { createdCommunity, createCommunity } = useCreateCommunity(createCommunityOptions);
await createCommunity();

// it is recommended to redirect to `p/${createdCommunity.address}` after creation
if (createdCommunity?.address) {
  console.log("created community with title", createdCommunity.title);
  history.push(`/p/${createdCommunity.address}`);
}

// after the community is created, fetch it using
const { accountCommunities } = useAccountCommunities();
const accountCommunityAddresses = Object.keys(accountCommunities);
const communities = useCommunities({
  communities: accountCommunityAddresses.map((communityAddress) => ({ name: communityAddress })),
});
// or
const _community = useCommunity({ community: { name: createdCommunity.address } });

(Desktop only) List the communities you created

const { accountCommunities } = useAccountCommunities();
const ownerCommunityAddresses = Object.keys(accountCommunities).filter(
  (communityAddress) => accountCommunities[communityAddress].role?.role === "owner",
);
const communities = useCommunities({
  communities: ownerCommunityAddresses.map((communityAddress) => ({ name: communityAddress })),
});

(Desktop only) Edit your community settings

const onChallenge = async (challenges: Challenge[], communityEdit: CommunityEdit) => {
  let challengeAnswers: string[]
  try {
    challengeAnswers = await getChallengeAnswersFromUser(challenges)
  }
  catch (e) {}
  if (challengeAnswers) {
    await communityEdit.publishChallengeAnswers(challengeAnswers)
  }
}

const onChallengeVerification = (challengeVerification, communityEdit) => {
  console.log('challenge verified', challengeVerification)
}

const onError = (error, communityEdit) => console.error(error)

// add ENS to your community
const editCommunityOptions = {
  communityAddress: '12D3KooWANwdyPERMQaCgiMnTT1t3Lr4XLFbK1z4ptFVhW2ozg1z', // the previous address before changing it
  address: 'your-community-address.eth', // the new address to change to
  onChallenge,
  onChallengeVerification,
  onError
}

await publishCommunityEdit()

// edit other community settings
const editCommunityOptions = {
  communityAddress: 'your-community-address.eth', // the address of the community to change
  title: 'Your title',
  description: 'Your description',
  onChallenge,
  onChallengeVerification,
  onError
}
const {publishCommunityEdit} = usePublishCommunityEdit(editCommunityOptions)
await publishCommunityEdit()

// verify if ENS was set correctly, use {cache: false} or it won't update
const {resolvedAddress} = useResolvedCommunityAddress({communityAddress: 'your-community-address.eth', cache: false})

// result
if (state === 'succeeded') {
  console.log('Succeeded resolving address', resolvedAddress)
  console.log('ENS set correctly', resolvedAddress === community.signer.address)
}
if (state === 'failed') {
  console.log('Failed resolving address', error.message)
}

// pending
if (state === 'resolving') {
  console.log('Resolving address from chain provider URL', chainProvider.urls)
}

Export and import account

import {
  exportAccount,
  importAccount,
  setActiveAccount,
  setAccountsOrder,
} from "@bitsocialnet/bitsocial-react-hooks";

// get active account 'Account 1'
const activeAccount = useAccount();

// export active account, tell user to copy or download this json
const activeAccountJson = await exportAccount();

// import account
await importAccount(activeAccountJson);

// get imported account 'Account 1 2' (' 2' gets added to account.name if account.name already exists)
const importedAccount = useAccount("Account 1 2");

// make imported account active account
await setActiveAccount("Account 1 2");

// reorder the accounts list
await setAccountsOrder(["Account 1 2", "Account 1"]);

View the status of a comment edit

let comment = useComment({ commentCid });
const { state: editedCommentState, editedComment } = useEditedComment({ comment });

// if the comment has a succeeded, failed or pending edit, use the edited comment
if (editedComment) {
  comment = editedComment;
}

let editLabel;
if (editedCommentState === "succeeded") {
  editLabel = { text: "EDITED", color: "green" };
}
if (editedCommentState === "pending") {
  editLabel = { text: "PENDING EDIT", color: "orange" };
}
if (editedCommentState === "failed") {
  editLabel = { text: "FAILED EDIT", color: "red" };
}

View the status of a specific comment edit property

const comment = useComment({ commentCid });
const editedComment = useEditedComment({ comment });
if (editedComment.failedEdits.removed !== undefined) {
  console.log("failed editing comment.removed property");
}
if (editedComment.succeededEdits.removed !== undefined) {
  console.log("succeeded editing comment.removed property");
}
if (editedCommentResult.pendingEdits.removed !== undefined) {
  console.log("pending editing comment.removed property");
}

// view the full comment with all edited properties (both succeeded and pending)
console.log(editedComment.editedComment);
console.log(editedComment.editedComment.commentModeration?.removed);

// view the state of all edits of the comment
console.log(editedComment.state); // 'unedited' | 'succeeded' | 'pending' | 'failed'

Moderation fields are mirrored on both the top-level keys like comment.removed and the nested comment.commentModeration.removed shape.

List all comment and community edits the account has performed

const { accountEdits } = useAccountEdits();
for (const accountEdit of accountEdits) {
  console.log(accountEdit);
}
console.log(`there's ${accountEdits.length} account edits`);

// get only the account edits of a specific comment
const commentCid = "Qm...";
const filter = useCallback((edit) => edit.commentCid === commentCid, [commentCid]); // important to use useMemo or the same function or will cause rerenders
const { accountEdits } = useAccountEdits({ filter });

// only get account edits in a specific community
const communityAddress = "news.eth";
const filter = useCallback(
  (edit) => edit.communityAddress === communityAddress,
  [communityAddress],
);
const { accountEdits } = useAccountEdits({ filter });

Get replies to a post (nested or flat)

import { useReplies, useComment, useAccountComment } from "@bitsocialnet/bitsocial-react-hooks";

// NOTE: recommended to use the same replies options for all depths, or will load slower
const useRepliesOptions = {
  sortType: "best",
  flat: false,
  repliesPerPage: 20,
  onlyIfCached: false,
  accountComments: { newerThan: Infinity, append: false },
};

const Reply = ({ reply, updatedReply }) => {
  const { replies, updatedReplies, bufferedReplies, hasMore, loadMore } = useReplies({
    ...useRepliesOptions,
    comment: reply,
  });

  // updatedReply updates values in real time, reply does not
  const score = (updatedReply?.upvoteCount || 0) - (updatedReply?.downvoteCount || 0);

  // bufferedReplies updates in real time, can show new replies count in real time
  const moreReplies =
    hasMore && bufferedReplies?.length !== 0 ? `(${bufferedReplies.length} more replies)` : "";

  // publishing states exist only on account comment
  const accountReply = useAccountComment({ commentIndex: reply.index });
  const state = accountReply?.state;
  const publishingStateString = useStateString(accountReply);

  return (
    <div>
      <div>
        {score} {reply.author.address} {reply.timestamp} {moreReplies}
      </div>
      {state === "pending" && <div>PENDING ({publishingStateString})</div>}
      {state === "failed" && <div>FAILED</div>}
      <div>{reply.content}</div>
      <div style={{ marginLeft: 4 }}>
        {replies.map((reply, index) => (
          <Reply
            key={reply?.index || reply?.cid}
            reply={reply}
            updatedReply={updatedReplies[index]}
          />
        ))}
      </div>
    </div>
  );
};

const comment = useComment({ commentCid });
const { replies, updatedReplies, hasMore, loadMore } = useReplies({
  ...useRepliesOptions,
  comment,
});
const repliesComponents = replies.map((reply, index) => (
  <Reply key={reply?.index || reply?.cid} reply={reply} updatedReply={updatedReplies[index]} />
));

Format short CIDs and addresses

import { useShortAddress, useShortCid } from "@bitsocialnet/bitsocial-react-hooks";

const shortParentCid = useShortCid(comment.parentCid);
const shortAddress = useShortAddress(address);

useBufferedFeeds with concurrency

const useBufferedFeedsWithConcurrency = ({feedOptions}) => {

  const communities = useCommunities()

  return useBufferedFeeds({feedsOptions})
}

const feedOptions = [
  {communities: [{name: 'news.eth'}, {name: 'crypto.eth'}], sortType: 'new'},
  {communities: [{name: 'memes.eth'}], sortType: 'topWeek'},
  {communities: [{publicKey: '12D3KooW...'}, {publicKey: '12D3KooW...'}, {publicKey: '12D3KooW...'}, {publicKey: '12D3KooW...'}], sortType: 'hot'},
  ...
]

useBufferedFeedsWithConcurrency({feedOptions})

About

Build decentralized social apps using simple React hooks

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

 

Packages

 
 
 

Contributors