Merge branch 'create-group' into 'develop'

Create group

See merge request soapbox-pub/soapbox!2325
develop^2
Alex Gleason 2023-03-13 18:28:43 +00:00
commit 1922e889f7
12 zmienionych plików z 210 dodań i 22 usunięć

Wyświetl plik

@ -0,0 +1,26 @@
<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_7_1989" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="73" y="38" width="46" height="65">
<path d="M95.9997 38C90.3542 38 85.7775 42.1122 85.7775 47.1848V56.3696C85.7775 59.2605 87.2924 61.9828 89.8664 63.7174C90.1516 63.9096 90.0851 64.347 89.7556 64.4456L81.7371 66.8472C76.8048 68.3245 73.3901 72.3258 73.0313 76.9334C72.9897 77.4671 73.0675 78.0013 73.2009 78.5197L79.5 103C106.943 103 115.635 97.2124 118.109 94.7132C118.778 94.0378 119 93.0929 119 92.1426V77.739C119 72.7973 115.481 68.4099 110.263 66.8472L102.244 64.4455C101.914 64.3468 101.848 63.9096 102.133 63.7174C104.707 61.9828 106.222 59.2605 106.222 56.3696V47.1848C106.222 42.1122 101.645 38 95.9997 38Z" fill="#5448EE"/>
</mask>
<g mask="url(#mask0_7_1989)">
<path d="M95.9997 38C90.3542 38 85.7775 42.1122 85.7775 47.1848V56.3696C85.7775 59.2605 87.2924 61.9828 89.8664 63.7174C90.1516 63.9096 90.0851 64.347 89.7556 64.4456L81.7371 66.8472C76.8048 68.3245 73.3901 72.3258 73.0313 76.9334C72.9897 77.4671 73.0675 78.0013 73.2009 78.5197L79.5 103C106.943 103 115.635 97.2124 118.109 94.7132C118.778 94.0378 119 93.0929 119 92.1426V77.739C119 72.7973 115.481 68.4099 110.263 66.8472L102.244 64.4455C101.914 64.3468 101.848 63.9096 102.133 63.7174C104.707 61.9828 106.222 59.2605 106.222 56.3696V47.1848C106.222 42.1122 101.645 38 95.9997 38Z" fill="#5448EE"/>
<path opacity="0.34" d="M79.4229 76L112.423 107.841L109.423 111.884H70.9998V77.3607L79.4229 76Z" fill="#322B4E"/>
</g>
<path d="M32.0003 38C37.6458 38 42.2225 42.1122 42.2225 47.1848V56.3696C42.2225 59.2605 40.7076 61.9828 38.1336 63.7174L37.4195 64.1986L46.2629 66.8472C51.4806 68.4099 55 72.7973 55 77.739L48.5 103C13.5 103 9 93.5862 9 93.5862V77.739C9 72.7973 12.5194 68.4099 17.7371 66.8472L26.5808 64.1985L25.8669 63.7174C23.2929 61.9828 21.778 59.2605 21.778 56.3696V47.1848C21.778 42.1122 26.3547 38 32.0003 38Z" fill="url(#paint0_linear_7_1989)"/>
<mask id="mask1_7_1989" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="54" width="52" height="50">
<path d="M38.3857 73.7909C38.3857 67.5992 42.3053 62.1022 48.1163 60.1442L64.0011 54.792L79.8859 60.1442C85.6969 62.1022 89.6165 67.5992 89.6165 73.7909V95.0005C89.6165 99.4188 86.0348 103.001 81.6165 103.001H46.3857C41.9675 103.001 38.3857 99.4188 38.3857 95.0005V73.7909Z" fill="#B0AEB8"/>
<path d="M38.3857 73.7909C38.3857 67.5992 42.3053 62.1022 48.1163 60.1442L64.0011 54.792L79.8859 60.1442C85.6969 62.1022 89.6165 67.5992 89.6165 73.7909V95.0005C89.6165 99.4188 86.0348 103.001 81.6165 103.001H46.3857C41.9675 103.001 38.3857 99.4188 38.3857 95.0005V73.7909Z" fill="#B0AEB8"/>
</mask>
<g mask="url(#mask1_7_1989)">
<path d="M38.3857 73.7909C38.3857 67.5992 42.3053 62.1022 48.1163 60.1442L64.0011 54.792L79.8859 60.1442C85.6969 62.1022 89.6165 67.5992 89.6165 73.7909V108.055H38.3857V73.7909Z" fill="#645F76"/>
<path d="M38.3857 73.7909C38.3857 67.5992 42.3053 62.1022 48.1163 60.1442L64.0011 54.792L79.8859 60.1442C85.6969 62.1022 89.6165 67.5992 89.6165 73.7909V108.055H38.3857V73.7909Z" fill="#645F76"/>
<path opacity="0.34" d="M90 86.7889L57 54.9479L60 50.9046H98.4231V85.4282L90 86.7889Z" fill="#322B4E"/>
</g>
<path d="M52.6162 35.3846C52.6162 29.0971 57.7133 24 64.0008 24C70.2884 24 75.3854 29.0971 75.3854 35.3846V47.1141C75.3854 50.6768 73.7177 54.034 70.8786 56.1863L69.1592 57.4899C66.109 59.8023 61.8926 59.8023 58.8425 57.4899L57.123 56.1863C54.284 54.034 52.6162 50.6768 52.6162 47.1141V35.3846Z" fill="#645F76"/>
<defs>
<linearGradient id="paint0_linear_7_1989" x1="-49" y1="-16.2414" x2="19.0934" y2="125.345" gradientUnits="userSpaceOnUse">
<stop stop-color="#E4E2FC" stop-opacity="0"/>
<stop offset="1" stop-color="#B7B2F8"/>
</linearGradient>
</defs>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.7 KiB

