From e42e0577f457934633a3d01628967eb5fbd66b3e Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 20 Mar 2023 13:27:22 -0400 Subject: [PATCH 1/9] Move Group mutations to entities --- .../entity-store/hooks/useEntityActions.ts | 8 +++ .../group/components/group-action-button.tsx | 38 ++++++++++--- .../components/discover/group-grid-item.tsx | 4 +- .../components/discover/group-list-item.tsx | 4 +- .../api/groups/useCancelMembershipRequest.ts | 21 ++++++++ app/soapbox/hooks/api/groups/useJoinGroup.ts | 20 +++++++ app/soapbox/hooks/api/groups/useLeaveGroup.ts | 18 +++++++ app/soapbox/hooks/api/index.ts | 5 +- app/soapbox/queries/groups.ts | 54 +------------------ 9 files changed, 106 insertions(+), 66 deletions(-) create mode 100644 app/soapbox/hooks/api/groups/useCancelMembershipRequest.ts create mode 100644 app/soapbox/hooks/api/groups/useJoinGroup.ts create mode 100644 app/soapbox/hooks/api/groups/useLeaveGroup.ts diff --git a/app/soapbox/entity-store/hooks/useEntityActions.ts b/app/soapbox/entity-store/hooks/useEntityActions.ts index eede5bcb3..2383c69c6 100644 --- a/app/soapbox/entity-store/hooks/useEntityActions.ts +++ b/app/soapbox/entity-store/hooks/useEntityActions.ts @@ -1,3 +1,4 @@ +import { useState } from 'react'; import { z } from 'zod'; import { useApi, useAppDispatch, useGetState } from 'soapbox/hooks'; @@ -42,9 +43,13 @@ function useEntityActions( const getState = useGetState(); const [entityType, listKey] = path; + const [isLoading, setIsLoading] = useState(false); + function createEntity(params: P, callbacks: EntityCallbacks = {}): Promise> { if (!endpoints.post) return Promise.reject(endpoints); + setIsLoading(true); + return api.post(endpoints.post, params).then((response) => { const schema = opts.schema || z.custom(); const entity = schema.parse(response.data); @@ -56,6 +61,8 @@ function useEntityActions( callbacks.onSuccess(entity); } + setIsLoading(false); + return { response, entity, @@ -89,6 +96,7 @@ function useEntityActions( return { createEntity: createEntity, deleteEntity: endpoints.delete ? deleteEntity : undefined, + isLoading, }; } diff --git a/app/soapbox/features/group/components/group-action-button.tsx b/app/soapbox/features/group/components/group-action-button.tsx index 64824866a..ba828c9c1 100644 --- a/app/soapbox/features/group/components/group-action-button.tsx +++ b/app/soapbox/features/group/components/group-action-button.tsx @@ -3,8 +3,11 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { openModal } from 'soapbox/actions/modals'; import { Button } from 'soapbox/components/ui'; +import { deleteEntities } from 'soapbox/entity-store/actions'; +import { Entities } from 'soapbox/entity-store/entities'; import { useAppDispatch } from 'soapbox/hooks'; -import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/queries/groups'; +import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/hooks/api'; +import toast from 'soapbox/toast'; import { Group } from 'soapbox/types/entities'; interface IGroupActionButton { @@ -12,35 +15,54 @@ interface IGroupActionButton { } const messages = defineMessages({ + confirmationConfirm: { id: 'confirmations.leave_group.confirm', defaultMessage: 'Leave' }, confirmationHeading: { id: 'confirmations.leave_group.heading', defaultMessage: 'Leave group' }, confirmationMessage: { id: 'confirmations.leave_group.message', defaultMessage: 'You are about to leave the group. Do you want to continue?' }, - confirmationConfirm: { id: 'confirmations.leave_group.confirm', defaultMessage: 'Leave' }, + joinRequestSuccess: { id: 'group.join.request_success', defaultMessage: 'Requested to join the group' }, + joinSuccess: { id: 'group.join.success', defaultMessage: 'Group joined successfully!' }, + leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' }, }); const GroupActionButton = ({ group }: IGroupActionButton) => { const dispatch = useAppDispatch(); const intl = useIntl(); - const joinGroup = useJoinGroup(); - const leaveGroup = useLeaveGroup(); - const cancelRequest = useCancelMembershipRequest(); + const joinGroup = useJoinGroup(group); + const leaveGroup = useLeaveGroup(group); + const cancelRequest = useCancelMembershipRequest(group); const isRequested = group.relationship?.requested; const isNonMember = !group.relationship?.member && !isRequested; const isAdmin = group.relationship?.role === 'owner'; const isBlocked = group.relationship?.blocked_by; - const onJoinGroup = () => joinGroup.mutate(group); + const onJoinGroup = () => joinGroup.mutate({}, { + onSuccess() { + toast.success( + group.locked + ? intl.formatMessage(messages.joinRequestSuccess) + : intl.formatMessage(messages.joinSuccess), + ); + }, + }); const onLeaveGroup = () => dispatch(openModal('CONFIRM', { heading: intl.formatMessage(messages.confirmationHeading), message: intl.formatMessage(messages.confirmationMessage), confirm: intl.formatMessage(messages.confirmationConfirm), - onConfirm: () => leaveGroup.mutate(group), + onConfirm: () => leaveGroup.mutate({}, { + onSuccess() { + toast.success(intl.formatMessage(messages.leaveSuccess)); + }, + }), })); - const onCancelRequest = () => cancelRequest.mutate(group); + const onCancelRequest = () => cancelRequest.mutate({}, { + onSuccess() { + dispatch(deleteEntities([group.id], Entities.GROUP_RELATIONSHIPS)); + }, + }); if (isBlocked) { return null; 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 9c743c44f..72bf4dc75 100644 --- a/app/soapbox/features/groups/components/discover/group-grid-item.tsx +++ b/app/soapbox/features/groups/components/discover/group-grid-item.tsx @@ -6,7 +6,7 @@ import GroupAvatar from 'soapbox/components/groups/group-avatar'; import { Button, HStack, Stack, Text } from 'soapbox/components/ui'; import GroupMemberCount from 'soapbox/features/group/components/group-member-count'; import GroupPrivacy from 'soapbox/features/group/components/group-privacy'; -import { useJoinGroup } from 'soapbox/queries/groups'; +import { useJoinGroup } from 'soapbox/hooks/api'; import { Group as GroupEntity } from 'soapbox/types/entities'; interface IGroup { @@ -17,7 +17,7 @@ interface IGroup { const GroupGridItem = forwardRef((props: IGroup, ref: React.ForwardedRef) => { const { group, width = 'auto' } = props; - const joinGroup = useJoinGroup(); + const joinGroup = useJoinGroup(group); const onJoinGroup = () => joinGroup.mutate(group); 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 8f2f5879c..55190b85e 100644 --- a/app/soapbox/features/groups/components/discover/group-list-item.tsx +++ b/app/soapbox/features/groups/components/discover/group-list-item.tsx @@ -4,7 +4,7 @@ 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/queries/groups'; +import { useJoinGroup } from 'soapbox/hooks/api'; import { Group as GroupEntity } from 'soapbox/types/entities'; import { shortNumberFormat } from 'soapbox/utils/numbers'; @@ -16,7 +16,7 @@ interface IGroup { const GroupListItem = (props: IGroup) => { const { group, withJoinAction = true } = props; - const joinGroup = useJoinGroup(); + const joinGroup = useJoinGroup(group); const onJoinGroup = () => joinGroup.mutate(group); diff --git a/app/soapbox/hooks/api/groups/useCancelMembershipRequest.ts b/app/soapbox/hooks/api/groups/useCancelMembershipRequest.ts new file mode 100644 index 000000000..3aad33f9c --- /dev/null +++ b/app/soapbox/hooks/api/groups/useCancelMembershipRequest.ts @@ -0,0 +1,21 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntityActions } from 'soapbox/entity-store/hooks'; +import { useOwnAccount } from 'soapbox/hooks'; + +import type { Group, GroupRelationship } from 'soapbox/schemas'; + +function useCancelMembershipRequest(group: Group) { + const me = useOwnAccount(); + + const { createEntity, isLoading } = useEntityActions( + [Entities.GROUP_RELATIONSHIPS, group.id], + { post: `/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject` }, + ); + + return { + mutate: createEntity, + isLoading, + }; +} + +export { useCancelMembershipRequest }; diff --git a/app/soapbox/hooks/api/groups/useJoinGroup.ts b/app/soapbox/hooks/api/groups/useJoinGroup.ts new file mode 100644 index 000000000..f30a9c25b --- /dev/null +++ b/app/soapbox/hooks/api/groups/useJoinGroup.ts @@ -0,0 +1,20 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntityActions } from 'soapbox/entity-store/hooks'; +import { groupRelationshipSchema } from 'soapbox/schemas'; + +import type { Group, GroupRelationship } from 'soapbox/schemas'; + +function useJoinGroup(group: Group) { + const { createEntity, isLoading } = useEntityActions( + [Entities.GROUP_RELATIONSHIPS, group.id], + { post: `/api/v1/groups/${group.id}/join` }, + { schema: groupRelationshipSchema }, + ); + + return { + mutate: createEntity, + isLoading, + }; +} + +export { useJoinGroup }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/groups/useLeaveGroup.ts b/app/soapbox/hooks/api/groups/useLeaveGroup.ts new file mode 100644 index 000000000..1f6b99f8d --- /dev/null +++ b/app/soapbox/hooks/api/groups/useLeaveGroup.ts @@ -0,0 +1,18 @@ +import { Entities } from 'soapbox/entity-store/entities'; +import { useEntityActions } from 'soapbox/entity-store/hooks'; +import { Group, GroupRelationship, groupRelationshipSchema } from 'soapbox/schemas'; + +function useLeaveGroup(group: Group) { + const { createEntity, isLoading } = useEntityActions( + [Entities.GROUP_RELATIONSHIPS, group.id], + { post: `/api/v1/groups/${group.id}/leave` }, + { schema: groupRelationshipSchema }, + ); + + return { + mutate: createEntity, + isLoading, + }; +} + +export { useLeaveGroup }; diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts index 144196a2c..3ec927873 100644 --- a/app/soapbox/hooks/api/index.ts +++ b/app/soapbox/hooks/api/index.ts @@ -2,5 +2,8 @@ * Groups */ export { useBlockGroupMember } from './groups/useBlockGroupMember'; +export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest'; export { useDemoteGroupMember } from './groups/useDemoteGroupMember'; -export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; \ No newline at end of file +export { useJoinGroup } from './groups/useJoinGroup'; +export { useLeaveGroup } from './groups/useLeaveGroup'; +export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; diff --git a/app/soapbox/queries/groups.ts b/app/soapbox/queries/groups.ts index 4ac826b4e..daf39806e 100644 --- a/app/soapbox/queries/groups.ts +++ b/app/soapbox/queries/groups.ts @@ -1,22 +1,12 @@ -import { useInfiniteQuery, useMutation, useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import { AxiosRequestConfig } from 'axios'; -import { defineMessages, useIntl } from 'react-intl'; import { getNextLink } from 'soapbox/api'; import { useApi, useFeatures, useOwnAccount } from 'soapbox/hooks'; import { normalizeGroup, normalizeGroupRelationship } from 'soapbox/normalizers'; -import toast from 'soapbox/toast'; import { Group, GroupRelationship } from 'soapbox/types/entities'; import { flattenPages, PaginatedResult } from 'soapbox/utils/queries'; -import { queryClient } from './client'; - -const messages = defineMessages({ - joinSuccess: { id: 'group.join.success', defaultMessage: 'Group joined successfully!' }, - joinRequestSuccess: { id: 'group.join.request_success', defaultMessage: 'Requested to join the group' }, - leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' }, -}); - const GroupKeys = { group: (id: string) => ['groups', 'group', id] as const, myGroups: (userId: string) => ['groups', userId] as const, @@ -168,50 +158,8 @@ const useGroup = (id: string) => { }; }; -const useJoinGroup = () => { - const api = useApi(); - const intl = useIntl(); - - return useMutation((group: Group) => api.post(`/api/v1/groups/${group.id}/join`), { - onSuccess(_response, group) { - queryClient.invalidateQueries(['groups']); - toast.success( - group.locked - ? intl.formatMessage(messages.joinRequestSuccess) - : intl.formatMessage(messages.joinSuccess), - ); - }, - }); -}; - -const useLeaveGroup = () => { - const api = useApi(); - const intl = useIntl(); - - return useMutation((group: Group) => api.post(`/api/v1/groups/${group.id}/leave`), { - onSuccess() { - queryClient.invalidateQueries({ queryKey: ['groups'] }); - toast.success(intl.formatMessage(messages.leaveSuccess)); - }, - }); -}; - -const useCancelMembershipRequest = () => { - const api = useApi(); - const me = useOwnAccount(); - - return useMutation((group: Group) => api.post(`/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject`), { - onSuccess() { - queryClient.invalidateQueries({ queryKey: ['groups'] }); - }, - }); -}; - export { - useCancelMembershipRequest, useGroup, useGroups, - useJoinGroup, - useLeaveGroup, usePendingGroups, }; From 7329c0bf258ae92eda6742efba3a50ec13e16c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Sun, 19 Mar 2023 20:32:53 +0100 Subject: [PATCH 2/9] Do not make requests to api/v1/groups if fffeature not available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/hooks/useGroups.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/soapbox/hooks/useGroups.ts b/app/soapbox/hooks/useGroups.ts index 865896e24..96f73dbe1 100644 --- a/app/soapbox/hooks/useGroups.ts +++ b/app/soapbox/hooks/useGroups.ts @@ -5,11 +5,15 @@ import { useEntities, useEntity } from 'soapbox/entity-store/hooks'; import { groupSchema, Group } from 'soapbox/schemas/group'; import { groupRelationshipSchema, GroupRelationship } from 'soapbox/schemas/group-relationship'; +import { useFeatures } from './useFeatures'; + function useGroups() { + const features = useFeatures(); + const { entities, ...result } = useEntities( [Entities.GROUPS, ''], '/api/v1/groups', - { schema: groupSchema }, + { enabled: features.groups, schema: groupSchema }, ); const { relationships } = useGroupRelationships(entities.map(entity => entity.id)); From fb8d543f7c5b73d58ace2c5d3bb02cf1bde99714 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 20 Mar 2023 20:57:49 -0500 Subject: [PATCH 3/9] Redirect group statuses to a custom path --- app/soapbox/features/status/index.tsx | 10 +++++++++- app/soapbox/features/ui/index.tsx | 1 + app/soapbox/selectors/index.ts | 16 +++++++++------- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/soapbox/features/status/index.tsx b/app/soapbox/features/status/index.tsx index bda274cd1..b51333883 100644 --- a/app/soapbox/features/status/index.tsx +++ b/app/soapbox/features/status/index.tsx @@ -114,7 +114,11 @@ export const getDescendantsIds = createSelector([ }); type DisplayMedia = 'default' | 'hide_all' | 'show_all'; -type RouteParams = { statusId: string }; + +type RouteParams = { + statusId: string + groupId?: string +}; interface IThread { params: RouteParams @@ -515,6 +519,10 @@ const Thread: React.FC = (props) => { children.push(...renderChildren(descendantsIds).toArray()); } + if (status.group && typeof status.group === 'object' && !props.params.groupId) { + return ; + } + return ( diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 89a156be4..0e8875c09 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -299,6 +299,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {features.groups && } {features.groups && } {features.groups && } + {features.groups && } diff --git a/app/soapbox/selectors/index.ts b/app/soapbox/selectors/index.ts index 12cb4df08..4679c1148 100644 --- a/app/soapbox/selectors/index.ts +++ b/app/soapbox/selectors/index.ts @@ -7,6 +7,7 @@ import { import { createSelector } from 'reselect'; import { getSettings } from 'soapbox/actions/settings'; +import { Entities } from 'soapbox/entity-store/entities'; import { getDomain } from 'soapbox/utils/accounts'; import { validId } from 'soapbox/utils/auth'; import ConfigDB from 'soapbox/utils/config-db'; @@ -14,9 +15,10 @@ import { getFeatures } from 'soapbox/utils/features'; import { shouldFilter } from 'soapbox/utils/timelines'; import type { ContextType } from 'soapbox/normalizers/filter'; +import type { ReducerAccount } from 'soapbox/reducers/accounts'; import type { ReducerChat } from 'soapbox/reducers/chats'; import type { RootState } from 'soapbox/store'; -import type { Filter as FilterEntity, Notification } from 'soapbox/types/entities'; +import type { Filter as FilterEntity, Notification, Status, Group } from 'soapbox/types/entities'; const normalizeId = (id: any): string => typeof id === 'string' ? id : ''; @@ -180,11 +182,11 @@ type APIStatus = { id: string, username?: string }; export const makeGetStatus = () => { return createSelector( [ - (state: RootState, { id }: APIStatus) => state.statuses.get(id), - (state: RootState, { id }: APIStatus) => state.statuses.get(state.statuses.get(id)?.reblog || ''), - (state: RootState, { id }: APIStatus) => state.accounts.get(state.statuses.get(id)?.account || ''), - (state: RootState, { id }: APIStatus) => state.accounts.get(state.statuses.get(state.statuses.get(id)?.reblog || '')?.account || ''), - (state: RootState, { id }: APIStatus) => state.groups.items.get(state.statuses.get(id)?.group || ''), + (state: RootState, { id }: APIStatus) => state.statuses.get(id) as Status | undefined, + (state: RootState, { id }: APIStatus) => state.statuses.get(state.statuses.get(id)?.reblog || '') as Status | undefined, + (state: RootState, { id }: APIStatus) => state.accounts.get(state.statuses.get(id)?.account || '') as ReducerAccount | undefined, + (state: RootState, { id }: APIStatus) => state.accounts.get(state.statuses.get(state.statuses.get(id)?.reblog || '')?.account || '') as ReducerAccount | undefined, + (state: RootState, { id }: APIStatus) => state.entities[Entities.GROUPS]?.store[state.statuses.get(id)?.group || ''] as Group | undefined, (_state: RootState, { username }: APIStatus) => username, getFilters, (state: RootState) => state.me, @@ -207,7 +209,7 @@ export const makeGetStatus = () => { statusReblog = undefined; } - return statusBase.withMutations(map => { + return statusBase.withMutations((map: Status) => { map.set('reblog', statusReblog || null); // @ts-ignore :( map.set('account', accountBase || null); From f369a7c76585b4a18695ce84e6830262e7ca29ca Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 20 Mar 2023 21:42:54 -0500 Subject: [PATCH 4/9] Use "danger" text for deleting account and group --- app/soapbox/features/group/manage-group.tsx | 4 ++-- app/soapbox/features/settings/index.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index e7dea7f20..384b2f888 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom'; import { deleteGroup, editGroup } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; import List, { ListItem } from 'soapbox/components/list'; -import { CardBody, Column, Spinner } from 'soapbox/components/ui'; +import { CardBody, Column, Spinner, Text } from 'soapbox/components/ui'; import { useAppDispatch, useGroup } from 'soapbox/hooks'; import ColumnForbidden from '../ui/components/column-forbidden'; @@ -78,7 +78,7 @@ const ManageGroup: React.FC = ({ params }) => { {group.relationship.role === 'owner' && ( - + {intl.formatMessage(messages.deleteGroup)}} onClick={onDeleteGroup} /> )} diff --git a/app/soapbox/features/settings/index.tsx b/app/soapbox/features/settings/index.tsx index e01ed848c..77bb22ef8 100644 --- a/app/soapbox/features/settings/index.tsx +++ b/app/soapbox/features/settings/index.tsx @@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom'; import { fetchMfa } from 'soapbox/actions/mfa'; import List, { ListItem } from 'soapbox/components/list'; -import { Card, CardBody, CardHeader, CardTitle, Column } from 'soapbox/components/ui'; +import { Card, CardBody, CardHeader, CardTitle, Column, Text } from 'soapbox/components/ui'; import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks'; import Preferences from '../preferences'; @@ -155,7 +155,7 @@ const Settings = () => { ))} {features.security && ( - + {intl.formatMessage(messages.deleteAccount)}} onClick={navigateToDeleteAccount} /> )} From cc3585f319c2381a49f7ca462205c9bbfc4f02c7 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 20 Mar 2023 21:52:44 -0500 Subject: [PATCH 5/9] Manage group: add headers --- app/soapbox/features/group/manage-group.tsx | 38 ++++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index 384b2f888..4fe027ee8 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom'; import { deleteGroup, editGroup } from 'soapbox/actions/groups'; import { openModal } from 'soapbox/actions/modals'; import List, { ListItem } from 'soapbox/components/list'; -import { CardBody, Column, Spinner, Text } from 'soapbox/components/ui'; +import { CardBody, CardHeader, CardTitle, Column, Spinner, Text } from 'soapbox/components/ui'; import { useAppDispatch, useGroup } from 'soapbox/hooks'; import ColumnForbidden from '../ui/components/column-forbidden'; @@ -21,6 +21,8 @@ const messages = defineMessages({ deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' }, deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete group' }, deleteMessage: { id: 'confirmations.delete_group.message', defaultMessage: 'Are you sure you want to delete this group? This is a permanent action that cannot be undone.' }, + members: { id: 'group.tabs.members', defaultMessage: 'Members' }, + other: { id: 'settings.other', defaultMessage: 'Other options' }, }); interface IManageGroup { @@ -66,20 +68,38 @@ const ManageGroup: React.FC = ({ params }) => { {group.relationship.role === 'owner' && ( - - - - - + <> + + + + + + + + + + )} + + + + + + {group.relationship.role === 'owner' && ( - - {intl.formatMessage(messages.deleteGroup)}} onClick={onDeleteGroup} /> - + <> + + + + + + {intl.formatMessage(messages.deleteGroup)}} onClick={onDeleteGroup} /> + + )} From f61e0d889af14e864cf27987a796592b4b96e7e8 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 20 Mar 2023 21:56:15 -0500 Subject: [PATCH 6/9] "Blocked members" --> "Banned members" --- app/soapbox/features/group/manage-group.tsx | 2 +- app/soapbox/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index 4fe027ee8..3f960fc51 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -16,7 +16,7 @@ const messages = defineMessages({ heading: { id: 'column.manage_group', defaultMessage: 'Manage group' }, editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit group' }, pendingRequests: { id: 'manage_group.pending_requests', defaultMessage: 'Pending requests' }, - blockedMembers: { id: 'manage_group.blocked_members', defaultMessage: 'Blocked members' }, + blockedMembers: { id: 'manage_group.blocked_members', defaultMessage: 'Banned members' }, deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete group' }, deleteConfirm: { id: 'confirmations.delete_group.confirm', defaultMessage: 'Delete' }, deleteHeading: { id: 'confirmations.delete_group.heading', defaultMessage: 'Delete group' }, diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 0c0b8aff0..1be8a0e20 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -927,7 +927,7 @@ "login_external.errors.instance_fail": "The instance returned an error.", "login_external.errors.network_fail": "Connection failed. Is a browser extension blocking it?", "login_form.header": "Sign In", - "manage_group.blocked_members": "Blocked members", + "manage_group.blocked_members": "Banned members", "manage_group.confirmation.copy": "Copy link", "manage_group.confirmation.info_1": "As the owner of this group, you can assign staff, delete posts and much more.", "manage_group.confirmation.info_2": "Post the group's first post and get the conversation started.", From 65070f651998c9857d5cdac2c5e4ca6a96c402d1 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Tue, 21 Mar 2023 16:34:15 -0500 Subject: [PATCH 7/9] Add MyGroupsPanel, improve layout on various group pages --- .../ui/components/panels/my-groups-panel.tsx | 33 +++++++++++++++++++ .../features/ui/util/async-components.ts | 4 +++ app/soapbox/pages/group-page.tsx | 4 +++ app/soapbox/pages/groups-page.tsx | 25 ++++++++++++-- 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 app/soapbox/features/ui/components/panels/my-groups-panel.tsx diff --git a/app/soapbox/features/ui/components/panels/my-groups-panel.tsx b/app/soapbox/features/ui/components/panels/my-groups-panel.tsx new file mode 100644 index 000000000..438d86de4 --- /dev/null +++ b/app/soapbox/features/ui/components/panels/my-groups-panel.tsx @@ -0,0 +1,33 @@ +import React from 'react'; + +import { Widget } from 'soapbox/components/ui'; +import GroupListItem from 'soapbox/features/groups/components/discover/group-list-item'; +import PlaceholderGroupSearch from 'soapbox/features/placeholder/components/placeholder-group-search'; +import { useGroups } from 'soapbox/hooks'; + +const MyGroupsPanel = () => { + const { groups, isFetching, isFetched, isError } = useGroups(); + const isEmpty = (isFetched && groups.length === 0) || isError; + + if (isEmpty) { + return null; + } + + return ( + + {isFetching ? ( + new Array(3).fill(0).map((_, idx) => ( + + )) + ) : ( + groups.slice(0, 3).map((group) => ( + + )) + )} + + ); +}; + +export default MyGroupsPanel; diff --git a/app/soapbox/features/ui/util/async-components.ts b/app/soapbox/features/ui/util/async-components.ts index e9b015d8c..63d2794e2 100644 --- a/app/soapbox/features/ui/util/async-components.ts +++ b/app/soapbox/features/ui/util/async-components.ts @@ -590,6 +590,10 @@ export function NewGroupPanel() { return import(/* webpackChunkName: "features/groups" */'../components/panels/new-group-panel'); } +export function MyGroupsPanel() { + return import(/* webpackChunkName: "features/groups" */'../components/panels/my-groups-panel'); +} + export function SuggestedGroupsPanel() { return import(/* webpackChunkName: "features/groups" */'../components/panels/suggested-groups-panel'); } diff --git a/app/soapbox/pages/group-page.tsx b/app/soapbox/pages/group-page.tsx index c182068f2..595079aea 100644 --- a/app/soapbox/pages/group-page.tsx +++ b/app/soapbox/pages/group-page.tsx @@ -10,6 +10,7 @@ import { CtaBanner, GroupMediaPanel, SignUpPanel, + SuggestedGroupsPanel, } from 'soapbox/features/ui/util/async-components'; import { useGroup, useOwnAccount } from 'soapbox/hooks'; import { Group } from 'soapbox/schemas'; @@ -127,6 +128,9 @@ const GroupPage: React.FC = ({ params, children }) => { {Component => } + + {Component => } + diff --git a/app/soapbox/pages/groups-page.tsx b/app/soapbox/pages/groups-page.tsx index dfe4bf58b..cce55f515 100644 --- a/app/soapbox/pages/groups-page.tsx +++ b/app/soapbox/pages/groups-page.tsx @@ -1,9 +1,10 @@ import React from 'react'; +import { Route, Routes } from 'react-router-dom-v5-compat'; import { Column, Layout } from 'soapbox/components/ui'; import LinkFooter from 'soapbox/features/ui/components/link-footer'; import BundleContainer from 'soapbox/features/ui/containers/bundle-container'; -import { NewGroupPanel } from 'soapbox/features/ui/util/async-components'; +import { MyGroupsPanel, NewGroupPanel, SuggestedGroupsPanel } from 'soapbox/features/ui/util/async-components'; interface IGroupsPage { children: React.ReactNode @@ -22,10 +23,28 @@ const GroupsPage: React.FC = ({ children }) => ( - {Component => } + {Component => } + + + {Component => } + + )} + /> + + {Component => } + + )} + /> + - + ); From ad98bf45cc1ef8048e71224699c59a786a3df1d2 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Mon, 20 Mar 2023 15:40:21 -0400 Subject: [PATCH 8/9] Add hook to delete Group --- .../entity-store/hooks/useEntityActions.ts | 16 ++++++++--- .../group/components/group-action-button.tsx | 28 ++++++++++--------- app/soapbox/features/group/manage-group.tsx | 26 +++++++++++++---- .../hooks/api/groups/useDeleteGroup.ts | 18 ++++++++++++ app/soapbox/hooks/api/index.ts | 1 + 5 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 app/soapbox/hooks/api/groups/useDeleteGroup.ts diff --git a/app/soapbox/entity-store/hooks/useEntityActions.ts b/app/soapbox/entity-store/hooks/useEntityActions.ts index 2383c69c6..3a60f4627 100644 --- a/app/soapbox/entity-store/hooks/useEntityActions.ts +++ b/app/soapbox/entity-store/hooks/useEntityActions.ts @@ -30,7 +30,7 @@ interface EntityActionEndpoints { } interface EntityCallbacks { - onSuccess?(entity: TEntity): void + onSuccess?(entity?: TEntity): void } function useEntityActions( @@ -70,14 +70,20 @@ function useEntityActions( }); } - function deleteEntity(entityId: string): Promise { + function deleteEntity(entityId: string, callbacks: EntityCallbacks = {}): Promise { if (!endpoints.delete) return Promise.reject(endpoints); // Get the entity before deleting, so we can reverse the action if the API request fails. const entity = getState().entities[entityType]?.store[entityId]; // Optimistically delete the entity from the _store_ but keep the lists in tact. dispatch(deleteEntities([entityId], entityType, { preserveLists: true })); + setIsLoading(true); + return api.delete(endpoints.delete.replaceAll(':id', entityId)).then((response) => { + if (callbacks.onSuccess) { + callbacks.onSuccess(); + } + // Success - finish deleting entity from the state. dispatch(deleteEntities([entityId], entityType)); @@ -90,12 +96,14 @@ function useEntityActions( dispatch(importEntities([entity], entityType)); } throw e; + }).finally(() => { + setIsLoading(false); }); } return { - createEntity: createEntity, - deleteEntity: endpoints.delete ? deleteEntity : undefined, + createEntity, + deleteEntity, isLoading, }; } diff --git a/app/soapbox/features/group/components/group-action-button.tsx b/app/soapbox/features/group/components/group-action-button.tsx index ba828c9c1..8c8a36db7 100644 --- a/app/soapbox/features/group/components/group-action-button.tsx +++ b/app/soapbox/features/group/components/group-action-button.tsx @@ -7,8 +7,10 @@ import { deleteEntities } from 'soapbox/entity-store/actions'; import { Entities } from 'soapbox/entity-store/entities'; import { useAppDispatch } from 'soapbox/hooks'; import { useCancelMembershipRequest, useJoinGroup, useLeaveGroup } from 'soapbox/hooks/api'; +import { GroupRoles } from 'soapbox/schemas/group-member'; import toast from 'soapbox/toast'; -import { Group } from 'soapbox/types/entities'; + +import type { Group } from 'soapbox/types/entities'; interface IGroupActionButton { group: Group @@ -33,7 +35,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { const isRequested = group.relationship?.requested; const isNonMember = !group.relationship?.member && !isRequested; - const isAdmin = group.relationship?.role === 'owner'; + const isOwner = group.relationship?.role === GroupRoles.OWNER; const isBlocked = group.relationship?.blocked_by; const onJoinGroup = () => joinGroup.mutate({}, { @@ -68,6 +70,17 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { return null; } + if (isOwner) { + return ( + + ); + } + if (isNonMember) { return ( - ); - } - return (