Left Search: Support drag-n-drop for attachments
This commit is contained in:
parent
3c3e8401db
commit
888e65cf6c
@ -94,9 +94,10 @@ type OwnProps = {
|
|||||||
isPreview?: boolean;
|
isPreview?: boolean;
|
||||||
previewMessageId?: number;
|
previewMessageId?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
withTags?: boolean;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
onDragEnter?: (chatId: string) => void;
|
onDragEnter?: (chatId: string) => void;
|
||||||
withTags?: boolean;
|
onDragLeave?: NoneToVoidFunction;
|
||||||
onReorderAnimationEnd?: NoneToVoidFunction;
|
onReorderAnimationEnd?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -167,6 +168,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
className,
|
className,
|
||||||
isSynced,
|
isSynced,
|
||||||
onDragEnter,
|
onDragEnter,
|
||||||
|
onDragLeave,
|
||||||
isAccountFrozen,
|
isAccountFrozen,
|
||||||
chatFolderIds,
|
chatFolderIds,
|
||||||
orderedFolderIds,
|
orderedFolderIds,
|
||||||
@ -408,9 +410,10 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
style={`top: ${offsetTop}px`}
|
style={`top: ${offsetTop}px`}
|
||||||
ripple={!isForum && !isMobile}
|
ripple={!isForum && !isMobile}
|
||||||
contextActions={contextActions}
|
contextActions={contextActions}
|
||||||
|
withPortalForMenu
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
withPortalForMenu
|
onDragLeave={onDragLeave}
|
||||||
>
|
>
|
||||||
<div className={buildClassName('status', 'status-clickable')}>
|
<div className={buildClassName('status', 'status-clickable')}>
|
||||||
<Avatar
|
<Avatar
|
||||||
|
|||||||
@ -1,8 +1,5 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '@teact';
|
||||||
import type React from '../../../lib/teact/teact';
|
import { memo, useEffect, useMemo, useRef, useState } from '@teact';
|
||||||
import {
|
|
||||||
memo, useEffect, useMemo, useRef, useState,
|
|
||||||
} from '../../../lib/teact/teact';
|
|
||||||
import { getActions } from '../../../global';
|
import { getActions } from '../../../global';
|
||||||
|
|
||||||
import type { ApiSession } from '../../../api/types';
|
import type { ApiSession } from '../../../api/types';
|
||||||
@ -21,12 +18,12 @@ import {
|
|||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_APP, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
|
import { IS_APP, IS_MAC_OS } from '../../../util/browser/windowEnvironment';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { onDragEnter, onDragLeave } from '../../../util/dragNDropHandlers.ts';
|
||||||
import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager';
|
import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager';
|
||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
|
|
||||||
import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
|
import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
|
||||||
import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll';
|
import useTopOverscroll from '../../../hooks/scroll/useTopOverscroll';
|
||||||
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
|
|
||||||
import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
|
import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
|
||||||
import { useHotkeys } from '../../../hooks/useHotkeys';
|
import { useHotkeys } from '../../../hooks/useHotkeys';
|
||||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||||
@ -58,7 +55,6 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const INTERSECTION_THROTTLE = 200;
|
const INTERSECTION_THROTTLE = 200;
|
||||||
const DRAG_ENTER_DEBOUNCE = 500;
|
|
||||||
const RESERVED_HOTKEYS = new Set(['9', '0']);
|
const RESERVED_HOTKEYS = new Set(['9', '0']);
|
||||||
|
|
||||||
const ChatList: FC<OwnProps> = ({
|
const ChatList: FC<OwnProps> = ({
|
||||||
@ -84,7 +80,6 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
openLeftColumnContent,
|
openLeftColumnContent,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
const containerRef = useRef<HTMLDivElement>();
|
const containerRef = useRef<HTMLDivElement>();
|
||||||
const shouldIgnoreDragRef = useRef(false);
|
|
||||||
const [unconfirmedSessionHeight, setUnconfirmedSessionHeight] = useState(0);
|
const [unconfirmedSessionHeight, setUnconfirmedSessionHeight] = useState(0);
|
||||||
|
|
||||||
const isArchived = folderType === 'archived';
|
const isArchived = folderType === 'archived';
|
||||||
@ -183,30 +178,6 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
openFrozenAccountModal();
|
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(() => {
|
const handleShowStoryRibbon = useLastCallback(() => {
|
||||||
toggleStoryRibbon({ isShown: true, isArchived });
|
toggleStoryRibbon({ isShown: true, isArchived });
|
||||||
});
|
});
|
||||||
@ -215,6 +186,18 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
toggleStoryRibbon({ isShown: false, isArchived });
|
toggleStoryRibbon({ isShown: false, isArchived });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleArchivedDragEnter = useLastCallback(() => {
|
||||||
|
onDragEnter(() => {
|
||||||
|
handleArchivedClick();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChatDragEnter = useLastCallback((chatId: string) => {
|
||||||
|
onDragEnter(() => {
|
||||||
|
openChat({ id: chatId, shouldReplaceHistory: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
useTopOverscroll({
|
useTopOverscroll({
|
||||||
containerRef,
|
containerRef,
|
||||||
onOverscroll: handleShowStoryRibbon,
|
onOverscroll: handleShowStoryRibbon,
|
||||||
@ -245,7 +228,8 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
onReorderAnimationEnd={onReorderAnimationEnd}
|
onReorderAnimationEnd={onReorderAnimationEnd}
|
||||||
offsetTop={offsetTop}
|
offsetTop={offsetTop}
|
||||||
observeIntersection={observe}
|
observeIntersection={observe}
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleChatDragEnter}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
withTags={withTags}
|
withTags={withTags}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -262,7 +246,6 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
withAbsolutePositioning
|
withAbsolutePositioning
|
||||||
maxHeight={chatsHeight + archiveHeight + frozenNotificationHeight + unconfirmedSessionHeight}
|
maxHeight={chatsHeight + archiveHeight + frozenNotificationHeight + unconfirmedSessionHeight}
|
||||||
onLoadMore={getMore}
|
onLoadMore={getMore}
|
||||||
onDragLeave={handleDragLeave}
|
|
||||||
>
|
>
|
||||||
{shouldShowUnconfirmedSessions && (
|
{shouldShowUnconfirmedSessions && (
|
||||||
<UnconfirmedSession
|
<UnconfirmedSession
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '@teact';
|
||||||
import { memo, useCallback } from '../../../lib/teact/teact';
|
import { memo, useCallback } from '@teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiChat, ApiUser } from '../../../api/types';
|
import type { ApiChat, ApiUser } from '../../../api/types';
|
||||||
@ -8,8 +8,13 @@ import { StoryViewerOrigin } from '../../../types';
|
|||||||
import { UNMUTE_TIMESTAMP } from '../../../config';
|
import { UNMUTE_TIMESTAMP } from '../../../config';
|
||||||
import { getIsChatMuted } from '../../../global/helpers/notifications';
|
import { getIsChatMuted } from '../../../global/helpers/notifications';
|
||||||
import {
|
import {
|
||||||
selectChat, selectIsChatPinned, selectNotifyDefaults, selectNotifyException, selectUser,
|
selectChat,
|
||||||
|
selectIsChatPinned,
|
||||||
|
selectNotifyDefaults,
|
||||||
|
selectNotifyException,
|
||||||
|
selectUser,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
|
import { onDragEnter, onDragLeave } from '../../../util/dragNDropHandlers.ts';
|
||||||
import { isUserId } from '../../../util/entities/ids';
|
import { isUserId } from '../../../util/entities/ids';
|
||||||
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
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(() => {
|
const buttonRef = useSelectWithEnter(() => {
|
||||||
onClick(chatId);
|
onClick(chatId);
|
||||||
});
|
});
|
||||||
@ -112,9 +125,11 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
className="chat-item-clickable search-result"
|
className="chat-item-clickable search-result"
|
||||||
onClick={handleClick}
|
|
||||||
contextActions={contextActions}
|
contextActions={contextActions}
|
||||||
buttonRef={buttonRef}
|
buttonRef={buttonRef}
|
||||||
|
onClick={handleClick}
|
||||||
|
onDragEnter={handleDragEnter}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
>
|
>
|
||||||
{isUserId(chatId) ? (
|
{isUserId(chatId) ? (
|
||||||
<PrivateChatInfo
|
<PrivateChatInfo
|
||||||
|
|||||||
@ -37,8 +37,6 @@ type OwnProps = {
|
|||||||
onWheel?: (e: React.WheelEvent<HTMLDivElement>) => void;
|
onWheel?: (e: React.WheelEvent<HTMLDivElement>) => void;
|
||||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
onKeyDown?: (e: React.KeyboardEvent<any>) => 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';
|
||||||
@ -69,8 +67,6 @@ const InfiniteScroll: FC<OwnProps> = ({
|
|||||||
onWheel,
|
onWheel,
|
||||||
onClick,
|
onClick,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
onDragOver,
|
|
||||||
onDragLeave,
|
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
let containerRef = useRef<HTMLDivElement>();
|
let containerRef = useRef<HTMLDivElement>();
|
||||||
if (ref) {
|
if (ref) {
|
||||||
@ -270,13 +266,11 @@ const InfiniteScroll: FC<OwnProps> = ({
|
|||||||
<div
|
<div
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
className={className}
|
className={className}
|
||||||
onWheel={onWheel}
|
|
||||||
teactFastList={!noFastList && !withAbsolutePositioning}
|
|
||||||
onKeyDown={onKeyDown}
|
|
||||||
onDragOver={onDragOver}
|
|
||||||
onDragLeave={onDragLeave}
|
|
||||||
onClick={onClick}
|
|
||||||
style={style}
|
style={style}
|
||||||
|
teactFastList={!noFastList && !withAbsolutePositioning}
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
onWheel={onWheel}
|
||||||
>
|
>
|
||||||
{beforeChildren}
|
{beforeChildren}
|
||||||
{withAbsolutePositioning && items?.length ? (
|
{withAbsolutePositioning && items?.length ? (
|
||||||
|
|||||||
@ -69,13 +69,14 @@ interface OwnProps {
|
|||||||
withPortalForMenu?: boolean;
|
withPortalForMenu?: boolean;
|
||||||
menuBubbleClassName?: string;
|
menuBubbleClassName?: string;
|
||||||
href?: string;
|
href?: string;
|
||||||
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
nonInteractive?: boolean;
|
||||||
onClick?: (e: React.MouseEvent<HTMLElement>, arg?: any) => void;
|
onClick?: (e: React.MouseEvent<HTMLElement>, arg?: any) => void;
|
||||||
onContextMenu?: (e: React.MouseEvent<HTMLElement>) => void;
|
|
||||||
clickArg?: any;
|
clickArg?: any;
|
||||||
|
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
|
onContextMenu?: (e: React.MouseEvent<HTMLElement>) => void;
|
||||||
onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
onSecondaryIconClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void;
|
onDragEnter?: (e: React.DragEvent<HTMLDivElement>) => void;
|
||||||
nonInteractive?: boolean;
|
onDragLeave?: NoneToVoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListItem = ({
|
const ListItem = ({
|
||||||
@ -107,13 +108,14 @@ const ListItem = ({
|
|||||||
contextActions,
|
contextActions,
|
||||||
withPortalForMenu,
|
withPortalForMenu,
|
||||||
href,
|
href,
|
||||||
onMouseDown,
|
nonInteractive,
|
||||||
onClick,
|
onClick,
|
||||||
onContextMenu,
|
|
||||||
clickArg,
|
clickArg,
|
||||||
|
onMouseDown,
|
||||||
|
onContextMenu,
|
||||||
onSecondaryIconClick,
|
onSecondaryIconClick,
|
||||||
onDragEnter,
|
onDragEnter,
|
||||||
nonInteractive,
|
onDragLeave,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
let containerRef = useRef<HTMLDivElement>();
|
let containerRef = useRef<HTMLDivElement>();
|
||||||
if (ref) {
|
if (ref) {
|
||||||
@ -229,6 +231,7 @@ const ListItem = ({
|
|||||||
style={style}
|
style={style}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onDragEnter={onDragEnter}
|
onDragEnter={onDragEnter}
|
||||||
|
onDragLeave={onDragLeave}
|
||||||
>
|
>
|
||||||
<ButtonElementTag
|
<ButtonElementTag
|
||||||
className={buildClassName('ListItem-button', isTouched && 'active', buttonClassName)}
|
className={buildClassName('ListItem-button', isTouched && 'active', buttonClassName)}
|
||||||
|
|||||||
34
src/util/dragNDropHandlers.ts
Normal file
34
src/util/dragNDropHandlers.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user