From fcde6789ff4d896e2170b36faa9e9fba7b4a0e57 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Mon, 18 Aug 2025 10:49:27 +0900 Subject: [PATCH] feat(frontend): introduce haptic feedback as experimental feature #16410 --- packages/frontend/package.json | 1 + packages/frontend/src/components/MkEmojiPicker.vue | 3 +++ packages/frontend/src/components/MkFollowButton.vue | 3 +++ .../frontend/src/components/MkPullToRefresh.vue | 3 +++ packages/frontend/src/components/MkSwitch.vue | 3 +++ packages/frontend/src/pages/settings/other.vue | 4 ++++ packages/frontend/src/preferences/def.ts | 3 +++ packages/frontend/src/utility/haptic.ts | 13 +++++++++++++ pnpm-lock.yaml | 8 ++++++++ 9 files changed, 41 insertions(+) create mode 100644 packages/frontend/src/utility/haptic.ts diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 903a0c09be..fe2f47ad1f 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -52,6 +52,7 @@ "icons-subsetter": "workspace:*", "idb-keyval": "6.2.2", "insert-text-at-cursor": "0.3.0", + "ios-haptics": "0.1.0", "is-file-animated": "1.0.2", "json5": "2.2.3", "magic-string": "0.30.17", diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 0248d75f75..df05bcc94c 100644 --- a/packages/frontend/src/components/MkEmojiPicker.vue +++ b/packages/frontend/src/components/MkEmojiPicker.vue @@ -141,6 +141,7 @@ import { $i } from '@/i.js'; import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js'; import { prefer } from '@/preferences.js'; import { useRouter } from '@/router.js'; +import { haptic } from '@/utility/haptic.js'; const router = useRouter(); @@ -431,6 +432,8 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, const key = getKey(emoji); emit('chosen', key); + haptic(); + // 最近使った絵文字更新 if (!pinned.value?.includes(key)) { let recents = store.s.recentlyUsedEmojis; diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index b65f610986..c7361a19c6 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -46,6 +46,7 @@ import { claimAchievement } from '@/utility/achievements.js'; import { pleaseLogin } from '@/utility/please-login.js'; import { $i } from '@/i.js'; import { prefer } from '@/preferences.js'; +import { haptic } from '@/utility/haptic.js'; const props = withDefaults(defineProps<{ user: Misskey.entities.UserDetailed, @@ -84,6 +85,8 @@ async function onClick() { wait.value = true; + haptic(); + try { if (isFollowing.value) { const { canceled } = await os.confirm({ diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue index c792ff3488..89aca5d29b 100644 --- a/packages/frontend/src/components/MkPullToRefresh.vue +++ b/packages/frontend/src/components/MkPullToRefresh.vue @@ -27,6 +27,7 @@ import { onMounted, onUnmounted, ref, useTemplateRef } from 'vue'; import { getScrollContainer } from '@@/js/scroll.js'; import { i18n } from '@/i18n.js'; import { isHorizontalSwipeSwiping } from '@/utility/touch.js'; +import { haptic } from '@/utility/haptic.js'; const SCROLL_STOP = 10; const MAX_PULL_DISTANCE = Infinity; @@ -203,6 +204,8 @@ function moving(event: MouseEvent | TouchEvent) { pullDistance.value = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE); isPulledEnough.value = pullDistance.value >= FIRE_THRESHOLD; + + if (isPulledEnough.value) haptic(); } /** diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue index 92359b773a..9a2bea3616 100644 --- a/packages/frontend/src/components/MkSwitch.vue +++ b/packages/frontend/src/components/MkSwitch.vue @@ -30,6 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { toRefs } from 'vue'; import type { Ref } from 'vue'; import XButton from '@/components/MkSwitch.button.vue'; +import { haptic } from '@/utility/haptic.js'; const props = defineProps<{ modelValue: boolean | Ref; @@ -48,6 +49,8 @@ const toggle = () => { if (props.disabled) return; emit('update:modelValue', !checked.value); emit('change', !checked.value); + + haptic(); }; diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 30ab2ce11e..730cce183a 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -99,6 +99,9 @@ SPDX-License-Identifier: AGPL-3.0-only + + + @@ -173,6 +176,7 @@ const skipNoteRender = prefer.model('skipNoteRender'); const devMode = prefer.model('devMode'); const stackingRouterView = prefer.model('experimental.stackingRouterView'); const enableFolderPageView = prefer.model('experimental.enableFolderPageView'); +const enableHapticFeedback = prefer.model('experimental.enableHapticFeedback'); watch(skipNoteRender, () => { suggestReload(); diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts index f6370c8c78..7b045687d6 100644 --- a/packages/frontend/src/preferences/def.ts +++ b/packages/frontend/src/preferences/def.ts @@ -498,4 +498,7 @@ export const PREF_DEF = definePreferences({ 'experimental.enableFolderPageView': { default: false, }, + 'experimental.enableHapticFeedback': { + default: false, + }, }); diff --git a/packages/frontend/src/utility/haptic.ts b/packages/frontend/src/utility/haptic.ts new file mode 100644 index 0000000000..6f4706d202 --- /dev/null +++ b/packages/frontend/src/utility/haptic.ts @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { haptic as _haptic } from 'ios-haptics'; +import { prefer } from '@/preferences.js'; + +export function haptic() { + if (prefer.s['experimental.enableHapticFeedback']) { + _haptic(); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0516ed457c..c54d7aa264 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -811,6 +811,9 @@ importers: insert-text-at-cursor: specifier: 0.3.0 version: 0.3.0 + ios-haptics: + specifier: 0.1.0 + version: 0.1.0 is-file-animated: specifier: 1.0.2 version: 1.0.2 @@ -7179,6 +7182,9 @@ packages: resolution: {integrity: sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==} engines: {node: '>=12.22.0'} + ios-haptics@0.1.0: + resolution: {integrity: sha512-Fk0RApBYJeZNZ9pW3Wx3WcunhdLlpEnVNy/BOn85tx39eZDOHLGhXEb7medoIURGBUjXatOZf5Ozy0+OG466YA==} + ip-address@9.0.5: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} @@ -18221,6 +18227,8 @@ snapshots: transitivePeerDependencies: - supports-color + ios-haptics@0.1.0: {} + ip-address@9.0.5: dependencies: jsbn: 1.1.0