Track active workspace from navigation state
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import "@/styles/unistyles";
|
import "@/styles/unistyles";
|
||||||
import { polyfillCrypto } from "@/polyfills/crypto";
|
import { polyfillCrypto } from "@/polyfills/crypto";
|
||||||
import { Stack, useGlobalSearchParams, usePathname, useRouter } from "expo-router";
|
import {
|
||||||
|
Stack,
|
||||||
|
useGlobalSearchParams,
|
||||||
|
useNavigationContainerRef,
|
||||||
|
usePathname,
|
||||||
|
useRouter,
|
||||||
|
} from "expo-router";
|
||||||
import { SafeAreaProvider } from "react-native-safe-area-context";
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||||
import { KeyboardProvider } from "react-native-keyboard-controller";
|
import { KeyboardProvider } from "react-native-keyboard-controller";
|
||||||
import { GestureHandlerRootView, Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { GestureHandlerRootView, Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
@@ -68,6 +74,7 @@ import {
|
|||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
import { getTauri } from "@/utils/tauri";
|
import { getTauri } from "@/utils/tauri";
|
||||||
import { attachConsole } from "@/utils/tauri-attach-console";
|
import { attachConsole } from "@/utils/tauri-attach-console";
|
||||||
|
import { syncNavigationActiveWorkspace } from "@/stores/navigation-active-workspace-store";
|
||||||
|
|
||||||
polyfillCrypto();
|
polyfillCrypto();
|
||||||
attachConsole();
|
attachConsole();
|
||||||
@@ -501,6 +508,26 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function NavigationActiveWorkspaceObserver() {
|
||||||
|
const navigationRef = useNavigationContainerRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
syncNavigationActiveWorkspace(navigationRef);
|
||||||
|
const unsubscribeState = navigationRef.addListener("state", () => {
|
||||||
|
syncNavigationActiveWorkspace(navigationRef);
|
||||||
|
});
|
||||||
|
const unsubscribeReady = navigationRef.addListener("ready" as never, () => {
|
||||||
|
syncNavigationActiveWorkspace(navigationRef);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
unsubscribeState();
|
||||||
|
unsubscribeReady();
|
||||||
|
};
|
||||||
|
}, [navigationRef]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function LoadingView({ message }: { message?: string } = {}) {
|
function LoadingView({ message }: { message?: string } = {}) {
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -557,6 +584,7 @@ export default function RootLayout() {
|
|||||||
<GestureHandlerRootView
|
<GestureHandlerRootView
|
||||||
style={{ flex: 1, backgroundColor: darkTheme.colors.surface0 }}
|
style={{ flex: 1, backgroundColor: darkTheme.colors.surface0 }}
|
||||||
>
|
>
|
||||||
|
<NavigationActiveWorkspaceObserver />
|
||||||
<PortalProvider>
|
<PortalProvider>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<KeyboardProvider>
|
<KeyboardProvider>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
type ReactElement,
|
type ReactElement,
|
||||||
type MutableRefObject,
|
type MutableRefObject,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { router, useGlobalSearchParams, usePathname } from 'expo-router'
|
import { router, usePathname } from 'expo-router'
|
||||||
import { navigateToWorkspace } from '@/hooks/use-workspace-navigation'
|
import { navigateToWorkspace } from '@/hooks/use-workspace-navigation'
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
||||||
import { type GestureType } from 'react-native-gesture-handler'
|
import { type GestureType } from 'react-native-gesture-handler'
|
||||||
@@ -35,10 +35,8 @@ import { projectIconQueryKey } from '@/hooks/use-project-icon-query'
|
|||||||
import {
|
import {
|
||||||
buildHostNewAgentRoute,
|
buildHostNewAgentRoute,
|
||||||
buildHostWorkspaceRoute,
|
buildHostWorkspaceRoute,
|
||||||
decodeWorkspaceIdFromPathSegment,
|
|
||||||
parseHostWorkspaceRouteFromPathname,
|
parseHostWorkspaceRouteFromPathname,
|
||||||
} from '@/utils/host-routes'
|
} from '@/utils/host-routes'
|
||||||
import { normalizeWorkspaceIdentity } from '@/utils/workspace-identity'
|
|
||||||
import {
|
import {
|
||||||
type SidebarProjectEntry,
|
type SidebarProjectEntry,
|
||||||
type SidebarWorkspaceEntry,
|
type SidebarWorkspaceEntry,
|
||||||
@@ -73,6 +71,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from '@/components/ui/tooltip'
|
} from '@/components/ui/tooltip'
|
||||||
import { buildSidebarProjectRowModel } from '@/utils/sidebar-project-row-model'
|
import { buildSidebarProjectRowModel } from '@/utils/sidebar-project-row-model'
|
||||||
|
import { useNavigationActiveWorkspaceSelection } from '@/stores/navigation-active-workspace-store'
|
||||||
|
|
||||||
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
|
function toProjectIconDataUri(icon: { mimeType: string; data: string } | null): string | null {
|
||||||
if (!icon) {
|
if (!icon) {
|
||||||
@@ -1212,10 +1211,7 @@ export function SidebarWorkspaceList({
|
|||||||
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
|
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
|
||||||
const isNative = Platform.OS !== 'web'
|
const isNative = Platform.OS !== 'web'
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const routeParams = useGlobalSearchParams<{
|
const activeWorkspaceSelection = useNavigationActiveWorkspaceSelection()
|
||||||
serverId?: string | string[]
|
|
||||||
workspaceId?: string | string[]
|
|
||||||
}>()
|
|
||||||
const isTauri = getIsTauri()
|
const isTauri = getIsTauri()
|
||||||
const altDown = useKeyboardShortcutsStore((state) => state.altDown)
|
const altDown = useKeyboardShortcutsStore((state) => state.altDown)
|
||||||
const cmdOrCtrlDown = useKeyboardShortcutsStore((state) => state.cmdOrCtrlDown)
|
const cmdOrCtrlDown = useKeyboardShortcutsStore((state) => state.cmdOrCtrlDown)
|
||||||
@@ -1226,40 +1222,8 @@ export function SidebarWorkspaceList({
|
|||||||
const getWorkspaceOrder = useSidebarOrderStore((state) => state.getWorkspaceOrder)
|
const getWorkspaceOrder = useSidebarOrderStore((state) => state.getWorkspaceOrder)
|
||||||
const setWorkspaceOrder = useSidebarOrderStore((state) => state.setWorkspaceOrder)
|
const setWorkspaceOrder = useSidebarOrderStore((state) => state.setWorkspaceOrder)
|
||||||
|
|
||||||
const activeWorkspaceSelection = useMemo(() => {
|
const isWorkspaceRoute = useMemo(() => Boolean(pathname && parseHostWorkspaceRouteFromPathname(pathname)), [pathname])
|
||||||
if (!pathname) {
|
const effectiveActiveWorkspaceSelection = isWorkspaceRoute ? activeWorkspaceSelection : null
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (!parseHostWorkspaceRouteFromPathname(pathname)) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverValue = Array.isArray(routeParams.serverId)
|
|
||||||
? routeParams.serverId[0]
|
|
||||||
: routeParams.serverId
|
|
||||||
const workspaceValue = Array.isArray(routeParams.workspaceId)
|
|
||||||
? routeParams.workspaceId[0]
|
|
||||||
: routeParams.workspaceId
|
|
||||||
const serverId = serverValue?.trim() ?? ''
|
|
||||||
const workspaceId = normalizeWorkspaceIdentity(
|
|
||||||
workspaceValue ? decodeWorkspaceIdFromPathSegment(workspaceValue) : null
|
|
||||||
)
|
|
||||||
if (!serverId || !workspaceId) {
|
|
||||||
const parsed = parseHostWorkspaceRouteFromPathname(pathname)
|
|
||||||
if (!parsed) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
serverId: parsed.serverId,
|
|
||||||
workspaceId: parsed.workspaceId,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
serverId,
|
|
||||||
workspaceId,
|
|
||||||
}
|
|
||||||
}, [pathname, routeParams.serverId, routeParams.workspaceId])
|
|
||||||
|
|
||||||
const projectIconRequests = useMemo(() => {
|
const projectIconRequests = useMemo(() => {
|
||||||
if (!serverId) {
|
if (!serverId) {
|
||||||
@@ -1409,7 +1373,7 @@ export function SidebarWorkspaceList({
|
|||||||
displayName={item.projectName}
|
displayName={item.projectName}
|
||||||
iconDataUri={projectIconByProjectKey.get(item.projectKey) ?? null}
|
iconDataUri={projectIconByProjectKey.get(item.projectKey) ?? null}
|
||||||
serverId={serverId}
|
serverId={serverId}
|
||||||
activeWorkspaceSelection={activeWorkspaceSelection}
|
activeWorkspaceSelection={effectiveActiveWorkspaceSelection}
|
||||||
showShortcutBadges={showShortcutBadges}
|
showShortcutBadges={showShortcutBadges}
|
||||||
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
|
shortcutIndexByWorkspaceKey={shortcutIndexByWorkspaceKey}
|
||||||
parentGestureRef={parentGestureRef}
|
parentGestureRef={parentGestureRef}
|
||||||
@@ -1425,8 +1389,8 @@ export function SidebarWorkspaceList({
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
activeWorkspaceSelection,
|
|
||||||
collapsedProjectKeys,
|
collapsedProjectKeys,
|
||||||
|
effectiveActiveWorkspaceSelection,
|
||||||
handleCreateWorktree,
|
handleCreateWorktree,
|
||||||
handleWorkspaceReorder,
|
handleWorkspaceReorder,
|
||||||
onWorkspacePress,
|
onWorkspacePress,
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { useSyncExternalStore } from "react";
|
||||||
|
import {
|
||||||
|
decodeWorkspaceIdFromPathSegment,
|
||||||
|
parseHostWorkspaceRouteFromPathname,
|
||||||
|
} from "@/utils/host-routes";
|
||||||
|
|
||||||
|
interface ActiveWorkspaceSelection {
|
||||||
|
serverId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type NavigationRouteLike = {
|
||||||
|
name?: unknown;
|
||||||
|
params?: unknown;
|
||||||
|
path?: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavigationObserverRef {
|
||||||
|
current: {
|
||||||
|
getCurrentRoute(): unknown;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot: ActiveWorkspaceSelection | null = null;
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
|
||||||
|
function subscribe(listener: () => void): () => void {
|
||||||
|
listeners.add(listener);
|
||||||
|
return () => {
|
||||||
|
listeners.delete(listener);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSnapshot(): ActiveWorkspaceSelection | null {
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitIfChanged(next: ActiveWorkspaceSelection | null) {
|
||||||
|
if (
|
||||||
|
snapshot?.serverId === next?.serverId &&
|
||||||
|
snapshot?.workspaceId === next?.workspaceId
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
snapshot = next;
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractActiveWorkspaceFromRoute(route: NavigationRouteLike | undefined): ActiveWorkspaceSelection | null {
|
||||||
|
if (!route) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof route.path === "string") {
|
||||||
|
const parsed = parseHostWorkspaceRouteFromPathname(route.path);
|
||||||
|
if (parsed) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const params =
|
||||||
|
route.params && typeof route.params === "object"
|
||||||
|
? (route.params as {
|
||||||
|
serverId?: string | string[];
|
||||||
|
workspaceId?: string | string[];
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
const serverValue = Array.isArray(params?.serverId) ? params?.serverId[0] : params?.serverId;
|
||||||
|
const workspaceValue = Array.isArray(params?.workspaceId)
|
||||||
|
? params?.workspaceId[0]
|
||||||
|
: params?.workspaceId;
|
||||||
|
const serverId = typeof serverValue === "string" ? serverValue.trim() : "";
|
||||||
|
const workspaceId =
|
||||||
|
typeof workspaceValue === "string"
|
||||||
|
? (decodeWorkspaceIdFromPathSegment(workspaceValue) ?? "")
|
||||||
|
: "";
|
||||||
|
|
||||||
|
if (!serverId || !workspaceId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { serverId, workspaceId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncNavigationActiveWorkspace(
|
||||||
|
navigationRef: NavigationObserverRef
|
||||||
|
) {
|
||||||
|
emitIfChanged(
|
||||||
|
extractActiveWorkspaceFromRoute(navigationRef.current?.getCurrentRoute() as NavigationRouteLike | undefined)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNavigationActiveWorkspaceSelection(): ActiveWorkspaceSelection | null {
|
||||||
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user