1 import type { DragEvent, DragEventHandler } from 'react';
2 import { useEffect, useRef, useState } from 'react';
4 import type { Location } from 'history';
5 import { c } from 'ttag';
7 import { SimpleSidebarListItemHeader } from '@proton/components';
8 import { MAILBOX_LABEL_IDS } from '@proton/shared/lib/constants';
9 import type { MailSettings } from '@proton/shared/lib/interfaces';
10 import clsx from '@proton/utils/clsx';
12 import { isConversationMode } from '../../helpers/mailSettings';
13 import type { MoveParams } from '../../hooks/actions/move/useMoveToFolder';
14 import useMoveSystemFolders, { SYSTEM_FOLDER_SECTION } from '../../hooks/useMoveSystemFolders';
15 import type { UnreadCounts } from './MailSidebarList';
16 import SidebarItem from './SidebarItem';
19 counterMap: UnreadCounts;
20 currentLabelID: string;
22 mailSettings: MailSettings;
23 setFocusedItem: (id: string) => void;
24 totalMessagesMap: UnreadCounts;
25 displayMoreItems: boolean;
26 showScheduled: boolean;
28 onToggleMoreItems: (display: boolean) => void;
30 moveToFolder: (params: MoveParams) => void;
31 applyLabels: (params: any) => void;
34 const DND_MORE_FOLDER_ID = 'DND_MORE_FOLDER_ID';
36 interface DnDWrapperProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
37 isDnDAllowed: boolean;
38 children: React.ReactElement;
40 const DnDElementWrapper = ({ isDnDAllowed, children, ...rest }: DnDWrapperProps) => {
41 return isDnDAllowed ? (
42 // eslint-disable-next-line jsx-a11y/prefer-tag-over-role
43 <div role="presentation" {...rest}>
51 const MailSidebarSystemFolders = ({
66 const { ShowMoved, AlmostAllMail } = mailSettings;
67 const [sidebarElements, moveSidebarElement] = useMoveSystemFolders({
71 showAlmostAllMail: AlmostAllMail,
73 const isConversation = isConversationMode(currentLabelID, mailSettings, location);
75 const lastDragTimeRef = useRef<number>();
76 const isDragging = useRef<boolean>();
77 const dragOverlay = useRef<HTMLDivElement>();
78 const [draggedElementId, setDraggedElementId] = useState<MAILBOX_LABEL_IDS | undefined>();
79 const [dragOveredElementId, setDragOveredElementId] = useState<string | undefined>();
80 const [isOverMoreFolder, setIsOverMoreFolder] = useState<boolean>();
82 const getCommonProps = (labelID: string) => ({
86 unreadCount: counterMap[labelID],
87 totalMessagesCount: totalMessagesMap[labelID] || 0,
90 type HandleDragOver = (
91 elementId: MAILBOX_LABEL_IDS | typeof DND_MORE_FOLDER_ID
92 ) => DragEventHandler<HTMLDivElement>;
93 const handleDragOver: HandleDragOver = (elementId) => (event) => {
94 if (!isDragging.current) {
95 event.preventDefault();
99 // "dragover" event must be prevented to allow "drop" event
100 event.preventDefault();
102 if (elementId === DND_MORE_FOLDER_ID && displayMoreItems === false) {
103 setIsOverMoreFolder(true);
105 setIsOverMoreFolder(false);
108 setDragOveredElementId(elementId);
109 lastDragTimeRef.current = Date.now();
112 const handleDragStart = (labelId: MAILBOX_LABEL_IDS) => (event: DragEvent<HTMLDivElement>) => {
113 if (labelId === MAILBOX_LABEL_IDS.INBOX) {
114 event.preventDefault();
115 event.stopPropagation();
119 const draggedSystemFolder = sidebarElements.find((element) => labelId === element.labelID);
121 if (!draggedSystemFolder) {
124 const sidebarElementName = draggedSystemFolder.text;
126 dragOverlay.current = document.createElement('div');
127 // translator: This is the text overlay following the cursor when dragging a sidebar element. Ex: Move Inbox
128 dragOverlay.current.innerHTML = c('Label').t`Move ${sidebarElementName}`;
129 dragOverlay.current.className = 'absolute bg-weak text-white py-2 px-4 rounded';
130 document.body.appendChild(dragOverlay.current);
132 event.dataTransfer.setDragImage(dragOverlay.current, 0, 30);
134 setDraggedElementId(labelId);
136 isDragging.current = true;
139 const handleResetDragState = () => {
140 isDragging.current = false;
141 setDraggedElementId(undefined);
142 setDragOveredElementId(undefined);
143 setIsOverMoreFolder(undefined);
144 if (dragOverlay.current) {
145 document.body.removeChild(dragOverlay.current);
146 lastDragTimeRef.current = undefined;
147 isDragging.current = undefined;
148 dragOverlay.current = undefined;
153 droppedId: MAILBOX_LABEL_IDS | 'MORE_FOLDER_ITEM',
154 draggedId: MAILBOX_LABEL_IDS | undefined
155 ) => DragEventHandler;
156 const handleDrop: HandleDrop = (droppedId, draggedId) => (event: DragEvent<HTMLDivElement>) => {
157 event.preventDefault();
159 if (!isDragging.current || !draggedId) {
163 handleResetDragState();
164 moveSidebarElement(draggedId, droppedId);
167 const getDnDClasses = (hoveredId: MAILBOX_LABEL_IDS, draggedId: MAILBOX_LABEL_IDS | undefined) => {
168 if (dragOveredElementId !== hoveredId || hoveredId === draggedId || draggedId === MAILBOX_LABEL_IDS.INBOX) {
172 const draggedElementOrder = sidebarElements.find((el) => el.labelID === draggedId)?.order;
173 const hoveredElementOrder = sidebarElements.find((el) => el.labelID === hoveredId)?.order;
175 if (!draggedElementOrder || !hoveredElementOrder) {
179 const goesUp = draggedElementOrder > hoveredElementOrder;
181 if (hoveredId === MAILBOX_LABEL_IDS.INBOX) {
182 return 'border-bottom';
185 return goesUp ? 'border-top' : 'border-bottom';
188 // Set dragOveredElementId
190 if (!dragOveredElementId) {
194 const interval = setInterval(() => {
195 const lastDragTime = lastDragTimeRef.current || 0;
196 const elapsedTime = Date.now() - lastDragTime;
198 if (elapsedTime > 300) {
199 setDragOveredElementId(undefined);
200 lastDragTimeRef.current = undefined;
205 clearInterval(interval);
207 }, [dragOveredElementId]);
209 // Opens the "more" folder
211 if (!isOverMoreFolder) {
215 const OPEN_FOLDER_AFTER = 1000;
216 const now = Date.now();
218 const timeout = setTimeout(() => {
219 const lastDragOverTime = lastDragTimeRef.current || 0;
220 if (now - 50 < lastDragOverTime) {
221 onToggleMoreItems(true);
223 }, OPEN_FOLDER_AFTER);
226 clearTimeout(timeout);
228 }, [isOverMoreFolder]);
233 .filter((element) => element.display === SYSTEM_FOLDER_SECTION.MAIN)
238 onDragStart={handleDragStart(element.labelID)}
239 onDragEnd={handleResetDragState}
240 onDragOver={handleDragOver(element.labelID)}
241 onDrop={handleDrop(element.labelID, draggedElementId)}
242 className={clsx([getDnDClasses(element.labelID, draggedElementId)])}
243 onClick={(e) => e.stopPropagation()}
246 {...getCommonProps(element.labelID)}
249 hideCountOnHover={false}
250 isFolder={element.labelID !== MAILBOX_LABEL_IDS.STARRED}
251 onFocus={setFocusedItem}
252 shortcutText={element.shortcutText}
254 collapsed={collapsed}
255 moveToFolder={moveToFolder}
256 applyLabels={applyLabels}
263 key={'MORE_FOLDER_ITEM'}
264 onDragOver={handleDragOver(DND_MORE_FOLDER_ID)}
265 onDrop={handleDrop('MORE_FOLDER_ITEM', draggedElementId)}
267 <SimpleSidebarListItemHeader
268 toggle={displayMoreItems}
269 onToggle={(display: boolean) => onToggleMoreItems(display)}
270 text={displayMoreItems ? c('Link').t`Less` : c('Link').t`More`}
271 title={displayMoreItems ? c('Link').t`Less` : c('Link').t`More`}
272 id="toggle-more-items"
273 onFocus={setFocusedItem}
274 collapsed={collapsed}
281 .filter((element) => element.display === SYSTEM_FOLDER_SECTION.MORE)
285 onClick={(e) => e.stopPropagation()}
287 onDragStart={handleDragStart(element.labelID)}
288 onDragEnd={handleResetDragState}
289 onDragOver={handleDragOver(element.labelID)}
290 onDrop={handleDrop(element.labelID, draggedElementId)}
291 className={clsx([getDnDClasses(element.labelID, draggedElementId)])}
294 {...getCommonProps(element.labelID)}
297 isFolder={element.labelID !== MAILBOX_LABEL_IDS.STARRED}
298 hideCountOnHover={false}
299 onFocus={setFocusedItem}
300 shortcutText={element.shortcutText}
302 collapsed={collapsed}
303 moveToFolder={moveToFolder}
304 applyLabels={applyLabels}
313 export default MailSidebarSystemFolders;