Files
SRCmail/hooks/use-email-drag.ts
T
Linus Rath cf02f587de feat: resizable columns, nav rail overhaul, multi-select & drag-drop, UI polish
Resizable Columns
- Add ResizeHandle component with mouse drag, keyboard (Arrow keys),
  and double-click to reset to default width
- Add sidebarWidth/emailListWidth to ui-store with clamping + persistence
- Wire resize handles between sidebar/email-list panels on desktop

Navigation Rail Overhaul
- Move StorageQuotaCircle, push-status, sign-out from Sidebar to NavigationRail
- Interactive SVG ring with popover breakdown (used/free/total)
- Sidebar collapse state lifted to ui-store
- Show total email count per mailbox alongside unread badge

Email Multi-Selection & Drag-and-Drop
- Ctrl+Click (toggle) and Shift+Click (range) on all list items
- Add selectRangeEmails and lastSelectedEmailId to email-store
- Enable drag-and-drop on thread items and thread headers
- useEmailDrag accepts optional threadEmails for full-thread drag

Email Viewer Layout
- Remove card wrapper for cleaner full-width reading
- Always render HTML body when available
- Adjust skeleton loader to match flat layout

Modal & UI Polish
- Standardise backdrops, close buttons, padding, border-radius, transitions
- Migrate template-string classNames to cn() in settings
- Unify focus-ring token to ring-ring on form controls

i18n
- Add storage_used/free/total keys to all 8 locales

Dev Mock JMAP Server (new, gated by DEV_MOCK_JMAP=true)
- Session, Mailbox/Email/Thread/Identity CRUD, back-references, upload
- GET /download with Content-Disposition, GET /eventsource SSE

Tests (46 new)
- ui-store (13), email-selection (10), resize-handle (9), mock-server (14)
2026-03-09 16:46:25 +01:00

99 lines
3.0 KiB
TypeScript

"use client";
import { useCallback, DragEvent } from "react";
import { Email } from "@/lib/jmap/types";
import { useEmailStore } from "@/stores/email-store";
import { useDragDropContext } from "@/contexts/drag-drop-context";
interface UseEmailDragOptions {
email: Email;
sourceMailboxId: string;
threadEmails?: Email[];
}
interface UseEmailDragReturn {
dragHandlers: {
draggable: boolean;
onDragStart: (e: DragEvent<HTMLDivElement>) => void;
onDragEnd: (e: DragEvent<HTMLDivElement>) => void;
};
isDragging: boolean;
}
function createDragPreview(count: number): HTMLElement {
const preview = document.createElement("div");
preview.className = "drag-preview";
preview.style.cssText = `
position: fixed;
top: -9999px;
left: 0;
padding: 8px 16px;
background-color: var(--color-primary, #3b82f6);
color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-size: 14px;
font-weight: 500;
z-index: 9999;
white-space: nowrap;
pointer-events: none;
`;
preview.textContent = count === 1 ? "1 email" : `${count} emails`;
document.body.appendChild(preview);
return preview;
}
export function useEmailDrag({ email, sourceMailboxId, threadEmails }: UseEmailDragOptions): UseEmailDragReturn {
const { selectedEmailIds, emails } = useEmailStore();
const { startDrag, endDrag, isDragging, draggedEmails } = useDragDropContext();
const handleDragStart = useCallback((e: DragEvent<HTMLDivElement>) => {
// Determine which emails to drag:
// - If current email is selected, drag all selected
// - If threadEmails provided (thread header), drag all thread emails
// - Otherwise, drag only this email
const isSelected = selectedEmailIds.has(email.id);
const emailsToDrag = isSelected
? emails.filter(em => selectedEmailIds.has(em.id))
: threadEmails || [email];
// Set data transfer
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData(
"application/x-email-ids",
JSON.stringify(emailsToDrag.map(em => em.id))
);
e.dataTransfer.setData(
"text/plain",
emailsToDrag.map(em => em.subject || "(no subject)").join(", ")
);
// Create custom drag image
const dragPreview = createDragPreview(emailsToDrag.length);
e.dataTransfer.setDragImage(dragPreview, 0, 0);
// Clean up preview after drag starts (browser keeps a snapshot)
requestAnimationFrame(() => {
dragPreview.remove();
});
startDrag(emailsToDrag, sourceMailboxId);
}, [email, selectedEmailIds, emails, sourceMailboxId, startDrag, threadEmails]);
const handleDragEnd = useCallback(() => {
endDrag();
}, [endDrag]);
// Check if this specific email is being dragged
const isThisEmailDragging = isDragging && draggedEmails.some(em => em.id === email.id);
return {
dragHandlers: {
draggable: true,
onDragStart: handleDragStart,
onDragEnd: handleDragEnd,
},
isDragging: isThisEmailDragging,
};
}