Participation management
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
3f7e593695
commit
3f4f820de7
|
@ -35,9 +35,19 @@ const EVENT_SUBMIT_REQUEST = 'EVENT_SUBMIT_REQUEST';
|
|||
const EVENT_SUBMIT_SUCCESS = 'EVENT_SUBMIT_SUCCESS';
|
||||
const EVENT_SUBMIT_FAIL = 'EVENT_SUBMIT_FAIL';
|
||||
|
||||
const EVENT_JOIN_REQUEST = 'EVENT_JOIN_REQUEST';
|
||||
const EVENT_JOIN_SUCCESS = 'EVENT_JOIN_SUCCESS';
|
||||
const EVENT_JOIN_FAIL = 'EVENT_JOIN_FAIL';
|
||||
|
||||
const EVENT_LEAVE_REQUEST = 'EVENT_LEAVE_REQUEST';
|
||||
const EVENT_LEAVE_SUCCESS = 'EVENT_LEAVE_SUCCESS';
|
||||
const EVENT_LEAVE_FAIL = 'EVENT_LEAVE_FAIL';
|
||||
|
||||
const messages = defineMessages({
|
||||
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' },
|
||||
joinSuccess: { id: 'join_event.success', defaultMessage: 'Joined the event' },
|
||||
joinRequestSuccess: { id: 'join_event.request_success', defaultMessage: 'Requested to join the event' },
|
||||
view: { id: 'snackbar.view', defaultMessage: 'View' },
|
||||
});
|
||||
|
||||
|
@ -191,8 +201,6 @@ const submitEvent = () =>
|
|||
|
||||
dispatch(submitEventRequest());
|
||||
|
||||
const idempotencyKey = state.compose.idempotencyKey;
|
||||
|
||||
const params: Record<string, any> = {
|
||||
name,
|
||||
status,
|
||||
|
@ -206,9 +214,9 @@ const submitEvent = () =>
|
|||
|
||||
return api(getState).post('/api/v1/pleroma/events', params).then(({ data }) => {
|
||||
dispatch(closeModal('CREATE_EVENT'));
|
||||
dispatch(importFetchedStatus(data, idempotencyKey));
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(submitEventSuccess(data));
|
||||
dispatch(snackbar.success(messages.success, messages.view, `/@${data.account.acct}/posts/${data.id}`));
|
||||
dispatch(snackbar.success(messages.success, messages.view, `/@${data.account.acct}/events/${data.id}`));
|
||||
}).catch(function(error) {
|
||||
dispatch(submitEventFail(error));
|
||||
});
|
||||
|
@ -228,6 +236,71 @@ const submitEventFail = (error: AxiosError) => ({
|
|||
error: error,
|
||||
});
|
||||
|
||||
const joinEvent = (id: string, participationMessage?: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const status = getState().statuses.get(id);
|
||||
|
||||
if (!status || !status.event || status.event.join_state) return;
|
||||
|
||||
dispatch(joinEventRequest());
|
||||
|
||||
return api(getState).post(`/api/v1/pleroma/events/${id}/join`, { participationMessage }).then(({ data }) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(joinEventSuccess(data));
|
||||
dispatch(snackbar.success(
|
||||
data.pleroma.event?.join_state === 'pending' ? messages.joinRequestSuccess : messages.joinSuccess,
|
||||
messages.view,
|
||||
`/@${data.account.acct}/events/${data.id}`,
|
||||
));
|
||||
}).catch(function(error) {
|
||||
dispatch(joinEventFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const joinEventRequest = () => ({
|
||||
type: EVENT_JOIN_REQUEST,
|
||||
});
|
||||
|
||||
const joinEventSuccess = (status: APIEntity) => ({
|
||||
type: EVENT_JOIN_SUCCESS,
|
||||
status: status,
|
||||
});
|
||||
|
||||
const joinEventFail = (error: AxiosError) => ({
|
||||
type: EVENT_JOIN_FAIL,
|
||||
error: error,
|
||||
});
|
||||
|
||||
const leaveEvent = (id: string) =>
|
||||
(dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const status = getState().statuses.get(id);
|
||||
|
||||
if (!status || !status.event || !status.event.join_state) return;
|
||||
|
||||
dispatch(leaveEventRequest());
|
||||
|
||||
return api(getState).post(`/api/v1/pleroma/events/${id}/leave`).then(({ data }) => {
|
||||
dispatch(importFetchedStatus(data));
|
||||
dispatch(leaveEventSuccess(data));
|
||||
}).catch(function(error) {
|
||||
dispatch(leaveEventFail(error));
|
||||
});
|
||||
};
|
||||
|
||||
const leaveEventRequest = () => ({
|
||||
type: EVENT_LEAVE_REQUEST,
|
||||
});
|
||||
|
||||
const leaveEventSuccess = (status: APIEntity) => ({
|
||||
type: EVENT_LEAVE_SUCCESS,
|
||||
status: status,
|
||||
});
|
||||
|
||||
const leaveEventFail = (error: AxiosError) => ({
|
||||
type: EVENT_LEAVE_FAIL,
|
||||
error: error,
|
||||
});
|
||||
|
||||
export {
|
||||
LOCATION_SEARCH_REQUEST,
|
||||
LOCATION_SEARCH_SUCCESS,
|
||||
|
@ -247,6 +320,12 @@ export {
|
|||
EVENT_SUBMIT_REQUEST,
|
||||
EVENT_SUBMIT_SUCCESS,
|
||||
EVENT_SUBMIT_FAIL,
|
||||
EVENT_JOIN_REQUEST,
|
||||
EVENT_JOIN_SUCCESS,
|
||||
EVENT_JOIN_FAIL,
|
||||
EVENT_LEAVE_REQUEST,
|
||||
EVENT_LEAVE_SUCCESS,
|
||||
EVENT_LEAVE_FAIL,
|
||||
locationSearch,
|
||||
changeCreateEventName,
|
||||
changeCreateEventDescription,
|
||||
|
@ -265,4 +344,12 @@ export {
|
|||
submitEventRequest,
|
||||
submitEventSuccess,
|
||||
submitEventFail,
|
||||
joinEvent,
|
||||
joinEventRequest,
|
||||
joinEventSuccess,
|
||||
joinEventFail,
|
||||
leaveEvent,
|
||||
leaveEventRequest,
|
||||
leaveEventSuccess,
|
||||
leaveEventFail,
|
||||
};
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { FormattedDate, FormattedMessage } from 'react-intl';
|
||||
import { defineMessages, FormattedDate, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { useAppSelector } from 'soapbox/hooks';
|
||||
import { joinEvent, leaveEvent } from 'soapbox/actions/events';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { buildStatus } from 'soapbox/features/scheduled_statuses/builder';
|
||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||
|
||||
import Icon from './icon';
|
||||
import { Button, HStack, Stack, Text } from './ui';
|
||||
|
@ -9,11 +12,19 @@ import VerificationBadge from './verification_badge';
|
|||
|
||||
import type { Account as AccountEntity, Status as StatusEntity } from 'soapbox/types/entities';
|
||||
|
||||
const messages = defineMessages({
|
||||
leaveConfirm: { id: 'confirmations.leave_event.confirm', defaultMessage: 'Leave event' },
|
||||
leaveMessage: { id: 'confirmations.leave_event.message', defaultMessage: 'If you want to rejoin the event, the request will be manually reviewed again. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
interface IEventPreview {
|
||||
status: StatusEntity,
|
||||
}
|
||||
|
||||
const EventPreview: React.FC<IEventPreview> = ({ status }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const me = useAppSelector((state) => state.me);
|
||||
|
||||
const account = status.account as AccountEntity;
|
||||
|
@ -21,6 +32,32 @@ const EventPreview: React.FC<IEventPreview> = ({ status }) => {
|
|||
|
||||
const banner = status.media_attachments?.find(({ description }) => description === 'Banner');
|
||||
|
||||
const handleJoin: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (event.join_mode === 'free') {
|
||||
dispatch(joinEvent(status.id));
|
||||
} else {
|
||||
dispatch(openModal('JOIN_EVENT', {
|
||||
statusId: status.id,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleLeave: React.EventHandler<React.MouseEvent> = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (event.join_mode === 'restricted') {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.leaveMessage),
|
||||
confirm: intl.formatMessage(messages.leaveConfirm),
|
||||
onConfirm: () => dispatch(leaveEvent(status.id)),
|
||||
}));
|
||||
} else {
|
||||
dispatch(leaveEvent(status.id));
|
||||
}
|
||||
};
|
||||
|
||||
const renderDate = useCallback(() => {
|
||||
if (!event.start_time) return null;
|
||||
|
||||
|
@ -64,6 +101,43 @@ const EventPreview: React.FC<IEventPreview> = ({ status }) => {
|
|||
);
|
||||
}, [event.start_time, event.end_time]);
|
||||
|
||||
const renderAction = useCallback(() => {
|
||||
let buttonLabel;
|
||||
let buttonIcon;
|
||||
let buttonDisabled = false;
|
||||
let buttonAction = handleLeave;
|
||||
|
||||
switch (event.join_state) {
|
||||
case 'accept':
|
||||
buttonLabel = <FormattedMessage id='event.join_state.accept' defaultMessage='Going' />;
|
||||
buttonIcon = require('@tabler/icons/check.svg');
|
||||
break;
|
||||
case 'pending':
|
||||
buttonLabel = <FormattedMessage id='event.join_state.pending' defaultMessage='Pending' />;
|
||||
break;
|
||||
case 'reject':
|
||||
buttonLabel = <FormattedMessage id='event.join_state.rejected' defaultMessage='Going' />;
|
||||
buttonIcon = require('@tabler/icons/ban.svg');
|
||||
buttonDisabled = true;
|
||||
break;
|
||||
default:
|
||||
buttonLabel = <FormattedMessage id='event.join_state.empty' defaultMessage='Participate' />;
|
||||
buttonAction = handleJoin;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
size='sm'
|
||||
theme='secondary'
|
||||
icon={buttonIcon}
|
||||
onClick={buttonAction}
|
||||
disabled={buttonDisabled}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
);
|
||||
}, [event.join_state]);
|
||||
|
||||
return (
|
||||
<div className='rounded-lg bg-gray-100 dark:bg-primary-800 shadow-xl relative overflow-hidden'>
|
||||
<div className='absolute top-28 right-3'>
|
||||
|
@ -75,22 +149,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status }) => {
|
|||
>
|
||||
<FormattedMessage id='event.manage' defaultMessage='Manage' />
|
||||
</Button>
|
||||
) : event.join_state === 'accept' ? (
|
||||
<Button
|
||||
size='sm'
|
||||
theme='secondary'
|
||||
icon={require('@tabler/icons/check.svg')}
|
||||
>
|
||||
<FormattedMessage id='event.join_state.accept' defaultMessage='Going' />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size='sm'
|
||||
theme='primary'
|
||||
>
|
||||
<FormattedMessage id='event.join_state.empty' defaultMessage='Participate' />
|
||||
</Button>
|
||||
)}
|
||||
) : renderAction()}
|
||||
</div>
|
||||
<div className='bg-primary-200 dark:bg-gray-600 h-40'>
|
||||
{banner && <img className='h-full w-full object-cover' src={banner.url} alt={banner.url} />}
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
VerifySmsModal,
|
||||
FamiliarFollowersModal,
|
||||
CreateEventModal,
|
||||
JoinEventModal,
|
||||
} from 'soapbox/features/ui/util/async-components';
|
||||
|
||||
import BundleContainer from '../containers/bundle_container';
|
||||
|
@ -71,6 +72,7 @@ const MODAL_COMPONENTS = {
|
|||
'VERIFY_SMS': VerifySmsModal,
|
||||
'FAMILIAR_FOLLOWERS': FamiliarFollowersModal,
|
||||
'CREATE_EVENT': CreateEventModal,
|
||||
'JOIN_EVENT': JoinEventModal,
|
||||
};
|
||||
|
||||
export default class ModalRoot extends React.PureComponent {
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
import React, { useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
import { joinEvent } from 'soapbox/actions/events';
|
||||
import { closeModal } from 'soapbox/actions/modals';
|
||||
import { Modal, Text } from 'soapbox/components/ui';
|
||||
import { useAppDispatch } from 'soapbox/hooks';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'join_event.placeholder', defaultMessage: 'Message to organizer' },
|
||||
join: { id: 'join_event.join', defaultMessage: 'Request join' },
|
||||
});
|
||||
|
||||
interface IAccountNoteModal {
|
||||
statusId: string,
|
||||
}
|
||||
|
||||
const AccountNoteModal: React.FC<IAccountNoteModal> = ({ statusId }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const [participationMessage, setParticipationMessage] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const onClose = () => {
|
||||
dispatch(closeModal('JOIN_EVENT'));
|
||||
};
|
||||
|
||||
const handleChange: React.ChangeEventHandler<HTMLTextAreaElement> = e => {
|
||||
setParticipationMessage(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsSubmitting(true);
|
||||
dispatch(joinEvent(statusId, participationMessage))?.then(() => {
|
||||
onClose();
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const handleKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = e => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
handleSubmit();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<FormattedMessage id='join_event.title' defaultMessage='Join event' />}
|
||||
onClose={onClose}
|
||||
confirmationAction={handleSubmit}
|
||||
confirmationText={intl.formatMessage(messages.join)}
|
||||
confirmationDisabled={isSubmitting}
|
||||
>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='join_event.hint' defaultMessage='You can tell the organizer why do you want to participate in this event:' />
|
||||
</Text>
|
||||
|
||||
<textarea
|
||||
className='setting-text light'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={participationMessage}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isSubmitting}
|
||||
autoFocus
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AccountNoteModal;
|
|
@ -529,3 +529,7 @@ export function AnnouncementsPanel() {
|
|||
export function CreateEventModal() {
|
||||
return import(/* webpackChunkName: "features/create_event_modal" */'../components/modals/create-event-modal/create-event-modal');
|
||||
}
|
||||
|
||||
export function JoinEventModal() {
|
||||
return import(/* webpackChunkName: "features/join_event_modal" */'../components/modals/join-event-modal');
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue