2022-11-19 05:00:17 +00:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:result_monad/result_monad.dart';
|
2023-02-08 15:41:29 +00:00
|
|
|
import 'package:uuid/uuid.dart';
|
2022-11-19 05:00:17 +00:00
|
|
|
|
2023-02-24 21:36:20 +00:00
|
|
|
import '../friendica_client/friendica_client.dart';
|
2023-02-10 14:36:41 +00:00
|
|
|
import '../friendica_client/paged_response.dart';
|
|
|
|
import '../friendica_client/pages_manager.dart';
|
|
|
|
import '../friendica_client/paging_data.dart';
|
2022-11-19 05:00:17 +00:00
|
|
|
import '../globals.dart';
|
|
|
|
import '../models/exec_error.dart';
|
|
|
|
import '../models/user_notification.dart';
|
2023-03-21 18:27:38 +00:00
|
|
|
import '../serializers/mastodon/follow_request_mastodon_extensions.dart';
|
2023-03-14 03:47:40 +00:00
|
|
|
import '../utils/active_profile_selector.dart';
|
2022-11-19 05:00:17 +00:00
|
|
|
import 'auth_service.dart';
|
2023-02-08 15:41:29 +00:00
|
|
|
import 'direct_message_service.dart';
|
2023-03-21 18:27:38 +00:00
|
|
|
import 'feature_version_checker.dart';
|
|
|
|
import 'follow_requests_manager.dart';
|
2023-03-14 03:47:40 +00:00
|
|
|
import 'network_status_service.dart';
|
2022-11-19 05:00:17 +00:00
|
|
|
|
|
|
|
class NotificationsManager extends ChangeNotifier {
|
|
|
|
static final _logger = Logger('NotificationManager');
|
|
|
|
final _notifications = <String, UserNotification>{};
|
2023-02-10 14:36:41 +00:00
|
|
|
final _pm = PagesManager<List<UserNotification>, String>(
|
|
|
|
idMapper: (nn) => nn.map((n) => n.id).toList(),
|
|
|
|
onRequest: _clientGetNotificationsRequest,
|
|
|
|
);
|
2023-03-15 01:31:02 +00:00
|
|
|
var _firstLoad = true;
|
2022-11-19 05:00:17 +00:00
|
|
|
|
|
|
|
List<UserNotification> get notifications {
|
2023-03-15 01:31:02 +00:00
|
|
|
if (_notifications.isEmpty && _firstLoad) {
|
|
|
|
updateNotifications();
|
|
|
|
_firstLoad = false;
|
|
|
|
}
|
2023-03-21 18:27:38 +00:00
|
|
|
final dms = <UserNotification>[];
|
|
|
|
final connectionRequests = <UserNotification>[];
|
|
|
|
final unread = <UserNotification>[];
|
|
|
|
final read = <UserNotification>[];
|
|
|
|
for (final n in _notifications.values) {
|
|
|
|
if (n.dismissed) {
|
|
|
|
read.add(n);
|
|
|
|
continue;
|
2022-11-19 05:00:17 +00:00
|
|
|
}
|
|
|
|
|
2023-03-21 18:27:38 +00:00
|
|
|
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);
|
2022-11-19 05:00:17 +00:00
|
|
|
}
|
2023-03-21 18:27:38 +00:00
|
|
|
}
|
|
|
|
dms.sort();
|
|
|
|
connectionRequests.sort();
|
|
|
|
unread.sort();
|
|
|
|
read.sort();
|
2022-11-19 05:00:17 +00:00
|
|
|
|
2023-03-21 18:27:38 +00:00
|
|
|
return [...connectionRequests, ...dms, ...unread, ...read];
|
2022-11-19 05:00:17 +00:00
|
|
|
}
|
|
|
|
|
2023-01-24 03:36:14 +00:00
|
|
|
void clear() {
|
|
|
|
_notifications.clear();
|
2023-03-11 03:17:03 +00:00
|
|
|
_pm.clear();
|
2023-01-24 03:36:14 +00:00
|
|
|
}
|
|
|
|
|
2022-11-19 05:00:17 +00:00
|
|
|
FutureResult<List<UserNotification>, ExecError> updateNotifications() async {
|
2023-03-14 21:04:22 +00:00
|
|
|
const initialPull = 100;
|
2023-03-21 18:27:38 +00:00
|
|
|
final notificationsFromRefresh = <UserNotification>[];
|
2023-02-10 14:36:41 +00:00
|
|
|
if (_pm.pages.isEmpty) {
|
2023-02-27 22:37:13 +00:00
|
|
|
final result = await _pm.initialize(initialPull);
|
2023-03-21 18:27:38 +00:00
|
|
|
result.andThenSuccess(
|
|
|
|
(response) => notificationsFromRefresh.addAll(response.data));
|
2023-02-10 14:36:41 +00:00
|
|
|
} else {
|
2023-02-27 22:37:13 +00:00
|
|
|
for (var i = 0; i < _pm.pages.length; i++) {
|
|
|
|
if (i > 0 && i == _pm.pages.length - 1) {
|
|
|
|
continue;
|
|
|
|
}
|
2023-02-10 14:36:41 +00:00
|
|
|
final page = _pm.pages[i];
|
|
|
|
if (i == 0) {
|
2023-03-22 17:28:05 +00:00
|
|
|
PagingData? pd;
|
|
|
|
bool initializedFirstPage = false;
|
|
|
|
if (page.next != null) {
|
|
|
|
final response = await _clientGetNotificationsRequest(page.next!);
|
2023-02-27 22:37:13 +00:00
|
|
|
response.match(
|
2023-03-22 17:28:05 +00:00
|
|
|
onSuccess: (response) => pd = response.previous,
|
2023-02-27 22:37:13 +00:00
|
|
|
onError: (error) =>
|
|
|
|
_logger.severe('Error getting previous page: $error'));
|
2023-03-22 17:28:05 +00:00
|
|
|
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');
|
2023-02-27 22:37:13 +00:00
|
|
|
}
|
|
|
|
|
2023-03-22 17:28:05 +00:00
|
|
|
if (!initializedFirstPage) {
|
2023-02-27 22:37:13 +00:00
|
|
|
_logger.severe(
|
2023-03-22 17:28:05 +00:00
|
|
|
'Unable to determine call to rebuild initial page so resetting');
|
2023-02-27 22:37:13 +00:00
|
|
|
_pm.clear();
|
|
|
|
final result = await _pm.initialize(initialPull);
|
2023-03-21 18:27:38 +00:00
|
|
|
result.andThenSuccess(
|
|
|
|
(response) => notificationsFromRefresh.addAll(response.data));
|
2023-02-27 22:37:13 +00:00
|
|
|
}
|
2023-02-10 14:36:41 +00:00
|
|
|
}
|
|
|
|
|
2023-02-27 22:37:13 +00:00
|
|
|
if (page.next == null) {
|
|
|
|
if (i != _pm.pages.length - 2) {
|
|
|
|
_logger
|
|
|
|
.severe('No forward paging data in middle page but expected');
|
|
|
|
}
|
2023-02-10 14:36:41 +00:00
|
|
|
continue;
|
|
|
|
}
|
2023-02-27 22:37:13 +00:00
|
|
|
|
|
|
|
final response = await _clientGetNotificationsRequest(page.next!);
|
2023-02-10 14:36:41 +00:00
|
|
|
response.match(
|
2023-03-21 18:27:38 +00:00
|
|
|
onSuccess: (response) =>
|
|
|
|
notificationsFromRefresh.addAll(response.data),
|
2023-02-10 14:36:41 +00:00
|
|
|
onError: (error) =>
|
2023-03-22 17:28:05 +00:00
|
|
|
_logger.severe('Error getting next page: $error'));
|
2023-02-10 14:36:41 +00:00
|
|
|
}
|
2022-11-19 05:00:17 +00:00
|
|
|
}
|
|
|
|
|
2023-02-08 15:46:26 +00:00
|
|
|
getIt<NetworkStatusService>().startNotificationUpdate();
|
2023-03-14 03:47:40 +00:00
|
|
|
await getIt<ActiveProfileSelector<DirectMessageService>>()
|
|
|
|
.activeEntry
|
|
|
|
.andThenSuccessAsync((dms) async => await dms.updateThreads());
|
2023-03-21 18:27:38 +00:00
|
|
|
|
|
|
|
final useActualRequests = getIt<FriendicaVersionChecker>()
|
|
|
|
.canUseFeature(RelaticaFeatures.usingActualFollowRequests);
|
|
|
|
|
|
|
|
if (useActualRequests) {
|
|
|
|
await getIt<ActiveProfileSelector<FollowRequestsManager>>()
|
|
|
|
.activeEntry
|
|
|
|
.andThenSuccessAsync((fm) async => fm.update());
|
|
|
|
}
|
|
|
|
|
|
|
|
_notifications.clear();
|
|
|
|
|
|
|
|
notificationsFromRefresh.removeWhere((n) =>
|
|
|
|
n.type == NotificationType.direct_message ||
|
|
|
|
(useActualRequests && n.type == NotificationType.follow_request));
|
|
|
|
for (final n in notificationsFromRefresh) {
|
|
|
|
_notifications[n.id] = n;
|
|
|
|
}
|
|
|
|
|
2023-02-08 15:46:26 +00:00
|
|
|
getIt<NetworkStatusService>().finishNotificationUpdate();
|
2023-03-21 18:27:38 +00:00
|
|
|
for (final n in buildUnreadMessageNotifications(useActualRequests)) {
|
2023-02-08 15:41:29 +00:00
|
|
|
_notifications[n.id] = n;
|
|
|
|
}
|
|
|
|
|
2022-11-19 05:00:17 +00:00
|
|
|
notifyListeners();
|
|
|
|
return Result.ok(notifications);
|
|
|
|
}
|
|
|
|
|
2023-02-10 14:36:41 +00:00
|
|
|
FutureResult<List<UserNotification>, ExecError>
|
|
|
|
loadNewerNotifications() async {
|
|
|
|
final result = await _pm.previousFromBeginning();
|
|
|
|
result.match(onSuccess: (response) {
|
2023-03-21 18:27:38 +00:00
|
|
|
if (response.data.isEmpty) {
|
|
|
|
return;
|
|
|
|
}
|
2023-02-10 14:36:41 +00:00
|
|
|
for (final n in response.data) {
|
|
|
|
_notifications[n.id] = n;
|
|
|
|
}
|
2023-02-14 13:38:08 +00:00
|
|
|
notifyListeners();
|
2023-02-10 14:36:41 +00:00
|
|
|
}, onError: (error) {
|
2023-02-14 13:38:08 +00:00
|
|
|
_logger.info('Error getting more updates: $error');
|
2023-02-10 14:36:41 +00:00
|
|
|
});
|
|
|
|
|
2023-02-14 13:38:08 +00:00
|
|
|
return result.mapValue((response) => response.data);
|
2023-02-10 14:36:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
FutureResult<List<UserNotification>, ExecError>
|
|
|
|
loadOlderNotifications() async {
|
|
|
|
final result = await _pm.nextFromEnd();
|
|
|
|
result.match(onSuccess: (response) {
|
|
|
|
for (final n in response.data) {
|
|
|
|
_notifications[n.id] = n;
|
|
|
|
}
|
2023-02-14 13:38:08 +00:00
|
|
|
notifyListeners();
|
2023-02-10 14:36:41 +00:00
|
|
|
}, onError: (error) {
|
2023-02-14 13:38:08 +00:00
|
|
|
_logger.info('Error getting more updates: $error');
|
2023-02-10 14:36:41 +00:00
|
|
|
});
|
|
|
|
|
2023-02-14 13:38:08 +00:00
|
|
|
return result.mapValue((response) => response.data);
|
2023-02-10 14:36:41 +00:00
|
|
|
}
|
|
|
|
|
2022-11-22 04:46:34 +00:00
|
|
|
FutureResult<bool, ExecError> markSeen(UserNotification notification) async {
|
2023-02-24 21:36:20 +00:00
|
|
|
final result =
|
2023-02-27 03:12:40 +00:00
|
|
|
await NotificationsClient(getIt<AccountsService>().currentProfile)
|
2023-02-24 21:36:20 +00:00
|
|
|
.clearNotification(notification);
|
2022-11-22 04:46:34 +00:00
|
|
|
if (result.isSuccess) {
|
|
|
|
notifyListeners();
|
2022-11-19 05:00:17 +00:00
|
|
|
}
|
|
|
|
|
2022-11-22 04:46:34 +00:00
|
|
|
updateNotifications();
|
|
|
|
return result;
|
2022-11-19 05:00:17 +00:00
|
|
|
}
|
2022-11-19 19:16:46 +00:00
|
|
|
|
|
|
|
FutureResult<List<UserNotification>, ExecError> markAllAsRead() async {
|
2023-02-24 20:56:39 +00:00
|
|
|
final result =
|
2023-02-27 03:12:40 +00:00
|
|
|
await NotificationsClient(getIt<AccountsService>().currentProfile)
|
2023-02-24 21:36:20 +00:00
|
|
|
.clearNotifications();
|
2022-11-19 19:16:46 +00:00
|
|
|
if (result.isFailure) {
|
|
|
|
return result.errorCast();
|
|
|
|
}
|
2023-02-14 13:38:08 +00:00
|
|
|
|
|
|
|
_pm.clear();
|
|
|
|
_notifications.clear();
|
2022-11-19 19:16:46 +00:00
|
|
|
return updateNotifications();
|
|
|
|
}
|
2023-02-08 15:41:29 +00:00
|
|
|
|
2023-03-21 18:27:38 +00:00
|
|
|
List<UserNotification> buildUnreadMessageNotifications(
|
|
|
|
bool useActualRequests) {
|
2023-02-27 03:12:40 +00:00
|
|
|
final myId = getIt<AccountsService>().currentProfile.userId;
|
2023-03-21 18:27:38 +00:00
|
|
|
final dmsResult = getIt<ActiveProfileSelector<DirectMessageService>>()
|
2023-03-14 03:47:40 +00:00
|
|
|
.activeEntry
|
2023-03-21 18:27:38 +00:00
|
|
|
.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(() => []);
|
2023-02-08 15:41:29 +00:00
|
|
|
|
2023-03-21 18:27:38 +00:00
|
|
|
final followRequestResult = !useActualRequests
|
|
|
|
? []
|
|
|
|
: getIt<ActiveProfileSelector<FollowRequestsManager>>()
|
|
|
|
.activeEntry
|
|
|
|
.andThenSuccess(
|
|
|
|
(fm) => fm.requests.map((r) => r.toUserNotification()).toList())
|
|
|
|
.getValueOrElse(() => []);
|
|
|
|
|
|
|
|
return [...dmsResult, ...followRequestResult];
|
2023-02-08 15:41:29 +00:00
|
|
|
}
|
2023-02-10 14:36:41 +00:00
|
|
|
|
|
|
|
static FutureResult<PagedResponse<List<UserNotification>>, ExecError>
|
|
|
|
_clientGetNotificationsRequest(PagingData page) async {
|
2023-02-24 20:56:39 +00:00
|
|
|
final result =
|
2023-02-27 03:12:40 +00:00
|
|
|
await NotificationsClient(getIt<AccountsService>().currentProfile)
|
2023-02-24 21:36:20 +00:00
|
|
|
.getNotifications(page);
|
2023-02-10 14:36:41 +00:00
|
|
|
return result;
|
|
|
|
}
|
2022-11-19 05:00:17 +00:00
|
|
|
}
|