diff --git a/flutter/lib/components/layout/app_bar.dart b/flutter/lib/components/layout/app_bar.dart index d9755b0..64f59c0 100644 --- a/flutter/lib/components/layout/app_bar.dart +++ b/flutter/lib/components/layout/app_bar.dart @@ -1,5 +1,4 @@ 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_filter_dialog.dart'; @@ -7,9 +6,9 @@ import 'package:simplecloudnotifier/components/layout/app_bar_progress_indicator import 'package:simplecloudnotifier/pages/debug/debug_main.dart'; import 'package:simplecloudnotifier/settings/app_settings.dart'; import 'package:simplecloudnotifier/state/app_bar_state.dart'; +import 'package:simplecloudnotifier/state/app_events.dart'; import 'package:simplecloudnotifier/state/app_theme.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; -import 'package:simplecloudnotifier/utils/toaster.dart'; class SCNAppBar extends StatefulWidget implements PreferredSizeWidget { SCNAppBar({ @@ -108,7 +107,7 @@ class _SCNAppBarState extends State { icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass), onPressed: () { value.setShowSearchField(false); - AppBarState().notifySearchListeners(_ctrlSearchField.text); + AppEvents().notifySearchListeners(_ctrlSearchField.text); _ctrlSearchField.clear(); }, ), @@ -157,15 +156,13 @@ class _SCNAppBarState extends State { ), onSubmitted: (value) { AppBarState().setShowSearchField(false); - AppBarState().notifySearchListeners(_ctrlSearchField.text); + AppEvents().notifySearchListeners(_ctrlSearchField.text); _ctrlSearchField.clear(); }, ); } void _showFilterDialog(BuildContext context) { - double vpWidth = MediaQuery.sizeOf(context).width; - showDialog( context: context, barrierDismissible: true, diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index c2ec8f3..67e9dda 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -12,6 +12,7 @@ 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_events.dart'; import 'package:simplecloudnotifier/state/app_theme.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/fb_message.dart'; @@ -19,6 +20,7 @@ import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/request_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:simplecloudnotifier/state/scn_data_cache.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/notifier.dart'; import 'package:toastification/toastification.dart'; @@ -308,35 +310,18 @@ Future _receiveMessage(RemoteMessage message, bool foreground) async { ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}'); - SCNMessage? receivedMessage; + String scn_msg_id; try { - final scn_msg_id = message.data['scn_msg_id'] as String; - final usr_msg_id = message.data['usr_msg_id'] as String; + scn_msg_id = message.data['scn_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); @@ -351,8 +336,14 @@ Future _receiveMessage(RemoteMessage message, bool foreground) async { return; } - //TODO add to scn-message-cache - //TODO refresh message_list view (if shown/initialized) + try { + final msg = await APIClient.getMessage(AppAuth(), scn_msg_id); + SCNDataCache().addToMessageCache([msg]); + if (foreground) AppEvents().notifyMessageReceivedListeners(msg); + } catch (exc, trace) { + ApplicationLog.error('Failed to query+persist message' + exc.toString(), trace: trace); + return; + } } void _receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) { diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index c0f49a0..1277a6e 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -9,9 +9,11 @@ import 'package:simplecloudnotifier/pages/message_list/message_filter_chiplet.da 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/app_events.dart'; import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/pages/message_list/message_list_item.dart'; +import 'package:simplecloudnotifier/state/scn_data_cache.dart'; import 'package:simplecloudnotifier/utils/navi.dart'; class MessageListPage extends StatefulWidget { @@ -41,7 +43,8 @@ class _MessageListPageState extends State with RouteAware { void initState() { super.initState(); - AppBarState().subscribeSearchListener(_onAppBarSearch); + AppEvents().subscribeSearchListener(_onAppBarSearch); + AppEvents().subscribeMessageReceivedListener(_onMessageReceivedViaNotification); _pagingController.addPageRequestListener(_fetchPage); @@ -68,18 +71,12 @@ class _MessageListPageState extends State with RouteAware { void _realInitState() { ApplicationLog.debug('MessageListPage::_realInitState'); - final chnCache = Hive.box('scn-channel-cache'); - final msgCache = Hive.box('scn-message-cache'); - - if (chnCache.isNotEmpty && msgCache.isNotEmpty) { + if (SCNDataCache().hasMessagesAndChannels()) { // ==== Use cache values - and refresh in background - _channels = {for (var v in chnCache.values) v.channelID: v}; + _channels = SCNDataCache().getChannelMap(); - final cacheMessages = msgCache.values.toList(); - cacheMessages.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp)); - - _pagingController.value = PagingState(nextPageKey: null, itemList: cacheMessages, error: null); + _pagingController.value = PagingState(nextPageKey: null, itemList: SCNDataCache().getMessagesSorted(), error: null); _backgroundRefresh(true); } else { @@ -99,7 +96,8 @@ class _MessageListPageState extends State with RouteAware { @override void dispose() { ApplicationLog.debug('MessageListPage::dispose'); - AppBarState().unsubscribeSearchListener(_onAppBarSearch); + AppEvents().unsubscribeSearchListener(_onAppBarSearch); + AppEvents().unsubscribeMessageReceivedListener(_onMessageReceivedViaNotification); Navi.modalRouteObserver.unsubscribe(this); _pagingController.dispose(); _lifecyleListener.dispose(); @@ -140,12 +138,12 @@ class _MessageListPageState extends State with RouteAware { final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny); _channels = {for (var v in channels) v.channel.channelID: v.channel}; - _setChannelCache(channels); // no await + SCNDataCache().setChannelCache(channels); // no await } final (npt, newItems) = await APIClient.getMessageList(acc, thisPageToken, pageSize: cfg.messagePageSize); - _addToMessageCache(newItems); // no await + SCNDataCache().addToMessageCache(newItems); // no await ApplicationLog.debug('Finished MessageList::_pagingController::_fetchPage [ ${newItems.length} items and npt: ${thisPageToken} --> ${npt} ]'); @@ -176,12 +174,12 @@ class _MessageListPageState extends State with RouteAware { setState(() { _channels = {for (var v in channels) v.channel.channelID: v.channel}; }); - _setChannelCache(channels); // no await + SCNDataCache().setChannelCache(channels); // no await } final (npt, newItems) = await APIClient.getMessageList(acc, '@start', pageSize: cfg.messagePageSize); - _addToMessageCache(newItems); // no await + SCNDataCache().addToMessageCache(newItems); // no await if (fullReplaceState) { // fully replace/reset state @@ -278,37 +276,15 @@ class _MessageListPageState extends State with RouteAware { ); } - Future _setChannelCache(List channels) async { - final cache = Hive.box('scn-channel-cache'); - - if (cache.length != channels.length) await cache.clear(); - - for (var chn in channels) await cache.put(chn.channel.channelID, chn.channel); - } - - 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 < cfg.messagePageSize) return; - - final allValues = cache.values.toList(); - - allValues.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp)); - - for (var val in allValues.sublist(cfg.messagePageSize)) { - await cache.delete(val.messageID); - } - } - void _onAppBarSearch(String str) { setState(() { _filterChiplets = _filterChiplets.where((element) => false).toList() + [MessageFilterChiplet(label: str, value: str, type: MessageFilterChipletType.search)]; }); } + + void _onMessageReceivedViaNotification(SCNMessage msg) { + setState(() { + _pagingController.itemList = [msg] + (_pagingController.itemList ?? []); + }); + } } diff --git a/flutter/lib/state/app_bar_state.dart b/flutter/lib/state/app_bar_state.dart index 0aeac53..e1c6a32 100644 --- a/flutter/lib/state/app_bar_state.dart +++ b/flutter/lib/state/app_bar_state.dart @@ -9,8 +9,6 @@ class AppBarState extends ChangeNotifier { AppBarState._internal() {} - List _searchListeners = []; - bool _loadingIndeterminate = false; bool get loadingIndeterminate => _loadingIndeterminate; @@ -28,18 +26,4 @@ class AppBarState extends ChangeNotifier { _showSearchField = v; notifyListeners(); } - - void subscribeSearchListener(void Function(String) listener) { - _searchListeners.add(listener); - } - - void unsubscribeSearchListener(void Function(String) listener) { - _searchListeners.remove(listener); - } - - void notifySearchListeners(String query) { - for (var listener in _searchListeners) { - listener(query); - } - } } diff --git a/flutter/lib/state/app_events.dart b/flutter/lib/state/app_events.dart new file mode 100644 index 0000000..5b5a74a --- /dev/null +++ b/flutter/lib/state/app_events.dart @@ -0,0 +1,47 @@ +import 'package:simplecloudnotifier/models/scn_message.dart'; +import 'package:simplecloudnotifier/state/application_log.dart'; + +class AppEvents { + static AppEvents? _singleton = AppEvents._internal(); + + factory AppEvents() { + return _singleton ?? (_singleton = AppEvents._internal()); + } + + AppEvents._internal() {} + + List _searchListeners = []; + List _messageReceivedListeners = []; + + void subscribeSearchListener(void Function(String) listener) { + _searchListeners.add(listener); + } + + void unsubscribeSearchListener(void Function(String) listener) { + _searchListeners.remove(listener); + } + + void notifySearchListeners(String query) { + ApplicationLog.debug('[AppEvents] onSearch: $query'); + + for (var listener in _searchListeners) { + listener(query); + } + } + + void subscribeMessageReceivedListener(void Function(SCNMessage) listener) { + _messageReceivedListeners.add(listener); + } + + void unsubscribeMessageReceivedListener(void Function(SCNMessage) listener) { + _messageReceivedListeners.remove(listener); + } + + void notifyMessageReceivedListeners(SCNMessage msg) { + ApplicationLog.debug('[AppEvents] onMessageReceived: ${msg.messageID}'); + + for (var listener in _messageReceivedListeners) { + listener(msg); + } + } +} diff --git a/flutter/lib/state/scn_data_cache.dart b/flutter/lib/state/scn_data_cache.dart new file mode 100644 index 0000000..f5da15a --- /dev/null +++ b/flutter/lib/state/scn_data_cache.dart @@ -0,0 +1,60 @@ +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:simplecloudnotifier/models/channel.dart'; +import 'package:simplecloudnotifier/models/scn_message.dart'; +import 'package:simplecloudnotifier/settings/app_settings.dart'; + +class SCNDataCache { + SCNDataCache._internal(); + static final SCNDataCache _instance = SCNDataCache._internal(); + factory SCNDataCache() => _instance; + + 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 < cfg.messagePageSize) return; + + final allValues = cache.values.toList(); + + allValues.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp)); + + for (var val in allValues.sublist(cfg.messagePageSize)) { + await cache.delete(val.messageID); + } + } + + Future setChannelCache(List channels) async { + final cache = Hive.box('scn-channel-cache'); + + if (cache.length != channels.length) await cache.clear(); + + for (var chn in channels) await cache.put(chn.channel.channelID, chn.channel); + } + + bool hasMessagesAndChannels() { + final chnCache = Hive.box('scn-channel-cache'); + final msgCache = Hive.box('scn-message-cache'); + + return chnCache.isNotEmpty && msgCache.isNotEmpty; + } + + Map getChannelMap() { + final chnCache = Hive.box('scn-channel-cache'); + + return {for (var v in chnCache.values) v.channelID: v}; + } + + List getMessagesSorted() { + final msgCache = Hive.box('scn-message-cache'); + + final cacheMessages = msgCache.values.toList(); + cacheMessages.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp)); + + return cacheMessages; + } +}