From e9ea573e33378f8011810c302e07f3ad5e1f9856 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sat, 15 Jun 2024 21:29:51 +0200 Subject: [PATCH] Notifications (android via local) work --- flutter/android/app/build.gradle | 9 +- flutter/android/app/proguard-rules.pro | 27 +++ .../android/app/src/main/AndroidManifest.xml | 3 + .../drawable-hdpi/ic_notification_white.png | Bin 0 -> 472 bytes .../drawable-mdpi/ic_notification_white.png | Bin 0 -> 320 bytes .../drawable-v24/ic_launcher_foreground.xml | 34 ++++ .../drawable-xhdpi/ic_notification_white.png | Bin 0 -> 551 bytes .../drawable-xxhdpi/ic_notification_white.png | Bin 0 -> 949 bytes .../ic_notification_white.png | Bin 0 -> 1067 bytes flutter/android/app/src/main/res/raw/keep.xml | 2 + flutter/android/build.gradle | 1 + flutter/ios/Runner/AppDelegate.swift | 14 ++ flutter/lib/api/api_client.dart | 10 +- flutter/lib/components/layout/app_bar.dart | 8 +- flutter/lib/components/layout/scaffold.dart | 3 - flutter/lib/main.dart | 162 ++++++++++++++++-- .../models/{message.dart => scn_message.dart} | 14 +- .../{message.g.dart => scn_message.g.dart} | 12 +- flutter/lib/nav_layout.dart | 1 - .../pages/channel_list/channel_list_item.dart | 4 +- flutter/lib/pages/debug/debug_actions.dart | 7 + flutter/lib/pages/debug/debug_main.dart | 1 - .../lib/pages/debug/debug_persistence.dart | 6 +- .../pages/debug/debug_persistence_hive.dart | 6 +- .../debug/debug_persistence_hiveentry.dart | 4 +- .../debug/debug_persistence_sharedprefs.dart | 1 - .../lib/pages/debug/debug_request_view.dart | 1 - .../lib/pages/message_list/message_list.dart | 29 ++-- .../pages/message_list/message_list_item.dart | 8 +- .../lib/pages/message_view/message_view.dart | 22 +-- flutter/lib/settings/app_settings.dart | 33 ++++ flutter/lib/state/application_log.dart | 5 + flutter/lib/state/fb_message.dart | 19 +- flutter/lib/state/fb_message.g.dart | 12 +- flutter/lib/state/globals.dart | 9 + flutter/lib/utils/notifier.dart | 70 ++++++++ .../Flutter/GeneratedPluginRegistrant.swift | 2 + flutter/pubspec.lock | 40 +++++ flutter/pubspec.yaml | 1 + 39 files changed, 476 insertions(+), 104 deletions(-) create mode 100644 flutter/android/app/proguard-rules.pro create mode 100644 flutter/android/app/src/main/res/drawable-hdpi/ic_notification_white.png create mode 100644 flutter/android/app/src/main/res/drawable-mdpi/ic_notification_white.png create mode 100644 flutter/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 flutter/android/app/src/main/res/drawable-xhdpi/ic_notification_white.png create mode 100644 flutter/android/app/src/main/res/drawable-xxhdpi/ic_notification_white.png create mode 100644 flutter/android/app/src/main/res/drawable-xxxhdpi/ic_notification_white.png create mode 100644 flutter/android/app/src/main/res/raw/keep.xml rename flutter/lib/models/{message.dart => scn_message.dart} (84%) rename flutter/lib/models/{message.g.dart => scn_message.g.dart} (88%) create mode 100644 flutter/lib/settings/app_settings.dart create mode 100644 flutter/lib/utils/notifier.dart diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle index 10a233e..2130f2f 100644 --- a/flutter/android/app/build.gradle +++ b/flutter/android/app/build.gradle @@ -37,6 +37,7 @@ android { ndkVersion flutter.ndkVersion compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -55,6 +56,7 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true } signingConfigs { @@ -77,4 +79,9 @@ flutter { source '../..' } -dependencies {} +dependencies { + implementation 'androidx.window:window:1.0.0' + implementation 'androidx.window:window-java:1.0.0' + + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' +} diff --git a/flutter/android/app/proguard-rules.pro b/flutter/android/app/proguard-rules.pro new file mode 100644 index 0000000..f8768e4 --- /dev/null +++ b/flutter/android/app/proguard-rules.pro @@ -0,0 +1,27 @@ +## Gson rules +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken \ No newline at end of file diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml index 629c8ae..29a0228 100644 --- a/flutter/android/app/src/main/AndroidManifest.xml +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -31,6 +31,9 @@ + + + 6P)Nklg+M-Q4 zI4WX|?=Qb6L(lW<^Rmy3!+h)0KEG{u=JU+V&NCZ}nV6WE_=^yt5p;ka&Bobf}T=1IM3MsS>7K6&PfpcECSzfS7zP9M~S=0AV zK|_$~A(=ht?y9^{!;Da#+t~!;p0@;ia2MQxVnjk#XiFIMvd0KTLaX2&jJgRefq5_m z($f6bYYkbfj>Eirf#S;3QPiBvf(|y3`~@@5k+gHb74C8lvlCC!q;V zBXsLDWzFD8gV3_uPk#uM6$-u3qpH>hNppP$=C~nRS1AwYAGbQi?O62yxTce*pTk(0 z@9Lc!b2}l{s&m|qRX;+qag00w O0000scEnV5YGeRPe6PRioXHz5+F9A)bJ=E-Uq~aK)f7?yPxxy%rRw=y7nH%04hckB2ZIUO^=b&Vn3` zEj8r;@eT@uXdw`5;>!>EKs*hImjUq=C|(bx4?!`q7>s=sh+BYIWVG%XVE_OUKbEM; S_AuoD0000 + + + + + + + + + + diff --git a/flutter/android/app/src/main/res/drawable-xhdpi/ic_notification_white.png b/flutter/android/app/src/main/res/drawable-xhdpi/ic_notification_white.png new file mode 100644 index 0000000000000000000000000000000000000000..125fc41ebd952bc289eb532e13cbe991420e83f1 GIT binary patch literal 551 zcmV+?0@(eDP)>EA7AHgi*&}3;4Y@9{{Id6SSon))^;^#{=}4 z0%5PrcY;@NWH7}OAPK&~gxltuz>8s61UZI~v6h7xaLarNjbTk-0H$Y9>!k4t&<{4i zrvm}**ni0h<9-kWA9Nu$Znqr>!0HY_gGloc@Bn7$qCxrt2GYLkRA)HGTRRDsM44Zp z8OH8i<*hvf^`Ov>`C-APa;d!mQBcWiz6v}^5J1;8=;aBp?1Cwt+7arlhO#kVL=9Km z#_H-+?KxXOtpc!q=2p@GtjkSW1sHW{SlFp;Gz&1B2LYz?J-~Xt521uvfL5?CxIGYA z8!>J8Xd1nj{0T&@W}-#O19Vvh@PS8316;CElFf=dNIwWk79h;i6V1^(on4PtV%8nJ znNFV;(`w4&0DE8!6z94(sG}3S(_>^O+{y5$$1)sC5V_W}cTOQuJG64pN1Kcoz07g~ pcC)5IhpkTg69ppP-;oYaIVW&%hXR$K1(Ga+5hfd=PSPZqEGoF?Y_~vNDlK zBoc{4B9TZW5{X2jtYul&1h4?C20K7I=m5Jx6Icu;h0T;o&;l-lx7NRU!8r8|^n-n1 zekmJM1&)CCmRg;RN@L(ESXye+s11AqufXdZ40;OgffR^=>S8yi2HXaJxc3`CoIhtG z%Bafew-NsQ8@vIH#cB>Mu+PDhAf^P`oCYp|Utm|UnX?3Z;AxR^jDU!bBgn?$5!zNP z=F9~z6>CTX-;unoLH*oDCQ(N;n{G}yxa)F(K^jE*0v4HU&|b@3K^sIGq>eJpm|0*0 z_*!6tXf66lyK+<38oHQAJpW03eEThwL9}Qe2P0r&p^TXgPJv{4a9loCV1wpxhixu| zInCf5cm}p{ur?8{6G{8t$qZ2MQeMlb%ImoeRkH@v1q4DkfXLmG%Gr=HR zltDP{`y&q;T*#ZRT!J&fAm?uT3Yh1YK{tF1&IE%jPP@>ETAQmmD;$e*DH*g5409G> zV=e|=2p~8U406Umd%2Zf%&FkVhL|JhR^Fp@Dv0uLqb_x7xgAxxy`NE)`yIm|XDO%X z8nlKTK33ueo#g9`>Kb$~55wDpDQ4@V)+j>dvmrBx)S_chZwL)Kr(@7S2n|Z;7<4m) z1|?+;`hN^n*&5XDcqgu#YA_+)p&P>kHfUzH2Cd@UZli}}l?e$m-5B=hPO-_3r}(f; zv0>fKVk~3^_2?S3tVEyYRWWSx^3ZxMBnHtzt_m*((eb#?Au#AbE_tD?JkQ0DQW<8((NZSRz>A?`tyMi{qO zHbXAu^xwBK#4ySSF6s(-2AXNklcJ0?VA^d#14WGGZsZ@ul_uqGjPAPm~;ky=gX(xgC9n!upQ}s4pqfl zEZY@+4nN!0iPkFJ$X>Kq6Xks>>hH(!*U2f`*{QW6Afc*wPs{#g`z`z$`D4EVc3E-j zt@2a&+vQKS$XO7D0z8!Q=4t$t=a<%OUcf_j>ur-Rm3iE~63_4_{P!#$i)aDt+=$Cq zf^?)^h)Aezx-CwUMtfO+!ia^c;(aWiFGw@J8n?ZN0`v$^6swl)ucRE)1y~ZnP<6cA zi8tM30Zt)QWiPj$(@cb)DsyO>L)5zU+Pg=JaM(lvmdckML;)Oo^K3oBvvmtljmwIb z_pELKnh>g%mq*qzbqa6?p=x`pbPDhmp=x`t>J*?Ip=x`7kcOfFiS=f=EkGMWHR8>6 zTY%dL)rhy$Z2=k)suAy6w*{z_FDOGx*p+vadMP~X{n}$CeI*L zWp9nivEdA>V*wiq;LOWM|47p_B=TjDfhj-%diAdMYLeCl`GF_4Z4{s@$Gx7axK)0L z&xZYI3IDx3Kk6KSAq|hme9;nVCG(iU-Ut_97U`mde~FYH&l%HMeRNqz;S$t&Pi%5=b_!VOtBRqJ+lX92)VdYGk6r#18KgkOk_Bq_4YODA8NPh8h@a5cL>9_S`+;zdP zj^uS@mr3ufaKQIEUY5mgV&oZvgMzYvn-_PCBh9cnULjc!Fy&YsYr|5K0000000000 l000000000000005{s2NxT@`miEHwZC002ovPDHLkV1kpM>~R19 literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/raw/keep.xml b/flutter/android/app/src/main/res/raw/keep.xml new file mode 100644 index 0000000..578b09c --- /dev/null +++ b/flutter/android/app/src/main/res/raw/keep.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/flutter/android/build.gradle b/flutter/android/build.gradle index e83fb5d..b7c2752 100644 --- a/flutter/android/build.gradle +++ b/flutter/android/build.gradle @@ -6,6 +6,7 @@ buildscript { } dependencies { + classpath 'com.android.tools.build:gradle:7.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift index 70693e4..749b061 100644 --- a/flutter/ios/Runner/AppDelegate.swift +++ b/flutter/ios/Runner/AppDelegate.swift @@ -1,13 +1,27 @@ import UIKit import Flutter +import flutter_local_notifications @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + + // This is required to make any communication available in the action isolate. + FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in + GeneratedPluginRegistrant.register(with: registry) + } + + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + } + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + } diff --git a/flutter/lib/api/api_client.dart b/flutter/lib/api/api_client.dart index 7d4d8d8..ceb3f25 100644 --- a/flutter/lib/api/api_client.dart +++ b/flutter/lib/api/api_client.dart @@ -11,7 +11,7 @@ import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/models/channel.dart'; -import 'package:simplecloudnotifier/models/message.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/state/token_source.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; @@ -211,7 +211,7 @@ class APIClient { ); } - static Future<(String, List)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List? channelIDs}) async { + static Future<(String, List)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List? channelIDs}) async { return await _request( name: 'getMessageList', method: 'GET', @@ -221,18 +221,18 @@ class APIClient { if (pageSize != null) 'page_size': pageSize.toString(), if (channelIDs != null) 'channel_id': channelIDs.join(","), }, - fn: (json) => Message.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), + fn: (json) => SCNMessage.fromPaginatedJsonArray(json, 'messages', 'next_page_token'), authToken: auth.getToken(), ); } - static Future getMessage(TokenSource auth, String msgid) async { + static Future getMessage(TokenSource auth, String msgid) async { return await _request( name: 'getMessage', method: 'GET', relURL: 'messages/$msgid', query: {}, - fn: Message.fromJson, + fn: SCNMessage.fromJson, authToken: auth.getToken(), ); } diff --git a/flutter/lib/components/layout/app_bar.dart b/flutter/lib/components/layout/app_bar.dart index 165e0fc..c0e63ed 100644 --- a/flutter/lib/components/layout/app_bar.dart +++ b/flutter/lib/components/layout/app_bar.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/components/layout/app_bar_progress_indicator.dart'; import 'package:simplecloudnotifier/pages/debug/debug_main.dart'; +import 'package:simplecloudnotifier/settings/app_settings.dart'; import 'package:simplecloudnotifier/state/app_theme.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; @@ -12,7 +12,6 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget { Key? key, required this.title, required this.showThemeSwitch, - required this.showDebug, required this.showSearch, required this.showShare, this.onShare = null, @@ -20,16 +19,17 @@ class SCNAppBar extends StatelessWidget implements PreferredSizeWidget { final String? title; final bool showThemeSwitch; - final bool showDebug; final bool showSearch; final bool showShare; final void Function()? onShare; @override Widget build(BuildContext context) { + final cfg = Provider.of(context); + var actions = []; - if (showDebug) { + if (cfg.showDebugButton) { actions.add(IconButton( icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow), tooltip: 'Debug', diff --git a/flutter/lib/components/layout/scaffold.dart b/flutter/lib/components/layout/scaffold.dart index ad8aeed..aaf44fc 100644 --- a/flutter/lib/components/layout/scaffold.dart +++ b/flutter/lib/components/layout/scaffold.dart @@ -7,7 +7,6 @@ class SCNScaffold extends StatelessWidget { required this.child, this.title, this.showThemeSwitch = true, - this.showDebug = true, this.showSearch = true, this.showShare = false, this.onShare = null, @@ -16,7 +15,6 @@ class SCNScaffold extends StatelessWidget { final Widget child; final String? title; final bool showThemeSwitch; - final bool showDebug; final bool showSearch; final bool showShare; final void Function()? onShare; @@ -27,7 +25,6 @@ class SCNScaffold extends StatelessWidget { appBar: SCNAppBar( title: title, showThemeSwitch: showThemeSwitch, - showDebug: showDebug, showSearch: showSearch, showShare: showShare, onShare: onShare ?? () {}, diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 8c960c5..6d540c2 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -2,13 +2,15 @@ import 'dart:io'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:provider/provider.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/client.dart'; -import 'package:simplecloudnotifier/models/message.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/nav_layout.dart'; +import 'package:simplecloudnotifier/settings/app_settings.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/app_theme.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; @@ -18,6 +20,7 @@ import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; +import 'package:simplecloudnotifier/utils/notifier.dart'; import 'package:toastification/toastification.dart'; import 'firebase_options.dart'; @@ -39,20 +42,10 @@ void main() async { Hive.registerAdapter(SCNRequestAdapter()); Hive.registerAdapter(SCNLogAdapter()); Hive.registerAdapter(SCNLogLevelAdapter()); - Hive.registerAdapter(MessageAdapter()); + Hive.registerAdapter(SCNMessageAdapter()); Hive.registerAdapter(ChannelAdapter()); Hive.registerAdapter(FBMessageAdapter()); - print('[INIT] Load Hive...'); - - try { - await Hive.openBox('scn-requests'); - } catch (exc, trace) { - Hive.deleteBoxFromDisk('scn-requests'); - await Hive.openBox('scn-requests'); - ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace); - } - print('[INIT] Load Hive...'); try { @@ -63,13 +56,23 @@ void main() async { ApplicationLog.error('Failed to open Hive-Box: scn-logs: ' + exc.toString(), trace: trace); } + print('[INIT] Load Hive...'); + + try { + await Hive.openBox('scn-requests'); + } catch (exc, trace) { + Hive.deleteBoxFromDisk('scn-requests'); + await Hive.openBox('scn-requests'); + ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace); + } + print('[INIT] Load Hive...'); try { - await Hive.openBox('scn-message-cache'); + await Hive.openBox('scn-message-cache'); } catch (exc, trace) { Hive.deleteBoxFromDisk('scn-message-cache'); - await Hive.openBox('scn-message-cache'); + await Hive.openBox('scn-message-cache'); ApplicationLog.error('Failed to open Hive-Box: scn-message-cache' + exc.toString(), trace: trace); } @@ -147,6 +150,38 @@ void main() async { print('[INIT] Skip Firebase init (Platform == Linux)...'); } + print('[INIT] Load Notifications...'); + + final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + final flutterLocalNotificationsPluginImpl = flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation(); + if (flutterLocalNotificationsPluginImpl == null) { + ApplicationLog.error('Failed to get AndroidFlutterLocalNotificationsPlugin', trace: StackTrace.current); + } else { + flutterLocalNotificationsPluginImpl.requestNotificationsPermission(); + + final initializationSettingsAndroid = AndroidInitializationSettings('ic_notification_white'); + final initializationSettingsDarwin = DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + onDidReceiveLocalNotification: _receiveLocalDarwinNotification, + notificationCategories: getDarwinNotificationCategories(), + ); + final initializationSettingsLinux = LinuxInitializationSettings(defaultActionName: 'Open notification'); + final initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsDarwin, + linux: initializationSettingsLinux, + ); + flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: _receiveLocalNotification); + + final appLaunchNotification = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); + if (appLaunchNotification != null) { + //TODO show message (also this only works on android+localnotifications, also handle ios) + ApplicationLog.info('App launched by notification: ${appLaunchNotification.notificationResponse?.id}'); + } + } + ApplicationLog.debug('[INIT] Application started'); runApp( @@ -155,6 +190,7 @@ void main() async { ChangeNotifierProvider(create: (context) => AppAuth(), lazy: false), ChangeNotifierProvider(create: (context) => AppTheme(), lazy: false), ChangeNotifierProvider(create: (context) => AppBarState(), lazy: false), + ChangeNotifierProvider(create: (context) => AppSettings(), lazy: false), ], child: SCNApp(), ), @@ -188,6 +224,11 @@ class SCNApp extends StatelessWidget { } } +@pragma('vm:entry-point') +void notificationTapBackground(NotificationResponse notificationResponse) { + ApplicationLog.info('Received local notification: ${notificationResponse.id}'); +} + void setFirebaseToken(String fcmToken) async { final acc = AppAuth(); @@ -233,9 +274,96 @@ void _onForegroundMessage(RemoteMessage message) { } Future _receiveMessage(RemoteMessage message, bool foreground) async { - // ensure init - Hive.openBox('scn-logs'); + try { + // ensure globals init + if (!Globals().isInitialized) { + print('[LATE-INIT] Init Globals() - to ensure working _receiveMessage($foreground)...'); + await Globals().init(); + } + + // ensure hive init + if (!Hive.isBoxOpen('scn-logs')) { + print('[LATE-INIT] Init Hive - to ensure working _receiveMessage($foreground)...'); + + await Hive.initFlutter(); + Hive.registerAdapter(SCNRequestAdapter()); + Hive.registerAdapter(SCNLogAdapter()); + Hive.registerAdapter(SCNLogLevelAdapter()); + Hive.registerAdapter(SCNMessageAdapter()); + Hive.registerAdapter(ChannelAdapter()); + Hive.registerAdapter(FBMessageAdapter()); + } + + print('[LATE-INIT] Ensure hive boxes are open for _receiveMessage($foreground)...'); + + await Hive.openBox('scn-logs'); + await Hive.openBox('scn-fb-messages'); + await Hive.openBox('scn-message-cache'); + } catch (exc, trace) { + ApplicationLog.error('Failed to init hive:' + exc.toString(), trace: trace); + Notifier.showLocalNotification("@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null); + return; + } ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}'); - FBMessageLog.insert(message); + + SCNMessage? receivedMessage; + + try { + final scn_msg_id = message.data['scn_msg_id'] as String; + final usr_msg_id = message.data['usr_msg_id'] as String; + final timestamp = DateTime.fromMillisecondsSinceEpoch(int.parse(message.data['timestamp'] as String) * 1000); + final priority = int.parse(message.data['priority'] as String); + final title = message.data['title'] as String; + final channel = message.data['channel'] as String; + final channel_id = message.data['channel_id'] as String; + final body = message.data['body'] as String; + + Notifier.showLocalNotification(channel_id, channel, 'Channel: ${channel}', title, body, timestamp); + + receivedMessage = SCNMessage( + messageID: scn_msg_id, + userMessageID: usr_msg_id, + timestamp: timestamp.toIso8601String(), + priority: priority, + trimmed: true, + title: title, + channelID: channel_id, + channelInternalName: channel, + content: body, + senderIP: '', + senderName: '', + senderUserID: '', + usedKeyID: '', + ); + } catch (exc, trace) { + ApplicationLog.error('Failed to decode received FB message' + exc.toString(), trace: trace); + Notifier.showLocalNotification("@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null); + return; + } + + try { + FBMessageLog.insert(message); + } catch (exc, trace) { + ApplicationLog.error('Failed to persist received FB message' + exc.toString(), trace: trace); + Notifier.showLocalNotification("@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null); + return; + } + + //TODO add to scn-message-cache + //TODO refresh message_list view (if shown/initialized) +} + +void _receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) { + ApplicationLog.info('Received local notification: $id -> [$title]'); +} + +void _receiveLocalNotification(NotificationResponse details) { + ApplicationLog.info('Received local notification: ${details.id}'); +} + +List getDarwinNotificationCategories() { + return [ + //TODO ?!? + ]; } diff --git a/flutter/lib/models/message.dart b/flutter/lib/models/scn_message.dart similarity index 84% rename from flutter/lib/models/message.dart rename to flutter/lib/models/scn_message.dart index 4d4b18c..1b2a8b1 100644 --- a/flutter/lib/models/message.dart +++ b/flutter/lib/models/scn_message.dart @@ -1,10 +1,10 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:simplecloudnotifier/state/interfaces.dart'; -part 'message.g.dart'; +part 'scn_message.g.dart'; @HiveType(typeId: 105) -class Message extends HiveObject implements FieldDebuggable { +class SCNMessage extends HiveObject implements FieldDebuggable { @HiveField(0) final String messageID; @@ -33,7 +33,7 @@ class Message extends HiveObject implements FieldDebuggable { @HiveField(21) final bool trimmed; - Message({ + SCNMessage({ required this.messageID, required this.senderUserID, required this.channelInternalName, @@ -49,8 +49,8 @@ class Message extends HiveObject implements FieldDebuggable { required this.trimmed, }); - factory Message.fromJson(Map json) { - return Message( + factory SCNMessage.fromJson(Map json) { + return SCNMessage( messageID: json['message_id'] as String, senderUserID: json['sender_user_id'] as String, channelInternalName: json['channel_internal_name'] as String, @@ -67,10 +67,10 @@ class Message extends HiveObject implements FieldDebuggable { ); } - static (String, List) fromPaginatedJsonArray(Map data, String keyMessages, String keyToken) { + static (String, List) fromPaginatedJsonArray(Map data, String keyMessages, String keyToken) { final npt = data[keyToken] as String; - final messages = (data[keyMessages] as List).map((e) => Message.fromJson(e as Map)).toList(); + final messages = (data[keyMessages] as List).map((e) => SCNMessage.fromJson(e as Map)).toList(); return (npt, messages); } diff --git a/flutter/lib/models/message.g.dart b/flutter/lib/models/scn_message.g.dart similarity index 88% rename from flutter/lib/models/message.g.dart rename to flutter/lib/models/scn_message.g.dart index 6bfc06a..cf8746d 100644 --- a/flutter/lib/models/message.g.dart +++ b/flutter/lib/models/scn_message.g.dart @@ -1,22 +1,22 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'message.dart'; +part of 'scn_message.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** -class MessageAdapter extends TypeAdapter { +class SCNMessageAdapter extends TypeAdapter { @override final int typeId = 105; @override - Message read(BinaryReader reader) { + SCNMessage read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; - return Message( + return SCNMessage( messageID: fields[0] as String, senderUserID: fields[10] as String, channelInternalName: fields[11] as String, @@ -34,7 +34,7 @@ class MessageAdapter extends TypeAdapter { } @override - void write(BinaryWriter writer, Message obj) { + void write(BinaryWriter writer, SCNMessage obj) { writer ..writeByte(13) ..writeByte(0) @@ -71,7 +71,7 @@ class MessageAdapter extends TypeAdapter { @override bool operator ==(Object other) => identical(this, other) || - other is MessageAdapter && + other is SCNMessageAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } diff --git a/flutter/lib/nav_layout.dart b/flutter/lib/nav_layout.dart index b5265e7..b08b90f 100644 --- a/flutter/lib/nav_layout.dart +++ b/flutter/lib/nav_layout.dart @@ -59,7 +59,6 @@ class _SCNNavLayoutState extends State { return Scaffold( appBar: SCNAppBar( title: null, - showDebug: true, showSearch: _selectedIndex == 0 || _selectedIndex == 1, showShare: false, showThemeSwitch: true, diff --git a/flutter/lib/pages/channel_list/channel_list_item.dart b/flutter/lib/pages/channel_list/channel_list_item.dart index a400015..ddadde7 100644 --- a/flutter/lib/pages/channel_list/channel_list_item.dart +++ b/flutter/lib/pages/channel_list/channel_list_item.dart @@ -3,7 +3,7 @@ import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; -import 'package:simplecloudnotifier/models/message.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; class ChannelListItem extends StatefulWidget { @@ -23,7 +23,7 @@ class ChannelListItem extends StatefulWidget { } class _ChannelListItemState extends State { - Message? lastMessage; + SCNMessage? lastMessage; @override void initState() { diff --git a/flutter/lib/pages/debug/debug_actions.dart b/flutter/lib/pages/debug/debug_actions.dart index 79d0937..e658e0e 100644 --- a/flutter/lib/pages/debug/debug_actions.dart +++ b/flutter/lib/pages/debug/debug_actions.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:simplecloudnotifier/utils/notifier.dart'; import 'package:simplecloudnotifier/utils/toaster.dart'; import 'package:simplecloudnotifier/utils/ui.dart'; @@ -52,6 +53,12 @@ class _DebugActionsPageState extends State { onPressed: _sendTokenToServer, text: 'Send FCM Token to Server', ), + SizedBox(height: 20), + UI.button( + big: false, + onPressed: () => Notifier.showLocalNotification('TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null), + text: 'Show local notification', + ), ], ), ), diff --git a/flutter/lib/pages/debug/debug_main.dart b/flutter/lib/pages/debug/debug_main.dart index 3b0b957..bbbd48a 100644 --- a/flutter/lib/pages/debug/debug_main.dart +++ b/flutter/lib/pages/debug/debug_main.dart @@ -30,7 +30,6 @@ class _DebugMainPageState extends State { return SCNScaffold( title: 'Debug', showSearch: false, - showDebug: false, child: Column( children: [ Padding( diff --git a/flutter/lib/pages/debug/debug_persistence.dart b/flutter/lib/pages/debug/debug_persistence.dart index 44d3328..1daaec9 100644 --- a/flutter/lib/pages/debug/debug_persistence.dart +++ b/flutter/lib/pages/debug/debug_persistence.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:simplecloudnotifier/models/channel.dart'; -import 'package:simplecloudnotifier/models/message.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/pages/debug/debug_persistence_hive.dart'; import 'package:simplecloudnotifier/pages/debug/debug_persistence_sharedprefs.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; @@ -36,7 +36,7 @@ class _DebugPersistencePageState extends State { _buildSharedPrefCard(context), _buildHiveCard(context, () => Hive.box('scn-requests'), 'scn-requests'), _buildHiveCard(context, () => Hive.box('scn-logs'), 'scn-logs'), - _buildHiveCard(context, () => Hive.box('scn-message-cache'), 'scn-message-cache'), + _buildHiveCard(context, () => Hive.box('scn-message-cache'), 'scn-message-cache'), _buildHiveCard(context, () => Hive.box('scn-channel-cache'), 'scn-channel-cache'), _buildHiveCard(context, () => Hive.box('scn-fb-messages'), 'scn-fb-messages'), ], @@ -71,7 +71,7 @@ class _DebugPersistencePageState extends State { padding: const EdgeInsets.all(8.0), child: GestureDetector( onTap: () { - Navi.push(context, () => DebugHiveBoxPage(boxName: boxname, box: Hive.box(boxname))); + Navi.push(context, () => DebugHiveBoxPage(boxName: boxname, box: boxFunc())); }, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, diff --git a/flutter/lib/pages/debug/debug_persistence_hive.dart b/flutter/lib/pages/debug/debug_persistence_hive.dart index 12aefd2..1aca642 100644 --- a/flutter/lib/pages/debug/debug_persistence_hive.dart +++ b/flutter/lib/pages/debug/debug_persistence_hive.dart @@ -16,7 +16,6 @@ class DebugHiveBoxPage extends StatelessWidget { return SCNScaffold( title: 'Hive: ' + boxName, showSearch: false, - showDebug: false, child: ListView.separated( itemCount: box.length, itemBuilder: (context, listIndex) { @@ -24,8 +23,9 @@ class DebugHiveBoxPage extends StatelessWidget { onTap: () { Navi.push(context, () => DebugHiveEntryPage(value: box.getAt(listIndex)!)); }, - child: ListTile( - title: Text(box.getAt(listIndex).toString(), style: TextStyle(fontWeight: FontWeight.bold)), + child: Container( + padding: EdgeInsets.fromLTRB(8, 4, 8, 4), + child: Text(box.getAt(listIndex).toString(), style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12)), ), ); }, diff --git a/flutter/lib/pages/debug/debug_persistence_hiveentry.dart b/flutter/lib/pages/debug/debug_persistence_hiveentry.dart index 52a2e52..6cd975f 100644 --- a/flutter/lib/pages/debug/debug_persistence_hiveentry.dart +++ b/flutter/lib/pages/debug/debug_persistence_hiveentry.dart @@ -13,11 +13,13 @@ class DebugHiveEntryPage extends StatelessWidget { return SCNScaffold( title: 'HiveEntry', showSearch: false, - showDebug: false, child: ListView.separated( itemCount: fields.length, itemBuilder: (context, listIndex) { return ListTile( + dense: true, + contentPadding: EdgeInsets.fromLTRB(8, 0, 8, 0), + visualDensity: VisualDensity(horizontal: 0, vertical: -4), title: Text(fields[listIndex].$1, style: TextStyle(fontWeight: FontWeight.bold)), subtitle: Text(fields[listIndex].$2, style: TextStyle(fontFamily: "monospace")), ); diff --git a/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart b/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart index d927dbe..c0cadab 100644 --- a/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart +++ b/flutter/lib/pages/debug/debug_persistence_sharedprefs.dart @@ -13,7 +13,6 @@ class DebugSharedPrefPage extends StatelessWidget { return SCNScaffold( title: 'SharedPreferences', showSearch: false, - showDebug: false, child: ListView.separated( itemCount: sharedPref.getKeys().length, itemBuilder: (context, listIndex) { diff --git a/flutter/lib/pages/debug/debug_request_view.dart b/flutter/lib/pages/debug/debug_request_view.dart index b6c7eed..9e5d804 100644 --- a/flutter/lib/pages/debug/debug_request_view.dart +++ b/flutter/lib/pages/debug/debug_request_view.dart @@ -16,7 +16,6 @@ class DebugRequestViewPage extends StatelessWidget { return SCNScaffold( title: 'Request', showSearch: false, - showDebug: false, child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index 8e91212..fe094dc 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -4,8 +4,9 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:provider/provider.dart'; import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/models/channel.dart'; -import 'package:simplecloudnotifier/models/message.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/pages/message_view/message_view.dart'; +import 'package:simplecloudnotifier/settings/app_settings.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; @@ -25,11 +26,9 @@ class MessageListPage extends StatefulWidget { } class _MessageListPageState extends State with RouteAware { - static const _pageSize = 128; - late final AppLifecycleListener _lifecyleListener; - PagingController _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start'); + PagingController _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start'); Map? _channels = null; @@ -65,7 +64,7 @@ class _MessageListPageState extends State with RouteAware { ApplicationLog.debug('MessageListPage::_realInitState'); final chnCache = Hive.box('scn-channel-cache'); - final msgCache = Hive.box('scn-message-cache'); + final msgCache = Hive.box('scn-message-cache'); if (chnCache.isNotEmpty && msgCache.isNotEmpty) { // ==== Use cache values - and refresh in background @@ -119,6 +118,7 @@ class _MessageListPageState extends State with RouteAware { Future _fetchPage(String thisPageToken) async { final acc = Provider.of(context, listen: false); + final cfg = Provider.of(context, listen: false); ApplicationLog.debug('Start MessageList::_pagingController::_fetchPage [ ${thisPageToken} ]'); @@ -135,7 +135,7 @@ class _MessageListPageState extends State with RouteAware { _setChannelCache(channels); // no await } - final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: _pageSize); + final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize); _addToMessageCache(newItems); // no await @@ -154,6 +154,7 @@ class _MessageListPageState extends State with RouteAware { Future _backgroundRefresh(bool fullReplaceState) async { final acc = Provider.of(context, listen: false); + final cfg = Provider.of(context, listen: false); ApplicationLog.debug('Start background refresh of message list (fullReplaceState: $fullReplaceState)'); @@ -170,7 +171,7 @@ class _MessageListPageState extends State with RouteAware { _setChannelCache(channels); // no await } - final (npt, newItems) = await APIClient.getMessageList(acc, '@start', pageSize: _pageSize); + final (npt, newItems) = await APIClient.getMessageList(acc, '@start', pageSize: cfg.messagePageSize); _addToMessageCache(newItems); // no await @@ -225,9 +226,9 @@ class _MessageListPageState extends State with RouteAware { onRefresh: () => Future.sync( () => _pagingController.refresh(), ), - child: PagedListView( + child: PagedListView( pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( + builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) => MessageListItem( message: item, allChannels: _channels ?? {}, @@ -249,20 +250,22 @@ class _MessageListPageState extends State with RouteAware { for (var chn in channels) await cache.put(chn.channel.channelID, chn.channel); } - Future _addToMessageCache(List newItems) async { - final cache = Hive.box('scn-message-cache'); + Future _addToMessageCache(List newItems) async { + final cfg = AppSettings(); + + final cache = Hive.box('scn-message-cache'); for (var msg in newItems) await cache.put(msg.messageID, msg); // delete all but the newest 128 messages - if (cache.length < _pageSize) return; + if (cache.length < cfg.messagePageSize) return; final allValues = cache.values.toList(); allValues.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp)); - for (var val in allValues.sublist(_pageSize)) { + for (var val in allValues.sublist(cfg.messagePageSize)) { await cache.delete(val.messageID); } } diff --git a/flutter/lib/pages/message_list/message_list_item.dart b/flutter/lib/pages/message_list/message_list_item.dart index 44f01fd..635c129 100644 --- a/flutter/lib/pages/message_list/message_list_item.dart +++ b/flutter/lib/pages/message_list/message_list_item.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:simplecloudnotifier/models/channel.dart'; -import 'package:simplecloudnotifier/models/message.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:intl/intl.dart'; import 'package:simplecloudnotifier/utils/ui.dart'; @@ -18,7 +18,7 @@ class MessageListItem extends StatelessWidget { super.key, }); - final Message message; + final SCNMessage message; final Map allChannels; final Null Function() onPressed; @@ -176,11 +176,11 @@ class MessageListItem extends StatelessWidget { return v; } - String resolveChannelName(Message message) { + String resolveChannelName(SCNMessage message) { return allChannels[message.channelID]?.displayName ?? message.channelInternalName; } - bool showChannel(Message message) { + bool showChannel(SCNMessage message) { return message.channelInternalName != 'main'; } } diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index f266ba2..29c51c9 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -8,7 +8,7 @@ import 'package:simplecloudnotifier/api/api_client.dart'; import 'package:simplecloudnotifier/components/layout/scaffold.dart'; import 'package:simplecloudnotifier/models/channel.dart'; import 'package:simplecloudnotifier/models/keytoken.dart'; -import 'package:simplecloudnotifier/models/message.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/user.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; @@ -18,15 +18,15 @@ import 'package:simplecloudnotifier/utils/ui.dart'; class MessageViewPage extends StatefulWidget { const MessageViewPage({super.key, required this.message}); - final Message message; // Potentially trimmed + final SCNMessage message; // Potentially trimmed @override State createState() => _MessageViewPageState(); } class _MessageViewPageState extends State { - late Future<(Message, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture; - (Message, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null; + late Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>? mainFuture; + (SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)? mainFutureSnapshot = null; static final _dateFormat = DateFormat('yyyy-MM-dd kk:mm'); bool _monospaceMode = false; @@ -37,7 +37,7 @@ class _MessageViewPageState extends State { super.initState(); } - Future<(Message, ChannelPreview, KeyTokenPreview, UserPreview)> fetchData() async { + Future<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)> fetchData() async { try { await Future.delayed(const Duration(seconds: 0), () {}); // this is annoyingly important - otherwise we call setLoadingIndeterminate directly in initStat() and get an exception.... @@ -79,7 +79,7 @@ class _MessageViewPageState extends State { showSearch: false, showShare: true, onShare: _share, - child: FutureBuilder<(Message, ChannelPreview, KeyTokenPreview, UserPreview)>( + child: FutureBuilder<(SCNMessage, ChannelPreview, KeyTokenPreview, UserPreview)>( future: mainFuture, builder: (context, snapshot) { if (snapshot.hasData) { @@ -118,7 +118,7 @@ class _MessageViewPageState extends State { } } - Widget _buildMessageView(BuildContext context, Message message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) { + Widget _buildMessageView(BuildContext context, SCNMessage message, ChannelPreview? channel, KeyTokenPreview? token, UserPreview? user) { final userAccUserID = context.select((v) => v.userID); return SingleChildScrollView( @@ -144,11 +144,11 @@ class _MessageViewPageState extends State { ); } - String _resolveChannelName(ChannelPreview? channel, Message message) { + String _resolveChannelName(ChannelPreview? channel, SCNMessage message) { return channel?.displayName ?? message.channelInternalName; } - List _buildMessageHeader(BuildContext context, Message message, ChannelPreview? channel) { + List _buildMessageHeader(BuildContext context, SCNMessage message, ChannelPreview? channel) { return [ Row( children: [ @@ -167,7 +167,7 @@ class _MessageViewPageState extends State { ]; } - List _buildMessageContent(BuildContext context, Message message) { + List _buildMessageContent(BuildContext context, SCNMessage message) { return [ Row( children: [ @@ -249,7 +249,7 @@ class _MessageViewPageState extends State { } } - String _preformatTitle(Message message) { + String _preformatTitle(SCNMessage message) { return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' '); } } diff --git a/flutter/lib/settings/app_settings.dart b/flutter/lib/settings/app_settings.dart new file mode 100644 index 0000000..a487fed --- /dev/null +++ b/flutter/lib/settings/app_settings.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +class AppSettings extends ChangeNotifier { + bool groupNotifications = true; + int messagePageSize = 128; + bool showDebugButton = true; + + static AppSettings? _singleton = AppSettings._internal(); + + factory AppSettings() { + return _singleton ?? (_singleton = AppSettings._internal()); + } + + AppSettings._internal() { + load(); + } + + void clear() { + //TODO + + notifyListeners(); + } + + void load() { + //TODO + + notifyListeners(); + } + + Future save() async { + //TODO + } +} diff --git a/flutter/lib/state/application_log.dart b/flutter/lib/state/application_log.dart index d997a7a..f5ae651 100644 --- a/flutter/lib/state/application_log.dart +++ b/flutter/lib/state/application_log.dart @@ -10,6 +10,7 @@ class ApplicationLog { static void debug(String message, {String? additional, StackTrace? trace}) { (additional != null && additional != '') ? print('[DEBUG] ${message}: ${additional}') : print('[DEBUG] ${message}'); + if (!Hive.isBoxOpen('scn-logs')) return; Hive.box('scn-logs').add(SCNLog( id: Xid().toString(), timestamp: DateTime.now(), @@ -23,6 +24,7 @@ class ApplicationLog { static void info(String message, {String? additional, StackTrace? trace}) { (additional != null && additional != '') ? print('[INFO] ${message}: ${additional}') : print('[INFO] ${message}'); + if (!Hive.isBoxOpen('scn-logs')) return; Hive.box('scn-logs').add(SCNLog( id: Xid().toString(), timestamp: DateTime.now(), @@ -36,6 +38,7 @@ class ApplicationLog { static void warn(String message, {String? additional, StackTrace? trace}) { (additional != null && additional != '') ? print('[WARN] ${message}: ${additional}') : print('[WARN] ${message}'); + if (!Hive.isBoxOpen('scn-logs')) return; Hive.box('scn-logs').add(SCNLog( id: Xid().toString(), timestamp: DateTime.now(), @@ -49,6 +52,7 @@ class ApplicationLog { static void error(String message, {String? additional, StackTrace? trace}) { (additional != null && additional != '') ? print('[ERROR] ${message}: ${additional}') : print('[ERROR] ${message}'); + if (!Hive.isBoxOpen('scn-logs')) return; Hive.box('scn-logs').add(SCNLog( id: Xid().toString(), timestamp: DateTime.now(), @@ -62,6 +66,7 @@ class ApplicationLog { static void fatal(String message, {String? additional, StackTrace? trace}) { (additional != null && additional != '') ? print('[FATAL] ${message}: ${additional}') : print('[FATAL] ${message}'); + if (!Hive.isBoxOpen('scn-logs')) return; Hive.box('scn-logs').add(SCNLog( id: Xid().toString(), timestamp: DateTime.now(), diff --git a/flutter/lib/state/fb_message.dart b/flutter/lib/state/fb_message.dart index b57e372..bd9782b 100644 --- a/flutter/lib/state/fb_message.dart +++ b/flutter/lib/state/fb_message.dart @@ -32,8 +32,6 @@ class FBMessage extends HiveObject implements FieldDebuggable { final String? messageType; @HiveField(8) final bool mutableContent; - @HiveField(9) - final RemoteNotification? notification; @HiveField(10) final DateTime? sentTime; @HiveField(11) @@ -54,7 +52,7 @@ class FBMessage extends HiveObject implements FieldDebuggable { @HiveField(25) final String? notificationAndroidLink; @HiveField(26) - final AndroidNotificationPriority? notificationAndroidPriority; + final String? notificationAndroidPriority; @HiveField(27) final String? notificationAndroidSmallIcon; @HiveField(28) @@ -62,14 +60,14 @@ class FBMessage extends HiveObject implements FieldDebuggable { @HiveField(29) final String? notificationAndroidTicker; @HiveField(30) - final AndroidNotificationVisibility? notificationAndroidVisibility; + final String? notificationAndroidVisibility; @HiveField(31) final String? notificationAndroidTag; @HiveField(40) final String? notificationAppleBadge; @HiveField(41) - final AppleNotificationSound? notificationAppleSound; + final String? notificationAppleSound; @HiveField(42) final String? notificationAppleImageUrl; @HiveField(43) @@ -109,7 +107,6 @@ class FBMessage extends HiveObject implements FieldDebuggable { required this.messageId, required this.messageType, required this.mutableContent, - required this.notification, required this.sentTime, required this.threadId, required this.ttl, @@ -152,7 +149,6 @@ class FBMessage extends HiveObject implements FieldDebuggable { this.messageId = rmsg.messageId, this.messageType = rmsg.messageType, this.mutableContent = rmsg.mutableContent, - this.notification = rmsg.notification, this.sentTime = rmsg.sentTime, this.threadId = rmsg.threadId, this.ttl = rmsg.ttl, @@ -162,14 +158,14 @@ class FBMessage extends HiveObject implements FieldDebuggable { this.notificationAndroidCount = rmsg.notification?.android?.count, this.notificationAndroidImageUrl = rmsg.notification?.android?.imageUrl, this.notificationAndroidLink = rmsg.notification?.android?.link, - this.notificationAndroidPriority = rmsg.notification?.android?.priority, + this.notificationAndroidPriority = rmsg.notification?.android?.priority?.toString(), this.notificationAndroidSmallIcon = rmsg.notification?.android?.smallIcon, this.notificationAndroidSound = rmsg.notification?.android?.sound, this.notificationAndroidTicker = rmsg.notification?.android?.ticker, - this.notificationAndroidVisibility = rmsg.notification?.android?.visibility, + this.notificationAndroidVisibility = rmsg.notification?.android?.visibility?.toString(), this.notificationAndroidTag = rmsg.notification?.android?.tag, this.notificationAppleBadge = rmsg.notification?.apple?.badge, - this.notificationAppleSound = rmsg.notification?.apple?.sound, + this.notificationAppleSound = rmsg.notification?.apple?.sound?.toString(), this.notificationAppleImageUrl = rmsg.notification?.apple?.imageUrl, this.notificationAppleSubtitle = rmsg.notification?.apple?.subtitle, this.notificationAppleSubtitleLocArgs = rmsg.notification?.apple?.subtitleLocArgs, @@ -195,12 +191,11 @@ class FBMessage extends HiveObject implements FieldDebuggable { ('category', this.category ?? ''), ('collapseKey', this.collapseKey ?? ''), ('contentAvailable', this.contentAvailable.toString()), - ('data', this.data.toString()), + ('data', this.data.entries.map((e) => '${e.key} := ${e.value}').join('\n')), ('from', this.from ?? ''), ('messageId', this.messageId ?? ''), ('messageType', this.messageType ?? ''), ('mutableContent', this.mutableContent.toString()), - ('notification', this.notification?.toString() ?? ''), ('sentTime', this.sentTime?.toString() ?? ''), ('threadId', this.threadId ?? ''), ('ttl', this.ttl?.toString() ?? ''), diff --git a/flutter/lib/state/fb_message.g.dart b/flutter/lib/state/fb_message.g.dart index e10f640..9cd1bc9 100644 --- a/flutter/lib/state/fb_message.g.dart +++ b/flutter/lib/state/fb_message.g.dart @@ -26,7 +26,6 @@ class FBMessageAdapter extends TypeAdapter { messageId: fields[6] as String?, messageType: fields[7] as String?, mutableContent: fields[8] as bool, - notification: fields[9] as RemoteNotification?, sentTime: fields[10] as DateTime?, threadId: fields[11] as String?, ttl: fields[12] as int?, @@ -36,15 +35,14 @@ class FBMessageAdapter extends TypeAdapter { notificationAndroidCount: fields[23] as int?, notificationAndroidImageUrl: fields[24] as String?, notificationAndroidLink: fields[25] as String?, - notificationAndroidPriority: fields[26] as AndroidNotificationPriority?, + notificationAndroidPriority: fields[26] as String?, notificationAndroidSmallIcon: fields[27] as String?, notificationAndroidSound: fields[28] as String?, notificationAndroidTicker: fields[29] as String?, - notificationAndroidVisibility: - fields[30] as AndroidNotificationVisibility?, + notificationAndroidVisibility: fields[30] as String?, notificationAndroidTag: fields[31] as String?, notificationAppleBadge: fields[40] as String?, - notificationAppleSound: fields[41] as AppleNotificationSound?, + notificationAppleSound: fields[41] as String?, notificationAppleImageUrl: fields[42] as String?, notificationAppleSubtitle: fields[43] as String?, notificationAppleSubtitleLocArgs: (fields[44] as List?)?.cast(), @@ -64,7 +62,7 @@ class FBMessageAdapter extends TypeAdapter { @override void write(BinaryWriter writer, FBMessage obj) { writer - ..writeByte(40) + ..writeByte(39) ..writeByte(0) ..write(obj.senderId) ..writeByte(1) @@ -83,8 +81,6 @@ class FBMessageAdapter extends TypeAdapter { ..write(obj.messageType) ..writeByte(8) ..write(obj.mutableContent) - ..writeByte(9) - ..write(obj.notification) ..writeByte(10) ..write(obj.sentTime) ..writeByte(11) diff --git a/flutter/lib/state/globals.dart b/flutter/lib/state/globals.dart index 572b16c..d54fa56 100644 --- a/flutter/lib/state/globals.dart +++ b/flutter/lib/state/globals.dart @@ -1,3 +1,4 @@ +import 'dart:ffi'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; @@ -13,6 +14,8 @@ class Globals { Globals._internal(); + bool _initialized = false; + String appName = ''; String packageName = ''; String version = ''; @@ -24,7 +27,11 @@ class Globals { late SharedPreferences sharedPrefs; + bool get isInitialized => _initialized; + Future init() async { + if (_initialized) return; + PackageInfo packageInfo = await PackageInfo.fromPlatform(); this.appName = packageInfo.appName; @@ -54,6 +61,8 @@ class Globals { } this.sharedPrefs = await SharedPreferences.getInstance(); + + this._initialized = true; } String? getPrefFCMToken() { diff --git a/flutter/lib/utils/notifier.dart b/flutter/lib/utils/notifier.dart new file mode 100644 index 0000000..0abb3ce --- /dev/null +++ b/flutter/lib/utils/notifier.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:simplecloudnotifier/settings/app_settings.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; +import 'package:simplecloudnotifier/state/globals.dart'; + +class Notifier { + static void showLocalNotification(String channelID, String channelName, String channelDescr, String title, String body, DateTime? timestamp) async { + final nid = Globals().sharedPrefs.getInt('notifier.nextid') ?? 1000; + Globals().sharedPrefs.setInt('notifier.nextid', nid + 7); + + final existingSummaryNID = Globals().sharedPrefs.getInt('notifier.summary.$channelID'); + + final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + + if (Platform.isAndroid && AppSettings().groupNotifications) { + final activeNotifications = (await flutterLocalNotificationsPlugin.getActiveNotifications()).where((p) => p.groupKey == channelID).toList(); + final summaryNotification = activeNotifications.where((p) => p.id == existingSummaryNID).toList(); + + ApplicationLog.debug('found ${activeNotifications.length} active notifications in this group (${summaryNotification.length} summary notifications for channel ${channelID} with nid [${existingSummaryNID}])'); + + if (activeNotifications.isNotEmpty && !activeNotifications.any((p) => p.id == existingSummaryNID)) { + // ======== SHOW SUMMARY/GROUPING NOTIFICATION ======== + final newSummaryNID = nid + 1; + ApplicationLog.debug('Create new summary notifications for channel ${channelID} with nid [${newSummaryNID}])'); + Globals().sharedPrefs.setInt('notifier.summary.$channelID', newSummaryNID); + await flutterLocalNotificationsPlugin.show( + newSummaryNID, + channelName, + "(multiple notifications)", + NotificationDetails( + android: AndroidNotificationDetails( + channelID, + channelName, + importance: Importance.max, + priority: Priority.high, + groupKey: channelID, + setAsGroupSummary: true, + subText: (channelName == 'main') ? null : channelName, + ), + ), + ); + } + } + + final newMessageNID = nid + 2; + + ApplicationLog.debug('Create new local notifications for message in channel ${channelID} with nid [${newMessageNID}])'); + + // ======== SHOW NOTIFICATION ======== + await flutterLocalNotificationsPlugin.show( + newMessageNID, + title, + body, + NotificationDetails( + android: AndroidNotificationDetails( + channelID, + channelName, + channelDescription: channelDescr, + importance: Importance.max, + priority: Priority.high, + when: timestamp?.millisecondsSinceEpoch, + groupKey: channelID, + subText: (channelName == 'main') ? null : channelName, + ), + ), + ); + } +} diff --git a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift index d39b825..04d1315 100644 --- a/flutter/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/flutter/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,6 +8,7 @@ import Foundation import device_info_plus import firebase_core import firebase_messaging +import flutter_local_notifications import package_info_plus import path_provider_foundation import share_plus @@ -18,6 +19,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 0e0c132..d410647 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -209,6 +209,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.6" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" device_info_plus: dependency: "direct main" description: @@ -342,6 +350,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" + url: "https://pub.dev" + source: hosted + version: "17.1.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" + url: "https://pub.dev" + source: hosted + version: "7.1.0" flutter_staggered_grid_view: dependency: transitive description: @@ -916,6 +948,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + timezone: + dependency: transitive + description: + name: timezone + sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 + url: "https://pub.dev" + source: hosted + version: "0.9.3" timing: dependency: transitive description: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index e3df6d1..15b46bd 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: toastification: ^2.0.0 uuid: ^4.4.0 share_plus: ^9.0.0 + flutter_local_notifications: ^17.1.2 dependency_overrides: