Merge branch 'develop' into develop
This commit is contained in:
BIN
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
@@ -64,30 +64,34 @@ The second type looks like this:
|
|||||||
We can use a `Stack` for achieving this:
|
We can use a `Stack` for achieving this:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
Scaffold(
|
Stack(
|
||||||
body: Stack(
|
children: <Widget>[
|
||||||
children: <Widget>[
|
// Add your video implementation here
|
||||||
// Add your video implementation here
|
ShaderMask(
|
||||||
ShaderMask(
|
shaderCallback: (rect) {
|
||||||
shaderCallback: (rect) {
|
return const LinearGradient(
|
||||||
return LinearGradient(
|
|
||||||
begin: Alignment.bottomCenter,
|
begin: Alignment.bottomCenter,
|
||||||
end: Alignment.topCenter,
|
end: Alignment.topCenter,
|
||||||
colors: [Colors.black, Colors.transparent],
|
colors: [Colors.black, Colors.transparent],
|
||||||
stops: [0.4, 0.65]
|
stops: [0.4, 0.8]).createShader(
|
||||||
).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height));
|
Rect.fromLTRB(0, 0, rect.width, rect.height),
|
||||||
},
|
);
|
||||||
blendMode: BlendMode.dstIn,
|
},
|
||||||
child: Column(
|
blendMode: BlendMode.dstIn,
|
||||||
children: [
|
child: Column(
|
||||||
Expanded(
|
children: const [
|
||||||
child: MessageListView(),
|
Expanded(
|
||||||
),
|
child: MessageListViewTheme(
|
||||||
MessageInput(),
|
data: MessageListViewThemeData(
|
||||||
],
|
backgroundColor: Colors.transparent,
|
||||||
),
|
),
|
||||||
),
|
child: MessageListView(),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
)
|
MessageInput(),
|
||||||
```
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
```
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
---
|
---
|
||||||
id: adding_push_notifications
|
id: adding_push_notifications
|
||||||
sidebar_position: 1
|
sidebar_position: 1
|
||||||
title: Adding Push Notifications
|
title: Adding Push Notifications (V1 legacy)
|
||||||
---
|
---
|
||||||
|
|
||||||
Adding Push Notifications To Your Application
|
Adding Push Notifications To Your Application
|
||||||
|
|
||||||
|
:::note
|
||||||
|
Version 1 (legacy) of push notifications won't be removed immediately but there won't be any new features. That's why new applications are highly recommended to use version 2 from the beginning to leverage upcoming new features.
|
||||||
|
:::
|
||||||
|
|
||||||
### Introduction
|
### Introduction
|
||||||
|
|
||||||
Push notifications are a core part of the experience for a messaging app. Users often need to be notified
|
Push notifications are a core part of the experience for a messaging app. Users often need to be notified
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
---
|
||||||
|
id: adding_push_notifications_v2
|
||||||
|
sidebar_position: 1
|
||||||
|
title: Adding Push Notifications (V2)
|
||||||
|
---
|
||||||
|
|
||||||
|
Adding Push Notifications To Your Application
|
||||||
|
|
||||||
|
### Introduction
|
||||||
|
|
||||||
|
Push notifications are a core part of the experience for a messaging app. Users often need to be notified
|
||||||
|
of new messages and old notifications sometimes need to be updated silently.
|
||||||
|
|
||||||
|
This guide details how to add push notifications to your app.
|
||||||
|
|
||||||
|
You can read more about Stream’s [push delivery logic](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart#push-delivery-rules).
|
||||||
|
|
||||||
|
### Setup FCM
|
||||||
|
|
||||||
|
To integrate push notifications in your Flutter app, you need to use the package [firebase_messaging](https://pub.dev/packages/firebase_messaging).
|
||||||
|
|
||||||
|
|
||||||
|
Follow the [Firebase documentation](https://firebase.flutter.dev/docs/messaging/overview/) to set up the plugin for Android and iOS.
|
||||||
|
|
||||||
|
|
||||||
|
Once that's done, FCM should be able to send push notifications to your devices.
|
||||||
|
|
||||||
|
### Integration With Stream
|
||||||
|
|
||||||
|
#### Step 1 - Get the Firebase Credentials
|
||||||
|
|
||||||
|
These credentials are the [private key file](https://firebase.google.com/docs/admin/setup#:~:text=To%20generate%20a%20private%20key%20file%20for%20your%20service%20account%3A) for your service account, in firebase console.
|
||||||
|
|
||||||
|
To generate a private key file for your service account, in the Firebase console:
|
||||||
|
|
||||||
|
- Open Settings > Service Accounts.
|
||||||
|
|
||||||
|
- Click **Generate New Private Key**, then confirm by clicking **Generate Key**.
|
||||||
|
|
||||||
|
- Securely store the JSON file containing the key.
|
||||||
|
|
||||||
|
This JSON file contains the credentials which needs to be uploaded to Stream’s server as explained in next step.
|
||||||
|
|
||||||
|
#### Step 2 - Upload the Firebase Credentials to Stream
|
||||||
|
|
||||||
|
You can upload your Firebase credentials using either the dashboard or the app settings API (available only in backend SDKs).
|
||||||
|
|
||||||
|
##### Using the Stream Dashboard
|
||||||
|
|
||||||
|
1. Go to the **Chat Overview** page on Stream Dashboard
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
2. Enable **Firebase Notification** toggle on **Chat Overview**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Enter your Firebase Credentials and press "Save".
|
||||||
|
|
||||||
|
##### Using the API
|
||||||
|
|
||||||
|
You can also enable Firebase notifications and upload the Firebase credentials using one of our server SDKs.
|
||||||
|
|
||||||
|
For example, using the JavaScript SDK:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const client = StreamChat.getInstance('api_key', 'api_secret');
|
||||||
|
client.updateAppSettings({
|
||||||
|
push_config: {
|
||||||
|
version: 'v2'
|
||||||
|
},
|
||||||
|
firebase_config: {
|
||||||
|
credentials_json: fs.readFileSync(
|
||||||
|
'./firebase-credentials.json',
|
||||||
|
'utf-8',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
### Registering a Device With Stream Backend
|
||||||
|
|
||||||
|
Once you configure a Firebase server key and set it up on Stream dashboard then a device that is supposed to receive push notifications needs to be registered on the Stream backend. This is usually done by listening for Firebase device token updates and passing them to the backend as follows:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
firebaseMessaging.onTokenRefresh.listen((token) {
|
||||||
|
client.addDevice(token, PushProvider.firebase);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Receiving Notifications
|
||||||
|
|
||||||
|
Push notifications behave a bit differently depending on whether you are using iOS or Android.
|
||||||
|
See [here](https://firebase.flutter.dev/docs/messaging/usage#message-types) to understand the difference between **notification** and **data** payloads.
|
||||||
|
|
||||||
|
#### iOS
|
||||||
|
|
||||||
|
On iOS we send both a **notification** and a **data** payload.
|
||||||
|
This means you don't need to do anything special to get the notification to show up. However, you might want to handle the data payload to perform some logic when the user taps on the notification.
|
||||||
|
|
||||||
|
To update the template, you can use a backend SDK.
|
||||||
|
For example, using the javascript SDK:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const client = StreamChat.getInstance(‘api_key’, ‘api_secret’);
|
||||||
|
const apn_template = `{
|
||||||
|
"aps": {
|
||||||
|
"alert": {
|
||||||
|
"title": "New message from {{ sender.name }}",
|
||||||
|
"body": "{{ truncate message.text 2000 }}"
|
||||||
|
},
|
||||||
|
"mutable-content": 1,
|
||||||
|
"category": "stream.chat"
|
||||||
|
},
|
||||||
|
"stream": {
|
||||||
|
"sender": "stream.chat",
|
||||||
|
"type": "message.new",
|
||||||
|
"version": "v2",
|
||||||
|
"id": "{{ message.id }}",
|
||||||
|
"cid": "{{ channel.cid }}"
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
client.updateAppSettings({
|
||||||
|
firebase_config: {
|
||||||
|
apn_template,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Android
|
||||||
|
On Android we send only a **data** payload. This gives you more flexibility and lets you decide what to do with the notification.
|
||||||
|
|
||||||
|
For example, you can listen and generate a notification from them.
|
||||||
|
|
||||||
|
To generate a notification when a **data-only** message is received and the app is in background:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> onBackgroundMessage(RemoteMessage message) async {
|
||||||
|
final chatClient = StreamChatClient(apiKey);
|
||||||
|
|
||||||
|
chatClient.connectUser(
|
||||||
|
User(id: userId),
|
||||||
|
userToken,
|
||||||
|
connectWebSocket: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
handleNotification(message, chatClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleNotification(
|
||||||
|
RemoteMessage message,
|
||||||
|
StreamChatClient chatClient,
|
||||||
|
) async {
|
||||||
|
|
||||||
|
final data = message.data;
|
||||||
|
|
||||||
|
if (data['type'] == 'message.new') {
|
||||||
|
final flutterLocalNotificationsPlugin = await setupLocalNotifications();
|
||||||
|
final messageId = data['id'];
|
||||||
|
final response = await chatClient.getMessage(messageId);
|
||||||
|
|
||||||
|
flutterLocalNotificationsPlugin.show(
|
||||||
|
1,
|
||||||
|
'New message from ${response.message.user.name} in ${response.channel.name}',
|
||||||
|
response.message.text,
|
||||||
|
NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'new_message',
|
||||||
|
'New message notifications channel',
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above example, you get the message details using the `getMessage` method and then you use the [flutter_local_notifications](https://pub.dev/packages/flutter_local_notifications) package to show the actual notification.
|
||||||
|
|
||||||
|
##### Using a Template on Android
|
||||||
|
|
||||||
|
It's still possible to add a **notification** payload to Android notifications.
|
||||||
|
You can do so by adding a template using a backend SDK.
|
||||||
|
For example, using the javascript SDK:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const client = StreamChat.getInstance(‘api_key’, ‘api_secret’);
|
||||||
|
const notification_template = `
|
||||||
|
{
|
||||||
|
"title": "{{ sender.name }} @ {{ channel.name }}",
|
||||||
|
"body": "{{ message.text }}",
|
||||||
|
"click_action": "OPEN_ACTIVITY_1",
|
||||||
|
"sound": "default"
|
||||||
|
}`;
|
||||||
|
|
||||||
|
client.updateAppSettings({
|
||||||
|
firebase_config: {
|
||||||
|
notification_template,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Possible Issues
|
||||||
|
|
||||||
|
Make sure to read the [general push notification docs](https://getstream.io/chat/docs/flutter-dart/push_introduction/?language=dart) in order to avoid known gotchas that may make your relationship with notifications difficult 😢.
|
||||||
|
|
||||||
|
### Testing if Push Notifications are Setup Correctly
|
||||||
|
|
||||||
|
If you're not sure whether you've set up push notifications correctly, for example, you don't always receive them, or they don’t work reliably, then you can follow these steps to make sure your config is correct and working:
|
||||||
|
1. Clone our repo for push testing: `git clone [email protected]:GetStream/chat-push-test.git`
|
||||||
|
2. `cd flutter`
|
||||||
|
3. In that folder run `flutter pub get`
|
||||||
|
4. Input your api key and secret in `lib/main.dart`
|
||||||
|
5. Change the bundle identifier/application ID and development team/user so you can run the app on your physical device.**Do not** run on an iOS simulator, as it will not work. Testing on an Android emulator is fine.
|
||||||
|
6. Add your `google-services.json/GoogleService-Info.plist`
|
||||||
|
7. Run the app
|
||||||
|
8. Accept push notification permission (iOS only)
|
||||||
|
9. Tap on `Device ID` and copy it
|
||||||
|
11. After configuring [stream-cli](https://github.com/GetStream/stream-cli), run the following command using your user ID:
|
||||||
|
```shell
|
||||||
|
stream chat:push:test -u <USER-ID>
|
||||||
|
```
|
||||||
|
|
||||||
|
You should get a test push notification 🥳
|
||||||
|
|
||||||
|
|
||||||
|
### 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 `FirebaseMessaging.onMessage.listen()` and handle them accordingly:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
FirebaseMessaging.onMessage.listen((message) async {
|
||||||
|
handleNotification(
|
||||||
|
message,
|
||||||
|
chatClient,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
:::note
|
||||||
|
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 (Only Android)
|
||||||
|
|
||||||
|
When the app is closed you may want to save received messages when you receive them via a notification so that later on when you open the app they're already there.
|
||||||
|
|
||||||
|
To do this you need to integrate the package [stream_chat_persistence](https://pub.dev/packages/stream_chat_persistence) in our app that exports a persistence client, see [here](https://pub.dev/packages/stream_chat_persistence#usage) how to set it up.
|
||||||
|
|
||||||
|
Then calling `FirebaseMessaging.onBackgroundMessage(...)` you need to use a TOP-LEVEL or STATIC function to handle background messages; here is an example:
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> onBackgroundMessage(RemoteMessage message) async {
|
||||||
|
final chatClient = StreamChatClient(apiKey);
|
||||||
|
final persistenceClient = StreamChatPersistenceClient();
|
||||||
|
|
||||||
|
await persistenceClient.connect(userId);
|
||||||
|
|
||||||
|
chatClient.connectUser(
|
||||||
|
User(id: userId),
|
||||||
|
userToken,
|
||||||
|
connectWebSocket: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
handleNotification(message, chatClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleNotification(
|
||||||
|
RemoteMessage message,
|
||||||
|
StreamChatClient chatClient,
|
||||||
|
) async {
|
||||||
|
final data = message.data;
|
||||||
|
if (data['type'] == 'message.new') {
|
||||||
|
final flutterLocalNotificationsPlugin = await setupLocalNotifications();
|
||||||
|
final messageId = data['id'];
|
||||||
|
final cid = data['cid'];
|
||||||
|
final response = await chatClient.getMessage(messageId);
|
||||||
|
await persistenceClient.updateMessages(cid, [response.message]);
|
||||||
|
|
||||||
|
persistenceClient.disconnect();
|
||||||
|
|
||||||
|
flutterLocalNotificationsPlugin.show(
|
||||||
|
1,
|
||||||
|
'New message from ${response.message.user.name} in ${response.channel.name}',
|
||||||
|
response.message.text,
|
||||||
|
NotificationDetails(
|
||||||
|
android: AndroidNotificationDetails(
|
||||||
|
'new_message',
|
||||||
|
'New message notifications channel',
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FirebaseMessaging.onBackgroundMessage(onBackgroundMessage);
|
||||||
|
```
|
||||||
|
|
||||||
@@ -1,6 +1,31 @@
|
|||||||
|
## 3.6.1
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#1081]](https://github.com/GetStream/stream-chat-flutter/issues/1081) Fixed a bug with user reconnection.
|
||||||
|
|
||||||
|
## 3.6.0
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- Fixed reactions not working for threads in offline mode.
|
||||||
|
- [[#1046]](https://github.com/GetStream/stream-chat-flutter/issues/1046) After `/mute` command on reload cannot access
|
||||||
|
any channel.
|
||||||
|
- [[#1047]](https://github.com/GetStream/stream-chat-flutter/issues/1047) `own_capabilities` extraData missing after
|
||||||
|
channel update.
|
||||||
|
- [[#1054]](https://github.com/GetStream/stream-chat-flutter/issues/1054) Fix `Unsupported operation: Cannot remove from an unmodifiable list`.
|
||||||
|
- [[#1033]](https://github.com/GetStream/stream-chat-flutter/issues/1033) Hard delete from dashboard does not delete message from client.
|
||||||
|
- Send only `user_id` while reconnecting.
|
||||||
|
|
||||||
|
✅ Added
|
||||||
|
|
||||||
|
- Handle `event.message` in `channel.truncate` events
|
||||||
|
- Added additional parameters to `channel.truncate`
|
||||||
|
|
||||||
## 3.5.1
|
## 3.5.1
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|
||||||
- `channel.unreadCount` was being set as using global unread count on a very specific case.
|
- `channel.unreadCount` was being set as using global unread count on a very specific case.
|
||||||
- The reconnection logic for the WebSocket connection is now more robust.
|
- The reconnection logic for the WebSocket connection is now more robust.
|
||||||
|
|
||||||
@@ -16,7 +41,7 @@
|
|||||||
- [[#890]](https://github.com/GetStream/stream-chat-flutter/pull/890) Fixed Reactions not updating on thread messages.
|
- [[#890]](https://github.com/GetStream/stream-chat-flutter/pull/890) Fixed Reactions not updating on thread messages.
|
||||||
Thanks [bstolinski](https://github.com/bstolinski).
|
Thanks [bstolinski](https://github.com/bstolinski).
|
||||||
- [[#897]](https://github.com/GetStream/stream-chat-flutter/issues/897) Fixed error type mis-match in `AuthInterceptor`.
|
- [[#897]](https://github.com/GetStream/stream-chat-flutter/issues/897) Fixed error type mis-match in `AuthInterceptor`.
|
||||||
- [[#891]](https://github.com/GetStream/stream-chat-flutter/pull/891) Fixed reply counter for parent message not
|
- [[#891]](https://github.com/GetStream/stream-chat-flutter/pull/891) Fixed reply counter for parent message not
|
||||||
updating correctly after deleting thread message.
|
updating correctly after deleting thread message.
|
||||||
- Fix `channelState.copyWith` with respect to pinnedMessages.
|
- Fix `channelState.copyWith` with respect to pinnedMessages.
|
||||||
|
|
||||||
|
|||||||
@@ -1033,10 +1033,23 @@ class Channel {
|
|||||||
return _client.deleteChannel(id!, type);
|
return _client.deleteChannel(id!, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes all messages from the channel.
|
/// Removes all messages from the channel up to [truncatedAt] or now if
|
||||||
Future<EmptyResponse> truncate() async {
|
/// [truncatedAt] is not provided.
|
||||||
|
/// If [skipPush] is true, no push notification will be sent.
|
||||||
|
/// [Message] is the system message that will be sent to the channel.
|
||||||
|
Future<EmptyResponse> truncate({
|
||||||
|
Message? message,
|
||||||
|
bool? skipPush,
|
||||||
|
DateTime? truncatedAt,
|
||||||
|
}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
return _client.truncateChannel(id!, type);
|
return _client.truncateChannel(
|
||||||
|
id!,
|
||||||
|
type,
|
||||||
|
message: message,
|
||||||
|
skipPush: skipPush,
|
||||||
|
truncatedAt: truncatedAt,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Accept invitation to the channel.
|
/// Accept invitation to the channel.
|
||||||
@@ -1094,7 +1107,6 @@ class Channel {
|
|||||||
// remove the passed message if response does
|
// remove the passed message if response does
|
||||||
// not contain message
|
// not contain message
|
||||||
state!.removeMessage(message);
|
state!.removeMessage(message);
|
||||||
await _client.chatPersistenceClient?.deleteMessageById(messageId);
|
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
@@ -1586,10 +1598,12 @@ class ChannelClientState {
|
|||||||
_subscriptions.add(_channel.on(EventType.memberRemoved).listen((Event e) {
|
_subscriptions.add(_channel.on(EventType.memberRemoved).listen((Event e) {
|
||||||
final user = e.user;
|
final user = e.user;
|
||||||
updateChannelState(channelState.copyWith(
|
updateChannelState(channelState.copyWith(
|
||||||
members: List.from(
|
members: channelState.members
|
||||||
channelState.members..removeWhere((m) => m.userId == user!.id),
|
.where((m) => m.userId != user!.id)
|
||||||
),
|
.toList(growable: false),
|
||||||
read: channelState.read..removeWhere((r) => r.user.id == user!.id),
|
read: channelState.read
|
||||||
|
.where((r) => r.user.id != user!.id)
|
||||||
|
.toList(growable: false),
|
||||||
));
|
));
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1598,7 +1612,7 @@ class ChannelClientState {
|
|||||||
_subscriptions.add(_channel.on(EventType.channelUpdated).listen((Event e) {
|
_subscriptions.add(_channel.on(EventType.channelUpdated).listen((Event e) {
|
||||||
final channel = e.channel!;
|
final channel = e.channel!;
|
||||||
updateChannelState(channelState.copyWith(
|
updateChannelState(channelState.copyWith(
|
||||||
channel: channel,
|
channel: channelState.channel?.merge(channel),
|
||||||
members: channel.members,
|
members: channel.members,
|
||||||
));
|
));
|
||||||
}));
|
}));
|
||||||
@@ -1612,6 +1626,9 @@ class ChannelClientState {
|
|||||||
await _channel._client.chatPersistenceClient
|
await _channel._client.chatPersistenceClient
|
||||||
?.deleteMessageByCid(channel.cid);
|
?.deleteMessageByCid(channel.cid);
|
||||||
truncate();
|
truncate();
|
||||||
|
if (event.message != null) {
|
||||||
|
updateMessage(event.message!);
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1846,7 +1863,9 @@ class ChannelClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a [message] from this [channelState].
|
/// Remove a [message] from this [channelState].
|
||||||
void removeMessage(Message message) {
|
void removeMessage(Message message) async {
|
||||||
|
await _channel._client.chatPersistenceClient?.deleteMessageById(message.id);
|
||||||
|
|
||||||
final parentId = message.parentId;
|
final parentId = message.parentId;
|
||||||
// i.e. it's a thread message, Remove it
|
// i.e. it's a thread message, Remove it
|
||||||
if (parentId != null) {
|
if (parentId != null) {
|
||||||
@@ -2119,12 +2138,12 @@ class ChannelClientState {
|
|||||||
final BehaviorSubject<Map<String, List<Message>>> _threadsController =
|
final BehaviorSubject<Map<String, List<Message>>> _threadsController =
|
||||||
BehaviorSubject.seeded({});
|
BehaviorSubject.seeded({});
|
||||||
|
|
||||||
set _threads(Map<String, List<Message>> v) {
|
set _threads(Map<String, List<Message>> threads) {
|
||||||
_channel.client.chatPersistenceClient?.updateMessages(
|
_threadsController.add(threads);
|
||||||
|
_channel.client.chatPersistenceClient?.updateChannelThreads(
|
||||||
_channel.cid!,
|
_channel.cid!,
|
||||||
v.values.expand((v) => v).toList(),
|
threads,
|
||||||
);
|
);
|
||||||
_threadsController.add(v);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Channel related typing users last value.
|
/// Channel related typing users last value.
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class StreamChatClient {
|
|||||||
StreamChatClient(
|
StreamChatClient(
|
||||||
String apiKey, {
|
String apiKey, {
|
||||||
this.logLevel = Level.WARNING,
|
this.logLevel = Level.WARNING,
|
||||||
LogHandlerFunction? logHandlerFunction,
|
this.logHandlerFunction = StreamChatClient.defaultLogHandler,
|
||||||
RetryPolicy? retryPolicy,
|
RetryPolicy? retryPolicy,
|
||||||
@Deprecated('''
|
@Deprecated('''
|
||||||
Location is now deprecated in favor of the new edge server. Will be removed in v4.0.0.
|
Location is now deprecated in favor of the new edge server. Will be removed in v4.0.0.
|
||||||
@@ -77,7 +77,6 @@ class StreamChatClient {
|
|||||||
WebSocket? ws,
|
WebSocket? ws,
|
||||||
AttachmentFileUploader? attachmentFileUploader,
|
AttachmentFileUploader? attachmentFileUploader,
|
||||||
}) {
|
}) {
|
||||||
this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler;
|
|
||||||
logger.info('Initiating new StreamChatClient');
|
logger.info('Initiating new StreamChatClient');
|
||||||
|
|
||||||
final options = StreamHttpClientOptions(
|
final options = StreamHttpClientOptions(
|
||||||
@@ -134,7 +133,7 @@ class StreamChatClient {
|
|||||||
'${CurrentPlatform.name}-'
|
'${CurrentPlatform.name}-'
|
||||||
'${PACKAGE_VERSION.split('+')[0]}';
|
'${PACKAGE_VERSION.split('+')[0]}';
|
||||||
|
|
||||||
/// Additionals headers for all requests
|
/// Additional headers for all requests
|
||||||
static Map<String, Object?> additionalHeaders = {};
|
static Map<String, Object?> additionalHeaders = {};
|
||||||
|
|
||||||
ChatPersistenceClient? _originalChatPersistenceClient;
|
ChatPersistenceClient? _originalChatPersistenceClient;
|
||||||
@@ -189,7 +188,7 @@ class StreamChatClient {
|
|||||||
/// final client = StreamChatClient("stream-chat-api-key",
|
/// final client = StreamChatClient("stream-chat-api-key",
|
||||||
/// logHandlerFunction: myLogHandlerFunction);
|
/// logHandlerFunction: myLogHandlerFunction);
|
||||||
///```
|
///```
|
||||||
late LogHandlerFunction logHandlerFunction;
|
final LogHandlerFunction logHandlerFunction;
|
||||||
|
|
||||||
StreamSubscription<ConnectionStatus>? _connectionStatusSubscription;
|
StreamSubscription<ConnectionStatus>? _connectionStatusSubscription;
|
||||||
|
|
||||||
@@ -214,17 +213,18 @@ class StreamChatClient {
|
|||||||
Stream<ConnectionStatus> get wsConnectionStatusStream =>
|
Stream<ConnectionStatus> get wsConnectionStatusStream =>
|
||||||
_wsConnectionStatusController.stream.distinct();
|
_wsConnectionStatusController.stream.distinct();
|
||||||
|
|
||||||
LogHandlerFunction get _defaultLogHandler => (LogRecord record) {
|
/// Default log handler function for the [StreamChatClient] logger.
|
||||||
print(
|
static void defaultLogHandler(LogRecord record) {
|
||||||
'${record.time} '
|
print(
|
||||||
'${_levelEmojiMapper[record.level] ?? record.level.name} '
|
'${record.time} '
|
||||||
'${record.loggerName} ${record.message} ',
|
'${_levelEmojiMapper[record.level] ?? record.level.name} '
|
||||||
);
|
'${record.loggerName} ${record.message} ',
|
||||||
if (record.error != null) print(record.error);
|
);
|
||||||
if (record.stackTrace != null) print(record.stackTrace);
|
if (record.error != null) print(record.error);
|
||||||
};
|
if (record.stackTrace != null) print(record.stackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
///
|
/// Default logger for the [StreamChatClient].
|
||||||
Logger detachedLogger(String name) => Logger.detached(name)
|
Logger detachedLogger(String name) => Logger.detached(name)
|
||||||
..level = logLevel
|
..level = logLevel
|
||||||
..onRecord.listen(logHandlerFunction);
|
..onRecord.listen(logHandlerFunction);
|
||||||
@@ -328,7 +328,9 @@ class StreamChatClient {
|
|||||||
_chatPersistenceClient = _originalChatPersistenceClient;
|
_chatPersistenceClient = _originalChatPersistenceClient;
|
||||||
await _chatPersistenceClient!.connect(ownUser.id);
|
await _chatPersistenceClient!.connect(ownUser.id);
|
||||||
}
|
}
|
||||||
final connectedUser = await openConnection();
|
final connectedUser = await openConnection(
|
||||||
|
includeUserDetailsInConnectCall: true,
|
||||||
|
);
|
||||||
return state.currentUser = connectedUser;
|
return state.currentUser = connectedUser;
|
||||||
} catch (e, stk) {
|
} catch (e, stk) {
|
||||||
if (e is StreamWebSocketError && e.isRetriable) {
|
if (e is StreamWebSocketError && e.isRetriable) {
|
||||||
@@ -341,7 +343,11 @@ class StreamChatClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a new WebSocket connection with the current user.
|
/// Creates a new WebSocket connection with the current user.
|
||||||
Future<OwnUser> openConnection() async {
|
/// If [includeUserDetailsInConnectCall] is true it will include the current
|
||||||
|
/// user details in the connect call.
|
||||||
|
Future<OwnUser> openConnection({
|
||||||
|
bool includeUserDetailsInConnectCall = false,
|
||||||
|
}) async {
|
||||||
assert(
|
assert(
|
||||||
state.currentUser != null,
|
state.currentUser != null,
|
||||||
'User is not set on client, '
|
'User is not set on client, '
|
||||||
@@ -371,7 +377,10 @@ class StreamChatClient {
|
|||||||
_ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler);
|
_ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final event = await _ws.connect(user);
|
final event = await _ws.connect(
|
||||||
|
user,
|
||||||
|
includeUserDetails: includeUserDetailsInConnectCall,
|
||||||
|
);
|
||||||
return user.merge(event.me);
|
return user.merge(event.me);
|
||||||
} catch (e, stk) {
|
} catch (e, stk) {
|
||||||
logger.severe('error connecting ws', e, stk);
|
logger.severe('error connecting ws', e, stk);
|
||||||
@@ -938,14 +947,23 @@ class StreamChatClient {
|
|||||||
channelType,
|
channelType,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Removes all messages from the channel
|
/// Removes all messages from the channel up to [truncatedAt] or now if
|
||||||
|
/// [truncatedAt] is not provided.
|
||||||
|
/// If [skipPush] is true, no push notification will be sent.
|
||||||
|
/// [Message] is the system message that will be sent to the channel.
|
||||||
Future<EmptyResponse> truncateChannel(
|
Future<EmptyResponse> truncateChannel(
|
||||||
String channelId,
|
String channelId,
|
||||||
String channelType,
|
String channelType, {
|
||||||
) =>
|
Message? message,
|
||||||
|
bool? skipPush,
|
||||||
|
DateTime? truncatedAt,
|
||||||
|
}) =>
|
||||||
_chatApi.channel.truncateChannel(
|
_chatApi.channel.truncateChannel(
|
||||||
channelId,
|
channelId,
|
||||||
channelType,
|
channelType,
|
||||||
|
message: message,
|
||||||
|
skipPush: skipPush,
|
||||||
|
truncatedAt: truncatedAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Mutes the channel
|
/// Mutes the channel
|
||||||
|
|||||||
@@ -265,10 +265,18 @@ class ChannelApi {
|
|||||||
/// Removes all messages from the channel
|
/// Removes all messages from the channel
|
||||||
Future<EmptyResponse> truncateChannel(
|
Future<EmptyResponse> truncateChannel(
|
||||||
String channelId,
|
String channelId,
|
||||||
String channelType,
|
String channelType, {
|
||||||
) async {
|
Message? message,
|
||||||
|
bool? skipPush,
|
||||||
|
DateTime? truncatedAt,
|
||||||
|
}) async {
|
||||||
final response = await _client.post(
|
final response = await _client.post(
|
||||||
'${_getChannelUrl(channelId, channelType)}/truncate',
|
'${_getChannelUrl(channelId, channelType)}/truncate',
|
||||||
|
data: {
|
||||||
|
if (message != null) 'message': message,
|
||||||
|
if (skipPush != null) 'skip_push': skipPush,
|
||||||
|
if (truncatedAt != null) 'truncated_at': truncatedAt,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return EmptyResponse.fromJson(response.data);
|
return EmptyResponse.fromJson(response.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ class ChannelModel {
|
|||||||
updatedAt: other.updatedAt,
|
updatedAt: other.updatedAt,
|
||||||
deletedAt: other.deletedAt,
|
deletedAt: other.deletedAt,
|
||||||
memberCount: other.memberCount,
|
memberCount: other.memberCount,
|
||||||
extraData: other.extraData,
|
extraData: {...extraData, ...other.extraData},
|
||||||
team: other.team,
|
team: other.team,
|
||||||
cooldown: other.cooldown,
|
cooldown: other.cooldown,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:stream_chat/src/core/models/channel_model.dart';
|
||||||
|
import 'package:stream_chat/src/core/models/user.dart';
|
||||||
|
|
||||||
|
part 'channel_mute.g.dart';
|
||||||
|
|
||||||
|
/// The class that contains the information about a muted channel
|
||||||
|
@JsonSerializable(createToJson: false)
|
||||||
|
class ChannelMute {
|
||||||
|
/// Constructor used for json serialization
|
||||||
|
ChannelMute({
|
||||||
|
required this.user,
|
||||||
|
required this.channel,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.expires,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Create a new instance from a json
|
||||||
|
factory ChannelMute.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ChannelMuteFromJson(json);
|
||||||
|
|
||||||
|
/// The user that performed the muting action
|
||||||
|
final User user;
|
||||||
|
|
||||||
|
/// The target channel
|
||||||
|
final ChannelModel channel;
|
||||||
|
|
||||||
|
/// The date in which the channel was muted
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
/// The date of the last update
|
||||||
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
/// The date in which the mute expires
|
||||||
|
final DateTime? expires;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'channel_mute.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
ChannelMute _$ChannelMuteFromJson(Map<String, dynamic> json) => ChannelMute(
|
||||||
|
user: User.fromJson(json['user'] as Map<String, dynamic>),
|
||||||
|
channel: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>),
|
||||||
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
expires: json['expires'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['expires'] as String),
|
||||||
|
);
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/user.dart';
|
import 'package:stream_chat/src/core/models/user.dart';
|
||||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
|
||||||
|
|
||||||
part 'mute.g.dart';
|
part 'mute.g.dart';
|
||||||
|
|
||||||
@@ -11,27 +9,27 @@ class Mute {
|
|||||||
/// Constructor used for json serialization
|
/// Constructor used for json serialization
|
||||||
Mute({
|
Mute({
|
||||||
required this.user,
|
required this.user,
|
||||||
required this.channel,
|
required this.target,
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
required this.updatedAt,
|
required this.updatedAt,
|
||||||
|
this.expires,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Create a new instance from a json
|
/// Create a new instance from a json
|
||||||
factory Mute.fromJson(Map<String, dynamic> json) => _$MuteFromJson(json);
|
factory Mute.fromJson(Map<String, dynamic> json) => _$MuteFromJson(json);
|
||||||
|
|
||||||
/// The user that performed the muting action
|
/// The user that performed the muting action
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
|
||||||
final User user;
|
final User user;
|
||||||
|
|
||||||
/// The target user
|
/// The target user
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
final User target;
|
||||||
final ChannelModel channel;
|
|
||||||
|
|
||||||
/// The date in which the use was muted
|
/// The date in which the use was muted
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
|
|
||||||
/// The date of the last update
|
/// The date of the last update
|
||||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
|
||||||
final DateTime updatedAt;
|
final DateTime updatedAt;
|
||||||
|
|
||||||
|
/// The date in which the mute expires
|
||||||
|
final DateTime? expires;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ part of 'mute.dart';
|
|||||||
|
|
||||||
Mute _$MuteFromJson(Map<String, dynamic> json) => Mute(
|
Mute _$MuteFromJson(Map<String, dynamic> json) => Mute(
|
||||||
user: User.fromJson(json['user'] as Map<String, dynamic>),
|
user: User.fromJson(json['user'] as Map<String, dynamic>),
|
||||||
channel: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>),
|
target: User.fromJson(json['target'] as Map<String, dynamic>),
|
||||||
createdAt: DateTime.parse(json['created_at'] as String),
|
createdAt: DateTime.parse(json['created_at'] as String),
|
||||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||||
|
expires: json['expires'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['expires'] as String),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:stream_chat/src/core/models/channel_mute.dart';
|
||||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
@@ -79,7 +80,7 @@ class OwnUser extends User {
|
|||||||
bool? banned,
|
bool? banned,
|
||||||
DateTime? banExpires,
|
DateTime? banExpires,
|
||||||
List<String>? teams,
|
List<String>? teams,
|
||||||
List<Mute>? channelMutes,
|
List<ChannelMute>? channelMutes,
|
||||||
List<Device>? devices,
|
List<Device>? devices,
|
||||||
List<Mute>? mutes,
|
List<Mute>? mutes,
|
||||||
int? totalUnreadCount,
|
int? totalUnreadCount,
|
||||||
@@ -142,7 +143,7 @@ class OwnUser extends User {
|
|||||||
|
|
||||||
/// List of channels muted by the user.
|
/// List of channels muted by the user.
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
final List<Mute> channelMutes;
|
final List<ChannelMute> channelMutes;
|
||||||
|
|
||||||
/// Total unread messages by the user.
|
/// Total unread messages by the user.
|
||||||
@JsonKey(includeIfNull: false)
|
@JsonKey(includeIfNull: false)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ OwnUser _$OwnUserFromJson(Map<String, dynamic> json) => OwnUser(
|
|||||||
totalUnreadCount: json['total_unread_count'] as int? ?? 0,
|
totalUnreadCount: json['total_unread_count'] as int? ?? 0,
|
||||||
unreadChannels: json['unread_channels'] as int? ?? 0,
|
unreadChannels: json['unread_channels'] as int? ?? 0,
|
||||||
channelMutes: (json['channel_mutes'] as List<dynamic>?)
|
channelMutes: (json['channel_mutes'] as List<dynamic>?)
|
||||||
?.map((e) => Mute.fromJson(e as Map<String, dynamic>))
|
?.map((e) => ChannelMute.fromJson(e as Map<String, dynamic>))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
|
|||||||
@@ -197,6 +197,28 @@ abstract class ChatPersistenceClient {
|
|||||||
/// Deletes all the members by channel [cids]
|
/// Deletes all the members by channel [cids]
|
||||||
Future<void> deleteMembersByCids(List<String> cids);
|
Future<void> deleteMembersByCids(List<String> cids);
|
||||||
|
|
||||||
|
/// Updates the channel [cid] threads data along with reactions and users.
|
||||||
|
Future<void> updateChannelThreads(
|
||||||
|
String cid,
|
||||||
|
Map<String, List<Message>> threads,
|
||||||
|
) async {
|
||||||
|
final messages = threads.values.expand((it) => it).toList();
|
||||||
|
|
||||||
|
// Removing old reactions before saving the new
|
||||||
|
final oldReactions = messages.map((it) => it.id).toList();
|
||||||
|
await deleteReactionsByMessageId(oldReactions);
|
||||||
|
|
||||||
|
// Adding new reactions and users data
|
||||||
|
final reactions = messages.expand(_expandReactions).toList();
|
||||||
|
final users = messages.map((it) => it.user).withNullifyer.toList();
|
||||||
|
|
||||||
|
await Future.wait([
|
||||||
|
updateMessages(cid, messages),
|
||||||
|
updateReactions(reactions),
|
||||||
|
updateUsers(users),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the channel state data using [channelState]
|
/// Update the channel state data using [channelState]
|
||||||
Future<void> updateChannelState(ChannelState channelState) =>
|
Future<void> updateChannelState(ChannelState channelState) =>
|
||||||
updateChannelStates([channelState]);
|
updateChannelStates([channelState]);
|
||||||
@@ -239,17 +261,8 @@ abstract class ChatPersistenceClient {
|
|||||||
channelWithMessages[cid] = messages;
|
channelWithMessages[cid] = messages;
|
||||||
channelWithPinnedMessages[cid] = pinnedMessages;
|
channelWithPinnedMessages[cid] = pinnedMessages;
|
||||||
|
|
||||||
List<Reaction> expandReactions(Message message) {
|
reactions.addAll(messages.expand(_expandReactions));
|
||||||
final own = message.ownReactions;
|
pinnedReactions.addAll(pinnedMessages.expand(_expandReactions));
|
||||||
final latest = message.latestReactions;
|
|
||||||
return [
|
|
||||||
if (own != null) ...own.where((r) => r.userId != null),
|
|
||||||
if (latest != null) ...latest.where((r) => r.userId != null),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
reactions.addAll(messages.expand(expandReactions));
|
|
||||||
pinnedReactions.addAll(pinnedMessages.expand(expandReactions));
|
|
||||||
|
|
||||||
users.addAll([
|
users.addAll([
|
||||||
channel.createdBy,
|
channel.createdBy,
|
||||||
@@ -292,4 +305,13 @@ abstract class ChatPersistenceClient {
|
|||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Reaction> _expandReactions(Message message) {
|
||||||
|
final own = message.ownReactions;
|
||||||
|
final latest = message.latestReactions;
|
||||||
|
return [
|
||||||
|
if (own != null) ...own.where((r) => r.userId != null),
|
||||||
|
if (latest != null) ...latest.where((r) => r.userId != null),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,12 +147,15 @@ class WebSocket with TimerHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Uri> _buildUri({bool refreshToken = false}) async {
|
Future<Uri> _buildUri({
|
||||||
|
bool refreshToken = false,
|
||||||
|
bool includeUserDetails = true,
|
||||||
|
}) async {
|
||||||
final user = _user!;
|
final user = _user!;
|
||||||
final token = await tokenManager.loadToken(refresh: refreshToken);
|
final token = await tokenManager.loadToken(refresh: refreshToken);
|
||||||
final params = {
|
final params = {
|
||||||
'user_id': user.id,
|
'user_id': user.id,
|
||||||
'user_details': user,
|
'user_details': includeUserDetails ? user : {'id': user.id},
|
||||||
'user_token': token.rawValue,
|
'user_token': token.rawValue,
|
||||||
'server_determines_connection_id': true,
|
'server_determines_connection_id': true,
|
||||||
};
|
};
|
||||||
@@ -176,7 +179,10 @@ class WebSocket with TimerHelper {
|
|||||||
bool _connectRequestInProgress = false;
|
bool _connectRequestInProgress = false;
|
||||||
|
|
||||||
/// Connect the WS using the parameters passed in the constructor
|
/// Connect the WS using the parameters passed in the constructor
|
||||||
Future<Event> connect(User user) async {
|
Future<Event> connect(
|
||||||
|
User user, {
|
||||||
|
bool includeUserDetails = false,
|
||||||
|
}) async {
|
||||||
if (_connectRequestInProgress) {
|
if (_connectRequestInProgress) {
|
||||||
throw const StreamWebSocketError('''
|
throw const StreamWebSocketError('''
|
||||||
You've called connect twice,
|
You've called connect twice,
|
||||||
@@ -191,7 +197,9 @@ class WebSocket with TimerHelper {
|
|||||||
connectionCompleter = Completer<Event>();
|
connectionCompleter = Completer<Event>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final uri = await _buildUri();
|
final uri = await _buildUri(
|
||||||
|
includeUserDetails: includeUserDetails,
|
||||||
|
);
|
||||||
_initWebSocketChannel(uri);
|
_initWebSocketChannel(uri);
|
||||||
} catch (e, stk) {
|
} catch (e, stk) {
|
||||||
_onConnectionError(e, stk);
|
_onConnectionError(e, stk);
|
||||||
@@ -219,7 +227,10 @@ class WebSocket with TimerHelper {
|
|||||||
setTimer(
|
setTimer(
|
||||||
Duration(milliseconds: delay),
|
Duration(milliseconds: delay),
|
||||||
() async {
|
() async {
|
||||||
final uri = await _buildUri(refreshToken: refreshToken);
|
final uri = await _buildUri(
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
includeUserDetails: false,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
_initWebSocketChannel(uri);
|
_initWebSocketChannel(uri);
|
||||||
} catch (e, stk) {
|
} catch (e, stk) {
|
||||||
|
|||||||
@@ -5,37 +5,36 @@ export 'package:dio/src/dio_error.dart';
|
|||||||
export 'package:dio/src/multipart_file.dart';
|
export 'package:dio/src/multipart_file.dart';
|
||||||
export 'package:dio/src/options.dart';
|
export 'package:dio/src/options.dart';
|
||||||
export 'package:dio/src/options.dart' show ProgressCallback;
|
export 'package:dio/src/options.dart' show ProgressCallback;
|
||||||
export 'package:logging/logging.dart' show Logger, Level;
|
export 'package:logging/logging.dart' show Logger, Level, LogRecord;
|
||||||
export 'package:rate_limiter/rate_limiter.dart';
|
export 'package:rate_limiter/rate_limiter.dart';
|
||||||
|
|
||||||
export './src/core/api/attachment_file_uploader.dart'
|
|
||||||
show AttachmentFileUploader;
|
|
||||||
export './src/core/api/requests.dart';
|
|
||||||
export './src/core/api/requests.dart';
|
|
||||||
export './src/core/api/responses.dart';
|
|
||||||
export './src/core/api/stream_chat_api.dart' show PushProvider;
|
|
||||||
export './src/core/error/error.dart';
|
|
||||||
export './src/core/models/action.dart';
|
|
||||||
export './src/core/models/attachment.dart';
|
|
||||||
export './src/core/models/attachment_file.dart';
|
|
||||||
export './src/core/models/channel_config.dart';
|
|
||||||
export './src/core/models/channel_model.dart';
|
|
||||||
export './src/core/models/channel_state.dart';
|
|
||||||
export './src/core/models/command.dart';
|
|
||||||
export './src/core/models/device.dart';
|
|
||||||
export './src/core/models/event.dart';
|
|
||||||
export './src/core/models/filter.dart' show Filter;
|
|
||||||
export './src/core/models/member.dart';
|
|
||||||
export './src/core/models/message.dart';
|
|
||||||
export './src/core/models/mute.dart';
|
|
||||||
export './src/core/models/own_user.dart';
|
|
||||||
export './src/core/models/reaction.dart';
|
|
||||||
export './src/core/models/read.dart';
|
|
||||||
export './src/core/models/user.dart';
|
|
||||||
export './src/core/util/extension.dart';
|
|
||||||
export './src/db/chat_persistence_client.dart';
|
|
||||||
export './src/event_type.dart';
|
|
||||||
export './src/location.dart';
|
|
||||||
export './src/ws/connection_status.dart';
|
|
||||||
export 'src/client/channel.dart';
|
export 'src/client/channel.dart';
|
||||||
export 'src/client/client.dart';
|
export 'src/client/client.dart';
|
||||||
|
export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader;
|
||||||
|
export 'src/core/api/requests.dart';
|
||||||
|
export 'src/core/api/requests.dart';
|
||||||
|
export 'src/core/api/responses.dart';
|
||||||
|
export 'src/core/api/stream_chat_api.dart' show PushProvider;
|
||||||
|
export 'src/core/error/error.dart';
|
||||||
|
export 'src/core/models/action.dart';
|
||||||
|
export 'src/core/models/attachment.dart';
|
||||||
|
export 'src/core/models/attachment_file.dart';
|
||||||
|
export 'src/core/models/channel_config.dart';
|
||||||
|
export 'src/core/models/channel_model.dart';
|
||||||
|
export 'src/core/models/channel_state.dart';
|
||||||
|
export 'src/core/models/command.dart';
|
||||||
|
export 'src/core/models/device.dart';
|
||||||
|
export 'src/core/models/event.dart';
|
||||||
|
export 'src/core/models/filter.dart' show Filter;
|
||||||
|
export 'src/core/models/member.dart';
|
||||||
|
export 'src/core/models/message.dart';
|
||||||
|
export 'src/core/models/mute.dart';
|
||||||
|
export 'src/core/models/own_user.dart';
|
||||||
|
export 'src/core/models/reaction.dart';
|
||||||
|
export 'src/core/models/read.dart';
|
||||||
|
export 'src/core/models/user.dart';
|
||||||
|
export 'src/core/util/extension.dart';
|
||||||
|
export 'src/db/chat_persistence_client.dart';
|
||||||
|
export 'src/event_type.dart';
|
||||||
|
export 'src/location.dart';
|
||||||
|
export 'src/ws/connection_status.dart';
|
||||||
|
|||||||
@@ -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 = '3.5.1';
|
const PACKAGE_VERSION = '3.6.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: 3.5.1
|
version: 3.6.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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"id": "super-band-9",
|
||||||
|
"role": "user",
|
||||||
|
"created_at": "2020-03-03T16:48:28.853674Z",
|
||||||
|
"updated_at": "2021-05-26T03:22:20.296181Z",
|
||||||
|
"last_active": "2021-06-16T11:42:29.466165498Z",
|
||||||
|
"banned": false,
|
||||||
|
"online": true,
|
||||||
|
"username": "Rioland",
|
||||||
|
"image": "https://placehold.jp/150x150.png",
|
||||||
|
"invisible": false,
|
||||||
|
"name": "Proud darkness",
|
||||||
|
"unread_count": 0
|
||||||
|
},
|
||||||
|
"channel": {
|
||||||
|
"id": "!members-Qsp7PpigdPkW0rJk0603y5GnTiF1iRfoDc4SAngMMmw",
|
||||||
|
"type": "messaging",
|
||||||
|
"cid": "messaging:!members-Qsp7PpigdPkW0rJk0603y5GnTiF1iRfoDc4SAngMMmw",
|
||||||
|
"last_message_at": "2020-12-02T06:56:18.003432Z",
|
||||||
|
"created_at": "2020-11-30T10:25:32.494601Z",
|
||||||
|
"updated_at": "2020-11-30T10:25:32.494601Z",
|
||||||
|
"created_by": {
|
||||||
|
"id": "super-band-9",
|
||||||
|
"role": "user",
|
||||||
|
"created_at": "2020-03-03T16:48:28.853674Z",
|
||||||
|
"updated_at": "2021-05-26T03:22:20.296181Z",
|
||||||
|
"last_active": "2021-06-16T11:42:29.466165498Z",
|
||||||
|
"banned": false,
|
||||||
|
"online": true,
|
||||||
|
"image": "https://placehold.jp/150x150.png",
|
||||||
|
"invisible": false,
|
||||||
|
"name": "Proud darkness",
|
||||||
|
"unread_count": 0,
|
||||||
|
"username": "Rioland"
|
||||||
|
},
|
||||||
|
"frozen": false,
|
||||||
|
"disabled": false,
|
||||||
|
"member_count": 2,
|
||||||
|
"config": {
|
||||||
|
"created_at": "2020-04-15T14:57:17.00966Z",
|
||||||
|
"updated_at": "2021-05-25T14:25:30.405621Z",
|
||||||
|
"name": "messaging",
|
||||||
|
"typing_events": true,
|
||||||
|
"read_events": true,
|
||||||
|
"connect_events": true,
|
||||||
|
"search": true,
|
||||||
|
"reactions": true,
|
||||||
|
"replies": true,
|
||||||
|
"mutes": true,
|
||||||
|
"uploads": true,
|
||||||
|
"url_enrichment": true,
|
||||||
|
"custom_events": false,
|
||||||
|
"push_notifications": true,
|
||||||
|
"message_retention": "infinite",
|
||||||
|
"max_message_length": 5000,
|
||||||
|
"automod": "disabled",
|
||||||
|
"automod_behavior": "flag",
|
||||||
|
"blocklist": "profanity_en_2020_v1",
|
||||||
|
"blocklist_behavior": "block",
|
||||||
|
"automod_thresholds": {},
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "giphy",
|
||||||
|
"description": "Post a random gif to the channel",
|
||||||
|
"args": "[text]",
|
||||||
|
"set": "fun_set"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"created_at": "2020-12-04T10:39:06.512021Z",
|
||||||
|
"updated_at": "2020-12-04T10:39:06.512021Z"
|
||||||
|
}
|
||||||
+13
-55
@@ -13,61 +13,19 @@
|
|||||||
"name": "Proud darkness",
|
"name": "Proud darkness",
|
||||||
"unread_count": 0
|
"unread_count": 0
|
||||||
},
|
},
|
||||||
"channel": {
|
"target": {
|
||||||
"id": "!members-Qsp7PpigdPkW0rJk0603y5GnTiF1iRfoDc4SAngMMmw",
|
"id": "super-band-10",
|
||||||
"type": "messaging",
|
"role": "user",
|
||||||
"cid": "messaging:!members-Qsp7PpigdPkW0rJk0603y5GnTiF1iRfoDc4SAngMMmw",
|
"created_at": "2020-03-03T16:48:28.853674Z",
|
||||||
"last_message_at": "2020-12-02T06:56:18.003432Z",
|
"updated_at": "2021-05-26T03:22:20.296181Z",
|
||||||
"created_at": "2020-11-30T10:25:32.494601Z",
|
"last_active": "2021-06-16T11:42:29.466165498Z",
|
||||||
"updated_at": "2020-11-30T10:25:32.494601Z",
|
"banned": false,
|
||||||
"created_by": {
|
"online": true,
|
||||||
"id": "super-band-9",
|
"username": "Holland",
|
||||||
"role": "user",
|
"image": "https://placehold.jp/150x150.png",
|
||||||
"created_at": "2020-03-03T16:48:28.853674Z",
|
"invisible": false,
|
||||||
"updated_at": "2021-05-26T03:22:20.296181Z",
|
"name": "Proud brightness",
|
||||||
"last_active": "2021-06-16T11:42:29.466165498Z",
|
"unread_count": 0
|
||||||
"banned": false,
|
|
||||||
"online": true,
|
|
||||||
"image": "https://placehold.jp/150x150.png",
|
|
||||||
"invisible": false,
|
|
||||||
"name": "Proud darkness",
|
|
||||||
"unread_count": 0,
|
|
||||||
"username": "Rioland"
|
|
||||||
},
|
|
||||||
"frozen": false,
|
|
||||||
"disabled": false,
|
|
||||||
"member_count": 2,
|
|
||||||
"config": {
|
|
||||||
"created_at": "2020-04-15T14:57:17.00966Z",
|
|
||||||
"updated_at": "2021-05-25T14:25:30.405621Z",
|
|
||||||
"name": "messaging",
|
|
||||||
"typing_events": true,
|
|
||||||
"read_events": true,
|
|
||||||
"connect_events": true,
|
|
||||||
"search": true,
|
|
||||||
"reactions": true,
|
|
||||||
"replies": true,
|
|
||||||
"mutes": true,
|
|
||||||
"uploads": true,
|
|
||||||
"url_enrichment": true,
|
|
||||||
"custom_events": false,
|
|
||||||
"push_notifications": true,
|
|
||||||
"message_retention": "infinite",
|
|
||||||
"max_message_length": 5000,
|
|
||||||
"automod": "disabled",
|
|
||||||
"automod_behavior": "flag",
|
|
||||||
"blocklist": "profanity_en_2020_v1",
|
|
||||||
"blocklist_behavior": "block",
|
|
||||||
"automod_thresholds": {},
|
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"name": "giphy",
|
|
||||||
"description": "Post a random gif to the channel",
|
|
||||||
"args": "[text]",
|
|
||||||
"set": "fun_set"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"created_at": "2020-12-04T10:39:06.512021Z",
|
"created_at": "2020-12-04T10:39:06.512021Z",
|
||||||
"updated_at": "2020-12-04T10:39:06.512021Z"
|
"updated_at": "2020-12-04T10:39:06.512021Z"
|
||||||
|
|||||||
@@ -645,8 +645,8 @@ void main() {
|
|||||||
|
|
||||||
when(() => persistence.getChannelThreads(any()))
|
when(() => persistence.getChannelThreads(any()))
|
||||||
.thenAnswer((_) async => {});
|
.thenAnswer((_) async => {});
|
||||||
when(() => persistence.updateMessages(any(), any()))
|
when(() => persistence.updateChannelThreads(any(), any()))
|
||||||
.thenAnswer((_) => Future.value());
|
.thenAnswer((_) async => {});
|
||||||
when(() => persistence.getChannelStateByCid(any(),
|
when(() => persistence.getChannelStateByCid(any(),
|
||||||
messagePagination: any(named: 'messagePagination'),
|
messagePagination: any(named: 'messagePagination'),
|
||||||
pinnedMessagePagination:
|
pinnedMessagePagination:
|
||||||
@@ -692,7 +692,7 @@ void main() {
|
|||||||
|
|
||||||
verify(() => persistence.getChannelThreads(any()))
|
verify(() => persistence.getChannelThreads(any()))
|
||||||
.called((persistentChannelStates + channelStates).length);
|
.called((persistentChannelStates + channelStates).length);
|
||||||
verify(() => persistence.updateMessages(any(), any()))
|
verify(() => persistence.updateChannelThreads(any(), any()))
|
||||||
.called((persistentChannelStates + channelStates).length);
|
.called((persistentChannelStates + channelStates).length);
|
||||||
verify(
|
verify(
|
||||||
() => persistence.getChannelStateByCid(any(),
|
() => persistence.getChannelStateByCid(any(),
|
||||||
@@ -733,8 +733,8 @@ void main() {
|
|||||||
|
|
||||||
when(() => persistence.getChannelThreads(any()))
|
when(() => persistence.getChannelThreads(any()))
|
||||||
.thenAnswer((_) async => {});
|
.thenAnswer((_) async => {});
|
||||||
when(() => persistence.updateMessages(any(), any()))
|
when(() => persistence.updateChannelThreads(any(), any()))
|
||||||
.thenAnswer((_) => Future.value());
|
.thenAnswer((_) async => {});
|
||||||
when(() => persistence.getChannelStateByCid(any(),
|
when(() => persistence.getChannelStateByCid(any(),
|
||||||
messagePagination: any(named: 'messagePagination'),
|
messagePagination: any(named: 'messagePagination'),
|
||||||
pinnedMessagePagination:
|
pinnedMessagePagination:
|
||||||
@@ -775,7 +775,7 @@ void main() {
|
|||||||
|
|
||||||
verify(() => persistence.getChannelThreads(any()))
|
verify(() => persistence.getChannelThreads(any()))
|
||||||
.called(persistentChannelStates.length);
|
.called(persistentChannelStates.length);
|
||||||
verify(() => persistence.updateMessages(any(), any()))
|
verify(() => persistence.updateChannelThreads(any(), any()))
|
||||||
.called(persistentChannelStates.length);
|
.called(persistentChannelStates.length);
|
||||||
verify(
|
verify(
|
||||||
() => persistence.getChannelStateByCid(any(),
|
() => persistence.getChannelStateByCid(any(),
|
||||||
|
|||||||
@@ -481,14 +481,21 @@ void main() {
|
|||||||
|
|
||||||
final path = '${_getChannelUrl(channelId, channelType)}/truncate';
|
final path = '${_getChannelUrl(channelId, channelType)}/truncate';
|
||||||
|
|
||||||
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.truncateChannel(channelId, channelType);
|
final res = await channelApi.truncateChannel(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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:stream_chat/src/core/models/channel_model.dart';
|
||||||
|
import 'package:stream_chat/src/core/models/channel_mute.dart';
|
||||||
|
import 'package:stream_chat/src/core/models/user.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
import '../../utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('src/models/channel_mute', () {
|
||||||
|
test('should parse json correctly', () {
|
||||||
|
final mute = ChannelMute.fromJson(jsonFixture('channel_mute.json'));
|
||||||
|
expect(mute.user, isA<User>());
|
||||||
|
expect(mute.channel, isA<ChannelModel>());
|
||||||
|
expect(mute.createdAt, DateTime.parse('2020-12-04T10:39:06.512021Z'));
|
||||||
|
expect(mute.updatedAt, DateTime.parse('2020-12-04T10:39:06.512021Z'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import 'package:stream_chat/src/core/models/channel_model.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/mute.dart';
|
import 'package:stream_chat/src/core/models/mute.dart';
|
||||||
import 'package:stream_chat/src/core/models/user.dart';
|
import 'package:stream_chat/src/core/models/user.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
@@ -6,12 +5,13 @@ import 'package:test/test.dart';
|
|||||||
import '../../utils.dart';
|
import '../../utils.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('src/models/mute', () {
|
group('src/models/channel_mute', () {
|
||||||
test('should parse json correctly', () {
|
test('should parse json correctly', () {
|
||||||
final mute = Mute.fromJson(jsonFixture('mute.json'));
|
final mute = Mute.fromJson(jsonFixture('mute.json'));
|
||||||
expect(mute.channel, isA<ChannelModel>());
|
|
||||||
expect(mute.user, isA<User>());
|
expect(mute.user, isA<User>());
|
||||||
|
expect(mute.target, isA<User>());
|
||||||
expect(mute.createdAt, DateTime.parse('2020-12-04T10:39:06.512021Z'));
|
expect(mute.createdAt, DateTime.parse('2020-12-04T10:39:06.512021Z'));
|
||||||
|
expect(mute.updatedAt, DateTime.parse('2020-12-04T10:39:06.512021Z'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
|
import 'package:stream_chat/src/core/models/channel_mute.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
@@ -6,12 +7,14 @@ import '../../utils.dart';
|
|||||||
|
|
||||||
class MockMute extends Mock implements Mute {}
|
class MockMute extends Mock implements Mute {}
|
||||||
|
|
||||||
|
class ChannelMockMute extends Mock implements ChannelMute {}
|
||||||
|
|
||||||
class MockDevice extends Mock implements Device {}
|
class MockDevice extends Mock implements Device {}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
final devices = [MockDevice(), MockDevice()];
|
final devices = [MockDevice(), MockDevice()];
|
||||||
final mutes = [MockMute(), MockMute()];
|
final mutes = [MockMute(), MockMute()];
|
||||||
final channelMutes = [MockMute()];
|
final channelMutes = [ChannelMockMute()];
|
||||||
final createdAt = DateTime.parse('2021-05-03 12:39:21.817646');
|
final createdAt = DateTime.parse('2021-05-03 12:39:21.817646');
|
||||||
final updatedAt = DateTime.parse('2021-04-03 12:39:21.817646');
|
final updatedAt = DateTime.parse('2021-04-03 12:39:21.817646');
|
||||||
final lastActive = DateTime.parse('2021-03-03 12:39:21.817646');
|
final lastActive = DateTime.parse('2021-03-03 12:39:21.817646');
|
||||||
|
|||||||
@@ -162,6 +162,23 @@ void main() {
|
|||||||
expect(channelState, isNotNull);
|
expect(channelState, isNotNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('updateChannelThreads', () async {
|
||||||
|
const cid = 'test:cid';
|
||||||
|
final user = User(id: 'test-user-id');
|
||||||
|
final threads = {
|
||||||
|
'parent-test-message': [
|
||||||
|
Message(
|
||||||
|
id: 'test-message',
|
||||||
|
text: 'test-message',
|
||||||
|
user: user,
|
||||||
|
ownReactions: [Reaction(type: 'test', user: user)],
|
||||||
|
latestReactions: [Reaction(type: 'test', user: user)],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
};
|
||||||
|
persistenceClient.updateChannelThreads(cid, threads);
|
||||||
|
});
|
||||||
|
|
||||||
test('updateChannelState', () async {
|
test('updateChannelState', () async {
|
||||||
final channelState = ChannelState();
|
final channelState = ChannelState();
|
||||||
persistenceClient.updateChannelState(channelState);
|
persistenceClient.updateChannelState(channelState);
|
||||||
|
|||||||
@@ -124,7 +124,10 @@ class FakeWebSocket extends Fake implements WebSocket {
|
|||||||
Completer<Event>? connectionCompleter;
|
Completer<Event>? connectionCompleter;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Event> connect(User user) async {
|
Future<Event> connect(
|
||||||
|
User user, {
|
||||||
|
bool? includeUserDetails = true,
|
||||||
|
}) async {
|
||||||
connectionStatus = ConnectionStatus.connecting;
|
connectionStatus = ConnectionStatus.connecting;
|
||||||
final event = Event(
|
final event = Event(
|
||||||
type: EventType.healthCheck,
|
type: EventType.healthCheck,
|
||||||
@@ -167,7 +170,10 @@ class FakeWebSocketWithConnectionError extends Fake implements WebSocket {
|
|||||||
Completer<Event>? connectionCompleter;
|
Completer<Event>? connectionCompleter;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Event> connect(User user) async {
|
Future<Event> connect(
|
||||||
|
User user, {
|
||||||
|
bool? includeUserDetails = true,
|
||||||
|
}) async {
|
||||||
connectionStatus = ConnectionStatus.connecting;
|
connectionStatus = ConnectionStatus.connecting;
|
||||||
const error = StreamWebSocketError('Error Connecting');
|
const error = StreamWebSocketError('Error Connecting');
|
||||||
connectionCompleter = Completer()..completeError(error);
|
connectionCompleter = Completer()..completeError(error);
|
||||||
|
|||||||
@@ -4,6 +4,25 @@
|
|||||||
|
|
||||||
- `centerTitle` and `elevation` properties to `ChannelHeader`, `ThreadHeader` and `ChannelListHeader`.
|
- `centerTitle` and `elevation` properties to `ChannelHeader`, `ThreadHeader` and `ChannelListHeader`.
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#1067]](https://github.com/GetStream/stream-chat-flutter/issues/1067): Fix name text overflow in reaction card.
|
||||||
|
- [[#842]](https://github.com/GetStream/stream-chat-flutter/issues/842): show date divider for first message.
|
||||||
|
- Loosen up url check for attachment download.
|
||||||
|
- Use `ogScrapeUrl` for LinkAttachments.
|
||||||
|
|
||||||
|
## 3.6.1
|
||||||
|
|
||||||
|
- Updated `stream_chat_flutter_core` dependency to [`3.6.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||||
|
|
||||||
|
## 3.6.0
|
||||||
|
|
||||||
|
🐞 Fixed
|
||||||
|
|
||||||
|
- [[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`.
|
||||||
|
- Fix `MessageInputTheme.inputBackgroundColor` color not being used in some widgets of `MessageInput`
|
||||||
|
- Removed dependency on `visibility_detector`
|
||||||
|
|
||||||
## 3.5.1
|
## 3.5.1
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
@@ -27,6 +46,7 @@
|
|||||||
- Fix default `Channel` route not opening from `ChannelListView` when `ChannelAvatar` is tapped
|
- Fix default `Channel` route not opening from `ChannelListView` when `ChannelAvatar` is tapped
|
||||||
|
|
||||||
## 3.4.0
|
## 3.4.0
|
||||||
|
|
||||||
- Updated `stream_chat_flutter_core` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
- Updated `stream_chat_flutter_core` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||||
|
|
||||||
🐞 Fixed
|
🐞 Fixed
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ android {
|
|||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||||
applicationId "com.example.example"
|
applicationId "com.example.example"
|
||||||
minSdkVersion 21
|
minSdkVersion 22
|
||||||
targetSdkVersion 31
|
targetSdkVersion 31
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
|||||||
-1
@@ -6,7 +6,6 @@ import 'dart:async';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:collection/collection.dart' show IterableExtension;
|
import 'package:collection/collection.dart' show IterableExtension;
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,11 @@ class AttachmentTitle extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final normalizedTitleLink = attachment.titleLink?.replaceFirst(
|
final ogScrapeUrl = attachment.ogScrapeUrl;
|
||||||
RegExp(r'https?://(www\.)?'),
|
|
||||||
'',
|
|
||||||
);
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final titleLink = attachment.titleLink;
|
final ogScrapeUrl = attachment.ogScrapeUrl;
|
||||||
if (titleLink != null) launchURL(context, titleLink);
|
if (ogScrapeUrl != null) launchURL(context, ogScrapeUrl);
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
@@ -42,8 +39,8 @@ class AttachmentTitle extends StatelessWidget {
|
|||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (normalizedTitleLink != null)
|
if (ogScrapeUrl != null)
|
||||||
Text(normalizedTitleLink, style: messageTheme.messageTextStyle),
|
Text(ogScrapeUrl, style: messageTheme.messageTextStyle),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ class UrlAttachment extends StatelessWidget {
|
|||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
final titleLink = urlAttachment.titleLink;
|
final ogScrapeUrl = urlAttachment.ogScrapeUrl;
|
||||||
if (titleLink != null) {
|
if (ogScrapeUrl != null) {
|
||||||
onLinkTap != null
|
onLinkTap != null
|
||||||
? onLinkTap!(titleLink)
|
? onLinkTap!(ogScrapeUrl)
|
||||||
: launchURL(context, titleLink);
|
: launchURL(context, ogScrapeUrl);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -225,3 +225,12 @@ extension UserListX on List<User> {
|
|||||||
return entries.map((e) => e.key).toList(growable: false);
|
return entries.map((e) => e.key).toList(growable: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Extensions on [Uri]
|
||||||
|
extension UriX on Uri {
|
||||||
|
/// Return the URI adding the http scheme if it is missing
|
||||||
|
Uri get withScheme {
|
||||||
|
if (hasScheme) return this;
|
||||||
|
return Uri.parse('http://${toString()}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -480,6 +480,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
if (widget.editMessage == null) {
|
if (widget.editMessage == null) {
|
||||||
child = Material(
|
child = Material(
|
||||||
elevation: 8,
|
elevation: 8,
|
||||||
|
color: _messageInputTheme.inputBackgroundColor,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -679,6 +680,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
gradient: _focusNode.hasFocus
|
gradient: _focusNode.hasFocus
|
||||||
? _messageInputTheme.activeBorderGradient
|
? _messageInputTheme.activeBorderGradient
|
||||||
: _messageInputTheme.idleBorderGradient,
|
: _messageInputTheme.idleBorderGradient,
|
||||||
|
color: _messageInputTheme.inputBackgroundColor,
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(1.5),
|
padding: const EdgeInsets.all(1.5),
|
||||||
@@ -1272,7 +1274,7 @@ class MessageInputState extends State<MessageInput> {
|
|||||||
Widget _buildReplyToMessage() {
|
Widget _buildReplyToMessage() {
|
||||||
if (!_hasQuotedMessage) return const Offstage();
|
if (!_hasQuotedMessage) return const Offstage();
|
||||||
final containsUrl = widget.quotedMessage!.attachments
|
final containsUrl = widget.quotedMessage!.attachments
|
||||||
.any((element) => element.titleLink != null);
|
.any((element) => element.ogScrapeUrl != null);
|
||||||
return QuotedMessageWidget(
|
return QuotedMessageWidget(
|
||||||
reverse: true,
|
reverse: true,
|
||||||
showBorder: !containsUrl,
|
showBorder: !containsUrl,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import 'package:stream_chat_flutter/scrollable_positioned_list/scrollable_positi
|
|||||||
import 'package:stream_chat_flutter/src/extension.dart';
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import 'package:stream_chat_flutter/src/swipeable.dart';
|
import 'package:stream_chat_flutter/src/swipeable.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:visibility_detector/visibility_detector.dart';
|
|
||||||
|
|
||||||
/// Widget builder for message
|
/// Widget builder for message
|
||||||
/// [defaultMessageWidget] is the default [MessageWidget] configuration
|
/// [defaultMessageWidget] is the default [MessageWidget] configuration
|
||||||
@@ -369,7 +368,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
double get _initialAlignment {
|
double get _initialAlignment {
|
||||||
final initialAlignment = widget.initialAlignment;
|
final initialAlignment = widget.initialAlignment;
|
||||||
if (initialAlignment != null) return initialAlignment;
|
if (initialAlignment != null) return initialAlignment;
|
||||||
return 0.1;
|
return streamChannel!.initialMessageId == null ? 0 : 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id;
|
bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id;
|
||||||
@@ -561,6 +560,9 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
if (widget.reverse
|
if (widget.reverse
|
||||||
? widget.headerBuilder == null
|
? widget.headerBuilder == null
|
||||||
: widget.footerBuilder == null) {
|
: widget.footerBuilder == null) {
|
||||||
|
if (messages.isNotEmpty) {
|
||||||
|
return _buildDateDivider(messages.last);
|
||||||
|
}
|
||||||
if (_isThreadConversation) return const Offstage();
|
if (_isThreadConversation) return const Offstage();
|
||||||
return const SizedBox(height: 52);
|
return const SizedBox(height: 52);
|
||||||
}
|
}
|
||||||
@@ -585,21 +587,12 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
message = messages[i - 2];
|
message = messages[i - 2];
|
||||||
nextMessage = messages[i - 1];
|
nextMessage = messages[i - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Jiffy(message.createdAt.toLocal()).isSame(
|
if (!Jiffy(message.createdAt.toLocal()).isSame(
|
||||||
nextMessage.createdAt.toLocal(),
|
nextMessage.createdAt.toLocal(),
|
||||||
Units.DAY,
|
Units.DAY,
|
||||||
)) {
|
)) {
|
||||||
final divider = widget.dateDividerBuilder != null
|
return _buildDateDivider(nextMessage);
|
||||||
? widget.dateDividerBuilder!(
|
|
||||||
nextMessage.createdAt.toLocal(),
|
|
||||||
)
|
|
||||||
: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
child: DateDivider(
|
|
||||||
dateTime: nextMessage.createdAt.toLocal(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return divider;
|
|
||||||
}
|
}
|
||||||
final timeDiff =
|
final timeDiff =
|
||||||
Jiffy(nextMessage.createdAt.toLocal()).diff(
|
Jiffy(nextMessage.createdAt.toLocal()).diff(
|
||||||
@@ -749,6 +742,20 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDateDivider(Message message) {
|
||||||
|
final divider = widget.dateDividerBuilder != null
|
||||||
|
? widget.dateDividerBuilder!(
|
||||||
|
message.createdAt.toLocal(),
|
||||||
|
)
|
||||||
|
: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
child: DateDivider(
|
||||||
|
dateTime: message.createdAt.toLocal(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return divider;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildThreadSeparator() {
|
Widget _buildThreadSeparator() {
|
||||||
if (widget.threadSeparatorBuilder != null) {
|
if (widget.threadSeparatorBuilder != null) {
|
||||||
return widget.threadSeparatorBuilder!.call(context);
|
return widget.threadSeparatorBuilder!.call(context);
|
||||||
@@ -805,7 +812,11 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
index = _getBottomElementIndex(values);
|
index = _getBottomElementIndex(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index == null) return const Offstage();
|
if ((index == null) ||
|
||||||
|
(!_isThreadConversation && index == itemCount - 2) ||
|
||||||
|
(_isThreadConversation && index == itemCount - 1)) {
|
||||||
|
return const Offstage();
|
||||||
|
}
|
||||||
|
|
||||||
if (index <= 2 || index >= itemCount - 3) {
|
if (index <= 2 || index >= itemCount - 3) {
|
||||||
if (widget.reverse) {
|
if (widget.reverse) {
|
||||||
@@ -884,7 +895,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
_scrollController!.jumpTo(index: 0);
|
_scrollController!.jumpTo(index: 0);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
_showScrollToBottom.value = false;
|
|
||||||
_scrollController!.scrollTo(
|
_scrollController!.scrollTo(
|
||||||
index: 0,
|
index: 0,
|
||||||
duration: const Duration(seconds: 1),
|
duration: const Duration(seconds: 1),
|
||||||
@@ -946,26 +956,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
int index,
|
int index,
|
||||||
) {
|
) {
|
||||||
final messageWidget = buildMessage(message, messages, index);
|
final messageWidget = buildMessage(message, messages, index);
|
||||||
return VisibilityDetector(
|
return messageWidget;
|
||||||
key: ValueKey('visibility: ${message.id}'),
|
|
||||||
onVisibilityChanged: (visibility) {
|
|
||||||
final isVisible = visibility.visibleBounds != Rect.zero;
|
|
||||||
if (isVisible) {
|
|
||||||
final channel = streamChannel.channel;
|
|
||||||
if (_upToDate &&
|
|
||||||
channel.config?.readEvents == true &&
|
|
||||||
channel.state!.unreadCount > 0) {
|
|
||||||
streamChannel.channel.markRead();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (mounted) {
|
|
||||||
if (_showScrollToBottom.value == isVisible) {
|
|
||||||
_showScrollToBottom.value = !isVisible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: messageWidget,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildParentMessage(
|
Widget buildParentMessage(
|
||||||
@@ -1101,7 +1092,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||||
|
|
||||||
final hasUrlAttachment =
|
final hasUrlAttachment =
|
||||||
message.attachments.any((it) => it.titleLink != null);
|
message.attachments.any((it) => it.ogScrapeUrl != null);
|
||||||
|
|
||||||
final borderSide =
|
final borderSide =
|
||||||
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
|
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
|
||||||
@@ -1284,6 +1275,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
_scrollController = widget.scrollController ?? ItemScrollController();
|
_scrollController = widget.scrollController ?? ItemScrollController();
|
||||||
_itemPositionListener =
|
_itemPositionListener =
|
||||||
widget.itemPositionListener ?? ItemPositionsListener.create();
|
widget.itemPositionListener ?? ItemPositionsListener.create();
|
||||||
|
_itemPositionListener.itemPositions
|
||||||
|
.addListener(_handleItemPositionsChanged);
|
||||||
|
|
||||||
_getOnThreadTap();
|
_getOnThreadTap();
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -1332,6 +1325,34 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleItemPositionsChanged() {
|
||||||
|
final _itemPositions = _itemPositionListener.itemPositions.value.toList();
|
||||||
|
final _firstItemIndex =
|
||||||
|
_itemPositions.indexWhere((element) => element.index == 1);
|
||||||
|
var _isFirstItemVisible = false;
|
||||||
|
if (_firstItemIndex != -1) {
|
||||||
|
final _firstItem = _itemPositions[_firstItemIndex];
|
||||||
|
_isFirstItemVisible =
|
||||||
|
_firstItem.itemLeadingEdge > 0 && _firstItem.itemTrailingEdge < 1;
|
||||||
|
}
|
||||||
|
if (_isFirstItemVisible) {
|
||||||
|
// most recent message is visible
|
||||||
|
final channel = streamChannel?.channel;
|
||||||
|
if (channel != null) {
|
||||||
|
if (_upToDate &&
|
||||||
|
channel.config?.readEvents == true &&
|
||||||
|
channel.state!.unreadCount > 0) {
|
||||||
|
streamChannel!.channel.markRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mounted) {
|
||||||
|
if (_showScrollToBottom.value == _isFirstItemVisible) {
|
||||||
|
_showScrollToBottom.value = !_isFirstItemVisible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _getOnThreadTap() {
|
void _getOnThreadTap() {
|
||||||
if (widget.onThreadTap != null) {
|
if (widget.onThreadTap != null) {
|
||||||
_onThreadTap = (Message message) {
|
_onThreadTap = (Message message) {
|
||||||
@@ -1369,6 +1390,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
streamChannel!.reloadChannel();
|
streamChannel!.reloadChannel();
|
||||||
}
|
}
|
||||||
_messageNewListener?.cancel();
|
_messageNewListener?.cancel();
|
||||||
|
_itemPositionListener.itemPositions
|
||||||
|
.removeListener(_handleItemPositionsChanged);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -236,6 +236,8 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
reaction.user!.name.split(' ')[0],
|
reaction.user!.name.split(' ')[0],
|
||||||
style: chatThemeData.textTheme.footnoteBold,
|
style: chatThemeData.textTheme.footnoteBold,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -574,11 +574,11 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
|
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
|
||||||
|
|
||||||
bool get hasNonUrlAttachments => widget.message.attachments
|
bool get hasNonUrlAttachments => widget.message.attachments
|
||||||
.where((it) => it.titleLink == null || it.type == 'giphy')
|
.where((it) => it.ogScrapeUrl == null || it.type == 'giphy')
|
||||||
.isNotEmpty;
|
.isNotEmpty;
|
||||||
|
|
||||||
bool get hasUrlAttachments => widget.message.attachments
|
bool get hasUrlAttachments => widget.message.attachments
|
||||||
.any((it) => it.titleLink != null && it.type != 'giphy');
|
.any((it) => it.ogScrapeUrl != null && it.type != 'giphy');
|
||||||
|
|
||||||
bool get showBottomRow =>
|
bool get showBottomRow =>
|
||||||
showThreadReplyIndicator ||
|
showThreadReplyIndicator ||
|
||||||
@@ -999,9 +999,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
|
|
||||||
Widget _buildUrlAttachment() {
|
Widget _buildUrlAttachment() {
|
||||||
final urlAttachment = widget.message.attachments
|
final urlAttachment = widget.message.attachments
|
||||||
.firstWhere((element) => element.titleLink != null);
|
.firstWhere((element) => element.ogScrapeUrl != null);
|
||||||
|
|
||||||
final host = Uri.parse(urlAttachment.titleLink!).host;
|
final host = Uri.parse(urlAttachment.ogScrapeUrl!).withScheme.host;
|
||||||
final splitList = host.split('.');
|
final splitList = host.split('.');
|
||||||
final hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
final hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
||||||
final hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
final hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
||||||
@@ -1173,7 +1173,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
|
|
||||||
widget.message.attachments
|
widget.message.attachments
|
||||||
.where((element) =>
|
.where((element) =>
|
||||||
(element.titleLink == null && element.type != null) ||
|
(element.ogScrapeUrl == null && element.type != null) ||
|
||||||
element.type == 'giphy')
|
element.type == 'giphy')
|
||||||
.forEach((e) {
|
.forEach((e) {
|
||||||
if (attachmentGroups[e.type] == null) {
|
if (attachmentGroups[e.type] == null) {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
bool get _hasAttachments => message.attachments.isNotEmpty;
|
bool get _hasAttachments => message.attachments.isNotEmpty;
|
||||||
|
|
||||||
bool get _containsLinkAttachment =>
|
bool get _containsLinkAttachment =>
|
||||||
message.attachments.any((element) => element.titleLink != null);
|
message.attachments.any((element) => element.ogScrapeUrl != null);
|
||||||
|
|
||||||
bool get _containsText => message.text?.isNotEmpty == true;
|
bool get _containsText => message.text?.isNotEmpty == true;
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
Attachment attachment;
|
Attachment attachment;
|
||||||
if (_containsLinkAttachment) {
|
if (_containsLinkAttachment) {
|
||||||
attachment = message.attachments.firstWhere(
|
attachment = message.attachments.firstWhere(
|
||||||
(element) => element.titleLink != null,
|
(element) => element.ogScrapeUrl != null,
|
||||||
);
|
);
|
||||||
child = _buildUrlAttachment(attachment);
|
child = _buildUrlAttachment(attachment);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
|
|
||||||
/// Launch URL
|
/// Launch URL
|
||||||
Future<void> launchURL(BuildContext context, String url) async {
|
Future<void> launchURL(BuildContext context, String url) async {
|
||||||
if (await canLaunch(url)) {
|
try {
|
||||||
await launch(url);
|
await launch(Uri.parse(url).withScheme.toString());
|
||||||
} else {
|
} catch (e) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(context.translations.launchUrlError)),
|
SnackBar(content: Text(context.translations.launchUrlError)),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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: 3.5.1
|
version: 3.6.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
|
||||||
|
|
||||||
@@ -36,14 +36,13 @@ dependencies:
|
|||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
share_plus: ^4.0.1
|
share_plus: ^4.0.1
|
||||||
shimmer: ^2.0.0
|
shimmer: ^2.0.0
|
||||||
stream_chat_flutter_core: ^3.5.1
|
stream_chat_flutter_core: ^3.6.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
|
||||||
video_compress: ^3.0.0
|
video_compress: ^3.0.0
|
||||||
video_player: ^2.1.0
|
video_player: ^2.1.0
|
||||||
video_thumbnail: ^0.4.3
|
video_thumbnail: ^0.5.0
|
||||||
visibility_detector: ^0.2.0
|
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
assets:
|
assets:
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
## 3.6.1
|
||||||
|
|
||||||
|
- Updated `stream_chat` dependency to [`3.6.1`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|
||||||
|
## 3.6.0
|
||||||
|
|
||||||
|
- Updated `stream_chat` dependency to [`3.6.0`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|
||||||
## 3.5.1
|
## 3.5.1
|
||||||
|
|
||||||
- Updated `stream_chat` dependency to [`3.5.1`](https://pub.dev/packages/stream_chat/changelog).
|
- Updated `stream_chat` dependency to [`3.5.1`](https://pub.dev/packages/stream_chat/changelog).
|
||||||
|
|||||||
@@ -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: 3.5.1
|
version: 3.6.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: ^3.5.1
|
stream_chat: ^3.6.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
dart_code_metrics: ^4.4.0
|
dart_code_metrics: ^4.4.0
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart' show LogRecord;
|
|
||||||
import 'package:mutex/mutex.dart';
|
import 'package:mutex/mutex.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user