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.
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.
nvm install
nvm use
corepack enable
yarn installRun corepack enable once per machine so plain yarn resolves to the pinned Yarn 4 release.
- Installation
- Documentation Links
- API Reference
- Recipes
- Getting started
- Get the active account, if none exist in browser database, a default account is generated
- Create accounts and change active account
- Get a post
- Get a comment
- Get author avatar
- Get author profile page
- Get a community
- Create a post or comment using callbacks
- Create a post or comment using hooks
- Create a post or comment anonymously (without account.signer or account.author)
- Create a vote
- Create a comment edit
- Create a comment moderation
- Delete a comment
- Subscribe to a community
- Get feed
- Get mod queue (pending approval)
- Approve a pending approval comment
- Edit an account
- Delete account
- Get your own comments and votes
- Determine if a comment is your own
- Get account notifications
- Block an address (author, community or multisub)
- Block a cid (hide a comment)
- (Desktop only) Create a community
- (Desktop only) List the communities you created
- (Desktop only) Edit your community settings
- Export and import account
- View the status of a comment edit
- View the status of a specific comment edit property
- List all comment and community edits the account has performed
- Get replies to a post (nested or flat)
- Format short CIDs and addresses
- useBufferedFeeds with concurrency
- Hooks API
- Getting started
- Install, testing and building: https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/docs/testing.md
- Mock content (for UI development): https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/docs/mock-content.md
- Algorithms: https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/docs/algorithms.md
- Schema (Types, IndexedDb and state management): https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/docs/schema.md
- Types: https://github.com/bitsocialnet/bitsocial-react-hooks/blob/master/src/types.ts
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}
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}
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.
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[])
useFeed({communities?: CommunityIdentifier[], sortType?: string, postsPerPage?: number, filter?: CommentsFilter, newerThan?: number, accountComments?: {newerThan: number, append?: boolean}, modQueue: ['pendingApproval']}): {feed: Comment[], loadMore: function, expandTimeWindow: 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.
useFeed().expandTimeWindow(newerThan) broadens newerThan in place for feeds whose derived sort type stays the same, so older posts can be appended without replacing the feed instance.
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}
useClientsStates({comment?: Comment, community?: Community}): {states, peers}
useCommunitiesStates({communities?: CommunityIdentifier[]}): {states, peers}
usePkcRpcSettings(): {pkcRpcSettings: {pkcOptions, challenges}, setPkcRpcSettings: Function}
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>
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
import { useComment, useAccount } from "@bitsocialnet/bitsocial-react-hooks";
const account = useAccount();
const comment = useComment({ commentCid });const account = useAccount();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();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 trueconst 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();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);
}// 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,
});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 trueconst 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)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();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();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`);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" };
}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" };
}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 accountComments2. 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.indexcan change after deletions. If you delete a comment, indices of comments after it may shift. Prefer usingcommentCidwhen you need a stable identifier, or re-fetchaccountCommentsafter 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);
}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);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,
})
// widen a freshness window without replacing the current feed instance
const {feed, expandTimeWindow} = useFeed({
communities: [{name: 'news.eth'}],
sortType: 'active',
newerThan: 60 * 60 * 24,
})
await expandTimeWindow(60 * 60 * 24 * 7)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.
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);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)
}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");// 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 },
});const account = useAccount();
const comment = useComment({ commentCid });
const isMyOwnComment = account?.author.address === comment?.author.address;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,
);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()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();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 } });const { accountCommunities } = useAccountCommunities();
const ownerCommunityAddresses = Object.keys(accountCommunities).filter(
(communityAddress) => accountCommunities[communityAddress].role?.role === "owner",
);
const communities = useCommunities({
communities: ownerCommunityAddresses.map((communityAddress) => ({ name: communityAddress })),
});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)
}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"]);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" };
}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.
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 });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]} />
));import { useShortAddress, useShortCid } from "@bitsocialnet/bitsocial-react-hooks";
const shortParentCid = useShortCid(comment.parentCid);
const shortAddress = useShortAddress(address);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})