From df4c4d486b2fd302861ce92cb91f7ec060026b3f Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Thu, 1 Jan 2026 20:35:00 +0000 Subject: [PATCH] Increase build --- apps/mobile/REFACTORING_PLAN.md | 573 ++++++++++++++++++ apps/mobile/REFACTORING_TASKS.md | 233 +++++++ .../src/hooks/useAmbientOrbAnimation.ts | 102 ++++ 3 files changed, 908 insertions(+) create mode 100644 apps/mobile/REFACTORING_PLAN.md create mode 100644 apps/mobile/REFACTORING_TASKS.md create mode 100644 apps/mobile/src/hooks/useAmbientOrbAnimation.ts diff --git a/apps/mobile/REFACTORING_PLAN.md b/apps/mobile/REFACTORING_PLAN.md new file mode 100644 index 0000000..f7ea092 --- /dev/null +++ b/apps/mobile/REFACTORING_PLAN.md @@ -0,0 +1,573 @@ +# Mobile App Refactoring Plan + +A systematic plan to improve code maintainability, reduce duplication, and establish patterns for long-term scalability. + +--- + +## Executive Summary + +| Metric | Current | Target | +|--------|---------|--------| +| Largest file (chat) | 2,213 lines | <400 lines per file | +| Store file | 666 lines (5 stores) | ~150 lines per store | +| Animation duplication | 3+ files | 1 shared hook | +| UI component duplication | 6+ instances | Shared components | + +--- + +## Phase 1: Extract Shared Hooks (Low Risk, High Impact) + +**Goal:** Eliminate animation and logic duplication across the app. + +### 1.1 Create `useAmbientOrbAnimation` Hook + +**Files affected:** +- `app/(app)/swipe.tsx` (lines 41-85) +- `app/(app)/chat/[id].tsx` (lines 70-122) +- `src/components/SwipeCard.native.tsx` (lines 60-70) + +**New file:** `src/hooks/useAmbientOrbAnimation.ts` + +```typescript +import { useEffect } from 'react'; +import { useSharedValue, withRepeat, withSequence, withTiming, useAnimatedStyle } from 'react-native-reanimated'; + +interface AmbientOrbConfig { + color1?: string; + color2?: string; + size?: number; + opacity?: number; +} + +export const useAmbientOrbAnimation = (config?: AmbientOrbConfig) => { + const orbBreathing1 = useSharedValue(0); + const orbBreathing2 = useSharedValue(0); + const orbDrift1X = useSharedValue(0); + const orbDrift1Y = useSharedValue(0); + const orbDrift2X = useSharedValue(0); + const orbDrift2Y = useSharedValue(0); + + useEffect(() => { + // Breathing animations + orbBreathing1.value = withRepeat( + withSequence( + withTiming(1, { duration: 3000 }), + withTiming(0, { duration: 3000 }) + ), + -1, + true + ); + // ... rest of animation setup + }, []); + + const orbStyle1 = useAnimatedStyle(() => ({ + // ... transform and opacity logic + })); + + const orbStyle2 = useAnimatedStyle(() => ({ + // ... transform and opacity logic + })); + + return { orbStyle1, orbStyle2 }; +}; +``` + +### 1.2 Create `useMediaPicker` Hook + +**Extract from:** `app/(app)/chat/[id].tsx` (lines 296-380) + +**New file:** `src/hooks/useMediaPicker.ts` + +```typescript +interface MediaPickerOptions { + allowsVideo?: boolean; + maxDuration?: number; + quality?: number; + onSelect: (uri: string, type: 'image' | 'video') => void; + onError?: (error: Error) => void; +} + +export const useMediaPicker = (options: MediaPickerOptions) => { + const pickImage = async () => { /* ... */ }; + const takePhoto = async () => { /* ... */ }; + const pickVideo = async () => { /* ... */ }; + const recordVideo = async () => { /* ... */ }; + + return { pickImage, takePhoto, pickVideo, recordVideo }; +}; +``` + +### 1.3 Create `useTypingIndicator` Hook + +**Extract from:** `app/(app)/chat/[id].tsx` (lines 200-260) + +**New file:** `src/hooks/useTypingIndicator.ts` + +```typescript +export const useTypingIndicator = (matchId: string, userId: string) => { + const [isPartnerTyping, setIsPartnerTyping] = useState(false); + + const sendTypingStatus = useCallback((isTyping: boolean) => { /* ... */ }, []); + + // Subscribe to typing channel + useEffect(() => { /* ... */ }, [matchId]); + + return { isPartnerTyping, sendTypingStatus }; +}; +``` + +### 1.4 Create `useMessageSubscription` Hook + +**Extract from:** `app/(app)/chat/[id].tsx` (lines 450-550) + +**New file:** `src/hooks/useMessageSubscription.ts` + +```typescript +export const useMessageSubscription = (matchId: string, userId: string) => { + const [messages, setMessages] = useState([]); + + // Real-time subscription + // Message status updates + // Optimistic updates + + return { messages, sendMessage, markAsRead }; +}; +``` + +--- + +## Phase 2: Create Shared UI Components (Low Risk, Medium Impact) + +**Goal:** Extract repeated UI patterns into reusable components. + +### 2.1 Create `DecorativeSeparator` Component + +**Duplicate instances found in:** +- `app/(app)/swipe.tsx` (6+ instances) +- `app/(app)/chat/[id].tsx` (lines 648-652) +- `src/components/SwipeTutorial.tsx` + +**New file:** `src/components/ui/DecorativeSeparator.tsx` + +```typescript +interface DecorativeSeparatorProps { + variant?: 'diamond' | 'dot' | 'line'; + color?: string; + width?: number | string; +} + +export const DecorativeSeparator: React.FC = ({ + variant = 'diamond', + color = theme.colors.glass.border, + width = '60%', +}) => ( + + + {variant === 'diamond' && } + {variant === 'dot' && } + + +); +``` + +### 2.2 Create `LoadingOverlay` Component + +**Similar patterns in:** +- `app/(app)/chat/[id].tsx` (UploadingSkeleton) +- `src/components/Paywall.tsx` +- Various loading states + +**New file:** `src/components/ui/LoadingOverlay.tsx` + +```typescript +interface LoadingOverlayProps { + visible: boolean; + message?: string; + progress?: number; + variant?: 'spinner' | 'shimmer' | 'progress'; +} + +export const LoadingOverlay: React.FC = ({ ... }); +``` + +### 2.3 Create `ReadReceipt` Component + +**Extract from:** `app/(app)/chat/[id].tsx` (lines 1354-1365) + +**New file:** `src/components/ui/ReadReceipt.tsx` + +```typescript +type ReceiptStatus = 'sent' | 'delivered' | 'read'; + +interface ReadReceiptProps { + status: ReceiptStatus; + size?: number; +} + +export const ReadReceipt: React.FC = ({ status, size = 14 }) => { + const icon = status === 'read' ? 'checkmark-done' : status === 'delivered' ? 'checkmark-done' : 'checkmark'; + const color = status === 'read' ? theme.colors.primary : theme.colors.text.secondary; + + return ; +}; +``` + +### 2.4 Create `AmbientOrbs` Component + +**New file:** `src/components/ui/AmbientOrbs.tsx` + +```typescript +interface AmbientOrbsProps { + variant?: 'default' | 'chat' | 'premium'; +} + +export const AmbientOrbs: React.FC = ({ variant = 'default' }) => { + const { orbStyle1, orbStyle2 } = useAmbientOrbAnimation(); + + return ( + <> + + + + ); +}; +``` + +--- + +## Phase 3: Split the Zustand Store (Medium Risk, High Impact) + +**Goal:** Improve maintainability by splitting the monolithic store into domain-specific modules. + +### Current Structure (666 lines, 1 file) +``` +src/store/index.ts +├── useAuthStore +├── useMatchStore +├── usePacksStore +├── useMessageStore +└── useSubscriptionStore +``` + +### Target Structure (5 files, ~100-150 lines each) +``` +src/store/ +├── index.ts # Re-exports all stores +├── authStore.ts # Auth, user, couple, partner +├── matchStore.ts # Matches, sorting, filtering +├── packsStore.ts # Question packs, enabled packs +├── messageStore.ts # Messages, unread counts +└── subscriptionStore.ts # RevenueCat, offerings, purchases +``` + +### Implementation Steps + +1. **Create store directory structure** +2. **Move `useAuthStore` to `authStore.ts`** + - Keep auth refresh logic isolated + - Export type definitions +3. **Move `useMatchStore` to `matchStore.ts`** +4. **Move `usePacksStore` to `packsStore.ts`** +5. **Move `useMessageStore` to `messageStore.ts`** +6. **Move `useSubscriptionStore` to `subscriptionStore.ts`** +7. **Update `index.ts` to re-export all stores** + +```typescript +// src/store/index.ts +export { useAuthStore } from './authStore'; +export { useMatchStore } from './matchStore'; +export { usePacksStore } from './packsStore'; +export { useMessageStore } from './messageStore'; +export { useSubscriptionStore } from './subscriptionStore'; +``` + +**Import changes:** None required - all imports from `@/store` will continue to work. + +--- + +## Phase 4: Decompose Chat Screen (Medium Risk, Highest Impact) + +**Goal:** Break the 2,213-line chat screen into maintainable, testable components. + +### Current Structure +``` +app/(app)/chat/[id].tsx (2,213 lines) +├── Animations (lines 70-122) +├── State management (lines 130-200) +├── Typing indicator logic (lines 200-260) +├── Media handling (lines 296-450) +├── Message subscription (lines 450-550) +├── UI Components (inline) +│ ├── MessageContent +│ ├── ChatVideoPlayer +│ ├── MessageMeta +│ ├── InputBar +│ └── UploadingSkeleton +└── Main render (lines 600-2213) +``` + +### Target Structure +``` +src/features/chat/ +├── index.ts # Public exports +├── ChatScreen.tsx # Main screen (orchestration only, <400 lines) +├── components/ +│ ├── ChatHeader.tsx # Header with match info +│ ├── ChatMessages.tsx # Message list + scroll logic +│ ├── MessageBubble.tsx # Single message rendering +│ ├── MessageContent.tsx # Text/image/video content +│ ├── ChatVideoPlayer.tsx # Video player with controls +│ ├── InputBar.tsx # Text input + media buttons +│ ├── MediaMenu.tsx # Expandable media picker +│ ├── TypingIndicator.tsx # Animated typing dots +│ └── UploadProgress.tsx # Upload skeleton/progress +├── hooks/ +│ ├── useChatMessages.ts # Message fetching & subscription +│ ├── useTypingIndicator.ts # Typing broadcast channel +│ ├── useMediaUpload.ts # Image/video compression & upload +│ └── useVideoCache.ts # Video download & caching +└── types.ts # Chat-specific types +``` + +### Implementation Steps + +#### Step 1: Create Feature Directory +```bash +mkdir -p src/features/chat/components +mkdir -p src/features/chat/hooks +``` + +#### Step 2: Extract Components (in order of dependency) + +**2a. Extract `MessageContent.tsx`** +- Lines 680-800 from current file +- Handles text, image, video rendering +- Props: `{ message, onImagePress, onVideoPress }` + +**2b. Extract `ChatVideoPlayer.tsx`** +- Lines 740-800 from current file +- Custom video player with controls +- Props: `{ uri, onClose, visible }` + +**2c. Extract `MessageBubble.tsx`** +- Lines 640-900 from current file +- Single message with content + meta +- Props: `{ message, isOwn, onMediaPress }` + +**2d. Extract `TypingIndicator.tsx`** +- Lines 870-910 from current file +- Animated typing dots +- Props: `{ visible }` + +**2e. Extract `InputBar.tsx`** +- Lines 1380-1510 from current file +- Text input + send button + media toggle +- Props: `{ onSend, onMediaSelect, disabled }` + +**2f. Extract `MediaMenu.tsx`** +- Lines 1510-1600 from current file +- Expandable media picker buttons +- Props: `{ visible, onSelect, onClose }` + +**2g. Extract `ChatHeader.tsx`** +- Lines 600-640 from current file +- Match card + question display +- Props: `{ match, onBack }` + +**2h. Extract `ChatMessages.tsx`** +- Lines 1600-1800 from current file +- FlatList with message rendering +- Props: `{ messages, userId, onMediaPress }` + +#### Step 3: Extract Hooks + +**3a. Create `useChatMessages.ts`** +```typescript +export const useChatMessages = (matchId: string) => { + // Fetch initial messages + // Subscribe to new messages + // Handle optimistic updates + // Mark messages as read + return { messages, sendMessage, loading, error }; +}; +``` + +**3b. Create `useMediaUpload.ts`** +```typescript +export const useMediaUpload = (matchId: string) => { + // Compress image/video + // Upload to storage + // Track progress + return { upload, progress, uploading }; +}; +``` + +#### Step 4: Create Main Screen +```typescript +// src/features/chat/ChatScreen.tsx (~300-400 lines) +export const ChatScreen: React.FC = () => { + const { id } = useLocalSearchParams(); + const { messages, sendMessage } = useChatMessages(id); + const { isPartnerTyping, sendTypingStatus } = useTypingIndicator(id); + const { upload, progress } = useMediaUpload(id); + + return ( + + + + + {isPartnerTyping && } + + + ); +}; +``` + +#### Step 5: Update Route File +```typescript +// app/(app)/chat/[id].tsx +export { ChatScreen as default } from '@/features/chat'; +``` + +--- + +## Phase 5: Decompose Profile Screen (Medium Risk, High Impact) + +**Goal:** Break the 1,703-line profile screen into manageable sections. + +### Target Structure +``` +src/features/profile/ +├── index.ts +├── ProfileScreen.tsx # Main orchestration (<400 lines) +├── components/ +│ ├── ProfileHeader.tsx # Avatar, name, edit +│ ├── CoupleStatus.tsx # Partner info, invite code +│ ├── SettingsSection.tsx # Generic settings group +│ ├── NotificationSettings.tsx +│ ├── PrivacySettings.tsx +│ ├── AppearanceSettings.tsx +│ └── DangerZone.tsx # Delete account, leave couple +└── hooks/ + ├── useProfileSettings.ts + └── useCoupleManagement.ts +``` + +--- + +## Phase 6: Improve Component Organization (Low Risk) + +**Goal:** Establish clear component categories and naming conventions. + +### Current Structure +``` +src/components/ +├── ui/ # Primitive UI components +├── [Feature].tsx # Mixed feature components +└── [Tutorial].tsx # Tutorial components +``` + +### Target Structure +``` +src/components/ +├── ui/ # Primitive UI (buttons, cards, inputs) +│ ├── GlassButton.tsx +│ ├── GlassCard.tsx +│ ├── GlassInput.tsx +│ ├── DecorativeSeparator.tsx +│ ├── LoadingOverlay.tsx +│ ├── ReadReceipt.tsx +│ ├── AmbientOrbs.tsx +│ └── ShimmerEffect.tsx +├── feedback/ # User feedback components +│ ├── FeedbackModal.tsx +│ └── QuestionFeedbackModal.tsx +├── tutorials/ # Onboarding tutorials +│ ├── SwipeTutorial.tsx +│ └── MatchesTutorial.tsx +├── paywall/ # Subscription components +│ ├── Paywall.tsx +│ └── PremiumBadge.tsx +└── swipe/ # Swipe card variants + ├── SwipeCard.native.tsx + ├── SwipeCard.web.tsx + └── SwipeCardPremium.tsx +``` + +--- + +## Phase 7: Add Testing Infrastructure (Long-term) + +**Goal:** Enable confident refactoring with test coverage. + +### Recommended Test Structure +``` +src/ +├── __tests__/ +│ ├── components/ +│ ├── hooks/ +│ ├── stores/ +│ └── utils/ +└── features/ + └── chat/ + └── __tests__/ + ├── ChatMessages.test.tsx + └── useChatMessages.test.ts +``` + +### Testing Priorities +1. **Hooks** - Pure logic, easy to test +2. **Store actions** - Critical business logic +3. **Utility functions** - Edge cases +4. **Components** - Snapshot + interaction tests + +--- + +## Implementation Order + +| Phase | Risk | Impact | Effort | Priority | +|-------|------|--------|--------|----------| +| 1. Shared Hooks | Low | High | 2-3 days | 1 | +| 2. Shared UI Components | Low | Medium | 1-2 days | 2 | +| 3. Split Store | Medium | High | 1 day | 3 | +| 4. Decompose Chat | Medium | Highest | 3-5 days | 4 | +| 5. Decompose Profile | Medium | High | 2-3 days | 5 | +| 6. Component Organization | Low | Low | 1 day | 6 | +| 7. Testing | Low | High | Ongoing | 7 | + +--- + +## Migration Strategy + +### For Each Phase: +1. **Create new files** without modifying existing code +2. **Import and use** new components/hooks in the target file +3. **Verify functionality** works identically +4. **Remove old code** from the original file +5. **Update imports** across the codebase if needed + +### Rollback Strategy: +- Each phase can be rolled back independently +- Keep original files until phase is complete +- Use feature flags for large changes if needed + +--- + +## Success Metrics + +| Metric | Current | Target | +|--------|---------|--------| +| Max lines per file | 2,213 | <500 | +| Duplicate animation code | 3 files | 1 hook | +| Store files | 1 (666 lines) | 5 (~130 lines each) | +| Test coverage | 0% | >50% for hooks/stores | +| Component reuse | Low | High (shared UI components) | + +--- + +## Notes + +- **Do not change** the tab bar background styling (see CLAUDE.md) +- **Follow** DESIGN.md for all UI changes +- **Use** theme imports, never hardcode colors +- **Preserve** all existing functionality during refactoring diff --git a/apps/mobile/REFACTORING_TASKS.md b/apps/mobile/REFACTORING_TASKS.md new file mode 100644 index 0000000..d8a9f5b --- /dev/null +++ b/apps/mobile/REFACTORING_TASKS.md @@ -0,0 +1,233 @@ +# Mobile App Refactoring Tasks + +Track progress by checking off completed items. + +--- + +## Phase 1: Extract Shared Hooks + +### 1.1 Ambient Orb Animation Hook +- [ ] Create `src/hooks/useAmbientOrbAnimation.ts` +- [ ] Extract animation logic from `app/(app)/swipe.tsx` +- [ ] Update `app/(app)/swipe.tsx` to use hook +- [ ] Update `app/(app)/chat/[id].tsx` to use hook +- [ ] Update `src/components/SwipeCard.native.tsx` to use hook +- [ ] Remove duplicate animation code from all files + +### 1.2 Media Picker Hook +- [ ] Create `src/hooks/useMediaPicker.ts` +- [ ] Extract image picker logic from chat screen +- [ ] Extract video picker logic from chat screen +- [ ] Extract camera logic from chat screen +- [ ] Update chat screen to use hook + +### 1.3 Typing Indicator Hook +- [ ] Create `src/hooks/useTypingIndicator.ts` +- [ ] Extract broadcast channel logic from chat +- [ ] Extract typing state management +- [ ] Update chat screen to use hook + +### 1.4 Message Subscription Hook +- [ ] Create `src/hooks/useMessageSubscription.ts` +- [ ] Extract real-time subscription logic +- [ ] Extract message status update logic +- [ ] Extract optimistic update logic +- [ ] Update chat screen to use hook + +--- + +## Phase 2: Shared UI Components + +### 2.1 Decorative Separator +- [ ] Create `src/components/ui/DecorativeSeparator.tsx` +- [ ] Support diamond, dot, and line variants +- [ ] Update `app/(app)/swipe.tsx` (6+ instances) +- [ ] Update `app/(app)/chat/[id].tsx` +- [ ] Update `src/components/SwipeTutorial.tsx` +- [ ] Remove duplicate separator styles + +### 2.2 Loading Overlay +- [ ] Create `src/components/ui/LoadingOverlay.tsx` +- [ ] Support spinner, shimmer, and progress variants +- [ ] Extract UploadingSkeleton from chat screen +- [ ] Update usage across app + +### 2.3 Read Receipt +- [ ] Create `src/components/ui/ReadReceipt.tsx` +- [ ] Support sent, delivered, read states +- [ ] Update chat screen MessageMeta +- [ ] Remove inline receipt logic + +### 2.4 Ambient Orbs Component +- [ ] Create `src/components/ui/AmbientOrbs.tsx` +- [ ] Use `useAmbientOrbAnimation` hook +- [ ] Support default, chat, premium variants +- [ ] Update swipe screen to use component +- [ ] Update chat screen to use component + +--- + +## Phase 3: Split Zustand Store + +### 3.1 Create Store Structure +- [ ] Create `src/store/` directory structure +- [ ] Create `src/store/authStore.ts` +- [ ] Create `src/store/matchStore.ts` +- [ ] Create `src/store/packsStore.ts` +- [ ] Create `src/store/messageStore.ts` +- [ ] Create `src/store/subscriptionStore.ts` + +### 3.2 Migrate Stores +- [ ] Move `useAuthStore` to `authStore.ts` +- [ ] Move `useMatchStore` to `matchStore.ts` +- [ ] Move `usePacksStore` to `packsStore.ts` +- [ ] Move `useMessageStore` to `messageStore.ts` +- [ ] Move `useSubscriptionStore` to `subscriptionStore.ts` + +### 3.3 Update Exports +- [ ] Update `src/store/index.ts` to re-export all stores +- [ ] Verify all imports still work +- [ ] Delete old monolithic code from index.ts + +--- + +## Phase 4: Decompose Chat Screen + +### 4.1 Create Feature Structure +- [ ] Create `src/features/chat/` directory +- [ ] Create `src/features/chat/components/` directory +- [ ] Create `src/features/chat/hooks/` directory +- [ ] Create `src/features/chat/types.ts` + +### 4.2 Extract Components +- [ ] Extract `ChatHeader.tsx` +- [ ] Extract `MessageContent.tsx` +- [ ] Extract `ChatVideoPlayer.tsx` +- [ ] Extract `MessageBubble.tsx` +- [ ] Extract `TypingIndicator.tsx` +- [ ] Extract `InputBar.tsx` +- [ ] Extract `MediaMenu.tsx` +- [ ] Extract `ChatMessages.tsx` +- [ ] Extract `UploadProgress.tsx` + +### 4.3 Extract Hooks +- [ ] Create `useChatMessages.ts` +- [ ] Create `useMediaUpload.ts` +- [ ] Create `useVideoCache.ts` + +### 4.4 Create Main Screen +- [ ] Create `src/features/chat/ChatScreen.tsx` +- [ ] Wire up all components and hooks +- [ ] Create `src/features/chat/index.ts` exports +- [ ] Update `app/(app)/chat/[id].tsx` to use feature + +### 4.5 Cleanup +- [ ] Remove old code from route file +- [ ] Verify all functionality works +- [ ] Test real-time messaging +- [ ] Test media upload/download +- [ ] Test video playback + +--- + +## Phase 5: Decompose Profile Screen + +### 5.1 Create Feature Structure +- [ ] Create `src/features/profile/` directory +- [ ] Create `src/features/profile/components/` directory +- [ ] Create `src/features/profile/hooks/` directory + +### 5.2 Extract Components +- [ ] Extract `ProfileHeader.tsx` +- [ ] Extract `CoupleStatus.tsx` +- [ ] Extract `SettingsSection.tsx` +- [ ] Extract `NotificationSettings.tsx` +- [ ] Extract `PrivacySettings.tsx` +- [ ] Extract `AppearanceSettings.tsx` +- [ ] Extract `DangerZone.tsx` + +### 5.3 Extract Hooks +- [ ] Create `useProfileSettings.ts` +- [ ] Create `useCoupleManagement.ts` + +### 5.4 Create Main Screen +- [ ] Create `src/features/profile/ProfileScreen.tsx` +- [ ] Create `src/features/profile/index.ts` exports +- [ ] Update `app/(app)/profile.tsx` to use feature + +--- + +## Phase 6: Component Organization + +### 6.1 Reorganize Components +- [ ] Create `src/components/feedback/` directory +- [ ] Move `FeedbackModal.tsx` to feedback/ +- [ ] Move `QuestionFeedbackModal.tsx` to feedback/ +- [ ] Create `src/components/tutorials/` directory +- [ ] Move `SwipeTutorial.tsx` to tutorials/ +- [ ] Move `MatchesTutorial.tsx` to tutorials/ +- [ ] Create `src/components/paywall/` directory +- [ ] Move `Paywall.tsx` to paywall/ +- [ ] Create `src/components/swipe/` directory +- [ ] Move `SwipeCard.native.tsx` to swipe/ +- [ ] Move `SwipeCard.web.tsx` to swipe/ +- [ ] Move `SwipeCardPremium.tsx` to swipe/ + +### 6.2 Update Imports +- [ ] Update all imports for moved components +- [ ] Create index.ts files for each directory +- [ ] Verify no broken imports + +--- + +## Phase 7: Testing Infrastructure + +### 7.1 Setup +- [ ] Install testing dependencies (jest, testing-library) +- [ ] Configure jest for React Native +- [ ] Create test utilities and mocks + +### 7.2 Hook Tests +- [ ] Test `useAmbientOrbAnimation` +- [ ] Test `useMediaPicker` +- [ ] Test `useTypingIndicator` +- [ ] Test `useMessageSubscription` +- [ ] Test `useChatMessages` +- [ ] Test `useMediaUpload` + +### 7.3 Store Tests +- [ ] Test `authStore` actions +- [ ] Test `matchStore` actions +- [ ] Test `packsStore` actions +- [ ] Test `messageStore` actions +- [ ] Test `subscriptionStore` actions + +### 7.4 Component Tests +- [ ] Test `DecorativeSeparator` variants +- [ ] Test `ReadReceipt` states +- [ ] Test `InputBar` interactions +- [ ] Test `MessageBubble` rendering + +--- + +## Progress Summary + +| Phase | Status | Completion | +|-------|--------|------------| +| 1. Shared Hooks | Not Started | 0% | +| 2. Shared UI Components | Not Started | 0% | +| 3. Split Store | Not Started | 0% | +| 4. Decompose Chat | Not Started | 0% | +| 5. Decompose Profile | Not Started | 0% | +| 6. Component Organization | Not Started | 0% | +| 7. Testing | Not Started | 0% | + +--- + +## Notes + +- Each phase can be completed independently +- Test after each component extraction +- Keep original files until phase is verified +- Follow DESIGN.md for any UI changes +- Do not modify tab bar background styling diff --git a/apps/mobile/src/hooks/useAmbientOrbAnimation.ts b/apps/mobile/src/hooks/useAmbientOrbAnimation.ts new file mode 100644 index 0000000..4e5a889 --- /dev/null +++ b/apps/mobile/src/hooks/useAmbientOrbAnimation.ts @@ -0,0 +1,102 @@ +import { useEffect } from 'react'; +import { + useSharedValue, + useAnimatedStyle, + withRepeat, + withSequence, + withTiming, + interpolate, + Easing, +} from 'react-native-reanimated'; + +export interface AmbientOrbConfig { + /** Opacity range for primary orb [min, max]. Default: [0.25, 0.5] */ + opacityRange1?: [number, number]; + /** Opacity range for secondary orb [min, max]. Default: [0.2, 0.4] */ + opacityRange2?: [number, number]; + /** Vertical drift distance in pixels. Default: 20 */ + driftDistance?: number; + /** Scale range [min, max]. Default: [1, 1.1] */ + scaleRange?: [number, number]; +} + +const DEFAULT_CONFIG: Required = { + opacityRange1: [0.25, 0.5], + opacityRange2: [0.2, 0.4], + driftDistance: 20, + scaleRange: [1, 1.1], +}; + +/** + * Hook for ambient orb breathing animations. + * Used across swipe, chat, and other premium screens for visual atmosphere. + * + * @param config - Optional configuration for animation parameters + * @returns Animated styles for two orbs + */ +export const useAmbientOrbAnimation = (config?: AmbientOrbConfig) => { + const settings = { ...DEFAULT_CONFIG, ...config }; + + const orbBreathing1 = useSharedValue(0); + const orbBreathing2 = useSharedValue(0); + const orbDrift = useSharedValue(0); + + useEffect(() => { + // Primary orb breathing - 6 second cycle + orbBreathing1.value = withRepeat( + withSequence( + withTiming(1, { duration: 3000, easing: Easing.inOut(Easing.sin) }), + withTiming(0, { duration: 3000, easing: Easing.inOut(Easing.sin) }) + ), + -1, + true + ); + + // Secondary orb breathing - offset timing for variation (8 second cycle) + orbBreathing2.value = withRepeat( + withSequence( + withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.sin) }), + withTiming(1, { duration: 4000, easing: Easing.inOut(Easing.sin) }), + withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.sin) }) + ), + -1, + true + ); + + // Subtle vertical drift - 8 second cycle + orbDrift.value = withRepeat( + withSequence( + withTiming(1, { duration: 4000, easing: Easing.inOut(Easing.sin) }), + withTiming(0, { duration: 4000, easing: Easing.inOut(Easing.sin) }) + ), + -1, + true + ); + }, []); + + const orbStyle1 = useAnimatedStyle(() => ({ + opacity: interpolate( + orbBreathing1.value, + [0, 1], + settings.opacityRange1 + ), + transform: [ + { translateY: interpolate(orbDrift.value, [0, 1], [0, -settings.driftDistance]) }, + { scale: interpolate(orbBreathing1.value, [0, 1], settings.scaleRange) }, + ], + })); + + const orbStyle2 = useAnimatedStyle(() => ({ + opacity: interpolate( + orbBreathing2.value, + [0, 1], + settings.opacityRange2 + ), + transform: [ + { translateY: interpolate(orbDrift.value, [0, 1], [settings.driftDistance, 0]) }, + { scale: interpolate(orbBreathing2.value, [0, 1], settings.scaleRange) }, + ], + })); + + return { orbStyle1, orbStyle2 }; +};