# 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 (
{score} {reply.author.address} {reply.timestamp} {moreReplies}
{state === "pending" &&
PENDING ({publishingStateString})
} {state === "failed" &&
FAILED
}
{reply.content}
{replies.map((reply, index) => ( ))}
); }; 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) } ``` ---