From 6f705a827e52b0ac874eeaad466681474d312e05 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 15:31:13 -0500 Subject: [PATCH 01/11] AuthorizeRejectButtons: add countdown functionality --- .../components/authorize-reject-buttons.tsx | 88 +++++++++++-------- 1 file changed, 52 insertions(+), 36 deletions(-) diff --git a/app/soapbox/components/authorize-reject-buttons.tsx b/app/soapbox/components/authorize-reject-buttons.tsx index 66d8d1d4a..bc398132f 100644 --- a/app/soapbox/components/authorize-reject-buttons.tsx +++ b/app/soapbox/components/authorize-reject-buttons.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import { HStack, IconButton, Text } from 'soapbox/components/ui'; @@ -6,50 +6,45 @@ import { HStack, IconButton, Text } from 'soapbox/components/ui'; interface IAuthorizeRejectButtons { onAuthorize(): Promise | unknown onReject(): Promise | unknown + countdown?: number } /** Buttons to approve or reject a pending item, usually an account. */ -const AuthorizeRejectButtons: React.FC = ({ onAuthorize, onReject }) => { - const [state, setState] = useState<'authorized' | 'rejected' | 'pending'>('pending'); +const AuthorizeRejectButtons: React.FC = ({ onAuthorize, onReject, countdown = 0 }) => { + const [state, setState] = useState<'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending'>('pending'); + const timeout = useRef(); - async function handleAuthorize() { - try { - await onAuthorize(); - setState('authorized'); - } catch (e) { - console.error(e); + function handleAction(present: 'authorizing' | 'rejecting', past: 'authorized' | 'rejected'): void { + if (state === present) { + if (timeout.current) { + clearTimeout(timeout.current); + } + setState('pending'); + } else { + setState(present); + timeout.current = setTimeout(async () => { + try { + await onAuthorize(); + setState(past); + } catch (e) { + console.error(e); + } + }, countdown); } } - async function handleReject() { - try { - await onReject(); - setState('rejected'); - } catch (e) { - console.error(e); - } - } + const handleAuthorize = async () => handleAction('authorizing', 'authorized'); + const handleReject = async () => handleAction('rejecting', 'rejected'); + + useEffect(() => { + return () => { + if (timeout.current) { + clearTimeout(timeout.current); + } + }; + }, []); switch (state) { - case 'pending': - return ( - - - - - ); case 'authorized': return (
@@ -66,6 +61,27 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize
); + default: + return ( + + + + + ); } }; From 9367b16200aa0b7d72bf06a15aac9b75d3918c3a Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 15:34:02 -0500 Subject: [PATCH 02/11] AuthorizeRejectButtons: swap out icon during action countdown --- app/soapbox/components/authorize-reject-buttons.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/soapbox/components/authorize-reject-buttons.tsx b/app/soapbox/components/authorize-reject-buttons.tsx index bc398132f..de28be80c 100644 --- a/app/soapbox/components/authorize-reject-buttons.tsx +++ b/app/soapbox/components/authorize-reject-buttons.tsx @@ -65,7 +65,7 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize return ( = ({ onAuthorize disabled={state === 'authorizing'} /> Date: Mon, 27 Mar 2023 15:36:55 -0500 Subject: [PATCH 03/11] AuthorizeRejectButtons: fix onReject never being called --- app/soapbox/components/authorize-reject-buttons.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/soapbox/components/authorize-reject-buttons.tsx b/app/soapbox/components/authorize-reject-buttons.tsx index de28be80c..10ab2bbf2 100644 --- a/app/soapbox/components/authorize-reject-buttons.tsx +++ b/app/soapbox/components/authorize-reject-buttons.tsx @@ -14,7 +14,11 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize const [state, setState] = useState<'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending'>('pending'); const timeout = useRef(); - function handleAction(present: 'authorizing' | 'rejecting', past: 'authorized' | 'rejected'): void { + function handleAction( + present: 'authorizing' | 'rejecting', + past: 'authorized' | 'rejected', + action: () => Promise | unknown, + ): void { if (state === present) { if (timeout.current) { clearTimeout(timeout.current); @@ -24,7 +28,7 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize setState(present); timeout.current = setTimeout(async () => { try { - await onAuthorize(); + await action(); setState(past); } catch (e) { console.error(e); @@ -33,8 +37,8 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize } } - const handleAuthorize = async () => handleAction('authorizing', 'authorized'); - const handleReject = async () => handleAction('rejecting', 'rejected'); + const handleAuthorize = async () => handleAction('authorizing', 'authorized', onAuthorize); + const handleReject = async () => handleAction('rejecting', 'rejected', onReject); useEffect(() => { return () => { From d08a2e215bff8a005a8504b2233c90417de7f23d Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 16:45:41 -0500 Subject: [PATCH 04/11] AuthorizeRejectButtons: add a loading animation --- .../components/authorize-reject-buttons.tsx | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/app/soapbox/components/authorize-reject-buttons.tsx b/app/soapbox/components/authorize-reject-buttons.tsx index 10ab2bbf2..81d25a63d 100644 --- a/app/soapbox/components/authorize-reject-buttons.tsx +++ b/app/soapbox/components/authorize-reject-buttons.tsx @@ -68,22 +68,32 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize default: return ( - - +
+ + {(state === 'rejecting') && ( +
+ )} +
+
+ + {(state === 'authorizing') && ( +
+ )} +
); } From 63394bb1b47e7f48e1fdb9f8abf28dc3ec443227 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 16:59:55 -0500 Subject: [PATCH 05/11] AuthorizeRejectButtons: refactor button into a separate component --- .../components/authorize-reject-buttons.tsx | 80 ++++++++++++------- 1 file changed, 53 insertions(+), 27 deletions(-) diff --git a/app/soapbox/components/authorize-reject-buttons.tsx b/app/soapbox/components/authorize-reject-buttons.tsx index 81d25a63d..d4d90732e 100644 --- a/app/soapbox/components/authorize-reject-buttons.tsx +++ b/app/soapbox/components/authorize-reject-buttons.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import React, { useEffect, useRef, useState } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -10,7 +11,7 @@ interface IAuthorizeRejectButtons { } /** Buttons to approve or reject a pending item, usually an account. */ -const AuthorizeRejectButtons: React.FC = ({ onAuthorize, onReject, countdown = 0 }) => { +const AuthorizeRejectButtons: React.FC = ({ onAuthorize, onReject, countdown }) => { const [state, setState] = useState<'authorizing' | 'rejecting' | 'authorized' | 'rejected' | 'pending'>('pending'); const timeout = useRef(); @@ -68,35 +69,60 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize default: return ( -
- - {(state === 'rejecting') && ( -
- )} -
-
- - {(state === 'authorizing') && ( -
- )} -
+ + ); } }; +interface IAuthorizeRejectButton { + theme: 'primary' | 'danger' + icon: string + action(): void + isLoading?: boolean + disabled?: boolean +} + +const AuthorizeRejectButton: React.FC = ({ theme, icon, action, isLoading, disabled }) => { + return ( +
+ + {(isLoading) && ( +
+ )} +
+ ); +}; + export { AuthorizeRejectButtons }; \ No newline at end of file From 09ed0bccabf528d18fddfac87af12169afe6b077 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 17:03:19 -0500 Subject: [PATCH 06/11] AuthorizeRejectButtons: refactor ActionEmblem into a separate component --- .../components/authorize-reject-buttons.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/app/soapbox/components/authorize-reject-buttons.tsx b/app/soapbox/components/authorize-reject-buttons.tsx index d4d90732e..347201e1b 100644 --- a/app/soapbox/components/authorize-reject-buttons.tsx +++ b/app/soapbox/components/authorize-reject-buttons.tsx @@ -52,19 +52,11 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize switch (state) { case 'authorized': return ( -
- - - -
+ } /> ); case 'rejected': return ( -
- - - -
+ } /> ); default: return ( @@ -88,6 +80,20 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize } }; +interface IActionEmblem { + text: React.ReactNode +} + +const ActionEmblem: React.FC = ({ text }) => { + return ( +
+ + {text} + +
+ ); +}; + interface IAuthorizeRejectButton { theme: 'primary' | 'danger' icon: string From f216b52b369f8e21559182bb0ca78e6c08bddf20 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 17:08:24 -0500 Subject: [PATCH 07/11] AuthorizeRejectButtons: skip animations if countdown is undefined --- app/soapbox/components/authorize-reject-buttons.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/app/soapbox/components/authorize-reject-buttons.tsx b/app/soapbox/components/authorize-reject-buttons.tsx index 347201e1b..9edd44189 100644 --- a/app/soapbox/components/authorize-reject-buttons.tsx +++ b/app/soapbox/components/authorize-reject-buttons.tsx @@ -26,15 +26,20 @@ const AuthorizeRejectButtons: React.FC = ({ onAuthorize } setState('pending'); } else { - setState(present); - timeout.current = setTimeout(async () => { + const doAction = async () => { try { await action(); setState(past); } catch (e) { console.error(e); } - }, countdown); + }; + if (typeof countdown === 'number') { + setState(present); + timeout.current = setTimeout(doAction, countdown); + } else { + doAction(); + } } } From 22474e3ca96e25418f49d2f629366f4566888d70 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 17:09:10 -0500 Subject: [PATCH 08/11] MembershipRequest: add 3s countdown on authorize/reject actions --- app/soapbox/features/group/group-membership-requests.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/features/group/group-membership-requests.tsx b/app/soapbox/features/group/group-membership-requests.tsx index d98517af8..3b5f91d5d 100644 --- a/app/soapbox/features/group/group-membership-requests.tsx +++ b/app/soapbox/features/group/group-membership-requests.tsx @@ -42,6 +42,7 @@ const MembershipRequest: React.FC = ({ account, onAuthorize, ); From 0522f333c3c1766ebaad5c764982ce79223ba9dd Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 17:13:44 -0500 Subject: [PATCH 09/11] Make follow requests use AuthorizeRejectButtons countdown --- .../features/follow-requests/components/account-authorize.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/soapbox/features/follow-requests/components/account-authorize.tsx b/app/soapbox/features/follow-requests/components/account-authorize.tsx index 91f492523..9e1387ba2 100644 --- a/app/soapbox/features/follow-requests/components/account-authorize.tsx +++ b/app/soapbox/features/follow-requests/components/account-authorize.tsx @@ -29,6 +29,7 @@ const AccountAuthorize: React.FC = ({ id }) => { } /> From d39e2cc7e038bf7ee2e2c1825394836a2516f16e Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 17:14:22 -0500 Subject: [PATCH 10/11] UnapprovedAccount: use countdown, remove rejectUserModal --- app/soapbox/actions/moderation.tsx | 22 ------------- .../admin/components/unapproved-account.tsx | 31 +++---------------- 2 files changed, 4 insertions(+), 49 deletions(-) diff --git a/app/soapbox/actions/moderation.tsx b/app/soapbox/actions/moderation.tsx index 5b0a4a5f2..c236a2986 100644 --- a/app/soapbox/actions/moderation.tsx +++ b/app/soapbox/actions/moderation.tsx @@ -112,27 +112,6 @@ const deleteUserModal = (intl: IntlShape, accountId: string, afterConfirm = () = })); }; -const rejectUserModal = (intl: IntlShape, accountId: string, afterConfirm = () => {}) => - (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const acct = state.accounts.get(accountId)!.acct; - const name = state.accounts.get(accountId)!.username; - - dispatch(openModal('CONFIRM', { - icon: require('@tabler/icons/user-off.svg'), - heading: intl.formatMessage(messages.rejectUserHeading, { acct }), - message: intl.formatMessage(messages.rejectUserPrompt, { acct }), - confirm: intl.formatMessage(messages.rejectUserConfirm, { name }), - onConfirm: () => { - dispatch(deleteUsers([accountId])) - .then(() => { - afterConfirm(); - }) - .catch(() => {}); - }, - })); - }; - const toggleStatusSensitivityModal = (intl: IntlShape, statusId: string, sensitive: boolean, afterConfirm = () => {}) => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); @@ -178,7 +157,6 @@ const deleteStatusModal = (intl: IntlShape, statusId: string, afterConfirm = () export { deactivateUserModal, deleteUserModal, - rejectUserModal, toggleStatusSensitivityModal, deleteStatusModal, }; diff --git a/app/soapbox/features/admin/components/unapproved-account.tsx b/app/soapbox/features/admin/components/unapproved-account.tsx index 26f4b661e..cf99baa6e 100644 --- a/app/soapbox/features/admin/components/unapproved-account.tsx +++ b/app/soapbox/features/admin/components/unapproved-account.tsx @@ -1,18 +1,10 @@ import React, { useCallback } from 'react'; -import { defineMessages, useIntl } from 'react-intl'; -import { approveUsers } from 'soapbox/actions/admin'; -import { rejectUserModal } from 'soapbox/actions/moderation'; +import { approveUsers, deleteUsers } from 'soapbox/actions/admin'; import { AuthorizeRejectButtons } from 'soapbox/components/authorize-reject-buttons'; import { Stack, HStack, Text } from 'soapbox/components/ui'; import { useAppSelector, useAppDispatch } from 'soapbox/hooks'; import { makeGetAccount } from 'soapbox/selectors'; -import toast from 'soapbox/toast'; - -const messages = defineMessages({ - approved: { id: 'admin.awaiting_approval.approved_message', defaultMessage: '{acct} was approved!' }, - rejected: { id: 'admin.awaiting_approval.rejected_message', defaultMessage: '{acct} was rejected.' }, -}); interface IUnapprovedAccount { accountId: string @@ -20,7 +12,6 @@ interface IUnapprovedAccount { /** Displays an unapproved account for moderation purposes. */ const UnapprovedAccount: React.FC = ({ accountId }) => { - const intl = useIntl(); const dispatch = useAppDispatch(); const getAccount = useCallback(makeGetAccount(), []); @@ -29,23 +20,8 @@ const UnapprovedAccount: React.FC = ({ accountId }) => { if (!account) return null; - const handleApprove = () => { - return dispatch(approveUsers([account.id])) - .then(() => { - const message = intl.formatMessage(messages.approved, { acct: `@${account.acct}` }); - toast.success(message); - }); - }; - - const handleReject = () => { - return new Promise((resolve) => { - dispatch(rejectUserModal(intl, account.id, () => { - const message = intl.formatMessage(messages.rejected, { acct: `@${account.acct}` }); - toast.info(message); - resolve(); - })); - }); - }; + const handleApprove = () => dispatch(approveUsers([account.id])); + const handleReject = () => dispatch(deleteUsers([account.id])); return ( @@ -62,6 +38,7 @@ const UnapprovedAccount: React.FC = ({ accountId }) => { From 232e387a5025988fb72ad7e9dc07fd1ed7fee2a3 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 27 Mar 2023 17:24:24 -0500 Subject: [PATCH 11/11] yarn i18n --- app/soapbox/locales/en.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index 1d4d12a31..d50a4e619 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -91,9 +91,7 @@ "admin.announcements.edit": "Edit", "admin.announcements.ends_at": "Ends at:", "admin.announcements.starts_at": "Starts at:", - "admin.awaiting_approval.approved_message": "{acct} was approved!", "admin.awaiting_approval.empty_message": "There is nobody waiting for approval. When a new user signs up, you can review them here.", - "admin.awaiting_approval.rejected_message": "{acct} was rejected.", "admin.dashboard.registration_mode.approval_hint": "Users can sign up, but their account only gets activated when an admin approves it.", "admin.dashboard.registration_mode.approval_label": "Approval Required", "admin.dashboard.registration_mode.closed_hint": "Nobody can sign up. You can still invite people.",