feat: mobile long-press context menu, settings logout button
Mobile: - Add useLongPress hook (500ms, movement cancellation, press feedback) - Disable drag-and-drop on mobile in useEmailDrag - Wire long-press context menu to all email list item components - Add select-none and visual press feedback (scale + ring) Settings: - Add logout button to settings page sidebar
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from '@/i18n/navigation';
|
import { useRouter } from '@/i18n/navigation';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { ArrowLeft, ChevronRight, Settings as SettingsIcon } from 'lucide-react';
|
import { ArrowLeft, ChevronRight, LogOut, Settings as SettingsIcon } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { AppearanceSettings } from '@/components/settings/appearance-settings';
|
import { AppearanceSettings } from '@/components/settings/appearance-settings';
|
||||||
import { EmailSettings } from '@/components/settings/email-settings';
|
import { EmailSettings } from '@/components/settings/email-settings';
|
||||||
@@ -29,6 +29,7 @@ type Tab = 'appearance' | 'email' | 'account' | 'security' | 'identities' | 'vac
|
|||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations('settings');
|
const t = useTranslations('settings');
|
||||||
|
const tSidebar = useTranslations('sidebar');
|
||||||
const { client, isAuthenticated, logout } = useAuthStore();
|
const { client, isAuthenticated, logout } = useAuthStore();
|
||||||
const { quota, isPushConnected } = useEmailStore();
|
const { quota, isPushConnected } = useEmailStore();
|
||||||
const { stalwartFeaturesEnabled } = useConfig();
|
const { stalwartFeaturesEnabled } = useConfig();
|
||||||
@@ -157,6 +158,17 @@ export default function SettingsPage() {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Logout */}
|
||||||
|
<div className="border-t border-border px-5 py-3">
|
||||||
|
<button
|
||||||
|
onClick={() => { logout(); router.push('/login'); }}
|
||||||
|
className="w-full flex items-center gap-3 py-2.5 text-sm text-destructive hover:bg-muted rounded-md px-2 transition-colors duration-150"
|
||||||
|
>
|
||||||
|
<LogOut className="w-4 h-4" />
|
||||||
|
<span>{tSidebar('sign_out')}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom Navigation */}
|
{/* Bottom Navigation */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useCallback } from "react";
|
||||||
import { formatDate } from "@/lib/utils";
|
import { formatDate } from "@/lib/utils";
|
||||||
import { Email } from "@/lib/jmap/types";
|
import { Email } from "@/lib/jmap/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -10,6 +11,8 @@ import { useEmailStore } from "@/stores/email-store";
|
|||||||
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
|
||||||
import { useAuthStore } from "@/stores/auth-store";
|
import { useAuthStore } from "@/stores/auth-store";
|
||||||
import { useEmailDrag } from "@/hooks/use-email-drag";
|
import { useEmailDrag } from "@/hooks/use-email-drag";
|
||||||
|
import { useLongPress } from "@/hooks/use-long-press";
|
||||||
|
import { useUIStore } from "@/stores/ui-store";
|
||||||
import { EmailIdentityBadge } from "./email-identity-badge";
|
import { EmailIdentityBadge } from "./email-identity-badge";
|
||||||
import { getEmailColorTag } from "@/lib/thread-utils";
|
import { getEmailColorTag } from "@/lib/thread-utils";
|
||||||
|
|
||||||
@@ -44,6 +47,19 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
|||||||
sourceMailboxId: selectedMailbox,
|
sourceMailboxId: selectedMailbox,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
|
|
||||||
|
const { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel, isPressed } = useLongPress(
|
||||||
|
useCallback((pos) => {
|
||||||
|
onContextMenu?.(
|
||||||
|
{ preventDefault: () => {}, stopPropagation: () => {}, clientX: pos.clientX, clientY: pos.clientY } as React.MouseEvent,
|
||||||
|
email
|
||||||
|
);
|
||||||
|
}, [onContextMenu, email]),
|
||||||
|
isMobile
|
||||||
|
);
|
||||||
|
const longPressHandlers = { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel };
|
||||||
|
|
||||||
const handleCheckboxClick = (e: React.MouseEvent) => {
|
const handleCheckboxClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleEmailSelection(email.id);
|
toggleEmailSelection(email.id);
|
||||||
@@ -56,8 +72,9 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...dragHandlers}
|
{...dragHandlers}
|
||||||
|
{...longPressHandlers}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative group cursor-pointer transition-all duration-200 border-b border-border",
|
"relative group cursor-pointer select-none transition-all duration-200 border-b border-border",
|
||||||
// Apply color tag as background, with selected and unread states
|
// Apply color tag as background, with selected and unread states
|
||||||
colorTag ? colorTag : (
|
colorTag ? colorTag : (
|
||||||
selected
|
selected
|
||||||
@@ -71,7 +88,9 @@ export function EmailListItem({ email, selected, onClick, onContextMenu }: Email
|
|||||||
// Add visual feedback for checked state
|
// Add visual feedback for checked state
|
||||||
isChecked && "ring-2 ring-primary/20 bg-blue-100 dark:bg-blue-900/30",
|
isChecked && "ring-2 ring-primary/20 bg-blue-100 dark:bg-blue-900/30",
|
||||||
// Drag state visual feedback
|
// Drag state visual feedback
|
||||||
isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30"
|
isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30",
|
||||||
|
// Long press visual feedback
|
||||||
|
isPressed && "bg-muted scale-[0.98] ring-2 ring-primary/30"
|
||||||
)}
|
)}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.ctrlKey || e.metaKey) {
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback } from "react";
|
||||||
import { formatDate } from "@/lib/utils";
|
import { formatDate } from "@/lib/utils";
|
||||||
import { Email } from "@/lib/jmap/types";
|
import { Email } from "@/lib/jmap/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Avatar } from "@/components/ui/avatar";
|
import { Avatar } from "@/components/ui/avatar";
|
||||||
import { Paperclip, Star, Circle, CheckSquare, Square } from "lucide-react";
|
import { Paperclip, Star, Circle, CheckSquare, Square } from "lucide-react";
|
||||||
import { useEmailDrag } from "@/hooks/use-email-drag";
|
import { useEmailDrag } from "@/hooks/use-email-drag";
|
||||||
|
import { useLongPress } from "@/hooks/use-long-press";
|
||||||
import { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore } from "@/stores/email-store";
|
||||||
import { useSettingsStore } from "@/stores/settings-store";
|
import { useSettingsStore } from "@/stores/settings-store";
|
||||||
|
import { useUIStore } from "@/stores/ui-store";
|
||||||
|
|
||||||
interface ThreadEmailItemProps {
|
interface ThreadEmailItemProps {
|
||||||
email: Email;
|
email: Email;
|
||||||
@@ -36,6 +39,19 @@ export function ThreadEmailItem({
|
|||||||
sourceMailboxId: selectedMailbox,
|
sourceMailboxId: selectedMailbox,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
|
|
||||||
|
const { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel, isPressed } = useLongPress(
|
||||||
|
useCallback((pos) => {
|
||||||
|
onContextMenu?.(
|
||||||
|
{ preventDefault: () => {}, stopPropagation: () => {}, clientX: pos.clientX, clientY: pos.clientY } as React.MouseEvent,
|
||||||
|
email
|
||||||
|
);
|
||||||
|
}, [onContextMenu, email]),
|
||||||
|
isMobile
|
||||||
|
);
|
||||||
|
const longPressHandlers = { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel };
|
||||||
|
|
||||||
const handleContextMenu = (e: React.MouseEvent) => {
|
const handleContextMenu = (e: React.MouseEvent) => {
|
||||||
onContextMenu?.(e, email);
|
onContextMenu?.(e, email);
|
||||||
};
|
};
|
||||||
@@ -61,8 +77,9 @@ export function ThreadEmailItem({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...dragHandlers}
|
{...dragHandlers}
|
||||||
|
{...longPressHandlers}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative cursor-pointer transition-all duration-150",
|
"relative cursor-pointer select-none transition-all duration-150",
|
||||||
"pl-12 pr-4",
|
"pl-12 pr-4",
|
||||||
"border-l-2 border-l-transparent",
|
"border-l-2 border-l-transparent",
|
||||||
selected
|
selected
|
||||||
@@ -71,7 +88,8 @@ export function ThreadEmailItem({
|
|||||||
isUnread && !selected && "bg-amber-50 dark:bg-amber-900/20",
|
isUnread && !selected && "bg-amber-50 dark:bg-amber-900/20",
|
||||||
!isLast && "border-b border-border/30",
|
!isLast && "border-b border-border/30",
|
||||||
isChecked && "ring-2 ring-primary/20 bg-accent/40",
|
isChecked && "ring-2 ring-primary/20 bg-accent/40",
|
||||||
isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30"
|
isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30",
|
||||||
|
isPressed && "bg-muted scale-[0.98] ring-2 ring-primary/30"
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useCallback } from "react";
|
||||||
import { formatDate } from "@/lib/utils";
|
import { formatDate } from "@/lib/utils";
|
||||||
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -11,6 +11,7 @@ import { useUIStore } from "@/stores/ui-store";
|
|||||||
import { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore } from "@/stores/email-store";
|
||||||
import { getThreadColorTag, getEmailColorTag } from "@/lib/thread-utils";
|
import { getThreadColorTag, getEmailColorTag } from "@/lib/thread-utils";
|
||||||
import { useEmailDrag } from "@/hooks/use-email-drag";
|
import { useEmailDrag } from "@/hooks/use-email-drag";
|
||||||
|
import { useLongPress } from "@/hooks/use-long-press";
|
||||||
import { ThreadEmailItem } from "./thread-email-item";
|
import { ThreadEmailItem } from "./thread-email-item";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
@@ -58,6 +59,19 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
sourceMailboxId: selectedMailbox,
|
sourceMailboxId: selectedMailbox,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
|
|
||||||
|
const { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel, isPressed } = useLongPress(
|
||||||
|
useCallback((pos) => {
|
||||||
|
onContextMenu?.(
|
||||||
|
{ preventDefault: () => {}, stopPropagation: () => {}, clientX: pos.clientX, clientY: pos.clientY } as React.MouseEvent,
|
||||||
|
email
|
||||||
|
);
|
||||||
|
}, [onContextMenu, email]),
|
||||||
|
isMobile
|
||||||
|
);
|
||||||
|
const longPressHandlers = { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel };
|
||||||
|
|
||||||
const handleCheckboxClick = (e: React.MouseEvent) => {
|
const handleCheckboxClick = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
toggleEmailSelection(email.id);
|
toggleEmailSelection(email.id);
|
||||||
@@ -84,8 +98,9 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...dragHandlers}
|
{...dragHandlers}
|
||||||
|
{...longPressHandlers}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative group cursor-pointer transition-all duration-200 border-b border-border",
|
"relative group cursor-pointer select-none transition-all duration-200 border-b border-border",
|
||||||
resolvedColorTag ? resolvedColorTag : (
|
resolvedColorTag ? resolvedColorTag : (
|
||||||
selected
|
selected
|
||||||
? "bg-accent"
|
? "bg-accent"
|
||||||
@@ -96,7 +111,8 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
|||||||
resolvedColorTag && "hover:brightness-95 dark:hover:brightness-110",
|
resolvedColorTag && "hover:brightness-95 dark:hover:brightness-110",
|
||||||
isUnread && !resolvedColorTag && "bg-accent/30",
|
isUnread && !resolvedColorTag && "bg-accent/30",
|
||||||
isChecked && "ring-2 ring-primary/20 bg-accent/40",
|
isChecked && "ring-2 ring-primary/20 bg-accent/40",
|
||||||
isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30"
|
isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30",
|
||||||
|
isPressed && "bg-muted scale-[0.98] ring-2 ring-primary/30"
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
@@ -231,6 +247,17 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
threadEmails: thread.emails,
|
threadEmails: thread.emails,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { onTouchStart: threadOnTouchStart, onTouchEnd: threadOnTouchEnd, onTouchMove: threadOnTouchMove, onTouchCancel: threadOnTouchCancel, isPressed: isThreadPressed } = useLongPress(
|
||||||
|
useCallback((pos) => {
|
||||||
|
onContextMenu?.(
|
||||||
|
{ preventDefault: () => {}, stopPropagation: () => {}, clientX: pos.clientX, clientY: pos.clientY } as React.MouseEvent,
|
||||||
|
latestEmail
|
||||||
|
);
|
||||||
|
}, [onContextMenu, latestEmail]),
|
||||||
|
isMobile
|
||||||
|
);
|
||||||
|
const threadLongPressHandlers = { onTouchStart: threadOnTouchStart, onTouchEnd: threadOnTouchEnd, onTouchMove: threadOnTouchMove, onTouchCancel: threadOnTouchCancel };
|
||||||
|
|
||||||
const threadColor = getThreadColorTag(thread.emails);
|
const threadColor = getThreadColorTag(thread.emails);
|
||||||
const emailKeywordDefs = useSettingsStore((state) => state.emailKeywords);
|
const emailKeywordDefs = useSettingsStore((state) => state.emailKeywords);
|
||||||
const keywordDef = threadColor ? emailKeywordDefs.find(k => k.id === threadColor) : null;
|
const keywordDef = threadColor ? emailKeywordDefs.find(k => k.id === threadColor) : null;
|
||||||
@@ -310,8 +337,9 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
<div ref={ref} className={cn("border-b border-border", isThreadDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30")}>
|
<div ref={ref} className={cn("border-b border-border", isThreadDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30")}>
|
||||||
<div
|
<div
|
||||||
{...dragHandlers}
|
{...dragHandlers}
|
||||||
|
{...threadLongPressHandlers}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative group cursor-pointer transition-all duration-200",
|
"relative group cursor-pointer select-none transition-all duration-200",
|
||||||
colorTag ? colorTag : (
|
colorTag ? colorTag : (
|
||||||
isSelected
|
isSelected
|
||||||
? "bg-accent"
|
? "bg-accent"
|
||||||
@@ -322,7 +350,8 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
|||||||
colorTag && "hover:brightness-95 dark:hover:brightness-110",
|
colorTag && "hover:brightness-95 dark:hover:brightness-110",
|
||||||
hasUnread && !colorTag && !isSelected && "bg-accent/30",
|
hasUnread && !colorTag && !isSelected && "bg-accent/30",
|
||||||
isExpanded && "border-b border-border/50",
|
isExpanded && "border-b border-border/50",
|
||||||
isChecked && "ring-2 ring-primary/20 bg-accent/40"
|
isChecked && "ring-2 ring-primary/20 bg-accent/40",
|
||||||
|
isThreadPressed && "bg-muted scale-[0.98] ring-2 ring-primary/30"
|
||||||
)}
|
)}
|
||||||
onClick={handleHeaderClick}
|
onClick={handleHeaderClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useCallback, DragEvent } from "react";
|
|||||||
import { Email } from "@/lib/jmap/types";
|
import { Email } from "@/lib/jmap/types";
|
||||||
import { useEmailStore } from "@/stores/email-store";
|
import { useEmailStore } from "@/stores/email-store";
|
||||||
import { useDragDropContext } from "@/contexts/drag-drop-context";
|
import { useDragDropContext } from "@/contexts/drag-drop-context";
|
||||||
|
import { useUIStore } from "@/stores/ui-store";
|
||||||
|
|
||||||
interface UseEmailDragOptions {
|
interface UseEmailDragOptions {
|
||||||
email: Email;
|
email: Email;
|
||||||
@@ -46,6 +47,7 @@ function createDragPreview(count: number): HTMLElement {
|
|||||||
export function useEmailDrag({ email, sourceMailboxId, threadEmails }: UseEmailDragOptions): UseEmailDragReturn {
|
export function useEmailDrag({ email, sourceMailboxId, threadEmails }: UseEmailDragOptions): UseEmailDragReturn {
|
||||||
const { selectedEmailIds, emails } = useEmailStore();
|
const { selectedEmailIds, emails } = useEmailStore();
|
||||||
const { startDrag, endDrag, isDragging, draggedEmails } = useDragDropContext();
|
const { startDrag, endDrag, isDragging, draggedEmails } = useDragDropContext();
|
||||||
|
const isMobile = useUIStore((state) => state.isMobile);
|
||||||
|
|
||||||
const handleDragStart = useCallback((e: DragEvent<HTMLDivElement>) => {
|
const handleDragStart = useCallback((e: DragEvent<HTMLDivElement>) => {
|
||||||
// Determine which emails to drag:
|
// Determine which emails to drag:
|
||||||
@@ -88,11 +90,13 @@ export function useEmailDrag({ email, sourceMailboxId, threadEmails }: UseEmailD
|
|||||||
const isThisEmailDragging = isDragging && draggedEmails.some(em => em.id === email.id);
|
const isThisEmailDragging = isDragging && draggedEmails.some(em => em.id === email.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dragHandlers: {
|
dragHandlers: isMobile
|
||||||
draggable: true,
|
? { draggable: false, onDragStart: () => {}, onDragEnd: () => {} }
|
||||||
onDragStart: handleDragStart,
|
: {
|
||||||
onDragEnd: handleDragEnd,
|
draggable: true,
|
||||||
},
|
onDragStart: handleDragStart,
|
||||||
|
onDragEnd: handleDragEnd,
|
||||||
|
},
|
||||||
isDragging: isThisEmailDragging,
|
isDragging: isThisEmailDragging,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
|
|
||||||
|
const LONG_PRESS_DURATION = 500;
|
||||||
|
const MOVE_THRESHOLD = 10;
|
||||||
|
|
||||||
|
export function useLongPress(
|
||||||
|
onLongPress: (position: { clientX: number; clientY: number }) => void,
|
||||||
|
enabled: boolean = true
|
||||||
|
) {
|
||||||
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
const startPosRef = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
const firedRef = useRef(false);
|
||||||
|
const [isPressed, setIsPressed] = useState(false);
|
||||||
|
|
||||||
|
const cancel = useCallback(() => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearTimeout(timerRef.current);
|
||||||
|
timerRef.current = null;
|
||||||
|
}
|
||||||
|
startPosRef.current = null;
|
||||||
|
setIsPressed(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onTouchStart = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
if (!enabled) return;
|
||||||
|
const touch = e.touches[0];
|
||||||
|
startPosRef.current = { x: touch.clientX, y: touch.clientY };
|
||||||
|
firedRef.current = false;
|
||||||
|
setIsPressed(true);
|
||||||
|
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
firedRef.current = true;
|
||||||
|
setIsPressed(false);
|
||||||
|
onLongPress({ clientX: touch.clientX, clientY: touch.clientY });
|
||||||
|
}, LONG_PRESS_DURATION);
|
||||||
|
},
|
||||||
|
[enabled, onLongPress]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTouchMove = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
if (!startPosRef.current || !timerRef.current) return;
|
||||||
|
const touch = e.touches[0];
|
||||||
|
const dx = Math.abs(touch.clientX - startPosRef.current.x);
|
||||||
|
const dy = Math.abs(touch.clientY - startPosRef.current.y);
|
||||||
|
if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) {
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[cancel]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTouchEnd = useCallback(
|
||||||
|
(e: React.TouchEvent) => {
|
||||||
|
if (firedRef.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
cancel();
|
||||||
|
firedRef.current = false;
|
||||||
|
},
|
||||||
|
[cancel]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTouchCancel = useCallback(() => {
|
||||||
|
cancel();
|
||||||
|
firedRef.current = false;
|
||||||
|
}, [cancel]);
|
||||||
|
|
||||||
|
return { onTouchStart, onTouchEnd, onTouchMove, onTouchCancel, isPressed };
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import createNextIntlPlugin from "next-intl/plugin";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
allowedDevOrigins: ["192.168.1.51"],
|
||||||
turbopack: {
|
turbopack: {
|
||||||
root: import.meta.dirname,
|
root: import.meta.dirname,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user