From b660b37a7b4b229d64c43a166cb0a8c024baab02 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 5 Aug 2022 19:22:51 +0200 Subject: [PATCH] Chat List: Allow chat selection when dragging file (#1962) --- src/App.tsx | 26 +++++++++++++++++++ src/components/common/UiLoader.tsx | 2 +- src/components/left/main/Chat.tsx | 8 ++++++ src/components/left/main/ChatList.tsx | 23 +++++++++++++++- src/components/main/Main.tsx | 3 +-- .../middle/composer/AttachmentModal.scss | 3 ++- .../middle/composer/AttachmentModal.tsx | 2 +- src/components/middle/composer/DropArea.tsx | 8 +++++- src/components/middle/composer/DropTarget.tsx | 4 +-- src/components/ui/InfiniteScroll.tsx | 18 ++++++++----- src/components/ui/ListItem.tsx | 3 +++ 11 files changed, 85 insertions(+), 15 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index ac6918dcb..4b759a269 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -55,6 +55,32 @@ const App: FC = ({ }); }, [disconnect, markInactive]); + // Prevent drop on elements that do not accept it + useEffect(() => { + const body = document.body; + const handleDrag = (e: DragEvent) => { + e.preventDefault(); + if (!e.dataTransfer) return; + if (!(e.target as HTMLElement).dataset.dropzone) { + e.dataTransfer.dropEffect = 'none'; + } else { + e.dataTransfer.dropEffect = 'copy'; + } + }; + const handleDrop = (e: DragEvent) => { + e.preventDefault(); + }; + body.addEventListener('drop', handleDrop); + body.addEventListener('dragover', handleDrag); + body.addEventListener('dragenter', handleDrag); + + return () => { + body.removeEventListener('drop', handleDrop); + body.removeEventListener('dragover', handleDrag); + body.removeEventListener('dragenter', handleDrag); + }; + }, []); + // return ; let activeKey: number; diff --git a/src/components/common/UiLoader.tsx b/src/components/common/UiLoader.tsx index c010efa13..baa918768 100644 --- a/src/components/common/UiLoader.tsx +++ b/src/components/common/UiLoader.tsx @@ -1,10 +1,10 @@ -import type { FC } from '../../lib/teact/teact'; import React, { useEffect } from '../../lib/teact/teact'; import { getActions, getGlobal, withGlobal } from '../../global'; import { ApiMediaFormat } from '../../api/types'; import type { GlobalState } from '../../global/types'; import type { ThemeKey } from '../../types'; +import type { FC } from '../../lib/teact/teact'; import { getChatAvatarHash } from '../../global/helpers/chats'; // Direct import for better module splitting import { selectIsRightColumnShown, selectTheme, selectIsCurrentUserPremium } from '../../global/selectors'; diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx index e4742ce6b..bb79eb83b 100644 --- a/src/components/left/main/Chat.tsx +++ b/src/components/left/main/Chat.tsx @@ -68,6 +68,7 @@ type OwnProps = { animationType: ChatAnimationTypes; isPinned?: boolean; observeIntersection?: ObserveFn; + onDragEnter?: (chatId: string) => void; }; type StateProps = { @@ -113,6 +114,7 @@ const Chat: FC = ({ canScrollDown, canChangeFolder, lastSyncTime, + onDragEnter, }) => { const { openChat, @@ -200,6 +202,11 @@ const Chat: FC = ({ focusLastMessage, ]); + const handleDragEnter = useCallback((e) => { + e.preventDefault(); + onDragEnter?.(chatId); + }, [chatId, onDragEnter]); + const handleDelete = useCallback(() => { markRenderDeleteModal(); openDeleteModal(); @@ -299,6 +306,7 @@ const Chat: FC = ({ ripple={!IS_SINGLE_COLUMN_LAYOUT} contextActions={contextActions} onClick={handleClick} + onDragEnter={handleDragEnter} >
= ({ folderType, @@ -50,6 +52,7 @@ const ChatList: FC = ({ const { openChat, openNextChat } = getActions(); // eslint-disable-next-line no-null/no-null const containerRef = useRef(null); + const shouldIgnoreDragRef = useRef(false); const resolvedFolderId = ( folderType === 'all' ? ALL_FOLDER_ID : folderType === 'archived' ? ARCHIVED_FOLDER_ID : folderId! @@ -126,6 +129,22 @@ const ChatList: FC = ({ throttleMs: INTERSECTION_THROTTLE, }); + const handleDragEnter = useDebouncedCallback((chatId: string) => { + if (shouldIgnoreDragRef.current) { + shouldIgnoreDragRef.current = false; + return; + } + openChat({ id: chatId, shouldReplaceHistory: true }); + }, [], DRAG_ENTER_DEBOUNCE, true); + + const handleDragLeave = useCallback((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; + }, []); + function renderChats() { const viewportOffset = orderedIds!.indexOf(viewportIds![0]); const pinnedCount = getPinnedChatsCount(resolvedFolderId) || 0; @@ -144,6 +163,7 @@ const ChatList: FC = ({ orderDiff={orderDiffById[id]} style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`} observeIntersection={observe} + onDragEnter={handleDragEnter} /> ); }); @@ -158,6 +178,7 @@ const ChatList: FC = ({ withAbsolutePositioning maxHeight={(orderedIds?.length || 0) * CHAT_HEIGHT_PX} onLoadMore={getMore} + onDragLeave={handleDragLeave} > {viewportIds?.length ? ( renderChats() diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 3d1843110..ead646d8d 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -27,7 +27,6 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck' import buildClassName from '../../util/buildClassName'; import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners'; import { processDeepLink } from '../../util/deeplink'; -import stopEvent from '../../util/stopEvent'; import windowSize from '../../util/windowSize'; import { getAllNotificationsCount } from '../../util/folderManager'; import useBackgroundMode from '../../hooks/useBackgroundMode'; @@ -385,7 +384,7 @@ const Main: FC = ({ usePreventPinchZoomGesture(isMediaViewerOpen); return ( -
+
diff --git a/src/components/middle/composer/AttachmentModal.scss b/src/components/middle/composer/AttachmentModal.scss index 3ac0ed4b0..d8f3e4c3b 100644 --- a/src/components/middle/composer/AttachmentModal.scss +++ b/src/components/middle/composer/AttachmentModal.scss @@ -119,7 +119,8 @@ .attachment-caption-wrapper, .document-wrapper, - .media-wrapper { + .media-wrapper, + .form-control { pointer-events: none; } diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx index f40f0a472..31f91004b 100644 --- a/src/components/middle/composer/AttachmentModal.tsx +++ b/src/components/middle/composer/AttachmentModal.tsx @@ -177,7 +177,6 @@ const AttachmentModal: FC = ({ function handleDragOver(e: React.MouseEvent) { e.preventDefault(); - e.stopPropagation(); if (hideTimeoutRef.current) { window.clearTimeout(hideTimeoutRef.current); @@ -258,6 +257,7 @@ const AttachmentModal: FC = ({ onDragOver={handleDragOver} onDragLeave={handleDragLeave} data-attach-description={lang('Preview.Dragging.AddItems', 10)} + data-dropzone > {isQuick ? (
diff --git a/src/components/middle/composer/DropArea.tsx b/src/components/middle/composer/DropArea.tsx index 92dd5c0e3..2bec7117f 100644 --- a/src/components/middle/composer/DropArea.tsx +++ b/src/components/middle/composer/DropArea.tsx @@ -96,7 +96,13 @@ const DropArea: FC = ({ return ( -
+
{(withQuick || prevWithQuick) && }
diff --git a/src/components/middle/composer/DropTarget.tsx b/src/components/middle/composer/DropTarget.tsx index 4ca8c2a9a..84277d086 100644 --- a/src/components/middle/composer/DropTarget.tsx +++ b/src/components/middle/composer/DropTarget.tsx @@ -14,7 +14,6 @@ export type OwnProps = { const DropTarget: FC = ({ isQuick, onFileSelect }) => { const [isHovered, markHovered, unmarkHovered] = useFlag(); - const handleDragEnter = () => { markHovered(); }; const handleDragLeave = (e: React.DragEvent) => { const { relatedTarget: toTarget } = e; @@ -34,8 +33,9 @@ const DropTarget: FC = ({ isQuick, onFileSelect }) => {
diff --git a/src/components/ui/InfiniteScroll.tsx b/src/components/ui/InfiniteScroll.tsx index 37ec3ad27..996e86a1e 100644 --- a/src/components/ui/InfiniteScroll.tsx +++ b/src/components/ui/InfiniteScroll.tsx @@ -14,9 +14,6 @@ import buildStyle from '../../util/buildStyle'; type OwnProps = { ref?: RefObject; className?: string; - onLoadMore?: ({ direction }: { direction: LoadMoreDirection; noScroll?: boolean }) => void; - onScroll?: (e: UIEvent) => void; - onKeyDown?: (e: React.KeyboardEvent) => void; items?: any[]; itemSelector?: string; preloadBackwards?: number; @@ -28,6 +25,11 @@ type OwnProps = { noFastList?: boolean; cacheBuster?: any; children: React.ReactNode; + onLoadMore?: ({ direction }: { direction: LoadMoreDirection; noScroll?: boolean }) => void; + onScroll?: (e: UIEvent) => void; + onKeyDown?: (e: React.KeyboardEvent) => void; + onDragOver?: (e: React.DragEvent) => void; + onDragLeave?: (e: React.DragEvent) => void; }; const DEFAULT_LIST_SELECTOR = '.ListItem'; @@ -37,9 +39,6 @@ const DEFAULT_SENSITIVE_AREA = 800; const InfiniteScroll: FC = ({ ref, className, - onLoadMore, - onScroll, - onKeyDown, items, itemSelector = DEFAULT_LIST_SELECTOR, preloadBackwards = DEFAULT_PRELOAD_BACKWARDS, @@ -53,6 +52,11 @@ const InfiniteScroll: FC = ({ // Used to re-query `listItemElements` if rendering is delayed by transition cacheBuster, children, + onLoadMore, + onScroll, + onKeyDown, + onDragOver, + onDragLeave, }: OwnProps) => { // eslint-disable-next-line no-null/no-null let containerRef = useRef(null); @@ -223,6 +227,8 @@ const InfiniteScroll: FC = ({ onScroll={handleScroll} teactFastList={!noFastList && !withAbsolutePositioning} onKeyDown={onKeyDown} + onDragOver={onDragOver} + onDragLeave={onDragLeave} > {withAbsolutePositioning && items?.length ? (
) => void; onClick?: (e: React.MouseEvent) => void; onSecondaryIconClick?: (e: React.MouseEvent) => void; + onDragEnter?: (e: React.DragEvent) => void; } const ListItem: FC = ({ @@ -70,6 +71,7 @@ const ListItem: FC = ({ onMouseDown, onClick, onSecondaryIconClick, + onDragEnter, }) => { // eslint-disable-next-line no-null/no-null let containerRef = useRef(null); @@ -167,6 +169,7 @@ const ListItem: FC = ({ dir={lang.isRtl ? 'rtl' : undefined} style={style} onMouseDown={onMouseDown} + onDragEnter={onDragEnter} >