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
- [ ] 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 from mattn to go-sqlite
- [ ] Single struct for model/db/json

View File

@ -2,6 +2,7 @@ import 'dart:io';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:provider/provider.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/scn_message.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/state/app_bar_state.dart';
import 'package:simplecloudnotifier/state/app_events.dart';
@ -174,12 +177,20 @@ void main() async {
iOS: initializationSettingsDarwin,
linux: initializationSettingsLinux,
);
flutterLocalNotificationsPlugin.initialize(initializationSettings, onDidReceiveNotificationResponse: _receiveLocalNotification);
flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: _receiveLocalNotification,
onDidReceiveBackgroundNotificationResponse: _notificationTapBackground,
);
final appLaunchNotification = await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
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}');
_handleNotificationClickAction(appLaunchNotification.notificationResponse?.payload, Duration(milliseconds: 600));
}
}
@ -201,6 +212,8 @@ void main() async {
class SCNApp extends StatelessWidget {
SCNApp({super.key});
static var materialKey = GlobalKey<NavigatorState>();
@override
Widget build(BuildContext context) {
return ToastificationWrapper(
@ -211,6 +224,7 @@ class SCNApp extends StatelessWidget {
),
child: Consumer<AppTheme>(
builder: (context, appTheme, child) => MaterialApp(
navigatorKey: SCNApp.materialKey,
title: 'SimpleCloudNotifier',
navigatorObservers: [Navi.routeObserver, Navi.modalRouteObserver],
theme: ThemeData(
@ -226,7 +240,8 @@ class SCNApp extends StatelessWidget {
}
@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}');
}
@ -268,11 +283,13 @@ void setFirebaseToken(String fcmToken) async {
@pragma('vm:entry-point')
Future<void> _onBackgroundMessage(RemoteMessage message) async {
// a firebase message was received while the app was in the background or terminated
await _receiveMessage(message, false);
}
@pragma('vm:entry-point')
void _onForegroundMessage(RemoteMessage message) {
// a firebase message was received while the app was in the foreground
_receiveMessage(message, true);
}
@ -305,7 +322,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
await Hive.openBox<SCNRequest>('scn-requests');
} 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);
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (init failed)", null);
return;
}
@ -322,10 +339,10 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
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);
Notifier.showLocalNotification(scn_msg_id, channel_id, channel, 'Channel: ${channel}', title, body, timestamp);
} 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);
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (decode failed)", null);
return;
}
@ -333,7 +350,7 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
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);
Notifier.showLocalNotification("", "@ERROR", "@ERROR", 'Error Channel', "Error", "Failed to receive SCN message (persist failed)", null);
return;
}
@ -348,11 +365,41 @@ Future<void> _receiveMessage(RemoteMessage message, bool foreground) async {
}
void _receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) {
//TODO iOS?
ApplicationLog.info('Received local notification<darwin>: $id -> [$title]');
}
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() {

View File

@ -94,7 +94,7 @@ class _ChannelMessageViewPageState extends State<ChannelMessageViewPage> {
message: item,
allChannels: {this.widget.channel.channelID: this.widget.channel},
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),
UI.button(
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',
),
],

View File

@ -249,7 +249,7 @@ class _MessageListPageState extends State<MessageListPage> with RouteAware {
message: item,
allChannels: _channels ?? {},
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';
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
State<MessageViewPage> createState() => _MessageViewPageState();
@ -33,8 +38,14 @@ class _MessageViewPageState extends State<MessageViewPage> {
bool _monospaceMode = false;
SCNMessage? message = null;
@override
void initState() {
if (widget.preloadedData != null) {
message = widget.preloadedData!.$1;
}
mainFuture = fetchData();
super.initState();
}
@ -47,7 +58,7 @@ class _MessageViewPageState extends State<MessageViewPage> {
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_key = APIClient.getKeyTokenPreview(acc, msg.usedKeyID);
@ -89,8 +100,8 @@ class _MessageViewPageState extends State<MessageViewPage> {
return _buildMessageView(context, msg, chn, tok, usr);
} else if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}')); //TODO nice error page
} else if (!widget.message.trimmed) {
return _buildMessageView(context, widget.message, null, null, null);
} else if (message != null && !this.message!.trimmed) {
return _buildMessageView(context, this.message!, null, null, null);
} else {
return const Center(child: CircularProgressIndicator());
}
@ -100,7 +111,9 @@ class _MessageViewPageState extends State<MessageViewPage> {
}
void _share() async {
var msg = widget.message;
if (this.message == null) return;
var msg = this.message!;
if (mainFutureSnapshot != null) {
(msg, _, _, _) = mainFutureSnapshot!;
}

View File

@ -6,7 +6,7 @@ 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 {
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;
Globals().sharedPrefs.setInt('notifier.nextid', nid + 7);
@ -25,6 +25,12 @@ class Notifier {
final newSummaryNID = nid + 1;
ApplicationLog.debug('Create new summary notifications for channel ${channelID} with nid [${newSummaryNID}])');
Globals().sharedPrefs.setInt('notifier.summary.$channelID', newSummaryNID);
var payload = '';
if (messageID != '') {
payload = ['@SCN_MESSAGE_SUMMARY', channelID, newSummaryNID].join("\n");
}
await flutterLocalNotificationsPlugin.show(
newSummaryNID,
channelName,
@ -40,6 +46,7 @@ class Notifier {
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}])');
var payload = '';
if (messageID != '') {
payload = ['@SCN_MESSAGE', messageID, channelID, newMessageNID].join("\n");
}
// ======== SHOW NOTIFICATION ========
await flutterLocalNotificationsPlugin.show(
newMessageNID,
@ -65,6 +77,7 @@ class Notifier {
subText: (channelName == 'main') ? null : channelName,
),
),
payload: payload,
);
}
}