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]);
// 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 />;
let activeKey: number;

View File

@ -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';

View File

@ -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<OwnProps & StateProps> = ({
canScrollDown,
canChangeFolder,
lastSyncTime,
onDragEnter,
}) => {
const {
openChat,
@ -200,6 +202,11 @@ const Chat: FC<OwnProps & StateProps> = ({
focusLastMessage,
]);
const handleDragEnter = useCallback((e) => {
e.preventDefault();
onDragEnter?.(chatId);
}, [chatId, onDragEnter]);
const handleDelete = useCallback(() => {
markRenderDeleteModal();
openDeleteModal();
@ -299,6 +306,7 @@ const Chat: FC<OwnProps & StateProps> = ({
ripple={!IS_SINGLE_COLUMN_LAYOUT}
contextActions={contextActions}
onClick={handleClick}
onDragEnter={handleDragEnter}
>
<div className="status">
<Avatar

View File

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

View File

@ -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<StateProps> = ({
usePreventPinchZoomGesture(isMediaViewerOpen);
return (
<div id="Main" className={className} onDrop={stopEvent} onDragOver={stopEvent}>
<div id="Main" className={className}>
<LeftColumn />
<MiddleColumn />
<RightColumn />

View File

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

View File

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

View File

@ -96,7 +96,13 @@ const DropArea: FC<OwnProps> = ({
return (
<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} />
{(withQuick || prevWithQuick) && <DropTarget onFileSelect={handleQuickFilesDrop} isQuick />}
</div>

View File

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

View File

@ -14,9 +14,6 @@ import buildStyle from '../../util/buildStyle';
type OwnProps = {
ref?: RefObject<HTMLDivElement>;
className?: string;
onLoadMore?: ({ direction }: { direction: LoadMoreDirection; noScroll?: boolean }) => void;
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<any>) => 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<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';
@ -37,9 +39,6 @@ const DEFAULT_SENSITIVE_AREA = 800;
const InfiniteScroll: FC<OwnProps> = ({
ref,
className,
onLoadMore,
onScroll,
onKeyDown,
items,
itemSelector = DEFAULT_LIST_SELECTOR,
preloadBackwards = DEFAULT_PRELOAD_BACKWARDS,
@ -53,6 +52,11 @@ const InfiniteScroll: FC<OwnProps> = ({
// 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<HTMLDivElement>(null);
@ -223,6 +227,8 @@ const InfiniteScroll: FC<OwnProps> = ({
onScroll={handleScroll}
teactFastList={!noFastList && !withAbsolutePositioning}
onKeyDown={onKeyDown}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
>
{withAbsolutePositioning && items?.length ? (
<div

View File

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