Fold followed hashtags into the Following feed and drop tag tabs
- useFollowingFeed now also queries posts for the user's followed hashtag interests (NIP-51 kind 10015 t tags) and merges them into the combined Following feed, subject to the same recency floor. - Drop the per-hashtag and per-geotag tabs from the home feed subheader. Legacy 'hashtag:'/'geotag:' session-storage values fall back to the Following tab. - Invalidate the new following-feed query keys when interests change so the Following feed refreshes immediately on follow/unfollow. - Remove the now-dead HashtagFeedContent and GeotagFeedContent components and their unused imports.
This commit is contained in:
+16
-158
@@ -1,7 +1,5 @@
|
||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useNostr } from '@nostrify/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { usePageRefresh } from '@/hooks/usePageRefresh';
|
||||
import { ComposeBox } from '@/components/ComposeBox';
|
||||
import { LandingHero } from '@/components/LandingHero';
|
||||
@@ -9,23 +7,19 @@ import { NoteCard } from '@/components/NoteCard';
|
||||
import { PullToRefresh } from '@/components/PullToRefresh';
|
||||
import { FeedEmptyState } from '@/components/FeedEmptyState';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Globe2, Loader2, MapPin } from 'lucide-react';
|
||||
import { Globe2, Loader2 } from 'lucide-react';
|
||||
import LoginDialog from '@/components/auth/LoginDialog';
|
||||
import { useOnboarding } from '@/hooks/useOnboarding';
|
||||
import { useFeed } from '@/hooks/useFeed';
|
||||
import { useFeedSettings } from '@/hooks/useFeedSettings';
|
||||
import { useFollowingFeed } from '@/hooks/useFollowingFeed';
|
||||
import { DITTO_RELAYS } from '@/lib/appRelays';
|
||||
import { useCurrentUser } from '@/hooks/useCurrentUser';
|
||||
import { useFeedTab } from '@/hooks/useFeedTab';
|
||||
import { useInterests } from '@/hooks/useInterests';
|
||||
import { useMuteList } from '@/hooks/useMuteList';
|
||||
import { useTabFeed } from '@/hooks/useProfileFeed';
|
||||
import { useSavedFeeds } from '@/hooks/useSavedFeeds';
|
||||
import { useResolveTabFilter } from '@/hooks/useResolveTabFilter';
|
||||
import { useWorldFeed } from '@/hooks/useWorldFeed';
|
||||
import { getEnabledFeedKinds } from '@/lib/extraKinds';
|
||||
import { isRepostKind, shouldHideFeedEvent } from '@/lib/feedUtils';
|
||||
import { shouldHideFeedEvent } from '@/lib/feedUtils';
|
||||
import { isEventMuted } from '@/lib/muteHelpers';
|
||||
import { SubHeaderBar } from '@/components/SubHeaderBar';
|
||||
import { ARC_OVERHANG_PX } from '@/components/ArcBackground';
|
||||
@@ -34,7 +28,6 @@ import { Button } from '@/components/ui/button';
|
||||
import { useNavHidden } from '@/contexts/LayoutContext';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { FeedItem } from '@/lib/feedUtils';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import type { SavedFeed } from '@/contexts/AppContext';
|
||||
|
||||
type CoreFeedTab = 'follows' | 'network' | 'global' | 'communities' | 'world';
|
||||
@@ -59,8 +52,6 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
|
||||
const { user } = useCurrentUser();
|
||||
const { muteItems } = useMuteList();
|
||||
const { savedFeeds } = useSavedFeeds();
|
||||
const { hashtags } = useInterests();
|
||||
const { hashtags: geotags } = useInterests('g');
|
||||
const navHidden = useNavHidden();
|
||||
|
||||
// Tab settings from localStorage
|
||||
@@ -103,6 +94,9 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
|
||||
if (!kinds) {
|
||||
// Migrate legacy 'ditto' tab to 'world'
|
||||
if (rawActiveTab === 'ditto') return 'world';
|
||||
// Legacy hashtag:/geotag: tabs are now part of the combined Following
|
||||
// feed; surface them there instead of rendering a missing sub-feed.
|
||||
if (rawActiveTab.startsWith('hashtag:') || rawActiveTab.startsWith('geotag:')) return 'follows';
|
||||
return rawActiveTab;
|
||||
}
|
||||
if (rawActiveTab === 'global') return 'global';
|
||||
@@ -116,14 +110,18 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
|
||||
[savedFeeds, activeTab],
|
||||
);
|
||||
|
||||
// Is the active tab a hashtag interest?
|
||||
const activeHashtag = activeTab.startsWith('hashtag:') ? activeTab.slice(8) : null;
|
||||
|
||||
// Is the active tab a geotag interest?
|
||||
const activeGeotag = activeTab.startsWith('geotag:') ? activeTab.slice(7) : null;
|
||||
// Migrate legacy hashtag:/geotag: tabs (which used to render their own
|
||||
// sub-feeds) back to the home Following feed. Followed hashtags/geotags
|
||||
// now contribute to the combined Following feed instead of getting
|
||||
// dedicated tabs.
|
||||
useEffect(() => {
|
||||
if (rawActiveTab.startsWith('hashtag:') || rawActiveTab.startsWith('geotag:')) {
|
||||
handleSetActiveTab('follows');
|
||||
}
|
||||
}, [rawActiveTab, handleSetActiveTab]);
|
||||
|
||||
// Kind-specific pages (e.g. Development, WebXDC) only show Follows + Global tabs.
|
||||
// Extra tabs (World, Community, saved feeds, hashtags) are only for the home feed.
|
||||
// Extra tabs (World, Community, saved feeds) are only for the home feed.
|
||||
const isKindSpecificPage = !!kinds;
|
||||
|
||||
// When logged out (and not on a kind-specific page), show the World feed.
|
||||
@@ -277,37 +275,12 @@ export function Feed({ kinds, tagFilters, header, hideCompose, emptyMessage, fee
|
||||
onClick={() => handleSetActiveTab(feed.id)}
|
||||
/>
|
||||
))}
|
||||
{showSavedFeedTabs && hashtags.map((tag) => (
|
||||
<TabButton
|
||||
key={`hashtag:${tag}`}
|
||||
label={`#${tag}`}
|
||||
active={activeTab === `hashtag:${tag}`}
|
||||
onClick={() => handleSetActiveTab(`hashtag:${tag}`)}
|
||||
/>
|
||||
))}
|
||||
{showSavedFeedTabs && geotags.map((tag) => (
|
||||
<TabButton
|
||||
key={`geotag:${tag}`}
|
||||
label={tag}
|
||||
active={activeTab === `geotag:${tag}`}
|
||||
onClick={() => handleSetActiveTab(`geotag:${tag}`)}
|
||||
>
|
||||
<span className="flex items-center justify-center gap-1">
|
||||
<MapPin className="size-3.5" />
|
||||
{tag}
|
||||
</span>
|
||||
</TabButton>
|
||||
))}
|
||||
</SubHeaderBar>
|
||||
)}
|
||||
|
||||
{/* Feed content — saved feed tab gets its own stream */}
|
||||
{user && <div style={{ height: ARC_OVERHANG_PX }} />}
|
||||
{activeHashtag ? (
|
||||
<HashtagFeedContent tag={activeHashtag} />
|
||||
) : activeGeotag ? (
|
||||
<GeotagFeedContent tag={activeGeotag} />
|
||||
) : activeSavedFeed ? (
|
||||
{activeSavedFeed ? (
|
||||
<SavedFeedContent feed={activeSavedFeed} />
|
||||
) : (
|
||||
<PullToRefresh onRefresh={isWorldActive ? handleWorldRefresh : handleRefresh}>
|
||||
@@ -506,121 +479,6 @@ function SavedFeedContent({ feed }: { feed: SavedFeed }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders a feed of posts tagged with a specific hashtag. */
|
||||
function HashtagFeedContent({ tag }: { tag: string }) {
|
||||
const { nostr } = useNostr();
|
||||
const { muteItems } = useMuteList();
|
||||
const { feedSettings } = useFeedSettings();
|
||||
const kinds = getEnabledFeedKinds(feedSettings).filter((k) => !isRepostKind(k));
|
||||
const kindsKey = [...kinds].sort().join(',');
|
||||
|
||||
const queryKey = useMemo(() => ['hashtag-feed', tag, kindsKey], [tag, kindsKey]);
|
||||
const handleRefresh = usePageRefresh(queryKey);
|
||||
|
||||
const { data: events, isLoading } = useQuery<NostrEvent[]>({
|
||||
queryKey,
|
||||
queryFn: async ({ signal }) => {
|
||||
const ditto = nostr.group(DITTO_RELAYS);
|
||||
return ditto.query(
|
||||
[{ kinds, '#t': [tag.toLowerCase()], limit: 40 }],
|
||||
{ signal: AbortSignal.any([signal, AbortSignal.timeout(10000)]) },
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const filteredEvents = useMemo((): NostrEvent[] => {
|
||||
if (!events) return [];
|
||||
if (muteItems.length === 0) return events;
|
||||
return events.filter((e) => !isEventMuted(e, muteItems));
|
||||
}, [events, muteItems]);
|
||||
|
||||
if (isLoading && filteredEvents.length === 0) {
|
||||
return (
|
||||
<div className="divide-y divide-border">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<NoteCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredEvents.length === 0) {
|
||||
return (
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<FeedEmptyState message={`No posts found with #${tag}.`} />
|
||||
</PullToRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<div>
|
||||
{filteredEvents.map((event) => (
|
||||
<NoteCard key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</PullToRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
/** Renders a feed of posts tagged with a specific geohash. */
|
||||
function GeotagFeedContent({ tag }: { tag: string }) {
|
||||
const { nostr } = useNostr();
|
||||
const { muteItems } = useMuteList();
|
||||
const { feedSettings } = useFeedSettings();
|
||||
const kinds = getEnabledFeedKinds(feedSettings).filter((k) => !isRepostKind(k));
|
||||
const kindsKey = [...kinds].sort().join(',');
|
||||
|
||||
const queryKey = useMemo(() => ['geotag-feed', tag, kindsKey], [tag, kindsKey]);
|
||||
const handleRefresh = usePageRefresh(queryKey);
|
||||
|
||||
const { data: events, isLoading } = useQuery<NostrEvent[]>({
|
||||
queryKey,
|
||||
queryFn: async ({ signal }) => {
|
||||
const ditto = nostr.group(DITTO_RELAYS);
|
||||
const filter = { kinds, limit: 40 } as Record<string, unknown>;
|
||||
filter['#g'] = [tag];
|
||||
return ditto.query([filter as Parameters<typeof ditto.query>[0][number]], {
|
||||
signal: AbortSignal.any([signal, AbortSignal.timeout(10000)]),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const filteredEvents = useMemo((): NostrEvent[] => {
|
||||
if (!events) return [];
|
||||
if (muteItems.length === 0) return events;
|
||||
return events.filter((e) => !isEventMuted(e, muteItems));
|
||||
}, [events, muteItems]);
|
||||
|
||||
if (isLoading && filteredEvents.length === 0) {
|
||||
return (
|
||||
<div className="divide-y divide-border">
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<NoteCardSkeleton key={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (filteredEvents.length === 0) {
|
||||
return (
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<FeedEmptyState message={`No posts found near ${tag}.`} />
|
||||
</PullToRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PullToRefresh onRefresh={handleRefresh}>
|
||||
<div>
|
||||
{filteredEvents.map((event) => (
|
||||
<NoteCard key={event.id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</PullToRefresh>
|
||||
);
|
||||
}
|
||||
|
||||
function NoteCardSkeleton() {
|
||||
return (
|
||||
<div className="px-4 py-3 border-b border-border">
|
||||
|
||||
+107
-14
@@ -6,21 +6,27 @@ import { useCommunityActivityFeed } from '@/hooks/useCommunityActivityFeed';
|
||||
import { useCountryFollows } from '@/hooks/useCountryFollows';
|
||||
import { useFeed } from '@/hooks/useFeed';
|
||||
import { useFeedRelays } from '@/hooks/useFeedRelays';
|
||||
import { useFeedSettings } from '@/hooks/useFeedSettings';
|
||||
import { useInterests } from '@/hooks/useInterests';
|
||||
import { getCountryFilterValues, parseCountryIdentifier } from '@/lib/countryIdentifiers';
|
||||
import type { FeedItem } from '@/lib/feedUtils';
|
||||
import { getEnabledFeedKinds } from '@/lib/extraKinds';
|
||||
import { isRepostKind, type FeedItem } from '@/lib/feedUtils';
|
||||
import { buildTagFilterValues } from '@/lib/tagFilterValues';
|
||||
|
||||
const COUNTRY_PAGE_SIZE = 40;
|
||||
const HASHTAG_PAGE_SIZE = 40;
|
||||
const CHALLENGE_T_ALIASES = ['agora-action', 'pathos-challenge', 'agora-challenge'];
|
||||
|
||||
/**
|
||||
* Sliding window used as a fallback recency floor for the Following feed
|
||||
* when the network feed has no events yet. Without this, an inactive
|
||||
* follow list lets very old community/country events take over the top
|
||||
* of the feed before the network feed has a chance to populate.
|
||||
* follow list lets very old community/country/hashtag events take over
|
||||
* the top of the feed before the network feed has a chance to populate.
|
||||
*
|
||||
* 14 days is intentionally short to keep the Following feed feeling
|
||||
* "current" — items older than this should only be reachable via
|
||||
* pagination on dedicated tabs (Network, Communities, country pages).
|
||||
* pagination on dedicated tabs (Network, Communities, country/hashtag
|
||||
* pages).
|
||||
*/
|
||||
const RECENCY_WINDOW_SECONDS = 14 * 24 * 60 * 60;
|
||||
|
||||
@@ -30,6 +36,12 @@ interface CountryFeedPage {
|
||||
totalFetched: number;
|
||||
}
|
||||
|
||||
interface HashtagFeedPage {
|
||||
events: NostrEvent[];
|
||||
oldestTimestamp: number | null;
|
||||
totalFetched: number;
|
||||
}
|
||||
|
||||
function eventCountryCode(event: NostrEvent): string | undefined {
|
||||
const iTag = event.tags.find(([name]) => name === 'i')?.[1];
|
||||
const parsedITag = iTag ? parseCountryIdentifier(iTag) : undefined;
|
||||
@@ -91,10 +103,66 @@ function useFollowedCountriesFeed(countryCodes: string[], enabled: boolean) {
|
||||
});
|
||||
}
|
||||
|
||||
function useFollowedHashtagsFeed(hashtags: string[], kinds: number[], enabled: boolean) {
|
||||
const feedRelays = useFeedRelays();
|
||||
const hashtagsKey = hashtags.join(',');
|
||||
const kindsKey = [...kinds].sort().join(',');
|
||||
|
||||
return useInfiniteQuery<HashtagFeedPage, Error>({
|
||||
queryKey: ['following-hashtag-feed', hashtagsKey, kindsKey],
|
||||
queryFn: async ({ pageParam, signal: querySignal }) => {
|
||||
if (hashtags.length === 0 || kinds.length === 0) {
|
||||
return { events: [], oldestTimestamp: null, totalFetched: 0 };
|
||||
}
|
||||
|
||||
const signal = AbortSignal.any([querySignal, AbortSignal.timeout(5000)]);
|
||||
const until = pageParam as number | undefined;
|
||||
|
||||
// Hashtags on Nostr are case-sensitive at the relay level but the UI
|
||||
// treats them as case-insensitive. Pass through the same expansion
|
||||
// used by the dedicated hashtag page so we don't miss posts that
|
||||
// tag, e.g., `#Bitcoin` instead of `#bitcoin`.
|
||||
const filterValues = Array.from(new Set(
|
||||
hashtags.flatMap((tag) => buildTagFilterValues(tag, '#t')),
|
||||
));
|
||||
|
||||
const filter: NostrFilter = {
|
||||
kinds,
|
||||
'#t': filterValues,
|
||||
limit: HASHTAG_PAGE_SIZE,
|
||||
...(until && { until }),
|
||||
};
|
||||
|
||||
const events = await feedRelays.query([filter], { signal });
|
||||
const sorted = [...events].sort((a, b) => b.created_at - a.created_at);
|
||||
const pageEvents = sorted.slice(0, HASHTAG_PAGE_SIZE);
|
||||
const oldestTimestamp = pageEvents.length > 0
|
||||
? pageEvents[pageEvents.length - 1].created_at
|
||||
: null;
|
||||
|
||||
return {
|
||||
events: pageEvents,
|
||||
oldestTimestamp,
|
||||
totalFetched: sorted.length,
|
||||
};
|
||||
},
|
||||
initialPageParam: undefined as number | undefined,
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.totalFetched < HASHTAG_PAGE_SIZE || !lastPage.oldestTimestamp) return undefined;
|
||||
return lastPage.oldestTimestamp - 1;
|
||||
},
|
||||
enabled: enabled && hashtags.length > 0 && kinds.length > 0,
|
||||
staleTime: 30_000,
|
||||
refetchOnWindowFocus: false,
|
||||
placeholderData: (previousData) => previousData,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Combined "Following" feed: people you follow + your communities' activity +
|
||||
* the countries you follow. Items are sorted strictly by recency
|
||||
* (`sortTimestamp` desc) with no per-source prioritisation.
|
||||
* the countries you follow + the hashtags you follow. Items are sorted
|
||||
* strictly by recency (`sortTimestamp` desc) with no per-source
|
||||
* prioritisation.
|
||||
*
|
||||
* Older content from sources with sparse activity is filtered out so the
|
||||
* top of the feed doesn't drift back in time while a higher-volume source
|
||||
@@ -105,11 +173,20 @@ function useFollowedCountriesFeed(countryCodes: string[], enabled: boolean) {
|
||||
* applies a recency floor).
|
||||
*/
|
||||
export function useFollowingFeed(enabled = true) {
|
||||
const { feedSettings } = useFeedSettings();
|
||||
const hashtagKinds = useMemo(
|
||||
() => getEnabledFeedKinds(feedSettings).filter((k) => !isRepostKind(k)),
|
||||
[feedSettings],
|
||||
);
|
||||
|
||||
const networkFeed = useFeed('network', { enabled });
|
||||
const communityFeed = useCommunityActivityFeed(enabled);
|
||||
const { followedCountries, isLoading: countryFollowsLoading } = useCountryFollows();
|
||||
const countryFeed = useFollowedCountriesFeed(followedCountries, enabled);
|
||||
const hasFollowedCountries = followedCountries.length > 0;
|
||||
const { hashtags: followedHashtags, isLoading: hashtagFollowsLoading } = useInterests('t');
|
||||
const hashtagFeed = useFollowedHashtagsFeed(followedHashtags, hashtagKinds, enabled);
|
||||
const hasFollowedHashtags = followedHashtags.length > 0;
|
||||
|
||||
const data = useMemo(() => {
|
||||
const networkItems = (networkFeed.data?.pages as unknown as { items: FeedItem[] }[] | undefined)
|
||||
@@ -124,9 +201,14 @@ export function useFollowingFeed(enabled = true) {
|
||||
.flatMap((page) => page.events)
|
||||
.map((event): FeedItem => ({ event, sortTimestamp: event.created_at }));
|
||||
|
||||
const hashtagItems = (hashtagFeed.data?.pages ?? [])
|
||||
.flatMap((page) => page.events)
|
||||
.map((event): FeedItem => ({ event, sortTimestamp: event.created_at }));
|
||||
|
||||
// Recency floor: prevent an older event from a sparse source (e.g. a
|
||||
// community or country with little recent activity) from out-ranking a
|
||||
// newer item that simply hasn't loaded into the network feed yet.
|
||||
// community/country/hashtag with little recent activity) from
|
||||
// out-ranking a newer item that simply hasn't loaded into the network
|
||||
// feed yet.
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
const networkOldest = networkItems.length > 0
|
||||
? Math.min(...networkItems.map((item) => item.sortTimestamp))
|
||||
@@ -137,9 +219,9 @@ export function useFollowingFeed(enabled = true) {
|
||||
: windowFloor;
|
||||
|
||||
// Network items pass through untouched — they define their own
|
||||
// recency floor. Community and country items are filtered to drop
|
||||
// anything older than the floor.
|
||||
const trimmedExternal = [...communityItems, ...countryItems]
|
||||
// recency floor. Community, country, and hashtag items are filtered
|
||||
// to drop anything older than the floor.
|
||||
const trimmedExternal = [...communityItems, ...countryItems, ...hashtagItems]
|
||||
.filter((item) => item.sortTimestamp >= recencyFloor);
|
||||
|
||||
const merged = [...networkItems, ...trimmedExternal];
|
||||
@@ -160,7 +242,7 @@ export function useFollowingFeed(enabled = true) {
|
||||
);
|
||||
|
||||
return { pages: [{ items: sorted }] };
|
||||
}, [networkFeed.data?.pages, communityFeed.data, countryFeed.data?.pages]);
|
||||
}, [networkFeed.data?.pages, communityFeed.data, countryFeed.data?.pages, hashtagFeed.data?.pages]);
|
||||
|
||||
const networkHasNextPage = networkFeed.hasNextPage;
|
||||
const networkFetchNextPage = networkFeed.fetchNextPage;
|
||||
@@ -168,12 +250,15 @@ export function useFollowingFeed(enabled = true) {
|
||||
const communityFetchNextPage = communityFeed.fetchNextPage;
|
||||
const countryHasNextPage = countryFeed.hasNextPage;
|
||||
const countryFetchNextPage = countryFeed.fetchNextPage;
|
||||
const hashtagHasNextPage = hashtagFeed.hasNextPage;
|
||||
const hashtagFetchNextPage = hashtagFeed.fetchNextPage;
|
||||
|
||||
const fetchNextPage = useCallback(async () => {
|
||||
await Promise.all([
|
||||
networkHasNextPage ? networkFetchNextPage() : Promise.resolve(),
|
||||
communityHasNextPage ? communityFetchNextPage() : Promise.resolve(),
|
||||
countryHasNextPage ? countryFetchNextPage() : Promise.resolve(),
|
||||
hashtagHasNextPage ? hashtagFetchNextPage() : Promise.resolve(),
|
||||
]);
|
||||
}, [
|
||||
networkHasNextPage,
|
||||
@@ -182,6 +267,8 @@ export function useFollowingFeed(enabled = true) {
|
||||
communityFetchNextPage,
|
||||
countryHasNextPage,
|
||||
countryFetchNextPage,
|
||||
hashtagHasNextPage,
|
||||
hashtagFetchNextPage,
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -191,19 +278,25 @@ export function useFollowingFeed(enabled = true) {
|
||||
|| communityFeed.isLoading
|
||||
|| countryFollowsLoading
|
||||
|| (hasFollowedCountries && countryFeed.isPending)
|
||||
|| hashtagFollowsLoading
|
||||
|| (hasFollowedHashtags && hashtagFeed.isPending)
|
||||
),
|
||||
isLoading: enabled && (
|
||||
networkFeed.isLoading
|
||||
|| communityFeed.isLoading
|
||||
|| countryFollowsLoading
|
||||
|| (hasFollowedCountries && countryFeed.isLoading)
|
||||
|| hashtagFollowsLoading
|
||||
|| (hasFollowedHashtags && hashtagFeed.isLoading)
|
||||
),
|
||||
fetchNextPage,
|
||||
hasNextPage: !!networkFeed.hasNextPage
|
||||
|| !!communityFeed.hasNextPage
|
||||
|| !!countryFeed.hasNextPage,
|
||||
|| !!countryFeed.hasNextPage
|
||||
|| !!hashtagFeed.hasNextPage,
|
||||
isFetchingNextPage: networkFeed.isFetchingNextPage
|
||||
|| communityFeed.isFetchingNextPage
|
||||
|| countryFeed.isFetchingNextPage,
|
||||
|| countryFeed.isFetchingNextPage
|
||||
|| hashtagFeed.isFetchingNextPage,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ export function useInterests(tagName: InterestTagName = 't') {
|
||||
|
||||
const invalidate = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['interests', user?.pubkey] });
|
||||
// The Following feed pulls from `t`/`g`/`i` interests, so any mutation
|
||||
// here can change what shows up there.
|
||||
queryClient.invalidateQueries({ queryKey: ['following-hashtag-feed'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['following-country-feed'] });
|
||||
};
|
||||
|
||||
/** Add an interest. */
|
||||
|
||||
Reference in New Issue
Block a user