Merge pull request #656 from GetStream/release/2.2.1
chore(llc,core,ui): update changelog and pubspecs
@@ -37,7 +37,7 @@ jobs:
|
|||||||
"llc": "packages/stream_chat",
|
"llc": "packages/stream_chat",
|
||||||
"ui": "packages/stream_chat_flutter",
|
"ui": "packages/stream_chat_flutter",
|
||||||
"core": "packages/stream_chat_flutter_core",
|
"core": "packages/stream_chat_flutter_core",
|
||||||
"localization": "packages/stream_chat_flutter_localizations",
|
"localization": "packages/stream_chat_localizations",
|
||||||
"persistence": "packages/stream_chat_persistence"
|
"persistence": "packages/stream_chat_persistence"
|
||||||
}
|
}
|
||||||
env:
|
env:
|
||||||
|
|||||||
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -187,8 +187,33 @@ StreamChat(
|
|||||||
|
|
||||||
As you can see we generate a local notification whenever a message.new or notification.message_new event is received.
|
As you can see we generate a local notification whenever a message.new or notification.message_new event is received.
|
||||||
|
|
||||||
|
### Foreground notifications
|
||||||
|
|
||||||
|
Sometimes you may want to show a notification when the app is in the foreground.
|
||||||
|
For example, when you're in a channel and you receive a new message from someone in another channel.
|
||||||
|
|
||||||
|
For this scenario, you can also use the `flutter_local_notifications` package to show a notification.
|
||||||
|
|
||||||
|
You need to listen for new events using `StreamChatClient.on` and handle them accordingly.
|
||||||
|
|
||||||
|
Here we're checking if the event is a `message.new` or `notification.message_new` event, and if the message is from a different user than the current user. In that case we'll show a notification.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
client.on(
|
||||||
|
EventType.messageNew,
|
||||||
|
EventType.notificationMessageNew,
|
||||||
|
).listen((event) {
|
||||||
|
if (event.message?.user?.id == client.state.currentUser?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showLocalNotification(event, client.state.currentUser!.id, context);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
Using `flutter_local_notifications` is a great way to implement notifications while the is in foreground too! You can generate a local notification listening to events using the method `streamChatClient.on()` and react to the events you want.
|
You should also check that the channel of the message is different than the channel in the foreground.
|
||||||
|
How you do this depends on your app infrastructure and how you handle navigation.
|
||||||
|
Take a look at the [Stream Chat v1 sample app](https://github.com/GetStream/flutter-samples/blob/main/packages/stream_chat_v1/lib/home_page.dart#L11) to see how we're doing it over there.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Saving notification messages to the offline storage
|
### Saving notification messages to the offline storage
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
---
|
||||||
|
id: customize_message_widget
|
||||||
|
sidebar_position: 11
|
||||||
|
title: Customizing The MessageWidget
|
||||||
|
---
|
||||||
|
|
||||||
|
Customizing Text Messages
|
||||||
|
|
||||||
|
### Introduction
|
||||||
|
|
||||||
|
Every application provides a unique look and feel to their own messaging interface including and not
|
||||||
|
limited to fonts, colors, and shapes.
|
||||||
|
|
||||||
|
This guide details how to customize the `MessageWidget` in the Stream Chat Flutter UI SDK.
|
||||||
|
|
||||||
|
### Building Custom Messages
|
||||||
|
|
||||||
|
This guide goes into detail about the ability to customize the `MessageWidget`. However, if you want
|
||||||
|
to customize the default `MessageWidget` in the `MessageListView` provided, you can use the `.copyWith()` method
|
||||||
|
provided inside the `messageBuilder` parameter of the `MessageListView` like this:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
MessageListView(
|
||||||
|
messageBuilder: (context, details, messageList, defaultImpl) {
|
||||||
|
// Your implementation of the message here
|
||||||
|
// E.g: return Text(details.message.text ?? '');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theming
|
||||||
|
|
||||||
|
You can customize the `MessageWidget` using the `StreamChatTheme` class, so that you can change the
|
||||||
|
message theme at the top instead of creating your own `MessageWidget` at the lower implementation level.
|
||||||
|
|
||||||
|
There are several things you can change in the theme including text styles and colors of various elements.
|
||||||
|
|
||||||
|
You can also set a different theme for the user's own messages and messages received by them.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Theming allows you to change minor factors like style while using the widget directly allows you much
|
||||||
|
more customization such as replacing a certain widget with another. Some things can only be customized
|
||||||
|
through the widget and not the theme.
|
||||||
|
:::
|
||||||
|
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
StreamChatThemeData(
|
||||||
|
|
||||||
|
/// Sets theme for user's messages
|
||||||
|
ownMessageTheme: MessageThemeData(
|
||||||
|
messageBackgroundColor: colorTheme.textHighEmphasis,
|
||||||
|
),
|
||||||
|
|
||||||
|
/// Sets theme for received messages
|
||||||
|
otherMessageTheme: MessageThemeData(
|
||||||
|
avatarTheme: AvatarThemeData(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Change message text style
|
||||||
|
|
||||||
|
The `MessageWidget` has multiple `Text` widgets that you can manipulate the styles of. The three main
|
||||||
|
are the actual message text, user name, message links, and the message timestamp.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
MessageThemeData(
|
||||||
|
messageTextStyle: TextStyle(...),
|
||||||
|
createdAtStyle: TextStyle(...),
|
||||||
|
messageAuthorStyle: TextStyle(...),
|
||||||
|
messageLinksStyle: TextStyle(...),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Change avatar theme
|
||||||
|
|
||||||
|
You can change the attributes of the avatar (if displayed) using the `avatarTheme` property.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
MessageThemeData(
|
||||||
|
avatarTheme: AvatarThemeData(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Changing Reaction theme
|
||||||
|
|
||||||
|
You also customize the reactions attached to every message using the theme.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
MessageThemeData(
|
||||||
|
reactionsBackgroundColor: Colors.red,
|
||||||
|
reactionsBorderColor: Colors.redAccent,
|
||||||
|
reactionsMaskColor: Colors.pink,
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Changing Message Actions
|
||||||
|
|
||||||
|
When a message is long pressed, the `MessageActionsModal` is shown.
|
||||||
|
|
||||||
|
The `MessageWidget` allows showing or hiding some options if you so choose.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
MessageWidget(
|
||||||
|
...
|
||||||
|
showUsername = true,
|
||||||
|
showTimestamp = true,
|
||||||
|
showReactions = true,
|
||||||
|
showDeleteMessage = true,
|
||||||
|
showEditMessage = true,
|
||||||
|
showReplyMessage = true,
|
||||||
|
showThreadReplyMessage = true,
|
||||||
|
showResendMessage = true,
|
||||||
|
showCopyMessage = true,
|
||||||
|
showFlagButton = true,
|
||||||
|
showPinButton = true,
|
||||||
|
showPinHighlight = true,
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Building attachments
|
||||||
|
|
||||||
|
The `customAttachmentBuilder` property allows you to build any kind of attachment (inbuilt or custom)
|
||||||
|
in your own way. While a separate guide is written for this, it is included here because of relevance.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
MessageListView(
|
||||||
|
messageBuilder: (context, details, messages, defaultMessage) {
|
||||||
|
return defaultMessage.copyWith(
|
||||||
|
customAttachmentBuilders: {
|
||||||
|
'location': (context, message, attachments) {
|
||||||
|
final attachmentWidget = Image.network(
|
||||||
|
_buildMapAttachment(
|
||||||
|
attachments[0].extraData['latitude'],
|
||||||
|
attachments[0].extraData['longitude'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return wrapAttachmentWidget(context, attachmentWidget, null, true, BorderRadius.circular(8.0));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
### Widget Builders
|
||||||
|
|
||||||
|
Some parameters allow you to construct your own widget in place of some elements in the `MessageWidget`.
|
||||||
|
|
||||||
|
These are:
|
||||||
|
* `userAvatarBuilder` : Allows user to substitute their own widget in place of the user avatar.
|
||||||
|
* `editMessageInputBuilder` : Allows user to substitute their own widget in place of the input in edit mode.
|
||||||
|
* `textBuilder` : Allows user to substitute their own widget in place of the text.
|
||||||
|
* `bottomRowBuilder` : Allows user to substitute their own widget in the bottom of the message when not deleted.
|
||||||
|
* `deletedBottomRowBuilder` : Allows user to substitute their own widget in the bottom of the message when deleted.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
MessageWidget(
|
||||||
|
...
|
||||||
|
textBuilder: (context, message) {
|
||||||
|
// Add your own text implementation here.
|
||||||
|
},
|
||||||
|
),
|
||||||
|
```
|
||||||
@@ -0,0 +1,448 @@
|
|||||||
|
---
|
||||||
|
id: token_generation_with_firebase
|
||||||
|
sidebar_position: 5
|
||||||
|
title: User Token Generation With Firebase Auth and Cloud Functions
|
||||||
|
---
|
||||||
|
|
||||||
|
Securely generate Stream Chat user tokens using Firebase Authentication and Cloud Functions.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
This guide assumes that you are familiar with Firebase Authentication and Cloud Functions for Flutter and using the Flutter Stream Chat SDK.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Introduction
|
||||||
|
|
||||||
|
In this guide, you'll explore how you can use Firebase Auth as an authentication provider and create Firebase Cloud functions to securely
|
||||||
|
generate Stream Chat user tokens.
|
||||||
|
|
||||||
|
You will use Stream's [NodeJS client](https://getstream.io/chat/docs/node/?language=javascript) for Stream account creation and
|
||||||
|
token generation, and [Flutter Cloud Functions for Firebase](https://firebase.flutter.dev/docs/functions/overview) to invoke the cloud functions
|
||||||
|
from your Flutter app.
|
||||||
|
|
||||||
|
Stream supports several different [backend clients](https://getstream.io/chat/sdk/#backend-clients) to integrate with your server. This guide only shows an easy way to integrate Stream Chat authentication using Firebase and Flutter.
|
||||||
|
|
||||||
|
### Flutter Firebase
|
||||||
|
|
||||||
|
See the [Flutter Firebase getting started](https://firebase.flutter.dev/docs/overview) docs for setup and installation instructions.
|
||||||
|
|
||||||
|
You will also need to add the [Flutter Firebase Authentication](https://firebase.flutter.dev/docs/auth/overview), and [Flutter Firebase Cloud Functions](https://firebase.flutter.dev/docs/functions/overview) packages to your app. Depending on the platform that you target, there may be specific configurations that you need to do.
|
||||||
|
|
||||||
|
#### Starting Code
|
||||||
|
|
||||||
|
The following code shows a basic application with **FirebaseAuth** and **FirebaseFunctions**.
|
||||||
|
|
||||||
|
You will extend this later to execute cloud functions.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:cloud_functions/cloud_functions.dart';
|
||||||
|
import 'package:firebase_core/firebase_core.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await Firebase.initializeApp();
|
||||||
|
runApp(MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Auth(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Auth extends StatefulWidget {
|
||||||
|
Auth({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AuthState createState() => _AuthState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AuthState extends State<Auth> {
|
||||||
|
late FirebaseAuth auth;
|
||||||
|
late FirebaseFunctions functions;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
auth = FirebaseAuth.instance;
|
||||||
|
functions = FirebaseFunctions.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
final email = '[email protected]';
|
||||||
|
final password = 'password';
|
||||||
|
|
||||||
|
Future<void> createAccount() async {
|
||||||
|
// Create Firebase account
|
||||||
|
await auth.createUserWithEmailAndPassword(email: email, password: password);
|
||||||
|
print('Firebase account created');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signIn() async {
|
||||||
|
// Sign in with Firebase
|
||||||
|
await auth.signInWithEmailAndPassword(email: email, password: password);
|
||||||
|
print('Firebase signed in');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> signOut() async {
|
||||||
|
// Revoke Stream chat token.
|
||||||
|
final callable = functions.httpsCallable('revokeStreamUserToken');
|
||||||
|
await callable();
|
||||||
|
print('Stream user token revoked');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
AuthenticationState(
|
||||||
|
streamUser: auth.authStateChanges(),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: createAccount,
|
||||||
|
child: Text('Create account'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: signIn,
|
||||||
|
child: Text('Sign in'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: signOut,
|
||||||
|
child: Text('Sign out'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticationState extends StatelessWidget {
|
||||||
|
const AuthenticationState({
|
||||||
|
Key? key,
|
||||||
|
required this.streamUser,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final Stream<User?> streamUser;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return StreamBuilder<User?>(
|
||||||
|
stream: streamUser,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
return (snapshot.data != null)
|
||||||
|
? Text('Authenticated')
|
||||||
|
: Text('Not Authenticated');
|
||||||
|
}
|
||||||
|
return Text('Not Authenticated');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Running the above will give this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The `Auth` widget handles all of the authentication logic. It initializes a `FirebaseAuth.instance` and uses that
|
||||||
|
in the `createAccount`, `signIn` and `signOut` methods. There is a button to envoke each of these methods.
|
||||||
|
|
||||||
|
The `FirebaseFunctions.instance` will be used later in this guide.
|
||||||
|
|
||||||
|
The `AuthenticationState` widget listens to `auth.authStateChanges()` to display a message
|
||||||
|
indicating if a user is authenticated.
|
||||||
|
|
||||||
|
### Firebase Cloud Functions
|
||||||
|
|
||||||
|
Firebase Cloud Functions allows you to extend Firebase with custom operations that an event can trigger:
|
||||||
|
- **Internal event**: For example, when creating a new Firebase account this is automatically triggered.
|
||||||
|
- **External event**: For example, directly calling a cloud function from your Flutter application.
|
||||||
|
|
||||||
|
To set up your local environment to deploy cloud functions, please see the
|
||||||
|
[Cloud Functions getting started](https://firebase.flutter.dev/docs/overview) docs.
|
||||||
|
|
||||||
|
After initializing your project with cloud functions, you should have a **functions** folder in your project, including a `package.json` file.
|
||||||
|
|
||||||
|
There should be two dependencies already added, **firebase-admin** and **firebase-functions**. You will also need to add the **stream-chat** dependency.
|
||||||
|
|
||||||
|
Navigate to the **functions** folder and run `npm install stream-chat --save-prod`.
|
||||||
|
|
||||||
|
This will install the node module and add it as a dependency to `package.json`.
|
||||||
|
|
||||||
|
Now open `index.js` and add the following (this is the complete example):
|
||||||
|
|
||||||
|
```js
|
||||||
|
const StreamChat = require('stream-chat').StreamChat;
|
||||||
|
const functions = require("firebase-functions");
|
||||||
|
const admin = require("firebase-admin");
|
||||||
|
|
||||||
|
admin.initializeApp();
|
||||||
|
|
||||||
|
const serverClient = StreamChat.getInstance(functions.config().stream.key, functions.config().stream.secret);
|
||||||
|
|
||||||
|
|
||||||
|
// When a user is deleted from Firebase their associated Stream account is also deleted.
|
||||||
|
exports.deleteStreamUser = functions.auth.user().onDelete((user, context) => {
|
||||||
|
return serverClient.deleteUser(user.uid);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a Stream user and return auth token.
|
||||||
|
exports.createStreamUserAndGetToken = functions.https.onCall(async (data, context) => {
|
||||||
|
// Checking that the user is authenticated.
|
||||||
|
if (!context.auth) {
|
||||||
|
// Throwing an HttpsError so that the client gets the error details.
|
||||||
|
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
|
||||||
|
'while authenticated.');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// Create user using the serverClient.
|
||||||
|
await serverClient.upsertUser({
|
||||||
|
id: context.auth.uid,
|
||||||
|
name: context.auth.token.name,
|
||||||
|
email: context.auth.token.email,
|
||||||
|
image: context.auth.token.image,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Create and return user auth token.
|
||||||
|
return serverClient.createToken(context.auth.uid);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Unable to create user with ID ${context.auth.uid} on Stream. Error ${err}`);
|
||||||
|
// Throwing an HttpsError so that the client gets the error details.
|
||||||
|
throw new functions.https.HttpsError('aborted', "Could not create Stream user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get Stream user token.
|
||||||
|
exports.getStreamUserToken = functions.https.onCall((data, context) => {
|
||||||
|
// Checking that the user is authenticated.
|
||||||
|
if (!context.auth) {
|
||||||
|
// Throwing an HttpsError so that the client gets the error details.
|
||||||
|
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
|
||||||
|
'while authenticated.');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return serverClient.createToken(context.auth.uid);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Unable to get user token with ID ${context.auth.uid} on Stream. Error ${err}`);
|
||||||
|
// Throwing an HttpsError so that the client gets the error details.
|
||||||
|
throw new functions.https.HttpsError('aborted', "Could not get Stream user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Revoke the authenticated user's Stream chat token.
|
||||||
|
exports.revokeStreamUserToken = functions.https.onCall((data, context) => {
|
||||||
|
// Checking that the user is authenticated.
|
||||||
|
if (!context.auth) {
|
||||||
|
// Throwing an HttpsError so that the client gets the error details.
|
||||||
|
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
|
||||||
|
'while authenticated.');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return serverClient.revokeUserToken(context.auth.uid);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Unable to revoke user token with ID ${context.auth.uid} on Stream. Error ${err}`);
|
||||||
|
// Throwing an HttpsError so that the client gets the error details.
|
||||||
|
throw new functions.https.HttpsError('aborted', "Could not get Stream user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
First, you import the necessary packages and call `admin.initializeApp();` to set up Firebase cloud functions.
|
||||||
|
|
||||||
|
Next, you initialize the **StreamChat** server client by calling `StreamChat.getInstance`. This function requires your Stream app's
|
||||||
|
**token** and **secret**. You can get this from the Stream Dashboard for your app.
|
||||||
|
|
||||||
|
Set these values as environment data on Firebase Functions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
firebase functions:config:set stream.key="app-key" stream.secret="app-secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
*Replace **app-key** and **app-secret** with the values for your Stream app.*
|
||||||
|
|
||||||
|
This creates an object of **stream** with properties **key** and **secret**. To access this environment
|
||||||
|
data use `functions.config().stream.key` and `functions.config().stream.secret`.
|
||||||
|
|
||||||
|
See the [Firebase environment configuration](https://firebase.google.com/docs/functions/config-env)
|
||||||
|
documentation for additional information.
|
||||||
|
|
||||||
|
To deploy these functions to Firebase, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
firebase deploy --only functions
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a Stream User and Get the User's Token
|
||||||
|
|
||||||
|
In the `createStreamUserAndGetToken` cloud function you create an `onCall` HTTPS handler, which exposes
|
||||||
|
a cloud function that can be envoked from your Flutter app.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// Create a Stream user and return auth token.
|
||||||
|
exports.createStreamUserAndGetToken = functions.https.onCall(async (data, context) => {
|
||||||
|
// Checking that the user is authenticated.
|
||||||
|
if (!context.auth) {
|
||||||
|
// Throwing an HttpsError so that the client gets the error details.
|
||||||
|
throw new functions.https.HttpsError('failed-precondition', 'The function must be called ' +
|
||||||
|
'while authenticated.');
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// Create user using the serverClient.
|
||||||
|
await serverClient.upsertUser({
|
||||||
|
id: context.auth.uid,
|
||||||
|
name: context.auth.token.name,
|
||||||
|
email: context.auth.token.email,
|
||||||
|
image: context.auth.token.image,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Create and return user auth token.
|
||||||
|
return serverClient.createToken(context.auth.uid);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Unable to create user with ID ${context.auth.uid} on Stream. Error ${err}`);
|
||||||
|
// Throwing an HttpsError so that the client gets the error details.
|
||||||
|
throw new functions.https.HttpsError('aborted', "Could not create Stream user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
This function first does a check to see that the client that calls it is authenticated,
|
||||||
|
by ensuring that `context.auth` is not null. If it is null, then it throws an `HttpsError` with a descriptive
|
||||||
|
message. This error can be caught in your Flutter application.
|
||||||
|
|
||||||
|
If the caller is authenticated the function proceeds to use the `serverClient` to create a new Stream Chat
|
||||||
|
user by calling the `upsertUser` method and passing in some user data. It uses the authenticated caller's **uid** as an **id**.
|
||||||
|
|
||||||
|
After the user is created it generates a token for that user. This token is then returned to the caller.
|
||||||
|
|
||||||
|
To call this from Flutter, you will need to use the `cloud_functions` package.
|
||||||
|
|
||||||
|
Update the **createAccount** method in your Flutter code to the following:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> createAccount() async {
|
||||||
|
// Create Firebase account
|
||||||
|
await auth.createUserWithEmailAndPassword(email: email, password: password);
|
||||||
|
print('Firebase account created');
|
||||||
|
|
||||||
|
// Create Stream user and get token
|
||||||
|
final callable = functions.httpsCallable('createStreamUserAndGetToken');
|
||||||
|
final results = await callable();
|
||||||
|
print('Stream account created, token: ${results.data}');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Calling this method will do the following:
|
||||||
|
1. Create a new Firebase User and authenticate that user.
|
||||||
|
2. Call the `createStreamUserAndGetToken` cloud function and get the Stream user token for the authenticated user.
|
||||||
|
|
||||||
|
As you can see, calling a cloud function is easy and will also send all the necessary user authentication information (such as the UID)
|
||||||
|
in the request.
|
||||||
|
|
||||||
|
Once you have the Stream user token, you can authenticate your Stream Chat user as you normally would.
|
||||||
|
|
||||||
|
Please see our [initialization documention](https://getstream.io/chat/docs/flutter-dart/init_and_users/?language=dart) for more information.
|
||||||
|
|
||||||
|
As you can see below, the User ID matches on both Firebase's and Stream's user database.
|
||||||
|
|
||||||
|
##### Firebase Authentication Database
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
##### Stream Chat User Database
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
### Get the Stream User Token
|
||||||
|
|
||||||
|
The `getStreamUserToken` cloud function is very similar to the `createStreamUserAndGetToken` function. The only difference is
|
||||||
|
that it only creates a user token and does not create a new user account on Stream.
|
||||||
|
|
||||||
|
Update the **signIn** method in your Flutter code to the following:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> signIn() async {
|
||||||
|
// Sign in with Firebase
|
||||||
|
await auth.signInWithEmailAndPassword(email: email, password: password);
|
||||||
|
print('Firebase signed in');
|
||||||
|
|
||||||
|
// Get Stream user token
|
||||||
|
final callable = functions.httpsCallable('getStreamUserToken');
|
||||||
|
final results = await callable();
|
||||||
|
print('Stream user token retrieved: ${results.data}');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Calling this method will do the following:
|
||||||
|
1. Sign in using Firebase Auth.
|
||||||
|
2. Call the `getStreamUserToken` cloud function to get a Stream user token.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
The user needs to be authenticated to call this cloud function. Otherwise, the function will throw
|
||||||
|
the **failed-precondition** error that you specified.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Revoke Stream User Token
|
||||||
|
|
||||||
|
You may also want to revoke the Stream user token if you sign out from Firebase.
|
||||||
|
|
||||||
|
Update the `signOut` method in your Flutter code to the following:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> signOut() async {
|
||||||
|
// Revoke Stream user token.
|
||||||
|
final callable = functions.httpsCallable('revokeStreamUserToken');
|
||||||
|
await callable();
|
||||||
|
print('Stream user token revoked');
|
||||||
|
|
||||||
|
// Sign out Firebase.
|
||||||
|
await auth.signOut();
|
||||||
|
print('Firebase signed out');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
:::note
|
||||||
|
Call the cloud function before signing out from Firebase.
|
||||||
|
:::
|
||||||
|
|
||||||
|
### Delete Stream User
|
||||||
|
|
||||||
|
When deleting a Firebase user account, it would make sense also to delete the
|
||||||
|
associated Stream user account.
|
||||||
|
|
||||||
|
The cloud function looks like this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// When a user is deleted from Firebase their associated Stream account is also deleted.
|
||||||
|
exports.deleteStreamUser = functions.auth.user().onDelete((user, context) => {
|
||||||
|
return serverClient.deleteUser(user.uid);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In this function, you are listening to delete events on Firebase auth. When an account is deleted, this function will be triggered, and you can get the
|
||||||
|
user's **uid** and call the `deleteUser` method on the `serverClient`.
|
||||||
|
|
||||||
|
This is not an external cloud function; it can only be triggered when an
|
||||||
|
account is deleted.
|
||||||
|
|
||||||
|
### Conclusion
|
||||||
|
|
||||||
|
In this guide, you have seen how to securely create Stream Chat tokens using
|
||||||
|
Firebase Authentication and Cloud Functions.
|
||||||
|
|
||||||
|
The principles shown in this guide can be applied to your preferred authentication
|
||||||
|
provider and cloud architecture of choice.
|
||||||
@@ -69,6 +69,12 @@ scripts:
|
|||||||
select-package:
|
select-package:
|
||||||
dir-exists: coverage
|
dir-exists: coverage
|
||||||
|
|
||||||
|
docs:
|
||||||
|
run: |
|
||||||
|
npm install -g https://github.com/GetStream/stream-chat-docusaurus-cli &&
|
||||||
|
npx stream-chat-docusaurus -i -s
|
||||||
|
description: Runs the docusaurus documentation locally.
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.12.0 <3.0.0'
|
sdk: '>=2.12.0 <3.0.0'
|
||||||
flutter: '>=1.22.4 <2.0.0'
|
flutter: '>=1.22.4 <2.0.0'
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
|
## 2.2.1
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- Fixed unread indicator not updating correctly
|
||||||
|
- Fix `channel.show` not working because of null body
|
||||||
|
|
||||||
## 2.2.0
|
## 2.2.0
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|||||||
@@ -1488,7 +1488,7 @@ class ChannelClientState {
|
|||||||
|
|
||||||
_listenMemberRemoved();
|
_listenMemberRemoved();
|
||||||
|
|
||||||
_computeInitialUnread();
|
_computeUnread();
|
||||||
|
|
||||||
_startCleaning();
|
_startCleaning();
|
||||||
|
|
||||||
@@ -1512,11 +1512,11 @@ class ChannelClientState {
|
|||||||
|
|
||||||
final _subscriptions = <StreamSubscription>[];
|
final _subscriptions = <StreamSubscription>[];
|
||||||
|
|
||||||
void _computeInitialUnread() {
|
void _computeUnread() {
|
||||||
final userRead = channelState.read.firstWhereOrNull(
|
final userRead = channelState.read.firstWhereOrNull(
|
||||||
(r) => r.user.id == _channel._client.state.currentUser?.id,
|
(r) => r.user.id == _channel._client.state.currentUser?.id,
|
||||||
);
|
);
|
||||||
if (userRead != null) {
|
if (userRead != null && userRead.unreadMessages > 0) {
|
||||||
unreadCount = userRead.unreadMessages;
|
unreadCount = userRead.unreadMessages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1934,6 +1934,8 @@ class ChannelClientState {
|
|||||||
read: newReads,
|
read: newReads,
|
||||||
pinnedMessages: updatedState.pinnedMessages,
|
pinnedMessages: updatedState.pinnedMessages,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
_computeUnread();
|
||||||
}
|
}
|
||||||
|
|
||||||
int _sortByCreatedAt(Message a, Message b) =>
|
int _sortByCreatedAt(Message a, Message b) =>
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ class ChannelApi {
|
|||||||
) async {
|
) async {
|
||||||
final response = await _client.post(
|
final response = await _client.post(
|
||||||
'${_getChannelUrl(channelId, channelType)}/show',
|
'${_getChannelUrl(channelId, channelType)}/show',
|
||||||
|
data: {},
|
||||||
);
|
);
|
||||||
return EmptyResponse.fromJson(response.data);
|
return EmptyResponse.fromJson(response.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
|
|||||||
/// Current package version
|
/// Current package version
|
||||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||||
// ignore: constant_identifier_names
|
// ignore: constant_identifier_names
|
||||||
const PACKAGE_VERSION = '2.2.0';
|
const PACKAGE_VERSION = '2.2.1';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat
|
name: stream_chat
|
||||||
homepage: https://getstream.io/
|
homepage: https://getstream.io/
|
||||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
description: The official Dart client for Stream Chat, a service for building chat applications.
|
||||||
version: 2.2.0
|
version: 2.2.1
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
|
|||||||
@@ -550,14 +550,21 @@ void main() {
|
|||||||
|
|
||||||
final path = '${_getChannelUrl(channelId, channelType)}/show';
|
final path = '${_getChannelUrl(channelId, channelType)}/show';
|
||||||
|
|
||||||
when(() => client.post(path)).thenAnswer(
|
when(() => client.post(
|
||||||
(_) async => successResponse(path, data: <String, dynamic>{}));
|
path,
|
||||||
|
data: {},
|
||||||
|
))
|
||||||
|
.thenAnswer(
|
||||||
|
(_) async => successResponse(path, data: <String, dynamic>{}));
|
||||||
|
|
||||||
final res = await channelApi.showChannel(channelId, channelType);
|
final res = await channelApi.showChannel(channelId, channelType);
|
||||||
|
|
||||||
expect(res, isNotNull);
|
expect(res, isNotNull);
|
||||||
|
|
||||||
verify(() => client.post(path)).called(1);
|
verify(() => client.post(
|
||||||
|
path,
|
||||||
|
data: {},
|
||||||
|
)).called(1);
|
||||||
verifyNoMoreInteractions(client);
|
verifyNoMoreInteractions(client);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## 2.2.1
|
||||||
|
|
||||||
|
- Updated `stream_chat_flutter_core` dependency to 2.2.1
|
||||||
|
|
||||||
## 2.2.0
|
## 2.2.0
|
||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter
|
name: stream_chat_flutter
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
|
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
|
||||||
version: 2.2.0
|
version: 2.2.1
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ dependencies:
|
|||||||
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
||||||
share_plus: ^2.0.3
|
share_plus: ^2.0.3
|
||||||
shimmer: ^2.0.0
|
shimmer: ^2.0.0
|
||||||
stream_chat_flutter_core: ^2.2.0
|
stream_chat_flutter_core: ^2.2.1
|
||||||
substring_highlight: ^1.0.26
|
substring_highlight: ^1.0.26
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
url_launcher: ^6.0.3
|
url_launcher: ^6.0.3
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
## 2.2.1
|
||||||
|
|
||||||
|
- Updated `stream_chat` dependency to 2.2.1
|
||||||
|
|
||||||
## 2.2.0
|
## 2.2.0
|
||||||
|
|
||||||
🛑️ Breaking Changes from `2.1.1`
|
🛑️ Breaking Changes from `2.1.1`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter_core
|
name: stream_chat_flutter_core
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
|
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
|
||||||
version: 2.2.0
|
version: 2.2.1
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
stream_chat: ^2.2.0
|
stream_chat: ^2.2.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
fake_async: ^1.2.0
|
fake_async: ^1.2.0
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
## Upcoming
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
* Fixed typos in `Italian` translations.
|
||||||
|
|
||||||
## 1.1.0
|
## 1.1.0
|
||||||
|
|
||||||
✅ Added
|
✅ Added
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
|
|||||||
String get yesterdayLabel => 'Ieri';
|
String get yesterdayLabel => 'Ieri';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get channelIsMutedText => 'Il canale è mutato';
|
String get channelIsMutedText => 'Il canale è silenziato';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get noTitleText => 'Nessun titolo';
|
String get noTitleText => 'Nessun titolo';
|
||||||
@@ -274,7 +274,7 @@ Il file è troppo grande per essere caricato. Il limite è di $limitInMB MB.''';
|
|||||||
String get loadingChannelsError => 'Errore durante il caricamento dei canali';
|
String get loadingChannelsError => 'Errore durante il caricamento dei canali';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deleteConversationLabel => 'Elemina conversazione';
|
String get deleteConversationLabel => 'Elimina conversazione';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get deleteConversationQuestion =>
|
String get deleteConversationQuestion =>
|
||||||
|
|||||||