✨ Notifications
This commit is contained in:
161
lib/providers/account.dart
Normal file
161
lib/providers/account.dart
Normal file
@ -0,0 +1,161 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:solian/models/notification.dart';
|
||||
import 'package:solian/models/packet.dart';
|
||||
import 'package:solian/models/pagination.dart';
|
||||
import 'package:solian/providers/auth.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:web_socket_channel/io.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
class AccountProvider extends GetxController {
|
||||
final FlutterLocalNotificationsPlugin localNotify =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
|
||||
RxBool isConnected = false.obs;
|
||||
RxBool isConnecting = false.obs;
|
||||
|
||||
RxInt notificationUnread = 0.obs;
|
||||
RxList<Notification> notifications =
|
||||
List<Notification>.empty(growable: true).obs;
|
||||
|
||||
IOWebSocketChannel? websocket;
|
||||
|
||||
@override
|
||||
onInit() {
|
||||
Permission.notification.request().then((status) {
|
||||
notifyInitialization();
|
||||
notifyPrefetch();
|
||||
});
|
||||
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
void connect({noRetry = false}) async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (!await auth.isAuthorized) throw Exception('unauthorized');
|
||||
|
||||
if (auth.credentials == null) await auth.loadCredentials();
|
||||
|
||||
final uri = Uri.parse(
|
||||
'${ServiceFinder.services['passport']}/api/ws?tk=${auth.credentials!.accessToken}'
|
||||
.replaceFirst('http', 'ws'),
|
||||
);
|
||||
|
||||
isConnecting.value = true;
|
||||
|
||||
try {
|
||||
websocket = IOWebSocketChannel.connect(uri);
|
||||
await websocket?.ready;
|
||||
} catch (e) {
|
||||
if (!noRetry) {
|
||||
await auth.refreshCredentials();
|
||||
return connect(noRetry: true);
|
||||
}
|
||||
}
|
||||
|
||||
listen();
|
||||
|
||||
isConnected.value = true;
|
||||
isConnecting.value = false;
|
||||
}
|
||||
|
||||
void disconnect() {
|
||||
websocket?.sink.close(WebSocketStatus.normalClosure);
|
||||
isConnected.value = false;
|
||||
}
|
||||
|
||||
void listen() {
|
||||
websocket?.stream.listen(
|
||||
(event) {
|
||||
final packet = NetworkPackage.fromJson(jsonDecode(event));
|
||||
switch (packet.method) {
|
||||
case 'notifications.new':
|
||||
final notification = Notification.fromJson(packet.payload!);
|
||||
notificationUnread++;
|
||||
notifications.add(notification);
|
||||
notifyMessage(notification.subject, notification.content);
|
||||
break;
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
isConnected.value = false;
|
||||
},
|
||||
onError: (err) {
|
||||
isConnected.value = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void notifyInitialization() {
|
||||
const androidSettings = AndroidInitializationSettings('app_icon');
|
||||
const darwinSettings = DarwinInitializationSettings(
|
||||
notificationCategories: [
|
||||
DarwinNotificationCategory('general'),
|
||||
],
|
||||
);
|
||||
const linuxSettings =
|
||||
LinuxInitializationSettings(defaultActionName: 'Open notification');
|
||||
const InitializationSettings initializationSettings =
|
||||
InitializationSettings(
|
||||
android: androidSettings,
|
||||
iOS: darwinSettings,
|
||||
macOS: darwinSettings,
|
||||
linux: linuxSettings,
|
||||
);
|
||||
|
||||
localNotify.initialize(initializationSettings);
|
||||
}
|
||||
|
||||
void notifyMessage(String title, String body) {
|
||||
const androidSettings = AndroidNotificationDetails(
|
||||
'general',
|
||||
'General',
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
silent: true,
|
||||
);
|
||||
const darwinSettings = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBanner: true,
|
||||
presentBadge: true,
|
||||
presentSound: false,
|
||||
);
|
||||
const linuxSettings = LinuxNotificationDetails();
|
||||
|
||||
localNotify.show(
|
||||
math.max(1, math.Random().nextInt(100000000)),
|
||||
title,
|
||||
body,
|
||||
const NotificationDetails(
|
||||
android: androidSettings,
|
||||
iOS: darwinSettings,
|
||||
macOS: darwinSettings,
|
||||
linux: linuxSettings,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> notifyPrefetch() async {
|
||||
final AuthProvider auth = Get.find();
|
||||
if (!await auth.isAuthorized) return;
|
||||
|
||||
final client = GetConnect();
|
||||
client.httpClient.baseUrl = ServiceFinder.services['passport'];
|
||||
client.httpClient.addAuthenticator(auth.requestAuthenticator);
|
||||
|
||||
final resp = await client.get('/api/notifications?skip=0&take=100');
|
||||
if (resp.statusCode == 200) {
|
||||
final result = PaginationResult.fromJson(resp.body);
|
||||
final data = result.data?.map((x) => Notification.fromJson(x)).toList();
|
||||
if (data != null) {
|
||||
notifications.addAll(data);
|
||||
notificationUnread.value = data.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get/get_connect/http/src/request/request.dart';
|
||||
import 'package:solian/providers/account.dart';
|
||||
import 'package:solian/services.dart';
|
||||
import 'package:oauth2/oauth2.dart' as oauth2;
|
||||
|
||||
@ -25,26 +26,30 @@ class AuthProvider extends GetConnect {
|
||||
|
||||
oauth2.Credentials? credentials;
|
||||
|
||||
Future<void> refreshCredentials() async {
|
||||
final resp = await post('/api/auth/token', {
|
||||
'refresh_token': credentials!.refreshToken,
|
||||
'grant_type': 'refresh_token',
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
}
|
||||
credentials = oauth2.Credentials(
|
||||
resp.body['access_token'],
|
||||
refreshToken: resp.body['refresh_token'],
|
||||
idToken: resp.body['access_token'],
|
||||
tokenEndpoint: tokenEndpoint,
|
||||
expiration: DateTime.now().add(const Duration(minutes: 3)),
|
||||
);
|
||||
storage.write(
|
||||
key: 'auth_credentials',
|
||||
value: jsonEncode(credentials!.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Request<T?>> requestAuthenticator<T>(Request<T?> request) async {
|
||||
if (credentials != null && credentials!.isExpired) {
|
||||
final resp = await post('/api/auth/token', {
|
||||
'refresh_token': credentials!.refreshToken,
|
||||
'grant_type': 'refresh_token',
|
||||
});
|
||||
if (resp.statusCode != 200) {
|
||||
throw Exception(resp.bodyString);
|
||||
}
|
||||
credentials = oauth2.Credentials(
|
||||
resp.body['access_token'],
|
||||
refreshToken: resp.body['refresh_token'],
|
||||
idToken: resp.body['access_token'],
|
||||
tokenEndpoint: tokenEndpoint,
|
||||
expiration: DateTime.now().add(const Duration(minutes: 3)),
|
||||
);
|
||||
storage.write(
|
||||
key: 'auth_credentials',
|
||||
value: jsonEncode(credentials!.toJson()),
|
||||
);
|
||||
refreshCredentials();
|
||||
}
|
||||
|
||||
if (credentials != null) {
|
||||
@ -91,12 +96,16 @@ class AuthProvider extends GetConnect {
|
||||
value: jsonEncode(credentials!.toJson()),
|
||||
);
|
||||
|
||||
Get.find<AccountProvider>().connect();
|
||||
|
||||
return credentials!;
|
||||
}
|
||||
|
||||
void signout() {
|
||||
_cacheUserProfileResponse = null;
|
||||
|
||||
Get.find<AccountProvider>().disconnect();
|
||||
|
||||
storage.deleteAll();
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user