feat(market): saved searches with in-app alerts (Wallapop-style)

Save the current market search (query + facets) and get notified when a
matching offer appears in your zone while the app is open, with a catch-up
scan on start. Adds a save affordance in the search bar, a saved-searches
list (apply/delete) with a per-search new-match badge, an AppBar entry
badged with the unseen total, and notification-tap routing to the list.
Alerts evaluate against the user's current area, dedup via seen keys, and
skip own/blocked/hidden/non-active offers. en+es strings.
This commit is contained in:
vjrj 2026-07-17 13:10:52 +02:00
parent c0cd299408
commit 93028c4933
9 changed files with 549 additions and 4 deletions

View file

@ -41,6 +41,8 @@ import '../services/plantare_service.dart';
import '../services/profile_cache.dart';
import '../services/profile_store.dart';
import '../services/saved_offers_store.dart';
import '../services/saved_search_alert_service.dart';
import '../services/saved_searches_store.dart';
import '../services/social_account_store.dart';
import '../services/social_connection.dart';
import '../services/social_service.dart';
@ -189,6 +191,9 @@ Future<void> configureDependencies() async {
..registerSingleton<SavedOffersStore>(
SavedOffersStore(secretStore, accountScope: scope),
)
..registerSingleton<SavedSearchesStore>(
SavedSearchesStore(secretStore, accountScope: scope),
)
..registerSingleton<ExportImportService>(
ExportImportService(
repository: varietyRepository,
@ -273,6 +278,18 @@ Future<void> configureDependencies() async {
selfSecretKey: socialService.identity.privateKeyHex,
connection: connection,
),
)
// Watches the market for offers matching the user's saved searches and
// fires an OS alert on a fresh match. Started in `Bootstrap`.
..registerSingleton<SavedSearchAlertService>(
SavedSearchAlertService(
connection: connection,
selfPubkey: socialService.publicKeyHex,
store: getIt<SavedSearchesStore>(),
settings: getIt<SocialSettings>(),
notifications: getIt<NotificationService>(),
alertTitle: (s) => t.savedSearches.alert(label: s.label),
),
);
}
@ -309,6 +326,10 @@ Future<void> switchSocialAccount(int account) async {
await getIt<PlantareService>().stop();
await getIt.unregister<PlantareService>();
}
if (getIt.isRegistered<SavedSearchAlertService>()) {
await getIt<SavedSearchAlertService>().stop();
await getIt.unregister<SavedSearchAlertService>();
}
if (getIt.isRegistered<SocialConnection>()) {
await getIt<SocialConnection>().dispose();
await getIt.unregister<SocialConnection>();
@ -324,6 +345,8 @@ Future<void> switchSocialAccount(int account) async {
await getIt.unregister<ProfileCache>();
await getIt<SavedOffersStore>().close();
await getIt.unregister<SavedOffersStore>();
await getIt<SavedSearchesStore>().close();
await getIt.unregister<SavedSearchesStore>();
// Re-register the per-identity stores under the new scope, then the identity.
getIt
@ -339,6 +362,9 @@ Future<void> switchSocialAccount(int account) async {
..registerSingleton<SavedOffersStore>(
SavedOffersStore(secretStore, accountScope: scope),
)
..registerSingleton<SavedSearchesStore>(
SavedSearchesStore(secretStore, accountScope: scope),
)
..registerSingleton<UnreadService>(
UnreadService(getIt<MessageStore>(), secretStore),
);
@ -373,16 +399,26 @@ Future<void> switchSocialAccount(int account) async {
selfSecretKey: social.identity.privateKeyHex,
connection: connection,
);
final savedSearchAlerts = SavedSearchAlertService(
connection: connection,
selfPubkey: social.publicKeyHex,
store: getIt<SavedSearchesStore>(),
settings: getIt<SocialSettings>(),
notifications: getIt<NotificationService>(),
alertTitle: (s) => t.savedSearches.alert(label: s.label),
);
getIt
..registerSingleton<SocialService>(social)
..registerSingleton<SocialConnection>(connection)
..registerSingleton<InboxService>(inbox)
..registerSingleton<SyncService>(sync)
..registerSingleton<PlantareService>(plantares);
..registerSingleton<PlantareService>(plantares)
..registerSingleton<SavedSearchAlertService>(savedSearchAlerts);
// Subscribe the listeners BEFORE the connection starts connecting.
inbox.start();
sync.start();
plantares.start();
savedSearchAlerts.start();
connection.start();
}