From 00b0d2ed957b8884603fc5e1d010bbbb200026a9 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 5 Aug 2026 10:02:20 -0300 Subject: [PATCH 1/2] fix(ui-voip): stop mutating the tracked view set registerView/unregisterView mutated prev.currentViews inside the state updater. StrictMode calls updaters twice, so the second call saw the already mutated set, bailed out with prev and the filtered set consumers read never changed - registering or unregistering a view was a no-op in development. --- .changeset/available-view-tracker-mutation.md | 6 +++ .../providers/useAvailableViewTracker.spec.ts | 42 +++++++++++++++++++ .../src/providers/useAvailableViewTracker.ts | 34 +++++++-------- 3 files changed, 62 insertions(+), 20 deletions(-) create mode 100644 .changeset/available-view-tracker-mutation.md create mode 100644 packages/ui-voip/src/providers/useAvailableViewTracker.spec.ts diff --git a/.changeset/available-view-tracker-mutation.md b/.changeset/available-view-tracker-mutation.md new file mode 100644 index 0000000000000..48ad16af11814 --- /dev/null +++ b/.changeset/available-view-tracker-mutation.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-voip': patch +'@rocket.chat/meteor': patch +--- + +Fixes the media call widget and popout not opening or closing when the views they are registered under change diff --git a/packages/ui-voip/src/providers/useAvailableViewTracker.spec.ts b/packages/ui-voip/src/providers/useAvailableViewTracker.spec.ts new file mode 100644 index 0000000000000..5acb7d4a9987e --- /dev/null +++ b/packages/ui-voip/src/providers/useAvailableViewTracker.spec.ts @@ -0,0 +1,42 @@ +import { act, renderHook } from '@testing-library/react'; +import { StrictMode } from 'react'; + +import useAvailableViewTracker from './useAvailableViewTracker'; + +describe('useAvailableViewTracker', () => { + it('should track registered views under StrictMode, which calls state updaters twice', () => { + const { result } = renderHook(() => useAvailableViewTracker(), { wrapper: StrictMode }); + + act(() => result.current.registerView('popout')); + expect(result.current.currentViews.has('popout')).toBe(true); + + act(() => result.current.unregisterView('popout')); + expect(result.current.currentViews.has('popout')).toBe(false); + }); + + it('should hide the widget while the room view is registered', () => { + const { result } = renderHook(() => useAvailableViewTracker(), { wrapper: StrictMode }); + + act(() => result.current.registerView('widget')); + expect(result.current.currentViews.has('widget')).toBe(true); + + act(() => result.current.registerView('room')); + expect(result.current.currentViews.has('widget')).toBe(false); + + act(() => result.current.unregisterView('room')); + expect(result.current.currentViews.has('widget')).toBe(true); + }); + + it('should return a new set only when the registered views change', () => { + const { result } = renderHook(() => useAvailableViewTracker(), { wrapper: StrictMode }); + + act(() => result.current.registerView('popout')); + const views = result.current.currentViews; + + act(() => result.current.registerView('popout')); + expect(result.current.currentViews).toBe(views); + + act(() => result.current.registerView('room')); + expect(result.current.currentViews).not.toBe(views); + }); +}); diff --git a/packages/ui-voip/src/providers/useAvailableViewTracker.ts b/packages/ui-voip/src/providers/useAvailableViewTracker.ts index 25ce5c9fa3a44..04ae746f656d9 100644 --- a/packages/ui-voip/src/providers/useAvailableViewTracker.ts +++ b/packages/ui-voip/src/providers/useAvailableViewTracker.ts @@ -13,37 +13,31 @@ const filter = (view: AvailableViews, _index: number, array: AvailableViews[]) = } }; +const withFilteredViews = (currentViews: Set) => ({ + currentViews, + filteredViews: new Set(Array.from(currentViews).filter(filter)), +}); + const useAvailableViewTracker = () => { - // keep in mind views.currentViews is a stable set, so please if you are going to use it in a useEffect, make sure to create a new set from it, otherwise you will not be able to track changes in the set. const [views, setViews] = useState<{ currentViews: Set; filteredViews: Set; - }>({ - currentViews: new Set(), - filteredViews: new Set(), - }); + }>(() => withFilteredViews(new Set())); const registerView = useCallback((view: AvailableViews) => { - setViews((prev) => { - if (prev.currentViews.has(view)) return prev; - - prev.currentViews.add(view); - return { - currentViews: prev.currentViews, - filteredViews: new Set(Array.from(prev.currentViews).filter(filter)), - }; - }); + // the updater must not touch prev: StrictMode calls it twice, and a mutated set would make the second call bail out + setViews((prev) => (prev.currentViews.has(view) ? prev : withFilteredViews(new Set(prev.currentViews).add(view)))); }, []); const unregisterView = useCallback((view: AvailableViews) => { setViews((prev) => { - if (!prev.currentViews.has(view)) return prev; + if (!prev.currentViews.has(view)) { + return prev; + } - prev.currentViews.delete(view); - return { - currentViews: prev.currentViews, - filteredViews: new Set(Array.from(prev.currentViews).filter(filter)), - }; + const currentViews = new Set(prev.currentViews); + currentViews.delete(view); + return withFilteredViews(currentViews); }); }, []); From ad42a4ac8deba251903f9bcfe3932aefbd92e4a8 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:40:07 -0300 Subject: [PATCH 2/2] remove changeset --- .changeset/available-view-tracker-mutation.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .changeset/available-view-tracker-mutation.md diff --git a/.changeset/available-view-tracker-mutation.md b/.changeset/available-view-tracker-mutation.md deleted file mode 100644 index 48ad16af11814..0000000000000 --- a/.changeset/available-view-tracker-mutation.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@rocket.chat/ui-voip': patch -'@rocket.chat/meteor': patch ---- - -Fixes the media call widget and popout not opening or closing when the views they are registered under change