Lay groundwork for background notifications
This commit is contained in:
parent
d617cd3337
commit
2d3970ecf0
3 changed files with 170 additions and 78 deletions
|
@ -4,10 +4,14 @@ import 'dart:io';
|
|||
import 'package:contacts_plus_plus/apis/friend_api.dart';
|
||||
import 'package:contacts_plus_plus/apis/message_api.dart';
|
||||
import 'package:contacts_plus_plus/apis/user_api.dart';
|
||||
import 'package:contacts_plus_plus/auxiliary.dart';
|
||||
import 'package:contacts_plus_plus/clients/notification_client.dart';
|
||||
import 'package:contacts_plus_plus/clients/settings_client.dart';
|
||||
import 'package:contacts_plus_plus/models/authentication_data.dart';
|
||||
import 'package:contacts_plus_plus/models/friend.dart';
|
||||
import 'package:contacts_plus_plus/models/settings.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
||||
|
@ -45,9 +49,16 @@ class MessagingClient extends ChangeNotifier {
|
|||
static const String eofChar = "";
|
||||
static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$eofChar";
|
||||
static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60];
|
||||
static const String taskName = "periodic-unread-check";
|
||||
|
||||
static const int _unreadCheckMinuteInterval = 30;
|
||||
static const String unreadCheckTaskName = "periodic-unread-check";
|
||||
static const String _storageNotifiedUnreadsKey = "notfiedUnreads";
|
||||
static const String _storageLastUpdateKey = "lastUnreadCheck";
|
||||
static const FlutterSecureStorage _storage = FlutterSecureStorage();
|
||||
|
||||
static const Duration _autoRefreshDuration = Duration(seconds: 90);
|
||||
static const Duration _refreshTimeoutDuration = Duration(seconds: 30);
|
||||
|
||||
final ApiClient _apiClient;
|
||||
final Map<String, Friend> _friendsCache = {};
|
||||
final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build()
|
||||
|
@ -55,13 +66,16 @@ class MessagingClient extends ChangeNotifier {
|
|||
final Map<String, List<Message>> _unreads = {};
|
||||
final Logger _logger = Logger("NeosHub");
|
||||
final Workmanager _workmanager = Workmanager();
|
||||
|
||||
final NotificationClient _notificationClient;
|
||||
final SettingsClient _settingsClient;
|
||||
|
||||
WebSocket? _wsChannel;
|
||||
Friend? selectedFriend;
|
||||
Timer? _notifyOnlineTimer;
|
||||
Timer? _autoRefresh;
|
||||
Timer? _refreshTimeout;
|
||||
int _attempts = 0;
|
||||
WebSocket? _wsChannel;
|
||||
bool _isConnecting = false;
|
||||
String? _initStatus;
|
||||
|
||||
|
@ -69,8 +83,9 @@ class MessagingClient extends ChangeNotifier {
|
|||
|
||||
bool get websocketConnected => _wsChannel != null;
|
||||
|
||||
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
|
||||
: _apiClient = apiClient, _notificationClient = notificationClient {
|
||||
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient,
|
||||
required SettingsClient settingsClient})
|
||||
: _apiClient = apiClient, _notificationClient = notificationClient, _settingsClient = settingsClient {
|
||||
refreshFriendsListWithErrorHandler();
|
||||
startWebsocket();
|
||||
_notifyOnlineTimer = Timer.periodic(const Duration(seconds: 60), (timer) async {
|
||||
|
@ -78,6 +93,70 @@ class MessagingClient extends ChangeNotifier {
|
|||
// but I don't feel like implementing that right now.
|
||||
UserApi.setStatus(apiClient, status: await UserApi.getUserStatus(apiClient, userId: apiClient.userId));
|
||||
});
|
||||
_settingsClient.addListener(onSettingsChanged);
|
||||
if (!_settingsClient.currentSettings.notificationsDenied.valueOrDefault) {
|
||||
registerNotificationTask();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> onSettingsChanged(Settings oldSettings, Settings newSettings) async {
|
||||
if (oldSettings.notificationsDenied.valueOrDefault != newSettings.notificationsDenied.valueOrDefault) {
|
||||
if (newSettings.notificationsDenied.valueOrDefault) {
|
||||
await unregisterNotificationTask();
|
||||
} else {
|
||||
await registerNotificationTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Future<List<Message>> updateNotified(List<Message> unreads) async {
|
||||
if (unreads.isEmpty) return [];
|
||||
const storage = FlutterSecureStorage();
|
||||
final data = await storage.read(key: _storageNotifiedUnreadsKey);
|
||||
|
||||
final existing = data == null ? <String>[] : (jsonDecode(data) as List).map((e) => "$e").toList();
|
||||
final unnotified = unreads.where((unread) => !existing.contains(unread.id));
|
||||
existing.addAll(unnotified.map((e) => e.id));
|
||||
await storage.write(key: _storageNotifiedUnreadsKey, value: jsonEncode(existing.unique()));
|
||||
return unnotified.toList();
|
||||
}
|
||||
|
||||
static Future<void> backgroundCheckUnreads(Map<String, dynamic>? inputData) async {
|
||||
if (inputData == null) throw "Unauthenticated";
|
||||
final auth = AuthenticationData.fromMap(inputData);
|
||||
const storage = FlutterSecureStorage();
|
||||
final lastCheckData = await storage.read(key: _storageLastUpdateKey);
|
||||
if (lastCheckData != null && DateTime.now().difference(DateTime.parse(lastCheckData)) < const Duration(
|
||||
minutes: _unreadCheckMinuteInterval,
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final client = ApiClient(authenticationData: auth);
|
||||
await client.extendSession();
|
||||
|
||||
final unreads = await MessageApi.getUserMessages(client, unreadOnly: true);
|
||||
|
||||
final unnotified = await updateNotified(unreads);
|
||||
|
||||
await NotificationClient().showUnreadMessagesNotification(unnotified);
|
||||
await storage.write(key: _storageLastUpdateKey, value: DateTime.now().toIso8601String());
|
||||
}
|
||||
|
||||
Future<void> registerNotificationTask() async {
|
||||
final auth = _apiClient.authenticationData;
|
||||
if (!auth.isAuthenticated) throw "Unauthenticated";
|
||||
_workmanager.registerPeriodicTask(
|
||||
unreadCheckTaskName,
|
||||
unreadCheckTaskName,
|
||||
frequency: const Duration(minutes: _unreadCheckMinuteInterval),
|
||||
inputData: auth.toMap(),
|
||||
existingWorkPolicy: ExistingWorkPolicy.replace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> unregisterNotificationTask() async {
|
||||
await _workmanager.cancelByUniqueName(unreadCheckTaskName);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -112,7 +191,14 @@ class MessagingClient extends ChangeNotifier {
|
|||
if (_refreshTimeout?.isActive == true) return;
|
||||
|
||||
_autoRefresh?.cancel();
|
||||
_autoRefresh = Timer(_autoRefreshDuration, () => refreshFriendsList());
|
||||
_autoRefresh = Timer(_autoRefreshDuration, () async {
|
||||
try {
|
||||
await refreshFriendsList();
|
||||
} catch (_) {
|
||||
// We don't really need to do anything if fetching unreads and messages fails in the background since we can
|
||||
// just keep showing the old state until refreshing succeeds.
|
||||
}
|
||||
});
|
||||
_refreshTimeout?.cancel();
|
||||
_refreshTimeout = Timer(_refreshTimeoutDuration, () {});
|
||||
|
||||
|
@ -128,6 +214,7 @@ class MessagingClient extends ChangeNotifier {
|
|||
_sortedFriendsCache.addAll(friends);
|
||||
_sortFriendsCache();
|
||||
_initStatus = "";
|
||||
await _storage.write(key: _storageLastUpdateKey, value: DateTime.now().toIso8601String());
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
@ -164,9 +251,13 @@ class MessagingClient extends ChangeNotifier {
|
|||
} else {
|
||||
messages.add(message);
|
||||
}
|
||||
messages.sort();
|
||||
_sortFriendsCache();
|
||||
_notificationClient.showUnreadMessagesNotification(messages.reversed);
|
||||
if (!_settingsClient.currentSettings.notificationsDenied.valueOrDefault) {
|
||||
updateNotified(messages).then((unnotified) {
|
||||
unnotified.sort();
|
||||
_notificationClient.showUnreadMessagesNotification(unnotified.reversed);
|
||||
});
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
@ -217,27 +308,6 @@ class MessagingClient extends ChangeNotifier {
|
|||
|
||||
MessageCache? getUserMessageCache(String userId) => _messageCache[userId];
|
||||
|
||||
static Future<void> backgroundCheckUnreads(Map<String, dynamic>? inputData) async {
|
||||
if (inputData == null) return;
|
||||
final auth = AuthenticationData.fromMap(inputData);
|
||||
final unreads = await MessageApi.getUserMessages(ApiClient(authenticationData: auth), unreadOnly: true);
|
||||
for (var message in unreads) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateNotificationTask(int minuteInterval) async {
|
||||
final auth = _apiClient.authenticationData;
|
||||
if (!auth.isAuthenticated) throw "Unauthenticated";
|
||||
await _workmanager.cancelByUniqueName(taskName);
|
||||
_workmanager.registerPeriodicTask(
|
||||
taskName,
|
||||
taskName,
|
||||
frequency: Duration(minutes: minuteInterval),
|
||||
inputData: auth.toMap(),
|
||||
);
|
||||
}
|
||||
|
||||
void _onDisconnected(error) async {
|
||||
_wsChannel = null;
|
||||
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
|
||||
|
@ -388,5 +458,10 @@ class MessagingClient extends ChangeNotifier {
|
|||
};
|
||||
_sendData(data);
|
||||
clearUnreadsForUser(batch.senderId);
|
||||
_storage.read(key: _storageNotifiedUnreadsKey).then((data) async {
|
||||
final existing = data == null ? [] : jsonDecode(data) as List<String>;
|
||||
final marked = existing.where((element) => !batch.ids.contains(element)).toList();
|
||||
await _storage.write(key: _storageNotifiedUnreadsKey, value: jsonEncode(marked));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -39,13 +39,15 @@ class NotificationClient {
|
|||
uname.hashCode,
|
||||
null,
|
||||
null,
|
||||
fln.NotificationDetails(android: fln.AndroidNotificationDetails(
|
||||
fln.NotificationDetails(
|
||||
android: fln.AndroidNotificationDetails(
|
||||
_messageChannel.id,
|
||||
_messageChannel.name,
|
||||
channelDescription: _messageChannel.description,
|
||||
importance: fln.Importance.high,
|
||||
priority: fln.Priority.max,
|
||||
actions: [], //TODO: Make clicking message notification open chat of specified user.
|
||||
actions: [],
|
||||
//TODO: Make clicking message notification open chat of specified user.
|
||||
styleInformation: fln.MessagingStyleInformation(
|
||||
fln.Person(
|
||||
name: uname,
|
||||
|
|
|
@ -22,22 +22,36 @@ void main() async {
|
|||
if (Platform.isAndroid) {
|
||||
await Workmanager().initialize(
|
||||
callbackDispatcher, // The top level function, aka callbackDispatcher
|
||||
isInDebugMode: true // If enabled it will post a notification whenever the task is running. Handy for debugging tasks
|
||||
isInDebugMode: false, // If enabled it will post a notification whenever the task is running. Handy for debugging tasks
|
||||
);
|
||||
}
|
||||
|
||||
Logger.root.onRecord.listen((event) => log(event.message, name: event.loggerName, time: event.time));
|
||||
final settingsClient = SettingsClient();
|
||||
await settingsClient.loadSettings();
|
||||
if (settingsClient.currentSettings.publicMachineId.value == null) {
|
||||
// If no machineId is set, write the generated one to disk
|
||||
settingsClient.changeSettings(
|
||||
settingsClient.currentSettings.copyWith(
|
||||
publicMachineId: settingsClient.currentSettings.publicMachineId.valueOrDefault,
|
||||
),
|
||||
);
|
||||
}
|
||||
runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,)));
|
||||
}
|
||||
|
||||
@pragma('vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+
|
||||
void callbackDispatcher() {
|
||||
Workmanager().executeTask((String task, Map<String, dynamic>? inputData) async {
|
||||
return Future.value(true); //Disable background tasks for now
|
||||
debugPrint("Native called background task: $task"); //simpleTask will be emitted here.
|
||||
if (task == MessagingClient.taskName) {
|
||||
final unreads = MessagingClient.backgroundCheckUnreads(inputData);
|
||||
if (task == MessagingClient.unreadCheckTaskName) {
|
||||
try {
|
||||
await MessagingClient.backgroundCheckUnreads(inputData);
|
||||
} catch (e) {
|
||||
Logger("Workman").severe(e);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
return Future.value(true);
|
||||
});
|
||||
|
@ -129,6 +143,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
|||
MessagingClient(
|
||||
apiClient: clientHolder.apiClient,
|
||||
notificationClient: clientHolder.notificationClient,
|
||||
settingsClient: widget.settingsClient,
|
||||
),
|
||||
child: const FriendsList(),
|
||||
) :
|
||||
|
|
Loading…
Reference in a new issue