Left Search: Support drag-n-drop for attachments

This commit is contained in:
Alexander Zinchuk 2025-10-15 19:57:19 +02:00
parent 3c3e8401db
commit 888e65cf6c
6 changed files with 88 additions and 56 deletions

View File

@ -94,9 +94,10 @@ type OwnProps = {
isPreview?: boolean;
previewMessageId?: number;
className?: string;
withTags?: boolean;
observeIntersection?: ObserveFn;
onDragEnter?: (chatId: string) => void;
withTags?: boolean;
onDragLeave?: NoneToVoidFunction;
onReorderAnimationEnd?: NoneToVoidFunction;
};
@ -167,6 +168,7 @@ const Chat: FC<OwnProps & StateProps> = ({
className,
isSynced,
onDragEnter,
onDragLeave,
isAccountFrozen,
chatFolderIds,
orderedFolderIds,
@ -408,9 +410,10 @@ const Chat: FC<OwnProps & StateProps> = ({
style={`top: ${offsetTop}px`}
ripple={!isForum && !isMobile}
contextActions={contextActions}
withPortalForMenu
onClick={handleClick}
onDragEnter={handleDragEnter}
withPortalForMenu
onDragLeave={onDragLeave}
>
<div className={buildClassName('status', 'status-clickable')}>
<Avatar

View File

@ -1,8 +1,5 @@
import type { FC } from '../../../lib/teact/teact';
import type React from '../../../lib/teact/teact';
import {
memo, useEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import type { FC } from '@teact';
import { memo, useEffect, useMemo, useRef, useState } from '@teact';
import { getActions } from '../../../global';
import type { ApiSession } from '../../../api/types';
@ -21,12 +18,12 @@ import {
} from '../../../config';
import { IS_APP, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
import { onDragEnter, onDragLeave } from '../../../util/dragNDropHandlers.ts';
import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager';
import { getServerTime } from '../../../util/serverTime';
import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll';
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
import { useHotkeys } from '../../../hooks/useHotkeys';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
@ -58,7 +55,6 @@ type OwnProps = {
};
const INTERSECTION_THROTTLE = 200;
const DRAG_ENTER_DEBOUNCE = 500;
const RESERVED_HOTKEYS = new Set(['9', '0']);
const ChatList: FC<OwnProps> = ({
@ -84,7 +80,6 @@ const ChatList: FC<OwnProps> = ({
openLeftColumnContent,
} = getActions();
const containerRef = useRef<HTMLDivElement>();
const shouldIgnoreDragRef = useRef(false);
const [unconfirmedSessionHeight, setUnconfirmedSessionHeight] = useState(0);
const isArchived = folderType === 'archived';
@ -183,30 +178,6 @@ const ChatList: FC<OwnProps> = ({
openFrozenAccountModal();
});
const handleArchivedDragEnter = useLastCallback(() => {
if (shouldIgnoreDragRef.current) {
shouldIgnoreDragRef.current = false;
return;
}
handleArchivedClick();
});
const handleDragEnter = useDebouncedCallback((chatId: string) => {
if (shouldIgnoreDragRef.current) {
shouldIgnoreDragRef.current = false;
return;
}
openChat({ id: chatId, shouldReplaceHistory: true });
}, [openChat], DRAG_ENTER_DEBOUNCE, true);
const handleDragLeave = useLastCallback((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;
});
const handleShowStoryRibbon = useLastCallback(() => {
toggleStoryRibbon({ isShown: true, isArchived });
});
@ -215,6 +186,18 @@ const ChatList: FC<OwnProps> = ({
toggleStoryRibbon({ isShown: false, isArchived });
});
const handleArchivedDragEnter = useLastCallback(() => {
onDragEnter(() => {
handleArchivedClick();
});
});
const handleChatDragEnter = useLastCallback((chatId: string) => {
onDragEnter(() => {
openChat({ id: chatId, shouldReplaceHistory: true });
});
});
useTopOverscroll({
containerRef,
onOverscroll: handleShowStoryRibbon,
@ -245,7 +228,8 @@ const ChatList: FC<OwnProps> = ({
onReorderAnimationEnd={onReorderAnimationEnd}
offsetTop={offsetTop}
observeIntersection={observe}
onDragEnter={handleDragEnter}
onDragEnter={handleChatDragEnter}
onDragLeave={onDragLeave}
withTags={withTags}
/>
);
@ -262,7 +246,6 @@ const ChatList: FC<OwnProps> = ({
withAbsolutePositioning
maxHeight={chatsHeight + archiveHeight + frozenNotificationHeight + unconfirmedSessionHeight}
onLoadMore={getMore}
onDragLeave={handleDragLeave}
>
{shouldShowUnconfirmedSessions && (
<UnconfirmedSession

View File

@ -1,5 +1,5 @@
import type { FC } from '../../../lib/teact/teact';
import { memo, useCallback } from '../../../lib/teact/teact';
import type { FC } from '@teact';
import { memo, useCallback } from '@teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiUser } from '../../../api/types';
@ -8,8 +8,13 @@ import { StoryViewerOrigin } from '../../../types';
import { UNMUTE_TIMESTAMP } from '../../../config';
import { getIsChatMuted } from '../../../global/helpers/notifications';
import {
selectChat, selectIsChatPinned, selectNotifyDefaults, selectNotifyException, selectUser,
selectChat,
selectIsChatPinned,
selectNotifyDefaults,
selectNotifyException,
selectUser,
} from '../../../global/selectors';
import { onDragEnter, onDragLeave } from '../../../util/dragNDropHandlers.ts';
import { isUserId } from '../../../util/entities/ids';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
@ -105,6 +110,14 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
});
});
const handleDragEnter = useLastCallback((e) => {
e.preventDefault();
onDragEnter(() => {
onClick(chatId);
}, true);
});
const buttonRef = useSelectWithEnter(() => {
onClick(chatId);
});
@ -112,9 +125,11 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
return (
<ListItem
className="chat-item-clickable search-result"
onClick={handleClick}
contextActions={contextActions}
buttonRef={buttonRef}
onClick={handleClick}
onDragEnter={handleDragEnter}
onDragLeave={onDragLeave}
>
{isUserId(chatId) ? (
<PrivateChatInfo

View File

@ -37,8 +37,6 @@ type OwnProps = {
onWheel?: (e: React.WheelEvent<HTMLDivElement>) => void;
onClick?: (e: React.MouseEvent<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';
@ -69,8 +67,6 @@ const InfiniteScroll: FC<OwnProps> = ({
onWheel,
onClick,
onKeyDown,
onDragOver,
onDragLeave,
}: OwnProps) => {
let containerRef = useRef<HTMLDivElement>();
if (ref) {
@ -270,13 +266,11 @@ const InfiniteScroll: FC<OwnProps> = ({
<div
ref={containerRef}
className={className}
onWheel={onWheel}
teactFastList={!noFastList && !withAbsolutePositioning}
onKeyDown={onKeyDown}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onClick={onClick}
style={style}
teactFastList={!noFastList && !withAbsolutePositioning}
onClick={onClick}
onKeyDown={onKeyDown}
onWheel={onWheel}
>
{beforeChildren}
{withAbsolutePositioning && items?.length ? (

View File

@ -69,13 +69,14 @@ interface OwnProps {
withPortalForMenu?: boolean;
menuBubbleClassName?: string;
href?: string;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
nonInteractive?: boolean;
onClick?: (e: React.MouseEvent<HTMLElement>, arg?: any) => void;
onContextMenu?: (e: React.MouseEvent<HTMLElement>) => void;
clickArg?: any;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
onContextMenu?: (e: React.MouseEvent<HTMLElement>) => void;
onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void;
nonInteractive?: boolean;
onDragLeave?: NoneToVoidFunction;
}
const ListItem = ({
@ -107,13 +108,14 @@ const ListItem = ({
contextActions,
withPortalForMenu,
href,
onMouseDown,
nonInteractive,
onClick,
onContextMenu,
clickArg,
onMouseDown,
onContextMenu,
onSecondaryIconClick,
onDragEnter,
nonInteractive,
onDragLeave,
}: OwnProps) => {
let containerRef = useRef<HTMLDivElement>();
if (ref) {
@ -229,6 +231,7 @@ const ListItem = ({
style={style}
onMouseDown={onMouseDown}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
>
<ButtonElementTag
className={buildClassName('ListItem-button', isTouched && 'active', buttonClassName)}

View File

@ -0,0 +1,34 @@
import { debounce } from './schedulers.ts';
const DRAG_ENTER_DEBOUNCE = 50; // Workaround for `dragenter` firing before previous `dragleave`
const DRAG_ENTER_ACTION_DELAY = 500;
let willSkipNext = false;
let timeout: number | undefined;
export const onDragEnter = debounce((cb: NoneToVoidFunction, shouldSkipNext = false) => {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
if (willSkipNext) {
willSkipNext = false;
return;
}
timeout = window.setTimeout(() => {
if (shouldSkipNext) {
willSkipNext = true;
}
cb();
}, DRAG_ENTER_ACTION_DELAY);
}, DRAG_ENTER_DEBOUNCE, false);
export function onDragLeave() {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
}