From 6f2e0749b665cfc3f2721ad7ecf2f17c877b2e46 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 28 Jun 2023 14:50:46 -0400 Subject: [PATCH 1/4] Refactor 'usePendingGroups' into new api hooks --- .../api/hooks/groups/usePendingGroups.ts | 30 +++++++++++ app/soapbox/api/hooks/index.ts | 1 + .../group/components/group-action-button.tsx | 12 ++--- .../groups/components/pending-groups-row.tsx | 2 +- .../features/groups/pending-requests.tsx | 2 +- app/soapbox/queries/groups.ts | 54 +------------------ 6 files changed, 40 insertions(+), 61 deletions(-) create mode 100644 app/soapbox/api/hooks/groups/usePendingGroups.ts diff --git a/app/soapbox/api/hooks/groups/usePendingGroups.ts b/app/soapbox/api/hooks/groups/usePendingGroups.ts new file mode 100644 index 000000000..b8cfb0789 --- /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 as string, '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/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 index a28f8cf87..9715e0614 100644 --- a/app/soapbox/queries/groups.ts +++ b/app/soapbox/queries/groups.ts @@ -1,11 +1,9 @@ -import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import { useQuery } from '@tanstack/react-query'; import { AxiosRequestConfig } from 'axios'; -import { getNextLink } from 'soapbox/api'; -import { useApi, useFeatures, useOwnAccount } from 'soapbox/hooks'; +import { useApi, useFeatures } 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, @@ -46,52 +44,6 @@ const useGroupsApi = () => { 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(); @@ -113,6 +65,4 @@ const useGroup = (id: string) => { export { useGroup, - usePendingGroups, - GroupKeys, }; From 5eef027ed05fdf7ea2211f756b52dd9d7d85267e Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 28 Jun 2023 14:54:59 -0400 Subject: [PATCH 2/4] Use new api hook for 'useGroup' --- app/soapbox/features/group/group-tags.tsx | 3 +- app/soapbox/queries/groups.ts | 68 ----------------------- 2 files changed, 1 insertion(+), 70 deletions(-) delete mode 100644 app/soapbox/queries/groups.ts 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/queries/groups.ts b/app/soapbox/queries/groups.ts deleted file mode 100644 index 9715e0614..000000000 --- a/app/soapbox/queries/groups.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { AxiosRequestConfig } from 'axios'; - -import { useApi, useFeatures } from 'soapbox/hooks'; -import { normalizeGroup, normalizeGroupRelationship } from 'soapbox/normalizers'; -import { Group, GroupRelationship } from 'soapbox/types/entities'; - -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 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, -}; From cadff9b4ab4445a0a3cad0a36f41cce6c4fca690 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 28 Jun 2023 15:04:08 -0400 Subject: [PATCH 3/4] Add test for 'usePendingGroups' --- .../groups/__tests__/usePendingGroups.test.ts | 47 +++++++++++++++++++ .../api/hooks/groups/usePendingGroups.ts | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 app/soapbox/api/hooks/groups/__tests__/usePendingGroups.test.ts 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..c30516962 --- /dev/null +++ b/app/soapbox/api/hooks/groups/__tests__/usePendingGroups.test.ts @@ -0,0 +1,47 @@ +import { __stub } from 'soapbox/api'; +import { buildGroup } from 'soapbox/jest/factory'; +import { renderHook, waitFor } from 'soapbox/jest/test-helpers'; +import { normalizeInstance } from 'soapbox/normalizers'; + +import { usePendingGroups } from '../usePendingGroups'; + +const group = buildGroup({ id: '1', display_name: 'soapbox' }); +const store = { + instance: normalizeInstance({ + version: '3.4.1 (compatible; TruthSocial 1.0.0+unreleased)', + }), +}; + +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 index b8cfb0789..f7b6b4cef 100644 --- a/app/soapbox/api/hooks/groups/usePendingGroups.ts +++ b/app/soapbox/api/hooks/groups/usePendingGroups.ts @@ -17,7 +17,7 @@ function usePendingGroups() { }), { schema: groupSchema, - enabled: !!account && features.groupsPending, + enabled: features.groupsPending, }, ); From 241ef58e88ec2543bcd419859eaabba016c4c837 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 29 Jun 2023 10:49:37 -0400 Subject: [PATCH 4/4] Add account check --- .../groups/__tests__/usePendingGroups.test.ts | 21 +++++++++++++++++-- .../api/hooks/groups/usePendingGroups.ts | 4 ++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/soapbox/api/hooks/groups/__tests__/usePendingGroups.test.ts b/app/soapbox/api/hooks/groups/__tests__/usePendingGroups.test.ts index c30516962..c5b85fe28 100644 --- a/app/soapbox/api/hooks/groups/__tests__/usePendingGroups.test.ts +++ b/app/soapbox/api/hooks/groups/__tests__/usePendingGroups.test.ts @@ -1,15 +1,32 @@ import { __stub } from 'soapbox/api'; -import { buildGroup } from 'soapbox/jest/factory'; +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 group = buildGroup({ id: '1', display_name: 'soapbox' }); +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', () => { diff --git a/app/soapbox/api/hooks/groups/usePendingGroups.ts b/app/soapbox/api/hooks/groups/usePendingGroups.ts index f7b6b4cef..f4ea16a43 100644 --- a/app/soapbox/api/hooks/groups/usePendingGroups.ts +++ b/app/soapbox/api/hooks/groups/usePendingGroups.ts @@ -9,7 +9,7 @@ function usePendingGroups() { const features = useFeatures(); const { entities, ...result } = useEntities( - [Entities.GROUPS, account?.id as string, 'pending'], + [Entities.GROUPS, account?.id!, 'pending'], () => api.get('/api/v1/groups', { params: { pending: true, @@ -17,7 +17,7 @@ function usePendingGroups() { }), { schema: groupSchema, - enabled: features.groupsPending, + enabled: !!account && features.groupsPending, }, );