diff --git a/packages/stream_chat_v1/android/app/google-services.json b/packages/stream_chat_v1/android/app/google-services.json
index 2611383..5f06d00 100644
--- a/packages/stream_chat_v1/android/app/google-services.json
+++ b/packages/stream_chat_v1/android/app/google-services.json
@@ -1,35 +1,554 @@
{
"project_info": {
- "project_number": "1004276287628",
- "firebase_url": "https://test-a0490.firebaseio.com",
- "project_id": "test-a0490",
- "storage_bucket": "test-a0490.appspot.com"
+ "project_number": "674907137625",
+ "firebase_url": "https://stream-chat-internal.firebaseio.com",
+ "project_id": "stream-chat-internal",
+ "storage_bucket": "stream-chat-internal.appspot.com"
},
"client": [
{
"client_info": {
- "mobilesdk_app_id": "1:1004276287628:android:7fff771d5e464c6a5f93a9",
+ "mobilesdk_app_id": "1:674907137625:android:e55b74fd5747e39ad7f348",
"android_client_info": {
"package_name": "com.example.example"
}
},
"oauth_client": [
{
- "client_id": "1004276287628-43j2uub5rd0968ois7h963i0np373t6k.apps.googleusercontent.com",
+ "client_id": "674907137625-2scfo9a5cs074dced5vhm712ej6hhtpm.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "com.example.example",
+ "certificate_hash": "84e6cb4b0fd5e03f18cc895f25771449543e5b0b"
+ }
+ },
+ {
+ "client_id": "674907137625-c4p9d8pffjokeurnhlkkg0n7hlqhohrp.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "com.example.example",
+ "certificate_hash": "df0590804ff40703acf8b82d07ed2f8a21cd4cc8"
+ }
+ },
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
- "current_key": "AIzaSyC2ESbd8qWtIUWwGn1L_Dbxzh2W6yfGqiU"
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
- "client_id": "1004276287628-43j2uub5rd0968ois7h963i0np373t6k.apps.googleusercontent.com",
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
"client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:5effa1cd0fef9003d7f348",
+ "android_client_info": {
+ "package_name": "com.sampleapp"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:07c76802bbfd5654d7f348",
+ "android_client_info": {
+ "package_name": "com.sampleapp.rnpushtest"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-bl2uf0ct9k22u5g5scak89ou9q5631vd.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "com.sampleapp.rnpushtest",
+ "certificate_hash": "5e8f16062ea3cd2c4a0d547876baa6f38cabf625"
+ }
+ },
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:c02fbdcc683148e1d7f348",
+ "android_client_info": {
+ "package_name": "io.getstream.chat.android.compose.sample"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-70lqivg5sj9plf8cadf3mb9d4kqpjgg7.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.getstream.chat.android.compose.sample",
+ "certificate_hash": "0df0d0349c0eb717eea90c0b41fc00ab1ab04e16"
+ }
+ },
+ {
+ "client_id": "674907137625-plbo43m7pjorusbm76qstde280asrf71.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.getstream.chat.android.compose.sample",
+ "certificate_hash": "c3188b0a93adae431af73d407565f38f284cbb4e"
+ }
+ },
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:e96a572655e6c0e8d7f348",
+ "android_client_info": {
+ "package_name": "io.getstream.chat.android.compose.sample.debug"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-ioqo27dsi1u5mg7uc9ubgpe0olma47hg.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.getstream.chat.android.compose.sample.debug",
+ "certificate_hash": "c3188b0a93adae431af73d407565f38f284cbb4e"
+ }
+ },
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:c41e48c4c799aee8d7f348",
+ "android_client_info": {
+ "package_name": "io.getstream.chat.example"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:c36d6f4ab7491192d7f348",
+ "android_client_info": {
+ "package_name": "io.getstream.chat.sample"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:d91b4d966e372431d7f348",
+ "android_client_info": {
+ "package_name": "io.getstream.chat.sample.debug"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:ff6593c705df1c43d7f348",
+ "android_client_info": {
+ "package_name": "io.getstream.chat.sdk"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-khntpmmm3s1756v4jugnhcifgjlqrmd0.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.getstream.chat.sdk",
+ "certificate_hash": "38f7d663a2ed58ae8990d365c41bd82df62185dc"
+ }
+ },
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:630333f72f5080b2d7f348",
+ "android_client_info": {
+ "package_name": "io.getstream.chat.ui.sample"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-8s27vqo4puao5d025nlsakt78j1pnb45.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.getstream.chat.ui.sample",
+ "certificate_hash": "0df0d0349c0eb717eea90c0b41fc00ab1ab04e16"
+ }
+ },
+ {
+ "client_id": "674907137625-j82a5eaohtarvub3agqtossi90fpefhg.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.getstream.chat.ui.sample",
+ "certificate_hash": "c3188b0a93adae431af73d407565f38f284cbb4e"
+ }
+ },
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:479d5763241739ded7f348",
+ "android_client_info": {
+ "package_name": "io.getstream.chat.ui.sample.debug"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-miq20f82jaeioq126h3sn3sagm5fdbo5.apps.googleusercontent.com",
+ "client_type": 1,
+ "android_info": {
+ "package_name": "io.getstream.chat.ui.sample.debug",
+ "certificate_hash": "c3188b0a93adae431af73d407565f38f284cbb4e"
+ }
+ },
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:47af6d69a27eac48d7f348",
+ "android_client_info": {
+ "package_name": "io.getstream.chat.virtualevent"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
+ }
+ ]
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:674907137625:android:e20ff8f1ccbf76e4d7f348",
+ "android_client_info": {
+ "package_name": "io.ionic.starter"
+ }
+ },
+ "oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ }
+ ],
+ "api_key": [
+ {
+ "current_key": "AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": [
+ {
+ "client_id": "674907137625-o5iqvv6dtmljtn069b5djv19ad9a7js0.apps.googleusercontent.com",
+ "client_type": 3
+ },
+ {
+ "client_id": "674907137625-5frv6vlii750pp5250ub20e3te88rhj8.apps.googleusercontent.com",
+ "client_type": 2,
+ "ios_info": {
+ "bundle_id": "io.stream.ionic-angular-starter"
+ }
}
]
}
diff --git a/packages/stream_chat_v1/android/app/src/debug/AndroidManifest.xml b/packages/stream_chat_v1/android/app/src/debug/AndroidManifest.xml
index c208884..57257d0 100644
--- a/packages/stream_chat_v1/android/app/src/debug/AndroidManifest.xml
+++ b/packages/stream_chat_v1/android/app/src/debug/AndroidManifest.xml
@@ -4,4 +4,8 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
+
+
+
+
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..6763066 100644
--- a/packages/stream_chat_v1/android/app/src/main/AndroidManifest.xml
+++ b/packages/stream_chat_v1/android/app/src/main/AndroidManifest.xml
@@ -30,6 +30,14 @@
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
+
@@ -44,5 +52,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/android/build.gradle b/packages/stream_chat_v1/android/build.gradle
index b8901a8..6fd1851 100644
--- a/packages/stream_chat_v1/android/build.gradle
+++ b/packages/stream_chat_v1/android/build.gradle
@@ -27,6 +27,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
-task clean(type: Delete) {
+tasks.register("clean", Delete) {
delete rootProject.buildDir
}
diff --git a/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj
index 8a0506c..0b9142a 100644
--- a/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj
+++ b/packages/stream_chat_v1/ios/Runner.xcodeproj/project.pbxproj
@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
+ 042035C495B4FCF371FF6929 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = DF7052F0ED7A0F0A6487676A /* GoogleService-Info.plist */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@@ -57,6 +58,7 @@
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
A4E5F7C8F1FB8AA92A6E2273 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ DF7052F0ED7A0F0A6487676A /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; };
ED2D4AF300CE6F2241B57624 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -91,6 +93,7 @@
97C146EF1CF9000F007C117D /* Products */,
CF168B61BAB91958681C7C21 /* Pods */,
F93B8F215F5456A6442CFA78 /* Frameworks */,
+ DF7052F0ED7A0F0A6487676A /* GoogleService-Info.plist */,
);
sourceTree = "";
};
@@ -182,9 +185,7 @@
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
- DevelopmentTeam = EHV7XZLAHA;
LastSwiftMigration = 1100;
- ProvisioningStyle = Manual;
};
};
};
@@ -215,6 +216,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ 042035C495B4FCF371FF6929 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -228,6 +230,7 @@
files = (
);
inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -246,6 +249,13 @@
"${BUILT_PRODUCTS_DIR}/DKImagePickerController/DKImagePickerController.framework",
"${BUILT_PRODUCTS_DIR}/DKPhotoGallery/DKPhotoGallery.framework",
"${BUILT_PRODUCTS_DIR}/FMDB/FMDB.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseInstallations/FirebaseInstallations.framework",
+ "${BUILT_PRODUCTS_DIR}/FirebaseMessaging/FirebaseMessaging.framework",
+ "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework",
+ "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework",
+ "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework",
"${BUILT_PRODUCTS_DIR}/ReachabilitySwift/Reachability.framework",
"${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework",
"${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework",
@@ -259,6 +269,7 @@
"${BUILT_PRODUCTS_DIR}/image_gallery_saver/image_gallery_saver.framework",
"${BUILT_PRODUCTS_DIR}/image_picker_ios/image_picker_ios.framework",
"${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework",
+ "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework",
"${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
"${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework",
@@ -278,6 +289,13 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKImagePickerController.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DKPhotoGallery.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FMDB.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseInstallations.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessaging.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework",
@@ -291,6 +309,7 @@
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_gallery_saver.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_picker_ios.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework",
@@ -439,8 +458,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
- CODE_SIGN_IDENTITY = "iPhone Distribution";
- CODE_SIGN_STYLE = Manual;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = EHV7XZLAHA;
ENABLE_BITCODE = NO;
@@ -460,7 +479,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
PRODUCT_NAME = "$(TARGET_NAME)";
- PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -581,8 +600,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
- CODE_SIGN_IDENTITY = "iPhone Distribution";
- CODE_SIGN_STYLE = Manual;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = EHV7XZLAHA;
ENABLE_BITCODE = NO;
@@ -602,7 +621,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
PRODUCT_NAME = "$(TARGET_NAME)";
- PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -618,8 +637,8 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
- CODE_SIGN_IDENTITY = "iPhone Distribution";
- CODE_SIGN_STYLE = Manual;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = EHV7XZLAHA;
ENABLE_BITCODE = NO;
@@ -639,7 +658,7 @@
);
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.flutter;
PRODUCT_NAME = "$(TARGET_NAME)";
- PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.flutter";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
diff --git a/packages/stream_chat_v1/ios/Runner/GoogleService-Info.plist b/packages/stream_chat_v1/ios/Runner/GoogleService-Info.plist
new file mode 100644
index 0000000..9faee9f
--- /dev/null
+++ b/packages/stream_chat_v1/ios/Runner/GoogleService-Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ CLIENT_ID
+ 674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com
+ REVERSED_CLIENT_ID
+ com.googleusercontent.apps.674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian
+ ANDROID_CLIENT_ID
+ 674907137625-2scfo9a5cs074dced5vhm712ej6hhtpm.apps.googleusercontent.com
+ API_KEY
+ AIzaSyBTAsaKFUPLAJqfsLiz0yPUVzwrgJkOwSE
+ GCM_SENDER_ID
+ 674907137625
+ PLIST_VERSION
+ 1
+ BUNDLE_ID
+ io.getstream.flutter
+ PROJECT_ID
+ stream-chat-internal
+ STORAGE_BUCKET
+ stream-chat-internal.appspot.com
+ IS_ADS_ENABLED
+
+ IS_ANALYTICS_ENABLED
+
+ IS_APPINVITE_ENABLED
+
+ IS_GCM_ENABLED
+
+ IS_SIGNIN_ENABLED
+
+ GOOGLE_APP_ID
+ 1:674907137625:ios:cafb9fb076a453c4d7f348
+ DATABASE_URL
+ https://stream-chat-internal.firebaseio.com
+
+
\ No newline at end of file
diff --git a/packages/stream_chat_v1/ios/Runner/Info.plist b/packages/stream_chat_v1/ios/Runner/Info.plist
index 17d3006..5167f27 100644
--- a/packages/stream_chat_v1/ios/Runner/Info.plist
+++ b/packages/stream_chat_v1/ios/Runner/Info.plist
@@ -1,75 +1,69 @@
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- ChatSample
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- NSAppleMusicUsageDescription
- Used to send message attachments
- NSCameraUsageDescription
- Used to send message attachments
- NSMicrophoneUsageDescription
- Used to send message attachments
- NSPhotoLibraryUsageDescription
- Used to send message attachments
- UIBackgroundModes
-
- fetch
- remote-notification
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIViewControllerBasedStatusBarAppearance
-
- UIBackgroundModes
-
- remote-notification
-
- UIViewControllerBasedStatusBarAppearance
-
- UIStatusBarHidden
-
- CFBundleLocalizations
-
- en
- it
-
- CADisableMinimumFrameDurationOnPhone
+
+ CADisableMinimumFrameDurationOnPhone
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleLocalizations
+
+ en
+ it
+
+ CFBundleName
+ ChatSample
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ NSAppleMusicUsageDescription
+ Used to send message attachments
+ NSCameraUsageDescription
+ Used to send message attachments
+ NSMicrophoneUsageDescription
+ Used to send message attachments
+ NSPhotoLibraryUsageDescription
+ Used to send message attachments
UIApplicationSupportsIndirectInputEvents
+ UIBackgroundModes
+
+ fetch
+ remote-notification
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIStatusBarHidden
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
diff --git a/packages/stream_chat_v1/ios/firebase_app_id_file.json b/packages/stream_chat_v1/ios/firebase_app_id_file.json
new file mode 100644
index 0000000..1f8a4f7
--- /dev/null
+++ b/packages/stream_chat_v1/ios/firebase_app_id_file.json
@@ -0,0 +1,7 @@
+{
+ "file_generated_by": "FlutterFire CLI",
+ "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory",
+ "GOOGLE_APP_ID": "1:674907137625:ios:cafb9fb076a453c4d7f348",
+ "FIREBASE_PROJECT_ID": "stream-chat-internal",
+ "GCM_SENDER_ID": "674907137625"
+}
\ No newline at end of file
diff --git a/packages/stream_chat_v1/lib/app.dart b/packages/stream_chat_v1/lib/app.dart
index 953c758..a986c89 100644
--- a/packages/stream_chat_v1/lib/app.dart
+++ b/packages/stream_chat_v1/lib/app.dart
@@ -1,13 +1,15 @@
import 'dart:async';
-import 'package:example/state/init_data.dart';
import 'package:example/pages/choose_user_page.dart';
import 'package:example/pages/splash_screen.dart';
import 'package:example/routes/app_routes.dart';
import 'package:example/routes/routes.dart';
+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_core/firebase_core.dart';
+import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
@@ -20,6 +22,66 @@ 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';
+
+/// Constructs callback for background notification handling.
+///
+/// Will be invoked from another Isolate, that's why it's required to
+/// initialize everything again:
+/// - Firebase
+/// - StreamChatClient
+/// - StreamChatPersistenceClient
+@pragma('vm:entry-point')
+Future _onFirebaseBackgroundMessage(RemoteMessage message) async {
+ debugPrint('[onBackgroundMessage] #firebase; message: ${message.toMap()}');
+ final data = message.data;
+ // ensure that Push Notification was sent by Stream.
+ if (data['sender'] != 'stream.chat') {
+ return;
+ }
+ // ensure that Push Notification relates to a new message event.
+ if (data['type'] != 'message.new') {
+ return;
+ }
+ // If you're going to use Firebase services in the background, make sure
+ // you call `initializeApp` before using Firebase services.
+ await Firebase.initializeApp(
+ options: DefaultFirebaseOptions.currentPlatform,
+ );
+ // read existing user info.
+ 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);
+ }
+ if (userId == null || token == null) {
+ return;
+ }
+ final chatClient = buildStreamChatClient(apiKey ?? kDefaultStreamApiKey);
+ try {
+ await chatClient.connectUser(
+ User(id: userId),
+ token,
+ // do not open WS connection
+ connectWebSocket: false,
+ );
+ // initialize persistence with current user
+ if (!chatPersistentClient.isConnected) {
+ await chatPersistentClient.connect(userId);
+ }
+
+ final messageId = data['id'];
+ final cid = data['cid'];
+ // pre-cache the new message using client and persistence.
+ final response = await chatClient.getMessage(messageId);
+ await chatPersistentClient.updateMessages(cid, [response.message]);
+ } catch (e, stk) {
+ debugPrint('[onBackgroundMessage] #firebase; failed: $e; $stk');
+ }
+}
+
final chatPersistentClient = StreamChatPersistenceClient(
logLevel: Level.SEVERE,
connectionMode: ConnectionMode.regular,
@@ -28,7 +90,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 +124,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 +136,6 @@ class _StreamChatSampleAppState extends State
userId = await secureStorage.read(key: kStreamUserId);
token = await secureStorage.read(key: kStreamToken);
}
-
final client = buildStreamChatClient(apiKey ?? kDefaultStreamApiKey);
if (userId != null && token != null) {
@@ -86,6 +150,90 @@ 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 {
+ // User logged in
+ if (userId != null) {
+ // Requests notification permission.
+ await FirebaseMessaging.instance.requestPermission();
+ // Sets callback for background messages.
+ FirebaseMessaging.onBackgroundMessage(_onFirebaseBackgroundMessage);
+ // Sets callback for the notification click event.
+ firebaseSubscriptions.add(FirebaseMessaging.onMessageOpenedApp
+ .listen(_onFirebaseMessageOpenedApp(client)));
+ // Sets callback for foreground messages
+ firebaseSubscriptions.add(FirebaseMessaging.onMessage
+ .listen(_onFirebaseForegroundMessage(client)));
+ // Sets callback for the token refresh event.
+ firebaseSubscriptions.add(FirebaseMessaging.instance.onTokenRefresh
+ .listen(_onFirebaseTokenRefresh(client)));
+
+ final token = await FirebaseMessaging.instance.getToken();
+ if (token != null) {
+ // add Token to Stream
+ await client.addDevice(token, PushProvider.firebase);
+ }
+ }
+ // User logged out
+ else {
+ firebaseSubscriptions.cancelAll();
+ final token = await FirebaseMessaging.instance.getToken();
+ if (token != null) {
+ // remove token from Stream
+ await client.removeDevice(token);
+ }
+ }
+ });
+ }
+
+ /// Constructs callback for notification click event.
+ OnRemoteMessage _onFirebaseMessageOpenedApp(StreamChatClient client) {
+ return (message) async {
+ debugPrint('[onMessageOpenedApp] #firebase; message: ${message.toMap()}');
+ // This callback is getting invoked when the user clicks
+ // on the notification in case if notification was shown by OS.
+ final channelType = (message.data['channel_type'] as String?) ?? '';
+ final channelId = (message.data['channel_id'] as String?) ?? '';
+ final channelCid = (message.data['cid'] as String?) ?? '';
+ var channel = client.state.channels[channelCid];
+ if (channel == null) {
+ channel = client.channel(
+ channelType,
+ id: channelId,
+ );
+ await channel.watch();
+ }
+ // Navigates to Channel page, which is associated with the notification.
+ GoRouter.of(_navigatorKey.currentContext!).pushNamed(
+ Routes.CHANNEL_PAGE.name,
+ pathParameters: Routes.CHANNEL_PAGE.params(channel),
+ );
+ };
+ }
+
+ /// Constructs callback for foreground notification handling.
+ OnRemoteMessage _onFirebaseForegroundMessage(StreamChatClient client) {
+ return (message) async {
+ debugPrint(
+ '[onForegroundMessage] #firebase; message: ${message.toMap()}');
+ };
+ }
+
+ /// Constructs callback for notification refresh event.
+ Future Function(String) _onFirebaseTokenRefresh(
+ StreamChatClient client,
+ ) {
+ return (token) async {
+ debugPrint('[onTokenRefresh] #firebase; token: $token');
+ // This callback is getting invoked when the token got refreshed.
+ await client.addDevice(token, PushProvider.firebase);
+ };
+ }
+
@override
void initState() {
final timeOfStartMs = DateTime.now().millisecondsSinceEpoch;
@@ -107,12 +255,20 @@ class _StreamChatSampleAppState extends State
forwardAnimations();
});
}
+ _initFirebaseMessaging(initData.client);
},
);
super.initState();
}
+ @override
+ void dispose() {
+ super.dispose();
+ userIdSubscription?.cancel();
+ firebaseSubscriptions.cancelAll();
+ }
+
final GlobalKey _navigatorKey = GlobalKey();
LocalNotificationObserver? localNotificationObserver;
@@ -201,3 +357,14 @@ class _StreamChatSampleAppState extends State
);
}
}
+
+typedef OnRemoteMessage = Future Function(RemoteMessage);
+
+extension on List {
+ void cancelAll() {
+ for (final subscription in this) {
+ unawaited(subscription.cancel());
+ }
+ clear();
+ }
+}
diff --git a/packages/stream_chat_v1/lib/firebase_options.dart b/packages/stream_chat_v1/lib/firebase_options.dart
new file mode 100644
index 0000000..5207206
--- /dev/null
+++ b/packages/stream_chat_v1/lib/firebase_options.dart
@@ -0,0 +1,89 @@
+// File generated by FlutterFire CLI.
+// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members
+import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
+import 'package:flutter/foundation.dart'
+ show defaultTargetPlatform, kIsWeb, TargetPlatform;
+
+/// Default [FirebaseOptions] for use with your Firebase apps.
+///
+/// Example:
+/// ```dart
+/// import 'firebase_options.dart';
+/// // ...
+/// await Firebase.initializeApp(
+/// options: DefaultFirebaseOptions.currentPlatform,
+/// );
+/// ```
+class DefaultFirebaseOptions {
+ static FirebaseOptions get currentPlatform {
+ if (kIsWeb) {
+ return web;
+ }
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.android:
+ return android;
+ case TargetPlatform.iOS:
+ return ios;
+ case TargetPlatform.macOS:
+ return macos;
+ case TargetPlatform.windows:
+ throw UnsupportedError(
+ 'DefaultFirebaseOptions have not been configured for windows - '
+ 'you can reconfigure this by running the FlutterFire CLI again.',
+ );
+ case TargetPlatform.linux:
+ throw UnsupportedError(
+ 'DefaultFirebaseOptions have not been configured for linux - '
+ 'you can reconfigure this by running the FlutterFire CLI again.',
+ );
+ default:
+ throw UnsupportedError(
+ 'DefaultFirebaseOptions are not supported for this platform.',
+ );
+ }
+ }
+
+ static const FirebaseOptions web = FirebaseOptions(
+ apiKey: 'AIzaSyA4Obi2paSQ1IZZ88OcMC98DJlpV16DAzE',
+ appId: '1:674907137625:web:a4b97e5d080ec165d7f348',
+ messagingSenderId: '674907137625',
+ projectId: 'stream-chat-internal',
+ authDomain: 'stream-chat-internal.firebaseapp.com',
+ databaseURL: 'https://stream-chat-internal.firebaseio.com',
+ storageBucket: 'stream-chat-internal.appspot.com',
+ measurementId: 'G-F2RV4P139L',
+ );
+
+ static const FirebaseOptions android = FirebaseOptions(
+ apiKey: 'AIzaSyBArS4RH7lUn3xbD-jhzWl5hVWZhliRgY0',
+ appId: '1:674907137625:android:e55b74fd5747e39ad7f348',
+ messagingSenderId: '674907137625',
+ projectId: 'stream-chat-internal',
+ databaseURL: 'https://stream-chat-internal.firebaseio.com',
+ storageBucket: 'stream-chat-internal.appspot.com',
+ );
+
+ static const FirebaseOptions ios = FirebaseOptions(
+ apiKey: 'AIzaSyBTAsaKFUPLAJqfsLiz0yPUVzwrgJkOwSE',
+ appId: '1:674907137625:ios:cafb9fb076a453c4d7f348',
+ messagingSenderId: '674907137625',
+ projectId: 'stream-chat-internal',
+ databaseURL: 'https://stream-chat-internal.firebaseio.com',
+ storageBucket: 'stream-chat-internal.appspot.com',
+ androidClientId: '674907137625-2scfo9a5cs074dced5vhm712ej6hhtpm.apps.googleusercontent.com',
+ iosClientId: '674907137625-flarfn9cefu4lermgpbc4b8rm8l15ian.apps.googleusercontent.com',
+ iosBundleId: 'io.getstream.flutter',
+ );
+
+ static const FirebaseOptions macos = FirebaseOptions(
+ apiKey: 'AIzaSyBTAsaKFUPLAJqfsLiz0yPUVzwrgJkOwSE',
+ appId: '1:674907137625:ios:c719c700198c28b1d7f348',
+ messagingSenderId: '674907137625',
+ projectId: 'stream-chat-internal',
+ databaseURL: 'https://stream-chat-internal.firebaseio.com',
+ storageBucket: 'stream-chat-internal.appspot.com',
+ androidClientId: '674907137625-2scfo9a5cs074dced5vhm712ej6hhtpm.apps.googleusercontent.com',
+ iosClientId: '674907137625-p3msks3snq0h22l7ekpqcf0frr0vt8mg.apps.googleusercontent.com',
+ iosBundleId: 'io.getstream.streamChatV1',
+ );
+}
diff --git a/packages/stream_chat_v1/lib/main.dart b/packages/stream_chat_v1/lib/main.dart
index 87aeed7..b50982a 100644
--- a/packages/stream_chat_v1/lib/main.dart
+++ b/packages/stream_chat_v1/lib/main.dart
@@ -1,13 +1,15 @@
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:example/app.dart';
+import 'firebase_options.dart';
-void main() async {
+Future main() async {
/// Captures errors reported by the Flutter framework.
FlutterError.onError = (FlutterErrorDetails details) {
if (kDebugMode) {
@@ -19,6 +21,8 @@ void main() async {
}
};
+ /// Captures errors reported by the native environment, including native iOS
+ /// and Android code.
Future reportError(dynamic error, StackTrace stackTrace) async {
// Print the exception to the console.
if (kDebugMode) {
@@ -31,11 +35,18 @@ void main() async {
}
}
+ /// Runs the app wrapped in a [Zone] that captures errors and sends them to
+ /// sentry.
runZonedGuarded(
() async {
- await SentryFlutter.init(
- (options) => options.dsn = sentryDsn,
- );
+ WidgetsFlutterBinding.ensureInitialized();
+
+ // Wait for Sentry and Firebase to initialize before running the app.
+ await Future.wait([
+ SentryFlutter.init((options) => options.dsn = sentryDsn),
+ Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform),
+ ]);
+
runApp(const StreamChatSampleApp());
},
reportError,
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 7ee34bd..d6aea78 100644
--- a/packages/stream_chat_v1/lib/pages/channel_list_page.dart
+++ b/packages/stream_chat_v1/lib/pages/channel_list_page.dart
@@ -1,9 +1,9 @@
import 'dart:async';
import 'package:example/app.dart';
-import 'package:example/state/init_data.dart';
import 'package:example/pages/user_mentions_page.dart';
import 'package:example/routes/routes.dart';
+import 'package:example/state/init_data.dart';
import 'package:example/utils/app_config.dart';
import 'package:example/utils/localizations.dart';
import 'package:example/widgets/channel_list.dart';
@@ -75,8 +75,9 @@ class _ChannelListPageState extends State {
return Scaffold(
backgroundColor: StreamChatTheme.of(context).colorTheme.appBg,
appBar: StreamChannelListHeader(
- onNewChatButtonTap: () =>
- GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name),
+ onNewChatButtonTap: () {
+ GoRouter.of(context).pushNamed(Routes.NEW_CHAT.name);
+ },
preNavigationCallback: () =>
FocusScope.of(context).requestFocus(FocusNode()),
),
diff --git a/packages/stream_chat_v1/lib/pages/channel_page.dart b/packages/stream_chat_v1/lib/pages/channel_page.dart
index 0121d79..2a8d785 100644
--- a/packages/stream_chat_v1/lib/pages/channel_page.dart
+++ b/packages/stream_chat_v1/lib/pages/channel_page.dart
@@ -108,8 +108,16 @@ class _ChannelPageState extends State {
Routes.CHANNEL_PAGE.queryParams(message),
);
},
- bottomRowBuilderWithDefaultWidget: (context, message, _) {
- return const StreamVisibleFootnote();
+ bottomRowBuilderWithDefaultWidget: (
+ context,
+ message,
+ defaultWidget,
+ ) {
+ return defaultWidget.copyWith(
+ deletedBottomRowBuilder: (context, message) {
+ return const StreamVisibleFootnote();
+ },
+ );
},
);
},
diff --git a/packages/stream_chat_v1/lib/pages/thread_page.dart b/packages/stream_chat_v1/lib/pages/thread_page.dart
index 15e6a39..8ea722a 100644
--- a/packages/stream_chat_v1/lib/pages/thread_page.dart
+++ b/packages/stream_chat_v1/lib/pages/thread_page.dart
@@ -64,8 +64,16 @@ class _ThreadPageState extends State {
messageBuilder: (context, details, messages, defaultMessage) {
return defaultMessage.copyWith(
onReplyTap: _reply,
- bottomRowBuilderWithDefaultWidget: (context, message, _) {
- return const StreamVisibleFootnote();
+ bottomRowBuilderWithDefaultWidget: (
+ context,
+ message,
+ defaultWidget,
+ ) {
+ return defaultWidget.copyWith(
+ deletedBottomRowBuilder: (context, message) {
+ return const StreamVisibleFootnote();
+ },
+ );
},
);
},
diff --git a/packages/stream_chat_v1/lib/utils/local_notification_observer.dart b/packages/stream_chat_v1/lib/utils/local_notification_observer.dart
index 8befd49..591a151 100644
--- a/packages/stream_chat_v1/lib/utils/local_notification_observer.dart
+++ b/packages/stream_chat_v1/lib/utils/local_notification_observer.dart
@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:example/routes/routes.dart';
-import 'package:example/utils/notifications_service.dart';
+import 'package:example/utils/notifications_service.dart' as pn;
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -36,7 +36,7 @@ class LocalNotificationObserver extends NavigatorObserver {
}
}
- showLocalNotification(
+ pn.showLocalNotification(
event,
client.state.currentUser!.id,
navigatorKey.currentState!.context,
diff --git a/packages/stream_chat_v1/lib/utils/notifications_service.dart b/packages/stream_chat_v1/lib/utils/notifications_service.dart
index ccca39c..0e858cc 100644
--- a/packages/stream_chat_v1/lib/utils/notifications_service.dart
+++ b/packages/stream_chat_v1/lib/utils/notifications_service.dart
@@ -1,5 +1,5 @@
-import 'package:example/utils/localizations.dart';
import 'package:example/routes/routes.dart';
+import 'package:example/utils/localizations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
hide Message;
@@ -11,52 +11,48 @@ void showLocalNotification(
String currentUserId,
BuildContext context,
) async {
+ // Don't show notification if the event is from the current user.
+ if (event.user!.id == currentUserId) return;
+
+ // Don't show notification if the event is not a message.
if (![
- EventType.messageNew,
- EventType.notificationMessageNew,
- ].contains(event.type) ||
- event.user!.id == currentUserId) {
- return;
- }
+ EventType.messageNew,
+ EventType.notificationMessageNew,
+ ].contains(event.type)) return;
+
+ // Return if the message is null.
if (event.message == null) return;
+
final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
- const initializationSettingsAndroid =
- AndroidInitializationSettings('launch_background');
- const initializationSettingsIOS = IOSInitializationSettings();
const initializationSettings = InitializationSettings(
- android: initializationSettingsAndroid,
- iOS: initializationSettingsIOS,
+ iOS: DarwinInitializationSettings(),
+ android: AndroidInitializationSettings('ic_notification_in_app'),
);
final appLocalizations = AppLocalizations.of(context);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
- onSelectNotification: (channelCid) async {
- if (channelCid != null) {
- final client = StreamChat.of(context).client;
- final router = GoRouter.of(context);
+ onDidReceiveNotificationResponse: (response) async {
+ final channelCid = response.payload;
+ if (channelCid == null) return;
- var channel = client.state.channels[channelCid];
+ final channelType = channelCid.split(':')[0];
+ final channelId = channelCid.split(':')[1];
- if (channel == null) {
- final splits = channelCid.split(':');
- final type = splits[0];
- final id = splits[1];
- channel = client.channel(
- type,
- id: id,
- );
- await channel.watch();
- }
+ final client = StreamChat.of(context).client;
+ final router = GoRouter.of(context);
- router.pushNamed(
- Routes.CHANNEL_PAGE.name,
- pathParameters: Routes.CHANNEL_PAGE.params(channel),
- );
- }
+ final channel = client.channel(channelType, id: channelId);
+ await channel.watch();
+
+ router.pushNamed(
+ Routes.CHANNEL_PAGE.name,
+ pathParameters: Routes.CHANNEL_PAGE.params(channel),
+ );
},
);
+
await flutterLocalNotificationsPlugin.show(
event.message!.id.hashCode,
event.message!.user!.name,
@@ -69,8 +65,13 @@ void showLocalNotification(
priority: Priority.high,
importance: Importance.high,
),
- iOS: const IOSNotificationDetails(),
+ iOS: const DarwinNotificationDetails(),
),
payload: '${event.channelType}:${event.channelId}',
);
}
+
+Future cancelLocalNotifications() async {
+ final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
+ await flutterLocalNotificationsPlugin.cancelAll();
+}
diff --git a/packages/stream_chat_v1/macos/Runner.xcodeproj/project.pbxproj b/packages/stream_chat_v1/macos/Runner.xcodeproj/project.pbxproj
index 237a137..f0c9f2c 100644
--- a/packages/stream_chat_v1/macos/Runner.xcodeproj/project.pbxproj
+++ b/packages/stream_chat_v1/macos/Runner.xcodeproj/project.pbxproj
@@ -21,6 +21,7 @@
/* End PBXAggregateTarget section */
/* Begin PBXBuildFile section */
+ 0D4CBA18257CE68ED24F92E8 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B7C148DD697EB38DCDC5FC57 /* GoogleService-Info.plist */; };
1E52A3376056853520695B44 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 782D013032AAC6BDCC56A273 /* Pods_Runner.framework */; };
335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; };
33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; };
@@ -72,6 +73,7 @@
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; };
929AC42CE2CDA27FE9235234 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; };
+ B7C148DD697EB38DCDC5FC57 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; };
CDF4BDDD28F9A6920059DB87 /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = ""; };
D4F4A1B883B395E86A7B745A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
/* End PBXFileReference section */
@@ -107,6 +109,7 @@
33CC10EE2044A3C60003C045 /* Products */,
D73912EC22F37F3D000D13A0 /* Frameworks */,
3A21EEB06B73EAB5B839B688 /* Pods */,
+ B7C148DD697EB38DCDC5FC57 /* GoogleService-Info.plist */,
);
sourceTree = "";
};
@@ -248,6 +251,7 @@
files = (
33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */,
33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */,
+ 0D4CBA18257CE68ED24F92E8 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
diff --git a/packages/stream_chat_v1/macos/Runner/GoogleService-Info.plist b/packages/stream_chat_v1/macos/Runner/GoogleService-Info.plist
new file mode 100644
index 0000000..0d4409d
--- /dev/null
+++ b/packages/stream_chat_v1/macos/Runner/GoogleService-Info.plist
@@ -0,0 +1,38 @@
+
+
+
+
+ CLIENT_ID
+ 674907137625-p3msks3snq0h22l7ekpqcf0frr0vt8mg.apps.googleusercontent.com
+ REVERSED_CLIENT_ID
+ com.googleusercontent.apps.674907137625-p3msks3snq0h22l7ekpqcf0frr0vt8mg
+ ANDROID_CLIENT_ID
+ 674907137625-2scfo9a5cs074dced5vhm712ej6hhtpm.apps.googleusercontent.com
+ API_KEY
+ AIzaSyBTAsaKFUPLAJqfsLiz0yPUVzwrgJkOwSE
+ GCM_SENDER_ID
+ 674907137625
+ PLIST_VERSION
+ 1
+ BUNDLE_ID
+ io.getstream.streamChatV1
+ PROJECT_ID
+ stream-chat-internal
+ STORAGE_BUCKET
+ stream-chat-internal.appspot.com
+ IS_ADS_ENABLED
+
+ IS_ANALYTICS_ENABLED
+
+ IS_APPINVITE_ENABLED
+
+ IS_GCM_ENABLED
+
+ IS_SIGNIN_ENABLED
+
+ GOOGLE_APP_ID
+ 1:674907137625:ios:c719c700198c28b1d7f348
+ DATABASE_URL
+ https://stream-chat-internal.firebaseio.com
+
+
\ No newline at end of file
diff --git a/packages/stream_chat_v1/macos/firebase_app_id_file.json b/packages/stream_chat_v1/macos/firebase_app_id_file.json
new file mode 100644
index 0000000..918fb78
--- /dev/null
+++ b/packages/stream_chat_v1/macos/firebase_app_id_file.json
@@ -0,0 +1,7 @@
+{
+ "file_generated_by": "FlutterFire CLI",
+ "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory",
+ "GOOGLE_APP_ID": "1:674907137625:ios:c719c700198c28b1d7f348",
+ "FIREBASE_PROJECT_ID": "stream-chat-internal",
+ "GCM_SENDER_ID": "674907137625"
+}
\ No newline at end of file
diff --git a/packages/stream_chat_v1/pubspec.yaml b/packages/stream_chat_v1/pubspec.yaml
index 242f35f..d6597b8 100644
--- a/packages/stream_chat_v1/pubspec.yaml
+++ b/packages/stream_chat_v1/pubspec.yaml
@@ -4,7 +4,7 @@ publish_to: "none"
version: 2.2.0
environment:
- sdk: ">=2.17.0 <4.0.0"
+ sdk: ">=3.0.0 <4.0.0"
flutter: ">=1.17.0"
dependencies:
@@ -26,7 +26,7 @@ dependencies:
url: https://github.com/GetStream/stream-chat-flutter.git
ref: develop
path: packages/stream_chat_localizations
- flutter_local_notifications: ^9.0.0
+ flutter_local_notifications: ^14.1.1
flutter_svg: ^2.0.7
flutter_secure_storage: ^8.0.0
yaml: ^3.1.2
@@ -39,6 +39,8 @@ dependencies:
go_router: ^8.0.5
provider: ^6.0.5
video_player: ^2.6.1
+ firebase_core: ^2.14.0
+ firebase_messaging: ^14.6.3
dev_dependencies:
flutter_launcher_icons: ^0.13.1
@@ -78,4 +80,4 @@ flutter_icons:
windows:
generate: true
macos:
- generate: true
+ generate: true
\ No newline at end of file