@hyperframes/studio 0.7.8 → 0.7.10
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/dist/assets/{index-BSyZKYmH.js → index-CWTSv_j9.js} +99 -99
- package/dist/assets/{index-Dgeszckd.js → index-ClZvune4.js} +1 -1
- package/dist/assets/{index--lOAjFJl.js → index-TqAN2o6c.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +16 -9
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/components/editor/manualOffsetDrag.test.ts +99 -0
- package/src/components/editor/manualOffsetDrag.ts +38 -7
- package/src/hooks/useEnableKeyframes.test.ts +40 -0
- package/src/hooks/useEnableKeyframes.ts +9 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"dompurify": "^3.2.4",
|
|
46
46
|
"marked": "^14.1.4",
|
|
47
47
|
"mediabunny": "^1.45.3",
|
|
48
|
-
"@hyperframes/
|
|
49
|
-
"@hyperframes/
|
|
50
|
-
"@hyperframes/
|
|
48
|
+
"@hyperframes/core": "0.7.10",
|
|
49
|
+
"@hyperframes/sdk": "0.7.10",
|
|
50
|
+
"@hyperframes/player": "0.7.10"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/react": "19",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"vite": "^6.4.2",
|
|
63
63
|
"vitest": "^3.2.4",
|
|
64
64
|
"zustand": "^5.0.0",
|
|
65
|
-
"@hyperframes/producer": "0.7.
|
|
65
|
+
"@hyperframes/producer": "0.7.10"
|
|
66
66
|
},
|
|
67
67
|
"peerDependencies": {
|
|
68
68
|
"react": "19",
|
|
@@ -2,6 +2,7 @@ import { Window } from "happy-dom";
|
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import {
|
|
4
4
|
applyManualOffsetDragCommit,
|
|
5
|
+
applyManualOffsetDragDraft,
|
|
5
6
|
applyManualOffsetDragMatrix,
|
|
6
7
|
createManualOffsetDragMember,
|
|
7
8
|
endManualOffsetDragMembers,
|
|
@@ -261,3 +262,101 @@ describe("createManualOffsetDragMember uses raw CSS var offset", () => {
|
|
|
261
262
|
}
|
|
262
263
|
});
|
|
263
264
|
});
|
|
265
|
+
|
|
266
|
+
// ── GSAP-element drag: the dot-a "flies" regressions ────────────────────────
|
|
267
|
+
// A static element positioned via the legacy `--hf-studio-offset` CSS var, dragged
|
|
268
|
+
// in a GSAP composition. Three independent failure modes, each fixed:
|
|
269
|
+
// 1. live drag integrated off-screen (base read from the live transform)
|
|
270
|
+
// 2. commit re-added the delta (stamped base wiped by a mid-drag re-render)
|
|
271
|
+
// 3. drop left the element offset (stale --hf-studio-offset var composing with
|
|
272
|
+
// the committed GSAP transform until a full reload)
|
|
273
|
+
function makeGsapDot(offsetX = 94, offsetY = 2) {
|
|
274
|
+
const window = new Window();
|
|
275
|
+
const element = window.document.createElement("div");
|
|
276
|
+
element.id = "dot-a";
|
|
277
|
+
element.setAttribute("data-hf-studio-path-offset", "true");
|
|
278
|
+
element.style.setProperty(STUDIO_OFFSET_X_PROP, `${offsetX}px`);
|
|
279
|
+
element.style.setProperty(STUDIO_OFFSET_Y_PROP, `${offsetY}px`);
|
|
280
|
+
element.style.translate = `var(${STUDIO_OFFSET_X_PROP}, 0px) var(${STUDIO_OFFSET_Y_PROP}, 0px)`;
|
|
281
|
+
window.document.body.append(element);
|
|
282
|
+
// Constant rect → the screen-to-offset probe can't measure movement → member
|
|
283
|
+
// uses the deterministic preview-scale fallback matrix. Both branches set baseGsap.
|
|
284
|
+
element.getBoundingClientRect = () => new window.DOMRect(10, 20, 100, 50);
|
|
285
|
+
const sets: Array<Record<string, unknown>> = [];
|
|
286
|
+
const win = element.ownerDocument.defaultView as unknown as {
|
|
287
|
+
gsap?: unknown;
|
|
288
|
+
__timelines?: unknown;
|
|
289
|
+
};
|
|
290
|
+
win.gsap = {
|
|
291
|
+
set: (el: HTMLElement, vars: Record<string, unknown>) => {
|
|
292
|
+
sets.push({ ...vars });
|
|
293
|
+
if (typeof vars.x === "number") {
|
|
294
|
+
el.style.setProperty("transform", `translate(${vars.x}px, ${(vars.y as number) ?? 0}px)`);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
// getProperty reads the LIVE transform — the exact value the old code fed back
|
|
298
|
+
// into `base + delta`, integrating the element off-screen.
|
|
299
|
+
getProperty: (el: HTMLElement, prop: string) => {
|
|
300
|
+
const m = /translate\(([-\d.]+)px,\s*([-\d.]+)px\)/.exec(
|
|
301
|
+
el.style.getPropertyValue("transform") || "",
|
|
302
|
+
);
|
|
303
|
+
if (!m) return 0;
|
|
304
|
+
return prop === "x" ? Number.parseFloat(m[1]!) : Number.parseFloat(m[2]!);
|
|
305
|
+
},
|
|
306
|
+
};
|
|
307
|
+
const member = () => {
|
|
308
|
+
const result = createManualOffsetDragMember({
|
|
309
|
+
key: "dot",
|
|
310
|
+
selection: { element } as never,
|
|
311
|
+
element,
|
|
312
|
+
rect: { left: 10, top: 20, width: 100, height: 50, editScaleX: 1, editScaleY: 1 },
|
|
313
|
+
});
|
|
314
|
+
if (!result.ok) throw new Error("member not created");
|
|
315
|
+
return result.member;
|
|
316
|
+
};
|
|
317
|
+
return { element, sets, member };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
describe("GSAP-element drag — dot-a flies regressions", () => {
|
|
321
|
+
it("live draft uses the stable gesture-start base, so repeated moves don't integrate", () => {
|
|
322
|
+
const { element, member } = makeGsapDot();
|
|
323
|
+
const m = member();
|
|
324
|
+
// Simulate a mid-drag re-render wiping the stamped base attr → the draft must
|
|
325
|
+
// fall back to the in-memory member.baseGsap, NOT the live (mutating) transform.
|
|
326
|
+
element.removeAttribute("data-hf-drag-gsap-base-x");
|
|
327
|
+
element.removeAttribute("data-hf-drag-gsap-base-y");
|
|
328
|
+
applyManualOffsetDragDraft(m, -50, 0);
|
|
329
|
+
const first = element.style.getPropertyValue("transform");
|
|
330
|
+
applyManualOffsetDragDraft(m, -50, 0);
|
|
331
|
+
const second = element.style.getPropertyValue("transform");
|
|
332
|
+
// Same pointer delta → same committed transform. The old bug integrated (the
|
|
333
|
+
// second frame added the delta on top of the first frame's result).
|
|
334
|
+
expect(second).toBe(first);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("commit re-stamps the stable base/initial attrs even after they're wiped", () => {
|
|
338
|
+
const { element, member } = makeGsapDot();
|
|
339
|
+
const m = member();
|
|
340
|
+
element.removeAttribute("data-hf-drag-gsap-base-x");
|
|
341
|
+
element.removeAttribute("data-hf-drag-initial-offset-x");
|
|
342
|
+
applyManualOffsetDragCommit(m, -50, 0);
|
|
343
|
+
expect(element.getAttribute("data-hf-drag-gsap-base-x")).toBe(String(m.baseGsap.x));
|
|
344
|
+
expect(element.getAttribute("data-hf-drag-initial-offset-x")).toBe(String(m.initialOffset.x));
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("a GSAP-committed drag migrates the element off --hf-studio-offset", () => {
|
|
348
|
+
const { element, member } = makeGsapDot();
|
|
349
|
+
expect(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)).toBe("94px");
|
|
350
|
+
const m = member();
|
|
351
|
+
applyManualOffsetDragCommit(m, -160, 0);
|
|
352
|
+
endManualOffsetDragMembers([m]);
|
|
353
|
+
// The legacy CSS-offset channel is fully cleared (single-sourced in GSAP): the
|
|
354
|
+
// var is removed, so any lingering `translate: var(--hf-studio-offset-x, 0px)`
|
|
355
|
+
// resolves to its 0px fallback and can no longer compose with the GSAP transform.
|
|
356
|
+
expect(element.style.getPropertyValue(STUDIO_OFFSET_X_PROP)).toBe("");
|
|
357
|
+
expect(element.style.getPropertyValue(STUDIO_OFFSET_Y_PROP)).toBe("");
|
|
358
|
+
expect(element.hasAttribute("data-hf-studio-path-offset")).toBe(false);
|
|
359
|
+
// ...and the position survives in the GSAP transform (no stale var to compose).
|
|
360
|
+
expect(element.style.getPropertyValue("transform")).toMatch(/translate\(/);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
applyStudioPathOffsetDraft,
|
|
5
5
|
beginStudioManualEditGesture,
|
|
6
6
|
captureStudioPathOffset,
|
|
7
|
+
clearStudioPathOffset,
|
|
7
8
|
endStudioManualEditGesture,
|
|
8
9
|
readAppliedStudioPathOffset,
|
|
9
10
|
restoreStudioPathOffset,
|
|
@@ -35,17 +36,17 @@ function getOffsetDragGsap(element: HTMLElement): OffsetDragGsap | null {
|
|
|
35
36
|
function applyOffsetDragDraftViaGsap(
|
|
36
37
|
element: HTMLElement,
|
|
37
38
|
offset: { x: number; y: number },
|
|
39
|
+
baseGsap: { x: number; y: number },
|
|
38
40
|
): boolean {
|
|
39
41
|
const gsap = getOffsetDragGsap(element);
|
|
40
42
|
if (!gsap) return false;
|
|
41
43
|
// GSAP owns the transform; neutralize the CSS translate longhand so the two
|
|
42
44
|
// channels can't compose into a doubled position.
|
|
43
45
|
element.style.setProperty("translate", "none");
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
48
|
-
const { newX, newY } = computeDraggedGsapPosition(element, offset, fallbackBase);
|
|
46
|
+
// Use the STABLE gesture-start base (captured in JS), NOT `gsap.getProperty`.
|
|
47
|
+
// After `translate: none`, getProperty reads the transform we set last frame,
|
|
48
|
+
// so `base + delta` would integrate frame-over-frame and fling the element.
|
|
49
|
+
const { newX, newY } = computeDraggedGsapPosition(element, offset, baseGsap);
|
|
49
50
|
gsap.set(element, { x: newX, y: newY });
|
|
50
51
|
return true;
|
|
51
52
|
}
|
|
@@ -96,6 +97,14 @@ export interface ManualOffsetDragMember {
|
|
|
96
97
|
selection: DomEditSelection;
|
|
97
98
|
element: HTMLElement;
|
|
98
99
|
initialOffset: { x: number; y: number };
|
|
100
|
+
/**
|
|
101
|
+
* The element's GSAP x/y at gesture start, captured in JS so a mid-drag
|
|
102
|
+
* re-render (which reverts inline style + wipes the `data-hf-drag-gsap-base-*`
|
|
103
|
+
* attrs) can't drop the base. Without this the draft falls back to the LIVE
|
|
104
|
+
* transform — i.e. the value it set last frame — and `base + delta` integrates,
|
|
105
|
+
* making the element accelerate away ("flies"). See applyOffsetDragDraftViaGsap.
|
|
106
|
+
*/
|
|
107
|
+
baseGsap: { x: number; y: number };
|
|
99
108
|
initialPathOffset: StudioPathOffsetSnapshot;
|
|
100
109
|
gestureToken: string;
|
|
101
110
|
screenToOffset: ManualOffsetDragMatrix;
|
|
@@ -343,6 +352,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
343
352
|
scaleX: input.rect.editScaleX,
|
|
344
353
|
scaleY: input.rect.editScaleY,
|
|
345
354
|
});
|
|
355
|
+
const baseGsap = { x: gsapX, y: gsapY };
|
|
346
356
|
if (!measured.ok) {
|
|
347
357
|
// Fallback: when GSAP transforms interfere with probe measurement, use
|
|
348
358
|
// the preview scale as an approximation. The commit path reads the actual
|
|
@@ -357,6 +367,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
357
367
|
selection: input.selection,
|
|
358
368
|
element: input.element,
|
|
359
369
|
initialOffset,
|
|
370
|
+
baseGsap,
|
|
360
371
|
initialPathOffset,
|
|
361
372
|
gestureToken,
|
|
362
373
|
screenToOffset: { a: 1 / scaleX, b: 0, c: 0, d: 1 / scaleY },
|
|
@@ -372,6 +383,7 @@ export function createManualOffsetDragMember(input: {
|
|
|
372
383
|
selection: input.selection,
|
|
373
384
|
element: input.element,
|
|
374
385
|
initialOffset,
|
|
386
|
+
baseGsap,
|
|
375
387
|
initialPathOffset,
|
|
376
388
|
gestureToken,
|
|
377
389
|
screenToOffset: measured.matrix,
|
|
@@ -402,7 +414,7 @@ export function applyManualOffsetDragDraft(
|
|
|
402
414
|
// Position is single-sourced on the GSAP timeline; preview through gsap.set so
|
|
403
415
|
// the live draft matches the committed `tl.set`/keyframe. CSS draft only when
|
|
404
416
|
// gsap is unavailable (no preview iframe runtime).
|
|
405
|
-
if (!applyOffsetDragDraftViaGsap(member.element, offset)) {
|
|
417
|
+
if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
|
|
406
418
|
applyStudioPathOffsetDraft(member.element, offset);
|
|
407
419
|
}
|
|
408
420
|
return offset;
|
|
@@ -413,12 +425,22 @@ export function applyManualOffsetDragCommit(
|
|
|
413
425
|
dx: number,
|
|
414
426
|
dy: number,
|
|
415
427
|
): { x: number; y: number } {
|
|
428
|
+
// Re-stamp the STABLE gesture-start base/offset before the source commit reads
|
|
429
|
+
// them. A mid-drag re-render can wipe these attrs; the commit converts the drop
|
|
430
|
+
// offset → gsap x/y via computeDraggedGsapPosition, which without the base falls
|
|
431
|
+
// back to the live (already-dragged) transform and re-adds the delta — so the
|
|
432
|
+
// element flies off-screen the instant you drop it. The member holds the true
|
|
433
|
+
// gesture-start values in JS, immune to the re-render.
|
|
434
|
+
member.element.setAttribute("data-hf-drag-gsap-base-x", String(member.baseGsap.x));
|
|
435
|
+
member.element.setAttribute("data-hf-drag-gsap-base-y", String(member.baseGsap.y));
|
|
436
|
+
member.element.setAttribute("data-hf-drag-initial-offset-x", String(member.initialOffset.x));
|
|
437
|
+
member.element.setAttribute("data-hf-drag-initial-offset-y", String(member.initialOffset.y));
|
|
416
438
|
const offset = resolveManualOffsetDragMemberOffset(member, dx, dy);
|
|
417
439
|
// Optimistic visual through the GSAP channel (same as the live draft and the
|
|
418
440
|
// committed `tl.set`), so the element holds its dropped position until the
|
|
419
441
|
// source mutation soft-reloads — no transient CSS `--hf-studio-offset` write.
|
|
420
442
|
// CSS apply only when gsap is unavailable.
|
|
421
|
-
if (!applyOffsetDragDraftViaGsap(member.element, offset)) {
|
|
443
|
+
if (!applyOffsetDragDraftViaGsap(member.element, offset, member.baseGsap)) {
|
|
422
444
|
applyStudioPathOffset(member.element, offset);
|
|
423
445
|
}
|
|
424
446
|
return offset;
|
|
@@ -451,6 +473,15 @@ export function endManualOffsetDragMembers(members: ManualOffsetDragMember[]): v
|
|
|
451
473
|
if (member.element.style.getPropertyValue("translate") === "none") {
|
|
452
474
|
member.element.style.removeProperty("translate");
|
|
453
475
|
}
|
|
476
|
+
// Migration: when GSAP owns the position (the committed value lives in the
|
|
477
|
+
// GSAP transform), the legacy `--hf-studio-offset` CSS channel is obsolete.
|
|
478
|
+
// Clear it on the LIVE element — otherwise the leftover `translate:
|
|
479
|
+
// var(--hf-studio-offset)` composes with the GSAP transform and the element
|
|
480
|
+
// renders offset by the stale value until a full page reload (the source is
|
|
481
|
+
// already stripped). clearStudioPathOffset leaves `transform` untouched.
|
|
482
|
+
if (getOffsetDragGsap(member.element)) {
|
|
483
|
+
clearStudioPathOffset(member.element);
|
|
484
|
+
}
|
|
454
485
|
resumeGsapTimelines(member.element);
|
|
455
486
|
}
|
|
456
487
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
|
|
3
|
+
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
3
4
|
import {
|
|
4
5
|
animatedProps,
|
|
5
6
|
buildExtendedKeyframes,
|
|
6
7
|
isPlayheadWithinTween,
|
|
8
|
+
promoteSetToKeyframes,
|
|
7
9
|
resolveNewTweenRange,
|
|
10
|
+
type EnableKeyframesSession,
|
|
8
11
|
} from "./useEnableKeyframes";
|
|
9
12
|
|
|
10
13
|
function anim(overrides: Partial<GsapAnimation>): GsapAnimation {
|
|
@@ -128,3 +131,40 @@ describe("buildExtendedKeyframes", () => {
|
|
|
128
131
|
expect(out.keyframes[1]!.percentage).toBeCloseTo(22.7, 1);
|
|
129
132
|
});
|
|
130
133
|
});
|
|
134
|
+
|
|
135
|
+
describe("promoteSetToKeyframes — auto endpoint", () => {
|
|
136
|
+
it("marks the 0% (held start) as `auto`, leaving the 100% (playhead) fixed", async () => {
|
|
137
|
+
let committed: Record<string, unknown> | undefined;
|
|
138
|
+
const session = {
|
|
139
|
+
commitMutation: async (mutation: Record<string, unknown>) => {
|
|
140
|
+
committed = mutation;
|
|
141
|
+
},
|
|
142
|
+
} as unknown as EnableKeyframesSession;
|
|
143
|
+
const sel = {
|
|
144
|
+
id: "card",
|
|
145
|
+
selector: "#card",
|
|
146
|
+
sourceFile: "index.html",
|
|
147
|
+
element: { isConnected: true } as unknown as HTMLElement,
|
|
148
|
+
} as unknown as DomEditSelection;
|
|
149
|
+
// readElementPosition reads gsap.getProperty off the iframe window.
|
|
150
|
+
const iframe = {
|
|
151
|
+
contentWindow: { gsap: { getProperty: () => -74 } },
|
|
152
|
+
} as unknown as HTMLIFrameElement;
|
|
153
|
+
const setAnim = anim({
|
|
154
|
+
id: "#card-set-0-position",
|
|
155
|
+
targetSelector: "#card",
|
|
156
|
+
method: "set",
|
|
157
|
+
global: true,
|
|
158
|
+
resolvedStart: 0,
|
|
159
|
+
properties: { x: -74, y: -469 },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await promoteSetToKeyframes(session, sel, setAnim, 1, iframe);
|
|
163
|
+
|
|
164
|
+
const kfs = committed?.keyframes as Array<{ percentage: number; auto?: boolean }>;
|
|
165
|
+
expect(committed?.type).toBe("replace-with-keyframes");
|
|
166
|
+
expect(kfs[0]).toMatchObject({ percentage: 0, auto: true });
|
|
167
|
+
expect(kfs[1].percentage).toBe(100);
|
|
168
|
+
expect(kfs[1].auto).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -107,6 +107,7 @@ export function buildExtendedKeyframes(
|
|
|
107
107
|
return { position: roundTo3(newStart), duration: newDuration, keyframes };
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
// fallow-ignore-next-line complexity
|
|
110
111
|
function readElementPosition(
|
|
111
112
|
iframe: HTMLIFrameElement | null,
|
|
112
113
|
sel: DomEditSelection,
|
|
@@ -238,8 +239,12 @@ async function applyKeyframeAtPlayhead(
|
|
|
238
239
|
* two-stop tween from the set's time to the playhead — the held value at 0%, the
|
|
239
240
|
* live value at 100% — giving the user something to animate. No-op if the playhead
|
|
240
241
|
* is at or before the set.
|
|
242
|
+
*
|
|
243
|
+
* The 0% endpoint is the held start, which the user didn't choose — mark it `auto`
|
|
244
|
+
* so it tracks the nearest keyframe until edited directly. The 100% is the real
|
|
245
|
+
* keyframe being placed at the playhead, so it stays fixed.
|
|
241
246
|
*/
|
|
242
|
-
async function promoteSetToKeyframes(
|
|
247
|
+
export async function promoteSetToKeyframes(
|
|
243
248
|
session: EnableKeyframesSession,
|
|
244
249
|
sel: DomEditSelection,
|
|
245
250
|
setAnim: GsapAnimation,
|
|
@@ -267,6 +272,7 @@ async function promoteSetToKeyframes(
|
|
|
267
272
|
{
|
|
268
273
|
percentage: 0,
|
|
269
274
|
properties: Object.keys(startPosition).length > 0 ? startPosition : endPosition,
|
|
275
|
+
auto: true,
|
|
270
276
|
},
|
|
271
277
|
{ percentage: 100, properties: endPosition },
|
|
272
278
|
],
|
|
@@ -283,6 +289,7 @@ async function promoteSetToKeyframes(
|
|
|
283
289
|
* the path, inserted at the matching segment so the curve is preserved. Outside the
|
|
284
290
|
* range, extend the duration so the motion reaches the playhead.
|
|
285
291
|
*/
|
|
292
|
+
// fallow-ignore-next-line complexity
|
|
286
293
|
async function applyArcWaypointAtPlayhead(
|
|
287
294
|
session: EnableKeyframesSession,
|
|
288
295
|
sel: DomEditSelection,
|
|
@@ -332,10 +339,10 @@ async function applyArcWaypointAtPlayhead(
|
|
|
332
339
|
);
|
|
333
340
|
}
|
|
334
341
|
|
|
335
|
-
// fallow-ignore-next-line complexity
|
|
336
342
|
export function useEnableKeyframes(
|
|
337
343
|
sessionRef: React.RefObject<EnableKeyframesSession | undefined>,
|
|
338
344
|
) {
|
|
345
|
+
// fallow-ignore-next-line complexity
|
|
339
346
|
return useCallback(async () => {
|
|
340
347
|
const session = sessionRef.current;
|
|
341
348
|
if (!session) return;
|