Skip to content

[MM-69549] Modularize Calls webapp state management (active_calls, sessions, hosts, availability) - #1231

Open
M-ZubairAhmed wants to merge 19 commits into
v2from
MM-69412_call-props-in-callstate-isphone
Open

[MM-69549] Modularize Calls webapp state management (active_calls, sessions, hosts, availability)#1231
M-ZubairAhmed wants to merge 19 commits into
v2from
MM-69412_call-props-in-callstate-isphone

Conversation

@M-ZubairAhmed

@M-ZubairAhmed M-ZubairAhmed commented Jun 23, 2026

Copy link
Copy Markdown
Member

Summary

This pull request introduces significant refactoring and cleanup of call state management and related actions in the codebase. The main focus is on removing legacy or redundant actions, consolidating state logic, and improving the structure and naming of selectors and actions related to calls, sessions, and hosts. It also improves test setup and updates some configuration and mock file references.

  • Removed legacy action types and actions such as CALL_STATE and CALL_HOST, and replaced their usage with more focused actions like activeCallAdded and hostChanged

  • Refactored session and host-related actions and selectors to use the new sessions and hosts state modules, replacing older, less descriptive imports and usages.

  • Removed a set of host-related async actions (hostMake, hostMute, hostScreenOff, hostLowerHand, hostRemove, hostMuteOthers) from the main actions file, likely moving them to a more appropriate location or replacing them with new implementations. (webapp/src/actions.ts)

  • Removed the legacy getCallActive action and replaced its usage with fetchIsCallActiveInChannel

  • Updated Jest configuration to point to the correct i18n mock file and removed a redundant test setup file from the build sync plan.

Ticket Link

Fixes https://mattermost.atlassian.net/browse/MM-69549

Screenshots

Release Note


@codecov-commenter

codecov-commenter commented Jun 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 38.64542% with 154 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (v2@c4b49dd). Learn more about missing BASE report.

Files with missing lines Patch % Lines
webapp/src/actions.ts 4.44% 43 Missing ⚠️
webapp/src/selectors.ts 16.21% 31 Missing ⚠️
webapp/src/state/calls_availability/selectors.ts 23.07% 20 Missing ⚠️
webapp/src/components/host_controls_menu.tsx 0.00% 9 Missing ⚠️
webapp/src/components/expanded_view/component.tsx 0.00% 8 Missing ⚠️
webapp/src/index.tsx 0.00% 8 Missing ⚠️
webapp/src/state/hosts/selectors.ts 45.45% 6 Missing ⚠️
webapp/src/reducers.ts 28.57% 5 Missing ⚠️
webapp/src/state/active_calls/actions.ts 33.33% 4 Missing ⚠️
webapp/src/state/hosts/actions.ts 86.66% 4 Missing ⚠️
... and 9 more
Additional details and impacted files
@@          Coverage Diff          @@
##             v2    #1231   +/-   ##
=====================================
  Coverage      ?   32.13%           
=====================================
  Files         ?      263           
  Lines         ?    13837           
  Branches      ?     1726           
=====================================
  Hits          ?     4446           
  Misses        ?     8910           
  Partials      ?      481           
