diff --git a/.eslintrc.js b/.eslintrc.js index 164949e65..0ecb15a5b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -277,6 +277,7 @@ module.exports = { files: ['**/*.ts', '**/*.tsx'], rules: { 'no-undef': 'off', // https://stackoverflow.com/a/69155899 + 'space-before-function-paren': 'off', }, parser: '@typescript-eslint/parser', }, diff --git a/app/soapbox/components/ui/widget/widget.tsx b/app/soapbox/components/ui/widget/widget.tsx index 7b966a6ef..3bced193f 100644 --- a/app/soapbox/components/ui/widget/widget.tsx +++ b/app/soapbox/components/ui/widget/widget.tsx @@ -41,8 +41,8 @@ const Widget: React.FC = ({ action, }): JSX.Element => { return ( - - + + {action || (onActionClick && ( { if (!isLoading && suggestedProfiles.size === 0) return null; return ( - + diff --git a/app/soapbox/features/follow-recommendations/index.tsx b/app/soapbox/features/follow-recommendations/index.tsx index 7fda03c7a..221ca1491 100644 --- a/app/soapbox/features/follow-recommendations/index.tsx +++ b/app/soapbox/features/follow-recommendations/index.tsx @@ -10,7 +10,7 @@ import Column from 'soapbox/features/ui/components/column'; import { useAppDispatch, useAppSelector, useFeatures } from 'soapbox/hooks'; const messages = defineMessages({ - heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested profiles' }, + heading: { id: 'followRecommendations.heading', defaultMessage: 'Suggested Profiles' }, }); const FollowRecommendations: React.FC = () => { diff --git a/app/soapbox/features/onboarding/steps/bio-step.tsx b/app/soapbox/features/onboarding/steps/bio-step.tsx index 895fd08e8..31c7bc6c9 100644 --- a/app/soapbox/features/onboarding/steps/bio-step.tsx +++ b/app/soapbox/features/onboarding/steps/bio-step.tsx @@ -17,10 +17,6 @@ const BioStep = ({ onNext }: { onNext: () => void }) => { const [isSubmitting, setSubmitting] = React.useState(false); const [errors, setErrors] = React.useState([]); - const trimmedValue = value.trim(); - const isValid = trimmedValue.length > 0; - const isDisabled = !isValid; - const handleSubmit = () => { setSubmitting(true); @@ -79,7 +75,7 @@ const BioStep = ({ onNext }: { onNext: () => void }) => { block theme='primary' type='submit' - disabled={isDisabled || isSubmitting} + disabled={isSubmitting} onClick={handleSubmit} > {isSubmitting ? ( diff --git a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx index 8db08d1a4..a05202ff9 100644 --- a/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx +++ b/app/soapbox/features/onboarding/steps/suggested-accounts-step.tsx @@ -5,7 +5,7 @@ import { FormattedMessage } from 'react-intl'; import ScrollableList from 'soapbox/components/scrollable_list'; import { Button, Card, CardBody, Stack, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; -import useOnboardingSuggestions from 'soapbox/queries/suggestions'; +import { useOnboardingSuggestions } from 'soapbox/queries/suggestions'; const SuggestedAccountsStep = ({ onNext }: { onNext: () => void }) => { const { data, fetchNextPage, hasNextPage, isFetching } = useOnboardingSuggestions(); diff --git a/app/soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx b/app/soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx new file mode 100644 index 000000000..e96268cb2 --- /dev/null +++ b/app/soapbox/features/placeholder/components/placeholder-sidebar-suggestions.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + +import { HStack, Stack } from 'soapbox/components/ui'; + +import { randomIntFromInterval, generateText } from '../utils'; + +export default ({ limit }: { limit: number }) => { + const length = randomIntFromInterval(15, 3); + const acctLength = randomIntFromInterval(15, 3); + + return ( + <> + {new Array(limit).fill(undefined).map((_, idx) => ( + + +
+ + + +

{generateText(length)}

+

{generateText(acctLength)}

+
+ + ))} + + ); +}; \ No newline at end of file diff --git a/app/soapbox/features/ui/components/__tests__/who-to-follow-panel.test.tsx b/app/soapbox/features/ui/components/__tests__/who-to-follow-panel.test.tsx index 84a645514..01a65dc49 100644 --- a/app/soapbox/features/ui/components/__tests__/who-to-follow-panel.test.tsx +++ b/app/soapbox/features/ui/components/__tests__/who-to-follow-panel.test.tsx @@ -1,123 +1,201 @@ -import { Map as ImmutableMap, OrderedSet as ImmutableOrderedSet } from 'immutable'; import React from 'react'; -import { render, screen } from '../../../../jest/test-helpers'; -import { normalizeAccount } from '../../../../normalizers'; +import { __stub } from 'soapbox/api'; + +import { render, rootState, screen, waitFor } from '../../../../jest/test-helpers'; +import { normalizeInstance } from '../../../../normalizers'; import WhoToFollowPanel from '../who-to-follow-panel'; +const buildTruthSuggestion = (id: string) => ({ + account_avatar: 'avatar', + account_id: id, + acct: 'acct', + display_name: 'my name', + note: 'hello', + verified: true, +}); + +const buildSuggestion = (id: string) => ({ + source: 'staff', + account: { + username: 'username', + verified: true, + id, + acct: 'acct', + avatar: 'avatar', + avatar_static: 'avatar', + display_name: 'my name', + }, +}); + describe('', () => { - it('renders suggested accounts', () => { - const store = { - accounts: ImmutableMap({ - '1': normalizeAccount({ - id: '1', - acct: 'username', - display_name: 'My name', - avatar: 'test.jpg', - }), - }), - suggestions: { - items: ImmutableOrderedSet([{ - source: 'staff', - account: '1', - }]), - }, - }; - - render(, undefined, store); - expect(screen.getByTestId('account')).toHaveTextContent(/my name/i); - }); + let store: any; - it('renders multiple accounts', () => { - const store = { - accounts: ImmutableMap({ - '1': normalizeAccount({ - id: '1', - acct: 'username', - display_name: 'My name', - avatar: 'test.jpg', - }), - '2': normalizeAccount({ - id: '1', - acct: 'username2', - display_name: 'My other name', - avatar: 'test.jpg', - }), - }), - suggestions: { - items: ImmutableOrderedSet([ - { - source: 'staff', - account: '1', - }, - { - source: 'staff', - account: '2', - }, - ]), - }, - }; - - render(, undefined, store); - expect(screen.queryAllByTestId('account')).toHaveLength(2); - }); + describe('using Truth Social software', () => { + beforeEach(() => { + store = rootState + .set('me', '1234') + .set('instance', normalizeInstance({ + version: '3.4.1 (compatible; TruthSocial 1.0.0)', + })); + }); + + describe('with a single suggestion', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions') + .reply(200, [buildTruthSuggestion('1')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('renders suggested accounts', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.getByTestId('account')).toHaveTextContent(/my name/i); + }); + }); + }); + + describe('with a multiple suggestion', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions') + .reply(200, [buildTruthSuggestion('1'), buildTruthSuggestion('2')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('renders suggested accounts', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(2); + }); + }); + }); + + describe('with a set limit', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions') + .reply(200, [buildTruthSuggestion('1'), buildTruthSuggestion('2')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('respects the limit prop', async () => { + render(, undefined, store); - it('respects the limit prop', () => { - const store = { - accounts: ImmutableMap({ - '1': normalizeAccount({ - id: '1', - acct: 'username', - display_name: 'My name', - avatar: 'test.jpg', - }), - '2': normalizeAccount({ - id: '1', - acct: 'username2', - display_name: 'My other name', - avatar: 'test.jpg', - }), - }), - suggestions: { - items: ImmutableOrderedSet([ - { - source: 'staff', - account: '1', - }, - { - source: 'staff', - account: '2', - }, - ]), - }, - }; - - render(, undefined, store); - expect(screen.queryAllByTestId('account')).toHaveLength(1); + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(1); + }); + }); + }); + + describe('when the API returns an empty list', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/truth/carousels/suggestions') + .reply(200, [], { + link: '', + }); + }); + }); + + it('renders empty', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(0); + }); + }); + }); }); - it('renders empty', () => { - const store = { - accounts: ImmutableMap({ - '1': normalizeAccount({ - id: '1', - acct: 'username', - display_name: 'My name', - avatar: 'test.jpg', - }), - '2': normalizeAccount({ - id: '1', - acct: 'username2', - display_name: 'My other name', - avatar: 'test.jpg', - }), - }), - suggestions: { - items: ImmutableOrderedSet([]), - }, - }; - - render(, undefined, store); - expect(screen.queryAllByTestId('account')).toHaveLength(0); + describe('using Pleroma software', () => { + beforeEach(() => { + store = rootState.set('me', '1234'); + }); + + describe('with a single suggestion', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v2/suggestions') + .reply(200, [buildSuggestion('1')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('renders suggested accounts', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.getByTestId('account')).toHaveTextContent(/my name/i); + }); + }); + }); + + describe('with a multiple suggestion', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v2/suggestions') + .reply(200, [buildSuggestion('1'), buildSuggestion('2')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('renders suggested accounts', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(2); + }); + }); + }); + + describe('with a set limit', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v2/suggestions') + .reply(200, [buildSuggestion('1'), buildSuggestion('2')], { + link: '; rel=\'prev\'', + }); + }); + }); + + it('respects the limit prop', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(1); + }); + }); + }); + + describe('when the API returns an empty list', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v2/suggestions') + .reply(200, [], { + link: '', + }); + }); + }); + + it('renders empty', async () => { + render(, undefined, store); + + await waitFor(() => { + expect(screen.queryAllByTestId('account')).toHaveLength(0); + }); + }); + }); }); }); diff --git a/app/soapbox/features/ui/components/who-to-follow-panel.tsx b/app/soapbox/features/ui/components/who-to-follow-panel.tsx index 2458997f4..418d2143c 100644 --- a/app/soapbox/features/ui/components/who-to-follow-panel.tsx +++ b/app/soapbox/features/ui/components/who-to-follow-panel.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { useDispatch } from 'react-redux'; +import { Link } from 'react-router-dom'; -import { fetchSuggestions, dismissSuggestion } from 'soapbox/actions/suggestions'; -import { Widget } from 'soapbox/components/ui'; +import { Text, Widget } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; -import { useAppSelector } from 'soapbox/hooks'; +import PlaceholderSidebarSuggestions from 'soapbox/features/placeholder/components/placeholder-sidebar-suggestions'; +import { useDismissSuggestion, useSuggestions } from 'soapbox/queries/suggestions'; import type { Account as AccountEntity } from 'soapbox/types/entities'; @@ -18,44 +18,40 @@ interface IWhoToFollowPanel { } const WhoToFollowPanel = ({ limit }: IWhoToFollowPanel) => { - const dispatch = useDispatch(); const intl = useIntl(); - const suggestions = useAppSelector((state) => state.suggestions.items); + const { data: suggestions, isFetching } = useSuggestions(); + const dismissSuggestion = useDismissSuggestion(); + const suggestionsToRender = suggestions.slice(0, limit); const handleDismiss = (account: AccountEntity) => { - dispatch(dismissSuggestion(account.id)); + dismissSuggestion.mutate(account.id); }; - React.useEffect(() => { - dispatch(fetchSuggestions()); - }, []); - - if (suggestionsToRender.isEmpty()) { - return null; - } - - // FIXME: This page actually doesn't look good right now - // const handleAction = () => { - // history.push('/suggestions'); - // }; - return ( } - // onAction={handleAction} + action={ + + View all + + } > - {suggestionsToRender.map((suggestion) => ( - , but it isn't - id={suggestion.account} - actionIcon={require('@tabler/icons/x.svg')} - actionTitle={intl.formatMessage(messages.dismissSuggestion)} - onActionClick={handleDismiss} - /> - ))} + {isFetching ? ( + + ) : ( + suggestionsToRender.map((suggestion: any) => ( + , but it isn't + id={suggestion.account} + actionIcon={require('@tabler/icons/x.svg')} + actionTitle={intl.formatMessage(messages.dismissSuggestion)} + onActionClick={handleDismiss} + /> + )) + )} ); }; diff --git a/app/soapbox/pages/default_page.tsx b/app/soapbox/pages/default_page.tsx index b64cf452d..c013eb63d 100644 --- a/app/soapbox/pages/default_page.tsx +++ b/app/soapbox/pages/default_page.tsx @@ -41,7 +41,7 @@ const DefaultPage: React.FC = ({ children }) => { )} {features.suggestions && ( - {Component => } + {Component => } )} diff --git a/app/soapbox/pages/home_page.tsx b/app/soapbox/pages/home_page.tsx index 23bd3e3c7..dc8c2c9f4 100644 --- a/app/soapbox/pages/home_page.tsx +++ b/app/soapbox/pages/home_page.tsx @@ -105,7 +105,7 @@ const HomePage: React.FC = ({ children }) => { )} {features.suggestions && ( - {Component => } + {Component => } )} diff --git a/app/soapbox/pages/profile_page.tsx b/app/soapbox/pages/profile_page.tsx index f4ff974e0..9f57e23ab 100644 --- a/app/soapbox/pages/profile_page.tsx +++ b/app/soapbox/pages/profile_page.tsx @@ -139,7 +139,7 @@ const ProfilePage: React.FC = ({ params, children }) => { ) : features.suggestions && ( - {Component => } + {Component => } )} diff --git a/app/soapbox/pages/status_page.tsx b/app/soapbox/pages/status_page.tsx index 2c35947ad..414df6783 100644 --- a/app/soapbox/pages/status_page.tsx +++ b/app/soapbox/pages/status_page.tsx @@ -45,7 +45,7 @@ const StatusPage: React.FC = ({ children }) => { )} {features.suggestions && ( - {Component => } + {Component => } )} diff --git a/app/soapbox/queries/__tests__/suggestions.test.ts b/app/soapbox/queries/__tests__/suggestions.test.ts index f38bf0dbc..aa352abe9 100644 --- a/app/soapbox/queries/__tests__/suggestions.test.ts +++ b/app/soapbox/queries/__tests__/suggestions.test.ts @@ -1,7 +1,7 @@ import { __stub } from 'soapbox/api'; import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; -import useOnboardingSuggestions from '../suggestions'; +import { useOnboardingSuggestions } from '../suggestions'; describe('useCarouselAvatars', () => { describe('with a successful query', () => { @@ -17,7 +17,7 @@ describe('useCarouselAvatars', () => { }); }); - it('is successful', async() => { + it('is successful', async () => { const { result } = renderHook(() => useOnboardingSuggestions()); await waitFor(() => expect(result.current.isFetching).toBe(false)); @@ -33,7 +33,7 @@ describe('useCarouselAvatars', () => { }); }); - it('is successful', async() => { + it('is successful', async () => { const { result } = renderHook(() => useOnboardingSuggestions()); await waitFor(() => expect(result.current.isFetching).toBe(false)); diff --git a/app/soapbox/queries/chats.ts b/app/soapbox/queries/chats.ts index 4e6011ec1..66a1c1f0d 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -8,7 +8,7 @@ import compareId from 'soapbox/compare_id'; import { useChatContext } from 'soapbox/contexts/chat-context'; import { useApi, useAppDispatch, useFeatures } from 'soapbox/hooks'; import { normalizeChatMessage } from 'soapbox/normalizers'; -import { flattenPages, updatePageItem } from 'soapbox/utils/queries'; +import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries'; import { queryClient } from './client'; @@ -50,12 +50,6 @@ export interface IChatSilence { target_account_id: number } -export interface PaginatedResult { - result: T[], - hasMore: boolean, - link?: string, -} - const chatKeys = { chatMessages: (chatId: string) => ['chats', 'messages', chatId] as const, chatSearch: (searchQuery?: string) => ['chats', 'search', searchQuery] as const, @@ -67,7 +61,7 @@ const reverseOrder = (a: IChat, b: IChat): number => compareId(a.id, b.id); const useChatMessages = (chatId: string) => { const api = useApi(); - const getChatMessages = async(chatId: string, pageParam?: any): Promise> => { + const getChatMessages = async (chatId: string, pageParam?: any): Promise> => { const nextPageLink = pageParam?.link; const uri = nextPageLink || `/api/v1/pleroma/chats/${chatId}/messages`; const response = await api.get(uri); @@ -108,7 +102,7 @@ const useChats = (search?: string) => { const dispatch = useAppDispatch(); const features = useFeatures(); - const getChats = async(pageParam?: any): Promise> => { + const getChats = async (pageParam?: any): Promise> => { const endpoint = features.chatsV2 ? '/api/v2/pleroma/chats' : '/api/v1/pleroma/chats'; const nextPageLink = pageParam?.link; const uri = nextPageLink || endpoint; @@ -194,7 +188,7 @@ const useChat = (chatId: string) => { const useChatSilences = () => { const api = useApi(); - const getChatSilences = async() => { + const getChatSilences = async () => { const { data } = await api.get('/api/v1/pleroma/chats/silences'); return data; @@ -211,12 +205,12 @@ const useChatSilence = (chat: IChat | null) => { const [isSilenced, setSilenced] = useState(false); - const getChatSilences = async() => { + const getChatSilences = async () => { const { data } = await api.get(`api/v1/pleroma/chats/silence?account_id=${chat?.account.id}`); return data; }; - const fetchChatSilence = async() => { + const fetchChatSilence = async () => { const data = await getChatSilences(); if (data) { setSilenced(true); diff --git a/app/soapbox/queries/suggestions.ts b/app/soapbox/queries/suggestions.ts index 4ef39f147..96cab48ee 100644 --- a/app/soapbox/queries/suggestions.ts +++ b/app/soapbox/queries/suggestions.ts @@ -1,9 +1,12 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useMutation } from '@tanstack/react-query'; import { fetchRelationships } from 'soapbox/actions/accounts'; import { importFetchedAccounts } from 'soapbox/actions/importer'; +import { SuggestedProfile } from 'soapbox/actions/suggestions'; import { getLinks } from 'soapbox/api'; -import { useApi, useAppDispatch } from 'soapbox/hooks'; +import { useApi, useAppDispatch, useFeatures } from 'soapbox/hooks'; + +import { PaginatedResult, removePageItem } from '../utils/queries'; import type { IAccount } from './accounts'; @@ -12,11 +15,124 @@ type Suggestion = { account: IAccount } -export default function useOnboardingSuggestions() { +type TruthSuggestion = { + account_avatar: string + account_id: string + acct: string + display_name: string + note: string + verified: boolean +} + +type Result = TruthSuggestion | { + account: string +} + +type PageParam = { + link?: string +} + +const suggestionKeys = { + suggestions: ['suggestions'] as const, +}; + +const mapSuggestedProfileToAccount = (suggestedProfile: SuggestedProfile) => ({ + id: suggestedProfile.account_id, + avatar: suggestedProfile.account_avatar, + avatar_static: suggestedProfile.account_avatar, + acct: suggestedProfile.acct, + display_name: suggestedProfile.display_name, + note: suggestedProfile.note, + verified: suggestedProfile.verified, +}); + +const useSuggestions = () => { const api = useApi(); const dispatch = useAppDispatch(); + const features = useFeatures(); + + const getV2Suggestions = async (pageParam: PageParam): Promise> => { + const endpoint = pageParam?.link || '/api/v2/suggestions'; + const response = await api.get(endpoint); + const hasMore = !!response.headers.link; + const nextLink = getLinks(response).refs.find(link => link.rel === 'next')?.uri; + + const accounts = response.data.map(({ account }) => account); + const accountIds = accounts.map((account) => account.id); + dispatch(importFetchedAccounts(accounts)); + dispatch(fetchRelationships(accountIds)); + + return { + result: response.data.map(x => ({ ...x, account: x.account.id })), + link: nextLink, + hasMore, + }; + }; + + const getTruthSuggestions = async (pageParam: PageParam): Promise> => { + const endpoint = pageParam?.link || '/api/v1/truth/carousels/suggestions'; + const response = await api.get(endpoint); + const hasMore = !!response.headers.link; + const nextLink = getLinks(response).refs.find(link => link.rel === 'next')?.uri; + + const accounts = response.data.map(mapSuggestedProfileToAccount); + dispatch(importFetchedAccounts(accounts, { should_refetch: true })); - const getV2Suggestions = async(pageParam: any): Promise<{ data: Suggestion[], link: string | undefined, hasMore: boolean }> => { + return { + result: response.data.map((x) => ({ ...x, account: x.account_id })), + link: nextLink, + hasMore, + }; + }; + + const getSuggestions = (pageParam: PageParam) => { + if (features.truthSuggestions) { + return getTruthSuggestions(pageParam); + } else { + return getV2Suggestions(pageParam); + } + }; + + const result = useInfiniteQuery( + suggestionKeys.suggestions, + ({ pageParam }: any) => getSuggestions(pageParam), + { + keepPreviousData: true, + getNextPageParam: (config) => { + if (config?.hasMore) { + return { nextLink: config?.link }; + } + + return undefined; + }, + }); + + const data: any = result.data?.pages.reduce( + (prev: any, curr: any) => [...prev, ...curr.result], + [], + ); + + return { + ...result, + data: data || [], + }; +}; + +const useDismissSuggestion = () => { + const api = useApi(); + + return useMutation((accountId: string) => api.delete(`/api/v1/suggestions/${accountId}`), { + onMutate(accountId: string) { + removePageItem(suggestionKeys.suggestions, accountId, (o: any, n: any) => o.account_id === n); + }, + }); +}; + +function useOnboardingSuggestions() { + const api = useApi(); + const dispatch = useAppDispatch(); + + const getV2Suggestions = async (pageParam: any): Promise<{ data: Suggestion[], link: string | undefined, hasMore: boolean }> => { const link = pageParam?.link || '/api/v2/suggestions'; const response = await api.get(link); const hasMore = !!response.headers.link; @@ -55,3 +171,5 @@ export default function useOnboardingSuggestions() { data, }; } + +export { useOnboardingSuggestions, useSuggestions, useDismissSuggestion }; \ No newline at end of file diff --git a/app/soapbox/utils/phone.ts b/app/soapbox/utils/phone.ts index 9cc175f5d..8fde39f12 100644 --- a/app/soapbox/utils/phone.ts +++ b/app/soapbox/utils/phone.ts @@ -1,7 +1,9 @@ /** List of supported E164 country codes. */ const COUNTRY_CODES = [ '1', + '351', '44', + '55', ] as const; /** Supported E164 country code. */ diff --git a/app/soapbox/utils/queries.ts b/app/soapbox/utils/queries.ts index 1a9961b43..d066f0855 100644 --- a/app/soapbox/utils/queries.ts +++ b/app/soapbox/utils/queries.ts @@ -1,7 +1,12 @@ import { queryClient } from 'soapbox/queries/client'; import type { InfiniteData, QueryKey, UseInfiniteQueryResult } from '@tanstack/react-query'; -import type { PaginatedResult } from 'soapbox/queries/chats'; + +export interface PaginatedResult { + result: T[], + hasMore: boolean, + link?: string, +} /** Flatten paginated results into a single array. */ const flattenPages = (queryInfo: UseInfiniteQueryResult>) => { @@ -35,8 +40,22 @@ const appendPageItem = (queryKey: QueryKey, newItem: T) => { }); }; +/** Remove an item inside if found. */ +const removePageItem = (queryKey: QueryKey, itemToRemove: T, isItem: (item: T, newItem: T) => boolean) => { + queryClient.setQueriesData>>(queryKey, (data) => { + if (data) { + const pages = data.pages.map(page => { + const result = page.result.filter(item => !isItem(item, itemToRemove)); + return { ...page, result }; + }); + return { ...data, pages }; + } + }); +}; + export { flattenPages, updatePageItem, appendPageItem, -}; \ No newline at end of file + removePageItem, +};