refactor(state-string): make loading state strings user-friendly, replace gateway hostnames with via IPFS

This commit is contained in:
plebeius
2026-03-09 19:36:54 +08:00
parent 61faa06ff9
commit af89b59cca
2 changed files with 76 additions and 66 deletions
+18 -7
View File
@@ -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)');
});
});
+58 -59
View File
@@ -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<string, string> = {
'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<string>();
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