diff --git a/app/soapbox/actions/groups.ts b/app/soapbox/actions/groups.ts index 2ae1792cb1..690b74540e 100644 --- a/app/soapbox/actions/groups.ts +++ b/app/soapbox/actions/groups.ts @@ -1,21 +1,15 @@ -import { defineMessages } from 'react-intl'; - import { deleteEntities } from 'soapbox/entity-store/actions'; -import toast from 'soapbox/toast'; import api, { getLinks } from '../api'; import { fetchRelationships } from './accounts'; import { importFetchedGroups, importFetchedAccounts } from './importer'; -import { closeModal, openModal } from './modals'; import { deleteFromTimelines } from './timelines'; import type { AxiosError } from 'axios'; import type { GroupRole } from 'soapbox/reducers/group-memberships'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { APIEntity, Group } from 'soapbox/types/entities'; - -const GROUP_EDITOR_SET = 'GROUP_EDITOR_SET'; +import type { APIEntity } from 'soapbox/types/entities'; const GROUP_CREATE_REQUEST = 'GROUP_CREATE_REQUEST'; const GROUP_CREATE_SUCCESS = 'GROUP_CREATE_SUCCESS'; @@ -97,100 +91,6 @@ const GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST = 'GROUP_MEMBERSHIP_REQUEST_REJECT const GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS = 'GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS'; const GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL = 'GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL'; -const GROUP_EDITOR_TITLE_CHANGE = 'GROUP_EDITOR_TITLE_CHANGE'; -const GROUP_EDITOR_DESCRIPTION_CHANGE = 'GROUP_EDITOR_DESCRIPTION_CHANGE'; -const GROUP_EDITOR_PRIVACY_CHANGE = 'GROUP_EDITOR_PRIVACY_CHANGE'; -const GROUP_EDITOR_MEDIA_CHANGE = 'GROUP_EDITOR_MEDIA_CHANGE'; - -const GROUP_EDITOR_RESET = 'GROUP_EDITOR_RESET'; - -const messages = defineMessages({ - success: { id: 'manage_group.submit_success', defaultMessage: 'The group was created' }, - editSuccess: { id: 'manage_group.edit_success', defaultMessage: 'The group was edited' }, - joinSuccess: { id: 'group.join.success', defaultMessage: 'Joined the group' }, - joinRequestSuccess: { id: 'group.join.request_success', defaultMessage: 'Request sent to group owner' }, - leaveSuccess: { id: 'group.leave.success', defaultMessage: 'Left the group' }, - view: { id: 'toast.view', defaultMessage: 'View' }, -}); - -const editGroup = (group: Group) => (dispatch: AppDispatch) => { - dispatch({ - type: GROUP_EDITOR_SET, - group, - }); - dispatch(openModal('MANAGE_GROUP')); -}; - -const createGroup = (params: Record, shouldReset?: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(createGroupRequest()); - - return api(getState).post('/api/v1/groups', params, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }) - .then(({ data }) => { - dispatch(importFetchedGroups([data])); - dispatch(createGroupSuccess(data)); - toast.success(messages.success, { - actionLabel: messages.view, - actionLink: `/groups/${data.id}`, - }); - - if (shouldReset) { - dispatch(resetGroupEditor()); - } - - return data; - }).catch(err => dispatch(createGroupFail(err))); - }; - -const createGroupRequest = () => ({ - type: GROUP_CREATE_REQUEST, -}); - -const createGroupSuccess = (group: APIEntity) => ({ - type: GROUP_CREATE_SUCCESS, - group, -}); - -const createGroupFail = (error: AxiosError) => ({ - type: GROUP_CREATE_FAIL, - error, -}); - -const updateGroup = (id: string, params: Record, shouldReset?: boolean) => - (dispatch: AppDispatch, getState: () => RootState) => { - dispatch(updateGroupRequest()); - - return api(getState).put(`/api/v1/groups/${id}`, params) - .then(({ data }) => { - dispatch(importFetchedGroups([data])); - dispatch(updateGroupSuccess(data)); - toast.success(messages.editSuccess); - - if (shouldReset) { - dispatch(resetGroupEditor()); - } - dispatch(closeModal('MANAGE_GROUP')); - }).catch(err => dispatch(updateGroupFail(err))); - }; - -const updateGroupRequest = () => ({ - type: GROUP_UPDATE_REQUEST, -}); - -const updateGroupSuccess = (group: APIEntity) => ({ - type: GROUP_UPDATE_SUCCESS, - group, -}); - -const updateGroupFail = (error: AxiosError) => ({ - type: GROUP_UPDATE_FAIL, - error, -}); - const deleteGroup = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch(deleteEntities([id], 'Group')); @@ -758,57 +658,7 @@ const rejectGroupMembershipRequestFail = (groupId: string, accountId: string, er error, }); -const changeGroupEditorTitle = (value: string) => ({ - type: GROUP_EDITOR_TITLE_CHANGE, - value, -}); - -const changeGroupEditorDescription = (value: string) => ({ - type: GROUP_EDITOR_DESCRIPTION_CHANGE, - value, -}); - -const changeGroupEditorPrivacy = (value: boolean) => ({ - type: GROUP_EDITOR_PRIVACY_CHANGE, - value, -}); - -const changeGroupEditorMedia = (mediaType: 'header' | 'avatar', file: File) => ({ - type: GROUP_EDITOR_MEDIA_CHANGE, - mediaType, - value: file, -}); - -const resetGroupEditor = () => ({ - type: GROUP_EDITOR_RESET, -}); - -const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { - const groupId = getState().group_editor.groupId; - const displayName = getState().group_editor.displayName; - const note = getState().group_editor.note; - const avatar = getState().group_editor.avatar; - const header = getState().group_editor.header; - const visibility = getState().group_editor.locked ? 'members_only' : 'everyone'; // Truth Social - - const params: Record = { - display_name: displayName, - group_visibility: visibility, - note, - }; - - if (avatar) params.avatar = avatar; - if (header) params.header = header; - - if (groupId === null) { - return dispatch(createGroup(params, shouldReset)); - } else { - return dispatch(updateGroup(groupId, params, shouldReset)); - } -}; - export { - GROUP_EDITOR_SET, GROUP_CREATE_REQUEST, GROUP_CREATE_SUCCESS, GROUP_CREATE_FAIL, @@ -869,20 +719,6 @@ export { GROUP_MEMBERSHIP_REQUEST_REJECT_REQUEST, GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS, GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL, - GROUP_EDITOR_TITLE_CHANGE, - GROUP_EDITOR_DESCRIPTION_CHANGE, - GROUP_EDITOR_PRIVACY_CHANGE, - GROUP_EDITOR_MEDIA_CHANGE, - GROUP_EDITOR_RESET, - editGroup, - createGroup, - createGroupRequest, - createGroupSuccess, - createGroupFail, - updateGroup, - updateGroupRequest, - updateGroupSuccess, - updateGroupFail, deleteGroup, deleteGroupRequest, deleteGroupSuccess, @@ -955,10 +791,4 @@ export { rejectGroupMembershipRequestRequest, rejectGroupMembershipRequestSuccess, rejectGroupMembershipRequestFail, - changeGroupEditorTitle, - changeGroupEditorDescription, - changeGroupEditorPrivacy, - changeGroupEditorMedia, - resetGroupEditor, - submitGroupEditor, }; diff --git a/app/soapbox/entity-store/hooks/useCreateEntity.ts b/app/soapbox/entity-store/hooks/useCreateEntity.ts index ba9dd802b8..31299344ef 100644 --- a/app/soapbox/entity-store/hooks/useCreateEntity.ts +++ b/app/soapbox/entity-store/hooks/useCreateEntity.ts @@ -20,7 +20,7 @@ function useCreateEntity( ) { const dispatch = useAppDispatch(); - const [isLoading, setPromise] = useLoading(); + const [isSubmitting, setPromise] = useLoading(); const { entityType, listKey } = parseEntitiesPath(expandedPath); async function createEntity(data: Data, callbacks: EntityCallbacks = {}): Promise { @@ -44,7 +44,7 @@ function useCreateEntity( return { createEntity, - isLoading, + isSubmitting, }; } diff --git a/app/soapbox/entity-store/hooks/useDeleteEntity.ts b/app/soapbox/entity-store/hooks/useDeleteEntity.ts index 767224af60..dac1d9a267 100644 --- a/app/soapbox/entity-store/hooks/useDeleteEntity.ts +++ b/app/soapbox/entity-store/hooks/useDeleteEntity.ts @@ -15,7 +15,7 @@ function useDeleteEntity( ) { const dispatch = useAppDispatch(); const getState = useGetState(); - const [isLoading, setPromise] = useLoading(); + const [isSubmitting, setPromise] = useLoading(); async function deleteEntity(entityId: string, callbacks: EntityCallbacks = {}): Promise { // Get the entity before deleting, so we can reverse the action if the API request fails. @@ -47,7 +47,7 @@ function useDeleteEntity( return { deleteEntity, - isLoading, + isSubmitting, }; } diff --git a/app/soapbox/entity-store/hooks/useEntityActions.ts b/app/soapbox/entity-store/hooks/useEntityActions.ts index dab6f7f77b..8b87c52fd5 100644 --- a/app/soapbox/entity-store/hooks/useEntityActions.ts +++ b/app/soapbox/entity-store/hooks/useEntityActions.ts @@ -24,16 +24,16 @@ function useEntityActions( const api = useApi(); const { entityType, path } = parseEntitiesPath(expandedPath); - const { deleteEntity, isLoading: deleteLoading } = + const { deleteEntity, isSubmitting: deleteSubmitting } = useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replaceAll(':id', entityId))); - const { createEntity, isLoading: createLoading } = + const { createEntity, isSubmitting: createSubmitting } = useCreateEntity(path, (data) => api.post(endpoints.post!, data), opts); return { createEntity, deleteEntity, - isLoading: createLoading || deleteLoading, + isSubmitting: createSubmitting || deleteSubmitting, }; } diff --git a/app/soapbox/entity-store/utils.ts b/app/soapbox/entity-store/utils.ts index e108639c2d..3f65f1ee0c 100644 --- a/app/soapbox/entity-store/utils.ts +++ b/app/soapbox/entity-store/utils.ts @@ -11,7 +11,7 @@ const updateStore = (store: EntityStore, entities: Entity[]): EntityStore => { /** Update the list with new entity IDs. */ const updateList = (list: EntityList, entities: Entity[]): EntityList => { const newIds = entities.map(entity => entity.id); - const ids = new Set([...Array.from(list.ids), ...newIds]); + const ids = new Set([...newIds, ...Array.from(list.ids)]); if (typeof list.state.totalCount === 'number') { const sizeDiff = ids.size - list.ids.size; diff --git a/app/soapbox/features/group/components/group-action-button.tsx b/app/soapbox/features/group/components/group-action-button.tsx index 45e8534b97..96f807f9dc 100644 --- a/app/soapbox/features/group/components/group-action-button.tsx +++ b/app/soapbox/features/group/components/group-action-button.tsx @@ -94,7 +94,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { @@ -119,7 +119,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => { diff --git a/app/soapbox/features/group/components/group-avatar-picker.tsx b/app/soapbox/features/group/components/group-avatar-picker.tsx new file mode 100644 index 0000000000..b13dfe80e5 --- /dev/null +++ b/app/soapbox/features/group/components/group-avatar-picker.tsx @@ -0,0 +1,45 @@ +import clsx from 'clsx'; +import React from 'react'; + +import Icon from 'soapbox/components/icon'; +import { Avatar, HStack } from 'soapbox/components/ui'; + +interface IMediaInput { + src: string | undefined + accept: string + onChange: React.ChangeEventHandler + disabled?: boolean +} + +const AvatarPicker = React.forwardRef(({ src, onChange, accept, disabled }, ref) => { + return ( + + ); +}); + +export default AvatarPicker; \ No newline at end of file diff --git a/app/soapbox/features/group/components/group-header-picker.tsx b/app/soapbox/features/group/components/group-header-picker.tsx new file mode 100644 index 0000000000..d2457ac1e5 --- /dev/null +++ b/app/soapbox/features/group/components/group-header-picker.tsx @@ -0,0 +1,52 @@ +import clsx from 'clsx'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; + +import Icon from 'soapbox/components/icon'; +import { HStack, Text } from 'soapbox/components/ui'; + +interface IMediaInput { + src: string | undefined + accept: string + onChange: React.ChangeEventHandler + disabled?: boolean +} + +const HeaderPicker = React.forwardRef(({ src, onChange, accept, disabled }, ref) => { + return ( + + ); +}); + +export default HeaderPicker; \ No newline at end of file diff --git a/app/soapbox/features/group/edit-group.tsx b/app/soapbox/features/group/edit-group.tsx index d385fb580c..23ff9ed98d 100644 --- a/app/soapbox/features/group/edit-group.tsx +++ b/app/soapbox/features/group/edit-group.tsx @@ -1,100 +1,27 @@ -import clsx from 'clsx'; import React, { useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import Icon from 'soapbox/components/icon'; -import { Avatar, Button, Column, Form, FormActions, FormGroup, HStack, Input, Spinner, Text, Textarea } from 'soapbox/components/ui'; +import { Button, Column, Form, FormActions, FormGroup, Input, Spinner, Textarea } from 'soapbox/components/ui'; import { useAppSelector, useInstance } from 'soapbox/hooks'; import { useGroup, useUpdateGroup } from 'soapbox/hooks/api'; import { useImageField, useTextField } from 'soapbox/hooks/forms'; import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts'; +import AvatarPicker from './components/group-avatar-picker'; +import HeaderPicker from './components/group-header-picker'; + 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 | undefined - accept: string - onChange: React.ChangeEventHandler - disabled: boolean -} - const messages = defineMessages({ heading: { id: 'navigation_bar.edit_group', defaultMessage: 'Edit Group' }, groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' }, groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' }, }); -const HeaderPicker = React.forwardRef(({ src, onChange, accept, disabled }, ref) => { - return ( - - ); -}); - -const AvatarPicker = React.forwardRef(({ src, onChange, accept, disabled }, ref) => { - return ( - - ); -}); - interface IEditGroup { params: { id: string diff --git a/app/soapbox/features/groups/index.tsx b/app/soapbox/features/groups/index.tsx index 7c392d28d8..2f6436cd50 100644 --- a/app/soapbox/features/groups/index.tsx +++ b/app/soapbox/features/groups/index.tsx @@ -33,7 +33,7 @@ const Groups: React.FC = () => { const { groups, isLoading } = useGroups(debouncedValue); const createGroup = () => { - dispatch(openModal('MANAGE_GROUP')); + dispatch(openModal('CREATE_GROUP')); }; const renderBlankslate = () => ( diff --git a/app/soapbox/features/ui/components/modal-root.tsx b/app/soapbox/features/ui/components/modal-root.tsx index 55e53c268e..08fd4c88cb 100644 --- a/app/soapbox/features/ui/components/modal-root.tsx +++ b/app/soapbox/features/ui/components/modal-root.tsx @@ -26,7 +26,7 @@ import { LandingPageModal, ListAdder, ListEditor, - ManageGroupModal, + CreateGroupModal, MediaModal, MentionsModal, MissingDescriptionModal, @@ -59,6 +59,7 @@ const MODAL_COMPONENTS = { 'COMPOSE': ComposeModal, 'COMPOSE_EVENT': ComposeEventModal, 'CONFIRM': ConfirmationModal, + 'CREATE_GROUP': CreateGroupModal, 'CRYPTO_DONATE': CryptoDonateModal, 'DISLIKES': DislikesModal, 'EDIT_ANNOUNCEMENT': EditAnnouncementModal, @@ -73,7 +74,6 @@ const MODAL_COMPONENTS = { 'LANDING_PAGE': LandingPageModal, 'LIST_ADDER': ListAdder, 'LIST_EDITOR': ListEditor, - 'MANAGE_GROUP': ManageGroupModal, 'MEDIA': MediaModal, 'MENTIONS': MentionsModal, 'MISSING_DESCRIPTION': MissingDescriptionModal, 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/create-group-modal.tsx similarity index 53% rename from app/soapbox/features/ui/components/modals/manage-group-modal/manage-group-modal.tsx rename to app/soapbox/features/ui/components/modals/manage-group-modal/create-group-modal.tsx index fcc7c14da1..bbeb2d43ec 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/create-group-modal.tsx @@ -1,10 +1,12 @@ +import { AxiosError } from 'axios'; import React, { useMemo, useState } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { submitGroupEditor } from 'soapbox/actions/groups'; import { Modal, Stack } from 'soapbox/components/ui'; -import { useAppDispatch, useAppSelector, useDebounce } from 'soapbox/hooks'; -import { useGroupValidation } from 'soapbox/hooks/api'; +import { useDebounce } from 'soapbox/hooks'; +import { useCreateGroup, useGroupValidation, type CreateGroupParams } from 'soapbox/hooks/api'; +import { type Group } from 'soapbox/schemas'; +import toast from 'soapbox/toast'; import ConfirmationStep from './steps/confirmation-step'; import DetailsStep from './steps/details-step'; @@ -13,7 +15,6 @@ import PrivacyStep from './steps/privacy-step'; const messages = defineMessages({ next: { id: 'manage_group.next', defaultMessage: 'Next' }, create: { id: 'manage_group.create', defaultMessage: 'Create' }, - update: { id: 'manage_group.update', defaultMessage: 'Update' }, done: { id: 'manage_group.done', defaultMessage: 'Done' }, }); @@ -23,47 +24,33 @@ enum Steps { THREE = 'THREE', } -const manageGroupSteps = { - ONE: PrivacyStep, - TWO: DetailsStep, - THREE: ConfirmationStep, -}; - -interface IManageGroupModal { +interface ICreateGroupModal { onClose: (type?: string) => void } -const ManageGroupModal: React.FC = ({ onClose }) => { +const CreateGroupModal: React.FC = ({ onClose }) => { const intl = useIntl(); const debounce = useDebounce; - const dispatch = useAppDispatch(); - const id = useAppSelector((state) => state.group_editor.groupId); - const [group, setGroup] = useState(null); + const [group, setGroup] = useState(null); + const [params, setParams] = useState({}); + const [currentStep, setCurrentStep] = useState(Steps.ONE); - const isSubmitting = useAppSelector((state) => state.group_editor.isSubmitting); - - const [currentStep, setCurrentStep] = useState(id ? Steps.TWO : Steps.ONE); - - const name = useAppSelector((state) => state.group_editor.displayName); - const debouncedName = debounce(name, 300); + const { createGroup, isSubmitting } = useCreateGroup(); + const debouncedName = debounce(params.display_name || '', 300); const { data: { isValid } } = useGroupValidation(debouncedName); const handleClose = () => { onClose('MANAGE_GROUP'); }; - const handleSubmit = () => { - return dispatch(submitGroupEditor(true)); - }; - const confirmationText = useMemo(() => { switch (currentStep) { case Steps.THREE: return intl.formatMessage(messages.done); case Steps.TWO: - return intl.formatMessage(id ? messages.update : messages.create); + return intl.formatMessage(messages.create); default: return intl.formatMessage(messages.next); } @@ -75,12 +62,20 @@ const ManageGroupModal: React.FC = ({ onClose }) => { setCurrentStep(Steps.TWO); break; case Steps.TWO: - handleSubmit() - .then((group) => { + createGroup(params, { + onSuccess(group) { setCurrentStep(Steps.THREE); setGroup(group); - }) - .catch(() => {}); + }, + onError(error) { + if (error instanceof AxiosError) { + const msg = error.response?.data.error; + if (typeof msg === 'string') { + toast.error(msg); + } + } + }, + }); break; case Steps.THREE: handleClose(); @@ -90,13 +85,20 @@ const ManageGroupModal: React.FC = ({ onClose }) => { } }; - const StepToRender = manageGroupSteps[currentStep]; + const renderStep = () => { + switch (currentStep) { + case Steps.ONE: + return ; + case Steps.TWO: + return ; + case Steps.THREE: + return ; + } + }; return ( - : } + title={} confirmationAction={handleNextStep} confirmationText={confirmationText} confirmationDisabled={isSubmitting || (currentStep === Steps.TWO && !isValid)} @@ -104,11 +106,10 @@ const ManageGroupModal: React.FC = ({ onClose }) => { onClose={handleClose} > - {/* @ts-ignore */} - + {renderStep()} ); }; -export default ManageGroupModal; +export default CreateGroupModal; 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 59b59b2eca..66b7b4b1d8 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 @@ -1,162 +1,73 @@ -import clsx from 'clsx'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; -import { - changeGroupEditorTitle, - changeGroupEditorDescription, - changeGroupEditorMedia, -} from 'soapbox/actions/groups'; -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 { Form, FormGroup, Input, Textarea } from 'soapbox/components/ui'; +import AvatarPicker from 'soapbox/features/group/components/group-avatar-picker'; +import HeaderPicker from 'soapbox/features/group/components/group-header-picker'; +import { useAppSelector, useDebounce, useInstance } from 'soapbox/hooks'; +import { CreateGroupParams, useGroupValidation } from 'soapbox/hooks/api'; +import { usePreview } from 'soapbox/hooks/forms'; import resizeImage from 'soapbox/utils/resize-image'; import type { List as ImmutableList } from 'immutable'; -interface IMediaInput { - src: string | null - accept: string - onChange: React.ChangeEventHandler - disabled: boolean -} - const messages = defineMessages({ groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' }, groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' }, }); -const HeaderPicker: React.FC = ({ src, onChange, accept, disabled }) => { - return ( - - ); -}; - -const AvatarPicker: React.FC = ({ src, onChange, accept, disabled }) => { - return ( - - ); -}; - -const DetailsStep = () => { +const DetailsStep: React.FC = ({ params, onChange }) => { const intl = useIntl(); const debounce = useDebounce; - 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 debouncedName = debounce(name, 300); + const { + display_name: displayName = '', + note = '', + } = params; + const debouncedName = debounce(displayName, 300); const { data: { isValid, message: errorMessage } } = useGroupValidation(debouncedName); - const [avatarSrc, setAvatarSrc] = useState(null); - const [headerSrc, setHeaderSrc] = useState(null); + const avatarSrc = usePreview(params.avatar); + const headerSrc = usePreview(params.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)); + const handleTextChange = (property: keyof CreateGroupParams): React.ChangeEventHandler => { + return (e) => { + onChange({ + ...params, + [property]: e.target.value, + }); + }; }; - const onChangeDescription: React.ChangeEventHandler = ({ target }) => { - dispatch(changeGroupEditorDescription(target.value)); + const handleImageChange = (property: keyof CreateGroupParams, maxPixels?: number): React.ChangeEventHandler => { + return async ({ target: { files } }) => { + const file = files ? files[0] : undefined; + if (file) { + const resized = await resizeImage(file, maxPixels); + onChange({ + ...params, + [property]: resized, + }); + } + }; }; - 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); - }); - }, [groupId]); - return (
- - + +
{ @@ -179,8 +90,8 @@ const DetailsStep = () => {