Skip to content

ReportActionCompose 3/6: ComposerProvider with subcomponents#87639

Merged
cristipaval merged 15 commits intoExpensify:mainfrom
callstack-internal:decompose/composer-pr3-orchestrator-leaves
Apr 20, 2026
Merged

ReportActionCompose 3/6: ComposerProvider with subcomponents#87639
cristipaval merged 15 commits intoExpensify:mainfrom
callstack-internal:decompose/composer-pr3-orchestrator-leaves

Conversation

@adhorodyski
Copy link
Copy Markdown
Contributor

@adhorodyski adhorodyski commented Apr 10, 2026

Explanation of Change

PR 3/6 in the ReportActionCompose decomposition series (issue #87097).

Wraps the 834-line ReportActionCompose monolith in ComposerProvider (from PR 1) and extracts 10 child components, each subscribing only to the contexts it needs. The orchestrator shrinks from 834 → 355 LOC with only 2 context subscriptions remaining (for shared attachment/DropZone state).

Extracted components (all new files):

Component LOC What it does Context subscriptions
ComposerBox 49 OfflineWithFeedback container + border styling State, SendState, Meta
ComposerLocalTime 39 Guard-wrapped participant time zone display None (Onyx only)
ComposerFooter 26 Pure layout container, accepts children None
ComposerSendButton 17 Thin context bridge → SendButton SendState, SendActions
ComposerExceededLength 18 Guard-wrapped character count warning SendState
AgentZeroAwareTypingIndicator 13 Suppresses typing indicator for Agent Zero None
ComposerActionMenu 68 Context→props bridge for AttachmentPickerWithMenuItems State, SendState, Actions, Meta
ComposerInputWrapper 134 Context→props bridge for ComposerWithSuggestions, owns inputPlaceholder + lastReportAction derivation State, SendState, Actions, SendActions, Meta
ComposerEmojiPicker 70 Self-contained emoji picker button + cleanup Actions, Meta

Other changes:

  • ComposerProvider: onValueChange now also calls setValue(v) to keep text context in sync
  • SendButton: removed memo() wrapper (React Compiler handles memoization)
  • ReportActionCompose: removed memo(), onSubmitAction (dead code), SuggestionsRef type (re-exported from ComposerContext), all useCallback/useMemo wrappers
  • Test: removed dead onSubmitAction import, uses keyPress instead

What stays inline in the orchestrator (355 LOC):

  • submitForm (message submission logic) — moves to useComposerSubmit hook in PR 4
  • useAttachmentUploadValidation + DropZone render — moves to split hooks + ComposerDropZone in PR 5
  • These share attachment state (validateAttachments, attachmentFileRef) which prevents clean separation without the hook extractions from PRs 4+5

Context subscription design:

  • No subscribe-to-everything middleman — each wrapper subscribes selectively
  • Orchestrator subscribes to only 2 of 6 contexts (SendState for exceededMaxLength, Meta for attachment refs)
  • ComposerFooter is context-free (pure layout), its children self-subscribe independently

💡 I recommend reviewing this with Hide whitespace.

Fixed Issues

$ #87097
PROPOSAL:

Tests

  1. Open a DM chat → verify composer renders correctly with input field, + button, emoji picker, send button
  2. Type a message and press Enter → verify it sends and appears in the chat
  3. Open a DM with a user in a different timezone → verify the local time display shows above the composer
  4. Open a workspace chat → verify local time does NOT show
  5. Type a very long message (4000+ characters) → verify the character count warning appears below the composer
  6. Start typing in any chat → verify the typing indicator appears in the footer area
  7. Open the + menu → verify it opens with all options, close it → verify composer refocuses
  8. Open emoji picker → select an emoji → verify it inserts into the composer
  9. Drag and drop a file onto the composer → verify the drop zone appears
  10. Go offline → type and send a message → verify it queues, go back online → verify it sends
  11. Navigate between different chats → verify composer state resets per room
  12. Verify no JS console errors throughout all steps

Offline tests

  1. Go offline → type a message → press Enter → verify it appears optimistically
  2. Go back online → verify the message syncs

QA Steps

Same as tests above.

  • 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 related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • 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 2 commits April 10, 2026 16:31
@adhorodyski adhorodyski changed the title [No QA] Decompose ReportActionCompose 3/6: wrap in ComposerProvider, extract Box/Footer/LocalTime/SendButton ReportActionCompose 3/6: ComposerProvider with subcomponents Apr 10, 2026
adhorodyski and others added 7 commits April 10, 2026 17:58
… as thin wrappers

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>
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 adhorodyski marked this pull request as ready for review April 14, 2026 10:49
@adhorodyski adhorodyski requested review from a team as code owners April 14, 2026 10:49
@melvin-bot melvin-bot Bot requested review from DylanDylann and joekaufmanexpensify and removed request for a team April 14, 2026 10:49
@melvin-bot
Copy link
Copy Markdown

melvin-bot Bot commented Apr 14, 2026

@DylanDylann Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot Bot removed the request for review from a team April 14, 2026 10:49
Copy link
Copy Markdown

@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: d5bbacc3ca

ℹ️ 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 thread src/pages/inbox/report/ReportActionCompose/ComposerActionMenu.tsx
disabled={isBlockedFromConcierge || isEmojiPickerVisible()}
setIsCommentEmpty={() => {}}
onEnterKeyPress={handleSendMessage}
shouldShowComposeInput={shouldShowComposeInput}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore one-way composer autofocus latch

This wrapper no longer passes any didHideComposerInput state into ComposerWithSuggestions, even though that component’s autofocus condition explicitly depends on !didHideComposerInput. The previous one-way latch prevented refocusing after the compose input had been hidden once; without it, flows where SHOULD_SHOW_COMPOSE_INPUT toggles off and back on can unexpectedly autofocus and reopen the keyboard.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The one-way latch (didHideComposerInput) was dead code in the original orchestrator. Our PR doesn't pass it (undefined), which produces identical behavior:

  • Default case: shouldShowComposeInput starts true → original latch would be false!false = true. Our !undefined = true. Same.
  • Small screens (native/mWeb): ReportFooter unmounts the entire composer when shouldShowComposeInput goes false. On re-show, the component remounts fresh — the latch never had a chance to fire.
  • Large screens (web): The refocus effect checks willBlurTextInputOnTapOutside || shouldAutoFocus. On wide layouts willBlurTextInputOnTapOutside() returns true, short-circuiting the check — shouldAutoFocus (and therefore the latch) is never evaluated.

The blueprint PR (#86899) removes the prop and latch entirely from ComposerWithSuggestions. Our split-plan does the same cleanup in PR 4.

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.

@MelvinBot Could you recheck if this is safe to remove didHideComposerInput

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.

@MelvinBot, You need to check the code to verify that we are not missing anything. Should not rely on above comments

Copy link
Copy Markdown
Contributor

@MelvinBot MelvinBot Apr 16, 2026

Choose a reason for hiding this comment

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

Verified directly from code on mainyes, safe to remove. The prop is already dead code today.

didHideComposerInput exists in only one file: ComposerWithSuggestions.tsx (type at line 152, destructured at line 249, used at line 296). No caller passes it. ComposerInput.tsx:104-131 is the sole place ComposerWithSuggestions is rendered, and it does not include didHideComposerInput in its JSX props. ReportActionCompose.tsx has zero references to it — the state hook and one-way latch effect that used to produce this value have already been removed.

Since the prop is optional (didHideComposerInput?: boolean), it defaults to undefined. The only usage is:

// ComposerWithSuggestions.tsx:296
const shouldAutoFocus = ... && !didHideComposerInput;

!undefined === true, so the term is already a no-op. Removing it changes zero runtime behavior.

Edge cases checked:

  • Large screens (web desktop): Composer stays mounted when shouldShowComposeInput flips false (ReportFooter.tsx:90), but didHideComposerInput is already undefined there too — no behavioral difference.
  • Tablets: Same as large screens — isSmallScreenWidth is false, composer stays mounted, prop already undefined.
  • Small screens: Composer unmounts entirely when shouldShowComposeInput is false, so the latch was never needed.

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.

@MelvinBot You need to check deeper to understand why don't pass didHideComposerInput into ComposerWithSuggestions in ComposerInput is fine

Copy link
Copy Markdown
Contributor

@MelvinBot MelvinBot Apr 16, 2026

Choose a reason for hiding this comment

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

Dug deeper. Here's the full trace of every code path where didHideComposerInput could matter.

The only logic reference is at ComposerWithSuggestions.tsx:296:

const shouldAutoFocus = (shouldFocusInputOnScreenFocus || !!draftComment)
    && shouldShowComposeInput && areAllModalsHidden() && isFocused
    && !didHideComposerInput;

!undefined === !false === true — so omitting the prop only differs from passing true. The question is: can the old latch ever fire (didHideComposerInput=true) while the composer stays mounted?

The latch fires when shouldShowComposeInput goes false. The only writer that sets it to false is ReportActionItemMessageEdit.tsx:557 (user focuses the edit composer for an existing message).

shouldAutoFocus has 4 consumers — here's what happens in each when didHideComposerInput is undefined instead of true:

# Usage Location Impact of undefined vs true
1 autoFocus prop line 967 None. React ignores autoFocus after mount. At mount time, shouldShowComposeInput defaults to true, so the old latch initialized to false — same as undefined.
2 updateMultilineInputRange line 840 None. Empty [] deps — only runs once at mount, same initial value as above.
3 Modal-close refocus line 827 None. Condition is willBlurTextInputOnTapOutside || shouldAutoFocus. The latch only differs on large screens (where the composer stays mounted per ReportFooter.tsx:90), and on large screens willBlurTextInputOnTapOutside returns true — short-circuiting the OR.
4 Delayed RHP focus lines 662-686 Theoretically different but near-impossible: only fires when shouldDelayAutoFocus=true (stacked SearchReport RHP) AND the user edited a message AND finished editing. In this case the old code blocked refocus, the new code allows it — arguably better behavior.
Platform-by-platform breakdown

Small screens (native + mWeb): ReportFooter.tsx:90 renders the composer only when shouldShowComposeInput \|\| !isSmallScreenWidth. On small screens, !isSmallScreenWidth is false, so when shouldShowComposeInput=false, the entire composer tree unmounts. On re-show it remounts fresh — the latch never persists across mount boundaries. didHideComposerInput is irrelevant.

Large screens, web wide layout: Composer stays mounted (!isSmallScreenWidth=true keeps the condition true). willBlurTextInputOnTapOutside() returns true (it's !getIsNarrowLayout() on web). This short-circuits all the relevant OR conditions, making shouldAutoFocus redundant for the modal-close refocus path (usage #3).

Large screens, web narrow layout: willBlurTextInputOnTapOutside() returns false, but isSmallScreenWidth would be true → composer unmounts → same as small screen case.

Bottom line: Safe to remove. In every reachable scenario, undefined produces identical behavior to what the old latch produced. The only theoretical gap (stacked SearchReport RHP + mid-edit toggle) is both unreachable in practice and results in better behavior if it ever fired.

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.

@adhorodyski At this point

Large screens (web): The refocus effect checks willBlurTextInputOnTapOutside || shouldAutoFocus. On wide layouts willBlurTextInputOnTapOutside() returns true, short-circuiting the check — shouldAutoFocus (and therefore the latch) is never evaluated.

shouldAutoFocus is used in other places. Could you also prove that it's safe in all places? Your above point only prove the safe in one place

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@DylanDylann verified directly in code — walking through each shouldAutoFocus consumer in ComposerWithSuggestions.tsx:

Mount-only (unaffected by latch removal):

  1. autoFocus prop on <Composer> (line 967) — React reads autoFocus only at mount. Latch init is useState(!shouldShowComposeInput); Onyx default is true → latch starts false!false === !undefined === true. Same.
  2. updateMultilineInputRange effect (line 840) — [] deps, mount-only. Same as above.

Post-mount (where the latch could matter):
3. Modal-close / screen-refocus effect (line 827) — guard is (willBlurTextInputOnTapOutside || shouldAutoFocus) && .... Per platform:

  • Web wide: willBlurTextInputOnTapOutside = !getIsNarrowLayout()true → short-circuits OR, shouldAutoFocus never evaluated.
  • Web narrow / native phone: isSmallScreenWidth=true. ReportFooter.tsx:90 unmounts the whole composer when shouldShowComposeInput=false (!shouldHideComposer && (shouldShowComposeInput || !isSmallScreenWidth)). Remount resets state — latch never persists.
  • Native tablet (iPad): willBlurTextInputOnTapOutside=false and !isSmallScreenWidth=true keeps the composer mounted — the one platform where the latch could theoretically differ. But the effect has a second guard at line 831: if (editFocused) { inputFocusChange(false); return; } — if the user is actively in edit mode, refocus is blocked anyway. editFocused is the real safety net; the latch was a redundant second layer.
  1. Delayed RHP focus effect (lines 662-686) — requires shouldDelayAutoFocus=true (stacked SearchReport RHP). Incompatible with the edit-mode flow that triggers the latch. Unreachable combo.

Only writer of shouldShowComposeInput=false is ReportActionItemMessageEdit.tsx:557 (via setShouldShowComposeInputKeyboardAware), so the latch was only ever tied to message-edit flows.

Safe to remove in every reachable path.

@rlinoz rlinoz self-requested a review April 14, 2026 14:23
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 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 Δ
...ortActionCompose/AgentZeroAwareTypingIndicator.tsx 100.00% <100.00%> (ø)
...s/inbox/report/ReportActionCompose/ComposerBox.tsx 100.00% <100.00%> (ø)
...nbox/report/ReportActionCompose/ComposerContext.ts 92.30% <ø> (+92.30%) ⬆️
...ort/ReportActionCompose/ComposerExceededLength.tsx 100.00% <100.00%> (ø)
...nbox/report/ReportActionCompose/ComposerFooter.tsx 100.00% <100.00%> (ø)
...ox/report/ReportActionCompose/ComposerProvider.tsx 80.95% <100.00%> (+80.95%) ⬆️
.../report/ReportActionCompose/ComposerSendButton.tsx 100.00% <100.00%> (ø)
...omposerWithSuggestions/ComposerWithSuggestions.tsx 58.73% <100.00%> (-0.58%) ⬇️
...es/inbox/report/ReportActionCompose/SendButton.tsx 100.00% <ø> (ø)
.../report/ReportActionCompose/ComposerActionMenu.tsx 87.50% <87.50%> (ø)
... and 5 more
... and 218 files with indirect coverage changes

…d architecture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@adhorodyski adhorodyski force-pushed the decompose/composer-pr3-orchestrator-leaves branch from 8e718f3 to 1bb139c Compare April 14, 2026 18:02
@joekaufmanexpensify
Copy link
Copy Markdown
Contributor

No product review needed

@joekaufmanexpensify joekaufmanexpensify removed their request for review April 14, 2026 20:49
@DylanDylann
Copy link
Copy Markdown
Contributor

Let's move this section into a new component

                    {shouldDisplayDualDropZone && (
                        <DualDropZone
                            isEditing={shouldAddOrReplaceReceipt && hasReceipt}
                            onAttachmentDrop={(dragEvent) => validateAttachments({dragEvent})}
                            onReceiptDrop={onReceiptDropped}
                            shouldAcceptSingleReceipt={shouldAddOrReplaceReceipt}
                        />
                    )}
                    {!shouldDisplayDualDropZone && (
                        <DragAndDropConsumer onDrop={(dragEvent) => validateAttachments({dragEvent})}>
                            <DropZoneUI
                                icon={icons.MessageInABottle}
                                dropTitle={translate('dropzone.addAttachments')}
                                dropStyles={styles.attachmentDropOverlay(true)}
                                dropTextStyles={styles.attachmentDropText}
                                dashedBorderStyles={[styles.dropzoneArea, styles.easeInOpacityTransition, styles.activeDropzoneDashedBorder(theme.attachmentDropBorderColorActive, true)]}
                            />
                        </DragAndDropConsumer>
                    )}

Comment thread src/pages/inbox/report/ReportActionCompose/ComposerLocalTime.tsx Outdated
Comment thread src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx Outdated
Comment on lines 336 to 340
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.

It's clean to have a new component for this

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.

@adhorodyski How about this?

@DylanDylann
Copy link
Copy Markdown
Contributor

DylanDylann commented Apr 15, 2026

This would result in a really nice structure

Screenshot 2026-04-15 at 12 11 32

@DylanDylann
Copy link
Copy Markdown
Contributor

@codex review

Copy link
Copy Markdown

@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: 1bb139c8f3

ℹ️ 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 thread src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx Outdated
Comment thread src/pages/inbox/report/ReportActionCompose/ComposerInput.tsx
Comment thread src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx Outdated
Comment thread src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx Outdated
Comment thread src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx Outdated
Comment thread src/pages/inbox/report/ReportActionCompose/ComposerProvider.tsx
Comment thread src/pages/inbox/report/ReportActionCompose/ComposerEmojiPicker.tsx Outdated
@adhorodyski
Copy link
Copy Markdown
Contributor Author

Let's move this section into a new component (Dropzone)

This is planned for PR 5 where we introduce useAttachmentUploadValidation hook. Extracting now would require prop-drilling validateAttachments which the hook eliminates. I'd leave it as-is for now.

…context for isBlockedFromConcierge

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

@DylanDylann responded to all comments, pushed updates with the latest commit. back to you

@rlinoz rlinoz requested a review from DylanDylann April 15, 2026 17:04
@DylanDylann
Copy link
Copy Markdown
Contributor

Checking again

onPasteFile={onPasteFile}
onClear={submitForm}
disabled={isBlockedFromConcierge || isEmojiPickerVisible()}
setIsCommentEmpty={() => {}}
Copy link
Copy Markdown
Contributor

@DylanDylann DylanDylann Apr 16, 2026

Choose a reason for hiding this comment

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

setIsCommentEmpty prop is redundant now, please remove this prop

disabled={isBlockedFromConcierge || isEmojiPickerVisible()}
setIsCommentEmpty={() => {}}
onEnterKeyPress={handleSendMessage}
shouldShowComposeInput={shouldShowComposeInput}
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.

@MelvinBot Could you recheck if this is safe to remove didHideComposerInput

@DylanDylann
Copy link
Copy Markdown
Contributor

DylanDylann commented Apr 16, 2026

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
    • MacOS: Desktop
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I verified proper code patterns were followed (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
    • 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 verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • 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
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • 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 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 related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Screen.Recording.2026-04-16.at.16.16.34.mov
Android: mWeb Chrome
Screen.Recording.2026-04-16.at.16.13.54.mov
iOS: HybridApp
Screen.Recording.2026-04-16.at.16.23.45.mov
iOS: mWeb Safari
Screen.Recording.2026-04-16.at.16.15.13.mov
MacOS: Chrome / Safari
Screen.Recording.2026-04-16.at.16.09.51.mov

@MelvinBot
Copy link
Copy Markdown
Contributor

Yes, removing didHideComposerInput is safe. Here's why:

The prop is effectively dead code in all scenarios:

  1. Default case: shouldShowComposeInput starts true → latch initializes to false!false = true. Without the prop, !undefined = true. Identical behavior.

  2. Small screens (native/mWeb): ReportFooter (ReportFooter.tsx:90) unmounts the entire composer tree when shouldShowComposeInput goes false. On re-show, the component remounts fresh — the latch never has a chance to fire.

  3. Large screens (web): willBlurTextInputOnTapOutside() returns true (index.ts:4), which short-circuits the only condition where shouldAutoFocus (and thus didHideComposerInput) matters — the modal-close refocus at ComposerWithSuggestions.tsx:827.

The blueprint PR (#86899) also removes this prop entirely from ComposerWithSuggestions, confirming the team's intent to treat it as dead code.

@DylanDylann
Copy link
Copy Markdown
Contributor

@adhorodyski Three questions. The rest looks fine

#87639 (comment)
#87639 (comment)
#87639 (comment)

@adhorodyski
Copy link
Copy Markdown
Contributor Author

Applied requested changes and responded here #87639 (comment) to the latch removal aspect, all ready.

@adhorodyski adhorodyski requested a review from DylanDylann April 17, 2026 16:06
Copy link
Copy Markdown
Contributor

@DylanDylann DylanDylann left a comment

Choose a reason for hiding this comment

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

LGTM


/** Function to set whether the comment is empty */
setIsCommentEmpty: (isCommentEmpty: boolean) => void;
setIsCommentEmpty?: (isCommentEmpty: boolean) => void;
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.

I mean we should remove this prop totally. Do you plan to do it in PR 4?

Copy link
Copy Markdown
Contributor Author

@adhorodyski adhorodyski Apr 20, 2026

Choose a reason for hiding this comment

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

Yess let's leave this for PR4 as I have other props that will be dropped there as per #87097 (comment)

@melvin-bot melvin-bot Bot requested a review from cristipaval April 19, 2026 07:12
@cristipaval cristipaval merged commit df08b0d into Expensify:main Apr 20, 2026
35 of 38 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🚧 @cristipaval has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify
Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@adhorodyski adhorodyski deleted the decompose/composer-pr3-orchestrator-leaves branch April 20, 2026 09:08
@OSBotify
Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/cristipaval in version: 9.3.61-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

@MelvinBot
Copy link
Copy Markdown
Contributor

I reviewed the changes in this PR. This is a purely internal refactoring that decomposes the ReportActionCompose monolith into smaller sub-components (ComposerBox, ComposerLocalTime, ComposerFooter, ComposerSendButton, etc.).

No user-facing behavior, UI labels, features, or settings are changed — the composer renders and functions identically before and after this PR.

No help site documentation updates are required.

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.

7 participants