From 4217142fe487a382e54577e32569810d5aefd48c Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Sat, 11 Jul 2026 15:05:27 +0700 Subject: [PATCH] fix(feed): show completed empty state --- .../empty-feed-message.module.css | 5 + .../empty-feed-message/empty-feed-message.tsx | 10 ++ .../feed-footer/feed-footer-utils.test.ts | 46 ++++++++ .../feed-footer/feed-footer-utils.ts | 27 +++++ .../feed-footer/feed-footer.module.css | 2 +- src/components/feed-footer/feed-footer.tsx | 108 +++++++++++------- src/views/community/community.tsx | 11 +- 7 files changed, 163 insertions(+), 46 deletions(-) create mode 100644 src/components/empty-feed-message/empty-feed-message.module.css create mode 100644 src/components/empty-feed-message/empty-feed-message.tsx create mode 100644 src/components/feed-footer/feed-footer-utils.test.ts create mode 100644 src/components/feed-footer/feed-footer-utils.ts diff --git a/src/components/empty-feed-message/empty-feed-message.module.css b/src/components/empty-feed-message/empty-feed-message.module.css new file mode 100644 index 00000000..3e4245eb --- /dev/null +++ b/src/components/empty-feed-message/empty-feed-message.module.css @@ -0,0 +1,5 @@ +.message { + color: var(--red); + font-size: 13px; + text-transform: lowercase; +} diff --git a/src/components/empty-feed-message/empty-feed-message.tsx b/src/components/empty-feed-message/empty-feed-message.tsx new file mode 100644 index 00000000..4989acb1 --- /dev/null +++ b/src/components/empty-feed-message/empty-feed-message.tsx @@ -0,0 +1,10 @@ +import { useTranslation } from 'react-i18next'; +import styles from './empty-feed-message.module.css'; + +const EmptyFeedMessage = () => { + const { t } = useTranslation(); + + return
{t('nothing_found')}
; +}; + +export default EmptyFeedMessage; diff --git a/src/components/feed-footer/feed-footer-utils.test.ts b/src/components/feed-footer/feed-footer-utils.test.ts new file mode 100644 index 00000000..5d3361bf --- /dev/null +++ b/src/components/feed-footer/feed-footer-utils.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { shouldShowEmptyFeed } from './feed-footer-utils'; + +describe('shouldShowEmptyFeed', () => { + it.each([0, 1, 60, 1000])('shows the empty state after all %i requested communities load even when pagination has more', (requestedCommunityCount) => { + const communities = Array.from({ length: requestedCommunityCount }, (_, index) => ({ updatedAt: index + 1 })); + const emptyLoadedFeed = { + requestedCommunityCount, + communities, + feedLength: 0, + hasMore: true, + }; + + expect(shouldShowEmptyFeed(emptyLoadedFeed)).toBe(true); + }); + + it('keeps loading while any requested community has not loaded', () => { + expect( + shouldShowEmptyFeed({ + requestedCommunityCount: 2, + communities: [{ updatedAt: 1 }, undefined], + feedLength: 0, + }), + ).toBe(false); + + expect( + shouldShowEmptyFeed({ + requestedCommunityCount: 2, + communities: [{ updatedAt: 1 }, {}], + feedLength: 0, + }), + ).toBe(false); + }); + + it('does not show the empty state when posts or a search are present', () => { + const loadedFeed = { + requestedCommunityCount: 1, + communities: [{ updatedAt: 1 }], + }; + + expect(shouldShowEmptyFeed({ ...loadedFeed, feedLength: 1 })).toBe(false); + expect(shouldShowEmptyFeed({ ...loadedFeed, feedLength: 0, isLoadingCommunityData: true })).toBe(false); + expect(shouldShowEmptyFeed({ ...loadedFeed, feedLength: 0, isSearching: true })).toBe(false); + expect(shouldShowEmptyFeed({ ...loadedFeed, feedLength: 0, searchQuery: 'query' })).toBe(false); + }); +}); diff --git a/src/components/feed-footer/feed-footer-utils.ts b/src/components/feed-footer/feed-footer-utils.ts new file mode 100644 index 00000000..290f914d --- /dev/null +++ b/src/components/feed-footer/feed-footer-utils.ts @@ -0,0 +1,27 @@ +interface FeedCommunityLoadState { + updatedAt?: number; +} + +interface ShouldShowEmptyFeedOptions { + requestedCommunityCount: number; + communities: Array; + feedLength: number; + isLoadingCommunityData?: boolean; + isSearching?: boolean; + searchQuery?: string; +} + +export const shouldShowEmptyFeed = ({ + requestedCommunityCount, + communities, + feedLength, + isLoadingCommunityData, + isSearching, + searchQuery, +}: ShouldShowEmptyFeedOptions): boolean => { + // `hasMore` also represents pagination. `updatedAt` is the hook's count-safe + // evidence that each requested community snapshot loaded. + const haveAllRequestedCommunitiesLoaded = communities.length === requestedCommunityCount && communities.every((community) => Boolean(community?.updatedAt)); + + return haveAllRequestedCommunitiesLoaded && feedLength === 0 && !isLoadingCommunityData && !isSearching && !searchQuery; +}; diff --git a/src/components/feed-footer/feed-footer.module.css b/src/components/feed-footer/feed-footer.module.css index 0172e728..432d3e84 100644 --- a/src/components/feed-footer/feed-footer.module.css +++ b/src/components/feed-footer/feed-footer.module.css @@ -48,4 +48,4 @@ color: var(--red); font-size: 13px; text-transform: lowercase; -} \ No newline at end of file +} diff --git a/src/components/feed-footer/feed-footer.tsx b/src/components/feed-footer/feed-footer.tsx index 56bba6a1..ea967694 100644 --- a/src/components/feed-footer/feed-footer.tsx +++ b/src/components/feed-footer/feed-footer.tsx @@ -1,9 +1,13 @@ import { useState, useEffect } from 'react'; import { Link, useLocation, useParams } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; +import { useCommunities } from '@bitsocial/bitsocial-react-hooks'; import { isAllView, isModView } from '../../lib/utils/view-utils'; +import { useCommunityIdentifiers } from '../../hooks/use-community-identifier'; import { useFeedStateString } from '../../hooks/use-state-string'; +import EmptyFeedMessage from '../empty-feed-message/empty-feed-message'; import LoadingEllipsis from '../loading-ellipsis'; +import { shouldShowEmptyFeed } from './feed-footer-utils'; import styles from './feed-footer.module.css'; import React from 'react'; @@ -45,12 +49,55 @@ const FeedFooter = ({ const isInModView = isModView(location.pathname); const isInAllView = isAllView(location.pathname); + const feedCommunityIdentifiers = useCommunityIdentifiers(communityAddresses); + const { communities } = useCommunities({ communities: feedCommunityIdentifiers }); const feedStateString = useFeedStateString(communityAddresses); const loadingStateString = - useFeedStateString(communityAddresses) || + feedStateString || (!hasFeedLoaded || (feedLength === 0 && !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > feedLength)) ? t('loading_feed') : t('looking_for_more_posts')); + const hasEmptyFeedData = shouldShowEmptyFeed({ + requestedCommunityCount: feedCommunityIdentifiers.length, + communities, + feedLength, + isLoadingCommunityData: Boolean(feedStateString), + isSearching, + searchQuery, + }); + + const widerFeedSuggestion = + weeklyFeedLength > feedLength && !searchQuery ? ( +
+ , + }} + /> +
+ ) : monthlyFeedLength > feedLength && !searchQuery ? ( +
+ , + }} + /> +
+ ) : yearlyFeedLength > feedLength && !searchQuery ? ( +
+ , + }} + /> +
+ ) : null; // Add state to track initial loading const [hasFetchedCommunityAddresses, setHasFetchedSubplebbitAddresses] = useState(false); @@ -63,6 +110,8 @@ const FeedFooter = ({ return () => clearTimeout(timer); }, []); + const showEmptyFeed = hasFetchedCommunityAddresses && hasEmptyFeedData; + if (!hasFetchedCommunityAddresses) { footerContent = ; } @@ -101,57 +150,32 @@ const FeedFooter = ({ ); - } else if ( - hasFeedLoaded && - feedLength === 0 && - !hasMore && - !isSearching && - !searchQuery && - !(weeklyFeedLength > feedLength || monthlyFeedLength > feedLength || yearlyFeedLength > feedLength) - ) { - footerContent = t('no_posts'); + } else if (showEmptyFeed) { + footerContent = ( + <> + {widerFeedSuggestion} + + + ); } else if (hasMore || communityAddresses.length > 0 || (communityAddresses && communityAddresses.length === 0)) { // Only show newer posts/weekly/monthly suggestions when not searching footerContent = ( <> - {weeklyFeedLength > feedLength && !searchQuery ? ( -
- , - }} - /> -
- ) : monthlyFeedLength > feedLength && !searchQuery ? ( -
- , - }} - /> -
- ) : yearlyFeedLength > feedLength && !searchQuery ? ( -
- , - }} - /> -
- ) : null} + {widerFeedSuggestion}
{communityAddresses.length === 0 ? ( isInModView ? (
{t('not_moderator')}
) : (
- https://github.com/bitsocialhq/lists]} /> + + https://github.com/bitsocialhq/lists + , + ]} + />
{t('connect_community_notice')}
diff --git a/src/views/community/community.tsx b/src/views/community/community.tsx index 773b1d12..c23cd5ae 100644 --- a/src/views/community/community.tsx +++ b/src/views/community/community.tsx @@ -18,6 +18,7 @@ import { isResolvableCommunityAddress } from '../../lib/utils/directory-codes'; import useTimeFilter, { isValidTimeFilterName } from '../../hooks/use-time-filter'; import { getCommunityIdentifier, getCommunityIdentifiers } from '../../hooks/use-community-identifier'; import ErrorDisplay from '../../components/error-display'; +import EmptyFeedMessage from '../../components/empty-feed-message/empty-feed-message'; import LoadingEllipsis from '../../components/loading-ellipsis'; import Over18Warning from '../../components/over-18-warning'; import Post from '../../components/post'; @@ -31,6 +32,7 @@ interface FooterProps { communityAddress: string; feedLength: number; isOnline: boolean; + hasCommunityLoaded: boolean; started: boolean; isSubCreatedButNotYetPublished: boolean; hasMore: boolean; @@ -46,6 +48,7 @@ const Footer = ({ communityAddress, feedLength, isOnline, + hasCommunityLoaded, started: _started, isSubCreatedButNotYetPublished: _isSubCreatedButNotYetPublished, hasMore, @@ -58,7 +61,8 @@ const Footer = ({ const { t } = useTranslation(); let footerFirstLine; let footerSecondLine; - const loadingStateString = useFeedStateString(communityAddresses) || t('loading'); + const feedStateString = useFeedStateString(communityAddresses); + const loadingStateString = feedStateString || t('loading'); const [showNoResults, setShowNoResults] = useState(false); const [searchAttemptCompleted, setSearchAttemptCompleted] = useState(false); @@ -180,8 +184,8 @@ const Footer = ({
); } - } else if (feedLength === 0 && isOnline && !hasMore) { - footerFirstLine = t('no_posts'); + } else if (feedLength === 0 && isOnline && hasCommunityLoaded && !feedStateString) { + footerFirstLine = ; } else if (feedLength === 0 || !isOnline) { footerFirstLine = loadingString; } else if (hasMore) { @@ -320,6 +324,7 @@ const CommunityView = () => { communityAddress, feedLength: combinedFeed.length || 0, isOnline, + hasCommunityLoaded: Boolean(updatedAt), started, isSubCreatedButNotYetPublished, hasMore,