mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Increase build
This commit is contained in:
1 parent
5bbc324290
commit
df4c4d486b
3 files changed
+908
No files matched your search
@@ -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<Message[]>([]);
|
||||||
|
|
||||||
|
// 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<DecorativeSeparatorProps> = ({
|
||||||
|
variant = 'diamond',
|
||||||
|
color = theme.colors.glass.border,
|
||||||
|
width = '60%',
|
||||||
|
}) => (
|
||||||
|
<View style={[styles.container, { width }]}>
|
||||||
|
<View style={[styles.line, { backgroundColor: color }]} />
|
||||||
|
{variant === 'diamond' && <View style={[styles.diamond, { backgroundColor: color }]} />}
|
||||||
|
{variant === 'dot' && <View style={[styles.dot, { backgroundColor: color }]} />}
|
||||||
|
<View style={[styles.line, { backgroundColor: color }]} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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<LoadingOverlayProps> = ({ ... });
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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<ReadReceiptProps> = ({ 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 <Ionicons name={icon} size={size} color={color} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Create `AmbientOrbs` Component
|
||||||
|
|
||||||
|
**New file:** `src/components/ui/AmbientOrbs.tsx`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface AmbientOrbsProps {
|
||||||
|
variant?: 'default' | 'chat' | 'premium';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AmbientOrbs: React.FC<AmbientOrbsProps> = ({ variant = 'default' }) => {
|
||||||
|
const { orbStyle1, orbStyle2 } = useAmbientOrbAnimation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Animated.View style={[styles.orb1, orbStyle1]} />
|
||||||
|
<Animated.View style={[styles.orb2, orbStyle2]} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<AmbientOrbs variant="chat" />
|
||||||
|
<ChatHeader match={match} onBack={router.back} />
|
||||||
|
<ChatMessages messages={messages} userId={userId} />
|
||||||
|
{isPartnerTyping && <TypingIndicator />}
|
||||||
|
<InputBar onSend={sendMessage} onMediaSelect={upload} />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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
|
||||||
@@ -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
|
||||||
@@ -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<AmbientOrbConfig> = {
|
||||||
|
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 };
|
||||||
|
};
|
||||||
Reference in new issue
Block a user