From 8ca50b8d1b0c74c94e19a69bf18e99dc8054cfd8 Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:21:11 +0100 Subject: [PATCH] [#592] temporary fix for slow attendance update (#516) --- .../AttendanceValidation.tsx | 13 ++- .../AttendanceValidation/RSVPButton.tsx | 110 ++++++++++++++---- .../AttendanceValidation/handleRSVPClick.tsx | 23 +++- src/features/Events/EventDisplayPreview.tsx | 1 - 4 files changed, 121 insertions(+), 26 deletions(-) diff --git a/src/features/Events/AttendanceValidation/AttendanceValidation.tsx b/src/features/Events/AttendanceValidation/AttendanceValidation.tsx index ddf6a5b..3f0d7b0 100644 --- a/src/features/Events/AttendanceValidation/AttendanceValidation.tsx +++ b/src/features/Events/AttendanceValidation/AttendanceValidation.tsx @@ -1,9 +1,10 @@ import { userData } from "@/features/User/userDataTypes"; import { Box, Typography } from "@linagora/twake-mui"; -import { Dispatch, SetStateAction } from "react"; +import { Dispatch, SetStateAction, useState } from "react"; import { useI18n } from "twake-i18n"; import { ContextualizedEvent } from "../EventsTypes"; import { RSVPButton } from "./RSVPButton"; +import { PartStat } from "@/features/User/models/attendee"; interface AttendanceValidationProps { contextualizedEvent: ContextualizedEvent; @@ -20,6 +21,8 @@ export function AttendanceValidation({ }: AttendanceValidationProps) { const { currentUserAttendee, isOwn } = contextualizedEvent; const { t } = useI18n(); + const [isLoading, setIsLoading] = useState(false); + const [loadingValue, setLoadingValue] = useState(null); // Check if we should show RSVP buttons const hasNoAttendeesOrOrganizer = @@ -30,11 +33,19 @@ export function AttendanceValidation({ return null; } + const handleLoadingChange = (loading: boolean, value?: PartStat) => { + setIsLoading(loading); + setLoadingValue(loading && value ? value : null); + }; + const commonButtonProps = { contextualizedEvent, user, setAfterChoiceFunc, setOpenEditModePopup, + isLoading, + onLoadingChange: handleLoadingChange, + loadingValue, }; return ( diff --git a/src/features/Events/AttendanceValidation/RSVPButton.tsx b/src/features/Events/AttendanceValidation/RSVPButton.tsx index 6b8a72f..ef21582 100644 --- a/src/features/Events/AttendanceValidation/RSVPButton.tsx +++ b/src/features/Events/AttendanceValidation/RSVPButton.tsx @@ -1,8 +1,8 @@ import { useAppDispatch } from "@/app/hooks"; import { PartStat } from "@/features/User/models/attendee"; import { userData } from "@/features/User/userDataTypes"; -import { Button } from "@linagora/twake-mui"; -import { Dispatch, SetStateAction } from "react"; +import { Button, CircularProgress, Box, Theme } from "@linagora/twake-mui"; +import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; import { useI18n } from "twake-i18n"; import { ContextualizedEvent } from "../EventsTypes"; import { handleRSVPClick } from "./handleRSVPClick"; @@ -21,6 +21,9 @@ interface RSVPButtonProps { user: userData | undefined; setAfterChoiceFunc: Dispatch>; setOpenEditModePopup: Dispatch>; + isLoading: boolean; + onLoadingChange: (loading: boolean, value?: PartStat) => void; + loadingValue: PartStat | null; } export function RSVPButton({ @@ -29,35 +32,98 @@ export function RSVPButton({ user, setAfterChoiceFunc, setOpenEditModePopup, + isLoading, + onLoadingChange, + loadingValue, }: RSVPButtonProps) { const { t } = useI18n(); const dispatch = useAppDispatch(); const { currentUserAttendee } = contextualizedEvent; + const showLoading = isLoading && loadingValue === rsvpValue; + const previousPartstatRef = useRef( + currentUserAttendee?.partstat + ); + + // Detect when attendee status changes (via WebSocket) and clear loading + useEffect(() => { + const currentPartstat = currentUserAttendee?.partstat; + + if ( + isLoading && + previousPartstatRef.current !== undefined && + currentPartstat !== previousPartstatRef.current + ) { + onLoadingChange(false); + } + + previousPartstatRef.current = currentPartstat; + }, [currentUserAttendee?.partstat, isLoading, rsvpValue, onLoadingChange]); + + const handleClick = async () => { + // Store current partstat before making changes + previousPartstatRef.current = currentUserAttendee?.partstat; + if (previousPartstatRef.current === rsvpValue) { + return; + } + + // For recurring events, don't set loading yet - wait for modal choice + if (!contextualizedEvent.isRecurring) { + onLoadingChange(true, rsvpValue); + } + + try { + await handleRSVPClick( + rsvpValue, + contextualizedEvent, + user, + setAfterChoiceFunc, + setOpenEditModePopup, + dispatch, + onLoadingChange + ); + } catch (error) { + console.error( + `[RSVPButton ${rsvpValue}] Error in handleRSVPClick:`, + error + ); + // Clear loading on error + onLoadingChange(false); + } + }; + + const isCurrentlyActive = currentUserAttendee?.partstat === rsvpValue; + + // Show as active (colored) if: + // 1. This button is currently loading, OR + // 2. This is the active status AND nothing is loading + const shouldShowActive = showLoading || (isCurrentlyActive && !isLoading); + + const buttonColor = shouldShowActive ? rsvpColor[rsvpValue] : "primary"; return ( ); } diff --git a/src/features/Events/AttendanceValidation/handleRSVPClick.tsx b/src/features/Events/AttendanceValidation/handleRSVPClick.tsx index ac69d2a..2a9a9c9 100644 --- a/src/features/Events/AttendanceValidation/handleRSVPClick.tsx +++ b/src/features/Events/AttendanceValidation/handleRSVPClick.tsx @@ -11,15 +11,31 @@ export async function handleRSVPClick( user: userData | undefined, setAfterChoiceFunc: Dispatch>, setOpenEditModePopup: Dispatch>, - dispatch: AppDispatch + dispatch: AppDispatch, + onLoadingChange?: (loading: boolean, value?: PartStat) => void ) { const { isRecurring, calendar, event } = contextualizedEvent; + if (isRecurring) { - setAfterChoiceFunc(() => async (type: string) => { + setAfterChoiceFunc(() => async (type: string | null) => { + // If user cancelled the modal, don't proceed + if (type === null) { + return; + } + + // Now start loading since user made a choice + if (onLoadingChange) { + onLoadingChange(true, rsvp); + } + try { await handleRSVP(dispatch, calendar, user, event, rsvp, type); } catch (error) { console.error("Error handling RSVP:", error); + // Clear loading on error + if (onLoadingChange) { + onLoadingChange(false); + } } }); setOpenEditModePopup("attendance"); @@ -28,6 +44,9 @@ export async function handleRSVPClick( await handleRSVP(dispatch, calendar, user, event, rsvp); } catch (error) { console.error("Error handling RSVP:", error); + if (onLoadingChange) { + onLoadingChange(false); + } } } } diff --git a/src/features/Events/EventDisplayPreview.tsx b/src/features/Events/EventDisplayPreview.tsx index e3705ca..70f058a 100644 --- a/src/features/Events/EventDisplayPreview.tsx +++ b/src/features/Events/EventDisplayPreview.tsx @@ -435,7 +435,6 @@ export default function EventPreviewModal({ actions={