Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
15 KiB
Upload Feedback Implementation
Overview
This document describes the implementation of upload feedback UI for audio recordings. The system provides real-time visual feedback to users during the audio upload process by showing a placeholder memo card that gets replaced with the actual memo when upload completes.
Problem Statement
Previously, after completing a recording:
- User stops recording
- Upload begins silently in the background
- Memo appears in the list only after upload + processing completes
- User has no feedback that anything is happening
This created confusion and uncertainty about whether the recording was being processed.
Solution: Placeholder Memo Cards
We implemented a placeholder memo card system that provides immediate visual feedback during upload:
- Recording stops → Upload starts
- Placeholder memo card appears immediately with "Uploading..." badge
- Upload completes → Backend creates real memo in database
- Realtime subscription fires → Placeholder gets replaced with real memo
- Badge disappears after 5 seconds (auto-cleanup)
Architecture
Components Modified
1. MemoStore (features/memos/store/memoStore.ts)
Changes:
- Added
isPlaceholderflag toMemoIteminterface - Added
audioFileIdto metadata for upload status tracking - Added
setUploadingPlaceholder()method (for future use) setLatestMemo()supports placeholder memos
Key Code:
export interface MemoItem {
id: string;
title: string;
timestamp?: Date;
isPlaceholder?: boolean; // Flag to indicate this is a placeholder during upload
// ... other fields
metadata?: {
audioFileId?: string; // ID of the audio file for upload status tracking
// ... other metadata
};
}
2. Upload Status Store (features/storage/store/uploadStatusStore.ts)
Changes:
- SUCCESS status now persists permanently
- Allows users to see upload history in Audio Archive
- Status only removed when recording is deleted
Key Code:
updateStatus: async (audioFileId, status, metadataUpdate) => {
// ... update logic ...
// Note: We no longer auto-cleanup SUCCESS status
// Users want to see which recordings have been uploaded permanently
// SUCCESS status persists until the recording is deleted from Audio Archive
}
3. Upload Progress Hook (features/storage/hooks/useUploadProgress.ts)
NEW FILE - Created a safe, memory-leak-free hook for tracking upload status.
Features:
- Uses Zustand subscriptions (no polling intervals!)
- Automatically cleans up when component unmounts
- Returns upload status with convenient boolean helpers
- Zero memory leaks
Key Code:
export function useUploadProgress(audioFileId: string | undefined): UploadProgressState {
// Subscribe directly to Zustand store - no manual cleanup needed
const status = useUploadStatusStore((state) =>
audioFileId ? state.getStatus(audioFileId) : UploadStatus.SUCCESS
);
const metadata = useUploadStatusStore((state) =>
audioFileId ? state.getMetadata(audioFileId) : undefined
);
return {
status,
error: metadata?.lastError,
attemptCount: metadata?.attemptCount || 0,
memoId: metadata?.memoId,
isUploading: status === UploadStatus.UPLOADING,
isPending: status === UploadStatus.PENDING,
isFailed: status === UploadStatus.FAILED,
isSuccess: status === UploadStatus.SUCCESS,
};
}
4. MemoPreview Component (components/molecules/MemoPreview.tsx)
Changes:
- Added imports for
useUploadProgresshook andUploadStatusenum - Added
audioFileIdtoMemoModelinterface - Integrated upload progress hook
- Added upload status badge UI (appears below metadata row)
Key Code:
// Track upload status for this memo's audio file
const audioFileId = currentMemo.metadata?.audioFileId;
const uploadProgress = useUploadProgress(audioFileId);
// ... in render ...
{/* Upload Status Badge */}
{uploadProgress.status !== UploadStatus.NOT_UPLOADED &&
uploadProgress.status !== UploadStatus.SUCCESS && (
<View style={{ marginTop: 8, marginBottom: 4 }}>
<View style={{
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 12,
backgroundColor: uploadProgress.isUploading
? (isDark ? '#1E3A8A' : '#DBEAFE')
: uploadProgress.isPending
? (isDark ? '#78350F' : '#FEF3C7')
: (isDark ? '#7F1D1D' : '#FEE2E2'),
}}>
{/* Icon and text based on status */}
</View>
</View>
)}
Badge States:
- Uploading: Blue background, cloud-upload icon, "Uploading..."
- Pending/Retry: Yellow background, cloud-upload icon, "Retry N..." or "Queued..."
- Failed: Red background, alert icon, "Upload Failed"
- Success: Badge hidden (auto-cleanup after 5 seconds)
5. Home Screen (app/(protected)/(tabs)/index.tsx)
Changes:
- Added
setLatestMemoto memoStore destructuring - Modified
uploadAudioRecordingto create placeholder memo - Enhanced realtime INSERT handler to replace placeholders
Placeholder Creation (lines 1021-1056):
// Set UPLOADING status and create placeholder memo if audioFileId is provided
if (audioFileId) {
await uploadStatusStore.updateStatus(audioFileId, UploadStatus.UPLOADING, {
lastAttemptAt: Date.now(),
memoId: memoId,
});
// Create placeholder memo card to show upload in progress
setLatestMemo({
id: memoId,
title: title || 'New Recording',
timestamp: new Date(),
isPlaceholder: true,
source: { type: 'audio' },
metadata: {
audioFileId,
transcriptionStatus: 'uploading',
blueprintId: blueprintId,
stats: { viewCount: 0, shareCount: 0, editCount: 0 },
},
...(spaceId && { space: { id: spaceId, name: '' } }),
});
}
Placeholder Replacement (lines 604-614):
if (payload.event === 'INSERT') {
const currentMemo = useMemoStore.getState().latestMemo;
const shouldLoad = !currentMemo ||
currentMemo.id !== payload.new.id ||
currentMemo.isPlaceholder; // Replace placeholder with real memo
if (shouldLoad) {
console.debug('Loading real memo to replace placeholder:', payload.new.id);
loadLatestMemo();
}
}
6. Memo Types (features/memos/types/memo.types.ts)
Changes:
- Added
audioFileIdfield toMemoMetadatainterface
export interface MemoMetadata {
// ... existing fields ...
audioFileId?: string; // ID of the audio file for upload status tracking
// ... more fields ...
}
Data Flow
1. Upload Start Flow
User stops recording
↓
handleRecordingComplete()
↓
uploadAudioRecording()
↓
Generate memoId (UUID v4)
↓
uploadStatusStore.updateStatus(UPLOADING)
↓
setLatestMemo(placeholder) ← Creates placeholder memo
↓
Upload file to cloud storage
↓
[Placeholder memo card visible to user with "Uploading..." badge]
2. Upload Complete Flow
Upload succeeds
↓
Backend processes audio
↓
Backend creates memo in Supabase
↓
Realtime INSERT event fires
↓
Check: is current memo a placeholder?
↓ (YES)
loadLatestMemo() ← Fetches real memo from DB
↓
Placeholder replaced with real memo
↓
uploadStatusStore.updateStatus(SUCCESS)
↓
[SUCCESS badge persists permanently]
↓
Status only removed when recording deleted from Audio Archive
UI States
During Upload (Placeholder Memo)
┌─────────────────────────────────┐
│ New Recording │
│ Wed, 13 Nov • 2:34 PM │
│ [↻ Uploading...] ← Blue badge │
└─────────────────────────────────┘
After Upload (Real Memo - First 5 seconds)
┌─────────────────────────────────┐
│ Team Meeting Notes │ ← Real title from backend
│ Wed, 13 Nov • 2:34 PM • 2:45 │
│ [✓ Uploaded] ← Green badge │
│ Discussed Q4 objectives and... │
└─────────────────────────────────┘
After Upload (Permanent State)
┌─────────────────────────────────┐
│ Team Meeting Notes │
│ Wed, 13 Nov • 2:34 PM • 2:45 │
│ [✓ Uploaded] ← Persists forever │
│ Discussed Q4 objectives and... │
└─────────────────────────────────┘
Note: SUCCESS status now persists permanently so users can see which recordings have been uploaded.
Error State (Upload Failed)
┌─────────────────────────────────┐
│ New Recording │
│ Wed, 13 Nov • 2:34 PM │
│ [⚠ Upload Failed] ← Red badge │
└─────────────────────────────────┘
Safety Considerations
Memory Management
✅ No Memory Leaks:
useUploadProgresshook uses Zustand subscriptions (automatic cleanup)- No manual intervals or timers that need cleanup
- React handles unmounting automatically
✅ Upload Status Persistence:
- SUCCESS status persists permanently to show upload history
- Status only removed when recording is deleted from Audio Archive
- Allows users to see which recordings have been uploaded
✅ Stale State Protection:
- Each placeholder linked to specific
audioFileId - Real memo check includes
isPlaceholderflag - No race conditions between placeholder and real memo
Edge Cases Handled
-
Multiple recordings in quick succession:
- Each gets unique
memoIdandaudioFileId - Placeholders tracked independently
- Realtime events match by
memoId
- Each gets unique
-
App backgrounded during upload:
- Upload attempt completes or fails
- Placeholder persists in store
- If upload succeeds, replaced when app returns and realtime fires
- If upload fails, user can manually retry from Audio Archive
Note: Automatic retry on network reconnection has been removed.
-
Upload fails:
- Badge shows "Upload Failed" (red)
- Placeholder remains (no real memo to replace it)
- User can manually retry from Audio Archive
Note: Automatic retry mechanism has been removed. Only manual retry is supported.
-
Real memo arrives before placeholder shown:
- Unlikely (placeholder created synchronously)
- Even if it happens, realtime logic checks
isPlaceholder - Will still call
loadLatestMemo()correctly
Performance Considerations
Rendering Performance
- Zustand subscriptions only trigger re-renders when relevant state changes
- Badge conditionally rendered - not present when not needed
- No polling - event-driven updates only
Network Efficiency
- No additional API calls - uses existing realtime subscriptions
- Single database query when replacing placeholder
- Minimal payload - placeholder stored only in client memory
Backend Requirements
What Backend Needs to Do
✅ Already implemented:
- Backend receives
memoIdin upload payload - Backend creates memo with that
memoIdin Supabase - Realtime subscription broadcasts INSERT event
❌ NOT required:
- Backend does NOT need to include
audioFileIdin memo metadata - Frontend handles upload tracking entirely
- Separation of concerns maintained
Testing Checklist
- Placeholder appears immediately after recording stops
- Upload badge shows "Uploading..." on placeholder
- Placeholder gets replaced by real memo when upload completes
- Badge shows "✓ Uploaded" briefly on real memo
- Badge auto-disappears after 5 seconds
- Error badge shows on upload failure
- Multiple recordings work correctly
- App backgrounding doesn't break flow
- Network interruption handling
- Theme changes (light/dark mode) work correctly
Future Enhancements
Potential Improvements
-
Progress Percentage:
- Show actual upload progress (0-100%)
- Requires XMLHttpRequest or fetch with progress events
- See:
UPLOAD_FEEDBACK_IMPLEMENTATION.mdStrategy 2
-
Retry Button:
- Add retry button on failed upload badge
- Allow users to manually retry failed uploads
- Better UX than navigating to Audio Archive
Note: Automatic upload retry has been removed. Users must manually retry failed uploads.
-
Toast Notification:
- Complementary feedback on upload start
- Auto-dismiss after 3 seconds
- More prominent for users who look away
-
Animated Transitions:
- Smooth fade from placeholder → real memo
- Skeleton loading state
- Progress bar animation
-
Upload Queue Indicator:
- Global badge showing total pending uploads
- Useful when multiple recordings queued
- Header icon with count
Troubleshooting
Badge Not Showing
Symptom: Upload badge doesn't appear on placeholder memo
Causes:
audioFileIdnot passed touploadAudioRecording()uploadStatusStore.updateStatus()not calleduseUploadProgresshook not integrated in MemoPreview
Solution: Check console logs for:
[Upload] Set status to UPLOADING for {audioFileId}
[Upload] Created placeholder memo card for {memoId}
Placeholder Not Replaced
Symptom: Placeholder memo stays, real memo doesn't replace it
Causes:
- Realtime subscription not active
isPlaceholdercheck not workingloadLatestMemo()not called
Solution: Check console logs for:
Neues Memo erkannt: {memoId}
Loading real memo to replace placeholder: {memoId}
Memory Growing Over Time
Symptom: App becomes slower after many recordings
Causes:
- Upload status accumulating for deleted recordings
- Upload status not being cleaned up when recordings deleted
Solution:
- Check
uploadStatusStore.statusMap.sizein console - Upload status should be removed when recordings are deleted
- Status is automatically cleaned up in
handleDeletefunction - If issues persist, check that
removeStatusis being called properly
Related Documentation
Contributors
- Implementation: Claude (AI Assistant)
- Product Requirements: @wuesteon
- Date: November 13, 2025