Files
Sauci/docs/live-draw-feature-plan.md
jjbrunton 1daf386765 Add question types, nudge feature, and share modal
- Add new question card components (photo, audio, text, who-likely)
- Add share preview modal for matches
- Add nudge notification system with rate limiting
- Add session-based notification improvements
- Add match confetti celebration animation
- Update onboarding flow with paywall tracking
- Remove deprecated swipe card and tutorial components
- Update GlassCard/GlassInput to use solid backgrounds
- Fix routing issues after packs screen removal
2026-01-25 19:17:39 +00:00

203 lines
7.8 KiB
Markdown

# Live Draw Feature - Implementation Plan
## Overview
Add a real-time collaborative drawing canvas where both partners can draw together and see each other's strokes live. Supports async drawing - one partner can draw and the other sees strokes when they open it later. Entry point from the chat media menu.
## Architecture Decisions
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Canvas library | `@shopify/react-native-skia` | High-performance Skia engine, great gesture integration, cross-platform |
| Real-time sync | Supabase Broadcast + DB persistence | Broadcast for live updates, DB for async support |
| State persistence | `live_draw_sessions` table | Store current canvas state per match for async viewing |
| Entry point | Chat media menu | Contextual to match, natural extension of chat |
| Storage | Existing `chat-media` bucket | Save completed drawings as PNG images to chat |
## File Structure
### New Files
```
apps/mobile/src/features/live-draw/
├── index.ts
├── LiveDrawScreen.tsx # Main screen
├── components/
│ ├── DrawingCanvas.tsx # Skia canvas + gesture handling
│ ├── DrawingToolbar.tsx # Sliders for thickness/color, eraser, undo/redo
│ ├── ThicknessSlider.tsx # Custom slider for brush thickness (2-24px)
│ ├── ColorSlider.tsx # Hue slider for color selection
│ └── PartnerCursor.tsx # Shows partner's position
├── hooks/
│ ├── useDrawingSync.ts # Broadcast sync + DB persistence
│ ├── useDrawingHistory.ts # Undo/redo stack management
│ └── useCanvasCapture.ts # Capture canvas as image
├── types.ts
└── constants.ts
apps/mobile/app/(app)/live-draw/
└── [matchId].tsx # Route file
apps/supabase/migrations/
└── YYYYMMDDHHMMSS_add_live_draw_sessions.sql # New table for canvas state
```
### Files to Modify
- `apps/mobile/package.json` - Add `@shopify/react-native-skia`
- `apps/mobile/src/features/chat/components/InputBar.tsx` - Add brush icon (4th media menu item)
- `apps/mobile/src/features/chat/ChatScreen.tsx` - Add `onOpenLiveDraw` handler
- `apps/mobile/app/(app)/_layout.tsx` - Register hidden `live-draw/[matchId]` route
## Database Schema
```sql
-- New table for persisting canvas state (async support)
CREATE TABLE live_draw_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
match_id UUID NOT NULL REFERENCES matches(id) ON DELETE CASCADE,
strokes JSONB NOT NULL DEFAULT '[]', -- Array of StrokeSegment
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(match_id) -- One session per match
);
-- Enable realtime for live updates
ALTER PUBLICATION supabase_realtime ADD TABLE live_draw_sessions;
-- RLS: Only couple members can access their match's drawing
CREATE POLICY "Users can view own couple drawings"
ON live_draw_sessions FOR SELECT
USING (match_id IN (
SELECT id FROM matches WHERE couple_id = get_auth_user_couple_id()
));
CREATE POLICY "Users can update own couple drawings"
ON live_draw_sessions FOR ALL
USING (match_id IN (
SELECT id FROM matches WHERE couple_id = get_auth_user_couple_id()
));
```
## Data Structures
```typescript
// Normalized coordinates (0-1) for cross-device compatibility
interface StrokePoint { x: number; y: number; }
interface StrokeSegment {
id: string;
userId: string;
points: StrokePoint[];
color: string; // HSL string from color slider
width: number; // 2-24px from thickness slider
timestamp: number;
isEraser: boolean; // If true, renders as white/background color
}
// Broadcast events (real-time when both online)
type LiveDrawEvent =
| { type: 'stroke_start'; stroke: StrokeSegment }
| { type: 'stroke_continue'; strokeId: string; points: StrokePoint[] }
| { type: 'stroke_end'; strokeId: string }
| { type: 'clear_canvas'; userId: string }
| { type: 'undo'; userId: string; strokeId: string }
| { type: 'redo'; userId: string; strokeId: string };
```
## Implementation Phases
### Phase 1: Setup & Database
1. Create migration for `live_draw_sessions` table
2. Install `@shopify/react-native-skia`
3. Run `pod install` for iOS
4. Create feature folder structure
### Phase 2: Drawing Canvas
- Skia `Canvas` with `Path` components for each stroke
- `GestureDetector` from react-native-gesture-handler for touch
- Quadratic bezier path smoothing for natural strokes
- Normalized coordinates for different screen sizes
### Phase 3: Toolbar with Sliders
- **ThicknessSlider**: Horizontal slider (2-24px range), shows preview circle
- **ColorSlider**: Hue slider (0-360°), displays current color
- **Eraser toggle**: Button that switches to eraser mode
- **Undo/Redo buttons**: Stack-based history (own strokes only)
- **Clear canvas**: With confirmation alert
### Phase 4: Real-time Sync + Persistence
- Channel: `livedraw:${matchId}` for broadcast
- Batch points every 50ms (reduces traffic ~80%)
- On stroke end: Update `live_draw_sessions` table
- On screen open: Load existing strokes from DB
- Subscribe to `live_draw_sessions` changes for async updates
### Phase 5: Undo/Redo System
- `useDrawingHistory` hook manages undo/redo stacks
- Track own strokes separately from partner strokes
- Undo removes last own stroke, adds to redo stack
- Redo restores from redo stack
- Clear redo stack when new stroke added
### Phase 6: Save & Send
- Capture canvas as PNG via Skia's `makeImageFromView`
- Upload to `chat-media` bucket: `{match_id}/livedraw_{timestamp}.png`
- Insert message with `media_type: 'image'`
- Clear `live_draw_sessions` for this match (optional - could keep for "continue drawing")
- Navigate back to chat
### Phase 7: Chat Integration
- Add brush icon to InputBar media menu
- Update menu width animation: 132px → 176px (4 items)
- Wire navigation: `router.push(\`/(app)/live-draw/${matchId}\`)`
## Feature Colors
- User strokes: Primary `#e94560`
- Partner strokes: Secondary `#9b59b6`
- Feature accent: Teal `#14B8A6`
## Edge Cases
| Scenario | Handling |
|----------|----------|
| Partner disconnects | Continue drawing, strokes persist in DB for when they return |
| Connection lost | "Reconnecting..." overlay, queue strokes locally, sync on reconnect |
| Partner not in canvas | Strokes saved to DB, they see when they open |
| Both save simultaneously | Either can save, creates two messages (fine) |
| Canvas too large | Limit to ~500 strokes, prompt to save and start fresh |
## Database Changes Required
- New `live_draw_sessions` table (see schema above)
- Migration file: `apps/supabase/migrations/YYYYMMDDHHMMSS_add_live_draw_sessions.sql`
- Add to realtime publication
- RLS policies for couple isolation
## Key Reference Files
These existing files should be referenced during implementation:
- `apps/mobile/src/hooks/useTypingIndicator.ts` - Pattern for Supabase Broadcast real-time sync
- `apps/mobile/src/features/chat/ChatScreen.tsx` - Parent screen pattern and navigation integration
- `apps/mobile/src/features/chat/components/InputBar.tsx` - Entry point for brush icon
- `apps/mobile/src/theme/index.ts` - Design tokens for consistent styling
## Verification
1. **Manual testing with 2 devices**:
- Open same chat on both devices
- Tap brush icon → both see Live Draw screen
- Draw on device A → strokes appear on device B in real-time
- Close app on device B, draw more on A, reopen B → see new strokes
- Save → image appears in chat on both devices
2. **Unit tests**:
- Stroke normalization/denormalization
- useDrawingSync broadcast handling
- useDrawingHistory undo/redo logic
3. **Platform testing**:
- iOS simulator
- Android emulator
- Web browser