diff --git a/flutter/TODO.md b/flutter/TODO.md index e0903f8..ef21488 100644 --- a/flutter/TODO.md +++ b/flutter/TODO.md @@ -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 diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 6feb2d1..5b552d4 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -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(); + @override Widget build(BuildContext context) { return ToastificationWrapper( @@ -211,6 +224,7 @@ class SCNApp extends StatelessWidget { ), child: Consumer( 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: ${notificationResponse.id}'); } @@ -268,11 +283,13 @@ void setFirebaseToken(String fcmToken) async { @pragma('vm:entry-point') Future _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 _receiveMessage(RemoteMessage message, bool foreground) async { await Hive.openBox('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 _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 _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 _receiveMessage(RemoteMessage message, bool foreground) async { } void _receiveLocalDarwinNotification(int id, String? title, String? body, String? payload) { + //TODO iOS? ApplicationLog.info('Received local notification: $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 getDarwinNotificationCategories() { diff --git a/flutter/lib/pages/channel_message_view/channel_message_view.dart b/flutter/lib/pages/channel_message_view/channel_message_view.dart index 2146dac..42a9068 100644 --- a/flutter/lib/pages/channel_message_view/channel_message_view.dart +++ b/flutter/lib/pages/channel_message_view/channel_message_view.dart @@ -94,7 +94,7 @@ class _ChannelMessageViewPageState extends State { 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,))); }, ), ), diff --git a/flutter/lib/pages/debug/debug_actions.dart b/flutter/lib/pages/debug/debug_actions.dart index e658e0e..aaef251 100644 --- a/flutter/lib/pages/debug/debug_actions.dart +++ b/flutter/lib/pages/debug/debug_actions.dart @@ -56,7 +56,7 @@ class _DebugActionsPageState extends State { 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', ), ], diff --git a/flutter/lib/pages/message_list/message_list.dart b/flutter/lib/pages/message_list/message_list.dart index bbe4cb0..cc39226 100644 --- a/flutter/lib/pages/message_list/message_list.dart +++ b/flutter/lib/pages/message_list/message_list.dart @@ -249,7 +249,7 @@ class _MessageListPageState extends State with RouteAware { message: item, allChannels: _channels ?? {}, onPressed: () { - Navi.push(context, () => MessageViewPage(message: item)); + Navi.push(context, () => MessageViewPage(messageID: item.messageID, preloadedData: (item,))); }, ), ), diff --git a/flutter/lib/pages/message_view/message_view.dart b/flutter/lib/pages/message_view/message_view.dart index bd3463f..57f8a2c 100644 --- a/flutter/lib/pages/message_view/message_view.dart +++ b/flutter/lib/pages/message_view/message_view.dart @@ -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 createState() => _MessageViewPageState(); @@ -33,8 +38,14 @@ class _MessageViewPageState extends State { 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 { final acc = Provider.of(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 { 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 { } void _share() async { - var msg = widget.message; + if (this.message == null) return; + + var msg = this.message!; if (mainFutureSnapshot != null) { (msg, _, _, _) = mainFutureSnapshot!; } diff --git a/flutter/lib/utils/notifier.dart b/flutter/lib/utils/notifier.dart index 0abb3ce..80c7050 100644 --- a/flutter/lib/utils/notifier.dart +++ b/flutter/lib/utils/notifier.dart @@ -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, ); } }