mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-14 12:25:05 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4683ddb3d1 | ||
|
|
17079f4a56 | ||
|
|
c701a1d936 |
@@ -418,7 +418,6 @@
|
||||
"packages/app/src/components/context-window-meter.tsx",
|
||||
"packages/app/src/components/dictation-controls.tsx",
|
||||
"packages/app/src/components/download-toast.tsx",
|
||||
"packages/app/src/components/draggable-list.native.tsx",
|
||||
"packages/app/src/components/headers/back-header.tsx",
|
||||
"packages/app/src/components/headers/screen-header.tsx",
|
||||
"packages/app/src/components/host-status-dot.tsx",
|
||||
|
||||
Generated
+32
-15
@@ -31853,20 +31853,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-draggable-flatlist": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-draggable-flatlist/-/react-native-draggable-flatlist-4.0.3.tgz",
|
||||
"integrity": "sha512-2F4x5BFieWdGq9SetD2nSAR7s7oQCSgNllYgERRXXtNfSOuAGAVbDb/3H3lP0y5f7rEyNwabKorZAD/SyyNbDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/preset-typescript": "^7.17.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-native": ">=0.64.0",
|
||||
"react-native-gesture-handler": ">=2.0.0",
|
||||
"react-native-reanimated": ">=2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-drawer-layout": {
|
||||
"version": "4.2.12",
|
||||
"resolved": "https://registry.npmjs.org/react-native-drawer-layout/-/react-native-drawer-layout-4.2.12.tgz",
|
||||
@@ -31909,6 +31895,22 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-haptic-feedback": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-haptic-feedback/-/react-native-haptic-feedback-3.0.0.tgz",
|
||||
"integrity": "sha512-yxBQFHhXU8S0xupGtzDRToGw5yMWezUdLzxtlx2Oi9YpjPwprWZWCw8foPt0S2t84B0ZikzZYyOHiMSXIelUxg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peerDependencies": {
|
||||
"react": ">=18.0.0",
|
||||
"react-native": ">=0.71.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-is-edge-to-edge": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.3.1.tgz",
|
||||
@@ -31992,6 +31994,21 @@
|
||||
"integrity": "sha512-b+4vILXHPu++t8H41PHLBVsTab2LPqwXNdzgdScyl4+Cu8Ta34aUQW3T469cB0ogAMPdu//KV00w4YVpDZoRUQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-native-sortables": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-sortables/-/react-native-sortables-1.10.1.tgz",
|
||||
"integrity": "sha512-3M+OnzE2zgN2meECvd5cGQrm5oFQSQ34HPm3DSLAo8Xaif9+Zvtv5V4uX8u/HmBtI+v1gEuw7rjuYWqvUhZB9Q==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"react-native-haptic-feedback": ">=2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*",
|
||||
"react-native-gesture-handler": ">=2.0.0",
|
||||
"react-native-reanimated": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-unistyles": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-unistyles/-/react-native-unistyles-3.2.4.tgz",
|
||||
@@ -37549,7 +37566,6 @@
|
||||
"react-freeze": "1.0.4",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-native": "0.86.3",
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-edge-to-edge": "^1.7.0",
|
||||
"react-native-enriched-markdown": "1.2.0-nightly-20261014-766fc0e67",
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
@@ -37558,6 +37574,7 @@
|
||||
"react-native-reanimated": "4.5.5",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-sortables": "^1.10.1",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-unistyles": "^3.2.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
|
||||
@@ -18,9 +18,9 @@ maestro test packages/app/maestro/ios-sidebar-close-regression.yaml --udid <simu
|
||||
|
||||
`test-sidebar-drag-cancellation-ios.sh` creates three temporary projects and
|
||||
uses real native slow swipes to cover successful reorder, a second touch during
|
||||
the drop spring, horizontal close cancellation, outer-list scroll recovery, and
|
||||
a subsequent reorder. The harness removes its daemon projects and local fixture
|
||||
directories on exit.
|
||||
the drop spring, a horizontal swipe after the lift that keeps the panel open
|
||||
and the order unchanged, outer-list scroll recovery, and a subsequent reorder.
|
||||
The harness removes its daemon projects and local fixture directories on exit.
|
||||
|
||||
Start the debug dev client against the intended Metro server, then run:
|
||||
|
||||
|
||||
@@ -49,29 +49,22 @@ appId: ${PASEO_MAESTRO_APP_ID}
|
||||
above:
|
||||
text: ".*${PASEO_MAESTRO_DRAG_A_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_A_NAME}, Project actions"
|
||||
|
||||
# Horizontal intent after the lift belongs to panel close. It must cancel the
|
||||
# drag without committing another reorder or leaving the outer list locked.
|
||||
# Horizontal movement after the lift stays with the lifted row: the panel does
|
||||
# not close, the drop commits no reorder, and the outer list stays scrollable.
|
||||
- swipe:
|
||||
from:
|
||||
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||
direction: LEFT
|
||||
duration: 20000
|
||||
- assertNotVisible:
|
||||
- assertVisible:
|
||||
id: "sidebar-project-list"
|
||||
|
||||
- tapOn:
|
||||
id: "menu-button"
|
||||
- extendedWaitUntil:
|
||||
visible:
|
||||
id: "sidebar-project-list"
|
||||
timeout: 10000
|
||||
- assertVisible:
|
||||
text: ".*${PASEO_MAESTRO_DRAG_B_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_B_NAME}, Project actions"
|
||||
above:
|
||||
text: ".*${PASEO_MAESTRO_DRAG_A_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_A_NAME}, Project actions"
|
||||
|
||||
# Reaching a fixture at the bottom proves the nestable outer scroll was
|
||||
# unlocked by cancellation.
|
||||
# Reaching a fixture at the bottom proves the outer scroll still works after
|
||||
# the horizontal drag.
|
||||
- scrollUntilVisible:
|
||||
element:
|
||||
text: ".*${PASEO_MAESTRO_DRAG_Z_NAME}, Create a new workspace for ${PASEO_MAESTRO_DRAG_Z_NAME}, Project actions"
|
||||
@@ -92,7 +85,7 @@ appId: ${PASEO_MAESTRO_APP_ID}
|
||||
visibilityPercentage: 50
|
||||
centerElement: true
|
||||
|
||||
# A real reorder after cancellation proves the list is not merely scrollable;
|
||||
# A real reorder after the horizontal drag proves the list is not merely scrollable;
|
||||
# its drag recognizer and semantic order update both recovered.
|
||||
- swipe:
|
||||
from:
|
||||
|
||||
@@ -122,7 +122,6 @@
|
||||
"react-freeze": "1.0.4",
|
||||
"react-i18next": "^17.0.8",
|
||||
"react-native": "0.86.3",
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-edge-to-edge": "^1.7.0",
|
||||
"react-native-enriched-markdown": "1.2.0-nightly-20261014-766fc0e67",
|
||||
"react-native-gesture-handler": "~2.32.0",
|
||||
@@ -131,6 +130,7 @@
|
||||
"react-native-reanimated": "4.5.5",
|
||||
"react-native-safe-area-context": "~5.7.0",
|
||||
"react-native-screens": "~4.26.0",
|
||||
"react-native-sortables": "^1.10.1",
|
||||
"react-native-svg": "15.15.4",
|
||||
"react-native-unistyles": "^3.2.4",
|
||||
"react-native-web": "~0.21.0",
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { orderDroppedItems } from "./draggable-list-drop-order";
|
||||
|
||||
interface Row {
|
||||
key: string;
|
||||
}
|
||||
|
||||
const rows = (...keys: string[]): Row[] => keys.map((key) => ({ key }));
|
||||
const keyExtractor = (row: Row) => row.key;
|
||||
|
||||
describe("orderDroppedItems", () => {
|
||||
it("returns the rows in the order the list shows at the drop", () => {
|
||||
const result = orderDroppedItems({
|
||||
data: rows("a", "b", "c"),
|
||||
keyExtractor,
|
||||
indexToKey: ["c", "a", "b"],
|
||||
});
|
||||
|
||||
expect(result.map(keyExtractor)).toEqual(["c", "a", "b"]);
|
||||
});
|
||||
|
||||
it("leaves out a row that disappeared while it was dropped", () => {
|
||||
const result = orderDroppedItems({
|
||||
data: rows("a", "c"),
|
||||
keyExtractor,
|
||||
indexToKey: ["c", "b", "a"],
|
||||
});
|
||||
|
||||
expect(result).toEqual(rows("c", "a"));
|
||||
});
|
||||
|
||||
it("leaves out a row that arrived while it was dropped, so its stored place is kept", () => {
|
||||
const result = orderDroppedItems({
|
||||
data: rows("a", "b", "new"),
|
||||
keyExtractor,
|
||||
indexToKey: ["b", "a"],
|
||||
});
|
||||
|
||||
expect(result).toEqual(rows("b", "a"));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* The rows in the order a native list shows when a drag ends.
|
||||
*
|
||||
* Sortables' own `data` for `onDragEnd` indexes rows through `keyToIndex`, a derived value that can
|
||||
* trail `indexToKey` by a frame. A drop right after a swap would then save the order from before the
|
||||
* swap, and a row added or removed during the drop leaves a hole. `indexToKey` is the order on
|
||||
* screen. Keys without a row are skipped, and rows without a key are left out, so callers keep
|
||||
* their stored place.
|
||||
*/
|
||||
export function orderDroppedItems<T>(input: {
|
||||
data: readonly T[];
|
||||
keyExtractor: (item: T) => string;
|
||||
indexToKey: readonly string[];
|
||||
}): T[] {
|
||||
const itemsByKey = new Map(input.data.map((item) => [input.keyExtractor(item), item]));
|
||||
const ordered: T[] = [];
|
||||
for (const key of input.indexToKey) {
|
||||
const item = itemsByKey.get(key);
|
||||
if (item !== undefined) {
|
||||
ordered.push(item);
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
@@ -1,133 +1,169 @@
|
||||
import { RefreshControl } from "react-native";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import DraggableFlatList, {
|
||||
NestableDraggableFlatList,
|
||||
type RenderItemParams,
|
||||
} from "react-native-draggable-flatlist";
|
||||
import { useUnistyles } from "react-native-unistyles";
|
||||
import type { DraggableListProps, DraggableRenderItemInfo } from "./draggable-list.types";
|
||||
import { createContext, useCallback, useContext, useState, type ReactElement } from "react";
|
||||
import { View } from "react-native";
|
||||
import { ScrollView as GestureScrollView } from "react-native-gesture-handler";
|
||||
import {
|
||||
createAnimatedComponent,
|
||||
useAnimatedReaction,
|
||||
useAnimatedRef,
|
||||
type AnimatedRef,
|
||||
} from "react-native-reanimated";
|
||||
import Sortable, {
|
||||
useItemContext,
|
||||
type SortableGridDragEndParams,
|
||||
type SortableGridRenderItem,
|
||||
} from "react-native-sortables";
|
||||
import { scheduleOnRN } from "react-native-worklets";
|
||||
import type {
|
||||
DraggableListDragHandleProps,
|
||||
DraggableListHandleProps,
|
||||
DraggableListProps,
|
||||
DraggableListScrollViewProps,
|
||||
DraggableRenderItemInfo,
|
||||
} from "./draggable-list.types";
|
||||
import { orderDroppedItems } from "./draggable-list-drop-order";
|
||||
|
||||
export type { DraggableListProps, DraggableRenderItemInfo };
|
||||
|
||||
const SCROLL_ENABLED_FLEX_STYLE = { flex: 1 };
|
||||
// Sortables places every row with UI-thread offsets, so the reorder commit never moves a row by
|
||||
// itself. Lists that position rows by React child order plus a held drag translate paint one
|
||||
// frame of new order + old translate when the reorder commits.
|
||||
//
|
||||
// Render lists inside `DraggableListScrollView` so a dragged row autoscrolls it.
|
||||
//
|
||||
// A drag interrupted by the system (app backgrounded, incoming call) commits the order it
|
||||
// previewed: sortables ends a cancelled drag through the same `onDragEnd` as a release.
|
||||
|
||||
// A row lifts after a still hold. Moving first fails the drag, so the touch scrolls the list or
|
||||
// swipes the panel instead.
|
||||
const DRAG_ACTIVATION_DELAY_MS = 180;
|
||||
const DRAG_ACTIVATION_FAIL_OFFSET_PX = 4;
|
||||
|
||||
// Native handles carry no props; their presence marks the row as the handle.
|
||||
const NATIVE_DRAG_HANDLE_PROPS: DraggableListDragHandleProps = {};
|
||||
|
||||
const AnimatedGestureScrollView = createAnimatedComponent(GestureScrollView);
|
||||
|
||||
const ScrollViewRefContext = createContext<AnimatedRef | null>(null);
|
||||
|
||||
// True inside a row of another DraggableList, e.g. the workspaces inside a project block.
|
||||
const NestedListContext = createContext(false);
|
||||
|
||||
export function DraggableListScrollView({
|
||||
children,
|
||||
simultaneousGestureRef,
|
||||
...scrollViewProps
|
||||
}: DraggableListScrollViewProps) {
|
||||
const scrollViewRef = useAnimatedRef();
|
||||
return (
|
||||
<AnimatedGestureScrollView
|
||||
{...scrollViewProps}
|
||||
ref={scrollViewRef}
|
||||
simultaneousHandlers={simultaneousGestureRef}
|
||||
>
|
||||
<ScrollViewRefContext.Provider value={scrollViewRef}>
|
||||
{children}
|
||||
</ScrollViewRefContext.Provider>
|
||||
</AnimatedGestureScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
export function DraggableListHandle({ children, dragHandleProps }: DraggableListHandleProps) {
|
||||
if (!dragHandleProps) {
|
||||
return children;
|
||||
}
|
||||
return <Sortable.Handle>{children}</Sortable.Handle>;
|
||||
}
|
||||
|
||||
export function DraggableList<T>({
|
||||
data,
|
||||
keyExtractor,
|
||||
renderItem,
|
||||
onDragEnd,
|
||||
style,
|
||||
containerStyle,
|
||||
contentContainerStyle,
|
||||
testID,
|
||||
ListFooterComponent,
|
||||
ListHeaderComponent,
|
||||
ListEmptyComponent,
|
||||
showsVerticalScrollIndicator = true,
|
||||
scrollEnabled = true,
|
||||
useDragHandle: _useDragHandle = false,
|
||||
refreshing,
|
||||
onRefresh,
|
||||
extraData,
|
||||
simultaneousGestureRef,
|
||||
gestureHostPresented,
|
||||
waitFor,
|
||||
onDragBegin: onDragBeginProp,
|
||||
nestable = false,
|
||||
useDragHandle = false,
|
||||
}: DraggableListProps<T>) {
|
||||
const { theme } = useUnistyles();
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const scrollViewRef = useContext(ScrollViewRefContext);
|
||||
const nested = useContext(NestedListContext);
|
||||
const dragHandleProps = useDragHandle ? NATIVE_DRAG_HANDLE_PROPS : undefined;
|
||||
|
||||
// Pass the ref directly to DraggableFlatList - it handles gesture
|
||||
// coordination internally for nestable lists.
|
||||
const simultaneousHandlers = useMemo(
|
||||
() => (simultaneousGestureRef ? [simultaneousGestureRef] : undefined),
|
||||
[simultaneousGestureRef],
|
||||
);
|
||||
|
||||
const refreshColors = useMemo(
|
||||
() => [theme.colors.foregroundMuted],
|
||||
[theme.colors.foregroundMuted],
|
||||
);
|
||||
|
||||
const handleRenderItem = useCallback(
|
||||
({ item, drag, isActive, getIndex }: RenderItemParams<T>) => {
|
||||
const index = getIndex() ?? 0;
|
||||
const info: DraggableRenderItemInfo<T> = {
|
||||
item,
|
||||
index,
|
||||
drag,
|
||||
isActive,
|
||||
};
|
||||
return renderItem(info);
|
||||
},
|
||||
[renderItem],
|
||||
const renderGridItem = useCallback<SortableGridRenderItem<T>>(
|
||||
({ item, index }) => (
|
||||
<DraggableListItem
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
dragHandleProps={dragHandleProps}
|
||||
/>
|
||||
),
|
||||
[dragHandleProps, renderItem],
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
({ data: newData }: { data: T[] }) => {
|
||||
setIsDragging(false);
|
||||
onDragEnd(newData);
|
||||
({ indexToKey }: SortableGridDragEndParams<T>) => {
|
||||
onDragEnd(orderDroppedItems({ data, keyExtractor, indexToKey }));
|
||||
},
|
||||
[onDragEnd],
|
||||
);
|
||||
|
||||
const handleDragBegin = useCallback(() => {
|
||||
setIsDragging(true);
|
||||
onDragBeginProp?.();
|
||||
}, [onDragBeginProp]);
|
||||
|
||||
const handleRelease = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
}, []);
|
||||
|
||||
const showRefreshControl = Boolean(onRefresh) && (!isDragging || Boolean(refreshing));
|
||||
const resolvedContainerStyle =
|
||||
containerStyle ?? (scrollEnabled ? SCROLL_ENABLED_FLEX_STYLE : undefined);
|
||||
const shouldShowRefreshControl = showRefreshControl && !nestable;
|
||||
const ListComponent: typeof DraggableFlatList = (
|
||||
nestable ? (NestableDraggableFlatList as unknown) : DraggableFlatList
|
||||
) as typeof DraggableFlatList;
|
||||
|
||||
const refreshControl = useMemo(
|
||||
() =>
|
||||
shouldShowRefreshControl ? (
|
||||
<RefreshControl
|
||||
refreshing={refreshing ?? false}
|
||||
onRefresh={onRefresh}
|
||||
tintColor={theme.colors.foregroundMuted}
|
||||
colors={refreshColors}
|
||||
/>
|
||||
) : undefined,
|
||||
[shouldShowRefreshControl, refreshing, onRefresh, theme.colors.foregroundMuted, refreshColors],
|
||||
[data, keyExtractor, onDragEnd],
|
||||
);
|
||||
|
||||
return (
|
||||
<ListComponent
|
||||
testID={testID}
|
||||
data={data}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={handleRenderItem}
|
||||
onDragEnd={handleDragEnd}
|
||||
style={style}
|
||||
containerStyle={resolvedContainerStyle}
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
|
||||
scrollEnabled={scrollEnabled}
|
||||
extraData={extraData}
|
||||
simultaneousHandlers={simultaneousHandlers}
|
||||
dragGestureHostPresented={gestureHostPresented}
|
||||
// Higher activation distance reduces accidental drag capture while nested
|
||||
// lists are inside a scroll container.
|
||||
activationDistance={20}
|
||||
onDragBegin={handleDragBegin}
|
||||
onRelease={handleRelease}
|
||||
// @ts-ignore - waitFor is supported by RNGH FlatList but missing from DraggableFlatList types
|
||||
waitFor={waitFor}
|
||||
refreshControl={refreshControl}
|
||||
/>
|
||||
<View testID={testID} style={containerStyle}>
|
||||
<Sortable.Grid
|
||||
columns={1}
|
||||
data={data}
|
||||
keyExtractor={keyExtractor}
|
||||
renderItem={renderGridItem}
|
||||
onDragEnd={handleDragEnd}
|
||||
customHandle={useDragHandle}
|
||||
dragActivationDelay={DRAG_ACTIVATION_DELAY_MS}
|
||||
dragActivationFailOffset={DRAG_ACTIVATION_FAIL_OFFSET_PX}
|
||||
scrollableRef={scrollViewRef ?? undefined}
|
||||
autoScrollEnabled={scrollViewRef !== null}
|
||||
// Clamped to the single column's width, so a dragged row moves only vertically. A nested
|
||||
// list also stays inside its row: its lifted row would draw under the rows that follow.
|
||||
overDrag={nested ? "none" : "vertical"}
|
||||
// The lifted row keeps the point where the finger grabbed it.
|
||||
enableActiveItemSnap={false}
|
||||
// Rows animate only while a drag reorders them. Otherwise a project expanding or
|
||||
// collapsing would slide the rows below over its rows, and added rows would fade in.
|
||||
itemsLayoutTransitionMode="reorder"
|
||||
itemEntering={null}
|
||||
itemExiting={null}
|
||||
// Rows style themselves from `isActive`; the other rows stay as they are.
|
||||
activeItemScale={1}
|
||||
activeItemShadowOpacity={0}
|
||||
inactiveItemOpacity={1}
|
||||
inactiveItemScale={1}
|
||||
hapticsEnabled
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function DraggableListItem<T>({
|
||||
item,
|
||||
index,
|
||||
renderItem,
|
||||
dragHandleProps,
|
||||
}: {
|
||||
item: T;
|
||||
index: number;
|
||||
renderItem: (info: DraggableRenderItemInfo<T>) => ReactElement;
|
||||
dragHandleProps: DraggableListDragHandleProps | undefined;
|
||||
}) {
|
||||
const { isActive } = useItemContext();
|
||||
const [active, setActive] = useState(false);
|
||||
useAnimatedReaction(
|
||||
() => isActive.value,
|
||||
(next, previous) => {
|
||||
if (previous !== null && next !== previous) {
|
||||
scheduleOnRN(setActive, next);
|
||||
}
|
||||
},
|
||||
);
|
||||
return (
|
||||
<NestedListContext.Provider value={true}>
|
||||
{renderItem({ item, index, isActive: active, dragHandleProps })}
|
||||
</NestedListContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { ReactElement, MutableRefObject } from "react";
|
||||
import type { MutableRefObject, PropsWithChildren, ReactElement } from "react";
|
||||
import type { StyleProp, ViewStyle } from "react-native";
|
||||
import type { GestureType } from "react-native-gesture-handler";
|
||||
|
||||
/**
|
||||
* Marks the row a drag starts from when the list uses a drag handle. On web, spread
|
||||
* `attributes` and `listeners` and assign `setActivatorNodeRef` on the handle element. On
|
||||
* every platform, wrap the handle in `DraggableListHandle` with these props.
|
||||
*/
|
||||
export interface DraggableListDragHandleProps {
|
||||
/**
|
||||
* Web-only drag handle props (from dnd-kit). Spread these onto the element
|
||||
* that should initiate the drag. Native uses the `drag()` callback instead.
|
||||
*/
|
||||
attributes?: Record<string, unknown>;
|
||||
listeners?: Record<string, unknown>;
|
||||
setActivatorNodeRef?: (node: unknown) => void;
|
||||
@@ -15,53 +16,34 @@ export interface DraggableListDragHandleProps {
|
||||
export interface DraggableRenderItemInfo<T> {
|
||||
item: T;
|
||||
index: number;
|
||||
drag: () => void;
|
||||
isActive: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
}
|
||||
|
||||
export interface DraggableListProps<T> {
|
||||
data: T[];
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
keyExtractor: (item: T) => string;
|
||||
renderItem: (info: DraggableRenderItemInfo<T>) => ReactElement;
|
||||
onDragEnd: (data: T[]) => void;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
/** Outer container style (useful for nested, non-scrolling lists). */
|
||||
containerStyle?: StyleProp<ViewStyle>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
testID?: string;
|
||||
ListFooterComponent?: ReactElement | null;
|
||||
ListHeaderComponent?: ReactElement | null;
|
||||
ListEmptyComponent?: ReactElement | null;
|
||||
showsVerticalScrollIndicator?: boolean;
|
||||
/** When false, disables internal scrolling (use outer list to scroll). */
|
||||
scrollEnabled?: boolean;
|
||||
/**
|
||||
* Web-only: when true, the drag can only be initiated from the handle props
|
||||
* passed to `renderItem` (prevents nested lists from fighting).
|
||||
* When true, a drag starts only from the `DraggableListHandle` that receives the row's
|
||||
* `dragHandleProps`, so a list nested inside a row keeps its own drags.
|
||||
*/
|
||||
useDragHandle?: boolean;
|
||||
refreshing?: boolean;
|
||||
onRefresh?: () => void;
|
||||
/** Fill remaining space when content is smaller than container */
|
||||
contentContainerFlexGrow?: boolean;
|
||||
/** External row state that should invalidate virtualized native cells. */
|
||||
extraData?: unknown;
|
||||
/** Gesture ref for simultaneous handling with parent gestures (e.g., sidebar close) */
|
||||
simultaneousGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||
/** Whether the retained native gesture host is currently presented. */
|
||||
gestureHostPresented?: boolean;
|
||||
/** Gesture ref(s) that the list should wait for before handling scroll */
|
||||
waitFor?: MutableRefObject<GestureType | undefined> | MutableRefObject<GestureType | undefined>[];
|
||||
/** Called when a drag gesture begins (before items are reordered) */
|
||||
onDragBegin?: () => void;
|
||||
/** Called immediately before invoking row `drag()` to lock outer owners. */
|
||||
onDragIntent?: () => void;
|
||||
/** Called when drag interaction ends (finger released). */
|
||||
onDragRelease?: () => void;
|
||||
/**
|
||||
* Native-only: use the nestable draggable-flatlist variant for nested drag
|
||||
* lists coordinated by a shared NestableScrollContainer.
|
||||
*/
|
||||
nestable?: boolean;
|
||||
}
|
||||
|
||||
export type DraggableListHandleProps = PropsWithChildren<{
|
||||
dragHandleProps: DraggableListDragHandleProps | undefined;
|
||||
}>;
|
||||
|
||||
/** The vertical scroll view that hosts draggable lists and scrolls while a row is dragged. */
|
||||
export type DraggableListScrollViewProps = PropsWithChildren<{
|
||||
style?: StyleProp<ViewStyle>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
showsVerticalScrollIndicator?: boolean;
|
||||
testID?: string;
|
||||
/** Native-only: a gesture that keeps recognizing while the list scrolls, e.g. a panel close. */
|
||||
simultaneousGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||
}>;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useMemo, useRef, type ReactElement } from "react";
|
||||
import { memo, useMemo, useRef, type ReactElement } from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import {
|
||||
DndContext,
|
||||
@@ -17,7 +17,12 @@ import {
|
||||
useSortable,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import type { DraggableListProps, DraggableRenderItemInfo } from "./draggable-list.types";
|
||||
import type {
|
||||
DraggableListHandleProps,
|
||||
DraggableListProps,
|
||||
DraggableListScrollViewProps,
|
||||
DraggableRenderItemInfo,
|
||||
} from "./draggable-list.types";
|
||||
import { getDragActivationConstraints, useDragReorderState } from "./drag-reorder";
|
||||
|
||||
export type { DraggableListProps, DraggableRenderItemInfo };
|
||||
@@ -116,18 +121,6 @@ function SortableItemInner<T>({
|
||||
isDragging,
|
||||
} = useSortable({ id });
|
||||
|
||||
const dragRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const drag = useCallback(() => {
|
||||
// dnd-kit handles drag initiation via listeners
|
||||
// This is a no-op but matches the mobile API
|
||||
}, []);
|
||||
|
||||
// Store listeners in ref so drag handle can access them
|
||||
dragRef.current = () => {
|
||||
// Trigger drag - handled by dnd-kit's listeners
|
||||
};
|
||||
|
||||
// dnd-kit can set `scaleX/scaleY` on the active item when dragging over a
|
||||
// differently-sized droppable. For variable-height rows this can look like
|
||||
// the "ghost" stretches. Keep the dragged item's size stable by zeroing
|
||||
@@ -166,7 +159,6 @@ function SortableItemInner<T>({
|
||||
const info: DraggableRenderItemInfo<T> = {
|
||||
item,
|
||||
index,
|
||||
drag,
|
||||
isActive: activeId === id,
|
||||
dragHandleProps,
|
||||
};
|
||||
@@ -184,31 +176,44 @@ function SortableItemInner<T>({
|
||||
|
||||
const SortableItem = memo(SortableItemInner) as typeof SortableItemInner;
|
||||
|
||||
export function DraggableListScrollView({
|
||||
children,
|
||||
style,
|
||||
contentContainerStyle,
|
||||
showsVerticalScrollIndicator,
|
||||
testID,
|
||||
}: DraggableListScrollViewProps) {
|
||||
return (
|
||||
<ScrollView
|
||||
style={style}
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
|
||||
testID={testID}
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
// The handle element itself spreads `dragHandleProps` on web.
|
||||
export function DraggableListHandle({ children }: DraggableListHandleProps) {
|
||||
return children;
|
||||
}
|
||||
|
||||
export function DraggableList<T>({
|
||||
data,
|
||||
keyExtractor,
|
||||
renderItem,
|
||||
onDragEnd,
|
||||
style,
|
||||
containerStyle,
|
||||
contentContainerStyle,
|
||||
testID,
|
||||
ListFooterComponent,
|
||||
ListHeaderComponent,
|
||||
ListEmptyComponent,
|
||||
showsVerticalScrollIndicator = true,
|
||||
scrollEnabled = true,
|
||||
extraData: _extraData,
|
||||
// Native-only, as before.
|
||||
testID: _testID,
|
||||
useDragHandle = false,
|
||||
// simultaneousGestureRef is native-only, ignored on web
|
||||
onDragBegin,
|
||||
nestable: _nestable = false,
|
||||
}: DraggableListProps<T>) {
|
||||
const { activeId, items, handlers } = useDragReorderState({
|
||||
data,
|
||||
keyExtractor,
|
||||
onDragEnd,
|
||||
onDragBegin,
|
||||
});
|
||||
const activationConstraints = getDragActivationConstraints(useDragHandle, DRAG_ACTIVATION_CONFIG);
|
||||
|
||||
@@ -224,89 +229,39 @@ export function DraggableList<T>({
|
||||
}),
|
||||
);
|
||||
|
||||
const ids = useMemo(
|
||||
() => items.map((item, index) => keyExtractor(item, index)),
|
||||
[items, keyExtractor],
|
||||
);
|
||||
const ids = useMemo(() => items.map((item) => keyExtractor(item)), [items, keyExtractor]);
|
||||
const wrapperStyle = useMemo(
|
||||
() => [
|
||||
{ position: "relative" as const },
|
||||
scrollEnabled ? { flex: 1, minHeight: 0 } : null,
|
||||
containerStyle,
|
||||
],
|
||||
[scrollEnabled, containerStyle],
|
||||
() => [{ position: "relative" as const }, containerStyle],
|
||||
[containerStyle],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={wrapperStyle}>
|
||||
{scrollEnabled ? (
|
||||
<ScrollView
|
||||
testID={testID}
|
||||
style={style}
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
showsVerticalScrollIndicator={showsVerticalScrollIndicator}
|
||||
>
|
||||
{ListHeaderComponent}
|
||||
{items.length === 0 && ListEmptyComponent}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={DND_MODIFIERS}
|
||||
onDragStart={handlers.onDragStart}
|
||||
onDragCancel={handlers.onDragCancel}
|
||||
onDragEnd={handlers.onDragEnd}
|
||||
>
|
||||
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
|
||||
{items.map((item, index) => {
|
||||
const id = keyExtractor(item, index);
|
||||
return (
|
||||
<SortableItem
|
||||
key={id}
|
||||
id={id}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
activeId={activeId}
|
||||
useDragHandle={useDragHandle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
{ListFooterComponent}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<>
|
||||
{ListHeaderComponent}
|
||||
{items.length === 0 && ListEmptyComponent}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={DND_MODIFIERS}
|
||||
onDragStart={handlers.onDragStart}
|
||||
onDragCancel={handlers.onDragCancel}
|
||||
onDragEnd={handlers.onDragEnd}
|
||||
>
|
||||
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
|
||||
{items.map((item, index) => {
|
||||
const id = keyExtractor(item, index);
|
||||
return (
|
||||
<SortableItem
|
||||
key={id}
|
||||
id={id}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
activeId={activeId}
|
||||
useDragHandle={useDragHandle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
{ListFooterComponent}
|
||||
</>
|
||||
)}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
modifiers={DND_MODIFIERS}
|
||||
onDragStart={handlers.onDragStart}
|
||||
onDragCancel={handlers.onDragCancel}
|
||||
onDragEnd={handlers.onDragEnd}
|
||||
>
|
||||
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
|
||||
{items.map((item, index) => {
|
||||
const id = keyExtractor(item);
|
||||
return (
|
||||
<SortableItem
|
||||
key={id}
|
||||
id={id}
|
||||
item={item}
|
||||
index={index}
|
||||
renderItem={renderItem}
|
||||
activeId={activeId}
|
||||
useDragHandle={useDragHandle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -669,7 +669,6 @@ function MobileSidebar({
|
||||
onAddProject={handleOpenProject}
|
||||
onImportSession={handleImportSession}
|
||||
parentGestureRef={closeGestureRef}
|
||||
dragGestureHostActive={active}
|
||||
listHeaderComponent={workspacesSectionHeaderElement}
|
||||
/>
|
||||
)}
|
||||
@@ -871,7 +870,6 @@ function DesktopSidebar({
|
||||
onImportSession={handleImportSession}
|
||||
onWorkspacePress={closeSidebar}
|
||||
parentGestureRef={closeGestureRef}
|
||||
dragGestureHostActive={active}
|
||||
listHeaderComponent={workspacesSectionHeaderElement}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
type GestureResponderEvent,
|
||||
type PressableStateCallbackType,
|
||||
type StyleProp,
|
||||
@@ -39,8 +38,12 @@ import { type GestureType } from "react-native-gesture-handler";
|
||||
import { useSidebarWorkspaceRename } from "@/workspace-rename";
|
||||
import { useWorkspaceClipboardActions } from "@/hooks/use-workspace-clipboard-actions";
|
||||
import { ExternalLink, Settings, MoreVertical, Plus, Trash2 } from "lucide-react-native";
|
||||
import { NestableScrollContainer } from "react-native-draggable-flatlist";
|
||||
import { DraggableList, type DraggableRenderItemInfo } from "./draggable-list";
|
||||
import {
|
||||
DraggableList,
|
||||
DraggableListHandle,
|
||||
DraggableListScrollView,
|
||||
type DraggableRenderItemInfo,
|
||||
} from "./draggable-list";
|
||||
import type { DraggableListDragHandleProps } from "./draggable-list.types";
|
||||
import {
|
||||
getHostRuntimeStore,
|
||||
@@ -76,7 +79,6 @@ import {
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuTrigger,
|
||||
useContextMenu,
|
||||
} from "@/components/ui/context-menu";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -97,7 +99,7 @@ import {
|
||||
SidebarWorkspaceContextMenu,
|
||||
SidebarWorkspaceMenu,
|
||||
} from "@/components/sidebar/sidebar-workspace-menu";
|
||||
import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction";
|
||||
import { useRowPressGuard } from "@/components/sidebar/use-row-press-guard";
|
||||
import { SidebarSectionHeader } from "@/components/sidebar/sidebar-section-header";
|
||||
import { SidebarGroupToggleRow } from "@/components/sidebar/sidebar-group-toggle-row";
|
||||
import { createWorkspaceVisibilityPolicy } from "@/components/sidebar/sidebar-group-visibility";
|
||||
@@ -240,7 +242,6 @@ interface SidebarWorkspaceListProps {
|
||||
listHeaderComponent?: ReactElement | null;
|
||||
/** Gesture ref for coordinating with parent gestures (e.g., sidebar close) */
|
||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||
dragGestureHostActive?: boolean;
|
||||
}
|
||||
|
||||
interface ProjectHeaderRowProps {
|
||||
@@ -257,10 +258,8 @@ interface ProjectHeaderRowProps {
|
||||
onWorktreeCreated?: (workspaceId: string) => void;
|
||||
shortcutNumber?: number | null;
|
||||
showShortcutBadge?: boolean;
|
||||
drag: () => void;
|
||||
isDragging: boolean;
|
||||
isArchiving?: boolean;
|
||||
menuController: ReturnType<typeof useContextMenu> | null;
|
||||
onRemoveProject?: () => void;
|
||||
removeProjectStatus?: "idle" | "pending";
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
@@ -275,12 +274,10 @@ interface WorkspaceRowInnerProps {
|
||||
shortcutNumber: number | null;
|
||||
showShortcutBadge: boolean;
|
||||
onPress: () => void;
|
||||
drag?: () => void;
|
||||
isDragging: boolean;
|
||||
isArchiving: boolean;
|
||||
isCreating?: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
menuController: ReturnType<typeof useContextMenu> | null;
|
||||
archiveLabel?: string;
|
||||
archiveStatus?: "idle" | "pending" | "success";
|
||||
archivePendingLabel?: string;
|
||||
@@ -874,10 +871,8 @@ function ProjectHeaderRow({
|
||||
onWorktreeCreated: _onWorktreeCreated,
|
||||
shortcutNumber = null,
|
||||
showShortcutBadge = false,
|
||||
drag,
|
||||
isDragging,
|
||||
isArchiving = false,
|
||||
menuController,
|
||||
onRemoveProject,
|
||||
removeProjectStatus = "idle",
|
||||
dragHandleProps,
|
||||
@@ -903,24 +898,14 @@ function ProjectHeaderRow({
|
||||
}) as Href,
|
||||
);
|
||||
}, [displayName, onWorkspacePress, worktreeTarget]);
|
||||
const interaction = useLongPressDragInteraction({
|
||||
drag,
|
||||
menuController,
|
||||
});
|
||||
const {
|
||||
role: _dragRole,
|
||||
tabIndex: _dragTabIndex,
|
||||
"aria-roledescription": _dragRoleDescription,
|
||||
...dragAttributes
|
||||
} = dragHandleProps?.attributes ?? {};
|
||||
const interaction = useRowPressGuard();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (interaction.didLongPressRef.current) {
|
||||
interaction.didLongPressRef.current = false;
|
||||
if (interaction.consumeMovedPress()) {
|
||||
return;
|
||||
}
|
||||
onPress();
|
||||
}, [interaction.didLongPressRef, onPress]);
|
||||
}, [interaction, onPress]);
|
||||
|
||||
const handlePointerEnter = useCallback(() => {
|
||||
if (!contextMenuOpen) setIsHovered(true);
|
||||
@@ -996,10 +981,8 @@ function ProjectHeaderRow({
|
||||
|
||||
if (!onRemoveProject) {
|
||||
return (
|
||||
<View
|
||||
{...dragAttributes}
|
||||
{...dragHandleProps?.listeners}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
||||
<ProjectHeaderDragHandle
|
||||
dragHandleProps={dragHandleProps}
|
||||
onPointerEnter={handlePointerEnter}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
>
|
||||
@@ -1015,16 +998,14 @@ function ProjectHeaderRow({
|
||||
>
|
||||
{rowChildren}
|
||||
</PressHighlight>
|
||||
</View>
|
||||
</ProjectHeaderDragHandle>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenu open={contextMenuOpen} onOpenChange={handleContextMenuOpenChange}>
|
||||
<View
|
||||
{...dragAttributes}
|
||||
{...dragHandleProps?.listeners}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
||||
<ProjectHeaderDragHandle
|
||||
dragHandleProps={dragHandleProps}
|
||||
onPointerEnter={handlePointerEnter}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
>
|
||||
@@ -1041,7 +1022,7 @@ function ProjectHeaderRow({
|
||||
>
|
||||
{rowChildren}
|
||||
</ContextMenuTrigger>
|
||||
</View>
|
||||
</ProjectHeaderDragHandle>
|
||||
<ContextMenuContent
|
||||
align="start"
|
||||
width={220}
|
||||
@@ -1060,6 +1041,38 @@ function ProjectHeaderRow({
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectHeaderDragHandle({
|
||||
children,
|
||||
dragHandleProps,
|
||||
onPointerEnter,
|
||||
onPointerLeave,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
dragHandleProps: DraggableListDragHandleProps | undefined;
|
||||
onPointerEnter: () => void;
|
||||
onPointerLeave: () => void;
|
||||
}) {
|
||||
const {
|
||||
role: _dragRole,
|
||||
tabIndex: _dragTabIndex,
|
||||
"aria-roledescription": _dragRoleDescription,
|
||||
...dragAttributes
|
||||
} = dragHandleProps?.attributes ?? {};
|
||||
return (
|
||||
<DraggableListHandle dragHandleProps={dragHandleProps}>
|
||||
<View
|
||||
{...dragAttributes}
|
||||
{...dragHandleProps?.listeners}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
||||
onPointerEnter={onPointerEnter}
|
||||
onPointerLeave={onPointerLeave}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</DraggableListHandle>
|
||||
);
|
||||
}
|
||||
|
||||
function WorkspaceRowInner({
|
||||
workspace,
|
||||
hostBadge,
|
||||
@@ -1069,12 +1082,10 @@ function WorkspaceRowInner({
|
||||
shortcutNumber,
|
||||
showShortcutBadge,
|
||||
onPress,
|
||||
drag,
|
||||
isDragging,
|
||||
isArchiving,
|
||||
isCreating = false,
|
||||
dragHandleProps,
|
||||
menuController,
|
||||
archiveLabel,
|
||||
archiveStatus = "idle",
|
||||
archivePendingLabel,
|
||||
@@ -1093,10 +1104,7 @@ function WorkspaceRowInner({
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
const isTouchPlatform = platformIsNative || isCompact;
|
||||
const interaction = useLongPressDragInteraction({
|
||||
drag,
|
||||
menuController,
|
||||
});
|
||||
const interaction = useRowPressGuard();
|
||||
const {
|
||||
role: _dragRole,
|
||||
tabIndex: _dragTabIndex,
|
||||
@@ -1105,12 +1113,11 @@ function WorkspaceRowInner({
|
||||
} = dragHandleProps?.attributes ?? {};
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (interaction.didLongPressRef.current) {
|
||||
interaction.didLongPressRef.current = false;
|
||||
if (interaction.consumeMovedPress()) {
|
||||
return;
|
||||
}
|
||||
onPress();
|
||||
}, [interaction.didLongPressRef, onPress]);
|
||||
}, [interaction, onPress]);
|
||||
const handleWorkspacePressIn = useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
setIsPressed(true);
|
||||
@@ -1143,85 +1150,87 @@ function WorkspaceRowInner({
|
||||
});
|
||||
const backdrop = getSidebarRowBackdrop({ isDragging, isPressed, selected, isHovered });
|
||||
return (
|
||||
<View
|
||||
{...dragAttributes}
|
||||
{...(isEditingTitle ? null : dragHandleProps?.listeners)}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
||||
style={styles.workspaceRowContainer}
|
||||
{...hoverHandlers}
|
||||
>
|
||||
<SidebarWorkspaceContextMenu
|
||||
contextMenuOpen={contextMenuOpen}
|
||||
onContextMenuOpenChange={onContextMenuOpenChange}
|
||||
workspace={workspace}
|
||||
leadingProjectName={leadingProjectName}
|
||||
hostBadgeLabel={hostBadge?.label}
|
||||
workspaceKey={workspace.workspaceKey}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onMarkAsUnread={onMarkAsUnread}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
openInFileManagerPath={workspace.workspaceDirectory}
|
||||
disabled={isArchiving}
|
||||
enabled={!isEditingTitle}
|
||||
aria-selected={selected}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={accessibilityState}
|
||||
style={workspaceRowStyle}
|
||||
highlightStyle={styles.workspaceRowPressed}
|
||||
onPressIn={isEditingTitle ? undefined : handleWorkspacePressIn}
|
||||
onTouchMove={isEditingTitle ? undefined : interaction.handleTouchMove}
|
||||
onPressOut={isEditingTitle ? undefined : handleWorkspacePressOut}
|
||||
onPress={isEditingTitle ? undefined : handlePress}
|
||||
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
||||
<DraggableListHandle dragHandleProps={isEditingTitle ? undefined : dragHandleProps}>
|
||||
<View
|
||||
{...dragAttributes}
|
||||
{...(isEditingTitle ? null : dragHandleProps?.listeners)}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
||||
style={styles.workspaceRowContainer}
|
||||
{...hoverHandlers}
|
||||
>
|
||||
<SidebarWorkspaceRowContent
|
||||
<SidebarWorkspaceContextMenu
|
||||
contextMenuOpen={contextMenuOpen}
|
||||
onContextMenuOpenChange={onContextMenuOpenChange}
|
||||
workspace={workspace}
|
||||
hostBadge={hostBadge}
|
||||
leadingProjectName={leadingProjectName}
|
||||
leadingProjectIconDataUri={leadingProjectIconDataUri}
|
||||
serviceSummary={serviceSummary}
|
||||
backdrop={backdrop}
|
||||
isHovered={isHovered}
|
||||
isLoading={isArchiving || isCreating}
|
||||
isCreating={isCreating}
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
||||
titleEditor={titleEditor}
|
||||
hostBadgeLabel={hostBadge?.label}
|
||||
workspaceKey={workspace.workspaceKey}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onMarkAsUnread={onMarkAsUnread}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
openInFileManagerPath={workspace.workspaceDirectory}
|
||||
disabled={isArchiving}
|
||||
enabled={!isEditingTitle}
|
||||
aria-selected={selected}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={accessibilityState}
|
||||
style={workspaceRowStyle}
|
||||
highlightStyle={styles.workspaceRowPressed}
|
||||
onPressIn={isEditingTitle ? undefined : handleWorkspacePressIn}
|
||||
onTouchMove={isEditingTitle ? undefined : interaction.handleTouchMove}
|
||||
onPressOut={isEditingTitle ? undefined : handleWorkspacePressOut}
|
||||
onPress={isEditingTitle ? undefined : handlePress}
|
||||
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
||||
>
|
||||
<WorkspaceRowRightGroup
|
||||
<SidebarWorkspaceRowContent
|
||||
workspace={workspace}
|
||||
hostBadge={hostBadge}
|
||||
leadingProjectName={leadingProjectName}
|
||||
leadingProjectIconDataUri={leadingProjectIconDataUri}
|
||||
serviceSummary={serviceSummary}
|
||||
backdrop={backdrop}
|
||||
isHovered={isHovered}
|
||||
isTouchPlatform={isTouchPlatform}
|
||||
isLoading={isArchiving || isCreating}
|
||||
isCreating={isCreating}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
shortcutNumber={shortcutNumber}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
onArchive={onArchive}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onCopyPath={onCopyPath}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onMarkAsUnread={onMarkAsUnread}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
/>
|
||||
</SidebarWorkspaceRowContent>
|
||||
</SidebarWorkspaceContextMenu>
|
||||
</View>
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
||||
titleEditor={titleEditor}
|
||||
>
|
||||
<WorkspaceRowRightGroup
|
||||
workspace={workspace}
|
||||
backdrop={backdrop}
|
||||
isHovered={isHovered}
|
||||
isTouchPlatform={isTouchPlatform}
|
||||
isCreating={isCreating}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
shortcutNumber={shortcutNumber}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
onArchive={onArchive}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onCopyPath={onCopyPath}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onMarkAsUnread={onMarkAsUnread}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
/>
|
||||
</SidebarWorkspaceRowContent>
|
||||
</SidebarWorkspaceContextMenu>
|
||||
</View>
|
||||
</DraggableListHandle>
|
||||
);
|
||||
}}
|
||||
</SidebarWorkspaceRowFrame>
|
||||
@@ -1237,7 +1246,6 @@ function WorkspaceRowWithMenu({
|
||||
shortcutNumber,
|
||||
showShortcutBadge,
|
||||
onPress,
|
||||
drag,
|
||||
isDragging,
|
||||
dragHandleProps,
|
||||
canCopyBranchName,
|
||||
@@ -1254,7 +1262,6 @@ function WorkspaceRowWithMenu({
|
||||
shortcutNumber: number | null;
|
||||
showShortcutBadge: boolean;
|
||||
onPress: () => void;
|
||||
drag?: () => void;
|
||||
isDragging: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
canCopyBranchName: boolean;
|
||||
@@ -1347,12 +1354,10 @@ function WorkspaceRowWithMenu({
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
onPress={onPress}
|
||||
drag={drag}
|
||||
isDragging={isDragging}
|
||||
isArchiving={isArchiving}
|
||||
isCreating={isCreating}
|
||||
dragHandleProps={dragHandleProps}
|
||||
menuController={null}
|
||||
archiveLabel={t("sidebar.workspace.actions.archive")}
|
||||
archiveStatus={isArchiving ? "pending" : "idle"}
|
||||
archivePendingLabel={t("sidebar.workspace.actions.archiving")}
|
||||
@@ -1389,7 +1394,6 @@ interface WorkspaceRowItemProps {
|
||||
selectionEnabled: boolean;
|
||||
activeWorkspaceSelection: ActiveWorkspaceSelection | null;
|
||||
onWorkspacePress?: () => void;
|
||||
drag?: () => void;
|
||||
isDragging?: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
}
|
||||
@@ -1409,7 +1413,6 @@ function WorkspaceRowItem({
|
||||
isCreating = false,
|
||||
selectionEnabled,
|
||||
activeWorkspaceSelection,
|
||||
drag,
|
||||
isDragging = false,
|
||||
dragHandleProps,
|
||||
}: WorkspaceRowItemProps) {
|
||||
@@ -1440,7 +1443,6 @@ function WorkspaceRowItem({
|
||||
enabled: selectionEnabled,
|
||||
})}
|
||||
onPress={handlePress}
|
||||
drag={drag}
|
||||
isDragging={isDragging}
|
||||
dragHandleProps={dragHandleProps}
|
||||
/>
|
||||
@@ -1477,7 +1479,6 @@ function areWorkspaceRowItemPropsEqual(
|
||||
previous.reserveIdleStatusIndicatorSpace === next.reserveIdleStatusIndicatorSpace &&
|
||||
previous.isCreating === next.isCreating &&
|
||||
previous.onWorkspacePress === next.onWorkspacePress &&
|
||||
previous.drag === next.drag &&
|
||||
previous.isDragging === next.isDragging &&
|
||||
previous.dragHandleProps === next.dragHandleProps &&
|
||||
previousSelected === nextSelected
|
||||
@@ -1494,7 +1495,6 @@ function WorkspaceRow({
|
||||
shortcutNumber,
|
||||
showShortcutBadge,
|
||||
onPress,
|
||||
drag,
|
||||
isDragging,
|
||||
dragHandleProps,
|
||||
canCopyBranchName,
|
||||
@@ -1511,7 +1511,6 @@ function WorkspaceRow({
|
||||
shortcutNumber: number | null;
|
||||
showShortcutBadge: boolean;
|
||||
onPress: () => void;
|
||||
drag?: () => void;
|
||||
isDragging: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
canCopyBranchName: boolean;
|
||||
@@ -1535,7 +1534,6 @@ function WorkspaceRow({
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
onPress={onPress}
|
||||
drag={drag}
|
||||
isDragging={isDragging}
|
||||
dragHandleProps={dragHandleProps}
|
||||
canCopyBranchName={canCopyBranchName}
|
||||
@@ -1558,13 +1556,9 @@ function ProjectBlock({
|
||||
shortcutIndexByWorkspaceKey,
|
||||
onToggleCollapsed,
|
||||
onWorkspacePress,
|
||||
parentGestureRef,
|
||||
onWorkspaceReorder,
|
||||
useNestable,
|
||||
dragGestureHostActive,
|
||||
manualSort,
|
||||
onWorktreeCreated,
|
||||
drag,
|
||||
isDragging,
|
||||
dragHandleProps,
|
||||
creatingWorkspaceIds,
|
||||
@@ -1584,13 +1578,9 @@ function ProjectBlock({
|
||||
shortcutIndexByWorkspaceKey: Map<string, number>;
|
||||
onToggleCollapsed: (projectViewKey: string) => void;
|
||||
onWorkspacePress?: () => void;
|
||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||
onWorkspaceReorder: (projectViewKey: string, workspaces: SidebarWorkspacePlacement[]) => void;
|
||||
useNestable: boolean;
|
||||
dragGestureHostActive?: boolean;
|
||||
manualSort: boolean;
|
||||
onWorktreeCreated?: (workspaceId: string) => void;
|
||||
drag: () => void;
|
||||
isDragging: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
creatingWorkspaceIds: ReadonlySet<string>;
|
||||
@@ -1646,7 +1636,6 @@ function ProjectBlock({
|
||||
(
|
||||
item: SidebarWorkspacePlacement,
|
||||
input?: {
|
||||
drag?: () => void;
|
||||
isDragging?: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
},
|
||||
@@ -1666,7 +1655,6 @@ function ProjectBlock({
|
||||
selectionEnabled={selectionEnabled}
|
||||
activeWorkspaceSelection={activeWorkspaceSelection}
|
||||
onWorkspacePress={onWorkspacePress}
|
||||
drag={input?.drag}
|
||||
isDragging={input?.isDragging}
|
||||
dragHandleProps={input?.dragHandleProps}
|
||||
/>
|
||||
@@ -1690,12 +1678,10 @@ function ProjectBlock({
|
||||
const renderWorkspace = useCallback(
|
||||
({
|
||||
item,
|
||||
drag: workspaceDrag,
|
||||
isActive,
|
||||
dragHandleProps: workspaceDragHandleProps,
|
||||
}: DraggableRenderItemInfo<SidebarWorkspacePlacement>) => {
|
||||
return renderWorkspaceRow(item, {
|
||||
drag: workspaceDrag,
|
||||
isDragging: isActive,
|
||||
dragHandleProps: workspaceDragHandleProps,
|
||||
});
|
||||
@@ -1785,12 +1771,7 @@ function ProjectBlock({
|
||||
keyExtractor={workspaceKeyExtractor}
|
||||
renderItem={renderWorkspace}
|
||||
onDragEnd={handleWorkspaceDragEnd}
|
||||
extraData={activeWorkspaceSelectionKey(activeWorkspaceSelection)}
|
||||
scrollEnabled={false}
|
||||
useDragHandle
|
||||
nestable={useNestable}
|
||||
simultaneousGestureRef={parentGestureRef}
|
||||
gestureHostPresented={dragGestureHostActive}
|
||||
containerStyle={styles.workspaceListContainer}
|
||||
/>
|
||||
</>
|
||||
@@ -1844,10 +1825,8 @@ function ProjectBlock({
|
||||
isProjectActive={active}
|
||||
onWorkspacePress={onWorkspacePress}
|
||||
onWorktreeCreated={onWorktreeCreated}
|
||||
drag={drag}
|
||||
isDragging={isDragging}
|
||||
isArchiving={isRemovingProject}
|
||||
menuController={null}
|
||||
onRemoveProject={handleRemoveProject}
|
||||
removeProjectStatus={isRemovingProject ? "pending" : "idle"}
|
||||
dragHandleProps={dragHandleProps}
|
||||
@@ -1877,13 +1856,9 @@ function areProjectBlockPropsEqual(previous: ProjectBlockProps, next: ProjectBlo
|
||||
previous.onToggleWorkspacePin === next.onToggleWorkspacePin &&
|
||||
previous.onToggleCollapsed === next.onToggleCollapsed &&
|
||||
previous.onWorkspacePress === next.onWorkspacePress &&
|
||||
previous.parentGestureRef === next.parentGestureRef &&
|
||||
previous.onWorkspaceReorder === next.onWorkspaceReorder &&
|
||||
previous.useNestable === next.useNestable &&
|
||||
previous.dragGestureHostActive === next.dragGestureHostActive &&
|
||||
previous.manualSort === next.manualSort &&
|
||||
previous.onWorktreeCreated === next.onWorktreeCreated &&
|
||||
previous.drag === next.drag &&
|
||||
previous.isDragging === next.isDragging &&
|
||||
previous.dragHandleProps === next.dragHandleProps &&
|
||||
previous.creatingWorkspaceIds === next.creatingWorkspaceIds &&
|
||||
@@ -1965,7 +1940,6 @@ export function SidebarWorkspaceList({
|
||||
listFooterComponent,
|
||||
listHeaderComponent,
|
||||
parentGestureRef,
|
||||
dragGestureHostActive,
|
||||
}: SidebarWorkspaceListProps) {
|
||||
const pathname = usePathname();
|
||||
const hosts = useHosts();
|
||||
@@ -2037,8 +2011,6 @@ export function SidebarWorkspaceList({
|
||||
onPinnedWorkspaceReorder={handlePinnedWorkspaceReorder}
|
||||
listHeaderComponent={listHeaderComponent}
|
||||
sidebarFilterEmpty={sidebarFilterEmpty}
|
||||
parentGestureRef={parentGestureRef}
|
||||
dragGestureHostActive={dragGestureHostActive}
|
||||
/>
|
||||
) : (
|
||||
<ProjectModeList
|
||||
@@ -2057,7 +2029,6 @@ export function SidebarWorkspaceList({
|
||||
sidebarFilterEmpty={sidebarFilterEmpty}
|
||||
hasActiveProjectFilter={hasActiveProjectFilter}
|
||||
parentGestureRef={parentGestureRef}
|
||||
dragGestureHostActive={dragGestureHostActive}
|
||||
pathname={pathname}
|
||||
hostBadgeByServerId={hostBadgeByServerId}
|
||||
supportsMultiplicityByServerId={supportsMultiplicityByServerId}
|
||||
@@ -2089,8 +2060,6 @@ function SidebarGroupedModeList({
|
||||
onPinnedWorkspaceReorder,
|
||||
listHeaderComponent,
|
||||
sidebarFilterEmpty,
|
||||
parentGestureRef,
|
||||
dragGestureHostActive,
|
||||
}: {
|
||||
workspaceGroups: SidebarWorkspaceGroup[];
|
||||
pinnedGroups: PinnedSidebarGroups;
|
||||
@@ -2104,8 +2073,6 @@ function SidebarGroupedModeList({
|
||||
onPinnedWorkspaceReorder: (workspaces: SidebarWorkspacePlacement[]) => void;
|
||||
listHeaderComponent?: ReactElement | null;
|
||||
sidebarFilterEmpty: boolean;
|
||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||
dragGestureHostActive?: boolean;
|
||||
}) {
|
||||
const showShortcutBadges = useShowShortcutBadges();
|
||||
const pinnedWorkspaces = useMemo(
|
||||
@@ -2131,8 +2098,6 @@ function SidebarGroupedModeList({
|
||||
onPinnedWorkspaceReorder={onPinnedWorkspaceReorder}
|
||||
listHeaderComponent={listHeaderComponent}
|
||||
sidebarFilterEmpty={sidebarFilterEmpty}
|
||||
parentGestureRef={parentGestureRef}
|
||||
dragGestureHostActive={dragGestureHostActive}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2153,7 +2118,6 @@ function ProjectModeList({
|
||||
sidebarFilterEmpty,
|
||||
hasActiveProjectFilter,
|
||||
parentGestureRef,
|
||||
dragGestureHostActive,
|
||||
pathname,
|
||||
hostBadgeByServerId,
|
||||
supportsMultiplicityByServerId,
|
||||
@@ -2214,19 +2178,6 @@ function ProjectModeList({
|
||||
items: pinnedChats,
|
||||
selection: selectionEnabled ? activeWorkspaceSelection : null,
|
||||
});
|
||||
const nativeScrollGestureProps = useMemo(
|
||||
() =>
|
||||
parentGestureRef
|
||||
? ({
|
||||
// NestableScrollContainer forwards props to RNGH ScrollView. Keep
|
||||
// vertical scroll and sidebar close pan simultaneous: vertical
|
||||
// intent scrolls immediately, clear horizontal intent can still
|
||||
// activate close from inside the list.
|
||||
simultaneousHandlers: parentGestureRef,
|
||||
} as object)
|
||||
: undefined,
|
||||
[parentGestureRef],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timeouts = creatingWorkspaceTimeoutsRef.current;
|
||||
@@ -2351,7 +2302,6 @@ function ProjectModeList({
|
||||
(
|
||||
item: SidebarProjectEntry,
|
||||
dragState: {
|
||||
drag: () => void;
|
||||
isDragging: boolean;
|
||||
dragHandleProps?: DraggableRenderItemInfo<SidebarProjectEntry>["dragHandleProps"];
|
||||
},
|
||||
@@ -2370,12 +2320,8 @@ function ProjectModeList({
|
||||
onToggleCollapsed={onToggleProjectCollapsed}
|
||||
onWorkspacePress={onWorkspacePress}
|
||||
manualSort={manualSort}
|
||||
parentGestureRef={parentGestureRef}
|
||||
onWorkspaceReorder={handleWorkspaceReorder}
|
||||
useNestable={platformIsNative}
|
||||
dragGestureHostActive={dragGestureHostActive}
|
||||
onWorktreeCreated={handleWorktreeCreated}
|
||||
drag={dragState.drag}
|
||||
isDragging={dragState.isDragging}
|
||||
dragHandleProps={dragState.dragHandleProps}
|
||||
creatingWorkspaceIds={creatingWorkspaceIds}
|
||||
@@ -2392,8 +2338,6 @@ function ProjectModeList({
|
||||
activeWorkspaceSelection,
|
||||
handleWorktreeCreated,
|
||||
manualSort,
|
||||
parentGestureRef,
|
||||
dragGestureHostActive,
|
||||
handleWorkspaceReorder,
|
||||
hostBadgeByServerId,
|
||||
supportsMultiplicityByServerId,
|
||||
@@ -2411,8 +2355,8 @@ function ProjectModeList({
|
||||
);
|
||||
|
||||
const renderProject = useCallback(
|
||||
({ item, drag, isActive, dragHandleProps }: DraggableRenderItemInfo<SidebarProjectEntry>) =>
|
||||
renderProjectBlock(item, { drag, isDragging: isActive, dragHandleProps }),
|
||||
({ item, isActive, dragHandleProps }: DraggableRenderItemInfo<SidebarProjectEntry>) =>
|
||||
renderProjectBlock(item, { isDragging: isActive, dragHandleProps }),
|
||||
[renderProjectBlock],
|
||||
);
|
||||
|
||||
@@ -2420,7 +2364,6 @@ function ProjectModeList({
|
||||
(
|
||||
workspace: SidebarWorkspacePlacement,
|
||||
input?: {
|
||||
drag?: () => void;
|
||||
isDragging?: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
},
|
||||
@@ -2443,7 +2386,6 @@ function ProjectModeList({
|
||||
selectionEnabled={selectionEnabled}
|
||||
activeWorkspaceSelection={activeWorkspaceSelection}
|
||||
onWorkspacePress={onWorkspacePress}
|
||||
drag={input?.drag}
|
||||
isDragging={input?.isDragging}
|
||||
dragHandleProps={input?.dragHandleProps}
|
||||
/>
|
||||
@@ -2466,12 +2408,10 @@ function ProjectModeList({
|
||||
const renderPinnedChat = useCallback(
|
||||
({
|
||||
item: workspace,
|
||||
drag,
|
||||
isActive,
|
||||
dragHandleProps,
|
||||
}: DraggableRenderItemInfo<SidebarWorkspacePlacement>) => {
|
||||
return renderPinnedChatRow(workspace, {
|
||||
drag,
|
||||
isDragging: isActive,
|
||||
dragHandleProps,
|
||||
});
|
||||
@@ -2489,12 +2429,7 @@ function ProjectModeList({
|
||||
keyExtractor={projectViewKeyExtractor}
|
||||
renderItem={renderProject}
|
||||
onDragEnd={handleProjectDragEnd}
|
||||
extraData={activeWorkspaceSelectionKey(activeWorkspaceSelection)}
|
||||
scrollEnabled={false}
|
||||
useDragHandle
|
||||
nestable={platformIsNative}
|
||||
simultaneousGestureRef={parentGestureRef}
|
||||
gestureHostPresented={dragGestureHostActive}
|
||||
containerStyle={styles.projectListContainer}
|
||||
/>
|
||||
);
|
||||
@@ -2518,12 +2453,7 @@ function ProjectModeList({
|
||||
keyExtractor={workspaceKeyExtractor}
|
||||
renderItem={renderPinnedChat}
|
||||
onDragEnd={onPinnedWorkspaceReorder}
|
||||
extraData={activeWorkspaceSelectionKey(activeWorkspaceSelection)}
|
||||
scrollEnabled={false}
|
||||
useDragHandle
|
||||
nestable={platformIsNative}
|
||||
simultaneousGestureRef={parentGestureRef}
|
||||
gestureHostPresented={dragGestureHostActive}
|
||||
containerStyle={styles.workspaceListContainer}
|
||||
/>
|
||||
{canTogglePinnedChats ? (
|
||||
@@ -2556,26 +2486,17 @@ function ProjectModeList({
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{platformIsNative ? (
|
||||
<NestableScrollContainer
|
||||
{...nativeScrollGestureProps}
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID="sidebar-project-workspace-list-scroll"
|
||||
>
|
||||
{content}
|
||||
</NestableScrollContainer>
|
||||
) : (
|
||||
<ScrollView
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID="sidebar-project-workspace-list-scroll"
|
||||
>
|
||||
{content}
|
||||
</ScrollView>
|
||||
)}
|
||||
{/* Vertical scroll and the sidebar close pan recognize together: vertical intent
|
||||
scrolls immediately, and clear horizontal intent still closes from inside the list. */}
|
||||
<DraggableListScrollView
|
||||
simultaneousGestureRef={parentGestureRef}
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID="sidebar-project-workspace-list-scroll"
|
||||
>
|
||||
{content}
|
||||
</DraggableListScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
import { PressHighlight as Pressable } from "@/components/ui/press-highlight";
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
type MutableRefObject,
|
||||
type ReactNode,
|
||||
type Ref,
|
||||
} from "react";
|
||||
import { memo, useCallback, useMemo, useState, type ReactNode, type Ref } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
type GestureResponderEvent,
|
||||
type PressableStateCallbackType,
|
||||
} from "react-native";
|
||||
import { NestableScrollContainer } from "react-native-draggable-flatlist";
|
||||
import type { GestureType } from "react-native-gesture-handler";
|
||||
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
|
||||
import { useActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
|
||||
import { type SidebarWorkspaceEntry } from "@/hooks/use-sidebar-workspaces-list";
|
||||
@@ -77,9 +66,14 @@ import { SidebarSectionHeader } from "@/components/sidebar/sidebar-section-heade
|
||||
import { SidebarGroupToggleRow } from "@/components/sidebar/sidebar-group-toggle-row";
|
||||
import { useLimitedSidebarGroup } from "@/components/sidebar/use-limited-sidebar-group";
|
||||
import type { ToggleSidebarWorkspacePin } from "@/hooks/use-sidebar-workspace-pin";
|
||||
import { DraggableList, type DraggableRenderItemInfo } from "@/components/draggable-list";
|
||||
import {
|
||||
DraggableList,
|
||||
DraggableListHandle,
|
||||
DraggableListScrollView,
|
||||
type DraggableRenderItemInfo,
|
||||
} from "@/components/draggable-list";
|
||||
import type { DraggableListDragHandleProps } from "@/components/draggable-list.types";
|
||||
import { useLongPressDragInteraction } from "@/components/sidebar/use-long-press-drag-interaction";
|
||||
import { useRowPressGuard } from "@/components/sidebar/use-row-press-guard";
|
||||
|
||||
// Themed icon wrappers
|
||||
const mutedGroupIconProps = densityIconProps("sm", (theme) => ({
|
||||
@@ -126,8 +120,6 @@ interface StatusWorkspaceListProps {
|
||||
listHeaderComponent?: ReactNode;
|
||||
/** Swaps the group list for the label filter's empty state. Never the header above it. */
|
||||
sidebarFilterEmpty?: boolean;
|
||||
parentGestureRef?: MutableRefObject<GestureType | undefined>;
|
||||
dragGestureHostActive?: boolean;
|
||||
}
|
||||
|
||||
export function SidebarStatusWorkspaceList({
|
||||
@@ -143,8 +135,6 @@ export function SidebarStatusWorkspaceList({
|
||||
onPinnedWorkspaceReorder,
|
||||
listHeaderComponent,
|
||||
sidebarFilterEmpty = false,
|
||||
parentGestureRef,
|
||||
dragGestureHostActive,
|
||||
}: StatusWorkspaceListProps) {
|
||||
const { t } = useTranslation();
|
||||
const activeWorkspaceSelection = useActiveWorkspaceSelection();
|
||||
@@ -170,7 +160,6 @@ export function SidebarStatusWorkspaceList({
|
||||
(
|
||||
workspace: SidebarWorkspaceEntry,
|
||||
input?: {
|
||||
drag?: () => void;
|
||||
isDragging?: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
},
|
||||
@@ -189,7 +178,6 @@ export function SidebarStatusWorkspaceList({
|
||||
canPin={supportsPinningByServerId.get(workspace.serverId) === true}
|
||||
onToggleWorkspacePin={onToggleWorkspacePin}
|
||||
onWorkspacePress={onWorkspacePress}
|
||||
drag={input?.drag}
|
||||
isDragging={input?.isDragging}
|
||||
dragHandleProps={input?.dragHandleProps}
|
||||
/>
|
||||
@@ -207,12 +195,10 @@ export function SidebarStatusWorkspaceList({
|
||||
const renderPinnedWorkspace = useCallback(
|
||||
({
|
||||
item: workspace,
|
||||
drag,
|
||||
isActive,
|
||||
dragHandleProps,
|
||||
}: DraggableRenderItemInfo<SidebarWorkspaceEntry>) =>
|
||||
renderPinnedWorkspaceRow(workspace, {
|
||||
drag,
|
||||
isDragging: isActive,
|
||||
dragHandleProps,
|
||||
}),
|
||||
@@ -237,11 +223,7 @@ export function SidebarStatusWorkspaceList({
|
||||
keyExtractor={statusWorkspaceKeyExtractor}
|
||||
renderItem={renderPinnedWorkspace}
|
||||
onDragEnd={onPinnedWorkspaceReorder}
|
||||
scrollEnabled={false}
|
||||
useDragHandle
|
||||
nestable={platformIsNative}
|
||||
simultaneousGestureRef={parentGestureRef}
|
||||
gestureHostPresented={dragGestureHostActive}
|
||||
/>
|
||||
{canTogglePinnedWorkspaces ? (
|
||||
<SidebarGroupToggleRow
|
||||
@@ -275,25 +257,14 @@ export function SidebarStatusWorkspaceList({
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{platformIsNative ? (
|
||||
<NestableScrollContainer
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID="sidebar-status-list-scroll"
|
||||
>
|
||||
{content}
|
||||
</NestableScrollContainer>
|
||||
) : (
|
||||
<ScrollView
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID="sidebar-status-list-scroll"
|
||||
>
|
||||
{content}
|
||||
</ScrollView>
|
||||
)}
|
||||
<DraggableListScrollView
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
testID="sidebar-status-list-scroll"
|
||||
>
|
||||
{content}
|
||||
</DraggableListScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -543,7 +514,6 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
||||
onToggleWorkspacePin,
|
||||
reserveIdleStatusIndicatorSpace = true,
|
||||
inStatusGroup = true,
|
||||
drag,
|
||||
isDragging = false,
|
||||
dragHandleProps,
|
||||
}: {
|
||||
@@ -562,7 +532,6 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
||||
*/
|
||||
inStatusGroup?: boolean;
|
||||
onWorkspacePress?: () => void;
|
||||
drag?: () => void;
|
||||
isDragging?: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
}) {
|
||||
@@ -590,7 +559,6 @@ const StatusWorkspaceRow = memo(function StatusWorkspaceRow({
|
||||
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
||||
inStatusGroup={inStatusGroup}
|
||||
onPress={handlePress}
|
||||
drag={drag}
|
||||
isDragging={isDragging}
|
||||
dragHandleProps={dragHandleProps}
|
||||
/>
|
||||
@@ -610,7 +578,6 @@ function StatusWorkspaceRowWithMenu({
|
||||
reserveIdleStatusIndicatorSpace = true,
|
||||
inStatusGroup = true,
|
||||
onPress,
|
||||
drag,
|
||||
isDragging = false,
|
||||
dragHandleProps,
|
||||
}: {
|
||||
@@ -630,7 +597,6 @@ function StatusWorkspaceRowWithMenu({
|
||||
*/
|
||||
inStatusGroup?: boolean;
|
||||
onPress: () => void;
|
||||
drag?: () => void;
|
||||
isDragging?: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
}) {
|
||||
@@ -735,7 +701,6 @@ function StatusWorkspaceRowWithMenu({
|
||||
onTogglePin={onTogglePin}
|
||||
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
||||
inStatusGroup={inStatusGroup}
|
||||
drag={drag}
|
||||
isDragging={isDragging}
|
||||
dragHandleProps={dragHandleProps}
|
||||
/>
|
||||
@@ -771,26 +736,20 @@ interface StatusWorkspaceRowInnerProps {
|
||||
reserveIdleStatusIndicatorSpace?: boolean;
|
||||
/** Pinned rows are flat under their own header; status-group rows indent from theirs. */
|
||||
inStatusGroup?: boolean;
|
||||
drag?: () => void;
|
||||
isDragging?: boolean;
|
||||
dragHandleProps?: DraggableListDragHandleProps;
|
||||
}
|
||||
|
||||
function StatusWorkspaceRowInner(props: StatusWorkspaceRowInnerProps) {
|
||||
if (props.drag) {
|
||||
return <DraggableStatusWorkspaceRowInner {...props} drag={props.drag} />;
|
||||
if (props.dragHandleProps) {
|
||||
return <DraggableStatusWorkspaceRowInner {...props} />;
|
||||
}
|
||||
return <StatusWorkspaceRowInnerContent {...props} />;
|
||||
}
|
||||
|
||||
function DraggableStatusWorkspaceRowInner(
|
||||
props: StatusWorkspaceRowInnerProps & { drag: () => void },
|
||||
) {
|
||||
const dragInteraction = useLongPressDragInteraction({
|
||||
drag: props.drag,
|
||||
menuController: null,
|
||||
});
|
||||
return <StatusWorkspaceRowInnerContent {...props} dragInteraction={dragInteraction} />;
|
||||
function DraggableStatusWorkspaceRowInner(props: StatusWorkspaceRowInnerProps) {
|
||||
const pressGuard = useRowPressGuard();
|
||||
return <StatusWorkspaceRowInnerContent {...props} pressGuard={pressGuard} />;
|
||||
}
|
||||
|
||||
function StatusWorkspaceRowInnerContent({
|
||||
@@ -820,9 +779,9 @@ function StatusWorkspaceRowInnerContent({
|
||||
inStatusGroup = true,
|
||||
isDragging = false,
|
||||
dragHandleProps,
|
||||
dragInteraction,
|
||||
pressGuard,
|
||||
}: StatusWorkspaceRowInnerProps & {
|
||||
dragInteraction?: ReturnType<typeof useLongPressDragInteraction>;
|
||||
pressGuard?: ReturnType<typeof useRowPressGuard>;
|
||||
}) {
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const isTouchPlatform = platformIsNative || isCompact;
|
||||
@@ -840,17 +799,16 @@ function StatusWorkspaceRowInnerContent({
|
||||
|
||||
const accessibilityState = useMemo(() => ({ selected }), [selected]);
|
||||
const isEditingTitle = titleEditor !== null;
|
||||
const didLongPressRef = dragInteraction?.didLongPressRef;
|
||||
const startDragPress = dragInteraction?.handlePressIn;
|
||||
const moveDragPress = dragInteraction?.handleTouchMove;
|
||||
const endDragPress = dragInteraction?.handlePressOut;
|
||||
const consumeMovedPress = pressGuard?.consumeMovedPress;
|
||||
const startDragPress = pressGuard?.handlePressIn;
|
||||
const moveDragPress = pressGuard?.handleTouchMove;
|
||||
const endDragPress = pressGuard?.handlePressOut;
|
||||
const handlePress = useCallback(() => {
|
||||
if (didLongPressRef?.current) {
|
||||
didLongPressRef.current = false;
|
||||
if (consumeMovedPress?.()) {
|
||||
return;
|
||||
}
|
||||
onPress();
|
||||
}, [didLongPressRef, onPress]);
|
||||
}, [consumeMovedPress, onPress]);
|
||||
const handlePressIn = useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
setIsPressed(true);
|
||||
@@ -895,87 +853,89 @@ function StatusWorkspaceRowInnerContent({
|
||||
});
|
||||
const backdrop = getSidebarRowBackdrop({ isDragging, isPressed, selected, isHovered });
|
||||
return (
|
||||
<View
|
||||
{...dragAttributes}
|
||||
{...(isEditingTitle ? null : dragHandleProps?.listeners)}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
||||
style={styles.workspaceRowContainer}
|
||||
{...hoverHandlers}
|
||||
>
|
||||
<SidebarWorkspaceContextMenu
|
||||
contextMenuOpen={contextMenuOpen}
|
||||
onContextMenuOpenChange={onContextMenuOpenChange}
|
||||
workspace={workspace}
|
||||
leadingProjectName={projectName}
|
||||
hostBadgeLabel={hostBadge?.label}
|
||||
serviceSummary={serviceSummary}
|
||||
workspaceKey={workspace.workspaceKey}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onMarkAsUnread={onMarkAsUnread}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
openInFileManagerPath={workspace.workspaceDirectory}
|
||||
disabled={isArchiving}
|
||||
enabled={!isEditingTitle}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={accessibilityState}
|
||||
style={workspaceRowStyle}
|
||||
highlightStyle={styles.workspaceRowPressed}
|
||||
onPressIn={isEditingTitle ? undefined : handlePressIn}
|
||||
onTouchMove={isEditingTitle ? undefined : moveDragPress}
|
||||
onPressOut={isEditingTitle ? undefined : handlePressOut}
|
||||
onPress={isEditingTitle ? undefined : handlePress}
|
||||
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
||||
<DraggableListHandle dragHandleProps={isEditingTitle ? undefined : dragHandleProps}>
|
||||
<View
|
||||
{...dragAttributes}
|
||||
{...(isEditingTitle ? null : dragHandleProps?.listeners)}
|
||||
ref={dragHandleProps?.setActivatorNodeRef as unknown as Ref<View>}
|
||||
style={styles.workspaceRowContainer}
|
||||
{...hoverHandlers}
|
||||
>
|
||||
<SidebarWorkspaceRowContent
|
||||
<SidebarWorkspaceContextMenu
|
||||
contextMenuOpen={contextMenuOpen}
|
||||
onContextMenuOpenChange={onContextMenuOpenChange}
|
||||
workspace={workspace}
|
||||
hostBadge={hostBadge}
|
||||
leadingProjectName={projectName}
|
||||
leadingProjectIconDataUri={projectIconDataUri}
|
||||
hostBadgeLabel={hostBadge?.label}
|
||||
serviceSummary={serviceSummary}
|
||||
backdrop={backdrop}
|
||||
isHovered={isHovered}
|
||||
isLoading={isArchiving}
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
||||
titleEditor={titleEditor}
|
||||
workspaceKey={workspace.workspaceKey}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onMarkAsUnread={onMarkAsUnread}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
openInFileManagerPath={workspace.workspaceDirectory}
|
||||
disabled={isArchiving}
|
||||
enabled={!isEditingTitle}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={accessibilityState}
|
||||
style={workspaceRowStyle}
|
||||
highlightStyle={styles.workspaceRowPressed}
|
||||
onPressIn={isEditingTitle ? undefined : handlePressIn}
|
||||
onTouchMove={isEditingTitle ? undefined : moveDragPress}
|
||||
onPressOut={isEditingTitle ? undefined : handlePressOut}
|
||||
onPress={isEditingTitle ? undefined : handlePress}
|
||||
testID={`sidebar-workspace-row-${workspace.workspaceKey}`}
|
||||
>
|
||||
{renderSlot ? (
|
||||
<StatusWorkspaceActionSlot
|
||||
workspace={workspace}
|
||||
backdrop={backdrop}
|
||||
trailing={trailing}
|
||||
trailingPresentation={trailingPresentation}
|
||||
showKebab={showKebabInSlot}
|
||||
showArchiveButton={showArchiveButton}
|
||||
showScrim={showScrim}
|
||||
reserveSlotWidth={reserveSlotWidth}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onMarkAsUnread={onMarkAsUnread}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
/>
|
||||
) : null}
|
||||
</SidebarWorkspaceRowContent>
|
||||
</SidebarWorkspaceContextMenu>
|
||||
</View>
|
||||
<SidebarWorkspaceRowContent
|
||||
workspace={workspace}
|
||||
hostBadge={hostBadge}
|
||||
leadingProjectName={projectName}
|
||||
leadingProjectIconDataUri={projectIconDataUri}
|
||||
serviceSummary={serviceSummary}
|
||||
backdrop={backdrop}
|
||||
isHovered={isHovered}
|
||||
isLoading={isArchiving}
|
||||
shortcutNumber={shortcutNumber}
|
||||
showShortcutBadge={showShortcutBadge}
|
||||
reserveIdleStatusIndicatorSpace={reserveIdleStatusIndicatorSpace}
|
||||
titleEditor={titleEditor}
|
||||
>
|
||||
{renderSlot ? (
|
||||
<StatusWorkspaceActionSlot
|
||||
workspace={workspace}
|
||||
backdrop={backdrop}
|
||||
trailing={trailing}
|
||||
trailingPresentation={trailingPresentation}
|
||||
showKebab={showKebabInSlot}
|
||||
showArchiveButton={showArchiveButton}
|
||||
showScrim={showScrim}
|
||||
reserveSlotWidth={reserveSlotWidth}
|
||||
isPinned={isPinned}
|
||||
onTogglePin={onTogglePin}
|
||||
onCopyPath={onCopyPath}
|
||||
onCopyBranchName={onCopyBranchName}
|
||||
onRename={onRename}
|
||||
onMarkAsRead={onMarkAsRead}
|
||||
onMarkAsUnread={onMarkAsUnread}
|
||||
onArchive={onArchive}
|
||||
archiveLabel={archiveLabel}
|
||||
archiveStatus={archiveStatus}
|
||||
archivePendingLabel={archivePendingLabel}
|
||||
archiveShortcutKeys={archiveShortcutKeys}
|
||||
/>
|
||||
) : null}
|
||||
</SidebarWorkspaceRowContent>
|
||||
</SidebarWorkspaceContextMenu>
|
||||
</View>
|
||||
</DraggableListHandle>
|
||||
);
|
||||
}}
|
||||
</SidebarWorkspaceRowFrame>
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import type { GestureResponderEvent } from "react-native";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { isWeb as platformIsWeb } from "@/constants/platform";
|
||||
import { decideLongPressMove } from "@/utils/sidebar-gesture-arbitration";
|
||||
import type { useContextMenu } from "@/components/ui/context-menu";
|
||||
|
||||
export function useLongPressDragInteraction(input: {
|
||||
drag?: () => void;
|
||||
menuController: ReturnType<typeof useContextMenu> | null;
|
||||
}) {
|
||||
const { drag, menuController } = input;
|
||||
const didLongPressRef = useRef(false);
|
||||
const dragArmedRef = useRef(false);
|
||||
const dragActivatedRef = useRef(false);
|
||||
const didStartDragRef = useRef(false);
|
||||
const scrollIntentRef = useRef(false);
|
||||
const menuOpenedRef = useRef(false);
|
||||
const touchStartRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const touchCurrentRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const dragArmTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const contextMenuTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const clearTimers = useCallback(() => {
|
||||
if (dragArmTimerRef.current) {
|
||||
clearTimeout(dragArmTimerRef.current);
|
||||
dragArmTimerRef.current = null;
|
||||
}
|
||||
if (contextMenuTimerRef.current) {
|
||||
clearTimeout(contextMenuTimerRef.current);
|
||||
contextMenuTimerRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openContextMenuAtStartPoint = useCallback(() => {
|
||||
if (!menuController || !touchStartRef.current) {
|
||||
return;
|
||||
}
|
||||
menuController.setAnchorRect({
|
||||
x: touchStartRef.current.x,
|
||||
y: touchStartRef.current.y,
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
menuController.setOpen(true);
|
||||
menuOpenedRef.current = true;
|
||||
didLongPressRef.current = true;
|
||||
}, [menuController]);
|
||||
|
||||
const handleLongPress = useCallback(() => {
|
||||
// Manual timers own long-press behavior on mobile.
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
clearTimers();
|
||||
};
|
||||
}, [clearTimers]);
|
||||
|
||||
const armTimers = useCallback(() => {
|
||||
clearTimers();
|
||||
if (platformIsWeb || !drag) {
|
||||
return;
|
||||
}
|
||||
|
||||
const DRAG_ARM_DELAY_MS = 180;
|
||||
const DRAG_ARM_STATIONARY_SLOP_PX = 4;
|
||||
const CONTEXT_MENU_DELAY_MS = 450;
|
||||
const CONTEXT_MENU_STATIONARY_SLOP_PX = 6;
|
||||
|
||||
dragArmTimerRef.current = setTimeout(() => {
|
||||
if (scrollIntentRef.current || didStartDragRef.current || menuOpenedRef.current) {
|
||||
return;
|
||||
}
|
||||
const start = touchStartRef.current;
|
||||
const current = touchCurrentRef.current ?? start;
|
||||
if (!start || !current) {
|
||||
return;
|
||||
}
|
||||
const dx = current.x - start.x;
|
||||
const dy = current.y - start.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance > DRAG_ARM_STATIONARY_SLOP_PX) {
|
||||
return;
|
||||
}
|
||||
dragArmedRef.current = true;
|
||||
dragActivatedRef.current = true;
|
||||
didLongPressRef.current = true;
|
||||
void Haptics.selectionAsync().catch(() => {});
|
||||
drag();
|
||||
}, DRAG_ARM_DELAY_MS);
|
||||
|
||||
if (!menuController) {
|
||||
return;
|
||||
}
|
||||
|
||||
contextMenuTimerRef.current = setTimeout(() => {
|
||||
if (scrollIntentRef.current || didStartDragRef.current || menuOpenedRef.current) {
|
||||
return;
|
||||
}
|
||||
const start = touchStartRef.current;
|
||||
const current = touchCurrentRef.current ?? start;
|
||||
if (!start || !current) {
|
||||
return;
|
||||
}
|
||||
const dx = current.x - start.x;
|
||||
const dy = current.y - start.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
if (distance > CONTEXT_MENU_STATIONARY_SLOP_PX) {
|
||||
return;
|
||||
}
|
||||
void Haptics.selectionAsync().catch(() => {});
|
||||
openContextMenuAtStartPoint();
|
||||
}, CONTEXT_MENU_DELAY_MS);
|
||||
}, [clearTimers, drag, menuController, openContextMenuAtStartPoint]);
|
||||
|
||||
const handleDragIntent = useCallback(
|
||||
(_details: { dx: number; dy: number; distance: number }) => {
|
||||
if (!dragActivatedRef.current) {
|
||||
return;
|
||||
}
|
||||
didStartDragRef.current = true;
|
||||
didLongPressRef.current = true;
|
||||
clearTimers();
|
||||
void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium).catch(() => {});
|
||||
},
|
||||
[clearTimers],
|
||||
);
|
||||
|
||||
const handleScrollIntent = useCallback(
|
||||
(_details: { dx: number; dy: number; distance: number }) => {
|
||||
scrollIntentRef.current = true;
|
||||
didLongPressRef.current = true;
|
||||
clearTimers();
|
||||
},
|
||||
[clearTimers],
|
||||
);
|
||||
|
||||
const handleSwipeIntent = useCallback(
|
||||
(_details: { dx: number; dy: number; distance: number }) => {
|
||||
didLongPressRef.current = true;
|
||||
clearTimers();
|
||||
},
|
||||
[clearTimers],
|
||||
);
|
||||
|
||||
const handlePressIn = useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
didLongPressRef.current = false;
|
||||
dragArmedRef.current = false;
|
||||
dragActivatedRef.current = false;
|
||||
didStartDragRef.current = false;
|
||||
scrollIntentRef.current = false;
|
||||
menuOpenedRef.current = false;
|
||||
touchStartRef.current = {
|
||||
x: event.nativeEvent.pageX,
|
||||
y: event.nativeEvent.pageY,
|
||||
};
|
||||
touchCurrentRef.current = {
|
||||
x: event.nativeEvent.pageX,
|
||||
y: event.nativeEvent.pageY,
|
||||
};
|
||||
armTimers();
|
||||
},
|
||||
[armTimers],
|
||||
);
|
||||
|
||||
const handleTouchMove = useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
const start = touchStartRef.current;
|
||||
if (!start || didStartDragRef.current || menuOpenedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const touch = event?.nativeEvent?.touches?.[0] ?? event?.nativeEvent;
|
||||
const x = touch?.pageX;
|
||||
const y = touch?.pageY;
|
||||
if (typeof x !== "number" || typeof y !== "number") {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = { x, y };
|
||||
touchCurrentRef.current = current;
|
||||
const dx = current.x - start.x;
|
||||
const dy = current.y - start.y;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const decision = decideLongPressMove({
|
||||
dragArmed: dragArmedRef.current,
|
||||
didStartDrag: didStartDragRef.current,
|
||||
startPoint: start,
|
||||
currentPoint: current,
|
||||
});
|
||||
|
||||
if (decision === "vertical_scroll") {
|
||||
handleScrollIntent({ dx, dy, distance });
|
||||
return;
|
||||
}
|
||||
|
||||
if (decision === "horizontal_swipe" || decision === "cancel_long_press") {
|
||||
handleSwipeIntent({ dx, dy, distance });
|
||||
return;
|
||||
}
|
||||
|
||||
if (decision === "start_drag") {
|
||||
handleDragIntent({ dx, dy, distance });
|
||||
}
|
||||
},
|
||||
[handleDragIntent, handleScrollIntent, handleSwipeIntent],
|
||||
);
|
||||
|
||||
const handlePressOut = useCallback(() => {
|
||||
clearTimers();
|
||||
dragArmedRef.current = false;
|
||||
dragActivatedRef.current = false;
|
||||
touchStartRef.current = null;
|
||||
touchCurrentRef.current = null;
|
||||
}, [clearTimers]);
|
||||
|
||||
return {
|
||||
didLongPressRef,
|
||||
handleLongPress,
|
||||
handlePressIn,
|
||||
handleTouchMove,
|
||||
handlePressOut,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import type { GestureResponderEvent } from "react-native";
|
||||
import { didPressMove } from "@/utils/sidebar-gesture-arbitration";
|
||||
|
||||
/**
|
||||
* Tracks a row press so a touch that scrolled, swiped, or drifted does not navigate when it is
|
||||
* released. Wire the handlers to the row's Pressable and ask `consumeMovedPress` in `onPress`.
|
||||
*/
|
||||
export function useRowPressGuard() {
|
||||
const movedRef = useRef(false);
|
||||
const startPointRef = useRef<{ x: number; y: number } | null>(null);
|
||||
|
||||
const handlePressIn = useCallback((event: GestureResponderEvent) => {
|
||||
movedRef.current = false;
|
||||
startPointRef.current = { x: event.nativeEvent.pageX, y: event.nativeEvent.pageY };
|
||||
}, []);
|
||||
|
||||
const handleTouchMove = useCallback((event: GestureResponderEvent) => {
|
||||
const startPoint = startPointRef.current;
|
||||
if (!startPoint || movedRef.current) {
|
||||
return;
|
||||
}
|
||||
const touch = event?.nativeEvent?.touches?.[0] ?? event?.nativeEvent;
|
||||
const x = touch?.pageX;
|
||||
const y = touch?.pageY;
|
||||
if (typeof x !== "number" || typeof y !== "number") {
|
||||
return;
|
||||
}
|
||||
movedRef.current = didPressMove({ startPoint, currentPoint: { x, y } });
|
||||
}, []);
|
||||
|
||||
const handlePressOut = useCallback(() => {
|
||||
startPointRef.current = null;
|
||||
}, []);
|
||||
|
||||
const consumeMovedPress = useCallback(() => {
|
||||
const moved = movedRef.current;
|
||||
movedRef.current = false;
|
||||
return moved;
|
||||
}, []);
|
||||
|
||||
return useMemo(
|
||||
() => ({ handlePressIn, handleTouchMove, handlePressOut, consumeMovedPress }),
|
||||
[consumeMovedPress, handlePressIn, handlePressOut, handleTouchMove],
|
||||
);
|
||||
}
|
||||
@@ -40,7 +40,6 @@ function SortableInlineListItem<T>({
|
||||
const info: DraggableRenderItemInfo<T> = {
|
||||
item,
|
||||
index,
|
||||
drag: () => {},
|
||||
isActive: false,
|
||||
};
|
||||
return renderItem(info);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo, type ReactElement } from "react";
|
||||
import { useMemo, type ReactElement } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@@ -61,11 +61,6 @@ function SortableItem<T>({
|
||||
isDragging,
|
||||
} = useSortable({ id, disabled, data: itemData });
|
||||
|
||||
const drag = useCallback(() => {
|
||||
// dnd-kit handles drag initiation via listeners
|
||||
// This is a no-op but matches the mobile API
|
||||
}, []);
|
||||
|
||||
// External DnD contexts render their own insertion affordance, so keep the
|
||||
// tab row static and let the DragOverlay carry the moving chip.
|
||||
const baseTransform = externalDndContext
|
||||
@@ -89,7 +84,6 @@ function SortableItem<T>({
|
||||
const info: DraggableRenderItemInfo<T> = {
|
||||
item,
|
||||
index,
|
||||
drag,
|
||||
isActive: activeId === id,
|
||||
dragHandleProps: useDragHandle
|
||||
? {
|
||||
|
||||
@@ -1,110 +1,27 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
decideLongPressMove,
|
||||
shouldOpenContextMenuOnPressOut,
|
||||
} from "./sidebar-gesture-arbitration";
|
||||
import { didPressMove } from "./sidebar-gesture-arbitration";
|
||||
|
||||
describe("decideLongPressMove", () => {
|
||||
it("keeps long press pending for small movement before long-press arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 3, y: 2 },
|
||||
}),
|
||||
).toBe("none");
|
||||
const start = { x: 0, y: 0 };
|
||||
|
||||
describe("didPressMove", () => {
|
||||
it("keeps a small movement a tap", () => {
|
||||
expect(didPressMove({ startPoint: start, currentPoint: { x: 3, y: 2 } })).toBe(false);
|
||||
});
|
||||
|
||||
it("cancels long press when movement exceeds cancel slop before arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 8, y: 8 },
|
||||
}),
|
||||
).toBe("cancel_long_press");
|
||||
it("counts movement past the cancel slop", () => {
|
||||
expect(didPressMove({ startPoint: start, currentPoint: { x: 8, y: 8 } })).toBe(true);
|
||||
});
|
||||
|
||||
it("yields to vertical scroll before drag arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 2, y: 7 },
|
||||
}),
|
||||
).toBe("vertical_scroll");
|
||||
it("counts a clearly vertical move sooner, as a scroll", () => {
|
||||
expect(didPressMove({ startPoint: start, currentPoint: { x: 2, y: 7 } })).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps diagonal motion neutral before drag arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 5, y: 7 },
|
||||
}),
|
||||
).toBe("none");
|
||||
it("keeps a short diagonal move a tap", () => {
|
||||
expect(didPressMove({ startPoint: start, currentPoint: { x: 5, y: 7 } })).toBe(false);
|
||||
});
|
||||
|
||||
it("yields to horizontal swipe before drag arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: false,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: -10, y: 2 },
|
||||
}),
|
||||
).toBe("horizontal_swipe");
|
||||
});
|
||||
|
||||
it("starts drag when movement exceeds drag slop after long-press arm", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: true,
|
||||
didStartDrag: false,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 0, y: 9 },
|
||||
}),
|
||||
).toBe("start_drag");
|
||||
});
|
||||
|
||||
it("does nothing when drag already started", () => {
|
||||
expect(
|
||||
decideLongPressMove({
|
||||
dragArmed: true,
|
||||
didStartDrag: true,
|
||||
startPoint: { x: 0, y: 0 },
|
||||
currentPoint: { x: 20, y: 20 },
|
||||
}),
|
||||
).toBe("none");
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldOpenContextMenuOnPressOut", () => {
|
||||
it("opens menu only when long-press is armed and drag did not start", () => {
|
||||
expect(
|
||||
shouldOpenContextMenuOnPressOut({
|
||||
longPressArmed: true,
|
||||
didStartDrag: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
shouldOpenContextMenuOnPressOut({
|
||||
longPressArmed: false,
|
||||
didStartDrag: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldOpenContextMenuOnPressOut({
|
||||
longPressArmed: true,
|
||||
didStartDrag: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
it("counts a clearly horizontal move sooner, as a swipe", () => {
|
||||
expect(didPressMove({ startPoint: start, currentPoint: { x: -10, y: 2 } })).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,57 +1,25 @@
|
||||
export type LongPressMoveDecision =
|
||||
| "none"
|
||||
| "cancel_long_press"
|
||||
| "vertical_scroll"
|
||||
| "horizontal_swipe"
|
||||
| "start_drag";
|
||||
|
||||
export function decideLongPressMove(input: {
|
||||
dragArmed: boolean;
|
||||
didStartDrag: boolean;
|
||||
startPoint: { x: number; y: number } | null;
|
||||
currentPoint: { x: number; y: number };
|
||||
cancelSlopPx?: number;
|
||||
scrollSlopPx?: number;
|
||||
swipeSlopPx?: number;
|
||||
directionalDominanceRatio?: number;
|
||||
dragSlopPx?: number;
|
||||
}): LongPressMoveDecision {
|
||||
const cancelSlopPx = input.cancelSlopPx ?? 10;
|
||||
const scrollSlopPx = input.scrollSlopPx ?? 6;
|
||||
const swipeSlopPx = input.swipeSlopPx ?? 8;
|
||||
const directionalDominanceRatio = input.directionalDominanceRatio ?? 1.5;
|
||||
const dragSlopPx = input.dragSlopPx ?? 8;
|
||||
|
||||
if (!input.startPoint) {
|
||||
return "none";
|
||||
}
|
||||
if (input.didStartDrag) {
|
||||
return "none";
|
||||
}
|
||||
|
||||
const dx = input.currentPoint.x - input.startPoint.x;
|
||||
const dy = input.currentPoint.y - input.startPoint.y;
|
||||
const absDx = Math.abs(dx);
|
||||
const absDy = Math.abs(dy);
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
const clearlyVertical = absDy >= absDx * directionalDominanceRatio;
|
||||
const clearlyHorizontal = absDx >= absDy * directionalDominanceRatio;
|
||||
|
||||
if (!input.dragArmed) {
|
||||
if (clearlyVertical && absDy > scrollSlopPx) {
|
||||
return "vertical_scroll";
|
||||
}
|
||||
if (clearlyHorizontal && absDx > swipeSlopPx) {
|
||||
return "horizontal_swipe";
|
||||
}
|
||||
return distance > cancelSlopPx ? "cancel_long_press" : "none";
|
||||
}
|
||||
return distance > dragSlopPx ? "start_drag" : "none";
|
||||
interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export function shouldOpenContextMenuOnPressOut(input: {
|
||||
longPressArmed: boolean;
|
||||
didStartDrag: boolean;
|
||||
}): boolean {
|
||||
return input.longPressArmed && !input.didStartDrag;
|
||||
const SCROLL_SLOP_PX = 6;
|
||||
const SWIPE_SLOP_PX = 8;
|
||||
const CANCEL_SLOP_PX = 10;
|
||||
const DIRECTIONAL_DOMINANCE_RATIO = 1.5;
|
||||
|
||||
/**
|
||||
* Whether a row press moved far enough to be a scroll, a swipe, or a drift rather than a tap.
|
||||
* A clearly vertical or horizontal move counts sooner than a diagonal one.
|
||||
*/
|
||||
export function didPressMove(input: { startPoint: Point; currentPoint: Point }): boolean {
|
||||
const absDx = Math.abs(input.currentPoint.x - input.startPoint.x);
|
||||
const absDy = Math.abs(input.currentPoint.y - input.startPoint.y);
|
||||
if (absDy >= absDx * DIRECTIONAL_DOMINANCE_RATIO && absDy > SCROLL_SLOP_PX) {
|
||||
return true;
|
||||
}
|
||||
if (absDx >= absDy * DIRECTIONAL_DOMINANCE_RATIO && absDx > SWIPE_SLOP_PX) {
|
||||
return true;
|
||||
}
|
||||
return Math.sqrt(absDx * absDx + absDy * absDy) > CANCEL_SLOP_PX;
|
||||
}
|
||||
|
||||
@@ -1,475 +0,0 @@
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts b/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts
|
||||
index d3a4f1f..083bf19 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts
|
||||
+++ b/node_modules/react-native-draggable-flatlist/lib/typescript/components/DraggableFlatList.d.ts
|
||||
@@ -16,12 +16,14 @@ declare const _default: <T>(props: Omit<FlatListProps<T>, "keyExtractor" | "data
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
onDragBegin?: ((index: number) => void) | undefined;
|
||||
onDragEnd?: ((params: import("../types").DragEndParams<T>) => void) | undefined;
|
||||
+ onDragTerminate?: (() => void) | undefined;
|
||||
onPlaceholderIndexChange?: ((placeholderIndex: number) => void) | undefined;
|
||||
onRelease?: ((index: number) => void) | undefined;
|
||||
onScrollOffsetChange?: ((scrollOffset: number) => void) | undefined;
|
||||
renderItem: import("../types").RenderItem<T>;
|
||||
renderPlaceholder?: import("../types").RenderPlaceholder<T> | undefined;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[] | undefined;
|
||||
+ dragGestureHostPresented?: boolean | undefined;
|
||||
outerScrollOffset?: Animated.SharedValue<number> | undefined;
|
||||
onAnimValInit?: ((animVals: {
|
||||
activeCellOffset: import("react-native-reanimated").SharedValue<number>;
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts b/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts
|
||||
index d3c6cbb..36e9645 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts
|
||||
+++ b/node_modules/react-native-draggable-flatlist/lib/typescript/components/NestableDraggableFlatList.d.ts
|
||||
@@ -15,12 +15,14 @@ export declare const NestableDraggableFlatList: <T>(props: Omit<import("react-na
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
onDragBegin?: ((index: number) => void) | undefined;
|
||||
onDragEnd?: ((params: import("../types").DragEndParams<T>) => void) | undefined;
|
||||
+ onDragTerminate?: (() => void) | undefined;
|
||||
onPlaceholderIndexChange?: ((placeholderIndex: number) => void) | undefined;
|
||||
onRelease?: ((index: number) => void) | undefined;
|
||||
onScrollOffsetChange?: ((scrollOffset: number) => void) | undefined;
|
||||
renderItem: import("../types").RenderItem<T>;
|
||||
renderPlaceholder?: import("../types").RenderPlaceholder<T> | undefined;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[] | undefined;
|
||||
+ dragGestureHostPresented?: boolean | undefined;
|
||||
outerScrollOffset?: Animated.SharedValue<number> | undefined;
|
||||
onAnimValInit?: ((animVals: {
|
||||
activeCellOffset: import("react-native-reanimated").SharedValue<number>;
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts b/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts
|
||||
index 2abb4a2..8ca967f 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts
|
||||
+++ b/node_modules/react-native-draggable-flatlist/lib/typescript/types.d.ts
|
||||
@@ -23,12 +23,16 @@ export declare type DraggableFlatListProps<T> = Modify<FlatListProps<T>, {
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
onDragBegin?: (index: number) => void;
|
||||
onDragEnd?: (params: DragEndParams<T>) => void;
|
||||
+ /** @internal Used by the nestable wrapper to release its outer-scroll lock on cancellation. */
|
||||
+ onDragTerminate?: () => void;
|
||||
onPlaceholderIndexChange?: (placeholderIndex: number) => void;
|
||||
onRelease?: (index: number) => void;
|
||||
onScrollOffsetChange?: (scrollOffset: number) => void;
|
||||
renderItem: RenderItem<T>;
|
||||
renderPlaceholder?: RenderPlaceholder<T>;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[];
|
||||
+ /** @internal Forces handler reattachment when a retained native host is presented. */
|
||||
+ dragGestureHostPresented?: boolean;
|
||||
outerScrollOffset?: Animated.SharedValue<number>;
|
||||
onAnimValInit?: (animVals: ReturnType<typeof useAnimatedValues>) => void;
|
||||
itemEnteringAnimation?: AnimateProps<Animated.View>["entering"];
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
||||
index 7c88afc..a74140f 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
||||
+++ b/node_modules/react-native-draggable-flatlist/src/components/DraggableFlatList.tsx
|
||||
@@ -16,9 +16,11 @@ import {
|
||||
FlatList,
|
||||
Gesture,
|
||||
GestureDetector,
|
||||
+ State,
|
||||
} from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
+ runOnUI,
|
||||
useAnimatedReaction,
|
||||
useAnimatedScrollHandler,
|
||||
useSharedValue,
|
||||
@@ -92,6 +94,17 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
activeCellOffset.value = -1;
|
||||
setActiveKey(null);
|
||||
});
|
||||
+ const terminalHandled = useSharedValue(false);
|
||||
+ const gestureSessionId = useSharedValue(0);
|
||||
+ const gestureSessionActive = useSharedValue(false);
|
||||
+ const currentGestureSessionRef = useRef<{
|
||||
+ id: number;
|
||||
+ active: boolean;
|
||||
+ } | null>(null);
|
||||
+ const activeDragRef = useRef<{
|
||||
+ sessionId: number;
|
||||
+ index: number;
|
||||
+ } | null>(null);
|
||||
|
||||
const {
|
||||
dragHitSlop = DEFAULT_PROPS.dragHitSlop,
|
||||
@@ -144,19 +157,88 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
});
|
||||
}, [props.data, keyExtractor, keyToIndexRef]);
|
||||
|
||||
+ const onDragCancel = useStableCallback((sessionId: number, index: number) => {
|
||||
+ if (activeDragRef.current?.sessionId !== sessionId) return;
|
||||
+ activeDragRef.current = null;
|
||||
+ reset();
|
||||
+ props.onRelease?.(index);
|
||||
+ props.onDragTerminate?.();
|
||||
+ });
|
||||
+
|
||||
+ useEffect(() => {
|
||||
+ return () => {
|
||||
+ const activeDrag = activeDragRef.current;
|
||||
+ if (!activeDrag) return;
|
||||
+ activeDragRef.current = null;
|
||||
+ propsRef.current.onRelease?.(activeDrag.index);
|
||||
+ propsRef.current.onDragTerminate?.();
|
||||
+ };
|
||||
+ }, [propsRef]);
|
||||
+
|
||||
+ const onGestureBegin = useStableCallback((sessionId: number) => {
|
||||
+ currentGestureSessionRef.current = { id: sessionId, active: true };
|
||||
+ });
|
||||
+
|
||||
+ const onGestureFinalize = useStableCallback(
|
||||
+ (sessionId: number, committed: boolean) => {
|
||||
+ if (currentGestureSessionRef.current?.id === sessionId) {
|
||||
+ currentGestureSessionRef.current.active = false;
|
||||
+ }
|
||||
+ if (!committed && activeDragRef.current?.sessionId === sessionId) {
|
||||
+ onDragCancel(sessionId, activeDragRef.current.index);
|
||||
+ }
|
||||
+ }
|
||||
+ );
|
||||
+
|
||||
+ const onDragActivationRejected = useStableCallback((sessionId: number) => {
|
||||
+ if (activeDragRef.current?.sessionId === sessionId) {
|
||||
+ onDragCancel(sessionId, activeDragRef.current.index);
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ const activateDragOnUI = (
|
||||
+ sessionId: number,
|
||||
+ index: number,
|
||||
+ cellOffset: number,
|
||||
+ cellSize: number,
|
||||
+ hasCellMeasurements: boolean
|
||||
+ ) => {
|
||||
+ "worklet";
|
||||
+ if (
|
||||
+ gestureSessionId.value !== sessionId ||
|
||||
+ !gestureSessionActive.value ||
|
||||
+ terminalHandled.value
|
||||
+ ) {
|
||||
+ runOnJS(onDragActivationRejected)(sessionId);
|
||||
+ return;
|
||||
+ }
|
||||
+ if (hasCellMeasurements) {
|
||||
+ activeCellOffset.value = cellOffset;
|
||||
+ activeCellSize.value = cellSize;
|
||||
+ }
|
||||
+ spacerIndexAnim.value = index;
|
||||
+ activeIndexAnim.value = index;
|
||||
+ };
|
||||
+
|
||||
const drag = useStableCallback((activeKey: string) => {
|
||||
- if (disabled.value) return;
|
||||
+ const gestureSession = currentGestureSessionRef.current;
|
||||
+ if (disabled.value || !gestureSession?.active) return;
|
||||
const index = keyToIndexRef.current.get(activeKey);
|
||||
const cellData = cellDataRef.current.get(activeKey);
|
||||
- if (cellData) {
|
||||
- activeCellOffset.value = cellData.measurements.offset;
|
||||
- activeCellSize.value = cellData.measurements.size;
|
||||
- }
|
||||
|
||||
const { onDragBegin } = propsRef.current;
|
||||
if (index !== undefined) {
|
||||
- spacerIndexAnim.value = index;
|
||||
- activeIndexAnim.value = index;
|
||||
+ runOnUI(activateDragOnUI)(
|
||||
+ gestureSession.id,
|
||||
+ index,
|
||||
+ cellData?.measurements.offset ?? -1,
|
||||
+ cellData?.measurements.size ?? -1,
|
||||
+ Boolean(cellData)
|
||||
+ );
|
||||
+ activeDragRef.current = {
|
||||
+ sessionId: gestureSession.id,
|
||||
+ index,
|
||||
+ };
|
||||
setActiveKey(activeKey);
|
||||
onDragBegin?.(index);
|
||||
}
|
||||
@@ -218,8 +300,25 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
props.onRelease?.(index);
|
||||
});
|
||||
|
||||
+ const finalizeCancelledDrag = () => {
|
||||
+ "worklet";
|
||||
+ const activeIndex = activeIndexAnim.value;
|
||||
+ if (terminalHandled.value || activeIndex < 0) return;
|
||||
+
|
||||
+ terminalHandled.value = true;
|
||||
+ isTouchActiveNative.value = false;
|
||||
+ touchTranslate.value = 0;
|
||||
+ activeIndexAnim.value = -1;
|
||||
+ spacerIndexAnim.value = -1;
|
||||
+ activeCellSize.value = -1;
|
||||
+ activeCellOffset.value = -1;
|
||||
+ disabled.value = false;
|
||||
+ };
|
||||
+
|
||||
const onDragEnd = useStableCallback(
|
||||
- ({ from, to }: { from: number; to: number }) => {
|
||||
+ ({ from, to, sessionId }: { from: number; to: number; sessionId: number }) => {
|
||||
+ if (activeDragRef.current?.sessionId !== sessionId) return;
|
||||
+ activeDragRef.current = null;
|
||||
const { onDragEnd, data } = props;
|
||||
|
||||
const newData = [...data];
|
||||
@@ -238,26 +337,6 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
props.onPlaceholderIndexChange?.(index);
|
||||
});
|
||||
|
||||
- // Handle case where user ends drag without moving their finger.
|
||||
- useAnimatedReaction(
|
||||
- () => {
|
||||
- return isTouchActiveNative.value;
|
||||
- },
|
||||
- (cur, prev) => {
|
||||
- if (cur !== prev && !cur) {
|
||||
- const hasMoved = !!touchTranslate.value;
|
||||
- if (!hasMoved && activeIndexAnim.value >= 0 && !disabled.value) {
|
||||
- runOnJS(onRelease)(activeIndexAnim.value);
|
||||
- runOnJS(onDragEnd)({
|
||||
- from: activeIndexAnim.value,
|
||||
- to: spacerIndexAnim.value,
|
||||
- });
|
||||
- }
|
||||
- }
|
||||
- },
|
||||
- [isTouchActiveNative, onDragEnd, onRelease]
|
||||
- );
|
||||
-
|
||||
useAnimatedReaction(
|
||||
() => {
|
||||
return spacerIndexAnim.value;
|
||||
@@ -271,11 +350,24 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
);
|
||||
|
||||
const gestureDisabled = useSharedValue(false);
|
||||
+ const touchStartX = useSharedValue(0);
|
||||
+ const touchStartY = useSharedValue(0);
|
||||
+ const manualPanActivated = useSharedValue(false);
|
||||
+ const coordinatesDragWithExternalGesture = Boolean(props.simultaneousHandlers);
|
||||
|
||||
- const panGesture = Gesture.Pan()
|
||||
+ let panGesture = Gesture.Pan()
|
||||
.onBegin((evt) => {
|
||||
+ manualPanActivated.value = false;
|
||||
gestureDisabled.value = disabled.value;
|
||||
if (gestureDisabled.value) return;
|
||||
+ const sessionId = gestureSessionId.value + 1;
|
||||
+ gestureSessionId.value = sessionId;
|
||||
+ gestureSessionActive.value = true;
|
||||
+ terminalHandled.value = false;
|
||||
+ runOnJS(onGestureBegin)(sessionId);
|
||||
+ panGestureState.value = evt.state;
|
||||
+ })
|
||||
+ .onStart((evt) => {
|
||||
panGestureState.value = evt.state;
|
||||
})
|
||||
.onUpdate((evt) => {
|
||||
@@ -286,8 +378,15 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
: evt.translationY;
|
||||
touchTranslate.value = translation;
|
||||
})
|
||||
- .onEnd((evt) => {
|
||||
+ .onEnd((evt, success) => {
|
||||
if (gestureDisabled.value) return;
|
||||
+ if (
|
||||
+ !success ||
|
||||
+ terminalHandled.value ||
|
||||
+ !gestureSessionActive.value
|
||||
+ )
|
||||
+ return;
|
||||
+
|
||||
// Set touch val to current translate val
|
||||
isTouchActiveNative.value = false;
|
||||
const translation = horizontalAnim.value
|
||||
@@ -299,6 +398,7 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
|
||||
// Only call onDragEnd if actually dragging a cell
|
||||
if (activeIndexAnim.value === -1 || disabled.value) return;
|
||||
+ terminalHandled.value = true;
|
||||
disabled.value = true;
|
||||
runOnJS(onRelease)(activeIndexAnim.value);
|
||||
const springTo = placeholderOffset.value - activeCellOffset.value;
|
||||
@@ -309,22 +409,84 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
runOnJS(onDragEnd)({
|
||||
from: activeIndexAnim.value,
|
||||
to: spacerIndexAnim.value,
|
||||
+ sessionId: gestureSessionId.value,
|
||||
});
|
||||
disabled.value = false;
|
||||
}
|
||||
);
|
||||
})
|
||||
- .onTouchesDown(() => {
|
||||
+ .onFinalize((evt) => {
|
||||
+ manualPanActivated.value = false;
|
||||
+ panGestureState.value = evt.state;
|
||||
+ runOnJS(onContainerTouchEnd)();
|
||||
+ if (gestureDisabled.value) return;
|
||||
+
|
||||
+ const sessionId = gestureSessionId.value;
|
||||
+ const committed = terminalHandled.value;
|
||||
+ gestureSessionActive.value = false;
|
||||
+ if (!committed) finalizeCancelledDrag();
|
||||
+ runOnJS(onGestureFinalize)(sessionId, committed);
|
||||
+ })
|
||||
+ .onTouchesDown((event) => {
|
||||
+ const touch = event.changedTouches[0];
|
||||
+ if (touch) {
|
||||
+ touchStartX.value = touch.absoluteX;
|
||||
+ touchStartY.value = touch.absoluteY;
|
||||
+ }
|
||||
runOnJS(onContainerTouchStart)();
|
||||
})
|
||||
- .onTouchesUp(() => {
|
||||
+ .onTouchesMove((event, stateManager) => {
|
||||
+ if (!coordinatesDragWithExternalGesture) return;
|
||||
+
|
||||
+ // Manual activation arbitrates only while the pan is waiting. Failing or
|
||||
+ // reactivating an active recognizer can leave RNGH/Reanimated livelocked.
|
||||
+ if (manualPanActivated.value) return;
|
||||
+
|
||||
+ 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);
|
||||
+
|
||||
+ if (activeIndexAnim.value < 0) {
|
||||
+ if (absDeltaX > 6 || absDeltaY > 6) {
|
||||
+ stateManager.fail();
|
||||
+ }
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ if (absDeltaX > 10 && absDeltaX > absDeltaY) {
|
||||
+ stateManager.fail();
|
||||
+ } else if (absDeltaY >= activationDistanceProp && absDeltaY > absDeltaX) {
|
||||
+ manualPanActivated.value = true;
|
||||
+ stateManager.activate();
|
||||
+ }
|
||||
+ })
|
||||
+ .onTouchesUp((event, stateManager) => {
|
||||
// Turning this into a worklet causes timing issues. We want it to run
|
||||
// just after the finger lifts.
|
||||
runOnJS(onContainerTouchEnd)();
|
||||
+ // A manually activated iOS pan can remain BEGAN after the last finger
|
||||
+ // lifts. Fail it explicitly so onFinalize cancels the armed row.
|
||||
+ if (event.numberOfTouches === 0 && panGestureState.value === State.BEGAN) {
|
||||
+ stateManager.fail();
|
||||
+ }
|
||||
+ })
|
||||
+ .onTouchesCancelled((_event, stateManager) => {
|
||||
+ if (panGestureState.value === State.BEGAN) {
|
||||
+ stateManager.fail();
|
||||
+ }
|
||||
});
|
||||
|
||||
if (dragHitSlop) panGesture.hitSlop(dragHitSlop);
|
||||
- if (activationDistanceProp) {
|
||||
+ if (coordinatesDragWithExternalGesture) {
|
||||
+ panGesture = panGesture.manualActivation(true);
|
||||
+ } else if (activationDistanceProp) {
|
||||
const activeOffset = [-activationDistanceProp, activationDistanceProp];
|
||||
if (props.horizontal) {
|
||||
panGesture.activeOffsetX(activeOffset);
|
||||
@@ -332,6 +494,21 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
panGesture.activeOffsetY(activeOffset);
|
||||
}
|
||||
}
|
||||
+ if (props.simultaneousHandlers) {
|
||||
+ panGesture.simultaneousWithExternalGesture(
|
||||
+ ...(Array.isArray(props.simultaneousHandlers)
|
||||
+ ? props.simultaneousHandlers
|
||||
+ : [props.simultaneousHandlers])
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
+ // GestureDetector only updates handler config when a retained host reappears
|
||||
+ // with the same native tag. Changing the gesture count forces a native
|
||||
+ // reattachment without remounting the list subtree.
|
||||
+ const detectorGesture =
|
||||
+ props.dragGestureHostPresented === false
|
||||
+ ? Gesture.Simultaneous(panGesture, Gesture.Manual().enabled(false))
|
||||
+ : panGesture;
|
||||
|
||||
const onScroll = useStableCallback((scrollOffset: number) => {
|
||||
props.onScrollOffsetChange?.(scrollOffset);
|
||||
@@ -373,8 +550,9 @@ function DraggableFlatListInner<T>(props: DraggableFlatListProps<T>) {
|
||||
horizontal={!!props.horizontal}
|
||||
layoutAnimationDisabled={layoutAnimationDisabled}
|
||||
>
|
||||
- <GestureDetector gesture={panGesture}>
|
||||
+ <GestureDetector gesture={detectorGesture}>
|
||||
<Animated.View
|
||||
+ collapsable={false}
|
||||
style={props.containerStyle}
|
||||
ref={containerRef}
|
||||
onLayout={onContainerLayout}
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx b/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
||||
index 1559352..33eb5de 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
||||
+++ b/node_modules/react-native-draggable-flatlist/src/components/NestableDraggableFlatList.tsx
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef, useState } from "react";
|
||||
-import { findNodeHandle, LogBox } from "react-native";
|
||||
+import { LogBox } from "react-native";
|
||||
import Animated, {
|
||||
useDerivedValue,
|
||||
useSharedValue,
|
||||
@@ -48,7 +48,7 @@ function NestableDraggableFlatListInner<T>(
|
||||
});
|
||||
|
||||
const onListContainerLayout = useStableCallback(async ({ containerRef }) => {
|
||||
- const nodeHandle = findNodeHandle(scrollableRef.current);
|
||||
+ const nodeHandle = scrollableRef.current;
|
||||
|
||||
const onSuccess = (_x: number, y: number) => {
|
||||
listVerticalOffset.value = y;
|
||||
@@ -74,6 +74,10 @@ function NestableDraggableFlatListInner<T>(
|
||||
}
|
||||
);
|
||||
|
||||
+ const onDragTerminate = useStableCallback(() => {
|
||||
+ setOuterScrollEnabled(true);
|
||||
+ });
|
||||
+
|
||||
const onAnimValInit: DraggableFlatListProps<T>["onAnimValInit"] = useStableCallback(
|
||||
(params) => {
|
||||
setListHoverOffset(params.hoverOffset);
|
||||
@@ -95,6 +99,7 @@ function NestableDraggableFlatListInner<T>(
|
||||
outerScrollOffset={outerScrollOffset}
|
||||
onDragBegin={onDragBegin}
|
||||
onDragEnd={onDragEnd}
|
||||
+ onDragTerminate={onDragTerminate}
|
||||
onAnimValInit={onAnimValInit}
|
||||
/>
|
||||
);
|
||||
diff --git a/node_modules/react-native-draggable-flatlist/src/types.ts b/node_modules/react-native-draggable-flatlist/src/types.ts
|
||||
index d6755c8..1b244a6 100644
|
||||
--- a/node_modules/react-native-draggable-flatlist/src/types.ts
|
||||
+++ b/node_modules/react-native-draggable-flatlist/src/types.ts
|
||||
@@ -36,12 +36,16 @@ export type DraggableFlatListProps<T> = Modify<
|
||||
keyExtractor: (item: T, index: number) => string;
|
||||
onDragBegin?: (index: number) => void;
|
||||
onDragEnd?: (params: DragEndParams<T>) => void;
|
||||
+ /** @internal Used by the nestable wrapper to release its outer-scroll lock on cancellation. */
|
||||
+ onDragTerminate?: () => void;
|
||||
onPlaceholderIndexChange?: (placeholderIndex: number) => void;
|
||||
onRelease?: (index: number) => void;
|
||||
onScrollOffsetChange?: (scrollOffset: number) => void;
|
||||
renderItem: RenderItem<T>;
|
||||
renderPlaceholder?: RenderPlaceholder<T>;
|
||||
simultaneousHandlers?: React.Ref<any> | React.Ref<any>[];
|
||||
+ /** @internal Forces handler reattachment when a retained native host is presented. */
|
||||
+ dragGestureHostPresented?: boolean;
|
||||
outerScrollOffset?: Animated.SharedValue<number>;
|
||||
onAnimValInit?: (animVals: ReturnType<typeof useAnimatedValues>) => void;
|
||||
itemEnteringAnimation?: AnimateProps<Animated.View>["entering"];
|
||||
@@ -48,10 +48,6 @@ const patchedPackages = [
|
||||
nodeModulesPath: "node_modules/@legendapp/list",
|
||||
patchPrefix: "@legendapp+list+",
|
||||
},
|
||||
{
|
||||
nodeModulesPath: "node_modules/react-native-draggable-flatlist",
|
||||
patchPrefix: "react-native-draggable-flatlist+",
|
||||
},
|
||||
// Web pointer capture: capture only pointers the handler still tracks after pointer-down, so a
|
||||
// touch-only pan that fails the mouse leaves browser text selection alone. Keep until upstream
|
||||
// stops capturing pointers a handler has rejected.
|
||||
|
||||
@@ -7,8 +7,9 @@ import { test } from "node:test";
|
||||
|
||||
const script = path.resolve("scripts", "postinstall-patches.mjs");
|
||||
const bin = path.resolve("node_modules", ".bin");
|
||||
const packageName = "react-native-draggable-flatlist";
|
||||
const patchFile = `${packageName}+4.0.3.patch`;
|
||||
const packageName = "react-native-gesture-handler";
|
||||
const packageVersion = "2.32.0";
|
||||
const patchFile = `${packageName}+${packageVersion}.patch`;
|
||||
|
||||
for (const platform of ["ios", "android"]) {
|
||||
test(`patched Markdown ships ${platform} props generated from its native specs`, () => {
|
||||
@@ -54,7 +55,7 @@ function createCheckout() {
|
||||
mkdirSync(packageDir, { recursive: true });
|
||||
writeFileSync(
|
||||
path.join(packageDir, "package.json"),
|
||||
JSON.stringify({ name: packageName, version: "4.0.3" }),
|
||||
JSON.stringify({ name: packageName, version: packageVersion }),
|
||||
);
|
||||
writeFileSync(path.join(packageDir, "index.js"), "export const value = 1;\n");
|
||||
mkdirSync(path.join(root, "patches"));
|
||||
|
||||
Reference in New Issue
Block a user