From d4c55e79e940464f39004892a3cf72cf675dba2a Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 31 Jan 2023 16:40:47 -0500 Subject: [PATCH 1/7] Report error data on failed connection responses. --- lib/services/connections_manager.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/services/connections_manager.dart b/lib/services/connections_manager.dart index 6c1344e..b05b77a 100644 --- a/lib/services/connections_manager.dart +++ b/lib/services/connections_manager.dart @@ -54,7 +54,7 @@ class ConnectionsManager extends ChangeNotifier { notifyListeners(); }, onError: (error) { - _logger.severe('Error following ${connection.name}'); + _logger.severe('Error following ${connection.name}: $error'); }, ); } @@ -73,7 +73,7 @@ class ConnectionsManager extends ChangeNotifier { notifyListeners(); }, onError: (error) { - _logger.severe('Error following ${connection.name}'); + _logger.severe('Error following ${connection.name}: $error'); }, ); } From 525b27c73ed5ada76249db520547945543c90d31 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 31 Jan 2023 16:41:13 -0500 Subject: [PATCH 2/7] Initial connection request endpoint addition --- lib/friendica_client/friendica_client.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index 462e8ff..8c04bb2 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -232,6 +232,23 @@ class FriendicaClient { return Result.ok(connection.copy(status: status)); } + FutureResult>, ExecError> + getConnectionRequests(PagingData page) async { + _logger.finest(() => 'Getting connection requests with page data $page'); + _networkStatusService.startConnectionUpdateStatus(); + final baseUrl = 'https://$serverName/api/v1/follow_requests'; + final result1 = await _getApiListRequest( + Uri.parse('$baseUrl&${page.toQueryParameters()}'), + ); + + _networkStatusService.finishConnectionUpdateStatus(); + return result1 + .andThenSuccess((response) => response.map((jsonArray) => jsonArray + .map((json) => ConnectionMastodonExtensions.fromJson(json)) + .toList())) + .execErrorCast(); + } + // TODO Convert groups for connection to using paging for real (if available) FutureResult, ExecError> getMemberGroupsForConnection( String connectionId) async { From 5b85fe27e32aaf66cf008e1feb00db490da0e64e Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 31 Jan 2023 16:41:40 -0500 Subject: [PATCH 3/7] Fix ObjectBox connections DB to properly handle adds which shouldn't replace --- lib/data/objectbox/objectbox_connections_repo.dart | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/data/objectbox/objectbox_connections_repo.dart b/lib/data/objectbox/objectbox_connections_repo.dart index 9f69f74..059d060 100644 --- a/lib/data/objectbox/objectbox_connections_repo.dart +++ b/lib/data/objectbox/objectbox_connections_repo.dart @@ -24,15 +24,18 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo { @override bool addAllConnections(Iterable newConnections) { - memCache.addAllConnections(newConnections); - final result = box.putMany(newConnections.toList()); - return result.length == newConnections.length; + var allNew = true; + for (final c in newConnections) { + allNew &= addConnection(c); + } + return allNew; } @override bool addConnection(Connection connection) { - memCache.addConnection(connection); - box.putAsync(connection); + if (memCache.addConnection(connection)) { + box.putAsync(connection); + } return true; } From 57f9129faf24206317c67744b749c3dc3b37959e Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Tue, 31 Jan 2023 16:42:31 -0500 Subject: [PATCH 4/7] Ensure Connection ID is always string (sometimes coming across as ints) --- lib/serializers/mastodon/connection_mastodon_extensions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/serializers/mastodon/connection_mastodon_extensions.dart b/lib/serializers/mastodon/connection_mastodon_extensions.dart index 119c8e3..5a58294 100644 --- a/lib/serializers/mastodon/connection_mastodon_extensions.dart +++ b/lib/serializers/mastodon/connection_mastodon_extensions.dart @@ -5,7 +5,7 @@ import '../../services/auth_service.dart'; extension ConnectionMastodonExtensions on Connection { static Connection fromJson(Map json) { final name = json['display_name'] ?? ''; - final id = json['id'] ?? ''; + final id = json['id']?.toString() ?? ''; final profileUrl = Uri.parse(json['url'] ?? ''); const network = 'Unknown'; final avatar = Uri.tryParse(json['avatar_static'] ?? '') ?? Uri(); From 6f9163d5b0d4c20421a7a5f676beb9b754a19ae1 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 8 Feb 2023 16:41:29 +0100 Subject: [PATCH 5/7] Add direct message notifications types/systems. --- lib/controls/notifications_control.dart | 9 +++++- lib/friendica_client/friendica_client.dart | 11 +++++-- lib/models/user_notification.dart | 3 ++ .../message_threads_browser_screen.dart | 15 +++++++-- lib/services/direct_message_service.dart | 6 +++- lib/services/network_status_service.dart | 11 ++++++- lib/services/notifications_manager.dart | 32 +++++++++++++++++++ 7 files changed, 79 insertions(+), 8 deletions(-) diff --git a/lib/controls/notifications_control.dart b/lib/controls/notifications_control.dart index 1374b8c..a1c0c46 100644 --- a/lib/controls/notifications_control.dart +++ b/lib/controls/notifications_control.dart @@ -92,6 +92,12 @@ class NotificationControl extends StatelessWidget { _goToStatus(context); }; break; + case NotificationType.direct_message: + onTap = () => context.pushNamed( + ScreenPaths.thread, + queryParams: {'uri': notification.iid}, + ); + break; } return ListTile( @@ -110,7 +116,8 @@ class NotificationControl extends StatelessWidget { ElapsedDateUtils.epochSecondsToString(notification.timestamp), ), ), - trailing: notification.dismissed + trailing: notification.dismissed || + notification.type == NotificationType.direct_message ? null : IconButton( onPressed: () async { diff --git a/lib/friendica_client/friendica_client.dart b/lib/friendica_client/friendica_client.dart index 8c04bb2..ffa9d3d 100644 --- a/lib/friendica_client/friendica_client.dart +++ b/lib/friendica_client/friendica_client.dart @@ -595,20 +595,25 @@ class FriendicaClient { FutureResult, ExecError> getDirectMessages( PagingData page) async { + _networkStatusService.startDirectMessageUpdateStatus(); final baseUrl = 'https://$serverName/api/direct_messages/all'; final pagingQP = page.toQueryParameters(limitKeyword: 'count'); final url = '$baseUrl?$pagingQP'; final request = Uri.parse(url); _logger.finest(() => 'Getting direct messages with paging data $page'); - return (await _getApiListRequest(request).andThenSuccessAsync( + final result = (await _getApiListRequest(request).andThenSuccessAsync( (response) async => response.data .map((json) => DirectMessageFriendicaExtension.fromJson(json)) .toList())) .execErrorCast(); + + _networkStatusService.finishDirectMessageUpdateStatus(); + return result; } FutureResult markDirectMessageRead( DirectMessage message) async { + _networkStatusService.startDirectMessageUpdateStatus(); final id = message.id; final url = Uri.parse( 'https://$serverName/api/friendica/direct_messages_setseen?id=$id'); @@ -616,7 +621,7 @@ class FriendicaClient { await _postUrl(url, {}).andThenSuccessAsync((jsonString) async { return message.copy(seen: true); }); - + _networkStatusService.finishDirectMessageUpdateStatus(); return result.execErrorCast(); } @@ -625,6 +630,7 @@ class FriendicaClient { String receivingUserId, String text, ) async { + _networkStatusService.startDirectMessageUpdateStatus(); final url = Uri.parse('https://$serverName/api/direct_messages/new'); final body = { 'user_id': receivingUserId, @@ -643,6 +649,7 @@ class FriendicaClient { DirectMessageFriendicaExtension.fromJson(jsonDecode(jsonString))); }); + _networkStatusService.finishDirectMessageUpdateStatus(); return result.execErrorCast(); } diff --git a/lib/models/user_notification.dart b/lib/models/user_notification.dart index 83e3832..1571f02 100644 --- a/lib/models/user_notification.dart +++ b/lib/models/user_notification.dart @@ -10,6 +10,7 @@ enum NotificationType { reshare, reblog, status, + direct_message, unknown; String toVerb() { @@ -27,6 +28,8 @@ enum NotificationType { return 'reshared'; case NotificationType.status: return 'updated'; + case NotificationType.direct_message: + return 'has sent you a new direct message'; case NotificationType.unknown: return 'unknowned'; } diff --git a/lib/screens/message_threads_browser_screen.dart b/lib/screens/message_threads_browser_screen.dart index 0ea8f01..d0ce7d5 100644 --- a/lib/screens/message_threads_browser_screen.dart +++ b/lib/screens/message_threads_browser_screen.dart @@ -4,16 +4,25 @@ import 'package:provider/provider.dart'; import '../controls/image_control.dart'; import '../controls/standard_appbar.dart'; +import '../controls/status_and_refresh_button.dart'; +import '../globals.dart'; import '../routes.dart'; import '../services/direct_message_service.dart'; +import '../services/network_status_service.dart'; import '../utils/dateutils.dart'; class MessagesScreen extends StatelessWidget { @override Widget build(BuildContext context) { final service = context.watch(); + final nss = getIt(); return Scaffold( appBar: StandardAppBar.build(context, 'Direct Message Threads', actions: [ + StatusAndRefreshButton( + valueListenable: nss.directMessageUpdateStatus, + refreshFunction: () async => await service.updateThreads(), + busyColor: Theme.of(context).colorScheme.background, + ), IconButton( onPressed: () { context.push('/messages/new_thread'); @@ -23,7 +32,7 @@ class MessagesScreen extends StatelessWidget { ]), body: RefreshIndicator( onRefresh: () async { - await service.updateThreads(); + service.updateThreads(); }, child: Center(child: buildBody(context, service)), ), @@ -31,7 +40,7 @@ class MessagesScreen extends StatelessWidget { } Widget buildBody(BuildContext context, DirectMessageService service) { - final threads = service.threads; + final threads = service.getThreads(); threads.sort((t1, t2) => t2.messages.last.createdAt.compareTo(t1.messages.last.createdAt)); return threads.isEmpty @@ -42,7 +51,7 @@ class MessagesScreen extends StatelessWidget { final thread = threads[index]; final style = thread.allSeen ? null - : TextStyle(fontWeight: FontWeight.bold); + : const TextStyle(fontWeight: FontWeight.bold); return ListTile( onTap: () => context.pushNamed( ScreenPaths.thread, diff --git a/lib/services/direct_message_service.dart b/lib/services/direct_message_service.dart index b1fd943..68dbe7a 100644 --- a/lib/services/direct_message_service.dart +++ b/lib/services/direct_message_service.dart @@ -14,11 +14,15 @@ class DirectMessageService extends ChangeNotifier { static final _logger = Logger('$DirectMessageService'); final _threads = {}; - List get threads { + List getThreads({bool unreadyOnly = false}) { if (_threads.isEmpty) { updateThreads(); } + if (unreadyOnly) { + return _threads.values.where((t) => !t.allSeen).toList(); + } + return _threads.values.toList(); } diff --git a/lib/services/network_status_service.dart b/lib/services/network_status_service.dart index a4ccf04..c058f05 100644 --- a/lib/services/network_status_service.dart +++ b/lib/services/network_status_service.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; class NetworkStatusService { final connectionUpdateStatus = ValueNotifier(false); + final directMessageUpdateStatus = ValueNotifier(false); final notificationsUpdateStatus = ValueNotifier(false); final interactionsLoadingStatus = ValueNotifier(false); final timelineLoadingStatus = ValueNotifier(false); @@ -14,7 +15,15 @@ class NetworkStatusService { void finishConnectionUpdateStatus() { connectionUpdateStatus.value = false; } - + + void startDirectMessageUpdateStatus() { + directMessageUpdateStatus.value = true; + } + + void finishDirectMessageUpdateStatus() { + directMessageUpdateStatus.value = false; + } + void startNotificationUpdate() { notificationsUpdateStatus.value = true; } diff --git a/lib/services/notifications_manager.dart b/lib/services/notifications_manager.dart index 4d49aa3..dedcaa0 100644 --- a/lib/services/notifications_manager.dart +++ b/lib/services/notifications_manager.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; +import 'package:uuid/uuid.dart'; import '../globals.dart'; import '../models/exec_error.dart'; import '../models/user_notification.dart'; import 'auth_service.dart'; +import 'direct_message_service.dart'; class NotificationsManager extends ChangeNotifier { static final _logger = Logger('NotificationManager'); @@ -50,6 +52,13 @@ class NotificationsManager extends ChangeNotifier { _notifications[n.id] = n; } + _notifications.removeWhere( + (key, value) => value.type == NotificationType.direct_message, + ); + for (final n in buildUnreadMessageNotifications()) { + _notifications[n.id] = n; + } + notifyListeners(); return Result.ok(notifications); } @@ -88,4 +97,27 @@ class NotificationsManager extends ChangeNotifier { notifyListeners(); return updateNotifications(); } + + List buildUnreadMessageNotifications() { + final myId = getIt().currentId; + final result = + getIt().getThreads(unreadyOnly: true).map((t) { + final fromAccount = t.participants.firstWhere((p) => p.id != myId); + final latestMessage = + t.messages.reduce((s, m) => s.createdAt > m.createdAt ? s : m); + return UserNotification( + id: const Uuid().v4(), + type: NotificationType.direct_message, + fromId: fromAccount.id, + fromName: fromAccount.name, + fromUrl: fromAccount.profileUrl, + timestamp: latestMessage.createdAt, + iid: t.parentUri, + dismissed: false, + content: '${fromAccount.name} sent you a direct message', + link: ''); + }).toList(); + + return result; + } } From 8fad5342a7ead7b606468c8c01400a58feb44c2b Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 8 Feb 2023 16:46:26 +0100 Subject: [PATCH 6/7] Have NotificationsManager refresh DMs when updating itself too. --- lib/services/notifications_manager.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/services/notifications_manager.dart b/lib/services/notifications_manager.dart index dedcaa0..df3df91 100644 --- a/lib/services/notifications_manager.dart +++ b/lib/services/notifications_manager.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; +import 'package:relatica/services/network_status_service.dart'; import 'package:result_monad/result_monad.dart'; import 'package:uuid/uuid.dart'; @@ -55,6 +56,9 @@ class NotificationsManager extends ChangeNotifier { _notifications.removeWhere( (key, value) => value.type == NotificationType.direct_message, ); + getIt().startNotificationUpdate(); + await getIt().updateThreads(); + getIt().finishNotificationUpdate(); for (final n in buildUnreadMessageNotifications()) { _notifications[n.id] = n; } From 119bae79539cfbddd7006e1905d16fc44212c377 Mon Sep 17 00:00:00 2001 From: Hank Grabowski Date: Wed, 8 Feb 2023 16:48:57 +0100 Subject: [PATCH 7/7] Add back swipe down to refresh notifications --- lib/screens/notifications_screen.dart | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/screens/notifications_screen.dart b/lib/screens/notifications_screen.dart index 58ec006..eb8effa 100644 --- a/lib/screens/notifications_screen.dart +++ b/lib/screens/notifications_screen.dart @@ -36,17 +36,22 @@ class NotificationsScreen extends StatelessWidget { } else { final unreadCount = notifications.where((e) => !e.dismissed).length; title = 'Notifications ($unreadCount)'; - body = ListView.separated( - itemBuilder: (context, index) { - return NotificationControl(notification: notifications[index]); - }, - separatorBuilder: (context, index) { - return const Divider( - color: Colors.black54, - height: 0.0, - ); - }, - itemCount: notifications.length + 1); + body = RefreshIndicator( + onRefresh: () async { + manager.updateNotifications(); + }, + child: ListView.separated( + itemBuilder: (context, index) { + return NotificationControl(notification: notifications[index]); + }, + separatorBuilder: (context, index) { + return const Divider( + color: Colors.black54, + height: 0.0, + ); + }, + itemCount: notifications.length + 1), + ); } return Scaffold(