From 51c7d6d0dc30e3d866a28c133eacde7eb8c28683 Mon Sep 17 00:00:00 2001 From: Gabry848 Date: Wed, 20 May 2026 11:38:33 +0200 Subject: [PATCH 1/2] add quick voice add control --- src/components/BotChat/QuickVoiceAdd.tsx | 359 +++++++++++++++++++++++ src/locales/en.json | 4 + src/locales/it.json | 4 + src/navigation/screens/Home.tsx | 11 + src/services/quickVoiceAddService.ts | 72 +++++ 5 files changed, 450 insertions(+) create mode 100644 src/components/BotChat/QuickVoiceAdd.tsx create mode 100644 src/services/quickVoiceAddService.ts diff --git a/src/components/BotChat/QuickVoiceAdd.tsx b/src/components/BotChat/QuickVoiceAdd.tsx new file mode 100644 index 0000000..fbc3790 --- /dev/null +++ b/src/components/BotChat/QuickVoiceAdd.tsx @@ -0,0 +1,359 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { + ActivityIndicator, + Alert, + Animated, + Easing, + Platform, + Pressable, + StyleProp, + StyleSheet, + Text, + View, + ViewStyle, +} from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { + RecordingPresets, + requestRecordingPermissionsAsync, + setAudioModeAsync, + useAudioRecorder, +} from 'expo-audio'; +import { useTranslation } from 'react-i18next'; +import { sendQuickVoiceAdd } from '../../services/quickVoiceAddService'; +import { colors } from '../../theme/tokens'; + +type QuickVoiceAddState = 'idle' | 'recording' | 'sending' | 'success'; + +interface QuickVoiceAddProps { + model: 'base' | 'advanced'; + disabled?: boolean; + showCompactIcon?: boolean; + containerStyle?: StyleProp; +} + +const BAR_COUNT = 18; + +const QuickVoiceAdd: React.FC = ({ + model, + disabled = false, + showCompactIcon = true, + containerStyle, +}) => { + const { t } = useTranslation(); + const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY); + const [state, setState] = useState('idle'); + const expandAnim = useRef(new Animated.Value(0)).current; + const bars = useRef(Array.from({ length: BAR_COUNT }, () => new Animated.Value(0.25))).current; + const recordingStartRef = useRef(0); + const resetTimerRef = useRef | null>(null); + const isRecordingRef = useRef(false); + + const isExpanded = state !== 'idle'; + + useEffect(() => { + Animated.spring(expandAnim, { + toValue: isExpanded ? 1 : 0, + damping: 18, + stiffness: 140, + mass: 0.9, + useNativeDriver: false, + }).start(); + }, [expandAnim, isExpanded]); + + useEffect(() => { + if (state !== 'recording') { + bars.forEach((bar) => bar.stopAnimation(() => bar.setValue(0.25))); + return; + } + + const loops = bars.map((bar, index) => { + const duration = 520 + (index % 5) * 90; + const delay = index * 38; + const loop = Animated.loop( + Animated.sequence([ + Animated.delay(delay), + Animated.timing(bar, { + toValue: 1, + duration, + easing: Easing.inOut(Easing.sin), + useNativeDriver: false, + }), + Animated.timing(bar, { + toValue: 0.18, + duration: duration + 120, + easing: Easing.inOut(Easing.sin), + useNativeDriver: false, + }), + ]) + ); + loop.start(); + return loop; + }); + + return () => loops.forEach((loop) => loop.stop()); + }, [bars, state]); + + useEffect(() => { + return () => { + if (resetTimerRef.current) clearTimeout(resetTimerRef.current); + if (isRecordingRef.current) { + recorder.stop().catch((error) => { + console.warn('[QuickVoiceAdd] stop error:', error); + }); + } + }; + }, [recorder]); + + const startRecording = async () => { + if (disabled || state !== 'idle') return; + + try { + const permission = await requestRecordingPermissionsAsync(); + if (!permission.granted) { + Alert.alert('Microfono non disponibile', 'Abilita il microfono per usare Quick add.'); + return; + } + + await setAudioModeAsync({ + allowsRecording: true, + playsInSilentMode: true, + }); + await recorder.prepareToRecordAsync(RecordingPresets.HIGH_QUALITY); + recorder.record(); + isRecordingRef.current = true; + recordingStartRef.current = Date.now(); + setState('recording'); + } catch (error) { + console.error('[QuickVoiceAdd] startRecording error:', error); + Alert.alert('Errore registrazione', 'Non sono riuscito ad avviare il microfono.'); + setState('idle'); + } + }; + + const sendRecording = async () => { + if (state !== 'recording') return; + + try { + const elapsedMs = Date.now() - recordingStartRef.current; + if (elapsedMs < 450) { + await new Promise((resolve) => setTimeout(resolve, 450 - elapsedMs)); + } + + await recorder.stop(); + isRecordingRef.current = false; + const uri = recorder.uri; + if (!uri) { + throw new Error('File audio non disponibile'); + } + + setState('sending'); + const result = await sendQuickVoiceAdd(uri, model); + if (!result.received && !result.recivied) { + throw new Error(result.error || 'Audio non ricevuto dal server'); + } + + setState('success'); + resetTimerRef.current = setTimeout(() => { + setState('idle'); + }, 1050); + } catch (error: any) { + console.error('[QuickVoiceAdd] sendRecording error:', error); + Alert.alert('Invio non riuscito', error?.message || 'Riprova tra poco.'); + setState('idle'); + isRecordingRef.current = false; + } finally { + setAudioModeAsync({ allowsRecording: false }).catch(() => undefined); + } + }; + + const cancelRecording = async () => { + if (state !== 'recording') return; + try { + await recorder.stop(); + } catch { + // Ignore stop errors while cancelling. + } finally { + isRecordingRef.current = false; + setAudioModeAsync({ allowsRecording: false }).catch(() => undefined); + setState('idle'); + } + }; + + const width = expandAnim.interpolate({ + inputRange: [0, 1], + outputRange: [138, 292], + }); + const height = expandAnim.interpolate({ + inputRange: [0, 1], + outputRange: [44, 64], + }); + const borderRadius = expandAnim.interpolate({ + inputRange: [0, 1], + outputRange: [22, 32], + }); + const compactOpacity = expandAnim.interpolate({ + inputRange: [0, 0.45], + outputRange: [1, 0], + extrapolate: 'clamp', + }); + const expandedOpacity = expandAnim.interpolate({ + inputRange: [0.35, 1], + outputRange: [0, 1], + extrapolate: 'clamp', + }); + + return ( + + + + {showCompactIcon && ( + + )} + + {t('home.quickAdd.label')} + + + + + + {state === 'success' ? ( + + + + {t('home.quickAdd.sent')} + + + ) : ( + <> + + + + + + {bars.map((bar, index) => { + const barHeight = bar.interpolate({ + inputRange: [0, 1], + outputRange: [8 + (index % 3) * 2, 34 - (index % 4) * 3], + }); + return ( + + ); + })} + + + + {state === 'sending' ? ( + + ) : ( + + )} + + + )} + + + ); +}; + +const styles = StyleSheet.create({ + shell: { + marginTop: 28, + backgroundColor: '#FFFFFF', + borderWidth: 1, + borderColor: colors.border, + shadowColor: '#000000', + shadowOffset: { width: 0, height: 8 }, + shadowOpacity: 0.08, + shadowRadius: 18, + elevation: 4, + overflow: 'hidden', + }, + compactContent: { + flex: 1, + gap: 8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + paddingHorizontal: 16, + }, + compactLabel: { + color: colors.textPrimary, + fontSize: 15, + fontWeight: '600', + letterSpacing: 0, + }, + disabledText: { + color: colors.textTertiary, + }, + expandedContent: { + ...StyleSheet.absoluteFillObject, + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 10, + gap: 10, + }, + cancelButton: { + width: 34, + height: 34, + borderRadius: 17, + alignItems: 'center', + justifyContent: 'center', + }, + waveform: { + flex: 1, + height: 42, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: Platform.OS === 'ios' ? 4 : 3, + }, + waveBar: { + width: 3, + borderRadius: 2, + backgroundColor: colors.textPrimary, + }, + submitButton: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: '#000000', + alignItems: 'center', + justifyContent: 'center', + }, + submitButtonDisabled: { + opacity: 0.78, + }, + successContent: { + flex: 1, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + gap: 8, + }, + successText: { + color: colors.textPrimary, + fontSize: 16, + fontWeight: '700', + letterSpacing: 0, + }, +}); + +export default QuickVoiceAdd; diff --git a/src/locales/en.json b/src/locales/en.json index 9d26d9c..f2b272a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -129,6 +129,10 @@ "error": "Sorry, an error occurred. Please try again later.", "suggestedCommand": "💡 What can you do?" }, + "quickAdd": { + "label": "Quick add", + "sent": "Sent" + }, "sync": { "syncing": "Syncing...", "offline": "Offline mode", diff --git a/src/locales/it.json b/src/locales/it.json index bceec67..ff46428 100644 --- a/src/locales/it.json +++ b/src/locales/it.json @@ -130,6 +130,10 @@ "error": "Mi dispiace, si è verificato un errore. Riprova più tardi.", "suggestedCommand": "💡 Cosa puoi fare?" }, + "quickAdd": { + "label": "Aggiunta rapida", + "sent": "Inviato" + }, "sync": { "syncing": "Sincronizzando...", "offline": "Modalità offline", diff --git a/src/navigation/screens/Home.tsx b/src/navigation/screens/Home.tsx index f43faa4..4a8d498 100644 --- a/src/navigation/screens/Home.tsx +++ b/src/navigation/screens/Home.tsx @@ -31,6 +31,7 @@ import SyncManager from '../../services/SyncManager'; import Badge from "../../components/UI/Badge"; import VoiceChatModal from "../../components/BotChat/VoiceChatModal"; import VoiceCalendarModal from "../../components/BotChat/VoiceCalendarModal"; +import QuickVoiceAdd from "../../components/BotChat/QuickVoiceAdd"; import { useTranslation } from 'react-i18next'; import { ChatHistory } from "../../components/BotChat/ChatHistory"; import { useTutorialContext } from "../../contexts/TutorialContext"; @@ -887,6 +888,13 @@ const HomeScreen = () => { )} + + )} @@ -1203,6 +1211,9 @@ const styles = StyleSheet.create({ marginTop: 20, alignItems: "center", }, + quickAddContainer: { + marginTop: 40, + }, suggestedCommandButton: { backgroundColor: "#f8f9fa", paddingHorizontal: 20, diff --git a/src/services/quickVoiceAddService.ts b/src/services/quickVoiceAddService.ts new file mode 100644 index 0000000..0e92989 --- /dev/null +++ b/src/services/quickVoiceAddService.ts @@ -0,0 +1,72 @@ +import { DEFAULT_BASE_URL } from '../constants/authConstants'; +import { getValidToken } from './authService'; + +export interface QuickVoiceAddResponse { + received: boolean; + recivied?: boolean; + processing?: boolean; + model?: 'base' | 'advanced'; + remaining?: { + voice_daily?: number | string; + voice_monthly?: number | string; + }; + rate_limit?: unknown; + error?: string; +} + +function getContentType(uri: string): string { + const lower = uri.toLowerCase(); + if (lower.endsWith('.m4a') || lower.endsWith('.mp4')) return 'audio/mp4'; + if (lower.endsWith('.mp3') || lower.endsWith('.mpeg')) return 'audio/mpeg'; + if (lower.endsWith('.wav')) return 'audio/wav'; + if (lower.endsWith('.webm')) return 'audio/webm'; + if (lower.endsWith('.ogg') || lower.endsWith('.oga')) return 'audio/ogg'; + return 'audio/mp4'; +} + +function getFileName(uri: string): string { + const name = uri.split('/').pop(); + return name && name.includes('.') ? name : `quick-voice-add-${Date.now()}.m4a`; +} + +export async function sendQuickVoiceAdd( + audioUri: string, + model: 'base' | 'advanced' = 'base' +): Promise { + const token = await getValidToken(); + if (!token) { + throw new Error('Token di autenticazione non disponibile'); + } + + const formData = new FormData(); + formData.append('model', model); + formData.append('audio', { + uri: audioUri, + name: getFileName(audioUri), + type: getContentType(audioUri), + } as any); + + const apiUrl = `${process.env.EXPO_PUBLIC_API_BASE_URL || DEFAULT_BASE_URL}/chat/voice-command`; + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/json', + }, + body: formData, + }); + + let data: QuickVoiceAddResponse | null = null; + try { + data = await response.json(); + } catch { + data = null; + } + + if (!response.ok) { + const detail = (data as any)?.detail || data?.error || `Errore HTTP ${response.status}`; + throw new Error(detail); + } + + return data ?? { received: false, error: 'Risposta non valida dal server' }; +} From 8db6e1ad7e1be20ad74c25623944b96a34c09b53 Mon Sep 17 00:00:00 2001 From: Gabry848 Date: Thu, 21 May 2026 12:30:15 +0200 Subject: [PATCH 2/2] integrate quick voice add in views --- src/components/BotChat/QuickVoiceAdd.tsx | 79 +++++++++-- src/components/Calendar/CalendarView.tsx | 129 ++++++++++++++++-- src/components/Calendar20/Calendar20View.tsx | 12 +- src/components/Calendar20/FABMenu.tsx | 48 +++++-- src/components/TaskList/TaskListContainer.tsx | 28 +++- src/components/TaskList/styles.ts | 28 +++- src/navigation/screens/Notes.tsx | 2 +- 7 files changed, 287 insertions(+), 39 deletions(-) diff --git a/src/components/BotChat/QuickVoiceAdd.tsx b/src/components/BotChat/QuickVoiceAdd.tsx index fbc3790..6290039 100644 --- a/src/components/BotChat/QuickVoiceAdd.tsx +++ b/src/components/BotChat/QuickVoiceAdd.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { ActivityIndicator, Alert, @@ -29,7 +29,12 @@ interface QuickVoiceAddProps { model: 'base' | 'advanced'; disabled?: boolean; showCompactIcon?: boolean; + variant?: 'pill' | 'fab'; + autoStart?: boolean; containerStyle?: StyleProp; + onStateChange?: (state: QuickVoiceAddState) => void; + onSuccess?: () => void; + onCancel?: () => void; } const BAR_COUNT = 18; @@ -38,7 +43,12 @@ const QuickVoiceAdd: React.FC = ({ model, disabled = false, showCompactIcon = true, + variant = 'pill', + autoStart = false, containerStyle, + onStateChange, + onSuccess, + onCancel, }) => { const { t } = useTranslation(); const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY); @@ -48,8 +58,14 @@ const QuickVoiceAdd: React.FC = ({ const recordingStartRef = useRef(0); const resetTimerRef = useRef | null>(null); const isRecordingRef = useRef(false); + const autoStartedRef = useRef(false); const isExpanded = state !== 'idle'; + const isFab = variant === 'fab'; + + useEffect(() => { + onStateChange?.(state); + }, [onStateChange, state]); useEffect(() => { Animated.spring(expandAnim, { @@ -105,7 +121,7 @@ const QuickVoiceAdd: React.FC = ({ }; }, [recorder]); - const startRecording = async () => { + const startRecording = useCallback(async () => { if (disabled || state !== 'idle') return; try { @@ -129,7 +145,23 @@ const QuickVoiceAdd: React.FC = ({ Alert.alert('Errore registrazione', 'Non sono riuscito ad avviare il microfono.'); setState('idle'); } - }; + }, [disabled, recorder, state]); + + useEffect(() => { + if (!autoStart) { + autoStartedRef.current = false; + return; + } + + if (autoStartedRef.current || disabled || state !== 'idle') return; + autoStartedRef.current = true; + + const timer = setTimeout(() => { + startRecording(); + }, 120); + + return () => clearTimeout(timer); + }, [autoStart, disabled, startRecording, state]); const sendRecording = async () => { if (state !== 'recording') return; @@ -154,6 +186,7 @@ const QuickVoiceAdd: React.FC = ({ } setState('success'); + onSuccess?.(); resetTimerRef.current = setTimeout(() => { setState('idle'); }, 1050); @@ -177,20 +210,29 @@ const QuickVoiceAdd: React.FC = ({ isRecordingRef.current = false; setAudioModeAsync({ allowsRecording: false }).catch(() => undefined); setState('idle'); + onCancel?.(); } }; const width = expandAnim.interpolate({ inputRange: [0, 1], - outputRange: [138, 292], + outputRange: [isFab ? 56 : 138, 292], }); const height = expandAnim.interpolate({ inputRange: [0, 1], - outputRange: [44, 64], + outputRange: [isFab ? 56 : 44, 64], }); const borderRadius = expandAnim.interpolate({ inputRange: [0, 1], - outputRange: [22, 32], + outputRange: [isFab ? 28 : 22, 32], + }); + const backgroundColor = expandAnim.interpolate({ + inputRange: [0, 1], + outputRange: [isFab ? '#000000' : '#FFFFFF', '#FFFFFF'], + }); + const borderColor = expandAnim.interpolate({ + inputRange: [0, 1], + outputRange: [isFab ? '#000000' : colors.border, colors.border], }); const compactOpacity = expandAnim.interpolate({ inputRange: [0, 0.45], @@ -204,19 +246,25 @@ const QuickVoiceAdd: React.FC = ({ }); return ( - + - - {showCompactIcon && ( - + + {isFab ? ( + + ) : ( + <> + {showCompactIcon && ( + + )} + + {t('home.quickAdd.label')} + + )} - - {t('home.quickAdd.label')} - @@ -276,9 +324,7 @@ const QuickVoiceAdd: React.FC = ({ const styles = StyleSheet.create({ shell: { marginTop: 28, - backgroundColor: '#FFFFFF', borderWidth: 1, - borderColor: colors.border, shadowColor: '#000000', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.08, @@ -294,6 +340,9 @@ const styles = StyleSheet.create({ justifyContent: 'center', paddingHorizontal: 16, }, + fabCompactContent: { + paddingHorizontal: 0, + }, compactLabel: { color: colors.textPrimary, fontSize: 15, diff --git a/src/components/Calendar/CalendarView.tsx b/src/components/Calendar/CalendarView.tsx index dd0150b..50c1464 100644 --- a/src/components/Calendar/CalendarView.tsx +++ b/src/components/Calendar/CalendarView.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { View, ScrollView, StyleSheet, Alert, ActivityIndicator, Dimensions } from 'react-native'; +import { View, ScrollView, StyleSheet, Alert, ActivityIndicator, Modal, Pressable, TouchableOpacity } from 'react-native'; import { useTranslation } from 'react-i18next'; import dayjs from 'dayjs'; import { Task as TaskType, getAllTasks, addTask, deleteTask, updateTask, completeTask, disCompleteTask } from '../../services/taskService'; @@ -12,7 +12,7 @@ import { useFocusEffect } from '@react-navigation/native'; import CalendarGrid from './CalendarGrid'; import Task from '../Task/Task'; import AddTask from '../Task/AddTask'; -import AddTaskButton from '../Task/AddTaskButton'; +import QuickVoiceAdd from '../BotChat/QuickVoiceAdd'; import { addTaskToList } from '../TaskList/types'; import { LoadingState, EmptyState, StatusChip, AppText } from '../UI/foundation'; @@ -26,12 +26,10 @@ const CalendarView: React.FC = () => { const [selectedDate, setSelectedDate] = useState(dayjs().format('YYYY-MM-DD')); const [tasks, setTasks] = useState([]); const [showAddTask, setShowAddTask] = useState(false); + const [voiceAddModalVisible, setVoiceAddModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(true); const [syncStatus, setSyncStatus] = useState(null); - // Screen dimensions - const screenWidth = Dimensions.get('window').width; - // Servizi const cacheService = useRef(TaskCacheService.getInstance()).current; const syncManager = useRef(SyncManager.getInstance()).current; @@ -355,6 +353,65 @@ const CalendarView: React.FC = () => { setShowAddTask(true); }; + const handleVoiceAddSuccess = () => { + fetchTasks(); + setTimeout(() => { + setVoiceAddModalVisible(false); + }, 1150); + }; + + const renderHeaderActions = () => ( + + + + + + setVoiceAddModalVisible(true)} + activeOpacity={0.82} + accessibilityRole="button" + accessibilityLabel={t("tasks.accessibility.addTaskLabel")} + accessibilityHint={t("tasks.accessibility.addTaskHint")} + > + + + + ); + + const renderVoiceAddModal = () => ( + setVoiceAddModalVisible(false)} + > + + setVoiceAddModalVisible(false)} + /> + + setVoiceAddModalVisible(false)} + containerStyle={styles.voiceAddFab} + /> + + + + ); + // Gestisce la chiusura del form const handleCloseAddTask = () => { setShowAddTask(false); @@ -421,7 +478,7 @@ const CalendarView: React.FC = () => { {t('calendar.commitmentsOf')} {dayjs(selectedDate).format('DD')} {t(`calendar.months.${MONTH_KEYS[dayjs(selectedDate).month()]}`)} {dayjs(selectedDate).year()} - + {renderHeaderActions()} {/* Componente di caricamento */} @@ -436,6 +493,8 @@ const CalendarView: React.FC = () => { categoryName={t('calendar.defaultCategory')} initialDate={selectedDate} /> + + {renderVoiceAddModal()} ); } @@ -475,7 +534,7 @@ const CalendarView: React.FC = () => { )} - + {renderHeaderActions()} @@ -499,6 +558,8 @@ const CalendarView: React.FC = () => { )} + {renderVoiceAddModal()} + {/* Componente AddTask con selezione categorie abilitata */} = ({ onClose }) => { const [searchVisible, setSearchVisible] = useState(false); const [miniCalendarVisible, setMiniCalendarVisible] = useState(false); const [addTaskVisible, setAddTaskVisible] = useState(false); + const [voiceAddActive, setVoiceAddActive] = useState(false); const [selectedDateForTask, setSelectedDateForTask] = useState(null); const cacheService = useRef(TaskCacheService.getInstance()).current; @@ -298,7 +299,7 @@ const Calendar20View: React.FC = ({ onClose }) => { } setAddTaskVisible(false); setSelectedDateForTask(null); - }, [currentDate, selectedDateForTask]); + }, [currentDate, selectedDateForTask, t]); const handleCategoryToggle = useCallback((categoryName: string) => { setEnabledCategories(prev => { @@ -370,11 +371,15 @@ const Calendar20View: React.FC = ({ onClose }) => { {renderView()} + {voiceAddActive && } + { setSelectedDateForTask(null); setAddTaskVisible(true); }} + onVoiceStateChange={setVoiceAddActive} + onVoiceSuccess={fetchTasks} /> void; + onVoiceStateChange?: (isActive: boolean) => void; + onVoiceSuccess?: () => void; } -const FABMenu: React.FC = ({ onNewTask }) => { +const FABMenu: React.FC = ({ onNewTask, onVoiceStateChange, onVoiceSuccess }) => { return ( - - - + + + + + + onVoiceStateChange?.(state !== 'idle')} + containerStyle={styles.voiceFab} + /> + ); }; const styles = StyleSheet.create({ - fab: { + dock: { position: 'absolute', bottom: 20, right: 20, + flexDirection: 'row', + alignItems: 'center', + gap: 10, + zIndex: 220, + }, + voiceFab: { + marginTop: 0, + }, + manualFab: { width: 56, height: 56, borderRadius: 28, - backgroundColor: '#000000', + backgroundColor: '#ffffff', alignItems: 'center', justifyContent: 'center', + borderWidth: 1, + borderColor: '#e5e5ea', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.08, shadowRadius: 12, elevation: 3, - zIndex: 100, }, }); diff --git a/src/components/TaskList/TaskListContainer.tsx b/src/components/TaskList/TaskListContainer.tsx index 005eae3..64e3458 100644 --- a/src/components/TaskList/TaskListContainer.tsx +++ b/src/components/TaskList/TaskListContainer.tsx @@ -2,15 +2,16 @@ import React, { useState, useEffect, useMemo, useCallback, useLayoutEffect } fro import { View, ScrollView, Alert, TouchableOpacity } from 'react-native'; import { useTranslation } from 'react-i18next'; import { useNavigation } from '@react-navigation/native'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Ionicons } from '@expo/vector-icons'; import { styles } from './styles'; import { Task as TaskType, globalTasksRef } from './types'; import eventEmitter, { EVENTS } from '../../utils/eventEmitter'; import { ActiveFilters } from './ActiveFilters'; import { FilterModal } from './FilterModal'; -import { AddTaskButton } from './AddTaskButton'; import { filterTasksByDay } from './TaskUtils'; import AddTask from '../Task/AddTask'; +import QuickVoiceAdd from '../BotChat/QuickVoiceAdd'; import { recurringTaskService, RecurringTask, CreateRecurringTaskPayload } from '../../services/recurringTaskService'; import { LoadingState, EmptyState } from '../UI/foundation'; import { CompletedTasksButton } from './CompletedTasksButton'; @@ -41,6 +42,7 @@ export const TaskListContainer = ({ taskService }: TaskListContainerProps) => { const { t } = useTranslation(); + const insets = useSafeAreaInsets(); const [tasks, setTasks] = useState([]); const [filtroImportanza, setFiltroImportanza] = useState("Tutte"); const [filtroScadenza, setFiltroScadenza] = useState("Tutte"); @@ -652,7 +654,29 @@ export const TaskListContainer = ({ onDeleteAll={handleDeleteAllCompleted} /> - + + + + + + + { export default function Notes() { return ( - +