Added Local Notification for Matches (Android Only)

master
Rafael 2024-07-12 13:14:09 +02:00
parent 7b82c1cbb8
commit 9ddfb5cfd5
9 changed files with 238 additions and 9 deletions

View File

@ -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'
}

View File

@ -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<FlutterLocalNotificationsPlugin>.value(
value: flutterLocalNotificationsPlugin),
],
child: const MyApp(),
),
);
}
// Global key for navigation
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
class MyApp extends StatelessWidget {
const MyApp({super.key});
@ -39,6 +62,7 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false,
title: Constants.appTitle,
theme: Provider.of<ThemeProvider>(context).themeData,
navigatorKey: navigatorKey,
home: const AuthGate(),
routes: {
'/discover': (context) => const UserMatchingPage(),

View File

@ -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<FlutterLocalNotificationsPlugin>(context);
final swipeStreamService = SwipeStreamService();
swipeStreamService.initialize(
currentUserId, flutterLocalNotificationsPlugin);
swipeStreamService.listenToSwipes();
return Scaffold(
appBar: AppBar(
title: const Text('Home'),

View File

@ -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<UserMatchingPage> {
.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<UserMatchingPage> {
'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<UserMatchingPage> {
} 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
}
}

View File

@ -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<void> signOut() async {
SwipeStreamService().stopListening();
await _auth.signOut();
}
}

View File

@ -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<String> _matchesInDB = [];
StreamSubscription<List<DocumentSnapshot>>? _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<List<DocumentSnapshot>> 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<String> 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);
}
}
}

View File

@ -346,4 +346,21 @@ class UserService {
throw Exception('Error fetching sectors from Firebase: $e');
}
}
static Future<String> 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();
}
}
}

View File

@ -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:

View File

@ -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: