Fix mobile web gestures, DnD activation, iOS zoom, and Enter behavior (#1048)
* fix(app): require long-press for sidebar dnd on mobile * fix(app): right-edge open gesture for explorer sidebar * fix(app): left-edge open gesture for workspace sidebar * fix(app): allow gesture children to scroll on web * fix(app): lock viewport zoom on compact web * fix(app): don't submit on enter in compact web composer --------- Co-authored-by: jon <nikuscs@gmail.com>
This commit is contained in:
@@ -56,6 +56,7 @@ import { useActiveWorktreeNewAction } from "@/hooks/use-active-worktree-new-acti
|
|||||||
import { useFaviconStatus } from "@/hooks/use-favicon-status";
|
import { useFaviconStatus } from "@/hooks/use-favicon-status";
|
||||||
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
|
import { useKeyboardShortcuts } from "@/hooks/use-keyboard-shortcuts";
|
||||||
import { useLatchedBoolean } from "@/hooks/use-latched-boolean";
|
import { useLatchedBoolean } from "@/hooks/use-latched-boolean";
|
||||||
|
import { useCompactWebViewportZoomLock } from "@/hooks/use-compact-web-viewport-zoom-lock";
|
||||||
import { useOpenProject } from "@/hooks/use-open-project";
|
import { useOpenProject } from "@/hooks/use-open-project";
|
||||||
import { useAppSettings } from "@/hooks/use-settings";
|
import { useAppSettings } from "@/hooks/use-settings";
|
||||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
@@ -375,6 +376,8 @@ function QueryProvider({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
const rowStyle = { flex: 1, flexDirection: "row" } as const;
|
const rowStyle = { flex: 1, flexDirection: "row" } as const;
|
||||||
const flexStyle = { flex: 1 } as const;
|
const flexStyle = { flex: 1 } as const;
|
||||||
|
const MOBILE_WEB_EDGE_SWIPE_WIDTH = 32;
|
||||||
|
const MOBILE_WEB_GESTURE_TOUCH_ACTION = isWeb ? "auto" : "pan-y";
|
||||||
|
|
||||||
interface AppContainerProps {
|
interface AppContainerProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
@@ -406,6 +409,7 @@ function AppContainer({
|
|||||||
}, [settings.theme, updateSettings]);
|
}, [settings.theme, updateSettings]);
|
||||||
|
|
||||||
const isCompactLayout = useIsCompactFormFactor();
|
const isCompactLayout = useIsCompactFormFactor();
|
||||||
|
useCompactWebViewportZoomLock(isCompactLayout);
|
||||||
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
|
const chromeEnabled = chromeEnabledOverride ?? daemons.length > 0;
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const activeServerId = useMemo(
|
const activeServerId = useMemo(
|
||||||
@@ -498,6 +502,7 @@ function MobileGestureWrapper({
|
|||||||
openGestureRef,
|
openGestureRef,
|
||||||
} = useSidebarAnimation();
|
} = useSidebarAnimation();
|
||||||
const touchStartX = useSharedValue(0);
|
const touchStartX = useSharedValue(0);
|
||||||
|
const touchStartY = useSharedValue(0);
|
||||||
const openGestureEnabled = chromeEnabled && mobileView === "agent";
|
const openGestureEnabled = chromeEnabled && mobileView === "agent";
|
||||||
|
|
||||||
const handleGestureOpen = useCallback(() => {
|
const handleGestureOpen = useCallback(() => {
|
||||||
@@ -516,6 +521,7 @@ function MobileGestureWrapper({
|
|||||||
const touch = event.changedTouches[0];
|
const touch = event.changedTouches[0];
|
||||||
if (touch) {
|
if (touch) {
|
||||||
touchStartX.value = touch.absoluteX;
|
touchStartX.value = touch.absoluteX;
|
||||||
|
touchStartY.value = touch.absoluteY;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.onTouchesMove((event, stateManager) => {
|
.onTouchesMove((event, stateManager) => {
|
||||||
@@ -523,13 +529,31 @@ function MobileGestureWrapper({
|
|||||||
if (!touch || event.numberOfTouches !== 1) return;
|
if (!touch || event.numberOfTouches !== 1) return;
|
||||||
|
|
||||||
const deltaX = touch.absoluteX - touchStartX.value;
|
const deltaX = touch.absoluteX - touchStartX.value;
|
||||||
|
const deltaY = touch.absoluteY - touchStartY.value;
|
||||||
|
const absDeltaX = Math.abs(deltaX);
|
||||||
|
const absDeltaY = Math.abs(deltaY);
|
||||||
|
|
||||||
if (horizontalScroll?.isAnyScrolledRight.value) {
|
if (horizontalScroll?.isAnyScrolledRight.value) {
|
||||||
stateManager.fail();
|
stateManager.fail();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deltaX > 15) {
|
if (isWeb && touchStartX.value > MOBILE_WEB_EDGE_SWIPE_WIDTH) {
|
||||||
|
stateManager.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaX <= -10) {
|
||||||
|
stateManager.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absDeltaY > 10 && absDeltaY > absDeltaX) {
|
||||||
|
stateManager.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaX > 15 && absDeltaX > absDeltaY) {
|
||||||
stateManager.activate();
|
stateManager.activate();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -571,11 +595,12 @@ function MobileGestureWrapper({
|
|||||||
openGestureRef,
|
openGestureRef,
|
||||||
horizontalScroll?.isAnyScrolledRight,
|
horizontalScroll?.isAnyScrolledRight,
|
||||||
touchStartX,
|
touchStartX,
|
||||||
|
touchStartY,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureDetector gesture={openGesture} touchAction="pan-y">
|
<GestureDetector gesture={openGesture} touchAction={MOBILE_WEB_GESTURE_TOUCH_ACTION}>
|
||||||
{children}
|
{children}
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ interface DndContextProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let latestDndContextProps: DndContextProps | null = null;
|
let latestDndContextProps: DndContextProps | null = null;
|
||||||
|
const dndKitMocks = vi.hoisted(() => ({
|
||||||
|
useSensor: vi.fn(() => ({})),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@dnd-kit/core", () => ({
|
vi.mock("@dnd-kit/core", () => ({
|
||||||
DndContext: ({ children, ...props }: React.PropsWithChildren<DndContextProps>) => {
|
DndContext: ({ children, ...props }: React.PropsWithChildren<DndContextProps>) => {
|
||||||
@@ -22,7 +25,7 @@ vi.mock("@dnd-kit/core", () => ({
|
|||||||
closestCenter: vi.fn(),
|
closestCenter: vi.fn(),
|
||||||
KeyboardSensor: vi.fn(),
|
KeyboardSensor: vi.fn(),
|
||||||
PointerSensor: vi.fn(),
|
PointerSensor: vi.fn(),
|
||||||
useSensor: vi.fn(() => ({})),
|
useSensor: dndKitMocks.useSensor,
|
||||||
useSensors: vi.fn(() => []),
|
useSensors: vi.fn(() => []),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -104,7 +107,7 @@ function renderItem({ item, isActive }: { item: string; isActive: boolean }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderList(): void {
|
function renderList({ useDragHandle = false }: { useDragHandle?: boolean } = {}): void {
|
||||||
act(() => {
|
act(() => {
|
||||||
root?.render(
|
root?.render(
|
||||||
<DraggableList
|
<DraggableList
|
||||||
@@ -113,6 +116,7 @@ function renderList(): void {
|
|||||||
onDragEnd={vi.fn()}
|
onDragEnd={vi.fn()}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
scrollEnabled={false}
|
scrollEnabled={false}
|
||||||
|
useDragHandle={useDragHandle}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -125,6 +129,28 @@ function getItemActiveState(item: string): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("DraggableList web", () => {
|
describe("DraggableList web", () => {
|
||||||
|
it("uses distance activation for default draggable rows", () => {
|
||||||
|
renderList();
|
||||||
|
|
||||||
|
expect(dndKitMocks.useSensor).toHaveBeenCalledWith(
|
||||||
|
expect.any(Function),
|
||||||
|
expect.objectContaining({
|
||||||
|
activationConstraint: { distance: 6 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requires a held pointer before activating handle-based drags", () => {
|
||||||
|
renderList({ useDragHandle: true });
|
||||||
|
|
||||||
|
expect(dndKitMocks.useSensor).toHaveBeenCalledWith(
|
||||||
|
expect.any(Function),
|
||||||
|
expect.objectContaining({
|
||||||
|
activationConstraint: { delay: 250, tolerance: 8 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("clears active drag state when a drag is cancelled", () => {
|
it("clears active drag state when a drag is cancelled", () => {
|
||||||
renderList();
|
renderList();
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ const restrictToVerticalAxis: Modifier = ({ transform }) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const DND_MODIFIERS = [restrictToVerticalAxis];
|
const DND_MODIFIERS = [restrictToVerticalAxis];
|
||||||
|
const DEFAULT_POINTER_ACTIVATION_CONSTRAINT = { distance: 6 };
|
||||||
|
const HANDLE_POINTER_ACTIVATION_CONSTRAINT = { delay: 250, tolerance: 8 };
|
||||||
|
|
||||||
interface SortableItemProps<T> {
|
interface SortableItemProps<T> {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -89,12 +91,6 @@ function SortableItem<T>({
|
|||||||
}),
|
}),
|
||||||
[combinedTransform, transition, isDragging],
|
[combinedTransform, transition, isDragging],
|
||||||
);
|
);
|
||||||
const {
|
|
||||||
role: _dragRole,
|
|
||||||
tabIndex: _dragTabIndex,
|
|
||||||
"aria-roledescription": _dragRoleDescription,
|
|
||||||
...dragHandleAttributes
|
|
||||||
} = attributes as unknown as Record<string, unknown>;
|
|
||||||
|
|
||||||
const info: DraggableRenderItemInfo<T> = {
|
const info: DraggableRenderItemInfo<T> = {
|
||||||
item,
|
item,
|
||||||
@@ -103,7 +99,7 @@ function SortableItem<T>({
|
|||||||
isActive: activeId === id,
|
isActive: activeId === id,
|
||||||
dragHandleProps: useDragHandle
|
dragHandleProps: useDragHandle
|
||||||
? {
|
? {
|
||||||
attributes: dragHandleAttributes,
|
attributes: attributes as unknown as Record<string, unknown>,
|
||||||
listeners: listeners as unknown as Record<string, unknown>,
|
listeners: listeners as unknown as Record<string, unknown>,
|
||||||
setActivatorNodeRef: setActivatorNodeRef as unknown as (node: unknown) => void,
|
setActivatorNodeRef: setActivatorNodeRef as unknown as (node: unknown) => void,
|
||||||
}
|
}
|
||||||
@@ -149,12 +145,13 @@ export function DraggableList<T>({
|
|||||||
const scrollbar = useWebScrollViewScrollbar(scrollViewRef, {
|
const scrollbar = useWebScrollViewScrollbar(scrollViewRef, {
|
||||||
enabled: showCustomScrollbar,
|
enabled: showCustomScrollbar,
|
||||||
});
|
});
|
||||||
|
const pointerActivationConstraint = useDragHandle
|
||||||
|
? HANDLE_POINTER_ACTIVATION_CONSTRAINT
|
||||||
|
: DEFAULT_POINTER_ACTIVATION_CONSTRAINT;
|
||||||
|
|
||||||
const sensors = useSensors(
|
const sensors = useSensors(
|
||||||
useSensor(PointerSensor, {
|
useSensor(PointerSensor, {
|
||||||
activationConstraint: {
|
activationConstraint: pointerActivationConstraint,
|
||||||
distance: 6,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
useSensor(KeyboardSensor, {
|
useSensor(KeyboardSensor, {
|
||||||
coordinateGetter: sortableKeyboardCoordinates,
|
coordinateGetter: sortableKeyboardCoordinates,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ import { getShortcutOs } from "@/utils/shortcut-platform";
|
|||||||
import type { MessageInputKeyboardActionKind } from "@/keyboard/actions";
|
import type { MessageInputKeyboardActionKind } from "@/keyboard/actions";
|
||||||
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
|
import { isImeComposingKeyboardEvent } from "@/utils/keyboard-ime";
|
||||||
import { isWeb } from "@/constants/platform";
|
import { isWeb } from "@/constants/platform";
|
||||||
|
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||||
import { useComposerHeightMirror } from "./composer-height-mirror";
|
import { useComposerHeightMirror } from "./composer-height-mirror";
|
||||||
import { computeCanStartDictation } from "./message-input-state";
|
import { computeCanStartDictation } from "./message-input-state";
|
||||||
|
|
||||||
@@ -365,6 +366,7 @@ function resolveSendTooltipLabel(input: {
|
|||||||
|
|
||||||
interface DesktopKeyPressContext {
|
interface DesktopKeyPressContext {
|
||||||
onKeyPressCallback: ((event: { key: string; preventDefault: () => void }) => boolean) | undefined;
|
onKeyPressCallback: ((event: { key: string; preventDefault: () => void }) => boolean) | undefined;
|
||||||
|
submitOnEnter: boolean;
|
||||||
isAgentRunning: boolean;
|
isAgentRunning: boolean;
|
||||||
onQueue: ((payload: MessagePayload) => void) | undefined;
|
onQueue: ((payload: MessagePayload) => void) | undefined;
|
||||||
isSubmitDisabled: boolean;
|
isSubmitDisabled: boolean;
|
||||||
@@ -391,6 +393,7 @@ function handleDesktopKeyPressImpl(
|
|||||||
const { shiftKey, metaKey, ctrlKey } = event.nativeEvent;
|
const { shiftKey, metaKey, ctrlKey } = event.nativeEvent;
|
||||||
|
|
||||||
if (event.nativeEvent.key !== "Enter") return;
|
if (event.nativeEvent.key !== "Enter") return;
|
||||||
|
if (!ctx.submitOnEnter) return;
|
||||||
if (shiftKey) return;
|
if (shiftKey) return;
|
||||||
|
|
||||||
if ((metaKey || ctrlKey) && ctx.isAgentRunning && ctx.onQueue) {
|
if ((metaKey || ctrlKey) && ctx.isAgentRunning && ctx.onQueue) {
|
||||||
@@ -1187,6 +1190,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
|||||||
inputWrapperStyle,
|
inputWrapperStyle,
|
||||||
attachmentSlot,
|
attachmentSlot,
|
||||||
} = resolveMessageInputProps(props);
|
} = resolveMessageInputProps(props);
|
||||||
|
const isCompact = useIsCompactFormFactor();
|
||||||
const buttonIconSize = isWeb ? ICON_SIZE.md : ICON_SIZE.lg;
|
const buttonIconSize = isWeb ? ICON_SIZE.md : ICON_SIZE.lg;
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const voice = useVoiceOptional();
|
const voice = useVoiceOptional();
|
||||||
@@ -1567,12 +1571,14 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
|||||||
[onSelectionChangeCallback],
|
[onSelectionChangeCallback],
|
||||||
);
|
);
|
||||||
|
|
||||||
const shouldHandleDesktopSubmit = isWeb;
|
const shouldHandleWebKeyPress = isWeb;
|
||||||
|
const shouldSubmitOnEnter = isWeb && !isCompact;
|
||||||
|
|
||||||
function handleDesktopKeyPress(event: WebTextInputKeyPressEvent) {
|
function handleDesktopKeyPress(event: WebTextInputKeyPressEvent) {
|
||||||
if (!shouldHandleDesktopSubmit) return;
|
if (!shouldHandleWebKeyPress) return;
|
||||||
handleDesktopKeyPressImpl(event, {
|
handleDesktopKeyPressImpl(event, {
|
||||||
onKeyPressCallback,
|
onKeyPressCallback,
|
||||||
|
submitOnEnter: shouldSubmitOnEnter,
|
||||||
isAgentRunning,
|
isAgentRunning,
|
||||||
onQueue,
|
onQueue,
|
||||||
isSubmitDisabled,
|
isSubmitDisabled,
|
||||||
@@ -1724,7 +1730,7 @@ export const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
|||||||
scrollEnabled={isWeb ? inputHeight >= MAX_INPUT_HEIGHT : true}
|
scrollEnabled={isWeb ? inputHeight >= MAX_INPUT_HEIGHT : true}
|
||||||
onContentSizeChange={handleContentSizeChange}
|
onContentSizeChange={handleContentSizeChange}
|
||||||
editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled}
|
editable={!isDictating && !isRealtimeVoiceForCurrentAgent && !disabled}
|
||||||
onKeyPress={shouldHandleDesktopSubmit ? handleDesktopKeyPress : undefined}
|
onKeyPress={shouldHandleWebKeyPress ? handleDesktopKeyPress : undefined}
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
autoFocus={isWeb && autoFocus}
|
autoFocus={isWeb && autoFocus}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { isWeb } from "@/constants/platform";
|
||||||
|
|
||||||
|
const COMPACT_WEB_VIEWPORT_CONTENT =
|
||||||
|
"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover";
|
||||||
|
const DEFAULT_WEB_VIEWPORT_CONTENT = "width=device-width, initial-scale=1, viewport-fit=cover";
|
||||||
|
|
||||||
|
export function useCompactWebViewportZoomLock(isCompactLayout: boolean) {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isWeb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewportMeta =
|
||||||
|
document.querySelector<HTMLMetaElement>('meta[name="viewport"]') ??
|
||||||
|
document.createElement("meta");
|
||||||
|
const hadViewportMeta = viewportMeta.parentElement !== null;
|
||||||
|
const previousContent = viewportMeta.getAttribute("content");
|
||||||
|
|
||||||
|
if (!hadViewportMeta) {
|
||||||
|
viewportMeta.name = "viewport";
|
||||||
|
document.head.appendChild(viewportMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
viewportMeta.setAttribute(
|
||||||
|
"content",
|
||||||
|
isCompactLayout ? COMPACT_WEB_VIEWPORT_CONTENT : DEFAULT_WEB_VIEWPORT_CONTENT,
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!hadViewportMeta) {
|
||||||
|
viewportMeta.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (previousContent !== null) {
|
||||||
|
viewportMeta.setAttribute("content", previousContent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isCompactLayout]);
|
||||||
|
}
|
||||||
@@ -3,12 +3,15 @@ import { Gesture } from "react-native-gesture-handler";
|
|||||||
import { Extrapolation, interpolate, runOnJS, useSharedValue } from "react-native-reanimated";
|
import { Extrapolation, interpolate, runOnJS, useSharedValue } from "react-native-reanimated";
|
||||||
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context";
|
||||||
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
import { useSidebarAnimation } from "@/contexts/sidebar-animation-context";
|
||||||
|
import { isWeb } from "@/constants/platform";
|
||||||
|
|
||||||
interface UseExplorerOpenGestureParams {
|
interface UseExplorerOpenGestureParams {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
onOpen: () => void;
|
onOpen: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MOBILE_WEB_EDGE_SWIPE_WIDTH = 32;
|
||||||
|
|
||||||
export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestureParams) {
|
export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestureParams) {
|
||||||
const {
|
const {
|
||||||
translateX,
|
translateX,
|
||||||
@@ -56,6 +59,12 @@ export function useExplorerOpenGesture({ enabled, onOpen }: UseExplorerOpenGestu
|
|||||||
const absDeltaX = Math.abs(deltaX);
|
const absDeltaX = Math.abs(deltaX);
|
||||||
const absDeltaY = Math.abs(deltaY);
|
const absDeltaY = Math.abs(deltaY);
|
||||||
|
|
||||||
|
// Browser back-swipe owns most of the viewport; keep this gesture on the right edge.
|
||||||
|
if (isWeb && touchStartX.value < windowWidth - MOBILE_WEB_EDGE_SWIPE_WIDTH) {
|
||||||
|
stateManager.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Fail quickly on rightward or clearly vertical intent.
|
// Fail quickly on rightward or clearly vertical intent.
|
||||||
if (deltaX >= 10) {
|
if (deltaX >= 10) {
|
||||||
stateManager.fail();
|
stateManager.fail();
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ const EMPTY_UI_TABS: WorkspaceTab[] = [];
|
|||||||
const EMPTY_WORKSPACE_SCRIPTS: WorkspaceDescriptor["scripts"] = [];
|
const EMPTY_WORKSPACE_SCRIPTS: WorkspaceDescriptor["scripts"] = [];
|
||||||
const EMPTY_PINNED_AGENT_IDS = new Set<string>();
|
const EMPTY_PINNED_AGENT_IDS = new Set<string>();
|
||||||
const EMPTY_SET = new Set<string>();
|
const EMPTY_SET = new Set<string>();
|
||||||
|
const COMPACT_WEB_GESTURE_TOUCH_ACTION = isWeb ? "auto" : "pan-y";
|
||||||
|
|
||||||
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
const ThemedActivityIndicator = withUnistyles(ActivityIndicator);
|
||||||
const ThemedEllipsis = withUnistyles(Ellipsis);
|
const ThemedEllipsis = withUnistyles(Ellipsis);
|
||||||
@@ -3213,7 +3214,10 @@ function WorkspaceScreenContent({
|
|||||||
|
|
||||||
<View style={styles.centerContent}>
|
<View style={styles.centerContent}>
|
||||||
{isMobile ? (
|
{isMobile ? (
|
||||||
<GestureDetector gesture={explorerOpenGesture} touchAction="pan-y">
|
<GestureDetector
|
||||||
|
gesture={explorerOpenGesture}
|
||||||
|
touchAction={COMPACT_WEB_GESTURE_TOUCH_ACTION}
|
||||||
|
>
|
||||||
<View style={styles.content}>{content}</View>
|
<View style={styles.content}>{content}</View>
|
||||||
</GestureDetector>
|
</GestureDetector>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user