refactor: restructure

monorepo with apps/ and services/
  directories
This commit is contained in:
Wuesteon 2025-11-26 03:03:24 +01:00
parent 25824ed0ac
commit ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions

View file

@ -0,0 +1,288 @@
# Anleitung zum Hinzufügen neuer Sprachen in Memoro
Diese Dokumentation beschreibt den Prozess zum Hinzufügen neuer Sprachen zur Memoro-App.
## Übersicht
Die Memoro-App verwendet i18next für die Internationalisierung. Die Sprachkonfiguration befindet sich im Verzeichnis `features/i18n`. Um eine neue Sprache hinzuzufügen, müssen folgende Schritte durchgeführt werden:
1. Erstellen einer neuen Übersetzungsdatei
2. Aktualisieren der i18n-Konfiguration
3. Testen der neuen Sprache
## 1. Erstellen einer neuen Übersetzungsdatei
### Schritt 1.1: Erstellen der JSON-Datei
Erstelle eine neue JSON-Datei im Verzeichnis `features/i18n/translations/` mit dem entsprechenden Sprachcode als Dateinamen (z.B. `es.json` für Spanisch).
Verwende eine bestehende Übersetzungsdatei (z.B. `en.json`) als Vorlage und übersetze alle Werte in die neue Sprache. Die Schlüssel müssen identisch bleiben.
Beispiel für eine Übersetzungsdatei:
```json
{
"common": {
"save": "Speichern",
"cancel": "Abbrechen",
// weitere Übersetzungen...
},
// weitere Kategorien...
}
```
### Schritt 1.2: Struktur der Übersetzungsdatei
Stelle sicher, dass die Struktur der neuen Übersetzungsdatei exakt der Struktur der vorhandenen Dateien entspricht. Alle Schlüssel müssen vorhanden sein, um Fehler zu vermeiden.
Die Hauptkategorien sind:
- `common`: Allgemeine Begriffe
- `auth`: Authentifizierungsbezogene Texte
- `home`: Texte für die Startseite
- `tabs`: Bezeichnungen für die Tabs
- `menu`: Menüeinträge
- `blueprints`: Texte für Blueprints
- `app`: App-spezifische Texte
- `tags`: Tag-bezogene Texte
- `memo`: Memo-bezogene Texte
- `settings`: Einstellungstexte
- `recording`: Aufnahme-bezogene Texte
- `layout`: Layout-bezogene Texte
- `audio_archive`: Texte für das Audio-Archiv
## 2. Aktualisieren der i18n-Konfiguration
### Schritt 2.1: Import der neuen Übersetzungsdatei
Öffne die Datei `features/i18n/index.ts` und füge einen Import für die neue Übersetzungsdatei hinzu:
```typescript
import xx from './translations/xx.json'; // xx ist der Sprachcode der neuen Sprache
```
### Schritt 2.2: Aktualisieren der LANGUAGES-Konstante
Füge die neue Sprache zur `LANGUAGES`-Konstante hinzu:
```typescript
export const LANGUAGES = {
// bestehende Sprachen...
xx: { nativeName: 'Sprachname', emoji: '🇽🇽' }, // Sprachname in der Originalsprache und entsprechendes Flaggen-Emoji
};
```
Verwende für das Emoji die entsprechende Länderflagge im Unicode-Format (z.B. 🇪🇸 für Spanien).
### Schritt 2.3: Aktualisieren der i18n-Ressourcen
Füge die neue Sprache zu den Ressourcen in der i18n-Initialisierung hinzu:
```typescript
i18n
.use(initReactI18next)
.init({
resources: {
// bestehende Sprachen...
xx: { translation: xx },
},
// weitere Konfigurationen...
});
```
## 3. Testen der neuen Sprache
### Schritt 3.1: Überprüfen der Sprachauswahl
Starte die App und navigiere zu den Einstellungen. Die neue Sprache sollte in der Sprachauswahl erscheinen.
### Schritt 3.2: Testen der Übersetzungen
Wähle die neue Sprache aus und überprüfe, ob alle Texte korrekt übersetzt werden. Achte besonders auf:
- Formatierungen (z.B. bei Datumsangaben)
- Pluralformen
- Spezielle Zeichen
- Textlängen (einige Übersetzungen können länger sein als die Originaltexte)
### Schritt 3.3: Überprüfen der RTL-Unterstützung (falls erforderlich)
Wenn die neue Sprache von rechts nach links geschrieben wird (z.B. Arabisch, Hebräisch), überprüfe, ob die RTL-Unterstützung korrekt funktioniert. In der Datei `features/i18n/LanguageContext.tsx` wird die RTL-Unterstützung für bestimmte Sprachen aktiviert:
```typescript
setIsRTL(['ar', 'he'].includes(language)); // Füge hier den neuen Sprachcode hinzu, falls nötig
```
## 4. Erweitern der Blueprint-Funktionalität
Wenn die App Blueprints mit mehrsprachigen Inhalten unterstützt, müssen möglicherweise auch die Blueprint-Formulare und -Komponenten aktualisiert werden, um die neue Sprache zu unterstützen.
### Schritt 4.1: Aktualisieren der Blueprint-Formulare
Füge neue Felder für die neue Sprache in den Blueprint-Erstellungs- und -Bearbeitungsformularen hinzu.
### Schritt 4.2: Aktualisieren der Blueprint-Anzeige
Stelle sicher, dass die Blueprint-Anzeige die Inhalte in der neuen Sprache korrekt darstellt.
## 5. Bekannte Einschränkungen
- Die Memoro-App unterstützt derzeit keine dynamische Nachladen von Übersetzungen. Alle Übersetzungen müssen zum Build-Zeitpunkt vorhanden sein.
- Einige Teile der App könnten hardcodierte Texte enthalten, die nicht über das i18n-System übersetzt werden.
## 6. Tipps für Übersetzer
- Verwende konsistente Terminologie innerhalb der Übersetzung.
- Achte auf den Kontext der Texte, um eine angemessene Übersetzung zu gewährleisten.
- Teste die Übersetzungen in der App, um sicherzustellen, dass sie im UI-Kontext gut funktionieren.
- Berücksichtige Platzbeschränkungen in der UI, besonders bei längeren Übersetzungen.
## 7. Beispiel: Hinzufügen neuer Sprachen
### 7.1 Beispiel: Hinzufügen von Spanisch
```typescript
// 1. Erstelle die Datei features/i18n/translations/es.json mit allen Übersetzungen
// 2. Aktualisiere features/i18n/index.ts
import es from './translations/es.json';
export const LANGUAGES = {
de: { nativeName: 'Deutsch', emoji: '🇩🇪' },
en: { nativeName: 'English', emoji: '🇬🇧' },
it: { nativeName: 'Italiano', emoji: '🇮🇹' },
fr: { nativeName: 'Français', emoji: '🇫🇷' },
es: { nativeName: 'Español', emoji: '🇪🇸' },
};
i18n
.use(initReactI18next)
.init({
resources: {
de: { translation: de },
en: { translation: en },
it: { translation: it },
fr: { translation: fr },
es: { translation: es },
},
// weitere Konfigurationen...
});
```
### 7.2 Beispiel: Hinzufügen von Griechisch
```typescript
// 1. Erstelle die Datei features/i18n/translations/el.json mit allen Übersetzungen
// 2. Aktualisiere features/i18n/index.ts
import el from './translations/el.json';
export const LANGUAGES = {
// bestehende Sprachen...
el: { nativeName: 'Ελληνικά', emoji: '🇬🇷' },
};
i18n
.use(initReactI18next)
.init({
resources: {
// bestehende Sprachen...
el: { translation: el },
},
// weitere Konfigurationen...
});
```
### 7.3 Beispiel: Hinzufügen von Lettisch
```typescript
// 1. Erstelle die Datei features/i18n/translations/lv.json mit allen Übersetzungen
// 2. Aktualisiere features/i18n/index.ts
import lv from './translations/lv.json';
export const LANGUAGES = {
// bestehende Sprachen...
lv: { nativeName: 'Latviešu', emoji: '🇱🇻' },
};
i18n
.use(initReactI18next)
.init({
resources: {
// bestehende Sprachen...
lv: { translation: lv },
},
// weitere Konfigurationen...
});
```
### 7.4 Beispiel: Hinzufügen von Litauisch
```typescript
// 1. Erstelle die Datei features/i18n/translations/lt.json mit allen Übersetzungen
// 2. Aktualisiere features/i18n/index.ts
import lt from './translations/lt.json';
export const LANGUAGES = {
// bestehende Sprachen...
lt: { nativeName: 'Lietuvių', emoji: '🇱🇹' },
};
i18n
.use(initReactI18next)
.init({
resources: {
// bestehende Sprachen...
lt: { translation: lt },
},
// weitere Konfigurationen...
});
```
### 7.5 Beispiel: Hinzufügen von Slowakisch
```typescript
// 1. Erstelle die Datei features/i18n/translations/sk.json mit allen Übersetzungen
// 2. Aktualisiere features/i18n/index.ts
import sk from './translations/sk.json';
export const LANGUAGES = {
// bestehende Sprachen...
sk: { nativeName: 'Slovenčina', emoji: '🇸🇰' },
};
i18n
.use(initReactI18next)
.init({
resources: {
// bestehende Sprachen...
sk: { translation: sk },
},
// weitere Konfigurationen...
});
```
### 7.6 Beispiel: Hinzufügen von Slowenisch
```typescript
// 1. Erstelle die Datei features/i18n/translations/sl.json mit allen Übersetzungen
// 2. Aktualisiere features/i18n/index.ts
import sl from './translations/sl.json';
export const LANGUAGES = {
// bestehende Sprachen...
sl: { nativeName: 'Slovenščina', emoji: '🇸🇮' },
};
i18n
.use(initReactI18next)
.init({
resources: {
// bestehende Sprachen...
sl: { translation: sl },
},
// weitere Konfigurationen...
});
```

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,280 @@
# Realtime Broadcast Fix - Implementation Summary
## Issue Description
The memo list and memo preview components were not updating in real-time when status changes occurred on the Supabase backend, particularly for operations performed by edge functions using `service_role` credentials.
### Root Cause
**Supabase Realtime respects RLS (Row Level Security) policies even for service_role operations.** This means that when edge functions (running with service_role credentials) update memo records, the postgres_changes realtime subscriptions don't fire for regular users because the RLS policies filter them out.
## Solution Implemented
Implemented a **hybrid subscription model** combining:
1. **postgres_changes subscriptions** - For user-initiated direct updates
2. **Broadcast channel subscriptions** - For service_role edge function updates
### Edge Functions (Backend)
Edge functions already send broadcasts to `memo-updates-{memoId}` channels when they complete processing:
- `batch-transcribe-callback/index.ts` - Sends broadcasts after transcription
- `headline/index.ts` - Sends broadcasts after headline generation
- `translate/index.ts` - Sends broadcasts after translation
- `blueprint/index.ts` - Sends broadcasts after blueprint processing
### Client Components (Frontend)
**Files Modified:**
1. `/memoro_app/components/molecules/MemoList.tsx`
2. `/memoro_app/components/molecules/MemoPreview.tsx`
#### MemoList.tsx Changes
**Import Added:**
```typescript
import { memoRealtimeService } from '~/features/memos/services/memoRealtimeService';
```
**New useEffect Hook (lines 387-437):**
```typescript
useEffect(() => {
const unsubscribeFunctions: (() => void)[] = [];
// Subscribe to broadcasts for all visible memos
memos.forEach((memo) => {
const unsubscribe = memoRealtimeService.subscribeToBroadcastChannel(
`memo-updates-${memo.id}`,
async (payload) => {
console.log('MemoList: Received broadcast for memo', memo.id, payload);
try {
// Fetch fresh memo data from Supabase
const supabase = await getAuthenticatedClient();
const { data: updatedMemo, error } = await supabase
.from('memos')
.select('*')
.eq('id', memo.id)
.single();
if (error) {
console.error('MemoList: Error fetching updated memo after broadcast:', error);
return;
}
if (updatedMemo) {
// Update the memo in the list immediately
setMemos(prevMemos =>
prevMemos.map(m => m.id === memo.id ? updatedMemo : m)
);
console.log('MemoList: Updated memo from broadcast', {
id: updatedMemo.id,
title: updatedMemo.title,
headlineStatus: updatedMemo.metadata?.processing?.headline_and_intro?.status
});
}
} catch (error) {
console.error('MemoList: Error processing broadcast update:', error);
}
}
);
unsubscribeFunctions.push(unsubscribe);
});
// Cleanup on unmount or memo list change
return () => {
unsubscribeFunctions.forEach(unsub => unsub());
};
}, [memos.map(m => m.id).join(',')]); // Re-subscribe when memo IDs change
```
#### MemoPreview.tsx Changes
**Import Added:**
```typescript
import { memoRealtimeService } from '~/features/memos/services/memoRealtimeService';
```
**New useEffect Hook (lines 242-287):**
```typescript
useEffect(() => {
if (!memo?.id) return;
const unsubscribe = memoRealtimeService.subscribeToBroadcastChannel(
`memo-updates-${memo.id}`,
async (payload) => {
console.log('MemoPreview: Received broadcast for memo', memo.id, payload);
try {
// Fetch fresh memo data from Supabase
const supabase = await getAuthenticatedClient();
const { data: updatedMemo, error } = await supabase
.from('memos')
.select('*')
.eq('id', memo.id)
.single();
if (error) {
console.error('MemoPreview: Error fetching updated memo after broadcast:', error);
return;
}
if (updatedMemo) {
// If this is the latest memo on recording page, update it in the store
if (reactToGlobalRecordingStatus) {
setLatestMemo(updatedMemo);
}
console.log('MemoPreview: Updated memo from broadcast', {
id: updatedMemo.id,
title: updatedMemo.title,
headlineStatus: updatedMemo.metadata?.processing?.headline_and_intro?.status
});
// The useMemoProcessing hook will automatically recalculate displayTitle
// based on the updated memo state
}
} catch (error) {
console.error('MemoPreview: Error processing broadcast update:', error);
}
}
);
return () => unsubscribe();
}, [memo?.id, reactToGlobalRecordingStatus, setLatestMemo]);
```
## How It Works
### Data Flow
```
Edge Function (service_role)
Updates Memo in Database
Sends Broadcast to memo-updates-{memoId}
MemoList/MemoPreview receives broadcast
Fetches fresh memo data from Supabase
Updates local component state
UI re-renders with new title/status
```
### Key Features
1. **Automatic Subscription Management**
- Subscriptions created when component mounts or memo list changes
- Automatic cleanup when component unmounts
- Re-subscribes when memo IDs change
2. **Error Handling**
- Try-catch blocks around all async operations
- Detailed error logging for debugging
- Graceful degradation if broadcast fails
3. **Performance Optimization**
- Only subscribes to visible memos in the list
- Immediate state updates without batching delay
- Leverages existing memoRealtimeService infrastructure
4. **Comprehensive Logging**
- Broadcasts received logged with payload details
- Updated memo data logged for verification
- Errors logged with context
## Testing
### Manual Testing Steps
1. **Create a new memo via recording**
- Verify memo appears in list with "Transcribing Memo" status
- Wait for transcription to complete
- Verify title updates to "Generating Headline"
- Wait for headline generation
- Verify title updates to generated headline
2. **Check MemoPreview on recording page**
- Create new memo
- Observe realtime title updates
- Verify processing status changes
3. **Check MemoList in memo index page**
- Navigate to memo list
- Create new memo in another tab/device
- Verify new memo appears in list
- Verify title updates as processing completes
### Expected Console Output
When broadcast received:
```
MemoList: Received broadcast for memo {memoId} {payload}
MemoList: Updated memo from broadcast {id, title, headlineStatus}
```
## Benefits
1. **Solves RLS Limitation**
- Works around Supabase Realtime RLS restriction
- Ensures updates from service_role operations reach clients
2. **Maintains Existing Functionality**
- postgres_changes subscriptions still active for direct updates
- Hybrid approach provides redundancy
3. **Follows Established Pattern**
- Uses same pattern as home screen (already working)
- Leverages existing memoRealtimeService
4. **Low Risk**
- Additive changes only, no removal of existing code
- Error handling prevents crashes
- Automatic cleanup prevents memory leaks
## Known Limitations
1. **Network Dependency**
- Requires active internet connection for broadcasts
- Falls back to postgres_changes for direct updates
2. **Subscription Overhead**
- Creates one broadcast subscription per visible memo
- Cleaned up automatically when memos change
3. **Eventual Consistency**
- Small delay between broadcast and UI update
- Acceptable for this use case
## Future Improvements
1. **Add Connection Status Indicator**
- Show user when realtime is disconnected
- Display "stale data" warning if appropriate
2. **Implement Offline Queue**
- Queue updates when offline
- Sync when connection restored
3. **Add Metrics**
- Track broadcast latency
- Monitor subscription health
- Alert on failures
## Related Documentation
- `/memoro_app/docs/SUPABASE_REALTIME_RLS_WORKAROUND.md` - Original RLS workaround documentation
- `/memoro_app/features/memos/services/memoRealtimeService.ts` - Realtime service implementation
- Edge function broadcast implementations in `/memoro_edgefunction/supabase/functions-dev/`
## Implementation Date
October 23, 2025
## Contributors
- Claude Code Swarm (Coordinator, Frontend Analyst, Backend Analyst, Solution Architect, Implementation Engineer)

View file

@ -0,0 +1,181 @@
# Recording Preview Implementation
## Overview
This document describes the temporary recording preview feature implemented on 2025-06-26 to improve UX during audio recording.
## Problem Statement
Previously, during recording, the memo preview would show old memo data with incorrect timestamps and metadata. This was confusing for users as they would see:
- Wrong date/time (from a previous memo)
- Old memo title instead of recording status
- Stale metadata that didn't reflect the current recording
## Solution
### Temporary Recording Preview
During active recording, the app now shows a dedicated temporary preview that displays:
1. **Current Recording Status**
- "Aufnahme läuft..." (Recording in progress) during recording
- "Aufnahme wird hochgeladen..." (Uploading recording) during upload
2. **Accurate Timestamp**
- Shows the actual recording start time
- Updates duration counter every second
3. **Recording Metadata**
- Displays recording-specific metadata
- No confusion with old memo data
### Implementation Details
#### Frontend Changes (`/app/(protected)/(tabs)/index.tsx`)
**State Management:**
```typescript
const [recordingStartTime, setRecordingStartTime] = useState<Date | null>(null);
const [recordingDuration, setRecordingDuration] = useState<number>(0);
```
**Recording Start Handler:**
```typescript
const handleRecordingStart = useCallback(() => {
setRecordingStatus(RecordingStatus.RECORDING);
setRecordingStartTime(new Date());
setRecordingDuration(0);
}, []);
```
**Duration Update Effect:**
```typescript
useEffect(() => {
let interval: NodeJS.Timeout;
if (recordingStatus === RecordingStatus.RECORDING && recordingStartTime) {
interval = setInterval(() => {
const elapsed = Math.floor((new Date().getTime() - recordingStartTime.getTime()) / 1000);
setRecordingDuration(elapsed);
}, 1000);
}
return () => {
if (interval) clearInterval(interval);
};
}, [recordingStatus, recordingStartTime]);
```
**Conditional Rendering:**
```typescript
{((recordingStatus === RecordingStatus.RECORDING || recordingStatus === RecordingStatus.UPLOADING) && recordingStartTime) ? (
// Show temporary recording preview
<MemoPreview
memo={{
id: 'recording-temp',
title: recordingStatus === RecordingStatus.UPLOADING
? t('memo.status.uploading_recording')
: t('memo.status.recording_in_progress'),
timestamp: recordingStartTime,
source: { duration: recordingDuration },
metadata: {
recordingStatus: recordingStatus === RecordingStatus.UPLOADING ? 'uploading' : 'recording'
}
}}
// ... other props
/>
) : latestMemo && isMemoPreviewVisible && (
// Show normal memo preview
)}
```
### Recording Time Preservation
#### Backend Changes (`memoro.service.ts`)
The backend now stores `recordingStartedAt` in memo metadata:
```typescript
metadata: {
processing: { /* ... */ },
...(recordingStartedAt && { recordingStartedAt }),
...(location && { location })
}
```
#### Frontend Time Handling
Multiple layers ensure the recording time is preserved:
1. **Recording Store** (`recordingStore.ts`):
```typescript
let memoTimestamp: Date;
if (transcriptionResult.memo.metadata?.recordingStartedAt) {
memoTimestamp = new Date(transcriptionResult.memo.metadata.recordingStartedAt);
} else if (recordingStartTime) {
memoTimestamp = recordingStartTime;
} else {
memoTimestamp = new Date(transcriptionResult.memo.created_at);
}
```
2. **Real-time Updates** (`index.tsx`):
```typescript
let timestamp: Date;
if (payload.new.metadata?.recordingStartedAt) {
timestamp = new Date(payload.new.metadata.recordingStartedAt);
} else if (payload.new.created_at) {
timestamp = new Date(payload.new.created_at);
} else if (existingMemo && existingMemo.id === payload.new.id) {
timestamp = existingMemo.timestamp;
} else {
timestamp = new Date();
}
```
3. **Memo Store** (`memoStore.ts`):
```typescript
const timestamp = bestMemo.metadata?.recordingStartedAt
? new Date(bestMemo.metadata.recordingStartedAt)
: new Date(bestMemo.created_at);
```
### Processing State Display
#### DirectMemoTitle Component Updates
The title component now properly shows processing states:
1. **Priority Order:**
- Recording/Uploading status (highest priority)
- Processing states (pending/processing)
- Actual title (when available)
- Fallback states
2. **No Premature "Ready" State:**
```typescript
// Only show "memo ready" if we truly have no processing information
if (memo.metadata?.processing) {
return t('memo.status.memo_transcribing');
}
```
## Benefits
1. **Clear Visual Feedback**: Users see exactly what's happening during recording
2. **Accurate Timestamps**: Shows when recording actually started, not when database entry was created
3. **No Confusion**: Old memo data doesn't appear during new recordings
4. **Consistent Experience**: Same preview style, just with recording-specific content
## Testing
To verify the implementation:
1. Start a new recording
2. Observe the preview shows current time and "Aufnahme läuft..."
3. Watch the duration counter increment
4. Stop recording and see "Aufnahme wird hochgeladen..."
5. After upload, see the new memo with correct recording timestamp
6. Verify processing states show correctly ("Memo transkribiert...", etc.)
## Future Enhancements
Potential improvements:
- Audio level visualization in recording preview
- Pause indicator when recording is paused
- Estimated upload time based on file size
- Recording quality indicators

View file

