Improve drag-and-drop of Home feed composer

environments/review-drop-zones-rl83gb/deployments/3237
Alex Gleason 1 year ago
parent aad7309470
commit ed0206c379
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

@ -88,7 +88,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
const [composeFocused, setComposeFocused] = useState(false); const [composeFocused, setComposeFocused] = useState(false);
const formRef = useRef(null); const formRef = useRef<HTMLDivElement>(null);
const spoilerTextRef = useRef<AutosuggestInput>(null); const spoilerTextRef = useRef<AutosuggestInput>(null);
const autosuggestTextareaRef = useRef<AutosuggestTextarea>(null); const autosuggestTextareaRef = useRef<AutosuggestTextarea>(null);

@ -1,16 +1,15 @@
'use strict'; 'use strict';
import React, { useState, useEffect, useRef } from 'react'; import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { useIntl } from 'react-intl';
import { Switch, useHistory, useLocation, Redirect } from 'react-router-dom'; import { Switch, useHistory, useLocation, Redirect } from 'react-router-dom';
import { fetchFollowRequests } from 'soapbox/actions/accounts'; import { fetchFollowRequests } from 'soapbox/actions/accounts';
import { fetchReports, fetchUsers, fetchConfig } from 'soapbox/actions/admin'; import { fetchReports, fetchUsers, fetchConfig } from 'soapbox/actions/admin';
import { fetchAnnouncements } from 'soapbox/actions/announcements'; import { fetchAnnouncements } from 'soapbox/actions/announcements';
import { uploadCompose, resetCompose } from 'soapbox/actions/compose'; import { resetCompose } from 'soapbox/actions/compose';
import { fetchCustomEmojis } from 'soapbox/actions/custom-emojis'; import { fetchCustomEmojis } from 'soapbox/actions/custom-emojis';
import { uploadEventBanner } from 'soapbox/actions/events';
import { fetchFilters } from 'soapbox/actions/filters'; import { fetchFilters } from 'soapbox/actions/filters';
import { fetchMarker } from 'soapbox/actions/markers'; import { fetchMarker } from 'soapbox/actions/markers';
import { openModal } from 'soapbox/actions/modals'; import { openModal } from 'soapbox/actions/modals';
@ -26,7 +25,7 @@ import SidebarNavigation from 'soapbox/components/sidebar-navigation';
import ThumbNavigation from 'soapbox/components/thumb-navigation'; import ThumbNavigation from 'soapbox/components/thumb-navigation';
import { Layout } from 'soapbox/components/ui'; import { Layout } from 'soapbox/components/ui';
import { useStatContext } from 'soapbox/contexts/stat-context'; import { useStatContext } from 'soapbox/contexts/stat-context';
import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useInstance } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig, useFeatures, useInstance, useDraggedFiles } from 'soapbox/hooks';
import AdminPage from 'soapbox/pages/admin-page'; import AdminPage from 'soapbox/pages/admin-page';
import ChatsPage from 'soapbox/pages/chats-page'; import ChatsPage from 'soapbox/pages/chats-page';
import DefaultPage from 'soapbox/pages/default-page'; import DefaultPage from 'soapbox/pages/default-page';
@ -101,7 +100,6 @@ import {
FollowRecommendations, FollowRecommendations,
Directory, Directory,
SidebarMenu, SidebarMenu,
UploadArea,
ProfileHoverCard, ProfileHoverCard,
StatusHoverCard, StatusHoverCard,
Share, Share,
@ -387,16 +385,12 @@ interface IUI {
} }
const UI: React.FC<IUI> = ({ children }) => { const UI: React.FC<IUI> = ({ children }) => {
const intl = useIntl();
const history = useHistory(); const history = useHistory();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const { data: pendingPolicy } = usePendingPolicy(); const { data: pendingPolicy } = usePendingPolicy();
const instance = useInstance(); const instance = useInstance();
const statContext = useStatContext(); const statContext = useStatContext();
const [draggingOver, setDraggingOver] = useState<boolean>(false);
const dragTargets = useRef<EventTarget[]>([]);
const disconnect = useRef<any>(null); const disconnect = useRef<any>(null);
const node = useRef<HTMLDivElement | null>(null); const node = useRef<HTMLDivElement | null>(null);
const hotkeys = useRef<HTMLDivElement | null>(null); const hotkeys = useRef<HTMLDivElement | null>(null);
@ -411,74 +405,7 @@ const UI: React.FC<IUI> = ({ children }) => {
const streamingUrl = instance.urls.get('streaming_api'); const streamingUrl = instance.urls.get('streaming_api');
const standalone = useAppSelector(isStandalone); const standalone = useAppSelector(isStandalone);
const handleDragEnter = (e: DragEvent) => { const { isDragging } = useDraggedFiles(node);
e.preventDefault();
if (e.target && !dragTargets.current.includes(e.target)) {
dragTargets.current.push(e.target);
}
if (e.dataTransfer && Array.from(e.dataTransfer.types).includes('Files')) {
setDraggingOver(true);
}
};
const handleDragOver = (e: DragEvent) => {
if (dataTransferIsText(e.dataTransfer)) return false;
e.preventDefault();
e.stopPropagation();
try {
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy';
}
} catch (err) {
// Do nothing
}
return false;
};
const handleDrop = (e: DragEvent) => {
if (!me) return;
if (dataTransferIsText(e.dataTransfer)) return;
e.preventDefault();
setDraggingOver(false);
dragTargets.current = [];
dispatch((_, getState) => {
if (e.dataTransfer && e.dataTransfer.files.length >= 1) {
const modals = getState().modals;
const isModalOpen = modals.last()?.modalType === 'COMPOSE';
const isEventsModalOpen = modals.last()?.modalType === 'COMPOSE_EVENT';
if (isEventsModalOpen) dispatch(uploadEventBanner(e.dataTransfer.files[0], intl));
else dispatch(uploadCompose(isModalOpen ? 'compose-modal' : 'home', e.dataTransfer.files, intl));
}
});
};
const handleDragLeave = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
dragTargets.current = dragTargets.current.filter(el => el !== e.target && node.current?.contains(el as Node));
if (dragTargets.current.length > 0) {
return;
}
setDraggingOver(false);
};
const dataTransferIsText = (dataTransfer: DataTransfer | null) => {
return (dataTransfer && Array.from(dataTransfer.types).includes('text/plain') && dataTransfer.items.length === 1);
};
const closeUploadModal = () => {
setDraggingOver(false);
};
const handleServiceWorkerPostMessage = ({ data }: MessageEvent) => { const handleServiceWorkerPostMessage = ({ data }: MessageEvent) => {
if (data.type === 'navigate') { if (data.type === 'navigate') {
@ -501,6 +428,11 @@ const UI: React.FC<IUI> = ({ children }) => {
} }
}; };
const handleDragEnter = (e: DragEvent) => e.preventDefault();
const handleDragLeave = (e: DragEvent) => e.preventDefault();
const handleDragOver = (e: DragEvent) => e.preventDefault();
const handleDrop = (e: DragEvent) => e.preventDefault();
/** Load initial data when a user is logged in */ /** Load initial data when a user is logged in */
const loadAccountData = () => { const loadAccountData = () => {
if (!account) return; if (!account) return;
@ -535,11 +467,6 @@ const UI: React.FC<IUI> = ({ children }) => {
}; };
useEffect(() => { useEffect(() => {
document.addEventListener('dragenter', handleDragEnter, false);
document.addEventListener('dragover', handleDragOver, false);
document.addEventListener('drop', handleDrop, false);
document.addEventListener('dragleave', handleDragLeave, false);
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('message', handleServiceWorkerPostMessage); navigator.serviceWorker.addEventListener('message', handleServiceWorkerPostMessage);
} }
@ -548,12 +475,21 @@ const UI: React.FC<IUI> = ({ children }) => {
window.setTimeout(() => Notification.requestPermission(), 120 * 1000); window.setTimeout(() => Notification.requestPermission(), 120 * 1000);
} }
return () => {
disconnectStreaming();
};
}, []);
useEffect(() => {
document.addEventListener('dragenter', handleDragEnter);
document.addEventListener('dragleave', handleDragLeave);
document.addEventListener('dragover', handleDragOver);
document.addEventListener('drop', handleDrop);
return () => { return () => {
document.removeEventListener('dragenter', handleDragEnter); document.removeEventListener('dragenter', handleDragEnter);
document.removeEventListener('dragleave', handleDragLeave);
document.removeEventListener('dragover', handleDragOver); document.removeEventListener('dragover', handleDragOver);
document.removeEventListener('drop', handleDrop); document.removeEventListener('drop', handleDrop);
document.removeEventListener('dragleave', handleDragLeave);
disconnectStreaming();
}; };
}, []); }, []);
@ -697,6 +633,12 @@ const UI: React.FC<IUI> = ({ children }) => {
return ( return (
<HotKeys keyMap={keyMap} handlers={me ? handlers : undefined} ref={setHotkeysRef} attach={window} focused> <HotKeys keyMap={keyMap} handlers={me ? handlers : undefined} ref={setHotkeysRef} attach={window} focused>
<div ref={node} style={style}> <div ref={node} style={style}>
<div
className={clsx('pointer-events-none fixed z-[9000] h-screen w-screen transition', {
'backdrop-blur': isDragging,
})}
/>
<BackgroundShapes /> <BackgroundShapes />
<div className='z-10 flex flex-col'> <div className='z-10 flex flex-col'>
@ -718,10 +660,6 @@ const UI: React.FC<IUI> = ({ children }) => {
</div> </div>
)} )}
<BundleContainer fetchComponent={UploadArea}>
{Component => <Component active={draggingOver} onClose={closeUploadModal} />}
</BundleContainer>
{me && ( {me && (
<BundleContainer fetchComponent={SidebarMenu}> <BundleContainer fetchComponent={SidebarMenu}>
{Component => <Component />} {Component => <Component />}

@ -1,6 +1,9 @@
import clsx from 'clsx';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { useIntl } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { uploadCompose } from 'soapbox/actions/compose';
import FeedCarousel from 'soapbox/features/feed-filtering/feed-carousel'; import FeedCarousel from 'soapbox/features/feed-filtering/feed-carousel';
import LinkFooter from 'soapbox/features/ui/components/link-footer'; import LinkFooter from 'soapbox/features/ui/components/link-footer';
import { import {
@ -14,7 +17,7 @@ import {
CtaBanner, CtaBanner,
AnnouncementsPanel, AnnouncementsPanel,
} from 'soapbox/features/ui/util/async-components'; } from 'soapbox/features/ui/util/async-components';
import { useAppSelector, useOwnAccount, useFeatures, useSoapboxConfig } from 'soapbox/hooks'; import { useAppSelector, useOwnAccount, useFeatures, useSoapboxConfig, useDraggedFiles, useAppDispatch } from 'soapbox/hooks';
import { Avatar, Card, CardBody, HStack, Layout } from '../components/ui'; import { Avatar, Card, CardBody, HStack, Layout } from '../components/ui';
import ComposeForm from '../features/compose/components/compose-form'; import ComposeForm from '../features/compose/components/compose-form';
@ -25,17 +28,25 @@ interface IHomePage {
} }
const HomePage: React.FC<IHomePage> = ({ children }) => { const HomePage: React.FC<IHomePage> = ({ children }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const me = useAppSelector(state => state.me); const me = useAppSelector(state => state.me);
const account = useOwnAccount(); const account = useOwnAccount();
const features = useFeatures(); const features = useFeatures();
const soapboxConfig = useSoapboxConfig(); const soapboxConfig = useSoapboxConfig();
const composeId = 'home';
const composeBlock = useRef<HTMLDivElement>(null); const composeBlock = useRef<HTMLDivElement>(null);
const hasPatron = soapboxConfig.extensions.getIn(['patron', 'enabled']) === true; const hasPatron = soapboxConfig.extensions.getIn(['patron', 'enabled']) === true;
const hasCrypto = typeof soapboxConfig.cryptoAddresses.getIn([0, 'ticker']) === 'string'; const hasCrypto = typeof soapboxConfig.cryptoAddresses.getIn([0, 'ticker']) === 'string';
const cryptoLimit = soapboxConfig.cryptoDonatePanel.get('limit', 0); const cryptoLimit = soapboxConfig.cryptoDonatePanel.get('limit', 0);
const { isDragging, isDraggedOver } = useDraggedFiles(composeBlock, (files) => {
dispatch(uploadCompose(composeId, files, intl));
});
const acct = account ? account.acct : ''; const acct = account ? account.acct : '';
const avatar = account ? account.avatar : ''; const avatar = account ? account.avatar : '';
@ -43,7 +54,14 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
<> <>
<Layout.Main className='space-y-3 pt-3 dark:divide-gray-800 sm:pt-0'> <Layout.Main className='space-y-3 pt-3 dark:divide-gray-800 sm:pt-0'>
{me && ( {me && (
<Card className='relative z-[1]' variant='rounded' ref={composeBlock}> <Card
className={clsx('relative z-[1] transition', {
'border-2 border-primary-600 border-dashed z-[9001]': isDragging,
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
})}
variant='rounded'
ref={composeBlock}
>
<CardBody> <CardBody>
<HStack alignItems='start' space={4}> <HStack alignItems='start' space={4}>
<Link to={`/@${acct}`}> <Link to={`/@${acct}`}>
@ -52,7 +70,7 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
<div className='w-full translate-y-0.5'> <div className='w-full translate-y-0.5'>
<ComposeForm <ComposeForm
id='home' id={composeId}
shouldCondense shouldCondense
autoFocus={false} autoFocus={false}
clickableAreaRef={composeBlock} clickableAreaRef={composeBlock}

Loading…
Cancel
Save