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:
|
||||
|
||||
```dart
|
||||
Scaffold(
|
||||
body: Stack(
|
||||
children: <Widget>[
|
||||
// Add your video implementation here
|
||||
ShaderMask(
|
||||
shaderCallback: (rect) {
|
||||
return LinearGradient(
|
||||
Stack(
|
||||
children: <Widget>[
|
||||
// Add your video implementation here
|
||||
ShaderMask(
|
||||
shaderCallback: (rect) {
|
||||
return const LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [Colors.black, Colors.transparent],
|
||||
stops: [0.4, 0.65]
|
||||
).createShader(Rect.fromLTRB(0, 0, rect.width, rect.height));
|
||||
},
|
||||
blendMode: BlendMode.dstIn,
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MessageListView(),
|
||||
),
|
||||
MessageInput(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
```
|
||||
colors: [Colors.black, Colors.transparent],
|
||||
stops: [0.4, 0.8]).createShader(
|
||||
Rect.fromLTRB(0, 0, rect.width, rect.height),
|
||||
);
|
||||
},
|
||||
blendMode: BlendMode.dstIn,
|
||||
child: Column(
|
||||
children: const [
|
||||
Expanded(
|
||||
child: MessageListViewTheme(
|
||||
data: MessageListViewThemeData(
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
child: MessageListView(),
|
||||
),
|
||||
),
|
||||
MessageInput(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
```
|
||||
@@ -1,11 +1,15 @@
|
||||
---
|
||||
id: adding_push_notifications
|
||||
sidebar_position: 1
|
||||
title: Adding Push Notifications
|
||||
title: Adding Push Notifications (V1 legacy)
|
||||
---
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- `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.
|
||||
|
||||
@@ -16,7 +41,7 @@
|
||||
- [[#890]](https://github.com/GetStream/stream-chat-flutter/pull/890) Fixed Reactions not updating on thread messages.
|
||||
Thanks [bstolinski](https://github.com/bstolinski).
|
||||
- [[#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.
|
||||
- Fix `channelState.copyWith` with respect to pinnedMessages.
|
||||
|
||||
|
||||
@@ -1033,10 +1033,23 @@ class Channel {
|
||||
return _client.deleteChannel(id!, type);
|
||||
}
|
||||
|
||||
/// Removes all messages from the channel.
|
||||
Future<EmptyResponse> truncate() async {
|
||||
/// 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> truncate({
|
||||
Message? message,
|
||||
bool? skipPush,
|
||||
DateTime? truncatedAt,
|
||||
}) async {
|
||||
_checkInitialized();
|
||||
return _client.truncateChannel(id!, type);
|
||||
return _client.truncateChannel(
|
||||
id!,
|
||||
type,
|
||||
message: message,
|
||||
skipPush: skipPush,
|
||||
truncatedAt: truncatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
/// Accept invitation to the channel.
|
||||
@@ -1094,7 +1107,6 @@ class Channel {
|
||||
// remove the passed message if response does
|
||||
// not contain message
|
||||
state!.removeMessage(message);
|
||||
await _client.chatPersistenceClient?.deleteMessageById(messageId);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -1586,10 +1598,12 @@ class ChannelClientState {
|
||||
_subscriptions.add(_channel.on(EventType.memberRemoved).listen((Event e) {
|
||||
final user = e.user;
|
||||
updateChannelState(channelState.copyWith(
|
||||
members: List.from(
|
||||
channelState.members..removeWhere((m) => m.userId == user!.id),
|
||||
),
|
||||
read: channelState.read..removeWhere((r) => r.user.id == user!.id),
|
||||
members: channelState.members
|
||||
.where((m) => m.userId != user!.id)
|
||||
.toList(growable: false),
|
||||
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) {
|
||||
final channel = e.channel!;
|
||||
updateChannelState(channelState.copyWith(
|
||||
channel: channel,
|
||||
channel: channelState.channel?.merge(channel),
|
||||
members: channel.members,
|
||||
));
|
||||
}));
|
||||
@@ -1612,6 +1626,9 @@ class ChannelClientState {
|
||||
await _channel._client.chatPersistenceClient
|
||||
?.deleteMessageByCid(channel.cid);
|
||||
truncate();
|
||||
if (event.message != null) {
|
||||
updateMessage(event.message!);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1846,7 +1863,9 @@ class ChannelClientState {
|
||||
}
|
||||
|
||||
/// 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;
|
||||
// i.e. it's a thread message, Remove it
|
||||
if (parentId != null) {
|
||||
@@ -2119,12 +2138,12 @@ class ChannelClientState {
|
||||
final BehaviorSubject<Map<String, List<Message>>> _threadsController =
|
||||
BehaviorSubject.seeded({});
|
||||
|
||||
set _threads(Map<String, List<Message>> v) {
|
||||
_channel.client.chatPersistenceClient?.updateMessages(
|
||||
set _threads(Map<String, List<Message>> threads) {
|
||||
_threadsController.add(threads);
|
||||
_channel.client.chatPersistenceClient?.updateChannelThreads(
|
||||
_channel.cid!,
|
||||
v.values.expand((v) => v).toList(),
|
||||
threads,
|
||||
);
|
||||
_threadsController.add(v);
|
||||
}
|
||||
|
||||
/// Channel related typing users last value.
|
||||
|
||||
@@ -64,7 +64,7 @@ class StreamChatClient {
|
||||
StreamChatClient(
|
||||
String apiKey, {
|
||||
this.logLevel = Level.WARNING,
|
||||
LogHandlerFunction? logHandlerFunction,
|
||||
this.logHandlerFunction = StreamChatClient.defaultLogHandler,
|
||||
RetryPolicy? retryPolicy,
|
||||
@Deprecated('''
|
||||
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,
|
||||
AttachmentFileUploader? attachmentFileUploader,
|
||||
}) {
|
||||
this.logHandlerFunction = logHandlerFunction ?? _defaultLogHandler;
|
||||
logger.info('Initiating new StreamChatClient');
|
||||
|
||||
final options = StreamHttpClientOptions(
|
||||
@@ -134,7 +133,7 @@ class StreamChatClient {
|
||||
'${CurrentPlatform.name}-'
|
||||
'${PACKAGE_VERSION.split('+')[0]}';
|
||||
|
||||
/// Additionals headers for all requests
|
||||
/// Additional headers for all requests
|
||||
static Map<String, Object?> additionalHeaders = {};
|
||||
|
||||
ChatPersistenceClient? _originalChatPersistenceClient;
|
||||
@@ -189,7 +188,7 @@ class StreamChatClient {
|
||||
/// final client = StreamChatClient("stream-chat-api-key",
|
||||
/// logHandlerFunction: myLogHandlerFunction);
|
||||
///```
|
||||
late LogHandlerFunction logHandlerFunction;
|
||||
final LogHandlerFunction logHandlerFunction;
|
||||
|
||||
StreamSubscription<ConnectionStatus>? _connectionStatusSubscription;
|
||||
|
||||
@@ -214,17 +213,18 @@ class StreamChatClient {
|
||||
Stream<ConnectionStatus> get wsConnectionStatusStream =>
|
||||
_wsConnectionStatusController.stream.distinct();
|
||||
|
||||
LogHandlerFunction get _defaultLogHandler => (LogRecord record) {
|
||||
print(
|
||||
'${record.time} '
|
||||
'${_levelEmojiMapper[record.level] ?? record.level.name} '
|
||||
'${record.loggerName} ${record.message} ',
|
||||
);
|
||||
if (record.error != null) print(record.error);
|
||||
if (record.stackTrace != null) print(record.stackTrace);
|
||||
};
|
||||
/// Default log handler function for the [StreamChatClient] logger.
|
||||
static void defaultLogHandler(LogRecord record) {
|
||||
print(
|
||||
'${record.time} '
|
||||
'${_levelEmojiMapper[record.level] ?? record.level.name} '
|
||||
'${record.loggerName} ${record.message} ',
|
||||
);
|
||||
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)
|
||||
..level = logLevel
|
||||
..onRecord.listen(logHandlerFunction);
|
||||
@@ -328,7 +328,9 @@ class StreamChatClient {
|
||||
_chatPersistenceClient = _originalChatPersistenceClient;
|
||||
await _chatPersistenceClient!.connect(ownUser.id);
|
||||
}
|
||||
final connectedUser = await openConnection();
|
||||
final connectedUser = await openConnection(
|
||||
includeUserDetailsInConnectCall: true,
|
||||
);
|
||||
return state.currentUser = connectedUser;
|
||||
} catch (e, stk) {
|
||||
if (e is StreamWebSocketError && e.isRetriable) {
|
||||
@@ -341,7 +343,11 @@ class StreamChatClient {
|
||||
}
|
||||
|
||||
/// 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(
|
||||
state.currentUser != null,
|
||||
'User is not set on client, '
|
||||
@@ -371,7 +377,10 @@ class StreamChatClient {
|
||||
_ws.connectionStatusStream.skip(1).listen(_connectionStatusHandler);
|
||||
|
||||
try {
|
||||
final event = await _ws.connect(user);
|
||||
final event = await _ws.connect(
|
||||
user,
|
||||
includeUserDetails: includeUserDetailsInConnectCall,
|
||||
);
|
||||
return user.merge(event.me);
|
||||
} catch (e, stk) {
|
||||
logger.severe('error connecting ws', e, stk);
|
||||
@@ -938,14 +947,23 @@ class StreamChatClient {
|
||||
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(
|
||||
String channelId,
|
||||
String channelType,
|
||||
) =>
|
||||
String channelType, {
|
||||
Message? message,
|
||||
bool? skipPush,
|
||||
DateTime? truncatedAt,
|
||||
}) =>
|
||||
_chatApi.channel.truncateChannel(
|
||||
channelId,
|
||||
channelType,
|
||||
message: message,
|
||||
skipPush: skipPush,
|
||||
truncatedAt: truncatedAt,
|
||||
);
|
||||
|
||||
/// Mutes the channel
|
||||
|
||||
@@ -265,10 +265,18 @@ class ChannelApi {
|
||||
/// Removes all messages from the channel
|
||||
Future<EmptyResponse> truncateChannel(
|
||||
String channelId,
|
||||
String channelType,
|
||||
) async {
|
||||
String channelType, {
|
||||
Message? message,
|
||||
bool? skipPush,
|
||||
DateTime? truncatedAt,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'${_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);
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ class ChannelModel {
|
||||
updatedAt: other.updatedAt,
|
||||
deletedAt: other.deletedAt,
|
||||
memberCount: other.memberCount,
|
||||
extraData: other.extraData,
|
||||
extraData: {...extraData, ...other.extraData},
|
||||
team: other.team,
|
||||
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:stream_chat/src/core/models/channel_model.dart';
|
||||
import 'package:stream_chat/src/core/models/user.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
|
||||
part 'mute.g.dart';
|
||||
|
||||
@@ -11,27 +9,27 @@ class Mute {
|
||||
/// Constructor used for json serialization
|
||||
Mute({
|
||||
required this.user,
|
||||
required this.channel,
|
||||
required this.target,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.expires,
|
||||
});
|
||||
|
||||
/// Create a new instance from a json
|
||||
factory Mute.fromJson(Map<String, dynamic> json) => _$MuteFromJson(json);
|
||||
|
||||
/// The user that performed the muting action
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final User user;
|
||||
|
||||
/// The target user
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final ChannelModel channel;
|
||||
final User target;
|
||||
|
||||
/// The date in which the use was muted
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime createdAt;
|
||||
|
||||
/// The date of the last update
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
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(
|
||||
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),
|
||||
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:stream_chat/src/core/models/channel_mute.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
@@ -79,7 +80,7 @@ class OwnUser extends User {
|
||||
bool? banned,
|
||||
DateTime? banExpires,
|
||||
List<String>? teams,
|
||||
List<Mute>? channelMutes,
|
||||
List<ChannelMute>? channelMutes,
|
||||
List<Device>? devices,
|
||||
List<Mute>? mutes,
|
||||
int? totalUnreadCount,
|
||||
@@ -142,7 +143,7 @@ class OwnUser extends User {
|
||||
|
||||
/// List of channels muted by the user.
|
||||
@JsonKey(includeIfNull: false)
|
||||
final List<Mute> channelMutes;
|
||||
final List<ChannelMute> channelMutes;
|
||||
|
||||
/// Total unread messages by the user.
|
||||
@JsonKey(includeIfNull: false)
|
||||
|
||||
@@ -18,7 +18,7 @@ OwnUser _$OwnUserFromJson(Map<String, dynamic> json) => OwnUser(
|
||||
totalUnreadCount: json['total_unread_count'] as int? ?? 0,
|
||||
unreadChannels: json['unread_channels'] as int? ?? 0,
|
||||
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() ??
|
||||
const [],
|
||||
id: json['id'] as String,
|
||||
|
||||
@@ -197,6 +197,28 @@ abstract class ChatPersistenceClient {
|
||||
/// Deletes all the members by channel [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]
|
||||
Future<void> updateChannelState(ChannelState channelState) =>
|
||||
updateChannelStates([channelState]);
|
||||
@@ -239,17 +261,8 @@ abstract class ChatPersistenceClient {
|
||||
channelWithMessages[cid] = messages;
|
||||
channelWithPinnedMessages[cid] = pinnedMessages;
|
||||
|
||||
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),
|
||||
];
|
||||
}
|
||||
|
||||
reactions.addAll(messages.expand(expandReactions));
|
||||
pinnedReactions.addAll(pinnedMessages.expand(expandReactions));
|
||||
reactions.addAll(messages.expand(_expandReactions));
|
||||
pinnedReactions.addAll(pinnedMessages.expand(_expandReactions));
|
||||
|
||||
users.addAll([
|
||||
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 token = await tokenManager.loadToken(refresh: refreshToken);
|
||||
final params = {
|
||||
'user_id': user.id,
|
||||
'user_details': user,
|
||||
'user_details': includeUserDetails ? user : {'id': user.id},
|
||||
'user_token': token.rawValue,
|
||||
'server_determines_connection_id': true,
|
||||
};
|
||||
@@ -176,7 +179,10 @@ class WebSocket with TimerHelper {
|
||||
bool _connectRequestInProgress = false;
|
||||
|
||||
/// 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) {
|
||||
throw const StreamWebSocketError('''
|
||||
You've called connect twice,
|
||||
@@ -191,7 +197,9 @@ class WebSocket with TimerHelper {
|
||||
connectionCompleter = Completer<Event>();
|
||||
|
||||
try {
|
||||
final uri = await _buildUri();
|
||||
final uri = await _buildUri(
|
||||
includeUserDetails: includeUserDetails,
|
||||
);
|
||||
_initWebSocketChannel(uri);
|
||||
} catch (e, stk) {
|
||||
_onConnectionError(e, stk);
|
||||
@@ -219,7 +227,10 @@ class WebSocket with TimerHelper {
|
||||
setTimer(
|
||||
Duration(milliseconds: delay),
|
||||
() async {
|
||||
final uri = await _buildUri(refreshToken: refreshToken);
|
||||
final uri = await _buildUri(
|
||||
refreshToken: refreshToken,
|
||||
includeUserDetails: false,
|
||||
);
|
||||
try {
|
||||
_initWebSocketChannel(uri);
|
||||
} 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/options.dart';
|
||||
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 './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/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
|
||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||
// ignore: constant_identifier_names
|
||||
const PACKAGE_VERSION = '3.5.1';
|
||||
const PACKAGE_VERSION = '3.6.1';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: stream_chat
|
||||
homepage: https://getstream.io/
|
||||
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
|
||||
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",
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
"target": {
|
||||
"id": "super-band-10",
|
||||
"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": "Holland",
|
||||
"image": "https://placehold.jp/150x150.png",
|
||||
"invisible": false,
|
||||
"name": "Proud brightness",
|
||||
"unread_count": 0
|
||||
},
|
||||
"created_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()))
|
||||
.thenAnswer((_) async => {});
|
||||
when(() => persistence.updateMessages(any(), any()))
|
||||
.thenAnswer((_) => Future.value());
|
||||
when(() => persistence.updateChannelThreads(any(), any()))
|
||||
.thenAnswer((_) async => {});
|
||||
when(() => persistence.getChannelStateByCid(any(),
|
||||
messagePagination: any(named: 'messagePagination'),
|
||||
pinnedMessagePagination:
|
||||
@@ -692,7 +692,7 @@ void main() {
|
||||
|
||||
verify(() => persistence.getChannelThreads(any()))
|
||||
.called((persistentChannelStates + channelStates).length);
|
||||
verify(() => persistence.updateMessages(any(), any()))
|
||||
verify(() => persistence.updateChannelThreads(any(), any()))
|
||||
.called((persistentChannelStates + channelStates).length);
|
||||
verify(
|
||||
() => persistence.getChannelStateByCid(any(),
|
||||
@@ -733,8 +733,8 @@ void main() {
|
||||
|
||||
when(() => persistence.getChannelThreads(any()))
|
||||
.thenAnswer((_) async => {});
|
||||
when(() => persistence.updateMessages(any(), any()))
|
||||
.thenAnswer((_) => Future.value());
|
||||
when(() => persistence.updateChannelThreads(any(), any()))
|
||||
.thenAnswer((_) async => {});
|
||||
when(() => persistence.getChannelStateByCid(any(),
|
||||
messagePagination: any(named: 'messagePagination'),
|
||||
pinnedMessagePagination:
|
||||
@@ -775,7 +775,7 @@ void main() {
|
||||
|
||||
verify(() => persistence.getChannelThreads(any()))
|
||||
.called(persistentChannelStates.length);
|
||||
verify(() => persistence.updateMessages(any(), any()))
|
||||
verify(() => persistence.updateChannelThreads(any(), any()))
|
||||
.called(persistentChannelStates.length);
|
||||
verify(
|
||||
() => persistence.getChannelStateByCid(any(),
|
||||
|
||||
@@ -481,14 +481,21 @@ void main() {
|
||||
|
||||
final path = '${_getChannelUrl(channelId, channelType)}/truncate';
|
||||
|
||||
when(() => client.post(path)).thenAnswer(
|
||||
(_) async => successResponse(path, data: <String, dynamic>{}));
|
||||
when(() => client.post(
|
||||
path,
|
||||
data: {},
|
||||
))
|
||||
.thenAnswer(
|
||||
(_) async => successResponse(path, data: <String, dynamic>{}));
|
||||
|
||||
final res = await channelApi.truncateChannel(channelId, channelType);
|
||||
|
||||
expect(res, isNotNull);
|
||||
|
||||
verify(() => client.post(path)).called(1);
|
||||
verify(() => client.post(
|
||||
path,
|
||||
data: {},
|
||||
)).called(1);
|
||||
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/user.dart';
|
||||
import 'package:test/test.dart';
|
||||
@@ -6,12 +5,13 @@ import 'package:test/test.dart';
|
||||
import '../../utils.dart';
|
||||
|
||||
void main() {
|
||||
group('src/models/mute', () {
|
||||
group('src/models/channel_mute', () {
|
||||
test('should parse json correctly', () {
|
||||
final mute = Mute.fromJson(jsonFixture('mute.json'));
|
||||
expect(mute.channel, isA<ChannelModel>());
|
||||
expect(mute.user, isA<User>());
|
||||
expect(mute.target, isA<User>());
|
||||
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:stream_chat/src/core/models/channel_mute.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
@@ -6,12 +7,14 @@ import '../../utils.dart';
|
||||
|
||||
class MockMute extends Mock implements Mute {}
|
||||
|
||||
class ChannelMockMute extends Mock implements ChannelMute {}
|
||||
|
||||
class MockDevice extends Mock implements Device {}
|
||||
|
||||
void main() {
|
||||
final devices = [MockDevice(), MockDevice()];
|
||||
final mutes = [MockMute(), MockMute()];
|
||||
final channelMutes = [MockMute()];
|
||||
final channelMutes = [ChannelMockMute()];
|
||||
final createdAt = DateTime.parse('2021-05-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');
|
||||
|
||||
@@ -162,6 +162,23 @@ void main() {
|
||||
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 {
|
||||
final channelState = ChannelState();
|
||||
persistenceClient.updateChannelState(channelState);
|
||||
|
||||
@@ -124,7 +124,10 @@ class FakeWebSocket extends Fake implements WebSocket {
|
||||
Completer<Event>? connectionCompleter;
|
||||
|
||||
@override
|
||||
Future<Event> connect(User user) async {
|
||||
Future<Event> connect(
|
||||
User user, {
|
||||
bool? includeUserDetails = true,
|
||||
}) async {
|
||||
connectionStatus = ConnectionStatus.connecting;
|
||||
final event = Event(
|
||||
type: EventType.healthCheck,
|
||||
@@ -167,7 +170,10 @@ class FakeWebSocketWithConnectionError extends Fake implements WebSocket {
|
||||
Completer<Event>? connectionCompleter;
|
||||
|
||||
@override
|
||||
Future<Event> connect(User user) async {
|
||||
Future<Event> connect(
|
||||
User user, {
|
||||
bool? includeUserDetails = true,
|
||||
}) async {
|
||||
connectionStatus = ConnectionStatus.connecting;
|
||||
const error = StreamWebSocketError('Error Connecting');
|
||||
connectionCompleter = Completer()..completeError(error);
|
||||
|
||||
@@ -4,6 +4,25 @@
|
||||
|
||||
- `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
|
||||
|
||||
🐞 Fixed
|
||||
@@ -27,6 +46,7 @@
|
||||
- Fix default `Channel` route not opening from `ChannelListView` when `ChannelAvatar` is tapped
|
||||
|
||||
## 3.4.0
|
||||
|
||||
- Updated `stream_chat_flutter_core` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
@@ -40,7 +40,7 @@ android {
|
||||
defaultConfig {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "com.example.example"
|
||||
minSdkVersion 21
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 31
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
-1
@@ -6,7 +6,6 @@ import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
|
||||
@@ -18,14 +18,11 @@ class AttachmentTitle extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final normalizedTitleLink = attachment.titleLink?.replaceFirst(
|
||||
RegExp(r'https?://(www\.)?'),
|
||||
'',
|
||||
);
|
||||
final ogScrapeUrl = attachment.ogScrapeUrl;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final titleLink = attachment.titleLink;
|
||||
if (titleLink != null) launchURL(context, titleLink);
|
||||
final ogScrapeUrl = attachment.ogScrapeUrl;
|
||||
if (ogScrapeUrl != null) launchURL(context, ogScrapeUrl);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
@@ -42,8 +39,8 @@ class AttachmentTitle extends StatelessWidget {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
if (normalizedTitleLink != null)
|
||||
Text(normalizedTitleLink, style: messageTheme.messageTextStyle),
|
||||
if (ogScrapeUrl != null)
|
||||
Text(ogScrapeUrl, style: messageTheme.messageTextStyle),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -37,11 +37,11 @@ class UrlAttachment extends StatelessWidget {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
final titleLink = urlAttachment.titleLink;
|
||||
if (titleLink != null) {
|
||||
final ogScrapeUrl = urlAttachment.ogScrapeUrl;
|
||||
if (ogScrapeUrl != null) {
|
||||
onLinkTap != null
|
||||
? onLinkTap!(titleLink)
|
||||
: launchURL(context, titleLink);
|
||||
? onLinkTap!(ogScrapeUrl)
|
||||
: launchURL(context, ogScrapeUrl);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
|
||||
@@ -225,3 +225,12 @@ extension UserListX on List<User> {
|
||||
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) {
|
||||
child = Material(
|
||||
elevation: 8,
|
||||
color: _messageInputTheme.inputBackgroundColor,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
@@ -679,6 +680,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
gradient: _focusNode.hasFocus
|
||||
? _messageInputTheme.activeBorderGradient
|
||||
: _messageInputTheme.idleBorderGradient,
|
||||
color: _messageInputTheme.inputBackgroundColor,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(1.5),
|
||||
@@ -1272,7 +1274,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
Widget _buildReplyToMessage() {
|
||||
if (!_hasQuotedMessage) return const Offstage();
|
||||
final containsUrl = widget.quotedMessage!.attachments
|
||||
.any((element) => element.titleLink != null);
|
||||
.any((element) => element.ogScrapeUrl != null);
|
||||
return QuotedMessageWidget(
|
||||
reverse: true,
|
||||
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/swipeable.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
/// Widget builder for message
|
||||
/// [defaultMessageWidget] is the default [MessageWidget] configuration
|
||||
@@ -369,7 +368,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
double get _initialAlignment {
|
||||
final initialAlignment = widget.initialAlignment;
|
||||
if (initialAlignment != null) return initialAlignment;
|
||||
return 0.1;
|
||||
return streamChannel!.initialMessageId == null ? 0 : 0.1;
|
||||
}
|
||||
|
||||
bool _isInitialMessage(String id) => streamChannel!.initialMessageId == id;
|
||||
@@ -561,6 +560,9 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
if (widget.reverse
|
||||
? widget.headerBuilder == null
|
||||
: widget.footerBuilder == null) {
|
||||
if (messages.isNotEmpty) {
|
||||
return _buildDateDivider(messages.last);
|
||||
}
|
||||
if (_isThreadConversation) return const Offstage();
|
||||
return const SizedBox(height: 52);
|
||||
}
|
||||
@@ -585,21 +587,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
message = messages[i - 2];
|
||||
nextMessage = messages[i - 1];
|
||||
}
|
||||
|
||||
if (!Jiffy(message.createdAt.toLocal()).isSame(
|
||||
nextMessage.createdAt.toLocal(),
|
||||
Units.DAY,
|
||||
)) {
|
||||
final divider = widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder!(
|
||||
nextMessage.createdAt.toLocal(),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: DateDivider(
|
||||
dateTime: nextMessage.createdAt.toLocal(),
|
||||
),
|
||||
);
|
||||
return divider;
|
||||
return _buildDateDivider(nextMessage);
|
||||
}
|
||||
final timeDiff =
|
||||
Jiffy(nextMessage.createdAt.toLocal()).diff(
|
||||
@@ -749,6 +742,20 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
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() {
|
||||
if (widget.threadSeparatorBuilder != null) {
|
||||
return widget.threadSeparatorBuilder!.call(context);
|
||||
@@ -805,7 +812,11 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
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 (widget.reverse) {
|
||||
@@ -884,7 +895,6 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_scrollController!.jumpTo(index: 0);
|
||||
});
|
||||
} else {
|
||||
_showScrollToBottom.value = false;
|
||||
_scrollController!.scrollTo(
|
||||
index: 0,
|
||||
duration: const Duration(seconds: 1),
|
||||
@@ -946,26 +956,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
int index,
|
||||
) {
|
||||
final messageWidget = buildMessage(message, messages, index);
|
||||
return VisibilityDetector(
|
||||
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,
|
||||
);
|
||||
return messageWidget;
|
||||
}
|
||||
|
||||
Widget buildParentMessage(
|
||||
@@ -1101,7 +1092,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
|
||||
|
||||
final hasUrlAttachment =
|
||||
message.attachments.any((it) => it.titleLink != null);
|
||||
message.attachments.any((it) => it.ogScrapeUrl != null);
|
||||
|
||||
final borderSide =
|
||||
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
|
||||
@@ -1284,6 +1275,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
_scrollController = widget.scrollController ?? ItemScrollController();
|
||||
_itemPositionListener =
|
||||
widget.itemPositionListener ?? ItemPositionsListener.create();
|
||||
_itemPositionListener.itemPositions
|
||||
.addListener(_handleItemPositionsChanged);
|
||||
|
||||
_getOnThreadTap();
|
||||
super.initState();
|
||||
@@ -1332,6 +1325,34 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
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() {
|
||||
if (widget.onThreadTap != null) {
|
||||
_onThreadTap = (Message message) {
|
||||
@@ -1369,6 +1390,8 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
streamChannel!.reloadChannel();
|
||||
}
|
||||
_messageNewListener?.cancel();
|
||||
_itemPositionListener.itemPositions
|
||||
.removeListener(_handleItemPositionsChanged);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +236,8 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
reaction.user!.name.split(' ')[0],
|
||||
style: chatThemeData.textTheme.footnoteBold,
|
||||
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 hasNonUrlAttachments => widget.message.attachments
|
||||
.where((it) => it.titleLink == null || it.type == 'giphy')
|
||||
.where((it) => it.ogScrapeUrl == null || it.type == 'giphy')
|
||||
.isNotEmpty;
|
||||
|
||||
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 =>
|
||||
showThreadReplyIndicator ||
|
||||
@@ -999,9 +999,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
|
||||
Widget _buildUrlAttachment() {
|
||||
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 hostName = splitList.length == 3 ? splitList[1] : splitList[0];
|
||||
final hostDisplayName = urlAttachment.authorName?.capitalize() ??
|
||||
@@ -1173,7 +1173,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
|
||||
widget.message.attachments
|
||||
.where((element) =>
|
||||
(element.titleLink == null && element.type != null) ||
|
||||
(element.ogScrapeUrl == null && element.type != null) ||
|
||||
element.type == 'giphy')
|
||||
.forEach((e) {
|
||||
if (attachmentGroups[e.type] == null) {
|
||||
|
||||
@@ -97,7 +97,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
bool get _hasAttachments => message.attachments.isNotEmpty;
|
||||
|
||||
bool get _containsLinkAttachment =>
|
||||
message.attachments.any((element) => element.titleLink != null);
|
||||
message.attachments.any((element) => element.ogScrapeUrl != null);
|
||||
|
||||
bool get _containsText => message.text?.isNotEmpty == true;
|
||||
|
||||
@@ -201,7 +201,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
Attachment attachment;
|
||||
if (_containsLinkAttachment) {
|
||||
attachment = message.attachments.firstWhere(
|
||||
(element) => element.titleLink != null,
|
||||
(element) => element.ogScrapeUrl != null,
|
||||
);
|
||||
child = _buildUrlAttachment(attachment);
|
||||
} else {
|
||||
|
||||
@@ -8,9 +8,9 @@ import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
/// Launch URL
|
||||
Future<void> launchURL(BuildContext context, String url) async {
|
||||
if (await canLaunch(url)) {
|
||||
await launch(url);
|
||||
} else {
|
||||
try {
|
||||
await launch(Uri.parse(url).withScheme.toString());
|
||||
} catch (e) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.translations.launchUrlError)),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: 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.
|
||||
version: 3.5.1
|
||||
version: 3.6.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
@@ -36,14 +36,13 @@ dependencies:
|
||||
rxdart: ^0.27.0
|
||||
share_plus: ^4.0.1
|
||||
shimmer: ^2.0.0
|
||||
stream_chat_flutter_core: ^3.5.1
|
||||
stream_chat_flutter_core: ^3.6.1
|
||||
substring_highlight: ^1.0.26
|
||||
synchronized: ^3.0.0
|
||||
url_launcher: ^6.0.3
|
||||
video_compress: ^3.0.0
|
||||
video_player: ^2.1.0
|
||||
video_thumbnail: ^0.4.3
|
||||
visibility_detector: ^0.2.0
|
||||
video_thumbnail: ^0.5.0
|
||||
|
||||
flutter:
|
||||
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
|
||||
|
||||
- Updated `stream_chat` dependency to [`3.5.1`](https://pub.dev/packages/stream_chat/changelog).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: stream_chat_flutter_core
|
||||
homepage: https://github.com/GetStream/stream-chat-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
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
@@ -16,7 +16,7 @@ dependencies:
|
||||
sdk: flutter
|
||||
meta: ^1.3.0
|
||||
rxdart: ^0.27.0
|
||||
stream_chat: ^3.5.1
|
||||
stream_chat: ^3.6.1
|
||||
|
||||
dev_dependencies:
|
||||
dart_code_metrics: ^4.4.0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logging/logging.dart' show LogRecord;
|
||||
import 'package:mutex/mutex.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user