();
-
-export const subscribeExternalSwipe = (listener: Listener): (() => void) => {
- listeners.add(listener);
- return () => {
- listeners.delete(listener);
- };
-};
-
-export const emitExternalSwipe = (touch: ExternalSwipeTouch): void => {
- listeners.forEach((listener) => listener(touch));
-};
diff --git a/src/app/components/swipe-back/useSwipeBackGesture.ts b/src/app/components/swipe-back/useSwipeBackGesture.ts
index 0fea59b2..9d33af62 100644
--- a/src/app/components/swipe-back/useSwipeBackGesture.ts
+++ b/src/app/components/swipe-back/useSwipeBackGesture.ts
@@ -1,6 +1,5 @@
import { MutableRefObject, useEffect, useRef } from 'react';
import { COMMIT_FRACTION, DEAD_ZONE_PX, EDGE_GUARD_PX, MIN_COMMIT_PX } from './geometry';
-import { subscribeExternalSwipe } from './externalSwipeFeed';
type Args = {
// The sliding chat CARD element the listeners bind to. Touches outside it
@@ -30,15 +29,10 @@ type Args = {
// `touch-action: pan-y`, so the browser reserves horizontal panning for us
// and our moves stay cancelable over the whole screen.
//
-// TWO input sources feed ONE state machine (begin/move/end/cancel below):
-// 1. Native touch listeners on the card root — every React-rendered
-// surface (chats, AI chat, hero headers).
-// 2. The external swipe feed (`externalSwipeFeed.ts`) — touches forwarded
-// by the bridge-bot widget iframes over the validated side-channel,
-// already mapped into host-viewport coordinates by `BotWidgetMount`.
-// An iframe consumes its touches natively, so without this feed the
-// gesture is dead over the widget body. External moves carry no
-// cancelable browser event — the widget side owns preventDefault.
+// Input is native touch listeners on the card root — every React-rendered
+// surface (chats, AI chat, hero headers). Touches that land inside a
+// cross-origin widget iframe (the bots tab) never reach these listeners, so
+// over the widget body the user falls back to the system / hardware back.
export function useSwipeBackGesture({ rootRef, enabled, disabled, setDrag, onBack }: Args): void {
const disabledRef = useRef(disabled);
const setDragRef = useRef(setDrag);
@@ -76,16 +70,9 @@ export function useSwipeBackGesture({ rootRef, enabled, disabled, setDrag, onBac
reset();
};
- // ── State machine (shared by both input sources) ──────────────────
+ // ── State machine (begin / move / end / cancel) ───────────────────
const begin = (x: number, y: number) => {
- // Heal any orphaned offset FIRST: unlike native touches, the external
- // feed has no guaranteed terminal event — an iframe dying mid-drag
- // (widget crash → destroy() drops the message listener) never posts
- // 'end', which would otherwise leave the card frozen half-slid with
- // no recovery (a plain tap's end() sees clean state and no-ops).
- // springHome is a no-op after a cleanly terminated gesture.
- springHome();
if (disabledRef.current) return;
const vw = window.innerWidth;
// The L/R edge strip belongs to the Android system back-gesture in
@@ -99,11 +86,9 @@ export function useSwipeBackGesture({ rootRef, enabled, disabled, setDrag, onBac
lastDragPx = 0;
};
- // `preventDefault` is the source-specific cancelation hook: native
- // touches pass the cancelable TouchEvent's preventDefault; the external
- // feed passes undefined (the widget side already prevented its own
- // default once it resolved the gesture as ours).
- const move = (x: number, y: number, preventDefault?: () => void) => {
+ // `preventDefault` cancels the browser's own handling once we engage —
+ // the native touchmove stays cancelable across the whole `pan-y` card.
+ const move = (x: number, y: number, preventDefault: () => void) => {
if (disabledRef.current) {
springHome();
bailed = true;
@@ -134,7 +119,7 @@ export function useSwipeBackGesture({ rootRef, enabled, disabled, setDrag, onBac
// screen (including over the vertical-scrolling timeline), so the
// gesture engages everywhere, not just over non-scrollable spots. The
// guard is defensive only.
- preventDefault?.();
+ preventDefault();
const vw = window.innerWidth;
// Follow the finger 1:1 to the right; clamp to [0, vw]. Never negative:
@@ -169,8 +154,6 @@ export function useSwipeBackGesture({ rootRef, enabled, disabled, setDrag, onBac
springHome();
};
- // ── Source 1: native touches on the card root ─────────────────────
-
const onTouchStart = (e: TouchEvent) => {
if (e.touches.length !== 1) {
springHome();
@@ -200,21 +183,11 @@ export function useSwipeBackGesture({ rootRef, enabled, disabled, setDrag, onBac
root.addEventListener('touchend', onTouchEnd, { passive: true });
root.addEventListener('touchcancel', onTouchCancel, { passive: true });
- // ── Source 2: the external (widget iframe) feed ───────────────────
-
- const unsubscribeExternal = subscribeExternalSwipe((touch) => {
- if (touch.phase === 'start') begin(touch.x, touch.y);
- else if (touch.phase === 'move') move(touch.x, touch.y);
- else if (touch.phase === 'end') end();
- else cancel();
- });
-
return () => {
root.removeEventListener('touchstart', onTouchStart);
root.removeEventListener('touchmove', onTouchMove);
root.removeEventListener('touchend', onTouchEnd);
root.removeEventListener('touchcancel', onTouchCancel);
- unsubscribeExternal();
};
// setDrag / onBack are mirrored via refs so the listener never re-binds
// mid-gesture; `enabled` is a real dep so toggling it binds/unbinds.
diff --git a/src/app/features/bots/BotShell.tsx b/src/app/features/bots/BotShell.tsx
index bcc7c5eb..7132d23e 100644
--- a/src/app/features/bots/BotShell.tsx
+++ b/src/app/features/bots/BotShell.tsx
@@ -42,9 +42,10 @@ export function BotShell({ preset, room }: BotShellProps) {
setShowChat(true);
}, [setFailed, setShowChat]);
- // Swipe-back over the widget body works via the forwarded touch stream
- // (widget → `swipe-touch` side-channel → BotWidgetMount →
- // externalSwipeFeed) — no host-side capture strip needed.
+ // The widget runs in a cross-origin iframe, so touches that start on its
+ // body never reach the host swipe-back gesture; over the widget the user
+ // falls back to the system / hardware back. Swipe-back stays live on the
+ // surrounding native chrome (the hero header).
return (
diff --git a/src/app/features/bots/BotWidgetEmbed.ts b/src/app/features/bots/BotWidgetEmbed.ts
index 4b1e56f0..d22a943f 100644
--- a/src/app/features/bots/BotWidgetEmbed.ts
+++ b/src/app/features/bots/BotWidgetEmbed.ts
@@ -49,13 +49,6 @@ export type BotWidgetEmbedOptions = {
// the host's own picker. Plumbed from `BotWidgetMount` (via a ref-shim, like
// `onOpenMatrixToRoom`) where `mx` + navigation are available.
onAddToChat?: () => void;
- // Forwarded raw touch stream from the widget (`swipe-touch` action) —
- // coordinates are IFRAME-local; `BotWidgetMount` maps them into host
- // viewport space and feeds the swipe-back gesture. An iframe consumes
- // its touches natively, so this is the only way the
- // swipe-back-from-widget gesture can see drags that start on the widget
- // body. Carries no privileges — just pointer coordinates.
- onSwipeTouch?: (phase: 'start' | 'move' | 'end' | 'cancel', x: number, y: number) => void;
};
const getBotWidgetId = (preset: BotPreset): string => `vojo-bot-${preset.id}`;
@@ -254,14 +247,7 @@ export class BotWidgetEmbed {
// doesn't go through ClientWidgetApi at all — keeps the SDK ignorant
// of our extension and avoids the «unknown action» reply path.
//
- // Four actions today:
- //
- // * `swipe-touch` — unidirectional forwarded touch stream driving the
- // host's swipe-back gesture over the iframe (an iframe consumes its
- // own touches, so the widget cooperates via `swipe-forward.ts`).
- // Carries only a phase + viewport-local coordinates; shape-validated
- // in the handler below, mapped to host coordinates by
- // `BotWidgetMount`, consumed by `externalSwipeFeed`.
+ // Three actions today:
//
// * `add-to-chat` — elevated verb, gated on the `vojo.add_to_chat`
// capability opt-in in config.json. Carries NO url and NO room/mxid:
@@ -321,20 +307,6 @@ export class BotWidgetEmbed {
if (!msg || typeof msg !== 'object') return;
if (msg.api !== 'io.vojo.bot-widget') return;
- // Forwarded touch stream for the swipe-back gesture (see
- // `onSwipeTouch` in the options). Validated shape only — finite
- // numbers and a known phase; everything else is silently dropped.
- if (msg.action === 'swipe-touch') {
- const data = msg.data as { phase?: unknown; x?: unknown; y?: unknown } | undefined;
- const phase = data?.phase;
- const { x, y } = data ?? {};
- if (phase !== 'start' && phase !== 'move' && phase !== 'end' && phase !== 'cancel') return;
- if (typeof x !== 'number' || !Number.isFinite(x)) return;
- if (typeof y !== 'number' || !Number.isFinite(y)) return;
- this.options.onSwipeTouch?.(phase, x, y);
- return;
- }
-
// Elevated verb — must be dispatched BEFORE any url extraction. Its `data`
// is `{}` (no url), so a hoisted `typeof url === 'string'` gate would
// early-return and the verb would never fire (F1). The host gate reads the
diff --git a/src/app/features/bots/BotWidgetMount.tsx b/src/app/features/bots/BotWidgetMount.tsx
index fd3d3808..4ecd2221 100644
--- a/src/app/features/bots/BotWidgetMount.tsx
+++ b/src/app/features/bots/BotWidgetMount.tsx
@@ -17,7 +17,6 @@ import {
import type { MatrixToRoom } from '../../plugins/matrix-to';
import { Membership } from '../../../types/matrix/room';
import { getRoomToParents, isOneOnOneRoom, isSpace } from '../../utils/room';
-import { emitExternalSwipe } from '../../components/swipe-back/externalSwipeFeed';
import { useBotWidgetEmbed } from './useBotWidgetEmbed';
import { BotAddToChatPicker } from './BotAddToChatPicker';
import * as css from './BotWidgetMount.css';
@@ -237,24 +236,6 @@ export function BotWidgetMount({ preset, room, onError }: BotWidgetMountProps) {
const [pickerOpen, setPickerOpen] = useState(false);
const handleAddToChat = useCallback(() => setPickerOpen(true), []);
- // The widget's forwarded touch stream → the swipe-back gesture. The
- // widget reports IFRAME-local clientX/Y; offset by the mount's viewport
- // rect so the host-side state machine (edge guard, axis resolve in
- // `useSwipeBackGesture`) sees true host-viewport coordinates. Always
- // emitted — the feed only has a subscriber when the swipe overlay is
- // mounted (mobile + native), everywhere else it's a no-op.
- const handleSwipeTouch = useCallback(
- (phase: 'start' | 'move' | 'end' | 'cancel', x: number, y: number) => {
- const rect = containerRef.current?.getBoundingClientRect();
- emitExternalSwipe({
- phase,
- x: x + (rect?.left ?? 0),
- y: y + (rect?.top ?? 0),
- });
- },
- []
- );
-
const { ready } = useBotWidgetEmbed({
containerRef,
preset,
@@ -262,7 +243,6 @@ export function BotWidgetMount({ preset, room, onError }: BotWidgetMountProps) {
onError,
onOpenMatrixToRoom: handleOpenMatrixToRoom,
onAddToChat: handleAddToChat,
- onSwipeTouch: handleSwipeTouch,
});
// Track Matrix sync state so the bot loading bar yields to the global
diff --git a/src/app/features/bots/useBotWidgetEmbed.ts b/src/app/features/bots/useBotWidgetEmbed.ts
index 61c24a82..c4ed5009 100644
--- a/src/app/features/bots/useBotWidgetEmbed.ts
+++ b/src/app/features/bots/useBotWidgetEmbed.ts
@@ -19,10 +19,6 @@ type UseBotWidgetEmbedOptions = {
// `add-to-chat` verb (host opens its own room picker). Plumbed from
// `BotWidgetMount` where `mx` is available.
onAddToChat?: () => void;
- // Forwarded into the embed — the widget's raw touch stream (iframe-local
- // coordinates) for the swipe-back gesture. `BotWidgetMount` maps the
- // coordinates and feeds `externalSwipeFeed`.
- onSwipeTouch?: (phase: 'start' | 'move' | 'end' | 'cancel', x: number, y: number) => void;
};
type UseBotWidgetEmbedResult = {
@@ -44,7 +40,6 @@ export const useBotWidgetEmbed = ({
onError,
onOpenMatrixToRoom,
onAddToChat,
- onSwipeTouch,
}: UseBotWidgetEmbedOptions): UseBotWidgetEmbedResult => {
const { i18n } = useTranslation();
const mx = useMatrixClient();
@@ -68,10 +63,6 @@ export const useBotWidgetEmbed = ({
// render) so the embed lifecycle effect doesn't remount the iframe.
const onAddToChatRef = useRef(onAddToChat);
onAddToChatRef.current = onAddToChat;
- // Same ref indirection for the swipe feed — per-render closure identity
- // must not remount the iframe.
- const onSwipeTouchRef = useRef(onSwipeTouch);
- onSwipeTouchRef.current = onSwipeTouch;
// Depend on primitive identity for the embed lifecycle — using `preset`
// directly would remount the iframe (and re-handshake with the widget)
@@ -105,7 +96,6 @@ export const useBotWidgetEmbed = ({
// navigate-callback closes over a new render's `mx`/`navigate`.
onOpenMatrixToRoom: (target) => onOpenMatrixToRoomRef.current?.(target),
onAddToChat: () => onAddToChatRef.current?.(),
- onSwipeTouch: (phase, x, y) => onSwipeTouchRef.current?.(phase, x, y),
});
embedRef.current = embed;
} catch (error) {