Files with missing lines Coverage Δ
webapp/src/action_types.ts 100.00% <ø> (ø)
webapp/src/components/call_widget/index.ts 0.00% <ø> (ø)
webapp/src/components/call_widget/participant.tsx 16.12% <ø> (ø)
...components/channel_header_dropdown_button/index.ts 0.00% <ø> (ø)
...rc/components/custom_post_types/post_type/index.ts 0.00% <ø> (ø)
.../src/components/expanded_view/call_participant.tsx 0.00% <ø> (ø)
.../components/expanded_view/call_participant_rhs.tsx 0.00% <ø> (ø)
webapp/src/components/expanded_view/index.ts 0.00% <ø> (ø)
webapp/src/state/active_calls/action_types.ts 100.00% <100.00%> (ø)
webapp/src/state/active_calls/reducer.ts 100.00% <100.00%> (ø)
... and 32 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…r future enhancements related to SIP call handling.
…on dispatches in various files to utilize the new `activeCallRegistered` function, improving code consistency and readability. Adjusted selectors to reflect changes in state structure for SIP call details.
…etSipCallDetailsFromCallState` to a more logical position within the import statements, enhancing readability and maintainability of the test file.
…mport paths for session actions and improved user ID retrieval from sessions. Introduced new selectors and actions for channel call availability, enhancing the overall structure and maintainability of the codebase. Removed deprecated components and streamlined the call state management process.
…dated action types and action creators to improve consistency, renaming `activeCallRegistered` to `activeCallAdded`. Adjusted import paths for call availability actions and selectors, enhancing code organization. Introduced new state management for channel call availability, including actions and reducers, to streamline functionality.
…t now only renders the disable calls message based on channel availability, simplifying the logic and improving clarity.
…cy in action type definitions across files, maintaining clarity in the state management structure.
…n types and actions for host changes, replacing the previous `CALL_HOST` action. Updated selectors and reducers to support the new host management structure, enhancing clarity and maintainability in call state management.
…nd functionality. Introduced new actions for participant management, including muting and removing participants, while updating selectors to utilize the new ActiveCall type. Removed deprecated host actions to streamline the codebase.
…or selectors and components to improve type safety and maintainability.
…ated setup file. Updated paths for improved clarity and maintainability in test setup.
… `fetchIsCallActiveInChannel` for improved clarity and functionality. Update selectors and imports to enhance maintainability, including the addition of `hasPermissionToRenderCallsButtonInChannelHeader` for better permission handling in channel headers.
@M-ZubairAhmed
M-ZubairAhmed marked this pull request as ready for review June 29, 2026 18:08
@M-ZubairAhmed

Copy link
Copy Markdown
Member Author

/update-branch

@mattermost-build

Copy link
Copy Markdown
Contributor

Error trying to update the PR.
Please do it manually.

The new phone-call tests initialized only botSession, but getBotID()
reads the independently-cached botID field. With botID empty,
isPhoneCallChannel short-circuits to false, so the SIP-hangup path
never runs:

- TestRemoveUserSessionPhoneCall: lingering SIP session not dropped
- TestHandleLiveKitSIPParticipant/last_SIP_participant_left...: call
  never ended (EndAt stayed 0)

Set botID alongside botSession (matching logs_test.go / limits_test.go),
including the non-bot-DM subtest so it exercises the channel-type check
rather than short-circuiting on the empty bot ID.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@M-ZubairAhmed M-ZubairAhmed changed the title [MM-69412] Surface call props (type/phone_number/display_number/label/user_id) into client CallState + isPhoneCall selector [MM-69549] Modularize Calls webapp state management (active_calls, sessions, hosts, availability) Jun 29, 2026
@M-ZubairAhmed

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough common_action_types : uses

common_selectors --> emptyRootReducer : fallback

</diagram>
</layer>

<layer id="active_calls_slice" title="active_calls slice" depends_on="common_foundation">
<summary>Introduces ACTIVE_CALL_ADDED action type, activeCallAdded creator, fetchIsCallActiveInChannel REST helper, and reducer keying ActiveCall objects by channelID; fully unit-tested.</summary>
<ranges>
range_edac55b8446f
range_fa706383b9fd
range_3586a098a505
range_ab5338058f83
range_eb48ea0f5ae9
range_d21c6362d612
range_ea4f952d61c4
range_01db9861f6f9
</ranges>
</layer>

<layer id="calls_availability_slice" title="calls_availability slice" depends_on="common_foundation">
<summary>Introduces CHANNEL_CALLS_AVAILABILITY_UPDATED action type, channelCallsAvailabilityUpdated creator, toggleCallsAvailabilityForChannel thunk, per-channel reducer, and selectors including shouldShowCallsButtonInChannelHeader and hasPermissionToRenderCallsButtonInChannelHeader; fully unit-tested.</summary>
<ranges>
range_340abe32429b
range_44e179cc3d55
range_6b3752999d5c
range_8c4670e0f079
range_3f98765db29c
range_8b49e81a6d7f
range_01b2f93ada64
range_8c852292ced1
range_bd4e7fcfe6cf
range_d282cc33aff1
range_01867ff5a712
range_746976c01e17
range_80c0c8cb3c5c
range_2ac68967e4e7
range_9f0e81688521
range_b0028fbc9f89
range_fef9a6b6f57b
</ranges>
</layer>

<layer id="hosts_slice" title="hosts slice" depends_on="common_foundation">
<summary>Introduces HOST_CHANGED action type, hostChanged creator, REST wrappers for all host-control operations (mute, remove, screen-off, lower-hand, make-host, mute-all), hosts reducer keyed by channelID, and channel/current-channel host selectors; fully unit-tested.</summary>
<ranges>
range_ffe7d0f6b8b8
range_e6995f714837
range_6c02536bf31b
range_b907bfb9f048
range_2d9ab53c2472
range_5ffdb5e1a319
range_f353f651cbe0
range_3cf7e171ca49
range_8fdd2596e60c
range_ccc155d3ce39
range_41dc34c8e675
range_39a0a7f55db7
range_43b31894abaf
range_9b9de02da90b
range_d2520c48b5ed
range_52786d8805d3
range_a73b224e4d06
range_852cabb7cb92
range_c55afbda3b48
range_b9835f774468
range_f820e508f968
range_8b9f07ce2198
range_5b2a1c693fb0
</ranges>
</layer>

<layer id="sessions_screen_sharing_cleanup" title="sessions and screen_sharing_ids cleanup" depends_on="common_foundation">
<summary>Removes locally-defined UN_INITIALIZED/CALL_ENDED from sessions action_types and actions (now sourced from common_actions), renames State to Sessions type alias in the reducer, adds getUserIDsFromSessions selector, removes getUserIDsForSessions from utils, and updates screen_sharing_ids imports; all with new unit tests.</summary>
<ranges>
range_334eea594a95
range_fd89176f0a7a
range_1d6b2d61c806
range_85aa4d491191
range_5433cef49c58
range_d7d5e934d3d5
range_fba29c098472
range_966f2a8e9f0b
range_66ec68881b67
range_6ac40d63b9a3
range_223caed65e7d
range_1008a9885c7d
range_99a4a8934cf2
range_1569a18e6d17
range_cee65dc53af3
range_fa95727cf98f
range_a9a257bbacea
range_6ae91824b91a
range_1778504070f2
range_11ff7e3af975
range_cb80cdc3652f
range_3a00eea7ecdf
range_0a4d49fc426b
range_107dc5a1b205
range_0651416faa9c
range_849a4c42e6f0
range_2499c5b6d6f4
range_2bda5c34a2f7
</ranges>
</layer>

<layer id="root_reducer_selectors" title="Root reducer restructure and global selector migration" depends_on="active_calls_slice, calls_availability_slice, hosts_slice">
<summary>Removes channels and calls/hosts reducers from reducers.ts, mounts callsAvailability and activeCalls slices, replaces RootState/initialRootState with RootReducer/emptyRootReducer, removes CALL_STATE/CALL_HOST/RECEIVED_CHANNEL_STATE from action_types.ts, and migrates all selectors in selectors.ts to read from getPluginStore(state) using the activeCalls shape (callID instead of ID).</summary>
<ranges>
range_f0f42f490907
range_57c4b88a630d
range_9e307cecb3ae
range_7ddfb05972e7
range_51c3ead28b9d
range_16bd8d986f98
range_4f45e51fd578
range_f9850d7cd255
range_9cc447058830
range_e8f072ba571d
range_72c9177b1491
range_0a176b6bfb33
range_8693473f4306
range_3592dff56380
range_557a110d7f14
range_6b03cd847294
range_1144bc961fe1
range_a07fab5acc31
range_57aad10bdfc1
range_e29782bc1377
range_eded20e67ff6
range_2dc96361de91
range_c25cf1364a5e
range_572dad63133b
range_e54dc90dd986
range_2fdf97c7b99f
</ranges>
</layer>

<layer id="websocket_hydration" title="Websocket handlers and actions.ts hydration refactor" depends_on="root_reducer_selectors">
<summary>Rewires handleCallStart and handleCallHostChanged to dispatch activeCallAdded and hostChanged instead of CALL_STATE/CALL_HOST; adds hydradeCallsAndChannelStatesExcept thunk; rewrites loadCallState to use action creators; removes hostMake/hostMute/hostScreenOff/hostLowerHand/hostRemove/hostMuteOthers from actions.ts; replaces getCallActive with fetchIsCallActiveInChannel.</summary>
<ranges>
range_15402a3b87f9
range_be5374cc2863
range_c4416ee658c4
range_2a6f94fe99a5
range_8bfb05964335
range_82ab1089ea5e
range_eb8a5c3acbc6
range_c4be8a9ce17d
range_f7a57592a2ec
range_a3aa58fe351b
range_aa27dc973937
range_dc5532354261
</ranges>
</layer>

<layer id="component_updates" title="Component migration to new host actions and selectors" depends_on="root_reducer_selectors, hosts_slice">
<summary>Updates all UI components to use new channel-scoped host selectors (getHostIDForCurrentChannel, getHostChangeAtForCurrentChannel, getHostID), new host action creators (hostRemoveParticipant, hostMuteAllParticipants, hostMakeParticipantHost, etc.), tightens callID prop types to ActiveCall['callID'], and switches channel_header_* components to shouldShowCallsButtonInChannelHeader and callsAvailableInCurrentChannelWithDefault.</summary>
<ranges>
range_0ceb49575891
range_80217b0522cf
range_e7dc91a341ce
range_70c468200b76
range_58e58f44fe58
range_a2e757166fe5
range_78cb6ded83b1
range_6983742082b7
range_bf7956965c78
range_f4e7db69c3a7
range_c0c60a5fd9ec
range_540ae2fd88d8
range_e1dc9d4439a5
range_a44b4a5003e9
range_dd542f3292ca
range_85a2fbfe16b0
range_8b7b01d45abe
range_9b516ddc7478
range_6a908d921c33
range_9b797533482d
range_960a13a7aca2
range_0f1fe509414d
range_b681ada5a2c4
range_60356267bc2a
range_7031eecbc42f
range_f4cc1220ba21
range_0de9456364fc
range_631953f34daf
range_93a9170e8985
range_0122593df661
range_c6ed1dfaae1e
range_9efd3a38e155
range_7cdb216b213c
range_3648dc828f28
range_e991239b01db
range_dfdae104f87d
range_2dccca20c0d4
range_fa9f5b200c80
range_047a031f8062
range_a5908af3351b
</ranges>
</layer>

<layer id="plugin_entry_standalone" title="Plugin entry point and standalone app migration" depends_on="websocket_hydration, calls_availability_slice">
<summary>Rewires index.tsx: websocket enable/disable handlers dispatch channelCallsAvailabilityUpdated, joinCall uses callsAvailableInChannelWithDefault/callsNotAvailableInChannel, channel-header menu switches to ChannelHeaderMenuItem + toggleCallsAvailabilityForChannel, onActivate replaces fetchChannels+batchActions with hydradeCallsAndChannelStatesExcept. Updates standalone embed app to use fetchIsCallActiveInChannel and getUserIDsFromSessions, and recording view/index to use getHostIDForCurrentChannel.</summary>
<ranges>
range_cf23c2f1380e
range_87991ca2cf1b
range_d3c35dd8d399
range_f52af52e7c0e
range_afbbd56daa3f
range_32cc63bd7e94
range_59585ca99fd7
range_062f3d5f74db
range_0a148f59f03f
range_7577e9150abd
range_443d64a3bb1b
range_cb7d55e30015
range_34c78ea7cc43
range_fc5323590b0b
range_0d44713ac2d1
range_632aab05e146
range_a94de10e6b7c
range_7aa8f13630bb
range_b287b4fc218c
range_990b098727c9
</ranges>
</layer>

<layer id="config_server_tests" title="Server test fixes and config updates">
<summary>Sets p.botID in SIP participant test cases and TestRemoveUserSessionPhoneCall; updates jest.config.js i18n mock path to src/i18n.mock.json; removes webapp/tests/setup.tsx from sync plan overwrite list.</summary>
<ranges>
range_8fb7ba5f041f
range_cac066517285
range_0fe91e60e2d5
range_5169ead68601
range_d262cec4217c
</ranges>
</layer>
</cohort>
<unassigned_ranges>
</unassigned_ranges>
-->

## Walkthrough

Introduces three new Redux slices (`active_calls`, `calls_availability`, `hosts`) with shared common action types, creators, and a typed `getPluginStore` accessor. Migrates the root reducer, all global selectors, websocket handlers, hydration thunks, and every UI component to use the new state layer, removing legacy `CALL_STATE`/`CALL_HOST`/`channels` reducer patterns.

**Calls Plugin Redux Refactor**

| Layer / File(s) | Summary |
|---|---|
| **Common state infrastructure** <br> `webapp/src/state/README.md`, `webapp/src/state/common_action_types.ts`, `webapp/src/state/common_actions.ts`, `webapp/src/state/common_actions.test.ts`, `webapp/src/state/common_selectors.ts` | Adds `UN_INITIALIZED`/`CALL_ENDED` shared action types, `unInitialized`/`callEnded` creators, and `getPluginStore` typed selector that reads the plugin reducer slice from the Mattermost global store with `emptyRootReducer` fallback. |
| **`active_calls` slice** <br> `webapp/src/state/active_calls/...` | Adds `ACTIVE_CALL_ADDED` action type, `activeCallAdded` creator, `fetchIsCallActiveInChannel` REST helper, and reducer keying `ActiveCall` objects by `channelID` with `UN_INITIALIZED`/`CALL_ENDED` handling; fully unit-tested. |
| **`calls_availability` slice** <br> `webapp/src/state/calls_availability/...` | Adds `CHANNEL_CALLS_AVAILABILITY_UPDATED` action type, `channelCallsAvailabilityUpdated` creator, `toggleCallsAvailabilityForChannel` thunk, per-channel reducer, and selectors including `shouldShowCallsButtonInChannelHeader` and `hasPermissionToRenderCallsButtonInChannelHeader`; fully unit-tested. |
| **`hosts` slice** <br> `webapp/src/state/hosts/...` | Adds `HOST_CHANGED` action type, `hostChanged` creator, REST wrappers for all host-control operations (mute, remove, screen-off, lower-hand, make-host, mute-all), `Hosts` reducer keyed by `channelID`, and channel/current-channel host selectors; fully unit-tested. |
| **`sessions` and `screen_sharing_ids` cleanup** <br> `webapp/src/state/sessions/...`, `webapp/src/state/screen_sharing_ids/...`, `webapp/src/utils.ts` | Removes locally-defined `UN_INITIALIZED`/`CALL_ENDED` from sessions (now from `common_actions`), renames `State` to `Sessions`, adds `getUserIDsFromSessions` selector, removes `getUserIDsForSessions` from utils, and updates `screen_sharing_ids` imports; all with new unit tests. |
| **Root reducer restructure and global selector migration** <br> `webapp/src/reducers.ts`, `webapp/src/selectors.ts`, `webapp/src/action_types.ts` | Removes `channels`/`calls`/`hosts` reducers, mounts `callsAvailability`/`activeCalls`, replaces `RootState`/`initialRootState` with `RootReducer`/`emptyRootReducer`, removes `CALL_STATE`/`CALL_HOST`/`RECEIVED_CHANNEL_STATE`, and migrates all selectors to read from `getPluginStore(state)` using `activeCalls.callID` instead of `calls.ID`. |
| **Websocket handlers and `actions.ts` hydration refactor** <br> `webapp/src/websocket_handlers.ts`, `webapp/src/actions.ts` | Rewires `handleCallStart`/`handleCallHostChanged` to dispatch `activeCallAdded`/`hostChanged`; adds `hydradeCallsAndChannelStatesExcept` thunk; rewrites `loadCallState` to use action creators; removes six host-management thunks and `getCallActive` from `actions.ts`. |
| **Component migration to new host actions and selectors** <br> `webapp/src/components/call_widget/...`, `webapp/src/components/expanded_view/...`, `webapp/src/components/host_controls_menu.tsx`, `webapp/src/components/channel_header_*.ts`, `webapp/src/components/channel_call_toast.tsx`, `webapp/src/components/custom_post_types/...`, `webapp/src/slash_commands.tsx` | All UI components switch to channel-scoped host selectors, new `src/state/hosts/actions` creators, tighten `callID` props to `ActiveCall['callID']`, and channel header components adopt `shouldShowCallsButtonInChannelHeader`/`callsAvailableInCurrentChannelWithDefault`. |
| **Plugin entry point and standalone app migration** <br> `webapp/src/index.tsx`, `standalone/src/index.ts`, `standalone/src/recording/...` | Rewires websocket enable/disable to `channelCallsAvailabilityUpdated`, `joinCall` to `callsAvailableInChannelWithDefault`/`callsNotAvailableInChannel`, channel-header menu to `ChannelHeaderMenuItem`+`toggleCallsAvailabilityForChannel`, and `onActivate` to `hydradeCallsAndChannelStatesExcept`; standalone uses `fetchIsCallActiveInChannel` and `getUserIDsFromSessions`. |
| **Server test fixes and config updates** <br> `server/api_livekit_webhook_test.go`, `server/session_test.go`, `webapp/jest.config.js`, `build/sync/plan.yml` | Sets `p.botID` in SIP participant test cases; updates jest i18n mock path to `src/i18n.mock.json`; removes `webapp/tests/setup.tsx` from sync plan. |

## Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

</details>

<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->

<details>
<summary>🚥 Pre-merge checks | ✅ 4 | ❌ 1</summary>

### ❌ Failed checks (1 warning)

|     Check name     | Status     | Explanation                                                                          | Resolution                                                                         |
| :----------------: | :--------- | :----------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------- |
| Docstring Coverage | ⚠️ Warning | Docstring coverage is 9.09% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |

<details>
<summary>✅ Passed checks (4 passed)</summary>

|         Check name         | Status   | Explanation                                                                                                                              |
| :------------------------: | :------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
|         Title check        | ✅ Passed | It clearly summarizes the refactor of Calls webapp state management and the new active_calls, sessions, hosts, and availability modules. |
|      Description check     | ✅ Passed | The description is clearly related to the refactor and config/test setup changes in the diff.                                            |
|     Linked Issues check    | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                 |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                 |

</details>

</details>

<!-- pre_merge_checks_walkthrough_end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

<details>
<summary>📝 Generate docstrings</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR
- [ ] <!-- {"checkboxId": "3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch

</details>
<details>
<summary>🧪 Generate unit tests (beta)</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-4837056660"} -->   Create PR with unit tests
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-4837056660"} -->   Commit unit tests in branch `MM-69412_call-props-in-callstate-isphone`

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---




<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>

<!-- tips_end -->

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
webapp/src/index.tsx (1)

843-859: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the channel-header menu action in sync with channel switches.

Line 854 now gates the Enable/Disable calls menu by channel-specific permissions, but the subscriber below only refreshes it on first load. This can leave the menu action visible or hidden based on a previous channel.

Proposed fix
-        let channelHeaderMenuID: string;
+        let channelHeaderMenuID = '';
+        const unregisterChannelHeaderMenuAction = () => {
+            if (channelHeaderMenuID) {
+                registry.unregisterComponent(channelHeaderMenuID);
+                channelHeaderMenuID = '';
+            }
+        };
+
         const registerChannelHeaderMenuAction = () => {
             channelHeaderMenuID = registry.registerChannelHeaderMenuAction(
                 ChannelHeaderMenuItem,
                 () => store.dispatch(toggleCallsAvailabilityForChannel()),
             );
         };
+        this.unsubscribers.push(unregisterChannelHeaderMenuAction);
 
         const registerHeaderMenuComponentIfNeeded = async (channelID: string) => {
             try {
-                registry.unregisterComponent(channelHeaderMenuID);
+                unregisterChannelHeaderMenuAction();
                 if (hasPermissionToRenderCallsButtonInChannelHeader(store.getState(), channelID)) {
                     registerChannelHeaderMenuAction();
                 }
             } catch (err) {
-                registry.unregisterComponent(channelHeaderMenuID);
+                unregisterChannelHeaderMenuAction();
                 logErr(err);
             }
         };
-                const firstLoad = !currChannelId;
                 currChannelId = currentChannelId;
 
-                // We only want to register the header menu component on first load and not
-                // on every channel switch.
-                if (firstLoad) {
-                    registerHeaderMenuComponentIfNeeded(currentChannelId);
-                }
+                registerHeaderMenuComponentIfNeeded(currentChannelId);

Also applies to: 983-987

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/index.tsx` around lines 843 - 859, The channel-header menu action
is now computed from channel-specific permissions, but it is only refreshed on
initial setup, so it can get out of sync when switching channels. Update the
channel change subscriber to call the existing refresh path, using
registerHeaderMenuComponentIfNeeded and the channel ID from the current channel
state, so the Enable/Disable calls item is re-evaluated on every channel switch.
Make sure the unregister/register flow remains centralized in
registerChannelHeaderMenuAction and registerHeaderMenuComponentIfNeeded to avoid
stale visibility.
🧹 Nitpick comments (1)
webapp/src/state/active_calls/actions.test.ts (1)

