Chat List: Allow chat selection when dragging file (#1962)
This commit is contained in:
parent
4f04889b63
commit
b660b37a7b
26
src/App.tsx
26
src/App.tsx
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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 />
|
||||
|
||||
@ -119,7 +119,8 @@
|
||||
|
||||
.attachment-caption-wrapper,
|
||||
.document-wrapper,
|
||||
.media-wrapper {
|
||||
.media-wrapper,
|
||||
.form-control {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'}`} />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user