diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 953c7e252..786d52d13 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -56,6 +56,7 @@ module.exports = { }, polyfills: [ 'es:all', // core-js + 'fetch', // not polyfilled, but ignore it 'IntersectionObserver', // npm:intersection-observer 'Promise', // core-js 'ResizeObserver', // npm:resize-observer-polyfill diff --git a/app/soapbox/features/group/edit-group.tsx b/app/soapbox/features/group/edit-group.tsx index 1b6f62fc4..001a8b1ab 100644 --- a/app/soapbox/features/group/edit-group.tsx +++ b/app/soapbox/features/group/edit-group.tsx @@ -1,22 +1,22 @@ import clsx from 'clsx'; -import React, { useEffect, useState } from 'react'; +import React, { useMemo } from 'react'; +import { useForm } from 'react-hook-form'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { - changeGroupEditorTitle, - changeGroupEditorDescription, - changeGroupEditorMedia, -} from 'soapbox/actions/groups'; import Icon from 'soapbox/components/icon'; -import { Avatar, Column, Form, FormGroup, HStack, Input, Text, Textarea } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks'; +import { Avatar, Button, Column, Form, FormActions, FormGroup, HStack, Input, Text, Textarea } from 'soapbox/components/ui'; +import { useAppSelector, useInstance } from 'soapbox/hooks'; +import { useGroup, useUpdateGroup } from 'soapbox/hooks/api'; import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts'; import resizeImage from 'soapbox/utils/resize-image'; import type { List as ImmutableList } from 'immutable'; +const nonDefaultAvatar = (url: string | undefined) => url && isDefaultAvatar(url) ? undefined : url; +const nonDefaultHeader = (url: string | undefined) => url && isDefaultHeader(url) ? undefined : url; + interface IMediaInput { - src: string | null + src: string | undefined accept: string onChange: React.ChangeEventHandler disabled: boolean @@ -28,7 +28,7 @@ const messages = defineMessages({ groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' }, }); -const HeaderPicker: React.FC = ({ src, onChange, accept, disabled }) => { +const HeaderPicker = React.forwardRef(({ src, onChange, accept, disabled }, ref) => { return ( ); -}; +}); -const AvatarPicker: React.FC = ({ src, onChange, accept, disabled }) => { +const AvatarPicker = React.forwardRef(({ src, onChange, accept, disabled }, ref) => { return ( ); -}; +}); -const EditGroup = () => { +interface EditGroupForm { + display_name: string + note: string + avatar?: FileList + header?: FileList +} + +interface IEditGroup { + params: { + id: string + } +} + +const EditGroup: React.FC = ({ params: { id: groupId } }) => { const intl = useIntl(); - const dispatch = useAppDispatch(); const instance = useInstance(); - const groupId = useAppSelector((state) => state.group_editor.groupId); - const isUploading = useAppSelector((state) => state.group_editor.isUploading); - const name = useAppSelector((state) => state.group_editor.displayName); - const description = useAppSelector((state) => state.group_editor.note); + const { group } = useGroup(groupId); + const { updateGroup } = useUpdateGroup(groupId); - const [avatarSrc, setAvatarSrc] = useState(null); - const [headerSrc, setHeaderSrc] = useState(null); + const maxName = Number(instance.configuration.getIn(['groups', 'max_characters_name'])); + const maxNote = Number(instance.configuration.getIn(['groups', 'max_characters_description'])); + + const { register, handleSubmit, formState: { isSubmitting }, watch } = useForm({ + values: group ? { + display_name: group.display_name, + note: group.note, + } : undefined, + }); + + const avatarSrc = usePreview(watch('avatar')?.item(0)) || nonDefaultAvatar(group?.avatar); + const headerSrc = usePreview(watch('header')?.item(0)) || nonDefaultHeader(group?.header); const attachmentTypes = useAppSelector( state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList, )?.filter(type => type.startsWith('image/')).toArray().join(','); - const onChangeName: React.ChangeEventHandler = ({ target }) => { - dispatch(changeGroupEditorTitle(target.value)); - }; + async function onSubmit(data: EditGroupForm) { + const avatar = data.avatar?.item(0); + const header = data.header?.item(0); - const onChangeDescription: React.ChangeEventHandler = ({ target }) => { - dispatch(changeGroupEditorDescription(target.value)); - }; - - const handleFileChange: React.ChangeEventHandler = e => { - const rawFile = e.target.files?.item(0); - - if (!rawFile) return; - - if (e.target.name === 'avatar') { - resizeImage(rawFile, 400 * 400).then(file => { - dispatch(changeGroupEditorMedia('avatar', file)); - setAvatarSrc(URL.createObjectURL(file)); - }).catch(console.error); - } else { - resizeImage(rawFile, 1920 * 1080).then(file => { - dispatch(changeGroupEditorMedia('header', file)); - setHeaderSrc(URL.createObjectURL(file)); - }).catch(console.error); - } - }; - - useEffect(() => { - if (!groupId) return; - - dispatch((_, getState) => { - const group = getState().groups.items.get(groupId); - if (!group) return; - if (group.avatar && !isDefaultAvatar(group.avatar)) setAvatarSrc(group.avatar); - if (group.header && !isDefaultHeader(group.header)) setHeaderSrc(group.header); + await updateGroup({ + display_name: data.display_name, + note: data.note, + avatar: avatar ? await resizeImage(avatar, 400 * 400) : undefined, + header: header ? await resizeImage(header, 1920 * 1080) : undefined, }); - }, [groupId]); + } return ( -
+
- - + +
} @@ -161,9 +158,7 @@ const EditGroup = () => { {