chore: update cinny submodule to latest commit and modify versioning for Android app
This commit is contained in:
@@ -27,8 +27,8 @@ android {
|
|||||||
applicationId "com.paarrot.app"
|
applicationId "com.paarrot.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 1783526741
|
versionCode 1783582912
|
||||||
versionName "2026-07-08.160541.766"
|
versionName "2026-07-09.074152.772"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -1,24 +1,38 @@
|
|||||||
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useCompactNav } from '../../hooks/useCompactNav';
|
import { useCompactNav } from '../../hooks/useCompactNav';
|
||||||
import { useBackRoute } from '../../hooks/useBackRoute';
|
import { useBackRoute } from '../../hooks/useBackRoute';
|
||||||
|
import {
|
||||||
|
claimMobileGesture,
|
||||||
|
clearMobileGesture,
|
||||||
|
getActiveMobileGesture,
|
||||||
|
} from './mobileGestureArbitration';
|
||||||
|
import { useWindowPointerDrag } from './useWindowPointerDrag';
|
||||||
import * as css from './mobile-gestures.css';
|
import * as css from './mobile-gestures.css';
|
||||||
|
|
||||||
const COMMIT_RATIO = 0.28;
|
const COMMIT_RATIO = 0.28;
|
||||||
const MIN_COMMIT_PX = 72;
|
const MIN_COMMIT_PX = 72;
|
||||||
const MAX_START_Y_RATIO = 0.88;
|
const MAX_START_Y_RATIO = 0.88;
|
||||||
|
const DRAG_THRESHOLD = 8;
|
||||||
|
|
||||||
type DragState = {
|
type DragState = {
|
||||||
pointerId: number;
|
pointerId: number;
|
||||||
startX: number;
|
startX: number;
|
||||||
startY: number;
|
startY: number;
|
||||||
dragging: boolean;
|
|
||||||
moved: boolean;
|
moved: boolean;
|
||||||
|
offset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MobileSwipeBackPanelProps = {
|
type MobileSwipeBackPanelProps = {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readTransformOffset = (el: HTMLElement | null): number => {
|
||||||
|
if (!el) return 0;
|
||||||
|
const transform = window.getComputedStyle(el).transform;
|
||||||
|
if (!transform || transform === 'none') return 0;
|
||||||
|
return new DOMMatrix(transform).m41;
|
||||||
|
};
|
||||||
|
|
||||||
export function MobileSwipeBackPanel({ children }: MobileSwipeBackPanelProps) {
|
export function MobileSwipeBackPanel({ children }: MobileSwipeBackPanelProps) {
|
||||||
const compact = useCompactNav();
|
const compact = useCompactNav();
|
||||||
const { canGoBack, goBack } = useBackRoute();
|
const { canGoBack, goBack } = useBackRoute();
|
||||||
@@ -27,22 +41,25 @@ export function MobileSwipeBackPanel({ children }: MobileSwipeBackPanelProps) {
|
|||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
const dragRef = useRef<DragState | null>(null);
|
const dragRef = useRef<DragState | null>(null);
|
||||||
const [offset, setOffset] = useState(0);
|
|
||||||
const [animating, setAnimating] = useState(false);
|
const [animating, setAnimating] = useState(false);
|
||||||
|
|
||||||
const resetTransform = useCallback((animate = true) => {
|
const resetTransform = useCallback((animate = true) => {
|
||||||
const content = contentRef.current;
|
const content = contentRef.current;
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
setAnimating(animate);
|
setAnimating(animate);
|
||||||
setOffset(0);
|
|
||||||
content.style.transition = animate ? 'transform 0.22s cubic-bezier(0.4, 0, 0.2, 1)' : 'none';
|
content.style.transition = animate ? 'transform 0.22s cubic-bezier(0.4, 0, 0.2, 1)' : 'none';
|
||||||
content.style.transform = 'translateX(0px)';
|
content.style.transform = 'translateX(0px)';
|
||||||
|
if (dragRef.current) {
|
||||||
|
dragRef.current.offset = 0;
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setTransform = useCallback((px: number, animate = false) => {
|
const setTransform = useCallback((px: number, animate = false) => {
|
||||||
const content = contentRef.current;
|
const content = contentRef.current;
|
||||||
if (!content) return;
|
if (!content) return;
|
||||||
setOffset(px);
|
if (dragRef.current) {
|
||||||
|
dragRef.current.offset = px;
|
||||||
|
}
|
||||||
content.style.transition = animate
|
content.style.transition = animate
|
||||||
? 'transform 0.22s cubic-bezier(0.4, 0, 0.2, 1)'
|
? 'transform 0.22s cubic-bezier(0.4, 0, 0.2, 1)'
|
||||||
: 'none';
|
: 'none';
|
||||||
@@ -69,6 +86,84 @@ export function MobileSwipeBackPanel({ children }: MobileSwipeBackPanelProps) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const releaseCapture = useCallback((pointerId: number) => {
|
||||||
|
const root = rootRef.current;
|
||||||
|
if (root?.hasPointerCapture(pointerId)) {
|
||||||
|
root.releasePointerCapture(pointerId);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const endDrag = useCallback(
|
||||||
|
(pointerId: number) => {
|
||||||
|
const drag = dragRef.current;
|
||||||
|
if (!drag || drag.pointerId !== pointerId) return;
|
||||||
|
|
||||||
|
releaseCapture(pointerId);
|
||||||
|
dragRef.current = null;
|
||||||
|
clearMobileGesture(pointerId);
|
||||||
|
|
||||||
|
if (!drag.moved) {
|
||||||
|
resetTransform(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = rootRef.current?.clientWidth ?? window.innerWidth;
|
||||||
|
const currentOffset = Math.max(
|
||||||
|
drag.offset,
|
||||||
|
readTransformOffset(contentRef.current)
|
||||||
|
);
|
||||||
|
const shouldCommit = currentOffset >= Math.max(width * COMMIT_RATIO, MIN_COMMIT_PX);
|
||||||
|
|
||||||
|
if (shouldCommit) {
|
||||||
|
commitBack();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetTransform(true);
|
||||||
|
},
|
||||||
|
[commitBack, releaseCapture, resetTransform]
|
||||||
|
);
|
||||||
|
|
||||||
|
const processPointerMove = useCallback(
|
||||||
|
(evt: { pointerId: number; clientX: number; clientY: number; preventDefault?: () => void }) => {
|
||||||
|
const drag = dragRef.current;
|
||||||
|
if (!drag || drag.pointerId !== evt.pointerId) return;
|
||||||
|
|
||||||
|
const activeGesture = getActiveMobileGesture(evt.pointerId);
|
||||||
|
if (activeGesture && activeGesture !== 'back') return;
|
||||||
|
|
||||||
|
const deltaX = evt.clientX - drag.startX;
|
||||||
|
const deltaY = evt.clientY - drag.startY;
|
||||||
|
|
||||||
|
if (!drag.moved) {
|
||||||
|
if (Math.abs(deltaX) < DRAG_THRESHOLD && Math.abs(deltaY) < DRAG_THRESHOLD) return;
|
||||||
|
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
||||||
|
dragRef.current = null;
|
||||||
|
clearMobileGesture(evt.pointerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deltaX <= 0) {
|
||||||
|
dragRef.current = null;
|
||||||
|
clearMobileGesture(evt.pointerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!claimMobileGesture('back', evt.pointerId)) return;
|
||||||
|
|
||||||
|
drag.moved = true;
|
||||||
|
try {
|
||||||
|
rootRef.current?.setPointerCapture(evt.pointerId);
|
||||||
|
} catch {
|
||||||
|
// Ignore capture failures on Android WebView.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.preventDefault?.();
|
||||||
|
const width = rootRef.current?.clientWidth ?? window.innerWidth;
|
||||||
|
setTransform(Math.min(Math.max(deltaX, 0), width), false);
|
||||||
|
},
|
||||||
|
[setTransform]
|
||||||
|
);
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||||
if (!enabled || animating || evt.button !== 0 || !evt.isPrimary) return;
|
if (!enabled || animating || evt.button !== 0 || !evt.isPrimary) return;
|
||||||
@@ -79,77 +174,23 @@ export function MobileSwipeBackPanel({ children }: MobileSwipeBackPanelProps) {
|
|||||||
pointerId: evt.pointerId,
|
pointerId: evt.pointerId,
|
||||||
startX: evt.clientX,
|
startX: evt.clientX,
|
||||||
startY: evt.clientY,
|
startY: evt.clientY,
|
||||||
dragging: true,
|
|
||||||
moved: false,
|
moved: false,
|
||||||
|
offset: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
rootRef.current?.setPointerCapture(evt.pointerId);
|
|
||||||
},
|
},
|
||||||
[animating, enabled]
|
[animating, enabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerMove = useCallback(
|
const isActivePointer = useCallback((pointerId: number) => {
|
||||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
return dragRef.current?.pointerId === pointerId;
|
||||||
const drag = dragRef.current;
|
}, []);
|
||||||
if (!drag || !drag.dragging || drag.pointerId !== evt.pointerId) return;
|
|
||||||
|
|
||||||
const deltaX = evt.clientX - drag.startX;
|
useWindowPointerDrag({
|
||||||
const deltaY = evt.clientY - drag.startY;
|
enabled,
|
||||||
|
isActivePointer,
|
||||||
if (!drag.moved) {
|
onMove: processPointerMove,
|
||||||
if (Math.abs(deltaX) < 8 && Math.abs(deltaY) < 8) return;
|
onEnd: endDrag,
|
||||||
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
});
|
||||||
dragRef.current = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (deltaX <= 0) {
|
|
||||||
dragRef.current = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
drag.moved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
evt.preventDefault();
|
|
||||||
const width = rootRef.current?.clientWidth ?? window.innerWidth;
|
|
||||||
setTransform(Math.min(Math.max(deltaX, 0), width), false);
|
|
||||||
},
|
|
||||||
[setTransform]
|
|
||||||
);
|
|
||||||
|
|
||||||
const endDrag = useCallback(
|
|
||||||
(pointerId: number) => {
|
|
||||||
const drag = dragRef.current;
|
|
||||||
if (!drag || drag.pointerId !== pointerId) return;
|
|
||||||
|
|
||||||
if (rootRef.current?.hasPointerCapture(pointerId)) {
|
|
||||||
rootRef.current.releasePointerCapture(pointerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
dragRef.current = null;
|
|
||||||
|
|
||||||
if (!drag.moved) {
|
|
||||||
resetTransform(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const width = rootRef.current?.clientWidth ?? window.innerWidth;
|
|
||||||
const shouldCommit = offset >= Math.max(width * COMMIT_RATIO, MIN_COMMIT_PX);
|
|
||||||
if (shouldCommit) {
|
|
||||||
commitBack();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resetTransform(true);
|
|
||||||
},
|
|
||||||
[commitBack, offset, resetTransform]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePointerUp = useCallback(
|
|
||||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
|
||||||
endDrag(evt.pointerId);
|
|
||||||
},
|
|
||||||
[endDrag]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetTransform(false);
|
resetTransform(false);
|
||||||
@@ -164,9 +205,10 @@ export function MobileSwipeBackPanel({ children }: MobileSwipeBackPanelProps) {
|
|||||||
ref={rootRef}
|
ref={rootRef}
|
||||||
className={css.SwipeBackRoot}
|
className={css.SwipeBackRoot}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={processPointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={(evt) => endDrag(evt.pointerId)}
|
||||||
onPointerCancel={handlePointerUp}
|
onPointerCancel={(evt) => endDrag(evt.pointerId)}
|
||||||
|
onLostPointerCapture={(evt) => endDrag(evt.pointerId)}
|
||||||
>
|
>
|
||||||
<div className={css.SwipeBackUnderlay} aria-hidden>
|
<div className={css.SwipeBackUnderlay} aria-hidden>
|
||||||
<div className={css.SwipeBackSidebarPeek} />
|
<div className={css.SwipeBackSidebarPeek} />
|
||||||
|
|||||||
@@ -6,10 +6,17 @@ import { Icon, Icons } from '../icons';
|
|||||||
import { useCompactNav } from '../../hooks/useCompactNav';
|
import { useCompactNav } from '../../hooks/useCompactNav';
|
||||||
import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
|
import { roomIdToReplyDraftAtomFamily } from '../../state/room/roomInputDrafts';
|
||||||
import { startReplyToEvent } from '../../features/room/replyToMessage';
|
import { startReplyToEvent } from '../../features/room/replyToMessage';
|
||||||
|
import {
|
||||||
|
claimMobileGesture,
|
||||||
|
clearMobileGesture,
|
||||||
|
getActiveMobileGesture,
|
||||||
|
} from './mobileGestureArbitration';
|
||||||
|
import { useWindowPointerDrag } from './useWindowPointerDrag';
|
||||||
import * as css from './mobile-gestures.css';
|
import * as css from './mobile-gestures.css';
|
||||||
|
|
||||||
const SWIPE_THRESHOLD = 56;
|
const SWIPE_THRESHOLD = 56;
|
||||||
const MAX_SWIPE = 88;
|
const MAX_SWIPE = 88;
|
||||||
|
const DRAG_THRESHOLD = 8;
|
||||||
|
|
||||||
type DragState = {
|
type DragState = {
|
||||||
pointerId: number;
|
pointerId: number;
|
||||||
@@ -18,6 +25,7 @@ type DragState = {
|
|||||||
messageEl: HTMLElement;
|
messageEl: HTMLElement;
|
||||||
messageId: string;
|
messageId: string;
|
||||||
moved: boolean;
|
moved: boolean;
|
||||||
|
offset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type MobileSwipeToReplyLayerProps = {
|
type MobileSwipeToReplyLayerProps = {
|
||||||
@@ -42,38 +50,34 @@ export function MobileSwipeToReplyLayer({ room, editor, children }: MobileSwipeT
|
|||||||
target.style.transform = 'translateX(0px)';
|
target.style.transform = 'translateX(0px)';
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const resetGesture = useCallback(() => {
|
const releaseCapture = useCallback((pointerId: number) => {
|
||||||
clearMessageTransform();
|
const layer = layerRef.current;
|
||||||
dragRef.current = null;
|
if (layer?.hasPointerCapture(pointerId)) {
|
||||||
setIndicatorTop(null);
|
layer.releasePointerCapture(pointerId);
|
||||||
setIndicatorActive(false);
|
}
|
||||||
}, [clearMessageTransform]);
|
}, []);
|
||||||
|
|
||||||
|
const resetGesture = useCallback(
|
||||||
|
(pointerId?: number) => {
|
||||||
|
if (pointerId !== undefined) {
|
||||||
|
releaseCapture(pointerId);
|
||||||
|
clearMobileGesture(pointerId);
|
||||||
|
}
|
||||||
|
clearMessageTransform();
|
||||||
|
dragRef.current = null;
|
||||||
|
setIndicatorTop(null);
|
||||||
|
setIndicatorActive(false);
|
||||||
|
},
|
||||||
|
[clearMessageTransform, releaseCapture]
|
||||||
|
);
|
||||||
|
|
||||||
const shouldIgnoreTarget = (target: EventTarget | null): boolean => {
|
const shouldIgnoreTarget = (target: EventTarget | null): boolean => {
|
||||||
if (!(target instanceof Element)) return true;
|
if (!(target instanceof Element)) return true;
|
||||||
if (
|
return Boolean(
|
||||||
target.closest(
|
target.closest(
|
||||||
'input, textarea, [contenteditable="true"], [data-allow-text-selection="true"], button, a, [role="button"], [data-disable-swipe-reply="true"]'
|
'input, textarea, [contenteditable="true"], [data-allow-text-selection="true"], [data-carousel-scroller], [data-disable-swipe-reply="true"]'
|
||||||
)
|
)
|
||||||
) {
|
);
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let el: Element | null = target;
|
|
||||||
while (el && layerRef.current?.contains(el)) {
|
|
||||||
if (el instanceof HTMLElement) {
|
|
||||||
const { overflowX } = window.getComputedStyle(el);
|
|
||||||
if (
|
|
||||||
(overflowX === 'auto' || overflowX === 'scroll') &&
|
|
||||||
el.scrollWidth > el.clientWidth + 8
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
el = el.parentElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const findMessageElement = (target: EventTarget | null): HTMLElement | null => {
|
const findMessageElement = (target: EventTarget | null): HTMLElement | null => {
|
||||||
@@ -90,6 +94,74 @@ export function MobileSwipeToReplyLayer({ room, editor, children }: MobileSwipeT
|
|||||||
setIndicatorActive(Math.abs(offset) >= SWIPE_THRESHOLD * 0.65);
|
setIndicatorActive(Math.abs(offset) >= SWIPE_THRESHOLD * 0.65);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const endDrag = useCallback(
|
||||||
|
(pointerId: number) => {
|
||||||
|
const drag = dragRef.current;
|
||||||
|
if (!drag || drag.pointerId !== pointerId) return;
|
||||||
|
|
||||||
|
releaseCapture(pointerId);
|
||||||
|
const { messageEl, messageId, moved, offset } = drag;
|
||||||
|
dragRef.current = null;
|
||||||
|
clearMobileGesture(pointerId);
|
||||||
|
|
||||||
|
if (!moved) {
|
||||||
|
resetGesture();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldReply = Math.abs(offset) >= SWIPE_THRESHOLD;
|
||||||
|
clearMessageTransform(messageEl, true);
|
||||||
|
setIndicatorTop(null);
|
||||||
|
setIndicatorActive(false);
|
||||||
|
|
||||||
|
if (shouldReply) {
|
||||||
|
startReplyToEvent(room, messageId, setReplyDraft, editor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[clearMessageTransform, editor, releaseCapture, resetGesture, room, setReplyDraft]
|
||||||
|
);
|
||||||
|
|
||||||
|
const processPointerMove = useCallback(
|
||||||
|
(evt: { pointerId: number; clientX: number; clientY: number; preventDefault?: () => void }) => {
|
||||||
|
const drag = dragRef.current;
|
||||||
|
if (!drag || drag.pointerId !== evt.pointerId) return;
|
||||||
|
|
||||||
|
const activeGesture = getActiveMobileGesture(evt.pointerId);
|
||||||
|
if (activeGesture && activeGesture !== 'reply') return;
|
||||||
|
|
||||||
|
const deltaX = evt.clientX - drag.startX;
|
||||||
|
const deltaY = evt.clientY - drag.startY;
|
||||||
|
|
||||||
|
if (!drag.moved) {
|
||||||
|
if (Math.abs(deltaX) < DRAG_THRESHOLD && Math.abs(deltaY) < DRAG_THRESHOLD) return;
|
||||||
|
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
||||||
|
resetGesture(evt.pointerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (deltaX >= 0) {
|
||||||
|
resetGesture(evt.pointerId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!claimMobileGesture('reply', evt.pointerId)) return;
|
||||||
|
|
||||||
|
drag.moved = true;
|
||||||
|
try {
|
||||||
|
layerRef.current?.setPointerCapture(evt.pointerId);
|
||||||
|
} catch {
|
||||||
|
// Ignore capture failures on Android WebView.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
evt.preventDefault?.();
|
||||||
|
const offset = Math.max(deltaX, -MAX_SWIPE);
|
||||||
|
drag.offset = offset;
|
||||||
|
drag.messageEl.style.transition = 'none';
|
||||||
|
drag.messageEl.style.transform = `translateX(${offset}px)`;
|
||||||
|
updateIndicator(drag.messageEl, offset);
|
||||||
|
},
|
||||||
|
[resetGesture, updateIndicator]
|
||||||
|
);
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
(evt: React.PointerEvent<HTMLDivElement>) => {
|
||||||
if (!enabled || evt.button !== 0 || !evt.isPrimary) return;
|
if (!enabled || evt.button !== 0 || !evt.isPrimary) return;
|
||||||
@@ -106,86 +178,26 @@ export function MobileSwipeToReplyLayer({ room, editor, children }: MobileSwipeT
|
|||||||
messageEl,
|
messageEl,
|
||||||
messageId,
|
messageId,
|
||||||
moved: false,
|
moved: false,
|
||||||
|
offset: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
layerRef.current?.setPointerCapture(evt.pointerId);
|
|
||||||
},
|
},
|
||||||
[enabled]
|
[enabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerMove = useCallback(
|
const isActivePointer = useCallback((pointerId: number) => {
|
||||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
return dragRef.current?.pointerId === pointerId;
|
||||||
const drag = dragRef.current;
|
}, []);
|
||||||
if (!drag || drag.pointerId !== evt.pointerId) return;
|
|
||||||
|
|
||||||
const deltaX = evt.clientX - drag.startX;
|
useWindowPointerDrag({
|
||||||
const deltaY = evt.clientY - drag.startY;
|
enabled,
|
||||||
|
isActivePointer,
|
||||||
|
onMove: processPointerMove,
|
||||||
|
onEnd: endDrag,
|
||||||
|
});
|
||||||
|
|
||||||
if (!drag.moved) {
|
useEffect(() => {
|
||||||
if (Math.abs(deltaX) < 8 && Math.abs(deltaY) < 8) return;
|
resetGesture();
|
||||||
if (Math.abs(deltaY) > Math.abs(deltaX)) {
|
}, [room.roomId, resetGesture]);
|
||||||
resetGesture();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (deltaX >= 0) {
|
|
||||||
resetGesture();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
drag.moved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
evt.preventDefault();
|
|
||||||
const offset = Math.max(deltaX, -MAX_SWIPE);
|
|
||||||
drag.messageEl.style.transition = 'none';
|
|
||||||
drag.messageEl.style.transform = `translateX(${offset}px)`;
|
|
||||||
updateIndicator(drag.messageEl, offset);
|
|
||||||
},
|
|
||||||
[resetGesture, updateIndicator]
|
|
||||||
);
|
|
||||||
|
|
||||||
const endDrag = useCallback(
|
|
||||||
(pointerId: number) => {
|
|
||||||
const drag = dragRef.current;
|
|
||||||
if (!drag || drag.pointerId !== pointerId) return;
|
|
||||||
|
|
||||||
if (layerRef.current?.hasPointerCapture(pointerId)) {
|
|
||||||
layerRef.current.releasePointerCapture(pointerId);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { messageEl, messageId, moved } = drag;
|
|
||||||
dragRef.current = null;
|
|
||||||
|
|
||||||
if (!moved) {
|
|
||||||
resetGesture();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const matrix = window.getComputedStyle(messageEl).transform;
|
|
||||||
const offset =
|
|
||||||
matrix && matrix !== 'none'
|
|
||||||
? Number(new DOMMatrix(matrix).m41)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const shouldReply = Math.abs(offset) >= SWIPE_THRESHOLD;
|
|
||||||
clearMessageTransform(messageEl, true);
|
|
||||||
setIndicatorTop(null);
|
|
||||||
setIndicatorActive(false);
|
|
||||||
|
|
||||||
if (shouldReply) {
|
|
||||||
startReplyToEvent(room, messageId, setReplyDraft, editor);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[clearMessageTransform, editor, resetGesture, room, setReplyDraft]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handlePointerUp = useCallback(
|
|
||||||
(evt: React.PointerEvent<HTMLDivElement>) => {
|
|
||||||
endDrag(evt.pointerId);
|
|
||||||
},
|
|
||||||
[endDrag]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => resetGesture, [room.roomId, resetGesture]);
|
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
return <>{children}</>;
|
return <>{children}</>;
|
||||||
@@ -196,9 +208,10 @@ export function MobileSwipeToReplyLayer({ room, editor, children }: MobileSwipeT
|
|||||||
ref={layerRef}
|
ref={layerRef}
|
||||||
className={css.SwipeToReplyLayer}
|
className={css.SwipeToReplyLayer}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={processPointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={(evt) => endDrag(evt.pointerId)}
|
||||||
onPointerCancel={handlePointerUp}
|
onPointerCancel={(evt) => endDrag(evt.pointerId)}
|
||||||
|
onLostPointerCapture={(evt) => endDrag(evt.pointerId)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
{indicatorTop !== null && (
|
{indicatorTop !== null && (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const SwipeBackRoot = style({
|
|||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
touchAction: 'pan-y',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SwipeBackUnderlay = style({
|
export const SwipeBackUnderlay = style({
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
export type MobileGestureKind = 'back' | 'reply';
|
||||||
|
|
||||||
|
type ActiveGesture = {
|
||||||
|
kind: MobileGestureKind;
|
||||||
|
pointerId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let activeGesture: ActiveGesture | null = null;
|
||||||
|
|
||||||
|
export const claimMobileGesture = (kind: MobileGestureKind, pointerId: number): boolean => {
|
||||||
|
if (!activeGesture || activeGesture.pointerId === pointerId) {
|
||||||
|
activeGesture = { kind, pointerId };
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return activeGesture.kind === kind && activeGesture.pointerId === pointerId;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getActiveMobileGesture = (pointerId: number): MobileGestureKind | null => {
|
||||||
|
if (!activeGesture || activeGesture.pointerId !== pointerId) return null;
|
||||||
|
return activeGesture.kind;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearMobileGesture = (pointerId: number) => {
|
||||||
|
if (activeGesture?.pointerId === pointerId) {
|
||||||
|
activeGesture = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
46
overlay/src/app/components/mobile/useWindowPointerDrag.ts
Normal file
46
overlay/src/app/components/mobile/useWindowPointerDrag.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
type PointerLikeEvent = {
|
||||||
|
pointerId: number;
|
||||||
|
clientX: number;
|
||||||
|
clientY: number;
|
||||||
|
preventDefault?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseWindowPointerDragOptions = {
|
||||||
|
enabled: boolean;
|
||||||
|
isActivePointer: (pointerId: number) => boolean;
|
||||||
|
onMove: (evt: PointerLikeEvent) => void;
|
||||||
|
onEnd: (pointerId: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useWindowPointerDrag = ({
|
||||||
|
enabled,
|
||||||
|
isActivePointer,
|
||||||
|
onMove,
|
||||||
|
onEnd,
|
||||||
|
}: UseWindowPointerDragOptions) => {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
const handlePointerMove = (evt: PointerEvent) => {
|
||||||
|
if (!isActivePointer(evt.pointerId)) return;
|
||||||
|
onMove(evt);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointerEnd = (evt: PointerEvent) => {
|
||||||
|
if (!isActivePointer(evt.pointerId)) return;
|
||||||
|
onEnd(evt.pointerId);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('pointermove', handlePointerMove, true);
|
||||||
|
window.addEventListener('pointerup', handlePointerEnd, true);
|
||||||
|
window.addEventListener('pointercancel', handlePointerEnd, true);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('pointermove', handlePointerMove, true);
|
||||||
|
window.removeEventListener('pointerup', handlePointerEnd, true);
|
||||||
|
window.removeEventListener('pointercancel', handlePointerEnd, true);
|
||||||
|
};
|
||||||
|
}, [enabled, isActivePointer, onEnd, onMove]);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user