diff --git a/android/app/capacitor.build.gradle b/android/app/capacitor.build.gradle index c0a5dd43a63..b5234abf6f0 100644 --- a/android/app/capacitor.build.gradle +++ b/android/app/capacitor.build.gradle @@ -10,6 +10,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-clipboard') + implementation project(':capacitor-haptics') implementation project(':capacitor-keyboard') implementation project(':capacitor-status-bar') diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle index 1e497e9f615..be4207a2625 100644 --- a/android/capacitor.settings.gradle +++ b/android/capacitor.settings.gradle @@ -5,6 +5,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/ include ':capacitor-clipboard' project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android') +include ':capacitor-haptics' +project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android') + include ':capacitor-keyboard' project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android') diff --git a/ios/App/Podfile b/ios/App/Podfile index 644ed7a6542..3080d97ec0e 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -12,6 +12,7 @@ def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' + pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' pod 'CapacitorKeyboard', :path => '../../node_modules/@capacitor/keyboard' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' end diff --git a/package.json b/package.json index 50a02a5ea4c..098a296a288 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@capacitor/android": "^6.1.2", "@capacitor/clipboard": "^6.0.2", "@capacitor/core": "^6.2.0", + "@capacitor/haptics": "^6.0.2", "@capacitor/ios": "^6.2.0", "@capacitor/keyboard": "^6.0.2", "@capacitor/status-bar": "^6.0.1", diff --git a/src/actions/deleteThought.ts b/src/actions/deleteThought.ts index 081c4fc0ac5..a0d02b47ff3 100644 --- a/src/actions/deleteThought.ts +++ b/src/actions/deleteThought.ts @@ -1,3 +1,5 @@ +import { Capacitor } from '@capacitor/core' +import { Haptics, NotificationType } from '@capacitor/haptics' import _ from 'lodash' import Index from '../@types/IndexType' import Lexeme from '../@types/Lexeme' @@ -53,6 +55,9 @@ const deleteThought = (state: State, { local = true, pathParent, thoughtId, orph // See: Payload.local const persist = local || remote + if (Capacitor.isNativePlatform()) { + Haptics.notification({ type: NotificationType.Warning }) + } // guard against missing lexeme // while this ideally shouldn't happen, there are some concurrency issues that can cause it to happen, as well as freeThoughts, so we should print an error and just delete the Parent diff --git a/src/components/ActionButton.tsx b/src/components/ActionButton.tsx index b8ad85014b0..0e7624cee61 100644 --- a/src/components/ActionButton.tsx +++ b/src/components/ActionButton.tsx @@ -39,7 +39,7 @@ const ActionButton = ({ }), css({ lineHeight: 2, marginInline: 5, whiteSpace: 'nowrap', fontWeight: 'normal' }), )} - {...(onClick && !isDisabled ? fastClick(onClick) : null)} + {...(onClick && !isDisabled ? fastClick(onClick, true) : null)} {...restProps} > {/* TODO: Animate on loader toggle. */} diff --git a/src/components/HamburgerMenu.tsx b/src/components/HamburgerMenu.tsx index 1bd2f830eea..cc0330a8a89 100644 --- a/src/components/HamburgerMenu.tsx +++ b/src/components/HamburgerMenu.tsx @@ -96,7 +96,7 @@ const HamburgerMenu = () => { setTimeout(() => { dispatch(toggleSidebar({})) }, 10) - })} + }, true)} > diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 0642a716890..d54eed81a2f 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -55,7 +55,7 @@ const Link = React.memo(({ simplePath, label, charLimit = 32, style, cssRaw, cla setCursor({ path: simplePath }), toggleSidebar({ value: false }), ]) - })} + }, true)} onMouseDown={e => { // prevent propagation to Content component which will trigger clickOnEmptySpace e.stopPropagation() diff --git a/src/components/MultiGesture.tsx b/src/components/MultiGesture.tsx index 10b3c17c995..2a3691d4a2f 100644 --- a/src/components/MultiGesture.tsx +++ b/src/components/MultiGesture.tsx @@ -1,3 +1,5 @@ +import { Capacitor } from '@capacitor/core' +import { Haptics, ImpactStyle, NotificationType } from '@capacitor/haptics' import React, { PropsWithChildren } from 'react' import { GestureResponderEvent, PanResponder, PanResponderInstance, View } from 'react-native' import Direction from '../@types/Direction' @@ -144,6 +146,9 @@ class MultiGesture extends React.Component { // touchcancel is fired when the user switches apps by swiping from the bottom of the screen window.addEventListener('touchcancel', e => { + if (Capacitor.isNativePlatform()) { + Haptics.notification({ type: NotificationType.Warning }) + } this.props.onCancel?.({ clientStart: this.clientStart, e }) this.reset() }) @@ -162,6 +167,9 @@ class MultiGesture extends React.Component { if (this.props.shouldCancelGesture?.()) { this.props.onCancel?.({ clientStart: this.clientStart, e }) gestureStore.update('') + if (Capacitor.isNativePlatform()) { + Haptics.notification({ type: NotificationType.Warning }) + } this.abandon = true return } @@ -179,6 +187,9 @@ class MultiGesture extends React.Component { this.scrollYStart = window.scrollY if (this.props.onStart) { this.props.onStart({ clientStart: this.clientStart!, e }) + if (Capacitor.isNativePlatform()) { + Haptics.impact({ style: ImpactStyle.Light }) + } } return } @@ -203,6 +214,9 @@ class MultiGesture extends React.Component { // append the gesture to the sequence and call the onGesture handler this.sequence += g this.props.onGesture?.({ gesture: g, sequence: this.sequence, clientStart: this.clientStart!, e }) + if (Capacitor.isNativePlatform()) { + Haptics.impact({ style: ImpactStyle.Light }) + } gestureStore.update(this.sequence) } } diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index 51b20276a9e..3dba6d92b49 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -8,6 +8,7 @@ Test: - Overlay hidden on touch "leave" */ +import { Haptics } from '@capacitor/haptics' import React, { FC, useCallback, useEffect, useRef, useState } from 'react' import { shallowEqual, useDispatch, useSelector } from 'react-redux' import { css, cva, cx } from '../../styled-system/css' @@ -137,6 +138,7 @@ const Toolbar: FC = ({ customize, onSelect, selected }) => { if (scrollDifference >= 5) { deselectPressingToolbarId() + Haptics.selectionChanged() } updateArrows() diff --git a/src/components/ToolbarButton.tsx b/src/components/ToolbarButton.tsx index 3de3244049f..931c2396227 100644 --- a/src/components/ToolbarButton.tsx +++ b/src/components/ToolbarButton.tsx @@ -207,7 +207,7 @@ const ToolbarButton: FC = ({ }), )} onMouseLeave={onMouseLeave} - {...fastClick(tapUp, tapDown, undefined, touchMove)} + {...fastClick(tapUp, true, tapDown, undefined, touchMove)} > { // selected top dash diff --git a/src/hooks/useDragHold.ts b/src/hooks/useDragHold.ts index 20dc01eadc6..544997f18ab 100644 --- a/src/hooks/useDragHold.ts +++ b/src/hooks/useDragHold.ts @@ -1,3 +1,5 @@ +import { Capacitor } from '@capacitor/core' +import { Haptics } from '@capacitor/haptics' import { useCallback, useEffect, useState } from 'react' import { useDispatch } from 'react-redux' import DragThoughtZone from '../@types/DragThoughtZone' @@ -35,6 +37,9 @@ const useDragHold = ({ if (disabled) return setIsPressed(true) dispatch([dragHold({ value: true, simplePath, sourceZone })]) + if (Capacitor.isNativePlatform()) { + Haptics.selectionStart() + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [], @@ -51,7 +56,9 @@ const useDragHold = ({ if (state.dragHold) { dispatch([dragHold({ value: false }), !hasMulticursor(state) ? alert(null) : null]) - + if (Capacitor.isNativePlatform()) { + Haptics.selectionEnd() + } if (toggleMulticursorOnLongPress) { dispatch(toggleMulticursor({ path: simplePath })) } diff --git a/src/hooks/useLongPress.ts b/src/hooks/useLongPress.ts index fbe44a7ebdb..03de5c7aae3 100644 --- a/src/hooks/useLongPress.ts +++ b/src/hooks/useLongPress.ts @@ -1,3 +1,4 @@ +import { Haptics } from '@capacitor/haptics' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useDispatch } from 'react-redux' import { editingActionCreator as editing } from '../actions/editing' @@ -41,6 +42,7 @@ const useLongPress = ( clientCoords.current = { x: e.touches?.[0]?.clientX, y: e.touches?.[0]?.clientY } } onTouchStart?.() + Haptics.selectionStart() // cast Timeout to number for compatibility with clearTimeout clearTimeout(timerIdRef.current) diff --git a/src/hooks/useToolbarLongPress.ts b/src/hooks/useToolbarLongPress.ts index 168f1679daa..1a7a2a97882 100644 --- a/src/hooks/useToolbarLongPress.ts +++ b/src/hooks/useToolbarLongPress.ts @@ -1,3 +1,5 @@ +import { Capacitor } from '@capacitor/core' +import { Haptics } from '@capacitor/haptics' import { useCallback, useEffect, useMemo, useState } from 'react' import { useDispatch } from 'react-redux' import DragShortcutZone from '../@types/DragShortcutZone' @@ -29,6 +31,9 @@ const useToolbarLongPress = ({ if (disabled) return setIsPressed(true) dispatch(toolbarLongPress({ shortcut, sourceZone })) + if (Capacitor.isNativePlatform()) { + Haptics.selectionStart() + } }, [disabled, dispatch, shortcut, sourceZone]) /** Turn off isPressed and dismiss an alert when long press ends. */ diff --git a/src/util/fastClick.ts b/src/util/fastClick.ts index 930c49198d6..2c19bd751e2 100644 --- a/src/util/fastClick.ts +++ b/src/util/fastClick.ts @@ -1,3 +1,5 @@ +import { Capacitor } from '@capacitor/core' +import { Haptics, ImpactStyle } from '@capacitor/haptics' import _ from 'lodash' import { isTouch } from '../browser' @@ -13,6 +15,9 @@ const fastClick = isTouch // triggered on mouseup or touchend // cancelled if the user scroll or drags tapUp: (e: React.TouchEvent) => void, + + isHaptics: boolean = false, + // triggered on mousedown or touchstart tapDown?: (e: React.TouchEvent) => void, // triggered when tapUp is cancelled due to scrolling or dragging @@ -27,7 +32,6 @@ const fastClick = isTouch const y = e.touches[0].clientY touchStart = { x, y } } - tapDown?.(e) }, // cancel tap if touchmove exceeds threshold (e.g. with scrolling or dragging) @@ -42,6 +46,9 @@ const fastClick = isTouch } }, 16.666), onTouchEnd: (e: React.TouchEvent) => { + if (Capacitor.isNativePlatform() && isHaptics) { + Haptics.impact({ style: ImpactStyle.Light }) + } let cancel = !touchStart if (touchStart && e.changedTouches.length > 0) { @@ -61,7 +68,7 @@ const fastClick = isTouch touchStart = null }, }) - : (tapUp: (e: React.MouseEvent) => void, tapDown?: (e: React.MouseEvent) => void) => ({ + : (tapUp: (e: React.MouseEvent) => void, isHaptics: boolean = false, tapDown?: (e: React.MouseEvent) => void) => ({ onMouseUp: tapUp, ...(tapDown ? { onMouseDown: tapDown } : null), }) diff --git a/yarn.lock b/yarn.lock index 64363ef8117..1fb71b3dfbd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1928,6 +1928,15 @@ __metadata: languageName: node linkType: hard +"@capacitor/haptics@npm:^6.0.2": + version: 6.0.2 + resolution: "@capacitor/haptics@npm:6.0.2" + peerDependencies: + "@capacitor/core": ^6.0.0 + checksum: 10c0/c514d3b822541b3ac09625a6dd1647c9a3f94f005470c318da9b680d85024d1938da2aeea76d2641c513126bb836e1fef665bb6a84c7d39a90c6d16237b266d5 + languageName: node + linkType: hard + "@capacitor/ios@npm:^6.2.0": version: 6.2.0 resolution: "@capacitor/ios@npm:6.2.0" @@ -7658,6 +7667,7 @@ __metadata: "@capacitor/cli": "npm:^6.2.0" "@capacitor/clipboard": "npm:^6.0.2" "@capacitor/core": "npm:^6.2.0" + "@capacitor/haptics": "npm:^6.0.2" "@capacitor/ios": "npm:^6.2.0" "@capacitor/keyboard": "npm:^6.0.2" "@capacitor/status-bar": "npm:^6.0.1"