From 404dc6d8e2c5fb8116e178beb00c42699ad626e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Sun, 15 Jun 2025 12:38:54 +0200 Subject: [PATCH] pl-fe: use mutations for event joining/leaving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: nicole mikołajczyk --- packages/pl-fe/src/actions/events.ts | 95 +++++-------------- .../event/components/event-action-button.tsx | 14 +-- packages/pl-fe/src/hooks/use-features.ts | 2 +- .../pl-fe/src/modals/join-event-modal.tsx | 13 +-- .../statuses/use-event-interactions.ts | 78 +++++++++++++++ 5 files changed, 118 insertions(+), 84 deletions(-) create mode 100644 packages/pl-fe/src/queries/statuses/use-event-interactions.ts diff --git a/packages/pl-fe/src/actions/events.ts b/packages/pl-fe/src/actions/events.ts index b65b4c85f..65e2ed8f9 100644 --- a/packages/pl-fe/src/actions/events.ts +++ b/packages/pl-fe/src/actions/events.ts @@ -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['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['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['join_state'] | null) => ({ - type: EVENT_LEAVE_FAIL, - statusId, - error, - previousState, -}); +interface LeaveEventFail { + type: typeof EVENT_LEAVE_FAIL; + statusId: string; + error: unknown; + previousState: Exclude['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 - | ReturnType - | ReturnType - | ReturnType + JoinEventRequest + | JoinEventFail + | LeaveEventRequest + | LeaveEventFail | ReturnType | EventFormSetAction; @@ -199,8 +154,6 @@ export { EVENT_COMPOSE_CANCEL, EVENT_FORM_SET, submitEvent, - joinEvent, - leaveEvent, fetchEventIcs, cancelEventCompose, initEventEdit, diff --git a/packages/pl-fe/src/features/event/components/event-action-button.tsx b/packages/pl-fe/src/features/event/components/event-action-button.tsx index ee44f8b33..7fd914280 100644 --- a/packages/pl-fe/src/features/event/components/event-action-button.tsx +++ b/packages/pl-fe/src/features/event/components/event-action-button.tsx @@ -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 = ({ 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 = ({ status, theme = 'secondary' const handleJoin: React.EventHandler = (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 = ({ 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(); } }; diff --git a/packages/pl-fe/src/hooks/use-features.ts b/packages/pl-fe/src/hooks/use-features.ts index 0b6560f76..8a9e168b1 100644 --- a/packages/pl-fe/src/hooks/use-features.ts +++ b/packages/pl-fe/src/hooks/use-features.ts @@ -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 }; diff --git a/packages/pl-fe/src/modals/join-event-modal.tsx b/packages/pl-fe/src/modals/join-event-modal.tsx index 9afab1007..84b9037c3 100644 --- a/packages/pl-fe/src/modals/join-event-modal.tsx +++ b/packages/pl-fe/src/modals/join-event-modal.tsx @@ -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 = ({ 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 = ({ onClos const handleSubmit = () => { setIsSubmitting(true); - dispatch(joinEvent(statusId, participationMessage)).then(() => { - handleClose(); - }).catch(() => {}); + joinEvent(participationMessage, { + onSuccess: () => handleClose(), + onError: () => setIsSubmitting(false), + }); }; const handleKeyDown: React.KeyboardEventHandler = e => { diff --git a/packages/pl-fe/src/queries/statuses/use-event-interactions.ts b/packages/pl-fe/src/queries/statuses/use-event-interactions.ts new file mode 100644 index 000000000..b242d8cc8 --- /dev/null +++ b/packages/pl-fe/src/queries/statuses/use-event-interactions.ts @@ -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['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({ type: 'EVENT_JOIN_REQUEST', statusId }), + onError: (error) => dispatch({ 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['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({ type: 'EVENT_LEAVE_REQUEST', statusId }), + onError: (error) => dispatch({ type: 'EVENT_LEAVE_FAIL', statusId, error, previousState }), + onSettled: (status) => { + if (!status) return; + dispatch(importEntities({ statuses: [status] })); + queryClient.invalidateQueries({ queryKey: ['accountsLists', 'joinedEvents'] }); + }, + }); +}; + +export { useJoinEventMutation, useLeaveEventMutation };