Skip to content

Commit 5d654ab

Browse files
xsahil03xkanat
andauthored
feat(repo): display new PN messages (#2133)
* fix sample app, display new PN mssages * chore: organize imports --------- Co-authored-by: kanat_k <[email protected]>
1 parent 5bb5944 commit 5d654ab

File tree

2 files changed

+164
-8
lines changed

2 files changed

+164
-8
lines changed

sample_app/android/gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
org.gradle.jvmargs=-Xmx1536M
1+
org.gradle.jvmargs=-Xmx4g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -Dlint.nullness.ignore-deprecated=true
22
android.enableR8=true
33
android.useAndroidX=true
44
android.enableJetifier=true

sample_app/lib/app.dart

+163-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import 'package:firebase_core/firebase_core.dart';
44
import 'package:firebase_messaging/firebase_messaging.dart';
55
import 'package:flutter/foundation.dart';
66
import 'package:flutter/material.dart';
7-
import 'package:flutter/scheduler.dart';
7+
import 'package:flutter/scheduler.dart' hide Priority;
8+
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
9+
hide Message;
810
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
911
import 'package:go_router/go_router.dart';
1012
import 'package:provider/provider.dart';
@@ -23,6 +25,25 @@ import 'package:stream_chat_localizations/stream_chat_localizations.dart';
2325
import 'package:stream_chat_persistence/stream_chat_persistence.dart';
2426
import 'package:streaming_shared_preferences/streaming_shared_preferences.dart';
2527

28+
// Define supported notification types
29+
const expectedNotificationTypes = [
30+
EventType.messageNew,
31+
EventType.messageUpdated,
32+
EventType.reactionNew,
33+
EventType.reactionUpdated,
34+
];
35+
36+
const notificationChannelId = 'stream_GetStreamFlutterClient';
37+
const notificationChannelName = 'Stream Notifications';
38+
const notificationChannelDescription = 'Notifications for Stream messages';
39+
40+
// Define platform constants
41+
const bool kIsIOS = bool.fromEnvironment('dart.io.is_ios');
42+
43+
// Initialize FlutterLocalNotificationsPlugin for background messages
44+
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
45+
FlutterLocalNotificationsPlugin();
46+
2647
/// Constructs callback for background notification handling.
2748
///
2849
/// Will be invoked from another Isolate, that's why it's required to
@@ -36,10 +57,13 @@ Future<void> _onFirebaseBackgroundMessage(RemoteMessage message) async {
3657
final data = message.data;
3758
// ensure that Push Notification was sent by Stream.
3859
if (data['sender'] != 'stream.chat') {
60+
debugPrint('[onBackgroundMessage] #firebase; not sent by Stream');
3961
return;
4062
}
41-
// ensure that Push Notification relates to a new message event.
42-
if (data['type'] != 'message.new') {
63+
final eventType = data['type'];
64+
// ensure that Push Notification relates to a supported event type
65+
if (!expectedNotificationTypes.contains(eventType)) {
66+
debugPrint('[onBackgroundMessage] #firebase; unexpected type: $eventType');
4367
return;
4468
}
4569
// If you're going to use Firebase services in the background, make sure
@@ -55,7 +79,12 @@ Future<void> _onFirebaseBackgroundMessage(RemoteMessage message) async {
5579
userId = await secureStorage.read(key: kStreamUserId);
5680
token = await secureStorage.read(key: kStreamToken);
5781
}
58-
if (userId == null || token == null) {
82+
if (userId == null) {
83+
debugPrint('[onBackgroundMessage] #firebase; user not found');
84+
return;
85+
}
86+
if (token == null) {
87+
debugPrint('[onBackgroundMessage] #firebase; token not found');
5988
return;
6089
}
6190
final chatClient = buildStreamChatClient(apiKey ?? kDefaultStreamApiKey);
@@ -71,16 +100,142 @@ Future<void> _onFirebaseBackgroundMessage(RemoteMessage message) async {
71100
await chatPersistentClient.connect(userId);
72101
}
73102

74-
final messageId = data['id'];
103+
final messageId = data['message_id'];
104+
if (messageId == null) {
105+
debugPrint('[onBackgroundMessage] #firebase; messageId not found');
106+
return;
107+
}
75108
final cid = data['cid'];
109+
if (cid == null) {
110+
debugPrint('[onBackgroundMessage] #firebase; cid not found');
111+
return;
112+
}
76113
// pre-cache the new message using client and persistence.
77114
final response = await chatClient.getMessage(messageId);
78115
await chatPersistentClient.updateMessages(cid, [response.message]);
116+
117+
final title = data['title'];
118+
final body = data['body'];
119+
120+
// Show Android notification
121+
if (!kIsWeb && !kIsIOS) {
122+
await _showAndroidNotification(
123+
eventType: eventType,
124+
cid: cid,
125+
messageId: messageId,
126+
title: message.notification?.title ?? title ?? 'Fallback title',
127+
body: message.notification?.body ?? body ?? 'Fallback body',
128+
);
129+
}
79130
} catch (e, stk) {
80131
debugPrint('[onBackgroundMessage] #firebase; failed: $e; $stk');
81132
}
82133
}
83134

135+
/// Shows an Android notification for a background message
136+
Future<void> _showAndroidNotification({
137+
required String eventType,
138+
required String cid,
139+
required String messageId,
140+
required String title,
141+
required String body,
142+
}) async {
143+
try {
144+
// Create notification channel for Android
145+
await _createNotificationChannel();
146+
147+
// Initialize the Android notification channel
148+
const androidNotificationDetails = AndroidNotificationDetails(
149+
notificationChannelId,
150+
notificationChannelName,
151+
channelDescription: notificationChannelDescription,
152+
importance: Importance.high,
153+
priority: Priority.high,
154+
showWhen: true,
155+
enableVibration: true,
156+
playSound: true,
157+
// Using default sound instead of custom sound resource
158+
// sound: RawResourceAndroidNotificationSound('notification_sound'),
159+
// Using default app icon instead of custom icon
160+
// icon: 'ic_notification',
161+
);
162+
163+
const notificationDetails = NotificationDetails(
164+
android: androidNotificationDetails,
165+
);
166+
167+
// Initialize the plugin with click handler
168+
// Use the default app icon for notifications
169+
const initializationSettingsAndroid = AndroidInitializationSettings(
170+
'ic_notification_in_app',
171+
);
172+
const initializationSettings = InitializationSettings(
173+
android: initializationSettingsAndroid,
174+
);
175+
176+
// Initialize the plugin with a notification click handler
177+
await flutterLocalNotificationsPlugin.initialize(
178+
initializationSettings,
179+
onDidReceiveNotificationResponse: (NotificationResponse response) async {
180+
debugPrint(
181+
'[onBackgroundMessage] #firebase; notification clicked: ${response.payload}');
182+
// The payload contains the channel information (channelType:channelId)
183+
// This will be handled when the app is opened
184+
},
185+
);
186+
187+
// Generate a unique notification ID
188+
final notificationId = (eventType + cid + messageId).hashCode;
189+
190+
// Show the notification
191+
await flutterLocalNotificationsPlugin.show(
192+
notificationId, // Notification ID
193+
title, // Notification title
194+
body, // Notification body
195+
notificationDetails,
196+
payload: cid,
197+
);
198+
199+
debugPrint(
200+
'[onBackgroundMessage] #firebase; android notification shown successfully: ID=$notificationId, Title="$title"');
201+
} catch (e) {
202+
debugPrint(
203+
'[onBackgroundMessage] #firebase; failed to show notification: $e');
204+
}
205+
}
206+
207+
/// Creates the notification channel for Android
208+
Future<void> _createNotificationChannel() async {
209+
try {
210+
const channel = AndroidNotificationChannel(
211+
notificationChannelId,
212+
notificationChannelName,
213+
description: notificationChannelDescription,
214+
importance: Importance.high,
215+
enableVibration: true,
216+
playSound: true,
217+
showBadge: true,
218+
);
219+
220+
// Create the channel
221+
final androidPlugin =
222+
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
223+
AndroidFlutterLocalNotificationsPlugin>();
224+
225+
if (androidPlugin != null) {
226+
await androidPlugin.createNotificationChannel(channel);
227+
debugPrint(
228+
'[onBackgroundMessage] #firebase; notification channel created');
229+
} else {
230+
debugPrint(
231+
'[onBackgroundMessage] #firebase; failed to resolve Android plugin');
232+
}
233+
} catch (e) {
234+
debugPrint(
235+
'[onBackgroundMessage] #firebase; notification channel failed: $e');
236+
}
237+
}
238+
84239
final chatPersistentClient = StreamChatPersistenceClient(
85240
logLevel: Level.SEVERE,
86241
);
@@ -200,9 +355,10 @@ class _StreamChatSampleAppState extends State<StreamChatSampleApp>
200355
debugPrint('[onMessageOpenedApp] #firebase; message: ${message.toMap()}');
201356
// This callback is getting invoked when the user clicks
202357
// on the notification in case if notification was shown by OS.
203-
final channelType = (message.data['channel_type'] as String?) ?? '';
204-
final channelId = (message.data['channel_id'] as String?) ?? '';
205358
final channelCid = (message.data['cid'] as String?) ?? '';
359+
final parts = channelCid.split(':');
360+
final channelType = parts[0];
361+
final channelId = parts[1];
206362
var channel = client.state.channels[channelCid];
207363
if (channel == null) {
208364
channel = client.channel(

0 commit comments

Comments
 (0)