Participation management

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2022-09-07 20:03:26 +02:00
parent 3f7e593695
commit 3f4f820de7
5 changed files with 245 additions and 22 deletions

View File

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

View File

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

View File

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

View File

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

View File

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