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 { fetchRelationships } from './accounts';
import { importFetchedGroups, importFetchedAccounts } from './importer'; import { importFetchedGroups, importFetchedAccounts } from './importer';
import { closeModal } from './modals';
import { deleteFromTimelines } from './timelines'; import { deleteFromTimelines } from './timelines';
import type { AxiosError } from 'axios'; 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_MEMBERSHIP_REQUEST_REJECT_FAIL = 'GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL';
const GROUP_EDITOR_TITLE_CHANGE = 'GROUP_EDITOR_TITLE_CHANGE'; 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 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: AppDispatch, getState: () => RootState) => {
dispatch(createGroupRequest()); dispatch(createGroupRequest());
api(getState).post('/api/v1/groups', { display_name: displayName }) api(getState).post('/api/v1/groups', { display_name: displayName, note })
.then(({ data }) => { .then(({ data }) => {
dispatch(importFetchedGroups([data])); dispatch(importFetchedGroups([data]));
dispatch(createGroupSuccess(data)); dispatch(createGroupSuccess(data));
@ -109,6 +111,7 @@ const createGroup = (displayName: string, shouldReset?: boolean) =>
if (shouldReset) { if (shouldReset) {
dispatch(resetGroupEditor()); dispatch(resetGroupEditor());
} }
dispatch(closeModal('MANAGE_GROUP'));
}).catch(err => dispatch(createGroupFail(err))); }).catch(err => dispatch(createGroupFail(err)));
}; };
@ -760,6 +763,11 @@ const changeGroupEditorTitle = (value: string) => ({
value, value,
}); });
const changeGroupEditorDescription = (value: string) => ({
type: GROUP_EDITOR_DESCRIPTION_CHANGE,
value,
});
const resetGroupEditor = () => ({ const resetGroupEditor = () => ({
type: GROUP_EDITOR_RESET, type: GROUP_EDITOR_RESET,
}); });
@ -767,9 +775,10 @@ const resetGroupEditor = () => ({
const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => { const submitGroupEditor = (shouldReset?: boolean) => (dispatch: AppDispatch, getState: () => RootState) => {
const groupId = getState().group_editor.groupId; const groupId = getState().group_editor.groupId;
const displayName = getState().group_editor.displayName; const displayName = getState().group_editor.displayName;
const note = getState().group_editor.note;
if (groupId === null) { if (groupId === null) {
dispatch(createGroup(displayName, shouldReset)); dispatch(createGroup(displayName, note, shouldReset));
} else { } else {
// TODO: dispatch(updateList(listId, title, shouldReset)); // TODO: dispatch(updateList(listId, title, shouldReset));
} }
@ -840,6 +849,7 @@ export {
GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS, GROUP_MEMBERSHIP_REQUEST_REJECT_SUCCESS,
GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL, GROUP_MEMBERSHIP_REQUEST_REJECT_FAIL,
GROUP_EDITOR_TITLE_CHANGE, GROUP_EDITOR_TITLE_CHANGE,
GROUP_EDITOR_DESCRIPTION_CHANGE,
GROUP_EDITOR_RESET, GROUP_EDITOR_RESET,
createGroup, createGroup,
createGroupRequest, createGroupRequest,
@ -926,6 +936,7 @@ export {
rejectGroupMembershipRequestSuccess, rejectGroupMembershipRequestSuccess,
rejectGroupMembershipRequestFail, rejectGroupMembershipRequestFail,
changeGroupEditorTitle, changeGroupEditorTitle,
changeGroupEditorDescription,
resetGroupEditor, resetGroupEditor,
submitGroupEditor, 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 scheduledStatusCount = useAppSelector((state) => state.get('scheduled_statuses').size);
const features = useFeatures(); 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 prevSpoiler = usePrevious(spoiler);
const hasPoll = !!compose.poll; const hasPoll = !!compose.poll;
@ -229,7 +229,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
{features.media && <UploadButtonContainer composeId={id} />} {features.media && <UploadButtonContainer composeId={id} />}
<EmojiPickerDropdown onPickEmoji={handleEmojiPick} /> <EmojiPickerDropdown onPickEmoji={handleEmojiPick} />
{features.polls && <PollButton composeId={id} />} {features.polls && <PollButton composeId={id} />}
{features.privacyScopes && !group && <PrivacyDropdown composeId={id} />} {features.privacyScopes && !group && !groupId && <PrivacyDropdown composeId={id} />}
{features.scheduledStatuses && <ScheduleButton composeId={id} />} {features.scheduledStatuses && <ScheduleButton composeId={id} />}
{features.spoilers && <SpoilerButton composeId={id} />} {features.spoilers && <SpoilerButton composeId={id} />}
{features.richText && <MarkdownButton composeId={id} />} {features.richText && <MarkdownButton composeId={id} />}

View file

@ -1,11 +1,11 @@
import { List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
import StillImage from 'soapbox/components/still-image'; 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 SvgIcon from 'soapbox/components/ui/icon/svg-icon';
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks'; import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
import { normalizeAttachment } from 'soapbox/normalizers'; import { normalizeAttachment } from 'soapbox/normalizers';
@ -83,7 +83,24 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
return menu; 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 menu = makeMenu();
const actionButton = makeActionButton();
return ( return (
<div className='-mt-4 -mx-4'> <div className='-mt-4 -mx-4'>
@ -155,6 +172,8 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
</MenuList> </MenuList>
</Menu> </Menu>
)} )}
{actionButton}
</HStack> </HStack>
</div> </div>
</HStack> </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, EventMapModal,
EventParticipantsModal, EventParticipantsModal,
PolicyModal, PolicyModal,
ManageGroupModal,
} from 'soapbox/features/ui/util/async-components'; } from 'soapbox/features/ui/util/async-components';
import BundleContainer from '../containers/bundle-container'; import BundleContainer from '../containers/bundle-container';
@ -79,6 +80,7 @@ const MODAL_COMPONENTS = {
'EVENT_MAP': EventMapModal, 'EVENT_MAP': EventMapModal,
'EVENT_PARTICIPANTS': EventParticipantsModal, 'EVENT_PARTICIPANTS': EventParticipantsModal,
'POLICY': PolicyModal, 'POLICY': PolicyModal,
'MANAGE_GROUP': ManageGroupModal,
}; };
export type ModalType = keyof typeof MODAL_COMPONENTS | null; 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' }, cancelEditing: { id: 'confirmations.cancel_editing.confirm', defaultMessage: 'Cancel editing' },
}); });
interface IAccount { interface IAccount {
eventId: string, eventId: string,
id: 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, EventInformation,
EventDiscussion, EventDiscussion,
Events, Events,
Groups,
GroupTimeline, GroupTimeline,
} from './util/async-components'; } from './util/async-components';
import { WrappedRoute } from './util/react-router-helpers'; 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} /> <WrappedRoute path='/@:username/events/:statusId/discussion' publicRoute exact page={EventPage} component={EventDiscussion} content={children} />
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' /> <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='/groups/:id' exact page={GroupPage} component={GroupTimeline} content={children} />
<WrappedRoute path='/statuses/new' page={DefaultPage} component={NewStatus} content={children} exact /> <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'); return import(/* webpackChunkName: "features/events" */'../../events');
} }
export function Groups() {
return import(/* webpackChunkName: "features/groups" */'../../groups');
}
export function GroupTimeline() { export function GroupTimeline() {
return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline'); return import(/* webpackChunkName: "features/groups" */'../../group/group-timeline');
} }
@ -549,3 +553,7 @@ export function GroupTimeline() {
export function GroupInfoPanel() { export function GroupInfoPanel() {
return import(/* webpackChunkName: "features/groups" */'../../group/components/group-info-panel'); 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 { import {
GROUP_EDITOR_RESET, GROUP_EDITOR_RESET,
GROUP_EDITOR_TITLE_CHANGE, GROUP_EDITOR_TITLE_CHANGE,
GROUP_EDITOR_DESCRIPTION_CHANGE,
GROUP_CREATE_REQUEST, GROUP_CREATE_REQUEST,
GROUP_CREATE_FAIL, GROUP_CREATE_FAIL,
GROUP_CREATE_SUCCESS, GROUP_CREATE_SUCCESS,
@ -12,12 +13,14 @@ import type { AnyAction } from 'redux';
const ReducerRecord = ImmutableRecord({ const ReducerRecord = ImmutableRecord({
groupId: null as string | null, groupId: null as string | null,
isUploading: false,
isSubmitting: false, isSubmitting: false,
isChanged: false, isChanged: false,
displayName: '', displayName: '',
note: '', note: '',
avatar: null, avatar: null,
header: null, header: null,
locked: false,
}); });
type State = ReturnType<typeof ReducerRecord>; 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('displayName', action.value);
map.set('isChanged', true); 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: case GROUP_CREATE_REQUEST:
return state.withMutations(map => { return state.withMutations(map => {
map.set('isSubmitting', true); map.set('isSubmitting', true);