diff --git a/app/soapbox/components/ui/button/useButtonStyles.ts b/app/soapbox/components/ui/button/useButtonStyles.ts index a5423855a..1b61fc7b4 100644 --- a/app/soapbox/components/ui/button/useButtonStyles.ts +++ b/app/soapbox/components/ui/button/useButtonStyles.ts @@ -39,7 +39,7 @@ const useButtonStyles = ({ size, }: IButtonStyles) => { const buttonStyle = clsx({ - 'inline-flex items-center border font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 appearance-none transition-all': true, + 'inline-flex items-center place-content-center border font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 appearance-none transition-all': true, 'select-none disabled:opacity-75 disabled:cursor-default': disabled, [`${themes[theme]}`]: true, [`${sizes[size]}`]: true, diff --git a/app/soapbox/features/group/components/group-action-button.tsx b/app/soapbox/features/group/components/group-action-button.tsx index 8c8a36db7..c697bc4ee 100644 --- a/app/soapbox/features/group/components/group-action-button.tsx +++ b/app/soapbox/features/group/components/group-action-button.tsx @@ -40,6 +40,8 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { const onJoinGroup = () => joinGroup.mutate({}, { onSuccess() { + joinGroup.invalidate(); + toast.success( group.locked ? intl.formatMessage(messages.joinRequestSuccess) @@ -53,8 +55,9 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { heading: intl.formatMessage(messages.confirmationHeading), message: intl.formatMessage(messages.confirmationMessage), confirm: intl.formatMessage(messages.confirmationConfirm), - onConfirm: () => leaveGroup.mutate({}, { + onConfirm: () => leaveGroup.mutate(group.relationship?.id as string, { onSuccess() { + leaveGroup.invalidate(); toast.success(intl.formatMessage(messages.leaveSuccess)); }, }), diff --git a/app/soapbox/features/group/components/group-relationship.tsx b/app/soapbox/features/group/components/group-relationship.tsx index 6b79ecda5..c71adbbe8 100644 --- a/app/soapbox/features/group/components/group-relationship.tsx +++ b/app/soapbox/features/group/components/group-relationship.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { FormattedMessage } from 'react-intl'; import { HStack, Icon, Text } from 'soapbox/components/ui'; +import { GroupRoles } from 'soapbox/schemas/group-member'; import { Group } from 'soapbox/types/entities'; interface IGroupRelationship { @@ -9,10 +10,10 @@ interface IGroupRelationship { } const GroupRelationship = ({ group }: IGroupRelationship) => { - const isAdmin = group.relationship?.role === 'admin'; - const isModerator = group.relationship?.role === 'moderator'; + const isOwner = group.relationship?.role === GroupRoles.OWNER; + const isAdmin = group.relationship?.role === GroupRoles.ADMIN; - if (!isAdmin || !isModerator) { + if (!isOwner || !isAdmin) { return null; } @@ -21,14 +22,14 @@ const GroupRelationship = ({ group }: IGroupRelationship) => { - {isAdmin + {isOwner ? : } diff --git a/app/soapbox/features/groups/components/discover/group-grid-item.tsx b/app/soapbox/features/groups/components/discover/group-grid-item.tsx index 72bf4dc75..c80a7159a 100644 --- a/app/soapbox/features/groups/components/discover/group-grid-item.tsx +++ b/app/soapbox/features/groups/components/discover/group-grid-item.tsx @@ -1,26 +1,22 @@ import React, { forwardRef } from 'react'; -import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; import GroupAvatar from 'soapbox/components/groups/group-avatar'; -import { Button, HStack, Stack, Text } from 'soapbox/components/ui'; +import { HStack, Stack, Text } from 'soapbox/components/ui'; +import GroupActionButton from 'soapbox/features/group/components/group-action-button'; import GroupMemberCount from 'soapbox/features/group/components/group-member-count'; import GroupPrivacy from 'soapbox/features/group/components/group-privacy'; -import { useJoinGroup } from 'soapbox/hooks/api'; -import { Group as GroupEntity } from 'soapbox/types/entities'; + +import type { Group } from 'soapbox/schemas'; interface IGroup { - group: GroupEntity + group: Group width?: number } const GroupGridItem = forwardRef((props: IGroup, ref: React.ForwardedRef) => { const { group, width = 'auto' } = props; - const joinGroup = useJoinGroup(group); - - const onJoinGroup = () => joinGroup.mutate(group); - return (
- +
); }); diff --git a/app/soapbox/features/groups/components/discover/group-list-item.tsx b/app/soapbox/features/groups/components/discover/group-list-item.tsx index 55190b85e..cb492503f 100644 --- a/app/soapbox/features/groups/components/discover/group-list-item.tsx +++ b/app/soapbox/features/groups/components/discover/group-list-item.tsx @@ -3,8 +3,8 @@ import { FormattedMessage } from 'react-intl'; import { Link } from 'react-router-dom'; import GroupAvatar from 'soapbox/components/groups/group-avatar'; -import { Button, HStack, Icon, Stack, Text } from 'soapbox/components/ui'; -import { useJoinGroup } from 'soapbox/hooks/api'; +import { HStack, Icon, Stack, Text } from 'soapbox/components/ui'; +import GroupActionButton from 'soapbox/features/group/components/group-action-button'; import { Group as GroupEntity } from 'soapbox/types/entities'; import { shortNumberFormat } from 'soapbox/utils/numbers'; @@ -16,10 +16,6 @@ interface IGroup { const GroupListItem = (props: IGroup) => { const { group, withJoinAction = true } = props; - const joinGroup = useJoinGroup(group); - - const onJoinGroup = () => joinGroup.mutate(group); - return ( { {withJoinAction && ( - + )} ); diff --git a/app/soapbox/features/groups/components/discover/search/results.tsx b/app/soapbox/features/groups/components/discover/search/results.tsx index 72bca24ad..14e1e5a67 100644 --- a/app/soapbox/features/groups/components/discover/search/results.tsx +++ b/app/soapbox/features/groups/components/discover/search/results.tsx @@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl'; import { Components, Virtuoso, VirtuosoGrid } from 'react-virtuoso'; import { HStack, Icon, Stack, Text } from 'soapbox/components/ui'; -import { useGroupSearch } from 'soapbox/queries/groups/search'; +import { useGroupSearch } from 'soapbox/hooks/api'; import { Group } from 'soapbox/types/entities'; import GroupGridItem from '../group-grid-item'; diff --git a/app/soapbox/features/groups/components/discover/search/search.tsx b/app/soapbox/features/groups/components/discover/search/search.tsx index ef8605435..4e3308353 100644 --- a/app/soapbox/features/groups/components/discover/search/search.tsx +++ b/app/soapbox/features/groups/components/discover/search/search.tsx @@ -4,7 +4,7 @@ import { FormattedMessage } from 'react-intl'; import { Stack } from 'soapbox/components/ui'; import PlaceholderGroupSearch from 'soapbox/features/placeholder/components/placeholder-group-search'; import { useDebounce, useOwnAccount } from 'soapbox/hooks'; -import { useGroupSearch } from 'soapbox/queries/groups/search'; +import { useGroupSearch } from 'soapbox/hooks/api'; import { saveGroupSearch } from 'soapbox/utils/groups'; import Blankslate from './blankslate'; diff --git a/app/soapbox/hooks/api/groups/useGroupSearch.ts b/app/soapbox/hooks/api/groups/useGroupSearch.ts new file mode 100644 index 000000000..17c10e90f --- /dev/null +++ b/app/soapbox/hooks/api/groups/useGroupSearch.ts @@ -0,0 +1,39 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntities } from 'soapbox/entity-store/hooks'; +import { groupSchema } from 'soapbox/schemas'; + +import { useApi } from '../../useApi'; +import { useFeatures } from '../../useFeatures'; + +import { useGroupRelationships } from './useGroups'; + +import type { Group } from 'soapbox/schemas'; + +function useGroupSearch(search: string) { + const api = useApi(); + const features = useFeatures(); + + const { entities, ...result } = useEntities( + [Entities.GROUPS, 'discover', 'search', search], + () => api.get('/api/v1/groups/search', { + params: { + q: search, + }, + }), + { enabled: features.groupsDiscovery && !!search, schema: groupSchema }, + ); + + const { relationships } = useGroupRelationships(entities.map(entity => entity.id)); + + const groups = entities.map((group) => ({ + ...group, + relationship: relationships[group.id] || null, + })); + + return { + ...result, + groups, + }; +} + +export { useGroupSearch }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/groups/useJoinGroup.ts b/app/soapbox/hooks/api/groups/useJoinGroup.ts index f30a9c25b..46cd1d5bd 100644 --- a/app/soapbox/hooks/api/groups/useJoinGroup.ts +++ b/app/soapbox/hooks/api/groups/useJoinGroup.ts @@ -2,9 +2,13 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntityActions } from 'soapbox/entity-store/hooks'; import { groupRelationshipSchema } from 'soapbox/schemas'; +import { useGroups } from './useGroups'; + import type { Group, GroupRelationship } from 'soapbox/schemas'; function useJoinGroup(group: Group) { + const { invalidate } = useGroups(); + const { createEntity, isLoading } = useEntityActions( [Entities.GROUP_RELATIONSHIPS, group.id], { post: `/api/v1/groups/${group.id}/join` }, @@ -14,6 +18,7 @@ function useJoinGroup(group: Group) { return { mutate: createEntity, isLoading, + invalidate, }; } diff --git a/app/soapbox/hooks/api/groups/useLeaveGroup.ts b/app/soapbox/hooks/api/groups/useLeaveGroup.ts index 1f6b99f8d..af78c6d35 100644 --- a/app/soapbox/hooks/api/groups/useLeaveGroup.ts +++ b/app/soapbox/hooks/api/groups/useLeaveGroup.ts @@ -1,8 +1,14 @@ import { Entities } from 'soapbox/entity-store/entities'; import { useEntityActions } from 'soapbox/entity-store/hooks'; -import { Group, GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas'; +import { groupRelationshipSchema } from 'soapbox/schemas'; + +import { useGroups } from './useGroups'; + +import type { Group, GroupRelationship } from 'soapbox/schemas'; function useLeaveGroup(group: Group) { + const { invalidate } = useGroups(); + const { createEntity, isLoading } = useEntityActions( [Entities.GROUP_RELATIONSHIPS, group.id], { post: `/api/v1/groups/${group.id}/leave` }, @@ -12,6 +18,7 @@ function useLeaveGroup(group: Group) { return { mutate: createEntity, isLoading, + invalidate, }; } diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts index d1c8ec09f..b70314633 100644 --- a/app/soapbox/hooks/api/index.ts +++ b/app/soapbox/hooks/api/index.ts @@ -6,6 +6,7 @@ export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest' export { useDeleteGroup } from './groups/useDeleteGroup'; export { useDemoteGroupMember } from './groups/useDemoteGroupMember'; export { useGroup, useGroups } from './groups/useGroups'; +export { useGroupSearch } from './groups/useGroupSearch'; export { useJoinGroup } from './groups/useJoinGroup'; export { useLeaveGroup } from './groups/useLeaveGroup'; export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; diff --git a/app/soapbox/normalizers/group-relationship.ts b/app/soapbox/normalizers/group-relationship.ts index c9326db91..4ac579e6d 100644 --- a/app/soapbox/normalizers/group-relationship.ts +++ b/app/soapbox/normalizers/group-relationship.ts @@ -8,13 +8,15 @@ import { fromJS, } from 'immutable'; +import { GroupRoles } from 'soapbox/schemas/group-member'; + export const GroupRelationshipRecord = ImmutableRecord({ id: '', blocked_by: false, member: false, notifying: null, requested: false, - role: null as 'admin' | 'moderator' | 'user' | null, + role: 'user' as GroupRoles, }); export const normalizeGroupRelationship = (relationship: Record) => { diff --git a/app/soapbox/queries/groups/members.ts b/app/soapbox/queries/groups/members.ts deleted file mode 100644 index 4d0a0bb66..000000000 --- a/app/soapbox/queries/groups/members.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; - -import { useApi } from 'soapbox/hooks'; -import { normalizeAccount } from 'soapbox/normalizers'; -import { GroupRoles } from 'soapbox/schemas/group-member'; - -const GroupMemberKeys = { - members: (id: string, role: string) => ['group', id, role] as const, -}; - -const useGroupMembers = (groupId: string, role: GroupRoles) => { - const api = useApi(); - - const getQuery = async () => { - const { data } = await api.get(`/api/v1/groups/${groupId}/memberships`, { - params: { - role, - }, - }); - - const result = data.map((member: any) => { - return { - ...member, - account: normalizeAccount(member.account), - }; - }); - - return result; - }; - - return useQuery( - GroupMemberKeys.members(groupId, role), - getQuery, - { - placeholderData: [], - }, - ); -}; - -export { useGroupMembers }; \ No newline at end of file diff --git a/app/soapbox/schemas/group-relationship.ts b/app/soapbox/schemas/group-relationship.ts index 9f6eaaf35..75584955e 100644 --- a/app/soapbox/schemas/group-relationship.ts +++ b/app/soapbox/schemas/group-relationship.ts @@ -1,10 +1,12 @@ import z from 'zod'; +import { GroupRoles } from './group-member'; + const groupRelationshipSchema = z.object({ id: z.string(), member: z.boolean().catch(false), requested: z.boolean().catch(false), - role: z.string().nullish().catch(null), + role: z.nativeEnum(GroupRoles).catch(GroupRoles.USER), blocked_by: z.boolean().catch(false), notifying: z.boolean().nullable().catch(null), });