From 888e65cf6cfc23d88d98acfaa84096be5b2ea65a Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 15 Oct 2025 19:57:19 +0200 Subject: [PATCH] Left Search: Support drag-n-drop for attachments --- src/components/left/main/Chat.tsx | 7 ++- src/components/left/main/ChatList.tsx | 51 +++++++------------ .../left/search/LeftSearchResultChat.tsx | 23 +++++++-- src/components/ui/InfiniteScroll.tsx | 14 ++--- src/components/ui/ListItem.tsx | 15 +++--- src/util/dragNDropHandlers.ts | 34 +++++++++++++ 6 files changed, 88 insertions(+), 56 deletions(-) create mode 100644 src/util/dragNDropHandlers.ts diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index e46d33fcb..45f988c10 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -94,9 +94,10 @@ type OwnProps = { isPreview?: boolean; previewMessageId?: number; className?: string; + withTags?: boolean; observeIntersection?: ObserveFn; onDragEnter?: (chatId: string) => void; - withTags?: boolean; + onDragLeave?: NoneToVoidFunction; onReorderAnimationEnd?: NoneToVoidFunction; }; @@ -167,6 +168,7 @@ const Chat: FC = ({ className, isSynced, onDragEnter, + onDragLeave, isAccountFrozen, chatFolderIds, orderedFolderIds, @@ -408,9 +410,10 @@ const Chat: FC = ({ style={`top: ${offsetTop}px`} ripple={!isForum && !isMobile} contextActions={contextActions} + withPortalForMenu onClick={handleClick} onDragEnter={handleDragEnter} - withPortalForMenu + onDragLeave={onDragLeave} >
= ({ @@ -84,7 +80,6 @@ const ChatList: FC = ({ openLeftColumnContent, } = getActions(); const containerRef = useRef(); - const shouldIgnoreDragRef = useRef(false); const [unconfirmedSessionHeight, setUnconfirmedSessionHeight] = useState(0); const isArchived = folderType === 'archived'; @@ -183,30 +178,6 @@ const ChatList: FC = ({ openFrozenAccountModal(); }); - const handleArchivedDragEnter = useLastCallback(() => { - if (shouldIgnoreDragRef.current) { - shouldIgnoreDragRef.current = false; - return; - } - handleArchivedClick(); - }); - - const handleDragEnter = useDebouncedCallback((chatId: string) => { - if (shouldIgnoreDragRef.current) { - shouldIgnoreDragRef.current = false; - return; - } - openChat({ id: chatId, shouldReplaceHistory: true }); - }, [openChat], DRAG_ENTER_DEBOUNCE, true); - - const handleDragLeave = useLastCallback((e: React.DragEvent) => { - const rect = e.currentTarget.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - if (x < rect.width || y < rect.y) return; - shouldIgnoreDragRef.current = true; - }); - const handleShowStoryRibbon = useLastCallback(() => { toggleStoryRibbon({ isShown: true, isArchived }); }); @@ -215,6 +186,18 @@ const ChatList: FC = ({ toggleStoryRibbon({ isShown: false, isArchived }); }); + const handleArchivedDragEnter = useLastCallback(() => { + onDragEnter(() => { + handleArchivedClick(); + }); + }); + + const handleChatDragEnter = useLastCallback((chatId: string) => { + onDragEnter(() => { + openChat({ id: chatId, shouldReplaceHistory: true }); + }); + }); + useTopOverscroll({ containerRef, onOverscroll: handleShowStoryRibbon, @@ -245,7 +228,8 @@ const ChatList: FC = ({ onReorderAnimationEnd={onReorderAnimationEnd} offsetTop={offsetTop} observeIntersection={observe} - onDragEnter={handleDragEnter} + onDragEnter={handleChatDragEnter} + onDragLeave={onDragLeave} withTags={withTags} /> ); @@ -262,7 +246,6 @@ const ChatList: FC = ({ withAbsolutePositioning maxHeight={chatsHeight + archiveHeight + frozenNotificationHeight + unconfirmedSessionHeight} onLoadMore={getMore} - onDragLeave={handleDragLeave} > {shouldShowUnconfirmedSessions && ( = ({ }); }); + const handleDragEnter = useLastCallback((e) => { + e.preventDefault(); + + onDragEnter(() => { + onClick(chatId); + }, true); + }); + const buttonRef = useSelectWithEnter(() => { onClick(chatId); }); @@ -112,9 +125,11 @@ const LeftSearchResultChat: FC = ({ return ( {isUserId(chatId) ? ( ) => void; onClick?: (e: React.MouseEvent) => void; onKeyDown?: (e: React.KeyboardEvent) => void; - onDragOver?: (e: React.DragEvent) => void; - onDragLeave?: (e: React.DragEvent) => void; }; const DEFAULT_LIST_SELECTOR = '.ListItem'; @@ -69,8 +67,6 @@ const InfiniteScroll: FC = ({ onWheel, onClick, onKeyDown, - onDragOver, - onDragLeave, }: OwnProps) => { let containerRef = useRef(); if (ref) { @@ -270,13 +266,11 @@ const InfiniteScroll: FC = ({
{beforeChildren} {withAbsolutePositioning && items?.length ? ( diff --git a/src/components/ui/ListItem.tsx b/src/components/ui/ListItem.tsx index e83afea85..4b5abf520 100644 --- a/src/components/ui/ListItem.tsx +++ b/src/components/ui/ListItem.tsx @@ -69,13 +69,14 @@ interface OwnProps { withPortalForMenu?: boolean; menuBubbleClassName?: string; href?: string; - onMouseDown?: (e: React.MouseEvent) => void; + nonInteractive?: boolean; onClick?: (e: React.MouseEvent, arg?: any) => void; - onContextMenu?: (e: React.MouseEvent) => void; clickArg?: any; + onMouseDown?: (e: React.MouseEvent) => void; + onContextMenu?: (e: React.MouseEvent) => void; onSecondaryIconClick?: (e: React.MouseEvent) => void; onDragEnter?: (e: React.DragEvent) => void; - nonInteractive?: boolean; + onDragLeave?: NoneToVoidFunction; } const ListItem = ({ @@ -107,13 +108,14 @@ const ListItem = ({ contextActions, withPortalForMenu, href, - onMouseDown, + nonInteractive, onClick, - onContextMenu, clickArg, + onMouseDown, + onContextMenu, onSecondaryIconClick, onDragEnter, - nonInteractive, + onDragLeave, }: OwnProps) => { let containerRef = useRef(); if (ref) { @@ -229,6 +231,7 @@ const ListItem = ({ style={style} onMouseDown={onMouseDown} onDragEnter={onDragEnter} + onDragLeave={onDragLeave} > { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } + + if (willSkipNext) { + willSkipNext = false; + return; + } + + timeout = window.setTimeout(() => { + if (shouldSkipNext) { + willSkipNext = true; + } + + cb(); + }, DRAG_ENTER_ACTION_DELAY); +}, DRAG_ENTER_DEBOUNCE, false); + +export function onDragLeave() { + if (timeout) { + clearTimeout(timeout); + timeout = undefined; + } +}