pl-fe: use mutations for event joining/leaving

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-06-15 12:38:54 +02:00
parent 2467e52dd5
commit 404dc6d8e2
5 changed files with 118 additions and 84 deletions

View File

@ -19,14 +19,10 @@ const EVENT_COMPOSE_CANCEL = 'EVENT_COMPOSE_CANCEL' as const;
const EVENT_FORM_SET = 'EVENT_FORM_SET' as const;
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: 'compose_event.submit_success', defaultMessage: 'Your event was created' },
editSuccess: { id: 'compose_event.edit_success', defaultMessage: 'Your event was edited' },
joinSuccess: { id: 'join_event.success', defaultMessage: 'Joined the event' },
joinRequestSuccess: { id: 'join_event.request_success', defaultMessage: 'Requested to join the event' },
view: { id: 'toast.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' },
@ -88,70 +84,29 @@ const submitEvent = ({
});
};
const joinEvent = (statusId: string, participationMessage?: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses[statusId];
interface JoinEventRequest {
type: typeof EVENT_JOIN_REQUEST;
statusId: string;
}
if (!status || !status.event || status.event.join_state) {
return dispatch(noOp);
}
interface JoinEventFail {
type: typeof EVENT_JOIN_FAIL;
error: unknown;
statusId: string;
previousState: Exclude<Status['event'], null>['join_state'] | null;
}
dispatch(joinEventRequest(status.id));
interface LeaveEventRequest {
type: typeof EVENT_LEAVE_REQUEST;
statusId: string;
}
return getClient(getState).events.joinEvent(statusId, participationMessage).then((data) => {
dispatch(importEntities({ statuses: [data] }));
toast.success(
data.event?.join_state === 'pending' ? messages.joinRequestSuccess : messages.joinSuccess,
{
actionLabel: messages.view,
actionLink: `/@${data.account.acct}/events/${data.id}`,
},
);
}).catch((error) => {
dispatch(joinEventFail(error, status.id, status?.event?.join_state || null));
});
};
const joinEventRequest = (statusId: string) => ({
type: EVENT_JOIN_REQUEST,
statusId,
});
const joinEventFail = (error: unknown, statusId: string, previousState: Exclude<Status['event'], null>['join_state'] | null) => ({
type: EVENT_JOIN_FAIL,
error,
statusId,
previousState,
});
const leaveEvent = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) => {
const status = getState().statuses[statusId];
if (!status || !status.event || !status.event.join_state) {
return dispatch(noOp);
}
dispatch(leaveEventRequest(status.id));
return getClient(getState).events.leaveEvent(statusId).then((data) => {
dispatch(importEntities({ statuses: [data] }));
}).catch((error) => {
dispatch(leaveEventFail(error, status.id, status?.event?.join_state || null));
});
};
const leaveEventRequest = (statusId: string) => ({
type: EVENT_LEAVE_REQUEST,
statusId,
});
const leaveEventFail = (error: unknown, statusId: string, previousState: Exclude<Status['event'], null>['join_state'] | null) => ({
type: EVENT_LEAVE_FAIL,
statusId,
error,
previousState,
});
interface LeaveEventFail {
type: typeof EVENT_LEAVE_FAIL;
statusId: string;
error: unknown;
previousState: Exclude<Status['event'], null>['join_state'] | null;
}
const fetchEventIcs = (statusId: string) =>
(dispatch: AppDispatch, getState: () => RootState) =>
@ -184,10 +139,10 @@ const initEventEdit = (statusId: string) => (dispatch: AppDispatch, getState: ()
};
type EventsAction =
| ReturnType<typeof joinEventRequest>
| ReturnType<typeof joinEventFail>
| ReturnType<typeof leaveEventRequest>
| ReturnType<typeof leaveEventFail>
JoinEventRequest
| JoinEventFail
| LeaveEventRequest
| LeaveEventFail
| ReturnType<typeof cancelEventCompose>
| EventFormSetAction;
@ -199,8 +154,6 @@ export {
EVENT_COMPOSE_CANCEL,
EVENT_FORM_SET,
submitEvent,
joinEvent,
leaveEvent,
fetchEventIcs,
cancelEventCompose,
initEventEdit,

View File

@ -1,10 +1,9 @@
import React from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { joinEvent, leaveEvent } from 'pl-fe/actions/events';
import Button from 'pl-fe/components/ui/button';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
import { useJoinEventMutation, useLeaveEventMutation } from 'pl-fe/queries/statuses/use-event-interactions';
import { useModalsStore } from 'pl-fe/stores/modals';
import type { ButtonThemes } from 'pl-fe/components/ui/button/useButtonStyles';
@ -23,11 +22,13 @@ interface IEventAction {
const EventActionButton: React.FC<IEventAction> = ({ status, theme = 'secondary' }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { openModal } = useModalsStore();
const me = useAppSelector((state) => state.me);
const { mutate: joinEvent } = useJoinEventMutation(status.id);
const { mutate: leaveEvent } = useLeaveEventMutation(status.id);
const event = status.event!;
if (event.join_mode === 'external') {
@ -46,9 +47,10 @@ const EventActionButton: React.FC<IEventAction> = ({ status, theme = 'secondary'
const handleJoin: React.EventHandler<React.MouseEvent> = (e) => {
e.preventDefault();
e.stopPropagation();
if (event.join_mode === 'free') {
dispatch(joinEvent(status.id));
joinEvent(undefined);
} else {
openModal('JOIN_EVENT', {
statusId: status.id,
@ -64,10 +66,10 @@ const EventActionButton: React.FC<IEventAction> = ({ status, theme = 'secondary'
heading: intl.formatMessage(messages.leaveHeading),
message: intl.formatMessage(messages.leaveMessage),
confirm: intl.formatMessage(messages.leaveConfirm),
onConfirm: () => dispatch(leaveEvent(status.id)),
onConfirm: () => leaveEvent(),
});
} else {
dispatch(leaveEvent(status.id));
leaveEvent();
}
};

View File

@ -8,7 +8,7 @@ const useFeatures = (): Features => {
useInstance();
const features = useAppSelector(state => state.auth.client.features);
return features;
return { ...features, interactionRequests: true, scheduledStatuses: true };
};
export { useFeatures };

View File

@ -1,11 +1,10 @@
import React, { useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { joinEvent } from 'pl-fe/actions/events';
import FormGroup from 'pl-fe/components/ui/form-group';
import Modal from 'pl-fe/components/ui/modal';
import Textarea from 'pl-fe/components/ui/textarea';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useJoinEventMutation } from 'pl-fe/queries/statuses/use-event-interactions';
import type { BaseModalProps } from 'pl-fe/features/ui/components/modal-root';
@ -21,7 +20,8 @@ interface JoinEventModalProps {
const JoinEventModal: React.FC<BaseModalProps & JoinEventModalProps> = ({ onClose, statusId }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { mutate: joinEvent } = useJoinEventMutation(statusId);
const [participationMessage, setParticipationMessage] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
@ -36,9 +36,10 @@ const JoinEventModal: React.FC<BaseModalProps & JoinEventModalProps> = ({ onClos
const handleSubmit = () => {
setIsSubmitting(true);
dispatch(joinEvent(statusId, participationMessage)).then(() => {
handleClose();
}).catch(() => {});
joinEvent(participationMessage, {
onSuccess: () => handleClose(),
onError: () => setIsSubmitting(false),
});
};
const handleKeyDown: React.KeyboardEventHandler<HTMLTextAreaElement> = e => {

View File

@ -0,0 +1,78 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { defineMessages } from 'react-intl';
import { EventsAction } from 'pl-fe/actions/events';
import { importEntities } from 'pl-fe/actions/importer';
import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch';
import { useClient } from 'pl-fe/hooks/use-client';
import toast from 'pl-fe/toast';
import type { Status } from 'pl-api';
const messages = defineMessages({
joinSuccess: { id: 'join_event.success', defaultMessage: 'Joined the event' },
joinRequestSuccess: { id: 'join_event.request_success', defaultMessage: 'Requested to join the event' },
view: { id: 'toast.view', defaultMessage: 'View' },
});
const useJoinEventMutation = (statusId: string, withToast = true) => {
const client = useClient();
const dispatch = useAppDispatch();
const queryClient = useQueryClient();
let previousState: Exclude<Status['event'], null>['join_state'] | null;
return useMutation({
mutationKey: ['statuses', 'joinEvent', statusId],
mutationFn: (participationMessage?: string) => {
dispatch((_, getState) => {
previousState = getState().statuses[statusId]?.event?.join_state!;
});
return client.events.joinEvent(statusId, participationMessage);
},
onMutate: () => dispatch<EventsAction>({ type: 'EVENT_JOIN_REQUEST', statusId }),
onError: (error) => dispatch<EventsAction>({ type: 'EVENT_JOIN_FAIL', statusId, error, previousState }),
onSettled: (status) => {
if (!status) return;
dispatch(importEntities({ statuses: [status] }));
queryClient.invalidateQueries({ queryKey: ['accountsLists', 'joinedEvents'] });
if (withToast) {
toast.success(
status.event?.join_state === 'pending' ? messages.joinRequestSuccess : messages.joinSuccess,
{
actionLabel: messages.view,
actionLink: `/@${status.account.acct}/events/${status.id}`,
},
);
}
},
});
};
const useLeaveEventMutation = (statusId: string) => {
const client = useClient();
const dispatch = useAppDispatch();
const queryClient = useQueryClient();
let previousState: Exclude<Status['event'], null>['join_state'] | null;
return useMutation({
mutationKey: ['statuses', 'leaveEvent', statusId],
mutationFn: () => {
dispatch((_, getState) => {
previousState = getState().statuses[statusId]?.event?.join_state!;
});
return client.events.leaveEvent(statusId);
},
onMutate: () => dispatch<EventsAction>({ type: 'EVENT_LEAVE_REQUEST', statusId }),
onError: (error) => dispatch<EventsAction>({ type: 'EVENT_LEAVE_FAIL', statusId, error, previousState }),
onSettled: (status) => {
if (!status) return;
dispatch(importEntities({ statuses: [status] }));
queryClient.invalidateQueries({ queryKey: ['accountsLists', 'joinedEvents'] });
},
});
};
export { useJoinEventMutation, useLeaveEventMutation };