diff --git a/.yarn/cache/contactjs-npm-2.1.5-c7bb4524ec-5be3d66835.zip b/.yarn/cache/contactjs-npm-2.1.5-c7bb4524ec-5be3d66835.zip new file mode 100644 index 000000000..c7a52e7b4 Binary files /dev/null and b/.yarn/cache/contactjs-npm-2.1.5-c7bb4524ec-5be3d66835.zip differ diff --git a/packages/web/package.json b/packages/web/package.json index 2e149362f..35a9c2db7 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -117,6 +117,7 @@ }, "dependencies": { "@lexical/headless": "^0.7.7", - "@simplewebauthn/browser": "^7.0.0" + "@simplewebauthn/browser": "^7.0.0", + "contactjs": "2.1.5" } } diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index ce4d7c148..5e89b32a2 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -43,6 +43,8 @@ import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalCo import { PaneController } from '@/Controllers/PaneController/PaneController' import EmptyFilesView from './EmptyFilesView' import { PaneLayout } from '@/Controllers/PaneController/PaneLayout' +import { usePaneSwipeGesture } from '../Panes/usePaneGesture' +import { mergeRefs } from '@/Hooks/mergeRefs' type Props = { accountMenuController: AccountMenuController @@ -303,12 +305,14 @@ const ContentListView = forwardRef( } }, [selectedUuids, innerRef, isCurrentNoteTemplate, renderedItems, panes]) + const [setElement] = usePaneSwipeGesture('right', () => setPaneLayout(PaneLayout.TagSelection)) + return (
{isMobileScreen && ( diff --git a/packages/web/src/javascripts/Components/NoteGroupView/EditorPane.tsx b/packages/web/src/javascripts/Components/NoteGroupView/EditorPane.tsx new file mode 100644 index 000000000..9e1842b8d --- /dev/null +++ b/packages/web/src/javascripts/Components/NoteGroupView/EditorPane.tsx @@ -0,0 +1,33 @@ +import { WebApplication } from '@/Application/Application' +import { PaneLayout } from '@/Controllers/PaneController/PaneLayout' +import { mergeRefs } from '@/Hooks/mergeRefs' +import { ForwardedRef, forwardRef } from 'react' +import { useResponsiveAppPane } from '../Panes/ResponsivePaneProvider' +import { usePaneSwipeGesture } from '../Panes/usePaneGesture' +import NoteGroupView from './NoteGroupView' + +type Props = { + application: WebApplication + className?: string + id: string +} + +const EditorPane = forwardRef(({ application, className, id }: Props, ref: ForwardedRef) => { + const { setPaneLayout } = useResponsiveAppPane() + + const [setElement] = usePaneSwipeGesture('right', () => { + setPaneLayout(PaneLayout.ItemSelection) + }) + + return ( +
+ +
+ ) +}) + +export default EditorPane diff --git a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx index 3a5ead97c..28b282e5c 100644 --- a/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx +++ b/packages/web/src/javascripts/Components/NoteGroupView/NoteGroupView.tsx @@ -21,8 +21,6 @@ type State = { type Props = { application: WebApplication className?: string - innerRef: (ref: HTMLDivElement) => void - id: string } class NoteGroupView extends AbstractComponent { @@ -99,11 +97,7 @@ class NoteGroupView extends AbstractComponent { const hasControllers = this.state.controllers.length > 0 return ( -
+ <> {this.state.showMultipleSelectedNotes && ( { })} )} -
+ ) } } diff --git a/packages/web/src/javascripts/Components/Panes/PanesSystemComponent.tsx b/packages/web/src/javascripts/Components/Panes/PanesSystemComponent.tsx index 69885b989..d26a35e45 100644 --- a/packages/web/src/javascripts/Components/Panes/PanesSystemComponent.tsx +++ b/packages/web/src/javascripts/Components/Panes/PanesSystemComponent.tsx @@ -7,7 +7,6 @@ import { observer } from 'mobx-react-lite' import { useCallback, useEffect, useState } from 'react' import { usePrevious } from '../ContentListView/Calendar/usePrevious' import ContentListView from '../ContentListView/ContentListView' -import NoteGroupView from '../NoteGroupView/NoteGroupView' import PanelResizer, { PanelResizeType, PanelSide, ResizeFinishCallback } from '../PanelResizer/PanelResizer' import { AppPaneId, AppPaneIdToDivId } from './AppPaneMetadata' import { useResponsiveAppPane } from './ResponsivePaneProvider' @@ -20,6 +19,7 @@ import { import { isPanesChangeLeafDismiss, isPanesChangePush } from '@/Controllers/PaneController/panesForLayout' import { log, LoggingDomain } from '@/Logging' import { useMediaQuery } from '@/Hooks/useMediaQuery' +import EditorPane from '../NoteGroupView/EditorPane' const NAVIGATION_PANEL_MIN_WIDTH = 48 const ITEMS_PANEL_MIN_WIDTH = 200 @@ -330,9 +330,9 @@ const PanesSystemComponent = () => { } else if (pane === AppPaneId.Editor) { return ( - setEditorRef(ref)} + ref={setEditorRef} className={className} application={application} /> diff --git a/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts b/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts new file mode 100644 index 000000000..4962f27a9 --- /dev/null +++ b/packages/web/src/javascripts/Components/Panes/usePaneGesture.ts @@ -0,0 +1,138 @@ +import { useStateRef } from '@/Hooks/useStateRef' +import { useEffect, useRef, useState } from 'react' +import { Direction, Pan, PointerListener, type GestureEventData } from 'contactjs' +import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' + +export const usePaneSwipeGesture = ( + direction: 'left' | 'right', + onSwipeEnd: (element: HTMLElement) => void, + gesture: 'pan' | 'swipe' = 'pan', +) => { + const overlayElementRef = useRef(null) + const [element, setElement] = useState(null) + + const onSwipeEndRef = useStateRef(onSwipeEnd) + const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) + + useEffect(() => { + if (!element) { + return + } + + if (!isMobileScreen) { + return + } + + const panRecognizer = new Pan(element, { + supportedDirections: direction === 'left' ? [Direction.Left] : [Direction.Right], + }) + + const pointerListener = new PointerListener(element, { + supportedGestures: [panRecognizer], + }) + + function onPan(e: unknown) { + const event = e as CustomEvent + if (!element) { + return + } + + const x = event.detail.global.deltaX + requestElementUpdate(x) + } + + let ticking = false + + function onPanEnd(e: unknown) { + const event = e as CustomEvent + if (ticking) { + setTimeout(function () { + onPanEnd(event) + }, 100) + } else { + if (!element) { + return + } + + if (direction === 'right' && event.detail.global.deltaX > 40) { + onSwipeEndRef.current(element) + } else if (direction === 'left' && event.detail.global.deltaX < -40) { + onSwipeEndRef.current(element) + } else { + requestElementUpdate(0) + } + + overlayElementRef.current?.remove() + } + } + + function requestElementUpdate(x: number) { + if (!ticking) { + requestAnimationFrame(function () { + if (!element) { + return + } + + if (!overlayElementRef.current) { + const overlayElement = document.createElement('div') + overlayElement.style.position = 'fixed' + overlayElement.style.top = '0' + overlayElement.style.left = '0' + overlayElement.style.width = '100%' + overlayElement.style.height = '100%' + overlayElement.style.pointerEvents = 'none' + overlayElement.style.backgroundColor = '#000' + overlayElement.style.opacity = '0' + overlayElement.style.willChange = 'opacity' + + element.before(overlayElement) + overlayElementRef.current = overlayElement + } + + const currentLeft = parseInt(element.style.left || '0') + const newLeft = direction === 'right' ? Math.max(x, 0) : Math.min(x, 0) + element.style.left = `${newLeft}px` + + const percent = Math.min(window.innerWidth / currentLeft / 10, 0.45) + overlayElementRef.current.animate([{ opacity: percent }], { + duration: 0, + fill: 'forwards', + }) + + ticking = false + }) + + ticking = true + } + } + + if (gesture === 'pan') { + element.addEventListener('panleft', onPan) + element.addEventListener('panright', onPan) + element.addEventListener('panend', onPanEnd) + } else { + if (direction === 'left') { + element.addEventListener('swipeleft', onPanEnd) + } else { + element.addEventListener('swiperight', onPanEnd) + } + } + + return () => { + pointerListener.destroy() + if (gesture === 'pan') { + element.removeEventListener('panleft', onPan) + element.removeEventListener('panright', onPan) + element.removeEventListener('panend', onPanEnd) + } else { + if (direction === 'left') { + element.removeEventListener('swipeleft', onPanEnd) + } else { + element.removeEventListener('swiperight', onPanEnd) + } + } + } + }, [direction, element, gesture, isMobileScreen, onSwipeEndRef]) + + return [setElement] +} diff --git a/packages/web/src/javascripts/Components/Tags/Navigation.tsx b/packages/web/src/javascripts/Components/Tags/Navigation.tsx index 6ec326ce5..bea87acc1 100644 --- a/packages/web/src/javascripts/Components/Tags/Navigation.tsx +++ b/packages/web/src/javascripts/Components/Tags/Navigation.tsx @@ -12,6 +12,8 @@ import { isIOS } from '@/Utils' import { PanelResizedData } from '@/Types/PanelResizedData' import { PANEL_NAME_NAVIGATION } from '@/Constants/Constants' import { PaneLayout } from '@/Controllers/PaneController/PaneLayout' +import { usePaneSwipeGesture } from '../Panes/usePaneGesture' +import { mergeRefs } from '@/Hooks/mergeRefs' type Props = { application: WebApplication @@ -44,6 +46,15 @@ const Navigation = forwardRef(({ application, className, }) }, [application]) + const [setElement] = usePaneSwipeGesture( + 'left', + (element) => { + setPaneLayout(PaneLayout.ItemSelection) + element.style.left = '0' + }, + 'swipe', + ) + return (
(({ application, className, 'sn-component section pb-[50px] md:pb-0', 'h-full max-h-full overflow-hidden pt-safe-top md:h-full md:max-h-full md:min-h-0', )} - ref={ref} + ref={mergeRefs([ref, setElement])} >