Manage event

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-09-25 23:18:11 +02:00
parent fe7333ddb0
commit 04b4a57e06
15 changed files with 587 additions and 304 deletions

View File

@ -6,8 +6,13 @@ import resizeImage from 'soapbox/utils/resize_image';
import { importFetchedAccounts, importFetchedStatus } from './importer'; import { importFetchedAccounts, importFetchedStatus } from './importer';
import { fetchMedia, uploadMedia } from './media'; import { fetchMedia, uploadMedia } from './media';
import { closeModal } from './modals'; import { closeModal, openModal } from './modals';
import snackbar from './snackbar'; import snackbar from './snackbar';
import {
STATUS_FETCH_SOURCE_FAIL,
STATUS_FETCH_SOURCE_REQUEST,
STATUS_FETCH_SOURCE_SUCCESS,
} from './statuses';
import type { AxiosError } from 'axios'; import type { AxiosError } from 'axios';
import type { AppDispatch, RootState } from 'soapbox/store'; import type { AppDispatch, RootState } from 'soapbox/store';
@ -17,13 +22,13 @@ const LOCATION_SEARCH_REQUEST = 'LOCATION_SEARCH_REQUEST';
const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS'; const LOCATION_SEARCH_SUCCESS = 'LOCATION_SEARCH_SUCCESS';
const LOCATION_SEARCH_FAIL = 'LOCATION_SEARCH_FAIL'; const LOCATION_SEARCH_FAIL = 'LOCATION_SEARCH_FAIL';
const CREATE_EVENT_NAME_CHANGE = 'CREATE_EVENT_NAME_CHANGE'; const EDIT_EVENT_NAME_CHANGE = 'EDIT_EVENT_NAME_CHANGE';
const CREATE_EVENT_DESCRIPTION_CHANGE = 'CREATE_EVENT_DESCRIPTION_CHANGE'; const EDIT_EVENT_DESCRIPTION_CHANGE = 'EDIT_EVENT_DESCRIPTION_CHANGE';
const CREATE_EVENT_START_TIME_CHANGE = 'CREATE_EVENT_START_TIME_CHANGE'; const EDIT_EVENT_START_TIME_CHANGE = 'EDIT_EVENT_START_TIME_CHANGE';
const CREATE_EVENT_HAS_END_TIME_CHANGE = 'CREATE_EVENT_HAS_END_TIME_CHANGE'; const EDIT_EVENT_HAS_END_TIME_CHANGE = 'EDIT_EVENT_HAS_END_TIME_CHANGE';
const CREATE_EVENT_END_TIME_CHANGE = 'CREATE_EVENT_END_TIME_CHANGE'; const EDIT_EVENT_END_TIME_CHANGE = 'EDIT_EVENT_END_TIME_CHANGE';
const CREATE_EVENT_APPROVAL_REQUIRED_CHANGE = 'CREATE_EVENT_APPROVAL_REQUIRED_CHANGE'; const EDIT_EVENT_APPROVAL_REQUIRED_CHANGE = 'EDIT_EVENT_APPROVAL_REQUIRED_CHANGE';
const CREATE_EVENT_LOCATION_CHANGE = 'CREATE_EVENT_LOCATION_CHANGE'; const EDIT_EVENT_LOCATION_CHANGE = 'EDIT_EVENT_LOCATION_CHANGE';
const EVENT_BANNER_UPLOAD_REQUEST = 'EVENT_BANNER_UPLOAD_REQUEST'; const EVENT_BANNER_UPLOAD_REQUEST = 'EVENT_BANNER_UPLOAD_REQUEST';
const EVENT_BANNER_UPLOAD_PROGRESS = 'EVENT_BANNER_UPLOAD_PROGRESS'; const EVENT_BANNER_UPLOAD_PROGRESS = 'EVENT_BANNER_UPLOAD_PROGRESS';
@ -59,14 +64,28 @@ const EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST = 'EVENT_PARTICIPATION_REQUEST
const EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS'; const EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS';
const EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL'; const EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL = 'EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL';
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST';
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS';
const EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL = 'EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL';
const EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST = 'EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST';
const EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS = 'EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS';
const EVENT_PARTICIPATION_REQUEST_REJECT_FAIL = 'EVENT_PARTICIPATION_REQUEST_REJECT_FAIL';
const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL';
const EVENT_FORM_SET = 'EVENT_FORM_SET';
const noOp = () => new Promise(f => f(undefined)); const noOp = () => new Promise(f => f(undefined));
const messages = defineMessages({ const messages = defineMessages({
exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' }, exceededImageSizeLimit: { id: 'upload_error.image_size_limit', defaultMessage: 'Image exceeds the current file size limit ({limit})' },
success: { id: 'create_event.submit_success', defaultMessage: 'Your event was created' }, success: { id: 'compose_event.submit_success', defaultMessage: 'Your event was created' },
joinSuccess: { id: 'join_event.success', defaultMessage: 'Joined the event' }, joinSuccess: { id: 'join_event.success', defaultMessage: 'Joined the event' },
joinRequestSuccess: { id: 'join_event.request_success', defaultMessage: 'Requested to join the event' }, joinRequestSuccess: { id: 'join_event.request_success', defaultMessage: 'Requested to join the event' },
view: { id: 'snackbar.view', defaultMessage: 'View' }, view: { id: 'snackbar.view', defaultMessage: 'View' },
authorized: { id: 'compose_event.participation_requests.authorize_success', defaultMessage: 'User accepted' },
rejected: { id: 'compose_event.participation_requests.reject_success', defaultMessage: 'User rejected' },
}); });
const locationSearch = (query: string, signal?: AbortSignal) => const locationSearch = (query: string, signal?: AbortSignal) =>
@ -81,37 +100,37 @@ const locationSearch = (query: string, signal?: AbortSignal) =>
}); });
}; };
const changeCreateEventName = (value: string) => ({ const changeEditEventName = (value: string) => ({
type: CREATE_EVENT_NAME_CHANGE, type: EDIT_EVENT_NAME_CHANGE,
value, value,
}); });
const changeCreateEventDescription = (value: string) => ({ const changeEditEventDescription = (value: string) => ({
type: CREATE_EVENT_DESCRIPTION_CHANGE, type: EDIT_EVENT_DESCRIPTION_CHANGE,
value, value,
}); });
const changeCreateEventStartTime = (value: Date) => ({ const changeEditEventStartTime = (value: Date) => ({
type: CREATE_EVENT_START_TIME_CHANGE, type: EDIT_EVENT_START_TIME_CHANGE,
value, value,
}); });
const changeCreateEventEndTime = (value: Date) => ({ const changeEditEventEndTime = (value: Date) => ({
type: CREATE_EVENT_END_TIME_CHANGE, type: EDIT_EVENT_END_TIME_CHANGE,
value, value,
}); });
const changeCreateEventHasEndTime = (value: boolean) => ({ const changeEditEventHasEndTime = (value: boolean) => ({
type: CREATE_EVENT_HAS_END_TIME_CHANGE, type: EDIT_EVENT_HAS_END_TIME_CHANGE,
value, value,
}); });
const changeCreateEventApprovalRequired = (value: boolean) => ({ const changeEditEventApprovalRequired = (value: boolean) => ({
type: CREATE_EVENT_APPROVAL_REQUIRED_CHANGE, type: EDIT_EVENT_APPROVAL_REQUIRED_CHANGE,
value, value,
}); });
const changeCreateEventLocation = (value: string | null) => const changeEditEventLocation = (value: string | null) =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
let location = null; let location = null;
@ -120,7 +139,7 @@ const changeCreateEventLocation = (value: string | null) =>
} }
dispatch({ dispatch({
type: CREATE_EVENT_LOCATION_CHANGE, type: EDIT_EVENT_LOCATION_CHANGE,
value: location, value: location,
}); });
}; };
@ -202,15 +221,15 @@ const submitEvent = () =>
(dispatch: AppDispatch, getState: () => RootState) => { (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState(); const state = getState();
const name = state.create_event.name; const name = state.compose_event.name;
const status = state.create_event.status; const status = state.compose_event.status;
const banner = state.create_event.banner; const banner = state.compose_event.banner;
const startTime = state.create_event.start_time; const startTime = state.compose_event.start_time;
const endTime = state.create_event.end_time; const endTime = state.compose_event.end_time;
const joinMode = state.create_event.approval_required ? 'restricted' : 'free'; const joinMode = state.compose_event.approval_required ? 'restricted' : 'free';
const location = state.create_event.location; const location = state.compose_event.location;
if (!status || !status.length) { if (!name || !name.length) {
return; return;
} }
@ -228,7 +247,7 @@ const submitEvent = () =>
if (location) params.location_id = location.origin_id; if (location) params.location_id = location.origin_id;
return api(getState).post('/api/v1/pleroma/events', params).then(({ data }) => { return api(getState).post('/api/v1/pleroma/events', params).then(({ data }) => {
dispatch(closeModal('CREATE_EVENT')); dispatch(closeModal('COMPOSE_EVENT'));
dispatch(importFetchedStatus(data)); dispatch(importFetchedStatus(data));
dispatch(submitEventSuccess(data)); dispatch(submitEventSuccess(data));
dispatch(snackbar.success(messages.success, messages.view, `/@${data.account.acct}/events/${data.id}`)); dispatch(snackbar.success(messages.success, messages.view, `/@${data.account.acct}/events/${data.id}`));
@ -261,7 +280,9 @@ const joinEvent = (id: string, participationMessage?: string) =>
dispatch(joinEventRequest(status)); dispatch(joinEventRequest(status));
return api(getState).post(`/api/v1/pleroma/events/${id}/join`, { participationMessage }).then(({ data }) => { return api(getState).post(`/api/v1/pleroma/events/${id}/join`, {
participation_message: participationMessage,
}).then(({ data }) => {
dispatch(importFetchedStatus(data)); dispatch(importFetchedStatus(data));
dispatch(joinEventSuccess(data)); dispatch(joinEventSuccess(data));
dispatch(snackbar.success( dispatch(snackbar.success(
@ -461,21 +482,108 @@ const expandEventParticipationRequestsFail = (id: string, error: AxiosError) =>
error, error,
}); });
const authorizeEventParticipationRequest = (id: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(authorizeEventParticipationRequestRequest(id, accountId));
return api(getState)
.post(`/api/v1/pleroma/events/${id}/participation_requests/${accountId}/authorize`)
.then(() => {
dispatch(authorizeEventParticipationRequestSuccess(id, accountId));
dispatch(snackbar.success(messages.authorized));
})
.catch(error => dispatch(authorizeEventParticipationRequestFail(id, accountId, error)));
};
const authorizeEventParticipationRequestRequest = (id: string, accountId: string) => ({
type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST,
id,
accountId,
});
const authorizeEventParticipationRequestSuccess = (id: string, accountId: string) => ({
type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS,
id,
accountId,
});
const authorizeEventParticipationRequestFail = (id: string, accountId: string, error: AxiosError) => ({
type: EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL,
id,
accountId,
error,
});
const rejectEventParticipationRequest = (id: string, accountId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
dispatch(rejectEventParticipationRequestRequest(id, accountId));
return api(getState)
.post(`/api/v1/pleroma/events/${id}/participation_requests/${accountId}/reject`)
.then(() => {
dispatch(rejectEventParticipationRequestSuccess(id, accountId));
dispatch(snackbar.success(messages.rejected));
})
.catch(error => dispatch(rejectEventParticipationRequestFail(id, accountId, error)));
};
const rejectEventParticipationRequestRequest = (id: string, accountId: string) => ({
type: EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST,
id,
accountId,
});
const rejectEventParticipationRequestSuccess = (id: string, accountId: string) => ({
type: EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS,
id,
accountId,
});
const rejectEventParticipationRequestFail = (id: string, accountId: string, error: AxiosError) => ({
type: EVENT_PARTICIPATION_REQUEST_REJECT_FAIL,
id,
accountId,
error,
});
const fetchEventIcs = (id: string) => const fetchEventIcs = (id: string) =>
(dispatch: any, getState: () => RootState) => (dispatch: any, getState: () => RootState) =>
api(getState).get(`/api/v1/pleroma/events/${id}/ics`); api(getState).get(`/api/v1/pleroma/events/${id}/ics`);
const cancelEventCompose = () => ({
type: EVENT_COMPOSE_CANCEL,
});
const editEvent = (id: string) => (dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses.get(id)!;
dispatch({ type: STATUS_FETCH_SOURCE_REQUEST });
api(getState).get(`/api/v1/statuses/${id}/source`).then(response => {
dispatch({ type: STATUS_FETCH_SOURCE_SUCCESS });
dispatch({
type: EVENT_FORM_SET,
status,
text: response.data.text,
location: response.data.location,
});
dispatch(openModal('COMPOSE_EVENT'));
}).catch(error => {
dispatch({ type: STATUS_FETCH_SOURCE_FAIL, error });
});
};
export { export {
LOCATION_SEARCH_REQUEST, LOCATION_SEARCH_REQUEST,
LOCATION_SEARCH_SUCCESS, LOCATION_SEARCH_SUCCESS,
LOCATION_SEARCH_FAIL, LOCATION_SEARCH_FAIL,
CREATE_EVENT_NAME_CHANGE, EDIT_EVENT_NAME_CHANGE,
CREATE_EVENT_DESCRIPTION_CHANGE, EDIT_EVENT_DESCRIPTION_CHANGE,
CREATE_EVENT_START_TIME_CHANGE, EDIT_EVENT_START_TIME_CHANGE,
CREATE_EVENT_END_TIME_CHANGE, EDIT_EVENT_END_TIME_CHANGE,
CREATE_EVENT_HAS_END_TIME_CHANGE, EDIT_EVENT_HAS_END_TIME_CHANGE,
CREATE_EVENT_APPROVAL_REQUIRED_CHANGE, EDIT_EVENT_APPROVAL_REQUIRED_CHANGE,
CREATE_EVENT_LOCATION_CHANGE, EDIT_EVENT_LOCATION_CHANGE,
EVENT_BANNER_UPLOAD_REQUEST, EVENT_BANNER_UPLOAD_REQUEST,
EVENT_BANNER_UPLOAD_PROGRESS, EVENT_BANNER_UPLOAD_PROGRESS,
EVENT_BANNER_UPLOAD_SUCCESS, EVENT_BANNER_UPLOAD_SUCCESS,
@ -502,14 +610,22 @@ export {
EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST, EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST,
EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS, EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS,
EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL, EVENT_PARTICIPATION_REQUESTS_EXPAND_FAIL,
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_REQUEST,
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS,
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_FAIL,
EVENT_PARTICIPATION_REQUEST_REJECT_REQUEST,
EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS,
EVENT_PARTICIPATION_REQUEST_REJECT_FAIL,
EVENT_COMPOSE_CANCEL,
EVENT_FORM_SET,
locationSearch, locationSearch,
changeCreateEventName, changeEditEventName,
changeCreateEventDescription, changeEditEventDescription,
changeCreateEventStartTime, changeEditEventStartTime,
changeCreateEventEndTime, changeEditEventEndTime,
changeCreateEventHasEndTime, changeEditEventHasEndTime,
changeCreateEventApprovalRequired, changeEditEventApprovalRequired,
changeCreateEventLocation, changeEditEventLocation,
uploadEventBanner, uploadEventBanner,
uploadEventBannerRequest, uploadEventBannerRequest,
uploadEventBannerProgress, uploadEventBannerProgress,
@ -544,5 +660,15 @@ export {
expandEventParticipationRequestsRequest, expandEventParticipationRequestsRequest,
expandEventParticipationRequestsSuccess, expandEventParticipationRequestsSuccess,
expandEventParticipationRequestsFail, expandEventParticipationRequestsFail,
authorizeEventParticipationRequest,
authorizeEventParticipationRequestRequest,
authorizeEventParticipationRequestSuccess,
authorizeEventParticipationRequestFail,
rejectEventParticipationRequest,
rejectEventParticipationRequestRequest,
rejectEventParticipationRequestSuccess,
rejectEventParticipationRequestFail,
fetchEventIcs, fetchEventIcs,
cancelEventCompose,
editEvent,
}; };

View File

@ -63,6 +63,7 @@ interface IAccount {
withRelationship?: boolean, withRelationship?: boolean,
showEdit?: boolean, showEdit?: boolean,
emoji?: string, emoji?: string,
note?: string,
} }
const Account = ({ const Account = ({
@ -86,6 +87,7 @@ const Account = ({
withRelationship = true, withRelationship = true,
showEdit = false, showEdit = false,
emoji, emoji,
note,
}: IAccount) => { }: IAccount) => {
const overflowRef = React.useRef<HTMLDivElement>(null); const overflowRef = React.useRef<HTMLDivElement>(null);
const actionRef = React.useRef<HTMLDivElement>(null); const actionRef = React.useRef<HTMLDivElement>(null);
@ -163,7 +165,7 @@ const Account = ({
return ( return (
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}> <div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
<HStack alignItems={actionAlignment} justifyContent='between'> <HStack alignItems={actionAlignment} justifyContent='between'>
<HStack alignItems={withAccountNote ? 'top' : 'center'} space={3}> <HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3}>
<ProfilePopper <ProfilePopper
condition={showProfileHoverCard} condition={showProfileHoverCard}
wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>} wrapper={(children) => <HoverRefWrapper className='relative' accountId={account.id} inline>{children}</HoverRefWrapper>}
@ -206,7 +208,7 @@ const Account = ({
</LinkEl> </LinkEl>
</ProfilePopper> </ProfilePopper>
<Stack space={withAccountNote ? 1 : 0}> <Stack space={withAccountNote || note ? 1 : 0}>
<HStack alignItems='center' space={1} style={style}> <HStack alignItems='center' space={1} style={style}>
<Text theme='muted' size='sm' truncate>@{username}</Text> <Text theme='muted' size='sm' truncate>@{username}</Text>
@ -237,7 +239,14 @@ const Account = ({
) : null} ) : null}
</HStack> </HStack>
{withAccountNote && ( {note ? (
<Text
size='sm'
className='mr-2'
>
{note}
</Text>
) : withAccountNote && (
<Text <Text
size='sm' size='sm'
dangerouslySetInnerHTML={{ __html: account.note_emojified }} dangerouslySetInnerHTML={{ __html: account.note_emojified }}

View File

@ -2,7 +2,7 @@ import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { fetchEventIcs } from 'soapbox/actions/events'; import { editEvent, fetchEventIcs } from 'soapbox/actions/events';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation'; import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
import Icon from 'soapbox/components/icon'; import Icon from 'soapbox/components/icon';
@ -159,9 +159,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
const handleManageClick: React.MouseEventHandler = e => { const handleManageClick: React.MouseEventHandler = e => {
e.stopPropagation(); e.stopPropagation();
dispatch(openModal('MANAGE_EVENT', { dispatch(editEvent(status.id));
statusId: status.id,
}));
}; };
const handleParticipantsClick: React.MouseEventHandler = e => { const handleParticipantsClick: React.MouseEventHandler = e => {
@ -228,7 +226,6 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
size='sm' size='sm'
theme='secondary' theme='secondary'
onClick={handleManageClick} onClick={handleManageClick}
to={`/@${account.acct}/events/${status.id}`}
> >
<FormattedMessage id='event.manage' defaultMessage='Manage' /> <FormattedMessage id='event.manage' defaultMessage='Manage' />
</Button> </Button>

View File

@ -53,6 +53,9 @@ const icons: Record<NotificationType, string> = {
'pleroma:emoji_reaction': require('@tabler/icons/mood-happy.svg'), 'pleroma:emoji_reaction': require('@tabler/icons/mood-happy.svg'),
user_approved: require('@tabler/icons/user-plus.svg'), user_approved: require('@tabler/icons/user-plus.svg'),
update: require('@tabler/icons/pencil.svg'), update: require('@tabler/icons/pencil.svg'),
'pleroma:event_reminder': require('@tabler/icons/calendar-time.svg'),
'pleroma:participation_request': require('@tabler/icons/calendar-event.svg'),
'pleroma:participation_accepted': require('@tabler/icons/calendar-event.svg'),
}; };
const messages: Record<NotificationType, MessageDescriptor> = defineMessages({ const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
@ -104,6 +107,18 @@ const messages: Record<NotificationType, MessageDescriptor> = defineMessages({
id: 'notification.update', id: 'notification.update',
defaultMessage: '{name} edited a post you interacted with', defaultMessage: '{name} edited a post you interacted with',
}, },
'pleroma:event_reminder': {
id: 'notification.pleroma:event_reminder',
defaultMessage: 'An event you are participating in starts soon',
},
'pleroma:participation_request': {
id: 'notification.pleroma:participation_request',
defaultMessage: '{name} wants to join your event',
},
'pleroma:participation_accepted': {
id: 'notification.pleroma:participation_accepted',
defaultMessage: 'You were accepted to join the event',
},
}); });
const buildMessage = ( const buildMessage = (
@ -302,6 +317,9 @@ const Notification: React.FC<INotificaton> = (props) => {
case 'poll': case 'poll':
case 'update': case 'update':
case 'pleroma:emoji_reaction': case 'pleroma:emoji_reaction':
case 'pleroma:event_reminder':
case 'pleroma:participation_accepted':
case 'pleroma:participation_request':
return status && typeof status === 'object' ? ( return status && typeof status === 'object' ? (
<StatusContainer <StatusContainer
id={status.id} id={status.id}

View File

@ -7,7 +7,7 @@ import { Button } from 'soapbox/components/ui';
const ComposeButton = () => { const ComposeButton = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const onOpenCompose = () => dispatch(openModal('CREATE_EVENT')); const onOpenCompose = () => dispatch(openModal('COMPOSE_EVENT'));
return ( return (
<div className='mt-4'> <div className='mt-4'>

View File

@ -32,7 +32,7 @@ import {
CompareHistoryModal, CompareHistoryModal,
VerifySmsModal, VerifySmsModal,
FamiliarFollowersModal, FamiliarFollowersModal,
CreateEventModal, ComposeEventModal,
JoinEventModal, JoinEventModal,
AccountModerationModal, AccountModerationModal,
EventMapModal, EventMapModal,
@ -74,7 +74,7 @@ const MODAL_COMPONENTS = {
'COMPARE_HISTORY': CompareHistoryModal, 'COMPARE_HISTORY': CompareHistoryModal,
'VERIFY_SMS': VerifySmsModal, 'VERIFY_SMS': VerifySmsModal,
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal, 'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
'CREATE_EVENT': CreateEventModal, 'COMPOSE_EVENT': ComposeEventModal,
'JOIN_EVENT': JoinEventModal, 'JOIN_EVENT': JoinEventModal,
'ACCOUNT_MODERATION': AccountModerationModal, 'ACCOUNT_MODERATION': AccountModerationModal,
'EVENT_MAP': EventMapModal, 'EVENT_MAP': EventMapModal,

View File

@ -0,0 +1,320 @@
import React, { useEffect, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Toggle from 'react-toggle';
import {
changeEditEventApprovalRequired,
changeEditEventDescription,
changeEditEventEndTime,
changeEditEventHasEndTime,
changeEditEventName,
changeEditEventStartTime,
changeEditEventLocation,
uploadEventBanner,
undoUploadEventBanner,
submitEvent,
fetchEventParticipationRequests,
rejectEventParticipationRequest,
authorizeEventParticipationRequest,
} from 'soapbox/actions/events';
import { ADDRESS_ICONS } from 'soapbox/components/autosuggest-location';
import LocationSearch from 'soapbox/components/location-search';
import { Button, Form, FormGroup, HStack, Icon, IconButton, Input, Modal, Spinner, Stack, Tabs, Text, Textarea } from 'soapbox/components/ui';
import AccountContainer from 'soapbox/containers/account_container';
import { isCurrentOrFutureDate } from 'soapbox/features/compose/components/schedule_form';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { DatePicker } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import UploadButton from './upload-button';
const messages = defineMessages({
eventNamePlaceholder: { id: 'compose_event.fields.name_placeholder', defaultMessage: 'Name' },
eventDescriptionPlaceholder: { id: 'compose_event.fields.description_placeholder', defaultMessage: 'Description' },
eventStartTimePlaceholder: { id: 'compose_event.fields.start_time_placeholder', defaultMessage: 'Event begins on…' },
eventEndTimePlaceholder: { id: 'compose_event.fields.end_time_placeholder', defaultMessage: 'Event ends on…' },
resetLocation: { id: 'compose_event.reset_location', defaultMessage: 'Reset location' },
edit: { id: 'compose_event.tabs.edit', defaultMessage: 'Edit details' },
pending: { id: 'compose_event.tabs.pending', defaultMessage: 'Manage requests' },
authorize: { id: 'compose_event.participation_requests.authorize', defaultMessage: 'Authorize' },
reject: { id: 'compose_event.participation_requests.reject', defaultMessage: 'Reject' },
});
interface IAccount {
eventId: string,
id: string,
participationMessage: string | null,
}
const Account: React.FC<IAccount> = ({ eventId, id, participationMessage }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const handleAuthorize = () => {
dispatch(authorizeEventParticipationRequest(eventId, id));
};
const handleReject = () => {
dispatch(rejectEventParticipationRequest(eventId, id));
};
return (
<AccountContainer
id={id}
note={participationMessage || undefined}
action={
<HStack space={2}>
<Button
theme='secondary'
size='sm'
text={intl.formatMessage(messages.authorize)}
onClick={handleAuthorize}
/>
<Button
theme='danger'
size='sm'
text={intl.formatMessage(messages.reject)}
onClick={handleReject}
/>
</HStack>
}
/>
);
};
interface IComposeEventModal {
onClose: (type?: string) => void,
}
const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const [tab, setTab] = useState<'edit' | 'pending'>('edit');
const banner = useAppSelector((state) => state.compose_event.banner);
const isUploading = useAppSelector((state) => state.compose_event.is_uploading);
const name = useAppSelector((state) => state.compose_event.name);
const description = useAppSelector((state) => state.compose_event.status);
const startTime = useAppSelector((state) => state.compose_event.start_time);
const endTime = useAppSelector((state) => state.compose_event.end_time);
const approvalRequired = useAppSelector((state) => state.compose_event.approval_required);
const location = useAppSelector((state) => state.compose_event.location);
const id = useAppSelector((state) => state.compose_event.id);
const isSubmitting = useAppSelector((state) => state.compose_event.is_submitting);
const onChangeName: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
dispatch(changeEditEventName(target.value));
};
const onChangeDescription: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) => {
dispatch(changeEditEventDescription(target.value));
};
const onChangeStartTime = (date: Date) => {
dispatch(changeEditEventStartTime(date));
};
const onChangeEndTime = (date: Date) => {
dispatch(changeEditEventEndTime(date));
};
const onChangeHasEndTime: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
dispatch(changeEditEventHasEndTime(target.checked));
};
const onChangeApprovalRequired: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
dispatch(changeEditEventApprovalRequired(target.checked));
};
const onChangeLocation = (value: string | null) => {
dispatch(changeEditEventLocation(value));
};
const onClickClose = () => {
onClose('COMPOSE_EVENT');
};
const handleFiles = (files: FileList) => {
dispatch(uploadEventBanner(files[0], intl));
};
const handleClearBanner = () => {
dispatch(undoUploadEventBanner());
};
const handleSubmit = () => {
dispatch(submitEvent());
};
const accounts = useAppSelector((state) => state.user_lists.event_participation_requests.get(id!)?.items);
useEffect(() => {
if (id) dispatch(fetchEventParticipationRequests(id));
}, []);
const renderLocation = () => location && (
<HStack className='h-[38px] text-gray-700 dark:text-gray-500' alignItems='center' space={2}>
<Icon src={ADDRESS_ICONS[location.type] || require('@tabler/icons/map-pin.svg')} />
<Stack className='flex-grow'>
<Text>{location.description}</Text>
<Text theme='muted' size='xs'>{[location.street, location.locality, location.country].filter(val => val.trim()).join(' · ')}</Text>
</Stack>
<IconButton title={intl.formatMessage(messages.resetLocation)} src={require('@tabler/icons/x.svg')} onClick={() => onChangeLocation(null)} />
</HStack>
);
const renderTabs = () => {
const items = [
{
text: intl.formatMessage(messages.edit),
action: () => setTab('edit'),
name: 'edit',
},
{
text: intl.formatMessage(messages.pending),
action: () => setTab('pending'),
name: 'pending',
},
];
return <Tabs items={items} activeItem={tab} />;
};
return (
<Modal
title={id
? <FormattedMessage id='navigation_bar.compose_event' defaultMessage='Manage event' />
: <FormattedMessage id='navigation_bar.create_event' defaultMessage='Create new event' />}
confirmationAction={tab === 'edit' ? handleSubmit : undefined}
confirmationText={id
? <FormattedMessage id='compose_event.update' defaultMessage='Update' />
: <FormattedMessage id='compose_event.create' defaultMessage='Create' />}
confirmationDisabled={isSubmitting}
onClose={onClickClose}
>
<Stack space={2}>
{id && renderTabs()}
{tab === 'edit' ? (
<Form>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.banner_label' defaultMessage='Event banner' />}
>
<div className='flex items-center justify-center bg-gray-200 dark:bg-gray-900/50 rounded-lg text-black dark:text-white sm:shadow dark:sm:shadow-inset overflow-hidden h-24 sm:h-32 relative'>
{banner ? (
<>
<img className='h-full w-full object-cover' src={banner.url} alt='' />
<IconButton className='absolute top-2 right-2' src={require('@tabler/icons/x.svg')} onClick={handleClearBanner} />
</>
) : (
<UploadButton disabled={isUploading} onSelectFile={handleFiles} />
)}
</div>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.name_label' defaultMessage='Event name' />}
>
<Input
type='text'
placeholder={intl.formatMessage(messages.eventNamePlaceholder)}
value={name}
onChange={onChangeName}
/>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.description_label' defaultMessage='Event description' />}
hintText={<FormattedMessage id='compose_event.fields.description_hint' defaultMessage='Markdown syntax is supported' />}
>
<Textarea
autoComplete='off'
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
value={description}
onChange={onChangeDescription}
/>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.location_label' defaultMessage='Event location' />}
>
{location ? renderLocation() : (
<LocationSearch
onSelected={onChangeLocation}
/>
)}
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.start_time_label' defaultMessage='Event start date' />}
>
<BundleContainer fetchComponent={DatePicker}>
{Component => (<Component
showTimeSelect
dateFormat='MMMM d, yyyy h:mm aa'
timeIntervals={15}
wrapperClassName='react-datepicker-wrapper'
placeholderText={intl.formatMessage(messages.eventStartTimePlaceholder)}
filterDate={isCurrentOrFutureDate}
selected={startTime}
onChange={onChangeStartTime}
/>)}
</BundleContainer>
</FormGroup>
<HStack alignItems='center' space={2}>
<Toggle
icons={false}
checked={!!endTime}
onChange={onChangeHasEndTime}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='compose_event.fields.has_end_time' defaultMessage='The event has end date' />
</Text>
</HStack>
{endTime && (
<FormGroup
labelText={<FormattedMessage id='compose_event.fields.end_time_label' defaultMessage='Event end date' />}
>
<BundleContainer fetchComponent={DatePicker}>
{Component => (<Component
showTimeSelect
dateFormat='MMMM d, yyyy h:mm aa'
timeIntervals={15}
wrapperClassName='react-datepicker-wrapper'
placeholderText={intl.formatMessage(messages.eventEndTimePlaceholder)}
filterDate={isCurrentOrFutureDate}
selected={endTime}
onChange={onChangeEndTime}
/>)}
</BundleContainer>
</FormGroup>
)}
<HStack alignItems='center' space={2}>
<Toggle
icons={false}
checked={approvalRequired}
onChange={onChangeApprovalRequired}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='compose_event.fields.approval_required' defaultMessage='I want to approve participation requests manually' />
</Text>
</HStack>
</Form>
) : accounts ? (
<Stack space={3}>
{accounts.size > 0 ? (
accounts.map(({ account, participation_message }) =>
<Account key={account} eventId={id!} id={account} participationMessage={participation_message} />,
)
) : (
<FormattedMessage id='empty_column.event_participant_requests' defaultMessage='There are no pending event participation requests.' />
)}
</Stack>
) : <Spinner />}
</Stack>
</Modal>
);
};
export default ComposeEventModal;

View File

@ -7,7 +7,7 @@ import { useAppSelector } from 'soapbox/hooks';
import type { List as ImmutableList } from 'immutable'; import type { List as ImmutableList } from 'immutable';
const messages = defineMessages({ const messages = defineMessages({
upload: { id: 'create_event.upload_banner', defaultMessage: 'Upload event banner' }, upload: { id: 'compose_event.upload_banner', defaultMessage: 'Upload event banner' },
}); });
interface IUploadButton { interface IUploadButton {

View File

@ -1,223 +0,0 @@
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import Toggle from 'react-toggle';
import {
changeCreateEventApprovalRequired,
changeCreateEventDescription,
changeCreateEventEndTime,
changeCreateEventHasEndTime,
changeCreateEventName,
changeCreateEventStartTime,
changeCreateEventLocation,
uploadEventBanner,
undoUploadEventBanner,
submitEvent,
} from 'soapbox/actions/events';
import { ADDRESS_ICONS } from 'soapbox/components/autosuggest-location';
import LocationSearch from 'soapbox/components/location-search';
import { Form, FormGroup, HStack, Icon, IconButton, Input, Modal, Stack, Text, Textarea } from 'soapbox/components/ui';
import { isCurrentOrFutureDate } from 'soapbox/features/compose/components/schedule_form';
import BundleContainer from 'soapbox/features/ui/containers/bundle_container';
import { DatePicker } from 'soapbox/features/ui/util/async-components';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import UploadButton from './upload-button';
const messages = defineMessages({
eventNamePlaceholder: { id: 'create_event.fields.name_placeholder', defaultMessage: 'Name' },
eventDescriptionPlaceholder: { id: 'create_event.fields.description_placeholder', defaultMessage: 'Description' },
eventStartTimePlaceholder: { id: 'create_event.fields.start_time_placeholder', defaultMessage: 'Event begins on…' },
eventEndTimePlaceholder: { id: 'create_event.fields.end_time_placeholder', defaultMessage: 'Event ends on…' },
resetLocation: { id: 'create_event.reset_location', defaultMessage: 'Reset location' },
});
interface ICreateEventModal {
onClose: (type?: string) => void,
}
const CreateEventModal: React.FC<ICreateEventModal> = ({ onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const banner = useAppSelector((state) => state.create_event.banner);
const isUploading = useAppSelector((state) => state.create_event.is_uploading);
const name = useAppSelector((state) => state.create_event.name);
const description = useAppSelector((state) => state.create_event.status);
const startTime = useAppSelector((state) => state.create_event.start_time);
const endTime = useAppSelector((state) => state.create_event.end_time);
const approvalRequired = useAppSelector((state) => state.create_event.approval_required);
const location = useAppSelector((state) => state.create_event.location);
const isSubmitting = useAppSelector((state) => state.create_event.is_submitting);
const onChangeName: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
dispatch(changeCreateEventName(target.value));
};
const onChangeDescription: React.ChangeEventHandler<HTMLTextAreaElement> = ({ target }) => {
dispatch(changeCreateEventDescription(target.value));
};
const onChangeStartTime = (date: Date) => {
dispatch(changeCreateEventStartTime(date));
};
const onChangeEndTime = (date: Date) => {
dispatch(changeCreateEventEndTime(date));
};
const onChangeHasEndTime: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
dispatch(changeCreateEventHasEndTime(target.checked));
};
const onChangeApprovalRequired: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
dispatch(changeCreateEventApprovalRequired(target.checked));
};
const onChangeLocation = (value: string | null) => {
dispatch(changeCreateEventLocation(value));
};
const onClickClose = () => {
onClose('CREATE_EVENT');
};
const handleFiles = (files: FileList) => {
dispatch(uploadEventBanner(files[0], intl));
};
const handleClearBanner = () => {
dispatch(undoUploadEventBanner());
};
const handleSubmit = () => {
dispatch(submitEvent());
};
const renderLocation = () => location && (
<HStack className='h-[38px] text-gray-700 dark:text-gray-500' alignItems='center' space={2}>
<Icon src={ADDRESS_ICONS[location.type] || require('@tabler/icons/map-pin.svg')} />
<Stack className='flex-grow'>
<Text>{location.description}</Text>
<Text theme='muted' size='xs'>{[location.street, location.locality, location.country].filter(val => val.trim()).join(' · ')}</Text>
</Stack>
<IconButton title={intl.formatMessage(messages.resetLocation)} src={require('@tabler/icons/x.svg')} onClick={() => onChangeLocation(null)} />
</HStack>
);
return (
<Modal
title={<FormattedMessage id='navigation_bar.create_event' defaultMessage='Create new event' />}
confirmationAction={handleSubmit}
confirmationText={<FormattedMessage id='create_event.create' defaultMessage='Create' />}
confirmationDisabled={isSubmitting}
onClose={onClickClose}
>
<Form>
<FormGroup
labelText={<FormattedMessage id='create_event.fields.banner_label' defaultMessage='Event banner' />}
>
<div className='flex items-center justify-center bg-gray-200 dark:bg-gray-900/50 rounded-lg text-black dark:text-white sm:shadow dark:sm:shadow-inset overflow-hidden h-24 sm:h-32 relative'>
{banner ? (
<>
<img className='h-full w-full object-cover' src={banner.url} alt='' />
<IconButton className='absolute top-2 right-2' src={require('@tabler/icons/x.svg')} onClick={handleClearBanner} />
</>
) : (
<UploadButton disabled={isUploading} onSelectFile={handleFiles} />
)}
</div>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='create_event.fields.name_label' defaultMessage='Event name' />}
>
<Input
type='text'
placeholder={intl.formatMessage(messages.eventNamePlaceholder)}
value={name}
onChange={onChangeName}
/>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='create_event.fields.description_label' defaultMessage='Event description' />}
hintText={<FormattedMessage id='create_event.fields.description_hint' defaultMessage='Markdown syntax is supported' />}
>
<Textarea
autoComplete='off'
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
value={description}
onChange={onChangeDescription}
/>
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='create_event.fields.location_label' defaultMessage='Event location' />}
>
{location ? renderLocation() : (
<LocationSearch
onSelected={onChangeLocation}
/>
)}
</FormGroup>
<FormGroup
labelText={<FormattedMessage id='create_event.fields.start_time_label' defaultMessage='Event start date' />}
>
<BundleContainer fetchComponent={DatePicker}>
{Component => (<Component
showTimeSelect
dateFormat='MMMM d, yyyy h:mm aa'
timeIntervals={15}
wrapperClassName='react-datepicker-wrapper'
placeholderText={intl.formatMessage(messages.eventStartTimePlaceholder)}
filterDate={isCurrentOrFutureDate}
selected={startTime}
onChange={onChangeStartTime}
/>)}
</BundleContainer>
</FormGroup>
<HStack alignItems='center' space={2}>
<Toggle
icons={false}
checked={!!endTime}
onChange={onChangeHasEndTime}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='create_event.fields.has_end_time' defaultMessage='The event has end date' />
</Text>
</HStack>
{endTime && (
<FormGroup
labelText={<FormattedMessage id='create_event.fields.end_time_label' defaultMessage='Event end date' />}
>
<BundleContainer fetchComponent={DatePicker}>
{Component => (<Component
showTimeSelect
dateFormat='MMMM d, yyyy h:mm aa'
timeIntervals={15}
wrapperClassName='react-datepicker-wrapper'
placeholderText={intl.formatMessage(messages.eventEndTimePlaceholder)}
filterDate={isCurrentOrFutureDate}
selected={endTime}
onChange={onChangeEndTime}
/>)}
</BundleContainer>
</FormGroup>
)}
<HStack alignItems='center' space={2}>
<Toggle
icons={false}
checked={approvalRequired}
onChange={onChangeApprovalRequired}
/>
<Text tag='span' theme='muted'>
<FormattedMessage id='create_event.fields.approval_required' defaultMessage='I want to approve participation requests manually' />
</Text>
</HStack>
</Form>
</Modal>
);
};
export default CreateEventModal;

View File

@ -1,6 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { cancelReplyCompose } from 'soapbox/actions/compose'; import { cancelReplyCompose } from 'soapbox/actions/compose';
import { cancelEventCompose } from 'soapbox/actions/events';
import { closeModal } from 'soapbox/actions/modals'; import { closeModal } from 'soapbox/actions/modals';
import { cancelReport } from 'soapbox/actions/reports'; import { cancelReport } from 'soapbox/actions/reports';
@ -24,6 +25,9 @@ const mapDispatchToProps = (dispatch) => ({
case 'COMPOSE': case 'COMPOSE':
dispatch(cancelReplyCompose()); dispatch(cancelReplyCompose());
break; break;
case 'COMPOSE_EVENT':
dispatch(cancelEventCompose());
break;
case 'REPORT': case 'REPORT':
dispatch(cancelReport()); dispatch(cancelReport());
break; break;

View File

@ -506,8 +506,8 @@ export function AnnouncementsPanel() {
return import(/* webpackChunkName: "features/announcements" */'../../../components/announcements/announcements-panel'); return import(/* webpackChunkName: "features/announcements" */'../../../components/announcements/announcements-panel');
} }
export function CreateEventModal() { export function ComposeEventModal() {
return import(/* webpackChunkName: "features/create_event_modal" */'../components/modals/create-event-modal/create-event-modal'); return import(/* webpackChunkName: "features/compose_event_modal" */'../components/modals/compose-event-modal/compose-event-modal');
} }
export function JoinEventModal() { export function JoinEventModal() {

View File

@ -2,13 +2,13 @@ import { fromJS, Record as ImmutableRecord } from 'immutable';
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { import {
CREATE_EVENT_APPROVAL_REQUIRED_CHANGE, EDIT_EVENT_APPROVAL_REQUIRED_CHANGE,
CREATE_EVENT_DESCRIPTION_CHANGE, EDIT_EVENT_DESCRIPTION_CHANGE,
CREATE_EVENT_END_TIME_CHANGE, EDIT_EVENT_END_TIME_CHANGE,
CREATE_EVENT_HAS_END_TIME_CHANGE, EDIT_EVENT_HAS_END_TIME_CHANGE,
CREATE_EVENT_LOCATION_CHANGE, EDIT_EVENT_LOCATION_CHANGE,
CREATE_EVENT_NAME_CHANGE, EDIT_EVENT_NAME_CHANGE,
CREATE_EVENT_START_TIME_CHANGE, EDIT_EVENT_START_TIME_CHANGE,
EVENT_BANNER_UPLOAD_REQUEST, EVENT_BANNER_UPLOAD_REQUEST,
EVENT_BANNER_UPLOAD_PROGRESS, EVENT_BANNER_UPLOAD_PROGRESS,
EVENT_BANNER_UPLOAD_SUCCESS, EVENT_BANNER_UPLOAD_SUCCESS,
@ -17,12 +17,15 @@ import {
EVENT_SUBMIT_REQUEST, EVENT_SUBMIT_REQUEST,
EVENT_SUBMIT_SUCCESS, EVENT_SUBMIT_SUCCESS,
EVENT_SUBMIT_FAIL, EVENT_SUBMIT_FAIL,
EVENT_COMPOSE_CANCEL,
EVENT_FORM_SET,
} from 'soapbox/actions/events'; } from 'soapbox/actions/events';
import { normalizeAttachment } from 'soapbox/normalizers'; import { normalizeAttachment, normalizeLocation } from 'soapbox/normalizers';
import type { import type {
Attachment as AttachmentEntity, Attachment as AttachmentEntity,
Location as LocationEntity, Location as LocationEntity,
Status as StatusEntity,
} from 'soapbox/types/entities'; } from 'soapbox/types/entities';
export const ReducerRecord = ImmutableRecord({ export const ReducerRecord = ImmutableRecord({
@ -36,6 +39,7 @@ export const ReducerRecord = ImmutableRecord({
progress: 0, progress: 0,
is_uploading: false, is_uploading: false,
is_submitting: false, is_submitting: false,
id: null as string | null,
}); });
type State = ReturnType<typeof ReducerRecord>; type State = ReturnType<typeof ReducerRecord>;
@ -48,22 +52,22 @@ const setHasEndTime = (state: State) => {
return state.set('end_time', endTime); return state.set('end_time', endTime);
}; };
export default function create_event(state = ReducerRecord(), action: AnyAction): State { export default function compose_event(state = ReducerRecord(), action: AnyAction): State {
switch (action.type) { switch (action.type) {
case CREATE_EVENT_NAME_CHANGE: case EDIT_EVENT_NAME_CHANGE:
return state.set('name', action.value); return state.set('name', action.value);
case CREATE_EVENT_DESCRIPTION_CHANGE: case EDIT_EVENT_DESCRIPTION_CHANGE:
return state.set('status', action.value); return state.set('status', action.value);
case CREATE_EVENT_START_TIME_CHANGE: case EDIT_EVENT_START_TIME_CHANGE:
return state.set('start_time', action.value); return state.set('start_time', action.value);
case CREATE_EVENT_END_TIME_CHANGE: case EDIT_EVENT_END_TIME_CHANGE:
return state.set('end_time', action.value); return state.set('end_time', action.value);
case CREATE_EVENT_HAS_END_TIME_CHANGE: case EDIT_EVENT_HAS_END_TIME_CHANGE:
if (action.value) return setHasEndTime(state); if (action.value) return setHasEndTime(state);
return state.set('end_time', null); return state.set('end_time', null);
case CREATE_EVENT_APPROVAL_REQUIRED_CHANGE: case EDIT_EVENT_APPROVAL_REQUIRED_CHANGE:
return state.set('approval_required', action.value); return state.set('approval_required', action.value);
case CREATE_EVENT_LOCATION_CHANGE: case EDIT_EVENT_LOCATION_CHANGE:
return state.set('location', action.value); return state.set('location', action.value);
case EVENT_BANNER_UPLOAD_REQUEST: case EVENT_BANNER_UPLOAD_REQUEST:
return state.set('is_uploading', true); return state.set('is_uploading', true);
@ -80,6 +84,23 @@ export default function create_event(state = ReducerRecord(), action: AnyAction)
case EVENT_SUBMIT_SUCCESS: case EVENT_SUBMIT_SUCCESS:
case EVENT_SUBMIT_FAIL: case EVENT_SUBMIT_FAIL:
return state.set('is_submitting', false); return state.set('is_submitting', false);
case EVENT_COMPOSE_CANCEL:
return ReducerRecord();
case EVENT_FORM_SET:
return ReducerRecord({
name: action.status.event.name,
status: action.text,
// location: null as LocationEntity | null,
start_time: new Date(action.status.event.start_time),
end_time: action.status.event.start_time ? new Date(action.status.event.end_time) : null,
approval_required: action.status.event.join_mode !== 'free',
banner: (action.status as StatusEntity).media_attachments.find(({ description }) => description === 'Banner') || null,
location: action.location ? normalizeLocation(action.location) : null,
progress: 0,
is_uploading: false,
is_submitting: false,
id: action.status.id,
});
default: default:
return state; return state;
} }

View File

@ -19,9 +19,9 @@ import chat_message_lists from './chat_message_lists';
import chat_messages from './chat_messages'; import chat_messages from './chat_messages';
import chats from './chats'; import chats from './chats';
import compose from './compose'; import compose from './compose';
import compose_event from './compose_event';
import contexts from './contexts'; import contexts from './contexts';
import conversations from './conversations'; import conversations from './conversations';
import create_event from './create_event';
import custom_emojis from './custom_emojis'; import custom_emojis from './custom_emojis';
import domain_lists from './domain_lists'; import domain_lists from './domain_lists';
import dropdown_menu from './dropdown_menu'; import dropdown_menu from './dropdown_menu';
@ -127,7 +127,7 @@ const reducers = {
rules, rules,
history, history,
announcements, announcements,
create_event, compose_event,
}; };
// Build a default state from all reducers: it has the key and `undefined` // Build a default state from all reducers: it has the key and `undefined`

View File

@ -34,6 +34,8 @@ import {
EVENT_PARTICIPATIONS_FETCH_SUCCESS, EVENT_PARTICIPATIONS_FETCH_SUCCESS,
EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS, EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS,
EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS, EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS,
EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS,
EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS,
} from 'soapbox/actions/events'; } from 'soapbox/actions/events';
import { import {
FAMILIAR_FOLLOWERS_FETCH_SUCCESS, FAMILIAR_FOLLOWERS_FETCH_SUCCESS,
@ -212,7 +214,7 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) {
case EVENT_PARTICIPATIONS_EXPAND_SUCCESS: case EVENT_PARTICIPATIONS_EXPAND_SUCCESS:
return appendToList(state, ['event_participations', action.id], action.accounts, action.next); return appendToList(state, ['event_participations', action.id], action.accounts, action.next);
case EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS: case EVENT_PARTICIPATION_REQUESTS_FETCH_SUCCESS:
return state.setIn(['event_participations', action.id], ParticipationRequestListRecord({ return state.setIn(['event_participation_requests', action.id], ParticipationRequestListRecord({
next: action.next, next: action.next,
items: ImmutableOrderedSet(action.participations.map(({ account, participation_message }: APIEntity) => ParticipationRequestRecord({ items: ImmutableOrderedSet(action.participations.map(({ account, participation_message }: APIEntity) => ParticipationRequestRecord({
account: account.id, account: account.id,
@ -221,13 +223,19 @@ export default function userLists(state = ReducerRecord(), action: AnyAction) {
})); }));
case EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS: case EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS:
return state.updateIn( return state.updateIn(
['event_participations', action.id, 'items'], ['event_participation_requests', action.id, 'items'],
(items) => (items as ImmutableOrderedSet<ParticipationRequest>) (items) => (items as ImmutableOrderedSet<ParticipationRequest>)
.union(action.participations.map(({ account, participation_message }: APIEntity) => ParticipationRequestRecord({ .union(action.participations.map(({ account, participation_message }: APIEntity) => ParticipationRequestRecord({
account: account.id, account: account.id,
participation_message, participation_message,
}))), }))),
); );
case EVENT_PARTICIPATION_REQUEST_AUTHORIZE_SUCCESS:
case EVENT_PARTICIPATION_REQUEST_REJECT_SUCCESS:
return state.updateIn(
['event_participation_requests', action.id, 'items'],
items => (items as ImmutableOrderedSet<ParticipationRequest>).filter(({ account }) => account !== action.accountId),
);
default: default:
return state; return state;
} }

View File

@ -12,6 +12,9 @@ const NOTIFICATION_TYPES = [
'pleroma:emoji_reaction', 'pleroma:emoji_reaction',
'user_approved', 'user_approved',
'update', 'update',
'pleroma:event_reminder',
'pleroma:participation_request',
'pleroma:participation_accepted',
] as const; ] as const;
type NotificationType = typeof NOTIFICATION_TYPES[number]; type NotificationType = typeof NOTIFICATION_TYPES[number];