Chat List: Allow chat selection when dragging file (#1962)

This commit is contained in:
Alexander Zinchuk 2022-08-05 19:22:51 +02:00
parent 4f04889b63
commit b660b37a7b
11 changed files with 85 additions and 15 deletions

View File

@ -55,6 +55,32 @@ const App: FC<StateProps> = ({
}); });
}, [disconnect, markInactive]); }, [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 <Test />; // return <Test />;
let activeKey: number; let activeKey: number;

View File

@ -1,10 +1,10 @@
import type { FC } from '../../lib/teact/teact';
import React, { useEffect } from '../../lib/teact/teact'; import React, { useEffect } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import { ApiMediaFormat } from '../../api/types'; import { ApiMediaFormat } from '../../api/types';
import type { GlobalState } from '../../global/types'; import type { GlobalState } from '../../global/types';
import type { ThemeKey } from '../../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 { getChatAvatarHash } from '../../global/helpers/chats'; // Direct import for better module splitting
import { selectIsRightColumnShown, selectTheme, selectIsCurrentUserPremium } from '../../global/selectors'; import { selectIsRightColumnShown, selectTheme, selectIsCurrentUserPremium } from '../../global/selectors';

View File

@ -68,6 +68,7 @@ type OwnProps = {
animationType: ChatAnimationTypes; animationType: ChatAnimationTypes;
isPinned?: boolean; isPinned?: boolean;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
onDragEnter?: (chatId: string) => void;
}; };
type StateProps = { type StateProps = {
@ -113,6 +114,7 @@ const Chat: FC<OwnProps & StateProps> = ({
canScrollDown, canScrollDown,
canChangeFolder, canChangeFolder,
lastSyncTime, lastSyncTime,
onDragEnter,
}) => { }) => {
const { const {
openChat, openChat,
@ -200,6 +202,11 @@ const Chat: FC<OwnProps & StateProps> = ({
focusLastMessage, focusLastMessage,
]); ]);
const handleDragEnter = useCallback((e) => {
e.preventDefault();
onDragEnter?.(chatId);
}, [chatId, onDragEnter]);
const handleDelete = useCallback(() => { const handleDelete = useCallback(() => {
markRenderDeleteModal(); markRenderDeleteModal();
openDeleteModal(); openDeleteModal();
@ -299,6 +306,7 @@ const Chat: FC<OwnProps & StateProps> = ({
ripple={!IS_SINGLE_COLUMN_LAYOUT} ripple={!IS_SINGLE_COLUMN_LAYOUT}
contextActions={contextActions} contextActions={contextActions}
onClick={handleClick} onClick={handleClick}
onDragEnter={handleDragEnter}
> >
<div className="status"> <div className="status">
<Avatar <Avatar

View File

@ -1,5 +1,5 @@
import React, { import React, {
memo, useMemo, useEffect, useRef, memo, useMemo, useEffect, useRef, useCallback,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
@ -23,6 +23,7 @@ import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
import { useChatAnimationType } from './hooks'; import { useChatAnimationType } from './hooks';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import { useHotkeys } from '../../../hooks/useHotkeys'; import { useHotkeys } from '../../../hooks/useHotkeys';
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
import InfiniteScroll from '../../ui/InfiniteScroll'; import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading'; import Loading from '../../ui/Loading';
@ -39,6 +40,7 @@ type OwnProps = {
}; };
const INTERSECTION_THROTTLE = 200; const INTERSECTION_THROTTLE = 200;
const DRAG_ENTER_DEBOUNCE = 500;
const ChatList: FC<OwnProps> = ({ const ChatList: FC<OwnProps> = ({
folderType, folderType,
@ -50,6 +52,7 @@ const ChatList: FC<OwnProps> = ({
const { openChat, openNextChat } = getActions(); const { openChat, openNextChat } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const shouldIgnoreDragRef = useRef(false);
const resolvedFolderId = ( const resolvedFolderId = (
folderType === 'all' ? ALL_FOLDER_ID : folderType === 'archived' ? ARCHIVED_FOLDER_ID : folderId! folderType === 'all' ? ALL_FOLDER_ID : folderType === 'archived' ? ARCHIVED_FOLDER_ID : folderId!
@ -126,6 +129,22 @@ const ChatList: FC<OwnProps> = ({
throttleMs: INTERSECTION_THROTTLE, 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<HTMLDivElement>) => {
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() { function renderChats() {
const viewportOffset = orderedIds!.indexOf(viewportIds![0]); const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
const pinnedCount = getPinnedChatsCount(resolvedFolderId) || 0; const pinnedCount = getPinnedChatsCount(resolvedFolderId) || 0;
@ -144,6 +163,7 @@ const ChatList: FC<OwnProps> = ({
orderDiff={orderDiffById[id]} orderDiff={orderDiffById[id]}
style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`} style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`}
observeIntersection={observe} observeIntersection={observe}
onDragEnter={handleDragEnter}
/> />
); );
}); });
@ -158,6 +178,7 @@ const ChatList: FC<OwnProps> = ({
withAbsolutePositioning withAbsolutePositioning
maxHeight={(orderedIds?.length || 0) * CHAT_HEIGHT_PX} maxHeight={(orderedIds?.length || 0) * CHAT_HEIGHT_PX}
onLoadMore={getMore} onLoadMore={getMore}
onDragLeave={handleDragLeave}
> >
{viewportIds?.length ? ( {viewportIds?.length ? (
renderChats() renderChats()

View File

@ -27,7 +27,6 @@ import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners'; import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
import { processDeepLink } from '../../util/deeplink'; import { processDeepLink } from '../../util/deeplink';
import stopEvent from '../../util/stopEvent';
import windowSize from '../../util/windowSize'; import windowSize from '../../util/windowSize';
import { getAllNotificationsCount } from '../../util/folderManager'; import { getAllNotificationsCount } from '../../util/folderManager';
import useBackgroundMode from '../../hooks/useBackgroundMode'; import useBackgroundMode from '../../hooks/useBackgroundMode';
@ -385,7 +384,7 @@ const Main: FC<StateProps> = ({
usePreventPinchZoomGesture(isMediaViewerOpen); usePreventPinchZoomGesture(isMediaViewerOpen);
return ( return (
<div id="Main" className={className} onDrop={stopEvent} onDragOver={stopEvent}> <div id="Main" className={className}>
<LeftColumn /> <LeftColumn />
<MiddleColumn /> <MiddleColumn />
<RightColumn /> <RightColumn />

View File

@ -119,7 +119,8 @@
.attachment-caption-wrapper, .attachment-caption-wrapper,
.document-wrapper, .document-wrapper,
.media-wrapper { .media-wrapper,
.form-control {
pointer-events: none; pointer-events: none;
} }

View File

@ -177,7 +177,6 @@ const AttachmentModal: FC<OwnProps> = ({
function handleDragOver(e: React.MouseEvent<HTMLDivElement, MouseEvent>) { function handleDragOver(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
e.preventDefault(); e.preventDefault();
e.stopPropagation();
if (hideTimeoutRef.current) { if (hideTimeoutRef.current) {
window.clearTimeout(hideTimeoutRef.current); window.clearTimeout(hideTimeoutRef.current);
@ -258,6 +257,7 @@ const AttachmentModal: FC<OwnProps> = ({
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
data-attach-description={lang('Preview.Dragging.AddItems', 10)} data-attach-description={lang('Preview.Dragging.AddItems', 10)}
data-dropzone
> >
{isQuick ? ( {isQuick ? (
<div className="media-wrapper custom-scroll"> <div className="media-wrapper custom-scroll">

View File

@ -96,7 +96,13 @@ const DropArea: FC<OwnProps> = ({
return ( return (
<Portal containerId="#middle-column-portals"> <Portal containerId="#middle-column-portals">
<div className={className} onDragLeave={handleDragLeave} onDragOver={handleDragOver} onDrop={onHide}> <div
className={className}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={onHide}
onClick={onHide}
>
<DropTarget onFileSelect={handleFilesDrop} /> <DropTarget onFileSelect={handleFilesDrop} />
{(withQuick || prevWithQuick) && <DropTarget onFileSelect={handleQuickFilesDrop} isQuick />} {(withQuick || prevWithQuick) && <DropTarget onFileSelect={handleQuickFilesDrop} isQuick />}
</div> </div>

View File

@ -14,7 +14,6 @@ export type OwnProps = {
const DropTarget: FC<OwnProps> = ({ isQuick, onFileSelect }) => { const DropTarget: FC<OwnProps> = ({ isQuick, onFileSelect }) => {
const [isHovered, markHovered, unmarkHovered] = useFlag(); const [isHovered, markHovered, unmarkHovered] = useFlag();
const handleDragEnter = () => { markHovered(); };
const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => { const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
const { relatedTarget: toTarget } = e; const { relatedTarget: toTarget } = e;
@ -34,8 +33,9 @@ const DropTarget: FC<OwnProps> = ({ isQuick, onFileSelect }) => {
<div <div
className={className} className={className}
onDrop={onFileSelect} onDrop={onFileSelect}
onDragEnter={handleDragEnter} onDragEnter={markHovered}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
data-dropzone
> >
<div className="target-content"> <div className="target-content">
<div className={`icon icon-${isQuick ? 'photo' : 'document'}`} /> <div className={`icon icon-${isQuick ? 'photo' : 'document'}`} />

View File

@ -14,9 +14,6 @@ import buildStyle from '../../util/buildStyle';
type OwnProps = { type OwnProps = {
ref?: RefObject<HTMLDivElement>; ref?: RefObject<HTMLDivElement>;
className?: string; className?: string;
onLoadMore?: ({ direction }: { direction: LoadMoreDirection; noScroll?: boolean }) => void;
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<any>) => void;
items?: any[]; items?: any[];
itemSelector?: string; itemSelector?: string;
preloadBackwards?: number; preloadBackwards?: number;
@ -28,6 +25,11 @@ type OwnProps = {
noFastList?: boolean; noFastList?: boolean;
cacheBuster?: any; cacheBuster?: any;
children: React.ReactNode; children: React.ReactNode;
onLoadMore?: ({ direction }: { direction: LoadMoreDirection; noScroll?: boolean }) => void;
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<any>) => void;
onDragOver?: (e: React.DragEvent<HTMLDivElement>) => void;
onDragLeave?: (e: React.DragEvent<HTMLDivElement>) => void;
}; };
const DEFAULT_LIST_SELECTOR = '.ListItem'; const DEFAULT_LIST_SELECTOR = '.ListItem';
@ -37,9 +39,6 @@ const DEFAULT_SENSITIVE_AREA = 800;
const InfiniteScroll: FC<OwnProps> = ({ const InfiniteScroll: FC<OwnProps> = ({
ref, ref,
className, className,
onLoadMore,
onScroll,
onKeyDown,
items, items,
itemSelector = DEFAULT_LIST_SELECTOR, itemSelector = DEFAULT_LIST_SELECTOR,
preloadBackwards = DEFAULT_PRELOAD_BACKWARDS, preloadBackwards = DEFAULT_PRELOAD_BACKWARDS,
@ -53,6 +52,11 @@ const InfiniteScroll: FC<OwnProps> = ({
// Used to re-query `listItemElements` if rendering is delayed by transition // Used to re-query `listItemElements` if rendering is delayed by transition
cacheBuster, cacheBuster,
children, children,
onLoadMore,
onScroll,
onKeyDown,
onDragOver,
onDragLeave,
}: OwnProps) => { }: OwnProps) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
let containerRef = useRef<HTMLDivElement>(null); let containerRef = useRef<HTMLDivElement>(null);
@ -223,6 +227,8 @@ const InfiniteScroll: FC<OwnProps> = ({
onScroll={handleScroll} onScroll={handleScroll}
teactFastList={!noFastList && !withAbsolutePositioning} teactFastList={!noFastList && !withAbsolutePositioning}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
> >
{withAbsolutePositioning && items?.length ? ( {withAbsolutePositioning && items?.length ? (
<div <div

View File

@ -46,6 +46,7 @@ interface OwnProps {
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void; onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void; onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void; onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void;
} }
const ListItem: FC<OwnProps> = ({ const ListItem: FC<OwnProps> = ({
@ -70,6 +71,7 @@ const ListItem: FC<OwnProps> = ({
onMouseDown, onMouseDown,
onClick, onClick,
onSecondaryIconClick, onSecondaryIconClick,
onDragEnter,
}) => { }) => {
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
let containerRef = useRef<HTMLDivElement>(null); let containerRef = useRef<HTMLDivElement>(null);
@ -167,6 +169,7 @@ const ListItem: FC<OwnProps> = ({
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
style={style} style={style}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onDragEnter={onDragEnter}
> >
<div <div
className={buildClassName('ListItem-button', isTouched && 'active', buttonClassName)} className={buildClassName('ListItem-button', isTouched && 'active', buttonClassName)}