From ae16ed7098bb2727223e85eb5858718478024a37 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Tue, 7 Oct 2025 17:12:47 +0530 Subject: [PATCH 1/4] add shimmer --- app/lib/pages/apps/explore_install_page.dart | 78 +++++++++++++++++--- 1 file changed, 67 insertions(+), 11 deletions(-) diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index 6475ea27070..3d3c19e80ef 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -395,18 +395,74 @@ class ExploreInstallPageState extends State with AutomaticKe } Widget _buildSearchLoadingSliver() { - return const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 48), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, + return SliverPadding( + padding: const EdgeInsets.only(bottom: 64, left: 20, right: 20, top: 20), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) => _buildShimmerListItem(), + childCount: 5, // Show 5 shimmer items + ), + ), + ); + } + + Widget _buildShimmerListItem() { + return Shimmer.fromColors( + baseColor: AppStyles.backgroundSecondary, + highlightColor: AppStyles.backgroundTertiary, + child: Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: AppStyles.backgroundSecondary, + borderRadius: BorderRadius.circular(16), + ), + child: Row( children: [ - SizedBox(height: 8), - CircularProgressIndicator(color: Colors.deepPurpleAccent, strokeWidth: 2), - SizedBox(height: 12), - Text( - 'Searching...', - style: TextStyle(color: Colors.white70, fontSize: 14), + // App icon shimmer + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: AppStyles.backgroundTertiary, + borderRadius: BorderRadius.circular(12), + ), + ), + const SizedBox(width: 16), + // App info shimmer + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 18, + decoration: BoxDecoration( + color: AppStyles.backgroundTertiary, + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 8), + Container( + width: 150, + height: 14, + decoration: BoxDecoration( + color: AppStyles.backgroundTertiary, + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ), + const SizedBox(width: 12), + // Button shimmer + Container( + width: 72, + height: 32, + decoration: BoxDecoration( + color: AppStyles.backgroundTertiary, + borderRadius: BorderRadius.circular(16), + ), ), ], ), From 33598cc014b9156f2c339fbc8f919778589cbb87 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Tue, 7 Oct 2025 17:13:05 +0530 Subject: [PATCH 2/4] fix filtering and search --- app/lib/pages/apps/list_item.dart | 8 +------- app/lib/providers/app_provider.dart | 26 ++++++++++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/app/lib/pages/apps/list_item.dart b/app/lib/pages/apps/list_item.dart index 19c1db53bf7..a360788993e 100644 --- a/app/lib/pages/apps/list_item.dart +++ b/app/lib/pages/apps/list_item.dart @@ -23,19 +23,13 @@ class AppListItem extends StatelessWidget { // Use Selector to only rebuild when this specific app's state or loading state changes return Selector( selector: (context, provider) { - // Find the current app state - final currentApp = provider.apps.firstWhere( - (a) => a.id == app.id, - orElse: () => app, - ); - // Check if this specific app is loading final isLoading = index != -1 && provider.appLoading.isNotEmpty && index < provider.appLoading.length && provider.appLoading[index]; - return (enabled: currentApp.enabled, isLoading: isLoading); + return (enabled: app.enabled, isLoading: isLoading); }, builder: (context, state, child) { return GestureDetector( diff --git a/app/lib/providers/app_provider.dart b/app/lib/providers/app_provider.dart index 379773932ea..1f0aacdf002 100644 --- a/app/lib/providers/app_provider.dart +++ b/app/lib/providers/app_provider.dart @@ -161,7 +161,8 @@ class AppProvider extends BaseProvider { notifyListeners(); return; } - notifyListeners(); + + await performServerSearch(); } bool _hasServerSideFilters() { @@ -172,13 +173,14 @@ class AppProvider extends BaseProvider { } Future performServerSearch() async { - if (isSearching) return; + if (isSearching) { + return; + } try { isSearching = true; notifyListeners(); - // Get category filter if active String? categoryFilter; if (filters.containsKey('Category') && filters['Category'] is Category) { categoryFilter = (filters['Category'] as Category).id; @@ -221,10 +223,8 @@ class AppProvider extends BaseProvider { ); searchResults = result.apps; - filteredApps = result.apps; // Use server results directly + filteredApps = result.apps; } catch (e) { - debugPrint('Error performing server search: $e'); - // Fallback to local search filterApps(); } finally { isSearching = false; @@ -242,11 +242,21 @@ class AppProvider extends BaseProvider { } void filterApps() { - if (apps.isEmpty) { + if (_hasServerSideFilters() && searchResults.isNotEmpty) { + filteredApps = searchResults; + return; + } + + if (apps.isEmpty && searchResults.isEmpty) { filteredApps = []; return; } + if (apps.isEmpty && searchResults.isNotEmpty) { + filteredApps = searchResults; + return; + } + final currentUid = SharedPreferencesUtil().uid; final lowercaseQuery = searchQuery.toLowerCase(); @@ -329,7 +339,7 @@ class AppProvider extends BaseProvider { notifyListeners(); // This should notify as it affects UI state } } else { - print("Error: Attempted to set loading state for invalid index $index"); + debugPrint("Error: Attempted to set loading state for invalid index $index"); } } From 748b035dc82122ac028e35f9dfcce822a462dcf7 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Tue, 7 Oct 2025 17:13:54 +0530 Subject: [PATCH 3/4] simplify installed apps condition --- backend/database/apps.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/database/apps.py b/backend/database/apps.py index 767f1fd3ce7..86fe1cded2f 100644 --- a/backend/database/apps.py +++ b/backend/database/apps.py @@ -125,13 +125,18 @@ def search_apps_db( if my_apps: filters.append(FieldFilter('uid', '==', uid)) - elif installed_apps and enabled_app_ids: + elif installed_apps: + if not enabled_app_ids or len(enabled_app_ids) == 0: + # User has no enabled apps + return [] + if len(enabled_app_ids) > 30: + # Firestore 'in' limited to 30 items filters.append(FieldFilter('approved', '==', True)) filters.append(FieldFilter('private', '==', False)) else: - if enabled_app_ids: - filters.append(FieldFilter('id', 'in', enabled_app_ids)) + # Query by specific IDs + filters.append(FieldFilter('id', 'in', enabled_app_ids)) else: # Default: Public approved apps From 2e5d39a2567f5e51e89d64378314ee7f0f69957d Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin Date: Tue, 7 Oct 2025 18:24:05 +0530 Subject: [PATCH 4/4] fix suggested and other apps not showing (due to no local data) --- .../conversation_detail_provider.dart | 12 + .../widgets/summarized_apps_sheet.dart | 220 +++++++++++++++--- 2 files changed, 195 insertions(+), 37 deletions(-) diff --git a/app/lib/pages/conversation_detail/conversation_detail_provider.dart b/app/lib/pages/conversation_detail/conversation_detail_provider.dart index 06dfb735897..5ece89b8644 100644 --- a/app/lib/pages/conversation_detail/conversation_detail_provider.dart +++ b/app/lib/pages/conversation_detail/conversation_detail_provider.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_provider_utilities/flutter_provider_utilities.dart'; +import 'package:omi/backend/http/api/apps.dart'; import 'package:omi/backend/http/api/conversations.dart'; import 'package:omi/backend/http/api/users.dart'; import 'package:omi/utils/platform/platform_manager.dart'; @@ -313,6 +314,17 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi } } + /// Returns the list of enabled apps that support conversations from the API + Future> getEnabledConversationAppsFromAPI() async { + try { + final result = await retrieveAppsSearch(installedApps: true, limit: 100); + return result.apps.where((app) => app.worksWithMemories() && app.enabled).toList(); + } catch (e) { + debugPrint('Error fetching enabled conversation apps: $e'); + return []; + } + } + /// Checks if an app is in the suggested apps list bool isAppSuggested(String appId) { return getSuggestedApps().contains(appId); diff --git a/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart b/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart index 7a215e4edcf..15fcc38448a 100644 --- a/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart +++ b/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart @@ -12,6 +12,7 @@ import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/extensions/string.dart'; import 'package:provider/provider.dart'; +import 'package:shimmer/shimmer.dart'; class SummarizedAppsBottomSheet extends StatelessWidget { const SummarizedAppsBottomSheet({super.key}); @@ -117,7 +118,7 @@ class _SheetHeader extends StatelessWidget { } } -class _AppsList extends StatelessWidget { +class _AppsList extends StatefulWidget { final ConversationDetailProvider provider; final String? currentAppId; @@ -126,25 +127,160 @@ class _AppsList extends StatelessWidget { required this.currentAppId, }); + @override + State<_AppsList> createState() => _AppsListState(); +} + +class _AppsListState extends State<_AppsList> { // Track app installation state static final Map _installingApps = {}; + List? _suggestedApps; + List? _enabledApps; + + @override + void initState() { + super.initState(); + _fetchApps(); + } + + Future _fetchApps() async { + // Fetch both suggested apps and enabled conversation apps in parallel + try { + final results = await Future.wait([ + widget.provider.getSuggestedAppsFromAPI(), + widget.provider.getEnabledConversationAppsFromAPI(), + ]); + + if (mounted) { + setState(() { + _suggestedApps = results[0]; + _enabledApps = results[1]; + }); + } + } catch (e) { + debugPrint('Error fetching apps: $e'); + if (mounted) { + setState(() { + _suggestedApps = []; + _enabledApps = []; + }); + } + } + } + + Widget _buildShimmerLoading() { + return ListView( + children: [ + // Auto option shimmer + _buildShimmerListItem(), + + // Suggested Apps section shimmer + const Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + 'Suggested Apps', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + _buildShimmerListItem(), + _buildShimmerListItem(), + + // Other Apps section shimmer + const Padding( + padding: EdgeInsets.fromLTRB(16, 16, 16, 8), + child: Text( + 'Available Apps', + style: TextStyle( + color: Colors.grey, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + _buildShimmerListItem(), + _buildShimmerListItem(), + _buildShimmerListItem(), + ], + ); + } + + Widget _buildShimmerListItem() { + return Shimmer.fromColors( + baseColor: const Color(0xFF1F1F25), + highlightColor: const Color(0xFF35343B), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + child: Row( + children: [ + // Leading icon placeholder + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: const Color(0xFF1F1F25), + borderRadius: BorderRadius.circular(16), + ), + ), + const SizedBox(width: 16), + // Title and subtitle placeholders + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: double.infinity, + height: 16, + decoration: BoxDecoration( + color: const Color(0xFF1F1F25), + borderRadius: BorderRadius.circular(4), + ), + ), + const SizedBox(height: 8), + Container( + width: 200, + height: 12, + decoration: BoxDecoration( + color: const Color(0xFF1F1F25), + borderRadius: BorderRadius.circular(4), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { - final availableApps = provider.appsList.where((app) => app.worksWithMemories() && app.enabled).toList(); - final suggestedAppIds = provider.getSuggestedApps(); - final lastUsedApp = provider.getLastUsedSummarizationApp(); - - // Convert suggested app IDs to App objects - final suggestedApps = suggestedAppIds - .map((appId) => provider.appsList.firstWhereOrNull((app) => app.id == appId)) - .where((app) => app != null) - .cast() - .toList(); + // Show shimmer loading while fetching apps + final isLoading = _suggestedApps == null || _enabledApps == null; + + if (isLoading) { + return _buildShimmerLoading(); + } + + // Use API-fetched apps instead of provider.appsList + final enabledApps = _enabledApps ?? []; + + // Get last used app ID and find it in the enabled apps + final lastUsedAppId = widget.provider.getLastUsedSummarizationAppId(); + final lastUsedApp = lastUsedAppId != null ? enabledApps.firstWhereOrNull((app) => app.id == lastUsedAppId) : null; + + // Use API-fetched suggested apps if available + final suggestedApps = _suggestedApps ?? []; + final suggestedAppIds = suggestedApps.map((app) => app.id).toList(); // Filter out suggested apps and last used app from other apps - final otherApps = availableApps - .where((app) => !provider.isAppSuggested(app.id) && (lastUsedApp == null || app.id != lastUsedApp.id)) + final otherApps = enabledApps + .where((app) => !suggestedAppIds.contains(app.id) && (lastUsedApp == null || app.id != lastUsedApp.id)) .toList(); return ListView( @@ -152,11 +288,11 @@ class _AppsList extends StatelessWidget { // Auto option _AppListItem( app: null, - isSelected: currentAppId == null, + isSelected: widget.currentAppId == null, onTap: () => _handleAutoAppTap(context), trailingIcon: const Icon(Icons.autorenew, color: Colors.white, size: 20), subtitle: 'Let Omi automatically choose the best app for this summary.', - provider: provider, + provider: widget.provider, ), // Suggested Apps section @@ -173,15 +309,15 @@ class _AppsList extends StatelessWidget { ), ), ...suggestedApps.map((app) { - final isAvailable = provider.isSuggestedAppAvailable(app.id); - final isInstalling = _AppsList._installingApps[app.id] == true; + final isAvailable = widget.provider.isSuggestedAppAvailable(app.id); + final isInstalling = _AppsListState._installingApps[app.id] == true; return _AppListItem( app: app, - isSelected: app.id == currentAppId, + isSelected: app.id == widget.currentAppId, onTap: () => isAvailable ? _handleAppTap(context, app) : _handleUnavailableAppTap(context, app), isSuggested: true, isInstalling: isInstalling, - provider: provider, + provider: widget.provider, ); }), ], @@ -203,17 +339,17 @@ class _AppsList extends StatelessWidget { if (lastUsedApp != null) _AppListItem( app: lastUsedApp, - isSelected: lastUsedApp.id == currentAppId, + isSelected: lastUsedApp.id == widget.currentAppId, onTap: () => _handleAppTap(context, lastUsedApp), isLastUsed: true, - provider: provider, + provider: widget.provider, ), // Then show other apps ...otherApps.map((app) => _AppListItem( app: app, - isSelected: app.id == currentAppId, + isSelected: app.id == widget.currentAppId, onTap: () => _handleAppTap(context, app), - provider: provider, + provider: widget.provider, )), ], @@ -266,12 +402,14 @@ class _AppsList extends StatelessWidget { void _handleUnavailableAppTap(BuildContext context, App app) async { // Check if app is already being installed - if (_AppsList._installingApps[app.id] == true) { + if (_AppsListState._installingApps[app.id] == true) { return; } // Set installing state - _AppsList._installingApps[app.id] = true; + setState(() { + _AppsListState._installingApps[app.id] = true; + }); try { final appProvider = context.read(); @@ -299,33 +437,41 @@ class _AppsList extends StatelessWidget { conversationProvider.trackLastUsedSummarizationApp(app.id); // Close the bottom sheet - Navigator.pop(context); + if (mounted) Navigator.pop(context); // Set the app for reprocessing and reprocess the conversation conversationProvider.setSelectedAppForReprocessing(installedApp); await conversationProvider.reprocessConversation(appId: app.id); } else { // Installation failed + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Failed to install ${app.name}. Please try again.'), + duration: const Duration(seconds: 3), + backgroundColor: Colors.red, + ), + ); + } + } + } catch (e) { + // Handle installation error + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('Failed to install ${app.name}. Please try again.'), + content: Text('Error installing ${app.name}: ${e.toString()}'), duration: const Duration(seconds: 3), backgroundColor: Colors.red, ), ); } - } catch (e) { - // Handle installation error - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error installing ${app.name}: ${e.toString()}'), - duration: const Duration(seconds: 3), - backgroundColor: Colors.red, - ), - ); } finally { // Clear installing state - _AppsList._installingApps[app.id] = false; + if (mounted) { + setState(() { + _AppsListState._installingApps[app.id] = false; + }); + } } } }