15-33: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for fetchIsCallActiveInChannel.

This file only exercises activeCallAdded, so the new REST helper in actions.ts still ships without success/error-path coverage. A small mocked RestClient.fetch suite would lock down the true and fallback-false branches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/state/active_calls/actions.test.ts` around lines 15 - 33, Add test
coverage for fetchIsCallActiveInChannel in actions.test.ts, since the current
suite only validates activeCallAdded. Create a small mocked RestClient.fetch
setup around fetchIsCallActiveInChannel to cover the success path returning true
and the failure/fallback path returning false, using the
fetchIsCallActiveInChannel symbol to locate the new helper and ensure both
branches are locked down.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@webapp/src/actions.ts`:
- Around line 590-621: The hydration path in the thunk that handles call state
should not skip Redux refreshes when a call already exists in state. Move the
call data reconciliation actions in this block—activeCallAdded, hostChanged, and
sessionsReceived—so they always run for the call in channel, and keep the
callStartAtForCallInChannel(...) guard only around the incoming notification
branch using ringingEnabled and incomingCallOnChannel. This ensures the stored
call, host, and session data are refreshed during activation/reconnect even for
already-known calls.

In `@webapp/src/components/expanded_view/component.tsx`:
- Around line 403-410: The host control calls are still using channel.id instead
of the active call’s callID, so update ExpandedView to thread the current
ActiveCall['callID'] from state and pass that through to
hostMuteAllParticipants, hostRemoveParticipant, and HostControlsMenu. Use the
existing symbols handleMuteOthers, handleRemoveParticipant, and HostControlsMenu
to replace any fallback ''/channel-based IDs with the active call ID
consistently.

