Merge branch 'group-modal' into 'develop'
Group modal See merge request soapbox-pub/soapbox!2408
This commit is contained in:
commit
21af38c13d
25 changed files with 255 additions and 538 deletions
|
@ -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<string, any>, 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<string, any>, 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<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 {
|
||||
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,
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ function useCreateEntity<TEntity extends Entity = Entity, Data = unknown>(
|
|||
) {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [isLoading, setPromise] = useLoading();
|
||||
const [isSubmitting, setPromise] = useLoading();
|
||||
const { entityType, listKey } = parseEntitiesPath(expandedPath);
|
||||
|
||||
async function createEntity(data: Data, callbacks: EntityCallbacks<TEntity> = {}): Promise<void> {
|
||||
|
@ -44,7 +44,7 @@ function useCreateEntity<TEntity extends Entity = Entity, Data = unknown>(
|
|||
|
||||
return {
|
||||
createEntity,
|
||||
isLoading,
|
||||
isSubmitting,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string> = {}): Promise<void> {
|
||||
// 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -24,16 +24,16 @@ function useEntityActions<TEntity extends Entity = Entity, Data = any>(
|
|||
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<TEntity, Data>(path, (data) => api.post(endpoints.post!, data), opts);
|
||||
|
||||
return {
|
||||
createEntity,
|
||||
deleteEntity,
|
||||
isLoading: createLoading || deleteLoading,
|
||||
isSubmitting: createSubmitting || deleteSubmitting,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -94,7 +94,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
|||
<Button
|
||||
theme='primary'
|
||||
onClick={onJoinGroup}
|
||||
disabled={joinGroup.isLoading}
|
||||
disabled={joinGroup.isSubmitting}
|
||||
>
|
||||
{group.locked
|
||||
? <FormattedMessage id='group.join.private' defaultMessage='Request Access' />
|
||||
|
@ -108,7 +108,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
|||
<Button
|
||||
theme='secondary'
|
||||
onClick={onCancelRequest}
|
||||
disabled={cancelRequest.isLoading}
|
||||
disabled={cancelRequest.isSubmitting}
|
||||
>
|
||||
<FormattedMessage id='group.cancel_request' defaultMessage='Cancel Request' />
|
||||
</Button>
|
||||
|
@ -119,7 +119,7 @@ const GroupActionButton = ({ group }: IGroupActionButton) => {
|
|||
<Button
|
||||
theme='secondary'
|
||||
onClick={onLeaveGroup}
|
||||
disabled={leaveGroup.isLoading}
|
||||
disabled={leaveGroup.isSubmitting}
|
||||
>
|
||||
<FormattedMessage id='group.leave' defaultMessage='Leave Group' />
|
||||
</Button>
|
||||
|
|
|
@ -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<HTMLInputElement>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const AvatarPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ src, onChange, accept, disabled }, ref) => {
|
||||
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-2 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
|
||||
ref={ref}
|
||||
name='avatar'
|
||||
type='file'
|
||||
accept={accept}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
className='hidden'
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
});
|
||||
|
||||
export default AvatarPicker;
|
|
@ -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<HTMLInputElement>
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
const HeaderPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ src, onChange, accept, disabled }, ref) => {
|
||||
return (
|
||||
<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'
|
||||
>
|
||||
{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'>
|
||||
<FormattedMessage id='group.upload_banner' defaultMessage='Upload photo' />
|
||||
</Text>
|
||||
|
||||
<input
|
||||
ref={ref}
|
||||
name='header'
|
||||
type='file'
|
||||
accept={accept}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
className='hidden'
|
||||
/>
|
||||
</HStack>
|
||||
</label>
|
||||
);
|
||||
});
|
||||
|
||||
export default HeaderPicker;
|
|
@ -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<HTMLInputElement>
|
||||
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<HTMLInputElement, IMediaInput>(({ src, onChange, accept, disabled }, ref) => {
|
||||
return (
|
||||
<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'
|
||||
>
|
||||
{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'>
|
||||
<FormattedMessage id='group.upload_banner' defaultMessage='Upload photo' />
|
||||
</Text>
|
||||
|
||||
<input
|
||||
ref={ref}
|
||||
name='header'
|
||||
type='file'
|
||||
accept={accept}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
className='hidden'
|
||||
/>
|
||||
</HStack>
|
||||
</label>
|
||||
);
|
||||
});
|
||||
|
||||
const AvatarPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ src, onChange, accept, disabled }, ref) => {
|
||||
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-2 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
|
||||
ref={ref}
|
||||
name='avatar'
|
||||
type='file'
|
||||
accept={accept}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
className='hidden'
|
||||
/>
|
||||
</label>
|
||||
);
|
||||
});
|
||||
|
||||
interface IEditGroup {
|
||||
params: {
|
||||
id: string
|
||||
|
|
|
@ -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 = () => (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<IManageGroupModal> = ({ onClose }) => {
|
||||
const CreateGroupModal: React.FC<ICreateGroupModal> = ({ onClose }) => {
|
||||
const intl = useIntl();
|
||||
const debounce = useDebounce;
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const id = useAppSelector((state) => state.group_editor.groupId);
|
||||
const [group, setGroup] = useState<any | null>(null);
|
||||
const [group, setGroup] = useState<Group | null>(null);
|
||||
const [params, setParams] = useState<CreateGroupParams>({});
|
||||
const [currentStep, setCurrentStep] = useState<Steps>(Steps.ONE);
|
||||
|
||||
const isSubmitting = useAppSelector((state) => state.group_editor.isSubmitting);
|
||||
|
||||
const [currentStep, setCurrentStep] = useState<Steps>(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<IManageGroupModal> = ({ 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<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 (
|
||||
<Modal
|
||||
title={id
|
||||
? <FormattedMessage id='navigation_bar.edit_group' defaultMessage='Edit Group' />
|
||||
: <FormattedMessage id='navigation_bar.create_group' defaultMessage='Create Group' />}
|
||||
title={<FormattedMessage id='navigation_bar.create_group' defaultMessage='Create Group' />}
|
||||
confirmationAction={handleNextStep}
|
||||
confirmationText={confirmationText}
|
||||
confirmationDisabled={isSubmitting || (currentStep === Steps.TWO && !isValid)}
|
||||
|
@ -104,11 +106,10 @@ const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
|
|||
onClose={handleClose}
|
||||
>
|
||||
<Stack space={2}>
|
||||
{/* @ts-ignore */}
|
||||
<StepToRender group={group} />
|
||||
{renderStep()}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageGroupModal;
|
||||
export default CreateGroupModal;
|
|
@ -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<HTMLInputElement>
|
||||
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<IMediaInput> = ({ src, onChange, accept, disabled }) => {
|
||||
return (
|
||||
<label
|
||||
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'
|
||||
/>
|
||||
interface IDetailsStep {
|
||||
params: CreateGroupParams
|
||||
onChange(params: CreateGroupParams): void
|
||||
}
|
||||
|
||||
<Text size='md' theme='primary' weight='semibold'>
|
||||
<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 DetailsStep: React.FC<IDetailsStep> = ({ 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<string | null>(null);
|
||||
const [headerSrc, setHeaderSrc] = useState<string | null>(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<string>,
|
||||
)?.filter(type => type.startsWith('image/')).toArray().join(',');
|
||||
|
||||
const onChangeName: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
||||
dispatch(changeGroupEditorTitle(target.value));
|
||||
const handleTextChange = (property: keyof CreateGroupParams): React.ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> => {
|
||||
return (e) => {
|
||||
onChange({
|
||||
...params,
|
||||
[property]: e.target.value,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
const onChangeDescription: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) => {
|
||||
dispatch(changeGroupEditorDescription(target.value));
|
||||
};
|
||||
|
||||
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = 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);
|
||||
const handleImageChange = (property: keyof CreateGroupParams, maxPixels?: number): React.ChangeEventHandler<HTMLInputElement> => {
|
||||
return async ({ target: { files } }) => {
|
||||
const file = files ? files[0] : undefined;
|
||||
if (file) {
|
||||
const resized = await resizeImage(file, maxPixels);
|
||||
onChange({
|
||||
...params,
|
||||
[property]: resized,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<Form>
|
||||
<div className='relative mb-12 flex'>
|
||||
<HeaderPicker src={headerSrc} accept={attachmentTypes} onChange={handleFileChange} disabled={isUploading} />
|
||||
<AvatarPicker src={avatarSrc} accept={attachmentTypes} onChange={handleFileChange} disabled={isUploading} />
|
||||
<HeaderPicker src={headerSrc} accept={attachmentTypes} onChange={handleImageChange('header', 1920 * 1080)} />
|
||||
<AvatarPicker src={avatarSrc} accept={attachmentTypes} onChange={handleImageChange('avatar', 400 * 400)} />
|
||||
</div>
|
||||
|
||||
<FormGroup
|
||||
|
@ -167,8 +78,8 @@ const DetailsStep = () => {
|
|||
<Input
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(messages.groupNamePlaceholder)}
|
||||
value={name}
|
||||
onChange={onChangeName}
|
||||
value={displayName}
|
||||
onChange={handleTextChange('display_name')}
|
||||
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_name']))}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
@ -179,8 +90,8 @@ const DetailsStep = () => {
|
|||
<Textarea
|
||||
autoComplete='off'
|
||||
placeholder={intl.formatMessage(messages.groupDescriptionPlaceholder)}
|
||||
value={description}
|
||||
onChange={onChangeDescription}
|
||||
value={note}
|
||||
onChange={handleTextChange('note')}
|
||||
maxLength={Number(instance.configuration.getIn(['groups', 'max_characters_description']))}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { changeGroupEditorPrivacy } from 'soapbox/actions/groups';
|
||||
import List, { ListItem } from 'soapbox/components/list';
|
||||
import { Form, FormGroup, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
import { type CreateGroupParams } from 'soapbox/hooks/api';
|
||||
|
||||
const PrivacyStep = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
interface IPrivacyStep {
|
||||
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) => {
|
||||
dispatch(changeGroupEditorPrivacy(value));
|
||||
const onChangePrivacy = (group_visibility: CreateGroupParams['group_visibility']) => {
|
||||
onChange({ ...params, group_visibility });
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -33,15 +35,15 @@ const PrivacyStep = () => {
|
|||
<ListItem
|
||||
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.' />}
|
||||
onSelect={() => onChangePrivacy(false)}
|
||||
isSelected={!locked}
|
||||
onSelect={() => onChangePrivacy('everyone')}
|
||||
isSelected={visibility === 'everyone'}
|
||||
/>
|
||||
|
||||
<ListItem
|
||||
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.' />}
|
||||
onSelect={() => onChangePrivacy(true)}
|
||||
isSelected={locked}
|
||||
onSelect={() => onChangePrivacy('members_only')}
|
||||
isSelected={visibility === 'members_only'}
|
||||
/>
|
||||
</List>
|
||||
</FormGroup>
|
||||
|
|
|
@ -12,7 +12,7 @@ const NewGroupPanel = () => {
|
|||
const canCreateGroup = useAppSelector((state) => hasPermission(state, PERMISSION_CREATE_GROUPS));
|
||||
|
||||
const createGroup = () => {
|
||||
dispatch(openModal('MANAGE_GROUP'));
|
||||
dispatch(openModal('CREATE_GROUP'));
|
||||
};
|
||||
|
||||
if (!canCreateGroup) return null;
|
||||
|
|
|
@ -590,8 +590,8 @@ export function GroupMembershipRequests() {
|
|||
return import(/* webpackChunkName: "features/groups" */'../../group/group-membership-requests');
|
||||
}
|
||||
|
||||
export function ManageGroupModal() {
|
||||
return import(/* webpackChunkName: "features/manage_group_modal" */'../components/modals/manage-group-modal/manage-group-modal');
|
||||
export function CreateGroupModal() {
|
||||
return import(/* webpackChunkName: "features/groups" */'../components/modals/manage-group-modal/create-group-modal');
|
||||
}
|
||||
|
||||
export function NewGroupPanel() {
|
||||
|
|
|
@ -8,14 +8,14 @@ function useCancelMembershipRequest(group: Group) {
|
|||
const api = useApi();
|
||||
const me = useOwnAccount();
|
||||
|
||||
const { createEntity, isLoading } = useCreateEntity(
|
||||
const { createEntity, isSubmitting } = useCreateEntity(
|
||||
[Entities.GROUP_RELATIONSHIPS],
|
||||
() => api.post(`/api/v1/groups/${group.id}/membership_requests/${me?.id}/reject`),
|
||||
);
|
||||
|
||||
return {
|
||||
mutate: createEntity,
|
||||
isLoading,
|
||||
isSubmitting,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
33
app/soapbox/hooks/api/groups/useCreateGroup.ts
Normal file
33
app/soapbox/hooks/api/groups/useCreateGroup.ts
Normal file
|
@ -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, 'search', ''], (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 };
|
|
@ -4,14 +4,14 @@ import { useEntityActions } from 'soapbox/entity-store/hooks';
|
|||
import type { Group } from 'soapbox/schemas';
|
||||
|
||||
function useDeleteGroup() {
|
||||
const { deleteEntity, isLoading } = useEntityActions<Group>(
|
||||
const { deleteEntity, isSubmitting } = useEntityActions<Group>(
|
||||
[Entities.GROUPS],
|
||||
{ delete: '/api/v1/groups/:id' },
|
||||
);
|
||||
|
||||
return {
|
||||
mutate: deleteEntity,
|
||||
isLoading,
|
||||
isSubmitting,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { Group, GroupRelationship } from 'soapbox/schemas';
|
|||
function useJoinGroup(group: Group) {
|
||||
const { invalidate } = useGroups();
|
||||
|
||||
const { createEntity, isLoading } = useEntityActions<GroupRelationship>(
|
||||
const { createEntity, isSubmitting } = useEntityActions<GroupRelationship>(
|
||||
[Entities.GROUP_RELATIONSHIPS, group.id],
|
||||
{ post: `/api/v1/groups/${group.id}/join` },
|
||||
{ schema: groupRelationshipSchema },
|
||||
|
@ -17,7 +17,7 @@ function useJoinGroup(group: Group) {
|
|||
|
||||
return {
|
||||
mutate: createEntity,
|
||||
isLoading,
|
||||
isSubmitting,
|
||||
invalidate,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { Group, GroupRelationship } from 'soapbox/schemas';
|
|||
function useLeaveGroup(group: Group) {
|
||||
const { invalidate } = useGroups();
|
||||
|
||||
const { createEntity, isLoading } = useEntityActions<GroupRelationship>(
|
||||
const { createEntity, isSubmitting } = useEntityActions<GroupRelationship>(
|
||||
[Entities.GROUP_RELATIONSHIPS, group.id],
|
||||
{ post: `/api/v1/groups/${group.id}/leave` },
|
||||
{ schema: groupRelationshipSchema },
|
||||
|
@ -17,7 +17,7 @@ function useLeaveGroup(group: Group) {
|
|||
|
||||
return {
|
||||
mutate: createEntity,
|
||||
isLoading,
|
||||
isSubmitting,
|
||||
invalidate,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export { useAccount } from './useAccount';
|
|||
*/
|
||||
export { useBlockGroupMember } from './groups/useBlockGroupMember';
|
||||
export { useCancelMembershipRequest } from './groups/useCancelMembershipRequest';
|
||||
export { useCreateGroup, type CreateGroupParams } from './groups/useCreateGroup';
|
||||
export { useDeleteGroup } from './groups/useDeleteGroup';
|
||||
export { useDemoteGroupMember } from './groups/useDemoteGroupMember';
|
||||
export { useGroup, useGroups } from './groups/useGroups';
|
||||
|
|
|
@ -945,7 +945,6 @@
|
|||
"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.cannot_change_hint": "This cannot be changed after the group is created.",
|
||||
"manage_group.fields.description_label": "Description",
|
||||
"manage_group.fields.description_placeholder": "Description",
|
||||
|
@ -962,9 +961,7 @@
|
|||
"manage_group.privacy.private.label": "Private (Owner approval required)",
|
||||
"manage_group.privacy.public.hint": "Discoverable. Anyone can join.",
|
||||
"manage_group.privacy.public.label": "Public",
|
||||
"manage_group.submit_success": "The group was created",
|
||||
"manage_group.tagline": "Groups connect you with others based on shared interests.",
|
||||
"manage_group.update": "Update",
|
||||
"media_panel.empty_message": "No media found.",
|
||||
"media_panel.title": "Media",
|
||||
"mfa.confirm.success_message": "MFA confirmed",
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
import { Record as ImmutableRecord } from 'immutable';
|
||||
|
||||
import {
|
||||
GROUP_EDITOR_RESET,
|
||||
GROUP_EDITOR_TITLE_CHANGE,
|
||||
GROUP_EDITOR_DESCRIPTION_CHANGE,
|
||||
GROUP_EDITOR_PRIVACY_CHANGE,
|
||||
GROUP_EDITOR_MEDIA_CHANGE,
|
||||
GROUP_CREATE_REQUEST,
|
||||
GROUP_CREATE_FAIL,
|
||||
GROUP_CREATE_SUCCESS,
|
||||
GROUP_UPDATE_REQUEST,
|
||||
GROUP_UPDATE_FAIL,
|
||||
GROUP_UPDATE_SUCCESS,
|
||||
GROUP_EDITOR_SET,
|
||||
} from 'soapbox/actions/groups';
|
||||
|
||||
import type { AnyAction } from 'redux';
|
||||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
groupId: null as string | null,
|
||||
progress: 0,
|
||||
isUploading: false,
|
||||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
displayName: '',
|
||||
note: '',
|
||||
avatar: null as File | null,
|
||||
header: null as File | null,
|
||||
locked: false,
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
||||
export default function groupEditor(state: State = ReducerRecord(), action: AnyAction) {
|
||||
switch (action.type) {
|
||||
case GROUP_EDITOR_RESET:
|
||||
return ReducerRecord();
|
||||
case GROUP_EDITOR_SET:
|
||||
return state.withMutations(map => {
|
||||
map.set('groupId', action.group.id);
|
||||
map.set('displayName', action.group.display_name);
|
||||
map.set('note', action.group.note);
|
||||
});
|
||||
case GROUP_EDITOR_TITLE_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('displayName', action.value);
|
||||
map.set('isChanged', true);
|
||||
});
|
||||
case GROUP_EDITOR_DESCRIPTION_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('note', action.value);
|
||||
map.set('isChanged', true);
|
||||
});
|
||||
case GROUP_EDITOR_PRIVACY_CHANGE:
|
||||
return state.withMutations(map => {
|
||||
map.set('locked', action.value);
|
||||
map.set('isChanged', true);
|
||||
});
|
||||
case GROUP_EDITOR_MEDIA_CHANGE:
|
||||
return state.set(action.mediaType, action.value);
|
||||
case GROUP_CREATE_REQUEST:
|
||||
case GROUP_UPDATE_REQUEST:
|
||||
return state.withMutations(map => {
|
||||
map.set('isSubmitting', true);
|
||||
map.set('isChanged', false);
|
||||
});
|
||||
case GROUP_CREATE_FAIL:
|
||||
case GROUP_UPDATE_FAIL:
|
||||
return state.set('isSubmitting', false);
|
||||
case GROUP_CREATE_SUCCESS:
|
||||
case GROUP_UPDATE_SUCCESS:
|
||||
return state.withMutations(map => {
|
||||
map.set('isSubmitting', false);
|
||||
map.set('groupId', action.group.id);
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
|
@ -28,7 +28,6 @@ import custom_emojis from './custom-emojis';
|
|||
import domain_lists from './domain-lists';
|
||||
import dropdown_menu from './dropdown-menu';
|
||||
import filters from './filters';
|
||||
import group_editor from './group-editor';
|
||||
import group_memberships from './group-memberships';
|
||||
import group_relationships from './group-relationships';
|
||||
import groups from './groups';
|
||||
|
@ -93,7 +92,6 @@ const reducers = {
|
|||
dropdown_menu,
|
||||
entities,
|
||||
filters,
|
||||
group_editor,
|
||||
group_memberships,
|
||||
group_relationships,
|
||||
groups,
|
||||
|
|
Loading…
Reference in a new issue