relatica/lib/services/notifications_manager.dart

368 wiersze
12 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 final _logger = Logger('NotificationManager');
late final PagesManager<List<UserNotification>, String> _pm;
final Profile profile;
final dms = <UserNotification>[];
final connectionRequests = <UserNotification>[];
final unread = <UserNotification>[];
final read = <UserNotification>[];
NotificationsManager(this.profile) {
_pm = PagesManager<List<UserNotification>, String>(
idMapper: (nn) => nn.map((n) => n.id).toList(),
onRequest: (pd) async =>
await _clientGetNotificationsRequest(profile, pd));
}
var _firstLoad = true;
List<UserNotification> get notifications {
if (_firstLoad) {
updateNotifications();
_firstLoad = false;
}
return [...connectionRequests, ...dms, ...unread, ...read];
}
void clear() {
_pm.clear();
dms.clear();
connectionRequests.clear();
unread.clear();
read.clear();
_firstLoad = true;
notifyListeners();
}
FutureResult<List<UserNotification>, ExecError> updateNotifications() async {
const initialPull = 25;
final notificationsFromRefresh = <UserNotification>[];
if (_pm.pages.isEmpty) {
final result = await _pm.initialize(initialPull);
result.andThenSuccess(
(response) => notificationsFromRefresh.addAll(response.data));
} else {
for (var i = 0; i < _pm.pages.length; i++) {
if (i > 0 && i == _pm.pages.length - 1) {
continue;
}
final page = _pm.pages[i];
if (i == 0) {
PagingData? pd;
bool initializedFirstPage = false;
if (page.next != null) {
final response = await _clientGetNotificationsRequest(
profile,
page.next!,
);
response.match(
onSuccess: (response) => pd = response.previous,
onError: (error) =>
_logger.severe('Error getting previous page: $error'));
if (pd != null) {
final response = await _clientGetNotificationsRequest(
profile,
pd!,
);
response.match(
onSuccess: (response) {
initializedFirstPage = true;
notificationsFromRefresh.addAll(response.data);
},
onError: (error) =>
_logger.severe('Error getting previous page: $error'));
} else if (pd == null && page.previous != null) {
final response = await _clientGetNotificationsRequest(
profile,
page.previous!,
).andThenAsync((previousData) async => previousData.next != null
? await _clientGetNotificationsRequest(
profile,
previousData.next!,
)
: buildErrorResult(
type: ErrorType.rangeError,
message: 'No "next" page from previous data either'));
response.match(
onSuccess: (response) {
initializedFirstPage = true;
notificationsFromRefresh.addAll(response.data);
},
onError: (error) =>
_logger.severe('Error getting previous page: $error'));
} else if (pd == null && page.previous == null) {
_logger.severe(
'Next page returned no results and no previous page so will need to re-initalize');
}
} else {
_logger.severe(
'There is no next page to query so will be forced to reset');
}
if (!initializedFirstPage) {
_logger.severe(
'Unable to determine call to rebuild initial page so resetting');
_pm.clear();
final result = await _pm.initialize(initialPull);
result.andThenSuccess(
(response) => notificationsFromRefresh.addAll(response.data));
}
}
if (page.next == null) {
if (i != _pm.pages.length - 2) {
_logger
.severe('No forward paging data in middle page but expected');
}
continue;
}
final response = await _clientGetNotificationsRequest(
profile,
page.next!,
);
response.match(
onSuccess: (response) =>
notificationsFromRefresh.addAll(response.data),
onError: (error) =>
_logger.severe('Error getting next page: $error'));
}
}
return await _postFetchOperations(notificationsFromRefresh, true);
}
FutureResult<List<UserNotification>, ExecError>
loadNewerNotifications() async {
final result = await _pm
.previousFromBeginning()
.andThenAsync(
(page) async => await _postFetchOperations(page.data, false),
)
.withError(
(error) => _logger.info('Error getting more updates: $error'));
return result.execErrorCast();
}
FutureResult<List<UserNotification>, ExecError>
loadOlderNotifications() async {
final result = await _pm
.nextFromEnd()
.andThenAsync(
(page) async => await _postFetchOperations(page.data, false),
)
.withError(
(error) => _logger.info('Error getting more updates: $error'));
return result.execErrorCast();
}
FutureResult<bool, ExecError> markSeen(UserNotification notification) async {
final result =
await NotificationsClient(profile).clearNotification(notification);
if (result.isSuccess) {
notifyListeners();
}
updateNotifications();
return result;
}
FutureResult<List<UserNotification>, ExecError> markAllAsRead() async {
final result =
await NotificationsClient(getIt<AccountsService>().currentProfile)
.clearNotifications();
if (result.isFailure) {
return result.errorCast();
}
return updateNotifications();
}
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];
}
FutureResult<List<UserNotification>, ExecError> _postFetchOperations(
List<UserNotification> notificationsFromRefresh,
bool clearAtStart,
) async {
getIt<NetworkStatusService>().startNotificationUpdate();
await getIt<ActiveProfileSelector<DirectMessageService>>()
.getForProfile(profile)
.transformAsync((dms) async => await dms.updateThreads());
final useActualRequests = getIt<FriendicaVersionChecker>()
.canUseFeature(RelaticaFeatures.usingActualFollowRequests);
if (useActualRequests) {
await getIt<ActiveProfileSelector<FollowRequestsManager>>()
.getForProfile(profile)
.transformAsync((fm) async => fm.update());
}
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, clearAtStart: clearAtStart);
notifyListeners();
return Result.ok(notifications.values.toList());
}
Future<void> _processNewNotifications(
Iterable<UserNotification> notifications, {
bool clearAtStart = false,
}) async {
final dmsMap = <String, UserNotification>{};
final crMap = <String, UserNotification>{};
final unreadMap = <String, UserNotification>{};
final readMap = <String, UserNotification>{};
final st = Stopwatch()..start();
if (!clearAtStart) {
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:
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();
}
static FutureResult<PagedResponse<List<UserNotification>>, ExecError>
_clientGetNotificationsRequest(Profile profile, PagingData page) async {
return NotificationsClient(profile).getNotifications(page);
}
}