diff --git a/app/soapbox/components/icon.tsx b/app/soapbox/components/icon.tsx index 300265ea5..421d937dd 100644 --- a/app/soapbox/components/icon.tsx +++ b/app/soapbox/components/icon.tsx @@ -14,6 +14,9 @@ export interface IIcon extends React.HTMLAttributes { className?: string } +/** + * @deprecated Use the UI Icon component directly. + */ const Icon: React.FC = ({ src, alt, className, ...rest }) => { return (
= ({ label, hint, children, onClick, onSelec return (
- {label} + {label} {hint ? ( {hint} @@ -83,12 +82,26 @@ const ListItem: React.FC = ({ label, hint, children, onClick, onSelec
{children} - {isSelected ? ( +
- ) : null} +
) : null} diff --git a/app/soapbox/features/ui/components/modals/manage-group-modal/manage-group-modal.tsx b/app/soapbox/features/ui/components/modals/manage-group-modal/manage-group-modal.tsx index 2863060c5..fcc7c14da 100644 --- a/app/soapbox/features/ui/components/modals/manage-group-modal/manage-group-modal.tsx +++ b/app/soapbox/features/ui/components/modals/manage-group-modal/manage-group-modal.tsx @@ -3,7 +3,8 @@ import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { submitGroupEditor } from 'soapbox/actions/groups'; import { Modal, Stack } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; +import { useAppDispatch, useAppSelector, useDebounce } from 'soapbox/hooks'; +import { useGroupValidation } from 'soapbox/hooks/api'; import ConfirmationStep from './steps/confirmation-step'; import DetailsStep from './steps/details-step'; @@ -34,6 +35,7 @@ interface IManageGroupModal { const ManageGroupModal: React.FC = ({ onClose }) => { const intl = useIntl(); + const debounce = useDebounce; const dispatch = useAppDispatch(); const id = useAppSelector((state) => state.group_editor.groupId); @@ -43,6 +45,11 @@ const ManageGroupModal: React.FC = ({ onClose }) => { const [currentStep, setCurrentStep] = useState(id ? Steps.TWO : Steps.ONE); + const name = useAppSelector((state) => state.group_editor.displayName); + const debouncedName = debounce(name, 300); + + const { data: { isValid } } = useGroupValidation(debouncedName); + const handleClose = () => { onClose('MANAGE_GROUP'); }; @@ -92,7 +99,7 @@ const ManageGroupModal: React.FC = ({ onClose }) => { : } confirmationAction={handleNextStep} confirmationText={confirmationText} - confirmationDisabled={isSubmitting} + confirmationDisabled={isSubmitting || (currentStep === Steps.TWO && !isValid)} confirmationFullWidth onClose={handleClose} > diff --git a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx index 2450dffea..56c755086 100644 --- a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx +++ b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/details-step.tsx @@ -7,9 +7,9 @@ import { changeGroupEditorDescription, changeGroupEditorMedia, } from 'soapbox/actions/groups'; -import Icon from 'soapbox/components/icon'; -import { Avatar, Form, FormGroup, HStack, Input, Text, Textarea } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks'; +import { Avatar, Form, FormGroup, HStack, Icon, Input, Text, Textarea } from 'soapbox/components/ui'; +import { useAppDispatch, useAppSelector, useDebounce, useInstance } from 'soapbox/hooks'; +import { useGroupValidation } from 'soapbox/hooks/api'; import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts'; import resizeImage from 'soapbox/utils/resize-image'; @@ -30,7 +30,7 @@ const messages = defineMessages({ const HeaderPicker: React.FC = ({ src, onChange, accept, disabled }) => { return (
+ } + hintText={} + errors={isValid ? [] : [errorMessage as string]} > { maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_name']))} /> + } > diff --git a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx index 22ad89305..2920de493 100644 --- a/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx +++ b/app/soapbox/features/ui/components/modals/manage-group-modal/steps/privacy-step.tsx @@ -17,11 +17,11 @@ const PrivacyStep = () => { return ( <> - + - + diff --git a/app/soapbox/hooks/api/groups/useGroupValidation.ts b/app/soapbox/hooks/api/groups/useGroupValidation.ts new file mode 100644 index 000000000..bfcd3bbb0 --- /dev/null +++ b/app/soapbox/hooks/api/groups/useGroupValidation.ts @@ -0,0 +1,47 @@ +import { useQuery } from '@tanstack/react-query'; + +import { useApi } from 'soapbox/hooks/useApi'; +import { useFeatures } from 'soapbox/hooks/useFeatures'; + +type Validation = { + error: string + message: string +} + +const ValidationKeys = { + validation: (name: string) => ['group', 'validation', name] as const, +}; + +function useGroupValidation(name: string = '') { + const api = useApi(); + const features = useFeatures(); + + const getValidation = async() => { + const { data } = await api.get('/api/v1/groups/validate', { + params: { name }, + }) + .catch((error) => { + if (error.response.status === 422) { + return { data: error.response.data }; + } + + throw error; + }); + + return data; + }; + + const queryInfo = useQuery(ValidationKeys.validation(name), getValidation, { + enabled: features.groupsValidation && !!name, + }); + + return { + ...queryInfo, + data: { + ...queryInfo.data, + isValid: !queryInfo.data?.error ?? true, + }, + }; +} + +export { useGroupValidation }; \ No newline at end of file diff --git a/app/soapbox/hooks/api/index.ts b/app/soapbox/hooks/api/index.ts index b70314633..936fda6ef 100644 --- a/app/soapbox/hooks/api/index.ts +++ b/app/soapbox/hooks/api/index.ts @@ -7,6 +7,7 @@ export { useDeleteGroup } from './groups/useDeleteGroup'; export { useDemoteGroupMember } from './groups/useDemoteGroupMember'; export { useGroup, useGroups } from './groups/useGroups'; export { useGroupSearch } from './groups/useGroupSearch'; +export { useGroupValidation } from './groups/useGroupValidation'; export { useJoinGroup } from './groups/useJoinGroup'; export { useLeaveGroup } from './groups/useLeaveGroup'; export { usePromoteGroupMember } from './groups/usePromoteGroupMember'; diff --git a/app/soapbox/locales/en.json b/app/soapbox/locales/en.json index ce45fc577..ba9c2644a 100644 --- a/app/soapbox/locales/en.json +++ b/app/soapbox/locales/en.json @@ -945,6 +945,7 @@ "manage_group.edit_success": "The group was edited", "manage_group.fields.description_label": "Description", "manage_group.fields.description_placeholder": "Description", + "manage_group.fields.name_help": "This cannot be changed after the group is created.", "manage_group.fields.name_label": "Group name (required)", "manage_group.fields.name_placeholder": "Group Name", "manage_group.get_started": "Let’s get started!", diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 5230546b5..8558e3b9d 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -554,6 +554,11 @@ const getInstanceFeatures = (instance: Instance) => { */ groupsSearch: v.software === TRUTHSOCIAL, + /** + * Can validate group names. + */ + groupsValidation: v.software === TRUTHSOCIAL, + /** * Can hide follows/followers lists and counts. * @see PATCH /api/v1/accounts/update_credentials