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/friend_api.dart';
|
||||||
import 'package:contacts_plus_plus/apis/message_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/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/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/authentication_data.dart';
|
||||||
import 'package:contacts_plus_plus/models/friend.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/widgets.dart';
|
||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import 'package:contacts_plus_plus/clients/api_client.dart';
|
import 'package:contacts_plus_plus/clients/api_client.dart';
|
||||||
|
@ -45,9 +49,16 @@ class MessagingClient extends ChangeNotifier {
|
||||||
static const String eofChar = "";
|
static const String eofChar = "";
|
||||||
static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$eofChar";
|
static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$eofChar";
|
||||||
static const List<int> _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60];
|
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 _autoRefreshDuration = Duration(seconds: 90);
|
||||||
static const Duration _refreshTimeoutDuration = Duration(seconds: 30);
|
static const Duration _refreshTimeoutDuration = Duration(seconds: 30);
|
||||||
|
|
||||||
final ApiClient _apiClient;
|
final ApiClient _apiClient;
|
||||||
final Map<String, Friend> _friendsCache = {};
|
final Map<String, Friend> _friendsCache = {};
|
||||||
final List<Friend> _sortedFriendsCache = []; // Keep a sorted copy so as to not have to sort during build()
|
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 Map<String, List<Message>> _unreads = {};
|
||||||
final Logger _logger = Logger("NeosHub");
|
final Logger _logger = Logger("NeosHub");
|
||||||
final Workmanager _workmanager = Workmanager();
|
final Workmanager _workmanager = Workmanager();
|
||||||
|
|
||||||
final NotificationClient _notificationClient;
|
final NotificationClient _notificationClient;
|
||||||
|
final SettingsClient _settingsClient;
|
||||||
|
|
||||||
|
WebSocket? _wsChannel;
|
||||||
Friend? selectedFriend;
|
Friend? selectedFriend;
|
||||||
Timer? _notifyOnlineTimer;
|
Timer? _notifyOnlineTimer;
|
||||||
Timer? _autoRefresh;
|
Timer? _autoRefresh;
|
||||||
Timer? _refreshTimeout;
|
Timer? _refreshTimeout;
|
||||||
int _attempts = 0;
|
int _attempts = 0;
|
||||||
WebSocket? _wsChannel;
|
|
||||||
bool _isConnecting = false;
|
bool _isConnecting = false;
|
||||||
String? _initStatus;
|
String? _initStatus;
|
||||||
|
|
||||||
|
@ -69,8 +83,9 @@ class MessagingClient extends ChangeNotifier {
|
||||||
|
|
||||||
bool get websocketConnected => _wsChannel != null;
|
bool get websocketConnected => _wsChannel != null;
|
||||||
|
|
||||||
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient})
|
MessagingClient({required ApiClient apiClient, required NotificationClient notificationClient,
|
||||||
: _apiClient = apiClient, _notificationClient = notificationClient {
|
required SettingsClient settingsClient})
|
||||||
|
: _apiClient = apiClient, _notificationClient = notificationClient, _settingsClient = settingsClient {
|
||||||
refreshFriendsListWithErrorHandler();
|
refreshFriendsListWithErrorHandler();
|
||||||
startWebsocket();
|
startWebsocket();
|
||||||
_notifyOnlineTimer = Timer.periodic(const Duration(seconds: 60), (timer) async {
|
_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.
|
// but I don't feel like implementing that right now.
|
||||||
UserApi.setStatus(apiClient, status: await UserApi.getUserStatus(apiClient, userId: apiClient.userId));
|
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
|
@override
|
||||||
|
@ -112,7 +191,14 @@ class MessagingClient extends ChangeNotifier {
|
||||||
if (_refreshTimeout?.isActive == true) return;
|
if (_refreshTimeout?.isActive == true) return;
|
||||||
|
|
||||||
_autoRefresh?.cancel();
|
_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?.cancel();
|
||||||
_refreshTimeout = Timer(_refreshTimeoutDuration, () {});
|
_refreshTimeout = Timer(_refreshTimeoutDuration, () {});
|
||||||
|
|
||||||
|
@ -128,6 +214,7 @@ class MessagingClient extends ChangeNotifier {
|
||||||
_sortedFriendsCache.addAll(friends);
|
_sortedFriendsCache.addAll(friends);
|
||||||
_sortFriendsCache();
|
_sortFriendsCache();
|
||||||
_initStatus = "";
|
_initStatus = "";
|
||||||
|
await _storage.write(key: _storageLastUpdateKey, value: DateTime.now().toIso8601String());
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,9 +251,13 @@ class MessagingClient extends ChangeNotifier {
|
||||||
} else {
|
} else {
|
||||||
messages.add(message);
|
messages.add(message);
|
||||||
}
|
}
|
||||||
messages.sort();
|
|
||||||
_sortFriendsCache();
|
_sortFriendsCache();
|
||||||
_notificationClient.showUnreadMessagesNotification(messages.reversed);
|
if (!_settingsClient.currentSettings.notificationsDenied.valueOrDefault) {
|
||||||
|
updateNotified(messages).then((unnotified) {
|
||||||
|
unnotified.sort();
|
||||||
|
_notificationClient.showUnreadMessagesNotification(unnotified.reversed);
|
||||||
|
});
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -217,27 +308,6 @@ class MessagingClient extends ChangeNotifier {
|
||||||
|
|
||||||
MessageCache? getUserMessageCache(String userId) => _messageCache[userId];
|
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 {
|
void _onDisconnected(error) async {
|
||||||
_wsChannel = null;
|
_wsChannel = null;
|
||||||
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
|
_logger.warning("Neos Hub connection died with error '$error', reconnecting...");
|
||||||
|
@ -388,5 +458,10 @@ class MessagingClient extends ChangeNotifier {
|
||||||
};
|
};
|
||||||
_sendData(data);
|
_sendData(data);
|
||||||
clearUnreadsForUser(batch.senderId);
|
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,55 +39,57 @@ class NotificationClient {
|
||||||
uname.hashCode,
|
uname.hashCode,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
fln.NotificationDetails(android: fln.AndroidNotificationDetails(
|
fln.NotificationDetails(
|
||||||
_messageChannel.id,
|
android: fln.AndroidNotificationDetails(
|
||||||
_messageChannel.name,
|
_messageChannel.id,
|
||||||
channelDescription: _messageChannel.description,
|
_messageChannel.name,
|
||||||
importance: fln.Importance.high,
|
channelDescription: _messageChannel.description,
|
||||||
priority: fln.Priority.max,
|
importance: fln.Importance.high,
|
||||||
actions: [], //TODO: Make clicking message notification open chat of specified user.
|
priority: fln.Priority.max,
|
||||||
styleInformation: fln.MessagingStyleInformation(
|
actions: [],
|
||||||
fln.Person(
|
//TODO: Make clicking message notification open chat of specified user.
|
||||||
name: uname,
|
styleInformation: fln.MessagingStyleInformation(
|
||||||
bot: false,
|
fln.Person(
|
||||||
|
name: uname,
|
||||||
|
bot: false,
|
||||||
|
),
|
||||||
|
groupConversation: false,
|
||||||
|
messages: entry.value.map((message) {
|
||||||
|
String content;
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageType.unknown:
|
||||||
|
content = "Unknown Message Type";
|
||||||
|
break;
|
||||||
|
case MessageType.text:
|
||||||
|
content = message.content;
|
||||||
|
break;
|
||||||
|
case MessageType.sound:
|
||||||
|
content = "Audio Message";
|
||||||
|
break;
|
||||||
|
case MessageType.sessionInvite:
|
||||||
|
try {
|
||||||
|
final session = Session.fromMap(jsonDecode(message.content));
|
||||||
|
content = "Session Invite to ${session.name}";
|
||||||
|
} catch (e) {
|
||||||
|
content = "Session Invite";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MessageType.object:
|
||||||
|
content = "Asset";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return fln.Message(
|
||||||
|
content,
|
||||||
|
message.sendTime,
|
||||||
|
fln.Person(
|
||||||
|
name: uname,
|
||||||
|
bot: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
),
|
),
|
||||||
groupConversation: false,
|
|
||||||
messages: entry.value.map((message) {
|
|
||||||
String content;
|
|
||||||
switch (message.type) {
|
|
||||||
case MessageType.unknown:
|
|
||||||
content = "Unknown Message Type";
|
|
||||||
break;
|
|
||||||
case MessageType.text:
|
|
||||||
content = message.content;
|
|
||||||
break;
|
|
||||||
case MessageType.sound:
|
|
||||||
content = "Audio Message";
|
|
||||||
break;
|
|
||||||
case MessageType.sessionInvite:
|
|
||||||
try {
|
|
||||||
final session = Session.fromMap(jsonDecode(message.content));
|
|
||||||
content = "Session Invite to ${session.name}";
|
|
||||||
} catch (e) {
|
|
||||||
content = "Session Invite";
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MessageType.object:
|
|
||||||
content = "Asset";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return fln.Message(
|
|
||||||
content,
|
|
||||||
message.sendTime,
|
|
||||||
fln.Person(
|
|
||||||
name: uname,
|
|
||||||
bot: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,23 +21,37 @@ void main() async {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
if (Platform.isAndroid) {
|
if (Platform.isAndroid) {
|
||||||
await Workmanager().initialize(
|
await Workmanager().initialize(
|
||||||
callbackDispatcher, // The top level function, aka callbackDispatcher
|
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));
|
Logger.root.onRecord.listen((event) => log(event.message, name: event.loggerName, time: event.time));
|
||||||
final settingsClient = SettingsClient();
|
final settingsClient = SettingsClient();
|
||||||
await settingsClient.loadSettings();
|
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,)));
|
runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@pragma('vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+
|
@pragma('vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+
|
||||||
void callbackDispatcher() {
|
void callbackDispatcher() {
|
||||||
Workmanager().executeTask((String task, Map<String, dynamic>? inputData) async {
|
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.
|
debugPrint("Native called background task: $task"); //simpleTask will be emitted here.
|
||||||
if (task == MessagingClient.taskName) {
|
if (task == MessagingClient.unreadCheckTaskName) {
|
||||||
final unreads = MessagingClient.backgroundCheckUnreads(inputData);
|
try {
|
||||||
|
await MessagingClient.backgroundCheckUnreads(inputData);
|
||||||
|
} catch (e) {
|
||||||
|
Logger("Workman").severe(e);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return Future.value(true);
|
return Future.value(true);
|
||||||
});
|
});
|
||||||
|
@ -129,6 +143,7 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
||||||
MessagingClient(
|
MessagingClient(
|
||||||
apiClient: clientHolder.apiClient,
|
apiClient: clientHolder.apiClient,
|
||||||
notificationClient: clientHolder.notificationClient,
|
notificationClient: clientHolder.notificationClient,
|
||||||
|
settingsClient: widget.settingsClient,
|
||||||
),
|
),
|
||||||
child: const FriendsList(),
|
child: const FriendsList(),
|
||||||
) :
|
) :
|
||||||
|
|
Loading…
Reference in a new issue