Properly handle click actions on notifications

This commit is contained in:
Mike Schwörer 2024-07-13 01:05:32 +02:00
parent 74a935f6f1
commit e93d125431
Signed by: Mikescher
GPG Key ID: D3C7172E0A70F8CF
7 changed files with 103 additions and 19 deletions

View File

@ -23,9 +23,20 @@
- [ ] Logout - [ ] Logout
- [ ] Send-page - [ ] Send-page
----- -----
# TODO iOS specific
- [ ] payment / pro
- [ ] show notifiactions (foreground/background/etc)
- [ ] handle click-on-notifications should open message
- [ ] share message
- [ ] scan QR
-----
# TODO Server
- [ ] Switch server to sq style from faby - [ ] Switch server to sq style from faby
- [ ] switch from mattn to go-sqlite - [ ] switch from mattn to go-sqlite
- [ ] Single struct for model/db/json - [ ] Single struct for model/db/json

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
@ -10,6 +11,8 @@ import 'package:simplecloudnotifier/models/channel.dart';
import 'package:simplecloudnotifier/models/client.dart'; import 'package:simplecloudnotifier/models/client.dart';
import 'package:simplecloudnotifier/models/scn_message.dart'; import 'package:simplecloudnotifier/models/scn_message.dart';
import 'package:simplecloudnotifier/nav_layout.dart'; import 'package:simplecloudnotifier/nav_layout.dart';
import 'package:simplecloudnotifier/pages/channel_view/channel_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/app_events.dart';
@ -174,12 +177,20 @@ void main() async {
iOS: initializationSettingsDarwin, iOS: initializationSettingsDarwin,
linux: initializationSettingsLinux, linux: initializationSettingsLinux,
); );
flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: _receiveLocalNotification); flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: _receiveLocalNotification,
onDidReceiveBackgroundNotificationResponse: _notificationTapBackground,
);
final appLaunchNotification = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(); final appLaunchNotification = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
if (appLaunchNotification != null) { if (appLaunchNotification != null) {
//TODO show message (also this only works on android+localnotifications, also handle ios) // Use has launched SCN by clicking on a loca notifiaction, if it was a summary or message notifiaction open the corresponding screen
// This is android only
//TODO same on iOS, somehow??
ApplicationLog.info('App launched by notification: ${appLaunchNotification.notificationResponse?.id}'); ApplicationLog.info('App launched by notification: ${appLaunchNotification.notificationResponse?.id}');
_handleNotificationClickAction(appLaunchNotification.notificationResponse?.payload, Duration(milliseconds: 600));
} }
} }
@ -201,6 +212,8 @@ void main() async {
class SCNApp extends StatelessWidget { class SCNApp extends StatelessWidget {
SCNApp({super.key}); SCNApp({super.key});
static var materialKey = GlobalKey<NavigatorState>();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ToastificationWrapper( return ToastificationWrapper(
@ -211,6 +224,7 @@ class SCNApp extends StatelessWidget {
), ),
child: Consumer<AppTheme>( child: Consumer<AppTheme>(
builder: (context, appTheme, child) => MaterialApp( builder: (context, appTheme, child) => MaterialApp(
navigatorKey: SCNApp.materialKey,
title: 'SimpleCloudNotifier', title: 'SimpleCloudNotifier',
navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver], navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver],
theme: ThemeData( theme: ThemeData(
@ -226,7 +240,8 @@ class SCNApp extends StatelessWidget {
} }
@pragma('vm:entry-point') @pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse notificationResponse) { void _notificationTapBackground(NotificationResponse notificationResponse) {
// I think only iOS triggers this, TODO
ApplicationLog.info('Received local notification<vm:entry-point>: ${notificationResponse.id}'); ApplicationLog.info('Received local notification<vm:entry-point>: ${notificationResponse.id}');
} }
@ -268,11 +283,13 @@ void setFirebaseToken(String fcmToken) async {
@pragma('vm:entry-point') @pragma('vm:entry-point')
Future<void> _onBackgroundMessage(RemoteMessage message) async { Future<void> _onBackgroundMessage(RemoteMessage message) async {
// a firebase message was received while the app was in the background or terminated
await _receiveMessage(message, false); await _receiveMessage(message, false);
} }
@pragma('vm:entry-point') @pragma('vm:entry-point')
void _onForegroundMessage(RemoteMessage message) { void _onForegroundMessage(RemoteMessage message) {
// a firebase message was received while the app was in the foreground
_receiveMessage(message, true); _receiveMessage(message, true);
} }
@ -305,7 +322,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
await Hive.openBox<SCNRequest>('scn-requests'); await Hive.openBox<SCNRequest>('scn-requests');
} catch (exc, trace) { } catch (exc, trace) {
ApplicationLog.error('Failed to init hive:' + exc.toString(), trace: 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); Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null);
return; return;
} }
@ -322,10 +339,10 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
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(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp);
} 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);
return; return;
} }
@ -333,7 +350,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
FBMessageLog.insert(message); FBMessageLog.insert(message);
} catch (exc, trace) { } catch (exc, trace) {
ApplicationLog.error('Failed to persist received FB message' + exc.toString(), trace: 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); Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null);
return; return;
} }
@ -348,11 +365,41 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
} }
void _receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) { void _receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) {
//TODO iOS?
ApplicationLog.info('Received local notification<darwin>: $id -> [$title]'); ApplicationLog.info('Received local notification<darwin>: $id -> [$title]');
} }
void _receiveLocalNotification(NotificationResponse details) { void _receiveLocalNotification(NotificationResponse details) {
ApplicationLog.info('Received local notification: ${details.id}'); // User has tapped a flutter_local notification, while the app was running
ApplicationLog.info('Tapped local notification: [[${details.id} | ${details.actionId} | ${details.input} | ${details.notificationResponseType} | ${details.payload}]]');
_handleNotificationClickAction(details.payload, Duration.zero);
}
void _handleNotificationClickAction(String? payload, Duration delay) {
final parts = payload?.split('\n') ?? [];
if (parts.length == 4 && parts[0] == '@SCN_MESSAGE') {
final messageID = parts[1];
() async {
await Future.delayed(delay);
SchedulerBinding.instance.addPostFrameCallback((_) {
ApplicationLog.info('Handle notification action @SCN_MESSAGE --> ${messageID}');
Navi.push(SCNApp.materialKey.currentContext!, () => MessageViewPage(messageID: messageID, preloadedData: null));
});
}();
} else if (parts.length == 3 && parts[0] == '@SCN_MESSAGE_SUMMARY') {
final channelID = parts[1];
() async {
await Future.delayed(delay);
SchedulerBinding.instance.addPostFrameCallback((_) {
ApplicationLog.info('Handle notification action @SCN_MESSAGE_SUMMARY --> ${channelID}');
Navi.push(SCNApp.materialKey.currentContext!, () => ChannelViewPage(channelID: channelID, preloadedData: null, needsReload: null));
});
}();
}
} }
List<DarwinNotificationCategory> getDarwinNotificationCategories() { List<DarwinNotificationCategory> getDarwinNotificationCategories() {

View File

@ -94,7 +94,7 @@ class _ChannelMessageViewPageState extends State<ChannelMessageViewPage> {
message: item, message: item,
allChannels: {this.widget.channel.channelID: this.widget.channel}, allChannels: {this.widget.channel.channelID: this.widget.channel},
onPressed: () { onPressed: () {
Navi.push(context, () => MessageViewPage(message: item)); Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,)));
}, },
), ),
), ),

