diff --git a/.yarn/cache/contactjs-npm-2.1.5-c7bb4524ec-5be3d66835.zip b/.yarn/cache/contactjs-npm-2.1.5-c7bb4524ec-5be3d66835.zip deleted file mode 100644 index c7a52e7b4..000000000 Binary files a/.yarn/cache/contactjs-npm-2.1.5-c7bb4524ec-5be3d66835.zip and /dev/null differ diff --git a/packages/web/package.json b/packages/web/package.json index 1b558563e..b761a931b 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -109,7 +109,6 @@ "dependencies": { "@ariakit/react": "^0.1.2", "@lexical/headless": "0.10.0", - "@radix-ui/react-slot": "^1.0.1", - "contactjs": "2.1.5" + "@radix-ui/react-slot": "^1.0.1" } } diff --git a/packages/web/src/javascripts/Components/ContentListView/ListItemTags.tsx b/packages/web/src/javascripts/Components/ContentListView/ListItemTags.tsx index 84808eac3..4f59c5c0c 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ListItemTags.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ListItemTags.tsx @@ -14,7 +14,7 @@ const ListItemTags: FunctionComponent = ({ hideTags, tags }) => { } return ( -
+
{tags.map((tag) => ( > = ({
node.clientHeight || node.scrollWidth > node.clientWidth) { + return node + } else { + return getScrollParent(node.parentElement) + } +} + +const supportsPassive = (() => { + let supportsPassive = false + try { + const opts = Object.defineProperty({}, 'passive', { + get: () => { + supportsPassive = true + }, + }) + window.addEventListener('test', null as never, opts) + window.removeEventListener('test', null as never, opts) + } catch (e) { + /* empty */ + } + return supportsPassive +})() export const usePaneSwipeGesture = ( direction: 'left' | 'right', @@ -12,16 +40,18 @@ export const usePaneSwipeGesture = ( ) => { const application = useApplication() - const overlayElementRef = useRef(null) + const underlayElementRef = useRef(null) const [element, setElement] = useState(null) const onSwipeEndRef = useStateRef(onSwipeEnd) const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm) - const [isEnabled, setIsEnabled] = useState(() => application.getPreference(PrefKey.PaneGesturesEnabled, false)) + const [isEnabled, setIsEnabled] = useState(() => + application.getPreference(PrefKey.PaneGesturesEnabled, PrefDefaults[PrefKey.PaneGesturesEnabled]), + ) useEffect(() => { return application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { - setIsEnabled(application.getPreference(PrefKey.PaneGesturesEnabled, false)) + setIsEnabled(application.getPreference(PrefKey.PaneGesturesEnabled, PrefDefaults[PrefKey.PaneGesturesEnabled])) }) }, [application]) @@ -38,126 +68,159 @@ export const usePaneSwipeGesture = ( return } - const panRecognizer = new Pan(element, { - supportedDirections: direction === 'left' ? [Direction.Left] : [Direction.Right], - }) + underlayElementRef.current = element.parentElement?.querySelector(`[data-pane-underlay="${element.id}"]`) || null - const pointerListener = new PointerListener(element, { - supportedGestures: [panRecognizer], - }) + let startX = 0 + let clientX = 0 + let closestScrollContainer: HTMLElement | null + let scrollContainerAxis: 'x' | 'y' | null = null + let canceled = false - function onPan(e: unknown) { - const event = e as CustomEvent - if (!element) { + const TouchMoveThreshold = 15 + const SwipeFinishThreshold = 40 + TouchMoveThreshold + + const scrollListener = () => { + canceled = true + } + + const touchStartListener = (event: TouchEvent) => { + closestScrollContainer = getScrollParent(event.target as HTMLElement) + if (closestScrollContainer) { + closestScrollContainer.addEventListener('scroll', scrollListener) + + if (closestScrollContainer.scrollWidth > closestScrollContainer.clientWidth) { + scrollContainerAxis = 'x' + } + } else { + scrollContainerAxis = null + } + + const touch = event.touches[0] + startX = touch.clientX + + canceled = false + + element.style.willChange = 'transform' + } + + const updateElement = (x: number) => { + if (!underlayElementRef.current) { + const underlayElement = document.createElement('div') + underlayElement.style.position = 'fixed' + underlayElement.style.top = '0' + underlayElement.style.left = '0' + underlayElement.style.width = '100%' + underlayElement.style.height = '100%' + underlayElement.style.pointerEvents = 'none' + underlayElement.style.backgroundColor = '#000' + underlayElement.style.opacity = '0' + underlayElement.style.willChange = 'opacity' + underlayElement.setAttribute('role', 'presentation') + underlayElement.ariaHidden = 'true' + underlayElement.setAttribute('data-pane-underlay', element.id) + + element.before(underlayElement) + underlayElementRef.current = underlayElement + } + + element.animate( + [ + { + transform: `translate3d(${x}px, 0, 0)`, + }, + ], + { + duration: 0, + fill: 'forwards', + }, + ) + + const percent = Math.min(window.innerWidth / x / 10, 0.45) + underlayElementRef.current.animate([{ opacity: percent }], { + duration: 0, + fill: 'forwards', + }) + } + + const touchMoveListener = (event: TouchEvent) => { + if (scrollContainerAxis === 'x') { return } - const x = event.detail.global.deltaX - requestElementUpdate(x) - } + if (canceled) { + return + } - let ticking = false + const touch = event.touches[0] + clientX = touch.clientX - function onPanEnd(e: unknown) { - const event = e as CustomEvent - if (ticking) { - setTimeout(function () { - onPanEnd(event) - }, 100) - } else { - if (!element) { - return - } + const deltaX = clientX - startX - 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) - } + if (Math.abs(deltaX) < TouchMoveThreshold) { + return + } - if (overlayElementRef.current) { - overlayElementRef.current - .animate([{ opacity: 0 }], { - duration: 5, - fill: 'forwards', - }) - .finished.then(() => { - if (overlayElementRef.current) { - overlayElementRef.current.remove() - overlayElementRef.current = null - } - }) - .catch(console.error) - } + if (closestScrollContainer) { + closestScrollContainer.style.touchAction = 'none' + } + + const x = + direction === 'right' ? Math.max(deltaX - TouchMoveThreshold, 0) : Math.min(deltaX + TouchMoveThreshold, 0) + + if (gesture === 'pan') { + updateElement(x) } } - function requestElementUpdate(x: number) { - if (!ticking) { - requestAnimationFrame(function () { - if (!element) { - return - } + const touchEndListener = () => { + if (closestScrollContainer) { + closestScrollContainer.removeEventListener('scroll', scrollListener) + closestScrollContainer.style.touchAction = '' + } - 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' + if (canceled) { + updateElement(0) + return + } - element.before(overlayElement) - overlayElementRef.current = overlayElement - } + const deltaX = clientX - startX - const newLeft = direction === 'right' ? Math.max(x, 0) : Math.min(x, 0) - element.animate([{ transform: `translate3d(${newLeft}px,0,0)` }], { duration: 0, fill: 'forwards' }) + element.style.willChange = '' - const percent = Math.min(window.innerWidth / newLeft / 10, 0.45) - overlayElementRef.current.animate([{ opacity: percent }], { - duration: 0, + if ( + (direction === 'right' && deltaX > SwipeFinishThreshold) || + (direction === 'left' && deltaX < -SwipeFinishThreshold) + ) { + onSwipeEndRef.current(element) + } else { + updateElement(0) + } + + if (underlayElementRef.current) { + underlayElementRef.current + .animate([{ opacity: 0 }], { + easing: 'cubic-bezier(.36,.66,.04,1)', + duration: 500, fill: 'forwards', }) - - ticking = false - }) - - ticking = true + .finished.then(() => { + if (underlayElementRef.current) { + underlayElementRef.current.remove() + underlayElementRef.current = null + } + }) + .catch(console.error) } } - 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) - } - } + element.addEventListener('touchstart', touchStartListener, supportsPassive ? { passive: true } : false) + element.addEventListener('touchmove', touchMoveListener, supportsPassive ? { passive: true } : false) + element.addEventListener('touchend', touchEndListener, supportsPassive ? { passive: true } : false) 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) - } - } + element.removeEventListener('touchstart', touchStartListener) + element.removeEventListener('touchmove', touchMoveListener) + element.removeEventListener('touchend', touchEndListener) } }, [direction, element, gesture, isMobileScreen, onSwipeEndRef, isEnabled]) diff --git a/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx b/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx index cab02a1cc..bafda97bc 100644 --- a/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx +++ b/packages/web/src/javascripts/Components/Preferences/Panes/General/Labs/Labs.tsx @@ -8,6 +8,7 @@ import PreferencesSegment from '../../../PreferencesComponents/PreferencesSegmen import LabsFeature from './LabsFeature' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery' +import { PrefDefaults } from '@/Constants/PrefDefaults' type ExperimentalFeatureItem = { identifier: FeatureIdentifier @@ -30,11 +31,13 @@ const LabsPane: FunctionComponent = ({ application }) => { const [experimentalFeatures, setExperimentalFeatures] = useState([]) const [isPaneGesturesEnabled, setIsPaneGesturesEnabled] = useState(() => - application.getPreference(PrefKey.PaneGesturesEnabled, false), + application.getPreference(PrefKey.PaneGesturesEnabled, PrefDefaults[PrefKey.PaneGesturesEnabled]), ) useEffect(() => { return application.addSingleEventObserver(ApplicationEvent.PreferencesChanged, async () => { - setIsPaneGesturesEnabled(application.getPreference(PrefKey.PaneGesturesEnabled, false)) + setIsPaneGesturesEnabled( + application.getPreference(PrefKey.PaneGesturesEnabled, PrefDefaults[PrefKey.PaneGesturesEnabled]), + ) }) }, [application]) diff --git a/packages/web/src/javascripts/Constants/PrefDefaults.ts b/packages/web/src/javascripts/Constants/PrefDefaults.ts index 054efedc3..22afc8246 100644 --- a/packages/web/src/javascripts/Constants/PrefDefaults.ts +++ b/packages/web/src/javascripts/Constants/PrefDefaults.ts @@ -29,4 +29,5 @@ export const PrefDefaults = { [PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A', [PrefKey.UpdateSavingStatusIndicator]: true, [PrefKey.DarkMode]: false, + [PrefKey.PaneGesturesEnabled]: true, } as const diff --git a/packages/web/src/javascripts/Controllers/Moments/PhotoRecorder.ts b/packages/web/src/javascripts/Controllers/Moments/PhotoRecorder.ts index f50e2c8b5..0e895a9fc 100644 --- a/packages/web/src/javascripts/Controllers/Moments/PhotoRecorder.ts +++ b/packages/web/src/javascripts/Controllers/Moments/PhotoRecorder.ts @@ -12,6 +12,9 @@ export class PhotoRecorder { constructor() {} public static async isSupported(): Promise { + if (!navigator.mediaDevices) { + return false + } const devices = await navigator.mediaDevices.enumerateDevices() const hasCamera = devices.some((device) => device.kind === 'videoinput') return hasCamera diff --git a/packages/web/web.webpack.config.js b/packages/web/web.webpack.config.js index 55114b91c..22df5f748 100644 --- a/packages/web/web.webpack.config.js +++ b/packages/web/web.webpack.config.js @@ -76,8 +76,7 @@ module.exports = (env) => { * Exclude all node_modules, except for those we need to run through our babel rules because * they may contain class properties and other ES6+ syntax. */ - exclude: - /node_modules\/(?!(@standardnotes\/common|@standardnotes\/domain-core|contactjs|webextension-polyfill))/, + exclude: /node_modules\/(?!(@standardnotes\/common|@standardnotes\/domain-core|webextension-polyfill))/, use: [ 'babel-loader', { diff --git a/yarn.lock b/yarn.lock index 76ac1092f..4c2f9eae7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5547,7 +5547,6 @@ __metadata: autoprefixer: ^10.4.13 babel-loader: ^9.1.0 circular-dependency-plugin: ^5.2.2 - contactjs: 2.1.5 copy-webpack-plugin: ^11.0.0 css-loader: "*" dayjs: ^1.11.7 @@ -9631,13 +9630,6 @@ __metadata: languageName: node linkType: hard -"contactjs@npm:2.1.5": - version: 2.1.5 - resolution: "contactjs@npm:2.1.5" - checksum: 5be3d66835e5a78a16abe6cdbf0c2f4a44471086e313ef8550c6747026cca9e7ff5144fdaaf6b4307cb296baf0fd394ca63758354e58099f1fde3c6756a390c3 - languageName: node - linkType: hard - "content-disposition@npm:0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4"