import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { View, StyleSheet, Pressable, ActivityIndicator, Platform } from 'react-native'; import { FlashList } from '@shopify/flash-list'; import { useActionSheet } from '@expo/react-native-action-sheet'; import { router } from 'expo-router'; import * as Clipboard from 'expo-clipboard'; import { getAuthenticatedClient } from '~/features/auth/lib/supabaseClient'; import Text from '~/components/atoms/Text'; import Icon from '~/components/atoms/Icon'; import MemoPreview from '~/components/molecules/MemoPreview'; import MemoPreviewSkeleton from '~/components/molecules/MemoPreviewSkeleton'; import { useToast } from '~/features/toast/contexts/ToastContext'; import { useMemoStore } from '~/features/memos/store/memoStore'; import { useAllMemoUpdates } from '~/features/memos/contexts/MemoRealtimeContext'; import { useMemoPhotos } from '~/features/storage/hooks/useMemoPhotos'; import { memoRealtimeService } from '~/features/memos/services/memoRealtimeService'; // Pagination constants const PAGE_SIZE = 20; export interface MemoModel { id: string; title: string; created_at: string; source?: { type?: string; content?: string; duration_seconds?: number; audio_path?: string; transcript?: string; }; is_archived: boolean; is_pinned?: boolean; metadata?: { processing?: { transcription?: { status?: string; timestamp?: string; }; headline?: { status?: string; timestamp?: string; }; headline_and_intro?: { status?: string; updated_at?: string; }; }; transcriptionStatus?: string; blueprintId?: string | null; }; } interface MemoListProps { showArchived?: boolean | null; spaceId?: string; memos?: MemoModel[]; tagIds?: string[]; selectionMode?: boolean; selectedMemoIds?: string[]; onMemoSelection?: (memoId: string, selected: boolean) => void; refreshTrigger?: number; // Trigger to force refresh onShare?: (memo: MemoModel) => void; // Callback for sharing a memo onMemosLoaded?: (memos: MemoModel[]) => void; // Callback when memos are loaded maintainScrollPosition?: boolean; // Whether to maintain scroll position on refresh } const MemoList = ({ showArchived = null, spaceId, memos: propMemos, tagIds, selectionMode = false, selectedMemoIds = [], onMemoSelection, refreshTrigger = 0, onShare, onMemosLoaded, maintainScrollPosition = false, }: MemoListProps) => { const [memos, setMemos] = useState(propMemos || []); const [loading, setLoading] = useState(!propMemos); const [loadingMore, setLoadingMore] = useState(false); const [hasMore, setHasMore] = useState(true); const [page, setPage] = useState(0); const [retryCount, setRetryCount] = useState(0); const [networkError, setNetworkError] = useState(false); const [hasNewContent, setHasNewContent] = useState(false); const loadedMemoIds = useRef(new Set()); const updateTimerRef = useRef>(null); const hasLoadedFromDB = useRef(false); // Track if we've loaded from database const pendingUpdates = useRef<{ inserts: MemoModel[]; updates: Map; deletes: Set; }>({ inserts: [], updates: new Map(), deletes: new Set() }); const { showActionSheetWithOptions } = useActionSheet(); const { showSuccess, showError } = useToast(); const flashListRef = useRef>(null); const scrollPositionRef = useRef({ index: 0, offset: 0 }); const shouldMaintainScroll = useRef(false); // Get the latest memo ID from the store at the component level const latestMemoId = useMemoStore((state) => state.latestMemo?.id); // Photo status hook const { hasPhotos: getMemoHasPhotos, checkPhotoStatus } = useMemoPhotos(); // Static theme colors to avoid useTheme hook issues const themeColors = useMemo(() => { return { primary: '#f8d62b', primaryWithOpacity: 'rgba(248, 214, 43, 0.5)', text: '#2c2c2c', }; }, []); // Fetch memos function with pagination support const fetchMemos = useCallback( async (pageNumber: number = 0, append: boolean = false) => { try { // Debug log removed if (pageNumber === 0) { setLoading(true); loadedMemoIds.current.clear(); } else { setLoadingMore(true); } // Verwende den authentifizierten Client const supabase = await getAuthenticatedClient(); // Removed debug log('MemoList: Got authenticated client'); // Base query for memos let query = supabase .from('memos') .select('id, title, created_at, source, is_archived, metadata, is_pinned') .order('is_pinned', { ascending: false }) .order('created_at', { ascending: false }); // Filter by archive status if specifically requested if (showArchived !== null) { query = query.eq('is_archived', showArchived); // Removed debug log('MemoList: Filtering by is_archived =', showArchived); } // If tag filtering is active, use optimized RPC function if (tagIds && tagIds.length > 0) { // Use the optimized RPC function to get memos with ALL tags in a single query const { data: memoIdsWithAllTags, error: tagError } = await supabase .rpc('get_memos_with_all_tags', { tag_ids: tagIds }); if (tagError) { console.error('Error fetching memos for tags:', tagError); setMemos([]); setLoading(false); return; } if (memoIdsWithAllTags && memoIdsWithAllTags.length > 0) { // Add filter for these memo IDs query = query.in('id', memoIdsWithAllTags.map(m => m.memo_id)); } else { // No memos with these tags, return empty array setMemos([]); setLoading(false); return; } } // If spaceId is provided, skip loading - spaces feature not yet implemented if (spaceId) { // Spaces feature is not yet implemented console.log('MemoList: Spaces feature not yet implemented, skipping space-specific memos'); setMemos([]); setLoading(false); return; } // Add pagination const from = pageNumber * PAGE_SIZE; const to = from + PAGE_SIZE - 1; query = query.range(from, to); // Removed debug log('MemoList: Executing query...'); const { data, error } = await query; if (error) { // Debug log removed return; } if (data) { if (append) { // For append operations, filter out duplicates const newMemos = data.filter((memo: MemoModel) => !loadedMemoIds.current.has(memo.id)); newMemos.forEach((memo: MemoModel) => loadedMemoIds.current.add(memo.id)); setMemos((prev) => [...prev, ...newMemos]); // Check photo status for newly loaded memos if (newMemos.length > 0) { const memoIds = newMemos.map((memo: MemoModel) => memo.id); checkPhotoStatus(memoIds); } } else { // For replace operations (pageNumber === 0), use all data and reset tracking loadedMemoIds.current.clear(); data.forEach((memo: MemoModel) => loadedMemoIds.current.add(memo.id)); setMemos(data); // Call onMemosLoaded only for initial database load if (!hasLoadedFromDB.current && onMemosLoaded) { onMemosLoaded(data); hasLoadedFromDB.current = true; } // Check photo status for all loaded memos if (data.length > 0) { const memoIds = data.map((memo: MemoModel) => memo.id); checkPhotoStatus(memoIds); } // Only scroll to top when explicitly refreshing (not when maintaining position) if (flashListRef.current && data.length > 0 && !maintainScrollPosition && !shouldMaintainScroll.current) { // Use setTimeout to ensure the list has been rendered with new data setTimeout(() => { flashListRef.current?.scrollToIndex({ index: 0, animated: false }); }, 100); } else if (shouldMaintainScroll.current && scrollPositionRef.current.index > 0) { // Restore scroll position after data reload setTimeout(() => { flashListRef.current?.scrollToIndex({ index: Math.min(scrollPositionRef.current.index, data.length - 1), animated: false }); shouldMaintainScroll.current = false; }, 100); } } // Update hasMore based on whether we got a full page setHasMore(data.length === PAGE_SIZE); // Reset retry count and network error on success setRetryCount(0); setNetworkError(false); } } catch (error: any) { console.error('Error loading memos:', error); // Check if it's a network error const isNetworkError = error?.message?.toLowerCase().includes('network') || error?.message?.toLowerCase().includes('fetch') || error?.code === 'NETWORK_ERROR' || !navigator.onLine; if (isNetworkError) { setNetworkError(true); if (pageNumber > 0) { // Only show error for pagination, not initial load showError('Unable to load more memos. Check your connection.'); } } // Keep hasMore true to allow retry if (pageNumber > 0) { setHasMore(true); } // Implement retry with exponential backoff for pagination if (pageNumber > 0 && retryCount < 3 && isNetworkError) { const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 5000); setTimeout(() => { setRetryCount((prev) => prev + 1); fetchMemos(pageNumber, append); }, retryDelay); } } finally { setLoading(false); setLoadingMore(false); } }, [showArchived, spaceId, tagIds, checkPhotoStatus] ); // Handle loading more memos on scroll const handleLoadMore = useCallback(() => { if (!loadingMore && hasMore && !loading) { // Don't update page until fetch succeeds const nextPage = page + 1; fetchMemos(nextPage, true) .then(() => { // Only update page on successful fetch setPage(nextPage); }) .catch(() => { // Page remains unchanged on error, allowing retry console.log('Failed to load page', nextPage, '- page counter remains at', page); }); } }, [loadingMore, hasMore, loading, page, fetchMemos]); // Process batched updates const processBatchedUpdates = useCallback(() => { const updates = pendingUpdates.current; if (updates.deletes.size === 0 && updates.updates.size === 0 && updates.inserts.length === 0) { return; } setMemos((prevMemos) => { let updatedMemos = [...prevMemos]; // Apply deletes if (updates.deletes.size > 0) { updatedMemos = updatedMemos.filter((memo) => !updates.deletes.has(memo.id)); updates.deletes.forEach((id) => loadedMemoIds.current.delete(id)); } // Apply updates in-place if (updates.updates.size > 0) { updatedMemos = updatedMemos.map((memo) => { const update = updates.updates.get(memo.id); if (update) { // Log title changes for debugging if (memo.title !== update.title) { console.log( 'MemoList: Title changed for memo', memo.id, 'from', memo.title, 'to', update.title ); } // Force new object creation with all properties to ensure React detects the change return { ...memo, ...update, // Ensure timestamp is preserved or updated timestamp: update.timestamp || memo.timestamp, // Force a unique key to trigger re-render _updateKey: Date.now() }; } return memo; }); } // Apply inserts if (updates.inserts.length > 0) { if (page === 0) { // We're at the top, prepend new memos const newMemos = updates.inserts.filter((memo) => !loadedMemoIds.current.has(memo.id)); newMemos.forEach((memo) => loadedMemoIds.current.add(memo.id)); // Combine and sort to maintain pinned/chronological order updatedMemos = [...newMemos, ...updatedMemos].sort((a, b) => { // Pinned items first if (a.is_pinned && !b.is_pinned) return -1; if (!a.is_pinned && b.is_pinned) return 1; // Then by created_at return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); }); // Check photo status for newly inserted memos if (newMemos.length > 0) { const memoIds = newMemos.map((memo) => memo.id); checkPhotoStatus(memoIds); // Auto-scroll to top with smooth animation when new memos arrive setTimeout(() => { if (flashListRef.current) { flashListRef.current.scrollToIndex({ index: 0, animated: true }); } }, 100); } } else { // User has scrolled, show notification setHasNewContent(true); } } return updatedMemos; }); // Clear pending updates pendingUpdates.current = { inserts: [], updates: new Map(), deletes: new Set() }; }, [page, checkPhotoStatus]); // Subscribe to broadcast channels for visible memos to catch service_role updates 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 // Handle real-time memo updates with batching (postgres_changes) useAllMemoUpdates( (payload) => { console.log( 'MemoList: Received postgres_changes update:', payload.event, payload.new?.id || payload.old?.id ); // Log more details for debugging if (payload.event === 'UPDATE' && payload.new) { console.log('MemoList: UPDATE details:', { id: payload.new.id, title: payload.new.title, transcriptionStatus: payload.new.metadata?.processing?.transcription?.status, headlineStatus: payload.new.metadata?.processing?.headline_and_intro?.status, }); // Check if this is a title update for a memo we're tracking const currentMemo = memos.find((m) => m.id === payload.new.id); if (currentMemo) { console.log( 'MemoList: Found memo in current list, current title:', currentMemo.title, 'new title:', payload.new.title ); } } // Queue the update if (payload.event === 'DELETE' && payload.old?.id) { pendingUpdates.current.deletes.add(payload.old.id); } else if (payload.event === 'UPDATE' && payload.new) { pendingUpdates.current.updates.set(payload.new.id, payload.new); // Check if this update is for a memo that's also in our insert queue // This handles the case where a memo is inserted without a title, then quickly updated with one const insertIndex = pendingUpdates.current.inserts.findIndex( (m) => m.id === payload.new.id ); if (insertIndex !== -1) { // Update the queued insert with the new data pendingUpdates.current.inserts[insertIndex] = payload.new; } // If this is an existing memo in our list, ensure we process the update quickly const existingMemoIndex = memos.findIndex((m) => m.id === payload.new.id); if (existingMemoIndex !== -1) { const existingMemo = memos[existingMemoIndex]; // Check if this is a significant update (title change or processing status change) const isTitleUpdate = existingMemo.title !== payload.new.title; const isSignificantUpdate = isTitleUpdate || (!existingMemo.title || existingMemo.title.trim() === '') && payload.new.title && payload.new.title.trim() !== ''; // Process title updates immediately, other updates with short delay if (updateTimerRef?.current) { clearTimeout(updateTimerRef.current); } updateTimerRef.current = setTimeout( () => { processBatchedUpdates(); }, isTitleUpdate ? 0 : (isSignificantUpdate ? 10 : 50) ); // Immediate for title updates return; } } else if (payload.event === 'INSERT' && payload.new) { // Check if memo passes current filters const memo = payload.new; const passesFilters = (showArchived === null || memo.is_archived === showArchived) && (!spaceId || true); // Space filtering would need additional check if (passesFilters) { // Check if we already have this memo in our list or updates (race condition protection) const alreadyExists = memos.some((m) => m.id === memo.id) || pendingUpdates.current.updates.has(memo.id) || pendingUpdates.current.inserts.some((m) => m.id === memo.id); if (!alreadyExists) { console.log('MemoList: INSERT details:', { id: memo.id, title: memo.title, created_at: memo.created_at, hasSource: !!memo.source, hasMetadata: !!memo.metadata, transcriptionStatus: memo.metadata?.processing?.transcription?.status, headlineStatus: memo.metadata?.processing?.headline_and_intro?.status, }); pendingUpdates.current.inserts.push(memo); // Process inserts immediately for new memos if (updateTimerRef.current) { clearTimeout(updateTimerRef.current); } updateTimerRef.current = setTimeout(() => { processBatchedUpdates(); }, 100); // Quick processing for new memos return; // Important: return here to avoid overriding the timer } } } // Clear existing timer (only if we didn't already set one above) if (updateTimerRef.current) { clearTimeout(updateTimerRef.current); } // Set new timer for batch processing updateTimerRef.current = setTimeout(() => { processBatchedUpdates(); }, 500); // 500ms batch window }, [showArchived, spaceId, tagIds, processBatchedUpdates, memos, page] ); // Load memos on mount and dependency changes useEffect(() => { // Wenn Memos als Props übergeben werden, verwenden wir diese direkt if (propMemos) { setMemos(propMemos); setLoading(false); // Check photo status for prop memos as well if (propMemos.length > 0) { const memoIds = propMemos.map((memo) => memo.id); checkPhotoStatus(memoIds); } // Don't call onMemosLoaded for prop memos to avoid infinite loop return; } // Reset flags when dependencies change (but not for propMemos) hasLoadedFromDB.current = false; // When maintainScrollPosition is true, preserve the scroll position if (maintainScrollPosition) { shouldMaintainScroll.current = true; } else { // Reset pagination when dependencies change setPage(0); setHasMore(true); loadedMemoIds.current.clear(); } // Memos laden, wenn die Komponente gemountet wird oder wenn sich Abhängigkeiten ändern fetchMemos(0, false); }, [showArchived, spaceId, propMemos, refreshTrigger, tagIds, fetchMemos, checkPhotoStatus, maintainScrollPosition]); const handleMemoPress = (memoId: string) => { if (selectionMode) { // In selection mode, toggle selection instead of navigating const isSelected = selectedMemoIds.includes(memoId); onMemoSelection?.(memoId, !isSelected); } else { // Normal mode - navigate to memo detail // Defensive check to ensure we have a valid ID if (!memoId || memoId === 'undefined' || memoId === 'null') { console.error('MemoList: Attempted to navigate with invalid memo ID:', memoId); return; } router.push(`/(protected)/(memo)/${memoId}`); } }; // Handle copying memo content const handleCopyMemo = async (memo: MemoModel) => { try { // Get authenticated client to fetch complete memo data const supabase = await getAuthenticatedClient(); // Fetch complete memo data with intro const { data: fullMemo, error: memoError } = await supabase .from('memos') .select('*') .eq('id', memo.id) .single(); if (memoError) { console.debug('Error fetching full memo for copy:', memoError); // Fallback to basic content if full memo fetch fails } // Fetch memories for this memo const { data: memories, error: memoriesError } = await supabase .from('memories') .select('*') .eq('memo_id', memo.id) .order('sort_order', { ascending: true }) .order('created_at', { ascending: false }); if (memoriesError) { console.debug('Error fetching memories for copy:', memoriesError); // Continue without memories } // Build the content string let content = `${fullMemo?.title || memo.title || 'Untitled'}\n\n`; // Add intro if available if (fullMemo?.intro) { content += `${fullMemo.intro}\n\n`; } // Add memories if available if (memories && memories.length > 0) { memories.forEach((memory: any) => { content += `${memory.title || 'Memory'}\n${memory.content}\n\n`; }); } // Add transcript if available const transcript = fullMemo?.source?.content || fullMemo?.source?.transcription || fullMemo?.source?.transcript || memo.source?.content; if (transcript) { content += `Transkript:\n${transcript}\n\n`; } await Clipboard.setStringAsync(content); showSuccess('Kompletter Memo-Inhalt wurde in die Zwischenablage kopiert'); } catch (error) { console.debug('Fehler beim Kopieren des Memo-Inhalts:', error); showSuccess('Der Memo-Inhalt konnte nicht kopiert werden'); } }; // Handle pinning/unpinning memo const handlePinMemo = async (memo: MemoModel) => { try { const supabase = await getAuthenticatedClient(); const newPinnedState = !memo.is_pinned; const { error } = await supabase .from('memos') .update({ is_pinned: newPinnedState }) .eq('id', memo.id); if (error) { console.debug('Error toggling pin state:', error); showSuccess('Das Memo konnte nicht angeheftet/entfernt werden'); return; } // Update the local state setMemos((prevMemos) => prevMemos.map((m) => (m.id === memo.id ? { ...m, is_pinned: newPinnedState } : m)) ); // Refresh the list to ensure proper sorting (pinned items go to top) fetchMemos(0, false); showSuccess(newPinnedState ? 'Memo wurde angeheftet' : 'Anheftung wurde entfernt'); } catch (error) { console.debug('Error in handlePinMemo:', error); showSuccess('Ein Fehler ist aufgetreten'); } }; const handleDeleteMemo = async (memoId: string, memoTitle: string) => { // Show confirmation dialog showActionSheetWithOptions( { options: ['Löschen', 'Abbrechen'], cancelButtonIndex: 1, destructiveButtonIndex: 0, title: 'Memo löschen', message: `Möchtest du "${memoTitle}" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.`, }, async (selectedIndex) => { if (selectedIndex === 0) { // Delete confirmed try { // Removed debug log('Deleting memo:', memoId); // Get authenticated supabase client const supabase = await getAuthenticatedClient(); // Get the memo to check if it has audio const { data: memoData } = await supabase .from('memos') .select('source') .eq('id', memoId) .single(); // First, delete related memories const { error: memoriesError } = await supabase .from('memories') .delete() .eq('memo_id', memoId); if (memoriesError) { // Removed debug log('Error deleting related memories:', memoriesError.message); return; } // If there's audio, delete it from storage if (memoData?.source?.audio_path) { const audioPath = memoData.source.audio_path; if (audioPath) { const { error: storageError } = await supabase.storage .from('user-uploads') .remove([audioPath]); if (storageError) { // Removed debug log('Error deleting audio file:', storageError.message); // Continue with memo deletion even if audio deletion fails } } } // Finally, delete the memo itself const { error: memoError } = await supabase.from('memos').delete().eq('id', memoId); if (memoError) { // Removed debug log('Error deleting memo:', memoError.message); return; } // Update the local state to remove the deleted memo setMemos((prevMemos) => prevMemos.filter((memo) => memo.id !== memoId)); // Fetch fresh data to ensure the list is up to date fetchMemos(0, false); // Show success toast showSuccess('Memo wurde erfolgreich gelöscht'); } catch (error) { // Removed debug log('Error in handleDeleteMemo:', error); } } } ); }; const renderMemoItem = ({ item }: { item: MemoModel }) => { // Only the latest memo should react to global recording status const shouldReactToRecordingStatus = item.id === latestMemoId; // Format the memo for MemoPreview component with all necessary data const formattedMemo = { id: item.id, title: item.title || '', // Let useMemoProcessing handle empty title timestamp: new Date(item.created_at), // Wichtig: Vollständige Source- und Metadaten übergeben source: item.source, metadata: item.metadata, is_pinned: item.is_pinned || false, // Leeres Tags-Array, das von der MemoPreview-Komponente gefüllt wird tags: [], }; // Check if this memo is selected const isSelected = selectedMemoIds.includes(item.id); // Debug: Check photo status const memoHasPhotos = getMemoHasPhotos(item.id); return ( {selectionMode && ( onMemoSelection?.(item.id, !isSelected)}> )} onMemoSelection && onMemoSelection(item.id, selected)} onPress={() => handleMemoPress(item.id)} reactToGlobalRecordingStatus={shouldReactToRecordingStatus} hasPhotos={memoHasPhotos} showMargins={true} onShare={() => { // Trigger the onShare callback if provided if (onShare) { onShare(item); } }} onCopy={() => handleCopyMemo(item)} onPinToTop={() => handlePinMemo(item)} onDelete={() => handleDeleteMemo(item.id, item.title || 'Untitled Memo')} /> ); }; const renderSkeletonList = () => ( {Array.from({ length: 10 }).map((_, index) => { // Calculate opacity fade-out towards the bottom const opacity = Math.max(0.2, 1 - index * 0.08); return ( ); })} ); const renderEmptyList = () => ( Keine Einträge {spaceId ? 'Dieser Space enthält noch keine Memos.' : showArchived === true ? 'Du hast noch keine archivierten Aufnahmen.' : showArchived === false ? 'Du hast noch keine aktiven Aufnahmen.' : 'Du hast noch keine Aufnahmen erstellt.'} ); const renderFooter = () => { if (!loadingMore && !networkError) return null; return ( {loadingMore ? ( <> Lade weitere Memos... ) : networkError && hasMore ? ( { setNetworkError(false); handleLoadMore(); }}> Tap to retry loading more memos ) : null} ); }; // Show skeleton loader during initial loading if (loading && memos.length === 0) { return renderSkeletonList(); } return ( {hasNewContent && page > 0 && ( { setPage(0); setHasNewContent(false); loadedMemoIds.current.clear(); fetchMemos(0, false); }}> New memos available - tap to refresh )} item.id} showsVerticalScrollIndicator={Platform.OS !== 'web'} ListEmptyComponent={renderEmptyList} ListFooterComponent={renderFooter} contentContainerStyle={styles.listContent} estimatedItemSize={180} extraData={[selectedMemoIds, selectionMode, loadingMore, memos]} onEndReached={handleLoadMore} onEndReachedThreshold={0.3} onScroll={(event) => { // Save current scroll position const offset = event.nativeEvent.contentOffset.y; const index = Math.floor(offset / 180); // Using estimatedItemSize scrollPositionRef.current = { index, offset }; }} scrollEventThrottle={100} // Web-specific props for better scrolling {...(Platform.OS === 'web' ? { style: { flex: 1, overflow: 'auto' }, showsVerticalScrollIndicator: true, } : {})} /> ); }; const styles = StyleSheet.create({ listWrapper: { flex: 1, width: '100%', }, listContent: { paddingBottom: 20, // Removed paddingHorizontal - MemoPreview now manages its own spacing with showMargins prop }, memoItem: { marginBottom: 12, flexDirection: 'row', alignItems: 'center', }, // selectedMemoItem style is now dynamic and inline selectionArea: { borderTopLeftRadius: 12, borderBottomLeftRadius: 12, paddingVertical: 8, marginRight: 4, }, selectionIndicator: { marginLeft: 4, width: 36, height: 36, justifyContent: 'center', alignItems: 'center', zIndex: 10, }, memoPreviewContainer: { flex: 1, width: '100%', marginTop: 0, }, memoPreviewContainerInSelectionMode: { width: '92%', // Macht das Element schmaler im Auswahlmodus }, emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, marginTop: 40, }, emptyText: { textAlign: 'center', marginTop: 16, opacity: 0.7, }, footer: { paddingVertical: 20, alignItems: 'center', justifyContent: 'center', }, loadingText: { marginTop: 8, opacity: 0.7, }, retryButton: { flexDirection: 'row', alignItems: 'center', padding: 12, gap: 8, }, retryText: { opacity: 0.7, }, newContentBanner: { position: 'absolute', top: 0, left: 20, right: 20, backgroundColor: '#4CAF50', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 20, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, zIndex: 100, elevation: 4, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, shadowRadius: 4, }, newContentText: { color: '#fff', fontWeight: '600', }, }); export default MemoList;