auto-refresh message-list on FB message receive

This commit is contained in:
Mike Schwörer 2024-06-17 23:23:35 +02:00
parent 600f3365f6
commit 59d28d3c49
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
6 changed files with 142 additions and 87 deletions

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:simplecloudnotifier/components/layout/app_bar_filter_dialog.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/pages/debug/debug_main.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart'; import 'package:simplecloudnotifier/settings/app_settings.dart';
import 'package:simplecloudnotifier/state/app_bar_state.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/app_theme.dart';
import 'package:simplecloudnotifier/utils/navi.dart'; import 'package:simplecloudnotifier/utils/navi.dart';
import 'package:simplecloudnotifier/utils/toaster.dart';
class SCNAppBar extends StatefulWidget implements PreferredSizeWidget { class SCNAppBar extends StatefulWidget implements PreferredSizeWidget {
SCNAppBar({ SCNAppBar({
@ -108,7 +107,7 @@ class _SCNAppBarState extends State<SCNAppBar> {
icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass), icon: const Icon(FontAwesomeIcons.solidMagnifyingGlass),
onPressed: () { onPressed: () {
value.setShowSearchField(false); value.setShowSearchField(false);
AppBarState().notifySearchListeners(_ctrlSearchField.text); AppEvents().notifySearchListeners(_ctrlSearchField.text);
_ctrlSearchField.clear(); _ctrlSearchField.clear();
}, },
), ),
@ -157,15 +156,13 @@ class _SCNAppBarState extends State<SCNAppBar> {
), ),
onSubmitted: (value) { onSubmitted: (value) {
AppBarState().setShowSearchField(false); AppBarState().setShowSearchField(false);
AppBarState().notifySearchListeners(_ctrlSearchField.text); AppEvents().notifySearchListeners(_ctrlSearchField.text);
_ctrlSearchField.clear(); _ctrlSearchField.clear();
}, },
); );
} }
void _showFilterDialog(BuildContext context) { void _showFilterDialog(BuildContext context) {
double vpWidth = MediaQuery.sizeOf(context).width;
showDialog<void>( showDialog<void>(
context: context, context: context,
barrierDismissible: true, barrierDismissible: true,

View File

@ -12,6 +12,7 @@ import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/nav_layout.dart'; import 'package:simplecloudnotifier/nav_layout.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart'; import 'package:simplecloudnotifier/settings/app_settings.dart';
import 'package:simplecloudnotifier/state/app_bar_state.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/app_theme.dart';
import 'package:simplecloudnotifier/state/application_log.dart'; import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/fb_message.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/request_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:firebase_core/firebase_core.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/navi.dart';
import 'package:simplecloudnotifier/utils/notifier.dart'; import 'package:simplecloudnotifier/utils/notifier.dart';
import 'package:toastification/toastification.dart'; import 'package:toastification/toastification.dart';
@ -308,35 +310,18 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}'); ApplicationLog.info('Received FB message (${foreground ? 'foreground' : 'background'}): ${message.messageId ?? 'NULL'}');
SCNMessage? receivedMessage; String scn_msg_id;
try { try {
final scn_msg_id = message.data['scn_msg_id'] as String; 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 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 title = message.data['title'] as String;
final channel = message.data['channel'] as String; final channel = message.data['channel'] as String;
final channel_id = message.data['channel_id'] as String; final channel_id = message.data['channel_id'] as String;
final body = message.data['body'] as String; final body = message.data['body'] as String;
Notifier.showLocalNotification(channel_id, channel, 'Channel: ${channel}', title, body, timestamp); 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) { } catch (exc, trace) {
ApplicationLog.error('Failed to decode received FB message' + exc.toString(), trace: 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); Notifier.showLocalNotification("@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null);
@ -351,8 +336,14 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
return; return;
} }
//TODO add to scn-message-cache try {
//TODO refresh message_list view (if shown/initialized) 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) { void _receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) {

View File

@ -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/pages/message_view/message_view.dart';
import 'package:simplecloudnotifier/settings/app_settings.dart'; import 'package:simplecloudnotifier/settings/app_settings.dart';
import 'package:simplecloudnotifier/state/app_bar_state.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/application_log.dart';
import 'package:simplecloudnotifier/state/app_auth.dart'; import 'package:simplecloudnotifier/state/app_auth.dart';
import 'package:simplecloudnotifier/pages/message_list/message_list_item.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'; import 'package:simplecloudnotifier/utils/navi.dart';
class MessageListPage extends StatefulWidget { class MessageListPage extends StatefulWidget {
@ -41,7 +43,8 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
void initState() { void initState() {
super.initState(); super.initState();
AppBarState().subscribeSearchListener(_onAppBarSearch); AppEvents().subscribeSearchListener(_onAppBarSearch);
AppEvents().subscribeMessageReceivedListener(_onMessageReceivedViaNotification);
_pagingController.addPageRequestListener(_fetchPage); _pagingController.addPageRequestListener(_fetchPage);
@ -68,18 +71,12 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
void _realInitState() { void _realInitState() {
ApplicationLog.debug('MessageListPage::_realInitState'); ApplicationLog.debug('MessageListPage::_realInitState');
final chnCache = Hive.box<Channel>('scn-channel-cache'); if (SCNDataCache().hasMessagesAndChannels()) {
final msgCache = Hive.box<SCNMessage>('scn-message-cache');
if (chnCache.isNotEmpty && msgCache.isNotEmpty) {
// ==== Use cache values - and refresh in background // ==== Use cache values - and refresh in background
_channels = <String, Channel>{for (var v in chnCache.values) v.channelID: v}; _channels = SCNDataCache().getChannelMap();
final cacheMessages = msgCache.values.toList(); _pagingController.value = PagingState(nextPageKey: null, itemList: SCNDataCache().getMessagesSorted(), error: null);
cacheMessages.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp));
_pagingController.value = PagingState(nextPageKey: null, itemList: cacheMessages, error: null);
_backgroundRefresh(true); _backgroundRefresh(true);
} else { } else {
@ -99,7 +96,8 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
@override @override
void dispose() { void dispose() {
ApplicationLog.debug('MessageListPage::dispose'); ApplicationLog.debug('MessageListPage::dispose');
AppBarState().unsubscribeSearchListener(_onAppBarSearch); AppEvents().unsubscribeSearchListener(_onAppBarSearch);
AppEvents().unsubscribeMessageReceivedListener(_onMessageReceivedViaNotification);
Navi.modalRouteObserver.unsubscribe(this); Navi.modalRouteObserver.unsubscribe(this);
_pagingController.dispose(); _pagingController.dispose();
_lifecyleListener.dispose(); _lifecyleListener.dispose();
@ -140,12 +138,12 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny); final channels = await APIClient.getChannelList(acc, ChannelSelector.allAny);
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel}; _channels = <String, Channel>{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); 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} ]'); ApplicationLog.debug('Finished MessageList::_pagingController::_fetchPage [ ${newItems.length} items and npt: ${thisPageToken} --> ${npt} ]');
@ -176,12 +174,12 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
setState(() { setState(() {
_channels = <String, Channel>{for (var v in channels) v.channel.channelID: v.channel}; _channels = <String, Channel>{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); final (npt, newItems) = await APIClient.getMessageList(acc, '@start', pageSize: cfg.messagePageSize);
_addToMessageCache(newItems); // no await SCNDataCache().addToMessageCache(newItems); // no await
if (fullReplaceState) { if (fullReplaceState) {
// fully replace/reset state // fully replace/reset state
@ -278,37 +276,15 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
); );
} }
Future<void> _setChannelCache(List<ChannelWithSubscription> channels) async {
final cache = Hive.box<Channel>('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<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 < 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) { void _onAppBarSearch(String str) {
setState(() { setState(() {
_filterChiplets = _filterChiplets.where((element) => false).toList() + [MessageFilterChiplet(label: str, value: str, type: MessageFilterChipletType.search)]; _filterChiplets = _filterChiplets.where((element) => false).toList() + [MessageFilterChiplet(label: str, value: str, type: MessageFilterChipletType.search)];
}); });
} }
void _onMessageReceivedViaNotification(SCNMessage msg) {
setState(() {
_pagingController.itemList = [msg] + (_pagingController.itemList ?? []);
});
}
} }

View File

@ -9,8 +9,6 @@ class AppBarState extends ChangeNotifier {
AppBarState._internal() {} AppBarState._internal() {}
List<void Function(String)> _searchListeners = [];
bool _loadingIndeterminate = false; bool _loadingIndeterminate = false;
bool get loadingIndeterminate => _loadingIndeterminate; bool get loadingIndeterminate => _loadingIndeterminate;
@ -28,18 +26,4 @@ class AppBarState extends ChangeNotifier {
_showSearchField = v; _showSearchField = v;
notifyListeners(); 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);
}
}
} }

View File

@ -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<void Function(String)> _searchListeners = [];
List<void Function(SCNMessage)> _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);
}
}
}

View File

@ -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<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 < 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<void> setChannelCache(List<ChannelWithSubscription> channels) async {
final cache = Hive.box<Channel>('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<Channel>('scn-channel-cache');
final msgCache = Hive.box<SCNMessage>('scn-message-cache');
return chnCache.isNotEmpty && msgCache.isNotEmpty;
}
Map<String, Channel> getChannelMap() {
final chnCache = Hive.box<Channel>('scn-channel-cache');
return <String, Channel>{for (var v in chnCache.values) v.channelID: v};
}
List<SCNMessage> getMessagesSorted() {
final msgCache = Hive.box<SCNMessage>('scn-message-cache');
final cacheMessages = msgCache.values.toList();
cacheMessages.sort((a, b) => -1 * a.timestamp.compareTo(b.timestamp));
return cacheMessages;
}
}