import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; import 'package:uuid/uuid.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/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'); final dms = []; final connectionRequests = []; final unread = []; final read = []; final _pm = PagesManager, String>( idMapper: (nn) => nn.map((n) => n.id).toList(), onRequest: _clientGetNotificationsRequest, ); 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 = 100; 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(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(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( page.previous!) .andThenAsync((previousData) async => previousData.next != null ? await _clientGetNotificationsRequest(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(page.next!); response.match( onSuccess: (response) => notificationsFromRefresh.addAll(response.data), onError: (error) => _logger.severe('Error getting next page: $error')); } } getIt().startNotificationUpdate(); await getIt>() .activeEntry .andThenSuccessAsync((dms) async => await dms.updateThreads()); final useActualRequests = getIt() .canUseFeature(RelaticaFeatures.usingActualFollowRequests); if (useActualRequests) { await getIt>() .activeEntry .andThenSuccessAsync((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: true); notifyListeners(); return Result.ok(notifications.values.toList()); } FutureResult, ExecError> loadNewerNotifications() async { final notifications = {}; final result = await _pm.previousFromBeginning(); result.match(onSuccess: (response) { if (response.data.isEmpty) { return; } for (final n in response.data) { notifications[n.id] = n; } _processNewNotifications(notifications.values); notifyListeners(); }, onError: (error) { _logger.info('Error getting more updates: $error'); }); return result.mapValue((response) => response.data); } FutureResult, ExecError> loadOlderNotifications() async { final notifications = {}; final result = await _pm.nextFromEnd(); result.match(onSuccess: (response) { for (final n in response.data) { notifications[n.id] = n; } notifyListeners(); }, onError: (error) { _logger.info('Error getting more updates: $error'); }); _processNewNotifications(notifications.values); return result.mapValue((response) => response.data); } FutureResult markSeen(UserNotification notification) async { final result = await NotificationsClient(getIt().currentProfile) .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 = getIt().currentProfile.userId; final dmsResult = getIt>() .activeEntry .andThenSuccess((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: 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()) .getValueOrElse(() => []); final followRequestResult = !useActualRequests ? [] : getIt>() .activeEntry .andThenSuccess( (fm) => fm.requests.map((r) => r.toUserNotification()).toList()) .getValueOrElse(() => []); return [...dmsResult, ...followRequestResult]; } Future _processNewNotifications( Iterable notifications, { bool clearAtStart = false, }) async { if (clearAtStart) { dms.clear(); connectionRequests.clear(); unread.clear(); read.clear(); } for (final n in notifications) { if (n.dismissed) { read.add(n); continue; } switch (n.type) { case NotificationType.direct_message: dms.add(n); break; case NotificationType.follow: case NotificationType.follow_request: connectionRequests.add(n); break; default: unread.add(n); } } dms.sort(); connectionRequests.sort(); unread.sort(); read.sort(); } static FutureResult>, ExecError> _clientGetNotificationsRequest(PagingData page) async { final result = await NotificationsClient(getIt().currentProfile) .getNotifications(page); return result; } }