diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000..11f355c Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000..f76a3d8 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000..0e92bc5 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000..a8bbb9a Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 0000000..b5067cf Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/assets/images/logo-white.png b/assets/images/logo-white.png new file mode 100644 index 0000000..8d04f2b Binary files /dev/null and b/assets/images/logo-white.png differ diff --git a/lib/apis/user_api.dart b/lib/apis/user_api.dart index 1bd78d8..8dec605 100644 --- a/lib/apis/user_api.dart +++ b/lib/apis/user_api.dart @@ -12,6 +12,13 @@ class UserApi { final data = jsonDecode(response.body) as List; return data.map((e) => User.fromMap(e)); } + + static Future getUser(ApiClient client, {required String userId}) async { + final response = await client.get("/users/$userId/"); + ApiClient.checkResponse(response); + final data = jsonDecode(response.body); + return User.fromMap(data); + } static Future addUserAsFriend(ApiClient client, {required User user}) async { final friend = Friend( diff --git a/lib/auxiliary.dart b/lib/auxiliary.dart index 7955fe5..03e5c2e 100644 --- a/lib/auxiliary.dart +++ b/lib/auxiliary.dart @@ -66,11 +66,13 @@ extension Unique on List { } } -extension StripHTLM on String { +extension Strip on String { String stripHtml() { final document = htmlparser.parse(this); return htmlparser.parse(document.body?.text).documentElement?.text ?? ""; } + + String stripUid() => startsWith("U-") ? substring(2) : this; } extension Format on Duration { diff --git a/lib/clients/api_client.dart b/lib/clients/api_client.dart index be646b9..e41ddd4 100644 --- a/lib/clients/api_client.dart +++ b/lib/clients/api_client.dart @@ -150,6 +150,7 @@ class ClientHolder extends InheritedWidget { final ApiClient apiClient; final SettingsClient settingsClient; late final MessagingClient messagingClient; + final NotificationClient notificationClient = NotificationClient(); ClientHolder({ super.key, @@ -157,7 +158,7 @@ class ClientHolder extends InheritedWidget { required this.settingsClient, required super.child }) : apiClient = ApiClient(authenticationData: authenticationData) { - messagingClient = MessagingClient(apiClient: apiClient); + messagingClient = MessagingClient(apiClient: apiClient, notificationClient: notificationClient); } static ClientHolder? maybeOf(BuildContext context) { diff --git a/lib/clients/messaging_client.dart b/lib/clients/messaging_client.dart index 23db9f5..d7ba827 100644 --- a/lib/clients/messaging_client.dart +++ b/lib/clients/messaging_client.dart @@ -1,9 +1,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:contacts_plus_plus/apis/message_api.dart'; +import 'package:contacts_plus_plus/auxiliary.dart'; import 'package:contacts_plus_plus/models/authentication_data.dart'; import 'package:contacts_plus_plus/models/friend.dart'; +import 'package:contacts_plus_plus/models/session.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln; import 'package:http/http.dart' as http; +import 'package:collection/collection.dart'; import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/config.dart'; @@ -19,7 +23,7 @@ enum EventType { enum EventTarget { unknown, messageSent, - messageReceived, + receiveMessage, messagesRead; factory EventTarget.parse(String? text) { @@ -41,11 +45,12 @@ class MessagingClient { final Map _updateListeners = {}; final Logger _logger = Logger("NeosHub"); final Workmanager _workmanager = Workmanager(); + final NotificationClient _notificationClient; WebSocket? _wsChannel; bool _isConnecting = false; - MessagingClient({required ApiClient apiClient}) - : _apiClient = apiClient { + MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient}) + : _apiClient = apiClient, _notificationClient = notificationClient { start(); } @@ -183,11 +188,14 @@ class MessagingClient { cache.addMessage(message); notifyListener(message.recipientId); break; - case EventTarget.messageReceived: + case EventTarget.receiveMessage: final msg = args[0]; final message = Message.fromMap(msg); final cache = await getMessageCache(message.senderId); cache.addMessage(message); + if (!_updateListeners.containsKey(message.senderId)) { + _notificationClient.showUnreadMessagesNotification([message]); + } notifyListener(message.senderId); break; case EventTarget.messagesRead: @@ -229,3 +237,90 @@ class MessagingClient { _sendData(data); } } + +class NotificationChannel { + final String id; + final String name; + final String description; + + const NotificationChannel({required this.name, required this.id, required this.description}); +} + +class NotificationClient { + static const NotificationChannel _messageChannel = NotificationChannel( + id: "messages", + name: "Messages", + description: "Messages received from your friends", + ); + + final fln.FlutterLocalNotificationsPlugin _notifier = fln.FlutterLocalNotificationsPlugin() + ..initialize( + const fln.InitializationSettings( + android: fln.AndroidInitializationSettings("ic_notification"), + ) + ); + + Future showUnreadMessagesNotification(List messages) async { + if (messages.isEmpty) return; + + final bySender = groupBy(messages, (p0) => p0.senderId); + + for (final entry in bySender.entries) { + + final uname = entry.key.stripUid(); + await _notifier.show( + uname.hashCode, + null, + null, + fln.NotificationDetails(android: fln.AndroidNotificationDetails( + _messageChannel.id, + _messageChannel.name, + channelDescription: _messageChannel.description, + importance: fln.Importance.high, + priority: fln.Priority.max, + styleInformation: fln.MessagingStyleInformation( + fln.Person( + name: uname, + bot: false, + ), + groupConversation: false, + messages: entry.value.map((message) { + String content; + switch (message.type) { + case MessageType.unknown: + content = "Unknown Message Type"; + break; + case MessageType.text: + content = message.content; + break; + case MessageType.sound: + content = "Audio Message"; + break; + case MessageType.sessionInvite: + try { + final session = Session.fromMap(jsonDecode(message.content)); + content = "Session Invite to ${session.name}"; + } catch (e) { + content = "Session Invite"; + } + break; + case MessageType.object: + content = "Asset"; + break; + } + return fln.Message( + content, + message.sendTime, + fln.Person( + name: uname, + bot: false, + ), + ); + }).toList(), + ), + ), + ), + ); + } + } +} \ No newline at end of file diff --git a/lib/widgets/friends_list.dart b/lib/widgets/friends_list.dart index 725edab..ab47e10 100644 --- a/lib/widgets/friends_list.dart +++ b/lib/widgets/friends_list.dart @@ -60,7 +60,6 @@ class _FriendsListState extends State { _friendsFuture = FriendApi.getFriendsList(_clientHolder!.apiClient).then((Iterable value) async { final unreadMessages = await MessageApi.getUserMessages(_clientHolder!.apiClient, unreadOnly: true); _unreads.clear(); - for (final msg in unreadMessages) { if (msg.senderId != _clientHolder!.apiClient.userId) { final value = _unreads[msg.senderId]; @@ -128,7 +127,7 @@ class _FriendsListState extends State { } else { _autoRefresh = Timer(_autoRefreshDuration, () => setState(() => _refreshFriendsList())); } - }) + }), ].map((item) => PopupMenuItem( value: item, diff --git a/lib/widgets/messages_list.dart b/lib/widgets/messages_list.dart index baa501f..8338278 100644 --- a/lib/widgets/messages_list.dart +++ b/lib/widgets/messages_list.dart @@ -49,8 +49,15 @@ class _MessagesListState extends State { _messageCacheFutureComplete = false; _messageCacheFuture = _clientHolder?.messagingClient.getMessageCache(widget.friend.id) .whenComplete(() => _messageCacheFutureComplete = true); - _clientHolder?.messagingClient.registerListener( - widget.friend.id, () => setState(() {})); + final mClient = _clientHolder?.messagingClient; + final id = widget.friend.id; + mClient?.registerListener(id, () { + if (context.mounted) { + setState(() {}); + } else { + mClient.unregisterListener(id); + } + }); } @override diff --git a/pubspec.lock b/pubspec.lock index dac875f..fbeecd8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -74,7 +74,7 @@ packages: source: hosted version: "1.1.1" collection: - dependency: transitive + dependency: "direct main" description: name: collection sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 diff --git a/pubspec.yaml b/pubspec.yaml index c258a6a..3650ada 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: url_launcher: ^6.1.10 workmanager: ^0.5.1 flutter_local_notifications: ^14.0.0+1 + collection: any dev_dependencies: flutter_test: