ManageGroupModal: use internal state instead of Redux
This commit is contained in:
parent
1b9c070a20
commit
659c186394
|
@ -783,30 +783,6 @@ const resetGroupEditor = () => ({
|
||||||
type: GROUP_EDITOR_RESET,
|
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<string, any> = {
|
|
||||||
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 {
|
export {
|
||||||
GROUP_EDITOR_SET,
|
GROUP_EDITOR_SET,
|
||||||
GROUP_CREATE_REQUEST,
|
GROUP_CREATE_REQUEST,
|
||||||
|
@ -960,5 +936,4 @@ export {
|
||||||
changeGroupEditorPrivacy,
|
changeGroupEditorPrivacy,
|
||||||
changeGroupEditorMedia,
|
changeGroupEditorMedia,
|
||||||
resetGroupEditor,
|
resetGroupEditor,
|
||||||
submitGroupEditor,
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -20,7 +20,7 @@ function useCreateEntity<TEntity extends Entity = Entity, Data = unknown>(
|
||||||
) {
|
) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const [isLoading, setPromise] = useLoading();
|
const [isSubmitting, setPromise] = useLoading();
|
||||||
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
||||||
|
|
||||||
async function createEntity(data: Data, callbacks: EntityCallbacks<TEntity> = {}): Promise<void> {
|
async function createEntity(data: Data, callbacks: EntityCallbacks<TEntity> = {}): Promise<void> {
|
||||||
|
@ -44,7 +44,7 @@ function useCreateEntity<TEntity extends Entity = Entity, Data = unknown>(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createEntity,
|
createEntity,
|
||||||
isLoading,
|
isSubmitting,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ function useEntityActions<TEntity extends Entity = Entity, Data = any>(
|
||||||
const { deleteEntity, isLoading: deleteLoading } =
|
const { deleteEntity, isLoading: deleteLoading } =
|
||||||
useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replaceAll(':id', entityId)));
|
useDeleteEntity(entityType, (entityId) => api.delete(endpoints.delete!.replaceAll(':id', entityId)));
|
||||||
|
|
||||||
const { createEntity, isLoading: createLoading } =
|
const { createEntity, isSubmitting: createLoading } =
|
||||||
useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts);
|
useCreateEntity<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -8,7 +8,7 @@ interface IMediaInput {
|
||||||
src: string | undefined
|
src: string | undefined
|
||||||
accept: string
|
accept: string
|
||||||
onChange: React.ChangeEventHandler<HTMLInputElement>
|
onChange: React.ChangeEventHandler<HTMLInputElement>
|
||||||
disabled: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const AvatarPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ src, onChange, accept, disabled }, ref) => {
|
const AvatarPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ src, onChange, accept, disabled }, ref) => {
|
||||||
|
|
|
@ -9,7 +9,7 @@ interface IMediaInput {
|
||||||
src: string | undefined
|
src: string | undefined
|
||||||
accept: string
|
accept: string
|
||||||
onChange: React.ChangeEventHandler<HTMLInputElement>
|
onChange: React.ChangeEventHandler<HTMLInputElement>
|
||||||
disabled: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const HeaderPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ src, onChange, accept, disabled }, ref) => {
|
const HeaderPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ src, onChange, accept, disabled }, ref) => {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useMemo, useState } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { submitGroupEditor } from 'soapbox/actions/groups';
|
|
||||||
import { Modal, Stack } from 'soapbox/components/ui';
|
import { Modal, Stack } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector, useDebounce } from 'soapbox/hooks';
|
import { useDebounce } from 'soapbox/hooks';
|
||||||
import { useGroupValidation } from 'soapbox/hooks/api';
|
import { useCreateGroup, useGroupValidation, type CreateGroupParams } from 'soapbox/hooks/api';
|
||||||
|
import { type Group } from 'soapbox/schemas';
|
||||||
|
|
||||||
import ConfirmationStep from './steps/confirmation-step';
|
import ConfirmationStep from './steps/confirmation-step';
|
||||||
import DetailsStep from './steps/details-step';
|
import DetailsStep from './steps/details-step';
|
||||||
|
@ -13,7 +13,6 @@ import PrivacyStep from './steps/privacy-step';
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
next: { id: 'manage_group.next', defaultMessage: 'Next' },
|
next: { id: 'manage_group.next', defaultMessage: 'Next' },
|
||||||
create: { id: 'manage_group.create', defaultMessage: 'Create' },
|
create: { id: 'manage_group.create', defaultMessage: 'Create' },
|
||||||
update: { id: 'manage_group.update', defaultMessage: 'Update' },
|
|
||||||
done: { id: 'manage_group.done', defaultMessage: 'Done' },
|
done: { id: 'manage_group.done', defaultMessage: 'Done' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,12 +22,6 @@ enum Steps {
|
||||||
THREE = 'THREE',
|
THREE = 'THREE',
|
||||||
}
|
}
|
||||||
|
|
||||||
const manageGroupSteps = {
|
|
||||||
ONE: PrivacyStep,
|
|
||||||
TWO: DetailsStep,
|
|
||||||
THREE: ConfirmationStep,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IManageGroupModal {
|
interface IManageGroupModal {
|
||||||
onClose: (type?: string) => void
|
onClose: (type?: string) => void
|
||||||
}
|
}
|
||||||
|
@ -36,34 +29,26 @@ interface IManageGroupModal {
|
||||||
const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
|
const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const debounce = useDebounce;
|
const debounce = useDebounce;
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
|
|
||||||
const id = useAppSelector((state) => state.group_editor.groupId);
|
const [group, setGroup] = useState<Group | null>(null);
|
||||||
const [group, setGroup] = useState<any | null>(null);
|
const [params, setParams] = useState<CreateGroupParams>({});
|
||||||
|
const [currentStep, setCurrentStep] = useState<Steps>(Steps.ONE);
|
||||||
|
|
||||||
const isSubmitting = useAppSelector((state) => state.group_editor.isSubmitting);
|
const { createGroup, isSubmitting } = useCreateGroup();
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState<Steps>(id ? Steps.TWO : Steps.ONE);
|
|
||||||
|
|
||||||
const name = useAppSelector((state) => state.group_editor.displayName);
|
|
||||||
const debouncedName = debounce(name, 300);
|
|
||||||
|
|
||||||
|
const debouncedName = debounce(params.display_name || '', 300);
|
||||||
const { data: { isValid } } = useGroupValidation(debouncedName);
|
const { data: { isValid } } = useGroupValidation(debouncedName);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
onClose('MANAGE_GROUP');
|
onClose('MANAGE_GROUP');
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
return dispatch(submitGroupEditor(true));
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmationText = useMemo(() => {
|
const confirmationText = useMemo(() => {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case Steps.THREE:
|
case Steps.THREE:
|
||||||
return intl.formatMessage(messages.done);
|
return intl.formatMessage(messages.done);
|
||||||
case Steps.TWO:
|
case Steps.TWO:
|
||||||
return intl.formatMessage(id ? messages.update : messages.create);
|
return intl.formatMessage(messages.create);
|
||||||
default:
|
default:
|
||||||
return intl.formatMessage(messages.next);
|
return intl.formatMessage(messages.next);
|
||||||
}
|
}
|
||||||
|
@ -75,12 +60,12 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
|
||||||
setCurrentStep(Steps.TWO);
|
setCurrentStep(Steps.TWO);
|
||||||
break;
|
break;
|
||||||
case Steps.TWO:
|
case Steps.TWO:
|
||||||
handleSubmit()
|
createGroup(params, {
|
||||||
.then((group) => {
|
onSuccess(group) {
|
||||||
setCurrentStep(Steps.THREE);
|
setCurrentStep(Steps.THREE);
|
||||||
setGroup(group);
|
setGroup(group);
|
||||||
})
|
},
|
||||||
.catch(() => {});
|
});
|
||||||
break;
|
break;
|
||||||
case Steps.THREE:
|
case Steps.THREE:
|
||||||
handleClose();
|
handleClose();
|
||||||
|
@ -90,13 +75,20 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const StepToRender = manageGroupSteps[currentStep];
|
const renderStep = () => {
|
||||||
|
switch (currentStep) {
|
||||||
|
case Steps.ONE:
|
||||||
|
return <PrivacyStep params={params} onChange={setParams} />;
|
||||||
|
case Steps.TWO:
|
||||||
|
return <DetailsStep params={params} onChange={setParams} />;
|
||||||
|
case Steps.THREE:
|
||||||
|
return <ConfirmationStep group={group!} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={id
|
title={<FormattedMessage id='navigation_bar.create_group' defaultMessage='Create Group' />}
|
||||||
? <FormattedMessage id='navigation_bar.edit_group' defaultMessage='Edit Group' />
|
|
||||||
: <FormattedMessage id='navigation_bar.create_group' defaultMessage='Create Group' />}
|
|
||||||
confirmationAction={handleNextStep}
|
confirmationAction={handleNextStep}
|
||||||
confirmationText={confirmationText}
|
confirmationText={confirmationText}
|
||||||
confirmationDisabled={isSubmitting || (currentStep === Steps.TWO && !isValid)}
|
confirmationDisabled={isSubmitting || (currentStep === Steps.TWO && !isValid)}
|
||||||
|
@ -104,8 +96,7 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
>
|
>
|
||||||
<Stack space={2}>
|
<Stack space={2}>
|
||||||
{/* @ts-ignore */}
|
{renderStep()}
|
||||||
<StepToRender group={group} />
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,162 +1,73 @@
|
||||||
import clsx from 'clsx';
|
import React from 'react';
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import {
|
import { Form, FormGroup, Input, Textarea } from 'soapbox/components/ui';
|
||||||
changeGroupEditorTitle,
|
import AvatarPicker from 'soapbox/features/group/components/group-avatar-picker';
|
||||||
changeGroupEditorDescription,
|
import HeaderPicker from 'soapbox/features/group/components/group-header-picker';
|
||||||
changeGroupEditorMedia,
|
import { useAppSelector, useDebounce, useInstance } from 'soapbox/hooks';
|
||||||
} from 'soapbox/actions/groups';
|
import { CreateGroupParams, useGroupValidation } from 'soapbox/hooks/api';
|
||||||
import { Avatar, Form, FormGroup, HStack, Icon, Input, Text, Textarea } from 'soapbox/components/ui';
|
import { usePreview } from 'soapbox/hooks/forms';
|
||||||
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';
|
import resizeImage from 'soapbox/utils/resize-image';
|
||||||
|
|
||||||
import type { List as ImmutableList } from 'immutable';
|
import type { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
interface IMediaInput {
|
|
||||||
src: string | null
|
|
||||||
accept: string
|
|
||||||
onChange: React.ChangeEventHandler<HTMLInputElement>
|
|
||||||
disabled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' },
|
groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Group Name' },
|
||||||
groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' },
|
groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' },
|
||||||
});
|
});
|
||||||
|
|
||||||
const HeaderPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }) => {
|
interface IDetailsStep {
|
||||||
return (
|
params: CreateGroupParams
|
||||||
<label
|
onChange(params: CreateGroupParams): void
|
||||||
className='dark:sm:shadow-inset relative h-24 w-full cursor-pointer overflow-hidden rounded-xl bg-primary-100 text-primary-500 dark:bg-gray-800 dark:text-accent-blue sm:h-44'
|
}
|
||||||
>
|
|
||||||
{src && <img className='h-full w-full object-cover' src={src} alt='' />}
|
|
||||||
<HStack
|
|
||||||
className={clsx('absolute top-0 h-full w-full transition-opacity', {
|
|
||||||
'opacity-0 hover:opacity-90 bg-primary-100 dark:bg-gray-800': src,
|
|
||||||
})}
|
|
||||||
space={1.5}
|
|
||||||
alignItems='center'
|
|
||||||
justifyContent='center'
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
src={require('@tabler/icons/photo-plus.svg')}
|
|
||||||
className='h-4.5 w-4.5'
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text size='md' theme='primary' weight='semibold'>
|
const DetailsStep: React.FC<IDetailsStep> = ({ params, onChange }) => {
|
||||||
<FormattedMessage id='group.upload_banner' defaultMessage='Upload photo' />
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<input
|
|
||||||
name='header'
|
|
||||||
type='file'
|
|
||||||
accept={accept}
|
|
||||||
onChange={onChange}
|
|
||||||
disabled={disabled}
|
|
||||||
className='hidden'
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AvatarPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }) => {
|
|
||||||
return (
|
|
||||||
<label className='absolute bottom-0 left-1/2 h-20 w-20 -translate-x-1/2 translate-y-1/2 cursor-pointer rounded-full bg-primary-500 ring-4 ring-white dark:ring-primary-900'>
|
|
||||||
{src && <Avatar src={src} size={80} />}
|
|
||||||
<HStack
|
|
||||||
alignItems='center'
|
|
||||||
justifyContent='center'
|
|
||||||
|
|
||||||
className={clsx('absolute left-0 top-0 h-full w-full rounded-full transition-opacity', {
|
|
||||||
'opacity-0 hover:opacity-90 bg-primary-500': src,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
src={require('@tabler/icons/camera-plus.svg')}
|
|
||||||
className='h-5 w-5 text-white'
|
|
||||||
/>
|
|
||||||
</HStack>
|
|
||||||
<span className='sr-only'>Upload avatar</span>
|
|
||||||
<input
|
|
||||||
name='avatar'
|
|
||||||
type='file'
|
|
||||||
accept={accept}
|
|
||||||
onChange={onChange}
|
|
||||||
disabled={disabled}
|
|
||||||
className='hidden'
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DetailsStep = () => {
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const debounce = useDebounce;
|
const debounce = useDebounce;
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const instance = useInstance();
|
const instance = useInstance();
|
||||||
|
|
||||||
const groupId = useAppSelector((state) => state.group_editor.groupId);
|
const {
|
||||||
const isUploading = useAppSelector((state) => state.group_editor.isUploading);
|
display_name: displayName = '',
|
||||||
const name = useAppSelector((state) => state.group_editor.displayName);
|
note = '',
|
||||||
const description = useAppSelector((state) => state.group_editor.note);
|
} = params;
|
||||||
|
|
||||||
const debouncedName = debounce(name, 300);
|
|
||||||
|
|
||||||
|
const debouncedName = debounce(displayName, 300);
|
||||||
const { data: { isValid, message: errorMessage } } = useGroupValidation(debouncedName);
|
const { data: { isValid, message: errorMessage } } = useGroupValidation(debouncedName);
|
||||||
|
|
||||||
const [avatarSrc, setAvatarSrc] = useState<string | null>(null);
|
const avatarSrc = usePreview(params.avatar);
|
||||||
const [headerSrc, setHeaderSrc] = useState<string | null>(null);
|
const headerSrc = usePreview(params.header);
|
||||||
|
|
||||||
const attachmentTypes = useAppSelector(
|
const attachmentTypes = useAppSelector(
|
||||||
state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>,
|
state => state.instance.configuration.getIn(['media_attachments', 'supported_mime_types']) as ImmutableList<string>,
|
||||||
)?.filter(type => type.startsWith('image/')).toArray().join(',');
|
)?.filter(type => type.startsWith('image/')).toArray().join(',');
|
||||||
|
|
||||||
const onChangeName: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
const handleTextChange = (property: keyof CreateGroupParams): React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> => {
|
||||||
dispatch(changeGroupEditorTitle(target.value));
|
return (e) => {
|
||||||
|
onChange({
|
||||||
|
...params,
|
||||||
|
[property]: e.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeDescription: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) => {
|
const handleImageChange = (property: keyof CreateGroupParams, maxPixels?: number): React.ChangeEventHandler<HTMLInputElement> => {
|
||||||
dispatch(changeGroupEditorDescription(target.value));
|
return async ({ target: { files } }) => {
|
||||||
};
|
const file = files ? files[0] : undefined;
|
||||||
|
if (file) {
|
||||||
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
const resized = await resizeImage(file, maxPixels);
|
||||||
const rawFile = e.target.files?.item(0);
|
onChange({
|
||||||
|
...params,
|
||||||
if (!rawFile) return;
|
[property]: resized,
|
||||||
|
});
|
||||||
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 (
|
return (
|
||||||
<Form>
|
<Form>
|
||||||
<div className='relative mb-12 flex'>
|
<div className='relative mb-12 flex'>
|
||||||
<HeaderPicker src={headerSrc} accept={attachmentTypes} onChange={handleFileChange} disabled={isUploading} />
|
<HeaderPicker src={headerSrc} accept={attachmentTypes} onChange={handleImageChange('header', 1920 * 1080)} />
|
||||||
<AvatarPicker src={avatarSrc} accept={attachmentTypes} onChange={handleFileChange} disabled={isUploading} />
|
<AvatarPicker src={avatarSrc} accept={attachmentTypes} onChange={handleImageChange('avatar', 400 * 400)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
|
@ -167,8 +78,8 @@ const DetailsStep = () => {
|
||||||
<Input
|
<Input
|
||||||
type='text'
|
type='text'
|
||||||
placeholder={intl.formatMessage(messages.groupNamePlaceholder)}
|
placeholder={intl.formatMessage(messages.groupNamePlaceholder)}
|
||||||
value={name}
|
value={displayName}
|
||||||
onChange={onChangeName}
|
onChange={handleTextChange('display_name')}
|
||||||
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_name']))}
|
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_name']))}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
@ -179,8 +90,8 @@ const DetailsStep = () => {
|
||||||
<Textarea
|
<Textarea
|
||||||
autoComplete='off'
|
autoComplete='off'
|
||||||
placeholder={intl.formatMessage(messages.groupDescriptionPlaceholder)}
|
placeholder={intl.formatMessage(messages.groupDescriptionPlaceholder)}
|
||||||
value={description}
|
value={note}
|
||||||
onChange={onChangeDescription}
|
onChange={handleTextChange('note')}
|
||||||
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_description']))}
|
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_description']))}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { changeGroupEditorPrivacy } from 'soapbox/actions/groups';
|
|
||||||
import List, { ListItem } from 'soapbox/components/list';
|
import List, { ListItem } from 'soapbox/components/list';
|
||||||
import { Form, FormGroup, Stack, Text } from 'soapbox/components/ui';
|
import { Form, FormGroup, Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { type CreateGroupParams } from 'soapbox/hooks/api';
|
||||||
|
|
||||||
const PrivacyStep = () => {
|
interface IPrivacyStep {
|
||||||
const dispatch = useAppDispatch();
|
params: CreateGroupParams
|
||||||
|
onChange(params: CreateGroupParams): void
|
||||||
|
}
|
||||||
|
|
||||||
const locked = useAppSelector((state) => state.group_editor.locked);
|
const PrivacyStep: React.FC<IPrivacyStep> = ({ params, onChange }) => {
|
||||||
|
const visibility = params.group_visibility || 'everyone';
|
||||||
|
|
||||||
const onChangePrivacy = (value: boolean) => {
|
const onChangePrivacy = (group_visibility: CreateGroupParams['group_visibility']) => {
|
||||||
dispatch(changeGroupEditorPrivacy(value));
|
onChange({ ...params, group_visibility });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -33,15 +35,15 @@ const PrivacyStep = () => {
|
||||||
<ListItem
|
<ListItem
|
||||||
label={<Text weight='medium'><FormattedMessage id='manage_group.privacy.public.label' defaultMessage='Public' /></Text>}
|
label={<Text weight='medium'><FormattedMessage id='manage_group.privacy.public.label' defaultMessage='Public' /></Text>}
|
||||||
hint={<FormattedMessage id='manage_group.privacy.public.hint' defaultMessage='Discoverable. Anyone can join.' />}
|
hint={<FormattedMessage id='manage_group.privacy.public.hint' defaultMessage='Discoverable. Anyone can join.' />}
|
||||||
onSelect={() => onChangePrivacy(false)}
|
onSelect={() => onChangePrivacy('everyone')}
|
||||||
isSelected={!locked}
|
isSelected={visibility === 'everyone'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
label={<Text weight='medium'><FormattedMessage id='manage_group.privacy.private.label' defaultMessage='Private (Owner approval required)' /></Text>}
|
label={<Text weight='medium'><FormattedMessage id='manage_group.privacy.private.label' defaultMessage='Private (Owner approval required)' /></Text>}
|
||||||
hint={<FormattedMessage id='manage_group.privacy.private.hint' defaultMessage='Discoverable. Users can join after their request is approved.' />}
|
hint={<FormattedMessage id='manage_group.privacy.private.hint' defaultMessage='Discoverable. Users can join after their request is approved.' />}
|
||||||
onSelect={() => onChangePrivacy(true)}
|
onSelect={() => onChangePrivacy('members_only')}
|
||||||
isSelected={locked}
|
isSelected={visibility === 'members_only'}
|
||||||
/>
|
/>
|
||||||
</List>
|
</List>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Entities } from 'soapbox/entity-store/entities';
|
||||||
|
import { useCreateEntity } from 'soapbox/entity-store/hooks';
|
||||||
|
import { useApi } from 'soapbox/hooks/useApi';
|
||||||
|
import { groupSchema } from 'soapbox/schemas';
|
||||||
|
|
||||||
|
interface CreateGroupParams {
|
||||||
|
display_name?: string
|
||||||
|
note?: string
|
||||||
|
avatar?: File
|
||||||
|
header?: File
|
||||||
|
group_visibility?: 'members_only' | 'everyone'
|
||||||
|
discoverable?: boolean
|
||||||
|
tags?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCreateGroup() {
|
||||||
|
const api = useApi();
|
||||||
|
|
||||||
|
const { createEntity, ...rest } = useCreateEntity([Entities.GROUPS], (params: CreateGroupParams) => {
|
||||||
|
return api.post('/api/v1/groups', params, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, { schema: groupSchema });
|
||||||
|
|
||||||
|
return {
|
||||||
|
createGroup: createEntity,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useCreateGroup, type CreateGroupParams };
|
|
@ -3,6 +3,7 @@
|
||||||
*/
|
*/
|
||||||
export { useBlockGroupMember } from './groups/useBlockGroupMember';
|
export { useBlockGroupMember } from './groups/useBlockGroupMember';
|
||||||
export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest';
|
export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest';
|
||||||
|
export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup';
|
||||||
export { useDeleteGroup } from './groups/useDeleteGroup';
|
export { useDeleteGroup } from './groups/useDeleteGroup';
|
||||||
export { useDemoteGroupMember } from './groups/useDemoteGroupMember';
|
export { useDemoteGroupMember } from './groups/useDemoteGroupMember';
|
||||||
export { useGroup, useGroups } from './groups/useGroups';
|
export { useGroup, useGroups } from './groups/useGroups';
|
||||||
|
|
|
@ -28,7 +28,6 @@ import custom_emojis from './custom-emojis';
|
||||||
import domain_lists from './domain-lists';
|
import domain_lists from './domain-lists';
|
||||||
import dropdown_menu from './dropdown-menu';
|
import dropdown_menu from './dropdown-menu';
|
||||||
import filters from './filters';
|
import filters from './filters';
|
||||||
import group_editor from './group-editor';
|
|
||||||
import group_memberships from './group-memberships';
|
import group_memberships from './group-memberships';
|
||||||
import group_relationships from './group-relationships';
|
import group_relationships from './group-relationships';
|
||||||
import groups from './groups';
|
import groups from './groups';
|
||||||
|
@ -93,7 +92,6 @@ const reducers = {
|
||||||
dropdown_menu,
|
dropdown_menu,
|
||||||
entities,
|
entities,
|
||||||
filters,
|
filters,
|
||||||
group_editor,
|
|
||||||
group_memberships,
|
group_memberships,
|
||||||
group_relationships,
|
group_relationships,
|
||||||
groups,
|
groups,
|
||||||
|
|
Loading…
Reference in New Issue