rebreak-monorepo/apps/rebreak-native/hooks/useSheetKeyboardLift.ts
chahinebrini 5d6c322129 wip: KeyboardAwareSheet migrations + Snake/Tetris UI + iron.png + useMe live-update
Sheets via neuer KeyboardAwareSheet-Composable (in Modal pattern, auto-grow
mit Tastatur, paddingBottom-Lift): EditMail, AddDomain, CreateRoom, ConnectMail.
GameOverScreen behält Spring-Slide-In, nutzt RN Keyboard.addListener für Lift.

- KeyboardAwareSheet.tsx — universal modal with sheet-grow + keyboard-padding
- react-native-keyboard-controller installiert + KeyboardProvider in Root
- Snake: time + ScoreProgressBar + useSnakeSounds (haptic, audio TODO)
- Tetris: title weg, Buttons zentriert, kein Pressable mit style-fn
- DPad-Buttons 60→48, more bg, no scale
- useMe: pub-sub listener pattern für app-weite avatar/nickname-Updates
- dm.tsx: resolveAvatar wrap (iron.png-Warning)
- Mail-error-humanizer + locales

Recovery-Doc-Update in docs/internal/RECOVERY_LOG_2026-05-10.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:59:25 +02:00

102 lines
3.4 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { Animated, Easing, Platform } from 'react-native';
import { useKeyboardHeight } from './useKeyboardHeight';
/**
* App-weite Composable für Sheets/Modals mit TextInput.
*
* Liefert ein **kombiniertes Animated.Value** für `transform: [{ translateY }]`,
* das gleichzeitig:
* - die Slide-In/Out-Animation des Sheets ausführt (von unten reinkommend)
* - das Sheet automatisch über die Tastatur lifted wenn TextInput fokussiert
*
* Beide Animationen laufen im **native driver** (Performance + smoother als
* height-Animationen). Kein Driver-Mix, kein Bouncing.
*
* Pattern (verifiziert auf EditMailAccountSheet + GameOverScreen):
* ```tsx
* const sheetH = SCREEN_HEIGHT * 0.5;
* const lift = useSheetKeyboardLift({ visible, offscreenY: sheetH });
*
* <Modal visible={visible}>
* <Animated.View
* style={{
* position: 'absolute',
* bottom: 0,
* height: sheetH,
* transform: [{ translateY: lift.translateY }],
* }}
* >
* {form content with TextInput}
* </Animated.View>
* </Modal>
* ```
*
* Anti-Pattern (was schief ging): `height: animatedValue` + `transform: animatedValue`
* auf demselben Animated.View → native-animated-module-Crash. Stattdessen feste
* height + nur translateY animieren.
*
* Anti-Pattern 2: `marginBottom: keyboardHeight` als JS-style + native transform
* im selben View → Bouncing weil zwei verschiedene Threads layouten.
*
* Für FlatList-basierte Sheets (PostCommentsSheet) ist das Pattern anders:
* dort wächst die Sheet-Höhe selbst weil eine variable Liste drin ist. Diese
* Composable ist für FIXED-HEIGHT-Form-Sheets gedacht.
*/
export interface SheetKeyboardLiftOptions {
/** Ob das Sheet aktuell sichtbar ist. Nur dann läuft Slide-In an. */
visible: boolean;
/** Y-Offset des Sheets im verborgenen Zustand (typischerweise = SHEET_HEIGHT). */
offscreenY: number;
/** Slide-Dauer in ms. Default 280. */
slideDurationMs?: number;
}
export function useSheetKeyboardLift({
visible,
offscreenY,
slideDurationMs = 280,
}: SheetKeyboardLiftOptions) {
const keyboardHeight = useKeyboardHeight();
const slideY = useRef(new Animated.Value(offscreenY)).current;
const keyboardLift = useRef(new Animated.Value(0)).current;
// Slide-In bei visible-Wechsel
useEffect(() => {
if (visible) {
slideY.setValue(offscreenY);
Animated.timing(slideY, {
toValue: 0,
duration: slideDurationMs,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}).start();
}
}, [visible, offscreenY, slideDurationMs, slideY]);
// Keyboard-Lift (iOS only — Android adjustResize macht das im Manifest)
useEffect(() => {
if (Platform.OS !== 'ios') return;
Animated.timing(keyboardLift, {
toValue: keyboardHeight,
duration: 220,
easing: Easing.out(Easing.cubic),
useNativeDriver: true,
}).start();
}, [keyboardHeight, keyboardLift]);
return {
/** Direkt in `transform: [{ translateY }]` einsetzen. */
translateY: Animated.subtract(slideY, keyboardLift),
/** Manuelle Slide-Out-Animation (z.B. beim Close-Tap statt nur visible=false). */
slideOut: (cb?: () => void) =>
Animated.timing(slideY, {
toValue: offscreenY,
duration: 220,
useNativeDriver: true,
}).start(() => cb?.()),
/** Live keyboard-Höhe für extra Layout-Berechnungen wenn nötig. */
keyboardHeight,
};
}