feat(mobile): haptic tick on PTT press/release and on reactions (#125)
utils/haptics.ts: tick('ptt-on' | 'ptt-off' | 'reaction') → 10/10/8 ms
navigator.vibrate, a no-op without the API (iOS), when the system prefers
reduced motion, or when the new Settings → Calls "Haptic Feedback" switch
(default on, only rendered where the API exists) is off. PTT is observed
once through pttActiveAtom so the keyboard, global-hotkey and on-screen
paths all tick; reactions tick where the reaction event is sent in the
room and thread timelines (quick bar, hover bar, sheet and emoji board
all funnel there).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { hapticsSupported, tick } from './haptics';
|
||||
|
||||
const g = globalThis as { navigator?: unknown; window?: unknown };
|
||||
|
||||
test('tick is a no-op without the API, with the setting off, or under reduced motion', () => {
|
||||
g.navigator = {};
|
||||
assert.equal(hapticsSupported(), false);
|
||||
assert.equal(tick('ptt-on'), false);
|
||||
|
||||
const calls: number[] = [];
|
||||
g.navigator = { vibrate: (ms: number) => calls.push(ms) === 1 };
|
||||
g.window = { matchMedia: () => ({ matches: true }) };
|
||||
assert.equal(tick('ptt-on'), false);
|
||||
assert.deepEqual(calls, []);
|
||||
|
||||
g.window = { matchMedia: () => ({ matches: false }) };
|
||||
assert.equal(tick('ptt-on', false), false);
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('tick vibrates per kind when allowed', () => {
|
||||
const calls: number[] = [];
|
||||
g.navigator = { vibrate: (ms: number) => calls.push(ms) > 0 };
|
||||
g.window = { matchMedia: () => ({ matches: false }) };
|
||||
assert.equal(tick('ptt-on'), true);
|
||||
assert.equal(tick('ptt-off'), true);
|
||||
assert.equal(tick('reaction'), true);
|
||||
assert.deepEqual(calls, [10, 10, 8]);
|
||||
delete g.navigator;
|
||||
delete g.window;
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* [Gitea #125] Tiny vibration ticks for touch feedback. Android Chrome/PWA
|
||||
* only — iOS has no web vibration API — and nothing at all when the user
|
||||
* prefers reduced motion. Every call is a cheap no-op elsewhere.
|
||||
*/
|
||||
export type HapticKind = 'ptt-on' | 'ptt-off' | 'reaction';
|
||||
|
||||
const PATTERN: Record<HapticKind, number> = { 'ptt-on': 10, 'ptt-off': 10, reaction: 8 };
|
||||
|
||||
export const hapticsSupported = (): boolean =>
|
||||
typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function';
|
||||
|
||||
const reducedMotion = (): boolean =>
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.matchMedia === 'function' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
/** Fire a tick; returns whether one was requested. */
|
||||
export function tick(kind: HapticKind, enabled = true): boolean {
|
||||
if (!enabled || !hapticsSupported() || reducedMotion()) return false;
|
||||
try {
|
||||
return navigator.vibrate(PATTERN[kind]) === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user