diff --git a/app/[locale]/settings/page.tsx b/app/[locale]/settings/page.tsx
index 786da908..410ed688 100644
--- a/app/[locale]/settings/page.tsx
+++ b/app/[locale]/settings/page.tsx
@@ -3,7 +3,7 @@
import { useState, useEffect } from 'react';
import { useRouter } from '@/i18n/navigation';
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 { AppearanceSettings } from '@/components/settings/appearance-settings';
import { EmailSettings } from '@/components/settings/email-settings';
@@ -29,6 +29,7 @@ type Tab = 'appearance' | 'email' | 'account' | 'security' | 'identities' | 'vac
export default function SettingsPage() {
const router = useRouter();
const t = useTranslations('settings');
+ const tSidebar = useTranslations('sidebar');
const { client, isAuthenticated, logout } = useAuthStore();
const { quota, isPushConnected } = useEmailStore();
const { stalwartFeaturesEnabled } = useConfig();
@@ -157,6 +158,17 @@ export default function SettingsPage() {
))}
+
+ {/* Logout */}
+
{
if (e.ctrlKey || e.metaKey) {
diff --git a/components/email/thread-email-item.tsx b/components/email/thread-email-item.tsx
index 0355eb6f..e9c9a782 100644
--- a/components/email/thread-email-item.tsx
+++ b/components/email/thread-email-item.tsx
@@ -1,13 +1,16 @@
"use client";
+import { useCallback } from "react";
import { formatDate } from "@/lib/utils";
import { Email } from "@/lib/jmap/types";
import { cn } from "@/lib/utils";
import { Avatar } from "@/components/ui/avatar";
import { Paperclip, Star, Circle, CheckSquare, Square } from "lucide-react";
import { useEmailDrag } from "@/hooks/use-email-drag";
+import { useLongPress } from "@/hooks/use-long-press";
import { useEmailStore } from "@/stores/email-store";
import { useSettingsStore } from "@/stores/settings-store";
+import { useUIStore } from "@/stores/ui-store";
interface ThreadEmailItemProps {
email: Email;
@@ -36,6 +39,19 @@ export function ThreadEmailItem({
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) => {
onContextMenu?.(e, email);
};
@@ -61,8 +77,9 @@ export function ThreadEmailItem({
return (
(
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) => {
e.stopPropagation();
toggleEmailSelection(email.id);
@@ -84,8 +98,9 @@ const SingleEmailItem = React.forwardRef
(
(
resolvedColorTag && "hover:brightness-95 dark:hover:brightness-110",
isUnread && !resolvedColorTag && "bg-accent/30",
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}
onContextMenu={handleContextMenu}
@@ -231,6 +247,17 @@ export const ThreadListItem = React.forwardRef
{
+ 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 emailKeywordDefs = useSettingsStore((state) => state.emailKeywords);
const keywordDef = threadColor ? emailKeywordDefs.find(k => k.id === threadColor) : null;
@@ -310,8 +337,9 @@ export const ThreadListItem = React.forwardRef
state.isMobile);
const handleDragStart = useCallback((e: DragEvent) => {
// 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);
return {
- dragHandlers: {
- draggable: true,
- onDragStart: handleDragStart,
- onDragEnd: handleDragEnd,
- },
+ dragHandlers: isMobile
+ ? { draggable: false, onDragStart: () => {}, onDragEnd: () => {} }
+ : {
+ draggable: true,
+ onDragStart: handleDragStart,
+ onDragEnd: handleDragEnd,
+ },
isDragging: isThisEmailDragging,
};
}
diff --git a/hooks/use-long-press.ts b/hooks/use-long-press.ts
new file mode 100644
index 00000000..972c5fd2
--- /dev/null
+++ b/hooks/use-long-press.ts
@@ -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 | 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 };
+}
diff --git a/next.config.ts b/next.config.ts
index ec6b506a..f0b267cd 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -3,6 +3,7 @@ import createNextIntlPlugin from "next-intl/plugin";
const nextConfig: NextConfig = {
output: "standalone",
+ allowedDevOrigins: ["192.168.1.51"],
turbopack: {
root: import.meta.dirname,
},