Wyświetl plik

@ -140,7 +140,8 @@ const createGroup = (params: Record<string, any>, shouldReset?: boolean) =>
if (shouldReset) {
dispatch(resetGroupEditor());
}
dispatch(closeModal('MANAGE_GROUP'));
return data;
}).catch(err => dispatch(createGroupFail(err)));
};
@ -797,9 +798,9 @@ const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, get
if (header) params.header = header;
if (groupId === null) {
dispatch(createGroup(params, shouldReset));
return dispatch(createGroup(params, shouldReset));
} else {
dispatch(updateGroup(groupId, params, shouldReset));
return dispatch(updateGroup(groupId, params, shouldReset));
}
};

Wyświetl plik

@ -84,7 +84,10 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
{children}
{isSelected ? (
<Icon src={require('@tabler/icons/check.svg')} className='ml-1 text-primary-500 dark:text-primary-400' />
<Icon
src={require('@tabler/icons/circle-check.svg')}
className='h-4 w-4 text-primary-600 dark:fill-white dark:text-primary-800'
/>
) : null}
</div>
) : null}

Wyświetl plik

@ -11,6 +11,7 @@ const spaces = {
4: 'space-y-4',
5: 'space-y-5',
6: 'space-y-6',
9: 'space-y-9',
10: 'space-y-10',
};

Wyświetl plik

