Merge branch 'develop' into develop
This commit is contained in:
@@ -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