Repository navigation
Add 'Mark all as read' context menu to the Unreads tab - #98950
Conversation
Co-authored-by: Hans Vu <hungvu193@users.noreply.github.com>
🦜 Polyglot Parrot! 🦜Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues: View the translation diffdiff --git a/src/languages/de.ts b/src/languages/de.ts
index 5bfd1118..18148d96 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -3068,7 +3068,13 @@ ${amount} für ${merchant} – ${date}`,
prompt: (priorityModePageUrl: string) =>
`Behalten Sie den Überblick, indem Sie nur ungelesene Chats oder Chats sehen, die Ihre Aufmerksamkeit erfordern. Keine Sorge, Sie können dies jederzeit in den <a href="${priorityModePageUrl}">Einstellungen</a> ändern.`,
},
- inboxTabs: {all: 'Alle', todo: 'Aufgaben', unread: 'Ungelesen', markAllAsRead: 'Alle als gelesen markieren', markAllAsReadConfirmationPrompt: 'Möchtest du wirklich alle Chats als gelesen markieren?'},
+ inboxTabs: {
+ all: 'Alle',
+ todo: 'Aufgaben',
+ unread: 'Ungelesen',
+ markAllAsRead: 'Alle als gelesen markieren',
+ markAllAsReadConfirmationPrompt: 'Sind Sie sicher, dass Sie alle Chats als gelesen markieren möchten?',
+ },
reportDetailsPage: {
inWorkspace: (policyName: string) => `in ${policyName}`,
generatingPDF: 'PDF erstellen',
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 14be9d0f..5994f221 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -3128,7 +3128,7 @@ ${amount} για ${merchant} - ${date}`,
todo: 'Εκκρεμότητες',
unread: 'Μη αναγνωσμένα',
markAllAsRead: 'Επισήμανση όλων ως αναγνωσμένων',
- markAllAsReadConfirmationPrompt: 'Είστε βέβαιοι ότι θέλετε να επισημάνετε όλες τις συνομιλίες ως αναγνωσμένες;',
+ markAllAsReadConfirmationPrompt: 'Είστε βέβαιοι ότι θέλετε να σημειώσετε όλες τις συνομιλίες ως αναγνωσμένες;',
},
reportDetailsPage: {
goToRoom: 'Μετάβαση στο δωμάτιο',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 89f83840..61e42289 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2974,7 +2974,13 @@ ${amount} para ${merchant} - ${date}`,
prompt: (priorityModePageUrl: string) =>
`Mantente al tanto de todo viendo solo los chats no leídos o los chats que necesitan tu atención. No te preocupes, puedes cambiarlo en cualquier momento en los <a href="${priorityModePageUrl}">ajustes</a>.`,
},
- inboxTabs: {all: 'Todo', todo: 'Pendientes', unread: 'No leído', markAllAsRead: 'Marcar todo como leído', markAllAsReadConfirmationPrompt: '¿Seguro que quieres marcar todos los chats como leídos?'},
+ inboxTabs: {
+ all: 'Todo',
+ todo: 'Pendientes',
+ unread: 'No leído',
+ markAllAsRead: 'Marcar todo como leído',
+ markAllAsReadConfirmationPrompt: '¿Seguro que quieres marcar todos los chats como leídos?',
+ },
reportDetailsPage: {
goToRoom: 'Ir a la sala',
inWorkspace: (policyName) => `en ${policyName}`,
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index ac60bee9..adc705b6 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -3074,7 +3074,13 @@ ${amount} pour ${merchant} - ${date}`,
prompt: (priorityModePageUrl: string) =>
`Gardez le contrôle en n’affichant que les discussions non lues ou celles qui nécessitent votre attention. Ne vous inquiétez pas, vous pouvez modifier ce réglage à tout moment dans les <a href="${priorityModePageUrl}">paramètres</a>.`,
},
- inboxTabs: {all: 'Tout', todo: 'Tâches', unread: 'Non lu', markAllAsRead: 'Tout marquer comme lu', markAllAsReadConfirmationPrompt: 'Voulez-vous vraiment marquer toutes les discussions comme lues ?'},
+ inboxTabs: {
+ all: 'Tout',
+ todo: 'Tâches',
+ unread: 'Non lu',
+ markAllAsRead: 'Tout marquer comme lu',
+ markAllAsReadConfirmationPrompt: 'Voulez-vous vraiment marquer toutes les discussions comme lues ?',
+ },
reportDetailsPage: {
inWorkspace: (policyName: string) => `dans ${policyName}`,
generatingPDF: 'Générer le PDF',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 951802ca..f0b41cbd 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -3059,7 +3059,13 @@ ${amount} per ${merchant} - ${date}`,
prompt: (priorityModePageUrl: string) =>
`Tieniti al passo vedendo solo le chat non lette o quelle che richiedono la tua attenzione. Non preoccuparti, puoi cambiare questa impostazione in qualsiasi momento nelle <a href="${priorityModePageUrl}">impostazioni</a>.`,
},
- inboxTabs: {all: 'Tutti', todo: 'Attività da fare', unread: 'Non letti', markAllAsRead: 'Segna tutto come letto', markAllAsReadConfirmationPrompt: 'Vuoi davvero segnare tutte le chat come lette?'},
+ inboxTabs: {
+ all: 'Tutti',
+ todo: 'Attività da fare',
+ unread: 'Non letti',
+ markAllAsRead: 'Segna tutto come letto',
+ markAllAsReadConfirmationPrompt: 'Sei sicuro di voler contrassegnare tutte le chat come lette?',
+ },
reportDetailsPage: {
inWorkspace: (policyName: string) => `in ${policyName}`,
generatingPDF: 'Genera PDF',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 87362e29..fa2682f2 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -3060,7 +3060,13 @@ ${amount} voor ${merchant} - ${date}`,
prompt: (priorityModePageUrl: string) =>
`Blijf op de hoogte door alleen ongelezen chats of chats te zien die je aandacht nodig hebben. Geen zorgen, je kunt dit op elk moment wijzigen in de <a href="${priorityModePageUrl}">instellingen</a>.`,
},
- inboxTabs: {all: 'Alles', todo: 'Te doen', unread: 'Ongelezen', markAllAsRead: 'Alles als gelezen markeren', markAllAsReadConfirmationPrompt: 'Weet je zeker dat je alle chats als gelezen wilt markeren?'},
+ inboxTabs: {
+ all: 'Alles',
+ todo: 'Te doen',
+ unread: 'Ongelezen',
+ markAllAsRead: 'Alles als gelezen markeren',
+ markAllAsReadConfirmationPrompt: 'Weet je zeker dat je alle chats als gelezen wilt markeren?',
+ },
reportDetailsPage: {
inWorkspace: (policyName: string) => `in ${policyName}`,
generatingPDF: 'PDF genereren',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 5b016300..57d2a692 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -3047,7 +3047,13 @@ ${amount} dla ${merchant} - ${date}`,
prompt: (priorityModePageUrl: string) =>
`Bądź na bieżąco, widząc tylko nieprzeczytane czaty lub czaty wymagające twojej uwagi. Spokojnie, możesz to zmienić w dowolnym momencie w <a href="${priorityModePageUrl}">ustawieniach</a>.`,
},
- inboxTabs: {all: 'Wszystko', todo: 'Zadania do wykonania', unread: 'Nieprzeczytane', markAllAsRead: 'Oznacz wszystkie jako przeczytane', markAllAsReadConfirmationPrompt: 'Czy na pewno chcesz oznaczyć wszystkie czaty jako przeczytane?'},
+ inboxTabs: {
+ all: 'Wszystko',
+ todo: 'Zadania do wykonania',
+ unread: 'Nieprzeczytane',
+ markAllAsRead: 'Oznacz wszystkie jako przeczytane',
+ markAllAsReadConfirmationPrompt: 'Na pewno chcesz oznaczyć wszystkie czaty jako przeczytane?',
+ },
reportDetailsPage: {
inWorkspace: (policyName: string) => `w ${policyName}`,
generatingPDF: 'Wygeneruj PDF',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 3927e917..245c766b 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -3050,7 +3050,13 @@ ${amount} para ${merchant} - ${date}`,
prompt: (priorityModePageUrl: string) =>
`Fique no controle vendo apenas chats não lidos ou que precisam da sua atenção. Não se preocupe, você pode mudar isso a qualquer momento em <a href="${priorityModePageUrl}">configurações</a>.`,
},
- inboxTabs: {all: 'Todos', todo: 'Pendências', unread: 'Não lidas', markAllAsRead: 'Marcar tudo como lido', markAllAsReadConfirmationPrompt: 'Tem certeza de que deseja marcar todos os chats como lidos?'},
+ inboxTabs: {
+ all: 'Todos',
+ todo: 'Pendências',
+ unread: 'Não lidas',
+ markAllAsRead: 'Marcar tudo como lido',
+ markAllAsReadConfirmationPrompt: 'Tem certeza de que quer marcar todos os chats como lidos?',
+ },
reportDetailsPage: {
inWorkspace: (policyName: string) => `em ${policyName}`,
generatingPDF: 'Gerar PDF',
Note You can apply these changes to your branch by copying the patch to your clipboard, then running |
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.
|
Co-authored-by: Hans Vu <hungvu193@users.noreply.github.com>
|
Fixed the failing Oxfmt check — pushed commit Cause: the PR added Fix: ran The only other failing check, Check independent approval, is a process gate (the PR still needs an independent reviewer approval) and isn't something a code change can resolve. |
|
|
@MelvinBot The right click on Tab Selector still doesn't work, let fix it with secondaryInteraction |
…eraction Co-authored-by: Hans Vu <hungvu193@users.noreply.github.com>
|
Fixed in dc57aa7. Right-click now opens the menu. The tab was using Static checks all pass: ESLint,
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc57aa76ac
ℹ️ 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".
| ]} | ||
| wrapperStyle={equalWidth ? styles.flex1 : styles.flexGrow1} | ||
| onLongPress={onLongPress} | ||
| onSecondaryInteraction={onLongPress} |
There was a problem hiding this comment.
Keep secondary interactions disabled for disabled tabs
On narrow web layouts while offline, SearchTypeMenuNarrow retains saved-search tabs with a pending delete and marks them isDisabled. The previous onLongPress was suppressed by the disabled pressable, but PressableWithSecondaryInteraction handles browser contextmenu events outside the pressable and does not check disabled, so this new handler still opens the rename/share/delete menu for an item already being deleted. Gate the secondary interaction when the tab is disabled, matching the existing press behavior.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 94b2901408.
PressableWithSecondaryInteraction wires the web contextmenu (right-click) listener directly and calls onSecondaryInteraction without checking the pressable's disabled prop (PressableWithSecondaryInteraction/index.tsx:83), so a right-click on a disabled tab (e.g. a saved-search tab with a pending delete in SearchTypeMenuNarrow) would still open the menu.
Gated it to match the primary press: onSecondaryInteraction={isPressableDisabled ? undefined : onLongPress}, where isPressableDisabled = !disabledAction && isDisabled is the same expression already used for the disabled prop (TabSelectorItem.tsx:76). When onSecondaryInteraction is undefined, the component skips registering the contextmenu listener entirely, so disabled tabs no longer open a menu on right-click or long-press.
| // Wait for the popover to fully hide before showing the confirmation modal so they don't overlap. | ||
| shouldCallAfterModalHide: true, | ||
| onSelected: confirmMarkAllAsRead, |
There was a problem hiding this comment.
Preserve modal sequencing in Safari
On Safari, PopoverMenu intentionally ignores shouldCallAfterModalHide unless its shouldAvoidSafariException prop is enabled. Because this instance leaves that prop at its default, selecting the action hides the popover via onItemSelected and invokes showConfirmModal immediately, so the confirmation appears while the popover and its focus trap are still dismissing rather than after it has hidden as intended. Enable the Safari override here or launch the confirmation from the popover's hide callback.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
@MelvinBot Same here, let's investigate and fix it
There was a problem hiding this comment.
Fixed in 94b2901408.
Confirmed: PopoverMenu only honors shouldCallAfterModalHide when !isSafari() || shouldAvoidSafariException (PopoverMenu/index.tsx:421). Since this instance left shouldAvoidSafariException at its default (false), on Safari the selection fell through to the immediate branch — onSelected (and thus showConfirmModal) fired while the popover and its focus trap were still dismissing.
Added shouldAvoidSafariException to this PopoverMenu (InboxTabSelector.tsx) so the after-hide sequencing the shouldCallAfterModalHide comment intends holds on Safari too — the confirmation modal now opens only after the popover has fully hidden.
There was a problem hiding this comment.
This one is already addressed in the latest commit 94b2901 on this branch.
The PopoverMenu in InboxTabSelector.tsx:131 now passes shouldAvoidSafariException, so shouldCallAfterModalHide is honored on Safari and the confirmation modal opens only after the popover has fully hidden — fixing the sequencing issue codex flagged.
No further change needed here.
|
@hungvu193 Please let me know once this PR is ready for review. Thanks! |
Sure |
…al sequencing Co-authored-by: Hans Vu <hungvu193@users.noreply.github.com>
|
No code-actionable failures remain. The Oxfmt check that was failing is now green after my earlier fix (commit The two checks still showing red are both non-actionable process gates, not code problems:
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94b2901408
ℹ️ 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".
| } | ||
| setActiveTab(key); | ||
| }} | ||
| onLongTabPress={openMarkAllAsReadMenu} |
There was a problem hiding this comment.
Avoid intercepting right-clicks on tabs without an action
On desktop web, passing openMarkAllAsReadMenu as the selector-wide handler installs a secondary-interaction callback on All and To-dos as well as Unread. PressableWithSecondaryInteraction prevents the browser context menu before this callback returns for those unsupported keys, so right-clicking All or To-dos now silently does nothing instead of retaining its prior browser behavior. Attach the secondary interaction only to the Unread item, rather than filtering after the event has already been consumed.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
we already early return with other tab, this won't be an issue.
There was a problem hiding this comment.
Do you think it will be considered as regression?
In staging, right clicking on other tabs opens browser context menu:
Screen.Recording.2026-08-20.at.09.37.53.mov
In this PR, right clicking on other tabs opens nothing:
Screen.Recording.2026-08-20.at.09.38.07.mov
There was a problem hiding this comment.
Yeah, that's definitely regression 👀
There was a problem hiding this comment.
@hungvu193 This issue was reintroduced when there are no unread chats in the Unread tab.
There was a problem hiding this comment.
@truph01 Let's revert it while waiting for Design team decision then?
There was a problem hiding this comment.
@hungvu193 Yes, then we can address it as follow-up PR if needed
|
No code fix needed — the only actionable failure (Oxfmt check) was already resolved by the earlier The two checks still showing red are process/human gates, not something a code push can fix:
|
|
No code fix needed — all code checks are green. The Oxfmt fix I pushed earlier ( The two remaining ❌ checks are process gates, not code issues, so there's nothing for me to push:
Both clear once the PR gets a review; no code change can resolve them. |
|
@MelvinBot Fill all the checkboxes, fill the QA and Offline section with "Same as Tests". Fill the below screenshots link to Screenshots/Video section: Screen.Recording.2026-08-19.at.16.50.02.movScreen.Recording.2026-08-19.at.16.51.34.movScreen.Recording.2026-08-19.at.16.54.08.movScreen.Recording.2026-08-19.at.16.58.12.mov |
|
Done ✅ — updated the PR description:
|
|
@MelvinBot Mark this PR ready for review. |
…eOnyx selector Read private_isArchived through reportNameValuePairsArchivedSelector in the components and pass it into markAllMessagesAsRead, so the action stays a plain function and consumers only re-render when the archived flags change. Co-authored-by: Hans Vu <hungvu193@users.noreply.github.com>
|
Done — reverted the What changed
The One note: I couldn't run |
|
@hungvu193 I have a comment #98950 (comment) |
|
@MelvinBot Revert 9b2cc1e |
This reverts commit 9b2cc1e.
|
🔄 Reverted commit The menu-gating logic in Checks
|
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-08-22.at.17.10.35.movAndroid: mWeb ChromeScreen.Recording.2026-08-22.at.16.57.29.moviOS: HybridAppScreen.Recording.2026-08-22.at.17.05.38.moviOS: mWeb SafariScreen.Recording.2026-08-22.at.17.09.35.movMacOS: Chrome / SafariScreen.Recording.2026-08-22.at.16.53.02.mov |
Part of me think it's worth keeping there just for the education, but it does seem a bit odd that it's an option. Curious what @shawnborton thinks. |
|
Hmm yeah tough one, I could see both sides but I kinda like just leaving it there as well. |
|
Let's go with leaving it then 👍 |
tgolen
left a comment
There was a problem hiding this comment.
I approve the Onyx.connectWithoutView() part of this review. Looks like there are no more references to it
|
@tgolen Can you merge it? Ty |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚧 tgolen has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/tgolen in version: 9.4.60-0 🚀
|
Help site review — changes required ✅This PR adds a user-facing Mark all as read action to the Unread tab (right-click on web / long-press on mobile → confirmation modal). That behavior is documentable and wasn't covered on the help site, so I've drafted an update. Docs PR: #99367 — Docs updates for E/A#98950 Mark all as read from Unread tab What changed: Added a task-based section "How to mark all chats as read from the Unread tab" to Verification notes
@MelvinBot, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR |
|
Deploy Blocker #99378 was identified to be related to this PR. |
|
Deploy Blocker #99379 was identified to be related to this PR. |
|
🚀 Deployed to production by https://github.com/AndrewGable in version: 9.4.60-2 🚀
Bundle Size Analysis (Sentry): |







Explanation of Change
Implements Part 1 of the approved proposal in #98057: a fast, discoverable way to clear an unread backlog from the LHN.
Right-clicking (or long-pressing) the Unread tab in the inbox tab row now opens a context menu with a single Mark all as read option. Selecting it shows a confirmation modal ("Are you sure you want to mark all chats as read?"); confirming marks every unread chat as read.
This is a UI-wiring change on top of the existing bulk primitive — it reuses the same
markAllMessagesAsRead()action that already backs the Shift+Escape keyboard shortcut, so there is no new Onyx/API/state work. Implementation notes:onLongTabPresshook onTabSelectorBaseintoInboxTabSelector, gated to only theUNREADtab.PopoverMenuanchored to the tab row viausePopoverPosition; the confirmation uses the (non-deprecated)useConfirmModalglobal-modal hook.inboxTabs.markAllAsReadandinboxTabs.markAllAsReadConfirmationPromptcopy across all locales.Currently the action clears all unread reports globally (matching the keyboard shortcut). Scoping it to only the visible/filtered Unread list would be a follow-up if desired.
Fixed Issues
$ #98057
PROPOSAL: #98057 (comment)
Tests
Offline tests
Same as Tests
QA Steps
Same as Tests
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, 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.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
Screen.Recording.2026-08-19.at.16.50.02.mov
Android: mWeb Chrome
Screen.Recording.2026-08-19.at.16.51.34.mov
iOS: Native
Screen.Recording.2026-08-19.at.16.54.08.mov
iOS: mWeb Safari
Screen.Recording.2026-08-19.at.16.58.12.mov
MacOS: Chrome / Safari
Screen.Recording.2026-08-19.at.16.50.02.mov