fix: show desktop notification when active chat is open but app is unfocused#93387
Conversation
|
@QichenZhu 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] |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 949ab84072
ℹ️ 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".
joekaufmanexpensify
left a comment
There was a problem hiding this comment.
Nice improvement!
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-06-15.at.3.36.34.PM.movAndroid: mWeb ChromeScreen.Recording.2026-06-15.at.3.34.30.PM.moviOS: HybridAppScreen.Recording.2026-06-15.at.3.33.18.PM.moviOS: mWeb SafariScreen.Recording.2026-06-15.at.3.33.51.PM.movMacOS: Chrome / Safarimac-desktop.mov |
QichenZhu
left a comment
There was a problem hiding this comment.
- Repeat while keeping NewDot focused on the same chat.
- Verify the message is marked read immediately and no unnecessary notification is shown.
These steps are incorrect. New messages won't be marked as read if the previous unread state is not cleared first.
| useEffect(() => { | ||
| const markAsReadWhenVisibleAndFocused = useCallback(() => { |
There was a problem hiding this comment.
We shouldn't add useCallback to components that use react-compiler.
| useEffect(() => { | ||
| markAsReadWhenVisibleAndFocused(); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [isVisible, isFocused, reportLoadingState?.hasOnceLoadedReportActions]); |
There was a problem hiding this comment.
Here we should explain the suppression, even though we didn't add it, since we changed this effect.
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [ |
There was a problem hiding this comment.
Please add an explanation or remove the suppression.
| useEffect(() => { | ||
| markAsReadWhenVisibleAndFocused(); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [isFocused, isVisible, reportLoadingState?.hasOnceLoadedReportActions]); |
There was a problem hiding this comment.
Add a comment for the suppression as well.
| useAppFocusEvent(markAsReadWhenVisibleAndFocused); | ||
|
|
There was a problem hiding this comment.
We need this for the desktop-app-switch case. The browser tab can stay visible while the NewDot window loses OS focus, so Visibility.onVisibilityChange may not fire when focus returns. useAppFocusEvent reruns the read catch-up when the user actually focuses NewDot again.
There was a problem hiding this comment.
Thanks for explaining. I'm still trying to understand it. What bugs will happen without it?
There was a problem hiding this comment.
Good point. After re-checking the flow, I agree this is not required for the core issue.
The bug is fixed by preventing readNewestAction while the window is visible but unfocused, and by keeping unread/skipped-read handling guarded by Visibility.hasFocus(). Without this extra app-focus listener, the original notification/new-marker issue still stays fixed; the only difference is that a skipped read is not consumed immediately on window focus and instead follows the normal scroll/viewability path.
I removed it to keep the PR smaller and focused on the issue scope.
There was a problem hiding this comment.
@nabi-ebrahimi thanks for checking! Since markAsReadWhenVisibleAndFocused() is now only used once, we don't need it. Same goes for src/hooks/useMarkAsRead.ts.
There was a problem hiding this comment.
Good point. Since useAppFocusEvent was removed, the helper was only used once. I inlined it in both places with no behavior change.
| useAppFocusEvent(markAsReadWhenVisibleAndFocused); | ||
|
|
There was a problem hiding this comment.
Same reason here: useIsFocused() only tells us the report route is focused inside navigation. In this bug, the route remains focused while the browser window itself is unfocused, so we need the app/window focus event to consume the skipped unread state when the user returns.
There was a problem hiding this comment.
Same as above.
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
@QichenZhu Thanks for the review. I addressed the feedback and pushed the updates. Summary:
Could you please take another look when you have a chance? |
luacmartins
left a comment
There was a problem hiding this comment.
@nabi-ebrahimi we also have conflicts now
Resolved, thanks |
|
@QichenZhu kindly bump, thanks. |
|
@nabi-ebrahimi please check this comment. Thanks! |
All done, thanks for the review! |
QichenZhu
left a comment
There was a problem hiding this comment.
@nabi-ebrahimi thanks! the code is much cleaner now.
| // This effect logic to `mark as read` will only run when the report focused has new messages and the App visibility | ||
| // is changed to visible(meaning user switched to app/web, while user was previously using different tab or application). | ||
| // We will mark the report as read in the above case which marks the LHN report item as read while showing the new message | ||
| // marker for the chat messages received while the user wasn't focused on the report or on another browser tab for web. | ||
| // This effect should only run when app visibility/focus changes; the helper reads the latest report/action values without making every action update mark the report as read. |
There was a problem hiding this comment.
NAB: since you reverted the previous changes and brought back the original comment, the new comment is not needed.
|
🚧 @luacmartins has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/luacmartins in version: 9.4.15-0 🚀
|
|
🤖 Help site review: no docs changes required. I reviewed the changes in this PR against the help site files under This is an internal behavior-correctness fix to the read/unread-marker and desktop-notification timing logic — auto-mark-as-read now requires window focus ( I searched the help site for articles documenting this behavior. The notification-related articles describe:
None of them document when a message is marked as read or when an in-app desktop notification appears relative to window focus. No documented feature, setting, label, button, or workflow is changed or made stale by this PR, so no help site update is needed and I did not create a draft PR. If you believe there is a specific article that should describe this behavior, reply with |
|
🚀 Deployed to production by https://github.com/puneetlath in version: 9.4.15-3 🚀
|
Explanation of Change
This PR makes the unread/read logic depend on whether the user is actually focused on the app window, not only whether the app is visible.
The issue happens when NewDot is open to the active chat, the browser window remains visible, but focus is on another desktop app. In that state, the app was treating the incoming message as seen because
Visibility.isVisible()was still true, so it marked the message read immediately. That advancedlastReadTime, which then caused the desktop notification to be suppressed as “already read.”This change prevents that premature mark-as-read by requiring
Visibility.hasFocus()before auto-reading messages in the open report. It also keeps the unread marker visible for new incoming messages while the window is unfocused, and re-runs the read logic when the app regains focus so normal focused behavior still works.Fixed Issues
$ #92273
PROPOSAL: #92273 (comment)
Tests
Offline tests
Not applicable. This issue is about desktop window focus while the app is online and visible, not offline behavior.
QA Steps
Same as tests.
// TODO: These must be filled out, or the issue title must include "[No QA]."
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-06-12.at.10.32.26.AM.mov