Skip to content

perf: decompose ReportScreen into child components#84971

Draft
adhorodyski wants to merge 40 commits intoExpensify:mainfrom
callstack-internal:decompose-report-screen-plan
Draft

perf: decompose ReportScreen into child components#84971
adhorodyski wants to merge 40 commits intoExpensify:mainfrom
callstack-internal:decompose-report-screen-plan

Conversation

@adhorodyski
Copy link
Contributor

@adhorodyski adhorodyski commented Mar 11, 2026

Explanation of Change

This PR continues a series of refactors aimed at simplifying ReportScreen by pushing Onyx subscriptions and business logic down into the child components that actually need them, rather than fetching data at the top level and passing it through props.

Each commit in this series targets a specific concern:

  1. Push compose-only concerns from ReportFooter into ReportActionCompose — props that were only needed by the compose area are now subscribed to directly inside ReportActionCompose, removing them from ReportFooter's interface.
  2. Make footer children self-subscribing, remove remaining pass-throughs — remaining prop pass-throughs in ReportFooter are eliminated; each child subscribes to the data it needs.
  3. Move ReportLifecycleHandler inside ReportNotFoundGuard — the lifecycle handler only makes sense when a report exists, so it's now rendered as a child of the not-found guard rather than alongside it.
  4. Push concierge + parent action concerns from ReportActionsList into ReportActionsView — concierge and parent action logic that lived in ReportActionsList moves into ReportActionsView, which is the appropriate owner.
  5. Restructure ReportHeader rendering with early returns — the header component is refactored to use early returns, making the rendering logic easier to follow and removing conditional complexity from the caller.

The end result is a leaner ReportScreen with a narrower prop surface, and child components that are more self-contained and independently readable.


⭐ This results in a ~5% net performance gain on all React tasks as well as code quality improvements, unlocking further refactors of core bottlenecks on this screen.

Sending a message (1st is baseline)
Screenshot 2026-03-13 at 13 35 32

Open a report (1st is baseline)
Screenshot 2026-03-13 at 13 29 53

Fixed Issues

$ #84895
PROPOSAL:

Tests

  • Verify that no errors appear in the JS console

Offline tests

N/A

QA Steps

Same as tests

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

adhorodyski and others added 18 commits March 11, 2026 17:20
Create IsInSidePanelContext + useIsInSidePanel hook. SidePanelReport wraps
children with the context provider. All consumers (ReportScreen, HeaderView,
ReportFooter, ReportActionCompose) now call the hook directly instead of
receiving the prop through multiple layers of components.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move the useFocusEffect that resolves reportID and validates
reportActionID from ReportScreen into a dedicated renderless
component. This removes useArchivedReportsIdSet and usePermissions
subscriptions from ReportScreen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move emoji picker hiding on blur, notification clearing, telemetry
span cancellation, and DeviceEventEmitter listener from ReportScreen
into a dedicated renderless component. Also removes the dead
isSkippingOpenReport ref.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create ReportHeader as a self-subscribing building block that owns:
- Report type subscription for variant selection
- onBackButtonPress derivation
- OfflineWithFeedback wrapper
- MoneyRequestHeader | MoneyReportHeader | HeaderView rendering

This removes the headerView useMemo (ground rule violation: JSX in
variable) and onBackButtonPress callback from ReportScreen. Also removes
MoneyReportHeader, MoneyRequestHeader, HeaderView, OfflineWithFeedback,
and useSidePanelActions imports from ReportScreen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move all report fetching, transaction thread creation, leaving events,
updateLastVisitTime, compose input init, and thread rejoin effects from
ReportScreen into a self-subscribing renderless component.

