Notifications (android via local) work

This commit is contained in:
2024-06-15 21:29:51 +02:00
parent e6fbf85e6e
commit e9ea573e33
39 changed files with 476 additions and 104 deletions
+5 -5
View File
@@ -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<Message>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? channelIDs}) async {
static Future<(String, List<SCNMessage>)> getMessageList(TokenSource auth, String pageToken, {int? pageSize, List<String>? 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<Message> getMessage(TokenSource auth, String msgid) async {
static Future<SCNMessage> 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(),
);
}
+4 -4
View File
@@ -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<AppSettings>(context);
var actions = <Widget>[];
if (showDebug) {
if (cfg.showDebugButton) {
actions.add(IconButton(
icon: const Icon(FontAwesomeIcons.solidSpiderBlackWidow),
tooltip: 'Debug',
@@ -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 ?? () {},
+145 -17
View File
@@ -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<scn-requests>...');
try {
await Hive.openBox<SCNRequest>('scn-requests');
} catch (exc, trace) {
Hive.deleteBoxFromDisk('scn-requests');
await Hive.openBox<SCNRequest>('scn-requests');
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
}
print('[INIT] Load Hive<scn-logs>...');
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<scn-requests>...');
try {
await Hive.openBox<SCNRequest>('scn-requests');
} catch (exc, trace) {
Hive.deleteBoxFromDisk('scn-requests');
await Hive.openBox<SCNRequest>('scn-requests');
ApplicationLog.error('Failed to open Hive-Box: scn-requests: ' + exc.toString(), trace: trace);
}
print('[INIT] Load Hive<scn-message-cache>...');
try {
await Hive.openBox<Message>('scn-message-cache');
await Hive.openBox<SCNMessage>('scn-message-cache');
} catch (exc, trace) {
Hive.deleteBoxFromDisk('scn-message-cache');
await Hive.openBox<Message>('scn-message-cache');
await Hive.openBox<SCNMessage>('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<AndroidFlutterLocalNotificationsPlugin>();
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<vm:entry-point>: ${notificationResponse.id}');
}
void setFirebaseToken(String fcmToken) async {
final acc = AppAuth();
@@ -233,9 +274,96 @@ void _onForegroundMessage(RemoteMessage message) {
}
Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
// ensure init
Hive.openBox<SCNLog>('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<SCNLog>('scn-logs');
await Hive.openBox<FBMessage>('scn-fb-messages');
await Hive.openBox<SCNMessage>('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<darwin>: $id -> [$title]');
}
void _receiveLocalNotification(NotificationResponse details) {
ApplicationLog.info('Received local notification: ${details.id}');
}
List<DarwinNotificationCategory> getDarwinNotificationCategories() {
return <DarwinNotificationCategory>[
//TODO ?!?
];
}
@@ -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<String, dynamic> json) {
return Message(
factory SCNMessage.fromJson(Map<String, dynamic> 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<Message>) fromPaginatedJsonArray(Map<String, dynamic> data, String keyMessages, String keyToken) {
static (String, List<SCNMessage>) fromPaginatedJsonArray(Map<String, dynamic> data, String keyMessages, String keyToken) {
final npt = data[keyToken] as String;
final messages = (data[keyMessages] as List<dynamic>).map<Message>((e) => Message.fromJson(e as Map<String, dynamic>)).toList();
final messages = (data[keyMessages] as List<dynamic>).map<SCNMessage>((e) => SCNMessage.fromJson(e as Map<String, dynamic>)).toList();
return (npt, messages);
}
@@ -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<Message> {
class SCNMessageAdapter extends TypeAdapter<SCNMessage> {
@override
final int typeId = 105;
@override
Message read(BinaryReader reader) {
SCNMessage read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
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<Message> {
}
@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<Message> {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MessageAdapter &&
other is SCNMessageAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
-1
View File
@@ -59,7 +59,6 @@ class _SCNNavLayoutState extends State<SCNNavLayout> {
return Scaffold(
appBar: SCNAppBar(
title: null,
showDebug: true,
showSearch: _selectedIndex == 0 || _selectedIndex == 1,
showShare: false,
showThemeSwitch: true,
@@ -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<ChannelListItem> {
Message? lastMessage;
SCNMessage? lastMessage;
@override
void initState() {
@@ -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<DebugActionsPage> {
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',
),
],
),
),
-1
View File
@@ -30,7 +30,6 @@ class _DebugMainPageState extends State<DebugMainPage> {
return SCNScaffold(
title: 'Debug',
showSearch: false,
showDebug: false,
child: Column(
children: [
Padding(
@@ -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<DebugPersistencePage> {
_buildSharedPrefCard(context),
_buildHiveCard(context, () => Hive.box<SCNRequest>('scn-requests'), 'scn-requests'),
_buildHiveCard(context, () => Hive.box<SCNLog>('scn-logs'), 'scn-logs'),
_buildHiveCard(context, () => Hive.box<Message>('scn-message-cache'), 'scn-message-cache'),
_buildHiveCard(context, () => Hive.box<SCNMessage>('scn-message-cache'), 'scn-message-cache'),
_buildHiveCard(context, () => Hive.box<Channel>('scn-channel-cache'), 'scn-channel-cache'),
_buildHiveCard(context, () => Hive.box<FBMessage>('scn-fb-messages'), 'scn-fb-messages'),
],
@@ -71,7 +71,7 @@ class _DebugPersistencePageState extends State<DebugPersistencePage> {
padding: const EdgeInsets.all(8.0),
child: GestureDetector(
onTap: () {
Navi.push(context, () => DebugHiveBoxPage(boxName: boxname, box: Hive.box<FBMessage>(boxname)));
Navi.push(context, () => DebugHiveBoxPage(boxName: boxname, box: boxFunc()));
},
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -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)),
),
);
},
@@ -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")),
);
@@ -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) {
@@ -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),
@@ -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<MessageListPage> with RouteAware {
static const _pageSize = 128;
late final AppLifecycleListener _lifecyleListener;
PagingController<String, Message> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
PagingController<String, SCNMessage> _pagingController = PagingController.fromValue(PagingState(nextPageKey: null, itemList: [], error: null), firstPageKey: '@start');
Map<String, Channel>? _channels = null;
@@ -65,7 +64,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
ApplicationLog.debug('MessageListPage::_realInitState');
final chnCache = Hive.box<Channel>('scn-channel-cache');
final msgCache = Hive.box<Message>('scn-message-cache');
final msgCache = Hive.box<SCNMessage>('scn-message-cache');
if (chnCache.isNotEmpty && msgCache.isNotEmpty) {
// ==== Use cache values - and refresh in background
@@ -119,6 +118,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
Future<void> _fetchPage(String thisPageToken) async {
final acc = Provider.of<AppAuth>(context, listen: false);
final cfg = Provider.of<AppSettings>(context, listen: false);
ApplicationLog.debug('Start MessageList::_pagingController::_fetchPage [ ${thisPageToken} ]');
@@ -135,7 +135,7 @@ class _MessageListPageState extends State<MessageListPage> 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<MessageListPage> with RouteAware {
Future<void> _backgroundRefresh(bool fullReplaceState) async {
final acc = Provider.of<AppAuth>(context, listen: false);
final cfg = Provider.of<AppSettings>(context, listen: false);
ApplicationLog.debug('Start background refresh of message list (fullReplaceState: $fullReplaceState)');
@@ -170,7 +171,7 @@ class _MessageListPageState extends State<MessageListPage> 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<MessageListPage> with RouteAware {
onRefresh: () => Future.sync(
() => _pagingController.refresh(),
),
child: PagedListView<String, Message>(
child: PagedListView<String, SCNMessage>(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Message>(
builderDelegate: PagedChildBuilderDelegate<SCNMessage>(
itemBuilder: (context, item, index) => MessageListItem(
message: item,
allChannels: _channels ?? {},
@@ -249,20 +250,22 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
for (var chn in channels) await cache.put(chn.channel.channelID, chn.channel);
}
Future<void> _addToMessageCache(List<Message> newItems) async {
final cache = Hive.box<Message>('scn-message-cache');
Future<void> _addToMessageCache(List<SCNMessage> newItems) async {
final cfg = AppSettings();
final cache = Hive.box<SCNMessage>('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);
}
}
@@ -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<String, Channel> 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';
}
}
@@ -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<MessageViewPage> createState() => _MessageViewPageState();
}
class _MessageViewPageState extends State<MessageViewPage> {
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<MessageViewPage> {
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<MessageViewPage> {
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<MessageViewPage> {
}
}
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<AppAuth, String?>((v) => v.userID);
return SingleChildScrollView(
@@ -144,11 +144,11 @@ class _MessageViewPageState extends State<MessageViewPage> {
);
}
String _resolveChannelName(ChannelPreview? channel, Message message) {
String _resolveChannelName(ChannelPreview? channel, SCNMessage message) {
return channel?.displayName ?? message.channelInternalName;
}
List<Widget> _buildMessageHeader(BuildContext context, Message message, ChannelPreview? channel) {
List<Widget> _buildMessageHeader(BuildContext context, SCNMessage message, ChannelPreview? channel) {
return [
Row(
children: [
@@ -167,7 +167,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
];
}
List<Widget> _buildMessageContent(BuildContext context, Message message) {
List<Widget> _buildMessageContent(BuildContext context, SCNMessage message) {
return [
Row(
children: [
@@ -249,7 +249,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
}
}
String _preformatTitle(Message message) {
String _preformatTitle(SCNMessage message) {
return message.title.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', ' ');
}
}
+33
View File
@@ -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<void> save() async {
//TODO
}
}
+5
View File
@@ -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<SCNLog>('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<SCNLog>('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<SCNLog>('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<SCNLog>('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<SCNLog>('scn-logs').add(SCNLog(
id: Xid().toString(),
timestamp: DateTime.now(),
+7 -12
View File
@@ -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() ?? ''),
+4 -8
View File
@@ -26,7 +26,6 @@ class FBMessageAdapter extends TypeAdapter<FBMessage> {
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<FBMessage> {
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<String>(),
@@ -64,7 +62,7 @@ class FBMessageAdapter extends TypeAdapter<FBMessage> {
@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<FBMessage> {
..write(obj.messageType)
..writeByte(8)
..write(obj.mutableContent)
..writeByte(9)
..write(obj.notification)
..writeByte(10)
..write(obj.sentTime)
..writeByte(11)
+9
View File
@@ -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<void> 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() {
+70
View File
@@ -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,
),
),
);
}
}