@ -5,6 +5,7 @@ import { submitGroupEditor } from 'soapbox/actions/groups';
import { Modal, Stack } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import ConfirmationStep from './steps/confirmation-step';
import DetailsStep from './steps/details-step';
import PrivacyStep from './steps/privacy-step';
@ -12,16 +13,19 @@ 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' },
});
enum Steps {
ONE = 'ONE',
TWO = 'TWO',
THREE = 'THREE',
}
const manageGroupSteps = {
ONE: PrivacyStep,
TWO: DetailsStep,
THREE: ConfirmationStep,
};
interface IManageGroupModal {
@ -33,21 +37,24 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
const dispatch = useAppDispatch();
const id = useAppSelector((state) => state.group_editor.groupId);
const [group, setGroup] = useState<any | null>(null);
const isSubmitting = useAppSelector((state) => state.group_editor.isSubmitting);
const [currentStep, setCurrentStep] = useState<Steps>(id ? Steps.TWO : Steps.ONE);
const onClickClose = () => {
const handleClose = () => {
onClose('MANAGE_GROUP');
};
const handleSubmit = () => {
dispatch(submitGroupEditor(true));
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);
default:
@ -61,8 +68,15 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
setCurrentStep(Steps.TWO);
break;
case Steps.TWO:
handleSubmit();
onClose();
handleSubmit()
.then((group) => {
setCurrentStep(Steps.THREE);
setGroup(group);
})
.catch(() => {});
break;
case Steps.THREE:
handleClose();
break;
default:
break;
@ -80,10 +94,11 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
confirmationText={confirmationText}
confirmationDisabled={isSubmitting}
confirmationFullWidth
onClose={onClickClose}
onClose={handleClose}
>
<Stack space={2}>
<StepToRender />
{/* @ts-ignore */}
<StepToRender group={group} />
</Stack>
</Modal>
);

Wyświetl plik

@ -0,0 +1,119 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { Avatar, Divider, HStack, Stack, Text, Button } from 'soapbox/components/ui';
interface IConfirmationStep {
group: any
}
const ConfirmationStep: React.FC<IConfirmationStep> = ({ group }) => {
const handleCopyLink = () => {
if (navigator.clipboard) {
navigator.clipboard.writeText(group.uri);
}
};
const handleShare = () => {
navigator.share({
text: group.display_name,
url: group.uri,
}).catch((e) => {
if (e.name !== 'AbortError') console.error(e);
});
};
return (
<Stack space={9}>
<Stack space={3}>
<Stack>
<label
className='dark:sm:shadow-inset relative h-24 w-full cursor-pointer overflow-hidden rounded-lg bg-primary-100 text-primary-500 dark:bg-gray-800 dark:text-accent-blue sm:h-36 sm:shadow'
>
{group.header && <img className='h-full w-full object-cover' src={group.header} alt='' />}
</label>
<label className='mx-auto -mt-10 cursor-pointer rounded-full bg-primary-500 ring-2 ring-white dark:ring-primary-900'>
{group.avatar && <Avatar src={group.avatar} size={80} />}
</label>
</Stack>
<Stack>
<Text size='2xl' weight='bold' align='center'>{group.display_name}</Text>
<Text size='md' className='mx-auto max-w-sm'>{group.note}</Text>
</Stack>
</Stack>
<Divider />
<Stack space={4}>
<Text size='3xl' weight='bold' align='center'>
<FormattedMessage id='manage_group.confirmation.title' defaultMessage='Youre all set!' />
</Text>
<Stack space={5}>
<InfoListItem number={1}>
<FormattedMessage
id='manage_group.confirmation.info_1'
defaultMessage='As the owner of this group, you can assign staff, delete posts and much more.'
/>
</InfoListItem>
<InfoListItem number={2}>
<FormattedMessage
id='manage_group.confirmation.info_2'
defaultMessage="Post the group's first post and get the conversation started."
/>
</InfoListItem>
<InfoListItem number={3}>
<FormattedMessage
id='manage_group.confirmation.info_3'
defaultMessage='Share your new group with friends, family and followers to grow its membership.'
/>
</InfoListItem>
</Stack>
</Stack>
<HStack space={2} justifyContent='center'>
{('share' in navigator) && (
<Button onClick={handleShare} theme='transparent' icon={require('@tabler/icons/share.svg')} className='text-primary-600'>
<FormattedMessage id='manage_group.confirmation.share' defaultMessage='Share this group' />
</Button>
)}
<Button onClick={handleCopyLink} theme='transparent' icon={require('@tabler/icons/link.svg')} className='text-primary-600'>
<FormattedMessage id='manage_group.confirmation.copy' defaultMessage='Copy link' />
</Button>
</HStack>
</Stack>
);
};
interface IInfoListNumber {
number: number
}
const InfoListNumber: React.FC<IInfoListNumber> = ({ number }) => {
return (
<div className='flex h-7 w-7 items-center justify-center rounded-full border border-gray-200'>
<Text theme='primary' size='sm' weight='bold'>{number}</Text>
</div>
);
};
interface IInfoListItem {
number: number
children: React.ReactNode
}
const InfoListItem: React.FC<IInfoListItem> = ({ number, children }) => {
return (
<HStack space={3}>
<div><InfoListNumber number={number} /></div>
<div>{children}</div>
</HStack>
);
};
export default ConfirmationStep;

Wyświetl plik

@ -9,7 +9,7 @@ import {
} 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 } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks';
import { isDefaultAvatar, isDefaultHeader } from 'soapbox/utils/accounts';
import resizeImage from 'soapbox/utils/resize-image';
@ -37,17 +37,17 @@ const HeaderPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }
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={3}
space={1.5}
alignItems='center'
justifyContent='center'
>
<Icon
src={require('@tabler/icons/photo-plus.svg')}
className='h-7 w-7'
className='h-4.5 w-4.5'
/>
<Text size='sm' theme='primary' weight='semibold' transform='uppercase'>
<FormattedMessage id='compose_event.upload_banner' defaultMessage='Upload photo' />
<Text size='md' theme='primary' weight='semibold'>
<FormattedMessage id='group.upload_banner' defaultMessage='Upload photo' />
</Text>
<input
@ -65,8 +65,8 @@ const HeaderPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }
const AvatarPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }) => {
return (
<label className='absolute left-1/2 bottom-0 h-[72px] w-[72px] -translate-x-1/2 translate-y-1/2 cursor-pointer rounded-full bg-primary-500 ring-2 ring-white dark:ring-primary-900'>
{src && <Avatar src={src} size={72} />}
<label className='absolute left-1/2 bottom-0 h-20 w-20 -translate-x-1/2 translate-y-1/2 cursor-pointer rounded-full bg-primary-500 ring-2 ring-white dark:ring-primary-900'>
{src && <Avatar src={src} size={80} />}
<HStack
alignItems='center'
justifyContent='center'
@ -77,7 +77,7 @@ const AvatarPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }
>
<Icon
src={require('@tabler/icons/camera-plus.svg')}
className='h-7 w-7 text-white'
className='h-5 w-5 text-white'
/>
</HStack>
<span className='sr-only'>Upload avatar</span>
@ -96,6 +96,7 @@ const AvatarPicker: React.FC<IMediaInput> = ({ src, onChange, accept, disabled }
const DetailsStep = () => {
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);
@ -146,7 +147,6 @@ const DetailsStep = () => {
});
}, [groupId]);
return (
<Form>
<div className='relative mb-12 flex'>
@ -161,6 +161,7 @@ const DetailsStep = () => {
placeholder={intl.formatMessage(messages.groupNamePlaceholder)}
value={name}
onChange={onChangeName}
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_name']))}
/>
</FormGroup>
<FormGroup
@ -171,6 +172,7 @@ const DetailsStep = () => {
placeholder={intl.formatMessage(messages.groupDescriptionPlaceholder)}
value={description}
onChange={onChangeDescription}
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_description']))}
/>
</FormGroup>
</Form>

Wyświetl plik

@ -18,8 +18,13 @@ const PrivacyStep = () => {
return (
<>
<Stack className='mx-auto max-w-sm' space={2}>
<img
className='mx-auto w-32'
src={require('assets/images/group.svg')}
alt=''
/>
<Text size='3xl' weight='bold' align='center'>
<FormattedMessage id='manage_group.get_started' defaultMessage="Let's get started!" />
<FormattedMessage id='manage_group.get_started' defaultMessage='Lets get started!' />
</Text>
<Text size='lg' theme='muted' align='center'>
<FormattedMessage id='manage_group.tagline' defaultMessage='Groups connect you with others based on shared interests.' />

Wyświetl plik

@ -800,6 +800,7 @@
"group.role.moderator": "Moderator",
"group.tabs.all": "All",
"group.tabs.members": "Members",
"group.upload_banner": "Upload photo",
"group.user_subheading": "Users",
"groups.discover.popular.empty": "Unable to fetch popular groups at this time. Please check back later.",
"groups.discover.popular.title": "Popular Groups",
@ -921,15 +922,22 @@
"login_external.errors.network_fail": "Connection failed. Is a browser extension blocking it?",
"login_form.header": "Sign In",
"manage_group.blocked_members": "Blocked members",
"manage_group.confirmation.copy": "Copy link",
"manage_group.confirmation.info_1": "As the owner of this group, you can assign staff, delete posts and much more.",
"manage_group.confirmation.info_2": "Post the group's first post and get the conversation started.",
"manage_group.confirmation.info_3": "Share your new group with friends, family and followers to grow its membership.",
"manage_group.confirmation.share": "Share this group",
"manage_group.confirmation.title": "Youre all set!",
"manage_group.create": "Create",
"manage_group.delete_group": "Delete group",
"manage_group.done": "Done",
"manage_group.edit_group": "Edit group",
"manage_group.edit_success": "The group was edited",
"manage_group.fields.description_label": "Description",
"manage_group.fields.description_placeholder": "Description",
"manage_group.fields.name_label": "Group name (required)",
"manage_group.fields.name_placeholder": "Group Name",
"manage_group.get_started": "Let's get started!",
"manage_group.get_started": "Lets get started!",
"manage_group.next": "Next",
"manage_group.pending_requests": "Pending requests",
"manage_group.privacy.hint": "These settings cannot be changed later.",

Wyświetl plik

@ -23,6 +23,10 @@ describe('normalizeInstance()', () => {
max_characters: 500,
max_media_attachments: 4,
},
groups: {
max_characters_name: 50,
max_characters_description: 100,
},
},
description: '',
description_limit: 1500,

Wyświetl plik

@ -35,6 +35,10 @@ export const InstanceRecord = ImmutableRecord({
max_characters: 500,
max_media_attachments: 4,
}),
groups: ImmutableMap<string, number>({
max_characters_name: 50,
max_characters_description: 100,
}),
}),
description: '',
description_limit: 1500,

Wyświetl plik

@ -10,7 +10,7 @@ type Permission = typeof PERMISSION_CREATE_GROUPS | typeof PERMISSION_INVITE_USE
export const hasPermission = (state: RootState, permission: Permission) => {
const account = state.accounts_meta.get(state.me as string)!;
if (!account?.role) return false;
if (!account?.role) return true;
const permissions = account.getIn(['role', 'permissions']) as number;