diff --git a/src/hooks/__tests__/use-state-string.test.tsx b/src/hooks/__tests__/use-state-string.test.tsx index 4086fad5..d13751b5 100644 --- a/src/hooks/__tests__/use-state-string.test.tsx +++ b/src/hooks/__tests__/use-state-string.test.tsx @@ -62,6 +62,7 @@ const FeedStateStringHarness = ({ addresses }: { addresses?: string[] }) => { describe('use-state-string', () => { beforeEach(() => { latestValue = undefined; + localStorage.removeItem('5chan:pure-p2p-browser-enabled'); testState.clientsStates = {}; testState.community = undefined; testState.communitiesStates = {}; @@ -88,6 +89,29 @@ describe('use-state-string', () => { expect(latestValue).toBe('Resolving address, downloading board via IPFS'); }); + it('formats browser libp2p client state strings as peer downloads', () => { + testState.clientsStates = { + 'fetching-ipns': ['libp2pjs'], + 'resolving-address': ['https://ens.example.com'], + }; + + act(() => { + root.render(createElement(StateStringHarness, { value: { state: 'updating' } })); + }); + + expect(latestValue).toBe('Resolving address, downloading board from peers'); + }); + + it('formats browser p2p fallback publishing states as peer downloads', () => { + localStorage.setItem('5chan:pure-p2p-browser-enabled', 'true'); + + act(() => { + root.render(createElement(StateStringHarness, { value: { publishingState: 'fetching-ipfs', state: 'publishing' } })); + }); + + expect(latestValue).toBe('Downloading thread from peers'); + }); + it('falls back to publishing and updating states when no client states are available', () => { act(() => { root.render(createElement(StateStringHarness, { value: { publishingState: 'fetching-ipfs', state: 'publishing' } })); @@ -100,6 +124,20 @@ describe('use-state-string', () => { expect(latestValue).toBe('Downloading board via IPFS'); }); + it('formats browser p2p single-board feed fallback states as peer downloads', () => { + localStorage.setItem('5chan:pure-p2p-browser-enabled', 'true'); + testState.community = { + state: 'updating', + updatingState: 'fetching-ipfs', + }; + + act(() => { + root.render(createElement(FeedStateStringHarness, { addresses: ['music-posting.eth'] })); + }); + + expect(latestValue).toBe('Downloading board from peers'); + }); + it('sanitizes single-board feed state strings to board wording', () => { testState.community = { state: 'updating', @@ -140,6 +178,29 @@ describe('use-state-string', () => { expect(latestValue).toBe('Resolving 2 board addresses, downloading 2 boards (music-posting.eth, tech-posting.eth), 1 thread, 1 page via IPFS'); }); + it('aggregates browser libp2p feed states as peer downloads', () => { + testState.communitiesStates = { + 'fetching-ipfs': { + clientUrls: ['libp2pjs'], + communityAddresses: ['music-posting.eth'], + }, + 'fetching-ipns': { + clientUrls: ['libp2pjs'], + communityAddresses: ['music-posting.eth', 'tech-posting.eth'], + }, + 'page-1': { + clientUrls: ['libp2pjs'], + communityAddresses: ['music-posting.eth'], + }, + }; + + act(() => { + root.render(createElement(FeedStateStringHarness, { addresses: ['music-posting.eth', 'tech-posting.eth'] })); + }); + + expect(latestValue).toBe('Downloading 2 boards (music-posting.eth, tech-posting.eth), 1 thread, 1 page from peers'); + }); + it('shows an immediate board-specific loading string before detailed multi-board states arrive', () => { act(() => { root.render(createElement(FeedStateStringHarness, { addresses: ['music-posting.eth', 'tech-posting.eth'] })); diff --git a/src/hooks/use-state-string.ts b/src/hooks/use-state-string.ts index 9bf93090..c4e1a243 100644 --- a/src/hooks/use-state-string.ts +++ b/src/hooks/use-state-string.ts @@ -2,6 +2,7 @@ import { useMemo } from 'react'; import { useClientsStates, useCommunity, useCommunitiesStates } from '@bitsocial/bitsocial-react-hooks'; import debounce from 'lodash/debounce'; import getShortAddress from '../lib/get-short-address'; +import { isBrowserPureP2PEnabled } from '../lib/p2p-runtime'; import { useCommunityIdentifiers } from './use-community-identifiers'; interface CommentOrCommunity { @@ -22,6 +23,11 @@ type CommunityLoadingState = { const isCommunityLoadingState = (state: string[] | CommunityLoadingState | undefined): state is CommunityLoadingState => Boolean(state && !Array.isArray(state) && 'communityAddresses' in state && 'clientUrls' in state); +const isBrowserLibp2pClient = (clientUrl: string) => clientUrl === 'libp2pjs'; + +const getDownloadSourceSuffix = (clientUrls: string[], isBrowserPureP2P: boolean) => + (clientUrls.length > 0 ? clientUrls.every(isBrowserLibp2pClient) : isBrowserPureP2P) ? ' from peers' : ' via IPFS'; + const friendlyStateNames: Record = { 'fetching-ipns': 'downloading board', 'fetching-ipfs': 'downloading thread', @@ -49,6 +55,7 @@ const sanitizeSingleFeedLoadingState = (stateString?: string): string | undefine const useStateString = (commentOrCommunity: CommentOrCommunity | undefined): string | undefined => { const { states: rawStates } = useClientsStates({ comment: commentOrCommunity }) as { states: States }; + const isBrowserPureP2P = isBrowserPureP2PEnabled(); const debouncedStates = useMemo(() => { const debouncedValue = debounce((value: States) => value, 300); @@ -59,6 +66,7 @@ const useStateString = (commentOrCommunity: CommentOrCommunity | undefined): str let stateString: string | undefined = ''; const resolvingParts: string[] = []; const downloadingParts: string[] = []; + const downloadingClientUrls: string[] = []; for (const state in debouncedStates) { if (debouncedStates[state].length === 0) continue; @@ -67,6 +75,7 @@ const useStateString = (commentOrCommunity: CommentOrCommunity | undefined): str resolvingParts.push(friendlyName); } else { downloadingParts.push(friendlyName); + downloadingClientUrls.push(...debouncedStates[state]); } } @@ -75,7 +84,7 @@ const useStateString = (commentOrCommunity: CommentOrCommunity | undefined): str } if (downloadingParts.length) { if (stateString) stateString += ', '; - stateString += downloadingParts.join(', ') + ' via IPFS'; + stateString += downloadingParts.join(', ') + getDownloadSourceSuffix(downloadingClientUrls, isBrowserPureP2P); } if (!stateString && commentOrCommunity?.state !== 'succeeded') { @@ -94,7 +103,7 @@ const useStateString = (commentOrCommunity: CommentOrCommunity | undefined): str .replace('community community', 'board') .replace('downloading community', 'downloading board'); if (isIpfsRelated) { - stateString += ' via IPFS'; + stateString += getDownloadSourceSuffix([], isBrowserPureP2P); } } } @@ -104,10 +113,11 @@ const useStateString = (commentOrCommunity: CommentOrCommunity | undefined): str } return stateString === '' ? undefined : stateString; - }, [debouncedStates, commentOrCommunity]); + }, [debouncedStates, commentOrCommunity, isBrowserPureP2P]); }; export const useFeedStateString = (communityAddresses?: string[]): string | undefined => { + const isBrowserPureP2P = isBrowserPureP2PEnabled(); const communities = useCommunityIdentifiers(communityAddresses); // single community feed state string @@ -136,11 +146,13 @@ export const useFeedStateString = (communityAddresses?: string[]): string | unde } const pagesStatesCommunityAddresses = new Set(); + const downloadingClientUrls: string[] = []; for (const state in states) { if (state.match('page')) { const communityState = states[state]; if (isCommunityLoadingState(communityState)) { communityState.communityAddresses.forEach((address: string) => pagesStatesCommunityAddresses.add(address)); + downloadingClientUrls.push(...communityState.clientUrls); } } } @@ -151,6 +163,7 @@ export const useFeedStateString = (communityAddresses?: string[]): string | unde if (states['fetching-ipns']) { const fetchingIpnsState = states['fetching-ipns']; if (isCommunityLoadingState(fetchingIpnsState)) { + downloadingClientUrls.push(...fetchingIpnsState.clientUrls); const count = fetchingIpnsState.communityAddresses.length; stateString += `${count} ${count === 1 ? 'board' : 'boards'}`; if (count <= 5) { @@ -162,6 +175,7 @@ export const useFeedStateString = (communityAddresses?: string[]): string | unde if (states['fetching-ipfs']) { const fetchingIpfsState = states['fetching-ipfs']; if (isCommunityLoadingState(fetchingIpfsState)) { + downloadingClientUrls.push(...fetchingIpfsState.clientUrls); if (stateString[stateString.length - 1] !== ' ') { stateString += ', '; } @@ -176,7 +190,7 @@ export const useFeedStateString = (communityAddresses?: string[]): string | unde stateString += `${count} ${count === 1 ? 'page' : 'pages'}`; } - stateString += ' via IPFS'; + stateString += getDownloadSourceSuffix(downloadingClientUrls, isBrowserPureP2P); } if (!stateString && communityAddresses?.length) { @@ -192,7 +206,7 @@ export const useFeedStateString = (communityAddresses?: string[]): string | unde // if string is empty, return undefined instead return stateString === '' ? undefined : stateString; - }, [states, communityAddress, communityAddresses]); + }, [states, communityAddress, communityAddresses, isBrowserPureP2P]); if (singleCommunityFeedStateString) { return singleCommunityFeedStateString;