| author | |
| committer | |
| log | c32ade41390be10783246fbbeed45de7e3add79b |
| tree | 0d5f8b3391f6e3268efe979ec5952f88859cae1f |
| parent | b9ad3daf8d489604e0eca9d32a7e08af7c4e9fb7 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>21 files changed, 326 insertions(+), 144 deletions(-)
apps/mobile/deps/react-native-screens-5.0.0-t3.25.tgz deleted| Binary files a/apps/mobile/deps/react-native-screens-5.0.0-t3.25.tgz and /dev/null differ |
apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz created| Binary files /dev/null and b/apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz differ |
apps/mobile/package.json+1-1| ... | ... | @@ -126,7 +126,7 @@ |
| 126 | 126 | "react-native-keyboard-controller": "1.22.6", |
| 127 | 127 | "react-native-reanimated": "4.7.0", |
| 128 | 128 | "react-native-safe-area-context": "~5.9.1", |
| 129 | "react-native-screens": "file:deps/react-native-screens-5.0.0-t3.25.tgz", | |
| 129 | "react-native-screens": "file:deps/react-native-screens-5.0.0-t3.26.tgz", | |
| 130 | 130 | "react-native-shiki-engine": "^0.3.12", |
| 131 | 131 | "react-native-svg": "15.15.5", |
| 132 | 132 | "react-native-webview": "14.0.1", |
apps/mobile/src/features/home/HomeRouteScreen.tsx+10-12| ... | ... | @@ -127,15 +127,15 @@ export function HomeRouteScreen() { |
| 127 | 127 | } = useHomeListOptions(availableEnvironmentIds); |
| 128 | 128 | const selectedEnvironmentId = listOptions.selectedEnvironmentId; |
| 129 | 129 | const projectScopeKey = listOptions.selectedProjectKey; |
| 130 | const selectedProjectKey = resolveScratchProjectScopeKey(projectScopeKey, projects, { | |
| 130 | const resolvedProjectKey = resolveScratchProjectScopeKey(projectScopeKey, projects, { | |
| 131 | 131 | sidebarProjectGroupingMode: listOptions.projectGroupingMode, |
| 132 | 132 | sidebarProjectGroupingOverrides: {}, |
| 133 | 133 | }); |
| 134 | 134 | useEffect(() => { |
| 135 | if (selectedProjectKey !== projectScopeKey) { | |
| 136 | setSelectedProjectKey(selectedProjectKey); | |
| 135 | if (resolvedProjectKey !== projectScopeKey) { | |
| 136 | setSelectedProjectKey(resolvedProjectKey); | |
| 137 | 137 | } |
| 138 | }, [projectScopeKey, selectedProjectKey, setSelectedProjectKey]); | |
| 138 | }, [projectScopeKey, resolvedProjectKey, setSelectedProjectKey]); | |
| 139 | 139 | const projectFilterOptions = useMemo( |
| 140 | 140 | () => |
| 141 | 141 | buildHomeProjectScopes({ |
| ... | ... | @@ -148,14 +148,12 @@ export function HomeRouteScreen() { |
| 148 | 148 | })), |
| 149 | 149 | [listOptions.projectGroupingMode, projects, selectedEnvironmentId], |
| 150 | 150 | ); |
| 151 | useEffect(() => { | |
| 152 | if ( | |
| 153 | selectedProjectKey !== null && | |
| 154 | !projectFilterOptions.some((project) => project.key === selectedProjectKey) | |
| 155 | ) { | |
| 156 | setSelectedProjectKey(null); | |
| 157 | } | |
| 158 | }, [projectFilterOptions, selectedProjectKey, setSelectedProjectKey]); | |
| 151 | // A saved project stays selected while its environment is offline or filtered out. | |
| 152 | const selectedProjectKey = projectFilterOptions.some( | |
| 153 | (project) => project.key === resolvedProjectKey, | |
| 154 | ) | |
| 155 | ? resolvedProjectKey | |
| 156 | : null; | |
| 159 | 157 | |
| 160 | 158 | // In split layouts the persistent sidebar IS the thread list — Home becomes |
| 161 | 159 | // an empty detail pane so selecting a thread never transitions layouts. |
apps/mobile/src/features/home/ThreadListFilters.tsx+2-2| ... | ... | @@ -24,7 +24,7 @@ export function ThreadListFilters( |
| 24 | 24 | <ScrollView |
| 25 | 25 | horizontal |
| 26 | 26 | showsHorizontalScrollIndicator={false} |
| 27 | contentContainerStyle={{ paddingHorizontal: 16, gap: 6, paddingBottom: 8 }} | |
| 27 | contentContainerStyle={{ paddingHorizontal: 16, paddingTop: 2, paddingBottom: 10, gap: 8 }} | |
| 28 | 28 | > |
| 29 | 29 | <ListFilterMenu |
| 30 | 30 | label={selectedEnvironment?.label ?? "All environments"} |
| ... | ... | @@ -85,7 +85,7 @@ export function ThreadListFilters( |
| 85 | 85 | <Pressable |
| 86 | 86 | accessibilityRole="button" |
| 87 | 87 | accessibilityLabel="Reset thread filters" |
| 88 | className="h-11 w-8 items-center justify-center" | |
| 88 | className="h-8 w-8 items-center justify-center" | |
| 89 | 89 | onPress={() => { |
| 90 | 90 | props.onEnvironmentChange(null); |
| 91 | 91 | props.onProjectChange(null); |
apps/mobile/src/features/home/home-list-options.test.ts+40-5| ... | ... | @@ -1,8 +1,24 @@ |
| 1 | import { RegistryContext } from "@effect/atom-react"; | |
| 1 | 2 | import { EnvironmentId } from "@t3tools/contracts"; |
| 3 | import { AtomRegistry } from "effect/reactivity"; | |
| 2 | 4 | import { act, createElement, useLayoutEffect } from "react"; |
| 3 | 5 | import { createRoot, type Root } from "react-dom/client"; |
| 4 | 6 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 5 | 7 | |
| 8 | const saved = vi.hoisted(() => [] as unknown[]); | |
| 9 | vi.mock("../../state/preferences", async () => { | |
| 10 | const { Atom } = await import("effect/reactivity"); | |
| 11 | return { | |
| 12 | updateMobilePreferencesAtom: Atom.writable( | |
| 13 | () => null, | |
| 14 | (_ctx, patch: unknown) => { | |
| 15 | saved.push(patch); | |
| 16 | }, | |
| 17 | ), | |
| 18 | }; | |
| 19 | }); | |
| 20 | ||
| 21 | import type { ThreadListFilters } from "../../persistence/mobile-preferences"; | |
| 6 | 22 | import { HomeListOptionsProvider, useHomeListOptions } from "./home-list-options"; |
| 7 | 23 | |
| 8 | 24 | describe("home list options across layout changes", () => { |
| ... | ... | @@ -19,14 +35,20 @@ describe("home list options across layout changes", () => { |
| 19 | 35 | return null; |
| 20 | 36 | } |
| 21 | 37 | |
| 22 | async function showLayout(layout: "compact" | "sidebar") { | |
| 38 | const registry = AtomRegistry.make(); | |
| 39 | ||
| 40 | async function showLayout(layout: "compact" | "sidebar", savedFilters?: ThreadListFilters) { | |
| 23 | 41 | await act(() => { |
| 24 | 42 | root.render( |
| 25 | 43 | createElement( |
| 26 | HomeListOptionsProvider, | |
| 27 | { projectGroupingMode: "repository" }, | |
| 28 | // Replace the list while keeping the workspace provider mounted. | |
| 29 | createElement(ThreadList, { key: layout }), | |
| 44 | RegistryContext, | |
| 45 | { value: registry }, | |
| 46 | createElement( | |
| 47 | HomeListOptionsProvider, | |
| 48 | { projectGroupingMode: "repository", savedFilters }, | |
| 49 | // Replace the list while keeping the workspace provider mounted. | |
| 50 | createElement(ThreadList, { key: layout }), | |
| 51 | ), | |
| 30 | 52 | ), |
| 31 | 53 | ); |
| 32 | 54 | }); |
| ... | ... | @@ -51,6 +73,7 @@ describe("home list options across layout changes", () => { |
| 51 | 73 | vi.stubGlobal("window", { document, HTMLIFrameElement: EventTarget }); |
| 52 | 74 | vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true); |
| 53 | 75 | root = createRoot(container as unknown as HTMLElement); |
| 76 | saved.length = 0; | |
| 54 | 77 | }); |
| 55 | 78 | |
| 56 | 79 | afterEach(async () => { |
| ... | ... | @@ -92,4 +115,16 @@ describe("home list options across layout changes", () => { |
| 92 | 115 | } |
| 93 | 116 | }, |
| 94 | 117 | ); |
| 118 | ||
| 119 | it("restores saved filters on launch and saves only real changes", async () => { | |
| 120 | await showLayout("compact", { environmentId, projectKey: "repository:one" }); | |
| 121 | expect(latest.options).toMatchObject({ | |
| 122 | selectedEnvironmentId: environmentId, | |
| 123 | selectedProjectKey: "repository:one", | |
| 124 | }); | |
| 125 | expect(saved).toEqual([]); | |
| 126 | ||
| 127 | await act(() => latest.setSelectedProjectKey(null)); | |
| 128 | expect(saved).toEqual([{ threadListFilters: { environmentId, projectKey: null } }]); | |
| 129 | }); | |
| 95 | 130 | }); |
apps/mobile/src/features/home/home-list-options.ts+27-5| ... | ... | @@ -1,17 +1,22 @@ |
| 1 | 1 | import type { EnvironmentId, SidebarProjectGroupingMode } from "@t3tools/contracts"; |
| 2 | 2 | import { DEFAULT_SIDEBAR_PROJECT_SORT_ORDER } from "@t3tools/contracts"; |
| 3 | import { useAtomSet } from "@effect/atom-react"; | |
| 3 | 4 | import { |
| 4 | 5 | createContext, |
| 5 | 6 | createElement, |
| 6 | 7 | useCallback, |
| 7 | 8 | useContext, |
| 9 | useEffect, | |
| 8 | 10 | useMemo, |
| 11 | useRef, | |
| 9 | 12 | useState, |
| 10 | 13 | type PropsWithChildren, |
| 11 | 14 | type Dispatch, |
| 12 | 15 | type SetStateAction, |
| 13 | 16 | } from "react"; |
| 14 | 17 | |
| 18 | import type { ThreadListFilters } from "../../persistence/mobile-preferences"; | |
| 19 | import { updateMobilePreferencesAtom } from "../../state/preferences"; | |
| 15 | 20 | import type { HomeProjectSortOrder } from "./homeThreadList"; |
| 16 | 21 | |
| 17 | 22 | export interface HomeListOptions { |
| ... | ... | @@ -24,10 +29,10 @@ export interface ResolvedHomeListOptions extends HomeListOptions { |
| 24 | 29 | readonly projectGroupingMode: SidebarProjectGroupingMode; |
| 25 | 30 | } |
| 26 | 31 | |
| 27 | function defaultHomeListOptions(): HomeListOptions { | |
| 32 | function defaultHomeListOptions(filters?: ThreadListFilters): HomeListOptions { | |
| 28 | 33 | return { |
| 29 | selectedEnvironmentId: null, | |
| 30 | selectedProjectKey: null, | |
| 34 | selectedEnvironmentId: filters?.environmentId ?? null, | |
| 35 | selectedProjectKey: filters?.projectKey ?? null, | |
| 31 | 36 | projectSortOrder: |
| 32 | 37 | DEFAULT_SIDEBAR_PROJECT_SORT_ORDER === "manual" |
| 33 | 38 | ? "updated_at" |
| ... | ... | @@ -43,14 +48,31 @@ interface HomeListOptionsContextValue { |
| 43 | 48 | |
| 44 | 49 | const HomeListOptionsContext = createContext<HomeListOptionsContextValue | null>(null); |
| 45 | 50 | |
| 46 | /** Keeps list preferences stable while the app moves between compact and split shells. */ | |
| 51 | /** | |
| 52 | * Keeps list preferences stable while the app moves between compact and split shells. | |
| 53 | * Filters start from the saved preference, and changes are saved for the next launch. | |
| 54 | */ | |
| 47 | 55 | export function HomeListOptionsProvider({ |
| 48 | 56 | children, |
| 49 | 57 | projectGroupingMode, |
| 58 | savedFilters, | |
| 50 | 59 | }: PropsWithChildren<{ |
| 51 | 60 | readonly projectGroupingMode: SidebarProjectGroupingMode; |
| 61 | readonly savedFilters?: ThreadListFilters | undefined; | |
| 52 | 62 | }>) { |
| 53 | const [options, setOptions] = useState<HomeListOptions>(defaultHomeListOptions); | |
| 63 | const [options, setOptions] = useState(() => defaultHomeListOptions(savedFilters)); | |
| 64 | const savePreferences = useAtomSet(updateMobilePreferencesAtom); | |
| 65 | const saved = useRef(savedFilters); | |
| 66 | const { selectedEnvironmentId: environmentId, selectedProjectKey: projectKey } = options; | |
| 67 | useEffect(() => { | |
| 68 | if ( | |
| 69 | (saved.current?.environmentId ?? null) === environmentId && | |
| 70 | (saved.current?.projectKey ?? null) === projectKey | |
| 71 | ) | |
| 72 | return; | |
| 73 | saved.current = { environmentId, projectKey }; | |
| 74 | savePreferences({ threadListFilters: saved.current }); | |
| 75 | }, [environmentId, projectKey, savePreferences]); | |
| 54 | 76 | const value = useMemo( |
| 55 | 77 | () => ({ options, setOptions, projectGroupingMode }), |
| 56 | 78 | [options, projectGroupingMode], |
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+11-2| ... | ... | @@ -46,6 +46,7 @@ import { |
| 46 | 46 | resolveThreadSelectionOverlayState, |
| 47 | 47 | } from "../../lib/adaptive-navigation"; |
| 48 | 48 | import { scopedThreadKey } from "../../lib/scopedEntities"; |
| 49 | import type { ThreadListFilters } from "../../persistence/mobile-preferences"; | |
| 49 | 50 | import { mobilePreferencesAtom } from "../../state/preferences"; |
| 50 | 51 | import { |
| 51 | 52 | DEFAULT_MOBILE_PROJECT_GROUPING_SETTINGS, |
| ... | ... | @@ -234,6 +235,7 @@ export function AdaptiveWorkspaceLayout(props: { |
| 234 | 235 | <AdaptiveWorkspaceLayoutContent |
| 235 | 236 | {...props} |
| 236 | 237 | projectGroupingMode={groupingSettings.sidebarProjectGroupingMode} |
| 238 | savedThreadListFilters={preferencesResult.value.threadListFilters} | |
| 237 | 239 | /> |
| 238 | 240 | ); |
| 239 | 241 | } |
| ... | ... | @@ -245,6 +247,7 @@ function AdaptiveWorkspaceLayoutContent( |
| 245 | 247 | readonly workspaceRouteKey: string | undefined; |
| 246 | 248 | } & { |
| 247 | 249 | readonly projectGroupingMode: SidebarProjectGroupingMode; |
| 250 | readonly savedThreadListFilters?: ThreadListFilters | undefined; | |
| 248 | 251 | }, |
| 249 | 252 | ) { |
| 250 | 253 | const projectGroupingMode = props.projectGroupingMode; |
| ... | ... | @@ -680,7 +683,10 @@ function AdaptiveWorkspaceLayoutContent( |
| 680 | 683 | |
| 681 | 684 | if (nativeWorkspace) { |
| 682 | 685 | return ( |
| 683 | <HomeListOptionsProvider projectGroupingMode={projectGroupingMode}> | |
| 686 | <HomeListOptionsProvider | |
| 687 | projectGroupingMode={projectGroupingMode} | |
| 688 | savedFilters={props.savedThreadListFilters} | |
| 689 | > | |
| 684 | 690 | <AdaptiveWorkspaceContext value={contextValue}> |
| 685 | 691 | <NativeWorkspaceInspectorContext |
| 686 | 692 | value={{ render: workspaceInspector?.render, visible: inspectorColumnTargetWidth > 0 }} |
| ... | ... | @@ -692,7 +698,10 @@ function AdaptiveWorkspaceLayoutContent( |
| 692 | 698 | ); |
| 693 | 699 | } |
| 694 | 700 | return ( |
| 695 | <HomeListOptionsProvider projectGroupingMode={projectGroupingMode}> | |
| 701 | <HomeListOptionsProvider | |
| 702 | projectGroupingMode={projectGroupingMode} | |
| 703 | savedFilters={props.savedThreadListFilters} | |
| 704 | > | |
| 696 | 705 | <AdaptiveWorkspaceContext.Provider value={contextValue}> |
| 697 | 706 | <View |
| 698 | 707 | testID="adaptive-workspace-layout" |
apps/mobile/src/features/pull-requests/PullRequestListFilters.tsx+1-1| ... | ... | @@ -196,7 +196,7 @@ export function PullRequestListFilters(props: { |
| 196 | 196 | <ScrollView |
| 197 | 197 | horizontal |
| 198 | 198 | showsHorizontalScrollIndicator={false} |
| 199 | contentContainerStyle={{ paddingHorizontal: 16, gap: 6, paddingBottom: 8 }} | |
| 199 | contentContainerStyle={{ paddingHorizontal: 16, paddingTop: 2, paddingBottom: 10, gap: 8 }} | |
| 200 | 200 | > |
| 201 | 201 | <ListFilterMenu |
| 202 | 202 | label={count ? String(count) : "Filters"} |
apps/mobile/src/features/pull-requests/PullRequestsScreen.tsx+8-8| ... | ... | @@ -2,7 +2,7 @@ import { LegendList, type LegendListRef } from "@legendapp/list/react-native"; |
| 2 | 2 | import { useNavigation } from "@react-navigation/native"; |
| 3 | 3 | import { use, useCallback, useEffect, useMemo, useRef, useState } from "react"; |
| 4 | 4 | import { ProjectId } from "@t3tools/contracts"; |
| 5 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; | |
| 5 | import { useAtomValue } from "@effect/atom-react"; | |
| 6 | 6 | import { |
| 7 | 7 | ActivityIndicator, |
| 8 | 8 | Keyboard, |
| ... | ... | @@ -24,7 +24,8 @@ import { useProjects } from "../../state/entities"; |
| 24 | 24 | import { environmentServerConfigsAtom } from "../../state/server"; |
| 25 | 25 | import { PullRequestListRow } from "./PullRequestListRow"; |
| 26 | 26 | import { PullRequestListFilters } from "./PullRequestListFilters"; |
| 27 | import { pullRequestListControlsAtom, usePullRequestList } from "./usePullRequestList"; | |
| 27 | import { usePullRequestList } from "./usePullRequestList"; | |
| 28 | import { usePullRequestListControls } from "./usePullRequestListControls"; | |
| 28 | 29 | import { usePullRequestListActions } from "./usePullRequestListActions"; |
| 29 | 30 | import { |
| 30 | 31 | filterPullRequestList, |
| ... | ... | @@ -74,7 +75,8 @@ export function PullRequestsScreen() { |
| 74 | 75 | const debouncedQuery = useDebouncedValue(tab.query.trim(), 180); |
| 75 | 76 | // Dismissal must restore the cached unfiltered list before restoring its offset. |
| 76 | 77 | const query = tab.searchMode === "resting" ? "" : debouncedQuery; |
| 77 | const selectedEnvironmentId = workspace.pullRequestEnvironmentId; | |
| 78 | const listControls = usePullRequestListControls(); | |
| 79 | const selectedEnvironmentId = listControls.environmentId; | |
| 78 | 80 | const environmentId = environments.some((item) => item.environmentId === selectedEnvironmentId) |
| 79 | 81 | ? selectedEnvironmentId |
| 80 | 82 | : null; |
| ... | ... | @@ -95,9 +97,7 @@ export function PullRequestsScreen() { |
| 95 | 97 | }, |
| 96 | 98 | [navigation, selectAndroidWorkspace], |
| 97 | 99 | ); |
| 98 | const controlsAtom = pullRequestListControlsAtom(environmentId ?? ""); | |
| 99 | const controls = useAtomValue(controlsAtom); | |
| 100 | const setControls = useAtomSet(controlsAtom); | |
| 100 | const controls = listControls.controlsFor(environmentId); | |
| 101 | 101 | const targets = useMemo( |
| 102 | 102 | () => |
| 103 | 103 | environments |
| ... | ... | @@ -269,10 +269,10 @@ export function PullRequestsScreen() { |
| 269 | 269 | label: item.environmentLabel, |
| 270 | 270 | }))} |
| 271 | 271 | environmentId={environmentId} |
| 272 | onEnvironmentChange={workspace.setPullRequestEnvironmentId} | |
| 272 | onEnvironmentChange={listControls.setEnvironmentId} | |
| 273 | 273 | value={controls} |
| 274 | 274 | onChange={(next) => { |
| 275 | setControls(next); | |
| 275 | listControls.setControls(environmentId, next); | |
| 276 | 276 | void listRef.current?.scrollToOffset({ |
| 277 | 277 | offset: Platform.OS === "ios" ? -insets.top : 0, |
| 278 | 278 | animated: false, |
apps/mobile/src/features/pull-requests/pullRequestList.ts+2-11| ... | ... | @@ -1,22 +1,13 @@ |
| 1 | 1 | import type { |
| 2 | 2 | EnvironmentId, |
| 3 | 3 | PullRequestListEntry, |
| 4 | PullRequestListFilters, | |
| 5 | 4 | PullRequestListInput, |
| 6 | 5 | PullRequestListResult, |
| 7 | PullRequestInvolvement, | |
| 8 | PullRequestListState, | |
| 9 | 6 | } from "@t3tools/contracts"; |
| 7 | import type { PullRequestListControls } from "../../persistence/mobile-preferences"; | |
| 10 | 8 | |
| 11 | 9 | export const PR_LIST_PAGE_SIZE = 30; |
| 12 | export interface PullRequestListControls { | |
| 13 | readonly state: PullRequestListState; | |
| 14 | readonly involvement: PullRequestInvolvement; | |
| 15 | readonly filters: PullRequestListFilters; | |
| 16 | readonly projectId?: string; | |
| 17 | readonly projectEnvironmentId?: string; | |
| 18 | readonly sort: "updated" | "created" | "oldest"; | |
| 19 | } | |
| 10 | export type { PullRequestListControls } from "../../persistence/mobile-preferences"; | |
| 20 | 11 | export const DEFAULT_PR_LIST_CONTROLS: PullRequestListControls = { |
| 21 | 12 | state: "open", |
| 22 | 13 | involvement: "all", |
apps/mobile/src/features/pull-requests/usePullRequestList.ts-5| ... | ... | @@ -8,7 +8,6 @@ import { use, useCallback, useEffect, useMemo, useRef, useState, useLayoutEffect |
| 8 | 8 | import { composerPullRequests } from "../../state/pull-requests"; |
| 9 | 9 | import { |
| 10 | 10 | appendPullRequestListPage, |
| 11 | DEFAULT_PR_LIST_CONTROLS, | |
| 12 | 11 | mergeEnvironmentPullRequestPages, |
| 13 | 12 | nextPullRequestListInput, |
| 14 | 13 | pullRequestListHead, |
| ... | ... | @@ -28,10 +27,6 @@ const statsFamily = Atom.family((_environmentId: string) => |
| 28 | 27 | const statKey = (row: Pick<PullRequestDiffStat, "projectId" | "repository" | "number">) => |
| 29 | 28 | `${row.projectId}:${row.repository}:${row.number}`; |
| 30 | 29 | |
| 31 | export const pullRequestListControlsAtom = Atom.family((_environmentId: string) => | |
| 32 | Atom.make(DEFAULT_PR_LIST_CONTROLS), | |
| 33 | ); | |
| 34 | ||
| 35 | 30 | type ListTarget = { readonly environmentId: EnvironmentId; readonly input: PullRequestListInput }; |
| 36 | 31 | const sourcesFamily = Atom.family((key: string) => { |
| 37 | 32 | const targets: readonly ListTarget[] = JSON.parse(key); |
apps/mobile/src/features/pull-requests/usePullRequestListControls.ts created+48| ... | ... | @@ -0,0 +1,48 @@ |
| 1 | import { useAtomSet, useAtomValue } from "@effect/atom-react"; | |
| 2 | import { AsyncResult } from "effect/reactivity"; | |
| 3 | import { useCallback } from "react"; | |
| 4 | ||
| 5 | import { mobilePreferencesAtom, updateMobilePreferencesAtom } from "../../state/preferences"; | |
| 6 | import { DEFAULT_PR_LIST_CONTROLS, type PullRequestListControls } from "./pullRequestList"; | |
| 7 | ||
| 8 | /** The pull request list's environment and its filters, restored on the next launch. */ | |
| 9 | export function usePullRequestListControls() { | |
| 10 | const preferences = useAtomValue(mobilePreferencesAtom); | |
| 11 | const savePreferences = useAtomSet(updateMobilePreferencesAtom); | |
| 12 | const saved = AsyncResult.isSuccess(preferences) | |
| 13 | ? preferences.value.pullRequestListFilters | |
| 14 | : undefined; | |
| 15 | const environmentId = saved?.environmentId ?? null; | |
| 16 | const controlsFor = useCallback( | |
| 17 | (environment: string | null) => saved?.controls[environment ?? ""] ?? DEFAULT_PR_LIST_CONTROLS, | |
| 18 | [saved], | |
| 19 | ); | |
| 20 | const setEnvironmentId = useCallback( | |
| 21 | (next: string | null) => | |
| 22 | savePreferences({ | |
| 23 | transform: (current) => ({ | |
| 24 | pullRequestListFilters: { | |
| 25 | controls: current.pullRequestListFilters?.controls ?? {}, | |
| 26 | environmentId: next, | |
| 27 | }, | |
| 28 | }), | |
| 29 | }), | |
| 30 | [savePreferences], | |
| 31 | ); | |
| 32 | const setControls = useCallback( | |
| 33 | (environment: string | null, controls: PullRequestListControls) => | |
| 34 | savePreferences({ | |
| 35 | transform: (current) => ({ | |
| 36 | pullRequestListFilters: { | |
| 37 | environmentId: current.pullRequestListFilters?.environmentId ?? null, | |
| 38 | controls: { | |
| 39 | ...current.pullRequestListFilters?.controls, | |
| 40 | [environment ?? ""]: controls, | |
| 41 | }, | |
| 42 | }, | |
| 43 | }), | |
| 44 | }), | |
| 45 | [savePreferences], | |
| 46 | ); | |
| 47 | return { environmentId, controlsFor, setEnvironmentId, setControls } as const; | |
| 48 | } |
apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx+8-17| ... | ... | @@ -208,15 +208,15 @@ function ThreadNavigationSidebarPane( |
| 208 | 208 | [threadSearch.matches], |
| 209 | 209 | ); |
| 210 | 210 | const projectScopeKey = options.selectedProjectKey; |
| 211 | const selectedProjectKey = resolveScratchProjectScopeKey(projectScopeKey, projects, { | |
| 211 | const resolvedProjectKey = resolveScratchProjectScopeKey(projectScopeKey, projects, { | |
| 212 | 212 | sidebarProjectGroupingMode: options.projectGroupingMode, |
| 213 | 213 | sidebarProjectGroupingOverrides: {}, |
| 214 | 214 | }); |
| 215 | 215 | useEffect(() => { |
| 216 | if (selectedProjectKey !== projectScopeKey) { | |
| 217 | setSelectedProjectKey(selectedProjectKey); | |
| 216 | if (resolvedProjectKey !== projectScopeKey) { | |
| 217 | setSelectedProjectKey(resolvedProjectKey); | |
| 218 | 218 | } |
| 219 | }, [projectScopeKey, selectedProjectKey, setSelectedProjectKey]); | |
| 219 | }, [projectScopeKey, resolvedProjectKey, setSelectedProjectKey]); | |
| 220 | 220 | const projectScopes = useMemo( |
| 221 | 221 | () => |
| 222 | 222 | buildHomeProjectScopes({ |
| ... | ... | @@ -250,20 +250,11 @@ function ThreadNavigationSidebarPane( |
| 250 | 250 | [projectScopes], |
| 251 | 251 | ); |
| 252 | 252 | const selectedProjectScope = useMemo( |
| 253 | () => | |
| 254 | selectedProjectKey === null | |
| 255 | ? null | |
| 256 | : (projectScopes.find((scope) => scope.key === selectedProjectKey) ?? null), | |
| 257 | [projectScopes, selectedProjectKey], | |
| 253 | () => projectScopes.find((scope) => scope.key === resolvedProjectKey) ?? null, | |
| 254 | [projectScopes, resolvedProjectKey], | |
| 258 | 255 | ); |
| 259 | useEffect(() => { | |
| 260 | if ( | |
| 261 | selectedProjectKey !== null && | |
| 262 | !projectFilterOptions.some((project) => project.key === selectedProjectKey) | |
| 263 | ) { | |
| 264 | setSelectedProjectKey(null); | |
| 265 | } | |
| 266 | }, [projectFilterOptions, selectedProjectKey, setSelectedProjectKey]); | |
| 256 | // A saved project stays selected while its environment is offline or filtered out. | |
| 257 | const selectedProjectKey = selectedProjectScope?.key ?? null; | |
| 267 | 258 | const selectedProjectRefs = useMemo( |
| 268 | 259 | () => |
| 269 | 260 | selectedProjectScope === null |
apps/mobile/src/features/workspace/ListFilterMenu.tsx+51-30| ... | ... | @@ -1,9 +1,20 @@ |
| 1 | import { GlassView } from "expo-glass-effect"; | |
| 1 | 2 | import { Pressable, View } from "react-native"; |
| 3 | import { withUniwind } from "uniwind"; | |
| 2 | 4 | import { AppText as Text } from "../../components/AppText"; |
| 3 | 5 | import { SymbolView } from "../../components/AppSymbol"; |
| 4 | 6 | import { ControlPillMenu } from "../../components/ControlPillMenu"; |
| 5 | 7 | import type { ControlPillMenuProps } from "../../components/ControlPillMenu.types"; |
| 6 | 8 | import { cn } from "../../lib/cn"; |
| 9 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; | |
| 10 | import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; | |
| 11 | ||
| 12 | const FilterGlass = withUniwind(GlassView, { | |
| 13 | style: { fromClassName: "className" }, | |
| 14 | tintColor: { fromClassName: "tintColorClassName", styleProperty: "accentColor" }, | |
| 15 | }); | |
| 16 | ||
| 17 | const PILL = "h-8 flex-row items-center gap-1.5 rounded-full px-3"; | |
| 7 | 18 | |
| 8 | 19 | /** A visible filter with a native menu containing only that filter's choices. */ |
| 9 | 20 | export function ListFilterMenu(props: { |
| ... | ... | @@ -14,6 +25,34 @@ export function ListFilterMenu(props: { |
| 14 | 25 | readonly actions: ControlPillMenuProps["actions"]; |
| 15 | 26 | readonly onSelect: (id: string) => void; |
| 16 | 27 | }) { |
| 28 | const { themeAppearance } = useAppearancePreferences(); | |
| 29 | const content = ( | |
| 30 | <> | |
| 31 | {props.icon ? ( | |
| 32 | <SymbolView | |
| 33 | name="line.3.horizontal.decrease" | |
| 34 | size={15} | |
| 35 | tintColorClassName={props.active ? "accent-primary-foreground" : "accent-icon-muted"} | |
| 36 | /> | |
| 37 | ) : null} | |
| 38 | <Text | |
| 39 | numberOfLines={1} | |
| 40 | className={cn( | |
| 41 | "max-w-[180px] text-sm font-t3-medium", | |
| 42 | props.active ? "text-primary-foreground" : "text-foreground-muted", | |
| 43 | )} | |
| 44 | > | |
| 45 | {props.label} | |
| 46 | </Text> | |
| 47 | {!props.icon ? ( | |
| 48 | <SymbolView | |
| 49 | name="chevron.down" | |
| 50 | size={11} | |
| 51 | tintColorClassName={props.active ? "accent-primary-foreground" : "accent-icon-muted"} | |
| 52 | /> | |
| 53 | ) : null} | |
| 54 | </> | |
| 55 | ); | |
| 17 | 56 | return ( |
| 18 | 57 | <ControlPillMenu |
| 19 | 58 | title={props.accessibilityLabel} |
| ... | ... | @@ -23,38 +62,20 @@ export function ListFilterMenu(props: { |
| 23 | 62 | <Pressable |
| 24 | 63 | accessibilityRole="button" |
| 25 | 64 | accessibilityLabel={props.accessibilityLabel} |
| 26 | className="h-11 justify-center" | |
| 65 | hitSlop={6} | |
| 27 | 66 | > |
| 28 | <View | |
| 29 | className={cn( | |
| 30 | "h-8 flex-row items-center gap-1.5 rounded-full px-3", | |
| 31 | props.active ? "bg-primary" : "bg-subtle", | |
| 32 | )} | |
| 33 | > | |
| 34 | {props.icon ? ( | |
| 35 | <SymbolView | |
| 36 | name="line.3.horizontal.decrease" | |
| 37 | size={15} | |
| 38 | tintColorClassName={props.active ? "accent-primary-foreground" : "accent-icon-muted"} | |
| 39 | /> | |
| 40 | ) : null} | |
| 41 | <Text | |
| 42 | numberOfLines={1} | |
| 43 | className={cn( | |
| 44 | "max-w-[180px] text-xs font-t3-medium", | |
| 45 | props.active ? "text-primary-foreground" : "text-foreground-muted", | |
| 46 | )} | |
| 67 | {NATIVE_LIQUID_GLASS_SUPPORTED ? ( | |
| 68 | <FilterGlass | |
| 69 | isInteractive | |
| 70 | colorScheme={themeAppearance} | |
| 71 | tintColorClassName={props.active ? "accent-primary" : undefined} | |
| 72 | className={PILL} | |
| 47 | 73 | > |
| 48 | {props.label} | |
| 49 | </Text> | |
| 50 | {!props.icon ? ( | |
| 51 | <SymbolView | |
| 52 | name="chevron.down" | |
| 53 | size={10} | |
| 54 | tintColorClassName={props.active ? "accent-primary-foreground" : "accent-icon-muted"} | |
| 55 | /> | |
| 56 | ) : null} | |
| 57 | </View> | |
| 74 | {content} | |
| 75 | </FilterGlass> | |
| 76 | ) : ( | |
| 77 | <View className={cn(PILL, props.active ? "bg-primary" : "bg-subtle")}>{content}</View> | |
| 78 | )} | |
| 58 | 79 | </Pressable> |
| 59 | 80 | </ControlPillMenu> |
| 60 | 81 | ); |
apps/mobile/src/features/workspace/WorkspaceNativeTabs.ios.tsx+34-10| ... | ... | @@ -1,7 +1,12 @@ |
| 1 | 1 | import { useNativeLayoutMetrics } from "../../native/native-layout-metrics"; |
| 2 | 2 | import { useCallback, useMemo, useState, type ReactNode } from "react"; |
| 3 | 3 | import { Keyboard, Platform, View, type NativeSyntheticEvent } from "react-native"; |
| 4 | import { Tabs, type TabSelectedEvent, type TabSelectionRejectedEvent } from "react-native-screens"; | |
| 4 | import { | |
| 5 | Tabs, | |
| 6 | type TabSelectedEvent, | |
| 7 | type TabSelectionPreventedEvent, | |
| 8 | type TabSelectionRejectedEvent, | |
| 9 | } from "react-native-screens"; | |
| 5 | 10 | |
| 6 | 11 | import { WorkspaceTabsContext, useWorkspaceTabs } from "./WorkspaceTabsContext"; |
| 7 | 12 | import type { WorkspaceTab } from "./workspace-tabs-state"; |
| ... | ... | @@ -10,6 +15,7 @@ import { useSystemWorkspaceTabBar } from "./useSystemWorkspaceTabBar"; |
| 10 | 15 | |
| 11 | 16 | // Tabler's git-pull-request geometry, rasterized at 1x/2x/3x for UIKit template images. |
| 12 | 17 | const PULL_REQUEST_ICON = require("../../../assets/icons/pull-request.png"); |
| 18 | const COMPOSE_TAB = "compose"; | |
| 13 | 19 | |
| 14 | 20 | /** Retain each native controller tree; removing its root can leave RNS's stack empty. */ |
| 15 | 21 | function RetainedWorkspaceTab(props: { |
| ... | ... | @@ -39,7 +45,11 @@ function RetainedWorkspaceTab(props: { |
| 39 | 45 | ) : null; |
| 40 | 46 | } |
| 41 | 47 | |
| 42 | /** UIKit retains both controller trees and displays the system tab bar or sidebar for the current size class. */ | |
| 48 | /** | |
| 49 | * UIKit retains both controller trees and displays the system tab bar or sidebar for the current size class. | |
| 50 | * On phones, compose is UIKit's own search-role tab, detached like Photos' search button; it never | |
| 51 | * becomes selected and opens a new chat instead. | |
| 52 | */ | |
| 43 | 53 | export function WorkspaceNativeTabs(props: { |
| 44 | 54 | readonly children: ReactNode; |
| 45 | 55 | readonly hidden: boolean; |
| ... | ... | @@ -62,13 +72,11 @@ export function WorkspaceNativeTabs(props: { |
| 62 | 72 | <Tabs.Host |
| 63 | 73 | navStateRequest={request} |
| 64 | 74 | tabBarHidden={props.hidden || !systemTabBar} |
| 65 | ios={{ | |
| 66 | tabBarControllerMode: "tabBar", | |
| 67 | tabBarTrailingActionSystemImage: phoneBar ? "square.and.pencil" : undefined, | |
| 68 | tabBarTrailingActionAccessibilityLabel: "New chat", | |
| 69 | onTabBarTrailingActionPress: props.onNewTask, | |
| 70 | tabBarMinimizeBehavior: "never", | |
| 71 | tabBarLeadingWidth: phoneBar ? 220 : 0, | |
| 75 | ios={{ tabBarControllerMode: "tabBar", tabBarMinimizeBehavior: "never" }} | |
| 76 | onTabSelectionPrevented={({ | |
| 77 | nativeEvent, | |
| 78 | }: NativeSyntheticEvent<TabSelectionPreventedEvent>) => { | |
| 79 | if (nativeEvent.preventedScreenKey === COMPOSE_TAB) props.onNewTask(); | |
| 72 | 80 | }} |
| 73 | 81 | onTabSelectionRejected={({ |
| 74 | 82 | nativeEvent, |
| ... | ... | @@ -89,7 +97,7 @@ export function WorkspaceNativeTabs(props: { |
| 89 | 97 | <Tabs.Screen |
| 90 | 98 | key={tab} |
| 91 | 99 | screenKey={tab} |
| 92 | title="" | |
| 100 | title={tab === "threads" ? "Threads" : "Pulls"} | |
| 93 | 101 | tabBarItemAccessibilityLabel={tab === "threads" ? "Threads" : "Pull requests"} |
| 94 | 102 | ios={{ |
| 95 | 103 | icon: |
| ... | ... | @@ -106,6 +114,22 @@ export function WorkspaceNativeTabs(props: { |
| 106 | 114 | </RetainedWorkspaceTab> |
| 107 | 115 | </Tabs.Screen> |
| 108 | 116 | ))} |
| 117 | {phoneBar ? ( | |
| 118 | <Tabs.Screen | |
| 119 | key={COMPOSE_TAB} | |
| 120 | screenKey={COMPOSE_TAB} | |
| 121 | title="New chat" | |
| 122 | preventNativeSelection | |
| 123 | ios={{ | |
| 124 | role: "search", | |
| 125 | // iOS 27 detaches a search tab only while it may activate search. | |
| 126 | automaticallyActivatesSearch: true, | |
| 127 | icon: { type: "sfSymbol", name: "square.and.pencil" }, | |
| 128 | }} | |
| 129 | > | |
| 130 | <View /> | |
| 131 | </Tabs.Screen> | |
| 132 | ) : null} | |
| 109 | 133 | </Tabs.Host> |
| 110 | 134 | </View> |
| 111 | 135 | ); |
apps/mobile/src/features/workspace/WorkspaceTabsContext.tsx+1-21| ... | ... | @@ -6,7 +6,6 @@ import { |
| 6 | 6 | useMemo, |
| 7 | 7 | useReducer, |
| 8 | 8 | useRef, |
| 9 | useState, | |
| 10 | 9 | type Dispatch, |
| 11 | 10 | type ReactNode, |
| 12 | 11 | } from "react"; |
| ... | ... | @@ -32,10 +31,6 @@ export const WorkspaceTabsContext = createContext<{ |
| 32 | 31 | readonly recordScrollOffset: (tab: WorkspaceTab, offset: number) => void; |
| 33 | 32 | readonly getScrollAnchor: (tab: WorkspaceTab) => WorkspaceScrollAnchor | undefined; |
| 34 | 33 | readonly recordScrollAnchor: (tab: WorkspaceTab, anchor: WorkspaceScrollAnchor) => void; |
| 35 | readonly threadsProjectKey: string | null; | |
| 36 | readonly setThreadsProjectKey: (key: string | null) => void; | |
| 37 | readonly pullRequestEnvironmentId: string | null; | |
| 38 | readonly setPullRequestEnvironmentId: (id: string | null) => void; | |
| 39 | 34 | } | null>(null); |
| 40 | 35 | |
| 41 | 36 | export function useWorkspaceTabs() { |
| ... | ... | @@ -50,8 +45,6 @@ export function WorkspaceTabsProvider(props: { readonly children: ReactNode }) { |
| 50 | 45 | activeTab.current = state.selectedTab; |
| 51 | 46 | }, [state.selectedTab]); |
| 52 | 47 | const isTabActive = useCallback((tab: WorkspaceTab) => activeTab.current === tab, []); |
| 53 | const [threadsProjectKey, setThreadsProjectKey] = useState<string | null>(null); | |
| 54 | const [pullRequestEnvironmentId, setPullRequestEnvironmentId] = useState<string | null>(null); | |
| 55 | 48 | const scrollOffsets = useRef({ threads: 0, pullRequests: 0 }); |
| 56 | 49 | const scrollAnchors = useRef<Partial<Record<WorkspaceTab, WorkspaceScrollAnchor>>>({}); |
| 57 | 50 | const getScrollOffset = useCallback((tab: WorkspaceTab) => scrollOffsets.current[tab], []); |
| ... | ... | @@ -72,21 +65,8 @@ export function WorkspaceTabsProvider(props: { readonly children: ReactNode }) { |
| 72 | 65 | recordScrollOffset, |
| 73 | 66 | getScrollAnchor, |
| 74 | 67 | recordScrollAnchor, |
| 75 | threadsProjectKey, | |
| 76 | setThreadsProjectKey, | |
| 77 | pullRequestEnvironmentId, | |
| 78 | setPullRequestEnvironmentId, | |
| 79 | 68 | }), |
| 80 | [ | |
| 81 | state, | |
| 82 | isTabActive, | |
| 83 | getScrollOffset, | |
| 84 | recordScrollOffset, | |
| 85 | getScrollAnchor, | |
| 86 | recordScrollAnchor, | |
| 87 | threadsProjectKey, | |
| 88 | pullRequestEnvironmentId, | |
| 89 | ], | |
| 69 | [state, isTabActive, getScrollOffset, recordScrollOffset, getScrollAnchor, recordScrollAnchor], | |
| 90 | 70 | ); |
| 91 | 71 | return <WorkspaceTabsContext value={context}>{props.children}</WorkspaceTabsContext>; |
| 92 | 72 | } |
apps/mobile/src/lib/storage.test.ts+26| ... | ... | @@ -276,6 +276,32 @@ describe("mobile connection storage", () => { |
| 276 | 276 | await expect(loadPreferences()).resolves.toEqual({ baseFontSize: 17 }); |
| 277 | 277 | }); |
| 278 | 278 | |
| 279 | it("keeps valid list filters and drops malformed ones", async () => { | |
| 280 | const threadListFilters = { environmentId: "environment-1", projectKey: null }; | |
| 281 | const pullRequestListFilters = { | |
| 282 | environmentId: null, | |
| 283 | controls: { | |
| 284 | "": { | |
| 285 | state: "all", | |
| 286 | involvement: "reviewing", | |
| 287 | filters: { labels: [["size:S"]] }, | |
| 288 | sort: "oldest", | |
| 289 | }, | |
| 290 | }, | |
| 291 | }; | |
| 292 | mocks.setPreferencesJson(JSON.stringify({ threadListFilters, pullRequestListFilters }), 10); | |
| 293 | await expect(loadPreferences()).resolves.toEqual({ threadListFilters, pullRequestListFilters }); | |
| 294 | ||
| 295 | mocks.setPreferencesJson( | |
| 296 | JSON.stringify({ | |
| 297 | threadListFilters: { environmentId: 1, projectKey: null }, | |
| 298 | pullRequestListFilters: { environmentId: null, controls: { "": { sort: "newest" } } }, | |
| 299 | }), | |
| 300 | 11, | |
| 301 | ); | |
| 302 | await expect(loadPreferences()).resolves.toEqual({}); | |
| 303 | }); | |
| 304 | ||
| 279 | 305 | it("reconciles fallback preferences after SQLite recovers", async () => { |
| 280 | 306 | mocks.setPreferencesJson(JSON.stringify({ baseFontSize: 15 }), 10); |
| 281 | 307 | await mocks.setItemAsync( |
apps/mobile/src/persistence/mobile-preferences.ts+46-4| ... | ... | @@ -5,10 +5,14 @@ import * as Option from "effect/Option"; |
| 5 | 5 | import * as Ref from "effect/Ref"; |
| 6 | 6 | import * as Schema from "effect/Schema"; |
| 7 | 7 | import * as Semaphore from "effect/Semaphore"; |
| 8 | import type { | |
| 9 | ProviderInstanceId, | |
| 10 | PullRequestMergeMethod, | |
| 11 | SidebarProjectGroupingMode, | |
| 8 | import { | |
| 9 | EnvironmentId, | |
| 10 | PullRequestInvolvement, | |
| 11 | PullRequestListFilters, | |
| 12 | PullRequestListState, | |
| 13 | type ProviderInstanceId, | |
| 14 | type PullRequestMergeMethod, | |
| 15 | type SidebarProjectGroupingMode, | |
| 12 | 16 | } from "@t3tools/contracts"; |
| 13 | 17 | import type { ComposerEnterBehavior } from "../lib/composerEnterBehavior"; |
| 14 | 18 | import type { FollowUpBehavior } from "../lib/followUpBehavior"; |
| ... | ... | @@ -21,6 +25,33 @@ import { MobileStorageDecodeError, MobileStorageEncodeError } from "./mobile-sto |
| 21 | 25 | const PREFERENCES_KEY = "t3code.preferences"; |
| 22 | 26 | const PREFERENCES_FALLBACK_KEY = "t3code.preferences.fallback"; |
| 23 | 27 | |
| 28 | /** The pull request list's filters and sort for one environment scope. */ | |
| 29 | export const PullRequestListControls = Schema.Struct({ | |
| 30 | state: PullRequestListState, | |
| 31 | involvement: PullRequestInvolvement, | |
| 32 | filters: PullRequestListFilters, | |
| 33 | projectId: Schema.optional(Schema.String), | |
| 34 | projectEnvironmentId: Schema.optional(Schema.String), | |
| 35 | sort: Schema.Literals(["updated", "created", "oldest"]), | |
| 36 | }); | |
| 37 | export type PullRequestListControls = typeof PullRequestListControls.Type; | |
| 38 | ||
| 39 | const ThreadListFilters = Schema.Struct({ | |
| 40 | environmentId: Schema.NullOr(EnvironmentId), | |
| 41 | projectKey: Schema.NullOr(Schema.String), | |
| 42 | }); | |
| 43 | export type ThreadListFilters = typeof ThreadListFilters.Type; | |
| 44 | ||
| 45 | /** Controls are keyed by environment id, with "" for all environments. */ | |
| 46 | const PullRequestListPreferences = Schema.Struct({ | |
| 47 | environmentId: Schema.NullOr(Schema.String), | |
| 48 | controls: Schema.Record(Schema.String, PullRequestListControls), | |
| 49 | }); | |
| 50 | export type PullRequestListPreferences = typeof PullRequestListPreferences.Type; | |
| 51 | ||
| 52 | const isThreadListFilters = Schema.is(ThreadListFilters); | |
| 53 | const isPullRequestListPreferences = Schema.is(PullRequestListPreferences); | |
| 54 | ||
| 24 | 55 | export interface Preferences { |
| 25 | 56 | readonly pullRequestHideTestFiles?: boolean; |
| 26 | 57 | readonly pullRequestMergeMethod?: PullRequestMergeMethod; |
| ... | ... | @@ -61,6 +92,9 @@ export interface Preferences { |
| 61 | 92 | readonly threadListSettledShelfExpanded?: boolean; |
| 62 | 93 | readonly threadListSnoozedShelfExpanded?: boolean; |
| 63 | 94 | readonly threadListWorkingShelfExpanded?: boolean; |
| 95 | /** Home list filters, restored on the next launch. */ | |
| 96 | readonly threadListFilters?: ThreadListFilters; | |
| 97 | readonly pullRequestListFilters?: PullRequestListPreferences; | |
| 64 | 98 | } |
| 65 | 99 | |
| 66 | 100 | export class MobilePreferencesLoadError extends Schema.TaggedError<MobilePreferencesLoadError>()( |
| ... | ... | @@ -127,6 +161,8 @@ function sanitizePreferences(parsed: Preferences): Preferences { |
| 127 | 161 | threadListSettledShelfExpanded?: boolean; |
| 128 | 162 | threadListSnoozedShelfExpanded?: boolean; |
| 129 | 163 | threadListWorkingShelfExpanded?: boolean; |
| 164 | threadListFilters?: ThreadListFilters; | |
| 165 | pullRequestListFilters?: PullRequestListPreferences; | |
| 130 | 166 | } = {}; |
| 131 | 167 | |
| 132 | 168 | if (typeof parsed.pullRequestHideTestFiles === "boolean") { |
| ... | ... | @@ -241,6 +277,12 @@ function sanitizePreferences(parsed: Preferences): Preferences { |
| 241 | 277 | if (typeof parsed.threadListWorkingShelfExpanded === "boolean") { |
| 242 | 278 | preferences.threadListWorkingShelfExpanded = parsed.threadListWorkingShelfExpanded; |
| 243 | 279 | } |
| 280 | if (isThreadListFilters(parsed.threadListFilters)) { | |
| 281 | preferences.threadListFilters = parsed.threadListFilters; | |
| 282 | } | |
| 283 | if (isPullRequestListPreferences(parsed.pullRequestListFilters)) { | |
| 284 | preferences.pullRequestListFilters = parsed.pullRequestListFilters; | |
| 285 | } | |
| 244 | 286 | return preferences; |
| 245 | 287 | } |
| 246 | 288 |
docs/operations/mobile-screens-fork.md+1-1| ... | ... | @@ -1,7 +1,7 @@ |
| 1 | 1 | # Building the mobile Screens dependency |
| 2 | 2 | |
| 3 | 3 | The mobile v5 stack uses a committed tarball from the |
| 4 | [T3 Screens fork](https://github.com/pingdotgg/react-native-screens/tree/t3-v5.0.0-t3.25). | |
| 4 | [T3 Screens fork](https://github.com/pingdotgg/react-native-screens/tree/t3-v5.0.0-t3.26). | |
| 5 | 5 | The fork owns the integration changes; the app does not patch this package. |
| 6 | 6 | Each archive includes compiled JavaScript, declarations, native source and |
| 7 | 7 | `t3-fork.json` recording its version, source commit and upstream base. |
pnpm-lock.yaml+9-9| ... | ... | @@ -348,7 +348,7 @@ importers: |
| 348 | 348 | version: 7.3.4(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) |
| 349 | 349 | '@react-navigation/native-stack': |
| 350 | 350 | specifier: 7.17.6 |
| 351 | version: 7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(db03c4b8a562e81843953ea133ec37d5) | |
| 351 | version: 7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(02486f76aa3d9c7d88119682fcbddcde) | |
| 352 | 352 | '@shikijs/core': |
| 353 | 353 | specifier: 4.2.0 |
| 354 | 354 | version: 4.2.0 |
| ... | ... | @@ -548,8 +548,8 @@ importers: |
| 548 | 548 | specifier: ~5.9.1 |
| 549 | 549 | version: 5.9.1(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) |
| 550 | 550 | react-native-screens: |
| 551 | specifier: file:deps/react-native-screens-5.0.0-t3.25.tgz | |
| 552 | version: file:apps/mobile/deps/react-native-screens-5.0.0-t3.25.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) | |
| 551 | specifier: file:deps/react-native-screens-5.0.0-t3.26.tgz | |
| 552 | version: file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) | |
| 553 | 553 | react-native-shiki-engine: |
| 554 | 554 | specifier: ^0.3.12 |
| 555 | 555 | version: 0.3.12(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) |
| ... | ... | @@ -10891,9 +10891,9 @@ packages: |
| 10891 | 10891 | react: '*' |
| 10892 | 10892 | react-native: '*' |
| 10893 | 10893 | |
| 10894 | react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.25.tgz: | |
| 10895 | resolution: {integrity: sha512-ACFyV8nA8zECX5EjmenA3ABTnxKRjb+r5I1ZCX4+EALjhsYdqEEDoQ8/G7OAMjzdp3WEOvIyGmGDOinjGOjPWg==, tarball: file:apps/mobile/deps/react-native-screens-5.0.0-t3.25.tgz} | |
| 10896 | version: 5.0.0-t3.25 | |
| 10894 | react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz: | |
| 10895 | resolution: {integrity: sha512-OD1yeZu1HKuvBaroReihFROaofzuF2w4ORtPJXJrpAGk8AgQNusy3KaRRpeiCwIzH2KQU52SnWxrtKaYMzzQ+A==, tarball: file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz} | |
| 10896 | version: 5.0.0-t3.26 | |
| 10897 | 10897 | peerDependencies: |
| 10898 | 10898 | react: '*' |
| 10899 | 10899 | react-native: '*' |
| ... | ... | @@ -16096,7 +16096,7 @@ snapshots: |
| 16096 | 16096 | optionalDependencies: |
| 16097 | 16097 | '@react-native-masked-view/masked-view': 0.3.2(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) |
| 16098 | 16098 | |
| 16099 | '@react-navigation/native-stack@7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(db03c4b8a562e81843953ea133ec37d5)': | |
| 16099 | '@react-navigation/native-stack@7.17.6(patch_hash=d2d52f3776e30765519748fd5e177592dc3b9cc778c52fb00e02674dfa9042c5)(02486f76aa3d9c7d88119682fcbddcde)': | |
| 16100 | 16100 | dependencies: |
| 16101 | 16101 | '@react-navigation/elements': 2.9.26(83de68753219ecf7f3321e065bd378be) |
| 16102 | 16102 | '@react-navigation/native': 7.3.4(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) |
| ... | ... | @@ -16104,7 +16104,7 @@ snapshots: |
| 16104 | 16104 | react: 19.3.0 |
| 16105 | 16105 | react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6) |
| 16106 | 16106 | react-native-safe-area-context: 5.9.1(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) |
| 16107 | react-native-screens: file:apps/mobile/deps/react-native-screens-5.0.0-t3.25.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) | |
| 16107 | react-native-screens: file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0) | |
| 16108 | 16108 | sf-symbols-typescript: 2.2.0 |
| 16109 | 16109 | warn-once: 0.1.1 |
| 16110 | 16110 | transitivePeerDependencies: |
| ... | ... | @@ -22097,7 +22097,7 @@ snapshots: |
| 22097 | 22097 | react: 19.3.0 |
| 22098 | 22098 | react-native: 0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6) |
| 22099 | 22099 | |
| 22100 | react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.25.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0): | |
| 22100 | react-native-screens@file:apps/mobile/deps/react-native-screens-5.0.0-t3.26.tgz(react-native@0.88.0-rc.3(@babel/core@7.29.7)(@react-native/metro-config@0.88.0-rc.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.3.0)(bufferutil@4.1.0)(react@19.3.0)(utf-8-validate@6.0.6))(react@19.3.0): | |
| 22101 | 22101 | dependencies: |
| 22102 | 22102 | react: 19.3.0 |
| 22103 | 22103 | react-freeze: 1.0.4(react@19.3.0) |