Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Date-based filtering for memories #1760

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions app/lib/backend/http/api/conversations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,27 @@ Future<(List<ServerConversation>, int, int)> searchConversationsServer(
}
return (<ServerConversation>[], 0, 0);
}

Future<(List<ServerConversation>, 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 (<ServerConversation>[], 0, 0);
if (response.statusCode == 200) {
List<dynamic> items = (jsonDecode(response.body))['items'];
int currentPage = (jsonDecode(response.body))['current_page'];
int totalPages = (jsonDecode(response.body))['total_pages'];
var convos = items.map<ServerConversation>((item) => ServerConversation.fromJson(item)).toList();
return (convos, currentPage, totalPages);
}
return (<ServerConversation>[], 0, 0);
}
104 changes: 104 additions & 0 deletions app/lib/pages/conversations/widgets/calendar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
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 {
const CalendarIconButton({super.key});

@override
Widget build(BuildContext context) {
return Consumer<ConversationProvider>(
builder: (context, convoProvider, child) {
return Container(
decoration: BoxDecoration(
color: Colors.grey.shade900,
borderRadius: const BorderRadius.all(Radius.circular(16)),
),
child: IconButton(
onPressed: () async {
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);
}
} 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(
Icons.calendar_today,
color: Colors.white,
size: 20,
),
),
);
},
);
}
}
5 changes: 5 additions & 0 deletions app/lib/pages/conversations/widgets/search_widget.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down Expand Up @@ -82,6 +83,10 @@ class _SearchWidgetState extends State<SearchWidget> {
const SizedBox(
width: 12,
),
const CalendarIconButton(),
const SizedBox(
width: 12,
),
Consumer<ConversationProvider>(
builder: (BuildContext context, ConversationProvider convoProvider, Widget? child) {
return Container(
Expand Down
34 changes: 34 additions & 0 deletions app/lib/providers/conversation_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,39 @@ class ConversationProvider extends ChangeNotifier implements IWalServiceListener
notifyListeners();
}

DateTime? selectedDate;
List<ServerConversation> dateFilteredConversations = [];
int currentDateSearchPage = 1;
int totalDateSearchPages = 1;

void selectDate(DateTime date) async {
selectedDate = date;
currentDateSearchPage = 0;
await _fetchConversationsByDate();
notifyListeners();
}

Future<void> _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<void> searchMoreConversations() async {
if (totalSearchPages < currentSearchPage + 1) {
return;
Expand Down Expand Up @@ -581,4 +614,5 @@ class ConversationProvider extends ChangeNotifier implements IWalServiceListener
isFetchingConversations = value;
notifyListeners();
}

}
6 changes: 6 additions & 0 deletions backend/models/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
7 changes: 7 additions & 0 deletions backend/routers/memories.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
54 changes: 54 additions & 0 deletions backend/utils/memories/calendar.py
Original file line number Diff line number Diff line change
@@ -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)}")