[#592] temporary fix for slow attendance update (#516)

This commit is contained in:
Camille Moussu
2026-02-06 11:21:11 +01:00
committed by GitHub
parent b44eba1a22
commit 8ca50b8d1b
4 changed files with 121 additions and 26 deletions
@@ -1,9 +1,10 @@
import { userData } from "@/features/User/userDataTypes"; import { userData } from "@/features/User/userDataTypes";
import { Box, Typography } from "@linagora/twake-mui"; import { Box, Typography } from "@linagora/twake-mui";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction, useState } from "react";
import { useI18n } from "twake-i18n"; import { useI18n } from "twake-i18n";
import { ContextualizedEvent } from "../EventsTypes"; import { ContextualizedEvent } from "../EventsTypes";
import { RSVPButton } from "./RSVPButton"; import { RSVPButton } from "./RSVPButton";
import { PartStat } from "@/features/User/models/attendee";
interface AttendanceValidationProps { interface AttendanceValidationProps {
contextualizedEvent: ContextualizedEvent; contextualizedEvent: ContextualizedEvent;
@@ -20,6 +21,8 @@ export function AttendanceValidation({
}: AttendanceValidationProps) { }: AttendanceValidationProps) {
const { currentUserAttendee, isOwn } = contextualizedEvent; const { currentUserAttendee, isOwn } = contextualizedEvent;
const { t } = useI18n(); const { t } = useI18n();
const [isLoading, setIsLoading] = useState(false);
const [loadingValue, setLoadingValue] = useState<PartStat | null>(null);
// Check if we should show RSVP buttons // Check if we should show RSVP buttons
const hasNoAttendeesOrOrganizer = const hasNoAttendeesOrOrganizer =
@@ -30,11 +33,19 @@ export function AttendanceValidation({
return null; return null;
} }
const handleLoadingChange = (loading: boolean, value?: PartStat) => {
setIsLoading(loading);
setLoadingValue(loading && value ? value : null);
};
const commonButtonProps = { const commonButtonProps = {
contextualizedEvent, contextualizedEvent,
user, user,
setAfterChoiceFunc, setAfterChoiceFunc,
setOpenEditModePopup, setOpenEditModePopup,
isLoading,
onLoadingChange: handleLoadingChange,
loadingValue,
}; };
return ( return (
@@ -1,8 +1,8 @@
import { useAppDispatch } from "@/app/hooks"; import { useAppDispatch } from "@/app/hooks";
import { PartStat } from "@/features/User/models/attendee"; import { PartStat } from "@/features/User/models/attendee";
import { userData } from "@/features/User/userDataTypes"; import { userData } from "@/features/User/userDataTypes";
import { Button } from "@linagora/twake-mui"; import { Button, CircularProgress, Box, Theme } from "@linagora/twake-mui";
import { Dispatch, SetStateAction } from "react"; import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { useI18n } from "twake-i18n"; import { useI18n } from "twake-i18n";
import { ContextualizedEvent } from "../EventsTypes"; import { ContextualizedEvent } from "../EventsTypes";
import { handleRSVPClick } from "./handleRSVPClick"; import { handleRSVPClick } from "./handleRSVPClick";
@@ -21,6 +21,9 @@ interface RSVPButtonProps {
user: userData | undefined; user: userData | undefined;
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>; setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>;
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>; setOpenEditModePopup: Dispatch<SetStateAction<string | null>>;
isLoading: boolean;
onLoadingChange: (loading: boolean, value?: PartStat) => void;
loadingValue: PartStat | null;
} }
export function RSVPButton({ export function RSVPButton({
@@ -29,35 +32,98 @@ export function RSVPButton({
user, user,
setAfterChoiceFunc, setAfterChoiceFunc,
setOpenEditModePopup, setOpenEditModePopup,
isLoading,
onLoadingChange,
loadingValue,
}: RSVPButtonProps) { }: RSVPButtonProps) {
const { t } = useI18n(); const { t } = useI18n();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { currentUserAttendee } = contextualizedEvent; const { currentUserAttendee } = contextualizedEvent;
const showLoading = isLoading && loadingValue === rsvpValue;
const previousPartstatRef = useRef<PartStat | undefined>(
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 ( return (
<Button <Button
variant={ variant={shouldShowActive ? "contained" : "outlined"}
currentUserAttendee?.partstat === rsvpValue ? "contained" : "outlined" color={buttonColor}
}
color={
currentUserAttendee?.partstat === rsvpValue
? rsvpColor[rsvpValue]
: "primary"
}
size="medium" size="medium"
sx={{ borderRadius: "50px" }} sx={{
onClick={() => borderRadius: "50px",
handleRSVPClick( // Override MUI's default disabled styles to keep the color
rsvpValue, "&.Mui-disabled": shouldShowActive
contextualizedEvent, ? {
user, backgroundColor: (theme: Theme) =>
setAfterChoiceFunc, theme.palette[buttonColor].main,
setOpenEditModePopup, color: (theme: Theme) => theme.palette[buttonColor].contrastText,
dispatch borderColor: (theme: Theme) => theme.palette[buttonColor].main,
) }
} : {},
}}
onClick={handleClick}
disabled={isLoading}
> >
{t(`eventPreview.${rsvpValue}`)} <Box display="flex" alignItems="center" gap={1}>
{showLoading && <CircularProgress size={20} color="inherit" />}
{t(`eventPreview.${rsvpValue}`)}
</Box>
</Button> </Button>
); );
} }
@@ -11,15 +11,31 @@ export async function handleRSVPClick(
user: userData | undefined, user: userData | undefined,
setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>, setAfterChoiceFunc: Dispatch<SetStateAction<Function | undefined>>,
setOpenEditModePopup: Dispatch<SetStateAction<string | null>>, setOpenEditModePopup: Dispatch<SetStateAction<string | null>>,
dispatch: AppDispatch dispatch: AppDispatch,
onLoadingChange?: (loading: boolean, value?: PartStat) => void
) { ) {
const { isRecurring, calendar, event } = contextualizedEvent; const { isRecurring, calendar, event } = contextualizedEvent;
if (isRecurring) { 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 { try {
await handleRSVP(dispatch, calendar, user, event, rsvp, type); await handleRSVP(dispatch, calendar, user, event, rsvp, type);
} catch (error) { } catch (error) {
console.error("Error handling RSVP:", error); console.error("Error handling RSVP:", error);
// Clear loading on error
if (onLoadingChange) {
onLoadingChange(false);
}
} }
}); });
setOpenEditModePopup("attendance"); setOpenEditModePopup("attendance");
@@ -28,6 +44,9 @@ export async function handleRSVPClick(
await handleRSVP(dispatch, calendar, user, event, rsvp); await handleRSVP(dispatch, calendar, user, event, rsvp);
} catch (error) { } catch (error) {
console.error("Error handling RSVP:", error); console.error("Error handling RSVP:", error);
if (onLoadingChange) {
onLoadingChange(false);
}
} }
} }
} }
@@ -435,7 +435,6 @@ export default function EventPreviewModal({
actions={ actions={
<AttendanceValidation <AttendanceValidation
contextualizedEvent={contextualizedEvent} contextualizedEvent={contextualizedEvent}
calendarList={calendarList}
user={user} user={user}
setAfterChoiceFunc={setAfterChoiceFunc} setAfterChoiceFunc={setAfterChoiceFunc}
setOpenEditModePopup={setOpenEditModePopup} setOpenEditModePopup={setOpenEditModePopup}