diff --git a/app/soapbox/api/hooks/groups/__tests__/usePendingGroups.test.ts b/app/soapbox/api/hooks/groups/__tests__/usePendingGroups.test.ts new file mode 100644 index 000000000..c5b85fe28 --- /dev/null +++ b/app/soapbox/api/hooks/groups/__tests__/usePendingGroups.test.ts @@ -0,0 +1,64 @@ +import { __stub } from 'soapbox/api'; +import { Entities } from 'soapbox/entity-store/entities'; +import { buildAccount, buildGroup } from 'soapbox/jest/factory'; +import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; +import { normalizeInstance } from 'soapbox/normalizers'; + +import { usePendingGroups } from '../usePendingGroups'; + +const id = '1'; +const group = buildGroup({ id, display_name: 'soapbox' }); +const store = { + instance: normalizeInstance({ + version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)', + }), + me: '1', + entities: { + [Entities.ACCOUNTS]: { + store: { + [id]: buildAccount({ + id, + acct: 'tiger', + display_name: 'Tiger', + avatar: 'test.jpg', + verified: true, + }), + }, + lists: {}, + }, + }, +}; + +describe('usePendingGroups hook', () => { + describe('with a successful request', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/groups').reply(200, [group]); + }); + }); + + it('is successful', async () => { + const { result } = renderHook(usePendingGroups, undefined, store); + + await waitFor(() => expect(result.current.isFetching).toBe(false)); + + expect(result.current.groups).toHaveLength(1); + }); + }); + + describe('with an unsuccessful query', () => { + beforeEach(() => { + __stub((mock) => { + mock.onGet('/api/v1/groups').networkError(); + }); + }); + + it('is has error state', async() => { + const { result } = renderHook(usePendingGroups, undefined, store); + + await waitFor(() => expect(result.current.isFetching).toBe(false)); + + expect(result.current.groups).toHaveLength(0); + }); + }); +}); \ No newline at end of file diff --git a/app/soapbox/api/hooks/groups/usePendingGroups.ts b/app/soapbox/api/hooks/groups/usePendingGroups.ts new file mode 100644 index 000000000..f4ea16a43 --- /dev/null +++ b/app/soapbox/api/hooks/groups/usePendingGroups.ts @@ -0,0 +1,30 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { useApi, useFeatures, useOwnAccount } from 'soapbox/hooks'; +import { Group, groupSchema } from 'soapbox/schemas'; + +function usePendingGroups() { + const api = useApi(); + const { account } = useOwnAccount(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, account?.id!, 'pending'], + () => api.get('/api/v1/groups', { + params: { + pending: true, + }, + }), + { + schema: groupSchema, + enabled: !!account && features.groupsPending, + }, + ); + + return { + ...result, + groups: entities, + }; +} + +export { usePendingGroups }; \ No newline at end of file diff --git a/app/soapbox/api/hooks/index.ts b/app/soapbox/api/hooks/index.ts index 096c8a065..e51a7d06c 100644 --- a/app/soapbox/api/hooks/index.ts +++ b/app/soapbox/api/hooks/index.ts @@ -35,6 +35,7 @@ export { useGroupsFromTag } from './groups/useGroupsFromTag'; export { useJoinGroup } from './groups/useJoinGroup'; export { useMuteGroup } from './groups/useMuteGroup'; export { useLeaveGroup } from './groups/useLeaveGroup'; +export { usePendingGroups } from './groups/usePendingGroups'; export { usePopularGroups } from './groups/usePopularGroups'; export { usePopularTags } from './groups/usePopularTags'; export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; diff --git a/app/soapbox/features/group/components/group-action-button.tsx b/app/soapbox/features/group/components/group-action-button.tsx index a5c335909..9111a2cde 100644 --- a/app/soapbox/features/group/components/group-action-button.tsx +++ b/app/soapbox/features/group/components/group-action-button.tsx @@ -3,13 +3,11 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { fetchGroupRelationshipsSuccess } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; -import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/api/hooks'; +import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup, usePendingGroups } from 'soapbox/api/hooks'; import { Button } from 'soapbox/components/ui'; import { importEntities } from 'soapbox/entity-store/actions'; import { Entities } from 'soapbox/entity-store/entities'; -import { useAppDispatch, useOwnAccount } from 'soapbox/hooks'; -import { queryClient } from 'soapbox/queries/client'; -import { GroupKeys } from 'soapbox/queries/groups'; +import { useAppDispatch } from 'soapbox/hooks'; import { GroupRoles } from 'soapbox/schemas/group-member'; import toast from 'soapbox/toast'; @@ -31,11 +29,11 @@ const messages = defineMessages({ const GroupActionButton = ({ group }: IGroupActionButton) => { const dispatch = useAppDispatch(); const intl = useIntl(); - const { account } = useOwnAccount(); const joinGroup = useJoinGroup(group); const leaveGroup = useLeaveGroup(group); const cancelRequest = useCancelMembershipRequest(group); + const { invalidate: invalidatePendingGroups } = usePendingGroups(); const isRequested = group.relationship?.requested; const isNonMember = !group.relationship?.member && !isRequested; @@ -46,8 +44,8 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { const onJoinGroup = () => joinGroup.mutate({}, { onSuccess(entity) { joinGroup.invalidate(); + invalidatePendingGroups(); dispatch(fetchGroupRelationshipsSuccess([entity])); - queryClient.invalidateQueries(GroupKeys.pendingGroups(account?.id as string)); toast.success( group.locked @@ -84,7 +82,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { requested: false, }; dispatch(importEntities([entity], Entities.GROUP_RELATIONSHIPS)); - queryClient.invalidateQueries(GroupKeys.pendingGroups(account?.id as string)); + invalidatePendingGroups(); }, }); diff --git a/app/soapbox/features/group/group-tags.tsx b/app/soapbox/features/group/group-tags.tsx index d5335e844..5ff48b2e0 100644 --- a/app/soapbox/features/group/group-tags.tsx +++ b/app/soapbox/features/group/group-tags.tsx @@ -1,10 +1,9 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; -import { useGroupTags } from 'soapbox/api/hooks'; +import { useGroup, useGroupTags } from 'soapbox/api/hooks'; import ScrollableList from 'soapbox/components/scrollable-list'; import { Icon, Stack, Text } from 'soapbox/components/ui'; -import { useGroup } from 'soapbox/queries/groups'; import PlaceholderAccount from '../placeholder/components/placeholder-account'; diff --git a/app/soapbox/features/groups/components/pending-groups-row.tsx b/app/soapbox/features/groups/components/pending-groups-row.tsx index 4d2760760..101da43bc 100644 --- a/app/soapbox/features/groups/components/pending-groups-row.tsx +++ b/app/soapbox/features/groups/components/pending-groups-row.tsx @@ -1,9 +1,9 @@ import React from 'react'; +import { usePendingGroups } from 'soapbox/api/hooks'; import { PendingItemsRow } from 'soapbox/components/pending-items-row'; import { Divider } from 'soapbox/components/ui'; import { useFeatures } from 'soapbox/hooks'; -import { usePendingGroups } from 'soapbox/queries/groups'; export default () => { const features = useFeatures(); diff --git a/app/soapbox/features/groups/pending-requests.tsx b/app/soapbox/features/groups/pending-requests.tsx index 1233ff3a7..34a5a56e0 100644 --- a/app/soapbox/features/groups/pending-requests.tsx +++ b/app/soapbox/features/groups/pending-requests.tsx @@ -2,10 +2,10 @@ import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { Link } from 'react-router-dom'; +import { usePendingGroups } from 'soapbox/api/hooks'; import GroupCard from 'soapbox/components/group-card'; import ScrollableList from 'soapbox/components/scrollable-list'; import { Column, Stack, Text } from 'soapbox/components/ui'; -import { usePendingGroups } from 'soapbox/queries/groups'; import PlaceholderGroupCard from '../placeholder/components/placeholder-group-card'; diff --git a/app/soapbox/queries/groups.ts b/app/soapbox/queries/groups.ts deleted file mode 100644 index a28f8cf87..000000000 --- a/app/soapbox/queries/groups.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; -import { AxiosRequestConfig } from 'axios'; - -import { getNextLink } from 'soapbox/api'; -import { useApi, useFeatures, useOwnAccount } from 'soapbox/hooks'; -import { normalizeGroup, normalizeGroupRelationship } from 'soapbox/normalizers'; -import { Group, GroupRelationship } from 'soapbox/types/entities'; -import { flattenPages, PaginatedResult } from 'soapbox/utils/queries'; - -const GroupKeys = { - group: (id: string) => ['groups', 'group', id] as const, - pendingGroups: (userId: string) => ['groups', userId, 'pending'] as const, -}; - -const useGroupsApi = () => { - const api = useApi(); - - const getGroupRelationships = async (ids: string[]) => { - const queryString = ids.map((id) => `id[]=${id}`).join('&'); - const { data } = await api.get(`/api/v1/groups/relationships?${queryString}`); - - return data; - }; - - const fetchGroups = async (endpoint: string, params: AxiosRequestConfig['params'] = {}) => { - const response = await api.get(endpoint, { - params, - }); - const groups = [response.data].flat(); - const relationships = await getGroupRelationships(groups.map((group) => group.id)); - const result = groups.map((group) => { - const relationship = relationships.find((relationship) => relationship.id === group.id); - - return normalizeGroup({ - ...group, - relationship: relationship ? normalizeGroupRelationship(relationship) : null, - }); - }); - - return { - response, - groups: result, - }; - }; - - return { fetchGroups }; -}; - -const usePendingGroups = () => { - const features = useFeatures(); - const { account } = useOwnAccount(); - const { fetchGroups } = useGroupsApi(); - - const getGroups = async (pageParam?: any): Promise> => { - const endpoint = '/api/v1/groups'; - const nextPageLink = pageParam?.link; - const uri = nextPageLink || endpoint; - const { response, groups } = await fetchGroups(uri, { - pending: true, - }); - - const link = getNextLink(response); - const hasMore = !!link; - - return { - result: groups, - hasMore, - link, - }; - }; - - const queryInfo = useInfiniteQuery( - GroupKeys.pendingGroups(account?.id as string), - ({ pageParam }: any) => getGroups(pageParam), - { - enabled: !!account && features.groupsPending, - keepPreviousData: true, - getNextPageParam: (config) => { - if (config?.hasMore) { - return { nextLink: config?.link }; - } - - return undefined; - }, - }); - - const data = flattenPages(queryInfo.data); - - return { - ...queryInfo, - groups: data || [], - }; -}; - -const useGroup = (id: string) => { - const features = useFeatures(); - const { fetchGroups } = useGroupsApi(); - - const getGroup = async () => { - const { groups } = await fetchGroups(`/api/v1/groups/${id}`); - return groups[0]; - }; - - const queryInfo = useQuery(GroupKeys.group(id), getGroup, { - enabled: features.groups && !!id, - }); - - return { - ...queryInfo, - group: queryInfo.data, - }; -}; - -export { - useGroup, - usePendingGroups, - GroupKeys, -};