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]);
|
}, [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;
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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'}`} />
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user