Merge branch 'notifications-enhancement' into 'main'

Notifications enhancement

See merge request mysocialportal/relatica!19
This commit is contained in:
HankG 2023-02-08 16:14:08 +00:00
commit 04fa5395d3
11 changed files with 127 additions and 27 deletions

View file

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

View file

@ -24,15 +24,18 @@ class ObjectBoxConnectionsRepo implements IConnectionsRepo {
@override
bool addAllConnections(Iterable<Connection> 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;
}

View file

@ -232,6 +232,23 @@ class FriendicaClient {
return Result.ok(connection.copy(status: status));
}
FutureResult<PagedResponse<List<Connection>>, 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<List<GroupData>, ExecError> getMemberGroupsForConnection(
String connectionId) async {
@ -578,20 +595,25 @@ class FriendicaClient {
FutureResult<List<DirectMessage>, 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<DirectMessage, ExecError> markDirectMessageRead(
DirectMessage message) async {
_networkStatusService.startDirectMessageUpdateStatus();
final id = message.id;
final url = Uri.parse(
'https://$serverName/api/friendica/direct_messages_setseen?id=$id');
@ -599,7 +621,7 @@ class FriendicaClient {
await _postUrl(url, {}).andThenSuccessAsync((jsonString) async {
return message.copy(seen: true);
});
_networkStatusService.finishDirectMessageUpdateStatus();
return result.execErrorCast();
}
@ -608,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,
@ -626,6 +649,7 @@ class FriendicaClient {
DirectMessageFriendicaExtension.fromJson(jsonDecode(jsonString)));
});
_networkStatusService.finishDirectMessageUpdateStatus();
return result.execErrorCast();
}

View file

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

View file

@ -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<DirectMessageService>();
final nss = getIt<NetworkStatusService>();
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,

View file

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

View file

@ -5,7 +5,7 @@ import '../../services/auth_service.dart';
extension ConnectionMastodonExtensions on Connection {
static Connection fromJson(Map<String, dynamic> 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();

View file

@ -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');
},
);
}

View file

@ -14,11 +14,15 @@ class DirectMessageService extends ChangeNotifier {
static final _logger = Logger('$DirectMessageService');
final _threads = <String, DirectMessageThread>{};
List<DirectMessageThread> get threads {
List<DirectMessageThread> getThreads({bool unreadyOnly = false}) {
if (_threads.isEmpty) {
updateThreads();
}
if (unreadyOnly) {
return _threads.values.where((t) => !t.allSeen).toList();
}
return _threads.values.toList();
}

View file

@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
class NetworkStatusService {
final connectionUpdateStatus = ValueNotifier<bool>(false);
final directMessageUpdateStatus = ValueNotifier<bool>(false);
final notificationsUpdateStatus = ValueNotifier<bool>(false);
final interactionsLoadingStatus = ValueNotifier<bool>(false);
final timelineLoadingStatus = ValueNotifier<bool>(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;
}

View file

@ -1,11 +1,14 @@
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';
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 +53,16 @@ class NotificationsManager extends ChangeNotifier {
_notifications[n.id] = n;
}
_notifications.removeWhere(
(key, value) => value.type == NotificationType.direct_message,
);
getIt<NetworkStatusService>().startNotificationUpdate();
await getIt<DirectMessageService>().updateThreads();
getIt<NetworkStatusService>().finishNotificationUpdate();
for (final n in buildUnreadMessageNotifications()) {
_notifications[n.id] = n;
}
notifyListeners();
return Result.ok(notifications);
}
@ -88,4 +101,27 @@ class NotificationsManager extends ChangeNotifier {
notifyListeners();
return updateNotifications();
}
List<UserNotification> buildUnreadMessageNotifications() {
final myId = getIt<AuthService>().currentId;
final result =
getIt<DirectMessageService>().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;
}
}