mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 16:06:43 +02:00
refactor: restructure
monorepo with apps/ and services/ directories
This commit is contained in:
parent
25824ed0ac
commit
ff80aeec1f
4062 changed files with 2592 additions and 1278 deletions
288
apps/memoro/apps/mobile/docs/ADDING_LANGUAGES.md
Normal file
288
apps/memoro/apps/mobile/docs/ADDING_LANGUAGES.md
Normal 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...
|
||||
});
|
||||
```
|
||||
1608
apps/memoro/apps/mobile/docs/REACT_TO_SVELTEKIT_MIGRATION_GUIDE.md
Normal file
1608
apps/memoro/apps/mobile/docs/REACT_TO_SVELTEKIT_MIGRATION_GUIDE.md
Normal file
File diff suppressed because it is too large
Load diff
280
apps/memoro/apps/mobile/docs/REALTIME_BROADCAST_FIX.md
Normal file
280
apps/memoro/apps/mobile/docs/REALTIME_BROADCAST_FIX.md
Normal 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)
|
||||
181
apps/memoro/apps/mobile/docs/RECORDING_PREVIEW_IMPLEMENTATION.md
Normal file
181
apps/memoro/apps/mobile/docs/RECORDING_PREVIEW_IMPLEMENTATION.md
Normal 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
|
||||
|
|
@ -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
|
||||
71
apps/memoro/apps/mobile/docs/REFRESH_TOKEN_GRACE_PERIOD.md
Normal file
71
apps/memoro/apps/mobile/docs/REFRESH_TOKEN_GRACE_PERIOD.md
Normal 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.
|
||||
278
apps/memoro/apps/mobile/docs/SPACES.md
Normal file
278
apps/memoro/apps/mobile/docs/SPACES.md
Normal 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
|
||||
120
apps/memoro/apps/mobile/docs/SPACES_IMPLEMENTATION_TASKS.md
Normal file
120
apps/memoro/apps/mobile/docs/SPACES_IMPLEMENTATION_TASKS.md
Normal 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
|
||||
153
apps/memoro/apps/mobile/docs/SPACES_TESTING.md
Normal file
153
apps/memoro/apps/mobile/docs/SPACES_TESTING.md
Normal 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.
|
||||
286
apps/memoro/apps/mobile/docs/SUPABASE_REALTIME_RLS_WORKAROUND.md
Normal file
286
apps/memoro/apps/mobile/docs/SUPABASE_REALTIME_RLS_WORKAROUND.md
Normal 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.
|
||||
821
apps/memoro/apps/mobile/docs/SUPPORTED_LANGUAGES.md
Normal file
821
apps/memoro/apps/mobile/docs/SUPPORTED_LANGUAGES.md
Normal 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.
|
||||
480
apps/memoro/apps/mobile/docs/UPLOAD_FEEDBACK_IMPLEMENTATION.md
Normal file
480
apps/memoro/apps/mobile/docs/UPLOAD_FEEDBACK_IMPLEMENTATION.md
Normal 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
|
||||
585
apps/memoro/apps/mobile/docs/analytics.md
Normal file
585
apps/memoro/apps/mobile/docs/analytics.md
Normal 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*
|
||||
149
apps/memoro/apps/mobile/docs/blueprints_and_prompts.md
Normal file
149
apps/memoro/apps/mobile/docs/blueprints_and_prompts.md
Normal 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*
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)**
|
||||
689
apps/memoro/apps/mobile/docs/features/automatic-app-rating.md
Normal file
689
apps/memoro/apps/mobile/docs/features/automatic-app-rating.md
Normal 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
969
apps/memoro/apps/mobile/docs/features/menus-zeego.md
Normal file
969
apps/memoro/apps/mobile/docs/features/menus-zeego.md
Normal 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.
|
||||
1201
apps/memoro/apps/mobile/docs/features/native-menu-solution.md
Normal file
1201
apps/memoro/apps/mobile/docs/features/native-menu-solution.md
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue