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