This removes 12 effects and 6 useOnyx subscriptions from ReportScreen
(introSelected, onboarding, isLoadingReportData, isLoadingApp,
transactionThreadReport, chatReport via the controller's own subs).

Also removes unused refs (hasCreatedLegacyThreadRef,
didSubscribeToReportLeavingEvents, prevIsAnonymousUser),
isLinkedMessagePageReady chain, and many unused imports.

ReportScreen: 952 → 704 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create WideRHPReceiptPanel as a self-subscribing building block that
owns shouldShowWideRHP derivation, useShowWideRHPVersion call, and
the receipt view rendering. This removes the WideRHP logic,
transactionThreadReport subscription, and isSmallScreenWidth from
ReportScreen.

Also removes Animated, ScrollView, MoneyRequestReceiptView,
useShowWideRHPVersion, and isTransactionThread imports from ReportScreen.

ReportScreen: 704 → 671 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create ReportActionsList as a self-subscribing building block that owns
reportActions, transactions, metadata, and view switching (skeleton |
ReportActionsView | MoneyRequestReportActionsList). Create ReportComposer
that owns lastReportAction, transactionThreadReportID, and wraps
ReportFooter.

This removes the data pass-throughs to ReportActionsView,
MoneyRequestReportActionsList, and ReportFooter from ReportScreen.
Also removes policy, chatReport, transactionThreadReportActions,
and ~10 derived values from ReportScreen.

ReportScreen: 671 → 541 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Create ReportNotFoundGuard as a self-subscribing building block that
owns all not-found page logic, linked action detection, deletion
navigation, report removal navigation, and the FullPageNotFoundView +
DragAndDropProvider wrappers.

ReportScreen is now a pure JSX composition shell with 0 useOnyx,
0 useEffect, 0 useState, 0 useMemo, 0 useCallback.

ReportScreen: 541 → 93 lines (92% reduction from original 1123).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both header components now accept reportID instead of report/policy/
reportActions/parentReportAction props. They subscribe to their own
data internally via useOnyx, useParentReportAction, and
usePaginatedReportActions.

ReportHeader is slimmed to only pass reportID + onBackButtonPress.
MoneyRequestReportView.tsx updated to pass new props.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
HeaderView now accepts only reportID + onNavigationMenuButtonClicked.
It subscribes to report and parentReportAction internally. ReportHeader
is slimmed further — no longer needs useParentReportAction or
shouldUseNarrowLayout. Tests updated to new prop interface.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ReportComposer now renders the footer UI directly (archived footer,
anonymous footer, blocked footer, compose area) instead of delegating
to a separate ReportFooter component. This eliminates the data-
intermediary wrapper that only existed to pass props.

MoneyRequestReportView updated to use ReportComposer directly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The component renders archived footers, anonymous footers, blocked
footers, banners, AND the composer — 'ReportFooter' better describes
its full scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the interleaved boolean conditionals
({!isArchivedRoom && !isAnonymousUser && !isBlockedFromChat && ...})
with structured early returns for each mutually exclusive state.

Each rendering path (compose, archived, anonymous, blocked, system
chat, admins-only) is now self-contained with its own return. No more
manual boolean exclusion chains. Easy to scan, modify, and extend.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move lastReportAction derivation, onSubmitComment/handleCreateTask,
concierge status indicator (useAgentZeroStatusIndicator), and all
supporting hooks into ReportActionCompose. This eliminates ~14
subscriptions from ReportFooter including 6 collection-level ones
from useAncestors that fired on every app event.

ReportFooter now only owns footer variant selection (archived,
anonymous, blocked, system chat, admins-only) and passes just
reportID + isComposerFullSize + didHideComposerInput to
ReportActionCompose.

ReportActionCompose accepts only reportID, isComposerFullSize,
didHideComposerInput, and optional composer focus/blur callbacks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
ArchivedReportFooter now accepts reportID, subscribes to report and
currentUserAccountID internally.

AnonymousReportFooter now accepts reportID, subscribes to report and
derives isSmallSizeLayout internally.

ReportActionCompose now owns isComposerFullSize, didHideComposerInput,
and the chatFooterStyles computation — accepts only reportID.

ReportFooter is now a pure view-switcher (136 lines) with 0 data
pass-throughs. It passes only reportID to each variant.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Notification clearing and telemetry span cancellation only make sense
when the report exists. Previously they fired with undefined reportID
on the not-found page. Now they only run when the guard renders its
children (report found).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eportActionsView

Move useAgentZeroStatusIndicator, useParentReportAction, concierge
side panel logic, hasUserSentMessage, sessionStartTime, and
isReportTransactionThread from ReportActionsList into ReportActionsView
where they're actually consumed.

ReportActionsList drops from 135 to 106 lines and sheds 6 hooks
(useIsInSidePanel, useCurrentUserPersonalDetails, useParentReportAction,
useSidePanelState, useAgentZeroStatusIndicator, conciergeReportID sub).
It's now a thin view-switcher: skeleton | table | list.

Also inlines hasUserSentMessage (was an IIFE, now a plain ternary).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace boolean exclusion chains ({!isTransactionThreadView &&
isMoneyRequestOrInvoiceReport && ...}) with structured early returns.
Each header variant (MoneyRequestHeader, MoneyReportHeader, HeaderView)
is now a self-contained return with its own OfflineWithFeedback wrapper.
No impossible states.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@adhorodyski adhorodyski changed the title [No QA] Decompose ReportScreen: push concerns into child components perf: decompose ReportScreen into child components Mar 11, 2026
adhorodyski and others added 10 commits March 11, 2026 22:49
…-plan

# Conflicts:
#	src/components/MoneyReportHeader.tsx
#	src/pages/inbox/ReportScreen.tsx
Move all linked-action not-found logic (VISIBLE_REPORT_ACTIONS subscription,
usePaginatedReportActions, linking state, whisper checks, and 4 related effects)
into a child LinkedActionNotFoundGuard component. This scopes high-frequency
re-renders from these subscriptions to just the child instead of the entire
report view tree (~50+ components).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move navigation-on-removal and navigation-on-deletion effects into a sibling
ReportNavigateAwayHandler component. This removes 3 exclusive Onyx subscriptions
(introSelected, conciergeReportID, useReportWasDeleted) and 2 large effects
from the guard, leaving it focused purely on not-found rendering.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Relocate the task report readNewestAction effect from ReportNotFoundGuard
to ReportFetchController where other fetch/init concerns already live.
Guard now has 7 Onyx subscriptions, 1 effect, ~120 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ldShow prop

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…liance

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract ReportDragAndDropProvider as a self-subscribing wrapper so
ReportNotFoundGuard becomes a pure guard and ReportScreen's composition
tree is fully transparent.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split into a cheap outer guard that only reads the route, and an inner
gate that holds all Onyx subscriptions. In the common case (no
reportActionID in the route), ~8-9 Onyx subs and all effects are skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…-plan

# Conflicts:
#	src/components/MoneyReportHeader.tsx
Copy link
Contributor

@TMisiukiewicz TMisiukiewicz left a comment

Choose a reason for hiding this comment

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

nice 💪

adhorodyski and others added 3 commits March 12, 2026 14:00
…ltering, inline header JSX

- Remove useCallback in ReportLifecycleHandler and ReportRouteParamHandler (React Compiler handles memoization)
- Short-circuit transaction filtering when offline and combine filter+map into single pass in ReportActionCompose
- Remove useMemo from reportHeaderView in MoneyRequestReportView, extract shared onBackButtonPress, inline header JSX

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@adhorodyski
Copy link
Contributor Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 38705c4c99

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

adhorodyski and others added 2 commits March 12, 2026 19:01
ReportActionsView now derives isConciergeSidePanel/hasUserSentMessage/sessionStartTime
from context instead of props. Wrap test renders in IsInSidePanelContext and
SidePanelStateContext providers, update mock to use sortedVisibleReportActions,
and remove stale prop usage.

HeaderView now self-subscribes to report via useOnyx, so seed report into Onyx
before render and add evictableKeys to prevent canEvict crash.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SwipeableView wrapper with onSwipeDown={Keyboard.dismiss} was
accidentally dropped during the ReportFooter refactor, removing the
swipe-down-to-dismiss-keyboard gesture on mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@adhorodyski
Copy link
Contributor Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4dfcae44b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

adhorodyski and others added 5 commits March 13, 2026 16:18
Replace the per-instance useAgentZeroStatusIndicator hook with a single
AgentZeroStatusProvider context. This fixes the Codex review item where
the composer's kickoffWaitingIndicator() only set local state that
ReportActionsView never saw.

Key changes:
- AgentZeroStatusProvider with cheap guard/gate pattern: non-Concierge
  reports short-circuit with zero subscriptions
- Pusher subscription owned directly by the provider (removed
  subscribeToReportReasoningEvents/unsubscribeFromReportReasoningChannel
  action functions and ConciergeReasoningStore module)
- ConciergeThinkingMessage self-subscribes to context (removed 3 prop
  pass-throughs from ReportActionsView → ReportActionsList)
- isConciergeChat derived from reportID === conciergeReportID (no
  report object subscription needed)
- Server label extracted into agentZeroProcessingIndicatorSelector in
  src/selectors/ReportNameValuePairs.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve 2 conflicts:
- ReportScreen.tsx: keep our thin shell, apply betas param in ReportFetchController
- MoneyReportHeader.tsx: merge upstream imports (handleUnvalidatedAccount,
  sortPoliciesByName, hasRequestFromCurrentAccount) with our self-subscribing imports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@adhorodyski
Copy link
Contributor Author

@codex review

@adhorodyski
Copy link
Contributor Author

The new results should be even better, I was able to short-circuit all concierge-chat-only logic on non-concierge chats.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 09ee10c3bf

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +115 to +118
Pusher.subscribe(channelName, Pusher.TYPE.CONCIERGE_REASONING, (data: Record<string, unknown>) => {
const eventData = data as {reasoning: string; agentZeroRequestID: string; loopCount: number};
addReasoning(eventData);
}).catch((error: unknown) => {

Choose a reason for hiding this comment

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

P1 Badge Deduplicate concierge reasoning subscriptions per report

This gate subscribes to Pusher.TYPE.CONCIERGE_REASONING every time it mounts, but cleanup later calls Pusher.unsubscribe(channel, event), which unbinds that event at the channel level; if the same concierge report is open in multiple mounted ReportScreen instances (for example split-pane/side-panel or modal variants), unmounting one instance removes the listener for the others, and the still-visible screen stops receiving reasoning/status updates. The previous flow avoided this with per-report subscription tracking, so this needs equivalent shared ref-counting before unbinding.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

valid, this only requires small tweaks on the pusher wrapper to enable multiple callbacks on the same channel

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed, definitely will need to be split down

@codecov
Copy link

codecov bot commented Mar 13, 2026

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/hooks/useIsInSidePanel.ts 100.00% <100.00%> (ø)
src/libs/actions/Report/index.ts 66.40% <ø> (-0.38%) ⬇️
src/pages/home/report/ConciergeThinkingMessage.tsx 25.00% <100.00%> (+25.00%) ⬆️
src/pages/inbox/HeaderView.tsx 97.80% <ø> (-0.03%) ⬇️
src/pages/inbox/ReportDragAndDropProvider.tsx 100.00% <100.00%> (ø)
src/pages/inbox/ReportLifecycleHandler.tsx 100.00% <100.00%> (ø)
src/pages/inbox/ReportNotFoundGuard.tsx 100.00% <100.00%> (ø)
src/pages/inbox/ReportScreen.tsx 100.00% <100.00%> (+29.22%) ⬆️
src/pages/inbox/report/ReportActionsList.tsx 78.74% <ø> (+0.40%) ⬆️
src/pages/inbox/report/ReportActionsView.tsx 81.50% <100.00%> (+0.65%) ⬆️
... and 17 more
... and 10 files with indirect coverage changes

…bind

The Pusher wrapper's unsubscribe() removed ALL callbacks for an event,
breaking concurrent listeners (e.g. concierge chat open in main pane +
side panel). subscribe() now returns a PusherSubscription handle with an
unsubscribe() method that removes only that specific callback, with full
cascading cleanup (event → channel) when the last listener unmounts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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.

2 participants