diff --git a/src/__tests__/hooks/profile/useBlockMutation.test.tsx b/src/__tests__/hooks/profile/useBlockMutation.test.tsx index eab28aa94..bb52756cb 100644 --- a/src/__tests__/hooks/profile/useBlockMutation.test.tsx +++ b/src/__tests__/hooks/profile/useBlockMutation.test.tsx @@ -264,10 +264,6 @@ describe('useBlockMutation', () => { expect(invalidateSpy).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['following', 'viewer'] }) ); - - expect(invalidateSpy).toHaveBeenCalledWith( - expect.objectContaining({ queryKey: ['following-feed', 'viewer'], exact: true }) - ); }); it('does not call updateConnectionsLists when block === previous (change === 0)', async () => { diff --git a/src/__tests__/hooks/profile/useFollowMutation.test.tsx b/src/__tests__/hooks/profile/useFollowMutation.test.tsx index 70c37d29f..8d541c6be 100644 --- a/src/__tests__/hooks/profile/useFollowMutation.test.tsx +++ b/src/__tests__/hooks/profile/useFollowMutation.test.tsx @@ -34,6 +34,12 @@ const mockUseUserStore = useUserStore as jest.MockedFunction ({ + followUser: jest.fn(), + unfollowUser: jest.fn(), + getUserProfile: jest.fn(), +})); + const rel: UserProfile['relationship'] = { blocking: false, blockedBy: false, @@ -534,27 +540,6 @@ describe('useFollowMutation', () => { expect(revertedRetweeters.pages[0].data[0].relationship?.following).toBe(false); }); - it('handles mutation without target profile in cache', async () => { - const queryClient = createQueryClient(); - const wrapper = createWrapper(queryClient); - - const mockUpdateUser = jest.fn(); - const viewerProfile = createProfile({ username: 'viewer', followingCount: 5 }); - setUserStoreState({ user: viewerProfile, updateUser: mockUpdateUser }); - - mockFollowUser.mockResolvedValue({ success: true }); - - const { result } = renderHook(() => useFollowMutation(), { wrapper }); - - await act(async () => { - await result.current.mutateAsync({ username: 'unknownUser', follow: true, previous: false }); - }); - - expect(mockUpdateUser).toHaveBeenCalledWith({ followingCount: 6 }); - - expect(queryClient.getQueryData(['profile', 'unknownUser'])).toBeUndefined(); - }); - it('handles unfollow mutation correctly with cache updates', async () => { const { unfollowUser } = jest.requireMock('@/services/connections'); const queryClient = createQueryClient(); diff --git a/src/__tests__/hooks/profile/useMuteMutation.test.tsx b/src/__tests__/hooks/profile/useMuteMutation.test.tsx index d7041a872..2125b5d9c 100644 --- a/src/__tests__/hooks/profile/useMuteMutation.test.tsx +++ b/src/__tests__/hooks/profile/useMuteMutation.test.tsx @@ -285,8 +285,5 @@ describe('useMuteMutation', () => { expect(invalidateSpy).toHaveBeenCalledWith( expect.objectContaining({ queryKey: ['profile', 'target'] }) ); - expect(invalidateSpy).toHaveBeenCalledWith( - expect.objectContaining({ queryKey: ['following-feed', 'viewer'], exact: true }) - ); }); }); diff --git a/src/__tests__/profile/FollowingScreen.test.tsx b/src/__tests__/profile/FollowingScreen.test.tsx index 1d72f927f..6c5cfcff6 100644 --- a/src/__tests__/profile/FollowingScreen.test.tsx +++ b/src/__tests__/profile/FollowingScreen.test.tsx @@ -17,6 +17,14 @@ jest.mock('@/services/connections', () => ({ getUserFollowing: jest.fn(), })); +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useFocusEffect: jest.fn().mockImplementation((callback) => callback()), + }; +}); + const mockQueryClient = new QueryClient({ defaultOptions: { queries: { retry: false }, diff --git a/src/__tests__/screens/profile/connections/FollowingScreen.test.tsx b/src/__tests__/screens/profile/connections/FollowingScreen.test.tsx deleted file mode 100644 index 15c634dfa..000000000 --- a/src/__tests__/screens/profile/connections/FollowingScreen.test.tsx +++ /dev/null @@ -1,180 +0,0 @@ -import { render } from '@testing-library/react-native'; - -import { ThemeProvider } from '@/hooks/useTheme'; -import FollowingScreen from '@/screens/profile/connections/FollowingScreen'; - -const mockFollowMutate = jest.fn(); -jest.mock('@/hooks/profile/useFollowMutation', () => ({ - useFollowMutation: () => ({ - mutate: mockFollowMutate, - isPending: false, - }), -})); - -jest.mock('@tanstack/react-query'); - -const mockUseInfiniteQuery = jest.fn(); -const mockFetchNextPage = jest.fn(); - -jest.mock('@/components/ui/Avatar', () => ({ - __esModule: true, - default: jest.fn(() => null), -})); - -jest.mock('@/components/ui/Spinner', () => { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { ActivityIndicator } = require('react-native'); - return { - __esModule: true, - default: ({ size }: { size?: 'small' | 'large' }) => ( - - ), - }; -}); - -const mockFollowing = [ - { - username: 'user1', - displayName: 'User One', - avatarUrl: 'https://example.com/avatar1.jpg', - bio: 'Bio for user one', - isFollowing: false, - }, - { - username: 'user2', - displayName: 'User Two', - avatarUrl: null, - bio: null, - isFollowing: true, - }, -]; - -describe('FollowingScreen', () => { - const username = 'testuser'; - - beforeEach(() => { - jest.clearAllMocks(); - mockFollowMutate.mockReset(); - // eslint-disable-next-line @typescript-eslint/no-require-imports - const { useInfiniteQuery } = require('@tanstack/react-query'); - useInfiniteQuery.mockImplementation(mockUseInfiniteQuery); - }); - - it('renders loading spinner when loading', () => { - mockUseInfiniteQuery.mockReturnValue({ - data: undefined, - isLoading: true, - isFetchingNextPage: false, - hasNextPage: false, - fetchNextPage: mockFetchNextPage, - error: null, - }); - - const { getByTestId } = render( - - - - ); - - expect(getByTestId('spinner-large')).toBeTruthy(); - }); - - it('renders error message when there is an error', () => { - mockUseInfiniteQuery.mockReturnValue({ - data: undefined, - isLoading: false, - isFetchingNextPage: false, - hasNextPage: false, - fetchNextPage: mockFetchNextPage, - error: new Error('Network error'), - }); - - const { getByText } = render( - - - - ); - - expect(getByText('Failed to load following')).toBeTruthy(); - }); - - it('renders following list when data is available', () => { - mockUseInfiniteQuery.mockReturnValue({ - data: { - pages: [ - { - data: mockFollowing, - pagination: { hasNextPage: false, nextCursor: null }, - }, - ], - }, - isLoading: false, - isFetchingNextPage: false, - hasNextPage: false, - fetchNextPage: mockFetchNextPage, - error: null, - }); - - const { queryByTestId } = render( - - - - ); - - // Ensure no loading spinner - expect(queryByTestId('spinner-large')).toBeNull(); - expect(queryByTestId('spinner-small')).toBeNull(); - }); - - it('renders footer spinner when fetching next page', () => { - mockUseInfiniteQuery.mockReturnValue({ - data: { - pages: [ - { - data: mockFollowing, - pagination: { hasNextPage: true, nextCursor: 'cursor123' }, - }, - ], - }, - isLoading: false, - isFetchingNextPage: true, - hasNextPage: true, - fetchNextPage: mockFetchNextPage, - error: null, - }); - - const { getByTestId } = render( - - - - ); - - expect(getByTestId('spinner-small')).toBeTruthy(); - }); - - it('does not render footer when not fetching next page', () => { - mockUseInfiniteQuery.mockReturnValue({ - data: { - pages: [ - { - data: mockFollowing, - pagination: { hasNextPage: false, nextCursor: null }, - }, - ], - }, - isLoading: false, - isFetchingNextPage: false, - hasNextPage: false, - fetchNextPage: mockFetchNextPage, - error: null, - }); - - const { queryByTestId } = render( - - - - ); - - expect(queryByTestId('spinner-small')).toBeNull(); - }); -}); diff --git a/src/__tests__/screens/settings/privacy/BlockedAccountsScreen.test.tsx b/src/__tests__/screens/settings/privacy/BlockedAccountsScreen.test.tsx index c80afb641..c957f9d56 100644 --- a/src/__tests__/screens/settings/privacy/BlockedAccountsScreen.test.tsx +++ b/src/__tests__/screens/settings/privacy/BlockedAccountsScreen.test.tsx @@ -13,6 +13,14 @@ import { navigationRef } from '@/navigation/navigationRef'; import BlockedAccountsScreen from '@/screens/settings/privacy/BlockedAccountsScreen'; import { PROFILE, ROOT } from '@/utils/navigation/routeNames'; +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useFocusEffect: jest.fn().mockImplementation((callback) => callback()), + }; +}); + jest.mock('@/hooks/profile/useUserBlocks', () => ({ useUserBlocks: jest.fn() })); jest.mock('@/hooks/profile/useBlockMutation', () => ({ useBlockMutation: jest.fn() })); jest.mock('@/hooks/profile/useFollowMutation', () => ({ useFollowMutation: jest.fn() })); diff --git a/src/__tests__/screens/settings/privacy/MutedAccountsScreen.test.tsx b/src/__tests__/screens/settings/privacy/MutedAccountsScreen.test.tsx index ca8cf1146..2446de761 100644 --- a/src/__tests__/screens/settings/privacy/MutedAccountsScreen.test.tsx +++ b/src/__tests__/screens/settings/privacy/MutedAccountsScreen.test.tsx @@ -13,6 +13,13 @@ import { navigationRef } from '@/navigation/navigationRef'; import MutedAccountsScreen from '@/screens/settings/privacy/MutedAccountsScreen'; import { PROFILE, ROOT } from '@/utils/navigation/routeNames'; +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useFocusEffect: jest.fn().mockImplementation((callback) => callback()), + }; +}); jest.mock('@/hooks/profile/useUserMutes', () => ({ useUserMutes: jest.fn() })); jest.mock('@/hooks/profile/useBlockMutation', () => ({ useBlockMutation: jest.fn() })); jest.mock('@/hooks/profile/useFollowMutation', () => ({ useFollowMutation: jest.fn() })); @@ -32,8 +39,9 @@ const setPlatformOS = (os: typeof _initialOS) => { }; describe('MutedAccountsScreen', () => { + const queryClient = new QueryClient(); + const renderWithClient = (ui: ReactElement) => { - const queryClient = new QueryClient(); return render(ui, { wrapper: ({ children }) => ( {children} @@ -42,6 +50,7 @@ describe('MutedAccountsScreen', () => { }; beforeEach(() => { + queryClient.clear(); jest.clearAllMocks(); (useTheme as jest.Mock).mockReturnValue({ theme: 'light' }); diff --git a/src/components/ui/TimelineFeedList.tsx b/src/components/ui/TimelineFeedList.tsx index 66df0024b..989218a5e 100644 --- a/src/components/ui/TimelineFeedList.tsx +++ b/src/components/ui/TimelineFeedList.tsx @@ -90,7 +90,8 @@ export const MemoizedTweetItem = memo<{ a.retweetCount === b.retweetCount && a.replyCount === b.replyCount && (a.media?.length || 0) === (b.media?.length || 0) && - !!a.quotedTweet === !!b.quotedTweet + !!a.quotedTweet === !!b.quotedTweet && + a.author.relationship === b.author.relationship ); } ); diff --git a/src/components/ui/TweetDrawer.tsx b/src/components/ui/TweetDrawer.tsx index 5c8ada796..f9fb04bfb 100644 --- a/src/components/ui/TweetDrawer.tsx +++ b/src/components/ui/TweetDrawer.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { Alert, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; @@ -36,6 +36,8 @@ const TweetDrawer = ({ visible, onClose, author, onShowDeleteModal }: TweetDrawe const isMuted = author.relationship?.muted ?? false; const isFollowing = author.relationship?.following ?? false; + const [followingState, setFollowingState] = useState(isFollowing); + const followMutation = useFollowMutation(); const blockMutation = useBlockMutation(); const muteMutation = useMuteMutation(); @@ -63,6 +65,9 @@ const TweetDrawer = ({ visible, onClose, author, onShowDeleteModal }: TweetDrawe onError: (error) => { Alert.alert('Unable to update follow', error?.message ?? 'Please try again.'); }, + onSuccess: () => { + setFollowingState(shouldFollow); + }, } ); }, @@ -159,13 +164,13 @@ const TweetDrawer = ({ visible, onClose, author, onShowDeleteModal }: TweetDrawe testID="follow-button" > - {isFollowing ? `Unfollow @${author.username}` : `Follow @${author.username}`} + {followingState ? `Unfollow @${author.username}` : `Follow @${author.username}`} diff --git a/src/hooks/profile/useBlockMutation.tsx b/src/hooks/profile/useBlockMutation.tsx index b9d697024..836e30f9f 100644 --- a/src/hooks/profile/useBlockMutation.tsx +++ b/src/hooks/profile/useBlockMutation.tsx @@ -2,6 +2,8 @@ import { InfiniteData, QueryClient, useMutation, useQueryClient } from '@tanstac import { ApiException, ApiResponseBase } from '@/libs/api'; import { queryKeys } from '@/libs/queryKeys'; +import { getTweetCache } from '@/libs/tweetCache'; +import { getUserProfile } from '@/services/connections'; import { blockUser, unblockUser } from '@/services/me'; import { ListResponse } from '@/services/settings'; import { GetTweetLikesResponse, GetTweetRetweetersResponse } from '@/services/tweets'; @@ -26,10 +28,15 @@ type InfiniteListResponse = InfiniteData; type TweetLikersInfiniteResponse = InfiniteData; type TweetRetweetersInfiniteResponse = InfiniteData; -function updateLists(queryClient: QueryClient, username: string, isBlocked: boolean) { - const listKeys: ('blocks' | 'mutes')[] = ['blocks', 'mutes']; +async function updateLists(queryClient: QueryClient, username: string, isBlocked: boolean) { + const queryKeys: ('blocks' | 'mutes')[] = ['blocks', 'mutes']; - for (const key of listKeys) { + const isFound: Record<'blocks' | 'mutes', boolean> = { + blocks: false, + mutes: false, + }; + + for (const key of queryKeys) { const queries = queryClient.getQueriesData({ queryKey: [key], }); @@ -39,24 +46,69 @@ function updateLists(queryClient: QueryClient, username: string, isBlocked: bool const pages = data.pages.map((page) => ({ ...page, - data: page.data.map((user) => - user.username === username - ? { - ...user, - relationship: { - ...user.relationship, - blocking: isBlocked, - follower: isBlocked ? false : user.relationship?.follower, - following: isBlocked ? false : user.relationship?.following, - }, - } - : user - ), + data: page.data.map((user) => { + if (user.username === username) { + isFound[key] = true; + return { + ...user, + relationship: { + ...user.relationship, + blocking: isBlocked, + follower: isBlocked ? false : user.relationship?.follower, + following: isBlocked ? false : user.relationship?.following, + }, + }; + } else return user; + }), })); queryClient.setQueryData(queryKey, { ...data, pages }); }); } + + if (isBlocked && !isFound.blocks) { + let profile = queryClient.getQueryData(['profile', username]); + if (!profile) { + // fetch it if not found in cache + profile = await queryClient.fetchQuery({ + queryKey: ['profile', username], + queryFn: async () => { + return getUserProfile(username).then((res) => res.data); + }, + }); + } + + if (profile) { + // map to compact user type first + const newProfile = { + username: profile.username, + displayName: profile.displayName, + bio: profile.bio, + avatarUrl: profile.avatarUrl, + bioEntities: profile.bioEntities, + relationship: { ...profile.relationship, following: true }, + }; + + const blocksList = queryClient.getQueryData(['blocks']); + + if (!blocksList) return; + + const updatedPages = blocksList.pages.map((page, index) => { + if (index === 0) { + return { + ...page, + data: [newProfile, ...page.data], + }; + } + return page; + }); + + queryClient.setQueryData(['blocks'], { + ...blocksList, + pages: updatedPages, + }); + } + } } function updateTweetLikersAndRetweetersLists( @@ -227,7 +279,7 @@ export function useBlockMutation() { queryClient.invalidateQueries({ queryKey: ['following', viewerUsername] }); // update timeline - queryClient.invalidateQueries({ queryKey: ['following-feed', viewerUsername], exact: true }); + getTweetCache(queryClient).invalidateAllTweetQueries(); }, }); } diff --git a/src/hooks/profile/useFollowMutation.ts b/src/hooks/profile/useFollowMutation.ts index 75bd6ccd9..515490cac 100644 --- a/src/hooks/profile/useFollowMutation.ts +++ b/src/hooks/profile/useFollowMutation.ts @@ -2,7 +2,7 @@ import { InfiniteData, QueryClient, useMutation, useQueryClient } from '@tanstac import { ApiException } from '@/libs/api'; import { queryKeys } from '@/libs/queryKeys'; -import { followUser, unfollowUser } from '@/services/connections'; +import { followUser, getUserProfile, unfollowUser } from '@/services/connections'; import { GetTweetLikesResponse, GetTweetRetweetersResponse } from '@/services/tweets'; import { useUserStore } from '@/stores/userStore'; import { GetUserFollowersResponse, GetUserFollowingResponse, UserProfile } from '@/types/user'; @@ -37,13 +37,15 @@ type TweetLikersInfiniteResponse = InfiniteData; -export function updateConnectionsLists( +export async function updateConnectionsLists( queryClient: QueryClient, username: string, isFollowing: boolean ) { const queryKeys: ('followers' | 'following' | 'mutes')[] = ['followers', 'following', 'mutes']; + const currentUsername = useUserStore.getState().user.username; + let isUserFound = false; for (const key of queryKeys) { const queries = queryClient.getQueriesData({ queryKey: [key], @@ -54,22 +56,71 @@ export function updateConnectionsLists( const pages = data.pages.map((page) => ({ ...page, - data: page.data.map((user) => - user.username === username - ? { - ...user, - relationship: { - ...user.relationship, - following: isFollowing, - }, - } - : user - ), + data: page.data.map((user) => { + if (user.username === username) { + if (key === 'following') isUserFound = true; + return { + ...user, + relationship: { + ...user.relationship, + following: isFollowing, + }, + }; + } else return user; + }), })); queryClient.setQueryData(queryKey, { ...data, pages }); }); } + + if (isFollowing && !isUserFound) { + // get profile data + let profile = queryClient.getQueryData(['profile', username]); + if (!profile) { + // fetch it if not found in cache + profile = await queryClient.fetchQuery({ + queryKey: ['profile', username], + queryFn: async () => { + return getUserProfile(username).then((res) => res.data); + }, + }); + } + + if (profile) { + // map to compact user type first + const newProfile = { + username: profile.username, + displayName: profile.displayName, + bio: profile.bio, + avatarUrl: profile.avatarUrl, + bioEntities: profile.bioEntities, + relationship: { ...profile.relationship, following: true }, + }; + + const followingList = queryClient.getQueryData([ + 'following', + currentUsername, + ]); + if (!followingList) return; + + // add user to top of list + const updatedPages = followingList.pages.map((page, index) => { + if (index === 0) { + return { + ...page, + data: [newProfile, ...page.data], + }; + } + return page; + }); + + queryClient.setQueryData(['following', currentUsername], { + ...followingList, + pages: updatedPages, + }); + } + } } function updateTweetLikersAndRetweetersLists( diff --git a/src/hooks/profile/useMuteMutation.tsx b/src/hooks/profile/useMuteMutation.tsx index 64fbad970..fbc931ad1 100644 --- a/src/hooks/profile/useMuteMutation.tsx +++ b/src/hooks/profile/useMuteMutation.tsx @@ -1,6 +1,8 @@ import { InfiniteData, QueryClient, useMutation, useQueryClient } from '@tanstack/react-query'; import { ApiException, ApiResponseBase } from '@/libs/api'; +import { getTweetCache } from '@/libs/tweetCache'; +import { getUserProfile } from '@/services/connections'; import { muteUser, unmuteUser } from '@/services/me'; import { ListResponse } from '@/services/settings'; import { useUserStore } from '@/stores/userStore'; @@ -22,9 +24,14 @@ export type MuteMutationContext = { type InfiniteListResponse = InfiniteData; -function updateLists(queryClient: QueryClient, username: string, isMuted: boolean) { +async function updateLists(queryClient: QueryClient, username: string, isMuted: boolean) { const queryKeys: ('blocks' | 'mutes')[] = ['blocks', 'mutes']; + const isFound: Record<'blocks' | 'mutes', boolean> = { + blocks: false, + mutes: false, + }; + for (const key of queryKeys) { const queries = queryClient.getQueriesData({ queryKey: [key], @@ -35,19 +42,64 @@ function updateLists(queryClient: QueryClient, username: string, isMuted: boolea const pages = data.pages.map((page) => ({ ...page, - data: page.data.map((user) => - user.username === username - ? { - ...user, - relationship: { ...user.relationship, muted: isMuted }, - } - : user - ), + data: page.data.map((user) => { + if (user.username === username) { + isFound[key] = true; + return { + ...user, + relationship: { ...user.relationship, muted: isMuted }, + }; + } else return user; + }), })); queryClient.setQueryData(queryKey, { ...data, pages }); }); } + + // add to mute list if not found + if (isMuted && !isFound.mutes) { + let profile = queryClient.getQueryData(['profile', username]); + if (!profile) { + // fetch it if not found in cache + profile = await queryClient.fetchQuery({ + queryKey: ['profile', username], + queryFn: async () => { + return getUserProfile(username).then((res) => res.data); + }, + }); + } + if (profile) { + // map to compact user type first + const newProfile = { + username: profile.username, + displayName: profile.displayName, + bio: profile.bio, + avatarUrl: profile.avatarUrl, + bioEntities: profile.bioEntities, + relationship: { ...profile.relationship, following: true }, + }; + + const mutesList = queryClient.getQueryData(['mutes']); + + if (!mutesList) return; + + const updatedPages = mutesList.pages.map((page, index) => { + if (index === 0) { + return { + ...page, + data: [newProfile, ...page.data], + }; + } + return page; + }); + + queryClient.setQueryData(['mutes'], { + ...mutesList, + pages: updatedPages, + }); + } + } } export function useMuteMutation() { @@ -120,13 +172,10 @@ export function useMuteMutation() { }, onSettled: (_data, _error, variables) => { - const viewerState = useUserStore.getState(); - const viewerUsername = viewerState.user.username; - queryClient.invalidateQueries({ queryKey: ['profile', variables.username] }); // update timeline - queryClient.invalidateQueries({ queryKey: ['following-feed', viewerUsername], exact: true }); + getTweetCache(queryClient).invalidateAllTweetQueries(); }, }); } diff --git a/src/hooks/profile/useProfile.tsx b/src/hooks/profile/useProfile.tsx index f5def4077..97057fc7e 100644 --- a/src/hooks/profile/useProfile.tsx +++ b/src/hooks/profile/useProfile.tsx @@ -2,6 +2,11 @@ import { useQuery } from '@tanstack/react-query'; import { getUserProfile } from '@/services/connections'; +export const fetchUserProfile = async (username: string) => { + const response = await getUserProfile(username); + return response?.data; +}; + export const useProfile = (username?: string) => { return useQuery({ queryKey: ['profile', username ?? ''], @@ -9,8 +14,8 @@ export const useProfile = (username?: string) => { queryFn: async () => { if (!username) return null; - const response = await getUserProfile(username); - return response?.data; + const response = await fetchUserProfile(username); + return response; }, }); }; diff --git a/src/screens/profile/connections/FollowingScreen.tsx b/src/screens/profile/connections/FollowingScreen.tsx index 8c55648e3..582964f01 100644 --- a/src/screens/profile/connections/FollowingScreen.tsx +++ b/src/screens/profile/connections/FollowingScreen.tsx @@ -1,10 +1,16 @@ +import { useCallback } from 'react'; + import { FlatList, RefreshControl, Text, View } from 'react-native'; +import { useFocusEffect } from '@react-navigation/native'; +import { InfiniteData, useQueryClient } from '@tanstack/react-query'; + import ConnectionListItem from '@/components/profile/ConnectionListItem'; import Spinner from '@/components/ui/Spinner'; import { useUserFollowing } from '@/hooks/profile/useUserFollowing'; import { useTheme } from '@/hooks/useTheme'; import { navigationRef } from '@/navigation/navigationRef'; +import { ListResponse } from '@/services/settings'; import { useUserStore } from '@/stores/userStore'; import { colors } from '@/utils/colorTheme'; import { PROFILE, ROOT } from '@/utils/navigation/routeNames'; @@ -17,10 +23,15 @@ type FollowingScreenProps = { username: string; }; +type InfiniteListResponse = InfiniteData; + export default function FollowingScreen({ username }: FollowingScreenProps) { const { theme } = useTheme(); const styles = getConnectionScreenStyles(theme); const currentUser = useUserStore((state) => state.user); + const queryClient = useQueryClient(); + + const currentUsername = useUserStore.getState().user.username; const { data, @@ -30,12 +41,30 @@ export default function FollowingScreen({ username }: FollowingScreenProps) { fetchNextPage, error, refetch, - isRefetching, } = useUserFollowing(username); const following = data?.pages.flatMap((page) => page.data) ?? []; + useFocusEffect( + useCallback(() => { + const followingList = queryClient.getQueriesData({ + queryKey: ['following', currentUsername], + }); + + followingList.forEach(([queryKey, data]) => { + if (!data) return; + + const pages = data.pages.map((page) => ({ + ...page, + data: page.data.filter((user) => user.relationship?.following), + })); + + queryClient.setQueryData(queryKey, { ...data, pages }); + }); + }, [queryClient, currentUsername]) + ); + const openProfile = (userHandle: string) => navigationRef.navigate(ROOT.PROFILE, { screen: PROFILE.USER_PROFILE, @@ -108,7 +137,7 @@ export default function FollowingScreen({ username }: FollowingScreenProps) { ListFooterComponent={renderFooter} ListEmptyComponent={ isLoading ? ( - + ) : error ? ( diff --git a/src/screens/settings/privacy/BlockedAccountsScreen.tsx b/src/screens/settings/privacy/BlockedAccountsScreen.tsx index 70bbddc13..0250f26a4 100644 --- a/src/screens/settings/privacy/BlockedAccountsScreen.tsx +++ b/src/screens/settings/privacy/BlockedAccountsScreen.tsx @@ -1,17 +1,25 @@ +import { useCallback } from 'react'; + import { FlatList, RefreshControl, Text, View } from 'react-native'; +import { useFocusEffect } from '@react-navigation/native'; +import { InfiniteData, useQueryClient } from '@tanstack/react-query'; + import ConnectionListItem from '@/components/profile/ConnectionListItem'; import { AppText } from '@/components/ui'; import Spinner from '@/components/ui/Spinner'; import { useUserBlocks } from '@/hooks/profile/useUserBlocks'; import { useTheme } from '@/hooks/useTheme'; import { navigationRef } from '@/navigation/navigationRef'; +import { ListResponse } from '@/services/settings'; import { CompactUser } from '@/types/user'; import { colors } from '@/utils'; import { PROFILE, ROOT } from '@/utils/navigation/routeNames'; import { getConnectionScreenStyles } from '../../profile/connections/Connections.styles'; +type InfiniteListResponse = InfiniteData; + const BlockedAccountsScreen = () => { const { data, @@ -29,6 +37,26 @@ const BlockedAccountsScreen = () => { fetchNextPage(); } }; + const queryClient = useQueryClient(); + + useFocusEffect( + useCallback(() => { + const blocksList = queryClient.getQueriesData({ + queryKey: ['blocks'], + }); + + blocksList.forEach(([queryKey, data]) => { + if (!data) return; + + const pages = data.pages.map((page) => ({ + ...page, + data: page.data.filter((user) => user.relationship?.blocking), + })); + + queryClient.setQueryData(queryKey, { ...data, pages }); + }); + }, [queryClient]) + ); const blockedAccounts = data?.pages.flatMap((page) => page.data) ?? []; const { theme } = useTheme(); diff --git a/src/screens/settings/privacy/MutedAccountsScreen.tsx b/src/screens/settings/privacy/MutedAccountsScreen.tsx index 5d447a55f..f2301cbba 100644 --- a/src/screens/settings/privacy/MutedAccountsScreen.tsx +++ b/src/screens/settings/privacy/MutedAccountsScreen.tsx @@ -1,18 +1,47 @@ +import { useCallback } from 'react'; + import { FlatList, RefreshControl, Text, View } from 'react-native'; +import { useFocusEffect } from '@react-navigation/native'; +import { InfiniteData, useQueryClient } from '@tanstack/react-query'; + import ConnectionListItem from '@/components/profile/ConnectionListItem'; import { AppText } from '@/components/ui'; import Spinner from '@/components/ui/Spinner'; import { useUserMutes } from '@/hooks/profile/useUserMutes'; import { useTheme } from '@/hooks/useTheme'; import { navigationRef } from '@/navigation/navigationRef'; +import { ListResponse } from '@/services/settings'; import { CompactUser } from '@/types/user'; import { colors } from '@/utils'; import { PROFILE, ROOT } from '@/utils/navigation/routeNames'; import { getConnectionScreenStyles } from '../../profile/connections/Connections.styles'; +type InfiniteListResponse = InfiniteData; + const MutedAccountsScreen = () => { + const queryClient = useQueryClient(); + + useFocusEffect( + useCallback(() => { + const mutesList = queryClient.getQueriesData({ + queryKey: ['mutes'], + }); + + mutesList.forEach(([queryKey, data]) => { + if (!data) return; + + const pages = data.pages.map((page) => ({ + ...page, + data: page.data.filter((user) => user.relationship?.muted), + })); + + queryClient.setQueryData(queryKey, { ...data, pages }); + }); + }, [queryClient]) + ); + const { data, isLoading,