@@ -9,12 +9,18 @@ import {
99import {
1010 computeInboxTabCounts ,
1111 INBOX_SCOPE_FOR_YOU ,
12+ isExcludedFromInbox ,
13+ isPullRequestReport ,
14+ isReportTabReport ,
1215 matchesReviewerScope ,
1316 parseTeammateInboxScope ,
1417} from "@posthog/core/inbox/reportMembership" ;
1518import { useOptionalAuthenticatedClient } from "@posthog/ui/features/auth/authClient" ;
1619import { useCurrentUser } from "@posthog/ui/features/auth/useCurrentUser" ;
17- import { useInboxReportsInfinite } from "@posthog/ui/features/inbox/hooks/useInboxReports" ;
20+ import {
21+ useInboxReports ,
22+ useInboxReportsInfinite ,
23+ } from "@posthog/ui/features/inbox/hooks/useInboxReports" ;
1824import { useInboxReviewerScopeStore } from "@posthog/ui/features/inbox/stores/inboxReviewerScopeStore" ;
1925import { useInboxSignalsFilterStore } from "@posthog/ui/features/inbox/stores/inboxSignalsFilterStore" ;
2026import { useMemo } from "react" ;
@@ -36,9 +42,15 @@ const EMPTY_FILTER_ARRAY: never[] = [];
3642export function useInboxAllReports ( options ?: {
3743 ignoreScope ?: boolean ;
3844 ignoreFilters ?: boolean ;
45+ pullRequestsOnly ?: boolean ;
3946} ) {
4047 const ignoreScope = options ?. ignoreScope ?? false ;
4148 const ignoreFilters = options ?. ignoreFilters ?? false ;
49+ // The Pull requests tab fetches a server-filtered list (reports that have a
50+ // shipped PR) so its list body comes from the same source as its count — a PR
51+ // sitting past the broad list's first page no longer renders an empty tab
52+ // under a positive badge.
53+ const pullRequestsOnly = options ?. pullRequestsOnly ?? false ;
4254 const scope = useInboxReviewerScopeStore ( ( s ) => s . scope ) ;
4355 const searchQuery = useInboxSignalsFilterStore ( ( s ) =>
4456 ignoreFilters ? "" : s . searchQuery ,
@@ -69,6 +81,7 @@ export function useInboxAllReports(options?: {
6981 const query = useInboxReportsInfinite (
7082 {
7183 status : INBOX_PIPELINE_STATUS_FILTER ,
84+ has_implementation_pr : pullRequestsOnly ? true : undefined ,
7285 ordering : buildSignalReportListOrdering ( sortField , sortDirection ) ,
7386 source_product :
7487 sourceProductFilter . length > 0
@@ -90,6 +103,36 @@ export function useInboxAllReports(options?: {
90103 } ,
91104 ) ;
92105
106+ // True count of pull-request reports for the active scope. The infinite list
107+ // only holds the first page(s), so deriving pulls from loaded reports caps at
108+ // the page size and depends on ordering (a PR can sit past page 1). A cheap
109+ // `limit: 1` count query with the server-side `has_implementation_pr` filter
110+ // returns the real total regardless of page size.
111+ const pullRequestCountQuery = useInboxReports (
112+ {
113+ status : INBOX_PIPELINE_STATUS_FILTER ,
114+ has_implementation_pr : true ,
115+ // Mirror the list query's active filters so the badge matches the tab
116+ // body. These are empty when `ignoreFilters` is set (sidebar usage), so
117+ // the count stays scope-only there.
118+ source_product :
119+ sourceProductFilter . length > 0
120+ ? sourceProductFilter . join ( "," )
121+ : undefined ,
122+ priority : buildPriorityFilterParam ( priorityFilter ) ,
123+ suggested_reviewers : reviewerUuid
124+ ? buildSuggestedReviewerFilterParam ( [ reviewerUuid ] )
125+ : undefined ,
126+ limit : 1 ,
127+ } ,
128+ {
129+ enabled : ! isForYou || reviewerUuid != null ,
130+ refetchInterval : INBOX_REFETCH_INTERVAL_MS ,
131+ refetchIntervalInBackground : false ,
132+ } ,
133+ ) ;
134+ const pullRequestTotal = pullRequestCountQuery . data ?. count ?? 0 ;
135+
93136 const scopedReports = useMemo ( ( ) => {
94137 const byScope = ignoreScope
95138 ? query . allReports
@@ -99,10 +142,33 @@ export function useInboxAllReports(options?: {
99142 : byScope ;
100143 } , [ query . allReports , scope , searchQuery , ignoreScope ] ) ;
101144
102- const counts = useMemo (
103- ( ) => computeInboxTabCounts ( query . allReports , scope ) ,
104- [ query . allReports , scope ] ,
105- ) ;
145+ const counts = useMemo ( ( ) => {
146+ const loaded = computeInboxTabCounts ( query . allReports , scope ) ;
147+ // The list is an infinite query that only holds the pages loaded so far
148+ // (100 per page), so the loaded-derived Reports count caps at the page size
149+ // and reads as a misleading "100". Reports is the dominant bucket, so derive
150+ // its true size from the backend total `count` (unaffected by the page cap)
151+ // minus the non-report items the total also includes. PRs use the true
152+ // `pullRequestTotal` (also a real backend count), so PRs sitting past the
153+ // loaded page don't inflate Reports. Runs/failed without a PR stay
154+ // loaded-derived — small, newest-first, and an accepted approximation.
155+ const loadedOtherNonReport = query . allReports . filter (
156+ ( r ) =>
157+ matchesReviewerScope ( r , scope ) &&
158+ ! isExcludedFromInbox ( r ) &&
159+ ! isReportTabReport ( r ) &&
160+ ! isPullRequestReport ( r ) ,
161+ ) . length ;
162+ return {
163+ ...loaded ,
164+ // True backend counts, unaffected by the list's page-size cap.
165+ pulls : pullRequestTotal ,
166+ reports : Math . max (
167+ 0 ,
168+ query . totalCount - pullRequestTotal - loadedOtherNonReport ,
169+ ) ,
170+ } ;
171+ } , [ query . allReports , query . totalCount , scope , pullRequestTotal ] ) ;
106172
107173 return {
108174 ...query ,
0 commit comments