diff --git a/packages/stream_chat_v1/android/app/src/main/AndroidManifest.xml b/packages/stream_chat_v1/android/app/src/main/AndroidManifest.xml index fa7cbe4..6f96e79 100644 --- a/packages/stream_chat_v1/android/app/src/main/AndroidManifest.xml +++ b/packages/stream_chat_v1/android/app/src/main/AndroidManifest.xml @@ -44,5 +44,11 @@ + + + \ No newline at end of file diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-anydpi-v24/ic_notification.xml b/packages/stream_chat_v1/android/app/src/main/res/drawable-anydpi-v24/ic_notification.xml new file mode 100644 index 0000000..b953a9c --- /dev/null +++ b/packages/stream_chat_v1/android/app/src/main/res/drawable-anydpi-v24/ic_notification.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-anydpi-v24/ic_notification_in_app.xml b/packages/stream_chat_v1/android/app/src/main/res/drawable-anydpi-v24/ic_notification_in_app.xml new file mode 100644 index 0000000..3c858bb --- /dev/null +++ b/packages/stream_chat_v1/android/app/src/main/res/drawable-anydpi-v24/ic_notification_in_app.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-hdpi/ic_notification.png b/packages/stream_chat_v1/android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000..89924d6 Binary files /dev/null and b/packages/stream_chat_v1/android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-hdpi/ic_notification_in_app.png b/packages/stream_chat_v1/android/app/src/main/res/drawable-hdpi/ic_notification_in_app.png new file mode 100644 index 0000000..738f9dd Binary files /dev/null and b/packages/stream_chat_v1/android/app/src/main/res/drawable-hdpi/ic_notification_in_app.png differ diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-mdpi/ic_notification.png b/packages/stream_chat_v1/android/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000..bc99f1b Binary files /dev/null and b/packages/stream_chat_v1/android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-mdpi/ic_notification_in_app.png b/packages/stream_chat_v1/android/app/src/main/res/drawable-mdpi/ic_notification_in_app.png new file mode 100644 index 0000000..e983bbd Binary files /dev/null and b/packages/stream_chat_v1/android/app/src/main/res/drawable-mdpi/ic_notification_in_app.png differ diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/packages/stream_chat_v1/android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000..c75ff08 Binary files /dev/null and b/packages/stream_chat_v1/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-xhdpi/ic_notification_in_app.png b/packages/stream_chat_v1/android/app/src/main/res/drawable-xhdpi/ic_notification_in_app.png new file mode 100644 index 0000000..1eec20b Binary files /dev/null and b/packages/stream_chat_v1/android/app/src/main/res/drawable-xhdpi/ic_notification_in_app.png differ diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/packages/stream_chat_v1/android/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000..f65b654 Binary files /dev/null and b/packages/stream_chat_v1/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/packages/stream_chat_v1/android/app/src/main/res/drawable-xxhdpi/ic_notification_in_app.png b/packages/stream_chat_v1/android/app/src/main/res/drawable-xxhdpi/ic_notification_in_app.png new file mode 100644 index 0000000..c0c3912 Binary files /dev/null and b/packages/stream_chat_v1/android/app/src/main/res/drawable-xxhdpi/ic_notification_in_app.png differ diff --git a/packages/stream_chat_v1/lib/app.dart b/packages/stream_chat_v1/lib/app.dart index 3e95328..4b7ed04 100644 --- a/packages/stream_chat_v1/lib/app.dart +++ b/packages/stream_chat_v1/lib/app.dart @@ -8,6 +8,7 @@ import 'package:example/state/init_data.dart'; import 'package:example/utils/app_config.dart'; import 'package:example/utils/local_notification_observer.dart'; import 'package:example/utils/localizations.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -15,11 +16,63 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_localizations/stream_chat_localizations.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; +import 'firebase_options.dart'; + +@pragma('vm:entry-point') +Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + debugPrint('[onBackgroundMessage] #firebase; message: ${message.toMap()}'); + + final data = message.data; + if (data['type'] != 'message.new') { + return; + } + String? apiKey, userId, token; + if (!kIsWeb) { + const secureStorage = FlutterSecureStorage(); + apiKey = await secureStorage.read(key: kStreamApiKey); + userId = await secureStorage.read(key: kStreamUserId); + token = await secureStorage.read(key: kStreamToken); + } + debugPrint('[onBackgroundMessage] #firebase; apiKey: $apiKey, userId: $userId, token: $token'); + if (userId == null || token == null) { + return; + } + final client = buildStreamChatClient(apiKey ?? kDefaultStreamApiKey); + final persistenceClient = StreamChatPersistenceClient(); + await persistenceClient.connect(userId); + + await client.connectUser( + User(id: userId), + token, + connectWebSocket: false, + ); + + final messageId = data['id']; + final cid = data['cid']; + final response = await client.getMessage(messageId); + debugPrint('[onBackgroundMessage] #firebase; response: $response'); + await persistenceClient.updateMessages(cid, [response.message]); + debugPrint('[onBackgroundMessage] #firebase; saved'); + +} + +Future _firebaseMessagingForegroundHandler(RemoteMessage message) async { + debugPrint('[onForegroundMessage] #firebase; message: ${message.toMap()}'); +} + +Future _firebaseMessagingOpenedHandler(RemoteMessage message) async { + debugPrint('[onOpenedMessage] #firebase; message: ${message.toMap()}'); +} + final chatPersistentClient = StreamChatPersistenceClient( logLevel: Level.SEVERE, connectionMode: ConnectionMode.regular, @@ -28,7 +81,7 @@ final chatPersistentClient = StreamChatPersistenceClient( void _sampleAppLogHandler(LogRecord record) async { if (kDebugMode) StreamChatClient.defaultLogHandler(record); - // report errors to setnry.io + // report errors to sentry.io if (record.error != null || record.stackTrace != null) { await Sentry.captureException( record.error, @@ -62,6 +115,9 @@ class _StreamChatSampleAppState extends State with SplashScreenStateMixin, TickerProviderStateMixin { final InitNotifier _initNotifier = InitNotifier(); + final firebaseSubscriptions = >[]; + StreamSubscription? userIdSubscription; + Future _initConnection() async { String? apiKey, userId, token; @@ -71,7 +127,7 @@ class _StreamChatSampleAppState extends State userId = await secureStorage.read(key: kStreamUserId); token = await secureStorage.read(key: kStreamToken); } - + debugPrint('[initConnection] #firebase; apiKey: $apiKey, userId: $userId, token: $token'); final client = buildStreamChatClient(apiKey ?? kDefaultStreamApiKey); if (userId != null && token != null) { @@ -86,6 +142,77 @@ class _StreamChatSampleAppState extends State return InitData(client, prefs); } + Future _initFirebaseMessaging(StreamChatClient client) async { + userIdSubscription?.cancel(); + userIdSubscription = client.state.currentUserStream + .map((it) => it?.id) + .distinct() + .listen((userId) async { + debugPrint('[onUserIdSet] #firebase; userId: "$userId"'); + + if (userId != null) { + FirebaseMessaging.onBackgroundMessage( + _firebaseMessagingBackgroundHandler); + firebaseSubscriptions.add(FirebaseMessaging.onMessage + .listen(_firebaseMessagingForegroundHandler)); + firebaseSubscriptions + .add(FirebaseMessaging.onMessageOpenedApp.listen((message) async { + debugPrint( + '[onOpenedMessage] #firebase; message: ${message.toMap()}'); + final channelCid = (message.data['cid'] as String?) ?? ''; + final channelType = (message.data['channel_type'] as String?) ?? ''; + final channelId = (message.data['channel_id'] as String?) ?? ''; + debugPrint('[onOpenedMessage] #firebase; channelCid; $channelCid, channelType: $channelType, channelId: $channelId'); + var channel = client.state.channels[channelCid]; + debugPrint('[onOpenedMessage] #firebase; channel1: $channel'); + if (channel == null) { + channel = client.channel( + channelId, + id: channelId, + ); + final state = await channel.watch(); + debugPrint('[onOpenedMessage] #firebase; channelState: $state'); + } + debugPrint('[onOpenedMessage] #firebase; channel2: $channel'); + + debugPrint('[onOpenedMessage] #firebase; #1'); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { + if (channel == null) { + debugPrint('[onOpenedMessage] #firebase; rejected (channel is null)'); + return; + } + try { + debugPrint('[onOpenedMessage] #firebase; #2: ${_navigatorKey.currentContext}'); + final router = GoRouter.of(_navigatorKey.currentContext!); + debugPrint('[onOpenedMessage] #firebase; #3: $router'); + + router.pushNamed( + Routes.CHANNEL_PAGE.name, + params: Routes.CHANNEL_PAGE.params(channel), + ); + debugPrint('[onOpenedMessage] #firebase; #4'); + } catch (e, stk) { + debugPrint('[onOpenedMessage] #firebase; failed: $e; $stk'); + } + }); + + })); + firebaseSubscriptions.add( + FirebaseMessaging.instance.onTokenRefresh.listen((token) async { + debugPrint('[onTokenRefresh] #firebase; token: "$token"'); + await client.addDevice(token, PushProvider.firebase); + debugPrint('[onTokenRefresh] #firebase; token set: $token'); + })); + final token = await FirebaseMessaging.instance.getToken(); + debugPrint('[initFirebaseMessaging] #firebase; token: "$token"'); + if (token != null) { + await client.addDevice(token, PushProvider.firebase); + debugPrint('[initFirebaseMessaging] #firebase; token set: $token'); + } + } + }); + } + @override void initState() { final timeOfStartMs = DateTime.now().millisecondsSinceEpoch; @@ -100,19 +227,38 @@ class _StreamChatSampleAppState extends State if (now - timeOfStartMs > 1500) { SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + debugPrint('[forwardAnimations] #firebase; context1: $context'); + debugPrint('[forwardAnimations] #firebase; _navigatorKey.currentContext1: ${_navigatorKey.currentContext}'); forwardAnimations(); + final router = GoRouter.of(_navigatorKey.currentContext!); + debugPrint('[forwardAnimations] #firebase; router1: ${router}'); }); } else { Future.delayed(const Duration(milliseconds: 1500)).then((value) { + debugPrint('[forwardAnimations] #firebase; context2: $context'); + debugPrint('[forwardAnimations] #firebase; _navigatorKey.currentContext2: ${_navigatorKey.currentContext}'); forwardAnimations(); + final router = GoRouter.of(_navigatorKey.currentContext!); + debugPrint('[forwardAnimations] #firebase; router2: ${router}'); }); } + _initFirebaseMessaging(initData.client); }, ); super.initState(); } + @override + void dispose() { + super.dispose(); + userIdSubscription?.cancel(); + for (final subscription in firebaseSubscriptions) { + unawaited(subscription.cancel()); + } + firebaseSubscriptions.clear(); + } + final GlobalKey _navigatorKey = GlobalKey(); LocalNotificationObserver? localNotificationObserver; @@ -154,6 +300,7 @@ class _StreamChatSampleAppState extends State @override Widget build(BuildContext context) { + debugPrint('[AppState.build] #firebase; context: $context'); return Stack( alignment: Alignment.center, children: [ diff --git a/packages/stream_chat_v1/lib/main.dart b/packages/stream_chat_v1/lib/main.dart index 40c5399..2211b9f 100644 --- a/packages/stream_chat_v1/lib/main.dart +++ b/packages/stream_chat_v1/lib/main.dart @@ -1,35 +1,21 @@ import 'dart:async'; +import 'package:example/app.dart'; import 'package:example/utils/app_config.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; - - -import 'package:example/app.dart'; import 'firebase_options.dart'; -@pragma('vm:entry-point') -Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { - print("#firebase; PN_2 "); - await Firebase.initializeApp(); - - // If you're going to use other Firebase services in the background, such as Firestore, - // make sure you call `initializeApp` before using other Firebase services. - print('[setupPushNotifications] newMessage: ${message.toMap()}'); -} - Future main() async { WidgetsFlutterBinding.ensureInitialized(); - print("#firebase; PN_1"); - await Firebase.initializeApp(); - Firebase.apps.forEach((it) { - print("#firebase; app: $it"); - }); - FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler); + debugPrint("#firebase; PN_1"); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + /// Captures errors reported by the Flutter framework. FlutterError.onError = (FlutterErrorDetails details) { if (kDebugMode) { diff --git a/packages/stream_chat_v1/lib/pages/channel_list_page.dart b/packages/stream_chat_v1/lib/pages/channel_list_page.dart index c045e0b..f4139e4 100644 --- a/packages/stream_chat_v1/lib/pages/channel_list_page.dart +++ b/packages/stream_chat_v1/lib/pages/channel_list_page.dart @@ -69,6 +69,7 @@ class _ChannelListPageState extends State { @override Widget build(BuildContext context) { + print(">>>>>>>>>> ChannelListPage"); final user = StreamChat.of(context).currentUser; if (user == null) { return const Offstage(); @@ -76,8 +77,10 @@ class _ChannelListPageState extends State { return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: StreamChannelListHeader( - onNewChatButtonTap: () => - GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name), + onNewChatButtonTap: () { + print(">>>>>>>>>> onNewChatButtonTap"); + GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name); + }, preNavigationCallback: () => FocusScope.of(context).requestFocus(FocusNode()), ), @@ -220,6 +223,7 @@ class LeftDrawer extends StatelessWidget { .withOpacity(.5), ), onTap: () { + print(">>>>>>>>>> NEW_CHAT"); Navigator.of(context).pop(); GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name); }, @@ -238,6 +242,7 @@ class LeftDrawer extends StatelessWidget { .withOpacity(.5), ), onTap: () { + print(">>>>>>>>>> NEW_GROUP_CHAT"); Navigator.of(context).pop(); GoRouter.of(context).pushNamed(Routes.NEW_GROUP_CHAT.name); }, @@ -253,6 +258,7 @@ class LeftDrawer extends StatelessWidget { alignment: Alignment.bottomCenter, child: ListTile( onTap: () async { + print(">>>>>>>>>> CHOOSE_USER"); final client = StreamChat.of(context).client; final router = GoRouter.of(context); final initNotifier = context.read(); diff --git a/packages/stream_chat_v1/lib/pages/channel_page.dart b/packages/stream_chat_v1/lib/pages/channel_page.dart index e2714ee..652f266 100644 --- a/packages/stream_chat_v1/lib/pages/channel_page.dart +++ b/packages/stream_chat_v1/lib/pages/channel_page.dart @@ -47,12 +47,14 @@ class _ChannelPageState extends State { @override Widget build(BuildContext context) { + print(">>>>>>>>>> CHANNEL PAGE"); return Scaffold( backgroundColor: StreamChatTheme.of(context).colorTheme.appBg, appBar: StreamChannelHeader( showTypingIndicator: false, onBackPressed: () => GoRouter.of(context).pop(), onImageTap: () async { + print(">>>>>>>>>> onImageTap"); final channel = StreamChannel.of(context).channel; final router = GoRouter.of(context); @@ -62,6 +64,7 @@ class _ChannelPageState extends State { (element) => element.user!.id != currentUser!.id, ); if (otherUser != null) { + print(">>>>>>>>>> GROUP_INFO_SCREEN otherUser"); router.pushNamed( Routes.CHAT_INFO_SCREEN.name, params: Routes.CHAT_INFO_SCREEN.params(channel), @@ -69,6 +72,7 @@ class _ChannelPageState extends State { ); } } else { + print(">>>>>>>>>> GROUP_INFO_SCREEN"); GoRouter.of(context).pushNamed( Routes.GROUP_INFO_SCREEN.name, params: Routes.GROUP_INFO_SCREEN.params(channel), diff --git a/packages/stream_chat_v1/lib/utils/notifications_service.dart b/packages/stream_chat_v1/lib/utils/notifications_service.dart index a02d2ee..35e21a5 100644 --- a/packages/stream_chat_v1/lib/utils/notifications_service.dart +++ b/packages/stream_chat_v1/lib/utils/notifications_service.dart @@ -21,7 +21,7 @@ void showLocalNotification( if (event.message == null) return; final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); const initializationSettingsAndroid = - AndroidInitializationSettings('launch_background'); + AndroidInitializationSettings('ic_notification_in_app'); const initializationSettingsIOS = IOSInitializationSettings(); const initializationSettings = InitializationSettings( android: initializationSettingsAndroid, @@ -33,6 +33,8 @@ void showLocalNotification( await flutterLocalNotificationsPlugin.initialize( initializationSettings, onSelectNotification: (channelCid) async { + debugPrint("[onSelectNotification] #firebase; channelCid: $channelCid"); + debugPrint("[onSelectNotification] #firebase; context: $context"); if (channelCid != null) { final client = StreamChat.of(context).client; final router = GoRouter.of(context);