From fb26379201ab0d6752c98330e7a338b4179c6b8e Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 8 Feb 2025 23:07:36 +0100 Subject: [PATCH] local push notification --- README.md | 7 +- lib/src/app.dart | 63 ++++++++------ lib/src/model/contacts_model.dart | 65 ++++++++++---- lib/src/providers/api/server_messages.dart | 5 ++ lib/src/providers/api_provider.dart | 19 ++-- lib/src/services/notification_service.dart | 87 +++++++++++++++++++ .../views/settings/settings_main_view.dart | 23 +---- 7 files changed, 196 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 9a90d1b..f58f345 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,10 @@ Don't be lonely, get twonly! Send pictures to a friend in real time and be sure ## TODOS bevor first beta - Send a picture first to only one person -> Kamera button - Context Menu - -- Pro Invitation codes -- Push Notification (Android) +- Invitation codes - Settings - - Notification - Real deployment aufsetzen, direkt auf Netcup? +- Firebase Push Notification - MediaView: - Bei weiteren geladenen Bildern -> Direkt anzeigen ohne pop @@ -21,6 +19,7 @@ Don't be lonely, get twonly! Send pictures to a friend in real time and be sure ## TODOS bevor first release - Settings - Subscription + - Notification - Webpage - Instagam & Marketing vorbereiten - IT-Startup der TU Darmstadt anschreiben diff --git a/lib/src/app.dart b/lib/src/app.dart index b6672fc..0b29866 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -33,7 +33,7 @@ class MyApp extends StatefulWidget { State createState() => _MyAppState(); } -class _MyAppState extends State { +class _MyAppState extends State with WidgetsBindingObserver { Future _isUserCreated = isUserCreated(); bool _showOnboarding = true; bool _isConnected = false; @@ -44,6 +44,7 @@ class _MyAppState extends State { @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); _startColorAnimation(); // init change provider to load data from the database @@ -73,8 +74,18 @@ class _MyAppState extends State { apiProvider.connect(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + print("STATE: $state"); + if (state == AppLifecycleState.resumed) { + apiProvider.connect(); + } + } + @override void dispose() { + WidgetsBinding.instance.removeObserver(this); // disable globalCallbacks to the flutter tree globalCallbackConnectionState = (a) {}; globalCallBackOnDownloadChange = (a, b) {}; @@ -143,29 +154,30 @@ class _MyAppState extends State { home: Stack( children: [ FutureBuilder( - future: _isUserCreated, - builder: (context, snapshot) { - if (snapshot.hasData) { - return snapshot.data! - ? HomeView() - : _showOnboarding - ? OnboardingView( - callbackOnSuccess: () { - setState(() { - _showOnboarding = false; - }); - }, - ) - : RegisterView( - callbackOnSuccess: () { - _isUserCreated = isUserCreated(); - setState(() {}); - }, - ); - } else { - return Container(); - } - }), + future: _isUserCreated, + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data! + ? HomeView() + : _showOnboarding + ? OnboardingView( + callbackOnSuccess: () { + setState(() { + _showOnboarding = false; + }); + }, + ) + : RegisterView( + callbackOnSuccess: () { + _isUserCreated = isUserCreated(); + setState(() {}); + }, + ); + } else { + return Container(); + } + }, + ), if (!_isConnected) Positioned( top: 3, // Position it at the top @@ -178,7 +190,8 @@ class _MyAppState extends State { color: Colors.red[600]!.withAlpha(redColorOpacity), width: 2.0), // Red border borderRadius: BorderRadius.all( - Radius.circular(10.0)), // Rounded top corners + Radius.circular(10.0), + ), // Rounded top corners ), ), ), diff --git a/lib/src/model/contacts_model.dart b/lib/src/model/contacts_model.dart index fa80cb0..5807f89 100644 --- a/lib/src/model/contacts_model.dart +++ b/lib/src/model/contacts_model.dart @@ -117,6 +117,53 @@ class DbContacts extends CvModelBase { } } + static List _parseContacts(List users) { + List parsedUsers = []; + for (int i = 0; i < users.length; i++) { + try { + int userId = users.cast()[i][columnUserId]; + parsedUsers.add( + Contact( + userId: Int64(userId), + totalMediaCounter: users.cast()[i][columnTotalMediaCounter], + displayName: users.cast()[i][columnDisplayName], + accepted: users[i][columnAccepted] == 1, + blocked: users[i][columnBlocked] == 1, + verified: users[i][columnVerified] == 1, + requested: users[i][columnRequested] == 1, + ), + ); + } catch (e) { + Logger("contacts_model/parse_single_user").shout("$e"); + return []; + } + } + return parsedUsers; + } + + static Future getUserById(int userId) async { + try { + var user = await dbProvider.db!.query(tableName, + columns: [ + columnUserId, + columnDisplayName, + columnAccepted, + columnRequested, + columnBlocked, + columnVerified, + columnTotalMediaCounter, + columnCreatedAt + ], + where: "$columnUserId = ?", + whereArgs: [userId]); + if (user.isEmpty) return null; + return _parseContacts(user)[0]; + } catch (e) { + Logger("contacts_model/getUserById").shout("$e"); + return null; + } + } + static Future> _getAllUsers() async { try { var users = await dbProvider.db!.query(tableName, columns: [ @@ -130,23 +177,7 @@ class DbContacts extends CvModelBase { columnCreatedAt ]); if (users.isEmpty) return []; - - List parsedUsers = []; - for (int i = 0; i < users.length; i++) { - int userId = users.cast()[i][columnUserId]; - parsedUsers.add( - Contact( - userId: Int64(userId), - totalMediaCounter: users.cast()[i][columnTotalMediaCounter], - displayName: users.cast()[i][columnDisplayName], - accepted: users[i][columnAccepted] == 1, - blocked: users[i][columnBlocked] == 1, - verified: users[i][columnVerified] == 1, - requested: users[i][columnRequested] == 1, - ), - ); - } - return parsedUsers; + return _parseContacts(users); } catch (e) { Logger("contacts_model/getUsers").shout("$e"); return []; diff --git a/lib/src/providers/api/server_messages.dart b/lib/src/providers/api/server_messages.dart index 40c8258..4b4441a 100644 --- a/lib/src/providers/api/server_messages.dart +++ b/lib/src/providers/api/server_messages.dart @@ -17,6 +17,7 @@ import 'package:twonly/src/proto/api/server_to_client.pb.dart' as server; import 'package:twonly/src/proto/api/server_to_client.pbserver.dart'; import 'package:twonly/src/providers/api/api.dart'; import 'package:twonly/src/providers/api/api_utils.dart'; +import 'package:twonly/src/services/notification_service.dart'; // ignore: library_prefixes import 'package:twonly/src/utils/signal.dart' as SignalHelper; @@ -103,6 +104,7 @@ Future handleNewMessage( Uint8List name = username.value.userdata.username; DbContacts.insertNewContact( utf8.decode(name), fromUserId.toInt(), true); + localPushNotificationNewMessage(fromUserId.toInt(), message, 999999); } break; case MessageKind.opened: @@ -117,6 +119,7 @@ Future handleNewMessage( break; case MessageKind.acceptRequest: DbContacts.acceptUser(fromUserId.toInt()); + localPushNotificationNewMessage(fromUserId.toInt(), message, 8888888); break; case MessageKind.ack: DbMessages.acknowledgeMessageByUser( @@ -164,6 +167,8 @@ Future handleNewMessage( tryDownloadMedia(downloadToken); } } + localPushNotificationNewMessage( + fromUserId.toInt(), message, messageId); } } } diff --git a/lib/src/providers/api_provider.dart b/lib/src/providers/api_provider.dart index c65f448..317a31a 100644 --- a/lib/src/providers/api_provider.dart +++ b/lib/src/providers/api_provider.dart @@ -26,6 +26,7 @@ import 'package:web_socket_channel/web_socket_channel.dart'; class ApiProvider { final String apiUrl; final String? backupApiUrl; + bool isAuthenticated = false; ApiProvider({required this.apiUrl, required this.backupApiUrl}); final log = Logger("ApiProvider"); @@ -72,13 +73,13 @@ class ApiProvider { log.info("Trying to connect to the backend $apiUrl!"); if (await _connectTo(apiUrl)) { - onConnected(); + await onConnected(); return true; } if (backupApiUrl != null) { log.info("Trying to connect to the backup backend $backupApiUrl!"); if (await _connectTo(backupApiUrl!)) { - onConnected(); + await onConnected(); return true; } } @@ -90,12 +91,14 @@ class ApiProvider { void _onDone() { globalCallbackConnectionState(false); _channel = null; + isAuthenticated = false; tryToReconnect(); } void _onError(dynamic e) { globalCallbackConnectionState(false); _channel = null; + isAuthenticated = false; tryToReconnect(); } @@ -155,12 +158,16 @@ class ApiProvider { _channel!.sink.add(response.writeToBuffer()); } - Future _sendRequestV0(ClientToServer request) async { + Future _sendRequestV0(ClientToServer request, + {bool authenticated = true}) async { if (_channel == null) { if (!await connect()) { return Result.error(ErrorCode.InternalError); } } + if (authenticated) { + await authenticate(); + } var seq = Int64(Random().nextInt(4294967296)); while (messagesV0.containsKey(seq)) { seq = Int64(Random().nextInt(4294967296)); @@ -175,6 +182,7 @@ class ApiProvider { } Future authenticate() async { + if (isAuthenticated) return; if (await SignalHelper.getSignalIdentity() == null) { return; } @@ -182,7 +190,7 @@ class ApiProvider { var handshake = Handshake()..getchallenge = Handshake_GetChallenge(); var req = createClientToServerFromHandshake(handshake); - final result = await _sendRequestV0(req); + final result = await _sendRequestV0(req, authenticated: false); if (result.isError) { log.shout("Error auth", result); return; @@ -206,13 +214,14 @@ class ApiProvider { var req2 = createClientToServerFromHandshake(opensession); - final result2 = await _sendRequestV0(req2); + final result2 = await _sendRequestV0(req2, authenticated: false); if (result2.isError) { log.shout("send request failed: ${result2.error}"); return; } log.info("Authenticated!"); + isAuthenticated = true; } Future register(String username, String? inviteCode) async { diff --git a/lib/src/services/notification_service.dart b/lib/src/services/notification_service.dart index bb168e5..9ad4103 100644 --- a/lib/src/services/notification_service.dart +++ b/lib/src/services/notification_service.dart @@ -1,7 +1,11 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:logging/logging.dart'; +import 'package:twonly/src/model/contacts_model.dart'; +import 'package:twonly/src/model/json/message.dart' as my; /// Streams are created so that app can respond to notification-related events /// since the plugin is initialized in the `main` function @@ -130,3 +134,86 @@ Future setupPushNotification() async { onDidReceiveBackgroundNotificationResponse: notificationTapBackground, ); } + +String getPushNotificationText(String key, String userName) { + String systemLanguage = Platform.localeName; + + Map pushNotificationText; + + if (systemLanguage.contains("de")) { + pushNotificationText = { + "newTextMessage": "%userName% hat die eine Nachricht gesendet.", + "newTwonly": "%userName% hat dir einen twonly gesendet.", + "newVideo": "%userName% hat die eine Video gesendet.", + "newImage": "%userName% hat die eine Bild gesendet.", + "contactRequest": "%userName% möchte sich mir dir vernetzen.", + "acceptRequest": "%userName% ist jetzt mit dir vernetzt.", + }; + } else { + pushNotificationText = { + "newTextMessage": "%userName% has sent you a message.", + "newTwonly": "%userName% has sent you a twonly.", + "newVideo": "%userName% has sent you a video.", + "newImage": "%userName% has sent you an image.", + "contactRequest": "%userName% wants to connect with you.", + "acceptRequest": "%userName% is now connected with you.", + }; + } + + // Replace %userName% with the actual user name + return pushNotificationText[key]?.replaceAll("%userName%", userName) ?? ""; +} + +Future localPushNotificationNewMessage( + int fromUserId, my.Message message, int messageId) async { + Contact? user = await DbContacts.getUserById(fromUserId); + if (user == null) return; + + String msg = ""; + + final content = message.content; + + if (content is my.TextMessageContent) { + msg = getPushNotificationText("newTextMessage", user.displayName); + } else if (content is my.MediaMessageContent) { + if (content.isRealTwonly) { + msg = getPushNotificationText("newTwonly", user.displayName); + } else if (content.isVideo) { + msg = getPushNotificationText("newVideo", user.displayName); + } else { + msg = getPushNotificationText("newImage", user.displayName); + } + } + + if (message.kind == my.MessageKind.contactRequest) { + msg = getPushNotificationText("contactRequest", user.displayName); + } + + if (message.kind == my.MessageKind.acceptRequest) { + msg = getPushNotificationText("acceptRequest", user.displayName); + } + + if (msg == "") { + Logger("localPushNotificationNewMessage") + .shout("No push notification type defined!"); + } + + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails( + '0', + 'Messages', + channelDescription: 'Messages from other users.', + importance: Importance.max, + priority: Priority.max, + ticker: 'You got a new message.', + ); + const NotificationDetails notificationDetails = + NotificationDetails(android: androidNotificationDetails); + await flutterLocalNotificationsPlugin.show( + messageId, + user.displayName, + msg, + notificationDetails, + // payload: 'test', + ); +} diff --git a/lib/src/views/settings/settings_main_view.dart b/lib/src/views/settings/settings_main_view.dart index 803b586..025aa0a 100644 --- a/lib/src/views/settings/settings_main_view.dart +++ b/lib/src/views/settings/settings_main_view.dart @@ -1,10 +1,8 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/src/components/better_list_title.dart'; import 'package:twonly/src/components/initialsavatar.dart'; import 'package:twonly/src/model/json/user_data.dart'; import 'package:flutter/material.dart'; -import 'package:twonly/src/services/notification_service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/account_view.dart'; @@ -119,26 +117,7 @@ class _ProfileViewState extends State { BetterListTile( icon: FontAwesomeIcons.bell, text: context.lang.settingsNotification, - onTap: () async { - const AndroidNotificationDetails - androidNotificationDetails = AndroidNotificationDetails( - '0', - 'Messages', - channelDescription: 'Messages from other users.', - importance: Importance.max, - priority: Priority.max, - ticker: 'You got a new message.', - ); - const NotificationDetails notificationDetails = - NotificationDetails( - android: androidNotificationDetails); - await flutterLocalNotificationsPlugin.show( - 0, - 'New message from x', - 'You got a new message from XX', - notificationDetails, - payload: 'test'); - }, + onTap: () async {}, ), const Divider(), BetterListTile(