@ -0,0 +1,304 @@
# RecordingStatus.UPLOADING Bug Fix
## Issue Description
After recording an audio memo, the title displayed incorrectly across the application:
- ✅ **Memo list** (memo index page): Shows CORRECT AI-generated title
- ❌ **Recording screen preview**: Shows "Aufnahme wird hochgeladen..." (placeholder)
- ❌ **Detail page**: Shows placeholder/wrong title
### User Impact
Users would record a memo, and while the memo list showed the correct AI-generated title, the preview card on the recording screen and the detail page would show placeholder text like "Aufnahme wird hochgeladen..." or "Uploading recording..." instead of the actual title.
## Investigation Journey
### Step 1: Initial Hypothesis - Backend Title Issue
**What we tried first:**
- Analyzed logs and found that `triggerTranscription` was being called with `title: "Memo"` (hardcoded)
- Discovered that the `title` and `blueprintId` parameters were being passed but not in the function signature
- **Fixed:** Added `title` and `blueprintId` to the `transcriptionUtils.ts` function signature
**Result:** Backend was now receiving the correct title, but the display issue persisted.
### Step 2: Deep Log Analysis
User provided logs showing:
```typescript
🎯 Triggering transcription with: {
"title": "Test 2", // User's actual title
"blueprintId": "11111111-2222-3333-4444-555555555555",
...
}
```
But also showing:
```typescript
DEBUG Showing "Uploading recording..." for memo: b094ff63-690c-4f07-9a8f-99a3579c3719
```
**Key insight:** The title was being set correctly in the backend, but something in the frontend display logic was overriding it.
### Step 3: Root Cause Discovery
Examined the `RecordingStatus` enum definition:
```typescript
// features/audioRecordingV2/types/index.ts
export enum RecordingStatus {
IDLE = 'idle',
PREPARING = 'preparing',
RECORDING = 'recording',
PAUSED = 'paused',
STOPPING = 'stopping',
STOPPED = 'stopped',
ERROR = 'error'
// ❌ NO UPLOADING STATUS EXISTS!
}
```
**Critical Finding:** Multiple files were checking for `RecordingStatus.UPLOADING` which doesn't exist in the enum!
### Files Referencing Non-Existent Status
1. **`useMemoProcessing.ts`** (lines 83, 96, 220)
```typescript
if (effectiveRecordingStatus === RecordingStatus.UPLOADING) {
return MemoProcessingStatus.UPLOADING; // This never executed correctly
}
```
2. **`index.tsx`** (line 1426)
```typescript
{(recordingStatus === RecordingStatus.RECORDING ||
recordingStatus === RecordingStatus.UPLOADING) && // Always undefined
recordingStartTime ? (
```
3. **`RecordingButton.tsx`** (lines 170, 316, 408, etc.)
```typescript
const isUploading = status === RecordingStatus.UPLOADING; // Always false/undefined
if (isUploading) return; // Never worked
```
### The Core Problem
When the code checked `status === RecordingStatus.UPLOADING`, it was comparing to `undefined` (since UPLOADING doesn't exist in the enum). This caused:
- Conditional logic to fail
- Display logic to show placeholder text instead of actual titles
- Animation logic to never trigger
- The `isUploading` variable to always be falsy, but still be referenced elsewhere causing ReferenceErrors
## Solution Implemented
### Changes Made
Removed **ALL** references to the non-existent `RecordingStatus.UPLOADING`:
#### 1. `useMemoProcessing.ts`
```typescript
// BEFORE (Lines 81-92):
const processingStatus = useMemo((): MemoProcessingStatus => {
if (effectiveRecordingStatus === RecordingStatus.UPLOADING) {
return MemoProcessingStatus.UPLOADING;
}
if (
effectiveRecordingStatus === RecordingStatus.RECORDING ||
effectiveRecordingStatus === RecordingStatus.PAUSED
) {
return MemoProcessingStatus.UPLOADING;
}
// ...
}
// AFTER:
const processingStatus = useMemo((): MemoProcessingStatus => {
// Note: RecordingStatus.UPLOADING doesn't exist in the enum, removed invalid check
if (
effectiveRecordingStatus === RecordingStatus.RECORDING ||
effectiveRecordingStatus === RecordingStatus.PAUSED
) {
return MemoProcessingStatus.UPLOADING;
}
// ...
}
```
Also removed from displayTitle logic:
```typescript
// REMOVED:
if (effectiveRecordingStatus === RecordingStatus.UPLOADING) {
return t('memo.status.uploading_recording');
}
```
#### 2. `index.tsx`
```typescript
// BEFORE (Line 1426):
{(recordingStatus === RecordingStatus.RECORDING ||
recordingStatus === RecordingStatus.UPLOADING) &&
recordingStartTime ? (
<MemoPreview
memo={{
title: recordingStatus === RecordingStatus.UPLOADING
? t('memo.status.uploading_recording')
: t('memo.status.recording_in_progress'),
// ...
}}
/>
)}
// AFTER:
{recordingStatus === RecordingStatus.RECORDING && recordingStartTime ? (
<MemoPreview
memo={{
title: t('memo.status.recording_in_progress'),
// ...
}}
/>
)}
```
#### 3. `RecordingButton.tsx`
**Removed isUploading variable (line 169-170):**
```typescript
// REMOVED:
const isUploading = status === RecordingStatus.UPLOADING;
```
**Removed from backgroundColor (line 182):**
```typescript
// BEFORE:
const backgroundColor = (isRecording || isUploading) ? themeColor : 'transparent';
// AFTER:
const backgroundColor = isRecording ? themeColor : 'transparent';
```
**Removed from handlePressIn (line 408):**
```typescript
// REMOVED:
if (isUploading) return;
// REMOVED from debug log:
console.debug('🔥 handlePressIn - Press started', { isRecording, isPaused, isUploading });
```
**Removed from animation logic (lines 316-327):**
```typescript
// REMOVED:
} else if (status === RecordingStatus.UPLOADING) {
cancelAnimation(rotationAnim);
uploadProgressAnim.value = withTiming(1, {
duration: 6000,
easing: Easing.out(Easing.ease)
});
```
**Removed from disabled prop (line 956):**
```typescript
// BEFORE:
disabled={isPaused || isUploading}
// AFTER:
disabled={isPaused}
```
**Removed upload progress overlay JSX (lines 974-976):**
```typescript
// REMOVED:
{isUploading && (
<Animated.View style={uploadProgressStyle} />
)}
```
**Removed from icon styling (lines 978-990):**
```typescript
// REMOVED conditional styling:
...(isUploading && {
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.3,
shadowRadius: 2,
elevation: 3,
})
```
## Why This Happened
The `RecordingStatus.UPLOADING` appears to be a **legacy value** that was removed from the enum in a previous refactor, but references weren't cleaned up throughout the codebase.
### Historical Context
The upload state was likely intended to show a distinct UI state between:
1. Recording audio
2. Uploading to storage
3. Backend processing (transcription/title generation)
However, the actual implementation removed the UPLOADING status from the enum and relied on the store's internal state management instead.
## Fix Verification
### Expected Behavior After Fix
1. **During recording:**
- Preview shows "Recording in progress..."
- Recording button shows filled state with animation
2. **After recording stops:**
- Preview immediately switches to show the latest memo
- Title displays actual memo title (or processing status)
- Real-time subscriptions update the title when AI generation completes
3. **No ReferenceErrors:**
- No crashes due to undefined `isUploading` variable
- All conditional checks work correctly
### Testing Steps
1. Record a new memo
2. Verify preview shows "Recording in progress..." during recording
3. Stop recording
4. Verify preview immediately shows the memo (not placeholder)
5. Wait for AI title generation
6. Verify title updates via real-time subscription
7. Navigate to detail page - verify correct title
8. Check memo list - verify correct title
## Files Modified
- `/features/memos/hooks/useMemoProcessing.ts` - Removed UPLOADING checks from display logic
- `/app/(protected)/(tabs)/index.tsx` - Simplified preview conditional
- `/components/atoms/RecordingButton.tsx` - Removed all UPLOADING references and animations
- `/features/storage/transcriptionUtils.ts` - Added title/blueprintId parameters (separate fix)
## Related Issues
- **Broadcast Channel Implementation**: Added broadcast subscriptions to catch service_role updates (separate feature)
- **Title Parameter Fix**: Ensured title is passed to backend API (completed earlier in session)
## Commit
```
🐛 fix: remove non-existent RecordingStatus.UPLOADING references
Commit: de1383e
```
## Implementation Date
October 23, 2025
## Key Takeaways
1. **Enum validation matters**: When an enum value is removed, all references must be found and removed
2. **Silent failures**: Checking for undefined enum values doesn't throw errors, just silently fails
3. **Comprehensive search**: Must search across entire codebase, not just the obvious files
4. **Testing limitations**: The bug wasn't caught by type checking because the checks were valid TypeScript (comparing to undefined)
## Prevention Strategies
1. **Use TypeScript exhaustive checks** where possible
2. **Add ESLint rules** to detect references to undefined enum values
3. **Search codebase** before removing enum values
4. **Add integration tests** that verify UI states match enum values

View file

@ -0,0 +1,71 @@
# Refresh Token Grace Period Implementation
## Overview
As of 2025-01-10, the backend implements a 5-minute grace period for refresh tokens to handle race conditions and network issues gracefully. This document explains how the frontend handles this feature.
## How It Works
1. **Normal Flow**: When refreshing a token, the old token is marked as "rotated" and remains valid for 5 minutes
2. **Grace Period**: If the same old token is used again within 5 minutes, the backend returns the previously generated new token
3. **No Duplicates**: This prevents creating multiple tokens for the same device
## Frontend Implementation
### TokenManager Updates
The `tokenManager.ts` already handles most scenarios correctly:
```typescript
// Handles both refresh_in_progress and rotation_in_progress errors
if (result.error === 'refresh_in_progress' || result.error === 'rotation_in_progress') {
console.debug('TokenManager: Token rotation in progress, waiting...');
await new Promise(resolve => setTimeout(resolve, 1000));
// Retry after waiting
}
```
### Non-Retryable Errors
The following errors indicate permanent failures and should not be retried:
- `invalid_token` - Token doesn't exist
- `token_expired` - Token or grace period expired
- `invalid_token_state` - Token in unexpected state
- `token_collision` - Very rare UUID collision
### What the Frontend Does
1. **Always saves the returned token** - Whether it's new or from grace period
2. **Retries with exponential backoff** - For network and temporary errors
3. **Handles rotation_in_progress** - Waits and retries when another request is rotating the token
4. **Respects permanent errors** - Doesn't retry when token is truly invalid
## Testing the Grace Period
To test the grace period implementation:
1. **Simulate Network Interruption**:
- Start a refresh request
- Kill the app before it completes
- Restart and try to refresh again
- Should succeed using grace period
2. **Test Concurrent Refreshes**:
- Make multiple refresh calls simultaneously
- All should succeed without creating duplicate tokens
3. **Test Grace Period Expiry**:
- Use an old token
- Wait 6+ minutes
- Try to use it again
- Should fail with "Token has been rotated and grace period has expired"
## No Additional Changes Required
The current frontend implementation is already compatible with the grace period feature because:
- It properly retries on temporary errors
- It saves whatever token is returned
- It handles the new `rotation_in_progress` error
- It respects permanent failure errors
The grace period is transparent to the frontend - it just makes the system more resilient to network issues and race conditions.

View file

@ -0,0 +1,278 @@
# Spaces Feature Documentation
## Overview
Spaces in Memoro allow users to organize their memories and memos into logical containers. This feature provides a way to categorize content, manage access, and facilitate organization of user data.
## Architecture
The Spaces feature uses a centralized approach for data management with fast local access:
1. **Express Backend API**: Serves as the source of truth for space management
2. **Memoro App (Client)**: Interfaces with the Express Backend API
3. **Supabase Database**: Used by both the Express Backend and the app for data storage and retrieval
This architecture allows the app to perform operations through the Express Backend API, which then directly updates the Supabase database. This ensures a single source of truth while still enabling fast data access.
## Implementation Details
### Database Schema
In Memoro's local Supabase database:
```sql
-- Spaces table
CREATE TABLE public.spaces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT DEFAULT '',
color TEXT DEFAULT '#4CAF50',
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
is_default BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Junction table linking memos to spaces
CREATE TABLE public.memo_spaces (
memo_id UUID NOT NULL REFERENCES public.memos(id) ON DELETE CASCADE,
space_id UUID NOT NULL REFERENCES public.spaces(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (memo_id, space_id)
);
```
The schema includes foreign key constraints to ensure referential integrity between the spaces, memos, and user tables.
### API Integration
The app integrates with the Memoro API:
1. **API Endpoints**:
- `GET /api/memoro/spaces` - List all spaces for the authenticated user
- `GET /api/memoro/spaces/:id` - Get space details
- `POST /api/memoro/spaces` - Create a new space
- `PUT /api/memoro/spaces/:id` - Update a space
- `DELETE /api/memoro/spaces/:id` - Delete a space
- `GET /api/memoro/spaces/:spaceId/memos` - Get all memos for a specific space
- `POST /api/memoro/spaces/memos/link` - Link a memo to a space
- `DELETE /api/memoro/spaces/memos/unlink` - Unlink a memo from a space
### Space-Memo Relationship
Spaces can contain multiple memos, and memos can belong to multiple spaces. This is a many-to-many relationship implemented through the `memo_spaces` junction table. The API provides specific endpoints to manage these relationships:
- **Retrieving memos in a space**: Using `GET /api/memoro/spaces/:spaceId/memos`
- **Linking a memo to a space**: Using `POST /api/memoro/spaces/memos/link`
- **Unlinking a memo from a space**: Using `DELETE /api/memoro/spaces/memos/unlink`
### Data Structures
The Space interface reflects the updated API response structure:
```typescript
export interface Space {
id: string;
name: string;
description?: string;
memoCount?: number;
isDefault?: boolean;
color?: string;
created_at?: string;
updated_at?: string;
owner_id?: string;
app_id?: string;
credits?: number;
roles?: {
members: {
[userId: string]: {
role: string;
added_at: string;
added_by: string;
};
};
};
apps?: {
name: string;
slug: string;
};
}
```
And the Memo interface for space-specific memos:
```typescript
export interface Memo {
id: string;
title: string;
user_id: string;
source?: any;
style?: any;
is_pinned?: boolean;
is_archived?: boolean;
is_public?: boolean;
metadata?: any;
created_at?: string;
updated_at?: string;
}
```
### Authentication
Custom JWT-based authentication is used throughout:
1. User authenticates with the Memoro middleware (mana-core)
2. JWT token contains a `sub` claim with the user ID
3. This token is used for all API requests
4. All API endpoints require a valid JWT token
### Client Implementation
The app uses:
- `spaceService.ts`: Service for space operations, including memo management
- `useSpaces.ts`: Hook for React components
- `SpaceContext.tsx`: Context provider for the app
- `SpacesScreen.tsx`: Main UI for space management
- `[id].tsx`: Space detail view showing space info and all associated memos
## User Interface
The Spaces UI includes:
### Spaces List Screen
- List of spaces with customizable colors
- Create/edit/delete functionality
- Space selection for filtering memos
- Visual indication of the active space
### Space Detail Screen
- Shows space details (name, description, color, etc.)
- Displays all memos associated with the space
- Supports adding new memos to the space
- Handles error cases gracefully with retry mechanisms
## Error Handling
The system handles several error cases:
- Network failures during API requests
- Authentication errors
- Permission issues
- Database constraints
Error handling improvements:
- Error messages are properly displayed in the UI
- Retry mechanisms for failed requests
- Cleanup functions to prevent memory leaks and state updates after component unmount
- Mock data support for development/testing
### Error Handling Example
The Space detail view implements robust error handling:
```typescript
// Error handling for memo loading
useEffect(() => {
// Create a flag to prevent state updates after unmount
let isMounted = true;
const fetchSpaceMemos = async () => {
if (!id) return;
try {
setLoadingMemos(true);
setMemosError(null);
// Fetch memos for this space
const memos = await spaceContext.getSpaceMemos(id);
// Only update state if component is still mounted
if (isMounted) {
setSpaceMemos(memos);
}
} catch (err) {
console.error('Error fetching space memos:', err);
// Only update state if component is still mounted
if (isMounted) {
setMemosError('Failed to load memos for this space');
}
} finally {
// Only update state if component is still mounted
if (isMounted) {
setLoadingMemos(false);
}
}
};
fetchSpaceMemos();
// Cleanup function to prevent state updates after unmount
return () => {
isMounted = false;
};
}, [id]);
```
UI for error states:
```jsx
{loadingMemos ? (
<LoadingIndicator />
) : memosError ? (
<ErrorView
message={memosError}
onRetry={() => {
setLoadingMemos(true);
setMemosError(null);
// Re-render triggers the useEffect again
}}
/>
) : (
<MemoList memos={spaceMemos} spaceId={id} />
)}
```
## Development and Testing Support
The implementation includes development aids:
```typescript
// Mock data support for development
async getSpaceMemos(spaceId: string): Promise<Memo[]> {
// For development/testing, use mock data if API is not available
const USE_MOCK_DATA = process.env.EXPO_PUBLIC_USE_MOCK_DATA === 'true';
if (USE_MOCK_DATA) {
console.debug('Using mock data for space memos');
return [];
}
// Regular API call implementation
// ...
}
```
## Navigation
Navigation between spaces-related screens:
- From spaces list to space detail view via `router.push(`/(protected)/(space)/${spaceId}`)`
- Add memo to space via navigation to memo creation with space ID in params
- Back to spaces list via built-in back navigation
## Future Enhancements
Potential improvements for the spaces feature:
1. **Offline Support**: Caching space data for offline use
2. **Batch Operations**: Moving multiple memos between spaces
3. **Advanced Filtering**: Filter memos by multiple spaces or other criteria
4. **Space Sharing**: Allowing users to share spaces with other users
5. **Nested Spaces**: Implementing hierarchical space organization
## Maintenance and Troubleshooting
Common issues and solutions:
1. **API Communication Errors**: Check console logs for detailed error messages
2. **Authentication Issues**: Verify JWT token format and claims
3. **Performance Concerns**: Monitor API call performance
4. **Mock Data**: Use `EXPO_PUBLIC_USE_MOCK_DATA=true` for testing without API

View file

@ -0,0 +1,120 @@
# Spaces Implementation Tasks
This document outlines the remaining implementation tasks for the Memoro Spaces feature.
## Core API Integration
### Backend Integration Tasks
- [ ] Configure space-memo relationship API endpoints on the backend
- [ ] Set up proper authentication for all space-related endpoints
- [ ] Implement database triggers for space deletion cleanup
### Frontend Integration Tasks
- [x] Update space service to use the new API endpoints
- [x] Add memo-space linking/unlinking functionality
- [x] Create space selector component for memos
- [ ] Implement backend API integration tests
## UI Enhancements
### Space List View
- [x] Update spaces list with proper memo counts
- [x] Implement space deletion with confirmation
- [ ] Add space color selection in creation/edit flow
- [ ] Implement space sorting options (by name, date, memo count)
### Space Detail View
- [x] Display space details with associated memos
- [ ] Add space editing functionality
- [ ] Implement memo filtering within a space
- [ ] Add space statistics section (creation date, memo count, etc.)
### Memo-Space Management
- [x] Add space management option to memo menu
- [x] Implement space selection modal for memos
- [ ] Show space tags on memo list items
- [ ] Add batch operations for memo-space management
## Data Management
### Local Storage
- [ ] Implement caching for spaces data
- [ ] Add offline support for basic space operations
- [ ] Create migration path for existing data
### Synchronization
- [ ] Implement proper error handling for failed sync operations
- [ ] Add background sync for space-memo relationships
- [ ] Create conflict resolution strategy for simultaneous edits
## Performance Optimization
### Efficiency Improvements
- [ ] Optimize space listing for large numbers of spaces
- [ ] Implement pagination for memos within a space
- [ ] Add lazy loading for space contents
### Memory Management
- [ ] Optimize space selector component to handle large numbers of spaces
- [ ] Implement memory-efficient rendering for space-memo relationships
## Testing & Quality Assurance
### Automated Tests
- [ ] Write unit tests for space service methods
- [ ] Create integration tests for space-memo relationships
- [ ] Implement UI component tests for space-related components
### Manual Testing
- [x] Create comprehensive testing guide
- [ ] Test on multiple device sizes (mobile, tablet)
- [ ] Verify proper handling of edge cases
- [ ] Create test data generator for spaces testing
## Documentation
### User Documentation
- [ ] Create user guide for spaces feature
- [ ] Add tooltips and help text for space operations
- [ ] Document limitations and best practices
### Developer Documentation
- [x] Update API documentation with new endpoints
- [x] Document component architecture
- [ ] Create troubleshooting guide for common issues
- [ ] Add JSDoc comments to all space-related methods
## Next Iteration Features
### Future Enhancements
- [ ] Implement space sharing between users
- [ ] Add nested spaces/subspaces
- [ ] Create space templates
- [ ] Implement space archiving
- [ ] Add advanced space statistics and analytics
- [ ] Create space export/import functionality
## Known Issues
- The space selector may not refresh properly after a memo is linked/unlinked
- Space deletion does not verify if memos would be orphaned
- Mock data mode has limitations for full testing of space functionality
- Error states need more comprehensive handling and recovery
## Implementation Notes
### Key Components
- `SpaceService`: API integration for spaces
- `SpaceContext`: Context provider for spaces state
- `SpaceSelector`: Component for selecting spaces for a memo
- `SpaceLinkSelector`: Modal for linking memos to spaces
### State Management
- Spaces state is managed through the `SpaceContext`
- Space-memo relationships use stateful operations with optimistic updates
- Error handling includes retry mechanisms and user feedback
### API Requirements
- All endpoints require a valid JWT token
- Error responses follow standard HTTP status codes
- Successful operations return appropriate confirmation data

View file

@ -0,0 +1,153 @@
# Memoro Spaces Testing Guide
This guide outlines the steps to test the spaces functionality in the Memoro app.
## Test Environment Setup
Before testing, ensure you have:
- A development build of the Memoro app running
- At least one test user account
- A few test memos created
You can enable mock data for testing without a backend by setting the environment variable:
```
EXPO_PUBLIC_USE_MOCK_DATA=true
```
## Testing Areas
### 1. Spaces List View
#### Create a New Space
- Navigate to the Spaces tab
- Tap "Create New Space" button
- Enter a space name (e.g., "Test Space")
- Tap "Create"
- **Expected**: A new space should appear in the list
#### Space List Display
- Check that the spaces list shows:
- Space name
- Description (if any)
- Color indicator
- Memo count
- **Expected**: All spaces should be listed with correct information
#### Delete a Space
- Long press or tap on a space to open the action menu
- Select "Delete" from the menu
- Confirm deletion in the alert dialog
- **Expected**:
- A confirmation dialog should show the space name
- After confirmation, the space should be removed from the list
- Success message should appear
### 2. Space Detail View
#### Navigate to Space Detail
- Tap on a space in the spaces list
- Select "View Details" from the menu
- **Expected**: Space detail screen should open showing space information
#### Space Information Display
- Check that the space detail view shows:
- Space name
- Description
- Color indicator
- Memo count
- **Expected**: All information should be displayed correctly
#### View Memos in a Space
- Scroll down to see the "Memos in this Space" section
- **Expected**: All memos associated with the space should be listed
- **Expected**: If no memos are associated, an appropriate message should be shown
#### Add New Memo to Space
- Tap "Create New Memo" button
- **Expected**: Navigation to memo creation screen
- **Note**: This may not be fully implemented yet
### 3. Memo-Space Management
#### Link a Memo to Spaces
- Navigate to a memo detail view
- Tap the menu (three dots) icon
- Select "Manage Spaces" from the menu
- Check/uncheck spaces to link/unlink the memo
- Tap "Save"
- **Expected**:
- Space selector modal should open showing all available spaces
- Spaces should be checkable/uncheckable
- After saving, changes should persist
#### Unlink a Memo from Spaces
- Navigate to a memo detail view
- Tap the menu icon
- Select "Manage Spaces"
- Uncheck a currently checked space
- Tap "Save"
- **Expected**: The memo should no longer be associated with the unchecked space
#### Verify Space-Memo Relationships
- Link a memo to a space
- Navigate to that space's detail view
- **Expected**: The linked memo should appear in the space's memo list
- Unlink the memo from the space
- **Expected**: The memo should no longer appear in the space's memo list
### 4. Error Handling
#### Network Errors
- Turn off internet connection
- Attempt to create a space or link a memo to a space
- **Expected**: Appropriate error message should be displayed
- **Expected**: The app should not crash
#### Invalid Operations
- Try to delete a space that has memos (if that's restricted)
- **Expected**: Appropriate warning or confirmation message
#### Recovery from Errors
- Cause an error (e.g., by network disconnect)
- Restore connectivity
- Retry the operation
- **Expected**: Operation should succeed after retry
## Edge Cases
### Space with Many Memos
- Create a space
- Link 10+ memos to it
- View the space detail
- **Expected**: All memos should load and display correctly
### Long Names and Descriptions
- Create a space with a very long name (>50 characters)
- Add a long description (>200 characters)
- View in both list and detail views
- **Expected**: Text should be properly truncated or wrapped
### Empty States
- Delete all spaces
- Check spaces list
- **Expected**: An appropriate "no spaces" message should be displayed
- Create a space with no memos
- View space detail
- **Expected**: An appropriate "no memos" message should be displayed
## Known Limitations
- Mock data mode does not fully simulate the linking/unlinking behavior
- Some UI elements may not be fully responsive on all device sizes
- Space deletion might not properly handle linked memos in some edge cases
## Reporting Issues
If you encounter any issues during testing, please document:
1. The steps to reproduce the issue
2. What you expected to happen
3. What actually happened
4. Any error messages displayed
5. Screenshots if applicable
Report these issues to the development team via the project management system.

View file

@ -0,0 +1,286 @@
# Supabase Realtime RLS Workaround: Broadcast Channels
## Problem Statement
When using Supabase Edge Functions with `service_role` keys to update database records, the updates bypass Row Level Security (RLS) policies. However, Supabase Realtime **still respects RLS policies** even for service_role operations. This means:
- ✅ Database updates work (service_role bypasses RLS)
- ❌ Realtime subscriptions don't receive updates (Realtime respects RLS)
This is a known limitation documented in [Supabase GitHub issue #226](https://github.com/supabase/realtime/issues/226).
### Symptoms
- Edge functions successfully update records in the database
- Client-side realtime subscriptions don't receive these updates
- Updates only appear when manually refreshing data or switching views
- Problem occurs specifically when edge functions use `service_role` for operations
## Solution: Broadcast Channels
Use Supabase Broadcast Channels to manually notify clients when edge functions make updates. This creates a parallel communication channel that bypasses the RLS limitation.
## Implementation Guide
### 1. Client-Side: Create a Broadcast Subscription Service
```typescript
// memoRealtimeService.ts
class MemoRealtimeService {
private supabaseClient: any = null;
/**
* Subscribe to a broadcast channel for receiving updates
* This is useful for receiving updates from service_role operations that bypass RLS
*/
subscribeToBroadcastChannel(
channelName: string,
callback: (payload: any) => void
): () => void {
if (!this.supabaseClient) {
console.warn('No authenticated client available for broadcast subscription');
return () => {};
}
console.log(`Subscribing to broadcast channel: ${channelName}`);
const channel = this.supabaseClient.channel(channelName);
channel
.on('broadcast', { event: '*' }, (payload: any) => {
console.log(`Broadcast received on ${channelName}:`, payload);
callback(payload);
})
.subscribe((status: string) => {
console.log(`Broadcast channel ${channelName} status:`, status);
});
// Return unsubscribe function
return () => {
console.log(`Unsubscribing from broadcast channel: ${channelName}`);
channel.unsubscribe();
};
}
/**
* Get current data without subscription
*/
async getCurrentMemoData(memoId: string): Promise<any | null> {
if (!this.supabaseClient) {
return null;
}
try {
const { data: memo, error } = await this.supabaseClient
.from('memos')
.select('*')
.eq('id', memoId)
.single();
if (error) {
console.error('Error fetching memo data:', error);
return null;
}
return memo;
} catch (error) {
console.error('Error in getCurrentMemoData:', error);
return null;
}
}
}
```
### 2. Client-Side: Subscribe to Updates for Specific Records
```typescript
// In your React component or similar
useEffect(() => {
if (memoId) {
// Subscribe to broadcast channel for this specific memo
const broadcastUnsubscribe = memoRealtimeService.subscribeToBroadcastChannel(
`memo-updates-${memoId}`,
async (payload) => {
console.log('📡 Broadcast update received:', payload);
// Handle the nested payload structure from broadcast
const broadcastData = payload.payload || payload;
if (broadcastData.type === 'memo-updated' && broadcastData.memoId === memoId) {
// Fetch fresh data when broadcast is received
const freshData = await memoRealtimeService.getCurrentMemoData(memoId);
if (freshData) {
// Update your local state with fresh data
updateLocalState(freshData);
}
}
}
);
// Cleanup on unmount
return () => {
broadcastUnsubscribe();
};
}
}, [memoId]);
```
### 3. Edge Function: Send Broadcast After Updates
```typescript
// In your Supabase Edge Function
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const supabase = createClient(SUPABASE_URL, SERVICE_KEY);
// After updating the database record
const { error: updateError } = await supabase
.from('memos')
.update({
title: newTitle,
updated_at: new Date().toISOString()
})
.eq('id', memoId);
if (!updateError) {
// Send broadcast update to notify clients
try {
const channel = supabase.channel(`memo-updates-${memoId}`);
// Subscribe first to ensure the channel is ready
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.send({
type: 'broadcast',
event: 'memo-updated',
payload: {
type: 'memo-updated',
memoId: memoId,
changes: {
title: newTitle,
updated_at: new Date().toISOString()
},
source: 'your-edge-function-name'
}
});
console.log(`Broadcast sent for memo ${memoId} update`);
// Clean up the channel after sending
supabase.removeChannel(channel);
}
});
} catch (broadcastError) {
console.warn('Failed to send broadcast update:', broadcastError);
// Don't fail the function if broadcast fails
}
}
```
## Key Considerations
### 1. Channel Naming Convention
Use a consistent naming pattern for channels:
- `{resource}-updates-{resourceId}` (e.g., `memo-updates-123`)
- This allows targeted updates for specific records
### 2. Payload Structure
Supabase broadcasts wrap payloads in an extra level. The actual structure is:
```json
{
"event": "memo-updated",
"type": "broadcast",
"payload": {
"type": "memo-updated",
"memoId": "123",
"changes": {...},
"source": "edge-function-name"
}
}
```
Always access the nested `payload.payload` in your client code.
### 3. Channel Cleanup
**Important**: Always clean up channels to prevent memory leaks:
- Client-side: Return and call the unsubscribe function
- Edge functions: Call `supabase.removeChannel(channel)` after sending
### 4. Error Handling
- Broadcast failures should not fail your edge function
- Wrap broadcast logic in try-catch blocks
- Log failures for debugging but continue execution
### 5. Subscribe Before Sending
In edge functions, subscribe to the channel before sending to ensure it's ready:
```typescript
channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Now safe to send
}
});
```
## Alternative Approaches
### 1. User Context Switching (Not Recommended)
Some suggest switching to user context in edge functions, but this:
- Requires passing user tokens to edge functions
- Adds complexity and security concerns
- May not work for all use cases
### 2. Separate Notification Table
Create a separate table without RLS for notifications:
- More complex to maintain
- Requires additional database operations
- The broadcast approach is cleaner
### 3. Polling (Not Recommended)
Periodically fetch data from the client:
- Inefficient and wastes resources
- Poor user experience with delays
- Should be avoided
## Testing the Implementation
1. **Verify Broadcast Reception**:
```typescript
// Add detailed logging
console.log('📡 Broadcast update received:', JSON.stringify(payload, null, 2));
```
2. **Check Channel Status**:
```typescript
.subscribe((status: string) => {
console.log(`Channel status: ${status}`);
// Should see: SUBSCRIBED
});
```
3. **Monitor Edge Function Logs**:
- Verify "Broadcast sent" messages appear
- Check for any error messages
## Common Issues and Solutions
### Issue: Updates Not Received
- **Check**: Is the channel name consistent between sender and receiver?
- **Check**: Are you accessing `payload.payload` for the nested structure?
- **Check**: Is the channel subscription active before sending?
### Issue: Memory Leaks
- **Solution**: Always cleanup channels
- **Solution**: Use unique channel names per resource
- **Solution**: Implement proper unsubscribe logic
### Issue: Delayed Updates
- **Solution**: Ensure edge function subscribes before sending
- **Solution**: Add small delay after subscription if needed
- **Solution**: Check network latency
## Example: Complete Implementation
See the Memoro app implementation:
- Client: `/app/(protected)/(tabs)/index.tsx`
- Service: `/features/memos/services/memoRealtimeService.ts`
- Edge Functions: `/supabase/functions/headline/index.ts`
This pattern can be adapted for any Supabase project facing RLS limitations with service_role operations.

View file

@ -0,0 +1,821 @@
# Supported Languages in Memoro
Memoro supports **48 languages** for the user interface. Below is the complete list of supported languages with their native names and flags.
## European Languages (30)
1. <<EFBFBD><<EFBFBD> **German** - Deutsch (de)
2. <<EFBFBD><<EFBFBD> **Swiss German** - Schwizerd<72>tsch (ch)
3. <<EFBFBD><<EFBFBD> **Austrian German** - <20>sterreichisch (at)
4. <<EFBFBD><<EFBFBD> **English** - English (en)
5. <<EFBFBD><<EFBFBD> **Norwegian** - Norsk (nb)
6. <<EFBFBD><<EFBFBD> **Italian** - Italiano (it)
7. <<EFBFBD><<EFBFBD> **French** - Fran<61>ais (fr)
8. <<EFBFBD><<EFBFBD> **Spanish** - Espa<70>ol (es)
9. <<EFBFBD><<EFBFBD> **Mexican Spanish** - Espa<70>ol (M<>xico) (es-MX)
10. <<EFBFBD><<EFBFBD> **Dutch** - Nederlands (nl)
11. <<EFBFBD><<EFBFBD> **Swedish** - Svenska (sv)
12. <<EFBFBD><<EFBFBD> **Danish** - Dansk (da)
13. <<EFBFBD><<EFBFBD> **Finnish** - Suomi (fi)
14. <<EFBFBD><<EFBFBD> **Czech** - eatina (cs)
15. <<EFBFBD><<EFBFBD> **Polish** - Polski (pl)
16. <<EFBFBD><<EFBFBD> **Ukrainian** - #:@0W=AL:0 (uk)
17. <<EFBFBD><<EFBFBD> **Serbian** - !@?A:8 (sr)
18. <<EFBFBD><<EFBFBD> **Croatian** - Hrvatski (hr)
19. <<EFBFBD><<EFBFBD> **Hungarian** - Magyar (hu)
20. <<EFBFBD><<EFBFBD> **Greek** - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> (el)
21. <<EFBFBD><<EFBFBD> **Lithuanian** - Lietuvis (lt)
22. <<EFBFBD><<EFBFBD> **Latvian** - Latvieau (lv)
23. <<EFBFBD><<EFBFBD> **Slovak** - Sloven
ina (sk)
24. <<EFBFBD><<EFBFBD> **Slovenian** - Slovena
ina (sl)
25. <<EFBFBD><<EFBFBD> **Romanian** - Rom<6F>n (ro)
26. <<EFBFBD><<EFBFBD> **Portuguese** - Portugu<67>s (pt)
27. <<EFBFBD><<EFBFBD> **Brazilian Portuguese** - Portugu<67>s (Brasil) (pt-BR)
28. <<EFBFBD><<EFBFBD> **Maltese** - Malti (mt)
29. <<EFBFBD><<EFBFBD> **Bulgarian** - J;30@A:8 (bg)
30. <<EFBFBD><<EFBFBD> **Estonian** - Eesti (et)
31. <<EFBFBD><<EFBFBD> **Irish** - Gaeilge (ga)
32. <<EFBFBD><<EFBFBD> **Russian** - CAA:89 (ru)
## Asian Languages (14)
33. <<EFBFBD><<EFBFBD> **Chinese** - -<2D> (zh)
34. <<EFBFBD><<EFBFBD> **Korean** - \m<> (ko)
35. <<EFBFBD><<EFBFBD> **Japanese** - <20>,<2C> (ja)
36. <<EFBFBD><<EFBFBD> **Turkish** - T<>rk<72>e (tr)
37. <<EFBFBD><<EFBFBD> **Hindi** - 9?(M&@ (hi)
38. <<EFBFBD><<EFBFBD> **Bengali** - <20><><EFBFBD><EFBFBD><EFBFBD> (bn)
39. <<EFBFBD><<EFBFBD> **Urdu** - '1/H (ur)
40. <<EFBFBD><<EFBFBD> **Indonesian** - Bahasa Indonesia (id)
41. <<EFBFBD><<EFBFBD> **Persian/Farsi** - A'13<31> (fa)
42. <<EFBFBD><<EFBFBD> **Vietnamese** - Ti<54>ng Vi<56>t (vi)
43. <<EFBFBD><<EFBFBD> **Thai** - D" (th)
44. <<EFBFBD><<EFBFBD> **Filipino/Tagalog** - Filipino (tl)
45. <<EFBFBD><<EFBFBD> **Malaysian** - Bahasa Melayu (ms)
## Middle Eastern Languages (2)
46. <<EFBFBD><<EFBFBD> **Arabic** - 'D91(J) (ar)
47. <<EFBFBD><<EFBFBD> **Hebrew** - <20><><EFBFBD><EFBFBD><EFBFBD> (he)
## African Languages (1)
48. <<EFBFBD><<EFBFBD> **Afrikaans** - Afrikaans (af)
## Notes
- Language codes in parentheses (e.g., `de`, `en`) are the ISO 639-1 codes used internally
- Some languages like Swiss German (ch) and Austrian German (at) may fall back to standard German (de) for certain translations
- The app automatically detects the device's language and sets the appropriate language if supported
- Users can manually change the language in the app's settings
## Recent Additions
The following languages were recently added to expand our global reach:
- Brazilian Portuguese (pt-BR)
- Mexican Spanish (es-MX)
- Thai (th)
- Filipino/Tagalog (tl)
- Malaysian (ms)
- Hebrew (he) - UI translation (audio recording was already supported)
- Afrikaans (af)
# Azure Speech
The table in this section summarizes the locales supported for real-time speech to text, fast transcription, and batch transcription transcription.
More remarks for speech to text locales are included in the custom speech section of this article.
Tip
Try out the Azure AI Speech Toolkit to easily build and run samples on Visual Studio Code.
Locale (BCP-47) Language Fast transcription support Custom speech support
af-ZA Afrikaans (South Africa) No Plain text
am-ET Amharic (Ethiopia) No Plain text
ar-AE Arabic (United Arab Emirates) No Audio + human-labeled transcript
Plain text
ar-BH Arabic (Bahrain) No Audio + human-labeled transcript
Plain text
ar-DZ Arabic (Algeria) No Audio + human-labeled transcript
Plain text
ar-EG Arabic (Egypt) No Audio + human-labeled transcript
Plain text
Structured text
ar-IL Arabic (Israel) No Audio + human-labeled transcript
Plain text
ar-IQ Arabic (Iraq) No Audio + human-labeled transcript
Plain text
ar-JO Arabic (Jordan) No Audio + human-labeled transcript
Plain text
ar-KW Arabic (Kuwait) No Audio + human-labeled transcript
Plain text
ar-LB Arabic (Lebanon) No Audio + human-labeled transcript
Plain text
ar-LY Arabic (Libya) No Audio + human-labeled transcript
Plain text
ar-MA Arabic (Morocco) No Audio + human-labeled transcript
Plain text
ar-OM Arabic (Oman) No Audio + human-labeled transcript
Plain text
ar-PS Arabic (Palestinian Authority) No Audio + human-labeled transcript
Plain text
ar-QA Arabic (Qatar) No Audio + human-labeled transcript
Plain text
ar-SA Arabic (Saudi Arabia) Yes Audio + human-labeled transcript
Plain text
Structured text
Phrase list
ar-SY Arabic (Syria) No Audio + human-labeled transcript
Plain text
ar-TN Arabic (Tunisia) No Audio + human-labeled transcript
Plain text
ar-YE Arabic (Yemen) No Audio + human-labeled transcript
Plain text
as-IN Assamese (India) No Audio + human-labeled transcript
az-AZ Azerbaijani (Latin, Azerbaijan) No Plain text
bg-BG Bulgarian (Bulgaria) No Plain text
bn-IN Bengali (India) No Plain text
bs-BA Bosnian (Bosnia and Herzegovina) No Plain text
ca-ES Catalan No Plain text
Pronunciation
cs-CZ Czech (Czechia) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
cy-GB Welsh (United Kingdom) No Plain text
da-DK Danish (Denmark) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
de-AT German (Austria) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
de-CH German (Switzerland) No Audio + human-labeled transcript
Plain text
Pronunciation
Phrase list
de-DE German (Germany) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
el-GR Greek (Greece) No Audio + human-labeled transcript
Plain text
Structured text
en-AU English (Australia) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
Phrase list
en-CA English (Canada) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
Phrase list
en-GB English (United Kingdom) Yes Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
Phrase list
en-GH English (Ghana) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Pronunciation
en-HK English (Hong Kong SAR) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
en-IE English (Ireland) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
Phrase list
en-IN English (India) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
en-KE English (Kenya) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Pronunciation
en-NG English (Nigeria) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
en-NZ English (New Zealand) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
en-PH English (Philippines) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
en-SG English (Singapore) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
en-TZ English (Tanzania) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Pronunciation
en-US English (United States) Yes Audio + human-labeled transcript
Audio
Plain text
Structured text
Output format
Pronunciation
Phrase list
en-ZA English (South Africa) No Audio + human-labeled transcript
Audio
Plain text
Structured text
Pronunciation
Phrase list
es-AR Spanish (Argentina) No Plain text
Structured text
Pronunciation
es-BO Spanish (Bolivia) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-CL Spanish (Chile) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-CO Spanish (Colombia) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-CR Spanish (Costa Rica) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-CU Spanish (Cuba) No Plain text
Structured text
Pronunciation
es-DO Spanish (Dominican Republic) No Plain text
Structured text
Pronunciation
es-EC Spanish (Ecuador) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-ES Spanish (Spain) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
es-GQ Spanish (Equatorial Guinea) No Audio + human-labeled transcript
Plain text
Structured text
es-GT Spanish (Guatemala) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-HN Spanish (Honduras) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-MX Spanish (Mexico) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
es-NI Spanish (Nicaragua) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-PA Spanish (Panama) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-PE Spanish (Peru) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-PR Spanish (Puerto Rico) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-PY Spanish (Paraguay) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-SV Spanish (El Salvador) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-US Spanish (United States)1 No Plain text
Structured text
Pronunciation
Phrase list
es-UY Spanish (Uruguay) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
es-VE Spanish (Venezuela) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
et-EE Estonian (Estonia) No Plain text
Pronunciation
eu-ES Basque No Plain text
fa-IR Persian (Iran) No Plain text
fi-FI Finnish (Finland) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
fil-PH Filipino (Philippines) No Plain text
Pronunciation
fr-BE French (Belgium) No Plain text
fr-CA French (Canada)1 No Plain text
Structured text
Output format
Pronunciation
Phrase list
fr-CH French (Switzerland) No Plain text
Pronunciation
fr-FR French (France) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
ga-IE Irish (Ireland) No Plain text
Pronunciation
gl-ES Galician No Plain text
gu-IN Gujarati (India) No Plain text
he-IL Hebrew (Israel) Yes Audio + human-labeled transcript
Plain text
hi-IN Hindi (India) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Phrase list
hr-HR Croatian (Croatia) No Plain text
Pronunciation
hu-HU Hungarian (Hungary) No Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
hy-AM Armenian (Armenia) No Plain text
id-ID Indonesian (Indonesia) Yes Audio + human-labeled transcript
Plain text
Structured text
Pronunciation
Phrase list
is-IS Icelandic (Iceland) No Plain text
it-CH Italian (Switzerland) No Plain text
it-IT Italian (Italy) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
ja-JP Japanese (Japan) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Phrase list
jv-ID Javanese (Latin, Indonesia) No Plain text
ka-GE Georgian (Georgia) No Plain text
kk-KZ Kazakh (Kazakhstan) No Plain text
km-KH Khmer (Cambodia) No Plain text
kn-IN Kannada (India) No Plain text
ko-KR Korean (Korea) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Phrase list
lo-LA Lao (Laos) No Plain text
lt-LT Lithuanian (Lithuania) No Plain text
Pronunciation
lv-LV Latvian (Latvia) No Plain text
Pronunciation
mk-MK Macedonian (North Macedonia) No Plain text
ml-IN Malayalam (India) No Plain text
mn-MN Mongolian (Mongolia) No Plain text
mr-IN Marathi (India) No Plain text
ms-MY Malay (Malaysia) No Plain text
mt-MT Maltese (Malta) No Plain text
my-MM Burmese (Myanmar) No Plain text
nb-NO Norwegian Bokmål (Norway) No Plain text
Output format
ne-NP Nepali (Nepal) No Plain text
nl-BE Dutch (Belgium) No Plain text
nl-NL Dutch (Netherlands) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
or-IN Odia (India) No Audio + human-labeled transcript
pa-IN Punjabi (India) No Audio + human-labeled transcript
pl-PL Polish (Poland) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
ps-AF Pashto (Afghanistan) No Plain text
pt-BR Portuguese (Brazil) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
pt-PT Portuguese (Portugal) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
ro-RO Romanian (Romania) No Plain text
Pronunciation
ru-RU Russian (Russia) Yes Audio + human-labeled transcript
Plain text
Structured text
Phrase list
si-LK Sinhala (Sri Lanka) No Plain text
sk-SK Slovak (Slovakia) No Plain text
Pronunciation
sl-SI Slovenian (Slovenia) No Plain text
Pronunciation
so-SO Somali (Somalia) No Plain text
sq-AL Albanian (Albania) No Plain text
sr-RS Serbian (Cyrillic, Serbia) No Plain text
sv-SE Swedish (Sweden) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Pronunciation
Phrase list
sw-KE Kiswahili (Kenya) No Plain text
sw-TZ Kiswahili (Tanzania) No Plain text
ta-IN Tamil (India) No Audio + human-labeled transcript
Plain text
Structured text
te-IN Telugu (India) No Audio + human-labeled transcript
Plain text
Structured text
th-TH Thai (Thailand) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Phrase list
tr-TR Turkish (Türkiye) No Audio + human-labeled transcript
Plain text
Structured text
Output format
uk-UA Ukrainian (Ukraine) No Audio + human-labeled transcript
Plain text
ur-IN Urdu (India) No Audio + human-labeled transcript
uz-UZ Uzbek (Latin, Uzbekistan) No Plain text
vi-VN Vietnamese (Vietnam) No Plain text
Phrase list
wuu-CN Chinese (Wu, Simplified) No Plain text
yue-CN Chinese (Cantonese, Simplified) No Plain text
zh-CN Chinese (Mandarin, Simplified) Yes Audio + human-labeled transcript
Plain text
Structured text
Output format
Phrase list
zh-CN-shandong Chinese (Jilu Mandarin, Simplified) No Plain text
zh-CN-sichuan Chinese (Southwestern Mandarin, Simplified) No Plain text
zh-HK Chinese (Cantonese, Traditional) No Audio + human-labeled transcript
Plain text
Structured text
Output format
Phrase list
zh-TW Chinese (Taiwanese Mandarin, Traditional) No Audio + human-labeled transcript
Plain text
Structured text
Output format
Phrase list
zu-ZA isiZulu (South Africa) No Plain text
1 The model is bilingual and also supports English.
Custom speech
To improve speech to text recognition accuracy, customization is available for some languages and base models. Depending on the locale, you can upload audio + human-labeled transcripts, plain text, structured text, and pronunciation data. By default, plain text customization is supported for all available base models. To learn more about customization, see custom speech.
These locales support the display text format feature: da-DK, de-DE, en-AU, en-CA, en-GB, en-HK, en-IE, en-IN, en-NG, en-NZ, en-PH, en-SG, en-US, es-ES, es-MX, fi-FI, fr-CA, fr-FR, hi-IN, it-IT, ja-JP, ko-KR, nb-NO, nl-NL, pl-PL, pt-BR, pt-PT, sv-SE, tr-TR, zh-CN, zh-HK.

View file

@ -0,0 +1,480 @@
# 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:
1. User stops recording
2. Upload begins silently in the background
3. Memo appears in the list only after upload + processing completes
4. **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:
1. **Recording stops** → Upload starts
2. **Placeholder memo card appears immediately** with "Uploading..." badge
3. **Upload completes** → Backend creates real memo in database
4. **Realtime subscription fires** → Placeholder gets replaced with real memo
5. **Badge disappears** after 5 seconds (auto-cleanup)
## Architecture
### Components Modified
#### 1. **MemoStore** (`features/memos/store/memoStore.ts`)
**Changes:**
- Added `isPlaceholder` flag to `MemoItem` interface
- Added `audioFileId` to metadata for upload status tracking
- Added `setUploadingPlaceholder()` method (for future use)
- `setLatestMemo()` supports placeholder memos
**Key Code:**
```typescript
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:**
```typescript
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:**
```typescript
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 `useUploadProgress` hook and `UploadStatus` enum
- Added `audioFileId` to `MemoModel` interface
- Integrated upload progress hook
- Added upload status badge UI (appears below metadata row)
**Key Code:**
```typescript
// 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 `setLatestMemo` to memoStore destructuring
- Modified `uploadAudioRecording` to create placeholder memo
- Enhanced realtime INSERT handler to replace placeholders
**Placeholder Creation (lines 1021-1056):**
```typescript
// 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):**
```typescript
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 `audioFileId` field to `MemoMetadata` interface
```typescript
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:**
- `useUploadProgress` hook 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 `isPlaceholder` flag
- No race conditions between placeholder and real memo
### Edge Cases Handled
1. **Multiple recordings in quick succession:**
- Each gets unique `memoId` and `audioFileId`
- Placeholders tracked independently
- Realtime events match by `memoId`
2. **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.
3. **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.
4. **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 `memoId` in upload payload
- Backend creates memo with that `memoId` in Supabase
- Realtime subscription broadcasts INSERT event
❌ **NOT required:**
- Backend does NOT need to include `audioFileId` in memo metadata
- Frontend handles upload tracking entirely
- Separation of concerns maintained
## Testing Checklist
- [x] Placeholder appears immediately after recording stops
- [x] Upload badge shows "Uploading..." on placeholder
- [x] Placeholder gets replaced by real memo when upload completes
- [x] Badge shows "✓ Uploaded" briefly on real memo
- [x] Badge auto-disappears after 5 seconds
- [x] Error badge shows on upload failure
- [x] Multiple recordings work correctly
- [ ] App backgrounding doesn't break flow
- [ ] Network interruption handling
- [ ] Theme changes (light/dark mode) work correctly
## Future Enhancements
### Potential Improvements
1. **Progress Percentage:**
- Show actual upload progress (0-100%)
- Requires XMLHttpRequest or fetch with progress events
- See: `UPLOAD_FEEDBACK_IMPLEMENTATION.md` Strategy 2
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.
3. **Toast Notification:**
- Complementary feedback on upload start
- Auto-dismiss after 3 seconds
- More prominent for users who look away
4. **Animated Transitions:**
- Smooth fade from placeholder → real memo
- Skeleton loading state
- Progress bar animation
5. **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:**
1. `audioFileId` not passed to `uploadAudioRecording()`
2. `uploadStatusStore.updateStatus()` not called
3. `useUploadProgress` hook 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:**
1. Realtime subscription not active
2. `isPlaceholder` check not working
3. `loadLatestMemo()` 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:**
1. Upload status accumulating for deleted recordings
2. Upload status not being cleaned up when recordings deleted
**Solution:**
- Check `uploadStatusStore.statusMap.size` in console
- Upload status should be removed when recordings are deleted
- Status is automatically cleaned up in `handleDelete` function
- If issues persist, check that `removeStatus` is being called properly
## Related Documentation
- [Audio Recording V2 Documentation](../features/audioRecordingV2/README.md)
- [Storage Service Documentation](../features/storage/README.md)
- [Upload Status Types](../features/storage/uploadStatus.types.ts)
- [Memo Types](../features/memos/types/memo.types.ts)
## Contributors
- Implementation: Claude (AI Assistant)
- Product Requirements: @wuesteon
- Date: November 13, 2025

View file

@ -0,0 +1,585 @@
# Analytics Integration Documentation
## Overview
The Memoro app uses a multi-platform analytics system with **PostHog** for mobile (iOS/Android) and **Umami** for web. This document describes all tracked events, implementation details, and best practices.
## Architecture
### Service Layer
- **MultiPlatformAnalyticsService** (`/features/analytics/services/multiPlatformAnalytics.ts`)
- Routes analytics calls to the appropriate service based on platform
- PostHog for mobile (iOS/Android)
- Umami for web
### Key Components
- **AnalyticsProvider** - Initializes analytics on app start
- **AnalyticsNavigationTracker** - Automatic screen tracking via Expo Router
- **useAnalytics** - Hook for tracking events
- **useScreenTracking** - Hook for manual screen tracking
- **useFeatureFlag** - Hook for PostHog feature flags (mobile only)
## Configuration
### PostHog (Mobile) - v3.x
- **API Key**: Set in `EXPO_PUBLIC_POSTHOG_KEY`
- **Host**: `https://eu.i.posthog.com` (EU datacenter)
- **Auto-capture**: Enabled for lifecycle events
- **Debug Mode**: Enabled in development
- **Initialization**: Automatic in constructor (no `initAsync()` needed)
- **Methods**: Synchronous (no `async/await` required)
### Umami (Web)
- **Website ID**: Set in `EXPO_PUBLIC_UMAMI_WEBSITE_ID`
- **Script URL**: `https://umami.manacore.ai/script.js`
## Tracked Events
### 🎙️ Recording Events
#### `recording_started`
Fired when user starts a recording
```typescript
{
blueprint_id: string,
space_id: string | null,
languages: string[],
language_count: number,
is_append: boolean,
theme: 'light' | 'dark'
}
```
#### `recording_stopped`
Fired when recording is stopped (manual or automatic)
```typescript
{
duration_seconds: number,
reason: 'manual' | 'auto' | 'error',
blueprint_id: string | null,
space_id: string | null,
has_result: boolean
}
```
#### `recording_failed`
Fired when recording fails
```typescript
{
error_type: 'insufficient_credits' | 'permission_denied' | 'network_error',
blueprint_id: string | null,
space_id: string | null,
can_ask_again?: boolean // For permission errors
}
```
#### `recording_permission_retry`
Fired when user retries after permission denial
```typescript
{
blueprint_id: string | null,
space_id: string | null
}
```
### 📝 Memo Events
#### `memo_viewed`
Fired when a memo is opened
```typescript
{
memo_id: string,
source: 'direct_navigation' | 'list' | 'search',
is_translated: boolean
}
```
#### `memo_shared`
Fired when user shares a memo
```typescript
{
memo_id: string,
has_transcript: boolean,
has_memories: boolean
}
```
#### `memo_deleted`
Fired when a memo is deleted
```typescript
{
memo_id: string,
has_transcript: boolean,
duration_seconds: number | null
}
```
#### `memo_pinned` / `memo_unpinned`
Fired when pin status changes
```typescript
{
memo_id: string
}
```
#### `transcript_copied`
Fired when transcript is copied to clipboard
```typescript
{
memo_id: string,
transcript_length: number
}
```
### 📋 List & Search Events
#### `memo_list_filtered`
Fired when filters are applied to memo list
```typescript
{
filter_type: 'tags' | 'space' | 'date',
tag_count?: number,
tag_ids?: string[]
}
```
#### `memo_searched`
Fired when user performs a search
```typescript
{
query_length: number,
has_results: boolean
}
```
#### `memo_bulk_action`
Fired for bulk operations on multiple memos
```typescript
{
action: 'delete' | 'add_tag' | 'remove_tag' | 'move_space',
memo_count: number,
tag_id?: string,
space_id?: string
}
```
### 🤖 AI Processing Events
#### `blueprint_selected`
Fired when user selects a blueprint
```typescript
{
blueprint_id: string | null,
is_standard: boolean,
space_id: string | null
}
```
#### `blueprint_applied`
Fired when blueprint processing completes
```typescript
{
memo_id: string,
blueprint_id: string,
mana_cost: number,
processing_time_ms: number
}
```
#### `question_asked`
Fired when user asks a question about a memo
```typescript
{
memo_id: string,
question_length: number,
mana_cost: 5 // Fixed cost
}
```
#### `memos_combined`
Fired when multiple memos are combined
```typescript
{
memo_count: number,
blueprint_id: string,
has_prompt: boolean,
mana_cost: number // 5 per memo
}
```
#### `memo_reprocessed`
Fired when memo is reprocessed with new settings
```typescript
{
memo_id: string,
language: string,
has_blueprint: boolean,
blueprint_id?: string
}
```
#### `memo_translated`
Fired when memo is translated
```typescript
{
memo_id: string,
target_language: string
}
```
### 💳 Subscription & Credits Events
#### `subscription_page`
Screen view for subscription page
```typescript
{
is_b2b: boolean
}
```
#### `subscription_purchase_attempted`
Fired when user initiates purchase
```typescript
{
plan_id: string,
plan_type: 'individual' | 'team' | 'enterprise',
billing_cycle: 'monthly' | 'yearly',
credits: number,
price: number
}
```
#### `subscription_purchased`
Fired on successful purchase
```typescript
{
plan_id: string,
plan_type: 'individual' | 'team' | 'enterprise',
billing_cycle: 'monthly' | 'yearly',
credits: number,
price: number
}
```
#### `subscription_restore_attempted` / `subscription_restore_completed`
Fired during purchase restoration flow
### 📱 Screen Tracking
Automatic screen tracking via `AnalyticsNavigationTracker`:
- `recording_screen` - Home/Recording tab
- `memos_list` - Memos tab
- `memo_detail` - Individual memo view
- `subscription_page` - Subscription/credits page
- `settings` - Settings page
- `profile` - User profile
Each screen event includes:
```typescript
{
path: string, // URL path
timestamp: string,
focused_at?: string // For manual tracking
}
```
### ⚡ Performance Events
#### `memo_load_time`
Tracks memo loading performance
```typescript
{
memo_id: string,
duration_ms: number
}
```
### 🔥 Error Tracking
#### `error_occurred`
Generic error tracking
```typescript
{
error_name: string,
error_message: string,
error_stack?: string, // Only in dev
screen?: string,
action?: string,
user_id?: string
}
```
## Implementation Examples
### Basic Event Tracking
```typescript
import { useAnalytics } from '~/features/analytics';
function MyComponent() {
const { track } = useAnalytics();
const handleAction = () => {
// Note: track() is synchronous in PostHog v3
track('custom_event', {
property1: 'value',
property2: 123
});
};
}
```
### Screen Tracking
```typescript
import { useScreenTracking } from '~/features/analytics';
function MyScreen() {
// Automatic screen tracking
useScreenTracking('my_screen', {
tab: 'main',
source: 'navigation'
});
}
```
### Error Tracking
```typescript
import { trackError } from '~/features/analytics';
try {
// Some operation
} catch (error) {
trackError(track, error, {
screen: 'memo_detail',
action: 'load_data',
memo_id: id
});
}
```
### Performance Tracking
```typescript
import { trackPerformance } from '~/features/analytics';
const startTime = Date.now();
// ... operation ...
trackPerformance(track, 'operation_name', Date.now() - startTime, {
additional_context: 'value'
});
```
### Feature Flags (Mobile Only)
```typescript
import { useFeatureFlag } from '~/features/analytics';
function MyComponent() {
const showNewFeature = useFeatureFlag('new-feature-flag');
if (showNewFeature) {
return <NewFeature />;
}
return <OldFeature />;
}
```
## PostHog Dashboard Setup
### Recommended Dashboards
#### 1. User Journey Funnel
Create a funnel visualization:
1. `recording_started`
2. `recording_stopped`
3. `memo_viewed`
4. `memo_shared`
#### 2. Feature Adoption
Track usage of key features:
- Blueprint usage (non-standard selections)
- AI features (questions, reprocessing, translation)
- Collaboration (space usage, sharing)
#### 3. Error Monitoring
- Group `recording_failed` by `error_type`
- Track `error_occurred` by screen and action
- Monitor permission denial rates
#### 4. Performance Metrics
- `memo_load_time` percentiles (P50, P95, P99)
- Track by device type and network conditions
#### 5. Engagement Metrics
- Daily/Weekly/Monthly Active Users
- Session duration
- Actions per session
- Retention cohorts
### Key Metrics to Monitor
1. **Recording Success Rate**
- `recording_stopped` / `recording_started`
- Identify drop-off points
2. **AI Feature Usage**
- Questions per user
- Blueprint adoption rate
- Mana consumption patterns
3. **User Retention**
- Day 1, 7, 30 retention
- Feature-specific retention
4. **Error Rate**
- Errors per user
- Error types distribution
- Recovery success rate
## Privacy & Compliance
### Data Collected
- No PII in event properties by default
- User IDs are anonymized
- Device info for debugging
- Usage patterns and feature adoption
### GDPR Compliance
- PostHog hosted in EU (eu.i.posthog.com)
- User consent required before tracking
- Option to opt-out in settings
- Data retention policies applied
## Testing Analytics
### Development Testing
1. Enable debug mode: Events logged to console
2. Use PostHog Live Events view for real-time monitoring
3. Test event in dev settings: `/app/(protected)/settings.tsx`
4. PostHog v3: Methods are synchronous, no need for await
### Production Validation
1. Check PostHog dashboard for event flow
2. Verify event properties are correct
3. Monitor for any tracking errors
4. Validate funnel completion rates
## Troubleshooting
### Events Not Appearing
1. Check API key configuration
2. Verify PostHog instance is created successfully
3. Check network connectivity
4. Look for console errors
5. Ensure `flush()` is called in dev (synchronous)
### PostHog v3 Specific Issues
- **`initAsync is not a function`**: PostHog v3 doesn't have `initAsync()`. Initialization happens in the constructor.
- **Async methods**: All PostHog v3 methods are synchronous. Don't use `await`.
- **TypeScript errors**: Ensure types match synchronous signatures (return `void` not `Promise<void>`)
### Missing Properties
1. Verify property names match schema
2. Check for undefined values
3. Ensure proper async/await usage
### Performance Issues
1. Batch events when possible
2. Use sampling for high-frequency events
3. Optimize property payload size
## Future Enhancements
### Planned Additions
- [ ] Session replay integration
- [ ] Heatmaps for UI interactions
- [ ] Custom user properties
- [ ] Revenue tracking
- [ ] A/B testing framework
- [ ] Crash reporting integration
- [ ] Network performance tracking
- [ ] Battery usage monitoring
### Potential Events
- Audio playback events (play, pause, seek)
- Memory creation/edit events
- Tag management events
- Space collaboration events
- Network status changes
- App background/foreground transitions
## Contact & Support
For analytics questions or to request new events:
- Create an issue in the repository
- Contact the development team
- Check PostHog documentation: https://posthog.com/docs
## Technical Implementation Details
### PostHog v3 React Native SDK
#### Initialization
```typescript
// PostHog v3 - Initialization happens in constructor
this.posthog = new PostHog(apiKey, {
host: 'https://eu.i.posthog.com',
debug: __DEV__,
captureApplicationLifecycleEvents: true,
captureDeepLinks: true,
recordScreenViews: true,
flushInterval: 30,
flushAt: 20,
maxQueueSize: 1000,
disableAutoCapture: false,
enableLogs: __DEV__,
});
// No initAsync() needed - ready to use immediately
```
#### Method Signatures (Synchronous)
```typescript
// All methods are synchronous in v3
posthog.identify(userId: string, properties?: object): void
posthog.capture(event: string, properties?: object): void
posthog.screen(name: string, properties?: object): void
posthog.reset(): void
posthog.flush(): void
```
#### Common Pitfalls
- ❌ Don't use `await posthog.initAsync()` - doesn't exist
- ❌ Don't use `await posthog.capture()` - synchronous
- ✅ Do use `posthog.flush()` in dev for immediate sending
- ✅ Do handle errors with try-catch around constructor
### Service Architecture
```typescript
// analyticsService.ts
class PostHogAnalyticsService implements AnalyticsService {
private posthog: any = null;
private initialized = false;
async initialize() {
// Create instance (initialization happens here)
this.posthog = new PostHog(key, options);
this.initialized = true;
// Test event with small delay
setTimeout(() => {
this.posthog.capture('posthog_initialized', {...});
}, 100);
}
// All methods are synchronous wrappers
track(event: string, properties?: object): void {
if (!this.initialized) return;
this.posthog.capture(event, properties);
if (__DEV__) this.posthog.flush();
}
}
```
---
*Last updated: 2025-01-09*
*Version: 1.1.0*
*PostHog SDK: v3.x*

View file

@ -0,0 +1,149 @@
# Memoro Blueprints und Prompts Dokumentation
Diese Dokumentation enthält eine Übersicht aller Blueprints und Prompts in der Memoro-Datenbank, sowie deren Verknüpfungen.
## Inhaltsverzeichnis
- [Blueprints](#blueprints)
- [Textanalyse / Text Analysis](#textanalyse--text-analysis)
- [Kreatives Schreiben / Creative Writing](#kreatives-schreiben--creative-writing)
- [Prompts](#prompts)
- [Blueprint-Prompt Verknüpfungen](#blueprint-prompt-verknüpfungen)
## Blueprints
In der Datenbank sind aktuell 2 Blueprints vorhanden:
| ID | Name (DE) | Name (EN) | Öffentlich | Erstellt am |
|----|-----------|-----------|------------|-------------|
| c05913ba-d062-477b-b3a4-183aed7f655a | Textanalyse | Text Analysis | Ja | 2025-05-08 11:09:22 |
| e7f9a2d8-3b45-4c12-9876-5432abcdef01 | Kreatives Schreiben | Creative Writing | Ja | 2025-05-15 23:20:58 |
### Textanalyse / Text Analysis
**ID:** c05913ba-d062-477b-b3a4-183aed7f655a
**Beschreibung:**
- DE: "Blueprint für die Analyse und Zusammenfassung von Texten"
- EN: "Blueprint for analyzing and summarizing texts"
**Advice-Tipps:**
- Metadata:
- Version: 1.0
- Zuletzt aktualisiert: 2025-05-16T01:14:51+02:00
- Unterstützte Sprachen: Deutsch, Englisch
| Tipp-ID | Reihenfolge | Inhalt (DE) | Inhalt (EN) |
|---------|-------------|-------------|-------------|
| tip1 | 1 | Sprechen Sie klar und deutlich, um eine präzise Textanalyse zu ermöglichen. | Speak clearly and distinctly to enable precise text analysis. |
| tip2 | 2 | Strukturieren Sie Ihre Gedanken in logische Abschnitte für bessere Übersichtlichkeit. | Structure your thoughts into logical sections for better clarity. |
| tip3 | 3 | Fassen Sie wichtige Punkte zusammen und heben Sie Schlüsselerkenntnisse hervor. | Summarize important points and highlight key insights. |
| tip4 | 4 | Verwenden Sie Schlüsselwörter für eine bessere Kategorisierung Ihrer Texte. | Use keywords for better categorization of your texts. |
**Verknüpfte Prompts:**
- Zusammenfassung / Summary
- To Dos / To Dos
### Kreatives Schreiben / Creative Writing
**ID:** e7f9a2d8-3b45-4c12-9876-5432abcdef01
**Beschreibung:**
- DE: "Entwickle kreative Texte, Geschichten und Ideen für verschiedene Formate"
- EN: "Develop creative texts, stories and ideas for various formats"
**Advice-Tipps:**
- Metadata:
- Version: 1.0
- Zuletzt aktualisiert: 2025-05-16T01:20:19+02:00
- Unterstützte Sprachen: Deutsch, Englisch
| Tipp-ID | Reihenfolge | Inhalt (DE) | Inhalt (EN) |
|---------|-------------|-------------|-------------|
| tip1 | 1 | Beschreibe deine Ideen detailliert mit lebendigen Bildern und konkreten Details. | Describe your ideas in detail with vivid imagery and concrete details. |
| tip2 | 2 | Nutze verschiedene Perspektiven, um deine Geschichte aus unterschiedlichen Blickwinkeln zu betrachten. | Use different perspectives to view your story from various angles. |
| tip3 | 3 | Experimentiere mit verschiedenen Erzählstilen und finde deinen eigenen kreativen Ausdruck. | Experiment with different narrative styles and find your own creative expression. |
| tip4 | 4 | Achte auf den Rhythmus deiner Sprache - kurze Sätze für Spannung, längere für ruhige Momente. | Pay attention to the rhythm of your language - short sentences for tension, longer ones for calm moments. |
**Verknüpfte Prompts:**
- Kurzgeschichte / Short Story
- Charakterprofil / Character Profile
- Szenenbeschreibung / Scene Description
- Dialog / Dialogue
- Gedicht/Songtext / Poem/Lyrics
## Prompts
In der Datenbank sind aktuell 7 Prompts vorhanden:
| ID | Titel (DE) | Titel (EN) | Öffentlich | Erstellt am |
|----|------------|------------|------------|-------------|
| 9043d4d0-86b0-41b7-b5d7-7077e5cabacd | Zusammenfassung | Summary | Ja | 2025-05-08 11:04:22 |
| da7bca61-a5c1-48f9-910b-b6eadcbcab17 | To Dos | To Dos | Ja | 2025-05-08 11:06:08 |
| a1b2c3d4-e5f6-4a3b-8c7d-9e0f1a2b3c4d | Kurzgeschichte | Short Story | Ja | 2025-05-15 23:21:14 |
| b2c3d4e5-f6a7-5b4c-9d8e-0f1a2b3c4d5e | Charakterprofil | Character Profile | Ja | 2025-05-15 23:21:14 |
| c3d4e5f6-a7b8-6c5d-0e9f-1a2b3c4d5e6f | Szenenbeschreibung | Scene Description | Ja | 2025-05-15 23:21:14 |
| d4e5f6a7-b8c9-7d6e-1f0a-2b3c4d5e6f7a | Dialog | Dialogue | Ja | 2025-05-15 23:21:14 |
| e5f6a7b8-c9d0-8e7f-2a1b-3c4d5e6f7a8b | Gedicht/Songtext | Poem/Lyrics | Ja | 2025-05-15 23:21:14 |
### Detaillierte Prompt-Informationen
#### Zusammenfassung / Summary
- **ID:** 9043d4d0-86b0-41b7-b5d7-7077e5cabacd
- **Prompt-Text:**
- DE: "Fasse den folgenden Text ausführlich zusammen"
- EN: "Summarize the following text in detail"
#### To Dos / To Dos
- **ID:** da7bca61-a5c1-48f9-910b-b6eadcbcab17
- **Prompt-Text:**
- DE: "Erstelle eine Liste mit Aufgaben aus dem folgenden Text"
- EN: "Create a list of tasks from the following text"
#### Kurzgeschichte / Short Story
- **ID:** a1b2c3d4-e5f6-4a3b-8c7d-9e0f1a2b3c4d
- **Prompt-Text:**
- DE: "Entwickle eine kurze Geschichte basierend auf den folgenden Elementen"
- EN: "Develop a short story based on the following elements"
#### Charakterprofil / Character Profile
- **ID:** b2c3d4e5-f6a7-5b4c-9d8e-0f1a2b3c4d5e
- **Prompt-Text:**
- DE: "Erstelle einen Charakter mit Hintergrundgeschichte, Persönlichkeit und Motivation"
- EN: "Create a character with background story, personality and motivation"
#### Szenenbeschreibung / Scene Description
- **ID:** c3d4e5f6-a7b8-6c5d-0e9f-1a2b3c4d5e6f
- **Prompt-Text:**
- DE: "Beschreibe eine Szene mit allen Sinneseindrücken (Sehen, Hören, Riechen, Schmecken, Fühlen)"
- EN: "Describe a scene with all sensory impressions (sight, sound, smell, taste, touch)"
#### Dialog / Dialogue
- **ID:** d4e5f6a7-b8c9-7d6e-1f0a-2b3c4d5e6f7a
- **Prompt-Text:**
- DE: "Entwickle einen Dialog zwischen zwei Charakteren mit unterschiedlichen Perspektiven"
- EN: "Develop a dialogue between two characters with different perspectives"
#### Gedicht/Songtext / Poem/Lyrics
- **ID:** e5f6a7b8-c9d0-8e7f-2a1b-3c4d5e6f7a8b
- **Prompt-Text:**
- DE: "Schreibe ein Gedicht oder einen Songtext zu einem bestimmten Thema oder Gefühl"
- EN: "Write a poem or song lyrics about a specific theme or feeling"
## Blueprint-Prompt Verknüpfungen
Die folgende Tabelle zeigt, welche Prompts mit welchen Blueprints verknüpft sind:
| Blueprint | Prompt | Erstellt am |
|-----------|--------|-------------|
| Textanalyse / Text Analysis | Zusammenfassung / Summary | 2025-05-08 11:09:29 |
| Textanalyse / Text Analysis | To Dos / To Dos | 2025-05-08 11:09:29 |
| Kreatives Schreiben / Creative Writing | Kurzgeschichte / Short Story | 2025-05-15 23:21:14 |
| Kreatives Schreiben / Creative Writing | Charakterprofil / Character Profile | 2025-05-15 23:21:14 |
| Kreatives Schreiben / Creative Writing | Szenenbeschreibung / Scene Description | 2025-05-15 23:21:14 |
| Kreatives Schreiben / Creative Writing | Dialog / Dialogue | 2025-05-15 23:21:14 |
| Kreatives Schreiben / Creative Writing | Gedicht/Songtext / Poem/Lyrics | 2025-05-15 23:21:14 |
---
*Letzte Aktualisierung: 16. Mai 2025*

View file

@ -0,0 +1,816 @@
# iOS Widget Implementation Guide für Expo React Native Quotes App
## Überblick
Diese Anleitung erklärt, wie Sie iOS Home Screen Widgets und Live Activities für Ihre Expo React Native Quotes App implementieren. Stand: Dezember 2025, basierend auf Expo SDK 54.
## Voraussetzungen
### System Requirements
- **macOS 15 Sequoia** oder höher
- **Xcode 16** oder höher
- **CocoaPods 1.16.2** oder höher (Ruby 3.2.0+)
- **Expo SDK 53+** (SDK 54 empfohlen, wie in Ihrem Projekt)
- **iOS 16+** für Live Activities, iOS 14+ für Home Screen Widgets
- Apple Developer Account (für Device Testing)
### Development Build
Widgets funktionieren **nicht** mit Expo Go. Sie benötigen einen Development Build:
```bash
# Erstellen Sie einen Development Build
npm run prebuild
npm run ios
```
## Implementation Strategie
### Ansatz 1: @bacons/apple-targets Plugin (Empfohlen)
Dies ist der modernste und am besten unterstützte Ansatz, entwickelt von Evan Bacon vom Expo Team.
#### Installation
```bash
# Plugin installieren
npm install @bacons/apple-targets
# Widget Target erstellen
npx create-target widget
```
Dies generiert automatisch:
- `/targets/widget/` Verzeichnis mit Swift-Dateien
- Basis Widget-Konfiguration
- Integration in app.json
#### Konfiguration in app.json
```json
{
"expo": {
"name": "Quote App",
"slug": "quote-app",
"version": "1.0.0",
"platforms": ["ios"],
"ios": {
"bundleIdentifier": "com.yourcompany.quoteapp",
"supportsTablet": true,
"entitlements": {
"com.apple.security.application-groups": [
"group.com.yourcompany.quoteapp.widget"
]
}
},
"plugins": [
[
"@bacons/apple-targets",
{
"appleTeamId": "YOUR_TEAM_ID"
}
]
]
}
}
```
#### Widget Konfiguration (targets/widget/expo-target.config.js)
```javascript
/** @type {import('@bacons/apple-targets/app.plugin').Config} */
module.exports = {
type: "widget",
name: "QuotesWidget",
bundleIdentifier: "$(PRODUCT_BUNDLE_IDENTIFIER).widget",
deploymentTarget: "16.0",
icon: "../../assets/widget-icon.png",
colors: {
$accent: {
color: "#007AFF",
darkColor: "#0A84FF"
},
$widgetBackground: {
color: "#FFFFFF",
darkColor: "#1C1C1E"
}
},
entitlements: {
"com.apple.security.application-groups": [
"group.com.yourcompany.quoteapp.widget"
]
},
frameworks: [
"SwiftUI",
"WidgetKit"
]
};
```
### Ansatz 2: Manuelle Implementation mit Config Plugins
Falls Sie mehr Kontrolle benötigen, können Sie einen eigenen Config Plugin erstellen.
#### Plugin Struktur (plugins/withIOSWidget.js)
```javascript
const {
withXcodeProject,
withDangerousMod,
withEntitlementsPlist,
withInfoPlist,
IOSConfig
} = require('@expo/config-plugins');
const fs = require('fs');
const path = require('path');
function withIOSWidget(config) {
// App Group hinzufügen
config = withEntitlementsPlist(config, async (config) => {
config.modResults['com.apple.security.application-groups'] = [
`group.${config.ios.bundleIdentifier}.widget`
];
return config;
});
// Widget Target zum Xcode Project hinzufügen
config = withXcodeProject(config, async (config) => {
const project = config.modResults;
// Widget Target Configuration
// (Detaillierte Implementation hier)
return config;
});
return config;
}
module.exports = withIOSWidget;
```
## Swift Widget Implementation
### Basic Quote Widget (targets/widget/QuotesWidget.swift)
```swift
import WidgetKit
import SwiftUI
// Widget Datenmodell
struct QuoteEntry: TimelineEntry {
let date: Date
let quote: String
let author: String
let category: String?
}
// Widget Datenprovider
struct QuoteProvider: TimelineProvider {
let userDefaults = UserDefaults(suiteName: "group.com.yourcompany.quoteapp.widget")
func placeholder(in context: Context) -> QuoteEntry {
QuoteEntry(
date: Date(),
quote: "Der beste Weg, die Zukunft vorauszusagen, ist, sie zu erfinden.",
author: "Alan Kay",
category: "Innovation"
)
}
func getSnapshot(in context: Context, completion: @escaping (QuoteEntry) -> ()) {
let entry = getQuoteFromStorage() ?? placeholder(in: context)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
var entries: [QuoteEntry] = []
// Quotes aus UserDefaults laden
let quotes = loadQuotesFromUserDefaults()
// Timeline mit stündlichen Updates erstellen
let currentDate = Date()
for hourOffset in 0..<24 {
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
let quote = quotes.randomElement() ?? placeholder(in: context)
entries.append(QuoteEntry(
date: entryDate,
quote: quote.quote,
author: quote.author,
category: quote.category
))
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
private func loadQuotesFromUserDefaults() -> [QuoteModel] {
guard let data = userDefaults?.data(forKey: "savedQuotes"),
let quotes = try? JSONDecoder().decode([QuoteModel].self, from: data) else {
return defaultQuotes()
}
return quotes
}
private func defaultQuotes() -> [QuoteModel] {
return [
QuoteModel(quote: "Innovation distinguishes between a leader and a follower.",
author: "Steve Jobs",
category: "Leadership"),
QuoteModel(quote: "The only way to do great work is to love what you do.",
author: "Steve Jobs",
category: "Motivation")
]
}
}
// Widget View
struct QuotesWidgetView : View {
var entry: QuoteProvider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall:
SmallWidgetView(entry: entry)
case .systemMedium:
MediumWidgetView(entry: entry)
case .systemLarge:
LargeWidgetView(entry: entry)
default:
Text("Unsupported")
}
}
}
// Small Widget Layout
struct SmallWidgetView: View {
let entry: QuoteEntry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(entry.quote)
.font(.system(size: 14, weight: .medium))
.lineLimit(3)
.foregroundColor(.primary)
Spacer()
Text("— \(entry.author)")
.font(.system(size: 12))
.foregroundColor(.secondary)
.lineLimit(1)
}
.padding()
.containerBackground(for: .widget) {
LinearGradient(
colors: [Color.blue.opacity(0.1), Color.purple.opacity(0.1)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
}
}
// Medium Widget Layout
struct MediumWidgetView: View {
let entry: QuoteEntry
var body: some View {
HStack(spacing: 12) {
VStack(alignment: .leading, spacing: 8) {
if let category = entry.category {
Text(category.uppercased())
.font(.system(size: 10, weight: .semibold))
.foregroundColor(.blue)
}
Text(entry.quote)
.font(.system(size: 16, weight: .medium))
.lineLimit(3)
.foregroundColor(.primary)
Text("— \(entry.author)")
.font(.system(size: 14))
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "quote.bubble.fill")
.font(.system(size: 30))
.foregroundColor(.blue.opacity(0.3))
}
.padding()
.containerBackground(for: .widget) {
Color.clear
}
}
}
// Widget Configuration
@main
struct QuotesWidget: Widget {
let kind: String = "QuotesWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: QuoteProvider()) { entry in
QuotesWidgetView(entry: entry)
}
.configurationDisplayName("Daily Quotes")
.description("Get inspired with a new quote every hour")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
.contentMarginsDisabled() // iOS 17+
}
}
// Quote Model
struct QuoteModel: Codable {
let quote: String
let author: String
let category: String?
}
```
## React Native Integration
### Daten-Sharing zwischen App und Widget
#### Installation der benötigten Pakete
```bash
npm install react-native-shared-group-preferences
# oder
npm install react-native-widget-extension
```
#### SharedDataManager (store/widgetDataManager.ts)
```typescript
import SharedGroupPreferences from 'react-native-shared-group-preferences';
const APP_GROUP = 'group.com.yourcompany.quoteapp.widget';
export class WidgetDataManager {
static async saveQuotesToWidget(quotes: Quote[]): Promise<void> {
try {
const quotesData = JSON.stringify(quotes);
await SharedGroupPreferences.setItem(
'savedQuotes',
quotesData,
APP_GROUP
);
// Widget Update triggern (iOS 14+)
if (Platform.OS === 'ios') {
const WidgetKit = NativeModules.WidgetKit;
WidgetKit?.reloadAllTimelines();
}
} catch (error) {
console.error('Failed to save quotes to widget:', error);
}
}
static async saveDailyQuote(quote: Quote): Promise<void> {
try {
const quoteData = JSON.stringify({
...quote,
date: new Date().toISOString()
});
await SharedGroupPreferences.setItem(
'dailyQuote',
quoteData,
APP_GROUP
);
// Widget aktualisieren
this.refreshWidget();
} catch (error) {
console.error('Failed to save daily quote:', error);
}
}
static async saveUserPreferences(preferences: WidgetPreferences): Promise<void> {
try {
await SharedGroupPreferences.setItem(
'widgetPreferences',
JSON.stringify(preferences),
APP_GROUP
);
} catch (error) {
console.error('Failed to save widget preferences:', error);
}
}
static refreshWidget(): void {
if (Platform.OS === 'ios') {
NativeModules.WidgetKit?.reloadAllTimelines();
}
}
}
interface WidgetPreferences {
updateFrequency: 'hourly' | 'daily' | 'manual';
categories: string[];
theme: 'light' | 'dark' | 'auto';
fontSize: 'small' | 'medium' | 'large';
}
```
#### Integration in Zustand Store (store/quotesStore.ts Update)
```typescript
import { WidgetDataManager } from './widgetDataManager';
// In Ihrem bestehenden Store
const useQuotesStore = create<QuotesState>()(
persist(
(set, get) => ({
// ... existing state ...
toggleFavorite: async (quoteId: string) => {
set((state) => {
const newFavorites = state.favorites.includes(quoteId)
? state.favorites.filter(id => id !== quoteId)
: [...state.favorites, quoteId];
// Widget mit aktualisierten Favoriten updaten
const favoriteQuotes = state.quotes.filter(q =>
newFavorites.includes(q.id)
);
WidgetDataManager.saveQuotesToWidget(favoriteQuotes);
return { favorites: newFavorites };
});
},
setDailyQuote: async (quote: Quote) => {
// Daily Quote im Widget speichern
await WidgetDataManager.saveDailyQuote(quote);
set({ dailyQuote: quote });
},
}),
// ... persist config ...
)
);
```
### Native Module für Widget Kit (optional)
Für erweiterte Widget-Kontrolle können Sie ein eigenes Native Module erstellen:
#### WidgetKitModule.swift
```swift
import Foundation
import WidgetKit
@objc(WidgetKitModule)
class WidgetKitModule: NSObject {
@objc
func reloadAllTimelines() {
if #available(iOS 14.0, *) {
WidgetCenter.shared.reloadAllTimelines()
}
}
@objc
func reloadTimelines(_ widgetKind: String) {
if #available(iOS 14.0, *) {
WidgetCenter.shared.reloadTimelines(ofKind: widgetKind)
}
}
@objc
static func requiresMainQueueSetup() -> Bool {
return false
}
}
```
#### WidgetKitModule.m (Bridge Header)
```objc
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(WidgetKitModule, NSObject)
RCT_EXTERN_METHOD(reloadAllTimelines)
RCT_EXTERN_METHOD(reloadTimelines:(NSString *)widgetKind)
@end
```
## Widget Features und Best Practices
### 1. Widget Sizes und Layouts
iOS unterstützt verschiedene Widget-Größen:
- **Small**: 2x2 Grid (minimaler Inhalt)
- **Medium**: 4x2 Grid (mehr Details)
- **Large**: 4x4 Grid (vollständiger Inhalt)
- **Extra Large** (iPad): 6x4 Grid
### 2. Timeline Updates
```swift
// In QuoteProvider
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let updatePolicy: TimelineReloadPolicy
// Update-Frequenz basierend auf User Preferences
if let updateFrequency = userDefaults?.string(forKey: "updateFrequency") {
switch updateFrequency {
case "hourly":
updatePolicy = .after(Date().addingTimeInterval(3600))
case "daily":
updatePolicy = .after(Calendar.current.startOfDay(for: Date()).addingTimeInterval(86400))
default:
updatePolicy = .atEnd
}
} else {
updatePolicy = .atEnd
}
let timeline = Timeline(entries: entries, policy: updatePolicy)
completion(timeline)
}
```
### 3. Deep Linking
Widget-Taps können die App mit spezifischen Inhalten öffnen:
```swift
// Widget View mit Link
struct QuoteWidgetView: View {
var entry: QuoteEntry
var body: some View {
VStack {
// Widget Content
}
.widgetURL(URL(string: "quoteapp://quote/\(entry.quoteId)"))
}
}
```
React Native App Handler:
```typescript
import { Linking } from 'react-native';
// In App.tsx
useEffect(() => {
const handleDeepLink = (url: string) => {
const route = url.replace('quoteapp://', '');
if (route.startsWith('quote/')) {
const quoteId = route.replace('quote/', '');
// Navigate to quote details
navigation.navigate('QuoteDetail', { quoteId });
}
};
const subscription = Linking.addEventListener('url', ({ url }) => {
handleDeepLink(url);
});
return () => subscription.remove();
}, []);
```
### 4. Live Activities (iOS 16+)
Für dynamische Updates (z.B. Quote of the Day Countdown):
```swift
// LiveActivity Attributes
struct QuoteLiveActivityAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
var quote: String
var author: String
var expiresAt: Date
}
var category: String
}
// Start Live Activity from React Native
NativeModules.LiveActivityModule.startQuoteActivity({
quote: "Your quote here",
author: "Author Name",
category: "Motivation",
duration: 3600 // seconds
});
```
## Testing und Debugging
### 1. Widget Testing im Simulator
```bash
# Build und Run
npx expo run:ios
# In Xcode:
# 1. Scheme zu "widgetExtension" wechseln
# 2. Run auf Simulator
# 3. Widget über Home Screen hinzufügen
```
### 2. Debug Console Logs
In widget.swift:
```swift
import os.log
extension Logger {
static let widget = Logger(subsystem: "com.yourcompany.quoteapp.widget", category: "widget")
}
// Usage
Logger.widget.info("Loading quotes from UserDefaults")
```
### 3. Widget Preview in SwiftUI
```swift
struct QuotesWidget_Previews: PreviewProvider {
static var previews: some View {
Group {
QuotesWidgetView(entry: QuoteEntry(
date: Date(),
quote: "Preview Quote",
author: "Preview Author",
category: "Preview"
))
.previewContext(WidgetPreviewContext(family: .systemSmall))
QuotesWidgetView(entry: QuoteEntry(
date: Date(),
quote: "Preview Quote",
author: "Preview Author",
category: "Preview"
))
.previewContext(WidgetPreviewContext(family: .systemMedium))
}
}
}
```
## Häufige Probleme und Lösungen
### Problem 1: Widget zeigt keine Daten
**Lösung:**
1. Überprüfen Sie App Group Configuration
2. Stellen Sie sicher, dass beide Targets dieselbe App Group verwenden
3. Prüfen Sie UserDefaults Suite Name
### Problem 2: Widget Updates nicht
**Lösung:**
```typescript
// Force Widget Reload
import { NativeModules } from 'react-native';
const forceWidgetUpdate = () => {
if (Platform.OS === 'ios') {
NativeModules.WidgetKit?.reloadAllTimelines();
}
};
```
### Problem 3: Build Fehler nach Widget Addition
**Lösung:**
```bash
# Clean Build
cd ios
rm -rf Pods Podfile.lock
pod install
cd ..
npx expo run:ios --clear
```
### Problem 4: Widget erscheint nicht in Widget Gallery
**Lösung:**
- Minimum Deployment Target prüfen (iOS 14.0+)
- Info.plist Einträge verifizieren
- Bundle Identifier Format überprüfen
## Performance Optimierung
### 1. Bildoptimierung
```swift
// Bilder für Widgets vorbereiten
func prepareImageForWidget(_ image: UIImage, size: CGSize) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
image.draw(in: CGRect(origin: .zero, size: size))
let optimizedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return optimizedImage
}
```
### 2. Daten-Caching
```typescript
// Efficient Data Storage
class WidgetCache {
static async cacheQuotes(quotes: Quote[]) {
// Nur die nötigen Felder speichern
const minimalQuotes = quotes.map(q => ({
id: q.id,
quote: q.quote.substring(0, 200), // Limit text length
author: q.author,
category: q.category
}));
await WidgetDataManager.saveQuotesToWidget(minimalQuotes);
}
}
```
### 3. Timeline Optimization
```swift
// Intelligente Timeline Generation
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
let entries: [QuoteEntry]
if context.isPreview {
// Minimal entries for preview
entries = [getPlaceholderEntry()]
} else if context.family == .systemSmall {
// Fewer entries for small widgets
entries = generateEntries(count: 12)
} else {
// Standard timeline
entries = generateEntries(count: 24)
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
```
## Deployment Checkliste
### Pre-Deployment
- [ ] App Group korrekt konfiguriert
- [ ] Widget Bundle Identifier folgt Apple Guidelines
- [ ] Alle erforderlichen Entitlements gesetzt
- [ ] Widget Icons in allen Größen vorhanden
- [ ] Lokalisierung implementiert (falls nötig)
- [ ] Privacy Descriptions in Info.plist
### Testing
- [ ] Widget auf verschiedenen iOS Versionen getestet
- [ ] Alle Widget Größen funktionieren
- [ ] Deep Links funktionieren korrekt
- [ ] Widget Updates korrekt bei App-Änderungen
- [ ] Memory Usage optimiert
- [ ] Offline-Funktionalität gewährleistet
### App Store Vorbereitung
- [ ] Widget Screenshots erstellt
- [ ] Widget Beschreibung vorbereitet
- [ ] App Store Connect Widget Information ausgefüllt
## Ressourcen und Weiterführende Links
### Offizielle Dokumentation
- [Apple WidgetKit Documentation](https://developer.apple.com/documentation/widgetkit)
- [Expo Config Plugins](https://docs.expo.dev/config-plugins/introduction/)
- [React Native Share Extension](https://github.com/alinz/react-native-share-extension)
### Community Resources
- [Evan Bacon's Apple Targets Plugin](https://github.com/EvanBacon/expo-apple-targets)
- [EAS Widget Example](https://github.com/gaishimo/eas-widget-example)
- [React Native Widget Bridge](https://github.com/fasky-software/react-native-widget-bridge)
### Tutorials und Beispiele
- [SwiftUI Widget Tutorial](https://www.hackingwithswift.com/books/ios-swiftui/introduction-to-widgetkit)
- [Expo Managed Workflow Widgets](https://www.peterarontoth.com/posts/interactive-widgets-in-expo-managed-workflows)
## Zusammenfassung
Die Implementation von iOS Widgets in einer Expo React Native App erfordert:
1. **Development Build** statt Expo Go
2. **Config Plugin** (@bacons/apple-targets empfohlen)
3. **App Groups** für Daten-Sharing
4. **Swift/SwiftUI** Kenntnisse für Widget UI
5. **UserDefaults/SharedGroupPreferences** für Kommunikation
6. **Timeline Provider** für Content Updates
Mit dieser Anleitung sollten Sie in der Lage sein, funktionale und ansprechende Widgets für Ihre Quotes App zu erstellen, die nahtlos mit Ihrer React Native App kommunizieren.

View file

@ -0,0 +1,525 @@
# Audio-Archiv: Upload- und Verarbeitungsstatus-Tracking
## Analyse der aktuellen Situation
### Aktuelle Datenstruktur
#### AudioFile (Lokales Archiv)
```typescript
interface AudioFile {
id: string; // Lokale ID
uri: string; // Lokaler Dateipfad
filename: string; // z.B. "recording-1234567890.m4a"
duration: number; // Dauer in Sekunden
createdAt: Date; // Erstellungsdatum
size?: number; // Dateigröße in Bytes
}
```
**Status:** Keine Tracking-Informationen für Upload oder Verarbeitung vorhanden.
#### Memo (Nach Verarbeitung)
```typescript
interface Memo {
id: string;
title: string;
source?: MemoSource;
metadata?: MemoMetadata;
transcript?: string;
// ... weitere Felder
}
interface MemoSource {
type?: 'audio' | 'text' | 'upload' | 'photo';
audio_path?: string; // z.B. "user-123/recording-1234567890.m4a"
transcript?: string;
duration?: number;
additional_recordings?: AdditionalRecording[];
// ... weitere Felder
}
interface MemoMetadata {
processing?: ProcessingMetadata;
transcriptionStatus?: string;
recordingStatus?: string;
// ... weitere Felder
}
```
### Aktueller Upload-Flow
1. **Lokale Aufnahme**`fileStorageService.saveRecording()`
- Speichert Audio lokal als `AudioFile`
- Keine Verknüpfung zu Memo
2. **Upload aus Archiv**`handleReupload()` in `RecordingsList.tsx`
- `cloudStorageService.uploadAudioForProcessing()` → Cloud Storage
- `triggerTranscription()` → memoro-service API
- **Problem:** Keine Statusverfolgung nach Upload
3. **Memo-Erstellung**
- Backend erstellt Memo nach erfolgreicher Transkription
- **Problem:** Keine Verbindung zwischen lokalem `AudioFile` und erstelltem `Memo`
### Identifizierte Probleme
1. **Fehlende Verbindung:** Kein Link zwischen lokalem `AudioFile` und hochgeladenem/verarbeitetem `Memo`
2. **Kein Status-Tracking:** User sieht nicht, ob Upload erfolgreich war
3. **Doppelter Upload möglich:** Keine Markierung verhindert erneutes Hochladen
4. **Keine Memo-Verlinkung:** User kann nicht direkt zum fertigen Memo navigieren
---
## Lösungsvorschläge
### Option 1: Minimale Lösung (Schnell & Einfach)
**Ansatz:** Erweitere `AudioFile` um Upload-Status-Felder
#### Änderungen
```typescript
// features/storage/storage.types.ts
interface AudioFile {
id: string;
uri: string;
filename: string;
duration: number;
createdAt: Date;
size?: number;
// NEU: Upload & Verarbeitung
uploadStatus?: 'pending' | 'uploading' | 'uploaded' | 'failed';
uploadedAt?: Date;
uploadError?: string;
processingStatus?: 'pending' | 'processing' | 'completed' | 'failed';
// NEU: Verbindung zum Memo
memoId?: string;
cloudPath?: string; // z.B. "user-123/recording-1234567890.m4a"
}
```
#### Implementierung
1. **Upload-Tracking in `RecordingsList.tsx`**
```typescript
const handleReupload = async (recording: AudioFile) => {
// Setze Status auf "uploading"
await fileStorageService.updateRecordingStatus(recording.id, {
uploadStatus: 'uploading'
});
try {
// Upload...
const uploadResult = await cloudStorageService.uploadAudioForProcessing(...);
if (uploadResult.success) {
await fileStorageService.updateRecordingStatus(recording.id, {
uploadStatus: 'uploaded',
uploadedAt: new Date(),
cloudPath: uploadResult.filePath,
processingStatus: 'processing'
});
// Trigger transcription...
const transcriptionResult = await triggerTranscription(...);
// Status bleibt 'processing' bis Memo erstellt wurde
}
} catch (error) {
await fileStorageService.updateRecordingStatus(recording.id, {
uploadStatus: 'failed',
uploadError: error.message
});
}
};
```
2. **Memo-Verbindung via Realtime**
```typescript
// Wenn neues Memo erstellt wird, prüfe ob es eine lokale Aufnahme gibt
useEffect(() => {
const handleNewMemo = async (memo: Memo) => {
if (memo.source?.audio_path) {
const filename = memo.source.audio_path.split('/').pop();
const localRecording = await fileStorageService.findRecordingByFilename(filename);
if (localRecording) {
await fileStorageService.updateRecordingStatus(localRecording.id, {
memoId: memo.id,
processingStatus: 'completed'
});
}
}
};
}, []);
```
3. **UI-Updates in `AudioPlayer` oder `RecordingsList`**
- Badge/Icon für Upload-Status
- Button "Zum Memo" wenn `memoId` vorhanden
- Status-Indikatoren
#### Vorteile
- ✅ Schnell implementierbar
- ✅ Keine Backend-Änderungen nötig
- ✅ Funktioniert mit bestehender Infrastruktur
#### Nachteile
- ⚠️ Status geht verloren bei App-Neuinstallation
- ⚠️ Matching über Dateiname kann fehlschlagen
- ⚠️ Keine Synchronisation über Geräte hinweg
---
### Option 2: Backend-Integration (Robust & Skalierbar)
**Ansatz:** Backend verfolgt Upload-Status und benachrichtigt Frontend
#### Änderungen
1. **Neue Datenbank-Tabelle: `audio_uploads`**
```sql
CREATE TABLE audio_uploads (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID REFERENCES auth.users(id),
filename TEXT NOT NULL,
cloud_path TEXT NOT NULL,
local_id TEXT, -- Optional: Lokale AudioFile ID
upload_status TEXT NOT NULL DEFAULT 'pending',
processing_status TEXT NOT NULL DEFAULT 'pending',
memo_id UUID REFERENCES memos(id),
uploaded_at TIMESTAMPTZ,
processing_started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
error_message TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
```
2. **Upload-Flow mit Tracking**
```typescript
const handleReupload = async (recording: AudioFile) => {
// 1. Erstelle Upload-Record
const uploadRecord = await supabase
.from('audio_uploads')
.insert({
filename: recording.filename,
local_id: recording.id,
upload_status: 'pending'
})
.select()
.single();
// 2. Upload & Update Status
const uploadResult = await cloudStorageService.uploadAudioForProcessing(...);
await supabase
.from('audio_uploads')
.update({
cloud_path: uploadResult.filePath,
upload_status: 'uploaded',
uploaded_at: new Date().toISOString(),
processing_status: 'processing'
})
.eq('id', uploadRecord.id);
// 3. Trigger Transcription mit Upload-ID
await triggerTranscription({
...,
uploadRecordId: uploadRecord.id
});
};
```
3. **Backend aktualisiert Status**
- memoro-service aktualisiert `audio_uploads` während Verarbeitung
- Setzt `memo_id` wenn Memo erstellt wurde
- Setzt `processing_status: 'completed'`
4. **Frontend Realtime Subscription**
```typescript
useEffect(() => {
const subscription = supabase
.channel('audio-uploads')
.on('postgres_changes', {
event: 'UPDATE',
schema: 'public',
table: 'audio_uploads'
}, (payload) => {
// Update lokalen AudioFile Status
updateLocalRecording(payload.new);
})
.subscribe();
}, []);
```
#### Vorteile
- ✅ Robust & Zuverlässig
- ✅ Geräteübergreifende Synchronisation
- ✅ Präzises Tracking über gesamten Lifecycle
- ✅ Fehlerbehandlung & Retry-Logik möglich
#### Nachteile
- ⚠️ Backend-Änderungen erforderlich
- ⚠️ Komplexere Implementierung
- ⚠️ Zusätzliche Datenbank-Tabelle
---
### Option 3: Hybrid-Ansatz (Empfohlen)
**Kombination:** Lokales Tracking (Option 1) + optionale Backend-Verifizierung
#### Implementierung
1. **Phase 1: Lokales Tracking** (wie Option 1)
- Erweitere `AudioFile` um Status-Felder
- Tracking in `fileStorageService`
- UI zeigt Status an
2. **Phase 2: Backend-Verifizierung** (später)
- Füge `audio_uploads` Tabelle hinzu
- Synchronisiere bei App-Start
- Korrigiere falsche lokale Stati
3. **Matching-Strategie**
```typescript
// Beim Upload: Speichere eindeutigen Identifier
const uploadMetadata = {
localId: recording.id,
deviceId: await getDeviceId(),
uploadTimestamp: Date.now()
};
// Backend speichert Metadata in memo.metadata.uploadInfo
// Frontend kann später matchen
```
#### Vorteile
- ✅ Schnelle initiale Implementierung
- ✅ Schrittweise Verbesserung möglich
- ✅ Guter Kompromiss zwischen Aufwand und Nutzen
---
## Empfehlung & Nächste Schritte
### Empfohlener Ansatz: **Option 3 (Hybrid)**
**Begründung:**
- Schneller Mehrwert für User (Phase 1)
- Technische Schuld bleibt überschaubar
- Einfach auf robuste Lösung erweiterbar
### Implementierungs-Roadmap
#### Phase 1: Lokales Tracking (1-2 Tage)
1. **Datenmodell erweitern**
- [ ] `AudioFile` Interface erweitern
- [ ] Migration für bestehende Daten
2. **Storage-Service anpassen**
- [ ] `updateRecordingStatus()` implementieren
- [ ] `findRecordingByFilename()` implementieren
- [ ] SQLite Queries anpassen
3. **Upload-Flow aktualisieren**
- [ ] Status-Tracking in `handleReupload()`
- [ ] Error-Handling verbessern
- [ ] Doppel-Upload verhindern
4. **UI-Komponenten**
- [ ] Status-Badge/Icon in `AudioPlayer`
- [ ] "Zum Memo"-Button hinzufügen
- [ ] Upload-Status-Indikator
- [ ] Translations für Status-Texte
5. **Memo-Verbindung**
- [ ] Realtime-Listener für neue Memos
- [ ] Matching-Logik über Dateiname
- [ ] Status-Update auf `completed`
#### Phase 2: Backend-Integration (optional, später)
1. **Datenbank**
- [ ] `audio_uploads` Tabelle erstellen
- [ ] RLS Policies definieren
- [ ] Indexes anlegen
2. **Backend-Service**
- [ ] Upload-Record Erstellung
- [ ] Status-Updates während Verarbeitung
- [ ] Memo-Verlinkung
3. **Frontend-Sync**
- [ ] Realtime Subscription
- [ ] Sync bei App-Start
- [ ] Conflict-Resolution
---
## UI/UX Konzept
### Status-Anzeige im Audio-Archiv
```
┌─────────────────────────────────────────┐
│ 🎤 Aufnahme │
│ 25.09.2024, 14:30 Uhr • 2:34 │
│ │
│ ⏸ ▶ ──────●────── [Waveform] │
│ │
│ ┌─────────────────────────────────┐ │
│ │ ✓ Hochgeladen & Verarbeitet │ │ <- Status-Badge
│ │ 📝 Memo ansehen → │ │ <- Action-Button
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
### Status-Varianten
1. **Nicht hochgeladen**
- Button: "Hochladen" (Primary)
- Badge: Keine
2. **Lädt hoch...**
- Button: "Lädt hoch..." (Disabled, Loading)
- Badge: "⏳ Wird hochgeladen..."
3. **Wird verarbeitet...**
- Button: Disabled
- Badge: "🔄 Wird verarbeitet..."
4. **Erfolgreich verarbeitet**
- Button: "Zum Memo →" (Link zum Memo)
- Badge: "✓ Fertig"
- Optional: Memo-Titel anzeigen
5. **Fehler**
- Button: "Erneut versuchen"
- Badge: "❌ Upload fehlgeschlagen"
- Fehlerdetails in Tooltip/Modal
---
## Technische Details
### SQLite Schema-Erweiterung
```sql
-- Migration: Add upload tracking to recordings
ALTER TABLE recordings ADD COLUMN upload_status TEXT;
ALTER TABLE recordings ADD COLUMN uploaded_at INTEGER;
ALTER TABLE recordings ADD COLUMN upload_error TEXT;
ALTER TABLE recordings ADD COLUMN processing_status TEXT;
ALTER TABLE recordings ADD COLUMN memo_id TEXT;
ALTER TABLE recordings ADD COLUMN cloud_path TEXT;
-- Index für schnelles Lookup
CREATE INDEX idx_recordings_filename ON recordings(filename);
CREATE INDEX idx_recordings_memo_id ON recordings(memo_id);
```
### Service-Methoden
```typescript
// features/storage/fileStorage.service.ts
interface RecordingStatusUpdate {
uploadStatus?: 'pending' | 'uploading' | 'uploaded' | 'failed';
uploadedAt?: Date;
uploadError?: string;
processingStatus?: 'pending' | 'processing' | 'completed' | 'failed';
memoId?: string;
cloudPath?: string;
}
class FileStorageService {
async updateRecordingStatus(
recordingId: string,
update: RecordingStatusUpdate
): Promise<void> {
// SQLite Update...
}
async findRecordingByFilename(filename: string): Promise<AudioFile | null> {
// SQLite Query...
}
async findRecordingByMemoId(memoId: string): Promise<AudioFile | null> {
// SQLite Query...
}
async getRecordingsWithStatus(
status: 'uploaded' | 'processing' | 'completed'
): Promise<AudioFile[]> {
// SQLite Query...
}
}
```
---
## Testing-Plan
### Unit Tests
- [ ] `updateRecordingStatus()` - Status-Updates
- [ ] `findRecordingByFilename()` - Filename-Matching
- [ ] Memo-Verbindungs-Logik
### Integration Tests
- [ ] Upload → Status-Update → Memo-Verlinkung
- [ ] Fehlerbehandlung bei Upload-Fehler
- [ ] Doppel-Upload Prevention
### E2E Tests
- [ ] Aufnahme → Upload → Verarbeitung → Memo anzeigen
- [ ] Offline-Upload → Online → Status-Sync
---
## Offene Fragen
1. **Dateinamen-Kollisionen:** Was passiert wenn zwei Geräte dieselbe Dateiname verwenden?
- **Lösung:** Device-ID in Dateinamen einbauen oder eindeutige Upload-ID verwenden
2. **Aufnahmen löschen:** Soll Memo-Verbindung erhalten bleiben wenn lokale Aufnahme gelöscht wird?
- **Empfehlung:** Ja, nur `uploadStatus` behalten für Referenz
3. **Alte Aufnahmen:** Wie mit bestehenden Aufnahmen ohne Status umgehen?
- **Empfehlung:** Initial alle auf `uploadStatus: null` setzen, User kann manuell hochladen
4. **Sync über Geräte:** Sollen Upload-Stati zwischen Geräten synchronisiert werden?
- **Phase 1:** Nein (nur lokal)
- **Phase 2:** Ja (via Backend)
---
## Zusammenfassung
### Ja, es ist machbar!
Die vorgeschlagene Lösung ermöglicht:
- ✅ **Status-Anzeige:** Upload & Verarbeitungsstatus im Archiv sichtbar
- ✅ **Memo-Verlinkung:** Direkte Navigation zum fertigen Memo
- ✅ **Fehlerbehandlung:** Klare Fehlermeldungen und Retry-Möglichkeit
- ✅ **Doppel-Upload Prevention:** Bereits hochgeladene Aufnahmen markiert
- ✅ **Progressive Enhancement:** Schnelle Basis-Implementierung, später erweiterbar
### Geschätzter Aufwand (Phase 1)
- Datenmodell & Migration: 2-3h
- Service-Implementierung: 3-4h
- Upload-Flow-Integration: 2-3h
- UI-Komponenten: 3-4h
- Testing & Bugfixes: 2-3h
**Gesamt: ~12-17 Stunden (1.5-2 Arbeitstage)**

View file

@ -0,0 +1,689 @@
# Automatic App Rating Feature
## Overview
The automatic app rating feature prompts users to rate the Memoro app in the App Store/Play Store after reaching specific engagement milestones. This system is designed to maximize positive ratings by requesting feedback at optimal moments—right after users have experienced success with the app.
## Architecture
### Components
```
features/rating/
├── components/
│ └── RatingPromptModal.tsx # Modal UI component
├── hooks/
│ ├── useRating.ts # Existing: Manual rating trigger
│ └── useRatingPrompt.ts # New: Automatic prompt logic
├── services/
│ └── ratingService.ts # Existing: expo-store-review wrapper
├── store/
│ └── ratingStore.ts # Zustand store with persistence
└── index.ts # Public API exports
```
### Key Features
- ✅ **Milestone-based prompts** (5, 15, 50 memos)
- ✅ **Rate limiting** (minimum 30 days between prompts)
- ✅ **User preferences** (never ask again option)
- ✅ **Analytics tracking** (all user interactions)
- ✅ **Persistent state** (survives app restarts)
- ✅ **Multi-language support** (i18n ready)
- ✅ **Theme-aware UI** (dark/light mode)
---
## Implementation Details
### 1. Rating Store (`ratingStore.ts`)
**Purpose**: Centralized state management with automatic persistence.
**State:**
```typescript
interface RatingState {
hasRatedApp: boolean; // User completed rating
hasDeclinedRating: boolean; // User selected "Maybe Later"
neverAskAgain: boolean; // User selected "Don't Ask Again"
memoCreatedCount: number; // Total memos created
lastPromptDate: string | null; // Last time prompt was shown
_hasHydrated: boolean; // Store rehydration status
}
```
**Actions:**
- `incrementMemoCount()` - Called after each memo creation
- `markRated()` - User completed rating
- `markDeclined()` - User deferred rating
- `markNeverAsk()` - User opted out permanently
- `resetForTesting()` - Development helper
**Persistence:**
Uses Zustand's `persist` middleware with AsyncStorage backend. Data is automatically saved on every state change and restored on app launch.
**Storage Key:** `memoro-rating`
---
### 2. Rating Prompt Hook (`useRatingPrompt.ts`)
**Purpose**: Contains eligibility logic and modal trigger management.
**Configuration Constants:**
```typescript
const RATING_MILESTONES = [5, 15, 50]; // Memo counts to trigger
const MIN_DAYS_BETWEEN_PROMPTS = 30; // Cooldown period
```
**Eligibility Criteria:**
A prompt is shown when ALL conditions are met:
1. ✅ Store has hydrated (data loaded from AsyncStorage)
2. ✅ User hasn't rated yet (`hasRatedApp === false`)
3. ✅ User hasn't opted out (`neverAskAgain === false`)
4. ✅ Current memo count matches a milestone (5, 15, or 50)
5. ✅ At least 30 days since last prompt (or first time)
**API:**
```typescript
const {
showPromptModal, // Boolean: Should modal be visible?
triggerPromptCheck, // Function: Check eligibility & show modal
closePrompt, // Function: Close the modal
isEligible, // Boolean: Is user currently eligible?
currentMemoCount, // Number: Current memo count
} = useRatingPrompt();
```
---
### 3. Rating Prompt Modal (`RatingPromptModal.tsx`)
**Purpose**: User-facing dialog that requests the rating.
**UI Structure:**
```
┌─────────────────────────────────┐
│ Gefällt dir Memoro? (Title) │
│ │
│ Du hast bereits X Memos │
│ erstellt! 🎉 (Milestone) │
│ │
│ Es würde uns sehr freuen... │
│ (Description) │
│ │
│ [Jetzt bewerten] (Primary) │
│ [Vielleicht später] (Second) │
│ [Nicht mehr fragen] (Text) │
└─────────────────────────────────┘
```
**Button Actions:**
1. **"Jetzt bewerten" (Rate Now)**
- Calls `ratingService.requestReview()`
- Opens native App Store review dialog
- Marks as rated: `markRated()`
- Tracks: `rating_accepted`
2. **"Vielleicht später" (Maybe Later)**
- Defers prompt for 30+ days
- Marks as declined: `markDeclined()`
- Tracks: `rating_declined`
3. **"Nicht mehr fragen" (Don't Ask Again)**
- Permanently suppresses prompts
- Marks: `markNeverAsk()`
- Tracks: `rating_never_ask`
**Props:**
```typescript
interface RatingPromptModalProps {
isVisible: boolean; // Modal visibility state
onClose: () => void; // Close handler
}
```
---
### 4. Integration Points
#### A. Counter Increment (`app/(protected)/(tabs)/index.tsx`)
**Location:** Line 846-848
**Trigger:** When a new memo is detected (different from previous memo ID)
```typescript
if (isNewMemo && !isFirstLoad) {
// ... animation code ...
// Increment rating counter for new memos
incrementMemoCount();
}
```
**Why here?** This ensures we count user-created memos only (not initial loads or updates).
#### B. Global Modal Rendering (`app/(protected)/_layout.tsx`)
**Location:** Line 159
**Rendered alongside other global modals:**
```typescript
{/* Global Rating Prompt Modal */}
<RatingPromptModal
isVisible={showPromptModal}
onClose={closePrompt}
/>
```
#### C. Automatic Prompt Check (`app/(protected)/_layout.tsx`)
**Location:** Lines 76-87
**Trigger:** When user navigates to home page
```typescript
useEffect(() => {
const isHomePage = pathname === '/' || pathname.endsWith('/index');
if (isHomePage) {
const timer = setTimeout(() => {
triggerPromptCheck();
}, 2000); // 2 second delay after landing on home page
return () => clearTimeout(timer);
}
}, [pathname, triggerPromptCheck]);
```
**Why 2 seconds?**
- Gives time for memo creation to complete
- Allows store to hydrate and update
- Prevents immediate popup (better UX)
---
## Analytics Events
All rating interactions are tracked in PostHog:
### Event: `rating_prompt_shown`
```typescript
{
memo_count: number, // Current memo count
milestone: 5 | 15 | 50, // Which milestone triggered
}
```
### Event: `rating_accepted`
```typescript
{
memo_count: number,
source: 'automatic_prompt', // vs 'settings' for manual
}
```
### Event: `rating_declined`
```typescript
{
memo_count: number,
action: 'maybe_later',
}
```
### Event: `rating_never_ask`
```typescript
{
memo_count: number,
action: 'never_ask',
}
```
**Analytics Configuration:** `features/analytics/events.ts` (Lines 66-72)
---
## Translation Keys
### German (`de.json`)
```json
{
"rating": {
"prompt_title": "Gefällt dir Memoro?",
"prompt_milestone": "Du hast bereits {{count}} Memos erstellt! 🎉",
"prompt_message": "Es würde uns sehr freuen, wenn du Memoro im App Store bewerten würdest. Deine Unterstützung hilft uns, die App weiter zu verbessern.",
"rate_now": "Jetzt bewerten",
"maybe_later": "Vielleicht später",
"never_ask": "Nicht mehr fragen"
}
}
```
### English (`en.json`)
```json
{
"rating": {
"prompt_title": "Enjoying Memoro?",
"prompt_milestone": "You've created {{count}} memos already! 🎉",
"prompt_message": "We would really appreciate it if you could rate Memoro in the App Store. Your support helps us continue improving the app.",
"rate_now": "Rate Now",
"maybe_later": "Maybe Later",
"never_ask": "Don't Ask Again"
}
}
```
**Note:** The `{{count}}` placeholder is dynamically replaced with the actual memo count.
---
## User Flow
### Happy Path (First Time - 5 Memos)
```
1. User creates 5th memo
2. Counter increments to 5
3. User navigates to home screen
4. After 2s delay, eligibility check runs
5. ✅ All conditions met → Modal appears
6. User taps "Jetzt bewerten"
7. Native App Store review opens
8. State: hasRatedApp = true
9. ✅ Never shown again (unless reset)
```
### Deferred Path (15 Memos)
```
1. User previously declined at 5 memos
2. User creates 15th memo (>30 days later)
3. Counter increments to 15
4. User navigates to home screen
5. After 2s delay, eligibility check runs
6. ✅ All conditions met → Modal appears
7. User taps "Vielleicht später"
8. State: hasDeclinedRating = true
9. lastPromptDate = now
10. ⏳ Won't show again for 30+ days
```
### Opt-Out Path
```
1. User sees prompt at any milestone
2. User taps "Nicht mehr fragen"
3. State: neverAskAgain = true
4. ❌ Never shown again (permanent)
```
---
## Configuration
### Adjusting Milestones
**File:** `features/rating/hooks/useRatingPrompt.ts` (Line 8)
```typescript
// Change these values to adjust when prompts appear
const RATING_MILESTONES = [5, 15, 50];
```
**Examples:**
- More aggressive: `[3, 10, 30]`
- Less aggressive: `[10, 25, 100]`
- More frequent: `[5, 10, 20, 50, 100]`
### Adjusting Cooldown Period
**File:** `features/rating/hooks/useRatingPrompt.ts` (Line 11)
```typescript
// Days between prompts
const MIN_DAYS_BETWEEN_PROMPTS = 30;
```
**Examples:**
- More frequent: `14` (2 weeks)
- Less frequent: `60` (2 months)
- No cooldown: `0` (not recommended)
---
## Testing
### Manual Testing
1. **Test Counter Increment:**
```typescript
// In React DevTools or console
import { useRatingStore } from '~/features/rating';
console.log(useRatingStore.getState().memoCreatedCount);
```
2. **Force Prompt Display:**
```typescript
// Set count to milestone
useRatingStore.getState().incrementMemoCount(); // Repeat 5x
// Navigate to home screen and wait 2s
```
3. **Reset for Testing:**
```typescript
// Reset all rating state
useRatingStore.getState().resetForTesting();
```
### Testing Scenarios
| Scenario | Setup | Expected Result |
|----------|-------|-----------------|
| First milestone | Count = 4 → 5 | Modal appears after 2s on home |
| Already rated | hasRatedApp = true | No modal, ever |
| Opted out | neverAskAgain = true | No modal, ever |
| Too soon | lastPromptDate = yesterday | No modal (needs 30 days) |
| Between milestones | Count = 12 | No modal (not milestone) |
| Second milestone | Count = 14 → 15, 30+ days | Modal appears |
### Developer Tools
**Check Current State:**
```typescript
import { useRatingStore } from '~/features/rating';
const state = useRatingStore.getState();
console.log({
count: state.memoCreatedCount,
hasRated: state.hasRatedApp,
neverAsk: state.neverAskAgain,
lastPrompt: state.lastPromptDate,
});
```
**Simulate Different Counts:**
```typescript
// Jump to milestone
const store = useRatingStore.getState();
store.memoCreatedCount = 15;
```
**Clear AsyncStorage:**
```typescript
import AsyncStorage from '@react-native-async-storage/async-storage';
await AsyncStorage.removeItem('memoro-rating');
// Then restart app to trigger fresh state
```
---
## Platform-Specific Behavior
### iOS
Uses `SKStoreReviewController`:
- Native iOS rating dialog
- Can only be triggered 3x per year per app version
- User can disable in iOS Settings → App Store → In-App Ratings & Reviews
- If disabled, nothing happens (silent failure)
**Store URL Fallback:**
```typescript
// In ratingService.ts (Line 49)
const appStoreId = '1234567890'; // TODO: Add actual App Store ID
```
### Android
Uses Google Play In-App Review API:
- Native Material Design dialog
- No rate limiting like iOS
- More reliable display
- Requires Google Play Services
**Store URL Fallback:**
```typescript
// In ratingService.ts (Line 53)
const packageName = 'com.memoro.app'; // Verify this is correct
```
---
## Edge Cases & Considerations
### 1. App Updates
- Counter persists across app updates
- User won't be re-prompted if already rated
- New milestones can be added without resetting user state
### 2. Multiple Devices
- State is local to each device
- User may see prompts on multiple devices
- This is intentional (each device = separate usage pattern)
### 3. Network Independence
- Feature works completely offline
- Only the actual rating submission requires network
### 4. Store Availability
- If `StoreReview.isAvailableAsync()` returns false:
- Fallback alert with link to store
- Implemented in existing `useRating.ts` hook
### 5. Time Zone Changes
- Date comparisons use ISO 8601 strings
- Time zone changes won't affect cooldown logic
### 6. First App Launch
- Store hydrates with default values
- Counter starts at 0
- First prompt at 5th memo creation
---
## Performance Considerations
### Memory
- Store state: ~200 bytes in memory
- AsyncStorage: ~200 bytes on disk
- Negligible impact
### CPU
- Eligibility check: O(1) operation
- Runs once every home page navigation
- 2-second debounce prevents excessive checks
### Network
- Zero network calls for eligibility checks
- Only network usage is the actual store review
---
## Future Enhancements
### Potential Improvements
1. **Sentiment Detection**
- Only prompt after successful transcriptions
- Skip prompt if recent errors occurred
- Use AI sentiment analysis on memo content
2. **A/B Testing**
- Test different milestone values
- Test different message wording
- Track conversion rates
3. **Personalization**
- Different milestones for power users
- Custom messages based on usage patterns
- Space-specific prompts
4. **Cross-Platform Sync**
- Sync rating state across devices
- Avoid duplicate prompts
- Requires backend integration
5. **Pre-Prompt Survey**
- Ask "Are you enjoying Memoro?" first
- Only show rating if positive response
- Capture negative feedback separately
---
## Troubleshooting
### Problem: Modal never appears
**Check:**
1. Verify memo count: `useRatingStore.getState().memoCreatedCount`
2. Check if user opted out: `useRatingStore.getState().neverAskAgain`
3. Check if already rated: `useRatingStore.getState().hasRatedApp`
4. Verify store hydrated: `useRatingStore.getState()._hasHydrated`
5. Check last prompt date: `useRatingStore.getState().lastPromptDate`
**Solution:**
```typescript
// Reset and test
useRatingStore.getState().resetForTesting();
// Create 5 memos and navigate to home
```
### Problem: Modal appears too often
**Check:**
- Milestone configuration might be too aggressive
- Cooldown period might be too short
**Solution:**
```typescript
// In useRatingPrompt.ts
const RATING_MILESTONES = [10, 25, 100]; // Less frequent
const MIN_DAYS_BETWEEN_PROMPTS = 60; // Longer cooldown
```
### Problem: Analytics not tracking
**Check:**
1. Verify analytics enabled: Settings → Analytics
2. Check PostHog integration
3. Look for events in PostHog dashboard
**Debug:**
```typescript
// Add console logging in RatingPromptModal.tsx
console.log('Rating event tracked:', eventName, properties);
```
### Problem: Store persists between sessions
**Expected Behavior:** This is intentional for production.
**For Development:**
```typescript
// Clear on every app launch (development only)
useEffect(() => {
if (__DEV__) {
useRatingStore.getState().resetForTesting();
}
}, []);
```
---
## Related Files
### Core Implementation
- `features/rating/store/ratingStore.ts` - State management
- `features/rating/hooks/useRatingPrompt.ts` - Logic
- `features/rating/components/RatingPromptModal.tsx` - UI
- `features/rating/index.ts` - Public API
### Integration Points
- `app/(protected)/(tabs)/index.tsx` - Counter increment
- `app/(protected)/_layout.tsx` - Global modal & trigger
### Configuration
- `features/analytics/events.ts` - Event definitions
- `features/i18n/translations/de.json` - German text
- `features/i18n/translations/en.json` - English text
### Existing Dependencies
- `features/rating/services/ratingService.ts` - Store review wrapper
- `features/rating/hooks/useRating.ts` - Manual rating trigger
- `components/atoms/BaseModal.tsx` - Modal component
- `components/atoms/Button.tsx` - Button component
---
## Changelog
### Version 1.0.0 (2025-01-30)
- ✨ Initial implementation
- ✅ Milestone-based prompts (5, 15, 50 memos)
- ✅ 30-day cooldown period
- ✅ Persistent state with AsyncStorage
- ✅ Analytics tracking
- ✅ Multi-language support (DE, EN)
- ✅ Theme-aware UI
---
## Maintenance
### Regular Checks
- [ ] Monitor analytics conversion rates monthly
- [ ] Review milestone effectiveness quarterly
- [ ] Update translations as needed
- [ ] Test on new iOS/Android versions
### When to Update
1. **App Store ID changes** (iOS)
- Update in `ratingService.ts` line 49
2. **Package name changes** (Android)
- Update in `ratingService.ts` line 53
3. **New languages added**
- Add translations in `features/i18n/translations/`
4. **Milestone strategy changes**
- Update in `useRatingPrompt.ts` line 8
---
## Contact
For questions or issues related to this feature:
- Check existing issues in GitHub
- Review analytics in PostHog dashboard
- Contact development team
---
**Last Updated:** 2025-01-30
**Author:** Claude Code
**Version:** 1.0.0

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,969 @@
Getting Started
Install Zeego
Start by installing Zeego:
yarn add zeego
For upgrading from Zeego 2 to Zeego 3, see the upgrade guide.
Install peer dependencies
A note for monorepo users: install these in the directory of your native app, not in the root of the monorepo.
The following exact versions must be installed. Please reference the compatibility table for the versions that work with your setup.
iOS Dependencies
yarn
yarn add react-native-ios-context-menu@3.1.0 react-native-ios-utilities@5.1.2
npm
npm i \
react-native-ios-context-menu@3.1.0 \
react-native-ios-utilities@5.1.2 \
--legacy-peer-deps
Android Dependencies
yarn
yarn add @react-native-menu/menu@1.2.2
npm
npm i @react-native-menu/menu@1.2.2 --legacy-peer-deps
Compatibility Table
Given a Zeego version (start there), see the dependencies to the right that it is compatible with.
Zeego React Native New Arch Expo SDK react-native-menu react-native-ios-context-menu react-native-ios-utilities
3 0.76 or 0.77 ✅ (optional) 52+ 1.2.2 3.1.0 5.1.2
2 0.74 ❌ 51, 50, 49* 1.0.2 2.5.1 4.5.3
*For Expo SDK 49 or lower, click here
Frameworks
Expo
Expo users need to use a custom development client, since Zeego uses native code.
After installing Zeego and its peer dependencies, you'll need to rebuild your custom development client:
expo run:ios -d
tip
To install your dev client on your iPhone, make sure it's plugged in to your Mac. If it doesn't show up, you may need to run expo prebuild -p ios, open ios/YourApp.xcworspace in XCode, and make sure your Apple team is properly set up.
After the development client build is complete, you can run your app in dev mode:
npx expo start --dev-client
If your app is on the App Store, you'll need to deploy a new build too:
eas build --platform ios --autosubmit
Zeego will not work with Expo Go.
If you aren't familiar with how Expo works / what custom development clients are, I recommend using EAS (Expo's hosted build service.) As long as you eas build, it will work.
Solito/Next.js
You need to add zeego to your transpilePackages in next.config.js.
// next.config.js
module.exports = {
transpilePackages: ['zeego'],
}
Vanilla React Native
Run pod install in your ios folder.
Usage
Here we'll look at a quick overview of what it looks like to use Zeego.
For a full overview of using custom styles and components, see the styling guide.
1. Create your primitives
Create a file for your primitives, and create each component.
Here, you can add custom styles and interactions. To keep your styles and components consistent, add them all in this file and re-export them.
If you've used Radix UI before, this will look familiar.
// design-system/dropdown-menu.tsx
import \* as DropdownMenu from 'zeego/dropdown-menu'
export const DropdownMenuRoot = DropdownMenu.Root
export const DropdownMenuContent = DropdownMenu.Content
// notice that we're using the create() function
export const DropdownMenuItem = DropdownMenu.create(
(props: React.ComponentProps<typeof DropdownMenu.Item>) => (
<DropdownMenu.Item {...props} style={{ height: 34 }} />
),
'Item'
)
// ...see "Full code samples" below to see the rest of the file
Notice that the DropdownMenuItem is wrapped with create(). This is a requirement for custom components.
Full code samples
Full dropdown-menu.tsx file
Full context-menu.tsx file 2. Build a menu
Build a menu using the primitives you created.
import { Text } from 'react-native'
import {
DropdownMenuRoot,
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenuItem,
DropdownMenuItemTitle,
} from '@/design-system/dropdown-menu'
function Menu() {
return (
<DropdownMenuRoot>
<DropdownMenuTrigger>
<Text>Open Dropdown Menu</Text>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem key="fernando rojo">
<DropdownMenuItemTitle>Fernando Rojo</DropdownMenuItemTitle>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuRoot>
)
} 3. Add styles
All Zeego primitives ship unstyled. See the styling guide use your own styles & build custom components.
The styles will apply on web, but other than the Trigger component, your iOS and Android apps will style using the native menu components.
You should make a habit of applying the styles in your primitive file we made above to keep your app's feel consistent.
4. Why the boilerplate?
You might look at step 1 and think, wait, why do I need to have such a big file to use this library?
The reality is, you don't need to, but you should. By wrapping third-party dependencies in one place and styling them yourself, you own the code more closely. It will allow for consumption of those files to be elegant.
If it looks overwhelming, don't worry. You can just copy-paste and continue.
Dropdown Menu
import \* as DropdownMenu from 'zeego/dropdown-menu'
A menu component anchored to a button pressed by a user.
Platform behavior
Web: Uses Radix UI's unstyled Dropdown Menu component
iOS & Android: Uses each platform's built-in native menu component
Features
Zeego's menu includes Radix UI's features:
Can be controlled or uncontrolled.
Supports submenus with configurable reading direction.
Supports items, labels, & groups of items.
Supports checkable items (single or multiple) with an optional indeterminate state.
Supports modal and non-modal modes.
Customize the side, alignment, offsets, collision handling.
Optionally render a pointing arrow.
Focus is fully managed.
Full keyboard navigation.
Typeahead support.
Dismissing and layering behavior is highly customizable.
Plus other unique ones:
SF Symbols on iOS with color customization
Android system icons
Images on menu items (remote images coming soon)
Installation
Make sure you've followed the getting started guide.
Usage
import \* as DropdownMenu from 'zeego/dropdown-menu'
export function MyMenu() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Label />
<DropdownMenu.Item>
<DropdownMenu.ItemTitle />
</DropdownMenu.Item>
<DropdownMenu.Group>
<DropdownMenu.Item />
</DropdownMenu.Group>
<DropdownMenu.CheckboxItem>
<DropdownMenu.ItemIndicator />
</DropdownMenu.CheckboxItem>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger />
<DropdownMenu.SubContent />
</DropdownMenu.Sub>
<DropdownMenu.Separator />
<DropdownMenu.Arrow />
</DropdownMenu.Content>
</DropdownMenu.Root>
)
}
Web Usage
As of Zeego v2, any component available on Radix UI's DropdownMenu is also available on Zeego's DropdownMenu, and accepts the same props, when rendered on web.
Since Zeego is directly built on top of Radix UI for Web, you can not use StyleSheet.create objects on Zeego's style props.
Instead, to style items, you can do one of the following:
Use inline style prop
Use className, common for Tailwind users a. You can optionally use CSS file imports or CSS modules
Use the create function to create your own component and style it however you want
Component API
Root
Required component at the root of your menu.
Prop Required Default Platforms
onOpenChange web, ios
onOpenWillChange ios
For more web props, see the Radix UI docs for DropdownMenu.Root.
Content
Holds all of the content for your menu.
See the Radix UI docs for DropdownMenu.Content to see usage for each prop.
Prop Required Default Platforms
side bottom web
sideOffset 0 web
align center web
alignOffset 0 web
collisionPadding 0 web
avoidCollisions true web
For more web props, see the Radix UI docs for DropdownMenu.Content.
Trigger
Wraps the trigger for your menu. The content will be anchored to the trigger.
Prop Required Default Platforms
style web, ios, android
action press ios, android
asChild false web, ios, android
The action can be longPress or press.
For more web props, see the Radix UI docs for DropdownMenu.Trigger.
Item
Dropdown menu item. Typically a row with text inside of it.
The key prop is required. The same key must not be used more than once in the same menu.
Prop Required Default Platforms
key Yes web, ios, android
disabled web, ios, android
destructive web, ios, android
hidden ios, android
style web
onSelect web
textValue web
onFocus web
onBlur web
On web, it will render its children and apply styles. On other platforms, it simply maps to a native menu item.
To render text, use the ItemTitle.
<DropdownMenu.Item key="item-1" onSelect={() => console.log('item-1 selected')}>
<DropdownMenu.ItemTitle>Item Title</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
For more web props, see the Radix UI docs for DropdownMenu.Item.
ItemTitle
The style prop will optionally style text on web.
Prop Required Default Platforms
style web,
children Yes web , ios, android
ItemTitle accepts either a string or React element as the child. A string is the most common usage.
<DropdownMenu.Item key="cars">
<DropdownMenu.ItemTitle>Cars</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
React element child
ItemTitle supports passing a text node as the child. However, you must pass a textValue prop to the parent Item for this to work. It will error otherwise.
<DropdownMenu.Item
// this is required when ItemTitle has a React element child
textValue="Cars"
key="cars"
> <DropdownMenu.ItemTitle>
<Text>
Cars
</Text>
<DropdownMenu.ItemTitle>
</DropdownMenu.Item>
This is useful for rendering custom text components on Web. The textValue prop supplied to Item will get used on iOS and Android as the title. On Web, textValue will be used for typeahead purposes, but it will not affect rendering.
ItemIcon
To render an icon on web, pass the icon component as a child.
For iOS and Android, use the respective platform's name as the prop.
On iOS, it renders an SF Symbol if you provide one.
Prop Required Default Platforms
ios ios
androidIconName android
children web
style web
className web
<DropdownMenu.ItemIcon
ios={{
name: '0.circle.fill', // required
pointSize: 5,
weight: 'semibold',
scale: 'medium',
// can also be a color string. Requires iOS 15+
hierarchicalColor: {
dark: 'blue',
light: 'green',
},
// alternative to hierarchical color. Requires iOS 15+
paletteColors: [
{
dark: 'blue',
light: 'green',
},
],
}}
> <MySvgIconForWeb />
> </DropdownMenu.ItemIcon>
> ItemImage
> Renders an image inside of the item. Works on Web and iOS. On Android, it will be ignored.
Prop Required Default Platforms
source web, ios
style web
resizeMode web
height web
width web
fadeDuration 0 web
accessibilityLabel web
For local images with require or import:
<DropdownMenu.ItemImage
source={require('./image.png')}
height={300}
width={300}
/>
For remote images, you can use the source prop with a URL.
<DropdownMenu.ItemImage
source={{
uri: `https://my-image-url`,
}}
height={300}
width={300}
/>
ItemImage with Expo Web / Metro Web
If you are using Solito, Vite, Next.js, or most web-only frameworks, then this does not apply to you.
However, as of Zeego v2, locally-imported images will not work as-is with Metro Web/Expo Web.
To fix this, you should create a custom ItemImage component which wraps Image from react-native:
import { Image } from 'react-native'
import \* as DropdownMenu from 'zeego/dropdown-menu'
const ItemImage = DropdownMenu.create<
React.ComponentProps<typeof DropdownMenu.ItemImage>
> ((props) => {
> return <Image {...props} />
> }, 'ItemImage')
> ItemSubtitle
> Receives children as a string. The style prop will optionally style text on web.
Prop Required Default Platforms
style web,
className web
children web , ios
Android menu items do not currently support subtitles.
Group
Used to group multiple items.
On iOS, items will visually group with a divider like Group Item 1 and Group Item 2 below:
On iOS, you can use the horizontal prop render items like so:
image
Prop Required Default Platforms
children web , ios, android
horizontal ios
To add a title to the group, pass a Label component inside of it:
<DropdownMenu.Group>
<DropdownMenu.Label>Fernando</DropdownMenu.Label>
</DropdownMenu.Group>
For more web props, see the Radix UI docs for DropdownMenu.Group.
CheckboxItem
Usage is similar to Item with added checkbox features.
Prop Required Default Platforms
key Yes web, ios, android
value Yes web, ios, android
disabled web , ios, android
destructive web , ios, android
onValueChange web , ios, android
hidden web , ios, android
textValue web
style web
onFocus web
onBlur web
<DropdownMenu.CheckboxItem
value="on" // or "off" or "mixed"
onValueChange={(next, previous) => {
// update state here
}}
key="fernando"
> <DropdownMenu.ItemIndicator />
> </DropdownMenu.CheckboxItem>
> You can also use a boolean for value, as of 1.3.0:
<DropdownMenu.CheckboxItem
value={true}
onValueChange={(next, previous) => {
// next and previous will still use "on" and "off"
}}
key="fernando"
> <DropdownMenu.ItemIndicator />
> </DropdownMenu.CheckboxItem>
> For more web props, see the Radix UI docs for DropdownMenu.CheckboxItem.
There are a few subtle differences, such as onValueChange vs onCheckedChange, and the Zeego's result being "on" or "off" instead of true or false in the change callback.
ItemIndicator
Used inside of CheckboxItem, the ItemIndicator only renders when the item is checked. This lets you conditionally render a checkmark.
You should pass a checkmark icon as a child for web. On iOS and Android, the built-in checkmark will be used instead.
Prop Required Default Platforms
style web
className web
children web
<DropdownMenu.ItemIndicator>
<CheckmarkIcon /> {/_ Renders on Web only _/}
</DropdownMenu.ItemIndicator>
For more web props, see the Radix UI docs for DropdownMenu.ItemIndicator.
Label
Renders a label. It won't be focusable using arrow keys.
On iOS & Android, only one label is supported (unless it is inside a submenu). It will be displayed at the top of the menu.
Prop Required Default Platforms
style web
className web
children Yes web, ios, android
<DropdownMenu.Label>My Label</DropdownMenu.Label>
Best used within a Group.
For more web props, see the Radix UI docs for DropdownMenu.Label.
Arrow
Renders an arrow on web only. This helps point the content to the trigger. The Arrow must be rendered inside of Content.
By default, Radix renders the arrow as an <svg> element. You can customize the SVG arrow color by passing a fill prop, className, or style object with a fill property.
caution
Because the arrow is an <svg> element, its style prop is not React Native compatible. Styling it with React Native libraries may not work as expected. If you would like to render a custom styled <View>, use the asChild prop instead of wrapping this component.
Prop Required Default Platforms
width 10 web
height 5 web
fill web
style web
className web
asChild false web
See the Radix UI docs for DropdownMenu.Arrow.
Separator
Renders a content separator on web only.
Prop Required Default Platforms
style web
className web
See the Radix UI docs for DropdownMenu.Separator.
Sub
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger key="sub-menu-trigger">
<DropdownMenu.ItemTitle>Sub Menu</DropdownMenu.ItemTitle>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent>
<DropdownMenu.Item key="sub-menu-item">
<DropdownMenu.ItemTitle>Sub Menu Item</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
</DropdownMenu.Content>
</DropdownMenu.Root>
Renders the parts of a submenu.
Prop Required Default Platforms
onOpenChange web
SubContent
See the Radix UI docs for DropdownMenu.SubContent to see usage for each prop.
Prop Required Default Platforms
side bottom web
sideOffset 0 web
align center web
alignOffset 0 web
collisionPadding 0 web
avoidCollisions true web
SubTrigger
An item that opens a submenu. Must be rendered inside DropdownMenu.Sub.
Prop Required Default Platforms
key Yes web, ios, android
disabled web , ios, android
destructive web , ios, android
hidden ios, android
style web
onSelect web
textValue web
onFocus web
onBlur web
For more web props, see the Radix UI docs for DropdownMenu.SubTrigger.
Context Menu
import \* as ContextMenu from 'zeego/context-menu'
A menu component anchored to an element. On web, the menu is triggered by a right click. On iOS & Android, it is triggered by a long press.
Platform behavior
Web: Uses Radix UI's unstyled Context Menu component
iOS & Android: Uses each platform's built-in native context menu component
Features
Zeego's menu includes Radix UI's features:
Can be controlled or uncontrolled.
Supports submenus with configurable reading direction.
Supports items, labels, & groups of items.
Supports checkable items (single or multiple) with an optional indeterminate state.
Supports modal and non-modal modes.
Customize the side, alignment, offsets, & collision handling.
Optionally render a pointing arrow.
Focus is fully managed.
Full keyboard navigation.
Typeahead support.
Dismissing and layering behavior is highly customizable.
Plus other unique ones:
Custom element preview on iOS
SF Symbols on iOS with color customization
Android system icons
Images on menu items (remote images coming soon)
Installation
Make sure you've followed the getting started guide.
Usage
import \* as ContextMenu from 'zeego/context-menu'
export function MyMenu() {
return (
<ContextMenu.Root>
<ContextMenu.Trigger>
<Button />
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Preview>{() => <Preview />}</ContextMenu.Preview>
<ContextMenu.Label />
<ContextMenu.Item>
<ContextMenu.ItemTitle />
</ContextMenu.Item>
<ContextMenu.Group>
<ContextMenu.Item />
</ContextMenu.Group>
<ContextMenu.CheckboxItem>
<ContextMenu.ItemIndicator />
</ContextMenu.CheckboxItem>
<ContextMenu.Sub>
<ContextMenu.SubTrigger />
<ContextMenu.SubContent />
</ContextMenu.Sub>
<ContextMenu.Separator />
<ContextMenu.Arrow />
</ContextMenu.Content>
</ContextMenu.Root>
)
}
Web Usage
As of Zeego v2, any component available on Radix UI's ContextMenu is also available on Zeego's ContextMenu, and accepts the same props, when rendered on web.
Since Zeego is directly built on top of Radix UI for Web, you can not use StyleSheet.create objects on Zeego's style props.
Instead, to style items, you can do one of the following:
Use inline style prop
Use className, common for Tailwind users a. You can optionally use CSS file imports or CSS modules
Use the create function to create your own component and style it however you want
Component API
Root
Required component at the root of your menu.
Prop Required Default Platforms
onOpenChange web, ios
onOpenWillChange ios
For more web props, see the Radix UI docs for ContextMenu.Root.
Content
Holds all of the content for your menu.
See the Radix UI docs for ContextMenu.Content to see usage for each prop.
Prop Required Default Platforms
side bottom web
sideOffset 0 web
align center web
alignOffset 0 web
collisionPadding 0 web
avoidCollisions true web
For more web props, see the Radix UI docs for ContextMenu.Content.
Trigger
Wraps the trigger for your menu. The content will be anchored to the trigger.
Prop Required Default Platforms
style web, ios, android
action longPress ios, android
asChild false web, ios, android
The action can be longPress or press.
For more web props, see the Radix UI docs for ContextMenu.Trigger.
Preview
Render a custom component when the context menu is visible on iOS.
Requires passing a function as a child.
The child element won't mount until the menu has been opened.
Prop Required Default Platforms
style ios
size ios
onPress ios
isResizeAnimated true ios
borderRadius ios
backgroundColor ios
preferredCommitStyle ios
<ContextMenu.Preview
// optional props:
preferredCommitStyle="pop" // or "dismiss"
backgroundColor={{
// or a color string directly
dark: 'black',
light: 'white',
}}
> {() => <Preview />}
> </ContextMenu.Preview>
> Item
> Context menu item. Typically a row with text inside of it.
The key prop is required. The key must be unique within the entire menu, including submenus.
Prop Required Default Platforms
key Yes web, ios, android
disabled web, ios, android
destructive web, ios, android
hidden ios, android
style web
onSelect web, android, ios
textValue web
onFocus web
onBlur web
On web, Item will render its a component as its child and apply styles. On other platforms, it simply renders a native menu item, and styles do not apply.
To render text inside of an item, use the ItemTitle.
<ContextMenu.Item key="item-1" onSelect={() => console.log('item-1 selected')}>
<ContextMenu.ItemTitle>Item Title</ContextMenu.ItemTitle>
</ContextMenu.Item>
For more web props, see the Radix UI docs for ContextMenu.Item.
ItemTitle
The style prop will optionally style text on web.
Prop Required Default Platforms
style web,
children Yes web , ios, android
ItemTitle either a string or React element as the child. A string is the most common usage. If you don't use a string, you must pass a textValue prop to the parent Item for it to work. It will error otherwise.
<ContextMenu.Item key="cars">
<ContextMenu.ItemTitle>Cars</ContextMenu.ItemTitle>
</ContextMenu.Item>
React element child
ItemTitle supports passing a text node as the child. However, you must pass a textValue prop to the parent Item for this to work. It will error otherwise.
<ContextMenu.Item
// this is required when ItemTitle has a React element child
textValue="Cars"
key="cars"
> <ContextMenu.ItemTitle>
<Text>
Cars
</Text>
<ContextMenu.ItemTitle>
</ContextMenu.Item>
The textValue prop will become the title on iOS and Android as the title. On Web, textValue will be used for typeahead, but it will not affect rendering.
ItemIcon
To render an icon on web, pass the icon component as a child.
For iOS and Android, use the ios prop and androidIconName prop.
On iOS, it renders an SF Symbol if you provide one.
On Android, it renders a Material Icon.
Prop Required Default Platforms
ios ios
androidIconName android
children web
style web
className web
<ContextMenu.ItemIcon
ios={{
name: '0.circle.fill', // required
pointSize: 5,
weight: 'semibold',
scale: 'medium',
// can also be a color string. Requires iOS 15+
hierarchicalColor: {
dark: 'blue',
light: 'green',
},
// alternative to hierarchical color. Requires iOS 15+
paletteColors: [
{
dark: 'blue',
light: 'green',
},
],
}}
> <MySvgIconForWeb />
> </ContextMenu.ItemIcon>
> ItemImage
> Renders an image inside of the item. Works on Web and iOS. On Android, it will be ignored.
Prop Required Default Platforms
source web, ios
style web
resizeMode web
height web
width web
fadeDuration 0 web
accessibilityLabel web
<ContextMenu.ItemImage
source={require('./image.png')}
height={300}
width={300}
/>
Once it works, usage will work like so:
<ContextMenu.ItemImage
source={{
uri: `https://my-image-url`,
}}
height={300}
width={300}
/>
ItemImage with Expo Web / Metro Web
If you are using Solito, Vite, Next.js, or most web-only frameworks, then this does not apply to you.
However, as of Zeego v2, locally-imported images will not work as-is with Metro Web/Expo Web.
To fix this, you should create a custom ItemImage component which wraps Image from react-native:
import { Image } from 'react-native'
import \* as ContextMenu from 'zeego/context-menu'
const ItemImage = ContextMenu.create<
React.ComponentProps<typeof ContextMenu.ItemImage>
> ((props) => {
> return <Image {...props} />
> }, 'ItemImage')
> ItemSubtitle
> Receives children as a string. The style prop will optionally style text on web.
Prop Required Default Platforms
style web,
className web
children web , ios
Android menu items do not currently support subtitles.
Group
Used to group multiple items.
On iOS, items will visually group with a divider like Group Item 1 and Group Item 2 below:
On iOS, you can use the horizontal prop render items like so:
image
Prop Required Default Platforms
children web , ios, android
horizontal ios
To add a title to the group, pass a Label component inside of it:
<ContextMenu.Group>
<ContextMenu.Label>Fernando's List</ContextMenu.Label>
<ContextMenu.Item key="patos">
<ContextMenu.ItemTitle>PATOS Shoes</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item key="moti">
<ContextMenu.ItemTitle>Moti</ContextMenu.ItemTitle>
</ContextMenu.Item>
<ContextMenu.Item key="solito">
<ContextMenu.ItemTitle>Solito</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.Group>
For more web props, see the Radix UI docs for ContextMenu.Group.
CheckboxItem
Usage is similar to Item with added checkbox features.
Prop Required Default Platforms
key Yes web, ios, android
value Yes web, ios, android
disabled web , ios, android
destructive web , ios, android
onValueChange web , ios, android
hidden web , ios, android
textValue web
style web
onFocus web
onBlur web
<ContextMenu.CheckboxItem
value="on" // or "off" or "mixed"
onValueChange={(next, previous) => {
// update state here
}}
key="fernando"
> <ContextMenu.ItemIndicator />
> </ContextMenu.CheckboxItem>
> You can also use a boolean for value, as of 1.3.0:
<ContextMenu.CheckboxItem
value={true}
onValueChange={(next, previous) => {
// next and previous will still use "on" and "off"
}}
key="fernando"
> <ContextMenu.ItemIndicator />
> </ContextMenu.CheckboxItem>
> For more web props, see the Radix UI docs for ContextMenu.CheckboxItem.
There are a few subtle differences, such as onValueChange vs onCheckedChange, and the Zeego's result being "on" or "off" instead of true or false in the change callback.
ItemIndicator
Used inside of CheckboxItem, the ItemIndicator only renders when the item is checked. This lets you conditionally render a checkmark.
You should pass a checkmark icon as a child for web. On iOS and Android, the built-in checkmark will be used instead.
Prop Required Default Platforms
style web
className web
children web
<ContextMenu.ItemIndicator>
<CheckmarkIcon /> {/_ Renders on Web only _/}
</ContextMenu.ItemIndicator>
For more web props, see the Radix UI docs for ContextMenu.ItemIndicator.
Label
Renders a label. It won't be focusable using arrow keys.
On iOS & Android, only one label is supported (unless it is inside a submenu). It will be displayed at the top of the menu.
Prop Required Default Platforms
style web
className web
children Yes web, ios, android
<ContextMenu.Label>My Label</ContextMenu.Label>
Best used within a Group.
For more web props, see the Radix UI docs for ContextMenu.Label.
Arrow
Renders an arrow on web only. This helps point the content to the trigger. The Arrow must be rendered inside of Content.
By default, Radix renders the arrow as an <svg> element. You can customize the SVG arrow color by passing a fill prop, className, or style object with a fill property.
caution
Because the arrow is an <svg> element, its style prop is not React Native compatible. Styling it with React Native libraries may not work as expected. If you would like to render a custom styled <View>, use the asChild prop instead of wrapping this component.
Prop Required Default Platforms
width 10 web
height 5 web
fill web
style web
className web
asChild false web
See the Radix UI docs for ContextMenu.Arrow.
Separator
Renders a content separator on web only.
Prop Required Default Platforms
style web
className web
See the Radix UI docs for ContextMenu.Separator.
Sub
<ContextMenu.Root>
<ContextMenu.Trigger>
<Button />
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Sub>
<ContextMenu.SubTrigger key="sub-menu-trigger">
<ContextMenu.ItemTitle>Sub Menu</ContextMenu.ItemTitle>
</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
<ContextMenu.Item key="sub-menu-item">
<ContextMenu.ItemTitle>Sub Menu Item</ContextMenu.ItemTitle>
</ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
</ContextMenu.Content>
</ContextMenu.Root>
Renders the parts of a submenu.
Prop Required Default Platforms
onOpenChange web
children web, ios, android
See the Radix UI docs for ContextMenu.Sub.
SubContent
See the Radix UI docs for ContextMenu.SubContent to see usage for each prop.
Prop Required Default Platforms
side bottom web
sideOffset 0 web
align center web
alignOffset 0 web
collisionPadding 0 web
avoidCollisions true web
children web, ios, android
For more web props, see the Radix UI docs for ContextMenu.SubContent.
SubTrigger
An item that opens a submenu. Must be rendered inside ContextMenu.Sub.
Prop Required Default Platforms
key Yes web, ios, android
disabled web , ios, android
destructive web , ios, android
hidden ios, android
style web
onSelect web
textValue web
onFocus web
onBlur web
children web, ios, android
For more web props, see the Radix UI docs for ContextMenu.SubTrigger.
Tailwind / Nativewind
Zero config ⚡️
As of Zeego 2, Zeego uses plain Radix UI on Web and therefore does not require any configuration for using Tailwind CSS or Nativewind.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,194 @@
# Split-View Implementation Plan für iPad & Web
## Übersicht
Implementation einer Apple Notes-ähnlichen Split-View für die Memos-Seite auf iPad und größeren Bildschirmen. Links die Memo-Liste, rechts die Detail-Ansicht.
## Verfügbare Optionen
### Option 1: Responsive Container-Ansatz (Empfohlen) ✅
**Vorteile**:
- Einfach und schnell umzusetzen
- Nutzt bestehende Komponenten wieder
- Keine Breaking Changes für mobile Ansicht
**Nachteile**:
- Keine native Split-View Navigation
- URL bleibt auf /memos statt /memos/[id]
**Umsetzung**:
- Conditional Rendering basierend auf `useResponsive()` Hook
- Flexbox Layout mit zwei Spalten
- Memo-Liste links (40%), Detail rechts (60%)
- State-basierte Navigation statt Router
### Option 2: Expo Router Groups mit Parallel Routes
**Vorteile**:
- Native Router-Integration
- Deep-Linking Support
- URLs spiegeln aktuelle Ansicht wider
**Nachteile**:
- Komplexer, mehr Refactoring nötig
- Potenzielle Breaking Changes
- Mehr Testing erforderlich
**Umsetzung**:
- Neue Route-Gruppe `(split)` für Tablet/Desktop
- Parallel Routes für Liste und Detail
- Platform-basiertes Routing
- Conditional exports je nach Bildschirmgröße
### Option 3: Custom Navigation Container
**Vorteile**:
- Volle Kontrolle über Navigation
- Optimale Performance
- Maximale Flexibilität
**Nachteile**:
- Sehr aufwändig
- Eigene Navigation-Logic nötig
- Wartungsintensiv
**Umsetzung**:
- Eigener SplitViewController
- State Management für aktives Memo
- Custom Navigation Handling
- Eigene Gesture-Handler
## Implementierungsschritte (Option 1)
### 1. Responsive Layout Component erstellen
```typescript
// components/containers/SplitViewContainer.tsx
- Layout-Logik für Split-View
- Nutzt useResponsive() für Breakpoint-Detection
- Conditional Rendering je nach Bildschirmgröße
- Props: leftPanel, rightPanel, showSplit
```
### 2. Embedded MemoDetail Component
```typescript
// components/organisms/MemoDetailEmbedded.tsx
- Variante der Memo-Detail-Seite als Component
- Akzeptiert memoId als Prop
- Eigenes Loading/Error Handling
- Keine Router-Navigation
```
### 3. Memos-Seite anpassen
```typescript
// app/(protected)/(tabs)/memos.tsx
- Split-View Modus erkennen mit useResponsive()
- selectedMemoId State hinzufügen
- MemoList onMemoPress Handler anpassen
- SplitViewContainer einbinden
```
### 4. State Management
- `selectedMemoId` in memos.tsx verwalten
- Bei Klick auf Memo:
- Mobile: router.push()
- Tablet/Desktop: setSelectedMemoId()
- Sync mit URL optional über searchParams
### 5. Navigation anpassen
- Back-Button nur auf Mobile
- ESC-Taste zum Schließen auf Desktop
- Swipe-Gesten auf Tablet
- Keyboard-Navigation (Arrow Keys)
### 6. UI Optimierungen
- Selektion-Highlighting in Liste
- Responsive Padding/Margins
- Smooth Transitions
- Loading States
- Empty States
## Technische Details
### Breakpoints
- **Mobile**: < 768px (normale Navigation)
- **Tablet**: ≥ 768px (Split-View)
- **Desktop**: ≥ 1024px (Split-View mit mehr Platz)
- **Wide**: ≥ 1440px (optimierte Breiten)
### Layout-Verhältnisse
- **Standard**: 40% Liste / 60% Detail
- **Kompakt**: 35% Liste / 65% Detail
- **Wide**: 30% Liste / 70% Detail
### Mindestbreiten
- **Liste**: min. 320px
- **Detail**: min. 480px
- **Gesamt**: min. 800px für Split-View
### State Management
```typescript
interface SplitViewState {
selectedMemoId: string | null;
isDetailOpen: boolean;
splitRatio: number;
}
```
## Implementierungs-Reihenfolge
1. **Phase 1: Grundgerüst**
- SplitViewContainer Component
- useResponsive Hook erweitern
- Basic Layout Test
2. **Phase 2: Integration**
- MemoDetailEmbedded Component
- Memos.tsx anpassen
- State Management
3. **Phase 3: Navigation**
- Conditional Navigation Logic
- Keyboard Support
- Touch/Swipe Gesten
4. **Phase 4: Polish**
- Animationen
- Loading States
- Error Boundaries
- Performance Optimierung
## Offene Fragen
1. **URL-Sync**: Soll die URL den ausgewählten Memo widerspiegeln?
2. **Persistenz**: Soll die Auswahl beim Reload erhalten bleiben?
3. **Gesten**: Welche Touch-Gesten sollen unterstützt werden?
4. **Resize**: Soll der User die Split-Ratio anpassen können?
5. **Master-Detail**: Soll auf Tablet im Portrait-Modus ein Overlay verwendet werden?
## Performance-Überlegungen
- Lazy Loading für MemoDetail
- Virtualisierung der Memo-Liste beibehalten
- Memoization für expensive Berechnungen
- Debounced Resize-Handler
- Optimistic UI Updates
## Testing-Strategie
1. **Unit Tests**
- SplitViewContainer Logic
- Responsive Hook
- State Management
2. **Integration Tests**
- Navigation Flow
- Data Loading
- Error Handling
3. **E2E Tests**
- Mobile Navigation
- Tablet Split-View
- Desktop Keyboard Nav
4. **Manual Testing**
- iPad (verschiedene Modelle)
- Web (verschiedene Browser)
- Resize-Verhalten
- Orientation Changes

View file

@ -0,0 +1,726 @@
# Zeego Kompatibilitätsprobleme und Alternativen-Analyse
## Zusammenfassung
Datum: 30. September 2025
Unser Projekt nutzt aktuell Zeego v3.0.6 für Context Menus und Dropdown Menus. Mit dem Upgrade auf Expo SDK 54 (React Native 0.81) und iOS 26 treten kritische Kompatibilitätsprobleme auf, die die App am Starten hindern.
## Aktueller Fehler
```
Unable to resolve module react-native-ios-context-menu from
/Users/tillschneider/Documents/__00__Code/memoro/node_modules/zeego/lib/module/menu/create-ios-menu/index.ios.js:
react-native-ios-context-menu could not be found within the project
```
## Problemanalyse
### Zeego Kompatibilitätsstatus
**Zeego Version 3.x Kompatibilität:**
- ✅ React Native: 0.76 oder 0.77
- ✅ Expo SDK: 52+
- ❌ React Native 0.81 (SDK 54): **Nicht offiziell unterstützt**
- ❌ iOS 26: **Keine offizielle Kompatibilität**
**Abhängigkeiten:**
- `react-native-menu`: 1.2.2
- `react-native-ios-context-menu`: 3.1.0
- `react-native-ios-utilities`: 5.1.2
### Ursache des Problems
Laut GitHub Issue #173 auf dem Zeego Repository:
1. **Pod Install Fehler:**
- `Unable to find a specification for RCT-Folly (= 2022.05.16.00) depended upon by react-native-ios-context-menu`
- `Unable to find a specification for RCT-Folly (= 2022.05.16.00) depended upon by react-native-ios-utilities`
2. **Status:**
- Der Maintainer (nandorojo) hat ein PR im upstream Library gemerged
- Wartet auf Veröffentlichung der Fixes
- **react-native-ios-context-menu befindet sich im "Maintenance Mode"** - der Autor macht aktuell kein OSS mehr
### Expo SDK 54 Spezifika
- React Native 0.81 mit precompiled XCFrameworks für iOS
- Xcode 26 erforderlich
- Node.js 20.19.4+ erforderlich
- **RCT-Folly Versionskonflikt** mit älteren nativen Dependencies
## Aktuelle Zeego-Nutzung im Projekt
Zeego wird an **15 Stellen** im Projekt verwendet:
### Context Menus (4 Verwendungen)
- `components/organisms/Memory.tsx`
- `components/molecules/PromptPreview.tsx`
- `components/molecules/MemoPreview.tsx`
### Dropdown Menus (11 Verwendungen)
- `components/molecules/TableOfContentsMenu.tsx`
- `components/atoms/Pill.tsx`
- `features/subscription/SubscriptionMenu.tsx`
- `features/menus/MemoHeaderMenu.tsx`
- `features/menus/MemoMenu.tsx`
- `features/menus/HeaderMenu.tsx`
- Weitere Komponenten
## Alternative Lösungen - Detaillierte Analyse
### Option 1: Warten auf Zeego-Update 🕐
**Beschreibung:**
Warten, bis Zeego offiziell Expo SDK 54 und React Native 0.81 unterstützt.
**Vorteile:**
- ✅ Keine Code-Änderungen nötig
- ✅ Behält bestehende API und Funktionalität
- ✅ Native Performance bleibt erhalten
**Nachteile:**
- ❌ Unbekannter Zeitrahmen
- ❌ `react-native-ios-context-menu` ist im Maintenance Mode (Autor macht kein OSS mehr)
- ❌ Blockiert SDK 54 Upgrade
- ❌ Keine iOS 26 Features verfügbar
- ❌ Sicherheits- und Performance-Updates von SDK 54 nicht verfügbar
**Zeitaufwand:** 0 Stunden (aber unbestimmte Wartezeit)
**Risiko:** Hoch - keine Garantie für zeitnahe Lösung
---
### Option 2: Expo/React Native Action Sheet 📱
**Beschreibung:**
Ersetze Dropdown- und Context-Menus durch `@expo/react-native-action-sheet` (bereits im Projekt als Dependency).
**Vorteile:**
- ✅ **Bereits im Projekt vorhanden** (`@expo/react-native-action-sheet@^4.1.1`)
- ✅ Offiziell von Expo maintained
- ✅ 100% Expo SDK 54 kompatibel
- ✅ Funktioniert mit Expo Go
- ✅ Native iOS und Android UIs
- ✅ Einfache API
- ✅ Keine zusätzlichen Dependencies
**Nachteile:**
- ❌ **Limitierte Funktionalität** - keine verschachtelten Menüs, keine Icons, keine Checkboxen
- ❌ Andere UX - Modal von unten statt Context Menu
- ❌ Funktioniert nicht auf Web (nur mobil)
- ❌ Keine Context Menu Gesten (Long Press)
**Beispiel Migration:**
```tsx
// Vorher (Zeego)
import * as DropdownMenu from 'zeego/dropdown-menu';
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button />
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Item key="delete" onSelect={handleDelete}>
<DropdownMenu.ItemTitle>Löschen</DropdownMenu.ItemTitle>
<DropdownMenu.ItemIcon ios={{ name: 'trash' }} />
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
// Nachher (Action Sheet)
import { useActionSheet } from '@expo/react-native-action-sheet';
const { showActionSheetWithOptions } = useActionSheet();
<Button onPress={() => {
showActionSheetWithOptions({
options: ['Löschen', 'Abbrechen'],
destructiveButtonIndex: 0,
cancelButtonIndex: 1,
}, (buttonIndex) => {
if (buttonIndex === 0) handleDelete();
});
}} />
```
**Zeitaufwand:** 6-8 Stunden (alle 15 Verwendungen anpassen)
**Risiko:** Niedrig - stabile, gut getestete Library
**Empfohlen für:** Schnelle Lösung, wenn advanced Features nicht kritisch sind
---
### Option 3: react-native-context-menu-view 📋
**Beschreibung:**
Cross-platform Context Menu Library (iOS + Android).
**Vorteile:**
- ✅ Context Menus auf iOS und Android
- ✅ Einfachere API als Zeego
- ✅ Native UI auf beiden Plattformen
- ✅ Unterstützt Icons und Submenus
**Nachteile:**
- ❌ **Wartungsprobleme** - viele offene Issues
- ❌ **Keine bestätigte New Architecture Unterstützung**
- ❌ **Unklare SDK 54 Kompatibilität**
- ❌ Nicht auf Web verfügbar
- ❌ Nur Context Menus, keine Dropdown Menus
**Status der Library:**
- Latest Version: 1.19.0 (vor 5 Monaten)
- Maintenance: "Not nearly as good" laut Community-Feedback
- Signifikante offene Issues
**Zeitaufwand:** 8-10 Stunden
**Risiko:** Mittel-Hoch - unklare Kompatibilität, Wartungsprobleme
**Nicht empfohlen** wegen fehlender New Architecture Unterstützung
---
### Option 4: Native Platform APIs direkt nutzen 🎯
**Beschreibung:**
Direkte Verwendung von Platform-spezifischen APIs ohne Wrapper Library.
**iOS:**
- `@react-native-menu/menu@2.0.0` für iOS Context Menus (UIMenu)
**Android:**
- `@react-native-menu/menu@2.0.0` für Android PopupMenu
**Web:**
- Radix UI Dropdown Menu / Context Menu
**Vorteile:**
- ✅ Maximale Kontrolle
- ✅ Native Features und Performance
- ✅ Keine Wrapper-Dependencies
- ✅ `@react-native-menu/menu` hat v2.0.0 vor 14 Tagen released
- ✅ Expo config plugin vorhanden
**Nachteile:**
- ❌ **3 verschiedene Implementierungen** (iOS, Android, Web)
- ❌ Viel Boilerplate Code
- ❌ Höherer Wartungsaufwand
- ❌ Inkonsistente APIs über Plattformen
**Beispiel Platform Switch:**
```tsx
import { Platform } from 'react-native';
import { ContextMenuView } from '@react-native-menu/menu'; // iOS/Android
import * as RadixDropdown from '@radix-ui/react-dropdown-menu'; // Web
const Menu = Platform.select({
ios: IOSContextMenu,
android: AndroidContextMenu,
web: RadixWebMenu,
});
```
**Zeitaufwand:** 12-16 Stunden
**Risiko:** Mittel - komplexer, aber volle Kontrolle
**Empfohlen für:** Teams mit Platform-spezifischem UI-Bedarf
---
### Option 5: Hybrid-Lösung mit Fallbacks 🔀
**Beschreibung:**
Kombiniere mehrere Libraries basierend auf Use Case.
**Strategie:**
- **Einfache Dropdown Menus:** Action Sheet
- **iOS Context Menus (kritisch):** `@react-native-menu/menu@2.0.0`
- **Android:** Action Sheet oder `@react-native-menu/menu`
- **Web:** Radix UI oder Action Sheet-ähnliche Modals
**Vorteile:**
- ✅ Best-of-both-worlds Ansatz
- ✅ Sofort funktionsfähig
- ✅ Optimiert für jeden Use Case
- ✅ Schrittweise Migration möglich
**Nachteile:**
- ❌ Höhere Code-Komplexität
- ❌ Mehr Dependencies
- ❌ Inkonsistente UX möglich
**Zeitaufwand:** 10-14 Stunden
**Risiko:** Mittel - erhöhte Komplexität
---
### Option 6: Custom Menu Components 🛠️
**Beschreibung:**
Baue eigene Menu-Komponenten mit React Native Modals, Pressables und Animations.
**Vorteile:**
- ✅ Volle Kontrolle über UX
- ✅ Cross-platform konsistent
- ✅ Keine nativen Dependencies
- ✅ Funktioniert mit Expo Go
- ✅ Keine Kompatibilitätsprobleme
**Nachteile:**
- ❌ **Sehr hoher Zeitaufwand** (20-30 Stunden)
- ❌ Kein natives Look-and-Feel
- ❌ Performance-Optimierung nötig
- ❌ Accessibility muss selbst implementiert werden
- ❌ Wartungsaufwand sehr hoch
**Zeitaufwand:** 20-30 Stunden initial + ongoing maintenance
**Risiko:** Hoch - Reinventing the wheel
**Nicht empfohlen** außer bei sehr spezifischen UI-Anforderungen
---
## Vergleichsmatrix
| Kriterium | Option 1: Warten | Option 2: Action Sheet | Option 3: context-menu-view | Option 4: Native APIs | Option 5: Hybrid | Option 6: Custom |
|-----------|------------------|------------------------|------------------------------|----------------------|------------------|------------------|
| **Zeitaufwand** | 0h (Wartezeit) | 6-8h | 8-10h | 12-16h | 10-14h | 20-30h |
| **SDK 54 Ready** | ❌ Nein | ✅ Ja | ⚠️ Unklar | ✅ Ja | ✅ Ja | ✅ Ja |
| **New Arch Support** | ⚠️ Teils | ✅ Ja | ❌ Unklar | ✅ Ja | ✅ Ja | ✅ Ja |
| **Native Look** | ✅ Ja | ✅ Ja | ✅ Ja | ✅ Ja | ✅ Ja | ❌ Nein |
| **Web Support** | ✅ Ja | ❌ Nein | ❌ Nein | ✅ Ja | ⚠️ Teils | ✅ Ja |
| **Maintenance** | ⚠️ Extern | ✅ Niedrig | ❌ Hoch | ⚠️ Mittel | ⚠️ Mittel | ❌ Sehr Hoch |
| **Feature-Vollständigkeit** | ✅✅✅ | ⚠️ | ✅✅ | ✅✅✅ | ✅✅✅ | ✅✅✅ |
| **Risiko** | 🔴 Hoch | 🟢 Niedrig | 🟡 Mittel-Hoch | 🟡 Mittel | 🟡 Mittel | 🔴 Hoch |
---
## Empfehlung
### 🥇 Primär-Empfehlung: Option 2 (Action Sheet) + Option 4 (Native APIs für kritische Features)
**Begründung:**
1. **Sofortige Lösung für SDK 54 Upgrade**
- Blockiert nicht länger den SDK 54 Rollout
- Zugriff auf iOS 26 und Android 16 Features
- Sicherheitsupdates von React Native 0.81
2. **Schrittweiser Migrations-Ansatz**
- **Phase 1 (Quick Win - 2-3 Tage):**
- Ersetze alle Dropdown Menus mit Action Sheets
- Funktionale App, SDK 54 kompatibel
- **Phase 2 (Optimierung - 1-2 Wochen):**
- Identifiziere kritische Context Menu Use Cases (z.B. Memory.tsx)
- Implementiere mit `@react-native-menu/menu@2.0.0` für native iOS Context Menus
- Fallback zu Action Sheet auf Android
- **Phase 3 (Langfristig - optional):**
- Monitoring von Zeego Updates
- Wenn Zeego SDK 54 Support erhält: Evaluation ob Rückmigration sinnvoll
3. **Risiko-Minimierung**
- Action Sheet ist production-ready und von Expo maintained
- `@react-native-menu/menu` v2.0.0 ist aktuell (14 Tage alt)
- Beide Libraries haben Expo SDK 54 Support
4. **UX Trade-off akzeptabel**
- Action Sheets sind bekanntes iOS/Android Pattern
- Für 80% der Use Cases ausreichend
- Native Context Menus nur für spezifische Features
---
## Migrations-Plan (Detailliert)
### Phase 1: Quick Migration (2-3 Tage)
**Ziel:** SDK 54 kompatible App mit Action Sheets
#### Tag 1: Vorbereitung
1. **Backup erstellen**
```bash
git checkout -b migration/zeego-to-actionsheet
```
2. **Action Sheet Hook vorbereiten**
```tsx
// hooks/useMenu.ts
import { useActionSheet } from '@expo/react-native-action-sheet';
export const useMenu = () => {
const { showActionSheetWithOptions } = useActionSheet();
return {
showMenu: (options: MenuOption[]) => {
showActionSheetWithOptions({
options: options.map(o => o.title),
destructiveButtonIndex: options.findIndex(o => o.destructive),
cancelButtonIndex: options.length - 1,
}, (index) => {
options[index]?.onSelect?.();
});
}
};
};
```
3. **Type Definitions**
```tsx
// types/menu.ts
export interface MenuOption {
title: string;
onSelect?: () => void;
destructive?: boolean;
disabled?: boolean;
}
```
#### Tag 2: Migration Dropdown Menus (11 Komponenten)
1. `features/menus/HeaderMenu.tsx`
2. `features/menus/MemoMenu.tsx`
3. `features/menus/MemoHeaderMenu.tsx`
4. `components/atoms/Pill.tsx`
5. `components/molecules/TableOfContentsMenu.tsx`
6. `features/subscription/SubscriptionMenu.tsx`
7. Weitere Dropdown-Komponenten
**Pro Komponente:**
- Zeego Imports entfernen
- Action Sheet Hook einbinden
- Trigger Button anpassen
- Menu Options Array erstellen
#### Tag 3: Migration Context Menus (4 Komponenten)
1. `components/organisms/Memory.tsx`
2. `components/molecules/PromptPreview.tsx`
3. `components/molecules/MemoPreview.tsx`
**Temporär:** Action Sheet mit Long Press Trigger
#### Tag 3 Nachmittag: Testing
- iOS Testing (Simulator + Device)
- Android Testing (Emulator + Device)
- Funktionale Tests aller Menu-Interaktionen
- Edge Cases (z.B. mehrere Menus gleichzeitig)
---
### Phase 2: Native Context Menus (1-2 Wochen)
**Nur für kritische Features wo Context Menu UX wichtig ist**
#### Woche 1: Setup + iOS Implementation
**Dependency Installation:**
```bash
npm install @react-native-menu/menu@2.0.0
```
**app.json Plugin:**
```json
{
"plugins": [
"@react-native-menu/menu"
]
}
```
**Rebuild:**
```bash
npx expo prebuild --clean
npx expo run:ios
```
#### Beispiel Implementation:
```tsx
// components/organisms/Memory.tsx
import { Platform } from 'react-native';
import { ContextMenuView } from '@react-native-menu/menu';
import { useMenu } from '~/hooks/useMenu'; // Action Sheet fallback
export const Memory = ({ memory }) => {
const { showMenu } = useMenu();
if (Platform.OS === 'ios') {
return (
<ContextMenuView
actions={[
{ id: 'share', title: 'Teilen', image: 'square.and.arrow.up' },
{ id: 'delete', title: 'Löschen', destructive: true },
]}
onPressAction={({ nativeEvent }) => {
if (nativeEvent.actionKey === 'delete') handleDelete();
if (nativeEvent.actionKey === 'share') handleShare();
}}
>
<MemoryContent memory={memory} />
</ContextMenuView>
);
}
// Android/Web Fallback: Long Press -> Action Sheet
return (
<Pressable
onLongPress={() => showMenu([
{ title: 'Teilen', onSelect: handleShare },
{ title: 'Löschen', onSelect: handleDelete, destructive: true },
])}
>
<MemoryContent memory={memory} />
</Pressable>
);
};
```
#### Woche 2: Testing + Optimierung
- iOS Context Menu Testing
- Android Fallback Testing
- Performance Testing
- UX Feedback sammeln
---
### Phase 3: Monitoring & Evaluation (Ongoing)
**Zeego Tracking:**
- GitHub Issue #173 monitoren
- Release Notes von Zeego beobachten
- Bei SDK 54 Support: Evaluation ob Rückmigration lohnt
**Kriterien für Rückmigration zu Zeego:**
- ✅ Offizieller Expo SDK 54 Support
- ✅ React Native 0.81+ Support
- ✅ Stabile Version (keine Beta)
- ✅ Positive Community Feedback
- ✅ `react-native-ios-context-menu` Maintenance Status verbessert
---
## Technische Details & Code-Beispiele
### Zeego Package entfernen
```bash
npm uninstall zeego
npm uninstall react-native-ios-context-menu react-native-ios-utilities
npm uninstall @react-native-menu/menu # Falls installiert als Zeego Dependency
```
### Action Sheet Provider Setup
```tsx
// app/_layout.tsx
import { ActionSheetProvider } from '@expo/react-native-action-sheet';
export default function RootLayout() {
return (
<ActionSheetProvider>
{/* Rest der App */}
</ActionSheetProvider>
);
}
```
### Utility Hook für konsistente Menus
```tsx
// hooks/useMenu.ts
import { useActionSheet } from '@expo/react-native-action-sheet';
import { useCallback } from 'react';
export interface MenuOption {
title: string;
onSelect?: () => void;
destructive?: boolean;
disabled?: boolean;
icon?: string; // Für zukünftige native menu implementation
}
export const useMenu = () => {
const { showActionSheetWithOptions } = useActionSheet();
const showMenu = useCallback((
options: MenuOption[],
config?: {
title?: string;
message?: string;
}
) => {
const actionOptions = options.map(o => o.title);
const cancelButtonIndex = actionOptions.length - 1;
const destructiveButtonIndex = options.findIndex(o => o.destructive);
showActionSheetWithOptions(
{
options: actionOptions,
cancelButtonIndex,
destructiveButtonIndex: destructiveButtonIndex >= 0 ? destructiveButtonIndex : undefined,
title: config?.title,
message: config?.message,
},
(buttonIndex) => {
if (buttonIndex !== undefined && buttonIndex !== cancelButtonIndex) {
options[buttonIndex]?.onSelect?.();
}
}
);
}, [showActionSheetWithOptions]);
return { showMenu };
};
```
---
## Risiken & Mitigationen
### Risiko 1: UX Verschlechterung
**Problem:** Action Sheets haben andere UX als Context/Dropdown Menus
**Mitigation:**
- User Testing durchführen
- Feedback sammeln
- Bei kritischen Features: Native Context Menus (Phase 2)
### Risiko 2: Web Support
**Problem:** Action Sheets funktionieren nicht auf Web
**Mitigation:**
- Falls Web wichtig: Phase 2 mit Radix UI für Web
- Oder: Web-spezifische Dropdown Implementierung mit React Native Web Modals
### Risiko 3: Feature Loss
**Problem:** Icons, Checkboxes, verschachtelte Menus gehen verloren
**Mitigation:**
- Dokumentiere fehlende Features
- Priorisiere nach Business Impact
- Alternative UI Patterns für kritische Features (z.B. separate Settings Screens statt Inline Checkboxes)
### Risiko 4: Zukünftige Zeego-Updates
**Problem:** Wenn Zeego später SDK 54 Support erhält, haben wir doppelten Aufwand
**Mitigation:**
- Code modular halten (useMenu Hook als Abstraction)
- Zeego Monitoring als Teil des Prozesses
- Rückmigration nur wenn klarer Mehrwert
---
## Entscheidungshilfe
### Wähle **Option 1 (Warten)** wenn:
- ❌ Nicht empfohlen - zu hohes Risiko
### Wähle **Option 2 (Action Sheet)** wenn:
- ✅ Schnelle Lösung benötigt (2-3 Tage)
- ✅ SDK 54 Upgrade blockiert
- ✅ Basic Menu-Funktionalität ausreichend
- ✅ Team-Kapazität limitiert
### Wähle **Option 4 (Native APIs)** wenn:
- ✅ Context Menu UX kritisch
- ✅ Platform-spezifische Features benötigt
- ✅ Team hat 2-3 Wochen Zeit
- ✅ Web Support wichtig
### Wähle **Option 2 + 4 Hybrid (Empfohlen)** wenn:
- ✅ Best-of-both-worlds gewünscht
- ✅ Schrittweise Migration möglich
- ✅ Risiko-Minimierung wichtig
- ✅ Langfristige Flexibilität gewünscht
---
## Nächste Schritte (Action Items)
1. **Sofort:** Entscheidung treffen (Hybrid-Ansatz empfohlen)
2. **Tag 1:** Git Branch erstellen, useMenu Hook implementieren
3. **Tag 2-3:** Migration aller Zeego Verwendungen zu Action Sheets
4. **Woche 1:** Testing + SDK 54 Upgrade durchführen
5. **Woche 2-3:** Native Context Menus für kritische Features (optional)
6. **Ongoing:** Zeego Repository monitoren für SDK 54 Support
---
## Ressourcen & Links
### Zeego
- [Zeego GitHub Issue #173 (Expo 54 Build fails)](https://github.com/nandorojo/zeego/issues/173)
- [Zeego Docs - Compatibility Table](https://zeego.dev/start)
### Action Sheet
- [@expo/react-native-action-sheet Docs](https://docs.expo.dev/versions/latest/sdk/action-sheet/)
- [GitHub Repository](https://github.com/expo/react-native-action-sheet)
### Native Menu
- [@react-native-menu/menu v2.0.0](https://www.npmjs.com/package/@react-native-menu/menu)
- [GitHub Repository](https://github.com/react-native-menu/menu)
### Expo SDK 54
- [Expo SDK 54 Changelog](https://expo.dev/changelog/sdk-54)
- [Expo SDK 54 Upgrade Guide](https://expo.dev/blog/expo-sdk-upgrade-guide)
---
## Appendix: Zeego Usage in Project
### Context Menu Verwendungen
1. **components/organisms/Memory.tsx** (Zeile 17)
- Use Case: Long-press auf Memory für Actions (Share, Delete, etc.)
- Kritikalität: Hoch - Kern-Feature
2. **components/molecules/PromptPreview.tsx** (Zeile 10)
- Use Case: Prompt Preview Context Menu
- Kritikalität: Mittel
3. **components/molecules/MemoPreview.tsx** (Zeile 34)
- Use Case: Memo Preview Actions
- Kritikalität: Hoch
### Dropdown Menu Verwendungen
4. **features/menus/HeaderMenu.tsx** (Zeile 8)
- Use Case: App Header Menu
- Kritikalität: Mittel
5. **features/menus/MemoMenu.tsx** (Zeile 7)
- Use Case: Memo-spezifische Aktionen
- Kritikalität: Hoch
6. **features/menus/MemoHeaderMenu.tsx** (Zeile 8)
- Use Case: Memo Header Dropdown
- Kritikalität: Mittel
7. **components/atoms/Pill.tsx** (Zeile 10)
- Use Case: Pill/Tag Dropdown
- Kritikalität: Niedrig
8. **components/molecules/TableOfContentsMenu.tsx** (Zeile 8)
- Use Case: TOC Navigation
- Kritikalität: Mittel
9. **features/subscription/SubscriptionMenu.tsx** (Zeile 9)
- Use Case: Subscription Settings
- Kritikalität: Hoch - Revenue-relevant
10. **components/organisms/PhotoGallery.tsx**
- Use Case: Photo Actions
- Kritikalität: Mittel
---
**Dokument erstellt:** 30. September 2025
**Letzte Aktualisierung:** 30. September 2025
**Autor:** Claude Code Analyse
**Status:** ✅ Bereit für Team Review

View file

@ -0,0 +1,76 @@
# Zeego Migration Status
## Abgeschlossen ✅
### Dependencies
- [x] Zeego deinstalliert
- [x] react-native-ios-context-menu deinstalliert
- [x] react-native-ios-utilities deinstalliert
- [x] @react-native-menu/menu@2.0.0 bereits vorhanden
### Utility Components
- [x] `config/menuActions.ts` - Zentralisierte Menu Actions
- [x] `utils/menuBuilder.ts` - Menu Action Builder
- [x] `components/ui/NativeMenu.tsx` - Wiederverwendbare Wrapper-Komponente
### Dropdown Menus (3/11 migriert)
- [x] `features/menus/HeaderMenu.tsx`
- [x] `features/menus/MemoMenu.tsx`
- [x] `features/menus/MemoHeaderMenu.tsx`
- [ ] `components/atoms/Pill.tsx`
- [ ] `components/molecules/TableOfContentsMenu.tsx`
- [ ] `features/subscription/SubscriptionMenu.tsx`
- [ ] `components/organisms/PhotoGallery.tsx`
- [ ] Weitere 4 Komponenten (noch zu identifizieren)
### Context Menus (0/4 migriert)
- [ ] `components/organisms/Memory.tsx`
- [ ] `components/molecules/PromptPreview.tsx`
- [ ] `components/molecules/MemoPreview.tsx`
- [ ] 1 weitere Komponente
## Nächste Schritte
1. ✅ Restliche Dropdown Menus migrieren
2. ✅ Context Menus migrieren
3. ✅ Native Rebuild (`npx expo prebuild --clean`)
4. ✅ iOS Testing
5. ✅ Android Testing
## Migration Pattern
### Dropdown Menu (Tap)
```tsx
// Vorher
import * as DropdownMenu from 'zeego/dropdown-menu';
// Nachher
import { MenuView } from '@react-native-menu/menu';
<MenuView
actions={actions}
onPressAction={({ nativeEvent }) => handleAction(nativeEvent.event)}
shouldOpenOnLongPress={false} // Dropdown = tap
>
{children}
</MenuView>
```
### Context Menu (Long Press)
```tsx
<MenuView
actions={actions}
onPressAction={({ nativeEvent }) => handleAction(nativeEvent.event)}
shouldOpenOnLongPress={true} // Context = long press (default)
>
{children}
</MenuView>
```
## Web Fallbacks
Alle migrierten Komponenten behalten ihre bestehenden Web-Implementierungen (Custom Modals).
---
**Status:** In Progress (3/15 Komponenten migriert)
**Letzte Aktualisierung:** 30. September 2025