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, String> _pm; final Profile profile; final dms = []; final connectionRequests = []; final unread = []; final read = []; NotificationsManager(this.profile) { _pm = PagesManager, String>( idMapper: (nn) => nn.map((n) => n.id).toList(), onRequest: (pd) async => await _clientGetNotificationsRequest(profile, pd)); } var _firstLoad = true; List 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, ExecError> updateNotifications() async { const initialPull = 25; final notificationsFromRefresh = []; 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, 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, 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 markSeen(UserNotification notification) async { final result = await NotificationsClient(profile).clearNotification(notification); if (result.isSuccess) { notifyListeners(); } updateNotifications(); return result; } FutureResult, ExecError> markAllAsRead() async { final result = await NotificationsClient(getIt().currentProfile) .clearNotifications(); if (result.isFailure) { return result.errorCast(); } return updateNotifications(); } List buildUnreadMessageNotifications( bool useActualRequests) { final myId = profile.userId; final dmsResult = getIt>() .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>() .getForProfile(profile) .transform( (fm) => fm.requests.map((r) => r.toUserNotification()).toList()) .getValueOrElse(() => []); return [...dmsResult, ...followRequestResult]; } FutureResult, ExecError> _postFetchOperations( List notificationsFromRefresh, bool clearAtStart, ) async { getIt().startNotificationUpdate(); await getIt>() .getForProfile(profile) .transformAsync((dms) async => await dms.updateThreads()); final useActualRequests = getIt() .canUseFeature(RelaticaFeatures.usingActualFollowRequests); if (useActualRequests) { await getIt>() .getForProfile(profile) .transformAsync((fm) async => fm.update()); } final notifications = {}; notificationsFromRefresh.removeWhere((n) => n.type == NotificationType.direct_message || (useActualRequests && n.type == NotificationType.follow_request)); for (final n in notificationsFromRefresh) { notifications[n.id] = n; } getIt().finishNotificationUpdate(); for (final n in buildUnreadMessageNotifications(useActualRequests)) { notifications[n.id] = n; } _processNewNotifications(notifications.values, clearAtStart: clearAtStart); notifyListeners(); return Result.ok(notifications.values.toList()); } Future _processNewNotifications( Iterable notifications, { bool clearAtStart = false, }) async { final dmsMap = {}; final crMap = {}; final unreadMap = {}; final readMap = {}; 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>, ExecError> _clientGetNotificationsRequest(Profile profile, PagingData page) async { return NotificationsClient(profile).getNotifications(page); } }