relatica/lib/services/notifications_manager.dart

401 wiersze
13 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:result_monad/result_monad.dart';
import '../friendica_client/friendica_client.dart';
import '../friendica_client/paged_response.dart';
import '../friendica_client/pages_manager.dart';
import '../friendica_client/paging_data.dart';
import '../globals.dart';
import '../models/auth/profile.dart';
import '../models/exec_error.dart';
import '../models/user_notification.dart';
import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
import '../utils/active_profile_selector.dart';
import 'auth_service.dart';
import 'direct_message_service.dart';
import 'feature_version_checker.dart';
import 'follow_requests_manager.dart';
import 'network_status_service.dart';
class NotificationsManager extends ChangeNotifier {
static const itemsPerQuery = 50;
static const minimumDmsAndCrsUpdateDuration = Duration(seconds: 30);
static final _logger = Logger('NotificationManager');
final Profile profile;
final dms = <UserNotification>[];
final connectionRequests = <UserNotification>[];
final unread = <UserNotification>[];
final read = <UserNotification>[];
var lastDmsUpdate = DateTime(1900);
var lastCrUpdate = DateTime(1900);
NotificationsManager(this.profile);
var _firstLoad = true;
List<UserNotification> get notifications {
if (_firstLoad) {
loadUnreadNotifications(true);
_firstLoad = false;
}
return [...connectionRequests, ...dms, ...unread, ...read];
}
void clear({bool withListenerNotification = true}) {
dms.clear();
connectionRequests.clear();
unread.clear();
read.clear();
_firstLoad = true;
notifyListeners();
}
void refreshNotifications() async {
clear(withListenerNotification: false);
await loadUnreadNotifications(false);
if (unread.isEmpty && read.isEmpty) {
await loadOlderNotifications(withListenerNotification: false);
}
notifyListeners();
}
FutureResult<List<UserNotification>, ExecError> loadUnreadNotifications(
bool withListenerNotification) async {
final notificationsFromRefresh = <UserNotification>[];
final pm = _buildPageManager(profile, false);
final useActualRequests = getIt<FriendicaVersionChecker>()
.canUseFeature(RelaticaFeatures.usingActualFollowRequests);
var hasMore = true;
var first = true;
while (hasMore) {
final result =
first ? await pm.initialize(itemsPerQuery) : await pm.nextFromEnd();
first = false;
result.match(
onSuccess: (nd) => print('Got ${nd.data.length} notifications'),
onError: (e) => debugPrint('Error getting notification: $e'));
final response = result.getValueOrElse(() => PagedResponse([]));
response.data
.where((n) =>
!useActualRequests || n.type != NotificationType.follow_request)
.forEach(notificationsFromRefresh.add);
hasMore = response.next != null;
}
// filter out connection requests if going to use the real service for that when doing the query
// get earliest and latest notification ID from unread notifications
// query all notifications over that in page increments of 25
// query unread notifications in increments of 25 after the latest ID
return await _postFetchOperations(
notificationsFromRefresh,
withListenerNotification,
);
}
FutureResult<List<UserNotification>, ExecError> loadNewerNotifications({
bool withListenerNotification = true,
}) async {
final (_, highestId) =
unread.isNotEmpty ? calcLowHigh(unread) : calcLowHigh(read);
final pm = _buildPageManager(
profile,
true,
initialPages: [
PagedResponse(
<String>[],
next: PagingData(minId: highestId),
)
],
);
final result = await (unread.isEmpty && read.isEmpty
? pm.initialize(itemsPerQuery)
: pm.previousFromBeginning())
.andThenAsync(
(page) async =>
await _postFetchOperations(page.data, withListenerNotification),
)
.withError(
(error) => _logger.info('Error getting more updates: $error'));
return result.execErrorCast();
}
FutureResult<List<UserNotification>, ExecError> loadOlderNotifications(
{bool withListenerNotification = true}) async {
final (lowestId, _) =
read.isNotEmpty ? calcLowHigh(read) : calcLowHigh(unread);
final pm = _buildPageManager(
profile,
true,
initialPages: read.isEmpty
? []
: [
PagedResponse(
<String>[],
next: PagingData(maxId: lowestId),
)
],
);
final notifications = <UserNotification>[];
if (read.isEmpty) {
var hasReadNotification = false;
var hasMorePages = false;
do {
await (notifications.isEmpty
? pm.initialize(itemsPerQuery)
: pm.nextFromEnd())
.match(onSuccess: (r) {
notifications.addAll(r.data);
hasMorePages = r.next != null;
hasReadNotification = r.data.map((e) => e.dismissed).firstWhere(
(t) => t == true,
orElse: () => false,
);
}, onError: (e) {
hasMorePages = false;
print('Error getting older notifications: $e');
});
} while (!hasReadNotification && hasMorePages);
} else {
await pm.nextFromEnd().withResult((r) => notifications.addAll(r.data));
}
return _postFetchOperations(notifications, withListenerNotification);
}
FutureResult<bool, ExecError> markSeen(UserNotification notification) async {
final result = await NotificationsClient(profile)
.clearNotification(notification)
.withResult((_) {
unread.remove(notification);
read.add(notification.copy(dismissed: true));
read.sort();
notifyListeners();
});
return result.execErrorCast();
}
FutureResult<bool, ExecError> markAllAsRead() async {
final result =
await NotificationsClient(getIt<AccountsService>().currentProfile)
.clearNotifications()
.withResult((_) {
unread.map((n) => n.copy(dismissed: true)).forEach(read.add);
unread.clear();
read.sort();
notifyListeners();
});
return result.execErrorCast();
}
List<UserNotification> buildUnreadMessageNotifications(
bool useActualRequests) {
final myId = profile.userId;
final dmsResult = getIt<ActiveProfileSelector<DirectMessageService>>()
.getForProfile(profile)
.transform((d) => d.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: (fromAccount.hashCode ^
t.parentUri.hashCode ^
t.title.hashCode)
.toString(),
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())
.getValueOrElse(() => []);
final followRequestResult = !useActualRequests
? []
: getIt<ActiveProfileSelector<FollowRequestsManager>>()
.getForProfile(profile)
.transform(
(fm) => fm.requests.map((r) => r.toUserNotification()).toList())
.getValueOrElse(() => []);
return [...dmsResult, ...followRequestResult];
}
void updateNotification(UserNotification notification) {}
FutureResult<List<UserNotification>, ExecError> _postFetchOperations(
List<UserNotification> notificationsFromRefresh,
bool withListenerNotification,
) async {
getIt<NetworkStatusService>().startNotificationUpdate();
if (DateTime.now().difference(lastDmsUpdate) >
minimumDmsAndCrsUpdateDuration) {
await getIt<ActiveProfileSelector<DirectMessageService>>()
.getForProfile(profile)
.transformAsync((dms) async => await dms.updateThreads());
lastDmsUpdate = DateTime.now();
}
final useActualRequests = getIt<FriendicaVersionChecker>()
.canUseFeature(RelaticaFeatures.usingActualFollowRequests);
if (useActualRequests) {
if (DateTime.now().difference(lastCrUpdate) >
minimumDmsAndCrsUpdateDuration) {
await getIt<ActiveProfileSelector<FollowRequestsManager>>()
.getForProfile(profile)
.transformAsync((fm) async => fm.update());
lastCrUpdate = DateTime.now();
}
}
final notifications = <String, UserNotification>{};
notificationsFromRefresh.removeWhere((n) =>
n.type == NotificationType.direct_message ||
(useActualRequests && n.type == NotificationType.follow_request));
for (final n in notificationsFromRefresh) {
notifications[n.id] = n;
}
getIt<NetworkStatusService>().finishNotificationUpdate();
for (final n in buildUnreadMessageNotifications(useActualRequests)) {
notifications[n.id] = n;
}
_processNewNotifications(notifications.values);
if (withListenerNotification) {
notifyListeners();
}
return Result.ok(notifications.values.toList());
}
Future<void> _processNewNotifications(
Iterable<UserNotification> notifications) async {
final dmsMap = <String, UserNotification>{};
final crMap = <String, UserNotification>{};
final unreadMap = <String, UserNotification>{};
final readMap = <String, UserNotification>{};
final st = Stopwatch()..start();
for (int i = 0; i < dms.length; i++) {
dmsMap[dms[i].id] = dms[i];
}
if (st.elapsedMilliseconds > maxProcessingMillis) {
await Future.delayed(processingSleep, () => st.reset());
}
for (int i = 0; i < connectionRequests.length; i++) {
crMap[connectionRequests[i].id] = connectionRequests[i];
}
if (st.elapsedMilliseconds > maxProcessingMillis) {
await Future.delayed(processingSleep, () => st.reset());
}
for (int i = 0; i < unread.length; i++) {
unreadMap[unread[i].id] = unread[i];
}
if (st.elapsedMilliseconds > maxProcessingMillis) {
await Future.delayed(processingSleep, () => st.reset());
}
for (int i = 0; i < read.length; i++) {
readMap[read[i].id] = read[i];
}
dms.clear();
connectionRequests.clear();
unread.clear();
read.clear();
for (final n in notifications) {
if (st.elapsedMilliseconds > maxProcessingMillis) {
await Future.delayed(processingSleep, () => st.reset());
}
dmsMap.remove(n.id);
crMap.remove(n.id);
unreadMap.remove(n.id);
readMap.remove(n.id);
if (n.dismissed) {
readMap[n.id] = n;
continue;
}
switch (n.type) {
case NotificationType.direct_message:
dmsMap[n.id] = n;
break;
case NotificationType.follow_request:
crMap[n.id] = n;
break;
default:
unreadMap[n.id] = n;
}
}
dms
..addAll(dmsMap.values)
..sort();
connectionRequests
..addAll(crMap.values)
..sort();
unread
..addAll(unreadMap.values)
..sort();
read
..addAll(readMap.values)
..sort();
}
}
(int lowest, int highest) calcLowHigh(List<UserNotification> notifications) {
int highestNotificationId = -1;
int lowestNotificationId = 0x7FFFFFFFFFFFFFFF;
final ids = notifications
.where((n) =>
n.type != NotificationType.direct_message &&
n.type != NotificationType.follow_request)
.map((n) => int.parse(n.id));
for (var id in ids) {
if (id > highestNotificationId) {
highestNotificationId = id;
}
if (id < lowestNotificationId) {
lowestNotificationId = id;
}
}
return (lowestNotificationId, highestNotificationId);
}
PagesManager<List<UserNotification>, String> _buildPageManager(
Profile profile, bool includeAll,
{List<PagedResponse> initialPages = const []}) =>
PagesManager<List<UserNotification>, String>(
initialPages: initialPages,
idMapper: (nn) => nn.map((n) => n.id).toList(),
onRequest: (pd) async =>
await NotificationsClient(profile).getNotifications(pd, includeAll),
);