# Bitsocial React Hooks v0.1.52 — full developer documentation
Generated from the same maintained Markdown as the website. Code, types, and tests remain authoritative.
# Your first feed
Documentation: /docs/quickstart/
Start with a feed. Bring your own components and styling; the hooks connect them to Bitsocial communities and their posts.
## Install in your React app
From an existing React application's directory, run:
```sh
yarn add @bitsocial/bitsocial-react-hooks
```
The package supports React 16.8 and later. Use Node.js 22 or later for your development environment. A browser-rendered React app, such as a Vite app, is a straightforward place to begin.
You do not need to add a provider or create an account manually. The library loads the browser's saved accounts and creates a default local account if none exist.
## Pick a community
Get a real community name or public key from the community you want to display. A name is passed as `{ name: communityName }`; a public key is passed as `{ publicKey: communityPublicKey }`.
The component below accepts the name as a prop, so you can connect it to a route, a community picker, or your app's configuration. It does not assume a particular community is online.
## Render the feed
Save this as `Feed.jsx` in your app:
```jsx
import {
useAccount,
useFeed,
} from "@bitsocial/bitsocial-react-hooks";
export default function Feed({ communityName = "" }) {
const name = communityName.trim();
const account = useAccount();
const { feed, hasMore, loadMore, error } = useFeed({
communities: name ? [{ name }] : [],
postsPerPage: 20,
});
if (!name) {
return
Choose a community to see its posts.
;
}
return (
{error &&
{error.message}
}
{!account &&
Opening your local account…
}
{account && feed.length === 0 && (
{hasMore ? "Waiting for posts…" : "No posts to show."}
)}
{feed.map((post) => (
{post.title || "Untitled post"}
{post.content}
{post.author?.displayName ||
post.author?.shortAddress ||
"Anonymous"}
))}
{account && feed.length > 0 && hasMore && (
)}
);
}
```
Render `` from your app, using the real name you chose. For a public key, change the `communities` entry to `{ publicKey: yourCommunityPublicKey }`.
The hook starts loading when the local account is ready. Omitting `sortType` lets it use the community's preloaded sort. Content renders as it arrives, and `loadMore()` requests the next page.
## Make loading feel right
An empty array can mean that data has not arrived yet. It does not, by itself, mean the community has no posts. Keep already loaded posts visible while more arrive, show available errors, and treat `hasMore` as a pagination signal.
Do not wait for `useFeed().state === "succeeded"` before displaying content: the feed's current state is tied to whether it has more pages. Likewise, awaiting `loadMore()` does not mean the network has finished loading that page.
## Using a framework with server rendering?
The package accesses browser APIs, including `window`, when imported. Keep the import and the component that uses it out of server execution. Use your framework's client-only loading mechanism with server rendering disabled for that component; a client-component directive alone may still allow server prerendering.
Browser storage holds the local account and cache. Treat account backup and recovery as part of your app's account experience before encouraging people to rely on it.
## Build the next piece
- [Understand accounts, shared state, and publishing](/docs/concepts/).
- [Explore feed options](/docs/api/feeds-hooks/) for sorting, filtering, and preloading.
- [Read a post and its comments](/docs/api/comments-hooks/).
- [Add publishing](/docs/recipes/create-a-post-or-comment-using-hooks/) with a challenge and cancellation flow.
- [Give your coding assistant the docs](/docs/ai/).
---
# How the hooks fit together
Documentation: /docs/concepts/
Your app owns the interface. The hooks connect it to shared protocol state: communities, posts, replies, authors, accounts, and the actions people take. The [Bitsocial protocol docs](https://docs.bitsocial.net/peer-to-peer-protocol/) explain how that state moves between peers without a server.
## Hooks read shared stores
The library uses Zustand stores underneath its React hooks. Components select the data they need; stores handle protocol requests, cached data, and subscriptions. There is no provider component to wrap around your app.
```jsx
const community = useCommunity({
community: { name: communityName },
});
const { feed } = useFeed({
communities: [{ name: communityName }],
});
```
Use these hooks wherever the data belongs in your component tree. Shared stores let components reuse fetched data and existing requests. You do not need a separate fetching effect that copies the same content into your own state.
Feed options such as the account, communities, sort, and filter identify a feed. Different options can intentionally produce different feeds. A custom filter needs a stable `key` that describes its behavior so its cached results can be identified correctly.
## A local account starts automatically
The account store restores accounts from browser storage when the library loads. If there are none, it creates a default account and signer. This happens asynchronously, so `useAccount()` can initially return `undefined`.
```jsx
const account = useAccount();
return (
{account
? `Posting as ${account.author?.shortAddress}`
: "Opening your local account…"}
);
```
Hooks use the active account by default. Where an options argument accepts `accountName`, pass `{ accountName }` to select another one. An account also carries local preferences and history, such as subscriptions, saved comments, and pending publications.
An account is stored in the browser, not recovered through an email login. Include export, import, and clear explanations of local storage in an app intended for everyday use. [Identity and ownership](https://docs.bitsocial.net/identity-and-ownership/) covers the key-based identity model behind it.
## Communities have identifiers
Reading hooks accept a community identifier object: `{ name }`, `{ publicKey }`, or both. A feed accepts an array of these objects so it can combine several communities.
Posts and replies are comments in the protocol. A published comment has a content identifier, or CID. Read it with `useComment({ commentCid })`; pass the resulting comment to `useReplies({ comment })` to load its replies.
```jsx
const comment = useComment({ commentCid });
const { replies, hasMore, loadMore } = useReplies({
comment,
repliesPerPage: 20,
});
```
`useComment` follows updates by default. Replies can be nested or flat. Use the same replies options throughout a nested thread where possible to reuse the same loading path.
## Data arrives progressively
Cached content, a comment's original content, and later updates can become available at different times. Render what you have and show loading or error feedback alongside it. Avoid hiding useful content every time a request is in progress.
For feeds and replies, `hasMore` drives pagination. Their `state` fields are not a promise that every network operation is idle. In particular, a feed with visible posts can still have the state `fetching-ipns` while more pages are available.
Use `loadMore()` to request more items and `reset()` to rebuild a feed. The return of a pagination call is not a signal that all requested items have arrived. Let the hook's data drive rendering.
## Publishing is a conversation with a community
`usePublishComment` creates posts and replies. Supply a `communityAddress` and content; add a `title` for a post or `parentCid` for a reply. The selected account supplies the signing identity.
```jsx
const publication = usePublishComment({
communityAddress,
title,
content,
});
// Call from a submit handler.
await publication.publishComment();
```
That awaited call starts the publication flow. It does **not** confirm that the community has accepted the post.
1. A pending local comment becomes available through the result's `index`.
2. A community may send a `challenge` message. Your interface presents its `challenges` and collects answers.
3. Call `publishChallengeAnswers(answers)` with an array of strings in challenge order.
4. Observe `challengeVerification.challengeSuccess`, errors, and the pending comment's eventual `cid` to show the outcome.
Use `useAccountComment({ commentIndex: index })` to display the pending comment. Check `index !== undefined`, because zero is a valid index. If the person cancels the flow, call `abandonPublish()` to cancel the pending attempt and remove its local pending comment.
Each community chooses its own anti-spam challenges, described in [custom challenges](https://docs.bitsocial.net/custom-challenges/). Some failed challenge answers trigger another challenge attempt. Terminal community rejections do not. Build feedback and cancellation into the form rather than assuming every failure should be retried by your app.
For replies, `accountComments: { newerThan: Infinity }` can keep local pending replies visible while canonical replies propagate. Pending comments may not have a CID yet; use their local index as a fallback key.
## Choose where to go next
- [Feed hooks](/docs/api/feeds-hooks/) cover pagination and feed configuration.
- [Comment hooks](/docs/api/comments-hooks/) cover individual content and its updates.
- [Action hooks](/docs/api/actions-hooks/) cover publishing, votes, subscriptions, and moderation.
- [The publishing recipe](/docs/recipes/create-a-post-or-comment-using-hooks/) shows the challenge flow in more detail.
- [Local moderation](https://docs.bitsocial.net/local-moderation/) explains how communities moderate without a global admin.
---
# Build with AI
Documentation: /docs/ai/
Give your coding assistant the same starting point you would give another developer: the package name, a small feature to build, and the relevant documentation.
## Choose the right amount of context
- [llms.txt](/llms.txt) is the compact entry point to the documentation.
- [llms-full.txt](/llms-full.txt) contains the expanded documentation for assistants that need more context.
- [Quickstart as Markdown](/docs/quickstart.md) gives an assistant the first-feed example without the website interface.
- [Core concepts as Markdown](/docs/concepts.md) explains accounts, loading, and publication behavior.
- [This page as Markdown](/docs/ai.md) keeps the starting prompt easy to reuse.
Start with the compact index and the page for the feature you need. Use the full file when your tool cannot follow documentation links, or when the work spans several parts of the package.
These files are documentation context. The installed package's exports, types, source, and tests remain the authority for what that version supports.
## A starting prompt
Copy this into your coding assistant and supply your app and community details:
```text
Help me add a Bitsocial feed to my existing React app using
@bitsocial/bitsocial-react-hooks.
Read the docs site's /llms.txt, /docs/quickstart.md, and
/docs/concepts.md first. Ask me for the docs site's base URL
if it is not already available in this conversation.
Inspect the app and installed package version before editing.
Use a real community identifier that I supply, or leave a clearly
labeled configuration value for it. Do not invent a live community.
Build a browser-rendered feed with useFeed, pagination, and useful
loading, empty, and error feedback. Keep loaded posts visible while
more data arrives. Reuse my app's components and styling.
No provider setup is required. Account initialization is automatic
and asynchronous. Prevent server evaluation of browser-only imports
if this app uses server rendering.
Verify the affected behavior and report what was checked. Keep the
scope to this feature; do not publish content or deploy the app.
```
For a new app, change the first sentence to describe what you want to make: a discussion forum, a reading app, a community browser, or another interface. Keep the first milestone small enough to run and inspect.
## Give it a useful next task
Once the feed works, ask for one complete interaction at a time:
- Open a post and load its replies using the [comment hooks](/docs/api/comments-hooks/).
- Add a community picker and pagination using the [feed hooks](/docs/api/feeds-hooks/).
- Add a post form with challenge handling, pending feedback, and cancellation using the [publishing recipe](/docs/recipes/create-a-post-or-comment-using-hooks/).
For publishing tasks, explicitly ask the assistant to distinguish “attempt started” from “accepted by the community.” Awaiting `publishComment()` is not publication confirmation.
## Keep examples aligned with the package
If an assistant suggests a provider, a `communityAddresses` feed option, or waiting for `useFeed().state === "succeeded"` before rendering any posts, have it check the current documentation and source.
The public package name is `@bitsocial/bitsocial-react-hooks`. Reading hooks use community identifier objects such as `{ name }` or `{ publicKey }`. Hook options and result fields can change between versions, so resolve disagreements against the version your app actually uses.
The docs and website source live with the [open-source library](https://github.com/bitsocialnet/bitsocial-react-hooks). Documentation improvements can travel with the code they describe.
---
# Accounts Hooks
Documentation: /docs/api/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
Documentation: /docs/api/comments-hooks/
```
useComment({commentCid: string, community?: CommunityIdentifier, initialComment?: Comment, onlyIfCached?: boolean, autoUpdate?: boolean}): Comment & {refresh: Function}
useCrosspost({crosspost: Crosspost, autoUpdate?: boolean}): Comment & {isCommunityVerified: boolean, 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
Documentation: /docs/api/communities-hooks/
```
useCommunity({community: {name?: string, publicKey?: string}, onlyIfCached?: boolean}): Community & {syncState: "initializing" | "loading" | "retrying" | "succeeded" | "failed" | "stopped", hasCachedData: boolean, lastFetchAttemptAt: number | undefined, lastSuccessfulFetchAt: number | undefined}
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.
`useCommunity` only exposes community error events that are not superseded by a later `update` event. Transient fetch errors are delayed briefly before surfacing through `error` or `errors`.
`useCommunity().state` remains `succeeded` when cached community data is available, even
while a refresh is running. Use `syncState` for the current refresh lifecycle:
`loading` covers active name, IPNS, and IPFS work, while `retrying` means a transient
failure is being retried. `lastFetchAttemptAt` and `lastSuccessfulFetchAt` are Unix
timestamps in seconds. A successful fetch only proves that a valid community record was
reachable; it does not prove that the community operator is currently online.
---
# Authors Hooks
Documentation: /docs/api/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}
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
Documentation: /docs/api/feeds-hooks/
```
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 without constructing a different sort name, so older posts can be appended without replacing the feed instance.
Feed and reply sort names are defined by each community record, not by a fixed hooks allowlist. Omit `sortType` to use the preloaded page, or discover the requestable names with `getAvailablePostSortTypes(community)` and `getAvailableReplySortTypes(comment)`. pkc-js preloads a single sort and only publishes `pageCids` once that page overflows, so when every post or reply fits in the preloaded page the standard sorts (`hot`, `new`, `active` and `top*` for posts; `best`, `new`, `old`, `newFlat` and `oldFlat` for replies) are also requestable and are sorted client-side from that page, including the time window of `top*` timeframe sorts. `getPostPageSortType(community, sortType)` and `getReplyPageSortType(comment, sortType)` return the page that serves a sort. A requested sort that is neither published nor computable client-side is not silently replaced with another sort. Hooks preserve protocol page order for unknown custom sorts because their scoring algorithm is not available to the client. The same applies to a sort file that a community publishes under a built-in name (for example a keyword no-bump `active`), which `community.pageSorts.posts[sortType]` reveals through a different `name` or options of its own: a single-community feed reading that sort's pages keeps the community's page order, while a feed merging several communities still orders them with the built-in sorter of that name.
---
# Actions Hooks
Documentation: /docs/api/actions-hooks/
```
useSubscribe({communityAddress: string}): {subscribed: boolean | undefined, subscribe: Function, unsubscribe: Function}
useBlock({address?: string, cid?: string}): {blocked: boolean | undefined, block: Function, unblock: Function}
useSaveComment({commentCid: string}): {saved: boolean | undefined, saveComment: Function, unsaveComment: Function}
usePublishComment(options: UsePublishCommentOptions): {index: number, abandonPublish: () => Promise, ...UsePublishCommentResult}
usePublishVote(options: UsePublishVoteOptions): {abandonPublish: () => Promise, ...UsePublishVoteResult}
usePublishCommentEdit(options: UsePublishCommentEditOptions): UsePublishCommentEditResult
usePublishCommentModeration(options: UsePublishCommentModerationOptions): UsePublishCommentModerationResult
usePublishCommunityEdit(options: UsePublishCommunityEditOptions): UsePublishCommunityEditResult
useCreateCommunity(options: CreateCommunityOptions): {createdCommunity: Community | undefined, createCommunity: Function}
useExportCommunity(options?: UseExportCommunityOptions): {
communityExports: {communityAddress: string, exportId: string}[],
exportCommunity: () => Promise,
state: string,
error: Error | undefined,
errors: Error[]
}
```
Before `publishComment`, `publishCommentEdit`, `publishVote`, `publishCommentModeration`, or `publishCommunityEdit` signs a publication, hooks load the community challenge configuration and apply compatible wordfilters. The current `@bitsocial/wordfilter-challenge` contract uses a JSON array of `{src, dst}` objects in `wordfilter/v1/rules` and an optional JSON array of dot-notation paths in `wordfilter/v1/fieldNames`, both published through `publicOptions`. Every path starts with the publication type and is resolved against the publication being signed, so a path for another type is simply skipped. Without configured paths the defaults apply: `comment.content`, `comment.title`, `comment.author.displayName`, `commentEdit.content`, `commentEdit.reason`, `commentEdit.author.displayName`, and `vote.author.displayName`. Moderator and owner text is opt-in through configured paths (`commentModeration.commentModeration.reason`, `communityEdit.communityEdit.title`, `communityEdit.communityEdit.description`); structural and signing fields are never rewritten by community configuration. Matching is literal and case-insensitive, not regular-expression or Unicode-evasion matching. The transformed values are the values signed, published, and stored as the pending account publication. If composed rule sets do not stabilize within eight passes, publication stops before signing.
---
# States Hooks
Documentation: /docs/api/states-hooks/
```
useClientsStates({comment?: Comment, community?: Community}): {states, peers}
useCommunitiesStates({communities?: CommunityIdentifier[]}): {states, peers}
```
---
# RPC Hooks
Documentation: /docs/api/rpc-hooks/
```
usePkcRpcSettings(): {pkcRpcSettings: {pkcOptions, challenges}, setPkcRpcSettings: Function}
```
Community edits preserve challenge `publicOptions` arrays, remote community records expose their published option values, and `usePkcRpcSettings` exposes each installed challenge's `optionInputs` metadata so clients can build challenge-specific configuration without hooks knowing each challenge schema.
---
# Actions with no hooks implementations yet
Documentation: /docs/api/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
```
---
# Utility functions
Documentation: /docs/api/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
createCrosspost(comment: Comment): Crosspost // build the immutable payload accepted by pkc-js
getAvailablePostSortTypes(community: Community): string[]
getAvailableReplySortTypes(comment: Comment): string[]
getPreloadedPostSortType(community: Community): string | undefined
getPreloadedReplySortType(comment: Comment): string | undefined
resolvePostSortType(community: Community, requestedSortType?: string): string | undefined
resolveReplySortType(comment: Comment, requestedSortType?: string): string | undefined
getPostPageSortType(community: Community, requestedSortType?: string): string | undefined // page sort serving the request, e.g. the preloaded page a single-page community re-sorts client-side
getReplyPageSortType(comment: Comment, requestedSortType?: string): string | undefined
getSortTimeframeSeconds(sortType?: string): number | undefined // time window of top*/controversial* timeframe sorts
isFlatSortType(sortType?: string): boolean
```
`createCrosspost` requires a fully loaded comment with `comment.cid` and
`comment.raw.comment`. Publish the returned payload through the existing action hook:
```jsx
const crosspost = createCrosspost(sourceComment);
const { publishComment } = usePublishComment({
communityAddress: targetCommunityAddress,
title: sourceComment.title,
crosspost,
});
await publishComment();
```
Render the author-signed embedded record immediately with `useCrosspost`. The referenced
community is only confirmed after its signed `CommentUpdate` has loaded, exposed as
`isCommunityVerified`:
```jsx
const original = useCrosspost({ crosspost: post.crosspost });
return ;
```
---
# Getting started
Documentation: /docs/recipes/getting-started/
```jsx
import { useComment, useAccount } from "@bitsocial/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
Documentation: /docs/recipes/get-the-active-account-if-none-exist-in-browser-database-a-default-account-is-generated/
```jsx
const account = useAccount();
```
---
# Create accounts and change active account
Documentation: /docs/recipes/create-accounts-and-change-active-account/
```jsx
import {
useAccount,
useAccounts,
createAccount,
setActiveAccount,
} from "@bitsocial/bitsocial-react-hooks";
const account = useAccount();
const { accounts } = useAccounts();
// on first render
console.log(accounts.length); // 1
console.log(account.name); // e.g. 'Account KoXpxTwfnjA5'
console.log(account.name === `Account ${account.author.shortAddress}`); // true
await createAccount(); // creates another uniquely named account
await createAccount();
const thirdAccountName = accounts[2].name;
await setActiveAccount(thirdAccountName);
// on render after updates
console.log(accounts.length); // 3
console.log(account.name); // thirdAccountName
// you are now publishing from the third account because it is active
const { publishComment } = usePublishComment(publishCommentOptions);
await publishComment();
```
---
# Get a post
Documentation: /docs/recipes/get-a-post/
```jsx
const post = useComment({ commentCid, community: { name: communityAddress, publicKey: communityPublicKey } });
// 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 });
// the initial load includes the first community update (including moderation)
// before freezing; a recent creation timestamp alone does not settle the snapshot
// resolves after one update cycle, also when no newer CommentUpdate exists, and right
// away when another instance (e.g. your own reply) already keeps the comment updating
await post.refresh();
// newly published comments still absorb canonical number, postNumber, and
// author shortAddress metadata as it arrives; later live updates stay frozen
// 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
Documentation: /docs/recipes/get-a-comment/
```jsx
const comment = useComment({ commentCid, community: { name: communityAddress, publicKey: communityPublicKey } });
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
Documentation: /docs/recipes/get-author-avatar/
```jsx
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
Documentation: /docs/recipes/get-author-profile-page/
```jsx
// NOTE: you must have a comment cid from the author to load his profile page
// e.g. the page url would be /#/u//c/
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";
}
useWindowScroll={true}
components={{ Footer: hasMore ? () => : 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
Documentation: /docs/recipes/get-a-community/
```jsx
const community = useCommunity({ community: { name: communityAddress, publicKey: communityPublicKey } });
const {
syncState,
hasCachedData,
lastFetchAttemptAt,
lastSuccessfulFetchAt,
} = community;
const communityStats = useCommunityStats({
community: { name: communityAddress, publicKey: communityPublicKey },
});
const { communities } = useCommunities({
communities: [
{ name: communityAddress, publicKey: communityPublicKey },
{ name: communityAddress2, publicKey: communityPublicKey2 },
{ name: communityAddress3, publicKey: communityPublicKey3 },
],
});
// fetched communities are refreshed immediately and then every 15 minutes
// so long-lived tabs keep following community IPNS updates
// 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
Documentation: /docs/recipes/create-a-post-or-comment-using-callbacks/
```jsx
const onChallenge = async (challenge, comment) => {
let challengeAnswers
try {
// ask the user to complete the challenges in a modal window
challengeAnswers = await getChallengeAnswersFromUser(challenge.challenges)
}
catch (e) {
// the modal rejects when the user declines
await abandonPublish()
return
}
if (challengeAnswers) {
await comment.publishChallengeAnswers(challengeAnswers)
}
}
const onChallengeVerification = (challengeVerification, comment) => {
// A failed verification with a reason or nonempty challengeErrors is terminal.
// Other failures retry automatically only if a challenge was received.
// Call abandonPublish() when the user cancels the challenge flow.
if (challengeVerification.challengeSuccess === true) {
console.log('challenge success', {publishedCid: challengeVerification.commentUpdate?.cid})
}
else if (challengeVerification.challengeSuccess === false) {
console.error('challenge failed', {reason: challengeVerification.reason, challengeErrors: challengeVerification.challengeErrors})
}
}
const onError = (error, comment) => console.error(error)
const onPendingComment = (accountCommentIndex, pendingComment) => {
// render or navigate to the pending comment immediately, before local persistence finishes
// this runs again with the corrected index if another local deletion shifts the comment while saving
history.push(`/profile/c/${accountCommentIndex}`, {pendingComment})
}
const publishCommentOptions = {
content: 'hello',
title: 'hello',
communityAddress: '12D3KooW...',
// partial author overrides keep the active account's author identity
author: {displayName: 'Alice'},
onPendingComment,
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)
// onPendingComment is called as soon as the provisional comment and index exist,
// and again with a corrected index if another local deletion shifts it while saving.
// Use it to immediately redirect the user to a page displaying the comment
// with a "pending" label. The index result is also defined after the pending
// comment has been stored locally.
if (index !== undefined) {
// 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
// See "Get replies to a post (nested or flat)" below for both layouts.
```
---
# Create a post or comment using hooks
Documentation: /docs/recipes/create-a-post-or-comment-using-hooks/
```jsx
const publishCommentOptions = {
content: "hello",
title: "hello",
communityAddress: "12D3KooW...",
};
const {
index,
state,
publishComment,
challenge,
challengeVerification,
publishChallengeAnswers,
abandonPublish,
error,
} = usePublishComment(publishCommentOptions);
if (challenge) {
// display challenge.challenges and call publishChallengeAnswers(challengeAnswers)
}
if (challengeVerification) {
// display challengeVerification.challengeSuccess to user
// on success, redirect once challengeVerification.commentUpdate?.cid is defined
}
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)
Documentation: /docs/recipes/create-a-post-or-comment-anonymously-without-accountsigner-or-accountauthor/
```jsx
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
Documentation: /docs/recipes/create-a-vote/
```jsx
const commentCid = "QmZVYzLChjKrYDVty6e5JokKffGDZivmEJz9318EYfp2ui";
const publishVoteOptions = {
commentCid,
vote: 1,
communityAddress: "news.eth",
onChallenge,
onChallengeVerification,
onError,
};
const { state, error, publishVote, abandonPublish } = usePublishVote(publishVoteOptions);
await publishVote();
console.log(state);
console.log(error);
// if the user closes the challenge modal and wants to cancel voting:
await abandonPublish();
// the vote publication is stopped and the account vote goes back to what it was
// before publishVote(), so the optimistic vote does not survive a cancelled challenge
// 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`);
```
`useComment`, `useComments`, `useReplies`, and `useFeed` apply the selected account's pending vote
to `upvoteCount` and `downvoteCount` immediately, so a score updates the same way whether the vote
was cast from a feed or from a comment page. The optimistic adjustment is replaced by the next
canonical comment update.
---
# Create a comment edit
Documentation: /docs/recipes/create-a-comment-edit/
```jsx
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
Documentation: /docs/recipes/create-a-comment-moderation/
```jsx
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
Documentation: /docs/recipes/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:
```jsx
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):
```jsx
import { deleteComment, useAccountComments } from "@bitsocial/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):**
```jsx
import { deleteComment, useAccountComments } from "@bitsocial/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
Documentation: /docs/recipes/subscribe-to-a-community/
```jsx
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
Documentation: /docs/recipes/get-feed/
```jsx
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'})
}
useWindowScroll={true}
components={{Footer: hasMore ? () => : 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)
```
---
# Get mod queue (pending approval)
Documentation: /docs/recipes/get-mod-queue-pending-approval/
```jsx
import {Virtuoso} from 'react-virtuoso'
const {feed, hasMore, loadMore} = useFeed({
communities: [{name: 'memes.eth'}, {publicKey: '12D3KooW...'}, {publicKey: '12D3KooW...'}],
modQueue: ['pendingApproval']
})
}
useWindowScroll={true}
components={{Footer: hasMore ? () => : 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
Documentation: /docs/recipes/approve-a-pending-approval-comment/
```jsx
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
Documentation: /docs/recipes/edit-an-account/
```jsx
import {useAccount, setAccount, useResolvedAuthorAddress} from '@bitsocial/bitsocial-react-hooks'
const account = useAccount() // or useAccount({ accountName: 'Account KoXpxTwfnjA5' }) to use an account by name
// `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.
// Defaults use multiple explicit Ethereum RPCs for `.eth` / `.bso` author-name resolution.
// Web defaults run a browser libp2p client and use multiple HTTP routers to discover peers.
// IPFS gateways and HTTP pubsub providers remain available as explicit account or app overrides.
// `account.nameResolversChainProviders` optionally overrides only the RPCs used for that resolution.
console.log(account.author.wallets.eth)
const ethResolverRpcUrls = [
'https://ethereum-rpc.publicnode.com',
'https://eth.drpc.org',
'https://ethereum.publicnode.com',
'https://rpc.mevblocker.io',
'https://1rpc.io/eth',
'https://eth-pokt.nodies.app',
]
const httpRoutersOptions = [
'https://peers.pleb.bot',
'https://routing.lol',
'https://peers.forumindex.com',
'https://peers.plebpubsub.xyz',
'https://routerofbitsocial.xyz',
'https://bsotracker.online',
]
const author: {...account.author, displayName: 'John'}
const editedAccount = {
...account,
author,
chainProviders: {
...account.chainProviders,
eth: { urls: [...ethResolverRpcUrls, 'ethers.js'], chainId: 1 },
},
pkcOptions: {
...account.pkcOptions,
httpRoutersOptions,
},
nameResolversChainProviders: {
eth: { urls: ethResolverRpcUrls, 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
Documentation: /docs/recipes/delete-account/
> Note: deleting account is unrecoverable, warn the user to export/backup his account before deleting
```jsx
import { deleteAccount } from "@bitsocial/bitsocial-react-hooks";
// delete active account
await deleteAccount();
// delete account by name
await deleteAccount("Account KoXpxTwfnjA5");
```
---
# Get your own comments and votes
Documentation: /docs/recipes/get-your-own-comments-and-votes/
```jsx
// 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.
// local account comments returned by useAccountComment and useAccountComments include the account author's address and shortAddress when an older cached account comment is missing author identity fields.
// comments published in a pseudonymous community (comment.pseudonymityMode is set) keep their community-assigned author instead, because the account identity was never published.
// 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 },
});
// pending local account comments are reconciled with their approved network version
// so the same post or reply is not shown twice after moderation approval
// published account replies stay visible and are deduplicated by cid while
// canonical replies propagate; explicitly purged replies are hidden after an
// exhausted canonical feed refreshes past their timestamp
```
---
# Determine if a comment is your own
Documentation: /docs/recipes/determine-if-a-comment-is-your-own/
```jsx
const account = useAccount();
const comment = useComment({ commentCid });
const isMyOwnComment = account?.author.address === comment?.author.address;
```
---
# Get account notifications
Documentation: /docs/recipes/get-account-notifications/
```jsx
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)
Documentation: /docs/recipes/block-an-address-author-community-or-multisub/
```jsx
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)
Documentation: /docs/recipes/block-a-cid-hide-a-comment/
```jsx
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();
```
---
# Save a comment
Documentation: /docs/recipes/save-a-comment/
Saved comments are kept in the account's `savedComments` list and are included when the account is exported.
```jsx
const { saved, saveComment, unsaveComment } = useSaveComment({ commentCid: "Qm..." });
if (saved) {
await unsaveComment();
} else {
await saveComment();
}
```
---
# (Desktop only) Create a community
Documentation: /docs/recipes/desktop-only-create-a-community/
```jsx
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) Export communities
Documentation: /docs/recipes/desktop-only-export-communities/
```jsx
// Export one community.
const { communityExports, exportCommunity, state, error } = useExportCommunity({
communityAddress: "your-community-address.eth",
});
await exportCommunity();
// Export several communities at the same time.
const exportMany = useExportCommunity({
communityAddresses: ["community-1.eth", "community-2.eth"],
});
await exportMany.exportCommunity();
// Export every community listed by the active account's pkc client.
const exportAll = useExportCommunity();
await exportAll.exportCommunity();
if (state === "succeeded") {
console.log("started exports", communityExports);
}
if (state === "failed") {
console.log("failed to start export", error.message);
}
```
---
# (Desktop only) List the communities you created
Documentation: /docs/recipes/desktop-only-list-the-communities-you-created/
```jsx
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
Documentation: /docs/recipes/desktop-only-edit-your-community-settings/
If the account's pkc client hosts the community (it is listed in `pkc.communities`), the edit is applied locally. Otherwise it is published as a `CommunityEdit` over pubsub, even when the account is the owner, and the hosting node accepts it from the owner or an admin (only the owner can change `address` or `roles`).
```jsx
const onChallenge = async (challenge, communityEdit) => {
let challengeAnswers
try {
challengeAnswers = await getChallengeAnswersFromUser(challenge.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
Documentation: /docs/recipes/export-and-import-account/
```jsx
import {
exportAccount,
importAccount,
setActiveAccount,
setAccountsOrder,
} from "@bitsocial/bitsocial-react-hooks";
// New accounts default to `Account {author short address}`, while custom and legacy names are preserved.
const activeAccount = useAccount();
const activeAccountName = activeAccount.name;
// export active account, tell user to copy or download this json
const activeAccountJson = await exportAccount();
// import account
await importAccount(activeAccountJson);
// Importing the same identity again uses the next available numeric suffix.
const importedAccountName = `${activeAccountName} 2`;
const importedAccount = useAccount({ accountName: importedAccountName });
// make imported account active account
await setActiveAccount(importedAccountName);
// reorder the accounts list
await setAccountsOrder([importedAccountName, activeAccountName]);
```
---
# View the status of a comment edit
Documentation: /docs/recipes/view-the-status-of-a-comment-edit/
```jsx
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
Documentation: /docs/recipes/view-the-status-of-a-specific-comment-edit-property/
```jsx
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
Documentation: /docs/recipes/list-all-comment-and-community-edits-the-account-has-performed/
```jsx
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)
Documentation: /docs/recipes/get-replies-to-a-post-nested-or-flat/
```jsx
import { useReplies, useComment, useAccountComment } from "@bitsocial/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 },
};
// accountComments keeps pending and published local replies visible while
// canonical replies propagate and deduplicates them by cid; explicitly purged
// replies are hidden after an exhausted feed refreshes past their timestamp
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 (
);
};
const comment = useComment({ commentCid });
const { replies, updatedReplies, hasMore, loadMore } = useReplies({
...useRepliesOptions,
comment,
});
const repliesComponents = replies.map((reply, index) => (
));
```
---
# Format short CIDs and addresses
Documentation: /docs/recipes/format-short-cids-and-addresses/
```jsx
import { useComment, useAuthorAddress } from "@bitsocial/bitsocial-react-hooks";
const comment = useComment({ commentCid });
const { shortAuthorAddress } = useAuthorAddress({ comment });
// CID labels are presentation logic in your app; keep the full CID for links.
const shortParentCid = comment.parentCid
? `${comment.parentCid.slice(0, 8)}…${comment.parentCid.slice(-6)}`
: undefined;
```
---
# useBufferedFeeds with concurrency
Documentation: /docs/recipes/usebufferedfeeds-with-concurrency/
Pass several feed configurations to buffer them in the background. There is no public concurrency-limit option; choose how many feeds your app preloads.
```jsx
import { useBufferedFeeds } from "@bitsocial/bitsocial-react-hooks";
// Replace these names with the communities your app uses.
const feedsOptions = [
{ communities: [{ name: "news.eth" }, { name: "crypto.eth" }], sortType: "new" },
{ communities: [{ name: "memes.eth" }], sortType: "topWeek" },
];
function FeedPreloader() {
useBufferedFeeds({ feedsOptions });
return null;
}
```
---
# algorithms
Documentation: /docs/advanced/algorithms/
### Algorithms
#### Account notifications and own comment updates
On startup, and every time a comment is created, it is added to the AccountsComments store and database. On the comment challengeverification event, the comment CID is received from the community owner, and we can start listening to comment update events, and update the store and database every time. Sometimes the user closes the page and the challengeverification event is never received, so every time a comment, community or community page is fetched, we awkwardly check to see if it has one of our own comment with a missing CID, and update it if found.
AccountsCommentsReplies are found on the comment update events and are stored in a last rencently used database and have the field "markedAsRead" once read. `useAccountNotifications` uses the AccountsCommentsReplies to compile the read/unread notifications. TODO: add notifications for upvotes e.g. "Your comment has 10 upvotes".
#### Feed pages and infinite scrolling
A "feed" is a combination of a list of communities to fetch, a sort type (hot/top/new/etc) and an account (for its IPFS settings). After using `useFeed(useFeedOptions)`, a feed with those options is added to the feedsStore. After a feed is added to store, its communities are fetched, then the first page of the community.posts `Pages` are fetched (if needed, usually the 'hot' sort is included with `pkc.getCommunity()`). Each feed has a `pageNumber` which gets incremented on `loadMore` (used by infinite scrolling). Each feed has a list of `CommunitiesPostsInfo` which keep track of `CommunityPostsInfo.bufferedPostCount` for each combination of community and sort type. When `CommunityPostsInfo.bufferedPostCount` gets below 50, the next page for the community and sort type is fetched.
When a new post page is received from IPFS, the `feedsStore.bufferedFeeds` are recalculated, but the `feedsStore.loadedFeeds` (which are displayed to the user) are not, new posts fetched will only be displayed to the user the next time he calls `loadMore`. If we detect that a `loadedFeed` is stale, we can prompt the user to load more posts, like Reddit/Facebook/Twitter do.
Post pages are cached in IndexedDb for a short time, in case the user reloads the app.
When a community updates, the buffered feeds are emptied of that community's posts, and the first page is immediately fetched to try to refill it. TODO: If an updated comment already in `loadedFeeds` is fetched by a new community page, it should replace the old comment with the new one with updated votes/replies.
#### Feeds stores
```
feedsStore {
feedsOptions: FeedsOptions
bufferedFeeds: Feeds
bufferedPostsCounts: {[communityAddress+sortType: string]: number}
loadedFeeds: Feeds
feedsHaveMore: {[feedName: string]: boolean}
// actions
addFeedToStore: (feedName: string, ...feedOptions: FeedOptions) => void
incrementFeedPageNumber: (feedName: string) => void
// recalculate all feeds using new communities.post.pages, communitiesPagesStore and page numbers
updateFeeds: () => void
}
communitiesStore {
communities: Communities
// actions
addCommunityToStore: (communityAddress: string) => void
}
communitiesPagesStore {
communitiesPages
// actions
// a community instance only knows its first page CID, so take the first page CID as an argument
// and scroll through every community next page in the store until you find the last page, then add it
addNextCommunityPageToStore: (communityFirstPageCid: string) => void
}
```
#### Flow of adding a new feed
1. user calls useFeed(communityAddresses, sortType) and feed gets added to feeds store
2. feed communities are added to communitiesStore
- in parallel: 3. each feed community+sortType subscribes to its community.posts.pages and firstPageCids (community.posts.pageCids[sortType]) value changing (a community update) 4. on each community.posts.pages and firstPageCids change, updateFeeds and bufferedFeedsCommunitiesPostCounts
- in parallel: 3. each feed community subscribes to its bufferedFeedsCommunitiesPostCounts value changing 4. on each bufferedFeedsCommunitiesPostCounts change, if the bufferedFeedsCommunitiesPostCounts is below threshold for the community, add the next community+sortType page to the communitiesPagesStore
- in parallel: 3. each feed subscribes to communitiesPagesStore changing
- on each communitiesPagesStore change, if any new pages are relevant to the feed: 5. the feed's buffered feeds is rebuilt and bufferedFeedsCommunitiesPostCounts updated 6. if the loaded feeds is missing posts and buffered feeds has them, rebuild the loaded feeds
- in parallel: 3. each feed subscribes to accountsStore changing 4. on each accounts change, like a blockedAddress added for example, updateFeeds
3. update feeds to rebuild the feeds using the already preloaded communities and pages if any
#### Flow of incrementing a feed's page
1. the feeds store gets updated with the new page number and loadedFeeds, bufferedFeeds and bufferedFeedsCommunitiesPostCounts are partially recalculated and updated
#### Replies stores
Similar to feeds store, but with a some differences:
- each reply depth needs its own feed, and all the nested feeds must be added at the same time with addFeedsToStore or all depths won't render simultaneously
- because nested feeds are added in bulk, not possible to set a custom feedStoreName, must use feedOptionsToFeedName
- feeds store take a community addresses argument and add the community to communities store, adding all nested replies to comments store and subscribing to updates would not scale, so instead useReplies takes a comment argument, which is passed to addFeedToStoreOrUpdateComment
- every time the comment changes, addFeedToStoreOrUpdateComment is called, which calls addFeedsToStore to add all new nested feeds simultaneously
- useReplies({ onlyIfCached: true }) still builds the replies feed from the passed comment, but it only uses preloaded replies and reply pages that are already cached in memory; it does not fetch missing reply pages from `nextCid`
- validating replies has a few 100ms delay, but this looks ugly in the ui, so show replies instantly with getRepliesFirstPageSkipValidation, and after validation remove invalid replies, can be turned off with validateOptimistically: false
- nested replies don't automatically stream new replies until repliesPerPage is reached, because that would displace the ui, loadMore must be called manually is feedOptions.streamPage is false
#### Accounts settings persistance, export, import and caching
All accounts settings, accounts comments and accounts votes are stored permanently in the various IndexedDb databases. Import from file and export to file are possible but not yet implemented. Ephemeral data like random communities, comments and feeds are stored in last recently used IndexedDb databases, and eventually erased.
#### Editing account.pkcOptions and replacing the account.pkc instance
Not implemented, but the easiest method would be to force a page reload, which will reset setting up all the comments and community listeners.
#### Author comments algorithm
1. Start with an author.address and a comment.cid, fetch the comment.cid and validate the comment.author.address
- in parallel: 2. Fetch the previous comment using comment.author.previousCommentCid and validate the comment.author.address 3. Continue this process until comment.author.previousCommentCid is undefined (no more comments)
- in parallel: 2. If one of the author comments receives an update with comment.author.community.lastCommentCid, fetch the lastCommentCid and validate the lastComment.author.address 3. If the lastComment is more recent than the original comment, replace the original comment with the lastComment and start fetching lastComment.author.previousCommentCid 4. Continue this process until comment.author.previousCommentCid is undefined (no more comments)
#### Flow of adding authorComments to authorsCommentsStore
1. user calls useAuthorComments(authorAddress, commentCid, filter) and authorAddress+filter gets added to author comments store
2. nextCommentCidToFetch gets set to commentCid
3. nextCommentCidToFetch gets added to commentsStore\*
4. on comments store update
- in parallel: 5. the fetched nextCommentCidToFetch comment gets added to bufferedCommentCids and filtered loadedComments
- in parallel: 5. nextCommentCidToFetch gets set to comment.author.previousCommentCid 6. go back to step 3
- in parallel: 5. if the updated comment has comment.author.community.lastCommentCid, add the lastCommentCid to commentsStore\* 6. go back to step 4
- in parallel: 5. if the updated comment was a lastCommentCid, and its comment.timestamp is newer than current lastCommentCid comment.timestamp, and newer than all bufferedCommentCids comment.timestamp, set lastCommentCid as comment.cid 6. comment gets added to bufferedCommentCids and filtered loadedComments 7. it is recommended to redirect the user to `/#/u//` so if they share the link they share the most recent commentCid
---
\*commentsStore: any commentCid added the the commentsStore will fetch the comment (and comment updates), and emit events on comments changes
#### Replies pages and infinite scrolling
A "replies feed" is a combination of a comment update to fetch, a sort type (best/new/old/etc) and an account (for its IPFS settings). After using `useReplies(useRepliesOptions)`, a replies feed with those options is added to the repliesStore. After a replies feed is added to store, its comment is fetched, then the first page of the comment.replies `Pages` are fetched (if needed, usually the 'best' sort is included with `pkc.getComment()`). Each replies feed has a `pageNumber` which gets incremented on `loadMore` (used by infinite scrolling). Each feed has a `CommentsRepliesInfo` which keep track of `CommentRepliesInfo.bufferedPostCount` for each combination of sort type. When `CommentRepliesInfo.bufferedPostCount` gets below 50, the next page for the comment and sort type is fetched.
When a new replies page is received from IPFS, the `repliesStore.bufferedFeeds` are recalculated, but the `repliesStore.loadedFeeds` (which are displayed to the user) are not, new replies fetched will only be displayed to the user the next time he calls `loadMore`. If we detect that a `loadedFeed` is stale, we can prompt the user to load more replies, like Reddit/Facebook/Twitter do.
Replies pages are cached in IndexedDb for a short time, in case the user reloads the app.
When a comment updates, the buffered feeds are emptied of that comment's replies, and the first page is immediately fetched to try to refill it. TODO: If an updated reply already in `loadedFeeds` is fetched by a new comment replies page, it should replace the old reply with the new one with updated votes/replies.
#### Replies stores
```
repliesStore {
feedsOptions: FeedsOptions
bufferedFeeds: Feeds
bufferedPostsCounts: {[communityAddress+sortType: string]: number}
loadedFeeds: Feeds
feedsHaveMore: {[feedName: string]: boolean}
// actions
addFeedToStore: (feedName: string, ...feedOptions: FeedOptions) => void
incrementFeedPageNumber: (feedName: string) => void
// recalculate all feeds using new communities.post.pages, communitiesPagesStore and page numbers
updateFeeds: () => void
}
commentsStore {
comments: Comments
// actions
addCommentToStore: (commentCid: string) => void
}
repliesPagesStore {
repliesPages
// actions
// a comment instance only knows its first page CID, so take the first page CID as an argument
// and scroll through every replies next page in the store until you find the last page, then add it
addNextRepliesPageToStore: (repliesFirstPageCid: string) => void
}
```
#### Flow of adding a new replies feed
1. user calls useReplies(commentCid, sortType) and replies feed gets added to replies store
2. comment is added to commentsStore
- in parallel: 3. each feed comment+sortType subscribes to its comment.replies.pages and firstPageCids (comment.replies.pageCids[sortType]) value changing (a comment update) 4. on each comment.replies.pages and firstPageCids change, updateFeeds and bufferedFeedsCommentsRepliesCounts
- in parallel: 3. each feed subscribes to its bufferedFeedsCommentsRepliesCounts value changing 4. on each bufferedFeedsCommentsRepliesCounts change, if the bufferedFeedsCommentsRepliesCounts is below threshold for the comment, add the next comment+sortType page to the repliesPagesStore
- in parallel: 3. each feed subscribes to repliesPagesStore changing
- on each repliesPagesStore change, if any new pages are relevant to the feed: 5. the feed's buffered feeds is rebuilt and bufferedFeedsCommentsRepliesCounts updated 6. if the loaded feeds is missing replies and buffered feeds has them, rebuild the loaded feeds
- in parallel: 3. each feed subscribes to accountsStore changing 4. on each accounts change, like a blockedAddress added for example, updateFeeds
3. update feeds to rebuild the feeds using the already preloaded comments and replies pages if any
#### Flow of incrementing a replies feed's page
1. the replies store gets updated with the new page number and loadedFeeds, bufferedFeeds and bufferedFeedsCommentsRepliesCounts are partially recalculated and updated
---
# clients
Documentation: /docs/advanced/clients/
#### useStateString example
```js
import { useMemo } from "react";
import { useClientsStates } from "@bitsocial/bitsocial-react-hooks";
const clientHosts = {};
const getClientHost = (clientUrl) => {
if (!clientHosts[clientUrl]) {
try {
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
} catch (e) {
clientHosts[clientUrl] = clientUrl;
}
}
return clientHosts[clientUrl];
};
const useStateString = (commentOrCommunity) => {
const { states } = useClientsStates({ comment: commentOrCommunity });
return useMemo(() => {
let stateString = "";
for (const state in states) {
const clientUrls = states[state];
const clientHosts = clientUrls.map((clientUrl) => getClientHost(clientUrl));
// if there are no valid hosts, skip this state
if (clientHosts.length === 0) {
continue;
}
// separate 2 different states using ' '
if (stateString) {
stateString += ", ";
}
// e.g. 'fetching IPFS from cloudflare-ipfs.com, ipfs.io'
const formattedState = state
.replaceAll("-", " ")
.replace("ipfs", "IPFS")
.replace("ipns", "IPNS");
stateString += `${formattedState} from ${clientHosts.join(", ")}`;
}
// fallback to comment or community state when possible
if (!stateString && commentOrCommunity?.state !== "succeeded") {
if (
commentOrCommunity?.publishingState &&
commentOrCommunity?.publishingState !== "stopped" &&
commentOrCommunity?.publishingState !== "succeeded"
) {
stateString = commentOrCommunity.publishingState;
} else if (
commentOrCommunity?.updatingState !== "stopped" &&
commentOrCommunity?.updatingState !== "succeeded"
) {
stateString = commentOrCommunity.updatingState;
}
if (stateString) {
stateString = stateString
.replaceAll("-", " ")
.replace("ipfs", "IPFS")
.replace("ipns", "IPNS");
}
}
// capitalize first letter
if (stateString) {
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
}
// if string is empty, return undefined instead
return stateString === "" ? undefined : stateString;
}, [states, commentOrCommunity]);
};
export default useStateString;
```
#### Get community with state string
```js
const community = useCommunity({ communityAddress });
const stateString = useStateString(community);
const errorString = useMemo(() => {
if (community?.state === "failed") {
let errorString = "Failed fetching community";
if (community.error) {
errorString += `: ${community.error.toString().slice(0, 300)}`;
}
return errorString;
}
}, [community?.state]);
if (stateString) {
console.log(stateString);
}
if (errorString) {
console.log(errorString);
}
```
#### Publish comment with state string
```js
const { index } = usePublishComment(publishCommentOptions);
const accountComment = useAccountComment({ commentIndex: index });
const { publishingState, error } = accountComment;
const stateString = useStateString(accountComment);
const errorString = useMemo(() => {
if (publishingState === "failed") {
let errorString = "Failed publishing comment";
if (error) {
errorString += `: ${error.toString().slice(0, 300)}`;
}
return errorString;
}
}, [publishingState]);
if (stateString) {
console.log(stateString);
}
if (errorString) {
console.log(errorString);
}
```
#### useFeedStateString example
```js
import { useMemo } from "react";
import { useCommunity, useCommunitiesStates } from "@bitsocial/bitsocial-react-hooks";
const clientHosts = {};
const getClientHost = (clientUrl) => {
if (!clientHosts[clientUrl]) {
try {
clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl;
} catch (e) {
clientHosts[clientUrl] = clientUrl;
}
}
return clientHosts[clientUrl];
};
const useFeedStateString = (communityAddresses) => {
// single community feed state string
const communityAddress = communityAddresses?.length === 1 ? communityAddresses[0] : undefined;
const community = useCommunity({ communityAddress });
const singleCommunityFeedStateString = useStateString(community);
// multiple community feed state string
const { states } = useCommunitiesStates({ communityAddresses });
const multipleCommunitiesFeedStateString = useMemo(() => {
if (communityAddress) {
return;
}
// e.g. Resolving 2 addresses from infura.io, fetching 2 IPNS, 1 IPFS from cloudflare-ipfs.com, ipfs.io
let stateString = "";
if (states["resolving-address"]) {
const { communityAddresses, clientUrls } = states["resolving-address"];
if (communityAddresses.length && clientUrls.length) {
stateString += `resolving ${communityAddresses.length} ${communityAddresses.length === 1 ? "address" : "addresses"} from ${clientUrls
.map(getClientHost)
.join(", ")}`;
}
}
// find all page client and sub addresses
const pagesStatesClientHosts = new Set();
const pagesStatesCommunityAddresses = new Set();
for (const state in states) {
if (state.match("page")) {
states[state].clientUrls.forEach((clientUrl) =>
pagesStatesClientHosts.add(getClientHost(clientUrl)),
);
states[state].communityAddresses.forEach((communityAddress) =>
pagesStatesCommunityAddresses.add(communityAddress),
);
}
}
if (states["fetching-ipns"] || states["fetching-ipfs"] || pagesStatesCommunityAddresses.size) {
// separate 2 different states using ', '
if (stateString) {
stateString += ", ";
}
// find all client urls
const clientHosts = new Set([...pagesStatesClientHosts]);
states["fetching-ipns"]?.clientUrls.forEach((clientUrl) =>
clientHosts.add(getClientHost(clientUrl)),
);
states["fetching-ipfs"]?.clientUrls.forEach((clientUrl) =>
clientHosts.add(getClientHost(clientUrl)),
);
if (clientHosts.size) {
stateString += "fetching ";
if (states["fetching-ipns"]) {
stateString += `${states["fetching-ipns"].communityAddresses.length} IPNS`;
}
if (states["fetching-ipfs"]) {
if (states["fetching-ipns"]) {
stateString += ", ";
}
stateString += `${states["fetching-ipfs"].communityAddresses.length} IPFS`;
}
if (pagesStatesCommunityAddresses.size) {
if (states["fetching-ipns"] || states["fetching-ipfs"]) {
stateString += ", ";
}
stateString += `${pagesStatesCommunityAddresses.size} ${pagesStatesCommunityAddresses.size === 1 ? "page" : "pages"}`;
}
stateString += ` from ${[...clientHosts].join(", ")}`;
}
}
// capitalize first letter
stateString = stateString.charAt(0).toUpperCase() + stateString.slice(1);
// if string is empty, return undefined instead
return stateString === "" ? undefined : stateString;
}, [states, communityAddress]);
if (singleCommunityFeedStateString) {
return singleCommunityFeedStateString;
}
return multipleCommunitiesFeedStateString;
};
export default useFeedStateString;
```
#### Get feed with single sub with state string
```js
const communityAddress = "memes.eth";
const { feed, hasMore, loadMore } = useFeed({
communityAddresses: [communityAddress],
sortType: "topAll",
});
const community = useCommunity({ communityAddress });
const stateString = useFeedStateString([communityAddress]);
const errorString = useMemo(() => {
if (community?.state === "failed") {
let errorString = "Failed fetching community";
if (community.error) {
errorString += `: ${community.error.toString().slice(0, 300)}`;
}
return errorString;
}
}, [community?.state]);
if (stateString) {
console.log(stateString);
}
if (errorString) {
console.log(errorString);
}
```
#### Get feed with multiple subs with state string
```js
const communityAddresses = ["memes.eth", "12D3KooW...", "12D3KooW..."];
const { feed, hasMore, loadMore } = useFeed({ communityAddresses, sortType: "topAll" });
const { communities } = useCommunities({ communityAddresses });
const stateString = useFeedStateString(communityAddresses);
const errorString = useMemo(() => {
// only show error string if all communities updating state are failed
for (const community of communities) {
if (community.updatingState !== "failed") {
return;
}
}
// only show the first error found because not possible to show all of them
for (const community of communities) {
if (community.error) {
return `Failed fetching community: ${community.error.toString().slice(0, 300)}`;
}
}
}, [communities]);
if (stateString) {
console.log(stateString);
}
if (errorString) {
console.log(errorString);
}
```
#### Get a comment clients states
```js
const comment = useComment({ commentCid });
// fetching from gateways
for (const ipfsGatewayUrl in comment.clients.ipfsGateways) {
const ipfsGateway = comment.clients.ipfsGateways[ipfsGatewayUrl];
if (ipfsGateway.state === "fetching-ipfs") {
console.log(`Fetching IPFS from ${ipfsGatewayUrl}`);
}
if (ipfsGateway.state === "fetching-ipns-update") {
console.log(`Fetching IPNS from ${ipfsGatewayUrl}`);
}
if (ipfsGateway.state === "succeeded") {
console.log(`Fetched comment from ${ipfsGatewayUrl}`);
}
}
// fetching from ipfs clients
for (const ipfsClientUrl in comment.clients.ipfsClients) {
const ipfsClient = comment.clients.ipfsClients[ipfsClientUrl];
if (ipfsClient.state === "fetching-ipfs") {
console.log(`Fetching IPFS from ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "fetching-ipns-update") {
console.log(`Fetching IPNS from ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "succeeded") {
console.log(`Fetched comment from ${ipfsClient.peers.length} peers`);
}
}
```
#### Get a community clients states
```js
const community = useCommunity({ communityAddress });
// resolving community address from chain providers
for (const chainProviderUrl in community.clients.chainProviders) {
const chainProvider = community.clients.chainProviders[chainProviderUrl];
if (chainProvider.state === "resolving-address") {
console.log(`Resolving community address from ${chainProviderUrl}`);
}
}
// fetching from gateways
for (const ipfsGatewayUrl in community.clients.ipfsGateways) {
const ipfsGateway = community.clients.ipfsGateways[ipfsGatewayUrl];
if (ipfsGateway.state === "fetching-ipns") {
console.log(`Fetching IPNS from ${ipfsGatewayUrl}`);
}
if (ipfsGateway.state === "fetching-page-ipfs") {
console.log(`Fetching page IPFS from ${ipfsGatewayUrl}`);
}
if (ipfsGateway.state === "succeeded") {
console.log(`Fetched community from ${ipfsGatewayUrl}`);
}
}
// fetching from ipfs clients
for (const ipfsClientUrl in community.clients.ipfsClients) {
const ipfsClient = community.clients.ipfsClients[ipfsClientUrl];
if (ipfsClient.state === "fetching-ipns") {
console.log(`Fetching IPNS from ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "fetching-ipfs") {
console.log(`Fetching IPFS from ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "fetching-page-ipfs") {
console.log(`Fetching page IPFS from ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "succeeded") {
console.log(`Fetched community from ${ipfsClient.peers.length} peers`);
}
}
```
#### Publish a comment with clients states
```js
const publishCommentOptions = {
content: "hello",
title: "hello",
communityAddress: "12D3KooW...",
onChallenge,
onChallengeVerification,
onError,
};
const { clients, publishComment } = usePublishComment(publishCommentOptions);
// start publishing
await publishComment();
// resolving community address from chain providers
for (const chainProviderUrl in clients.chainProviders) {
const chainProvider = clients.chainProviders[chainProviderUrl];
if (chainProvider.state === "resolving-address") {
console.log(`Resolving community address from ${chainProviderUrl}`);
}
}
// fetching from gateways
for (const ipfsGatewayUrl in clients.ipfsGateways) {
const ipfsGateway = clients.ipfsGateways[ipfsGatewayUrl];
if (ipfsGateway.state === "fetching-community-ipns") {
console.log(`Fetching community IPNS from ${ipfsGatewayUrl}`);
}
}
// publishing from pubsub client
for (const pubsubClientUrl in clients.pubsubClients) {
const pubsubClient = clients.pubsubClients[pubsubClientUrl];
if (pubsubClient.state === "publishing-challenge-request") {
console.log(`Publishing challenge request using ${pubsubClientUrl}`);
}
if (pubsubClient.state === "waiting-challenge") {
console.log(`Waiting for challenge from ${pubsubClientUrl}`);
}
if (pubsubClient.state === "publishing-challenge-answer") {
console.log(`Publishing challenge answer using ${pubsubClientUrl}`);
}
if (pubsubClient.state === "waiting-challenge-verification") {
console.log(`Waiting for challenge verification from ${pubsubClientUrl}`);
}
if (pubsubClient.state === "succeeded") {
console.log(`Published comment using ${pubsubClientUrl}`);
}
}
// fetching and publishing from ipfs client
for (const ipfsClientUrl in clients.ipfsClients) {
const ipfsClient = clients.ipfsClients[ipfsClientUrl];
if (ipfsClient.state === "fetching-community-ipns") {
console.log(`Fetching community IPNS from ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "fetching-community-ipfs") {
console.log(`Fetching community IPFS from ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "publishing-challenge-request") {
console.log(`Publishing challenge request to ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "waiting-challenge") {
console.log(`Waiting for challenge from ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "publishing-challenge-answer") {
console.log(`Publishing challenge answer to ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "waiting-challenge-verification") {
console.log(`Waiting for challenge verification from ${ipfsClient.peers.length} peers`);
}
if (ipfsClient.state === "succeeded") {
console.log(`Published comment to ${ipfsClient.peers.length} peers`);
}
}
```
#### Get IPFS and other clients stats
```js
const { ipfsGateways, ipfsClients, pubsubClients, chainProviders } = useClientsStats();
for (const ipfsGatewayUrl in ipfsGateways) {
const ipfsGateway = ipfsGateways[ipfsGatewayUrl];
console.log("IPFS gateway URL:", ipfsGatewayUrl);
console.log("Total downloaded:", ipfsGateway.totalIn);
console.log("Total uploaded:", ipfsGateway.totalOut);
console.log("Session downloaded:", ipfsGateway.sessionTotalIn);
console.log("Session uploaded:", ipfsGateway.sessionTotalOut);
console.log("Succeeded IPFS:", ipfsGateway.succeededIpfsCount);
console.log("Failed IPFS:", ipfsGateway.failedIpfsCount);
console.log("Succeeded IPFS averate time:", ipfsGateway.succeededIpfsAverageTime);
console.log("Succeeded IPFS median time:", ipfsGateway.succeededIpfsMedianTime);
console.log("Session succeeded IPFS:", ipfsGateway.sessionSucceededIpfsCount);
console.log("Session failed IPFS:", ipfsGateway.sessionFailedIpfsCount);
console.log("Session succeeded IPFS averate time:", ipfsGateway.sessionSucceededIpfsAverageTime);
console.log("Session succeeded IPFS median time:", ipfsGateway.sessionSucceededIpfsMedianTime);
console.log("Succeeded IPNS:", ipfsGateway.succeededIpnsCount);
console.log("Failed IPNS:", ipfsGateway.failedIpnsCount);
console.log("Succeeded IPNS averate time:", ipfsGateway.succeededIpnsAverageTime);
console.log("Succeeded IPNS median time:", ipfsGateway.succeededIpnsMedianTime);
console.log("Session succeeded IPNS:", ipfsGateway.sessionSucceededIpnsCount);
console.log("Session failed IPNS:", ipfsGateway.sessionFailedIpnsCount);
console.log("Session succeeded IPNS averate time:", ipfsGateway.sessionSucceededIpnsAverageTime);
console.log("Session succeeded IPNS median time:", ipfsGateway.sessionSucceededIpnsMedianTime);
for (const communityAddress in ipfsGateway.communities) {
const community = ipfsGateway.communities[communityAddress];
console.log("Community:", communityAddress);
console.log("Succeeded community updates:", community.succeededCommunityUpdateCount);
console.log("Failed community updates:", community.failedCommunityUpdateCount);
console.log(
"Succeeded community updates average time:",
community.succeededCommunityUpdateAverageTime,
);
console.log(
"Succeeded community updates median time:",
community.succeededCommunityUpdateMedianTime,
);
console.log(
"Session succeeded community updates:",
community.sessionSucceededCommunityUpdateCount,
);
console.log("Session failed community updates:", community.sessionFailedCommunityUpdateCount);
console.log(
"Session succeeded community updates average time:",
community.sessionSucceededCommunityUpdateAverageTime,
);
console.log(
"Session succeeded community updates median time:",
community.sessionSucceededCommunityUpdateMedianTime,
);
console.log("Succeeded community pages:", community.succeededCommunityPageCount);
console.log("Failed community pages:", community.failedCommunityPageCount);
console.log(
"Succeeded community pages average time:",
community.succeededCommunityPageAverageTime,
);
console.log(
"Succeeded community pages median time:",
community.succeededCommunityPageMedianTime,
);
console.log("Session succeeded community pages:", community.sessionSucceededCommunityPageCount);
console.log("Session failed community pages:", community.sessionFailedCommunityPageCount);
console.log(
"Session succeeded community pages average time:",
community.sessionSucceededCommunityPageAverageTime,
);
console.log(
"Session succeeded community pages median time:",
community.sessionSucceededCommunityPageMedianTime,
);
console.log("Succeeded comments:", community.succeededCommentCount);
console.log("Failed comments:", community.failedCommentCount);
console.log("Succeeded comments average time:", community.succeededCommentAverageTime);
console.log("Succeeded comments median time:", community.succeededCommentMedianTime);
console.log("Session succeeded comments:", community.sessionSucceededCommentCount);
console.log("Session failed comments:", community.sessionFailedCommentCount);
console.log(
"Session succeeded comments average time:",
community.sessionSucceededCommentAverageTime,
);
console.log(
"Session succeeded comments median time:",
community.sessionSucceededCommentMedianTime,
);
console.log("Succeeded comment updates:", community.succeededCommentUpdateCount);
console.log("Failed comment updates:", community.failedCommentUpdateCount);
console.log(
"Succeeded comment updates average time:",
community.succeededCommentUpdateAverageTime,
);
console.log(
"Succeeded comment updates median time:",
community.succeededCommentUpdateMedianTime,
);
console.log("Session succeeded comment updates:", community.sessionSucceededCommentUpdateCount);
console.log("Session failed comment updates:", community.sessionFailedCommentUpdateCount);
console.log(
"Session succeeded comment updates average time:",
community.sessionSucceededCommentUpdateAverageTime,
);
console.log(
"Session succeeded comment updates median time:",
community.sessionSucceededCommentUpdateMedianTime,
);
}
}
for (const ipfsClientUrl in ipfsClients) {
const ipfsClient = ipfsClients[ipfsClientUrl];
console.log("IPFS Client URL:", ipfsClientUrl);
console.log("Connected peers:", ipfsClient.peers.length); // IPFS peers https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-swarm-peers
console.log("Total downloaded:", ipfsClient.totalIn); // IPFS stats https://docs.ipfs.tech/reference/kubo/rpc/#api-v0-stats-bw
console.log("Total uploaded:", ipfsClient.totalOut);
console.log("Download rate:", ipfsClient.rateIn);
console.log("Upload rate:", ipfsClient.rateOut);
console.log("Session downloaded:", ipfsClient.sessionTotalIn);
console.log("Session uploaded:", ipfsClient.sessionTotalOut);
console.log("Session download rate:", ipfsClient.sessionRateIn);
console.log("Session upload rate:", ipfsClient.sessionRateOut);
console.log("Succeeded IPFS:", ipfsClient.succeededIpfsCount);
console.log("Failed IPFS:", ipfsClient.failedIpfsCount);
console.log("Succeeded IPFS averate time:", ipfsClient.succeededIpfsAverageTime);
console.log("Succeeded IPFS median time:", ipfsClient.succeededIpfsMedianTime);
console.log("Session succeeded IPFS:", ipfsClient.sessionSucceededIpfsCount);
console.log("Session failed IPFS:", ipfsClient.sessionFailedIpfsCount);
console.log("Session succeeded IPFS averate time:", ipfsClient.sessionSucceededIpfsAverageTime);
console.log("Session succeeded IPFS median time:", ipfsClient.sessionSucceededIpfsMedianTime);
console.log("Succeeded IPNS:", ipfsClient.succeededIpnsCount);
console.log("Failed IPNS:", ipfsClient.failedIpnsCount);
console.log("Succeeded IPNS averate time:", ipfsClient.succeededIpnsAverageTime);
console.log("Succeeded IPNS median time:", ipfsClient.succeededIpnsMedianTime);
console.log("Session succeeded IPNS:", ipfsClient.sessionSucceededIpnsCount);
console.log("Session failed IPNS:", ipfsClient.sessionFailedIpnsCount);
console.log("Session succeeded IPNS averate time:", ipfsClient.sessionSucceededIpnsAverageTime);
console.log("Session succeeded IPNS median time:", ipfsClient.sessionSucceededIpnsMedianTime);
for (const communityAddress in ipfsClient.communities) {
const community = ipfsClient.communities[communityAddress];
console.log("Community:", communityAddress);
console.log("Succeeded community updates:", community.succeededCommunityUpdateCount);
console.log("Failed community updates:", community.failedCommunityUpdateCount);
console.log(
"Succeeded community updates average time:",
community.succeededCommunityUpdateAverageTime,
);
console.log(
"Succeeded community updates median time:",
community.succeededCommunityUpdateMedianTime,
);
console.log(
"Session succeeded community updates:",
community.sessionSucceededCommunityUpdateCount,
);
console.log("Session failed community updates:", community.sessionFailedCommunityUpdateCount);
console.log(
"Session succeeded community updates average time:",
community.sessionSucceededCommunityUpdateAverageTime,
);
console.log(
"Session succeeded community updates median time:",
community.sessionSucceededCommunityUpdateMedianTime,
);
console.log("Succeeded community pages:", community.succeededCommunityPageCount);
console.log("Failed community pages:", community.failedCommunityPageCount);
console.log(
"Succeeded community pages average time:",
community.succeededCommunityPageAverageTime,
);
console.log(
"Succeeded community pages median time:",
community.succeededCommunityPageMedianTime,
);
console.log("Session succeeded community pages:", community.sessionSucceededCommunityPageCount);
console.log("Session failed community pages:", community.sessionFailedCommunityPageCount);
console.log(
"Session succeeded community pages average time:",
community.sessionSucceededCommunityPageAverageTime,
);
console.log(
"Session succeeded community pages median time:",
community.sessionSucceededCommunityPageMedianTime,
);
console.log("Succeeded comments:", community.succeededCommentCount);
console.log("Failed comments:", community.failedCommentCount);
console.log("Succeeded comments average time:", community.succeededCommentAverageTime);
console.log("Succeeded comments median time:", community.succeededCommentMedianTime);
console.log("Session succeeded comments:", community.sessionSucceededCommentCount);
console.log("Session failed comments:", community.sessionFailedCommentCount);
console.log(
"Session succeeded comments average time:",
community.sessionSucceededCommentAverageTime,
);
console.log(
"Session succeeded comments median time:",
community.sessionSucceededCommentMedianTime,
);
console.log("Succeeded comment updates:", community.succeededCommentUpdateCount);
console.log("Failed comment updates:", community.failedCommentUpdateCount);
console.log(
"Succeeded comment updates average time:",
community.succeededCommentUpdateAverageTime,
);
console.log(
"Succeeded comment updates median time:",
community.succeededCommentUpdateMedianTime,
);
console.log("Session succeeded comment updates:", community.sessionSucceededCommentUpdateCount);
console.log("Session failed comment updates:", community.sessionFailedCommentUpdateCount);
console.log(
"Session succeeded comment updates average time:",
community.sessionSucceededCommentUpdateAverageTime,
);
console.log(
"Session succeeded comment updates median time:",
community.sessionSucceededCommentUpdateMedianTime,
);
}
}
for (const pubsubClientUrl in pubsubClients) {
const pubsubClient = pubsubClients[pubsubClientUrl];
console.log("Pubsub Client URL:", pubsubClientUrl);
console.log("Connected peers:", pubsubClient.peers.length);
console.log("Total downloaded:", pubsubClient.totalIn);
console.log("Total uploaded:", pubsubClient.totalOut);
console.log("Download rate:", pubsubClient.rateIn);
console.log("Upload rate:", pubsubClient.rateOut);
console.log("Session downloaded:", pubsubClient.sessionTotalIn);
console.log("Session uploaded:", pubsubClient.sessionTotalOut);
console.log("Session download rate:", pubsubClient.sessionRateIn);
console.log("Session upload rate:", pubsubClient.sessionRateOut);
console.log(
"Succeeded challenge request messages:",
pubsubClient.succeededChallengeRequestMessageCount,
);
console.log(
"Failed challenge request messages:",
pubsubClient.failedChallengeRequestMessageCount,
);
console.log(
"Succeeded challenge request messages average time:",
pubsubClient.succeededChallengeRequestMessageAverageTime,
);
console.log(
"Succeeded challenge request messages median time:",
pubsubClient.succeededChallengeRequestMessageMedianTime,
);
console.log(
"Succeeded challenge answer messages:",
pubsubClient.succeededChallengeAnswerMessageCount,
);
console.log("Failed challenge answer messages:", pubsubClient.failedChallengeAnswerMessageCount);
console.log(
"Succeeded challenge answer messages average time:",
pubsubClient.succeededChallengeAnswerMessageAverageTime,
);
console.log(
"Succeeded challenge answer messages median time:",
pubsubClient.succeededChallengeAnswerMessageMedianTime,
);
console.log(
"Session succeeded challenge request messages:",
pubsubClient.sessionSucceededChallengeRequestMessageCount,
);
console.log(
"Session failed challenge request messages:",
pubsubClient.sessionFailedChallengeRequestMessageCount,
);
console.log(
"Session succeeded challenge request messages average time:",
pubsubClient.sessionSucceededChallengeRequestMessageAverageTime,
);
console.log(
"Session succeeded challenge request messages median time:",
pubsubClient.sessionSucceededChallengeRequestMessageMedianTime,
);
console.log(
"Session succeeded challenge answer messages:",
pubsubClient.sessionSucceededChallengeAnswerMessageCount,
);
console.log(
"Session failed challenge answer messages:",
pubsubClient.sessionFailedChallengeAnswerMessageCount,
);
console.log(
"Session succeeded challenge answer messages average time:",
pubsubClient.sessionSucceededChallengeAnswerMessageAverageTime,
);
console.log(
"Session succeeded challenge answer messages median time:",
pubsubClient.sessionSucceededChallengeAnswerMessageMedianTime,
);
for (const communityAddress in pubsubClient.communities) {
const community = pubsubClient.communities[communityAddress];
console.log("Community:", communityAddress);
console.log(
"Succeeded challenge request messages:",
community.succeededChallengeRequestMessageCount,
);
console.log("Failed challenge request messages:", community.failedChallengeRequestMessageCount);
console.log(
"Succeeded challenge request messages average time:",
community.succeededChallengeRequestMessageAverageTime,
);
console.log(
"Succeeded challenge request messages median time:",
community.succeededChallengeRequestMessageMedianTime,
);
console.log(
"Succeeded challenge answer messages:",
community.succeededChallengeAnswerMessageCount,
);
console.log("Failed challenge answer messages:", community.failedChallengeAnswerMessageCount);
console.log(
"Succeeded challenge answer messages average time:",
community.succeededChallengeAnswerMessageAverageTime,
);
console.log(
"Succeeded challenge answer messages median time:",
community.succeededChallengeAnswerMessageMedianTime,
);
console.log(
"Session succeeded challenge request messages:",
community.sessionSucceededChallengeRequestMessageCount,
);
console.log(
"Session failed challenge request messages:",
community.sessionFailedChallengeRequestMessageCount,
);
console.log(
"Session succeeded challenge request messages average time:",
community.sessionSucceededChallengeRequestMessageAverageTime,
);
console.log(
"Session succeeded challenge request messages median time:",
community.sessionSucceededChallengeRequestMessageMedianTime,
);
console.log(
"Session succeeded challenge answer messages:",
community.sessionSucceededChallengeAnswerMessageCount,
);
console.log(
"Session failed challenge answer messages:",
community.sessionFailedChallengeAnswerMessageCount,
);
console.log(
"Session succeeded challenge answer messages average time:",
community.sessionSucceededChallengeAnswerMessageAverageTime,
);
console.log(
"Session succeeded challenge answer messages median time:",
community.sessionSucceededChallengeAnswerMessageMedianTime,
);
}
}
for (const chainProviderUrl in chainProviders) {
const chainProvider = chainProviders[chainProviderUrl];
console.log("Chain provider URL:", chainProviderUrl);
}
```
---
# Mock content
Documentation: /docs/advanced/mock-content/
Use synthetic communities and posts while developing your interface.
#### Install the node module
```sh
yarn add @bitsocial/bitsocial-react-hooks
```
If you are developing in this repo, run `corepack enable` once first so `yarn` uses the pinned Yarn 4 release.
#### Add env variable for mocking
The mock client reads these `process.env` values at import time. In setups that expose `REACT_APP_*` variables to the browser bundle, add them to your React app's `.env` file:
```sh
REACT_APP_BITSOCIAL_REACT_HOOKS_MOCK_CONTENT=1
REACT_APP_BITSOCIAL_REACT_HOOKS_MOCK_CONTENT_LOADING_TIME=1000
```
Your bundler must substitute these exact `process.env` keys. Adding a `.env` file alone is not sufficient in every React setup. Restart the development server after changing them.
#### Get a mock feed
```js
import { useFeed } from "@bitsocial/bitsocial-react-hooks";
function App() {
const { feed, hasMore, loadMore } = useFeed({
communities: [{ name: "news.eth" }],
sortType: "new",
});
console.log({ feed });
return ;
}
```
#### Disable caching (warning: extremely slow loading)
- Add to your .env file:
```sh
REACT_APP_BITSOCIAL_REACT_HOOKS_NO_CACHE=1
```
#### Delete databases and caches
```js
import { deleteCaches, deleteDatabases } from "@bitsocial/bitsocial-react-hooks";
// delete all databases, including all caches and accounts data
await deleteDatabases();
// delete the cached comments, cached communities and cached pages only, no accounts data
await deleteCaches();
```
---
# schema
Documentation: /docs/advanced/schema/
### Schema
> For full schema see https://github.com/pkcprotocol/pkc-js#schema
```
Account {
id: string // random immutable string
name: string // local account name; defaults to "Account {author short address}"
author: Author,
signer: Signer,
pkc: PKC,
pkcOptions: PKCOptions,
// subscriptions to show in feed
subscriptions: string[], // community subscriptions
multisubSubscriptions: string[],
authorSubscriptions: string[],
// notifications turned on for addresses/cids
notifyingCommunities: {[address: string]: boolean}
notifyingMultisubs: {[address: string]: boolean}
notifyingAuthors: {[address: string]: boolean}
notifyingComments: {[commentCid: string]: boolean}
blockedAddresses: {[address: string]: boolean}, // hide address from feed and notifications
blockedCids: {[cid: string]: boolean}, // hide a specific comment cid from feed and notifications
limitedAddresses: {[address: string]: number}, // limit how many times per feed page an address can appear, e.g. 1 = 100%, 0.1 = 10%, 0.001 = 0.1%
savedComments: string[], // saved comment cids, newest saved first
karma: Karma
unreadNotificationCount: number
communities: {[communityAddress: string]: AccountCommunity} // the communities moderated or created by the user
}
Karma {
replyUpvoteCount
replyDownvoteCount
replyScore
postUpvoteCount
postDownvoteCount
postScore
upvoteCount
downvoteCount
score
}
AccountCommunity { // the communities moderated or created by the user
role: Role
}
AccountComment extends Comment {
index: number // the index of the comment in the AccountComments array and database
accountId: string
upvoteCountMarkedAsRead: number // upvote count the last time the user read it, needed for upvote notifications
}
AccountCommentReply extends Comment {
markedAsRead: boolean // has the user read this reply, needed for reply notifications
}
UseAccountsCommentsOptions {
accountName?: string
filter: UseAccountCommentsFilter
}
UseAccountCommentsFilter { // only get your own account's comments/votes on a certain community, thread, etc useful for certain UI pages
communityAddresses?: string[]
postCids?: string[]
commentCids?: string[]
parentCommentCids?: string[]
hasParentCommentCid?: boolean // get only posts, no comments
}
AccountVote extends Vote {
previousAccountVoteCid: string // needed to scroll to every vote an account has published
}
Author {
displayName: string
address: string
}
Signer {
privateKey?: string | buffer
type: 'rsa'
}
Challenge {
type: 'image' | 'text' | 'audio' | 'video' | 'html' // tells the client how to display the challenge, start with implementing image and text only first
challenge: string // data required to complete the challenge, could be html, png, etc.
}
```
### IndexedDb Schema
> Each top level object is its own key-value store database.
```
Accounts {
[accountId: string]: Account
}
AccountsMetadata {
accountIds: strings[] // this array sets the order of the accounts
activeAccountId: string // the default account to use with all hooks and actions
accountNamesToAccountIds: {[accountName: string]: accountId}
}
AccountsComments (each database named accountComments-[accountId]) {
[commentIndex: number]: AccountComment // store in array because cid is still unknown
}
AccountsCommentsReplies (each database named accountCommentsReplies-[accountId]) {
[commentCid: string]: AccountCommentReply // keep replies to own account comments in a separate last recently used database because they should be cached for a different amount of time than regular comments and account comments
}
AccountsVotes (each database named accountVotes-[accountId]) {
[commentCid: string]: AccountVote
}
Communities {
[communityAddress: string]: Community // last recently used database, delete oldest data
}
Comments {
[commentCid: string]: Comment // last recently used database, delete oldest data, different from AccountsComments that never expire
}
CommunityPages {
[pageCid: string]: CommunityPage // last recently used database, delete oldest data
}
```
### Stores
```
accountsStore (store in indexeddb permanently) {
accounts: {[accountName: string]: Account}
accountNames: string[]
activeAccountName: string
accountNamesToAccountIds: {[accountName: string]: accountId}
accountsComments: {[accountName: string]: AccountComment[]} // cid of comment unknown at time of posting, so store it in array
accountsVotes: {[accountName: string]: {[commentCid: string]: AccountVote}}
accountsCommentsReplies: {[accountName: string]: {[replyCid: string]: AccountCommentReply}}
accountsActions: AccountsActions
// internal
addCidToAccountComment(comment: Comment)
// internal
markAccountNotificationsAsRead(account: Account)
}
commentsStore (store in indexeddb last recently used) {
comments: {[commentCid: string]: Comment}
// internal
addCommentToStore(commentCid)
}
communitiesStore (store in indexeddb last recently used) {
communities: {[communityAddress: string]: Community}
// internal
addCommunityToStore(communityAddress)
}
feedsStore (no persistant storage, can be rebuilt from Communities and CommunityPages databases) {
bufferedFeeds: {[feedName: string]: Comment[]}
loadedFeeds: {[feedName: string]: Comment[]}
feedsHaveMore: {[feedName: string]: boolean}
// internal
addFeedToStore(feedName, communityAddresses, sortType, account)
}
```
---