View File

@ -56,7 +56,7 @@ class _DebugActionsPageState extends State<DebugActionsPage> {
SizedBox(height: 20), SizedBox(height: 20),
UI.button( UI.button(
big: false, big: false,
onPressed: () => Notifier.showLocalNotification('TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null), onPressed: () => Notifier.showLocalNotification('', 'TEST_CHANNEL', "Test Channel", "Channel for testing", "Hello World", "Local Notification test", null),
text: 'Show local notification', text: 'Show local notification',
), ),
], ],

View File

@ -249,7 +249,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
message: item, message: item,
allChannels: _channels ?? {}, allChannels: _channels ?? {},
onPressed: () { onPressed: () {
Navi.push(context, () => MessageViewPage(message: item)); Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,)));
}, },
), ),
), ),

View File

@ -18,9 +18,14 @@ import 'package:simplecloudnotifier/utils/toaster.dart';
import 'package:simplecloudnotifier/utils/ui.dart'; import 'package:simplecloudnotifier/utils/ui.dart';
class MessageViewPage extends StatefulWidget { class MessageViewPage extends StatefulWidget {
const MessageViewPage({super.key, required this.message}); const MessageViewPage({
super.key,
required this.messageID,
required this.preloadedData,
});
final SCNMessage message; // Potentially trimmed final String messageID; // Potentially trimmed
final (SCNMessage,)? preloadedData; // Message is potentially trimmed, whole object is potentially null
@override @override
State<MessageViewPage> createState() => _MessageViewPageState(); State<MessageViewPage> createState() => _MessageViewPageState();
@ -33,8 +38,14 @@ class _MessageViewPageState extends State<MessageViewPage> {
bool _monospaceMode = false; bool _monospaceMode = false;
SCNMessage? message = null;
@override @override
void initState() { void initState() {
if (widget.preloadedData != null) {
message = widget.preloadedData!.$1;
}
mainFuture = fetchData(); mainFuture = fetchData();
super.initState(); super.initState();
} }
@ -47,7 +58,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
final acc = Provider.of<AppAuth>(context, listen: false); final acc = Provider.of<AppAuth>(context, listen: false);
final msg = await APIClient.getMessage(acc, widget.message.messageID); final msg = await APIClient.getMessage(acc, widget.messageID);
final fut_chn = APIClient.getChannelPreview(acc, msg.channelID); final fut_chn = APIClient.getChannelPreview(acc, msg.channelID);
final fut_key = APIClient.getKeyTokenPreview(acc, msg.usedKeyID); final fut_key = APIClient.getKeyTokenPreview(acc, msg.usedKeyID);
@ -89,8 +100,8 @@ class _MessageViewPageState extends State<MessageViewPage> {
return _buildMessageView(context, msg, chn, tok, usr); return _buildMessageView(context, msg, chn, tok, usr);
} else if (snapshot.hasError) { } else if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}')); //TODO nice error page return Center(child: Text('${snapshot.error}')); //TODO nice error page
} else if (!widget.message.trimmed) { } else if (message != null && !this.message!.trimmed) {
return _buildMessageView(context, widget.message, null, null, null); return _buildMessageView(context, this.message!, null, null, null);
} else { } else {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
@ -100,7 +111,9 @@ class _MessageViewPageState extends State<MessageViewPage> {
} }
void _share() async { void _share() async {
var msg = widget.message; if (this.message == null) return;
var msg = this.message!;
if (mainFutureSnapshot != null) { if (mainFutureSnapshot != null) {
(msg, _, _, _) = mainFutureSnapshot!; (msg, _, _, _) = mainFutureSnapshot!;
} }

View File

@ -6,7 +6,7 @@ import 'package:simplecloudnotifier/state/application_log.dart';
import 'package:simplecloudnotifier/state/globals.dart'; import 'package:simplecloudnotifier/state/globals.dart';
class Notifier { class Notifier {
static void showLocalNotification(String channelID, String channelName, String channelDescr, String title, String body, DateTime? timestamp) async { static void showLocalNotification(String messageID, String channelID, String channelName, String channelDescr, String title, String body, DateTime? timestamp) async {
final nid = Globals().sharedPrefs.getInt('notifier.nextid') ?? 1000; final nid = Globals().sharedPrefs.getInt('notifier.nextid') ?? 1000;
Globals().sharedPrefs.setInt('notifier.nextid', nid + 7); Globals().sharedPrefs.setInt('notifier.nextid', nid + 7);
@ -25,6 +25,12 @@ class Notifier {
final newSummaryNID = nid + 1; final newSummaryNID = nid + 1;
ApplicationLog.debug('Create new summary notifications for channel ${channelID} with nid [${newSummaryNID}])'); ApplicationLog.debug('Create new summary notifications for channel ${channelID} with nid [${newSummaryNID}])');
Globals().sharedPrefs.setInt('notifier.summary.$channelID', newSummaryNID); Globals().sharedPrefs.setInt('notifier.summary.$channelID', newSummaryNID);
var payload = '';
if (messageID != '') {
payload = ['@SCN_MESSAGE_SUMMARY', channelID, newSummaryNID].join("\n");
}
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
newSummaryNID, newSummaryNID,
channelName, channelName,
@ -40,6 +46,7 @@ class Notifier {
subText: (channelName == 'main') ? null : channelName, subText: (channelName == 'main') ? null : channelName,
), ),
), ),
payload: payload,
); );
} }
} }
@ -48,6 +55,11 @@ class Notifier {
ApplicationLog.debug('Create new local notifications for message in channel ${channelID} with nid [${newMessageNID}])'); ApplicationLog.debug('Create new local notifications for message in channel ${channelID} with nid [${newMessageNID}])');
var payload = '';
if (messageID != '') {
payload = ['@SCN_MESSAGE', messageID, channelID, newMessageNID].join("\n");
}
// ======== SHOW NOTIFICATION ======== // ======== SHOW NOTIFICATION ========
await flutterLocalNotificationsPlugin.show( await flutterLocalNotificationsPlugin.show(
newMessageNID, newMessageNID,
@ -65,6 +77,7 @@ class Notifier {
subText: (channelName == 'main') ? null : channelName, subText: (channelName == 'main') ? null : channelName,
), ),
), ),
payload: payload,
); );
} }
} }