Allow creating events, events list

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-12-14 17:41:16 +01:00
parent 7c4aca51dc
commit 683504c997
10 changed files with 234 additions and 8 deletions

View file

@ -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,
};

View file

@ -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} />}

View file

@ -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>

View 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;

View file

@ -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;

View file

@ -45,7 +45,6 @@ const messages = defineMessages({
cancelEditing: { id: 'confirmations.cancel_editing.confirm', defaultMessage: 'Cancel editing' },
});
interface IAccount {
eventId: string,
id: string,

View file

@ -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;

View file

@ -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 />

View file

@ -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');
}

View file

@ -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);