In `@webapp/src/selectors.ts`:
- Around line 257-258: The call dismissal lookup in callDismissedNotification is
using channelID to read dismissedCalls, but this store is keyed by call ID.
Update the selector to accept and use the call’s callID consistently, matching
dismissedCallForCurrentChannel and any callers such as actions.ts, so previously
dismissed calls are correctly recognized.

In `@webapp/src/state/active_calls/reducer.ts`:
- Around line 42-45: The CALL_ENDED branch in active_calls/reducer.ts should not
remove an entry using only channelID; it needs to verify the stored callID
matches action.data.callID before deleting. Update the CALL_ENDED handling in
the reducer so it looks up the active call for that channel and only clears it
when both channelID and callID correspond to the same call, preventing an older
end event from removing a newer active call.

In `@webapp/src/state/calls_availability/actions.ts`:
- Line 13: The toggle logic is only flipping callsNotAvailableInChannel(...),
which covers explicit stored overrides but ignores the effective availability
when callsConfig.DefaultEnabled is false and no row exists yet. Update the
thunk/action in calls_availability/actions.ts to compute the current effective
state first, then invert that state when building the payload so the first
enable/disable action works correctly for default-disabled channels. Use
callsNotAvailableInChannel and the surrounding toggle action as the key places
to adjust the logic and payload generation.

