diff --git a/src/hooks/__tests__/use-state-string.test.tsx b/src/hooks/__tests__/use-state-string.test.tsx index 713e58e4..e4d51305 100644 --- a/src/hooks/__tests__/use-state-string.test.tsx +++ b/src/hooks/__tests__/use-state-string.test.tsx @@ -75,7 +75,7 @@ describe('use-state-string', () => { container.remove(); }); - it('formats client state strings with normalized hostnames', () => { + it('formats client state strings with friendly names and via IPFS', () => { testState.clientsStates = { 'fetching-ipns': ['https://rpc.example.com/path', 'https://ipfs.io/api'], 'resolving-address': ['https://ens.example.com'], @@ -85,19 +85,19 @@ describe('use-state-string', () => { root.render(createElement(StateStringHarness, { value: { state: 'updating' } })); }); - expect(latestValue).toBe('Fetching IPNS from rpc.example.com, ipfs.io, resolving address from ens.example.com'); + expect(latestValue).toBe('Resolving address, downloading board via IPFS'); }); 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' } })); }); - expect(latestValue).toBe('Downloading thread'); + expect(latestValue).toBe('Downloading thread via IPFS'); act(() => { root.render(createElement(StateStringHarness, { value: { state: 'updating', updatingState: 'fetching-ipns' } })); }); - expect(latestValue).toBe('Downloading board'); + expect(latestValue).toBe('Downloading board via IPFS'); }); it('sanitizes single-board feed state strings to board wording', () => { @@ -110,7 +110,7 @@ describe('use-state-string', () => { root.render(createElement(FeedStateStringHarness, { addresses: ['music-posting.eth'] })); }); - expect(latestValue).toBe('Downloading board'); + expect(latestValue).toBe('Downloading board via IPFS'); }); it('aggregates multi-board feed states across address resolution, threads, and pages', () => { @@ -137,7 +137,7 @@ describe('use-state-string', () => { root.render(createElement(FeedStateStringHarness, { addresses: ['music-posting.eth', 'tech-posting.eth'] })); }); - expect(latestValue).toBe('Resolving 2 addresses from ens.example.com, downloading 2 boards, 1 threads, 1 page from gateway.example.com, ipfs.io'); + expect(latestValue).toBe('Resolving 2 board addresses, downloading 2 boards (music-posting.eth, tech-posting.eth), 1 thread, 1 page via IPFS'); }); it('shows an immediate board-specific loading string before detailed multi-board states arrive', () => { @@ -145,6 +145,17 @@ describe('use-state-string', () => { root.render(createElement(FeedStateStringHarness, { addresses: ['music-posting.eth', 'tech-posting.eth'] })); }); - expect(latestValue).toBe('Downloading 2 boards'); + expect(latestValue).toBe('Downloading 2 boards (music-posting.eth, tech-posting.eth)'); + }); + + it('shortens long hash addresses in board list using getShortAddress', () => { + const longAddr1 = 'AdnytMQQMvAkG3XbzoVyAE6YLmHuG3UDigUC'; + const longAddr2 = 'NFgjQWX2EUEsZbzoVyAE6YLmHuG3UDigUCxx'; + + act(() => { + root.render(createElement(FeedStateStringHarness, { addresses: [longAddr1, longAddr2] })); + }); + + expect(latestValue).toBe('Downloading 2 boards (MvAkG3XbzoVy, EUEsZbzoVyAE)'); }); }); diff --git a/src/hooks/use-state-string.ts b/src/hooks/use-state-string.ts index 507b664b..2c360108 100644 --- a/src/hooks/use-state-string.ts +++ b/src/hooks/use-state-string.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import { useClientsStates, useSubplebbit, useSubplebbitsStates } from '@bitsocialnet/bitsocial-react-hooks'; import debounce from 'lodash/debounce'; +import getShortAddress from '../lib/get-short-address'; interface CommentOrSubplebbit { state?: string; @@ -12,19 +13,19 @@ interface States { [key: string]: string[]; } -const clientHosts: { [key: string]: string } = {}; - -const getClientHost = (clientUrl: string): string => { - if (!clientHosts[clientUrl]) { - try { - clientHosts[clientUrl] = new URL(clientUrl).hostname || clientUrl; - } catch { - clientHosts[clientUrl] = clientUrl; - } - } - return clientHosts[clientUrl]; +const friendlyStateNames: Record = { + 'fetching-ipns': 'downloading board', + 'fetching-ipfs': 'downloading thread', + 'fetching-subplebbit-ipns': 'downloading board', + 'fetching-subplebbit-ipfs': 'downloading board', + 'fetching-update-ipfs': 'downloading update', + 'resolving-address': 'resolving address', + 'resolving-subplebbit-address': 'resolving board address', + 'resolving-author-address': 'resolving author address', }; +const getFriendlyStateName = (state: string): string => friendlyStateNames[state] || state.replaceAll('-', ' '); + const sanitizeSingleFeedLoadingState = (stateString?: string): string | undefined => { if (!stateString) { return stateString; @@ -47,21 +48,25 @@ const useStateString = (commentOrSubplebbit: CommentOrSubplebbit): string | unde return useMemo(() => { let stateString: string | undefined = ''; + const resolvingParts: string[] = []; + const downloadingParts: string[] = []; for (const state in debouncedStates) { - const clientUrls = debouncedStates[state]; - const clientHosts = clientUrls.map((clientUrl: string) => getClientHost(clientUrl)); - - if (clientHosts.length === 0) { - continue; + if (debouncedStates[state].length === 0) continue; + const friendlyName = getFriendlyStateName(state); + if (state.includes('resolving')) { + resolvingParts.push(friendlyName); + } else { + downloadingParts.push(friendlyName); } + } - if (stateString) { - stateString += ', '; - } - - const formattedState = state.replaceAll('-', ' ').replace('ipfs', 'IPFS').replace('ipns', 'IPNS'); - stateString += `${formattedState} from ${clientHosts.join(', ')}`; + if (resolvingParts.length) { + stateString = resolvingParts.join(', '); + } + if (downloadingParts.length) { + if (stateString) stateString += ', '; + stateString += downloadingParts.join(', ') + ' via IPFS'; } if (!stateString && commentOrSubplebbit?.state !== 'succeeded') { @@ -71,6 +76,7 @@ const useStateString = (commentOrSubplebbit: CommentOrSubplebbit): string | unde stateString = commentOrSubplebbit?.updatingState; } if (stateString) { + const isIpfsRelated = stateString.includes('ipfs') || stateString.includes('ipns'); stateString = stateString .replaceAll('-', ' ') .replace('ipfs', 'thread') @@ -78,6 +84,9 @@ const useStateString = (commentOrSubplebbit: CommentOrSubplebbit): string | unde .replace('fetching', 'downloading') .replace('subplebbit subplebbit', 'board') .replace('downloading subplebbit', 'downloading board'); + if (isIpfsRelated) { + stateString += ' via IPFS'; + } } } @@ -103,62 +112,52 @@ export const useFeedStateString = (subplebbitAddresses?: string[]): string | und 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 { subplebbitAddresses, clientUrls } = states['resolving-address']; if (subplebbitAddresses.length && clientUrls.length) { - stateString += `resolving ${subplebbitAddresses.length} ${subplebbitAddresses.length === 1 ? 'address' : 'addresses'} from ${clientUrls - .map(getClientHost) - .join(', ')}`; + const count = subplebbitAddresses.length; + stateString += `resolving ${count} board ${count === 1 ? 'address' : 'addresses'}`; } } - // find all page client and sub addresses - const pagesStatesClientHosts = new Set(); - const pagesStatesSubplebbitAddresses = new Set(); + const pagesStatesSubplebbitAddresses = new Set(); for (const state in states) { if (state.match('page')) { - states[state].clientUrls.forEach((clientUrl) => pagesStatesClientHosts.add(getClientHost(clientUrl))); - states[state].subplebbitAddresses.forEach((subplebbitAddress) => pagesStatesSubplebbitAddresses.add(subplebbitAddress)); + states[state].subplebbitAddresses.forEach((subplebbitAddress: string) => pagesStatesSubplebbitAddresses.add(subplebbitAddress)); } } if (states['fetching-ipns'] || states['fetching-ipfs'] || pagesStatesSubplebbitAddresses.size) { - // separate 2 different states using ', ' - if (stateString) { - stateString += ', '; + if (stateString) stateString += ', '; + stateString += 'downloading '; + if (states['fetching-ipns']) { + const count = states['fetching-ipns'].subplebbitAddresses.length; + stateString += `${count} ${count === 1 ? 'board' : 'boards'}`; + if (count <= 5) { + stateString += ` (${states['fetching-ipns'].subplebbitAddresses.map((a: string) => getShortAddress(a) || a).join(', ')})`; + } } - - // 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 += 'downloading '; - if (states['fetching-ipns']) { - stateString += `${states['fetching-ipns'].subplebbitAddresses.length} boards`; - } - if (states['fetching-ipfs']) { - if (states['fetching-ipns']) { - stateString += ', '; - } - stateString += `${states['fetching-ipfs'].subplebbitAddresses.length} threads`; - } - if (pagesStatesSubplebbitAddresses.size) { - if (states['fetching-ipns'] || states['fetching-ipfs']) { - stateString += ', '; - } - stateString += `${pagesStatesSubplebbitAddresses.size} ${pagesStatesSubplebbitAddresses.size === 1 ? 'page' : 'pages'}`; - } - stateString += ` from ${[...clientHosts].join(', ')}`; + if (states['fetching-ipfs']) { + if (states['fetching-ipns']) stateString += ', '; + const count = states['fetching-ipfs'].subplebbitAddresses.length; + stateString += `${count} ${count === 1 ? 'thread' : 'threads'}`; } + if (pagesStatesSubplebbitAddresses.size) { + if (states['fetching-ipns'] || states['fetching-ipfs']) stateString += ', '; + const count = pagesStatesSubplebbitAddresses.size; + stateString += `${count} ${count === 1 ? 'page' : 'pages'}`; + } + stateString += ' via IPFS'; } if (!stateString && subplebbitAddresses?.length) { - stateString = `downloading ${subplebbitAddresses.length} boards`; + const count = subplebbitAddresses.length; + stateString = `downloading ${count} ${count === 1 ? 'board' : 'boards'}`; + if (count <= 5) { + stateString += ` (${subplebbitAddresses.map((a) => getShortAddress(a) || a).join(', ')})`; + } } // capitalize first letter