diff --git a/android/app/build.gradle b/android/app/build.gradle index 531029b..a191ee4 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -27,10 +27,13 @@ if (flutterVersionName == null) { android { namespace "com.example.cofounderella" - compileSdk flutter.compileSdkVersion + compileSdk 34 // 34 required by flutter_local_notification; old value: flutter.compileSdkVersion ndkVersion "26.1.10909125" compileOptions { + // Flag to enable support for the new language APIs + coreLibraryDesugaringEnabled true + // Sets Java compatibility to Java 8 sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -67,4 +70,10 @@ flutter { source '../..' } -dependencies {} +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.3' + // There have been reports that enabling desugaring may result in a Flutter apps crashing on + // Android 12L and above. https://pub.dev/packages/flutter_local_notifications#-android-setup + implementation 'androidx.window:window:1.0.0' + implementation 'androidx.window:window-java:1.0.0' +} diff --git a/lib/main.dart b/lib/main.dart index 1ba9e81..5098ce9 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../constants.dart'; import '../services/auth/auth_gate.dart'; @@ -10,6 +11,7 @@ import 'pages/conversations_page.dart'; import 'pages/liked_users_page.dart'; import 'pages/user_matching_page.dart'; import 'pages/user_profile_page.dart'; +import 'services/swipe_stream_service.dart'; void main() async { // Firebase stuff @@ -22,14 +24,35 @@ void main() async { final prefs = await SharedPreferences.getInstance(); final isDarkMode = prefs.getBool(Constants.prefKeyThemeDarkMode) ?? false; + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + const InitializationSettings initializationSettings = + InitializationSettings(android: initializationSettingsAndroid); + await flutterLocalNotificationsPlugin.initialize(initializationSettings); + + // Initialize the singleton instance with a dummy userId. + // The actual userId is set later in the HomePage. + SwipeStreamService() + .initialize('dummy_initial_user_id', flutterLocalNotificationsPlugin); + runApp( - ChangeNotifierProvider( - create: (context) => ThemeProvider(initialIsDarkMode: isDarkMode), + MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (context) => ThemeProvider(initialIsDarkMode: isDarkMode)), + Provider.value( + value: flutterLocalNotificationsPlugin), + ], child: const MyApp(), ), ); } +// Global key for navigation +final GlobalKey navigatorKey = GlobalKey(); + class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -39,6 +62,7 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, title: Constants.appTitle, theme: Provider.of(context).themeData, + navigatorKey: navigatorKey, home: const AuthGate(), routes: { '/discover': (context) => const UserMatchingPage(), diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 761dba1..2dfdafb 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,11 +1,23 @@ import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:provider/provider.dart'; import '../components/my_drawer.dart'; +import '../services/auth/auth_service.dart'; +import '../services/swipe_stream_service.dart'; class HomePage extends StatelessWidget { const HomePage({super.key}); @override Widget build(BuildContext context) { + final currentUserId = AuthService().getCurrentUser()!.uid; + final flutterLocalNotificationsPlugin = + Provider.of(context); + final swipeStreamService = SwipeStreamService(); + swipeStreamService.initialize( + currentUserId, flutterLocalNotificationsPlugin); + swipeStreamService.listenToSwipes(); + return Scaffold( appBar: AppBar( title: const Text('Home'), diff --git a/lib/pages/user_matching_page.dart b/lib/pages/user_matching_page.dart index 3e67fe7..d904d04 100644 --- a/lib/pages/user_matching_page.dart +++ b/lib/pages/user_matching_page.dart @@ -12,6 +12,7 @@ import '../constants.dart'; import '../forms/matched_screen.dart'; import '../models/user_profile.dart'; import '../services/auth/auth_service.dart'; +import '../services/swipe_stream_service.dart'; import '../services/user_service.dart'; import '../utils/helper.dart'; import '../utils/list_utils.dart'; @@ -173,6 +174,9 @@ class UserMatchingPageState extends State { .doc(swipedUserId) .collection(Constants.dbCollectionMatches); + // current user swiped, so avoid local push notification + SwipeStreamService().addUser(swipedUserId); + await matchesCurrentUser.doc(swipedUserId).set({ 'otherUserId': swipedUserId, 'timestamp': FieldValue.serverTimestamp(), @@ -183,9 +187,6 @@ class UserMatchingPageState extends State { 'timestamp': FieldValue.serverTimestamp(), }); - // - // TODO Notify other user? - // showMatchedScreen(currentUserId, swipedUserId); // Remove matched user from the list of potential users @@ -237,9 +238,9 @@ class UserMatchingPageState extends State { } else if (percentage >= 40) { return Colors.amber.shade200; // 54 - 40 } else if (percentage >= 20) { - return Colors.orange.shade200; // 39 - 20 + return Colors.orange; // 39 - 20 } else { - return Colors.orange.shade300; // 19 - 0 + return Colors.red.shade400; // 19 - 0 } } diff --git a/lib/services/auth/auth_service.dart b/lib/services/auth/auth_service.dart index 7992ca6..89d49a4 100644 --- a/lib/services/auth/auth_service.dart +++ b/lib/services/auth/auth_service.dart @@ -1,5 +1,7 @@ import 'package:firebase_auth/firebase_auth.dart'; +import '../swipe_stream_service.dart'; + class AuthService { // instance of auth final FirebaseAuth _auth = FirebaseAuth.instance; @@ -45,6 +47,7 @@ class AuthService { // sign out Future signOut() async { + SwipeStreamService().stopListening(); await _auth.signOut(); } } diff --git a/lib/services/swipe_stream_service.dart b/lib/services/swipe_stream_service.dart new file mode 100644 index 0000000..ca0b1be --- /dev/null +++ b/lib/services/swipe_stream_service.dart @@ -0,0 +1,122 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart' show debugPrint, kDebugMode; +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import '../constants.dart'; +import '../main.dart'; +import '../utils/helper_dialogs.dart'; +import 'user_service.dart'; + +class SwipeStreamService { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final List _matchesInDB = []; + + StreamSubscription>? _subscription; + + late String userId; + late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin; + late bool matchesRead = false; + + // Singleton instance + static final SwipeStreamService _instance = SwipeStreamService._internal(); + + // Private constructor + SwipeStreamService._internal(); + + // Factory constructor + factory SwipeStreamService() { + return _instance; + } + + // Initialization method + void initialize(String userId, FlutterLocalNotificationsPlugin plugin) { + _instance.userId = userId; + _instance.flutterLocalNotificationsPlugin = plugin; + // Reset old data + _matchesInDB.clear(); + matchesRead = false; + } + + Stream> get swipesStream { + return _firestore + .collection(Constants.dbCollectionUsers) + .doc(userId) + .collection(Constants.dbCollectionMatches) + .snapshots() + .map((snapshot) => snapshot.docs); + } + + void _showNotification(String swipeId) async { + String matchName = await UserService.getUserName(swipeId); + const AndroidNotificationDetails androidPlatformChannelSpecifics = + AndroidNotificationDetails( + 'my_match_channel_id', 'my_match_channel_name', + channelShowBadge: true, + visibility: NotificationVisibility.private, + importance: Importance.max, + priority: Priority.high, + showWhen: true); + const NotificationDetails platformChannelSpecifics = + NotificationDetails(android: androidPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.show(0, 'New Match', + 'You have a new match with $matchName', platformChannelSpecifics); + } + + void listenToSwipes() { + _subscription = swipesStream.listen( + (swipes) async { + List userNames = []; + String tempUser = ''; + + if (!matchesRead) { + for (var swipe in swipes) { + debugPrint('Init Match Notify --> ${swipe.id}'); + _matchesInDB.add(swipe.id); + tempUser = await UserService.getUserName(swipe.id); + userNames.add('INIT $tempUser'); + } + if (kDebugMode) { + showErrorSnackBar( + navigatorKey.currentContext!, + userNames.join(', '), + ); + } + + matchesRead = true; + } else { + for (var swipe in swipes) { + tempUser = await UserService.getUserName(swipe.id); + + if (!_matchesInDB.contains(swipe.id)) { + userNames.add('NEW! $tempUser'); + debugPrint('NEW Match Notify --> ${swipe.id}'); + _matchesInDB.add(swipe.id); + _showNotification(swipe.id); + } else { + userNames.add('OLD $tempUser'); + debugPrint('Old Match Notify --> ${swipe.id}'); + } + } + if (kDebugMode) { + showErrorSnackBar( + navigatorKey.currentContext!, + userNames.join(', '), + ); + } + } + }, + ); + } + + void stopListening() { + _subscription?.cancel(); + _subscription = null; + } + + void addUser(String userId) { + if (!_matchesInDB.contains(userId)) { + debugPrint('Notify: SKIP Match Notify for --> $userId'); + _matchesInDB.add(userId); + } + } +} diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 753ecd7..d52f856 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -346,4 +346,21 @@ class UserService { throw Exception('Error fetching sectors from Firebase: $e'); } } + + static Future getUserName(String userId) async { + try { + DocumentSnapshot userDoc = await FirebaseFirestore.instance + .collection(Constants.dbCollectionUsers) + .doc(userId) + .get(); + + if (userDoc.exists) { + return userDoc[Constants.dbFieldUsersName] ?? 'Unknown User'; + } else { + return 'User not found'; + } + } catch (e) { + return e.toString(); + } + } } diff --git a/pubspec.lock b/pubspec.lock index 372c186..5a31670 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" expandable_text: dependency: "direct main" description: @@ -294,6 +302,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" + url: "https://pub.dev" + source: hosted + version: "17.1.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" + url: "https://pub.dev" + source: hosted + version: "7.1.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -821,6 +853,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + timezone: + dependency: transitive + description: + name: timezone + sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 + url: "https://pub.dev" + source: hosted + version: "0.9.3" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3437c6c..a843a46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -31,6 +31,7 @@ dependencies: shared_preferences: ^2.2.3 flutter_launcher_icons: ^0.13.1 url_launcher: ^6.3.0 + flutter_local_notifications: ^17.1.2 dev_dependencies: flutter_test: