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"