From 5d9eb8e19d3b0722ddad388d385a42f2309251f4 Mon Sep 17 00:00:00 2001 From: Camille Moussu <66134347+Eriikah@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:52:23 +0100 Subject: [PATCH] [#522] added push for calendars updates (#541) --- .../features/Calendars/CalendarSlice.test.tsx | 39 ------------------- .../utils/notificationStorm.test.tsx | 4 ++ .../websocket/utils/updateCalendars.test.tsx | 3 ++ src/features/Calendars/CalendarSlice.ts | 28 +------------ .../services/getCalendarsListAsync.ts | 5 ++- src/websocket/WebSocketGate.tsx | 2 + src/websocket/messaging/parseMessage.ts | 8 +++- .../type/UpdateCalendarsAccumulators.ts | 1 + src/websocket/messaging/updateCalendars.ts | 31 +++++++++++++-- src/websocket/protocols/index.ts | 1 + 10 files changed, 50 insertions(+), 72 deletions(-) diff --git a/__test__/features/Calendars/CalendarSlice.test.tsx b/__test__/features/Calendars/CalendarSlice.test.tsx index 96de7e1..aa1412f 100644 --- a/__test__/features/Calendars/CalendarSlice.test.tsx +++ b/__test__/features/Calendars/CalendarSlice.test.tsx @@ -336,45 +336,6 @@ describe("CalendarSlice", () => { expect(result.payload.errors).toBeTruthy(); }); - it("patchCalendarAsync.fulfilled updates calendar fields", () => { - const calId = "c1"; - const prev = { - ...initialState, - list: { c1: { id: calId, events: { e1: { uid: "e1" } } } as any }, - }; - const patch = { name: "N", desc: "D", color: { "apple:color": "#00f" } }; - const state = reducer( - prev, - patchCalendarAsync.fulfilled( - { calId, calLink: "link", patch }, - "req1", - { - calId, - calLink: "link", - patch, - } - ) - ); - expect(state.list[calId].name).toBe("N"); - expect(state.list[calId].description).toBe("D"); - expect(state.list[calId].color?.["apple:color"]).toBe("#00f"); - }); - - it("removeCalendarAsync.fulfilled deletes calendar", () => { - const prev = { - ...initialState, - list: { c1: { id: "c1" } as any }, - }; - const state = reducer( - prev, - removeCalendarAsync.fulfilled({ calId: "c1" }, "req2", { - calId: "c1", - calLink: "l", - }) - ); - expect(state.list).toEqual({}); - }); - it("patchACLCalendarAsync.fulfilled sets visibility", () => { const prev = { ...initialState, diff --git a/__test__/features/websocket/utils/notificationStorm.test.tsx b/__test__/features/websocket/utils/notificationStorm.test.tsx index 483b9b1..2ef2d90 100644 --- a/__test__/features/websocket/utils/notificationStorm.test.tsx +++ b/__test__/features/websocket/utils/notificationStorm.test.tsx @@ -41,10 +41,12 @@ const mockAccumulators: { calendarsToRefresh: Map; calendarsToHide: Set; debouncedUpdateFn?: (dispatch: AppDispatch) => void; + shouldRefreshCalendarListRef: React.MutableRefObject; currentDebouncePeriod?: number; } = { calendarsToRefresh: new Map(), calendarsToHide: new Set(), + shouldRefreshCalendarListRef: { current: false }, currentDebouncePeriod: 0, debouncedUpdateFn: undefined, }; @@ -60,7 +62,9 @@ describe("websocket messages storm", () => { window.WS_DEBOUNCE_PERIOD_MS = 500; mockAccumulators.calendarsToRefresh = new Map(); mockAccumulators.calendarsToHide = new Set(); + mockAccumulators.shouldRefreshCalendarListRef.current = false; mockAccumulators.currentDebouncePeriod = 0; + mockAccumulators.debouncedUpdateFn = undefined; }); it("debounces calendar updates during message storm", () => { diff --git a/__test__/features/websocket/utils/updateCalendars.test.tsx b/__test__/features/websocket/utils/updateCalendars.test.tsx index fea93b8..a117642 100644 --- a/__test__/features/websocket/utils/updateCalendars.test.tsx +++ b/__test__/features/websocket/utils/updateCalendars.test.tsx @@ -33,10 +33,12 @@ describe("updateCalendars", () => { calendarsToRefresh: Map; calendarsToHide: Set; debouncedUpdateFn?: (dispatch: AppDispatch) => void; + shouldRefreshCalendarListRef: React.MutableRefObject; currentDebouncePeriod?: number; } = { calendarsToRefresh: new Map(), calendarsToHide: new Set(), + shouldRefreshCalendarListRef: { current: false }, currentDebouncePeriod: 0, debouncedUpdateFn: jest.fn(), }; @@ -49,6 +51,7 @@ describe("updateCalendars", () => { mockAccumulators.calendarsToRefresh = new Map(); mockAccumulators.calendarsToHide = new Set(); mockAccumulators.currentDebouncePeriod = 0; + mockAccumulators.shouldRefreshCalendarListRef.current = false; mockAccumulators.debouncedUpdateFn = jest.fn(); }); diff --git a/src/features/Calendars/CalendarSlice.ts b/src/features/Calendars/CalendarSlice.ts index 1b9d2ff..9c71060 100644 --- a/src/features/Calendars/CalendarSlice.ts +++ b/src/features/Calendars/CalendarSlice.ts @@ -256,31 +256,8 @@ const CalendarSlice = createSlice({ } as Calendar; state.error = null; }) - .addCase(patchCalendarAsync.fulfilled, (state, action) => { + .addCase(patchCalendarAsync.fulfilled, (state) => { state.pending = false; - if (action.payload.patch.color) { - state.list[action.payload.calId] = { - ...state.list[action.payload.calId], - color: action.payload.patch.color, - }; - Object.keys(state.list[action.payload.calId].events).forEach( - (evId) => - (state.list[action.payload.calId].events[evId].color = - action.payload.patch.color) - ); - } - if (action.payload.patch.desc) { - state.list[action.payload.calId] = { - ...state.list[action.payload.calId], - description: action.payload.patch.desc, - }; - } - if (action.payload.patch.name) { - state.list[action.payload.calId] = { - ...state.list[action.payload.calId], - name: action.payload.patch.name, - }; - } state.error = null; }) .addCase(addSharedCalendarAsync.fulfilled, (state, action) => { @@ -297,9 +274,8 @@ const CalendarSlice = createSlice({ } as Calendar; state.error = null; }) - .addCase(removeCalendarAsync.fulfilled, (state, action) => { + .addCase(removeCalendarAsync.fulfilled, (state) => { state.pending = false; - delete state.list[action.payload.calId]; state.error = null; }) .addCase(patchACLCalendarAsync.fulfilled, (state, action) => { diff --git a/src/features/Calendars/services/getCalendarsListAsync.ts b/src/features/Calendars/services/getCalendarsListAsync.ts index 0667843..2ee9ed9 100644 --- a/src/features/Calendars/services/getCalendarsListAsync.ts +++ b/src/features/Calendars/services/getCalendarsListAsync.ts @@ -105,7 +105,10 @@ export const getCalendarsListAsync = createAsyncThunk< if (fetchedCal) { importedCalendars[id] = { ...fetchedCal, - color: existingCal.color, + color: { + ...existingCal.color, + light: fetchedCal.color?.light, + }, events: existingCal.events || {}, }; } diff --git a/src/websocket/WebSocketGate.tsx b/src/websocket/WebSocketGate.tsx index fbb5030..2714311 100644 --- a/src/websocket/WebSocketGate.tsx +++ b/src/websocket/WebSocketGate.tsx @@ -57,6 +57,7 @@ export function WebSocketGate() { Map >(new Map()); const calendarsToHideRef = useRef>(new Set()); + const shouldRefreshCalendarListRef = useRef(false); const debouncedUpdateFnRef = useRef< ((dispatch: AppDispatch) => void) | undefined >(); @@ -67,6 +68,7 @@ export function WebSocketGate() { const accumulators = { calendarsToRefresh: calendarsToRefreshRef.current, calendarsToHide: calendarsToHideRef.current, + shouldRefreshCalendarListRef, debouncedUpdateFn: debouncedUpdateFnRef.current, currentDebouncePeriod: currentDebouncePeriodRef.current, }; diff --git a/src/websocket/messaging/parseMessage.ts b/src/websocket/messaging/parseMessage.ts index c3b419c..cd6c7ac 100644 --- a/src/websocket/messaging/parseMessage.ts +++ b/src/websocket/messaging/parseMessage.ts @@ -3,8 +3,9 @@ import { WS_INBOUND_EVENTS } from "../protocols"; export function parseMessage(message: unknown) { const calendarsToRefresh = new Set(); const calendarsToHide = new Set(); + let shouldRefreshCalendarList = false; if (typeof message !== "object" || message === null) { - return { calendarsToRefresh, calendarsToHide }; + return { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList }; } for (const [key, value] of Object.entries(message)) { @@ -21,11 +22,14 @@ export function parseMessage(message: unknown) { break; case WS_INBOUND_EVENTS.CALENDAR_CLIENT_REGISTERED: break; + case WS_INBOUND_EVENTS.CALENDAR_LIST: + shouldRefreshCalendarList = true; + break; default: { calendarsToRefresh.add(key); } } } - return { calendarsToRefresh, calendarsToHide }; + return { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList }; } diff --git a/src/websocket/messaging/type/UpdateCalendarsAccumulators.ts b/src/websocket/messaging/type/UpdateCalendarsAccumulators.ts index 575f1c9..cc4d07e 100644 --- a/src/websocket/messaging/type/UpdateCalendarsAccumulators.ts +++ b/src/websocket/messaging/type/UpdateCalendarsAccumulators.ts @@ -4,6 +4,7 @@ import { Calendar } from "@/features/Calendars/CalendarTypes"; export interface UpdateCalendarsAccumulators { calendarsToRefresh: Map; calendarsToHide: Set; + shouldRefreshCalendarListRef: React.MutableRefObject; debouncedUpdateFn?: (dispatch: AppDispatch) => void; currentDebouncePeriod?: number; } diff --git a/src/websocket/messaging/updateCalendars.ts b/src/websocket/messaging/updateCalendars.ts index 68912df..6964ab8 100644 --- a/src/websocket/messaging/updateCalendars.ts +++ b/src/websocket/messaging/updateCalendars.ts @@ -1,7 +1,10 @@ import type { AppDispatch } from "@/app/store"; import { store } from "@/app/store"; import { Calendar } from "@/features/Calendars/CalendarTypes"; -import { refreshCalendarWithSyncToken } from "@/features/Calendars/services"; +import { + getCalendarsListAsync, + refreshCalendarWithSyncToken, +} from "@/features/Calendars/services"; import { findCalendarById, getDisplayedCalendarRange } from "@/utils"; import { setSelectedCalendars } from "@/utils/storage/setSelectedCalendars"; import { debounce } from "lodash"; @@ -17,7 +20,9 @@ function createDebouncedUpdate( string, { calendar: Calendar; type?: "temp" } >, - getCalendarsToHide: () => Set + getCalendarsToHide: () => Set, + getShouldRefreshCalendarList: () => boolean, + resetShouldRefreshCalendarList: () => void ) { return debounce( (dispatch: AppDispatch) => { @@ -26,14 +31,20 @@ function createDebouncedUpdate( // Snapshot state const calendarsToProcess = new Map(getCalendarsToRefresh()); const calendarsToHideSnapshot = new Set(getCalendarsToHide()); + const shouldRefresh = getShouldRefreshCalendarList(); // Clear accumulators getCalendarsToRefresh().clear(); getCalendarsToHide().clear(); + resetShouldRefreshCalendarList(); try { processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess); processCalendarsToHide(calendarsToHideSnapshot); + + if (shouldRefresh) { + dispatch(getCalendarsListAsync()); + } } catch (error) { console.warn("Error processing accumulated calendar updates:", error); } @@ -49,7 +60,8 @@ export function updateCalendars( accumulators: UpdateCalendarsAccumulators ) { const state = store.getState(); - const { calendarsToRefresh, calendarsToHide } = parseMessage(message); + const { calendarsToRefresh, calendarsToHide, shouldRefreshCalendarList } = + parseMessage(message); // Accumulate accumulateCalendarsToRefresh( @@ -59,6 +71,9 @@ export function updateCalendars( ); accumulateCalendarsToHide(calendarsToHide, accumulators.calendarsToHide); + accumulators.shouldRefreshCalendarListRef.current ||= + shouldRefreshCalendarList; + const debouncePeriod = window.WS_DEBOUNCE_PERIOD_MS ?? DEFAULT_DEBOUNCE_MS; if (debouncePeriod > 0) { @@ -69,7 +84,11 @@ export function updateCalendars( accumulators.debouncedUpdateFn = createDebouncedUpdate( debouncePeriod, () => accumulators.calendarsToRefresh, - () => accumulators.calendarsToHide + () => accumulators.calendarsToHide, + () => accumulators.shouldRefreshCalendarListRef.current, + () => { + accumulators.shouldRefreshCalendarListRef.current = false; + } ); accumulators.currentDebouncePeriod = debouncePeriod; } @@ -84,10 +103,14 @@ export function updateCalendars( accumulators.calendarsToRefresh.clear(); accumulators.calendarsToHide.clear(); + accumulators.shouldRefreshCalendarListRef.current = false; try { processCalendarsToRefresh(dispatch, currentRange, calendarsToProcess); processCalendarsToHide(calendarsToHideSnapshot); + if (shouldRefreshCalendarList) { + dispatch(getCalendarsListAsync()); + } } catch (error) { console.warn("Error processing calendar updates:", error); } diff --git a/src/websocket/protocols/index.ts b/src/websocket/protocols/index.ts index 95bb84a..d70af26 100644 --- a/src/websocket/protocols/index.ts +++ b/src/websocket/protocols/index.ts @@ -6,4 +6,5 @@ export const WS_INBOUND_EVENTS = { CLIENT_REGISTERED: "registered", CLIENT_UNREGISTERED: "unregistered", CALENDAR_CLIENT_REGISTERED: "calendarListRegistered", + CALENDAR_LIST: "calendarList", } as const;