Allow creating events, events list
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
7c4aca51dc
commit
683504c997
10 changed files with 234 additions and 8 deletions
|
@ -4,6 +4,7 @@ import api, { getLinks } from '../api';
|
|||
|
||||
import { fetchRelationships } from './accounts';
|
||||
import { importFetchedGroups, importFetchedAccounts } from './importer';
|
||||
import { closeModal } from './modals';
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
@ -95,13 +96,14 @@ const GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS = 'GROUP_MEMBERSHIP_REQUEST_REJECT
|
|||
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_RESET = 'GROUP_EDITOR_RESET';
|
||||
|
||||
const createGroup = (displayName: string, shouldReset?: boolean) =>
|
||||
const createGroup = (displayName: string, note: string, shouldReset?: boolean) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
dispatch(createGroupRequest());
|
||||
|
||||
api(getState).post('/api/v1/groups', { display_name: displayName })
|
||||
api(getState).post('/api/v1/groups', { display_name: displayName, note })
|
||||
.then(({ data }) => {
|
||||
dispatch(importFetchedGroups([data]));
|
||||
dispatch(createGroupSuccess(data));
|
||||
|
@ -109,6 +111,7 @@ const createGroup = (displayName: string, shouldReset?: boolean) =>
|
|||
if (shouldReset) {
|
||||
dispatch(resetGroupEditor());
|
||||
}
|
||||
dispatch(closeModal('MANAGE_GROUP'));
|
||||
}).catch(err => dispatch(createGroupFail(err)));
|
||||
};
|
||||
|
||||
|
@ -760,6 +763,11 @@ const changeGroupEditorTitle = (value: string) => ({
|
|||
value,
|
||||
});
|
||||
|
||||
const changeGroupEditorDescription = (value: string) => ({
|
||||
type: GROUP_EDITOR_DESCRIPTION_CHANGE,
|
||||
value,
|
||||
});
|
||||
|
||||
const resetGroupEditor = () => ({
|
||||
type: GROUP_EDITOR_RESET,
|
||||
});
|
||||
|
@ -767,9 +775,10 @@ const resetGroupEditor = () => ({
|
|||
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;
|
||||
|
||||
if (groupId === null) {
|
||||
dispatch(createGroup(displayName, shouldReset));
|
||||
dispatch(createGroup(displayName, note, shouldReset));
|
||||
} else {
|
||||
// TODO: dispatch(updateList(listId, title, shouldReset));
|
||||
}
|
||||
|
@ -840,6 +849,7 @@ export {
|
|||
GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS,
|
||||
GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL,
|
||||
GROUP_EDITOR_TITLE_CHANGE,
|
||||
GROUP_EDITOR_DESCRIPTION_CHANGE,
|
||||
GROUP_EDITOR_RESET,
|
||||
createGroup,
|
||||
createGroupRequest,
|
||||
|
@ -926,6 +936,7 @@ export {
|
|||
rejectGroupMembershipRequestSuccess,
|
||||
rejectGroupMembershipRequestFail,
|
||||
changeGroupEditorTitle,
|
||||
changeGroupEditorDescription,
|
||||
resetGroupEditor,
|
||||
submitGroupEditor,
|
||||
};
|
||||
|
|
|
@ -79,7 +79,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
const scheduledStatusCount = useAppSelector((state) => state.get('scheduled_statuses').size);
|
||||
const features = useFeatures();
|
||||
|
||||
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt } = compose;
|
||||
const { text, suggestions, spoiler, spoiler_text: spoilerText, privacy, focusDate, caretPosition, is_submitting: isSubmitting, is_changing_upload: isChangingUpload, is_uploading: isUploading, schedule: scheduledAt, group_id: groupId } = compose;
|
||||
const prevSpoiler = usePrevious(spoiler);
|
||||
|
||||
const hasPoll = !!compose.poll;
|
||||
|
@ -229,7 +229,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
|||
{features.media && <UploadButtonContainer composeId={id} />}
|
||||
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
|
||||
{features.polls && <PollButton composeId={id} />}
|
||||
{features.privacyScopes && !group && <PrivacyDropdown composeId={id} />}
|
||||
{features.privacyScopes && !group && !groupId && <PrivacyDropdown composeId={id} />}
|
||||
{features.scheduledStatuses && <ScheduleButton composeId={id} />}
|
||||
{features.spoilers && <SpoilerButton composeId={id} />}
|
||||
{features.richText && <MarkdownButton composeId={id} />}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
import { Avatar, HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui';
|
||||
import { Avatar, Button, HStack, IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuLink, MenuList } from 'soapbox/components/ui';
|
||||
import SvgIcon from 'soapbox/components/ui/icon/svg-icon';
|
||||
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||
import { normalizeAttachment } from 'soapbox/normalizers';
|
||||
|
@ -83,7 +83,24 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
return menu;
|
||||
};
|
||||
|
||||
const makeActionButton = () => {
|
||||
if (group.relationship?.role === 'admin') {
|
||||
return (
|
||||
<Button
|
||||
size='sm'
|
||||
theme='primary'
|
||||
// to={`/@${account.acct}/events/${status.id}`}
|
||||
>
|
||||
<FormattedMessage id='group.manage' defaultMessage='Manage' />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const menu = makeMenu();
|
||||
const actionButton = makeActionButton();
|
||||
|
||||
return (
|
||||
<div className='-mt-4 -mx-4'>
|
||||
|
@ -155,6 +172,8 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
|||
</MenuList>
|
||||
</Menu>
|
||||
)}
|
||||
|
||||
{actionButton}
|
||||
</HStack>
|
||||
</div>
|
||||
</HStack>
|
||||
|
|
85
app/soapbox/features/groups/index.tsx
Normal file
85
app/soapbox/features/groups/index.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { fetchGroups } from 'soapbox/actions/groups';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import Icon from 'soapbox/components/icon';
|
||||
import ScrollableList from 'soapbox/components/scrollable-list';
|
||||
import { Button, Column, HStack, Spinner } from 'soapbox/components/ui';
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
const messages = defineMessages({
|
||||
heading: { id: 'column.groups', defaultMessage: 'Groups' },
|
||||
});
|
||||
|
||||
const getOrderedGroups = createSelector([
|
||||
(state: RootState) => state.groups,
|
||||
(state: RootState) => state.group_relationships,
|
||||
], (groups, group_relationships) => {
|
||||
if (!groups) {
|
||||
return groups;
|
||||
}
|
||||
|
||||
return groups.toList().filter(item => !!item && group_relationships.get(item.id)?.member).sort((a, b) => a.display_name.localeCompare(b.display_name));
|
||||
});
|
||||
|
||||
const Lists: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
|
||||
const groups = useAppSelector((state) => getOrderedGroups(state));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchGroups());
|
||||
}, []);
|
||||
|
||||
const onCreateGroup = () => {
|
||||
dispatch(openModal('MANAGE_GROUP'));
|
||||
};
|
||||
|
||||
if (!groups) {
|
||||
return (
|
||||
<Column>
|
||||
<Spinner />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
const emptyMessage = <FormattedMessage id='empty_column.groups' defaultMessage='You are not in any group yet. When you join one, it will show up here.' />;
|
||||
|
||||
return (
|
||||
<Column label={intl.formatMessage(messages.heading)}>
|
||||
<HStack>
|
||||
<Button
|
||||
className='ml-auto'
|
||||
theme='primary'
|
||||
size='sm'
|
||||
onClick={onCreateGroup}
|
||||
>
|
||||
<FormattedMessage id='groups.create_group' defaultMessage='Create group' />
|
||||
</Button>
|
||||
</HStack>
|
||||
<div className='space-y-4'>
|
||||
<ScrollableList
|
||||
scrollKey='lists'
|
||||
emptyMessage={emptyMessage}
|
||||
itemClassName='py-2'
|
||||
>
|
||||
{groups.map((group: any) => (
|
||||
<Link key={group.id} to={`/groups/${group.id}`} className='flex items-center gap-1.5 p-2 text-gray-900 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg'>
|
||||
<Icon src={require('@tabler/icons/users.svg')} fixedWidth />
|
||||
<span className='flex-grow' dangerouslySetInnerHTML={{ __html: group.display_name_html }} />
|
||||
</Link>
|
||||
))}
|
||||
</ScrollableList>
|
||||
</div>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default Lists;
|
|
@ -36,6 +36,7 @@ import {
|
|||
EventMapModal,
|
||||
EventParticipantsModal,
|
||||
PolicyModal,
|
||||
ManageGroupModal,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
import BundleContainer from '../containers/bundle-container';
|
||||
|
@ -79,6 +80,7 @@ const MODAL_COMPONENTS = {
|
|||
'EVENT_MAP': EventMapModal,
|
||||
'EVENT_PARTICIPANTS': EventParticipantsModal,
|
||||
'POLICY': PolicyModal,
|
||||
'MANAGE_GROUP': ManageGroupModal,
|
||||
};
|
||||
|
||||
export type ModalType = keyof typeof MODAL_COMPONENTS | null;
|
||||
|
|
|
@ -45,7 +45,6 @@ const messages = defineMessages({
|
|||
cancelEditing: { id: 'confirmations.cancel_editing.confirm', defaultMessage: 'Cancel editing' },
|
||||
});
|
||||
|
||||
|
||||
interface IAccount {
|
||||
eventId: string,
|
||||
id: string,
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import {
|
||||
changeGroupEditorTitle,
|
||||
changeGroupEditorDescription,
|
||||
submitGroupEditor,
|
||||
} from 'soapbox/actions/groups';
|
||||
import { Form, FormGroup, Input, Modal, Stack, Textarea } from 'soapbox/components/ui';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
groupNamePlaceholder: { id: 'manage_group.fields.name_placeholder', defaultMessage: 'Name' },
|
||||
groupDescriptionPlaceholder: { id: 'manage_group.fields.description_placeholder', defaultMessage: 'Description' },
|
||||
});
|
||||
|
||||
interface IManageGroupModal {
|
||||
onClose: (type?: string) => void,
|
||||
}
|
||||
|
||||
const ManageGroupModal: React.FC<IManageGroupModal> = ({ onClose }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const name = useAppSelector((state) => state.group_editor.displayName);
|
||||
const description = useAppSelector((state) => state.group_editor.note);
|
||||
|
||||
const id = useAppSelector((state) => state.group_editor.groupId);
|
||||
|
||||
const isSubmitting = useAppSelector((state) => state.group_editor.isSubmitting);
|
||||
|
||||
const onChangeName: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
||||
dispatch(changeGroupEditorTitle(target.value));
|
||||
};
|
||||
|
||||
const onChangeDescription: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) => {
|
||||
dispatch(changeGroupEditorDescription(target.value));
|
||||
};
|
||||
|
||||
const onClickClose = () => {
|
||||
onClose('manage_group');
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
dispatch(submitGroupEditor(true));
|
||||
};
|
||||
|
||||
const body = (
|
||||
<Form>
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='manage_group.fields.name_label' defaultMessage='Group name' />}
|
||||
>
|
||||
<Input
|
||||
type='text'
|
||||
placeholder={intl.formatMessage(messages.groupNamePlaceholder)}
|
||||
value={name}
|
||||
onChange={onChangeName}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='manage_group.fields.description_label' defaultMessage='Group description' />}
|
||||
>
|
||||
<Textarea
|
||||
autoComplete='off'
|
||||
placeholder={intl.formatMessage(messages.groupDescriptionPlaceholder)}
|
||||
value={description}
|
||||
onChange={onChangeDescription}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={id
|
||||
? <FormattedMessage id='navigation_bar.manage_group' defaultMessage='Manage group' />
|
||||
: <FormattedMessage id='navigation_bar.create_group' defaultMessage='Create new group' />}
|
||||
confirmationAction={handleSubmit}
|
||||
confirmationText={id
|
||||
? <FormattedMessage id='manage_group.update' defaultMessage='Update' />
|
||||
: <FormattedMessage id='manage_group.create' defaultMessage='Create' />}
|
||||
confirmationDisabled={isSubmitting}
|
||||
onClose={onClickClose}
|
||||
>
|
||||
<Stack space={2}>
|
||||
{body}
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageGroupModal;
|
|
@ -112,6 +112,7 @@ import {
|
|||
EventInformation,
|
||||
EventDiscussion,
|
||||
Events,
|
||||
Groups,
|
||||
GroupTimeline,
|
||||
} from './util/async-components';
|
||||
import { WrappedRoute } from './util/react-router-helpers';
|
||||
|
@ -274,6 +275,7 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
<WrappedRoute path='/@:username/events/:statusId/discussion' publicRoute exact page={EventPage} component={EventDiscussion} content={children} />
|
||||
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
|
||||
|
||||
<WrappedRoute path='/groups' exact page={DefaultPage} component={Groups} content={children} />
|
||||
<WrappedRoute path='/groups/:id' exact page={GroupPage} component={GroupTimeline} content={children} />
|
||||
|
||||
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact />
|
||||
|
|
|
@ -542,6 +542,10 @@ export function Events() {
|
|||
return import(/* webpackChunkName: "features/events" */'../../events');
|
||||
}
|
||||
|
||||
export function Groups() {
|
||||
return import(/* webpackChunkName: "features/groups" */'../../groups');
|
||||
}
|
||||
|
||||
export function GroupTimeline() {
|
||||
return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline');
|
||||
}
|
||||
|
@ -549,3 +553,7 @@ export function GroupTimeline() {
|
|||
export function GroupInfoPanel() {
|
||||
return import(/* webpackChunkName: "features/groups" */'../../group/components/group-info-panel');
|
||||
}
|
||||
|
||||
export function ManageGroupModal() {
|
||||
return import(/* webpackChunkName: "features/manage_group_modal" */'../components/modals/manage-group-modal/manage-group-modal');
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { Record as ImmutableRecord } from 'immutable';
|
|||
import {
|
||||
GROUP_EDITOR_RESET,
|
||||
GROUP_EDITOR_TITLE_CHANGE,
|
||||
GROUP_EDITOR_DESCRIPTION_CHANGE,
|
||||
GROUP_CREATE_REQUEST,
|
||||
GROUP_CREATE_FAIL,
|
||||
GROUP_CREATE_SUCCESS,
|
||||
|
@ -12,12 +13,14 @@ import type { AnyAction } from 'redux';
|
|||
|
||||
const ReducerRecord = ImmutableRecord({
|
||||
groupId: null as string | null,
|
||||
isUploading: false,
|
||||
isSubmitting: false,
|
||||
isChanged: false,
|
||||
displayName: '',
|
||||
note: '',
|
||||
avatar: null,
|
||||
header: null,
|
||||
locked: false,
|
||||
});
|
||||
|
||||
type State = ReturnType<typeof ReducerRecord>;
|
||||
|
@ -31,6 +34,11 @@ export default function groupEditor(state: State = ReducerRecord(), action: AnyA
|
|||
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_CREATE_REQUEST:
|
||||
return state.withMutations(map => {
|
||||
map.set('isSubmitting', true);
|
||||
|
|
Loading…
Reference in a new issue