palmier 0.7.7 → 0.7.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/agents/agent.d.ts +3 -0
- package/dist/agents/agent.js +1 -1
- package/dist/agents/aider.d.ts +1 -0
- package/dist/agents/aider.js +1 -0
- package/dist/agents/claude.d.ts +1 -0
- package/dist/agents/claude.js +1 -0
- package/dist/agents/cline.d.ts +1 -0
- package/dist/agents/cline.js +1 -0
- package/dist/agents/codex.d.ts +1 -0
- package/dist/agents/codex.js +1 -0
- package/dist/agents/copilot.d.ts +1 -0
- package/dist/agents/copilot.js +1 -0
- package/dist/agents/cursor.d.ts +1 -0
- package/dist/agents/cursor.js +1 -0
- package/dist/agents/deepagents.d.ts +1 -0
- package/dist/agents/deepagents.js +1 -0
- package/dist/agents/droid.d.ts +1 -0
- package/dist/agents/droid.js +1 -0
- package/dist/agents/gemini.d.ts +1 -0
- package/dist/agents/gemini.js +1 -0
- package/dist/agents/goose.d.ts +1 -0
- package/dist/agents/goose.js +1 -0
- package/dist/agents/hermes.d.ts +1 -0
- package/dist/agents/hermes.js +1 -0
- package/dist/agents/kimi.d.ts +1 -0
- package/dist/agents/kimi.js +1 -0
- package/dist/agents/kiro.d.ts +1 -0
- package/dist/agents/kiro.js +1 -0
- package/dist/agents/openclaw.d.ts +1 -0
- package/dist/agents/openclaw.js +2 -2
- package/dist/agents/opencode.d.ts +1 -0
- package/dist/agents/opencode.js +1 -0
- package/dist/agents/qoder.d.ts +1 -0
- package/dist/agents/qoder.js +1 -0
- package/dist/agents/qwen.d.ts +1 -0
- package/dist/agents/qwen.js +1 -0
- package/dist/commands/pair.js +2 -2
- package/dist/mcp-tools.d.ts +2 -0
- package/dist/mcp-tools.js +20 -9
- package/dist/pending-requests.d.ts +30 -8
- package/dist/pending-requests.js +28 -15
- package/dist/platform/linux.js +11 -8
- package/dist/platform/windows.d.ts +5 -6
- package/dist/platform/windows.js +15 -12
- package/dist/pwa/assets/index-FP1Mipr6.js +120 -0
- package/dist/pwa/assets/index-bLTn8zBj.css +1 -0
- package/dist/pwa/assets/{web-CkWrlNwc.js → web-BpM3fNCn.js} +1 -1
- package/dist/pwa/assets/{web-lx34oBi7.js → web-CF-N8Di6.js} +1 -1
- package/dist/pwa/index.html +2 -2
- package/dist/pwa/service-worker.js +1 -1
- package/dist/rpc-handler.js +35 -24
- package/dist/task.js +1 -1
- package/dist/transports/http-transport.js +9 -8
- package/dist/types.d.ts +11 -6
- package/package.json +1 -1
- package/palmier-server/README.md +3 -3
- package/palmier-server/pwa/src/App.css +175 -28
- package/palmier-server/pwa/src/App.tsx +1 -0
- package/palmier-server/pwa/src/components/HostMenu.tsx +7 -2
- package/palmier-server/pwa/src/components/PullToRefreshIndicator.tsx +46 -0
- package/palmier-server/pwa/src/components/RunDetailView.tsx +58 -15
- package/palmier-server/pwa/src/components/SessionComposer.tsx +147 -0
- package/palmier-server/pwa/src/components/{RunsView.tsx → SessionsView.tsx} +79 -45
- package/palmier-server/pwa/src/components/TabBar.tsx +17 -10
- package/palmier-server/pwa/src/components/TaskCard.tsx +33 -35
- package/palmier-server/pwa/src/components/TaskForm.tsx +275 -349
- package/palmier-server/pwa/src/components/TasksView.tsx +172 -0
- package/palmier-server/pwa/src/constants.ts +1 -1
- package/palmier-server/pwa/src/contexts/HostStoreContext.tsx +16 -8
- package/palmier-server/pwa/src/draftGuard.ts +24 -0
- package/palmier-server/pwa/src/hooks/usePullToRefresh.ts +102 -0
- package/palmier-server/pwa/src/pages/Dashboard.tsx +343 -37
- package/palmier-server/pwa/src/types.ts +5 -14
- package/palmier-server/spec.md +39 -26
- package/src/agents/agent.ts +5 -1
- package/src/agents/aider.ts +1 -0
- package/src/agents/claude.ts +1 -0
- package/src/agents/cline.ts +1 -0
- package/src/agents/codex.ts +1 -0
- package/src/agents/copilot.ts +1 -0
- package/src/agents/cursor.ts +1 -0
- package/src/agents/deepagents.ts +1 -0
- package/src/agents/droid.ts +1 -0
- package/src/agents/gemini.ts +1 -0
- package/src/agents/goose.ts +1 -0
- package/src/agents/hermes.ts +1 -0
- package/src/agents/kimi.ts +1 -0
- package/src/agents/kiro.ts +1 -0
- package/src/agents/openclaw.ts +2 -2
- package/src/agents/opencode.ts +1 -0
- package/src/agents/qoder.ts +1 -0
- package/src/agents/qwen.ts +1 -0
- package/src/commands/pair.ts +2 -2
- package/src/mcp-tools.ts +22 -9
- package/src/pending-requests.ts +47 -15
- package/src/platform/linux.ts +10 -8
- package/src/platform/windows.ts +15 -12
- package/src/rpc-handler.ts +39 -26
- package/src/task.ts +1 -1
- package/src/transports/http-transport.ts +9 -8
- package/src/types.ts +10 -8
- package/test/pairing.test.ts +2 -2
- package/dist/pwa/assets/index-B-ByUHPS.css +0 -1
- package/dist/pwa/assets/index-Bt8Hhaw3.js +0 -118
- package/palmier-server/pwa/src/components/TaskListView.tsx +0 -431
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import TaskCard from "./TaskCard";
|
|
3
|
+
import TaskForm from "./TaskForm";
|
|
4
|
+
import PullToRefreshIndicator from "./PullToRefreshIndicator";
|
|
5
|
+
import { usePullToRefresh } from "../hooks/usePullToRefresh";
|
|
6
|
+
import type { AgentInfo, Task, TaskStatus } from "../types";
|
|
7
|
+
|
|
8
|
+
interface TasksViewProps {
|
|
9
|
+
connected: boolean;
|
|
10
|
+
hostId: string | null;
|
|
11
|
+
request<T = unknown>(method: string, params?: unknown, opts?: { timeout?: number }): Promise<T>;
|
|
12
|
+
subscribeEvents(hostId: string, callback: (msg: { subject: string; data: Uint8Array }) => void): () => void;
|
|
13
|
+
agents: AgentInfo[];
|
|
14
|
+
hostPlatform?: string;
|
|
15
|
+
onViewRun(taskId: string, runId?: string): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function TasksView({ connected, hostId, request, subscribeEvents, agents, hostPlatform, onViewRun }: TasksViewProps) {
|
|
19
|
+
const [tasks, setTasks] = useState<Task[]>([]);
|
|
20
|
+
const [loadingTasks, setLoadingTasks] = useState(false);
|
|
21
|
+
const [taskError, setTaskError] = useState<string | null>(null);
|
|
22
|
+
const [taskStatuses, setTaskStatuses] = useState<Map<string, TaskStatus>>(new Map());
|
|
23
|
+
|
|
24
|
+
const [showForm, setShowForm] = useState(false);
|
|
25
|
+
const [editingTask, setEditingTask] = useState<Task | undefined>(undefined);
|
|
26
|
+
|
|
27
|
+
const closeForm = useCallback(() => { setShowForm(false); setEditingTask(undefined); }, []);
|
|
28
|
+
|
|
29
|
+
const loadTasks = useCallback(async () => {
|
|
30
|
+
if (!connected) return;
|
|
31
|
+
setLoadingTasks(true);
|
|
32
|
+
setTaskError(null);
|
|
33
|
+
try {
|
|
34
|
+
const result = await request<{ tasks?: (Task & { status?: TaskStatus })[] }>("task.list");
|
|
35
|
+
const taskList = result.tasks ?? [];
|
|
36
|
+
const initialStatuses = new Map<string, TaskStatus>();
|
|
37
|
+
for (const t of taskList) {
|
|
38
|
+
if (t.status) initialStatuses.set(t.id, t.status);
|
|
39
|
+
}
|
|
40
|
+
setTaskStatuses(initialStatuses);
|
|
41
|
+
setTasks(taskList);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
44
|
+
if (errMsg === "Not connected" || errMsg.includes("503") || errMsg.toLowerCase().includes("no responders")) {
|
|
45
|
+
setTaskError(null);
|
|
46
|
+
} else {
|
|
47
|
+
setTaskError(errMsg);
|
|
48
|
+
}
|
|
49
|
+
setTasks([]);
|
|
50
|
+
} finally {
|
|
51
|
+
setLoadingTasks(false);
|
|
52
|
+
}
|
|
53
|
+
}, [connected, hostId, request]);
|
|
54
|
+
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (connected) loadTasks();
|
|
57
|
+
else { setTasks([]); setLoadingTasks(false); }
|
|
58
|
+
}, [connected, hostId, loadTasks]);
|
|
59
|
+
|
|
60
|
+
const ptr = usePullToRefresh({ onRefresh: loadTasks, enabled: connected });
|
|
61
|
+
|
|
62
|
+
// While mounted, keep task-card state fresh by refetching task.status on
|
|
63
|
+
// running-state / permission-resolved events. Dashboard owns the modal
|
|
64
|
+
// subscription; this one only drives the card indicators.
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (!connected || !hostId) return;
|
|
67
|
+
const unsubscribe = subscribeEvents(hostId, async (msg) => {
|
|
68
|
+
const tokens = msg.subject.split(".");
|
|
69
|
+
if (tokens.length < 3) return;
|
|
70
|
+
const taskId = tokens.slice(2).join(".");
|
|
71
|
+
|
|
72
|
+
let parsed: Record<string, unknown> = {};
|
|
73
|
+
try {
|
|
74
|
+
parsed = JSON.parse(new TextDecoder().decode(msg.data)) as Record<string, unknown>;
|
|
75
|
+
} catch { return; }
|
|
76
|
+
const eventType = parsed.event_type as string | undefined;
|
|
77
|
+
if (eventType !== "running-state" && eventType !== "permission-resolved") return;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const status = await request<TaskStatus & { error?: string }>("task.status", { id: taskId }, { timeout: 5000 });
|
|
81
|
+
if (status.error) return;
|
|
82
|
+
setTaskStatuses((prev) => { const next = new Map(prev); next.set(taskId, status); return next; });
|
|
83
|
+
} catch { /* skip */ }
|
|
84
|
+
});
|
|
85
|
+
return unsubscribe;
|
|
86
|
+
}, [connected, hostId, subscribeEvents, request]);
|
|
87
|
+
|
|
88
|
+
function handleTaskSaved(task: Task) {
|
|
89
|
+
setTasks((prev) => {
|
|
90
|
+
const idx = prev.findIndex((t) => t.id === task.id);
|
|
91
|
+
if (idx >= 0) { const next = [...prev]; next[idx] = task; return next; }
|
|
92
|
+
return [task, ...prev];
|
|
93
|
+
});
|
|
94
|
+
setShowForm(false);
|
|
95
|
+
setEditingTask(undefined);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function handleTaskDeleted(taskId: string) {
|
|
99
|
+
setTasks((prev) => prev.filter((t) => t.id !== taskId));
|
|
100
|
+
setTaskStatuses((prev) => { const next = new Map(prev); next.delete(taskId); return next; });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<>
|
|
105
|
+
<PullToRefreshIndicator pullDistance={ptr.pullDistance} refreshing={ptr.refreshing} threshold={ptr.threshold} />
|
|
106
|
+
{taskError && <div className="form-error">{taskError}{taskError.toLowerCase().includes("failed to fetch") && <>{" "}<a href="#" onClick={(e) => { e.preventDefault(); window.location.reload(); }}>Reload</a></>}</div>}
|
|
107
|
+
|
|
108
|
+
{loadingTasks && !tasks.length ? (
|
|
109
|
+
<div className="task-list">
|
|
110
|
+
{[0, 1, 2].map((i) => (
|
|
111
|
+
<div key={i} className="task-card" style={{ pointerEvents: "none" }}>
|
|
112
|
+
<div className="task-card-header">
|
|
113
|
+
<div className="task-card-title-row">
|
|
114
|
+
<div className="skeleton-line" style={{ width: 10, height: 10, borderRadius: "50%" }} />
|
|
115
|
+
<div className="skeleton-line" style={{ width: `${60 + i * 12}%` }} />
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<div className="task-card-meta">
|
|
119
|
+
<div className="skeleton-line" style={{ width: "30%" }} />
|
|
120
|
+
<div className="skeleton-line" style={{ width: "25%" }} />
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
))}
|
|
124
|
+
</div>
|
|
125
|
+
) : (
|
|
126
|
+
<div className="task-list">
|
|
127
|
+
{tasks.map((task) => (
|
|
128
|
+
<TaskCard
|
|
129
|
+
key={task.id}
|
|
130
|
+
task={task}
|
|
131
|
+
lastEvent={taskStatuses.get(task.id)}
|
|
132
|
+
onEdit={async (t) => {
|
|
133
|
+
try {
|
|
134
|
+
const latest = await request<Task & { error?: string }>("task.get", { id: t.id });
|
|
135
|
+
setEditingTask(latest.error ? t : latest);
|
|
136
|
+
} catch {
|
|
137
|
+
setEditingTask(t);
|
|
138
|
+
}
|
|
139
|
+
setShowForm(true);
|
|
140
|
+
}}
|
|
141
|
+
onDelete={handleTaskDeleted}
|
|
142
|
+
onViewRun={onViewRun}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
|
|
148
|
+
<button
|
|
149
|
+
className="fab"
|
|
150
|
+
onClick={() => { setEditingTask(undefined); setShowForm(true); }}
|
|
151
|
+
aria-label="New task"
|
|
152
|
+
title="New task"
|
|
153
|
+
>
|
|
154
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
155
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
156
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
157
|
+
</svg>
|
|
158
|
+
</button>
|
|
159
|
+
|
|
160
|
+
{showForm && (
|
|
161
|
+
<TaskForm
|
|
162
|
+
initial={editingTask}
|
|
163
|
+
agents={agents}
|
|
164
|
+
hostPlatform={hostPlatform}
|
|
165
|
+
onSaved={handleTaskSaved}
|
|
166
|
+
onRun={onViewRun}
|
|
167
|
+
onCancel={closeForm}
|
|
168
|
+
/>
|
|
169
|
+
)}
|
|
170
|
+
</>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
/** Bump when a breaking host change is made. */
|
|
2
|
-
export const MIN_HOST_VERSION = "0.7.
|
|
2
|
+
export const MIN_HOST_VERSION = "0.7.8";
|
|
@@ -88,17 +88,25 @@ export function HostStoreProvider({ children }: { children: ReactNode }) {
|
|
|
88
88
|
}, []);
|
|
89
89
|
|
|
90
90
|
const removePairedHost = useCallback((hostId: string) => {
|
|
91
|
-
setPairedHosts((prev) =>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
return filtered;
|
|
91
|
+
setPairedHosts((prev) => prev.filter((h) => h.hostId !== hostId));
|
|
92
|
+
// Only change the active host when the active one is the one being deleted.
|
|
93
|
+
setActiveHostIdState((current) => {
|
|
94
|
+
if (current !== hostId) return current;
|
|
95
|
+
// The deleted host was active; we'll re-select once pairedHosts settles.
|
|
96
|
+
return null;
|
|
99
97
|
});
|
|
100
98
|
}, []);
|
|
101
99
|
|
|
100
|
+
// When the active host disappears (e.g. it was just unpaired), fall back to
|
|
101
|
+
// any remaining paired host, otherwise null.
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (activeHostId !== null && !pairedHosts.find((h) => h.hostId === activeHostId)) {
|
|
104
|
+
setActiveHostIdState(pairedHosts[0]?.hostId ?? null);
|
|
105
|
+
} else if (activeHostId === null && pairedHosts.length > 0) {
|
|
106
|
+
setActiveHostIdState(pairedHosts[0].hostId);
|
|
107
|
+
}
|
|
108
|
+
}, [pairedHosts, activeHostId]);
|
|
109
|
+
|
|
102
110
|
const renamePairedHost = useCallback((hostId: string, name: string) => {
|
|
103
111
|
setPairedHosts((prev) =>
|
|
104
112
|
prev.map((h) => (h.hostId === hostId ? { ...h, name } : h))
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
let currentDraftMessage: string | null = null;
|
|
2
|
+
|
|
3
|
+
function handleBeforeUnload(e: BeforeUnloadEvent) {
|
|
4
|
+
if (currentDraftMessage) {
|
|
5
|
+
e.preventDefault();
|
|
6
|
+
e.returnValue = "";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function setDraftMessage(message: string | null) {
|
|
11
|
+
const wasActive = !!currentDraftMessage;
|
|
12
|
+
currentDraftMessage = message;
|
|
13
|
+
const nowActive = !!currentDraftMessage;
|
|
14
|
+
if (nowActive && !wasActive) {
|
|
15
|
+
window.addEventListener("beforeunload", handleBeforeUnload);
|
|
16
|
+
} else if (!nowActive && wasActive) {
|
|
17
|
+
window.removeEventListener("beforeunload", handleBeforeUnload);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function confirmLeaveDraft(): boolean {
|
|
22
|
+
if (!currentDraftMessage) return true;
|
|
23
|
+
return window.confirm(currentDraftMessage);
|
|
24
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface Options {
|
|
4
|
+
onRefresh: () => Promise<unknown> | void;
|
|
5
|
+
/** Pixels the user must pull past before a release triggers a refresh. */
|
|
6
|
+
threshold?: number;
|
|
7
|
+
/** Maximum visual pull distance after resistance dampening. */
|
|
8
|
+
maxPull?: number;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Window-level pull-to-refresh for the tasks/sessions lists. Activates only
|
|
14
|
+
* when the user starts a touch at `scrollY === 0` and drags downward.
|
|
15
|
+
* Returns `pullDistance` (dampened pixels to visually translate an indicator)
|
|
16
|
+
* and `refreshing` (true while the consumer's onRefresh promise is in flight).
|
|
17
|
+
*/
|
|
18
|
+
export function usePullToRefresh({
|
|
19
|
+
onRefresh,
|
|
20
|
+
threshold = 70,
|
|
21
|
+
maxPull = 110,
|
|
22
|
+
enabled = true,
|
|
23
|
+
}: Options) {
|
|
24
|
+
const [pullDistance, setPullDistance] = useState(0);
|
|
25
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
26
|
+
|
|
27
|
+
const startY = useRef<number | null>(null);
|
|
28
|
+
const pulling = useRef(false);
|
|
29
|
+
const distance = useRef(0);
|
|
30
|
+
const refreshingRef = useRef(false);
|
|
31
|
+
const onRefreshRef = useRef(onRefresh);
|
|
32
|
+
useEffect(() => { onRefreshRef.current = onRefresh; });
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!enabled) return;
|
|
36
|
+
|
|
37
|
+
const onTouchStart = (e: TouchEvent) => {
|
|
38
|
+
if (refreshingRef.current || window.scrollY > 0) {
|
|
39
|
+
startY.current = null;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
startY.current = e.touches[0].clientY;
|
|
43
|
+
pulling.current = false;
|
|
44
|
+
distance.current = 0;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const onTouchMove = (e: TouchEvent) => {
|
|
48
|
+
if (refreshingRef.current || startY.current == null) return;
|
|
49
|
+
const dy = e.touches[0].clientY - startY.current;
|
|
50
|
+
if (dy <= 0) {
|
|
51
|
+
pulling.current = false;
|
|
52
|
+
distance.current = 0;
|
|
53
|
+
setPullDistance(0);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
pulling.current = true;
|
|
57
|
+
const adjusted = Math.min(maxPull, dy * 0.5);
|
|
58
|
+
distance.current = adjusted;
|
|
59
|
+
setPullDistance(adjusted);
|
|
60
|
+
if (e.cancelable) e.preventDefault();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const onTouchEnd = () => {
|
|
64
|
+
if (!pulling.current) {
|
|
65
|
+
startY.current = null;
|
|
66
|
+
setPullDistance(0);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const pulled = distance.current;
|
|
70
|
+
startY.current = null;
|
|
71
|
+
pulling.current = false;
|
|
72
|
+
distance.current = 0;
|
|
73
|
+
if (pulled >= threshold) {
|
|
74
|
+
refreshingRef.current = true;
|
|
75
|
+
setRefreshing(true);
|
|
76
|
+
setPullDistance(threshold);
|
|
77
|
+
Promise.resolve(onRefreshRef.current())
|
|
78
|
+
.catch(() => { /* swallow — indicator still closes */ })
|
|
79
|
+
.finally(() => {
|
|
80
|
+
refreshingRef.current = false;
|
|
81
|
+
setRefreshing(false);
|
|
82
|
+
setPullDistance(0);
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
setPullDistance(0);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
window.addEventListener("touchstart", onTouchStart, { passive: true });
|
|
90
|
+
window.addEventListener("touchmove", onTouchMove, { passive: false });
|
|
91
|
+
window.addEventListener("touchend", onTouchEnd, { passive: true });
|
|
92
|
+
window.addEventListener("touchcancel", onTouchEnd, { passive: true });
|
|
93
|
+
return () => {
|
|
94
|
+
window.removeEventListener("touchstart", onTouchStart);
|
|
95
|
+
window.removeEventListener("touchmove", onTouchMove);
|
|
96
|
+
window.removeEventListener("touchend", onTouchEnd);
|
|
97
|
+
window.removeEventListener("touchcancel", onTouchEnd);
|
|
98
|
+
};
|
|
99
|
+
}, [enabled, threshold, maxPull]);
|
|
100
|
+
|
|
101
|
+
return { pullDistance, refreshing, threshold };
|
|
102
|
+
}
|