In `@webapp/src/state/calls_availability/selectors.ts`:
- Around line 44-46: The selector in calls_availability/selectors.ts is
short-circuiting all non-system-admins when defaultEnabled(state) is false,
which overrides explicit per-channel enables. Update the availability logic in
the selector that drives the header button so it still returns true when
callsAvailability[channelId].enabled is explicitly set, and only falls back to
the global default when there is no channel-specific setting; keep the behavior
aligned with callsAvailableInChannelWithDefault(...) and the render path in
index.tsx.

In `@webapp/src/state/hosts/selectors.ts`:
- Around line 17-27: The host selectors are using the currently viewed channel
instead of the active call channel, which can make the live call UI read the
wrong host record. Update getHostForCurrentCall in selectors.ts to key off the
active call channel from the call state/store used by the call widget, and keep
getHostIDForCurrentChannel/getHostChangeAtForCurrentChannel delegating through
that corrected selector so the call widget always reflects the active call.

---

Outside diff comments:
In `@webapp/src/index.tsx`:
- Around line 843-859: The channel-header menu action is now computed from
channel-specific permissions, but it is only refreshed on initial setup, so it
can get out of sync when switching channels. Update the channel change
subscriber to call the existing refresh path, using
registerHeaderMenuComponentIfNeeded and the channel ID from the current channel
state, so the Enable/Disable calls item is re-evaluated on every channel switch.
Make sure the unregister/register flow remains centralized in
registerChannelHeaderMenuAction and registerHeaderMenuComponentIfNeeded to avoid
stale visibility.

---

Nitpick comments:
In `@webapp/src/state/active_calls/actions.test.ts`:
- Around line 15-33: Add test coverage for fetchIsCallActiveInChannel in
actions.test.ts, since the current suite only validates activeCallAdded. Create
a small mocked RestClient.fetch setup around fetchIsCallActiveInChannel to cover
the success path returning true and the failure/fallback path returning false,
using the fetchIsCallActiveInChannel symbol to locate the new helper and ensure
both branches are locked down.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b38bdc3f-5af6-415d-88d9-48d22931c65d

📥 Commits

Reviewing files that changed from the base of the PR and between c4b49dd and 4546346.

