refactor: extract explorer open gesture and standardize icon sizes
This commit is contained in:
@@ -51,4 +51,19 @@ describe("useArchiveAgent", () => {
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("removes an archived agent from cached list payloads", () => {
|
||||
const payload = {
|
||||
entries: [
|
||||
{ agent: { id: "agent-1" } },
|
||||
{ agent: { id: "agent-2" } },
|
||||
],
|
||||
pageInfo: { hasMore: false },
|
||||
};
|
||||
|
||||
const next = __private__.removeAgentFromListPayload(payload, "agent-1");
|
||||
|
||||
expect(next.entries).toEqual([{ agent: { id: "agent-2" } }]);
|
||||
expect(next.pageInfo).toEqual({ hasMore: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,10 @@ interface IsAgentArchivingInput extends ArchiveAgentInput {
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
|
||||
interface AgentsListQueryData {
|
||||
entries?: Array<{ agent?: { id?: string | null } | null } | null>;
|
||||
}
|
||||
|
||||
function toArchiveKey(input: ArchiveAgentInput): string {
|
||||
const serverId = input.serverId.trim();
|
||||
const agentId = input.agentId.trim();
|
||||
@@ -73,6 +77,66 @@ function isAgentArchiving(input: IsAgentArchivingInput): boolean {
|
||||
return Boolean(readPendingState(input.queryClient)[key]);
|
||||
}
|
||||
|
||||
function removeAgentFromListPayload<T extends AgentsListQueryData | undefined>(
|
||||
payload: T,
|
||||
agentId: string
|
||||
): T {
|
||||
if (!payload || !Array.isArray(payload.entries) || !agentId) {
|
||||
return payload;
|
||||
}
|
||||
const filtered = payload.entries.filter((entry) => entry?.agent?.id !== agentId);
|
||||
if (filtered.length === payload.entries.length) {
|
||||
return payload;
|
||||
}
|
||||
return {
|
||||
...payload,
|
||||
entries: filtered,
|
||||
} as T;
|
||||
}
|
||||
|
||||
function removeAgentFromCachedLists(
|
||||
queryClient: QueryClient,
|
||||
input: ArchiveAgentInput
|
||||
): void {
|
||||
const agentId = input.agentId.trim();
|
||||
if (!agentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
queryClient.setQueryData<AgentsListQueryData | undefined>(
|
||||
["sidebarAgentsList", input.serverId],
|
||||
(current) => removeAgentFromListPayload(current, agentId)
|
||||
);
|
||||
queryClient.setQueryData<AgentsListQueryData | undefined>(
|
||||
["allAgents", input.serverId],
|
||||
(current) => removeAgentFromListPayload(current, agentId)
|
||||
);
|
||||
}
|
||||
|
||||
function markAgentArchivedInStore(input: ArchiveAgentInput & { archivedAt: string }): void {
|
||||
const archivedAt = new Date(input.archivedAt);
|
||||
if (Number.isNaN(archivedAt.getTime())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setAgents = useSessionStore.getState().setAgents;
|
||||
setAgents(input.serverId, (prev) => {
|
||||
const existing = prev.get(input.agentId);
|
||||
if (!existing) {
|
||||
return prev;
|
||||
}
|
||||
if (existing.archivedAt && existing.archivedAt.getTime() === archivedAt.getTime()) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
next.set(input.agentId, {
|
||||
...existing,
|
||||
archivedAt,
|
||||
});
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
export function clearArchiveAgentPending(input: IsAgentArchivingInput): void {
|
||||
setAgentArchiving({
|
||||
...input,
|
||||
@@ -92,12 +156,12 @@ export function useArchiveAgent() {
|
||||
});
|
||||
|
||||
const archiveMutation = useMutation({
|
||||
mutationFn: async (input: ArchiveAgentInput) => {
|
||||
mutationFn: async (input: ArchiveAgentInput): Promise<{ archivedAt: string }> => {
|
||||
const client = useSessionStore.getState().sessions[input.serverId]?.client ?? null;
|
||||
if (!client) {
|
||||
throw new Error("Daemon client not available");
|
||||
}
|
||||
await client.archiveAgent(input.agentId);
|
||||
return await client.archiveAgent(input.agentId);
|
||||
},
|
||||
onMutate: (input) => {
|
||||
setAgentArchiving({
|
||||
@@ -107,7 +171,21 @@ export function useArchiveAgent() {
|
||||
isArchiving: true,
|
||||
});
|
||||
},
|
||||
onError: (_error, input) => {
|
||||
onSuccess: (result, input) => {
|
||||
markAgentArchivedInStore({
|
||||
serverId: input.serverId,
|
||||
agentId: input.agentId,
|
||||
archivedAt: result.archivedAt,
|
||||
});
|
||||
removeAgentFromCachedLists(queryClient, input);
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["sidebarAgentsList", input.serverId],
|
||||
});
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: ["allAgents", input.serverId],
|
||||
});
|
||||
},
|
||||
onSettled: (_result, _error, input) => {
|
||||
clearArchiveAgentPending({
|
||||
queryClient,
|
||||
serverId: input.serverId,
|
||||
@@ -154,4 +232,5 @@ export const __private__ = {
|
||||
readPendingState,
|
||||
setAgentArchiving,
|
||||
isAgentArchiving,
|
||||
removeAgentFromListPayload,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { useMemo } from "react";
|
||||
import { Gesture } from "react-native-gesture-handler";
|
||||
import {
|
||||
Extrapolation,
|
||||
interpolate,
|
||||
runOnJS,
|
||||
useSharedValue,
|
||||
} from "react-native-reanimated";
|
||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||
|
||||
interface UseExplorerOpenGestureParams {
|
||||
enabled: boolean;
|
||||
onOpen: () => void;
|
||||
}
|
||||
|
||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||
|
||||
function logExplorerOpenGesture(
|
||||
event: string,
|
||||
details: Record<string, unknown>
|
||||
): void {
|
||||
if (!IS_DEV) {
|
||||
return;
|
||||
}
|
||||
console.log(`[ExplorerOpenGesture] ${event}`, details);
|
||||
}
|
||||
|
||||
export function useExplorerOpenGesture({
|
||||
enabled,
|
||||
onOpen,
|
||||
}: UseExplorerOpenGestureParams) {
|
||||
const {
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
windowWidth,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
isGesturing,
|
||||
} = useExplorerSidebarAnimation();
|
||||
const touchStartX = useSharedValue(0);
|
||||
const touchStartY = useSharedValue(0);
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Gesture.Pan()
|
||||
.enabled(enabled)
|
||||
.manualActivation(true)
|
||||
.onTouchesDown((event) => {
|
||||
const touch = event.changedTouches[0];
|
||||
if (!touch) {
|
||||
return;
|
||||
}
|
||||
touchStartX.value = touch.absoluteX;
|
||||
touchStartY.value = touch.absoluteY;
|
||||
})
|
||||
.onTouchesMove((event, stateManager) => {
|
||||
const touch = event.changedTouches[0];
|
||||
if (!touch || event.numberOfTouches !== 1) {
|
||||
stateManager.fail();
|
||||
return;
|
||||
}
|
||||
|
||||
const deltaX = touch.absoluteX - touchStartX.value;
|
||||
const deltaY = touch.absoluteY - touchStartY.value;
|
||||
const absDeltaX = Math.abs(deltaX);
|
||||
const absDeltaY = Math.abs(deltaY);
|
||||
|
||||
// Fail quickly on rightward or clearly vertical intent.
|
||||
if (deltaX >= 10) {
|
||||
stateManager.fail();
|
||||
return;
|
||||
}
|
||||
if (absDeltaY > 10 && absDeltaY > absDeltaX) {
|
||||
stateManager.fail();
|
||||
return;
|
||||
}
|
||||
|
||||
// Activate only on intentional leftward movement.
|
||||
if (deltaX <= -15 && absDeltaX > absDeltaY) {
|
||||
stateManager.activate();
|
||||
}
|
||||
})
|
||||
.onStart(() => {
|
||||
isGesturing.value = true;
|
||||
runOnJS(logExplorerOpenGesture)("start", { enabled });
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
// Right sidebar: start from closed position (+windowWidth) and move towards 0.
|
||||
const newTranslateX = Math.max(
|
||||
0,
|
||||
Math.min(windowWidth, windowWidth + event.translationX)
|
||||
);
|
||||
translateX.value = newTranslateX;
|
||||
backdropOpacity.value = interpolate(
|
||||
newTranslateX,
|
||||
[windowWidth, 0],
|
||||
[0, 1],
|
||||
Extrapolation.CLAMP
|
||||
);
|
||||
})
|
||||
.onEnd((event) => {
|
||||
isGesturing.value = false;
|
||||
const shouldOpenByPosition = translateX.value < (windowWidth * 2) / 3;
|
||||
const shouldOpenByVelocity = event.velocityX < -500;
|
||||
const shouldOpen = shouldOpenByPosition || shouldOpenByVelocity;
|
||||
runOnJS(logExplorerOpenGesture)("end", {
|
||||
translationX: event.translationX,
|
||||
velocityX: event.velocityX,
|
||||
panelTranslateX: translateX.value,
|
||||
windowWidth,
|
||||
shouldOpenByPosition,
|
||||
shouldOpenByVelocity,
|
||||
shouldOpen,
|
||||
});
|
||||
if (shouldOpen) {
|
||||
animateToOpen();
|
||||
runOnJS(onOpen)();
|
||||
} else {
|
||||
animateToClose();
|
||||
}
|
||||
})
|
||||
.onFinalize(() => {
|
||||
isGesturing.value = false;
|
||||
}),
|
||||
[
|
||||
enabled,
|
||||
windowWidth,
|
||||
translateX,
|
||||
backdropOpacity,
|
||||
animateToOpen,
|
||||
animateToClose,
|
||||
isGesturing,
|
||||
onOpen,
|
||||
touchStartX,
|
||||
touchStartY,
|
||||
]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user