1+ import { useAtomValue } from "@effect/atom-react" ;
12import { sortProjectsByGroupOrder } from "@t3tools/client-runtime/state/project-grouping" ;
3+ import type { ScopedProjectRef , SidebarProjectGroupingMode } from "@t3tools/contracts" ;
4+ import { Atom } from "effect/reactivity" ;
25import { useMemo } from "react" ;
36import { Platform , ScrollView } from "react-native" ;
47import type { MenuProps } from "../../components/ui/Menu.types" ;
58import { ListFilterMenu } from "../workspace/ListFilterMenu" ;
6- import { usePublishPullRequestFilterMenu } from "./PullRequestFilterMenuContext " ;
7- import { useProjects , useThreadShells } from "../../state/entities " ;
9+ import { environmentProjects } from "../../state/projects " ;
10+ import { environmentThreadShells } from "../../state/threads " ;
811import { useMobileProjectGroupingSettings } from "../../state/project-grouping" ;
912import { buildHomeProjectScopes , sortHomeProjectScopes } from "../home/homeThreadList" ;
1013import { DEFAULT_PR_LIST_CONTROLS , type PullRequestListControls } from "./pullRequestList" ;
@@ -39,32 +42,58 @@ function choices(field: string, options: Readonly<Record<string, string>>, selec
3942 } ) ) ;
4043}
4144
45+ type ProjectOrder = readonly { readonly memberProjectRefs : readonly ScopedProjectRef [ ] } [ ] ;
46+ const sameProjectOrder = ( previous : ProjectOrder , next : ProjectOrder ) =>
47+ previous . length === next . length &&
48+ previous . every ( ( group , index ) => {
49+ const members = next [ index ] ! . memberProjectRefs ;
50+ return (
51+ group . memberProjectRefs . length === members . length &&
52+ group . memberProjectRefs . every (
53+ ( ref , member ) =>
54+ ref . environmentId === members [ member ] ! . environmentId &&
55+ ref . projectId === members [ member ] ! . projectId ,
56+ )
57+ ) ;
58+ } ) ;
59+
60+ // Thread shells change on every agent tick, but the sidebar order they decide rarely does. The
61+ // equality check keeps the list screen, which builds these filters, from re-rendering on each tick.
62+ const sidebarProjectOrderFamily = Atom . family ( ( mode : SidebarProjectGroupingMode ) =>
63+ Atom . make ( ( get ) =>
64+ sortHomeProjectScopes ( {
65+ scopes : buildHomeProjectScopes ( {
66+ projects : get ( environmentProjects . projectsAtom ) ,
67+ environmentId : null ,
68+ projectGroupingMode : mode ,
69+ } ) ,
70+ threads : get ( environmentThreadShells . threadShellsAtom ) ,
71+ pendingTasks : [ ] ,
72+ projectSortOrder : "updated_at" ,
73+ } ) . map ( ( scope ) => ( { memberProjectRefs : scope . projectRefs } ) ) ,
74+ ) . pipe ( Atom . withEquality ( sameProjectOrder ) ) ,
75+ ) ;
76+
4277function useProjectsInSidebarOrder < Project extends { id : string ; environmentId : string } > (
4378 projects : readonly Project [ ] ,
4479) {
45- const allProjects = useProjects ( ) ;
46- const threads = useThreadShells ( ) ;
4780 const { sidebarProjectGroupingMode } = useMobileProjectGroupingSettings ( ) ;
48- const scopes = useMemo (
49- ( ) =>
50- sortHomeProjectScopes ( {
51- scopes : buildHomeProjectScopes ( {
52- projects : allProjects ,
53- environmentId : null ,
54- projectGroupingMode : sidebarProjectGroupingMode ,
55- } ) ,
56- threads,
57- pendingTasks : [ ] ,
58- projectSortOrder : "updated_at" ,
59- } ) . map ( ( scope ) => ( { memberProjectRefs : scope . projectRefs } ) ) ,
60- [ allProjects , sidebarProjectGroupingMode , threads ] ,
61- ) ;
81+ const scopes = useAtomValue ( sidebarProjectOrderFamily ( sidebarProjectGroupingMode ) ) ;
6282 return useMemo ( ( ) => sortProjectsByGroupOrder ( projects , scopes ) , [ projects , scopes ] ) ;
6383}
6484
65- export function PullRequestListFilters ( props : {
66- /** Android sidebar rows sit further in than full-width rows; the pills follow them. */
67- readonly sidebar ?: boolean ;
85+ export interface PullRequestFilterMenu {
86+ readonly actions : MenuProps [ "actions" ] ;
87+ readonly onSelect : ( id : string ) => void ;
88+ /** Whether any filter, the environment, or a non-default sort is active. */
89+ readonly customized : boolean ;
90+ }
91+
92+ /**
93+ * Builds the filter pills and, on Android, the header's full filter menu. The list screen owns
94+ * the call so both the pills and the header it frames read the same menu.
95+ */
96+ export function usePullRequestListFilters ( props : {
6897 readonly environments : readonly { id : string ; label : string } [ ] ;
6998 readonly environmentId : string | null ;
7099 readonly onEnvironmentChange : ( id : string | null ) => void ;
@@ -280,7 +309,7 @@ export function PullRequestListFilters(props: {
280309 ] ;
281310 const customized = count > 0 || value . sort !== "updated" || props . environmentId !== null ;
282311 // The Android header's filter button opens every filter.
283- const headerMenu =
312+ const headerMenu : PullRequestFilterMenu | null =
284313 Platform . OS === "android"
285314 ? {
286315 actions : [
@@ -296,7 +325,35 @@ export function PullRequestListFilters(props: {
296325 customized,
297326 }
298327 : null ;
299- usePublishPullRequestFilterMenu ( headerMenu ) ;
328+ return {
329+ headerMenu,
330+ select,
331+ count,
332+ more,
333+ reset,
334+ environmentLabel :
335+ props . environments . length > 1
336+ ? ( props . environments . find ( ( environment ) => environment . id === props . environmentId )
337+ ?. label ?? "All environments" )
338+ : null ,
339+ environmentActions,
340+ state : value . state ,
341+ stateActions,
342+ involvement : value . involvement ,
343+ involvementActions,
344+ selectedLabelCount : selectedLabels . length ,
345+ labelActions,
346+ author,
347+ authorActions,
348+ } ;
349+ }
350+
351+ export function PullRequestListFilters ( props : {
352+ /** Android sidebar rows sit further in than full-width rows; the pills follow them. */
353+ readonly sidebar ?: boolean ;
354+ readonly filters : ReturnType < typeof usePullRequestListFilters > ;
355+ } ) {
356+ const { count, more, reset, select, author, selectedLabelCount } = props . filters ;
300357 return (
301358 < ScrollView
302359 horizontal
@@ -319,43 +376,40 @@ export function PullRequestListFilters(props: {
319376 onSelect = { select }
320377 />
321378 ) }
322- { props . environments . length > 1 ? (
379+ { props . filters . environmentLabel !== null ? (
323380 < ListFilterMenu
324- label = {
325- props . environments . find ( ( environment ) => environment . id === props . environmentId )
326- ?. label ?? "All environments"
327- }
381+ label = { props . filters . environmentLabel }
328382 accessibilityLabel = "Choose pull request environment"
329- actions = { environmentActions }
383+ actions = { props . filters . environmentActions }
330384 onSelect = { select }
331385 />
332386 ) : null }
333387 < ListFilterMenu
334- label = { STATES [ value . state ] }
335- active = { value . state !== "open" }
388+ label = { STATES [ props . filters . state ] }
389+ active = { props . filters . state !== "open" }
336390 accessibilityLabel = "Filter by state"
337- actions = { stateActions }
391+ actions = { props . filters . stateActions }
338392 onSelect = { select }
339393 />
340394 < ListFilterMenu
341- label = { INVOLVEMENT [ value . involvement ] }
342- active = { value . involvement !== "all" }
395+ label = { INVOLVEMENT [ props . filters . involvement ] }
396+ active = { props . filters . involvement !== "all" }
343397 accessibilityLabel = "Filter by involvement"
344- actions = { involvementActions }
398+ actions = { props . filters . involvementActions }
345399 onSelect = { select }
346400 />
347401 < ListFilterMenu
348- label = { selectedLabels . length ? `Labels ${ selectedLabels . length } ` : "Labels" }
349- active = { selectedLabels . length > 0 }
402+ label = { selectedLabelCount ? `Labels ${ selectedLabelCount } ` : "Labels" }
403+ active = { selectedLabelCount > 0 }
350404 accessibilityLabel = "Filter by label"
351- actions = { labelActions }
405+ actions = { props . filters . labelActions }
352406 onSelect = { select }
353407 />
354408 < ListFilterMenu
355409 label = { author === "me" ? "By you" : ( author ?? "Author" ) }
356410 active = { Boolean ( author ) }
357411 accessibilityLabel = "Filter by author"
358- actions = { authorActions }
412+ actions = { props . filters . authorActions }
359413 onSelect = { select }
360414 />
361415 </ ScrollView >
0 commit comments