📒 Files selected for processing (64)
  • build/sync/plan.yml
  • server/api_livekit_webhook_test.go
  • server/session_test.go
  • standalone/src/index.ts
  • standalone/src/recording/components/recording_view/index.tsx
  • standalone/src/recording/index.tsx
  • webapp/jest.config.js
  • webapp/src/action_types.ts
  • webapp/src/actions.ts
  • webapp/src/components/call_widget/component.tsx
  • webapp/src/components/call_widget/index.ts
  • webapp/src/components/call_widget/participant.tsx
  • webapp/src/components/call_widget/participants_list.tsx
  • webapp/src/components/channel_call_toast.tsx
  • webapp/src/components/channel_header_button.tsx
  • webapp/src/components/channel_header_dropdown_button/index.ts
  • webapp/src/components/channel_header_menu_button/index.ts
  • webapp/src/components/channel_header_menu_item/index.tsx
  • webapp/src/components/custom_post_types/post_type/index.ts
  • webapp/src/components/expanded_view/call_participant.tsx
  • webapp/src/components/expanded_view/call_participant_rhs.tsx
  • webapp/src/components/expanded_view/component.tsx
  • webapp/src/components/expanded_view/index.ts
  • webapp/src/components/host_controls_menu.tsx
  • webapp/src/i18n.mock.json
  • webapp/src/index.tsx
  • webapp/src/reducers.ts
  • webapp/src/selectors.ts
  • webapp/src/slash_commands.tsx
  • webapp/src/state/README.md
  • webapp/src/state/active_calls/action_types.ts
  • webapp/src/state/active_calls/actions.test.ts
  • webapp/src/state/active_calls/actions.ts
  • webapp/src/state/active_calls/reducer.test.ts
  • webapp/src/state/active_calls/reducer.ts
  • webapp/src/state/calls_availability/action_types.ts
  • webapp/src/state/calls_availability/actions.test.ts
  • webapp/src/state/calls_availability/actions.ts
  • webapp/src/state/calls_availability/reducer.test.ts
  • webapp/src/state/calls_availability/reducer.ts
  • webapp/src/state/calls_availability/selectors.ts
  • webapp/src/state/common_action_types.ts
  • webapp/src/state/common_actions.test.ts
  • webapp/src/state/common_actions.ts
  • webapp/src/state/common_selectors.ts
  • webapp/src/state/hosts/action_types.ts
  • webapp/src/state/hosts/actions.test.ts
  • webapp/src/state/hosts/actions.ts
  • webapp/src/state/hosts/reducer.test.ts
  • webapp/src/state/hosts/reducer.ts
  • webapp/src/state/hosts/selectors.ts
  • webapp/src/state/screen_sharing_ids/actions.test.ts
  • webapp/src/state/screen_sharing_ids/actions.ts
  • webapp/src/state/screen_sharing_ids/reducer.test.ts
  • webapp/src/state/screen_sharing_ids/reducer.ts
  • webapp/src/state/sessions/action_types.ts
  • webapp/src/state/sessions/actions.test.ts
  • webapp/src/state/sessions/actions.ts
  • webapp/src/state/sessions/reducer.test.ts
  • webapp/src/state/sessions/reducer.ts
  • webapp/src/state/sessions/selectors.ts
  • webapp/src/utils.ts
  • webapp/src/websocket_handlers.ts
  • webapp/tests/setup.js
💤 Files with no reviewable changes (5)
  • webapp/tests/setup.js
  • build/sync/plan.yml
  • webapp/src/components/channel_header_menu_button/index.ts
  • webapp/src/state/sessions/action_types.ts
  • webapp/src/action_types.ts

