diff --git a/app/soapbox/actions/events.ts b/app/soapbox/actions/events.ts index ac2cbdf74..05e3feef8 100644 --- a/app/soapbox/actions/events.ts +++ b/app/soapbox/actions/events.ts @@ -6,8 +6,13 @@ import resizeImage from 'soapbox/utils/resize_image'; import { importFetchedAccounts, importFetchedStatus } from './importer'; import { fetchMedia, uploadMedia } from './media'; -import { closeModal } from './modals'; +import { closeModal, openModal } from './modals'; 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 { 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_FAIL = 'LOCATION_SEARCH_FAIL'; -const CREATE_EVENT_NAME_CHANGE = 'CREATE_EVENT_NAME_CHANGE'; -const CREATE_EVENT_DESCRIPTION_CHANGE = 'CREATE_EVENT_DESCRIPTION_CHANGE'; -const CREATE_EVENT_START_TIME_CHANGE = 'CREATE_EVENT_START_TIME_CHANGE'; -const CREATE_EVENT_HAS_END_TIME_CHANGE = 'CREATE_EVENT_HAS_END_TIME_CHANGE'; -const CREATE_EVENT_END_TIME_CHANGE = 'CREATE_EVENT_END_TIME_CHANGE'; -const CREATE_EVENT_APPROVAL_REQUIRED_CHANGE = 'CREATE_EVENT_APPROVAL_REQUIRED_CHANGE'; -const CREATE_EVENT_LOCATION_CHANGE = 'CREATE_EVENT_LOCATION_CHANGE'; +const EDIT_EVENT_NAME_CHANGE = 'EDIT_EVENT_NAME_CHANGE'; +const EDIT_EVENT_DESCRIPTION_CHANGE = 'EDIT_EVENT_DESCRIPTION_CHANGE'; +const EDIT_EVENT_START_TIME_CHANGE = 'EDIT_EVENT_START_TIME_CHANGE'; +const EDIT_EVENT_HAS_END_TIME_CHANGE = 'EDIT_EVENT_HAS_END_TIME_CHANGE'; +const EDIT_EVENT_END_TIME_CHANGE = 'EDIT_EVENT_END_TIME_CHANGE'; +const EDIT_EVENT_APPROVAL_REQUIRED_CHANGE = 'EDIT_EVENT_APPROVAL_REQUIRED_CHANGE'; +const EDIT_EVENT_LOCATION_CHANGE = 'EDIT_EVENT_LOCATION_CHANGE'; const EVENT_BANNER_UPLOAD_REQUEST = 'EVENT_BANNER_UPLOAD_REQUEST'; 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_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 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' }, + success: { id: 'compose_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' }, + 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) => @@ -81,37 +100,37 @@ const locationSearch = (query: string, signal?: AbortSignal) => }); }; -const changeCreateEventName = (value: string) => ({ - type: CREATE_EVENT_NAME_CHANGE, +const changeEditEventName = (value: string) => ({ + type: EDIT_EVENT_NAME_CHANGE, value, }); -const changeCreateEventDescription = (value: string) => ({ - type: CREATE_EVENT_DESCRIPTION_CHANGE, +const changeEditEventDescription = (value: string) => ({ + type: EDIT_EVENT_DESCRIPTION_CHANGE, value, }); -const changeCreateEventStartTime = (value: Date) => ({ - type: CREATE_EVENT_START_TIME_CHANGE, +const changeEditEventStartTime = (value: Date) => ({ + type: EDIT_EVENT_START_TIME_CHANGE, value, }); -const changeCreateEventEndTime = (value: Date) => ({ - type: CREATE_EVENT_END_TIME_CHANGE, +const changeEditEventEndTime = (value: Date) => ({ + type: EDIT_EVENT_END_TIME_CHANGE, value, }); -const changeCreateEventHasEndTime = (value: boolean) => ({ - type: CREATE_EVENT_HAS_END_TIME_CHANGE, +const changeEditEventHasEndTime = (value: boolean) => ({ + type: EDIT_EVENT_HAS_END_TIME_CHANGE, value, }); -const changeCreateEventApprovalRequired = (value: boolean) => ({ - type: CREATE_EVENT_APPROVAL_REQUIRED_CHANGE, +const changeEditEventApprovalRequired = (value: boolean) => ({ + type: EDIT_EVENT_APPROVAL_REQUIRED_CHANGE, value, }); -const changeCreateEventLocation = (value: string | null) => +const changeEditEventLocation = (value: string | null) => (dispatch: AppDispatch, getState: () => RootState) => { let location = null; @@ -120,7 +139,7 @@ const changeCreateEventLocation = (value: string | null) => } dispatch({ - type: CREATE_EVENT_LOCATION_CHANGE, + type: EDIT_EVENT_LOCATION_CHANGE, value: location, }); }; @@ -202,15 +221,15 @@ const submitEvent = () => (dispatch: AppDispatch, getState: () => RootState) => { const state = getState(); - const name = state.create_event.name; - const status = state.create_event.status; - const banner = state.create_event.banner; - const startTime = state.create_event.start_time; - const endTime = state.create_event.end_time; - const joinMode = state.create_event.approval_required ? 'restricted' : 'free'; - const location = state.create_event.location; + const name = state.compose_event.name; + const status = state.compose_event.status; + const banner = state.compose_event.banner; + const startTime = state.compose_event.start_time; + const endTime = state.compose_event.end_time; + const joinMode = state.compose_event.approval_required ? 'restricted' : 'free'; + const location = state.compose_event.location; - if (!status || !status.length) { + if (!name || !name.length) { return; } @@ -228,7 +247,7 @@ const submitEvent = () => if (location) params.location_id = location.origin_id; return api(getState).post('/api/v1/pleroma/events', params).then(({ data }) => { - dispatch(closeModal('CREATE_EVENT')); + dispatch(closeModal('COMPOSE_EVENT')); dispatch(importFetchedStatus(data)); dispatch(submitEventSuccess(data)); 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)); - 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(joinEventSuccess(data)); dispatch(snackbar.success( @@ -461,21 +482,108 @@ const expandEventParticipationRequestsFail = (id: string, error: AxiosError) => 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) => (dispatch: any, getState: () => RootState) => 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 { LOCATION_SEARCH_REQUEST, LOCATION_SEARCH_SUCCESS, LOCATION_SEARCH_FAIL, - CREATE_EVENT_NAME_CHANGE, - CREATE_EVENT_DESCRIPTION_CHANGE, - CREATE_EVENT_START_TIME_CHANGE, - CREATE_EVENT_END_TIME_CHANGE, - CREATE_EVENT_HAS_END_TIME_CHANGE, - CREATE_EVENT_APPROVAL_REQUIRED_CHANGE, - CREATE_EVENT_LOCATION_CHANGE, + EDIT_EVENT_NAME_CHANGE, + EDIT_EVENT_DESCRIPTION_CHANGE, + EDIT_EVENT_START_TIME_CHANGE, + EDIT_EVENT_END_TIME_CHANGE, + EDIT_EVENT_HAS_END_TIME_CHANGE, + EDIT_EVENT_APPROVAL_REQUIRED_CHANGE, + EDIT_EVENT_LOCATION_CHANGE, EVENT_BANNER_UPLOAD_REQUEST, EVENT_BANNER_UPLOAD_PROGRESS, EVENT_BANNER_UPLOAD_SUCCESS, @@ -502,14 +610,22 @@ export { EVENT_PARTICIPATION_REQUESTS_EXPAND_REQUEST, EVENT_PARTICIPATION_REQUESTS_EXPAND_SUCCESS, 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, - changeCreateEventName, - changeCreateEventDescription, - changeCreateEventStartTime, - changeCreateEventEndTime, - changeCreateEventHasEndTime, - changeCreateEventApprovalRequired, - changeCreateEventLocation, + changeEditEventName, + changeEditEventDescription, + changeEditEventStartTime, + changeEditEventEndTime, + changeEditEventHasEndTime, + changeEditEventApprovalRequired, + changeEditEventLocation, uploadEventBanner, uploadEventBannerRequest, uploadEventBannerProgress, @@ -544,5 +660,15 @@ export { expandEventParticipationRequestsRequest, expandEventParticipationRequestsSuccess, expandEventParticipationRequestsFail, + authorizeEventParticipationRequest, + authorizeEventParticipationRequestRequest, + authorizeEventParticipationRequestSuccess, + authorizeEventParticipationRequestFail, + rejectEventParticipationRequest, + rejectEventParticipationRequestRequest, + rejectEventParticipationRequestSuccess, + rejectEventParticipationRequestFail, fetchEventIcs, + cancelEventCompose, + editEvent, }; diff --git a/app/soapbox/components/account.tsx b/app/soapbox/components/account.tsx index 438d2f0f4..a662460ca 100644 --- a/app/soapbox/components/account.tsx +++ b/app/soapbox/components/account.tsx @@ -63,6 +63,7 @@ interface IAccount { withRelationship?: boolean, showEdit?: boolean, emoji?: string, + note?: string, } const Account = ({ @@ -86,6 +87,7 @@ const Account = ({ withRelationship = true, showEdit = false, emoji, + note, }: IAccount) => { const overflowRef = React.useRef(null); const actionRef = React.useRef(null); @@ -163,7 +165,7 @@ const Account = ({ return (
- + {children}} @@ -206,7 +208,7 @@ const Account = ({ - + @{username} @@ -237,7 +239,14 @@ const Account = ({ ) : null} - {withAccountNote && ( + {note ? ( + + {note} + + ) : withAccountNote && ( = ({ status }) => { const handleManageClick: React.MouseEventHandler = e => { e.stopPropagation(); - dispatch(openModal('MANAGE_EVENT', { - statusId: status.id, - })); + dispatch(editEvent(status.id)); }; const handleParticipantsClick: React.MouseEventHandler = e => { @@ -228,7 +226,6 @@ const EventHeader: React.FC = ({ status }) => { size='sm' theme='secondary' onClick={handleManageClick} - to={`/@${account.acct}/events/${status.id}`} > diff --git a/app/soapbox/features/notifications/components/notification.tsx b/app/soapbox/features/notifications/components/notification.tsx index e1fa87c5a..06d329bb9 100644 --- a/app/soapbox/features/notifications/components/notification.tsx +++ b/app/soapbox/features/notifications/components/notification.tsx @@ -53,6 +53,9 @@ const icons: Record = { 'pleroma:emoji_reaction': require('@tabler/icons/mood-happy.svg'), user_approved: require('@tabler/icons/user-plus.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 = defineMessages({ @@ -104,6 +107,18 @@ const messages: Record = defineMessages({ id: 'notification.update', 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 = ( @@ -302,6 +317,9 @@ const Notification: React.FC = (props) => { case 'poll': case 'update': case 'pleroma:emoji_reaction': + case 'pleroma:event_reminder': + case 'pleroma:participation_accepted': + case 'pleroma:participation_request': return status && typeof status === 'object' ? ( { const dispatch = useDispatch(); - const onOpenCompose = () => dispatch(openModal('CREATE_EVENT')); + const onOpenCompose = () => dispatch(openModal('COMPOSE_EVENT')); return (
diff --git a/app/soapbox/features/ui/components/modal_root.js b/app/soapbox/features/ui/components/modal_root.js index 86cd868ac..8ee7b5fc5 100644 --- a/app/soapbox/features/ui/components/modal_root.js +++ b/app/soapbox/features/ui/components/modal_root.js @@ -32,7 +32,7 @@ import { CompareHistoryModal, VerifySmsModal, FamiliarFollowersModal, - CreateEventModal, + ComposeEventModal, JoinEventModal, AccountModerationModal, EventMapModal, @@ -74,7 +74,7 @@ const MODAL_COMPONENTS = { 'COMPARE_HISTORY': CompareHistoryModal, 'VERIFY_SMS': VerifySmsModal, 'FAMILIAR_FOLLOWERS': FamiliarFollowersModal, - 'CREATE_EVENT': CreateEventModal, + 'COMPOSE_EVENT': ComposeEventModal, 'JOIN_EVENT': JoinEventModal, 'ACCOUNT_MODERATION': AccountModerationModal, 'EVENT_MAP': EventMapModal, diff --git a/app/soapbox/features/ui/components/modals/compose-event-modal/compose-event-modal.tsx b/app/soapbox/features/ui/components/modals/compose-event-modal/compose-event-modal.tsx new file mode 100644 index 000000000..0c24d21f1 --- /dev/null +++ b/app/soapbox/features/ui/components/modals/compose-event-modal/compose-event-modal.tsx @@ -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 = ({ eventId, id, participationMessage }) => { + const intl = useIntl(); + const dispatch = useAppDispatch(); + + const handleAuthorize = () => { + dispatch(authorizeEventParticipationRequest(eventId, id)); + }; + + const handleReject = () => { + dispatch(rejectEventParticipationRequest(eventId, id)); + }; + + return ( + +