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:
2026-09-20 15:15:04 -04:00
co-authored by Claude Opus 5
parent 0c45bde832
commit 492b57c60d
8 changed files with 102 additions and 2 deletions
+4 -1
View File
@@ -91,6 +91,7 @@ import {
reactionOrEditEvent,
} from '../../utils/room';
import { getLastEditDiff } from '../../utils/editDiff';
import { tick } from '../../utils/haptics';
import { GroupCandidate, GroupPlan, planMediaGroups } from '../../utils/mediaGroups';
import { MediaGroupGrid, RegroupChip } from './message/MediaGroupGrid';
import { Lightbox, toLightboxItems } from './MediaGallery';
@@ -478,6 +479,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
// Read positions are computed once in Room.tsx and provided via ReadPositionsContext
// so both RoomTimeline and ThreadTimeline consume the same value (Gitea #38).
const [hideMembershipEvents] = useSetting(settingsAtom, 'hideMembershipEvents');
const [hapticFeedback] = useSetting(settingsAtom, 'hapticFeedback');
// [Gitea #137] Re-render after "Show separately" (the Set itself is module-level).
const [, setSeparatedTick] = useState(0);
const [hideNickAvatarEvents] = useSetting(settingsAtom, 'hideNickAvatarEvents');
@@ -1157,13 +1159,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
const rShortcode =
shortcode ||
(reactions.find(eventWithShortcode)?.getContent().shortcode as string | undefined);
tick('reaction', hapticFeedback);
mx.sendEvent(
room.roomId,
MessageEvent.Reaction as any,
getReactionContent(targetEventId, key, rShortcode),
);
},
[mx, room],
[mx, room, hapticFeedback],
);
const handleEdit = useCallback(
(editEvtId?: string) => {