merge origin/develop into ref/segregate-api-layer
Signed-off-by: Sahil Kumar <[email protected]>
This commit is contained in:
@@ -1,3 +1,10 @@
|
|||||||
|
## 2.0.0-nullsafety.5
|
||||||
|
|
||||||
|
- Minor fixes
|
||||||
|
- Performance improvements
|
||||||
|
- Fixed `skip_push` in `client.sendMessage`
|
||||||
|
- Added partial message update method
|
||||||
|
|
||||||
## 2.0.0-nullsafety.2
|
## 2.0.0-nullsafety.2
|
||||||
|
|
||||||
- Added new `Filter.raw` constructor
|
- Added new `Filter.raw` constructor
|
||||||
|
|||||||
@@ -4,15 +4,15 @@ import 'dart:math';
|
|||||||
import 'package:collection/collection.dart'
|
import 'package:collection/collection.dart'
|
||||||
show IterableExtension, ListEquality;
|
show IterableExtension, ListEquality;
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:rate_limiter/rate_limiter.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
import 'package:stream_chat/src/client/retry_queue.dart';
|
import 'package:stream_chat/src/client/retry_queue.dart';
|
||||||
import 'package:stream_chat/src/core/util/utils.dart';
|
|
||||||
import 'package:stream_chat/src/core/error/error.dart';
|
import 'package:stream_chat/src/core/error/error.dart';
|
||||||
import 'package:stream_chat/src/event_type.dart';
|
|
||||||
import 'package:rate_limiter/rate_limiter.dart';
|
|
||||||
import 'package:stream_chat/src/core/models/attachment_file.dart';
|
import 'package:stream_chat/src/core/models/attachment_file.dart';
|
||||||
import 'package:stream_chat/src/core/models/channel_state.dart';
|
import 'package:stream_chat/src/core/models/channel_state.dart';
|
||||||
import 'package:stream_chat/src/core/models/user.dart';
|
import 'package:stream_chat/src/core/models/user.dart';
|
||||||
|
import 'package:stream_chat/src/core/util/utils.dart';
|
||||||
|
import 'package:stream_chat/src/event_type.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
/// This a the class that manages a specific channel.
|
/// This a the class that manages a specific channel.
|
||||||
@@ -70,8 +70,11 @@ class Channel {
|
|||||||
true;
|
true;
|
||||||
|
|
||||||
/// Returns true if the channel is muted as a stream
|
/// Returns true if the channel is muted as a stream
|
||||||
Stream<bool>? get isMutedStream => _client.state.userStream.map((event) =>
|
Stream<bool>? get isMutedStream => _client.state.userStream
|
||||||
event!.channelMutes.any((element) => element.channel.cid == cid) == true);
|
.map((event) =>
|
||||||
|
event!.channelMutes.any((element) => element.channel.cid == cid) ==
|
||||||
|
true)
|
||||||
|
.distinct();
|
||||||
|
|
||||||
/// True if the channel is a group
|
/// True if the channel is a group
|
||||||
bool get isGroup => memberCount != 2;
|
bool get isGroup => memberCount != 2;
|
||||||
@@ -253,7 +256,10 @@ class Channel {
|
|||||||
String messageId,
|
String messageId,
|
||||||
Iterable<String> attachmentIds,
|
Iterable<String> attachmentIds,
|
||||||
) {
|
) {
|
||||||
final message = state!.messages.firstWhereOrNull(
|
final message = [
|
||||||
|
...state!.messages,
|
||||||
|
...state!.threads.values.expand((messages) => messages),
|
||||||
|
].firstWhereOrNull(
|
||||||
(it) => it.id == messageId,
|
(it) => it.id == messageId,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -351,9 +357,13 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send a [message] to this channel.
|
/// Send a [message] to this channel.
|
||||||
|
/// If [skipPush] is true the message will not send a push notification
|
||||||
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
||||||
/// before actually sending the message.
|
/// before actually sending the message.
|
||||||
Future<SendMessageResponse> sendMessage(Message message) async {
|
Future<SendMessageResponse> sendMessage(
|
||||||
|
Message message, {
|
||||||
|
bool skipPush = false,
|
||||||
|
}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
// Cancelling previous completer in case it's called again in the process
|
// Cancelling previous completer in case it's called again in the process
|
||||||
// Eg. Updating the message while the previous call is in progress.
|
// Eg. Updating the message while the previous call is in progress.
|
||||||
@@ -386,7 +396,6 @@ class Channel {
|
|||||||
_messageAttachmentsUploadCompleter[message.id] =
|
_messageAttachmentsUploadCompleter[message.id] =
|
||||||
attachmentsUploadCompleter;
|
attachmentsUploadCompleter;
|
||||||
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
_uploadAttachments(
|
_uploadAttachments(
|
||||||
message.id,
|
message.id,
|
||||||
message.attachments.map((it) => it.id),
|
message.attachments.map((it) => it.id),
|
||||||
@@ -396,7 +405,12 @@ class Channel {
|
|||||||
message = await attachmentsUploadCompleter.future;
|
message = await attachmentsUploadCompleter.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
final response = await _client.sendMessage(message, id!, type);
|
final response = await _client.sendMessage(
|
||||||
|
message,
|
||||||
|
id!,
|
||||||
|
type,
|
||||||
|
skipPush: skipPush,
|
||||||
|
);
|
||||||
state!.addMessage(response.message);
|
state!.addMessage(response.message);
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -411,6 +425,8 @@ class Channel {
|
|||||||
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
||||||
/// before actually updating the message.
|
/// before actually updating the message.
|
||||||
Future<UpdateMessageResponse> updateMessage(Message message) async {
|
Future<UpdateMessageResponse> updateMessage(Message message) async {
|
||||||
|
final originalMessage = message;
|
||||||
|
|
||||||
// Cancelling previous completer in case it's called again in the process
|
// Cancelling previous completer in case it's called again in the process
|
||||||
// Eg. Updating the message while the previous call is in progress.
|
// Eg. Updating the message while the previous call is in progress.
|
||||||
_messageAttachmentsUploadCompleter
|
_messageAttachmentsUploadCompleter
|
||||||
@@ -432,12 +448,11 @@ class Channel {
|
|||||||
state?.addMessage(message);
|
state?.addMessage(message);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (message.attachments.any((it) => !it.uploadState.isSuccess) == true) {
|
if (message.attachments.any((it) => !it.uploadState.isSuccess)) {
|
||||||
final attachmentsUploadCompleter = Completer<Message>();
|
final attachmentsUploadCompleter = Completer<Message>();
|
||||||
_messageAttachmentsUploadCompleter[message.id] =
|
_messageAttachmentsUploadCompleter[message.id] =
|
||||||
attachmentsUploadCompleter;
|
attachmentsUploadCompleter;
|
||||||
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
_uploadAttachments(
|
_uploadAttachments(
|
||||||
message.id,
|
message.id,
|
||||||
message.attachments.map((it) => it.id),
|
message.attachments.map((it) => it.id),
|
||||||
@@ -455,6 +470,40 @@ class Channel {
|
|||||||
|
|
||||||
state?.addMessage(m);
|
state?.addMessage(m);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
if (e is StreamChatNetworkError) {
|
||||||
|
if (e.isRetriable) {
|
||||||
|
state!._retryQueue.add([message]);
|
||||||
|
} else {
|
||||||
|
state?.addMessage(originalMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Partially updates the [message] in this channel.
|
||||||
|
/// Use [set] to define values to be set
|
||||||
|
/// Use [unset] to define values to be unset
|
||||||
|
Future<UpdateMessageResponse> partialUpdateMessage(
|
||||||
|
Message message, {
|
||||||
|
Map<String, Object?>? set,
|
||||||
|
List<String>? unset,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final response = await _client.partialUpdateMessage(
|
||||||
|
message.id,
|
||||||
|
set: set,
|
||||||
|
unset: unset,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedMessage = response.message.copyWith(
|
||||||
|
ownReactions: message.ownReactions,
|
||||||
|
);
|
||||||
|
|
||||||
|
state?.addMessage(updatedMessage);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e is StreamChatNetworkError && e.isRetriable) {
|
if (e is StreamChatNetworkError && e.isRetriable) {
|
||||||
@@ -527,17 +576,23 @@ class Channel {
|
|||||||
Duration(seconds: timeoutOrExpirationDate.toInt()),
|
Duration(seconds: timeoutOrExpirationDate.toInt()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return updateMessage(
|
return partialUpdateMessage(
|
||||||
message.copyWith(
|
message,
|
||||||
pinned: true,
|
set: {
|
||||||
pinExpires: pinExpires,
|
'pinned': true,
|
||||||
),
|
'pin_expires': pinExpires?.toUtc().toIso8601String(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unpins provided message
|
/// Unpins provided message
|
||||||
Future<UpdateMessageResponse> unpinMessage(Message message) =>
|
Future<UpdateMessageResponse> unpinMessage(Message message) =>
|
||||||
updateMessage(message.copyWith(pinned: false));
|
partialUpdateMessage(
|
||||||
|
message,
|
||||||
|
set: {
|
||||||
|
'pinned': false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/// Send a file to this channel
|
/// Send a file to this channel
|
||||||
Future<SendFileResponse> sendFile(
|
Future<SendFileResponse> sendFile(
|
||||||
@@ -747,11 +802,12 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Edit the channel custom data
|
/// Edit the channel custom data
|
||||||
Future<PartialUpdateChannelResponse> updatePartial(
|
Future<PartialUpdateChannelResponse> updatePartial({
|
||||||
Map<String, dynamic> channelData,
|
Map<String, Object?>? set,
|
||||||
) async {
|
List<String>? unset,
|
||||||
|
}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
return _client.updateChannelPartial(id!, type, channelData);
|
return _client.updateChannelPartial(id!, type, set: set, unset: unset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete this channel. Messages are permanently removed.
|
/// Delete this channel. Messages are permanently removed.
|
||||||
@@ -856,9 +912,9 @@ class Channel {
|
|||||||
/// particular message as read
|
/// particular message as read
|
||||||
Future<EmptyResponse> markRead({String? messageId}) async {
|
Future<EmptyResponse> markRead({String? messageId}) async {
|
||||||
_checkInitialized();
|
_checkInitialized();
|
||||||
client.state.totalUnreadCount = max(
|
client.state.totalUnreadCount =
|
||||||
0, (client.state.totalUnreadCount ?? 0) - (state!.unreadCount ?? 0));
|
max(0, (client.state.totalUnreadCount) - (state!.unreadCount));
|
||||||
state!._unreadCountController.add(0);
|
state!.unreadCount = 0;
|
||||||
return _client.markChannelRead(id!, type, messageId: messageId);
|
return _client.markChannelRead(id!, type, messageId: messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1252,7 +1308,7 @@ class ChannelClientState {
|
|||||||
(r) => r.user.id == _channel._client.state.user?.id,
|
(r) => r.user.id == _channel._client.state.user?.id,
|
||||||
);
|
);
|
||||||
if (userRead != null) {
|
if (userRead != null) {
|
||||||
_unreadCountController.add(userRead.unreadMessages);
|
unreadCount = userRead.unreadMessages;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1432,7 +1488,7 @@ class ChannelClientState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (_countMessageAsUnread(message)) {
|
if (_countMessageAsUnread(message)) {
|
||||||
_unreadCountController.add(_unreadCountController.value + 1);
|
unreadCount += 1;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -1488,7 +1544,7 @@ class ChannelClientState {
|
|||||||
if (userReadIndex != null && userReadIndex != -1) {
|
if (userReadIndex != null && userReadIndex != -1) {
|
||||||
final userRead = readList.removeAt(userReadIndex);
|
final userRead = readList.removeAt(userReadIndex);
|
||||||
if (userRead.user.id == _channel._client.state.user!.id) {
|
if (userRead.user.id == _channel._client.state.user!.id) {
|
||||||
_unreadCountController.add(0);
|
unreadCount = 0;
|
||||||
}
|
}
|
||||||
readList.add(Read(
|
readList.add(Read(
|
||||||
user: event.user!,
|
user: event.user!,
|
||||||
@@ -1508,7 +1564,7 @@ class ChannelClientState {
|
|||||||
/// Channel message list as a stream
|
/// Channel message list as a stream
|
||||||
Stream<List<Message>?> get messagesStream => channelStateStream
|
Stream<List<Message>?> get messagesStream => channelStateStream
|
||||||
.map((cs) => cs.messages)
|
.map((cs) => cs.messages)
|
||||||
.distinct((prev, next) => const ListEquality().equals(prev, next));
|
.distinct(const ListEquality().equals);
|
||||||
|
|
||||||
/// Channel pinned message list
|
/// Channel pinned message list
|
||||||
List<Message>? get pinnedMessages => _channelState.pinnedMessages.toList();
|
List<Message>? get pinnedMessages => _channelState.pinnedMessages.toList();
|
||||||
@@ -1538,7 +1594,7 @@ class ChannelClientState {
|
|||||||
_channel.client.state.usersStream,
|
_channel.client.state.usersStream,
|
||||||
(members, users) =>
|
(members, users) =>
|
||||||
members!.map((e) => e!.copyWith(user: users[e.user!.id])).toList(),
|
members!.map((e) => e!.copyWith(user: users[e.user!.id])).toList(),
|
||||||
);
|
).distinct(const ListEquality().equals);
|
||||||
|
|
||||||
/// Channel watcher count
|
/// Channel watcher count
|
||||||
int? get watcherCount => _channelState.watcherCount;
|
int? get watcherCount => _channelState.watcherCount;
|
||||||
@@ -1568,11 +1624,13 @@ class ChannelClientState {
|
|||||||
|
|
||||||
final BehaviorSubject<int> _unreadCountController = BehaviorSubject.seeded(0);
|
final BehaviorSubject<int> _unreadCountController = BehaviorSubject.seeded(0);
|
||||||
|
|
||||||
|
set unreadCount(int value) => _unreadCountController.add(value);
|
||||||
|
|
||||||
/// Unread count getter as a stream
|
/// Unread count getter as a stream
|
||||||
Stream<int> get unreadCountStream => _unreadCountController.stream;
|
Stream<int> get unreadCountStream => _unreadCountController.stream.distinct();
|
||||||
|
|
||||||
/// Unread count getter
|
/// Unread count getter
|
||||||
int? get unreadCount => _unreadCountController.value;
|
int get unreadCount => _unreadCountController.value;
|
||||||
|
|
||||||
bool _countMessageAsUnread(Message message) {
|
bool _countMessageAsUnread(Message message) {
|
||||||
final userId = _channel.client.state.user?.id;
|
final userId = _channel.client.state.user?.id;
|
||||||
@@ -1708,7 +1766,9 @@ class ChannelClientState {
|
|||||||
List<User> get typingEvents => _typingEventsController.value;
|
List<User> get typingEvents => _typingEventsController.value;
|
||||||
|
|
||||||
/// Channel related typing users stream
|
/// Channel related typing users stream
|
||||||
Stream<List<User>> get typingEventsStream => _typingEventsController.stream;
|
Stream<List<User>> get typingEventsStream =>
|
||||||
|
_typingEventsController.stream.distinct(const ListEquality().equals);
|
||||||
|
|
||||||
final BehaviorSubject<List<User>> _typingEventsController =
|
final BehaviorSubject<List<User>> _typingEventsController =
|
||||||
BehaviorSubject.seeded([]);
|
BehaviorSubject.seeded([]);
|
||||||
|
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ class StreamChatClient {
|
|||||||
_wsConnectionStatusController.add(status);
|
_wsConnectionStatusController.add(status);
|
||||||
|
|
||||||
/// The current status value of the websocket connection
|
/// The current status value of the websocket connection
|
||||||
ConnectionStatus? get wsConnectionStatus =>
|
ConnectionStatus get wsConnectionStatus =>
|
||||||
_wsConnectionStatusController.value;
|
_wsConnectionStatusController.value;
|
||||||
|
|
||||||
/// This notifies the connection status of the websocket connection.
|
/// This notifies the connection status of the websocket connection.
|
||||||
@@ -745,13 +745,15 @@ class StreamChatClient {
|
|||||||
/// Updates the [channelId] of type [ChannelType] data with [data]
|
/// Updates the [channelId] of type [ChannelType] data with [data]
|
||||||
Future<PartialUpdateChannelResponse> updateChannelPartial(
|
Future<PartialUpdateChannelResponse> updateChannelPartial(
|
||||||
String channelId,
|
String channelId,
|
||||||
String channelType,
|
String channelType, {
|
||||||
Map<String, dynamic> data,
|
Map<String, Object?>? set,
|
||||||
) =>
|
List<String>? unset,
|
||||||
|
}) =>
|
||||||
_chatApi.channel.updateChannelPartial(
|
_chatApi.channel.updateChannelPartial(
|
||||||
channelId,
|
channelId,
|
||||||
channelType,
|
channelType,
|
||||||
data,
|
set: set,
|
||||||
|
unset: unset,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Add a device for Push Notifications.
|
/// Add a device for Push Notifications.
|
||||||
@@ -1125,12 +1127,14 @@ class StreamChatClient {
|
|||||||
Future<SendMessageResponse> sendMessage(
|
Future<SendMessageResponse> sendMessage(
|
||||||
Message message,
|
Message message,
|
||||||
String channelId,
|
String channelId,
|
||||||
String channelType,
|
String channelType, {
|
||||||
) =>
|
bool skipPush = false,
|
||||||
|
}) =>
|
||||||
_chatApi.message.sendMessage(
|
_chatApi.message.sendMessage(
|
||||||
channelId,
|
channelId,
|
||||||
channelType,
|
channelType,
|
||||||
message,
|
message,
|
||||||
|
skipPush: skipPush,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Lists all the message replies for the [parentId]
|
/// Lists all the message replies for the [parentId]
|
||||||
@@ -1157,6 +1161,20 @@ class StreamChatClient {
|
|||||||
Future<UpdateMessageResponse> updateMessage(Message message) =>
|
Future<UpdateMessageResponse> updateMessage(Message message) =>
|
||||||
_chatApi.message.updateMessage(message);
|
_chatApi.message.updateMessage(message);
|
||||||
|
|
||||||
|
/// Partially update the given [messageId]
|
||||||
|
/// Use [set] to define values to be set
|
||||||
|
/// Use [unset] to define values to be unset
|
||||||
|
Future<UpdateMessageResponse> partialUpdateMessage(
|
||||||
|
String messageId, {
|
||||||
|
Map<String, Object?>? set,
|
||||||
|
List<String>? unset,
|
||||||
|
}) =>
|
||||||
|
_chatApi.message.partialUpdateMessage(
|
||||||
|
messageId,
|
||||||
|
set: set,
|
||||||
|
unset: unset,
|
||||||
|
);
|
||||||
|
|
||||||
/// Deletes the given message
|
/// Deletes the given message
|
||||||
Future<EmptyResponse> deleteMessage(String messageId) =>
|
Future<EmptyResponse> deleteMessage(String messageId) =>
|
||||||
_chatApi.message.deleteMessage(messageId);
|
_chatApi.message.deleteMessage(messageId);
|
||||||
@@ -1192,7 +1210,7 @@ class StreamChatClient {
|
|||||||
/// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds
|
/// [timeoutOrExpirationDate] can either be a [DateTime] or a value in seconds
|
||||||
/// to be added to [DateTime.now]
|
/// to be added to [DateTime.now]
|
||||||
Future<UpdateMessageResponse> pinMessage(
|
Future<UpdateMessageResponse> pinMessage(
|
||||||
Message message, {
|
String messageId, {
|
||||||
Object? /*num|DateTime*/ timeoutOrExpirationDate,
|
Object? /*num|DateTime*/ timeoutOrExpirationDate,
|
||||||
}) {
|
}) {
|
||||||
assert(() {
|
assert(() {
|
||||||
@@ -1212,17 +1230,23 @@ class StreamChatClient {
|
|||||||
Duration(seconds: timeoutOrExpirationDate.toInt()),
|
Duration(seconds: timeoutOrExpirationDate.toInt()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return updateMessage(
|
return partialUpdateMessage(
|
||||||
message.copyWith(
|
messageId,
|
||||||
pinned: true,
|
set: {
|
||||||
pinExpires: pinExpires,
|
'pinned': true,
|
||||||
),
|
'pin_expires': pinExpires?.toUtc().toIso8601String(),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Unpins provided message
|
/// Unpins provided message
|
||||||
Future<UpdateMessageResponse> unpinMessage(Message message) =>
|
Future<UpdateMessageResponse> unpinMessage(String messageId) =>
|
||||||
updateMessage(message.copyWith(pinned: false));
|
partialUpdateMessage(
|
||||||
|
messageId,
|
||||||
|
set: {
|
||||||
|
'pinned': false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/// Closes the [_ws] connection and resets the [state]
|
/// Closes the [_ws] connection and resets the [state]
|
||||||
/// If [flushChatPersistence] is true the client deletes all offline
|
/// If [flushChatPersistence] is true the client deletes all offline
|
||||||
@@ -1275,23 +1299,25 @@ class ClientState {
|
|||||||
.map((e) => e.me)
|
.map((e) => e.me)
|
||||||
.listen((user) {
|
.listen((user) {
|
||||||
_userController.add(user);
|
_userController.add(user);
|
||||||
if (user?.totalUnreadCount != null) {
|
final totalUnreadCount = user?.totalUnreadCount;
|
||||||
_totalUnreadCountController.add(user?.totalUnreadCount);
|
if (totalUnreadCount != null) {
|
||||||
|
_totalUnreadCountController.add(totalUnreadCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user?.unreadChannels != null) {
|
final unreadChannels = user?.unreadChannels;
|
||||||
_unreadChannelsController.add(user?.unreadChannels);
|
if (unreadChannels != null) {
|
||||||
|
_unreadChannelsController.add(unreadChannels);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
_client
|
_client
|
||||||
.on()
|
.on()
|
||||||
.where((event) => event.unreadChannels != null)
|
.map((event) => event.unreadChannels)
|
||||||
.map((e) => e.unreadChannels)
|
.whereType<int>()
|
||||||
.listen(_unreadChannelsController.add),
|
.listen(_unreadChannelsController.add),
|
||||||
_client
|
_client
|
||||||
.on()
|
.on()
|
||||||
.where((event) => event.totalUnreadCount != null)
|
.map((event) => event.totalUnreadCount)
|
||||||
.map((e) => e.totalUnreadCount)
|
.whereType<int>()
|
||||||
.listen(_totalUnreadCountController.add),
|
.listen(_totalUnreadCountController.add),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -1305,8 +1331,8 @@ class ClientState {
|
|||||||
final _subscriptions = <StreamSubscription>[];
|
final _subscriptions = <StreamSubscription>[];
|
||||||
|
|
||||||
/// Used internally for optimistic update of unread count
|
/// Used internally for optimistic update of unread count
|
||||||
set totalUnreadCount(int? unreadCount) {
|
set totalUnreadCount(int unreadCount) {
|
||||||
_totalUnreadCountController.add(unreadCount ?? 0);
|
_totalUnreadCountController.add(unreadCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _listenChannelHidden() {
|
void _listenChannelHidden() {
|
||||||
@@ -1374,16 +1400,16 @@ class ClientState {
|
|||||||
Stream<Map<String, User>> get usersStream => _usersController.stream;
|
Stream<Map<String, User>> get usersStream => _usersController.stream;
|
||||||
|
|
||||||
/// The current unread channels count
|
/// The current unread channels count
|
||||||
int? get unreadChannels => _unreadChannelsController.valueOrNull;
|
int get unreadChannels => _unreadChannelsController.value;
|
||||||
|
|
||||||
/// The current unread channels count as a stream
|
/// The current unread channels count as a stream
|
||||||
Stream<int?> get unreadChannelsStream => _unreadChannelsController.stream;
|
Stream<int> get unreadChannelsStream => _unreadChannelsController.stream;
|
||||||
|
|
||||||
/// The current total unread messages count
|
/// The current total unread messages count
|
||||||
int? get totalUnreadCount => _totalUnreadCountController.valueOrNull;
|
int get totalUnreadCount => _totalUnreadCountController.value;
|
||||||
|
|
||||||
/// The current total unread messages count as a stream
|
/// The current total unread messages count as a stream
|
||||||
Stream<int?> get totalUnreadCountStream => _totalUnreadCountController.stream;
|
Stream<int> get totalUnreadCountStream => _totalUnreadCountController.stream;
|
||||||
|
|
||||||
/// The current list of channels in memory as a stream
|
/// The current list of channels in memory as a stream
|
||||||
Stream<Map<String, Channel>> get channelsStream => _channelsController.stream;
|
Stream<Map<String, Channel>> get channelsStream => _channelsController.stream;
|
||||||
@@ -1399,8 +1425,8 @@ class ClientState {
|
|||||||
final _channelsController = BehaviorSubject<Map<String, Channel>>.seeded({});
|
final _channelsController = BehaviorSubject<Map<String, Channel>>.seeded({});
|
||||||
final _userController = BehaviorSubject<OwnUser?>();
|
final _userController = BehaviorSubject<OwnUser?>();
|
||||||
final _usersController = BehaviorSubject<Map<String, User>>.seeded({});
|
final _usersController = BehaviorSubject<Map<String, User>>.seeded({});
|
||||||
final _unreadChannelsController = BehaviorSubject<int?>();
|
final _unreadChannelsController = BehaviorSubject<int>.seeded(0);
|
||||||
final _totalUnreadCountController = BehaviorSubject<int?>();
|
final _totalUnreadCountController = BehaviorSubject<int>.seeded(0);
|
||||||
|
|
||||||
/// Call this method to dispose this object
|
/// Call this method to dispose this object
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
|||||||
@@ -109,12 +109,16 @@ class ChannelApi {
|
|||||||
/// Updates the [channelId] of type [ChannelType] data with [data]
|
/// Updates the [channelId] of type [ChannelType] data with [data]
|
||||||
Future<PartialUpdateChannelResponse> updateChannelPartial(
|
Future<PartialUpdateChannelResponse> updateChannelPartial(
|
||||||
String channelId,
|
String channelId,
|
||||||
String channelType,
|
String channelType, {
|
||||||
Map<String, dynamic> data,
|
Map<String, Object?>? set,
|
||||||
) async {
|
List<String>? unset,
|
||||||
|
}) async {
|
||||||
final response = await _client.patch(
|
final response = await _client.patch(
|
||||||
_getChannelUrl(channelId, channelType),
|
_getChannelUrl(channelId, channelType),
|
||||||
data: data,
|
data: {
|
||||||
|
if (set != null) 'set': set,
|
||||||
|
if (unset != null) 'unset': unset,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return PartialUpdateChannelResponse.fromJson(response.data);
|
return PartialUpdateChannelResponse.fromJson(response.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,15 @@ class MessageApi {
|
|||||||
Future<SendMessageResponse> sendMessage(
|
Future<SendMessageResponse> sendMessage(
|
||||||
String channelId,
|
String channelId,
|
||||||
String channelType,
|
String channelType,
|
||||||
Message message,
|
Message message, {
|
||||||
) async {
|
bool skipPush = false,
|
||||||
|
}) async {
|
||||||
final response = await _client.post(
|
final response = await _client.post(
|
||||||
'/channels/$channelType/$channelId/message',
|
'/channels/$channelType/$channelId/message',
|
||||||
data: {'message': message},
|
data: {
|
||||||
|
'message': message,
|
||||||
|
'skip_push': skipPush,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
return SendMessageResponse.fromJson(response.data);
|
return SendMessageResponse.fromJson(response.data);
|
||||||
}
|
}
|
||||||
@@ -56,6 +60,24 @@ class MessageApi {
|
|||||||
return UpdateMessageResponse.fromJson(response.data);
|
return UpdateMessageResponse.fromJson(response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Partially update the given [messageId]
|
||||||
|
/// Use [set] to define values to be set
|
||||||
|
/// Use [unset] to define values to be unset
|
||||||
|
Future<UpdateMessageResponse> partialUpdateMessage(
|
||||||
|
String messageId, {
|
||||||
|
Map<String, Object?>? set,
|
||||||
|
List<String>? unset,
|
||||||
|
}) async {
|
||||||
|
final response = await _client.put(
|
||||||
|
'/messages/$messageId',
|
||||||
|
data: {
|
||||||
|
if (set != null) 'set': set,
|
||||||
|
if (unset != null) 'unset': unset,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return UpdateMessageResponse.fromJson(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
/// Deletes the given [messageId]
|
/// Deletes the given [messageId]
|
||||||
Future<EmptyResponse> deleteMessage(
|
Future<EmptyResponse> deleteMessage(
|
||||||
String messageId,
|
String messageId,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:stream_chat/src/core/models/user.dart';
|
import 'package:stream_chat/src/core/models/user.dart';
|
||||||
|
|
||||||
@@ -6,7 +7,7 @@ part 'member.g.dart';
|
|||||||
/// The class that contains the information about the user membership
|
/// The class that contains the information about the user membership
|
||||||
/// in a channel
|
/// in a channel
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Member {
|
class Member extends Equatable {
|
||||||
/// Constructor used for json serialization
|
/// Constructor used for json serialization
|
||||||
Member({
|
Member({
|
||||||
this.user,
|
this.user,
|
||||||
@@ -98,4 +99,19 @@ class Member {
|
|||||||
|
|
||||||
/// Serialize to json
|
/// Serialize to json
|
||||||
Map<String, dynamic> toJson() => _$MemberToJson(this);
|
Map<String, dynamic> toJson() => _$MemberToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
user,
|
||||||
|
inviteAcceptedAt,
|
||||||
|
inviteRejectedAt,
|
||||||
|
invited,
|
||||||
|
role,
|
||||||
|
userId,
|
||||||
|
isModerator,
|
||||||
|
banned,
|
||||||
|
shadowBanned,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ class Message extends Equatable {
|
|||||||
this.extraData = const {},
|
this.extraData = const {},
|
||||||
this.deletedAt,
|
this.deletedAt,
|
||||||
this.status = MessageSendingStatus.sent,
|
this.status = MessageSendingStatus.sent,
|
||||||
this.skipPush = false,
|
|
||||||
}) : id = id ?? const Uuid().v4(),
|
}) : id = id ?? const Uuid().v4(),
|
||||||
pinExpires = pinExpires?.toUtc(),
|
pinExpires = pinExpires?.toUtc(),
|
||||||
createdAt = createdAt ?? DateTime.now(),
|
createdAt = createdAt ?? DateTime.now(),
|
||||||
@@ -158,10 +157,6 @@ class Message extends Equatable {
|
|||||||
@JsonKey(defaultValue: false)
|
@JsonKey(defaultValue: false)
|
||||||
final bool silent;
|
final bool silent;
|
||||||
|
|
||||||
/// If true the message will not send a push notification
|
|
||||||
@JsonKey(defaultValue: false)
|
|
||||||
final bool skipPush;
|
|
||||||
|
|
||||||
/// If true the message is shadowed
|
/// If true the message is shadowed
|
||||||
@JsonKey(
|
@JsonKey(
|
||||||
includeIfNull: false,
|
includeIfNull: false,
|
||||||
@@ -253,7 +248,6 @@ class Message extends Equatable {
|
|||||||
'pinned_at',
|
'pinned_at',
|
||||||
'pin_expires',
|
'pin_expires',
|
||||||
'pinned_by',
|
'pinned_by',
|
||||||
'skip_push',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Serialize to json
|
/// Serialize to json
|
||||||
@@ -291,7 +285,6 @@ class Message extends Equatable {
|
|||||||
User? pinnedBy,
|
User? pinnedBy,
|
||||||
Map<String, Object?>? extraData,
|
Map<String, Object?>? extraData,
|
||||||
MessageSendingStatus? status,
|
MessageSendingStatus? status,
|
||||||
bool? skipPush,
|
|
||||||
}) {
|
}) {
|
||||||
assert(() {
|
assert(() {
|
||||||
if (pinExpires is! DateTime &&
|
if (pinExpires is! DateTime &&
|
||||||
@@ -331,7 +324,6 @@ class Message extends Equatable {
|
|||||||
pinnedBy: pinnedBy ?? this.pinnedBy,
|
pinnedBy: pinnedBy ?? this.pinnedBy,
|
||||||
pinExpires:
|
pinExpires:
|
||||||
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
|
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
|
||||||
skipPush: skipPush ?? this.skipPush,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +390,6 @@ class Message extends Equatable {
|
|||||||
pinnedBy,
|
pinnedBy,
|
||||||
extraData,
|
extraData,
|
||||||
status,
|
status,
|
||||||
skipPush,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ Message _$MessageFromJson(Map<String, dynamic> json) {
|
|||||||
deletedAt: json['deleted_at'] == null
|
deletedAt: json['deleted_at'] == null
|
||||||
? null
|
? null
|
||||||
: DateTime.parse(json['deleted_at'] as String),
|
: DateTime.parse(json['deleted_at'] as String),
|
||||||
skipPush: json['skip_push'] as bool? ?? false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +96,6 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
|
|||||||
writeNotNull('thread_participants', readonly(instance.threadParticipants));
|
writeNotNull('thread_participants', readonly(instance.threadParticipants));
|
||||||
val['show_in_channel'] = instance.showInChannel;
|
val['show_in_channel'] = instance.showInChannel;
|
||||||
val['silent'] = instance.silent;
|
val['silent'] = instance.silent;
|
||||||
val['skip_push'] = instance.skipPush;
|
|
||||||
writeNotNull('shadowed', readonly(instance.shadowed));
|
writeNotNull('shadowed', readonly(instance.shadowed));
|
||||||
writeNotNull('command', readonly(instance.command));
|
writeNotNull('command', readonly(instance.command));
|
||||||
writeNotNull('created_at', readonly(instance.createdAt));
|
writeNotNull('created_at', readonly(instance.createdAt));
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||||
|
|
||||||
@@ -5,7 +6,7 @@ part 'user.g.dart';
|
|||||||
|
|
||||||
/// The class that defines the user model
|
/// The class that defines the user model
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class User {
|
class User extends Equatable {
|
||||||
/// Constructor used for json serialization
|
/// Constructor used for json serialization
|
||||||
User({
|
User({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -129,4 +130,17 @@ class User {
|
|||||||
banned: banned ?? this.banned,
|
banned: banned ?? this.banned,
|
||||||
teams: teams ?? this.teams,
|
teams: teams ?? this.teams,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
role,
|
||||||
|
teams,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
lastActive,
|
||||||
|
online,
|
||||||
|
banned,
|
||||||
|
extraData,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
|
|||||||
/// Current package version
|
/// Current package version
|
||||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||||
// ignore: constant_identifier_names
|
// ignore: constant_identifier_names
|
||||||
const PACKAGE_VERSION = '2.0.0-nullsafety.2';
|
const PACKAGE_VERSION = '2.0.0-nullsafety.5';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat
|
name: stream_chat
|
||||||
homepage: https://getstream.io/
|
homepage: https://getstream.io/
|
||||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
description: The official Dart client for Stream Chat, a service for building chat applications.
|
||||||
version: 2.0.0-nullsafety.2
|
version: 2.0.0-nullsafety.5
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -30,4 +30,4 @@ dev_dependencies:
|
|||||||
freezed: ^0.14.1+3
|
freezed: ^0.14.1+3
|
||||||
json_serializable: ^4.1.0
|
json_serializable: ^4.1.0
|
||||||
mocktail: ^0.1.1
|
mocktail: ^0.1.1
|
||||||
test: ^1.16.8
|
test: ^1.17.7
|
||||||
@@ -425,6 +425,54 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('`.partialUpdateMessage`', () async {
|
||||||
|
final message = Message(id: 'test-message-id');
|
||||||
|
|
||||||
|
const set = {'text': 'Update Message text'};
|
||||||
|
const unset = ['pinExpires'];
|
||||||
|
|
||||||
|
final updateMessageResponse = UpdateMessageResponse()
|
||||||
|
..message = message.copyWith(text: set['text'], pinExpires: null);
|
||||||
|
|
||||||
|
when(
|
||||||
|
() => client.partialUpdateMessage(message.id, set: set, unset: unset),
|
||||||
|
).thenAnswer((_) async => updateMessageResponse);
|
||||||
|
|
||||||
|
channel.state?.messagesStream.skip(1).listen(print);
|
||||||
|
|
||||||
|
expectLater(
|
||||||
|
// skipping first seed message list -> [] messages
|
||||||
|
channel.state?.messagesStream.skip(1),
|
||||||
|
emitsInOrder([
|
||||||
|
[
|
||||||
|
isSameMessageAs(
|
||||||
|
updateMessageResponse.message.copyWith(
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
),
|
||||||
|
matchText: true,
|
||||||
|
matchSendingStatus: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
final res = await channel.partialUpdateMessage(
|
||||||
|
message,
|
||||||
|
set: set,
|
||||||
|
unset: unset,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res, isNotNull);
|
||||||
|
expect(res.message.id, message.id);
|
||||||
|
expect(res.message.id, message.id);
|
||||||
|
expect(res.message.text, set['text']);
|
||||||
|
expect(res.message.pinExpires, isNull);
|
||||||
|
|
||||||
|
verify(
|
||||||
|
() => client.partialUpdateMessage(message.id, set: set, unset: unset),
|
||||||
|
).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
group('`.deleteMessage`', () {
|
group('`.deleteMessage`', () {
|
||||||
test('should work fine', () async {
|
test('should work fine', () async {
|
||||||
const messageId = 'test-message-id';
|
const messageId = 'test-message-id';
|
||||||
@@ -493,21 +541,21 @@ void main() {
|
|||||||
() async {
|
() async {
|
||||||
final message = Message(id: 'test-message-id');
|
final message = Message(id: 'test-message-id');
|
||||||
|
|
||||||
when(() => client.updateMessage(any(that: isSameMessageAs(message))))
|
when(() => client.partialUpdateMessage(
|
||||||
.thenAnswer((invocation) async => UpdateMessageResponse()
|
message.id,
|
||||||
..message = (invocation.positionalArguments.first as Message)
|
set: any(named: 'set'),
|
||||||
.copyWith(status: MessageSendingStatus.sent));
|
unset: any(named: 'unset'),
|
||||||
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
||||||
|
..message = message.copyWith(
|
||||||
|
pinned: true,
|
||||||
|
pinExpires: null,
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
));
|
||||||
|
|
||||||
expectLater(
|
expectLater(
|
||||||
// skipping first seed message list -> [] messages
|
// skipping first seed message list -> [] messages
|
||||||
channel.state?.messagesStream.skip(1),
|
channel.state?.messagesStream.skip(1),
|
||||||
emitsInOrder([
|
emitsInOrder([
|
||||||
[
|
|
||||||
isSameMessageAs(
|
|
||||||
message.copyWith(status: MessageSendingStatus.updating),
|
|
||||||
matchSendingStatus: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
isSameMessageAs(
|
isSameMessageAs(
|
||||||
message.copyWith(status: MessageSendingStatus.sent),
|
message.copyWith(status: MessageSendingStatus.sent),
|
||||||
@@ -523,8 +571,11 @@ void main() {
|
|||||||
expect(res.message.pinned, isTrue);
|
expect(res.message.pinned, isTrue);
|
||||||
expect(res.message.pinExpires, isNull);
|
expect(res.message.pinExpires, isNull);
|
||||||
|
|
||||||
verify(() => client.updateMessage(any(that: isSameMessageAs(message))))
|
verify(() => client.partialUpdateMessage(
|
||||||
.called(1);
|
message.id,
|
||||||
|
set: any(named: 'set'),
|
||||||
|
unset: any(named: 'unset'),
|
||||||
|
)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test(
|
test(
|
||||||
@@ -533,21 +584,23 @@ void main() {
|
|||||||
final message = Message(id: 'test-message-id');
|
final message = Message(id: 'test-message-id');
|
||||||
const timeoutOrExpirationDate = 300; // 300 seconds
|
const timeoutOrExpirationDate = 300; // 300 seconds
|
||||||
|
|
||||||
when(() => client.updateMessage(any(that: isSameMessageAs(message))))
|
when(() => client.partialUpdateMessage(
|
||||||
.thenAnswer((invocation) async => UpdateMessageResponse()
|
message.id,
|
||||||
..message = (invocation.positionalArguments.first as Message)
|
set: any(named: 'set'),
|
||||||
.copyWith(status: MessageSendingStatus.sent));
|
unset: any(named: 'unset'),
|
||||||
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
||||||
|
..message = message.copyWith(
|
||||||
|
pinned: true,
|
||||||
|
pinExpires: DateTime.now().add(
|
||||||
|
const Duration(seconds: timeoutOrExpirationDate),
|
||||||
|
),
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
));
|
||||||
|
|
||||||
expectLater(
|
expectLater(
|
||||||
// skipping first seed message list -> [] messages
|
// skipping first seed message list -> [] messages
|
||||||
channel.state?.messagesStream.skip(1),
|
channel.state?.messagesStream.skip(1),
|
||||||
emitsInOrder([
|
emitsInOrder([
|
||||||
[
|
|
||||||
isSameMessageAs(
|
|
||||||
message.copyWith(status: MessageSendingStatus.updating),
|
|
||||||
matchSendingStatus: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
isSameMessageAs(
|
isSameMessageAs(
|
||||||
message.copyWith(status: MessageSendingStatus.sent),
|
message.copyWith(status: MessageSendingStatus.sent),
|
||||||
@@ -566,9 +619,11 @@ void main() {
|
|||||||
expect(res.message.pinned, isTrue);
|
expect(res.message.pinned, isTrue);
|
||||||
expect(res.message.pinExpires, isNotNull);
|
expect(res.message.pinExpires, isNotNull);
|
||||||
|
|
||||||
verify(() =>
|
verify(() => client.partialUpdateMessage(
|
||||||
client.updateMessage(any(that: isSameMessageAs(message))))
|
message.id,
|
||||||
.called(1);
|
set: any(named: 'set'),
|
||||||
|
unset: any(named: 'unset'),
|
||||||
|
)).called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -579,21 +634,21 @@ void main() {
|
|||||||
final timeoutOrExpirationDate =
|
final timeoutOrExpirationDate =
|
||||||
DateTime.now().add(const Duration(days: 3)); // 3 days
|
DateTime.now().add(const Duration(days: 3)); // 3 days
|
||||||
|
|
||||||
when(() => client.updateMessage(any(that: isSameMessageAs(message))))
|
when(() => client.partialUpdateMessage(
|
||||||
.thenAnswer((invocation) async => UpdateMessageResponse()
|
message.id,
|
||||||
..message = (invocation.positionalArguments.first as Message)
|
set: any(named: 'set'),
|
||||||
.copyWith(status: MessageSendingStatus.sent));
|
unset: any(named: 'unset'),
|
||||||
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
||||||
|
..message = message.copyWith(
|
||||||
|
pinned: true,
|
||||||
|
pinExpires: timeoutOrExpirationDate,
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
));
|
||||||
|
|
||||||
expectLater(
|
expectLater(
|
||||||
// skipping first seed message list -> [] messages
|
// skipping first seed message list -> [] messages
|
||||||
channel.state?.messagesStream.skip(1),
|
channel.state?.messagesStream.skip(1),
|
||||||
emitsInOrder([
|
emitsInOrder([
|
||||||
[
|
|
||||||
isSameMessageAs(
|
|
||||||
message.copyWith(status: MessageSendingStatus.updating),
|
|
||||||
matchSendingStatus: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
isSameMessageAs(
|
isSameMessageAs(
|
||||||
message.copyWith(status: MessageSendingStatus.sent),
|
message.copyWith(status: MessageSendingStatus.sent),
|
||||||
@@ -613,9 +668,11 @@ void main() {
|
|||||||
expect(res.message.pinExpires, isNotNull);
|
expect(res.message.pinExpires, isNotNull);
|
||||||
expect(res.message.pinExpires, timeoutOrExpirationDate.toUtc());
|
expect(res.message.pinExpires, timeoutOrExpirationDate.toUtc());
|
||||||
|
|
||||||
verify(
|
verify(() => client.partialUpdateMessage(
|
||||||
() => client.updateMessage(any(that: isSameMessageAs(message))),
|
message.id,
|
||||||
).called(1);
|
set: any(named: 'set'),
|
||||||
|
unset: any(named: 'unset'),
|
||||||
|
)).called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -640,21 +697,19 @@ void main() {
|
|||||||
test('`.unpinMessage`', () async {
|
test('`.unpinMessage`', () async {
|
||||||
final message = Message(id: 'test-message-id', pinned: true);
|
final message = Message(id: 'test-message-id', pinned: true);
|
||||||
|
|
||||||
when(() => client.updateMessage(any(that: isSameMessageAs(message))))
|
when(() => client.partialUpdateMessage(
|
||||||
.thenAnswer((invocation) async => UpdateMessageResponse()
|
message.id,
|
||||||
..message = (invocation.positionalArguments.first as Message)
|
set: {'pinned': false},
|
||||||
.copyWith(status: MessageSendingStatus.sent));
|
)).thenAnswer((_) async => UpdateMessageResponse()
|
||||||
|
..message = message.copyWith(
|
||||||
|
pinned: false,
|
||||||
|
status: MessageSendingStatus.sent,
|
||||||
|
));
|
||||||
|
|
||||||
expectLater(
|
expectLater(
|
||||||
// skipping first seed message list -> [] messages
|
// skipping first seed message list -> [] messages
|
||||||
channel.state?.messagesStream.skip(1),
|
channel.state?.messagesStream.skip(1),
|
||||||
emitsInOrder([
|
emitsInOrder([
|
||||||
[
|
|
||||||
isSameMessageAs(
|
|
||||||
message.copyWith(status: MessageSendingStatus.updating),
|
|
||||||
matchSendingStatus: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
isSameMessageAs(
|
isSameMessageAs(
|
||||||
message.copyWith(status: MessageSendingStatus.sent),
|
message.copyWith(status: MessageSendingStatus.sent),
|
||||||
@@ -669,44 +724,12 @@ void main() {
|
|||||||
expect(res, isNotNull);
|
expect(res, isNotNull);
|
||||||
expect(res.message.pinned, isFalse);
|
expect(res.message.pinned, isFalse);
|
||||||
|
|
||||||
verify(
|
verify(() => client.partialUpdateMessage(
|
||||||
() => client.updateMessage(any(that: isSameMessageAs(message))),
|
message.id,
|
||||||
).called(1);
|
set: {'pinned': false},
|
||||||
|
)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
//
|
|
||||||
// /// Send a file to this channel
|
|
||||||
// Future<SendFileResponse> sendFile(
|
|
||||||
// AttachmentFile file, {
|
|
||||||
// ProgressCallback? onSendProgress,
|
|
||||||
// CancelToken? cancelToken,
|
|
||||||
// }) {
|
|
||||||
// _checkInitialized();
|
|
||||||
// return _client.sendFile(
|
|
||||||
// file,
|
|
||||||
// id!,
|
|
||||||
// type,
|
|
||||||
// onSendProgress: onSendProgress,
|
|
||||||
// cancelToken: cancelToken,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// /// Send an image to this channel
|
|
||||||
// Future<SendImageResponse> sendImage(
|
|
||||||
// AttachmentFile file, {
|
|
||||||
// ProgressCallback? onSendProgress,
|
|
||||||
// CancelToken? cancelToken,
|
|
||||||
// }) {
|
|
||||||
// _checkInitialized();
|
|
||||||
// return _client.sendImage(
|
|
||||||
// file,
|
|
||||||
// id!,
|
|
||||||
// type,
|
|
||||||
// onSendProgress: onSendProgress,
|
|
||||||
// cancelToken: cancelToken,
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
group('`.search`', () {
|
group('`.search`', () {
|
||||||
final filter = Filter.in_('cid', const [channelCid]);
|
final filter = Filter.in_('cid', const [channelCid]);
|
||||||
|
|
||||||
@@ -1123,40 +1146,44 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('`.updatePartial`', () async {
|
test('`.updatePartial`', () async {
|
||||||
const channelData = {
|
const set = {
|
||||||
'name': 'Stream Team',
|
'name': 'Stream Team',
|
||||||
'profile_image': 'test-profile-image',
|
'profile_image': 'test-profile-image',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const unset = ['tag', 'last_name'];
|
||||||
|
|
||||||
final channelModel = ChannelModel(
|
final channelModel = ChannelModel(
|
||||||
cid: channelCid,
|
cid: channelCid,
|
||||||
extraData: {
|
extraData: {
|
||||||
'coolness': 999,
|
'coolness': 999,
|
||||||
...channelData,
|
...set,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => client.updateChannelPartial(
|
when(() => client.updateChannelPartial(
|
||||||
channelId,
|
channelId,
|
||||||
channelType,
|
channelType,
|
||||||
channelData,
|
set: set,
|
||||||
|
unset: unset,
|
||||||
)).thenAnswer(
|
)).thenAnswer(
|
||||||
(_) async => PartialUpdateChannelResponse()..channel = channelModel,
|
(_) async => PartialUpdateChannelResponse()..channel = channelModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
final res = await channel.updatePartial(channelData);
|
final res = await channel.updatePartial(set: set, unset: unset);
|
||||||
|
|
||||||
expect(res, isNotNull);
|
expect(res, isNotNull);
|
||||||
expect(res.channel.cid, channelModel.cid);
|
expect(res.channel.cid, channelModel.cid);
|
||||||
expect(
|
expect(
|
||||||
res.channel.extraData,
|
res.channel.extraData,
|
||||||
{'coolness': 999, ...channelData},
|
{'coolness': 999, ...set},
|
||||||
);
|
);
|
||||||
|
|
||||||
verify(() => client.updateChannelPartial(
|
verify(() => client.updateChannelPartial(
|
||||||
channelId,
|
channelId,
|
||||||
channelType,
|
channelType,
|
||||||
channelData,
|
set: set,
|
||||||
|
unset: unset,
|
||||||
)).called(1);
|
)).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1380,12 +1407,6 @@ void main() {
|
|||||||
when(() => client.markChannelRead(channelId, channelType,
|
when(() => client.markChannelRead(channelId, channelType,
|
||||||
messageId: messageId)).thenAnswer((_) async => EmptyResponse());
|
messageId: messageId)).thenAnswer((_) async => EmptyResponse());
|
||||||
|
|
||||||
expectLater(
|
|
||||||
// skipping first seed unread count -> 0 unread count
|
|
||||||
channel.state?.unreadCountStream.skip(1),
|
|
||||||
emitsInOrder([0]),
|
|
||||||
);
|
|
||||||
|
|
||||||
final res = await channel.markRead(messageId: messageId);
|
final res = await channel.markRead(messageId: messageId);
|
||||||
|
|
||||||
expect(res, isNotNull);
|
expect(res, isNotNull);
|
||||||
|
|||||||
@@ -190,30 +190,39 @@ void main() {
|
|||||||
test('updateChannelPartial', () async {
|
test('updateChannelPartial', () async {
|
||||||
const channelId = 'test-channel-id';
|
const channelId = 'test-channel-id';
|
||||||
const channelType = 'test-channel-type';
|
const channelType = 'test-channel-type';
|
||||||
const data = {'name': 'test-channel-name'};
|
const set = {
|
||||||
|
'name': 'Stream Team',
|
||||||
|
'profile_image': 'test-profile-image',
|
||||||
|
};
|
||||||
|
|
||||||
|
const unset = ['tag', 'last_name'];
|
||||||
|
|
||||||
final path = _getChannelUrl(channelId, channelType);
|
final path = _getChannelUrl(channelId, channelType);
|
||||||
|
|
||||||
final channelModel = ChannelModel(
|
final channelModel = ChannelModel(
|
||||||
id: channelId,
|
id: channelId,
|
||||||
type: channelType,
|
type: channelType,
|
||||||
extraData: data,
|
extraData: set,
|
||||||
);
|
);
|
||||||
|
|
||||||
when(() => client.patch(path, data: any(named: 'data')))
|
when(
|
||||||
.thenAnswer((_) async => successResponse(path, data: {
|
() => client.patch(path, data: {'set': set, 'unset': unset}),
|
||||||
'channel': channelModel.toJson(),
|
).thenAnswer((_) async => successResponse(path, data: {
|
||||||
}));
|
'channel': channelModel.toJson(),
|
||||||
|
}));
|
||||||
|
|
||||||
final res = await channelApi.updateChannelPartial(
|
final res = await channelApi.updateChannelPartial(
|
||||||
channelId,
|
channelId,
|
||||||
channelType,
|
channelType,
|
||||||
data,
|
set: set,
|
||||||
|
unset: unset,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(res, isNotNull);
|
expect(res, isNotNull);
|
||||||
|
|
||||||
verify(() => client.patch(path, data: any(named: 'data'))).called(1);
|
verify(
|
||||||
|
() => client.patch(path, data: {'set': set, 'unset': unset}),
|
||||||
|
).called(1);
|
||||||
verifyNoMoreInteractions(client);
|
verifyNoMoreInteractions(client);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,40 @@ void main() {
|
|||||||
verifyNoMoreInteractions(client);
|
verifyNoMoreInteractions(client);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('partialUpdateMessage', () async {
|
||||||
|
const messageId = 'test-message-id';
|
||||||
|
|
||||||
|
const set = {'text': 'Update Message text'};
|
||||||
|
const unset = ['pinExpires'];
|
||||||
|
|
||||||
|
const path = '/messages/$messageId';
|
||||||
|
final message = Message(id: 'test-message-id', text: set['text']);
|
||||||
|
|
||||||
|
when(() => client.put(
|
||||||
|
path,
|
||||||
|
data: {'set': set, 'unset': unset},
|
||||||
|
)).thenAnswer(
|
||||||
|
(_) async => successResponse(path, data: {'message': message.toJson()}),
|
||||||
|
);
|
||||||
|
|
||||||
|
final res = await messageApi.partialUpdateMessage(
|
||||||
|
messageId,
|
||||||
|
set: set,
|
||||||
|
unset: unset,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(res, isNotNull);
|
||||||
|
expect(res.message.id, message.id);
|
||||||
|
expect(res.message.text, set['text']);
|
||||||
|
expect(res.message.pinExpires, isNull);
|
||||||
|
|
||||||
|
verify(() => client.put(
|
||||||
|
path,
|
||||||
|
data: {'set': set, 'unset': unset},
|
||||||
|
)).called(1);
|
||||||
|
verifyNoMoreInteractions(client);
|
||||||
|
});
|
||||||
|
|
||||||
test('deleteMessage', () async {
|
test('deleteMessage', () async {
|
||||||
const messageId = 'test-message-id';
|
const messageId = 'test-message-id';
|
||||||
|
|
||||||
|
|||||||
@@ -902,7 +902,6 @@ void main() {
|
|||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"skip_push": false,
|
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
@@ -919,7 +918,6 @@ void main() {
|
|||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"skip_push": false,
|
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
@@ -929,7 +927,6 @@ void main() {
|
|||||||
{
|
{
|
||||||
"id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0",
|
"id": "dry-meadow-0-53e6299f-9b97-4a9c-a27e-7e2dde49b7e0",
|
||||||
"text": "test message",
|
"text": "test message",
|
||||||
"skip_push": false,
|
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
@@ -952,7 +949,6 @@ void main() {
|
|||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"skip_push": false,
|
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
@@ -964,7 +960,6 @@ void main() {
|
|||||||
"id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe",
|
"id": "dry-meadow-0-64d7970f-ede8-4b31-9738-1bc1756d2bfe",
|
||||||
"text": "test",
|
"text": "test",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"skip_push": false,
|
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
@@ -982,7 +977,6 @@ void main() {
|
|||||||
"text": "hi",
|
"text": "hi",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
@@ -1000,7 +994,6 @@ void main() {
|
|||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
@@ -1023,7 +1016,6 @@ void main() {
|
|||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"skip_push": false,
|
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
@@ -1041,7 +1033,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1053,7 +1044,6 @@ void main() {
|
|||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"skip_push": false,
|
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
@@ -1071,7 +1061,6 @@ void main() {
|
|||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"skip_push": false,
|
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
@@ -1090,7 +1079,6 @@ void main() {
|
|||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"skip_push": false,
|
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
@@ -1100,7 +1088,6 @@ void main() {
|
|||||||
"id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055",
|
"id": "icy-recipe-7-935c396e-ddf8-4a9a-951c-0a12fa5bf055",
|
||||||
"text": "what are you doing?",
|
"text": "what are you doing?",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"skip_push": false,
|
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
@@ -1118,7 +1105,6 @@ void main() {
|
|||||||
"text": "👍",
|
"text": "👍",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
@@ -1134,7 +1120,6 @@ void main() {
|
|||||||
"id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21",
|
"id": "snowy-credit-3-3e0c1a0d-d22f-42ee-b2a1-f9f49477bf21",
|
||||||
"text": "sdasas",
|
"text": "sdasas",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"skip_push": false,
|
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
@@ -1155,7 +1140,6 @@ void main() {
|
|||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"skip_push": false,
|
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
"status": "SENT",
|
"status": "SENT",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
@@ -1168,7 +1152,6 @@ void main() {
|
|||||||
"id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d",
|
"id": "snowy-credit-3-cfaf0b46-1daa-49c5-947c-b16d6697487d",
|
||||||
"text": "nhisagdhsadz",
|
"text": "nhisagdhsadz",
|
||||||
"attachments": [],
|
"attachments": [],
|
||||||
"skip_push": false,
|
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
@@ -1187,7 +1170,6 @@ void main() {
|
|||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
@@ -1204,7 +1186,6 @@ void main() {
|
|||||||
"attachments": [],
|
"attachments": [],
|
||||||
"parent_id": null,
|
"parent_id": null,
|
||||||
"quoted_message": null,
|
"quoted_message": null,
|
||||||
"skip_push": false,
|
|
||||||
"quoted_message_id": null,
|
"quoted_message_id": null,
|
||||||
"show_in_channel": null,
|
"show_in_channel": null,
|
||||||
"mentioned_users": [],
|
"mentioned_users": [],
|
||||||
@@ -1212,7 +1193,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1229,7 +1209,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1246,7 +1225,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1263,7 +1241,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1280,7 +1257,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1297,7 +1273,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
},
|
},
|
||||||
@@ -1314,7 +1289,6 @@ void main() {
|
|||||||
"silent": false,
|
"silent": false,
|
||||||
"pinned": false,
|
"pinned": false,
|
||||||
"pinned_at": null,
|
"pinned_at": null,
|
||||||
"skip_push": false,
|
|
||||||
"pin_expires": null,
|
"pin_expires": null,
|
||||||
"pinned_by": null
|
"pinned_by": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,6 @@ void main() {
|
|||||||
"id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f",
|
"id": "4637f7e4-a06b-42db-ba5a-8d8270dd926f",
|
||||||
"text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA",
|
"text": "https://giphy.com/gifs/the-lion-king-live-action-5zvN79uTGfLMOVfQaA",
|
||||||
"silent": false,
|
"silent": false,
|
||||||
"skip_push": false,
|
|
||||||
"attachments": [
|
"attachments": [
|
||||||
{
|
{
|
||||||
"type": "video",
|
"type": "video",
|
||||||
|
|||||||
@@ -87,15 +87,8 @@ class FakeClientState extends Fake implements ClientState {
|
|||||||
@override
|
@override
|
||||||
OwnUser? get user => OwnUser(id: 'test-user-id');
|
OwnUser? get user => OwnUser(id: 'test-user-id');
|
||||||
|
|
||||||
var _totalUnreadCount = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int? get totalUnreadCount => _totalUnreadCount;
|
int totalUnreadCount = 0;
|
||||||
|
|
||||||
@override
|
|
||||||
set totalUnreadCount(int? unreadCount) {
|
|
||||||
_totalUnreadCount += unreadCount ?? 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeMessage extends Fake implements Message {}
|
class FakeMessage extends Fake implements Message {}
|
||||||
|
|||||||
@@ -40,11 +40,13 @@ class _IsSameEventAs extends Matcher {
|
|||||||
|
|
||||||
Matcher isSameMessageAs(
|
Matcher isSameMessageAs(
|
||||||
Message targetMessage, {
|
Message targetMessage, {
|
||||||
|
bool matchText = false,
|
||||||
bool matchReactions = false,
|
bool matchReactions = false,
|
||||||
bool matchSendingStatus = false,
|
bool matchSendingStatus = false,
|
||||||
}) =>
|
}) =>
|
||||||
_IsSameMessageAs(
|
_IsSameMessageAs(
|
||||||
targetMessage: targetMessage,
|
targetMessage: targetMessage,
|
||||||
|
matchText: matchText,
|
||||||
matchReactions: matchReactions,
|
matchReactions: matchReactions,
|
||||||
matchSendingStatus: matchSendingStatus,
|
matchSendingStatus: matchSendingStatus,
|
||||||
);
|
);
|
||||||
@@ -52,11 +54,13 @@ Matcher isSameMessageAs(
|
|||||||
class _IsSameMessageAs extends Matcher {
|
class _IsSameMessageAs extends Matcher {
|
||||||
const _IsSameMessageAs({
|
const _IsSameMessageAs({
|
||||||
required this.targetMessage,
|
required this.targetMessage,
|
||||||
|
this.matchText = false,
|
||||||
this.matchReactions = false,
|
this.matchReactions = false,
|
||||||
this.matchSendingStatus = false,
|
this.matchSendingStatus = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Message targetMessage;
|
final Message targetMessage;
|
||||||
|
final bool matchText;
|
||||||
final bool matchReactions;
|
final bool matchReactions;
|
||||||
final bool matchSendingStatus;
|
final bool matchSendingStatus;
|
||||||
|
|
||||||
@@ -67,6 +71,9 @@ class _IsSameMessageAs extends Matcher {
|
|||||||
@override
|
@override
|
||||||
bool matches(covariant Message message, Map matchState) {
|
bool matches(covariant Message message, Map matchState) {
|
||||||
var matches = message.id == targetMessage.id;
|
var matches = message.id == targetMessage.id;
|
||||||
|
if (matchText) {
|
||||||
|
matches &= message.text == targetMessage.text;
|
||||||
|
}
|
||||||
if (matchSendingStatus) {
|
if (matchSendingStatus) {
|
||||||
matches &= message.status == targetMessage.status;
|
matches &= message.status == targetMessage.status;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
## 2.0.0-nullsafety.5
|
||||||
|
|
||||||
|
- Minor fixes and improvements
|
||||||
|
- Updated `stream_chat_core` dependency
|
||||||
|
- Performance improvements
|
||||||
|
- Added pinMessage ui support
|
||||||
|
- Added `MessageListView.threadSeparatorBuilder` property
|
||||||
|
|
||||||
## 2.0.0-nullsafety.4
|
## 2.0.0-nullsafety.4
|
||||||
|
|
||||||
- Minor fixes and improvements
|
- Minor fixes and improvements
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
||||||
import 'package:stream_chat_flutter/src/utils.dart';
|
import 'package:stream_chat_flutter/src/utils.dart';
|
||||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
import 'package:stream_chat_flutter/src/upload_progress_indicator.dart';
|
|
||||||
|
|
||||||
// ignore: always_use_package_imports
|
// ignore: always_use_package_imports
|
||||||
import 'attachment_widget.dart';
|
import 'attachment_widget.dart';
|
||||||
@@ -103,7 +103,7 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
Widget _getFileTypeImage(BuildContext context) {
|
Widget _getFileTypeImage(BuildContext context) {
|
||||||
if (isImageAttachment) {
|
if (isImageAttachment) {
|
||||||
return Material(
|
return Material(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.hardEdge,
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
shape: _getDefaultShape(context),
|
shape: _getDefaultShape(context),
|
||||||
child: source.when(
|
child: source.when(
|
||||||
@@ -154,7 +154,7 @@ class FileAttachment extends AttachmentWidget {
|
|||||||
|
|
||||||
if (isVideoAttachment) {
|
if (isVideoAttachment) {
|
||||||
return Material(
|
return Material(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.hardEdge,
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
shape: _getDefaultShape(context),
|
shape: _getDefaultShape(context),
|
||||||
child: source.when(
|
child: source.when(
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class GiphyAttachment extends AttachmentWidget {
|
|||||||
Card(
|
Card(
|
||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
color: StreamChatTheme.of(context).colorTheme.white,
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.hardEdge,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topRight: Radius.circular(16),
|
topRight: Radius.circular(16),
|
||||||
|
|||||||
@@ -74,16 +74,16 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
if (imageUri.host == 'stream-io-cdn.com') {
|
if (imageUri.host == 'stream-io-cdn.com') {
|
||||||
imageUri = imageUri.replace(queryParameters: {
|
imageUri = imageUri.replace(queryParameters: {
|
||||||
...imageUri.queryParameters,
|
...imageUri.queryParameters,
|
||||||
'h': '500',
|
'h': '400',
|
||||||
'w': '500',
|
'w': '400',
|
||||||
'crop': 'center',
|
'crop': 'center',
|
||||||
'resize': 'crop',
|
'resize': 'crop',
|
||||||
});
|
});
|
||||||
} else if (imageUri.host == 'stream-cloud-uploads.imgix.net') {
|
} else if (imageUri.host == 'stream-cloud-uploads.imgix.net') {
|
||||||
imageUri = imageUri.replace(queryParameters: {
|
imageUri = imageUri.replace(queryParameters: {
|
||||||
...imageUri.queryParameters,
|
...imageUri.queryParameters,
|
||||||
'height': '500',
|
'height': '400',
|
||||||
'width': '500',
|
'width': '400',
|
||||||
'fit': 'crop',
|
'fit': 'crop',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -92,10 +92,10 @@ class ImageAttachment extends AttachmentWidget {
|
|||||||
return _buildImageAttachment(
|
return _buildImageAttachment(
|
||||||
context,
|
context,
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
cacheKey: imageUri.path,
|
cacheKey: imageUrl,
|
||||||
height: size?.height,
|
height: size?.height,
|
||||||
width: size?.width,
|
width: size?.width,
|
||||||
placeholder: (_, __) {
|
placeholder: (context, __) {
|
||||||
final image = Image.asset(
|
final image = Image.asset(
|
||||||
'images/placeholder.png',
|
'images/placeholder.png',
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
|||||||
@@ -133,8 +133,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return InfoTile(
|
return InfoTile(
|
||||||
// ignore: avoid_bool_literals_in_conditional_expressions
|
showMessage: showConnectionStateTile && showStatus,
|
||||||
showMessage: showConnectionStateTile ? showStatus : false,
|
|
||||||
message: statusString,
|
message: statusString,
|
||||||
child: AppBar(
|
child: AppBar(
|
||||||
textTheme: Theme.of(context).textTheme,
|
textTheme: Theme.of(context).textTheme,
|
||||||
|
|||||||
@@ -83,26 +83,28 @@ class ChannelImage extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final streamChat = StreamChat.of(context);
|
final streamChat = StreamChat.of(context);
|
||||||
final channel = this.channel ?? StreamChannel.of(context).channel;
|
final channel = this.channel ?? StreamChannel.of(context).channel;
|
||||||
return StreamBuilder<Map<String, dynamic>>(
|
return BetterStreamBuilder<Map<String, dynamic>>(
|
||||||
stream: channel.extraDataStream,
|
stream: channel.extraDataStream,
|
||||||
initialData: channel.extraData,
|
initialData: channel.extraData,
|
||||||
builder: (context, snapshot) {
|
builder: (context, data) {
|
||||||
String? image;
|
String? image;
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
if (snapshot.data!.containsKey('image') == true) {
|
if (data.containsKey('image') == true) {
|
||||||
image = snapshot.data!['image'];
|
image = data['image'];
|
||||||
} else if (channel.state?.members.length == 2) {
|
} else if (channel.state?.members.length == 2) {
|
||||||
final otherMember = channel.state?.members
|
final otherMember = channel.state?.members
|
||||||
.firstWhere((member) => member.user?.id != streamChat.user?.id);
|
.firstWhere((member) => member.user?.id != streamChat.user?.id);
|
||||||
return StreamBuilder<User>(
|
return BetterStreamBuilder<User?>(
|
||||||
stream: streamChat.client.state.usersStream.map(
|
stream: streamChat.client.state.usersStream
|
||||||
(users) => users[otherMember?.userId] ?? otherMember!.user!),
|
.map((users) =>
|
||||||
|
users[otherMember?.userId] ?? otherMember!.user!)
|
||||||
|
.distinct(),
|
||||||
initialData: otherMember!.user,
|
initialData: otherMember!.user,
|
||||||
builder: (context, snapshot) => UserAvatar(
|
builder: (context, user) => UserAvatar(
|
||||||
borderRadius: borderRadius ??
|
borderRadius: borderRadius ??
|
||||||
chatThemeData
|
chatThemeData
|
||||||
.channelPreviewTheme.avatarTheme?.borderRadius,
|
.channelPreviewTheme.avatarTheme?.borderRadius,
|
||||||
user: snapshot.data ?? otherMember.user!,
|
user: user ?? otherMember.user!,
|
||||||
constraints: constraints ??
|
constraints: constraints ??
|
||||||
chatThemeData
|
chatThemeData
|
||||||
.channelPreviewTheme.avatarTheme?.constraints,
|
.channelPreviewTheme.avatarTheme?.constraints,
|
||||||
@@ -153,9 +155,7 @@ class ChannelImage extends StatelessWidget {
|
|||||||
imageUrl: image,
|
imageUrl: image,
|
||||||
errorWidget: (_, __, ___) => Center(
|
errorWidget: (_, __, ___) => Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
snapshot.data?.containsKey('name') ?? false
|
data.containsKey('name') ? data['name'][0] : '',
|
||||||
? snapshot.data!['name'][0]
|
|
||||||
: '',
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: chatThemeData.colorTheme.white,
|
color: chatThemeData.colorTheme.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ class ChannelInfo extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = StreamChat.of(context).client;
|
final client = StreamChat.of(context).client;
|
||||||
return StreamBuilder<List<Member>>(
|
return BetterStreamBuilder<List<Member>>(
|
||||||
stream: channel.state?.membersStream,
|
stream: channel.state!.membersStream,
|
||||||
initialData: channel.state?.members,
|
initialData: channel.state!.members,
|
||||||
builder: (context, snapshot) => ConnectionStatusBuilder(
|
builder: (context, data) => ConnectionStatusBuilder(
|
||||||
statusBuilder: (context, status) {
|
statusBuilder: (context, status) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case ConnectionStatus.connected:
|
case ConnectionStatus.connected:
|
||||||
return _buildConnectedTitleState(context, snapshot.data);
|
return _buildConnectedTitleState(context, data);
|
||||||
case ConnectionStatus.connecting:
|
case ConnectionStatus.connecting:
|
||||||
return _buildConnectingTitleState(context);
|
return _buildConnectingTitleState(context);
|
||||||
case ConnectionStatus.disconnected:
|
case ConnectionStatus.disconnected:
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'package:collection/collection.dart' show IterableExtension;
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||||
import 'package:shimmer/shimmer.dart';
|
import 'package:shimmer/shimmer.dart';
|
||||||
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
|
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/utils.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
@@ -257,10 +256,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return AnimatedSwitcher(
|
return child;
|
||||||
duration: const Duration(milliseconds: 500),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder(
|
Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder(
|
||||||
@@ -477,104 +473,105 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
|
|
||||||
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
Widget _listItemBuilder(BuildContext context, int i, List<Channel> channels) {
|
||||||
final channelsBloc = ChannelsBloc.of(context);
|
final channelsBloc = ChannelsBloc.of(context);
|
||||||
|
final onTap = _getChannelTap(context);
|
||||||
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
|
final backgroundColor = chatThemeData.colorTheme.whiteSmoke;
|
||||||
|
|
||||||
if (i < channels.length) {
|
if (i < channels.length) {
|
||||||
final channel = channels[i];
|
final channel = channels[i];
|
||||||
final onTap = _getChannelTap(context);
|
|
||||||
|
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
|
||||||
final backgroundColor = chatThemeData.colorTheme.whiteSmoke;
|
|
||||||
return StreamChannel(
|
return StreamChannel(
|
||||||
key: ValueKey<String>('CHANNEL-${channel.id}'),
|
key: ValueKey<String>('CHANNEL-${channel.cid}'),
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: Builder(
|
child: Slidable(
|
||||||
builder: (context) => Slidable(
|
controller: _slideController,
|
||||||
controller: _slideController,
|
enabled: widget.swipeToAction,
|
||||||
enabled: widget.swipeToAction,
|
actionPane: const SlidableBehindActionPane(),
|
||||||
actionPane: const SlidableBehindActionPane(),
|
actionExtentRatio: 0.12,
|
||||||
actionExtentRatio: 0.12,
|
secondaryActions: widget.swipeActions
|
||||||
secondaryActions: widget.swipeActions
|
?.map((e) => IconSlideAction(
|
||||||
?.map((e) => IconSlideAction(
|
color: e.color,
|
||||||
color: e.color,
|
iconWidget: e.iconWidget,
|
||||||
iconWidget: e.iconWidget,
|
onTap: () {
|
||||||
onTap: () {
|
e.onTap?.call(channel);
|
||||||
e.onTap?.call(channel);
|
},
|
||||||
},
|
))
|
||||||
))
|
.toList() ??
|
||||||
.toList() ??
|
<Widget>[
|
||||||
<Widget>[
|
IconSlideAction(
|
||||||
|
color: backgroundColor,
|
||||||
|
icon: Icons.more_horiz,
|
||||||
|
onTap: widget.onMoreDetailsPressed != null
|
||||||
|
? () {
|
||||||
|
widget.onMoreDetailsPressed!(channel);
|
||||||
|
}
|
||||||
|
: () {
|
||||||
|
showModalBottomSheet(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(32),
|
||||||
|
topRight: Radius.circular(32),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
context: context,
|
||||||
|
builder: (context) => StreamChannel(
|
||||||
|
channel: channel,
|
||||||
|
child: ChannelBottomSheet(
|
||||||
|
onViewInfoTap: () {
|
||||||
|
widget.onViewInfoTap?.call(channel);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if ([
|
||||||
|
'admin',
|
||||||
|
'owner',
|
||||||
|
].contains(channel.state!.members
|
||||||
|
.firstWhereOrNull(
|
||||||
|
(m) => m.userId == channel.client.state.user?.id)
|
||||||
|
?.role))
|
||||||
IconSlideAction(
|
IconSlideAction(
|
||||||
color: backgroundColor,
|
color: backgroundColor,
|
||||||
icon: Icons.more_horiz,
|
iconWidget: StreamSvgIcon.delete(
|
||||||
onTap: widget.onMoreDetailsPressed != null
|
color: chatThemeData.colorTheme.accentRed,
|
||||||
|
),
|
||||||
|
onTap: widget.onDeletePressed != null
|
||||||
? () {
|
? () {
|
||||||
widget.onMoreDetailsPressed!(channel);
|
widget.onDeletePressed!(channel);
|
||||||
}
|
}
|
||||||
: () {
|
: () async {
|
||||||
showModalBottomSheet(
|
final res = await showConfirmationDialog(
|
||||||
clipBehavior: Clip.hardEdge,
|
context,
|
||||||
shape: const RoundedRectangleBorder(
|
title: 'Delete Conversation',
|
||||||
borderRadius: BorderRadius.only(
|
okText: 'DELETE',
|
||||||
topLeft: Radius.circular(32),
|
question:
|
||||||
topRight: Radius.circular(32),
|
// ignore: lines_longer_than_80_chars
|
||||||
),
|
'Are you sure you want to delete this conversation?',
|
||||||
),
|
cancelText: 'CANCEL',
|
||||||
context: context,
|
icon: StreamSvgIcon.delete(
|
||||||
builder: (context) => StreamChannel(
|
color: chatThemeData.colorTheme.accentRed,
|
||||||
channel: channel,
|
|
||||||
child: ChannelBottomSheet(
|
|
||||||
onViewInfoTap: () {
|
|
||||||
widget.onViewInfoTap?.call(channel);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
if (res == true) {
|
||||||
|
await channel.delete();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if ([
|
],
|
||||||
'admin',
|
child: DecoratedBox(
|
||||||
'owner',
|
decoration: BoxDecoration(
|
||||||
].contains(channel.state!.members
|
|
||||||
.firstWhereOrNull(
|
|
||||||
(m) => m.userId == channel.client.state.user?.id)
|
|
||||||
?.role))
|
|
||||||
IconSlideAction(
|
|
||||||
color: backgroundColor,
|
|
||||||
iconWidget: StreamSvgIcon.delete(
|
|
||||||
color: chatThemeData.colorTheme.accentRed,
|
|
||||||
),
|
|
||||||
onTap: widget.onDeletePressed != null
|
|
||||||
? () {
|
|
||||||
widget.onDeletePressed!(channel);
|
|
||||||
}
|
|
||||||
: () async {
|
|
||||||
final res = await showConfirmationDialog(
|
|
||||||
context,
|
|
||||||
title: 'Delete Conversation',
|
|
||||||
okText: 'DELETE',
|
|
||||||
question:
|
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
'Are you sure you want to delete this conversation?',
|
|
||||||
cancelText: 'CANCEL',
|
|
||||||
icon: StreamSvgIcon.delete(
|
|
||||||
color: chatThemeData.colorTheme.accentRed,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (res == true) {
|
|
||||||
await channel.delete();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: Container(
|
|
||||||
color: chatThemeData.colorTheme.whiteSnow,
|
color: chatThemeData.colorTheme.whiteSnow,
|
||||||
child: widget.channelPreviewBuilder?.call(context, channel) ??
|
|
||||||
ChannelPreview(
|
|
||||||
onLongPress: widget.onChannelLongPress,
|
|
||||||
channel: channel,
|
|
||||||
onImageTap: () => widget.onImageTap?.call(channel),
|
|
||||||
onTap: (channel) => onTap(channel, widget.channelWidget),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
child: widget.channelPreviewBuilder?.call(context, channel) ??
|
||||||
|
ChannelPreview(
|
||||||
|
onLongPress: widget.onChannelLongPress,
|
||||||
|
channel: channel,
|
||||||
|
onImageTap: () => widget.onImageTap?.call(channel),
|
||||||
|
onTap: (channel) => onTap(channel, widget.channelWidget),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -648,12 +645,10 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
context,
|
context,
|
||||||
ChannelsBlocState channelsProvider,
|
ChannelsBlocState channelsProvider,
|
||||||
) =>
|
) =>
|
||||||
StreamBuilder<bool>(
|
BetterStreamBuilder<bool>(
|
||||||
stream: channelsProvider.queryChannelsLoading,
|
stream: channelsProvider.queryChannelsLoading,
|
||||||
initialData: false,
|
initialData: false,
|
||||||
builder: (context, snapshot) {
|
errorBuilder: (context, err) => Container(
|
||||||
if (snapshot.hasError) {
|
|
||||||
return Container(
|
|
||||||
color: StreamChatTheme.of(context)
|
color: StreamChatTheme.of(context)
|
||||||
.colorTheme
|
.colorTheme
|
||||||
.accentRed
|
.accentRed
|
||||||
@@ -664,17 +659,15 @@ class _ChannelListViewState extends State<ChannelListView> {
|
|||||||
child: Text('Error loading channels'),
|
child: Text('Error loading channels'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
}
|
builder: (context, data) => data
|
||||||
return snapshot.data!
|
? const Center(
|
||||||
? const Center(
|
child: Padding(
|
||||||
child: Padding(
|
padding: EdgeInsets.all(16),
|
||||||
padding: EdgeInsets.all(16),
|
child: CircularProgressIndicator(),
|
||||||
child: CircularProgressIndicator(),
|
),
|
||||||
),
|
)
|
||||||
)
|
: const Offstage());
|
||||||
: const Offstage();
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget _separatorBuilder(context, i) {
|
Widget _separatorBuilder(context, i) {
|
||||||
final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
|
final effect = StreamChatTheme.of(context).colorTheme.borderBottom;
|
||||||
|
|||||||
@@ -26,11 +26,11 @@ class ChannelName extends StatelessWidget {
|
|||||||
final client = StreamChat.of(context);
|
final client = StreamChat.of(context);
|
||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
|
||||||
return StreamBuilder<Map<String, dynamic>>(
|
return BetterStreamBuilder<Map<String, Object?>>(
|
||||||
stream: channel.extraDataStream,
|
stream: channel.extraDataStream,
|
||||||
initialData: channel.extraData,
|
initialData: channel.extraData,
|
||||||
builder: (context, snapshot) => _buildName(
|
builder: (context, data) => _buildName(
|
||||||
snapshot.data!,
|
data,
|
||||||
channel.state?.members,
|
channel.state?.members,
|
||||||
client,
|
client,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:collection/collection.dart' show IterableExtension;
|
import 'package:collection/collection.dart'
|
||||||
|
show IterableExtension, ListEquality;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
@@ -69,12 +70,12 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
|
final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme;
|
||||||
final streamChatState = StreamChat.of(context);
|
final streamChatState = StreamChat.of(context);
|
||||||
|
return BetterStreamBuilder<bool>(
|
||||||
return StreamBuilder<bool>(
|
|
||||||
stream: channel.isMutedStream,
|
stream: channel.isMutedStream,
|
||||||
initialData: channel.isMuted,
|
initialData: channel.isMuted,
|
||||||
builder: (context, snapshot) => Opacity(
|
builder: (context, data) => AnimatedOpacity(
|
||||||
opacity: snapshot.data! ? 0.5 : 1,
|
opacity: data ? 0.5 : 1,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
@@ -103,14 +104,16 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
textStyle: channelPreviewTheme.title,
|
textStyle: channelPreviewTheme.title,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
StreamBuilder<List<Member>>(
|
BetterStreamBuilder<List<Member>?>(
|
||||||
stream: channel.state?.membersStream,
|
stream: channel.state?.membersStream,
|
||||||
initialData: channel.state?.members,
|
initialData: channel.state?.members,
|
||||||
builder: (context, snapshot) {
|
comparator: const ListEquality().equals,
|
||||||
if (!snapshot.hasData ||
|
builder: (context, members) {
|
||||||
snapshot.data!.isEmpty ||
|
if (members?.isEmpty == true ||
|
||||||
!snapshot.data!.any((Member e) =>
|
members?.any((Member e) =>
|
||||||
e.user!.id == channel.client.state.user?.id)) {
|
e.user!.id ==
|
||||||
|
channel.client.state.user?.id) !=
|
||||||
|
true) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
return UnreadIndicator(
|
return UnreadIndicator(
|
||||||
@@ -159,14 +162,14 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDate(BuildContext context) => StreamBuilder<DateTime?>(
|
Widget _buildDate(BuildContext context) => BetterStreamBuilder<DateTime?>(
|
||||||
stream: channel.lastMessageAtStream,
|
stream: channel.lastMessageAtStream,
|
||||||
initialData: channel.lastMessageAt,
|
initialData: channel.lastMessageAt,
|
||||||
builder: (context, snapshot) {
|
builder: (context, data) {
|
||||||
if (!snapshot.hasData) {
|
if (data == null) {
|
||||||
return const SizedBox();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
final lastMessageAt = snapshot.data!.toLocal();
|
final lastMessageAt = data.toLocal();
|
||||||
|
|
||||||
String stringDate;
|
String stringDate;
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
@@ -219,12 +222,12 @@ class ChannelPreview extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildLastMessage(BuildContext context) =>
|
Widget _buildLastMessage(BuildContext context) =>
|
||||||
StreamBuilder<List<Message>?>(
|
BetterStreamBuilder<List<Message>?>(
|
||||||
stream: channel.state!.messagesStream,
|
stream: channel.state!.messagesStream,
|
||||||
initialData: channel.state!.messages,
|
initialData: channel.state!.messages,
|
||||||
builder: (context, snapshot) {
|
builder: (context, data) {
|
||||||
final lastMessage = snapshot.data
|
final lastMessage =
|
||||||
?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted);
|
data?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted);
|
||||||
if (lastMessage == null) {
|
if (lastMessage == null) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,11 @@ class ConnectionStatusBuilder extends StatelessWidget {
|
|||||||
const ConnectionStatusBuilder({
|
const ConnectionStatusBuilder({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.statusBuilder,
|
required this.statusBuilder,
|
||||||
this.initialStatus = ConnectionStatus.disconnected,
|
|
||||||
this.connectionStatusStream,
|
this.connectionStatusStream,
|
||||||
this.errorBuilder,
|
this.errorBuilder,
|
||||||
this.loadingBuilder,
|
this.loadingBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// The connection status that will be used to create the initial snapshot.
|
|
||||||
final ConnectionStatus initialStatus;
|
|
||||||
|
|
||||||
/// The asynchronous computation to which this builder is currently connected.
|
/// The asynchronous computation to which this builder is currently connected.
|
||||||
final Stream<ConnectionStatus>? connectionStatusStream;
|
final Stream<ConnectionStatus>? connectionStatusStream;
|
||||||
|
|
||||||
@@ -38,22 +34,18 @@ class ConnectionStatusBuilder extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final stream = connectionStatusStream ??
|
final stream = connectionStatusStream ??
|
||||||
StreamChat.of(context).client.wsConnectionStatusStream;
|
StreamChat.of(context).client.wsConnectionStatusStream;
|
||||||
return StreamBuilder<ConnectionStatus>(
|
final client = StreamChat.of(context).client;
|
||||||
initialData: initialStatus,
|
return BetterStreamBuilder<ConnectionStatus>(
|
||||||
|
initialData: client.wsConnectionStatus,
|
||||||
stream: stream,
|
stream: stream,
|
||||||
builder: (context, snapshot) {
|
loadingBuilder: loadingBuilder,
|
||||||
if (snapshot.hasError) {
|
errorBuilder: (context, error) {
|
||||||
if (errorBuilder != null) {
|
if (errorBuilder != null) {
|
||||||
return errorBuilder!(context, snapshot.error);
|
return errorBuilder!(context, error);
|
||||||
}
|
|
||||||
return const Offstage();
|
|
||||||
}
|
}
|
||||||
if (!snapshot.hasData) {
|
return const Offstage();
|
||||||
if (loadingBuilder != null) return loadingBuilder!(context);
|
|
||||||
return const Offstage();
|
|
||||||
}
|
|
||||||
return statusBuilder(context, snapshot.data!);
|
|
||||||
},
|
},
|
||||||
|
builder: statusBuilder,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114325,6 +114325,9 @@ class Emoji {
|
|||||||
/// Get all Emojis
|
/// Get all Emojis
|
||||||
static List<Emoji> all() => List.unmodifiable(_emojis);
|
static List<Emoji> all() => List.unmodifiable(_emojis);
|
||||||
|
|
||||||
|
static Iterable<String> chars() =>
|
||||||
|
_emojis.map((e) => e.char).whereType<String>();
|
||||||
|
|
||||||
/// Returns Emoji by [char] and character
|
/// Returns Emoji by [char] and character
|
||||||
static Emoji? byChar(String char) {
|
static Emoji? byChar(String char) {
|
||||||
return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == char);
|
return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == char);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
import 'package:stream_chat_flutter/src/emoji/emoji.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
final _emojis = Emoji.all();
|
final _emojiChars = Emoji.chars();
|
||||||
|
|
||||||
/// String extension
|
/// String extension
|
||||||
extension StringExtension on String {
|
extension StringExtension on String {
|
||||||
@@ -17,10 +17,10 @@ extension StringExtension on String {
|
|||||||
/// 1 to 3 emojis: big size with no text bubble.
|
/// 1 to 3 emojis: big size with no text bubble.
|
||||||
/// 4+ emojis or emojis+text: standard size with text bubble.
|
/// 4+ emojis or emojis+text: standard size with text bubble.
|
||||||
bool get isOnlyEmoji {
|
bool get isOnlyEmoji {
|
||||||
|
if (isEmpty) return false;
|
||||||
|
if (length > 3) return false;
|
||||||
final characters = trim().characters;
|
final characters = trim().characters;
|
||||||
if (characters.isEmpty) return false;
|
return characters.every(_emojiChars.contains);
|
||||||
if (characters.length > 3) return false;
|
|
||||||
return characters.every((c) => _emojis.map((e) => e.char).contains(c));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
this.showResendMessage = true,
|
this.showResendMessage = true,
|
||||||
this.showThreadReplyMessage = true,
|
this.showThreadReplyMessage = true,
|
||||||
this.showFlagButton = true,
|
this.showFlagButton = true,
|
||||||
|
this.showPinButton = true,
|
||||||
|
this.showPinHighlight = false,
|
||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.editMessageInputBuilder,
|
this.editMessageInputBuilder,
|
||||||
this.messageShape,
|
this.messageShape,
|
||||||
@@ -35,6 +37,7 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
this.customActions = const [],
|
this.customActions = const [],
|
||||||
this.attachmentBorderRadiusGeometry,
|
this.attachmentBorderRadiusGeometry,
|
||||||
this.onCopyTap,
|
this.onCopyTap,
|
||||||
|
this.textBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Builder for edit message
|
/// Builder for edit message
|
||||||
@@ -79,6 +82,12 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
/// Flag for showing flag action
|
/// Flag for showing flag action
|
||||||
final bool showFlagButton;
|
final bool showFlagButton;
|
||||||
|
|
||||||
|
/// Flag for showing pin action
|
||||||
|
final bool showPinButton;
|
||||||
|
|
||||||
|
/// Display Pin Highlight
|
||||||
|
final bool showPinHighlight;
|
||||||
|
|
||||||
/// Flag for reversing message
|
/// Flag for reversing message
|
||||||
final bool reverse;
|
final bool reverse;
|
||||||
|
|
||||||
@@ -97,6 +106,9 @@ class MessageActionsModal extends StatefulWidget {
|
|||||||
/// List of custom actions
|
/// List of custom actions
|
||||||
final List<MessageAction> customActions;
|
final List<MessageAction> customActions;
|
||||||
|
|
||||||
|
/// Customize the MessageWidget textBuilder
|
||||||
|
final Widget Function(BuildContext context, Message message)? textBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageActionsModalState createState() => _MessageActionsModalState();
|
_MessageActionsModalState createState() => _MessageActionsModalState();
|
||||||
}
|
}
|
||||||
@@ -135,6 +147,127 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
widget.message.attachments.any((it) => it.type == 'file') == true;
|
widget.message.attachments.any((it) => it.type == 'file') == true;
|
||||||
|
|
||||||
final streamChatThemeData = StreamChatTheme.of(context);
|
final streamChatThemeData = StreamChatTheme.of(context);
|
||||||
|
|
||||||
|
final numberOfReactions = streamChatThemeData.reactionIcons.length;
|
||||||
|
final shiftFactor =
|
||||||
|
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0;
|
||||||
|
|
||||||
|
final child = Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: widget.reverse
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
if (widget.showReactions &&
|
||||||
|
(widget.message.status == MessageSendingStatus.sent))
|
||||||
|
Align(
|
||||||
|
alignment: Alignment(
|
||||||
|
user?.id == widget.message.user?.id
|
||||||
|
? (divFactor >= 1.0
|
||||||
|
? -0.2 - shiftFactor
|
||||||
|
: (1.2 - divFactor))
|
||||||
|
: (divFactor >= 1.0
|
||||||
|
? 0.2 + shiftFactor
|
||||||
|
: -(1.2 - divFactor)),
|
||||||
|
0),
|
||||||
|
child: ReactionPicker(
|
||||||
|
message: widget.message,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
IgnorePointer(
|
||||||
|
child: MessageWidget(
|
||||||
|
key: const Key('MessageWidget'),
|
||||||
|
reverse: widget.reverse,
|
||||||
|
attachmentBorderRadiusGeometry: widget
|
||||||
|
.attachmentBorderRadiusGeometry
|
||||||
|
?.mirrorBorderIfReversed(reverse: !widget.reverse),
|
||||||
|
message: widget.message.copyWith(
|
||||||
|
text: widget.message.text!.length > 200
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
? '${widget.message.text!.substring(0, 200)}...'
|
||||||
|
: widget.message.text,
|
||||||
|
),
|
||||||
|
messageTheme: widget.messageTheme,
|
||||||
|
showReactions: false,
|
||||||
|
showUsername: false,
|
||||||
|
showReplyMessage: false,
|
||||||
|
showUserAvatar: widget.showUserAvatar,
|
||||||
|
attachmentPadding: EdgeInsets.all(
|
||||||
|
hasFileAttachment ? 4 : 2,
|
||||||
|
),
|
||||||
|
showTimestamp: false,
|
||||||
|
translateUserAvatar: false,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
textPadding: EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: widget.message.text!.isOnlyEmoji ? 0 : 16.0,
|
||||||
|
),
|
||||||
|
showReactionPickerIndicator: widget.showReactions &&
|
||||||
|
(widget.message.status == MessageSendingStatus.sent),
|
||||||
|
showSendingIndicator: false,
|
||||||
|
shape: widget.messageShape,
|
||||||
|
attachmentShape: widget.attachmentShape,
|
||||||
|
showPinHighlight: false,
|
||||||
|
textBuilder: widget.textBuilder,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
left: widget.reverse ? 0 : 40,
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
width: mediaQueryData.size.width * 0.75,
|
||||||
|
child: Material(
|
||||||
|
color: streamChatThemeData.colorTheme.whiteSnow,
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (widget.showReplyMessage &&
|
||||||
|
widget.message.status == MessageSendingStatus.sent)
|
||||||
|
_buildReplyButton(context),
|
||||||
|
if (widget.showThreadReplyMessage &&
|
||||||
|
(widget.message.status ==
|
||||||
|
MessageSendingStatus.sent) &&
|
||||||
|
widget.message.parentId == null)
|
||||||
|
_buildThreadReplyButton(context),
|
||||||
|
if (widget.showResendMessage)
|
||||||
|
_buildResendMessage(context),
|
||||||
|
if (widget.showEditMessage) _buildEditMessage(context),
|
||||||
|
if (widget.showCopyMessage) _buildCopyButton(context),
|
||||||
|
if (widget.showFlagButton) _buildFlagButton(context),
|
||||||
|
if (widget.showPinButton) _buildPinButton(context),
|
||||||
|
if (widget.showDeleteMessage)
|
||||||
|
_buildDeleteButton(context),
|
||||||
|
...widget.customActions
|
||||||
|
.map((action) => _buildCustomAction(
|
||||||
|
context,
|
||||||
|
action,
|
||||||
|
))
|
||||||
|
].insertBetween(
|
||||||
|
Container(
|
||||||
|
height: 1,
|
||||||
|
color: streamChatThemeData.colorTheme.greyWhisper,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onTap: () => Navigator.maybePop(context),
|
onTap: () => Navigator.maybePop(context),
|
||||||
@@ -156,132 +289,11 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
tween: Tween(begin: 0, end: 1),
|
tween: Tween(begin: 0, end: 1),
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOutBack,
|
curve: Curves.easeInOutBack,
|
||||||
builder: (context, val, snapshot) => Transform.scale(
|
builder: (context, val, child) => Transform.scale(
|
||||||
scale: val,
|
scale: val,
|
||||||
child: Center(
|
child: child,
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: widget.reverse
|
|
||||||
? CrossAxisAlignment.end
|
|
||||||
: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
if (widget.showReactions &&
|
|
||||||
(widget.message.status ==
|
|
||||||
MessageSendingStatus.sent))
|
|
||||||
Align(
|
|
||||||
alignment: Alignment(
|
|
||||||
user?.id == widget.message.user?.id
|
|
||||||
? (divFactor >= 1.0
|
|
||||||
? -0.2
|
|
||||||
: (1.2 - divFactor))
|
|
||||||
: (divFactor >= 1.0
|
|
||||||
? 0.2
|
|
||||||
: -(1.2 - divFactor)),
|
|
||||||
0),
|
|
||||||
child: ReactionPicker(
|
|
||||||
message: widget.message,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
IgnorePointer(
|
|
||||||
child: MessageWidget(
|
|
||||||
key: const Key('MessageWidget'),
|
|
||||||
reverse: widget.reverse,
|
|
||||||
attachmentBorderRadiusGeometry: widget
|
|
||||||
.attachmentBorderRadiusGeometry
|
|
||||||
?.mirrorBorderIfReversed(
|
|
||||||
reverse: !widget.reverse),
|
|
||||||
message: widget.message.copyWith(
|
|
||||||
text: widget.message.text!.length > 200
|
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
? '${widget.message.text!.substring(0, 200)}...'
|
|
||||||
: widget.message.text,
|
|
||||||
),
|
|
||||||
messageTheme: widget.messageTheme,
|
|
||||||
showReactions: false,
|
|
||||||
showUsername: false,
|
|
||||||
showReplyMessage: false,
|
|
||||||
showUserAvatar: widget.showUserAvatar,
|
|
||||||
attachmentPadding: EdgeInsets.all(
|
|
||||||
hasFileAttachment ? 4 : 2,
|
|
||||||
),
|
|
||||||
showTimestamp: false,
|
|
||||||
translateUserAvatar: false,
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
textPadding: EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
horizontal:
|
|
||||||
widget.message.text!.isOnlyEmoji ? 0 : 16.0,
|
|
||||||
),
|
|
||||||
showReactionPickerIndicator:
|
|
||||||
widget.showReactions &&
|
|
||||||
(widget.message.status ==
|
|
||||||
MessageSendingStatus.sent),
|
|
||||||
showSendingIndicator: false,
|
|
||||||
shape: widget.messageShape,
|
|
||||||
attachmentShape: widget.attachmentShape,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: widget.reverse ? 0 : 40,
|
|
||||||
),
|
|
||||||
child: SizedBox(
|
|
||||||
width: mediaQueryData.size.width * 0.75,
|
|
||||||
child: Material(
|
|
||||||
color: streamChatThemeData.colorTheme.whiteSnow,
|
|
||||||
clipBehavior: Clip.hardEdge,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
if (widget.showReplyMessage &&
|
|
||||||
widget.message.status ==
|
|
||||||
MessageSendingStatus.sent)
|
|
||||||
_buildReplyButton(context),
|
|
||||||
if (widget.showThreadReplyMessage &&
|
|
||||||
(widget.message.status ==
|
|
||||||
MessageSendingStatus.sent) &&
|
|
||||||
widget.message.parentId == null)
|
|
||||||
_buildThreadReplyButton(context),
|
|
||||||
if (widget.showResendMessage)
|
|
||||||
_buildResendMessage(context),
|
|
||||||
if (widget.showEditMessage)
|
|
||||||
_buildEditMessage(context),
|
|
||||||
if (widget.showCopyMessage)
|
|
||||||
_buildCopyButton(context),
|
|
||||||
if (widget.showFlagButton)
|
|
||||||
_buildFlagButton(context),
|
|
||||||
if (widget.showDeleteMessage)
|
|
||||||
_buildDeleteButton(context),
|
|
||||||
...widget.customActions
|
|
||||||
.map((action) => _buildCustomAction(
|
|
||||||
context,
|
|
||||||
action,
|
|
||||||
))
|
|
||||||
].insertBetween(
|
|
||||||
Container(
|
|
||||||
height: 1,
|
|
||||||
color: streamChatThemeData
|
|
||||||
.colorTheme.greyWhisper,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
child: child,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -360,6 +372,21 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _togglePin() async {
|
||||||
|
final channel = StreamChannel.of(context).channel;
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
try {
|
||||||
|
if (!widget.message.pinned) {
|
||||||
|
await channel.pinMessage(widget.message);
|
||||||
|
} else {
|
||||||
|
await channel.unpinMessage(widget.message);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
_showErrorAlert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _showDeleteDialog() async {
|
void _showDeleteDialog() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showActions = false;
|
_showActions = false;
|
||||||
@@ -452,6 +479,29 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildPinButton(BuildContext context) {
|
||||||
|
final streamChatThemeData = StreamChatTheme.of(context);
|
||||||
|
return InkWell(
|
||||||
|
onTap: _togglePin,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 11, horizontal: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
StreamSvgIcon.pin(
|
||||||
|
color: streamChatThemeData.primaryIconTheme.color,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Text(
|
||||||
|
'${widget.message.pinned ? 'Unpin from' : 'Pin to'} Conversation',
|
||||||
|
style: streamChatThemeData.textTheme.body,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDeleteButton(BuildContext context) {
|
Widget _buildDeleteButton(BuildContext context) {
|
||||||
final isDeleteFailed =
|
final isDeleteFailed =
|
||||||
widget.message.status == MessageSendingStatus.failed_delete;
|
widget.message.status == MessageSendingStatus.failed_delete;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:jiffy/jiffy.dart';
|
import 'package:jiffy/jiffy.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
@@ -156,8 +157,12 @@ class MessageListView extends StatefulWidget {
|
|||||||
this.onMessageTap,
|
this.onMessageTap,
|
||||||
this.onSystemMessageTap,
|
this.onSystemMessageTap,
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
this.textBuilder,
|
|
||||||
this.onLinkTap,
|
this.onLinkTap,
|
||||||
|
this.pinPermissions = const [],
|
||||||
|
this.textBuilder,
|
||||||
|
this.usernameBuilder,
|
||||||
|
this.showFloatingDateDivider = true,
|
||||||
|
this.threadSeparatorBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Function used to build a custom message widget
|
/// Function used to build a custom message widget
|
||||||
@@ -224,6 +229,9 @@ class MessageListView extends StatefulWidget {
|
|||||||
/// Flag for showing tile on header
|
/// Flag for showing tile on header
|
||||||
final bool showConnectionStateTile;
|
final bool showConnectionStateTile;
|
||||||
|
|
||||||
|
/// Flag for showing the floating date divider
|
||||||
|
final bool showFloatingDateDivider;
|
||||||
|
|
||||||
/// Function called when messages are fetched
|
/// Function called when messages are fetched
|
||||||
final Widget Function(BuildContext, List<Message>)? messageListBuilder;
|
final Widget Function(BuildContext, List<Message>)? messageListBuilder;
|
||||||
|
|
||||||
@@ -259,11 +267,20 @@ class MessageListView extends StatefulWidget {
|
|||||||
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
||||||
|
|
||||||
/// Customize the MessageWidget textBuilder
|
/// Customize the MessageWidget textBuilder
|
||||||
final void Function(BuildContext context, Message message)? textBuilder;
|
final Widget Function(BuildContext context, Message message)? textBuilder;
|
||||||
|
|
||||||
|
/// Customize the MessageWidget usernameBuilder
|
||||||
|
final Widget Function(BuildContext context, Message message)? usernameBuilder;
|
||||||
|
|
||||||
/// Callback for when link is tapped
|
/// Callback for when link is tapped
|
||||||
final void Function(String link)? onLinkTap;
|
final void Function(String link)? onLinkTap;
|
||||||
|
|
||||||
|
/// A List of user types that have permission to pin messages
|
||||||
|
final List<String> pinPermissions;
|
||||||
|
|
||||||
|
/// Builder used to build the thread separator in case it's a thread view
|
||||||
|
final WidgetBuilder? threadSeparatorBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MessageListViewState createState() => _MessageListViewState();
|
_MessageListViewState createState() => _MessageListViewState();
|
||||||
}
|
}
|
||||||
@@ -273,8 +290,10 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
void Function(Message)? _onThreadTap;
|
void Function(Message)? _onThreadTap;
|
||||||
bool _showScrollToBottom = false;
|
bool _showScrollToBottom = false;
|
||||||
late final ItemPositionsListener _itemPositionListener;
|
late final ItemPositionsListener _itemPositionListener;
|
||||||
|
late final Stream<Iterable<ItemPosition>> _itemPositionStream;
|
||||||
int? _messageListLength;
|
int? _messageListLength;
|
||||||
StreamChannelState? streamChannel;
|
StreamChannelState? streamChannel;
|
||||||
|
late StreamChatThemeData _streamTheme;
|
||||||
|
|
||||||
int? get _initialIndex {
|
int? get _initialIndex {
|
||||||
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
|
if (widget.initialScrollIndex != null) return widget.initialScrollIndex;
|
||||||
@@ -316,37 +335,34 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final MessageListController _messageListController = MessageListController();
|
final MessageListController _messageListController = MessageListController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) => MessageListCore(
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
messageFilter: widget.messageFilter,
|
||||||
return MessageListCore(
|
loadingBuilder: widget.loadingBuilder ??
|
||||||
messageFilter: widget.messageFilter,
|
(context) => const Center(
|
||||||
loadingBuilder: widget.loadingBuilder ??
|
child: CircularProgressIndicator(),
|
||||||
(context) => const Center(
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
emptyBuilder: widget.emptyBuilder ??
|
|
||||||
(context) => Center(
|
|
||||||
child: Text(
|
|
||||||
'No chats here yet...',
|
|
||||||
style: chatThemeData.textTheme.footnote.copyWith(
|
|
||||||
color: chatThemeData.colorTheme.black.withOpacity(.5)),
|
|
||||||
),
|
),
|
||||||
),
|
emptyBuilder: widget.emptyBuilder ??
|
||||||
messageListBuilder:
|
(context) => Center(
|
||||||
widget.messageListBuilder ?? (context, list) => _buildListView(list),
|
child: Text(
|
||||||
messageListController: _messageListController,
|
'No chats here yet...',
|
||||||
parentMessage: widget.parentMessage,
|
style: _streamTheme.textTheme.footnote.copyWith(
|
||||||
showScrollToBottom: widget.showScrollToBottom,
|
color: _streamTheme.colorTheme.black.withOpacity(.5)),
|
||||||
errorWidgetBuilder: widget.errorWidgetBuilder ??
|
),
|
||||||
(BuildContext context, Object error) => Center(
|
|
||||||
child: Text(
|
|
||||||
'Something went wrong',
|
|
||||||
style: chatThemeData.textTheme.footnote.copyWith(
|
|
||||||
color: chatThemeData.colorTheme.black.withOpacity(.5)),
|
|
||||||
),
|
),
|
||||||
),
|
messageListBuilder: widget.messageListBuilder ??
|
||||||
);
|
(context, list) => _buildListView(list),
|
||||||
}
|
messageListController: _messageListController,
|
||||||
|
parentMessage: widget.parentMessage,
|
||||||
|
showScrollToBottom: widget.showScrollToBottom,
|
||||||
|
errorWidgetBuilder: widget.errorWidgetBuilder ??
|
||||||
|
(BuildContext context, Object error) => Center(
|
||||||
|
child: Text(
|
||||||
|
'Something went wrong',
|
||||||
|
style: _streamTheme.textTheme.footnote.copyWith(
|
||||||
|
color: _streamTheme.colorTheme.black.withOpacity(.5)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Widget _buildListView(List<Message> data) {
|
Widget _buildListView(List<Message> data) {
|
||||||
messages = data;
|
messages = data;
|
||||||
@@ -392,8 +408,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return InfoTile(
|
return InfoTile(
|
||||||
// ignore: avoid_bool_literals_in_conditional_expressions
|
showMessage: widget.showConnectionStateTile && showStatus,
|
||||||
showMessage: widget.showConnectionStateTile ? showStatus : false,
|
|
||||||
tileAnchor: Alignment.topCenter,
|
tileAnchor: Alignment.topCenter,
|
||||||
childAnchor: Alignment.topCenter,
|
childAnchor: Alignment.topCenter,
|
||||||
message: statusString,
|
message: statusString,
|
||||||
@@ -432,29 +447,14 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
physics: widget.scrollPhysics,
|
physics: widget.scrollPhysics,
|
||||||
itemScrollController: _scrollController,
|
itemScrollController: _scrollController,
|
||||||
reverse: true,
|
reverse: true,
|
||||||
|
addAutomaticKeepAlives: false,
|
||||||
itemCount:
|
itemCount:
|
||||||
messages.length + 2 + (_isThreadConversation ? 1 : 0),
|
messages.length + 2 + (_isThreadConversation ? 1 : 0),
|
||||||
separatorBuilder: (context, i) {
|
separatorBuilder: (context, i) {
|
||||||
if (i == messages.length) return const Offstage();
|
if (i == messages.length) return const Offstage();
|
||||||
if (i == 0) return const SizedBox(height: 30);
|
if (i == 0) return const SizedBox(height: 30);
|
||||||
if (i == messages.length + 1) {
|
if (i == messages.length + 1) {
|
||||||
final replyCount = widget.parentMessage!.replyCount;
|
return _buildThreadSeparator();
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: chatThemeData.colorTheme.bgGradient,
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Text(
|
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: chatThemeData
|
|
||||||
.channelTheme.channelHeaderTheme.subtitle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final message = messages[i];
|
final message = messages[i];
|
||||||
@@ -560,40 +560,73 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
if (widget.showScrollToBottom) _buildScrollToBottom(),
|
||||||
Positioned(
|
if (widget.showFloatingDateDivider) _buildFloatingDateDivider(),
|
||||||
top: 20,
|
|
||||||
child: ValueListenableBuilder<Iterable<ItemPosition>>(
|
|
||||||
valueListenable: _itemPositionListener.itemPositions,
|
|
||||||
builder: (context, values, _) {
|
|
||||||
final items = _itemPositionListener.itemPositions.value;
|
|
||||||
if (items.isEmpty || messages.isEmpty) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
var index = _getTopElement(values)?.index;
|
|
||||||
|
|
||||||
if (index == null || index > messages.length) {
|
|
||||||
return const SizedBox();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (index == messages.length) {
|
|
||||||
index = max(index - 1, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return widget.dateDividerBuilder != null
|
|
||||||
? widget.dateDividerBuilder!(
|
|
||||||
messages[index].createdAt.toLocal(),
|
|
||||||
)
|
|
||||||
: DateDivider(
|
|
||||||
dateTime: messages[index].createdAt.toLocal(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildThreadSeparator() {
|
||||||
|
if (widget.threadSeparatorBuilder != null) {
|
||||||
|
return widget.threadSeparatorBuilder!.call(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
final replyCount = widget.parentMessage!.replyCount;
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: _streamTheme.colorTheme.bgGradient,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: Text(
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
'$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: _streamTheme.channelTheme.channelHeaderTheme.subtitle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Positioned _buildFloatingDateDivider() => Positioned(
|
||||||
|
top: 20,
|
||||||
|
child: BetterStreamBuilder<Iterable<ItemPosition>>(
|
||||||
|
initialData: _itemPositionListener.itemPositions.value,
|
||||||
|
stream: _itemPositionStream,
|
||||||
|
comparator: (a, b) {
|
||||||
|
if (a == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final aTop = _getTopElement(a)?.index;
|
||||||
|
final bTop = _getTopElement(b)?.index;
|
||||||
|
return aTop == bTop;
|
||||||
|
},
|
||||||
|
builder: (context, values) {
|
||||||
|
final items = _itemPositionListener.itemPositions.value;
|
||||||
|
if (items.isEmpty || messages.isEmpty) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = _getTopElement(values)?.index;
|
||||||
|
|
||||||
|
if (index == null || index > messages.length) {
|
||||||
|
return const SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index == messages.length) {
|
||||||
|
index = max(index - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget.dateDividerBuilder != null
|
||||||
|
? widget.dateDividerBuilder!(
|
||||||
|
messages[index].createdAt.toLocal(),
|
||||||
|
)
|
||||||
|
: DateDivider(
|
||||||
|
dateTime: messages[index].createdAt.toLocal(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
Future<void> _paginateData(
|
Future<void> _paginateData(
|
||||||
StreamChannelState? channel, QueryDirection direction) =>
|
StreamChannelState? channel, QueryDirection direction) =>
|
||||||
_messageListController.paginateData!(direction: direction);
|
_messageListController.paginateData!(direction: direction);
|
||||||
@@ -612,8 +645,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
|
|
||||||
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
Widget _buildScrollToBottom() => StreamBuilder<Tuple2<bool, int>>(
|
||||||
stream: Rx.combineLatest2(
|
stream: Rx.combineLatest2(
|
||||||
streamChannel!.channel.state!.isUpToDateStream,
|
streamChannel!.channel.state!.isUpToDateStream.distinct(),
|
||||||
streamChannel!.channel.state!.unreadCountStream,
|
streamChannel!.channel.state!.unreadCountStream.distinct(),
|
||||||
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
(bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount),
|
||||||
),
|
),
|
||||||
builder: (_, snapshot) {
|
builder: (_, snapshot) {
|
||||||
@@ -631,7 +664,6 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
final showUnreadCount = unreadCount > 0 &&
|
final showUnreadCount = unreadCount > 0 &&
|
||||||
streamChannel!.channel.state!.members.any((e) =>
|
streamChannel!.channel.state!.members.any((e) =>
|
||||||
e.userId == streamChannel!.channel.client.state.user!.id);
|
e.userId == streamChannel!.channel.client.state.user!.id);
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
|
||||||
return Positioned(
|
return Positioned(
|
||||||
bottom: 8,
|
bottom: 8,
|
||||||
right: 8,
|
right: 8,
|
||||||
@@ -641,7 +673,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
backgroundColor: chatThemeData.colorTheme.white,
|
backgroundColor: _streamTheme.colorTheme.white,
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (unreadCount > 0) {
|
if (unreadCount > 0) {
|
||||||
streamChannel!.channel.markRead();
|
streamChannel!.channel.markRead();
|
||||||
@@ -660,7 +692,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: StreamSvgIcon.down(
|
child: StreamSvgIcon.down(
|
||||||
color: chatThemeData.colorTheme.black,
|
color: _streamTheme.colorTheme.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showUnreadCount)
|
if (showUnreadCount)
|
||||||
@@ -691,44 +723,13 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
Widget _buildLoadingIndicator(
|
Widget _buildLoadingIndicator(
|
||||||
StreamChannelState streamChannel,
|
StreamChannelState streamChannel,
|
||||||
QueryDirection direction,
|
QueryDirection direction,
|
||||||
) {
|
) =>
|
||||||
final stream = direction == QueryDirection.top
|
_LoadingIndicator(
|
||||||
? streamChannel.queryTopMessages
|
direction: direction,
|
||||||
: streamChannel.queryBottomMessages;
|
streamTheme: _streamTheme,
|
||||||
return StreamBuilder<bool>(
|
streamChannel: streamChannel,
|
||||||
key: const Key('LOADING-INDICATOR'),
|
isThreadConversation: _isThreadConversation,
|
||||||
stream: stream,
|
);
|
||||||
initialData: false,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
return Container(
|
|
||||||
color: StreamChatTheme.of(context)
|
|
||||||
.colorTheme
|
|
||||||
.accentRed
|
|
||||||
.withOpacity(.2),
|
|
||||||
child: const Center(
|
|
||||||
child: Text('Error loading messages'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!snapshot.data!) {
|
|
||||||
if (!_isThreadConversation && direction == QueryDirection.top) {
|
|
||||||
return const SizedBox(
|
|
||||||
height: 52,
|
|
||||||
width: double.infinity,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return const Offstage();
|
|
||||||
}
|
|
||||||
return const Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.all(8),
|
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTopMessage(
|
Widget _buildTopMessage(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -795,7 +796,9 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() => _showScrollToBottom = !isVisible);
|
if (_showScrollToBottom == isVisible) {
|
||||||
|
setState(() => _showScrollToBottom = !isVisible);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: messageWidget,
|
child: messageWidget,
|
||||||
@@ -807,8 +810,11 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
) {
|
) {
|
||||||
final isMyMessage = message.user!.id == StreamChat.of(context).user!.id;
|
final isMyMessage = message.user!.id == StreamChat.of(context).user!.id;
|
||||||
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
final isOnlyEmoji = message.text!.isOnlyEmoji;
|
||||||
|
final currentUser = StreamChat.of(context).user;
|
||||||
|
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
||||||
|
final currentUserMember =
|
||||||
|
members.firstWhere((e) => e.user!.id == currentUser!.id);
|
||||||
|
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
|
||||||
return MessageWidget(
|
return MessageWidget(
|
||||||
showReplyMessage: false,
|
showReplyMessage: false,
|
||||||
showResendMessage: false,
|
showResendMessage: false,
|
||||||
@@ -837,8 +843,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null,
|
borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null,
|
||||||
showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show,
|
showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show,
|
||||||
messageTheme: isMyMessage
|
messageTheme: isMyMessage
|
||||||
? chatThemeData.ownMessageTheme
|
? _streamTheme.ownMessageTheme
|
||||||
: chatThemeData.otherMessageTheme,
|
: _streamTheme.otherMessageTheme,
|
||||||
onShowMessage: widget.onShowMessage,
|
onShowMessage: widget.onShowMessage,
|
||||||
onReturnAction: (action) {
|
onReturnAction: (action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -857,9 +863,10 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
}
|
}
|
||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
},
|
},
|
||||||
textBuilder:
|
textBuilder: widget.textBuilder,
|
||||||
widget.textBuilder as Widget Function(BuildContext, Message)?,
|
usernameBuilder: widget.usernameBuilder,
|
||||||
onLinkTap: widget.onLinkTap,
|
onLinkTap: widget.onLinkTap,
|
||||||
|
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -946,7 +953,11 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
? BorderSide.none
|
? BorderSide.none
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final currentUser = StreamChat.of(context).user;
|
||||||
|
final members = StreamChannel.of(context).channel.state?.members ?? [];
|
||||||
|
final currentUserMember =
|
||||||
|
members.firstWhere((e) => e.user!.id == currentUser!.id);
|
||||||
|
|
||||||
Widget child = MessageWidget(
|
Widget child = MessageWidget(
|
||||||
key: ValueKey<String>('MESSAGE-${message.id}'),
|
key: ValueKey<String>('MESSAGE-${message.id}'),
|
||||||
message: message,
|
message: message,
|
||||||
@@ -1033,8 +1044,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
horizontal: isOnlyEmoji ? 0 : 16.0,
|
horizontal: isOnlyEmoji ? 0 : 16.0,
|
||||||
),
|
),
|
||||||
messageTheme: isMyMessage
|
messageTheme: isMyMessage
|
||||||
? chatThemeData.ownMessageTheme
|
? _streamTheme.ownMessageTheme
|
||||||
: chatThemeData.otherMessageTheme,
|
: _streamTheme.otherMessageTheme,
|
||||||
readList: readList,
|
readList: readList,
|
||||||
allRead: allRead,
|
allRead: allRead,
|
||||||
onShowMessage: widget.onShowMessage,
|
onShowMessage: widget.onShowMessage,
|
||||||
@@ -1056,9 +1067,10 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
FocusScope.of(context).unfocus();
|
FocusScope.of(context).unfocus();
|
||||||
},
|
},
|
||||||
onAttachmentTap: widget.onAttachmentTap,
|
onAttachmentTap: widget.onAttachmentTap,
|
||||||
textBuilder:
|
textBuilder: widget.textBuilder,
|
||||||
widget.textBuilder as Widget Function(BuildContext, Message)?,
|
usernameBuilder: widget.usernameBuilder,
|
||||||
onLinkTap: widget.onLinkTap,
|
onLinkTap: widget.onLinkTap,
|
||||||
|
showPinButton: widget.pinPermissions.contains(currentUserMember.role),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!message.isDeleted &&
|
if (!message.isDeleted &&
|
||||||
@@ -1074,7 +1086,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
widget.onMessageSwiped?.call(message);
|
widget.onMessageSwiped?.call(message);
|
||||||
},
|
},
|
||||||
backgroundIcon: StreamSvgIcon.reply(
|
backgroundIcon: StreamSvgIcon.reply(
|
||||||
color: chatThemeData.colorTheme.accentBlue,
|
color: _streamTheme.colorTheme.accentBlue,
|
||||||
),
|
),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
@@ -1084,7 +1096,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
if (!initialMessageHighlightComplete &&
|
if (!initialMessageHighlightComplete &&
|
||||||
widget.highlightInitialMessage &&
|
widget.highlightInitialMessage &&
|
||||||
_isInitialMessage(message.id)) {
|
_isInitialMessage(message.id)) {
|
||||||
final colorTheme = chatThemeData.colorTheme;
|
final colorTheme = _streamTheme.colorTheme;
|
||||||
final highlightColor =
|
final highlightColor =
|
||||||
widget.messageHighlightColor ?? colorTheme.highlight;
|
widget.messageHighlightColor ?? colorTheme.highlight;
|
||||||
child = TweenAnimationBuilder<Color?>(
|
child = TweenAnimationBuilder<Color?>(
|
||||||
@@ -1114,6 +1126,8 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
_scrollController = widget.scrollController ?? ItemScrollController();
|
_scrollController = widget.scrollController ?? ItemScrollController();
|
||||||
_itemPositionListener =
|
_itemPositionListener =
|
||||||
widget.itemPositionListener ?? ItemPositionsListener.create();
|
widget.itemPositionListener ?? ItemPositionsListener.create();
|
||||||
|
_itemPositionStream =
|
||||||
|
_valueListenableToStreamAdapter(_itemPositionListener.itemPositions);
|
||||||
|
|
||||||
_getOnThreadTap();
|
_getOnThreadTap();
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -1122,6 +1136,7 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
final newStreamChannel = StreamChannel.of(context);
|
final newStreamChannel = StreamChannel.of(context);
|
||||||
|
_streamTheme = StreamChatTheme.of(context);
|
||||||
|
|
||||||
if (newStreamChannel != streamChannel) {
|
if (newStreamChannel != streamChannel) {
|
||||||
streamChannel = newStreamChannel;
|
streamChannel = newStreamChannel;
|
||||||
@@ -1167,14 +1182,14 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (_) => StreamBuilder<Message>(
|
builder: (_) => BetterStreamBuilder<Message>(
|
||||||
stream: streamChannel!.channel.state!.messagesStream.map(
|
stream: streamChannel!.channel.state!.messagesStream.map(
|
||||||
(messages) =>
|
(messages) =>
|
||||||
messages!.firstWhere((m) => m.id == message.id)),
|
messages!.firstWhere((m) => m.id == message.id)),
|
||||||
initialData: message,
|
initialData: message,
|
||||||
builder: (_, snapshot) => StreamChannel(
|
builder: (_, data) => StreamChannel(
|
||||||
channel: streamChannel!.channel,
|
channel: streamChannel!.channel,
|
||||||
child: widget.threadBuilder!(context, snapshot.data),
|
child: widget.threadBuilder!(context, data),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1192,3 +1207,79 @@ class _MessageListViewState extends State<MessageListView> {
|
|||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _LoadingIndicator extends StatelessWidget {
|
||||||
|
const _LoadingIndicator({
|
||||||
|
Key? key,
|
||||||
|
required this.streamTheme,
|
||||||
|
required this.isThreadConversation,
|
||||||
|
required this.direction,
|
||||||
|
required this.streamChannel,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
final StreamChatThemeData streamTheme;
|
||||||
|
final bool isThreadConversation;
|
||||||
|
final QueryDirection direction;
|
||||||
|
final StreamChannelState streamChannel;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final stream = direction == QueryDirection.top
|
||||||
|
? streamChannel.queryTopMessages
|
||||||
|
: streamChannel.queryBottomMessages;
|
||||||
|
return BetterStreamBuilder<bool>(
|
||||||
|
key: Key('LOADING-INDICATOR $direction'),
|
||||||
|
stream: stream,
|
||||||
|
initialData: false,
|
||||||
|
errorBuilder: (context, error) => Container(
|
||||||
|
color: streamTheme.colorTheme.accentRed.withOpacity(.2),
|
||||||
|
child: const Center(
|
||||||
|
child: Text('Error loading messages'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
builder: (context, data) {
|
||||||
|
if (!data) {
|
||||||
|
if (!isThreadConversation && direction == QueryDirection.top) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 52,
|
||||||
|
width: double.infinity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const Offstage();
|
||||||
|
}
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<T> _valueListenableToStreamAdapter<T>(ValueListenable<T> listenable) {
|
||||||
|
// ignore: close_sinks
|
||||||
|
late StreamController<T> _controller;
|
||||||
|
|
||||||
|
void listener() {
|
||||||
|
_controller.add(listenable.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void start() {
|
||||||
|
listenable.addListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
void end() {
|
||||||
|
listenable.removeListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
_controller = StreamController<T>(
|
||||||
|
onListen: start,
|
||||||
|
onPause: end,
|
||||||
|
onResume: start,
|
||||||
|
onCancel: end,
|
||||||
|
);
|
||||||
|
|
||||||
|
return _controller.stream;
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
this.showUserAvatar = DisplayWidget.show,
|
this.showUserAvatar = DisplayWidget.show,
|
||||||
this.onUserAvatarTap,
|
this.onUserAvatarTap,
|
||||||
this.attachmentBorderRadiusGeometry,
|
this.attachmentBorderRadiusGeometry,
|
||||||
|
this.textBuilder,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
/// Message to display reactions of
|
/// Message to display reactions of
|
||||||
@@ -52,6 +53,9 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
/// [BorderRadius] to apply to attachments
|
/// [BorderRadius] to apply to attachments
|
||||||
final BorderRadius? attachmentBorderRadiusGeometry;
|
final BorderRadius? attachmentBorderRadiusGeometry;
|
||||||
|
|
||||||
|
/// Customize the MessageWidget textBuilder
|
||||||
|
final Widget Function(BuildContext context, Message message)? textBuilder;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final size = MediaQuery.of(context).size;
|
final size = MediaQuery.of(context).size;
|
||||||
@@ -73,111 +77,119 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
final divFactor = message.attachments.isNotEmpty == true
|
final divFactor = message.attachments.isNotEmpty == true
|
||||||
? 1
|
? 1
|
||||||
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
: (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize));
|
||||||
|
final hasFileAttachment =
|
||||||
|
message.attachments.any((it) => it.type == 'file') == true;
|
||||||
|
|
||||||
return TweenAnimationBuilder<double>(
|
final numberOfReactions = StreamChatTheme.of(context).reactionIcons.length;
|
||||||
tween: Tween(begin: 0, end: 1),
|
final shiftFactor =
|
||||||
duration: const Duration(milliseconds: 300),
|
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0;
|
||||||
curve: Curves.easeInOutBack,
|
|
||||||
builder: (context, val, snapshot) {
|
final child = Center(
|
||||||
final hasFileAttachment =
|
child: SingleChildScrollView(
|
||||||
message.attachments.any((it) => it.type == 'file') == true;
|
child: Padding(
|
||||||
return GestureDetector(
|
padding: const EdgeInsets.all(8),
|
||||||
behavior: HitTestBehavior.translucent,
|
child: Column(
|
||||||
onTap: () => Navigator.maybePop(context),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: Stack(
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Positioned.fill(
|
if (showReactions &&
|
||||||
child: BackdropFilter(
|
(message.status == MessageSendingStatus.sent))
|
||||||
filter: ImageFilter.blur(
|
Align(
|
||||||
sigmaX: 10,
|
alignment: Alignment(
|
||||||
sigmaY: 10,
|
user!.id == message.user!.id
|
||||||
),
|
? (divFactor >= 1.0
|
||||||
child: Container(
|
? -0.2 - shiftFactor
|
||||||
color: StreamChatTheme.of(context).colorTheme.overlay,
|
: (1.2 - divFactor))
|
||||||
|
: (divFactor >= 1.0
|
||||||
|
? 0.2 + shiftFactor
|
||||||
|
: -(1.2 - divFactor)),
|
||||||
|
0),
|
||||||
|
child: ReactionPicker(
|
||||||
|
message: message,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
Transform.scale(
|
IgnorePointer(
|
||||||
scale: val,
|
child: MessageWidget(
|
||||||
child: Center(
|
key: const Key('MessageWidget'),
|
||||||
child: SingleChildScrollView(
|
reverse: reverse,
|
||||||
child: Padding(
|
message: message.copyWith(
|
||||||
padding: const EdgeInsets.all(8),
|
text: message.text!.length > 200
|
||||||
child: Column(
|
? '${message.text!.substring(0, 200)}...'
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
: message.text,
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: <Widget>[
|
|
||||||
if (showReactions &&
|
|
||||||
(message.status == MessageSendingStatus.sent))
|
|
||||||
Align(
|
|
||||||
alignment: Alignment(
|
|
||||||
user!.id == message.user!.id
|
|
||||||
? (divFactor >= 1.0
|
|
||||||
? -0.2
|
|
||||||
: (1.2 - divFactor))
|
|
||||||
: (divFactor >= 1.0
|
|
||||||
? 0.2
|
|
||||||
: -(1.2 - divFactor)),
|
|
||||||
0),
|
|
||||||
child: ReactionPicker(
|
|
||||||
message: message,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
IgnorePointer(
|
|
||||||
child: MessageWidget(
|
|
||||||
key: const Key('MessageWidget'),
|
|
||||||
reverse: reverse,
|
|
||||||
message: message.copyWith(
|
|
||||||
text: message.text!.length > 200
|
|
||||||
? '${message.text!.substring(0, 200)}...'
|
|
||||||
: message.text,
|
|
||||||
),
|
|
||||||
messageTheme: messageTheme,
|
|
||||||
showReactions: false,
|
|
||||||
showUsername: false,
|
|
||||||
showUserAvatar: showUserAvatar,
|
|
||||||
showTimestamp: false,
|
|
||||||
translateUserAvatar: false,
|
|
||||||
showSendingIndicator: false,
|
|
||||||
shape: messageShape,
|
|
||||||
attachmentShape: attachmentShape,
|
|
||||||
padding: const EdgeInsets.all(0),
|
|
||||||
attachmentBorderRadiusGeometry:
|
|
||||||
attachmentBorderRadiusGeometry
|
|
||||||
?.mirrorBorderIfReversed(
|
|
||||||
reverse: !reverse),
|
|
||||||
attachmentPadding: EdgeInsets.all(
|
|
||||||
hasFileAttachment ? 4 : 2,
|
|
||||||
),
|
|
||||||
textPadding: EdgeInsets.symmetric(
|
|
||||||
vertical: 8,
|
|
||||||
horizontal:
|
|
||||||
message.text!.isOnlyEmoji ? 0 : 16.0,
|
|
||||||
),
|
|
||||||
showReactionPickerIndicator: showReactions &&
|
|
||||||
(message.status == MessageSendingStatus.sent),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (message.latestReactions?.isNotEmpty == true) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
_buildReactionCard(context),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
messageTheme: messageTheme,
|
||||||
|
showReactions: false,
|
||||||
|
showUsername: false,
|
||||||
|
showUserAvatar: showUserAvatar,
|
||||||
|
showTimestamp: false,
|
||||||
|
translateUserAvatar: false,
|
||||||
|
showSendingIndicator: false,
|
||||||
|
shape: messageShape,
|
||||||
|
attachmentShape: attachmentShape,
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry
|
||||||
|
?.mirrorBorderIfReversed(reverse: !reverse),
|
||||||
|
attachmentPadding: EdgeInsets.all(
|
||||||
|
hasFileAttachment ? 4 : 2,
|
||||||
|
),
|
||||||
|
textPadding: EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: message.text!.isOnlyEmoji ? 0 : 16.0,
|
||||||
|
),
|
||||||
|
showReactionPickerIndicator: showReactions &&
|
||||||
|
(message.status == MessageSendingStatus.sent),
|
||||||
|
textBuilder: textBuilder,
|
||||||
|
showPinHighlight: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (message.latestReactions?.isNotEmpty == true) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
_buildReactionCard(
|
||||||
|
context,
|
||||||
|
user,
|
||||||
|
),
|
||||||
|
]
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: () => Navigator.maybePop(context),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(
|
||||||
|
sigmaX: 10,
|
||||||
|
sigmaY: 10,
|
||||||
|
),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: StreamChatTheme.of(context).colorTheme.overlay,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0, end: 1),
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOutBack,
|
||||||
|
builder: (context, val, widget) => Transform.scale(
|
||||||
|
scale: val,
|
||||||
|
child: widget,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildReactionCard(BuildContext context) {
|
Widget _buildReactionCard(BuildContext context, User? user) {
|
||||||
final currentUser = StreamChat.of(context).user;
|
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = StreamChatTheme.of(context);
|
||||||
return Card(
|
return Card(
|
||||||
color: chatThemeData.colorTheme.white,
|
color: chatThemeData.colorTheme.white,
|
||||||
@@ -204,7 +216,7 @@ class MessageReactionsModal extends StatelessWidget {
|
|||||||
children: message.latestReactions!
|
children: message.latestReactions!
|
||||||
.map((e) => _buildReaction(
|
.map((e) => _buildReaction(
|
||||||
e,
|
e,
|
||||||
currentUser!,
|
user!,
|
||||||
context,
|
context,
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.showResendMessage = true,
|
this.showResendMessage = true,
|
||||||
this.showCopyMessage = true,
|
this.showCopyMessage = true,
|
||||||
this.showFlagButton = true,
|
this.showFlagButton = true,
|
||||||
|
this.showPinButton = true,
|
||||||
|
this.showPinHighlight = true,
|
||||||
this.onUserAvatarTap,
|
this.onUserAvatarTap,
|
||||||
this.onLinkTap,
|
this.onLinkTap,
|
||||||
this.onMessageActions,
|
this.onMessageActions,
|
||||||
@@ -102,6 +104,7 @@ class MessageWidget extends StatefulWidget {
|
|||||||
this.onQuotedMessageTap,
|
this.onQuotedMessageTap,
|
||||||
this.customActions = const [],
|
this.customActions = const [],
|
||||||
this.onAttachmentTap,
|
this.onAttachmentTap,
|
||||||
|
this.usernameBuilder,
|
||||||
}) : attachmentBuilders = {
|
}) : attachmentBuilders = {
|
||||||
'image': (context, message, attachments) {
|
'image': (context, message, attachments) {
|
||||||
final border = RoundedRectangleBorder(
|
final border = RoundedRectangleBorder(
|
||||||
@@ -265,6 +268,9 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// Widget builder for building text
|
/// Widget builder for building text
|
||||||
final Widget Function(BuildContext, Message)? textBuilder;
|
final Widget Function(BuildContext, Message)? textBuilder;
|
||||||
|
|
||||||
|
/// Widget builder for building username
|
||||||
|
final Widget Function(BuildContext, Message)? usernameBuilder;
|
||||||
|
|
||||||
/// Function called on long press
|
/// Function called on long press
|
||||||
final void Function(BuildContext, Message)? onMessageActions;
|
final void Function(BuildContext, Message)? onMessageActions;
|
||||||
|
|
||||||
@@ -367,6 +373,12 @@ class MessageWidget extends StatefulWidget {
|
|||||||
/// Show flag action
|
/// Show flag action
|
||||||
final bool showFlagButton;
|
final bool showFlagButton;
|
||||||
|
|
||||||
|
/// Show flag action
|
||||||
|
final bool showPinButton;
|
||||||
|
|
||||||
|
/// Display Pin Highlight
|
||||||
|
final bool showPinHighlight;
|
||||||
|
|
||||||
/// Builder for respective attachment types
|
/// Builder for respective attachment types
|
||||||
final Map<String, AttachmentBuilder> attachmentBuilders;
|
final Map<String, AttachmentBuilder> attachmentBuilders;
|
||||||
|
|
||||||
@@ -401,37 +413,39 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
|
|
||||||
bool get showTimeStamp => widget.showTimestamp;
|
bool get showTimeStamp => widget.showTimestamp;
|
||||||
|
|
||||||
bool get isMessageRead => widget.readList?.isNotEmpty == true;
|
late final bool isMessageRead = widget.readList?.isNotEmpty == true;
|
||||||
|
|
||||||
bool get showInChannel => widget.showInChannelIndicator;
|
bool get showInChannel => widget.showInChannelIndicator;
|
||||||
|
|
||||||
bool get hasQuotedMessage => widget.message.quotedMessage != null;
|
bool get hasQuotedMessage => widget.message.quotedMessage != null;
|
||||||
|
|
||||||
bool get isSendFailed => widget.message.status == MessageSendingStatus.failed;
|
late final bool isSendFailed =
|
||||||
|
widget.message.status == MessageSendingStatus.failed;
|
||||||
|
|
||||||
bool get isUpdateFailed =>
|
late final bool isUpdateFailed =
|
||||||
widget.message.status == MessageSendingStatus.failed_update;
|
widget.message.status == MessageSendingStatus.failed_update;
|
||||||
|
|
||||||
bool get isDeleteFailed =>
|
late final bool isDeleteFailed =
|
||||||
widget.message.status == MessageSendingStatus.failed_delete;
|
widget.message.status == MessageSendingStatus.failed_delete;
|
||||||
|
|
||||||
bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed;
|
late final bool isFailedState =
|
||||||
|
isSendFailed || isUpdateFailed || isDeleteFailed;
|
||||||
|
|
||||||
bool get isGiphy =>
|
late final bool isGiphy =
|
||||||
widget.message.attachments.any((element) => element.type == 'giphy') ==
|
widget.message.attachments.any((element) => element.type == 'giphy') ==
|
||||||
true;
|
true;
|
||||||
|
|
||||||
bool get hasNonUrlAttachments =>
|
late final bool isOnlyEmoji = widget.message.text?.isOnlyEmoji == true;
|
||||||
widget.message.attachments
|
|
||||||
|
late final bool hasNonUrlAttachments = widget.message.attachments
|
||||||
.where((it) => it.ogScrapeUrl == null)
|
.where((it) => it.ogScrapeUrl == null)
|
||||||
.isNotEmpty ==
|
.isNotEmpty ==
|
||||||
true;
|
true;
|
||||||
|
|
||||||
bool get hasUrlAttachments =>
|
late final bool hasUrlAttachments =
|
||||||
widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true;
|
widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true;
|
||||||
|
|
||||||
bool get showBottomRow =>
|
late final bool showBottomRow = showThreadReplyIndicator ||
|
||||||
showThreadReplyIndicator ||
|
|
||||||
showUsername ||
|
showUsername ||
|
||||||
showTimeStamp ||
|
showTimeStamp ||
|
||||||
showInChannel ||
|
showInChannel ||
|
||||||
@@ -441,6 +455,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => widget.message.attachments.isNotEmpty == true;
|
bool get wantKeepAlive => widget.message.attachments.isNotEmpty == true;
|
||||||
|
|
||||||
|
late StreamChatThemeData _streamChatTheme;
|
||||||
|
late StreamChatState _streamChat;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
@@ -450,7 +467,12 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5;
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
type: MaterialType.transparency,
|
type: widget.message.pinned && widget.showPinHighlight
|
||||||
|
? MaterialType.card
|
||||||
|
: MaterialType.transparency,
|
||||||
|
color: widget.message.pinned && widget.showPinHighlight
|
||||||
|
? _streamChatTheme.colorTheme.highlight
|
||||||
|
: null,
|
||||||
child: Portal(
|
child: Portal(
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -477,148 +499,165 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
? AlignmentDirectional.bottomEnd
|
? AlignmentDirectional.bottomEnd
|
||||||
: AlignmentDirectional.bottomStart,
|
: AlignmentDirectional.bottomStart,
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Padding(
|
||||||
crossAxisAlignment: widget.reverse
|
padding: EdgeInsets.only(
|
||||||
? CrossAxisAlignment.end
|
bottom:
|
||||||
: CrossAxisAlignment.start,
|
isPinned && widget.showPinHighlight ? 8.0 : 0.0,
|
||||||
mainAxisSize: MainAxisSize.min,
|
),
|
||||||
children: [
|
child: Column(
|
||||||
Row(
|
crossAxisAlignment: widget.reverse
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
? CrossAxisAlignment.end
|
||||||
mainAxisSize: MainAxisSize.min,
|
: CrossAxisAlignment.start,
|
||||||
children: <Widget>[
|
mainAxisSize: MainAxisSize.min,
|
||||||
if (widget.showUserAvatar == DisplayWidget.show &&
|
children: [
|
||||||
widget.message.user != null) ...[
|
if (widget.message.pinned &&
|
||||||
_buildUserAvatar(),
|
widget.message.pinnedBy != null &&
|
||||||
const SizedBox(width: 4),
|
widget.showPinHighlight)
|
||||||
],
|
_buildPinnedMessage(widget.message),
|
||||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
Row(
|
||||||
SizedBox(width: avatarWidth + 4),
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
Flexible(
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: PortalEntry(
|
children: <Widget>[
|
||||||
portal: Container(
|
if (widget.showUserAvatar ==
|
||||||
transform: Matrix4.translationValues(
|
DisplayWidget.show &&
|
||||||
widget.reverse ? 12 : -12, 0, 0),
|
widget.message.user != null) ...[
|
||||||
constraints: const BoxConstraints(
|
_buildUserAvatar(),
|
||||||
maxWidth: 22 * 6.0),
|
const SizedBox(width: 4),
|
||||||
child: _buildReactionIndicator(context),
|
],
|
||||||
),
|
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||||
portalAnchor:
|
SizedBox(width: avatarWidth + 4),
|
||||||
Alignment(widget.reverse ? 1 : -1, -1),
|
Flexible(
|
||||||
childAnchor:
|
child: PortalEntry(
|
||||||
Alignment(widget.reverse ? -1 : 1, -1),
|
portal: Container(
|
||||||
child: Stack(
|
transform: Matrix4.translationValues(
|
||||||
clipBehavior: Clip.none,
|
widget.reverse ? 12 : -12, 0, 0),
|
||||||
children: [
|
constraints: const BoxConstraints(
|
||||||
Padding(
|
maxWidth: 22 * 6.0,
|
||||||
padding: widget.showReactions
|
),
|
||||||
? EdgeInsets.only(
|
child: _buildReactionIndicator(context),
|
||||||
top: widget
|
),
|
||||||
.message
|
portalAnchor:
|
||||||
.reactionCounts
|
Alignment(widget.reverse ? 1 : -1, -1),
|
||||||
?.isNotEmpty ==
|
childAnchor:
|
||||||
true
|
Alignment(widget.reverse ? -1 : 1, -1),
|
||||||
? 18
|
child: Stack(
|
||||||
: 0,
|
clipBehavior: Clip.none,
|
||||||
)
|
children: [
|
||||||
: EdgeInsets.zero,
|
Padding(
|
||||||
child: (widget.message.isDeleted &&
|
padding: widget.showReactions
|
||||||
!isFailedState)
|
? EdgeInsets.only(
|
||||||
? Container(
|
top: widget
|
||||||
// ignore: lines_longer_than_80_chars
|
.message
|
||||||
margin: EdgeInsets.symmetric(
|
.reactionCounts
|
||||||
horizontal:
|
?.isNotEmpty ==
|
||||||
|
true
|
||||||
|
? 18
|
||||||
|
: 0,
|
||||||
|
)
|
||||||
|
: EdgeInsets.zero,
|
||||||
|
child: (widget.message.isDeleted &&
|
||||||
|
!isFailedState)
|
||||||
|
? Container(
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
horizontal:
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
widget.showUserAvatar ==
|
||||||
|
// ignore: lines_longer_than_80_chars
|
||||||
|
DisplayWidget.gone
|
||||||
|
? 0
|
||||||
|
: 4.0),
|
||||||
|
child: DeletedMessage(
|
||||||
|
borderRadiusGeometry: widget
|
||||||
|
.borderRadiusGeometry,
|
||||||
|
borderSide:
|
||||||
|
widget.borderSide,
|
||||||
|
shape: widget.shape,
|
||||||
|
messageTheme:
|
||||||
|
widget.messageTheme,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Card(
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
elevation: 0,
|
||||||
|
margin: EdgeInsets.symmetric(
|
||||||
|
horizontal: (isFailedState
|
||||||
|
? 15.0
|
||||||
|
: 0.0) +
|
||||||
// ignore: lines_longer_than_80_chars
|
// ignore: lines_longer_than_80_chars
|
||||||
widget.showUserAvatar ==
|
(widget.showUserAvatar ==
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
DisplayWidget
|
DisplayWidget
|
||||||
.gone
|
.gone
|
||||||
? 0
|
? 0
|
||||||
: 4.0),
|
: 4.0),
|
||||||
child: DeletedMessage(
|
),
|
||||||
borderRadiusGeometry: widget
|
shape: widget.shape ??
|
||||||
.borderRadiusGeometry,
|
RoundedRectangleBorder(
|
||||||
borderSide: widget.borderSide,
|
side: widget
|
||||||
shape: widget.shape,
|
.borderSide ??
|
||||||
messageTheme:
|
BorderSide(
|
||||||
widget.messageTheme,
|
color: widget
|
||||||
),
|
// ignore: lines_longer_than_80_chars
|
||||||
)
|
.messageTheme
|
||||||
: Card(
|
// ignore: lines_longer_than_80_chars
|
||||||
clipBehavior: Clip.antiAlias,
|
.messageBorderColor ??
|
||||||
elevation: 0,
|
Colors.grey,
|
||||||
margin: EdgeInsets.symmetric(
|
),
|
||||||
horizontal: (isFailedState
|
borderRadius: widget
|
||||||
? 15.0
|
// ignore: lines_longer_than_80_chars
|
||||||
: 0.0) +
|
.borderRadiusGeometry ??
|
||||||
// ignore: lines_longer_than_80_chars
|
BorderRadius.zero,
|
||||||
(widget.showUserAvatar ==
|
),
|
||||||
DisplayWidget.gone
|
color: _getBackgroundColor(),
|
||||||
? 0
|
child: Column(
|
||||||
: 4.0),
|
crossAxisAlignment:
|
||||||
),
|
CrossAxisAlignment.end,
|
||||||
shape: widget.shape ??
|
mainAxisSize:
|
||||||
RoundedRectangleBorder(
|
MainAxisSize.min,
|
||||||
side: widget.borderSide ??
|
children: <Widget>[
|
||||||
BorderSide(
|
if (hasQuotedMessage)
|
||||||
color: widget
|
_buildQuotedMessage(),
|
||||||
// ignore: lines_longer_than_80_chars
|
if (hasNonUrlAttachments)
|
||||||
.messageTheme
|
_parseAttachments(),
|
||||||
// ignore: lines_longer_than_80_chars
|
if (!isGiphy)
|
||||||
.messageBorderColor ??
|
_buildTextBubble(),
|
||||||
Colors.grey,
|
],
|
||||||
),
|
),
|
||||||
borderRadius: widget
|
|
||||||
// ignore: lines_longer_than_80_chars
|
|
||||||
.borderRadiusGeometry ??
|
|
||||||
BorderRadius.zero,
|
|
||||||
),
|
|
||||||
color: _getBackgroundColor(),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.end,
|
|
||||||
mainAxisSize:
|
|
||||||
MainAxisSize.min,
|
|
||||||
children: <Widget>[
|
|
||||||
if (hasQuotedMessage)
|
|
||||||
_buildQuotedMessage(),
|
|
||||||
if (hasNonUrlAttachments)
|
|
||||||
_parseAttachments(),
|
|
||||||
if (!isGiphy)
|
|
||||||
_buildTextBubble(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
if (widget.showReactionPickerIndicator)
|
||||||
|
Positioned(
|
||||||
|
right: widget.reverse ? null : 4,
|
||||||
|
left: widget.reverse ? 4 : null,
|
||||||
|
top: -8,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: ReactionBubblePainter(
|
||||||
|
_streamChatTheme
|
||||||
|
.colorTheme.white,
|
||||||
|
Colors.transparent,
|
||||||
|
Colors.transparent,
|
||||||
|
tailCirclesSpace: 1,
|
||||||
),
|
),
|
||||||
),
|
|
||||||
if (widget.showReactionPickerIndicator)
|
|
||||||
Positioned(
|
|
||||||
right: widget.reverse ? null : 4,
|
|
||||||
left: widget.reverse ? 4 : null,
|
|
||||||
top: -8,
|
|
||||||
child: CustomPaint(
|
|
||||||
painter: ReactionBubblePainter(
|
|
||||||
StreamChatTheme.of(context)
|
|
||||||
.colorTheme
|
|
||||||
.white,
|
|
||||||
Colors.transparent,
|
|
||||||
Colors.transparent,
|
|
||||||
tailCirclesSpace: 1,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
if (showBottomRow)
|
||||||
if (showBottomRow)
|
SizedBox(height: context.textScaleFactor * 18.0),
|
||||||
SizedBox(height: context.textScaleFactor * 18.0),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
if (showBottomRow)
|
if (showBottomRow)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: leftPadding),
|
padding: EdgeInsets.only(
|
||||||
|
left: leftPadding,
|
||||||
|
bottom:
|
||||||
|
isPinned && widget.showPinHighlight ? 6.0 : 0.0,
|
||||||
|
),
|
||||||
child: _bottomRow,
|
child: _bottomRow,
|
||||||
),
|
),
|
||||||
if (isFailedState)
|
if (isFailedState)
|
||||||
@@ -639,14 +678,20 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
_streamChatTheme = StreamChatTheme.of(context);
|
||||||
|
_streamChat = StreamChat.of(context);
|
||||||
|
super.didChangeDependencies();
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildQuotedMessage() {
|
Widget _buildQuotedMessage() {
|
||||||
final isMyMessage =
|
final isMyMessage = widget.message.user?.id == _streamChat.user?.id;
|
||||||
widget.message.user?.id == StreamChat.of(context).user?.id;
|
|
||||||
final onTap = widget.message.quotedMessage?.isDeleted != true &&
|
final onTap = widget.message.quotedMessage?.isDeleted != true &&
|
||||||
widget.onQuotedMessageTap != null
|
widget.onQuotedMessageTap != null
|
||||||
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
|
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
|
||||||
: null;
|
: null;
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = _streamChatTheme;
|
||||||
return QuotedMessageWidget(
|
return QuotedMessageWidget(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
message: widget.message.quotedMessage!,
|
message: widget.message.quotedMessage!,
|
||||||
@@ -661,7 +706,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
|
|
||||||
Widget get _bottomRow {
|
Widget get _bottomRow {
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
final chatThemeData = StreamChatTheme.of(context);
|
final chatThemeData = _streamChatTheme;
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
@@ -721,14 +766,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
child: Text(msg, style: widget.messageTheme.replies),
|
child: Text(msg, style: widget.messageTheme.replies),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (showUsername)
|
if (showUsername) _buildUsername(usernameKey),
|
||||||
Text(
|
|
||||||
widget.message.user!.name,
|
|
||||||
maxLines: 1,
|
|
||||||
key: usernameKey,
|
|
||||||
style: widget.messageTheme.messageAuthor,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
if (showTimeStamp)
|
if (showTimeStamp)
|
||||||
Text(
|
Text(
|
||||||
Jiffy(widget.message.createdAt.toLocal()).jm,
|
Jiffy(widget.message.createdAt.toLocal()).jm,
|
||||||
@@ -791,6 +829,19 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildUsername(Key usernameKey) {
|
||||||
|
if (widget.usernameBuilder != null) {
|
||||||
|
return widget.usernameBuilder!(context, widget.message);
|
||||||
|
}
|
||||||
|
return Text(
|
||||||
|
widget.message.user!.name,
|
||||||
|
maxLines: 1,
|
||||||
|
key: usernameKey,
|
||||||
|
style: widget.messageTheme.messageAuthor,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildUrlAttachment() {
|
Widget _buildUrlAttachment() {
|
||||||
final urlAttachment = widget.message.attachments
|
final urlAttachment = widget.message.attachments
|
||||||
.firstWhere((element) => element.ogScrapeUrl != null);
|
.firstWhere((element) => element.ogScrapeUrl != null);
|
||||||
@@ -809,36 +860,16 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThreadParticipantsIndicator(Iterable<User> threadParticipants) {
|
Widget _buildThreadParticipantsIndicator(Iterable<User> threadParticipants) =>
|
||||||
var padding = 0.0;
|
_ThreadParticipants(
|
||||||
return Stack(
|
streamChatTheme: _streamChatTheme,
|
||||||
children: threadParticipants.map((user) {
|
threadParticipants: threadParticipants,
|
||||||
padding += 8.0;
|
);
|
||||||
return Positioned(
|
|
||||||
right: padding - 8,
|
|
||||||
bottom: 0,
|
|
||||||
top: 0,
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: StreamChatTheme.of(context).colorTheme.white,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(1),
|
|
||||||
child: UserAvatar(
|
|
||||||
user: user,
|
|
||||||
constraints: BoxConstraints.loose(const Size.fromRadius(7)),
|
|
||||||
showOnlineStatus: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildReactionIndicator(
|
Widget _buildReactionIndicator(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) {
|
) {
|
||||||
final ownId = StreamChat.of(context).user!.id;
|
final ownId = _streamChat.user!.id;
|
||||||
final reactionsMap = <String, Reaction>{};
|
final reactionsMap = <String, Reaction>{};
|
||||||
widget.message.latestReactions?.forEach((element) {
|
widget.message.latestReactions?.forEach((element) {
|
||||||
if (!reactionsMap.containsKey(element.type) ||
|
if (!reactionsMap.containsKey(element.type) ||
|
||||||
@@ -878,10 +909,11 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
barrierColor: _streamChatTheme.colorTheme.overlay,
|
||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: MessageActionsModal(
|
child: MessageActionsModal(
|
||||||
|
textBuilder: widget.textBuilder,
|
||||||
onCopyTap: (message) =>
|
onCopyTap: (message) =>
|
||||||
Clipboard.setData(ClipboardData(text: message.text)),
|
Clipboard.setData(ClipboardData(text: message.text)),
|
||||||
attachmentBorderRadiusGeometry:
|
attachmentBorderRadiusGeometry:
|
||||||
@@ -918,6 +950,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
!isFailedState &&
|
!isFailedState &&
|
||||||
widget.onThreadTap != null,
|
widget.onThreadTap != null,
|
||||||
showFlagButton: widget.showFlagButton,
|
showFlagButton: widget.showFlagButton,
|
||||||
|
showPinButton: widget.showPinButton,
|
||||||
customActions: widget.customActions,
|
customActions: widget.customActions,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
@@ -927,10 +960,11 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
final channel = StreamChannel.of(context).channel;
|
final channel = StreamChannel.of(context).channel;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
barrierColor: StreamChatTheme.of(context).colorTheme.overlay,
|
barrierColor: _streamChatTheme.colorTheme.overlay,
|
||||||
builder: (context) => StreamChannel(
|
builder: (context) => StreamChannel(
|
||||||
channel: channel,
|
channel: channel,
|
||||||
child: MessageReactionsModal(
|
child: MessageReactionsModal(
|
||||||
|
textBuilder: widget.textBuilder,
|
||||||
attachmentBorderRadiusGeometry:
|
attachmentBorderRadiusGeometry:
|
||||||
widget.attachmentBorderRadiusGeometry as BorderRadius?,
|
widget.attachmentBorderRadiusGeometry as BorderRadius?,
|
||||||
showUserAvatar:
|
showUserAvatar:
|
||||||
@@ -957,7 +991,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
side: hasFiles
|
side: hasFiles
|
||||||
? widget.attachmentBorderSide ??
|
? widget.attachmentBorderSide ??
|
||||||
BorderSide(
|
BorderSide(
|
||||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
color: _streamChatTheme.colorTheme.greyWhisper,
|
||||||
)
|
)
|
||||||
: BorderSide.none,
|
: BorderSide.none,
|
||||||
borderRadius: widget.attachmentBorderRadiusGeometry ?? BorderRadius.zero,
|
borderRadius: widget.attachmentBorderRadiusGeometry ?? BorderRadius.zero,
|
||||||
@@ -967,7 +1001,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
|
ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder(
|
||||||
side: widget.borderSide ??
|
side: widget.borderSide ??
|
||||||
BorderSide(
|
BorderSide(
|
||||||
color: StreamChatTheme.of(context).colorTheme.greyWhisper,
|
color: _streamChatTheme.colorTheme.greyWhisper,
|
||||||
),
|
),
|
||||||
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
|
borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero,
|
||||||
);
|
);
|
||||||
@@ -1058,7 +1092,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
Text(
|
Text(
|
||||||
widget.readList!.length.toString(),
|
widget.readList!.length.toString(),
|
||||||
style: style.copyWith(
|
style: style.copyWith(
|
||||||
color: StreamChatTheme.of(context).colorTheme.accentBlue,
|
color: _streamChatTheme.colorTheme.accentBlue,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
@@ -1113,7 +1147,35 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isOnlyEmoji => widget.message.text!.isOnlyEmoji;
|
Widget _buildPinnedMessage(Message message) {
|
||||||
|
final pinnedBy = message.pinnedBy;
|
||||||
|
final pinnedByMe = _streamChat.user!.id == pinnedBy!.id;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
StreamSvgIcon.pin(
|
||||||
|
size: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
width: 4,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}',
|
||||||
|
style: TextStyle(
|
||||||
|
color: _streamChatTheme.colorTheme.grey,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
late final bool isPinned = widget.message.pinned;
|
||||||
|
|
||||||
Color? _getBackgroundColor() {
|
Color? _getBackgroundColor() {
|
||||||
if (hasQuotedMessage) {
|
if (hasQuotedMessage) {
|
||||||
@@ -1121,7 +1183,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUrlAttachments) {
|
if (hasUrlAttachments) {
|
||||||
return StreamChatTheme.of(context).colorTheme.blueAlice;
|
return _streamChatTheme.colorTheme.blueAlice;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isOnlyEmoji) {
|
if (isOnlyEmoji) {
|
||||||
@@ -1153,6 +1215,45 @@ class _MessageWidgetState extends State<MessageWidget>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ThreadParticipants extends StatelessWidget {
|
||||||
|
const _ThreadParticipants({
|
||||||
|
Key? key,
|
||||||
|
required StreamChatThemeData streamChatTheme,
|
||||||
|
required this.threadParticipants,
|
||||||
|
}) : _streamChatTheme = streamChatTheme,
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
final StreamChatThemeData _streamChatTheme;
|
||||||
|
final Iterable<User> threadParticipants;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
var padding = 0.0;
|
||||||
|
return Stack(
|
||||||
|
children: threadParticipants.map((user) {
|
||||||
|
padding += 8.0;
|
||||||
|
return Positioned(
|
||||||
|
right: padding - 8,
|
||||||
|
bottom: 0,
|
||||||
|
top: 0,
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: _streamChatTheme.colorTheme.white,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(1),
|
||||||
|
child: UserAvatar(
|
||||||
|
user: user,
|
||||||
|
constraints: BoxConstraints.loose(const Size.fromRadius(7)),
|
||||||
|
showOnlineStatus: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ThreadReplyPainter extends CustomPainter {
|
class _ThreadReplyPainter extends CustomPainter {
|
||||||
const _ThreadReplyPainter({
|
const _ThreadReplyPainter({
|
||||||
this.context,
|
this.context,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
import 'package:video_player/video_player.dart';
|
import 'package:video_player/video_player.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
|
||||||
import 'package:stream_chat_flutter/src/extension.dart';
|
|
||||||
|
|
||||||
/// Widget builder for quoted message attachment thumnail
|
/// Widget builder for quoted message attachment thumnail
|
||||||
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
|
typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
|
||||||
@@ -217,7 +217,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
child = AbsorbPointer(child: child);
|
child = AbsorbPointer(child: child);
|
||||||
return Material(
|
return Material(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.hardEdge,
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
shape: attachment.type == 'file' ? null : _getDefaultShape(context),
|
||||||
child: child,
|
child: child,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
import 'package:stream_chat_flutter/src/reaction_icon.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// Creates reaction bubble widget for displaying over messages
|
/// Creates reaction bubble widget for displaying over messages
|
||||||
@@ -130,13 +129,13 @@ class ReactionBubble extends StatelessWidget {
|
|||||||
horizontal: 4,
|
horizontal: 4,
|
||||||
),
|
),
|
||||||
child: reactionIcon != null
|
child: reactionIcon != null
|
||||||
? StreamSvgIcon(
|
? ConstrainedBox(
|
||||||
assetName: reactionIcon.assetName,
|
constraints: BoxConstraints.tight(const Size.square(16)),
|
||||||
width: 16,
|
child: reactionIcon.builder(
|
||||||
height: 16,
|
context,
|
||||||
color: (!highlightOwnReactions || reaction.user?.id == userId)
|
!highlightOwnReactions || reaction.user?.id == userId,
|
||||||
? chatThemeData.colorTheme.accentBlue
|
16,
|
||||||
: chatThemeData.colorTheme.black.withOpacity(.5),
|
),
|
||||||
)
|
)
|
||||||
: Icon(
|
: Icon(
|
||||||
Icons.help_outline_rounded,
|
Icons.help_outline_rounded,
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
/// Reaction icon data
|
/// Reaction icon data
|
||||||
class ReactionIcon {
|
class ReactionIcon {
|
||||||
/// Constructor for creating [ReactionIcon]
|
/// Constructor for creating [ReactionIcon]
|
||||||
ReactionIcon({
|
ReactionIcon({
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.assetName,
|
required this.builder,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Type of reaction
|
/// Type of reaction
|
||||||
final String type;
|
final String type;
|
||||||
|
|
||||||
/// Asset to display for reaction
|
/// Asset to display for reaction
|
||||||
final String assetName;
|
final Widget Function(
|
||||||
|
BuildContext,
|
||||||
|
bool highlighted,
|
||||||
|
double size,
|
||||||
|
) builder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:ezanimation/ezanimation.dart';
|
import 'package:ezanimation/ezanimation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/src/extension.dart';
|
import 'package:stream_chat_flutter/src/extension.dart';
|
||||||
import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
|
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
|
|
||||||
/// 
|
/// 
|
||||||
@@ -50,95 +47,89 @@ class _ReactionPickerState extends State<ReactionPicker>
|
|||||||
triggerAnimations();
|
triggerAnimations();
|
||||||
}
|
}
|
||||||
|
|
||||||
return TweenAnimationBuilder<double>(
|
final child = Material(
|
||||||
tween: Tween(begin: 0, end: 1),
|
borderRadius: BorderRadius.circular(24),
|
||||||
curve: Curves.easeInOutBack,
|
color: chatThemeData.colorTheme.white,
|
||||||
duration: const Duration(milliseconds: 500),
|
clipBehavior: Clip.hardEdge,
|
||||||
builder: (context, val, wid) => Transform.scale(
|
child: Padding(
|
||||||
scale: val,
|
padding: const EdgeInsets.symmetric(
|
||||||
child: Material(
|
horizontal: 16,
|
||||||
borderRadius: BorderRadius.circular(24),
|
vertical: 8,
|
||||||
color: chatThemeData.colorTheme.white,
|
),
|
||||||
clipBehavior: Clip.hardEdge,
|
child: Row(
|
||||||
child: Padding(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
padding: const EdgeInsets.symmetric(
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
horizontal: 16,
|
mainAxisSize: MainAxisSize.min,
|
||||||
vertical: 8,
|
children: reactionIcons
|
||||||
),
|
.map<Widget>((reactionIcon) {
|
||||||
child: Row(
|
final ownReactionIndex = widget.message.ownReactions
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
?.indexWhere(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
(reaction) => reaction.type == reactionIcon.type) ??
|
||||||
mainAxisSize: MainAxisSize.min,
|
-1;
|
||||||
children: reactionIcons
|
final index = reactionIcons.indexOf(reactionIcon);
|
||||||
.map<Widget>((reactionIcon) {
|
|
||||||
final ownReactionIndex = widget.message.ownReactions
|
|
||||||
?.indexWhere((reaction) =>
|
|
||||||
reaction.type == reactionIcon.type) ??
|
|
||||||
-1;
|
|
||||||
final index = reactionIcons.indexOf(reactionIcon);
|
|
||||||
|
|
||||||
return ConstrainedBox(
|
final child = reactionIcon.builder(
|
||||||
constraints: const BoxConstraints.tightFor(
|
context,
|
||||||
height: 24,
|
ownReactionIndex != -1,
|
||||||
width: 24,
|
24,
|
||||||
),
|
);
|
||||||
child: RawMaterialButton(
|
|
||||||
elevation: 0,
|
return ConstrainedBox(
|
||||||
shape: ContinuousRectangleBorder(
|
constraints: const BoxConstraints.tightFor(
|
||||||
borderRadius: BorderRadius.circular(16),
|
height: 24,
|
||||||
),
|
width: 24,
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
height: 24,
|
|
||||||
width: 24,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
if (ownReactionIndex != -1) {
|
|
||||||
removeReaction(
|
|
||||||
context,
|
|
||||||
widget.message
|
|
||||||
.ownReactions![ownReactionIndex],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
sendReaction(
|
|
||||||
context,
|
|
||||||
reactionIcon.type,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: AnimatedBuilder(
|
|
||||||
animation: animations[index],
|
|
||||||
builder: (context, val) => Transform.scale(
|
|
||||||
scale: animations[index].value,
|
|
||||||
child: StreamSvgIcon(
|
|
||||||
assetName: reactionIcon.assetName,
|
|
||||||
height: max(
|
|
||||||
0,
|
|
||||||
animations[index].value * 24.0,
|
|
||||||
),
|
|
||||||
width: max(
|
|
||||||
0,
|
|
||||||
animations[index].value * 24.0,
|
|
||||||
),
|
|
||||||
color: ownReactionIndex != -1
|
|
||||||
? chatThemeData
|
|
||||||
.colorTheme.accentBlue
|
|
||||||
: Theme.of(context)
|
|
||||||
.iconTheme
|
|
||||||
.color!
|
|
||||||
.withOpacity(.5),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.insertBetween(const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
),
|
),
|
||||||
),
|
child: RawMaterialButton(
|
||||||
),
|
elevation: 0,
|
||||||
));
|
shape: ContinuousRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints.tightFor(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
if (ownReactionIndex != -1) {
|
||||||
|
removeReaction(
|
||||||
|
context,
|
||||||
|
widget.message.ownReactions![ownReactionIndex],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
sendReaction(
|
||||||
|
context,
|
||||||
|
reactionIcon.type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: animations[index],
|
||||||
|
builder: (context, child) => Transform.scale(
|
||||||
|
scale: animations[index].value,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.insertBetween(const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return TweenAnimationBuilder<double>(
|
||||||
|
tween: Tween(begin: 0, end: 1),
|
||||||
|
curve: Curves.easeInOutBack,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
builder: (context, val, widget) => Transform.scale(
|
||||||
|
scale: val,
|
||||||
|
child: widget,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void triggerAnimations() async {
|
void triggerAnimations() async {
|
||||||
|
|||||||
@@ -217,10 +217,11 @@ class StreamChatThemeData {
|
|||||||
TextTheme textTheme,
|
TextTheme textTheme,
|
||||||
) {
|
) {
|
||||||
final accentColor = colorTheme.accentBlue;
|
final accentColor = colorTheme.accentBlue;
|
||||||
|
final iconTheme = IconThemeData(color: colorTheme.black.withOpacity(.5));
|
||||||
return StreamChatThemeData.raw(
|
return StreamChatThemeData.raw(
|
||||||
textTheme: textTheme,
|
textTheme: textTheme,
|
||||||
colorTheme: colorTheme,
|
colorTheme: colorTheme,
|
||||||
primaryIconTheme: IconThemeData(color: colorTheme.black.withOpacity(.5)),
|
primaryIconTheme: iconTheme,
|
||||||
defaultChannelImage: (context, channel) => const SizedBox(),
|
defaultChannelImage: (context, channel) => const SizedBox(),
|
||||||
defaultUserImage: (context, user) => Center(
|
defaultUserImage: (context, user) => Center(
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
@@ -342,23 +343,63 @@ class StreamChatThemeData {
|
|||||||
reactionIcons: [
|
reactionIcons: [
|
||||||
ReactionIcon(
|
ReactionIcon(
|
||||||
type: 'love',
|
type: 'love',
|
||||||
assetName: 'Icon_love_reaction.svg',
|
builder: (context, highlighted, size) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
return StreamSvgIcon.loveReaction(
|
||||||
|
color: highlighted
|
||||||
|
? theme.colorTheme.accentBlue
|
||||||
|
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
ReactionIcon(
|
ReactionIcon(
|
||||||
type: 'like',
|
type: 'like',
|
||||||
assetName: 'Icon_thumbs_up_reaction.svg',
|
builder: (context, highlighted, size) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
return StreamSvgIcon.thumbsUpReaction(
|
||||||
|
color: highlighted
|
||||||
|
? theme.colorTheme.accentBlue
|
||||||
|
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
ReactionIcon(
|
ReactionIcon(
|
||||||
type: 'sad',
|
type: 'sad',
|
||||||
assetName: 'Icon_thumbs_down_reaction.svg',
|
builder: (context, highlighted, size) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
return StreamSvgIcon.thumbsDownReaction(
|
||||||
|
color: highlighted
|
||||||
|
? theme.colorTheme.accentBlue
|
||||||
|
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
ReactionIcon(
|
ReactionIcon(
|
||||||
type: 'haha',
|
type: 'haha',
|
||||||
assetName: 'Icon_LOL_reaction.svg',
|
builder: (context, highlighted, size) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
return StreamSvgIcon.lolReaction(
|
||||||
|
color: highlighted
|
||||||
|
? theme.colorTheme.accentBlue
|
||||||
|
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
ReactionIcon(
|
ReactionIcon(
|
||||||
type: 'wow',
|
type: 'wow',
|
||||||
assetName: 'Icon_wut_reaction.svg',
|
builder: (context, highlighted, size) {
|
||||||
|
final theme = StreamChatTheme.of(context);
|
||||||
|
return StreamSvgIcon.wutReaction(
|
||||||
|
color: highlighted
|
||||||
|
? theme.colorTheme.accentBlue
|
||||||
|
: theme.primaryIconTheme.color!.withOpacity(.5),
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -49,6 +49,66 @@ class StreamSvgIcon extends StatelessWidget {
|
|||||||
height: size,
|
height: size,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// [StreamSvgIcon] type
|
||||||
|
factory StreamSvgIcon.loveReaction({
|
||||||
|
double? size,
|
||||||
|
Color? color,
|
||||||
|
}) =>
|
||||||
|
StreamSvgIcon(
|
||||||
|
assetName: 'Icon_love_reaction.svg',
|
||||||
|
color: color,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// [StreamSvgIcon] type
|
||||||
|
factory StreamSvgIcon.thumbsUpReaction({
|
||||||
|
double? size,
|
||||||
|
Color? color,
|
||||||
|
}) =>
|
||||||
|
StreamSvgIcon(
|
||||||
|
assetName: 'Icon_thumbs_up_reaction.svg',
|
||||||
|
color: color,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// [StreamSvgIcon] type
|
||||||
|
factory StreamSvgIcon.thumbsDownReaction({
|
||||||
|
double? size,
|
||||||
|
Color? color,
|
||||||
|
}) =>
|
||||||
|
StreamSvgIcon(
|
||||||
|
assetName: 'Icon_thumbs_down_reaction.svg',
|
||||||
|
color: color,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// [StreamSvgIcon] type
|
||||||
|
factory StreamSvgIcon.lolReaction({
|
||||||
|
double? size,
|
||||||
|
Color? color,
|
||||||
|
}) =>
|
||||||
|
StreamSvgIcon(
|
||||||
|
assetName: 'Icon_LOL_reaction.svg',
|
||||||
|
color: color,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// [StreamSvgIcon] type
|
||||||
|
factory StreamSvgIcon.wutReaction({
|
||||||
|
double? size,
|
||||||
|
Color? color,
|
||||||
|
}) =>
|
||||||
|
StreamSvgIcon(
|
||||||
|
assetName: 'Icon_wut_reaction.svg',
|
||||||
|
color: color,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
);
|
||||||
|
|
||||||
/// [StreamSvgIcon] type
|
/// [StreamSvgIcon] type
|
||||||
factory StreamSvgIcon.smile({
|
factory StreamSvgIcon.smile({
|
||||||
double? size,
|
double? size,
|
||||||
@@ -889,6 +949,18 @@ class StreamSvgIcon extends StatelessWidget {
|
|||||||
height: size,
|
height: size,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// [StreamSvgIcon] type
|
||||||
|
factory StreamSvgIcon.pin({
|
||||||
|
double? size,
|
||||||
|
Color? color,
|
||||||
|
}) =>
|
||||||
|
StreamSvgIcon(
|
||||||
|
assetName: 'icon_pin.svg',
|
||||||
|
color: color,
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
);
|
||||||
|
|
||||||
/// Name of icon asset
|
/// Name of icon asset
|
||||||
final String? assetName;
|
final String? assetName;
|
||||||
|
|
||||||
|
|||||||
@@ -33,13 +33,22 @@ class TypingIndicator extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final channelState =
|
final channelState =
|
||||||
channel?.state ?? StreamChannel.of(context).channel.state!;
|
channel?.state ?? StreamChannel.of(context).channel.state!;
|
||||||
return StreamBuilder<List<User>>(
|
|
||||||
|
final altWidget = Align(
|
||||||
|
key: const Key('alternative'),
|
||||||
|
alignment: alignment,
|
||||||
|
child: Container(
|
||||||
|
child: alternativeWidget ?? const Offstage(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return BetterStreamBuilder<List<User>>(
|
||||||
initialData: channelState.typingEvents,
|
initialData: channelState.typingEvents,
|
||||||
stream: channelState.typingEventsStream,
|
stream: channelState.typingEventsStream,
|
||||||
builder: (context, snapshot) => AnimatedSwitcher(
|
builder: (context, data) => AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: snapshot.data?.isNotEmpty == true
|
child: data.isNotEmpty == true
|
||||||
? Padding(
|
? Padding(
|
||||||
|
key: const Key('main'),
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: Align(
|
child: Align(
|
||||||
key: const Key('typings'),
|
key: const Key('typings'),
|
||||||
@@ -54,7 +63,7 @@ class TypingIndicator extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
// ignore: lines_longer_than_80_chars
|
// ignore: lines_longer_than_80_chars
|
||||||
' ${snapshot.data![0].name}${snapshot.data!.length == 1 ? '' : ' and ${snapshot.data!.length - 1} more'} ${snapshot.data!.length == 1 ? 'is' : 'are'} typing',
|
' ${data[0].name}${data.length == 1 ? '' : ' and ${data.length - 1} more'} ${data.length == 1 ? 'is' : 'are'} typing',
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
style: style,
|
style: style,
|
||||||
),
|
),
|
||||||
@@ -62,13 +71,7 @@ class TypingIndicator extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: Align(
|
: altWidget,
|
||||||
key: const Key('alternative'),
|
|
||||||
alignment: alignment,
|
|
||||||
child: Container(
|
|
||||||
child: alternativeWidget ?? const Offstage(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,16 +17,16 @@ class UnreadIndicator extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final client = StreamChat.of(context).client;
|
final client = StreamChat.of(context).client;
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
child: StreamBuilder<int?>(
|
child: BetterStreamBuilder<int?>(
|
||||||
stream: cid != null
|
stream: cid != null
|
||||||
? client.state.channels[cid]?.state?.unreadCountStream
|
? client.state.channels[cid]?.state?.unreadCountStream
|
||||||
: client.state.totalUnreadCountStream,
|
: client.state.totalUnreadCountStream,
|
||||||
initialData: cid != null
|
initialData: cid != null
|
||||||
? client.state.channels[cid]?.state?.unreadCount
|
? client.state.channels[cid]?.state?.unreadCount
|
||||||
: client.state.totalUnreadCount,
|
: client.state.totalUnreadCount,
|
||||||
builder: (context, snapshot) {
|
builder: (context, data) {
|
||||||
if (!snapshot.hasData || snapshot.data == 0) {
|
if (data == null || data == 0) {
|
||||||
return const SizedBox();
|
return const Offstage();
|
||||||
}
|
}
|
||||||
return Material(
|
return Material(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
@@ -42,7 +42,7 @@ class UnreadIndicator extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${snapshot.data! > 99 ? '99+' : snapshot.data}',
|
'${data > 99 ? '99+' : data}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class UrlAttachment extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (urlAttachment.imageUrl != null)
|
if (urlAttachment.imageUrl != null)
|
||||||
Container(
|
Container(
|
||||||
clipBehavior: Clip.antiAliasWithSaveLayer,
|
clipBehavior: Clip.hardEdge,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
@@ -332,7 +334,7 @@ Widget wrapAttachmentWidget(
|
|||||||
bool reverse,
|
bool reverse,
|
||||||
) =>
|
) =>
|
||||||
Material(
|
Material(
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.hardEdge,
|
||||||
shape: attachmentShape,
|
shape: attachmentShape,
|
||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
child: attachmentWidget,
|
child: attachmentWidget,
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.694 1.52896C10.5304 1.36532 10.2937 1.29808 10.0685 1.35125C9.84327 1.40442 9.6616 1.57041 9.5884 1.78994L9.16613 3.05682L6.72027 5.50272C5.12809 5.14204 3.59999 5.79015 2.19557 7.19458C1.93481 7.45531 1.93481 7.87811 2.19557 8.13884L4.55625 10.4995L3.1399 11.9159C2.87915 12.1767 2.87915 12.5994 3.1399 12.8601C3.40065 13.1209 3.82342 13.1209 4.08417 12.8601L5.50051 11.4438L7.8612 13.8045C8.12193 14.0652 8.54473 14.0652 8.80547 13.8045C10.2099 12.4 10.858 10.872 10.4973 9.27978L12.9432 6.83391L14.2101 6.41161C14.4296 6.33843 14.5956 6.1568 14.6488 5.93158C14.7019 5.70636 14.6347 5.46967 14.4711 5.30604L10.694 1.52896ZM10.3832 3.62864L10.5137 3.23716L12.7629 5.48638L12.3714 5.61688C12.2731 5.64965 12.1837 5.70488 12.1104 5.77818L9.2776 8.61098C9.09873 8.78984 9.03633 9.05431 9.11627 9.29424C9.43453 10.2489 9.2382 11.2565 8.31287 12.3676L3.6324 7.68718C4.74353 6.76184 5.7511 6.56552 6.7058 6.88378C6.94573 6.96371 7.2102 6.90131 7.38906 6.72244L10.2219 3.88963C10.2951 3.81634 10.3504 3.72698 10.3832 3.62864Z" fill="#7A7A7A"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter
|
name: stream_chat_flutter
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
|
description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter.
|
||||||
version: 2.0.0-nullsafety.4
|
version: 2.0.0-nullsafety.5
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ environment:
|
|||||||
dependencies:
|
dependencies:
|
||||||
cached_network_image: ^3.0.0
|
cached_network_image: ^3.0.0
|
||||||
characters: ^1.1.0
|
characters: ^1.1.0
|
||||||
chewie: ^1.0.0
|
chewie: ^1.2.0
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
dio: ^4.0.0
|
dio: ^4.0.0
|
||||||
ezanimation: ^0.5.0
|
ezanimation: ^0.5.0
|
||||||
@@ -25,7 +25,7 @@ dependencies:
|
|||||||
flutter_svg: ^0.22.0
|
flutter_svg: ^0.22.0
|
||||||
http_parser: ^4.0.0
|
http_parser: ^4.0.0
|
||||||
image_gallery_saver: ^1.6.9
|
image_gallery_saver: ^1.6.9
|
||||||
image_picker: ^0.7.4
|
image_picker: ^0.8.0
|
||||||
jiffy: ^4.1.0
|
jiffy: ^4.1.0
|
||||||
lottie: ^1.0.1
|
lottie: ^1.0.1
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
@@ -36,12 +36,12 @@ dependencies:
|
|||||||
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
scrollable_positioned_list: ^0.2.0-nullsafety.0
|
||||||
share_plus: ^2.0.3
|
share_plus: ^2.0.3
|
||||||
shimmer: ^2.0.0
|
shimmer: ^2.0.0
|
||||||
stream_chat_flutter_core: ^2.0.0-nullsafety.3
|
stream_chat_flutter_core: ^2.0.0-nullsafety.5
|
||||||
substring_highlight: ^1.0.26
|
substring_highlight: ^1.0.26
|
||||||
synchronized: ^3.0.0
|
synchronized: ^3.0.0
|
||||||
url_launcher: ^6.0.3
|
url_launcher: ^6.0.3
|
||||||
video_compress: ^3.0.0
|
video_compress: ^3.0.0
|
||||||
video_player: ^2.1.1
|
video_player: ^2.1.0
|
||||||
video_thumbnail: ^0.3.3
|
video_thumbnail: ^0.3.3
|
||||||
visibility_detector: ^0.2.0
|
visibility_detector: ^0.2.0
|
||||||
|
|
||||||
|
|||||||
@@ -107,6 +107,8 @@ void main() {
|
|||||||
]);
|
]);
|
||||||
when(() => client.wsConnectionStatusStream)
|
when(() => client.wsConnectionStatusStream)
|
||||||
.thenAnswer((_) => Stream.value(ConnectionStatus.disconnected));
|
.thenAnswer((_) => Stream.value(ConnectionStatus.disconnected));
|
||||||
|
when(() => client.wsConnectionStatus)
|
||||||
|
.thenReturn(ConnectionStatus.disconnected);
|
||||||
when(() => clientState.totalUnreadCountStream)
|
when(() => clientState.totalUnreadCountStream)
|
||||||
.thenAnswer((i) => Stream.value(1));
|
.thenAnswer((i) => Stream.value(1));
|
||||||
|
|
||||||
|
|||||||
@@ -53,15 +53,6 @@ void main() {
|
|||||||
)
|
)
|
||||||
]));
|
]));
|
||||||
|
|
||||||
when(() => channelState.typingEvents).thenAnswer((i) => [
|
|
||||||
User(id: 'other-user', extraData: {'name': 'demo'})
|
|
||||||
]);
|
|
||||||
when(() => channelState.typingEventsStream)
|
|
||||||
.thenAnswer((i) => Stream.value([
|
|
||||||
User(id: 'other-user', extraData: {'name': 'demo'}),
|
|
||||||
User(id: 'other-user', extraData: {'name': 'demo'}),
|
|
||||||
]));
|
|
||||||
|
|
||||||
await tester.pumpWidget(MaterialApp(
|
await tester.pumpWidget(MaterialApp(
|
||||||
home: StreamChat(
|
home: StreamChat(
|
||||||
client: client,
|
client: client,
|
||||||
@@ -75,7 +66,6 @@ void main() {
|
|||||||
));
|
));
|
||||||
|
|
||||||
expect(find.byType(TextField), findsOneWidget);
|
expect(find.byType(TextField), findsOneWidget);
|
||||||
expect(find.byType(StreamSvgIcon), findsNWidgets(8));
|
|
||||||
expect(find.byKey(const Key('messageInputText')), findsOneWidget);
|
expect(find.byKey(const Key('messageInputText')), findsOneWidget);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:mocktail/mocktail.dart';
|
import 'package:mocktail/mocktail.dart';
|
||||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||||
|
|
||||||
class MockClient extends Mock implements StreamChatClient {}
|
class MockClient extends Mock implements StreamChatClient {
|
||||||
|
MockClient() {
|
||||||
|
when(() => wsConnectionStatus).thenReturn(ConnectionStatus.connected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MockClientState extends Mock implements ClientState {}
|
class MockClientState extends Mock implements ClientState {}
|
||||||
|
|
||||||
@@ -17,7 +21,12 @@ class MockChannel extends Mock implements Channel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockChannelState extends Mock implements ChannelClientState {}
|
class MockChannelState extends Mock implements ChannelClientState {
|
||||||
|
MockChannelState() {
|
||||||
|
when(() => typingEvents).thenReturn([]);
|
||||||
|
when(() => typingEventsStream).thenAnswer((_) => Stream.value([]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
|
class MockNavigatorObserver extends Mock implements NavigatorObserver {}
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(find.byType(SizedBox), findsOneWidget);
|
expect(find.text('0'), findsNothing);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
## 2.0.0-nullsafety.5
|
||||||
|
|
||||||
|
* Update llc dependency
|
||||||
|
* Minor fixes and improvements
|
||||||
|
* Performance improvements
|
||||||
|
* Monitor connection using `connectivity_plus` package
|
||||||
|
|
||||||
## 2.0.0-nullsafety.3
|
## 2.0.0-nullsafety.3
|
||||||
|
|
||||||
* Update llc dependency
|
* Update llc dependency
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// A more efficient [StreamBuilder]
|
||||||
|
/// It requires [initialData] and will rebuild
|
||||||
|
/// only when the new data is different than the current data
|
||||||
|
/// The [comparator] is used to check if the new data is different
|
||||||
|
class BetterStreamBuilder<T> extends StatefulWidget {
|
||||||
|
/// Creates a new BetterStreamBuilder
|
||||||
|
const BetterStreamBuilder({
|
||||||
|
required this.stream,
|
||||||
|
required this.initialData,
|
||||||
|
required this.builder,
|
||||||
|
this.loadingBuilder,
|
||||||
|
this.errorBuilder,
|
||||||
|
this.comparator,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
/// The stream to listen to
|
||||||
|
final Stream<T>? stream;
|
||||||
|
|
||||||
|
/// The initial data available
|
||||||
|
final T initialData;
|
||||||
|
|
||||||
|
/// Comparator used to check if the new data is different than the last one
|
||||||
|
final bool Function(T?, T)? comparator;
|
||||||
|
|
||||||
|
/// Builder that builds based on the new snapshot
|
||||||
|
final Widget Function(BuildContext context, T data) builder;
|
||||||
|
|
||||||
|
/// Builder that builds when the data is null
|
||||||
|
final Widget Function(BuildContext context)? loadingBuilder;
|
||||||
|
|
||||||
|
/// Builder used when there is an error
|
||||||
|
final Widget Function(BuildContext context, Object error)? errorBuilder;
|
||||||
|
|
||||||
|
@override
|
||||||
|
_BetterStreamBuilderState createState() => _BetterStreamBuilderState<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BetterStreamBuilderState<T> extends State<BetterStreamBuilder<T>> {
|
||||||
|
T? _lastEvent;
|
||||||
|
StreamSubscription? _subscription;
|
||||||
|
Object? _lastError;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_lastError != null) {
|
||||||
|
return widget.errorBuilder!(context, _lastError!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastEvent == null) {
|
||||||
|
return widget.loadingBuilder?.call(context) ?? const Offstage();
|
||||||
|
}
|
||||||
|
return widget.builder(context, _lastEvent ?? widget.initialData);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_lastEvent = widget.initialData;
|
||||||
|
_subscription = widget.stream?.listen(
|
||||||
|
_onEvent,
|
||||||
|
onError: _onError,
|
||||||
|
);
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant BetterStreamBuilder<T> oldWidget) {
|
||||||
|
if (oldWidget.stream != widget.stream) {
|
||||||
|
_subscription?.cancel();
|
||||||
|
_subscription = widget.stream?.listen(
|
||||||
|
_onEvent,
|
||||||
|
onError: _onError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onError(error) {
|
||||||
|
if (widget.errorBuilder != null && error != _lastError) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
_lastError = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEvent(T event) {
|
||||||
|
_lastError = null;
|
||||||
|
final isEqual = widget.comparator != null
|
||||||
|
? widget.comparator!(_lastEvent, event)
|
||||||
|
: event == _lastEvent;
|
||||||
|
if (!isEqual) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
_lastEvent = event;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
import 'package:stream_chat/stream_chat.dart';
|
||||||
|
import 'package:stream_chat_flutter_core/src/better_stream_builder.dart';
|
||||||
import 'package:stream_chat_flutter_core/src/stream_channel.dart';
|
import 'package:stream_chat_flutter_core/src/stream_channel.dart';
|
||||||
import 'package:stream_chat_flutter_core/src/typedef.dart';
|
import 'package:stream_chat_flutter_core/src/typedef.dart';
|
||||||
|
|
||||||
@@ -127,6 +129,10 @@ class MessageListCoreState extends State<MessageListCore> {
|
|||||||
.map((threads) => threads[widget.parentMessage!.id])
|
.map((threads) => threads[widget.parentMessage!.id])
|
||||||
: _streamChannel!.channel.state?.messagesStream;
|
: _streamChannel!.channel.state?.messagesStream;
|
||||||
|
|
||||||
|
final initialData = _isThreadConversation
|
||||||
|
? _streamChannel!.channel.state?.threads[widget.parentMessage!.id]
|
||||||
|
: _streamChannel!.channel.state?.messages;
|
||||||
|
|
||||||
bool defaultFilter(Message m) {
|
bool defaultFilter(Message m) {
|
||||||
final isMyMessage = m.user?.id == _currentUser?.id;
|
final isMyMessage = m.user?.id == _currentUser?.id;
|
||||||
final isDeletedOrShadowed = m.isDeleted == true || m.shadowed == true;
|
final isDeletedOrShadowed = m.isDeleted == true || m.shadowed == true;
|
||||||
@@ -134,28 +140,27 @@ class MessageListCoreState extends State<MessageListCore> {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return StreamBuilder<List<Message>?>(
|
return BetterStreamBuilder<List<Message>?>(
|
||||||
stream: messagesStream?.map((messages) =>
|
initialData: initialData,
|
||||||
messages?.where(widget.messageFilter ?? defaultFilter).toList(
|
comparator: const ListEquality().equals,
|
||||||
growable: false,
|
stream: messagesStream!.map(
|
||||||
)),
|
(messages) =>
|
||||||
builder: (context, snapshot) {
|
messages?.where(widget.messageFilter ?? defaultFilter).toList(
|
||||||
if (snapshot.hasError) {
|
growable: false,
|
||||||
return widget.errorWidgetBuilder(context, snapshot.error!);
|
),
|
||||||
} else if (!snapshot.hasData) {
|
),
|
||||||
return widget.loadingBuilder(context);
|
errorBuilder: widget.errorWidgetBuilder,
|
||||||
} else {
|
loadingBuilder: widget.loadingBuilder,
|
||||||
final messageList =
|
builder: (context, data) {
|
||||||
snapshot.data?.reversed.toList(growable: false) ?? [];
|
final messageList = data?.reversed.toList(growable: false) ?? [];
|
||||||
if (messageList.isEmpty && !_isThreadConversation) {
|
if (messageList.isEmpty && !_isThreadConversation) {
|
||||||
if (_upToDate) {
|
if (_upToDate) {
|
||||||
return widget.emptyBuilder(context);
|
return widget.emptyBuilder(context);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
_messages = messageList;
|
|
||||||
}
|
}
|
||||||
return widget.messageListBuilder(context, _messages);
|
} else {
|
||||||
|
_messages = messageList;
|
||||||
}
|
}
|
||||||
|
return widget.messageListBuilder(context, _messages);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ library stream_chat_flutter_core;
|
|||||||
export 'package:connectivity_plus/connectivity_plus.dart';
|
export 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
export 'package:stream_chat/stream_chat.dart';
|
export 'package:stream_chat/stream_chat.dart';
|
||||||
|
|
||||||
|
export 'src/better_stream_builder.dart';
|
||||||
export 'src/channel_list_core.dart' hide ChannelListCoreState;
|
export 'src/channel_list_core.dart' hide ChannelListCoreState;
|
||||||
export 'src/channels_bloc.dart';
|
export 'src/channels_bloc.dart';
|
||||||
export 'src/lazy_load_scroll_view.dart';
|
export 'src/lazy_load_scroll_view.dart';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_flutter_core
|
name: stream_chat_flutter_core
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
|
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter.
|
||||||
version: 2.0.0-nullsafety.3
|
version: 2.0.0-nullsafety.5
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
rxdart: ^0.27.0
|
rxdart: ^0.27.0
|
||||||
stream_chat: ^2.0.0-nullsafety.2
|
stream_chat: ^2.0.0-nullsafety.5
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
stream_chat:
|
stream_chat:
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ void main() {
|
|||||||
when(() => mockChannel.state.isUpToDate).thenReturn(true);
|
when(() => mockChannel.state.isUpToDate).thenReturn(true);
|
||||||
when(() => mockChannel.state.messagesStream)
|
when(() => mockChannel.state.messagesStream)
|
||||||
.thenAnswer((_) => Stream.value([]));
|
.thenAnswer((_) => Stream.value([]));
|
||||||
|
when(() => mockChannel.state.messages).thenReturn([]);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
StreamChannel(
|
StreamChannel(
|
||||||
@@ -133,6 +134,7 @@ void main() {
|
|||||||
when(() => mockChannel.state.isUpToDate).thenReturn(true);
|
when(() => mockChannel.state.isUpToDate).thenReturn(true);
|
||||||
when(() => mockChannel.state.messagesStream)
|
when(() => mockChannel.state.messagesStream)
|
||||||
.thenAnswer((_) => Stream.value([]));
|
.thenAnswer((_) => Stream.value([]));
|
||||||
|
when(() => mockChannel.state.messages).thenReturn([]);
|
||||||
when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true));
|
when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true));
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@@ -174,6 +176,7 @@ void main() {
|
|||||||
when(() => mockChannel.state.messages).thenReturn(messages);
|
when(() => mockChannel.state.messages).thenReturn(messages);
|
||||||
when(() => mockChannel.state.messagesStream)
|
when(() => mockChannel.state.messagesStream)
|
||||||
.thenAnswer((_) => Stream.value(messages));
|
.thenAnswer((_) => Stream.value(messages));
|
||||||
|
when(() => mockChannel.state.messages).thenReturn(messages);
|
||||||
when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true));
|
when(() => mockChannel.initialized).thenAnswer((_) => Future.value(true));
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
@@ -220,6 +223,7 @@ void main() {
|
|||||||
const error = 'Error! Error! Error!';
|
const error = 'Error! Error! Error!';
|
||||||
when(() => mockChannel.state.messagesStream)
|
when(() => mockChannel.state.messagesStream)
|
||||||
.thenAnswer((_) => Stream.error(error));
|
.thenAnswer((_) => Stream.error(error));
|
||||||
|
when(() => mockChannel.state.messages).thenReturn([]);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Directionality(
|
Directionality(
|
||||||
@@ -259,6 +263,7 @@ void main() {
|
|||||||
const messages = <Message>[];
|
const messages = <Message>[];
|
||||||
when(() => mockChannel.state.messagesStream)
|
when(() => mockChannel.state.messagesStream)
|
||||||
.thenAnswer((_) => Stream.value(messages));
|
.thenAnswer((_) => Stream.value(messages));
|
||||||
|
when(() => mockChannel.state.messages).thenReturn(messages);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Directionality(
|
Directionality(
|
||||||
@@ -305,6 +310,7 @@ void main() {
|
|||||||
const messages = <Message>[];
|
const messages = <Message>[];
|
||||||
when(() => mockChannel.state.messagesStream)
|
when(() => mockChannel.state.messagesStream)
|
||||||
.thenAnswer((_) => Stream.value(messages));
|
.thenAnswer((_) => Stream.value(messages));
|
||||||
|
when(() => mockChannel.state.messages).thenReturn(messages);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Directionality(
|
Directionality(
|
||||||
@@ -349,6 +355,7 @@ void main() {
|
|||||||
final messages = _generateMessages();
|
final messages = _generateMessages();
|
||||||
when(() => mockChannel.state.messagesStream)
|
when(() => mockChannel.state.messagesStream)
|
||||||
.thenAnswer((_) => Stream.value(messages));
|
.thenAnswer((_) => Stream.value(messages));
|
||||||
|
when(() => mockChannel.state.messages).thenReturn(messages);
|
||||||
|
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
Directionality(
|
Directionality(
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import 'package:stream_chat/stream_chat.dart';
|
|||||||
class MockLogger extends Mock implements Logger {}
|
class MockLogger extends Mock implements Logger {}
|
||||||
|
|
||||||
class MockClient extends Mock implements StreamChatClient {
|
class MockClient extends Mock implements StreamChatClient {
|
||||||
|
MockClient() {
|
||||||
|
when(() => wsConnectionStatus).thenReturn(ConnectionStatus.connected);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final Logger logger = MockLogger();
|
final Logger logger = MockLogger();
|
||||||
|
|
||||||
|
|||||||
@@ -235,6 +235,14 @@ void main() {
|
|||||||
final mockClient = MockClient();
|
final mockClient = MockClient();
|
||||||
const streamChatCoreKey = Key('streamChatCore');
|
const streamChatCoreKey = Key('streamChatCore');
|
||||||
const childKey = Key('child');
|
const childKey = Key('child');
|
||||||
|
|
||||||
|
final event = Event();
|
||||||
|
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
||||||
|
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
||||||
|
when(() => mockClient.disconnect()).thenAnswer((_) async => null);
|
||||||
|
when(() => mockClient.wsConnectionStatus)
|
||||||
|
.thenReturn(ConnectionStatus.disconnected);
|
||||||
|
|
||||||
final streamChatCore = StreamChatCore(
|
final streamChatCore = StreamChatCore(
|
||||||
key: streamChatCoreKey,
|
key: streamChatCoreKey,
|
||||||
client: mockClient,
|
client: mockClient,
|
||||||
@@ -247,13 +255,6 @@ void main() {
|
|||||||
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
expect(find.byKey(childKey), findsOneWidget);
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
|
|
||||||
final event = Event();
|
|
||||||
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
|
||||||
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
|
||||||
when(mockClient.disconnect).thenAnswer((_) async => null);
|
|
||||||
when(() => mockClient.wsConnectionStatus)
|
|
||||||
.thenReturn(ConnectionStatus.disconnected);
|
|
||||||
|
|
||||||
final streamChatCoreState = tester.state<StreamChatCoreState>(
|
final streamChatCoreState = tester.state<StreamChatCoreState>(
|
||||||
find.byKey(streamChatCoreKey),
|
find.byKey(streamChatCoreKey),
|
||||||
);
|
);
|
||||||
@@ -323,6 +324,14 @@ void main() {
|
|||||||
const childKey = Key('child');
|
const childKey = Key('child');
|
||||||
final _connectivityController =
|
final _connectivityController =
|
||||||
BehaviorSubject.seeded(ConnectivityResult.none);
|
BehaviorSubject.seeded(ConnectivityResult.none);
|
||||||
|
|
||||||
|
final event = Event();
|
||||||
|
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
||||||
|
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
||||||
|
when(() => mockClient.disconnect()).thenAnswer((_) async => null);
|
||||||
|
when(() => mockClient.wsConnectionStatus)
|
||||||
|
.thenReturn(ConnectionStatus.disconnected);
|
||||||
|
|
||||||
final streamChatCore = StreamChatCore(
|
final streamChatCore = StreamChatCore(
|
||||||
key: streamChatCoreKey,
|
key: streamChatCoreKey,
|
||||||
client: mockClient,
|
client: mockClient,
|
||||||
@@ -335,13 +344,6 @@ void main() {
|
|||||||
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
expect(find.byKey(childKey), findsOneWidget);
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
|
|
||||||
final event = Event();
|
|
||||||
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
|
||||||
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
|
||||||
when(mockClient.disconnect).thenAnswer((_) async => null);
|
|
||||||
when(() => mockClient.wsConnectionStatus)
|
|
||||||
.thenReturn(ConnectionStatus.disconnected);
|
|
||||||
|
|
||||||
_connectivityController.add(ConnectivityResult.mobile);
|
_connectivityController.add(ConnectivityResult.mobile);
|
||||||
|
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
@@ -397,6 +399,14 @@ void main() {
|
|||||||
const childKey = Key('child');
|
const childKey = Key('child');
|
||||||
final _connectivityController =
|
final _connectivityController =
|
||||||
BehaviorSubject.seeded(ConnectivityResult.none);
|
BehaviorSubject.seeded(ConnectivityResult.none);
|
||||||
|
|
||||||
|
final event = Event();
|
||||||
|
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
||||||
|
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
||||||
|
when(() => mockClient.disconnect()).thenAnswer((_) async => null);
|
||||||
|
when(() => mockClient.wsConnectionStatus)
|
||||||
|
.thenReturn(ConnectionStatus.disconnected);
|
||||||
|
|
||||||
final streamChatCore = StreamChatCore(
|
final streamChatCore = StreamChatCore(
|
||||||
key: streamChatCoreKey,
|
key: streamChatCoreKey,
|
||||||
client: mockClient,
|
client: mockClient,
|
||||||
@@ -409,13 +419,6 @@ void main() {
|
|||||||
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
expect(find.byKey(streamChatCoreKey), findsOneWidget);
|
||||||
expect(find.byKey(childKey), findsOneWidget);
|
expect(find.byKey(childKey), findsOneWidget);
|
||||||
|
|
||||||
final event = Event();
|
|
||||||
when(() => mockClient.on()).thenAnswer((_) => Stream.value(event));
|
|
||||||
when(() => mockClient.connect()).thenAnswer((_) async => event);
|
|
||||||
when(mockClient.disconnect).thenAnswer((_) async => null);
|
|
||||||
when(() => mockClient.wsConnectionStatus)
|
|
||||||
.thenReturn(ConnectionStatus.disconnected);
|
|
||||||
|
|
||||||
final streamChatCoreState = tester.state<StreamChatCoreState>(
|
final streamChatCoreState = tester.state<StreamChatCoreState>(
|
||||||
find.byKey(streamChatCoreKey),
|
find.byKey(streamChatCoreKey),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
## 2.0.0-nullsafety.5
|
||||||
|
|
||||||
|
* Update llc dependency
|
||||||
|
* Minor fixes and improvements
|
||||||
|
|
||||||
## 2.0.0-nullsafety.2
|
## 2.0.0-nullsafety.2
|
||||||
|
|
||||||
* Update llc dependency
|
* Update llc dependency
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: stream_chat_persistence
|
name: stream_chat_persistence
|
||||||
homepage: https://github.com/GetStream/stream-chat-flutter
|
homepage: https://github.com/GetStream/stream-chat-flutter
|
||||||
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter.
|
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter.
|
||||||
version: 2.0.0-nullsafety.2
|
version: 2.0.0-nullsafety.5
|
||||||
repository: https://github.com/GetStream/stream-chat-flutter
|
repository: https://github.com/GetStream/stream-chat-flutter
|
||||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ dependencies:
|
|||||||
mutex: ^3.0.0
|
mutex: ^3.0.0
|
||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
path_provider: ^2.0.1
|
path_provider: ^2.0.1
|
||||||
sqlite3_flutter_libs: ^0.4.2
|
sqlite3_flutter_libs: ^0.5.0
|
||||||
stream_chat: ^2.0.0-nullsafety.2
|
stream_chat: ^2.0.0-nullsafety.5
|
||||||
|
|
||||||
dependency_overrides:
|
dependency_overrides:
|
||||||
stream_chat:
|
stream_chat:
|
||||||
@@ -29,4 +29,4 @@ dev_dependencies:
|
|||||||
mocktail: ^0.1.1
|
mocktail: ^0.1.1
|
||||||
moor_generator: ^4.2.1
|
moor_generator: ^4.2.1
|
||||||
pedantic: ^1.11.0
|
pedantic: ^1.11.0
|
||||||
test: ^1.16.8
|
test: ^1.17.7
|
||||||
|
|||||||
Reference in New Issue
Block a user