From bc0e69a5c5b4b4a3a777304ca23f9f42c2d3b626 Mon Sep 17 00:00:00 2001 From: Aazam Thakur Date: Thu, 6 Feb 2025 17:44:59 +0530 Subject: [PATCH 1/5] initial commit --- .../pages/conversations/widgets/calendar.dart | 70 +++++++++++++++++++ .../conversations/widgets/search_widget.dart | 5 ++ 2 files changed, 75 insertions(+) create mode 100644 app/lib/pages/conversations/widgets/calendar.dart diff --git a/app/lib/pages/conversations/widgets/calendar.dart b/app/lib/pages/conversations/widgets/calendar.dart new file mode 100644 index 000000000..d8419f6b6 --- /dev/null +++ b/app/lib/pages/conversations/widgets/calendar.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:friend_private/providers/conversation_provider.dart'; + +class CalendarIconButton extends StatefulWidget { + const CalendarIconButton({super.key}); + + @override + State createState() => _CalendarIconButtonState(); +} + +class _CalendarIconButtonState extends State { + DateTime? _selectedDate; // Store the selected date + + Future _showDatePicker(BuildContext context) async { + final DateTime now = DateTime.now(); // Get the current date + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate ?? DateTime.now(), // Start with current or selected date + firstDate: DateTime(2000), // Set a reasonable first date + lastDate: now, // Set a reasonable last date + ); + + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + // filter conversations based on the date. + //_filterConversationsByDate(_selectedDate); + }); + } + } +/* + void _filterConversationsByDate(DateTime? date) { + // filter conversations based on the selected date. + // ConversationProvider or similar state management. + + if (date != null) { + print("Filtering conversations by: ${DateFormat('yyyy-MM-dd').format(date)}"); + // Example: Call a function in your provider + // Provider.of(context, listen: false).filterByDate(date); + } else { + print("Clearing date filter"); + //Provider.of(context, listen: false).clearDateFilter(); + } + + } */ + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (BuildContext context, ConversationProvider convoProvider, Widget? child) { + return Container( + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: const BorderRadius.all(Radius.circular(16)), + ), + child: IconButton( + onPressed: () => _showDatePicker(context), + icon: const Icon( + Icons.calendar_today, + color: Colors.white, + size: 20, // Match the filter button size + ), + ), + ); + } + ); + } +} \ No newline at end of file diff --git a/app/lib/pages/conversations/widgets/search_widget.dart b/app/lib/pages/conversations/widgets/search_widget.dart index a584683b6..c1bac381c 100644 --- a/app/lib/pages/conversations/widgets/search_widget.dart +++ b/app/lib/pages/conversations/widgets/search_widget.dart @@ -3,6 +3,7 @@ import 'package:friend_private/providers/conversation_provider.dart'; import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/utils/other/debouncer.dart'; import 'package:provider/provider.dart'; +import 'calendar.dart'; // Relative path import class SearchWidget extends StatefulWidget { const SearchWidget({super.key}); @@ -82,6 +83,10 @@ class _SearchWidgetState extends State { const SizedBox( width: 12, ), + const CalendarIconButton(), + const SizedBox( + width: 12, + ), Consumer( builder: (BuildContext context, ConversationProvider convoProvider, Widget? child) { return Container( From ca7afb2c11337f57870596c361544c6cd00851f1 Mon Sep 17 00:00:00 2001 From: Aazam Thakur Date: Fri, 7 Feb 2025 21:12:13 +0530 Subject: [PATCH 2/5] added filtering functionality --- .../pages/conversations/widgets/calendar.dart | 62 +++++-------------- app/lib/providers/conversation_provider.dart | 40 ++++++++++++ 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/app/lib/pages/conversations/widgets/calendar.dart b/app/lib/pages/conversations/widgets/calendar.dart index d8419f6b6..025fca92f 100644 --- a/app/lib/pages/conversations/widgets/calendar.dart +++ b/app/lib/pages/conversations/widgets/calendar.dart @@ -1,70 +1,40 @@ import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:friend_private/providers/conversation_provider.dart'; -class CalendarIconButton extends StatefulWidget { +class CalendarIconButton extends StatelessWidget { // StatelessWidget now const CalendarIconButton({super.key}); - @override - State createState() => _CalendarIconButtonState(); -} - -class _CalendarIconButtonState extends State { - DateTime? _selectedDate; // Store the selected date - - Future _showDatePicker(BuildContext context) async { - final DateTime now = DateTime.now(); // Get the current date - final DateTime? picked = await showDatePicker( - context: context, - initialDate: _selectedDate ?? DateTime.now(), // Start with current or selected date - firstDate: DateTime(2000), // Set a reasonable first date - lastDate: now, // Set a reasonable last date - ); - - if (picked != null && picked != _selectedDate) { - setState(() { - _selectedDate = picked; - // filter conversations based on the date. - //_filterConversationsByDate(_selectedDate); - }); - } - } -/* - void _filterConversationsByDate(DateTime? date) { - // filter conversations based on the selected date. - // ConversationProvider or similar state management. - - if (date != null) { - print("Filtering conversations by: ${DateFormat('yyyy-MM-dd').format(date)}"); - // Example: Call a function in your provider - // Provider.of(context, listen: false).filterByDate(date); - } else { - print("Clearing date filter"); - //Provider.of(context, listen: false).clearDateFilter(); - } - - } */ - @override Widget build(BuildContext context) { return Consumer( - builder: (BuildContext context, ConversationProvider convoProvider, Widget? child) { + builder: (context, convoProvider, child) { return Container( decoration: BoxDecoration( color: Colors.grey.shade900, borderRadius: const BorderRadius.all(Radius.circular(16)), ), child: IconButton( - onPressed: () => _showDatePicker(context), + onPressed: () async { + DateTime? picked = await showDatePicker( + context: context, + initialDate: convoProvider.selectedDate ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now(), + ); + + if (picked != null && picked != convoProvider.selectedDate) { + convoProvider.selectDate(picked); + } + }, icon: const Icon( Icons.calendar_today, color: Colors.white, - size: 20, // Match the filter button size + size: 20, ), ), ); - } + }, ); } } \ No newline at end of file diff --git a/app/lib/providers/conversation_provider.dart b/app/lib/providers/conversation_provider.dart index a9bae28e5..c98fba3e5 100644 --- a/app/lib/providers/conversation_provider.dart +++ b/app/lib/providers/conversation_provider.dart @@ -397,6 +397,7 @@ class ConversationProvider extends ChangeNotifier implements IWalServiceListener @override void dispose() { _processingConversationWatchTimer?.cancel(); + _selectedDate = null; _wal.unsubscribe(this); super.dispose(); } @@ -581,4 +582,43 @@ class ConversationProvider extends ChangeNotifier implements IWalServiceListener isFetchingConversations = value; notifyListeners(); } + + DateTime? _selectedDate; // Store the selected date + DateTime? get selectedDate => _selectedDate; + + void selectDate(DateTime? date) { + _selectedDate = date; + filterConversationsByDate(); + notifyListeners(); + } + + void filterConversationsByDate() { + groupedConversations = {}; // Clear existing groups + + if (_selectedDate == null) { + // Show all conversations if no date is selected. Restore the previous view + if (previousQuery.isNotEmpty) { + groupSearchConvosByDate(); + } else { + groupConversationsByDate(); + } + return; + } + for (var conversation in _filterOutConvos(conversations)) { + var date = DateTime(conversation.createdAt.year, conversation.createdAt.month, conversation.createdAt.day); + if (date == _selectedDate) { + if (!groupedConversations.containsKey(date)) { + groupedConversations[date] = []; + } + groupedConversations[date]?.add(conversation); + } + } + // Sort + for (final date in groupedConversations.keys) { + groupedConversations[date]?.sort((a, b) => b.createdAt.compareTo(a.createdAt)); + } + + notifyListeners(); + } + } From 0a5f075ed8a4fdf0a81692ffacd8185a735a3a7c Mon Sep 17 00:00:00 2001 From: Aazam Thakur Date: Mon, 10 Feb 2025 02:12:36 +0530 Subject: [PATCH 3/5] improved ui for android and ios --- .../pages/conversations/widgets/calendar.dart | 84 ++++++++++++++++--- app/lib/providers/conversation_provider.dart | 1 - 2 files changed, 74 insertions(+), 11 deletions(-) diff --git a/app/lib/pages/conversations/widgets/calendar.dart b/app/lib/pages/conversations/widgets/calendar.dart index 025fca92f..7e759ed69 100644 --- a/app/lib/pages/conversations/widgets/calendar.dart +++ b/app/lib/pages/conversations/widgets/calendar.dart @@ -1,8 +1,10 @@ +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; import 'package:provider/provider.dart'; import 'package:friend_private/providers/conversation_provider.dart'; -class CalendarIconButton extends StatelessWidget { // StatelessWidget now +class CalendarIconButton extends StatelessWidget { const CalendarIconButton({super.key}); @override @@ -16,15 +18,77 @@ class CalendarIconButton extends StatelessWidget { // StatelessWidget now ), child: IconButton( onPressed: () async { - DateTime? picked = await showDatePicker( - context: context, - initialDate: convoProvider.selectedDate ?? DateTime.now(), - firstDate: DateTime(2000), - lastDate: DateTime.now(), - ); + if (Platform.isAndroid) { + // Android: Show Material Design DatePicker + DateTime? picked = await showDatePicker( + context: context, + initialDate: convoProvider.selectedDate ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: DateTime.now(), + builder: (context, child) { + return Dialog( + backgroundColor: Colors.transparent, + child: SizedBox( + height: MediaQuery.of(context).size.height * 0.75, + child: Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Colors.white, + onPrimary: Colors.grey.shade900, + surface: Colors.grey.shade900, + onSurface: Colors.white, + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: Colors.white, + ), + ), + ), + child: child!, + ), + ), + ); + }, + ); - if (picked != null && picked != convoProvider.selectedDate) { - convoProvider.selectDate(picked); + if (picked != null && picked != convoProvider.selectedDate) { + convoProvider.selectDate(picked); + } + } else if (Platform.isIOS) { + // iOS: Show Cupertino-style DatePicker + showCupertinoModalPopup( + context: context, + builder: (context) { + DateTime selectedDate = + convoProvider.selectedDate ?? DateTime.now(); + return Container( + height: 250, + color: Colors.white, + child: Column( + children: [ + SizedBox( + height: 200, + child: CupertinoDatePicker( + mode: CupertinoDatePickerMode.date, + initialDateTime: selectedDate, + minimumDate: DateTime(2000), + maximumDate: DateTime.now(), + onDateTimeChanged: (DateTime newDate) { + convoProvider.selectDate(newDate); + }, + ), + ), + CupertinoButton( + child: const Text('Done'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + ); + }, + ); } }, icon: const Icon( @@ -37,4 +101,4 @@ class CalendarIconButton extends StatelessWidget { // StatelessWidget now }, ); } -} \ No newline at end of file +} diff --git a/app/lib/providers/conversation_provider.dart b/app/lib/providers/conversation_provider.dart index c98fba3e5..ce96edbd5 100644 --- a/app/lib/providers/conversation_provider.dart +++ b/app/lib/providers/conversation_provider.dart @@ -617,7 +617,6 @@ class ConversationProvider extends ChangeNotifier implements IWalServiceListener for (final date in groupedConversations.keys) { groupedConversations[date]?.sort((a, b) => b.createdAt.compareTo(a.createdAt)); } - notifyListeners(); } From 569cbd8ba80b5a683974a74215ff30e6ab3ebddc Mon Sep 17 00:00:00 2001 From: Aazam Thakur Date: Mon, 10 Feb 2025 17:32:10 +0530 Subject: [PATCH 4/5] testing on backend --- app/lib/backend/http/api/conversations.dart | 24 +++++++ app/lib/providers/conversation_provider.dart | 71 +++++++++----------- 2 files changed, 57 insertions(+), 38 deletions(-) diff --git a/app/lib/backend/http/api/conversations.dart b/app/lib/backend/http/api/conversations.dart index baf396826..70ccc26ea 100644 --- a/app/lib/backend/http/api/conversations.dart +++ b/app/lib/backend/http/api/conversations.dart @@ -380,3 +380,27 @@ Future<(List, int, int)> searchConversationsServer( } return ([], 0, 0); } + +Future<(List, int, int)> searchConversationsByDateServer( + DateTime date, { + int? page, + int? limit, + bool includeDiscarded = true, +}) async { + + var response = await makeApiCall( + url: '${Env.apiBaseUrl}v1/memories/date_search', // Call the new endpoint + headers: {}, + method: 'POST', + body: jsonEncode({'date': date,'page': page ?? 1, 'per_page': limit ?? 10, 'include_discarded': includeDiscarded,}), + ); + if (response == null) return ([], 0, 0); + if (response.statusCode == 200) { + List items = (jsonDecode(response.body))['items']; + int currentPage = (jsonDecode(response.body))['current_page']; + int totalPages = (jsonDecode(response.body))['total_pages']; + var convos = items.map((item) => ServerConversation.fromJson(item)).toList(); + return (convos, currentPage, totalPages); + } + return ([], 0, 0); +} diff --git a/app/lib/providers/conversation_provider.dart b/app/lib/providers/conversation_provider.dart index ce96edbd5..7193a58e9 100644 --- a/app/lib/providers/conversation_provider.dart +++ b/app/lib/providers/conversation_provider.dart @@ -93,6 +93,39 @@ class ConversationProvider extends ChangeNotifier implements IWalServiceListener notifyListeners(); } + DateTime? selectedDate; + List dateFilteredConversations = []; + int currentDateSearchPage = 1; + int totalDateSearchPages = 1; + + void selectDate(DateTime date) async { + selectedDate = date; + currentDateSearchPage = 0; + await _fetchConversationsByDate(); + notifyListeners(); + } + + Future _fetchConversationsByDate() async { + if (selectedDate == null) { + dateFilteredConversations = []; + return; + } + + setIsFetchingConversations(true); + var (convos, current, total) = await searchConversationsByDateServer( + selectedDate!, + includeDiscarded: showDiscardedConversations, + page: currentDateSearchPage, + ); + + dateFilteredConversations = convos; + currentDateSearchPage = current; + totalDateSearchPages = total; + groupSearchConvosByDate(); + setIsFetchingConversations(false); + notifyListeners(); + } + Future searchMoreConversations() async { if (totalSearchPages < currentSearchPage + 1) { return; @@ -397,7 +430,6 @@ class ConversationProvider extends ChangeNotifier implements IWalServiceListener @override void dispose() { _processingConversationWatchTimer?.cancel(); - _selectedDate = null; _wal.unsubscribe(this); super.dispose(); } @@ -583,41 +615,4 @@ class ConversationProvider extends ChangeNotifier implements IWalServiceListener notifyListeners(); } - DateTime? _selectedDate; // Store the selected date - DateTime? get selectedDate => _selectedDate; - - void selectDate(DateTime? date) { - _selectedDate = date; - filterConversationsByDate(); - notifyListeners(); - } - - void filterConversationsByDate() { - groupedConversations = {}; // Clear existing groups - - if (_selectedDate == null) { - // Show all conversations if no date is selected. Restore the previous view - if (previousQuery.isNotEmpty) { - groupSearchConvosByDate(); - } else { - groupConversationsByDate(); - } - return; - } - for (var conversation in _filterOutConvos(conversations)) { - var date = DateTime(conversation.createdAt.year, conversation.createdAt.month, conversation.createdAt.day); - if (date == _selectedDate) { - if (!groupedConversations.containsKey(date)) { - groupedConversations[date] = []; - } - groupedConversations[date]?.add(conversation); - } - } - // Sort - for (final date in groupedConversations.keys) { - groupedConversations[date]?.sort((a, b) => b.createdAt.compareTo(a.createdAt)); - } - notifyListeners(); - } - } From 7bc515a302bc92ab6c1ed89e119eeefe1d422f69 Mon Sep 17 00:00:00 2001 From: Aazam Thakur Date: Mon, 10 Feb 2025 17:33:08 +0530 Subject: [PATCH 5/5] added changes backend --- backend/models/memory.py | 6 ++++ backend/routers/memories.py | 7 ++++ backend/utils/memories/calendar.py | 54 ++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 backend/utils/memories/calendar.py diff --git a/backend/models/memory.py b/backend/models/memory.py index 82ba61ce5..a2033b8d1 100644 --- a/backend/models/memory.py +++ b/backend/models/memory.py @@ -286,3 +286,9 @@ class SearchRequest(BaseModel): page: Optional[int] = 1 per_page: Optional[int] = 10 include_discarded: Optional[bool] = True + +class DateRequest(BaseModel): + date: datetime + page: Optional[int] = 1 + per_page: Optional[int] = 10 + include_discarded: Optional[bool] = True diff --git a/backend/routers/memories.py b/backend/routers/memories.py index a0e663f26..b0b045b78 100644 --- a/backend/routers/memories.py +++ b/backend/routers/memories.py @@ -8,6 +8,7 @@ from routers.transcribe_v2 import retrieve_in_progress_memory from utils.memories.process_memory import process_memory from utils.memories.search import search_memories +from utils.memories.calendar import filter_memories_by_date from utils.other import endpoints as auth from utils.other.storage import get_memory_recording_if_exists, \ delete_additional_profile_audio, delete_speech_sample_for_people @@ -344,3 +345,9 @@ def get_public_memories(offset: int = 0, limit: int = 1000): def search_memories_endpoint(search_request: SearchRequest, uid: str = Depends(auth.get_current_user_uid)): return search_memories(query=search_request.query, page=search_request.page, per_page=search_request.per_page, uid=uid, include_discarded=search_request.include_discarded) + +@router.post("/v1/memories/date_search", response_model=dict, tags=['memories']) # New endpoint for date search +def filter_memories_by_date_endpoint(date_request: DateRequest,uid: str = Depends(auth.get_current_user_uid)): + return filter_memories_by_date(date=date_request.date, page=date_request.page, + per_page=date_request.per_page, uid=uid, + include_discarded=date_request.include_discarded) diff --git a/backend/utils/memories/calendar.py b/backend/utils/memories/calendar.py new file mode 100644 index 000000000..66f6a4bc7 --- /dev/null +++ b/backend/utils/memories/calendar.py @@ -0,0 +1,54 @@ +import math +import os +from datetime import datetime +from typing import Dict + +import typesense + +client = typesense.Client({ + 'nodes': [{ + 'host': os.getenv('TYPESENSE_HOST'), + 'port': os.getenv('TYPESENSE_HOST_PORT'), + 'protocol': 'https' + }], + 'api_key': os.getenv('TYPESENSE_API_KEY'), + 'connection_timeout_seconds': 2 +}) + + +def filter_memories_by_date( + uid: str, + date: str, + page: int = 1, + per_page: int = 10, + include_discarded: bool = True, +) -> Dict: + try: + filter_by = f'userId:={uid} && deleted:=false' + if not include_discarded: + filter_by += ' && discarded:=false' + + filter_by += f' && created_at:={date}' + + search_parameters = { + 'filter_by': filter_by, # Only filter by date + 'sort_by': 'created_at:desc', + 'per_page': per_page, + 'page': page, + } + + results = client.collections['memories'].documents.search(search_parameters) + memories = [] + for item in results['hits']: + item['document']['created_at'] = datetime.utcfromtimestamp(item['document']['created_at']).isoformat() + item['document']['started_at'] = datetime.utcfromtimestamp(item['document']['started_at']).isoformat() + item['document']['finished_at'] = datetime.utcfromtimestamp(item['document']['finished_at']).isoformat() + memories.append(item['document']) + return { + 'items': memories, + 'total_pages': math.ceil(results['found'] / per_page), + 'current_page': page, + 'per_page': per_page + } + except Exception as e: + raise Exception(f"Failed to search conversations: {str(e)}")