From ebaff75e388c8167c143002fa041249b936102a6 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Wed, 3 May 2023 14:03:13 -0400 Subject: [PATCH 1/3] Improve Group UX --- .../api/hooks/groups/useGroupMembershipRequests.ts | 3 ++- .../components/authorize-reject-buttons.tsx | 2 +- .../features/group/group-membership-requests.tsx | 14 ++++++++------ app/soapbox/features/group/manage-group.tsx | 10 +++++----- .../features/ui/components/group-media-panel.tsx | 4 ++-- .../manage-group-modal/steps/confirmation-step.tsx | 2 +- app/soapbox/locales/en.json | 10 +++++----- 7 files changed, 24 insertions(+), 21 deletions(-) diff --git a/app/soapbox/api/hooks/groups/useGroupMembershipRequests.ts b/app/soapbox/api/hooks/groups/useGroupMembershipRequests.ts index 16ff924c7..a6e068091 100644 --- a/app/soapbox/api/hooks/groups/useGroupMembershipRequests.ts +++ b/app/soapbox/api/hooks/groups/useGroupMembershipRequests.ts @@ -14,7 +14,7 @@ function useGroupMembershipRequests(groupId: string) { const { entity: relationship } = useGroupRelationship(groupId); - const { entities, invalidate, ...rest } = useEntities( + const { entities, invalidate, fetchEntities, ...rest } = useEntities( path, () => api.get(`/api/v1/groups/${groupId}/membership_requests`), { @@ -37,6 +37,7 @@ function useGroupMembershipRequests(groupId: string) { return { accounts: entities, + refetch: fetchEntities, authorize, reject, ...rest, diff --git a/app/soapbox/components/authorize-reject-buttons.tsx b/app/soapbox/components/authorize-reject-buttons.tsx index a5fddc61a..5dfb37a31 100644 --- a/app/soapbox/components/authorize-reject-buttons.tsx +++ b/app/soapbox/components/authorize-reject-buttons.tsx @@ -51,7 +51,7 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize await action(); setState(past); } catch (e) { - console.error(e); + if (e) console.error(e); } }; if (typeof countdown === 'number') { diff --git a/app/soapbox/features/group/group-membership-requests.tsx b/app/soapbox/features/group/group-membership-requests.tsx index e50092887..eebe387bb 100644 --- a/app/soapbox/features/group/group-membership-requests.tsx +++ b/app/soapbox/features/group/group-membership-requests.tsx @@ -58,7 +58,7 @@ const GroupMembershipRequests: React.FC = ({ params }) const { group } = useGroup(id); - const { accounts, authorize, reject, isLoading } = useGroupMembershipRequests(id); + const { accounts, authorize, reject, refetch, isLoading } = useGroupMembershipRequests(id); const { invalidate } = useGroupMembers(id, GroupRoles.USER); useEffect(() => { @@ -80,11 +80,13 @@ const GroupMembershipRequests: React.FC = ({ params }) } async function handleAuthorize(account: AccountEntity) { - try { - await authorize(account.id); - } catch (_e) { - toast.error(intl.formatMessage(messages.authorizeFail, { name: account.username })); - } + return authorize(account.id) + .then(() => Promise.resolve()) + .catch(() => { + refetch(); + toast.error(intl.formatMessage(messages.authorizeFail, { name: account.username })); + return Promise.reject(); + }); } async function handleReject(account: AccountEntity) { diff --git a/app/soapbox/features/group/manage-group.tsx b/app/soapbox/features/group/manage-group.tsx index 648593318..d57b8792a 100644 --- a/app/soapbox/features/group/manage-group.tsx +++ b/app/soapbox/features/group/manage-group.tsx @@ -16,16 +16,16 @@ import ColumnForbidden from '../ui/components/column-forbidden'; type RouteParams = { groupId: string }; const messages = defineMessages({ - heading: { id: 'column.manage_group', defaultMessage: 'Manage group' }, - editGroup: { id: 'manage_group.edit_group', defaultMessage: 'Edit group' }, + 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: 'Banned Members' }, - deleteGroup: { id: 'manage_group.delete_group', defaultMessage: 'Delete group' }, + 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' }, + 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' }, + other: { id: 'settings.other', defaultMessage: 'Other Options' }, deleteSuccess: { id: 'group.delete.success', defaultMessage: 'Group successfully deleted' }, }); diff --git a/app/soapbox/features/ui/components/group-media-panel.tsx b/app/soapbox/features/ui/components/group-media-panel.tsx index 1d9428f33..9f52af5a2 100644 --- a/app/soapbox/features/ui/components/group-media-panel.tsx +++ b/app/soapbox/features/ui/components/group-media-panel.tsx @@ -40,7 +40,7 @@ const GroupMediaPanel: React.FC = ({ group }) => { useEffect(() => { setLoading(true); - if (group && (isMember || !isPrivate)) { + if (group && !group.deleted_at && (isMember || !isPrivate)) { dispatch(expandGroupMediaTimeline(group.id)) // @ts-ignore .then(() => setLoading(false)) @@ -72,7 +72,7 @@ const GroupMediaPanel: React.FC = ({ group }) => { } }; - if (isPrivate && !isMember) { + if ((isPrivate && !isMember) || group?.deleted_at) { return null; } diff --git a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/confirmation-step.tsx b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/confirmation-step.tsx index 4c388751f..11244c3c5 100644 --- a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/confirmation-step.tsx +++ b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/confirmation-step.tsx @@ -19,7 +19,7 @@ const ConfirmationStep: React.FC = ({ group }) => { const intl = useIntl(); const handleCopyLink = () => { - copy(`${window.location.origin}/group/${group?.slug}`, () => { + copy(group?.url as string, () => { toast.success(intl.formatMessage(messages.copied)); }); }; diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 96efd8530..91fad97e0 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -358,7 +358,7 @@ "column.import_data": "Import data", "column.info": "Server information", "column.lists": "Lists", - "column.manage_group": "Manage group", + "column.manage_group": "Manage Group", "column.mentions": "Mentions", "column.mfa": "Multi-Factor Authentication", "column.mfa_cancel": "Cancel", @@ -491,7 +491,7 @@ "confirmations.delete_from_group.heading": "Delete from group", "confirmations.delete_from_group.message": "Are you sure you want to delete @{name}'s post?", "confirmations.delete_group.confirm": "Delete", - "confirmations.delete_group.heading": "Delete group", + "confirmations.delete_group.heading": "Delete Group", "confirmations.delete_group.message": "Are you sure you want to delete this group? This is a permanent action that cannot be undone.", "confirmations.delete_list.confirm": "Delete", "confirmations.delete_list.heading": "Delete list", @@ -970,9 +970,9 @@ "manage_group.confirmation.share": "Share this group", "manage_group.confirmation.title": "You’re all set!", "manage_group.create": "Create Group", - "manage_group.delete_group": "Delete group", + "manage_group.delete_group": "Delete Group", "manage_group.done": "Done", - "manage_group.edit_group": "Edit group", + "manage_group.edit_group": "Edit Group", "manage_group.fields.cannot_change_hint": "This cannot be changed after the group is created.", "manage_group.fields.description_label": "Description", "manage_group.fields.description_placeholder": "Description", @@ -1354,7 +1354,7 @@ "settings.delete_account": "Delete Account", "settings.edit_profile": "Edit Profile", "settings.messages.label": "Allow users to start a new chat with you", - "settings.other": "Other options", + "settings.other": "Other Options", "settings.preferences": "Preferences", "settings.profile": "Profile", "settings.save.success": "Your preferences have been saved!", From 4f081abb7add7f1a9d8efb83b3af23db400af6eb Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Thu, 4 May 2023 08:14:43 -0400 Subject: [PATCH 2/3] d --- .../group/group-membership-requests.tsx | 17 +++++++++-------- app/soapbox/locales/en.json | 3 +-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/soapbox/features/group/group-membership-requests.tsx b/app/soapbox/features/group/group-membership-requests.tsx index eebe387bb..2ea708c3d 100644 --- a/app/soapbox/features/group/group-membership-requests.tsx +++ b/app/soapbox/features/group/group-membership-requests.tsx @@ -17,8 +17,7 @@ type RouteParams = { groupId: string }; const messages = defineMessages({ heading: { id: 'column.group_pending_requests', defaultMessage: 'Pending requests' }, - authorizeFail: { id: 'group.group_mod_authorize.fail', defaultMessage: 'Failed to approve @{name}' }, - rejectFail: { id: 'group.group_mod_reject.fail', defaultMessage: 'Failed to reject @{name}' }, + authorizeRejectFail: { id: 'group.membership_requests.fail', defaultMessage: 'Group owner or admin has already taken action on this request.' }, }); interface IMembershipRequest { @@ -84,17 +83,19 @@ const GroupMembershipRequests: React.FC = ({ params }) .then(() => Promise.resolve()) .catch(() => { refetch(); - toast.error(intl.formatMessage(messages.authorizeFail, { name: account.username })); + toast.error(intl.formatMessage(messages.authorizeRejectFail)); return Promise.reject(); }); } async function handleReject(account: AccountEntity) { - try { - await reject(account.id); - } catch (_e) { - toast.error(intl.formatMessage(messages.rejectFail, { name: account.username })); - } + return reject(account.id) + .then(() => Promise.resolve()) + .catch(() => { + refetch(); + toast.error(intl.formatMessage(messages.authorizeRejectFail)); + return Promise.reject(); + }); } return ( diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 91fad97e0..2c81f307f 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -773,14 +773,12 @@ "group.delete.success": "Group successfully deleted", "group.deleted.message": "This group has been deleted.", "group.demote.user.success": "@{name} is now a member", - "group.group_mod_authorize.fail": "Failed to approve @{name}", "group.group_mod_block": "Ban from group", "group.group_mod_block.success": "@{name} is banned", "group.group_mod_demote": "Remove {role} role", "group.group_mod_kick": "Kick @{name} from group", "group.group_mod_kick.success": "Kicked @{name} from group", "group.group_mod_promote_mod": "Assign {role} role", - "group.group_mod_reject.fail": "Failed to reject @{name}", "group.group_mod_unblock": "Unban", "group.group_mod_unblock.success": "Unbanned @{name} from group", "group.header.alt": "Group header", @@ -794,6 +792,7 @@ "group.manage": "Manage Group", "group.member.admin.limit.summary": "You can assign up to {count} admins for the group at this time.", "group.member.admin.limit.title": "Admin limit reached", + "group.membership_requests.fail": "Group owner or admin has already taken action on this request.", "group.popover.action": "View Group", "group.popover.summary": "You must be a member of the group in order to reply to this status.", "group.popover.title": "Membership required", From 5b61aa39a7b23a7a48b648ccc366b7532a7a14c7 Mon Sep 17 00:00:00 2001 From: Chewbacca Date: Fri, 5 May 2023 08:41:25 -0400 Subject: [PATCH 3/3] Account for 409 response codes --- .../group/group-membership-requests.tsx | 24 +++++++++++++++---- app/soapbox/locales/en.json | 3 ++- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/app/soapbox/features/group/group-membership-requests.tsx b/app/soapbox/features/group/group-membership-requests.tsx index 2ea708c3d..79700e1a7 100644 --- a/app/soapbox/features/group/group-membership-requests.tsx +++ b/app/soapbox/features/group/group-membership-requests.tsx @@ -1,3 +1,4 @@ +import { AxiosError } from 'axios'; import React, { useEffect } from 'react'; import { FormattedMessage, defineMessages, useIntl } from 'react-intl'; @@ -17,7 +18,8 @@ type RouteParams = { groupId: string }; const messages = defineMessages({ heading: { id: 'column.group_pending_requests', defaultMessage: 'Pending requests' }, - authorizeRejectFail: { id: 'group.membership_requests.fail', defaultMessage: 'Group owner or admin has already taken action on this request.' }, + authorizeFail: { id: 'group.group_mod_authorize.fail', defaultMessage: 'Failed to approve @{name}' }, + rejectFail: { id: 'group.group_mod_reject.fail', defaultMessage: 'Failed to reject @{name}' }, }); interface IMembershipRequest { @@ -81,9 +83,15 @@ const GroupMembershipRequests: React.FC = ({ params }) async function handleAuthorize(account: AccountEntity) { return authorize(account.id) .then(() => Promise.resolve()) - .catch(() => { + .catch((error: AxiosError) => { refetch(); - toast.error(intl.formatMessage(messages.authorizeRejectFail)); + + let message = intl.formatMessage(messages.authorizeFail, { name: account.username }); + if (error.response?.status === 409) { + message = (error.response?.data as any).error; + } + toast.error(message); + return Promise.reject(); }); } @@ -91,9 +99,15 @@ const GroupMembershipRequests: React.FC = ({ params }) async function handleReject(account: AccountEntity) { return reject(account.id) .then(() => Promise.resolve()) - .catch(() => { + .catch((error: AxiosError) => { refetch(); - toast.error(intl.formatMessage(messages.authorizeRejectFail)); + + let message = intl.formatMessage(messages.rejectFail, { name: account.username }); + if (error.response?.status === 409) { + message = (error.response?.data as any).error; + } + toast.error(message); + return Promise.reject(); }); } diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 2c81f307f..91fad97e0 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -773,12 +773,14 @@ "group.delete.success": "Group successfully deleted", "group.deleted.message": "This group has been deleted.", "group.demote.user.success": "@{name} is now a member", + "group.group_mod_authorize.fail": "Failed to approve @{name}", "group.group_mod_block": "Ban from group", "group.group_mod_block.success": "@{name} is banned", "group.group_mod_demote": "Remove {role} role", "group.group_mod_kick": "Kick @{name} from group", "group.group_mod_kick.success": "Kicked @{name} from group", "group.group_mod_promote_mod": "Assign {role} role", + "group.group_mod_reject.fail": "Failed to reject @{name}", "group.group_mod_unblock": "Unban", "group.group_mod_unblock.success": "Unbanned @{name} from group", "group.header.alt": "Group header", @@ -792,7 +794,6 @@ "group.manage": "Manage Group", "group.member.admin.limit.summary": "You can assign up to {count} admins for the group at this time.", "group.member.admin.limit.title": "Admin limit reached", - "group.membership_requests.fail": "Group owner or admin has already taken action on this request.", "group.popover.action": "View Group", "group.popover.summary": "You must be a member of the group in order to reply to this status.", "group.popover.title": "Membership required",