Comment thread webapp/src/actions.ts
Comment on lines +590 to +621
if (!callStartAtForCallInChannel(getState(), callAndChannelState.channel_id)) {
actions.push(
activeCallAdded(callAndChannelState.channel_id, {
callID: callAndChannelState.call.id,
startAt: callAndChannelState.call.start_at,
ownerID: callAndChannelState.call.owner_id,
threadID: callAndChannelState.call.thread_id,
}),
);

actions.push(hostChanged(callAndChannelState.channel_id, callAndChannelState.call.host_id, callAndChannelState.call.start_at));

actions.push(sessionsReceived(callAndChannelState.channel_id, getSessionsMapFromSessions(callAndChannelState.call.sessions)));

if (ringingEnabled(getState())) {
// dismissedNotification is populated after the actions array has been batched, so manually check:
const dismissed = callAndChannelState.call.dismissed_notification;
if (dismissed) {
const currentUserID = getCurrentUserId(getState());
if (Object.hasOwn(dismissed, currentUserID) && dismissed[currentUserID]) {
actions.push({
type: DISMISS_CALL,
data: {
callID: callAndChannelState.call.id,
},
});
continue;
}
}
dispatch(incomingCallOnChannel(callAndChannelState.channel_id, callAndChannelState.call.id, callAndChannelState.call.owner_id, callAndChannelState.call.start_at));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Refresh known calls during hydration.

The if (!callStartAtForCallInChannel(...)) guard skips activeCallAdded, hostChanged, and sessionsReceived for calls already in Redux. Since this thunk is used during activation/reconnect, any host or session changes missed while offline can stay stale. Keep the guard only around the “new incoming call” notification path.

Proposed fix
-            if (!callStartAtForCallInChannel(getState(), callAndChannelState.channel_id)) {
-                actions.push(
-                    activeCallAdded(callAndChannelState.channel_id, {
-                        callID: callAndChannelState.call.id,
-                        startAt: callAndChannelState.call.start_at,
-                        ownerID: callAndChannelState.call.owner_id,
-                        threadID: callAndChannelState.call.thread_id,
-                    }),
-                );
-
-                actions.push(hostChanged(callAndChannelState.channel_id, callAndChannelState.call.host_id, callAndChannelState.call.start_at));
-
-                actions.push(sessionsReceived(callAndChannelState.channel_id, getSessionsMapFromSessions(callAndChannelState.call.sessions)));
-
-                if (ringingEnabled(getState())) {
+            const isKnownActiveCall = Boolean(callStartAtForCallInChannel(getState(), callAndChannelState.channel_id));
+            actions.push(
+                activeCallAdded(callAndChannelState.channel_id, {
+                    callID: callAndChannelState.call.id,
+                    startAt: callAndChannelState.call.start_at,
+                    ownerID: callAndChannelState.call.owner_id,
+                    threadID: callAndChannelState.call.thread_id,
+                }),
+            );
+
+            actions.push(hostChanged(callAndChannelState.channel_id, callAndChannelState.call.host_id, callAndChannelState.call.start_at));
+            actions.push(sessionsReceived(callAndChannelState.channel_id, getSessionsMapFromSessions(callAndChannelState.call.sessions)));
+
+            if (!isKnownActiveCall && ringingEnabled(getState())) {
                 // dismissedNotification is populated after the actions array has been batched, so manually check:
                 const dismissed = callAndChannelState.call.dismissed_notification;
                 if (dismissed) {
                     const currentUserID = getCurrentUserId(getState());
                     if (Object.hasOwn(dismissed, currentUserID) && dismissed[currentUserID]) {
@@
                     }
                 }
                 dispatch(incomingCallOnChannel(callAndChannelState.channel_id, callAndChannelState.call.id, callAndChannelState.call.owner_id, callAndChannelState.call.start_at));
-                }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!callStartAtForCallInChannel(getState(), callAndChannelState.channel_id)) {
actions.push(
activeCallAdded(callAndChannelState.channel_id, {
callID: callAndChannelState.call.id,
startAt: callAndChannelState.call.start_at,
ownerID: callAndChannelState.call.owner_id,
threadID: callAndChannelState.call.thread_id,
}),
);
actions.push(hostChanged(callAndChannelState.channel_id, callAndChannelState.call.host_id, callAndChannelState.call.start_at));
actions.push(sessionsReceived(callAndChannelState.channel_id, getSessionsMapFromSessions(callAndChannelState.call.sessions)));
if (ringingEnabled(getState())) {
// dismissedNotification is populated after the actions array has been batched, so manually check:
const dismissed = callAndChannelState.call.dismissed_notification;
if (dismissed) {
const currentUserID = getCurrentUserId(getState());
if (Object.hasOwn(dismissed, currentUserID) && dismissed[currentUserID]) {
actions.push({
type: DISMISS_CALL,
data: {
callID: callAndChannelState.call.id,
},
});
continue;
}
}
dispatch(incomingCallOnChannel(callAndChannelState.channel_id, callAndChannelState.call.id, callAndChannelState.call.owner_id, callAndChannelState.call.start_at));
}
}
const isKnownActiveCall = Boolean(callStartAtForCallInChannel(getState(), callAndChannelState.channel_id));
actions.push(
activeCallAdded(callAndChannelState.channel_id, {
callID: callAndChannelState.call.id,
startAt: callAndChannelState.call.start_at,
ownerID: callAndChannelState.call.owner_id,
threadID: callAndChannelState.call.thread_id,
}),
);
actions.push(hostChanged(callAndChannelState.channel_id, callAndChannelState.call.host_id, callAndChannelState.call.start_at));
actions.push(sessionsReceived(callAndChannelState.channel_id, getSessionsMapFromSessions(callAndChannelState.call.sessions)));
if (!isKnownActiveCall && ringingEnabled(getState())) {
// dismissedNotification is populated after the actions array has been batched, so manually check:
const dismissed = callAndChannelState.call.dismissed_notification;
if (dismissed) {
const currentUserID = getCurrentUserId(getState());
if (Object.hasOwn(dismissed, currentUserID) && dismissed[currentUserID]) {
actions.push({
type: DISMISS_CALL,
data: {
callID: callAndChannelState.call.id,
},
});
continue;
}
}
dispatch(incomingCallOnChannel(callAndChannelState.channel_id, callAndChannelState.call.id, callAndChannelState.call.owner_id, callAndChannelState.call.start_at));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/actions.ts` around lines 590 - 621, The hydration path in the
thunk that handles call state should not skip Redux refreshes when a call
already exists in state. Move the call data reconciliation actions in this
block—activeCallAdded, hostChanged, and sessionsReceived—so they always run for
the call in channel, and keep the callStartAtForCallInChannel(...) guard only
around the incoming notification branch using ringingEnabled and
incomingCallOnChannel. This ensures the stored call, host, and session data are
refreshed during activation/reconnect even for already-known calls.

Comment on lines +403 to +410
handleMuteOthers = () => {
if (!this.props.channel) {
logErr('ExpandedView: host muting other failed, channel should be defined');
return;
}

logDebug('ExpandedView: host muting others');
hostMuteAllParticipants(this.props.channel.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Pass the active call ID here, not channel.id.

These paths now call host endpoints shaped as /calls/{callID}/host/..., and the new action signatures are typed as ActiveCall['callID']. This component still passes this.props.channel.id (or '') into hostMuteAllParticipants, hostRemoveParticipant, and HostControlsMenu, so host controls will target the wrong resource whenever the call ID differs from the channel ID. Please thread the current active call’s callID from state and use that value consistently here.

Also applies to: 909-911, 1165-1176

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/expanded_view/component.tsx` around lines 403 - 410,
The host control calls are still using channel.id instead of the active call’s
callID, so update ExpandedView to thread the current ActiveCall['callID'] from
state and pass that through to hostMuteAllParticipants, hostRemoveParticipant,
and HostControlsMenu. Use the existing symbols handleMuteOthers,
handleRemoveParticipant, and HostControlsMenu to replace any fallback
''/channel-based IDs with the active call ID consistently.

Comment thread webapp/src/selectors.ts
Comment on lines 257 to +258
export const callDismissedNotification = (state: GlobalState, channelID: string) => {
return Boolean(pluginReduxStore(state).dismissedCalls[channelID]);
return Boolean(getPluginStore(state).dismissedCalls[channelID]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Index dismissed calls by call ID, not channel ID.

Line 258 now reads from dismissedCalls[channelID], but this slice is keyed by call ID and the adjacent dismissedCallForCurrentChannel selector uses call.callID. Callers passing a channel ID, such as webapp/src/actions.ts, will miss prior dismissals and can re-notify for dismissed calls.

Proposed fix
 export const callDismissedNotification = (state: GlobalState, channelID: string) => {
-    return Boolean(getPluginStore(state).dismissedCalls[channelID]);
+    const callID = getPluginStore(state).activeCalls[channelID]?.callID;
+    return Boolean(callID && getPluginStore(state).dismissedCalls[callID]);
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const callDismissedNotification = (state: GlobalState, channelID: string) => {
return Boolean(pluginReduxStore(state).dismissedCalls[channelID]);
return Boolean(getPluginStore(state).dismissedCalls[channelID]);
export const callDismissedNotification = (state: GlobalState, channelID: string) => {
const callID = getPluginStore(state).activeCalls[channelID]?.callID;
return Boolean(callID && getPluginStore(state).dismissedCalls[callID]);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/selectors.ts` around lines 257 - 258, The call dismissal lookup in
callDismissedNotification is using channelID to read dismissedCalls, but this
store is keyed by call ID. Update the selector to accept and use the call’s
callID consistently, matching dismissedCallForCurrentChannel and any callers
such as actions.ts, so previously dismissed calls are correctly recognized.

Comment on lines +42 to +45
case CALL_ENDED: {
const nextState = {...initialState};
delete nextState[action.data.channelID];
return nextState;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard CALL_ENDED with the stored callID.

This branch drops the entry purely by channelID, even though the action carries callID as well. If an older end event arrives after a new call has already started in the same channel, the reducer will remove the newer call from activeCalls.

Suggested fix
     case CALL_ENDED: {
+        const current = initialState[action.data.channelID];
+        if (!current || current.callID !== action.data.callID) {
+            return initialState;
+        }
+
         const nextState = {...initialState};
         delete nextState[action.data.channelID];
         return nextState;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case CALL_ENDED: {
const nextState = {...initialState};
delete nextState[action.data.channelID];
return nextState;
case CALL_ENDED: {
const current = initialState[action.data.channelID];
if (!current || current.callID !== action.data.callID) {
return initialState;
}
const nextState = {...initialState};
delete nextState[action.data.channelID];
return nextState;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/state/active_calls/reducer.ts` around lines 42 - 45, The
CALL_ENDED branch in active_calls/reducer.ts should not remove an entry using
only channelID; it needs to verify the stored callID matches action.data.callID
before deleting. Update the CALL_ENDED handling in the reducer so it looks up
the active call for that channel and only clears it when both channelID and
callID correspond to the same call, preventing an older end event from removing
a newer active call.

import {getPluginPath} from 'src/utils';

import {CHANNEL_CALLS_AVAILABILITY_UPDATED} from './action_types';
import {callsNotAvailableInChannel} from './selectors';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Toggle the effective availability, not only explicit overrides.

Line 33 only flips callsNotAvailableInChannel(...), which is true only for stored enabled: false rows. If callsConfig.DefaultEnabled is false and the channel has no row yet, the effective state is still disabled, but this thunk posts {enabled: false} again. That makes the first enable attempt a no-op in default-disabled channels.

Suggested fix
-import {callsNotAvailableInChannel} from './selectors';
+import {callsAvailableInChannelWithDefault} from './selectors';
@@
-                body: JSON.stringify({enabled: callsNotAvailableInChannel(getState(), currentChannelID)}),
+                body: JSON.stringify({enabled: !callsAvailableInChannelWithDefault(getState(), currentChannelID)}),

Also applies to: 31-33

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/state/calls_availability/actions.ts` at line 13, The toggle logic
is only flipping callsNotAvailableInChannel(...), which covers explicit stored
overrides but ignores the effective availability when callsConfig.DefaultEnabled
is false and no row exists yet. Update the thunk/action in
calls_availability/actions.ts to compute the current effective state first, then
invert that state when building the payload so the first enable/disable action
works correctly for default-disabled channels. Use callsNotAvailableInChannel
and the surrounding toggle action as the key places to adjust the logic and
payload generation.

Comment on lines +44 to +46
if (!defaultEnabled(state)) {
return false;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't short-circuit explicit channel enables when the global default is off.

Line 44 returns false for every non-system-admin whenever DefaultEnabled is false. That also hides the header button in channels whose callsAvailability[channelId].enabled was explicitly set to true, even though callsAvailableInChannelWithDefault(...) treats those channels as available and index.tsx uses this selector to decide whether the button renders.

Suggested fix
-    if (!defaultEnabled(state)) {
+    if (!defaultEnabled(state) && !callsAvailableInChannel(state, channelId)) {
         return false;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!defaultEnabled(state)) {
return false;
}
if (!defaultEnabled(state) && !callsAvailableInChannel(state, channelId)) {
return false;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/state/calls_availability/selectors.ts` around lines 44 - 46, The
selector in calls_availability/selectors.ts is short-circuiting all
non-system-admins when defaultEnabled(state) is false, which overrides explicit
per-channel enables. Update the availability logic in the selector that drives
the header button so it still returns true when
callsAvailability[channelId].enabled is explicitly set, and only falls back to
the global default when there is no channel-specific setting; keep the behavior
aligned with callsAvailableInChannelWithDefault(...) and the render path in
index.tsx.

Comment on lines +17 to +27
export const getHostForCurrentCall = (state: GlobalState) => {
const currentChannelID = getCurrentChannelId(state);
return getPluginStore(state).hosts[currentChannelID];
};

export const getHostIDForCurrentChannel = (state: GlobalState) => {
return getHostForCurrentCall(state)?.hostID;
};

export const getHostChangeAtForCurrentChannel = (state: GlobalState) => {
return getHostForCurrentCall(state)?.hostChangeAt;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the active call channel here, not getCurrentChannelId.

getHostForCurrentCall is keyed off the currently viewed channel, but the global call widget consumes these selectors for the live call UI (webapp/src/components/call_widget/index.ts, Lines 83-84). If I join a call and then browse to another channel, the widget will start reading that other channel’s host record, so host controls/timestamps can drift away from the actual active call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/state/hosts/selectors.ts` around lines 17 - 27, The host selectors
are using the currently viewed channel instead of the active call channel, which
can make the live call UI read the wrong host record. Update
getHostForCurrentCall in selectors.ts to key off the active call channel from
the call state/store used by the call widget, and keep
getHostIDForCurrentChannel/getHostChangeAtForCurrentChannel delegating through
that corrected selector so the call widget always reflects the active call.

@bgardner8008 bgardner8008 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Zubair. Mostly I have to rely on claude to review react code, but between claude and coderabbit I think the review looks complete.

};

export const getHostForCurrentCall = (state: GlobalState) => {
const currentChannelID = getCurrentChannelId(state);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This returns channelID for currently viewed channel, the call might be on another channel. It should use "const currentChannelID = channelIDForCurrentCall(state);" instead.

Comment thread webapp/src/actions.ts
};
};

export const hydradeCallsAndChannelStatesExcept = (skipChannelID?: string) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be "hydrate" instead?

return getHostForCurrentCall(state)?.hostID;
};

export const getHostChangeAtForCurrentChannel = (state: GlobalState) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably be renamed to getHostChangeAtForCurrentCall once fixed below?

return getPluginStore(state).hosts[currentChannelID];
};

export const getHostIDForCurrentChannel = (state: GlobalState) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be renamed to getHostIDForCurrentCall?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants