feat: integrate mobile work chat and Gitea delivery
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import type {
|
||||
PointerEvent as ReactPointerEvent,
|
||||
TransitionEvent as ReactTransitionEvent,
|
||||
} from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
|
||||
export type SwipeDirection = "left" | "right";
|
||||
|
||||
interface UseSwipeCardOptions {
|
||||
readonly itemKey: string;
|
||||
readonly onSwipe: (direction: SwipeDirection) => void;
|
||||
}
|
||||
|
||||
const SWIPE_THRESHOLD = 82;
|
||||
const EXIT_DISTANCE = 460;
|
||||
const EXIT_DURATION_MS = 180;
|
||||
|
||||
export const useSwipeCard = ({ itemKey, onSwipe }: UseSwipeCardOptions) => {
|
||||
const cardRef = useRef<HTMLElement>(null);
|
||||
const checkedLabelRef = useRef<HTMLSpanElement>(null);
|
||||
const backLabelRef = useRef<HTMLSpanElement>(null);
|
||||
const startXRef = useRef(0);
|
||||
const distanceRef = useRef(0);
|
||||
const draggingRef = useRef(false);
|
||||
const swipingRef = useRef(false);
|
||||
const pendingDirectionRef = useRef<SwipeDirection | null>(null);
|
||||
const exitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const updateVisuals = useCallback((distance: number, animate = false) => {
|
||||
const card = cardRef.current;
|
||||
if (!card) {
|
||||
return;
|
||||
}
|
||||
card.style.transition = animate
|
||||
? `transform ${EXIT_DURATION_MS}ms cubic-bezier(0.16, 1, 0.3, 1)`
|
||||
: "none";
|
||||
card.style.transform = `translate3d(${distance}px, 0, 0) rotate(${distance / 32}deg)`;
|
||||
const actionOpacity = Math.min(Math.abs(distance) / SWIPE_THRESHOLD, 1);
|
||||
if (checkedLabelRef.current) {
|
||||
checkedLabelRef.current.style.opacity =
|
||||
distance > 0 ? String(actionOpacity) : "0";
|
||||
}
|
||||
if (backLabelRef.current) {
|
||||
backLabelRef.current.style.opacity =
|
||||
distance < 0 ? String(actionOpacity) : "0";
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
if (exitTimerRef.current) {
|
||||
clearTimeout(exitTimerRef.current);
|
||||
exitTimerRef.current = null;
|
||||
}
|
||||
distanceRef.current = 0;
|
||||
draggingRef.current = false;
|
||||
swipingRef.current = false;
|
||||
pendingDirectionRef.current = null;
|
||||
updateVisuals(0, true);
|
||||
}, [updateVisuals]);
|
||||
|
||||
const finishSwipe = useCallback(() => {
|
||||
const direction = pendingDirectionRef.current;
|
||||
if (!direction) {
|
||||
return;
|
||||
}
|
||||
pendingDirectionRef.current = null;
|
||||
if (exitTimerRef.current) {
|
||||
clearTimeout(exitTimerRef.current);
|
||||
exitTimerRef.current = null;
|
||||
}
|
||||
onSwipe(direction);
|
||||
}, [onSwipe]);
|
||||
|
||||
const swipe = useCallback(
|
||||
(direction: SwipeDirection) => {
|
||||
if (swipingRef.current) {
|
||||
return;
|
||||
}
|
||||
swipingRef.current = true;
|
||||
pendingDirectionRef.current = direction;
|
||||
const distance = direction === "right" ? EXIT_DISTANCE : -EXIT_DISTANCE;
|
||||
distanceRef.current = distance;
|
||||
updateVisuals(distance, true);
|
||||
exitTimerRef.current = setTimeout(finishSwipe, EXIT_DURATION_MS + 80);
|
||||
},
|
||||
[finishSwipe, updateVisuals]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
reset();
|
||||
return reset;
|
||||
}, [itemKey, reset]);
|
||||
|
||||
const onPointerDown = (event: ReactPointerEvent<HTMLElement>) => {
|
||||
if (
|
||||
swipingRef.current ||
|
||||
(event.pointerType === "mouse" && event.button !== 0)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
draggingRef.current = true;
|
||||
startXRef.current = event.clientX;
|
||||
distanceRef.current = 0;
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
};
|
||||
|
||||
const onPointerMove = (event: ReactPointerEvent<HTMLElement>) => {
|
||||
if (!draggingRef.current) {
|
||||
return;
|
||||
}
|
||||
distanceRef.current = event.clientX - startXRef.current;
|
||||
updateVisuals(distanceRef.current);
|
||||
};
|
||||
|
||||
const onPointerUp = (event: ReactPointerEvent<HTMLElement>) => {
|
||||
if (!draggingRef.current) {
|
||||
return;
|
||||
}
|
||||
draggingRef.current = false;
|
||||
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
|
||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
if (Math.abs(distanceRef.current) >= SWIPE_THRESHOLD) {
|
||||
swipe(distanceRef.current > 0 ? "right" : "left");
|
||||
return;
|
||||
}
|
||||
reset();
|
||||
};
|
||||
|
||||
const onTransitionEnd = (event: ReactTransitionEvent<HTMLElement>) => {
|
||||
if (
|
||||
event.currentTarget === event.target &&
|
||||
event.propertyName === "transform" &&
|
||||
swipingRef.current
|
||||
) {
|
||||
finishSwipe();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
backLabelRef,
|
||||
cardRef,
|
||||
checkedLabelRef,
|
||||
dragged: () => Math.abs(distanceRef.current) > 8,
|
||||
pointerHandlers: {
|
||||
onPointerCancel: reset,
|
||||
onPointerDown,
|
||||
onPointerMove,
|
||||
onPointerUp,
|
||||
},
|
||||
swipe,
|
||||
transitionHandlers: {
|
||||
onTransitionEnd,
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
Reference in New Issue
Block a user