Merge branch 'develop' of github.com:GetStream/stream-chat-flutter into develop
This commit is contained in:
@@ -1,9 +1,43 @@
|
||||
## Upcoming
|
||||
## 4.0.1
|
||||
|
||||
- Minor fixes
|
||||
|
||||
## 4.0.0
|
||||
|
||||
For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migration_guide_4_0/)
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added `push_provider_name` to `addDevice` API call
|
||||
|
||||
## 4.0.0-beta.2
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Fixed reactions not working for threads in offline mode.
|
||||
- [[#1046]](https://github.com/GetStream/stream-chat-flutter/issues/1046) After `/mute` command on reload cannot access
|
||||
any channel.
|
||||
- [[#1047]](https://github.com/GetStream/stream-chat-flutter/issues/1047) `own_capabilities` extraData missing after
|
||||
channel update.
|
||||
- [[#1054]](https://github.com/GetStream/stream-chat-flutter/issues/1054) Fix `Unsupported operation: Cannot remove from an unmodifiable list`.
|
||||
- [[#1033]](https://github.com/GetStream/stream-chat-flutter/issues/1033) Hard delete from dashboard does not delete message from client.
|
||||
- Send only `user_id` while reconnecting.
|
||||
|
||||
✅ Added
|
||||
|
||||
- Handle `event.message` in `channel.truncate` events
|
||||
- Added additional parameters to `channel.truncate`
|
||||
|
||||
## 4.0.0-beta.0
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added support for ownCapabilities.
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Minor fixes and improvements.
|
||||
|
||||
## 3.6.1
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
- [Chat UI Kit](https://getstream.io/chat/ui-kit/)
|
||||
- [Chat Client Docs](https://getstream.io/chat/docs/flutter-dart/?language=dart)
|
||||
|
||||
**V4 Migration Guide**
|
||||
|
||||
For upgrading from V3 to V4, please refer to the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migration_guide_4_0/)
|
||||
|
||||
### Changelog
|
||||
|
||||
Check out the [changelog on pub.dev](https://pub.dev/packages/stream_chat/changelog) to see the latest changes in the package.
|
||||
|
||||
@@ -11,7 +11,8 @@ dependencies:
|
||||
cupertino_icons: ^1.0.0
|
||||
flutter:
|
||||
sdk: flutter
|
||||
stream_chat: ^2.2.1
|
||||
stream_chat:
|
||||
path: ../
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -294,6 +294,18 @@ class Channel {
|
||||
return data;
|
||||
}
|
||||
|
||||
/// List of user permissions on this channel
|
||||
List<String> get ownCapabilities =>
|
||||
state?._channelState.channel?.ownCapabilities ?? [];
|
||||
|
||||
/// List of user permissions on this channel
|
||||
Stream<List<String>> get ownCapabilitiesStream {
|
||||
_checkInitialized();
|
||||
return state!.channelStateStream
|
||||
.map((cs) => cs.channel?.ownCapabilities ?? [])
|
||||
.distinct();
|
||||
}
|
||||
|
||||
/// Channel extra data as a stream.
|
||||
Stream<Map<String, Object?>> get extraDataStream {
|
||||
_checkInitialized();
|
||||
@@ -489,6 +501,7 @@ class Channel {
|
||||
Future<SendMessageResponse> sendMessage(
|
||||
Message message, {
|
||||
bool skipPush = false,
|
||||
bool skipEnrichUrl = false,
|
||||
}) async {
|
||||
_checkInitialized();
|
||||
// Cancelling previous completer in case it's called again in the process
|
||||
@@ -536,6 +549,7 @@ class Channel {
|
||||
id!,
|
||||
type,
|
||||
skipPush: skipPush,
|
||||
skipEnrichUrl: skipEnrichUrl,
|
||||
);
|
||||
state!.updateMessage(response.message);
|
||||
if (cooldown > 0) cooldownStartedAt = DateTime.now();
|
||||
@@ -552,7 +566,10 @@ class Channel {
|
||||
///
|
||||
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
|
||||
/// before actually updating the message.
|
||||
Future<UpdateMessageResponse> updateMessage(Message message) async {
|
||||
Future<UpdateMessageResponse> updateMessage(
|
||||
Message message, {
|
||||
bool skipEnrichUrl = false,
|
||||
}) async {
|
||||
final originalMessage = message;
|
||||
|
||||
// Cancelling previous completer in case it's called again in the process
|
||||
@@ -590,7 +607,10 @@ class Channel {
|
||||
message = await attachmentsUploadCompleter.future;
|
||||
}
|
||||
|
||||
final response = await _client.updateMessage(message);
|
||||
final response = await _client.updateMessage(
|
||||
message,
|
||||
skipEnrichUrl: skipEnrichUrl,
|
||||
);
|
||||
|
||||
final m = response.message.copyWith(
|
||||
ownReactions: message.ownReactions,
|
||||
@@ -620,12 +640,14 @@ class Channel {
|
||||
Message message, {
|
||||
Map<String, Object?>? set,
|
||||
List<String>? unset,
|
||||
bool skipEnrichUrl = false,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _client.partialUpdateMessage(
|
||||
message.id,
|
||||
set: set,
|
||||
unset: unset,
|
||||
skipEnrichUrl: skipEnrichUrl,
|
||||
);
|
||||
|
||||
final updatedMessage = response.message.copyWith(
|
||||
@@ -1247,7 +1269,9 @@ class Channel {
|
||||
if (preferOffline && cid != null) {
|
||||
final updatedState = await _client.chatPersistenceClient
|
||||
?.getChannelStateByCid(cid!, messagePagination: messagesPagination);
|
||||
if (updatedState != null && updatedState.messages.isNotEmpty) {
|
||||
if (updatedState != null &&
|
||||
updatedState.messages != null &&
|
||||
updatedState.messages!.isNotEmpty) {
|
||||
if (this.state == null) {
|
||||
_initState(updatedState);
|
||||
} else {
|
||||
@@ -1330,14 +1354,6 @@ class Channel {
|
||||
return _client.unmuteChannel(cid!);
|
||||
}
|
||||
|
||||
/// Bans the user with given [userID] from the channel.
|
||||
@Deprecated("Use 'banMember' instead. This method will be removed in v4.0.0")
|
||||
Future<EmptyResponse> banUser(
|
||||
String userID,
|
||||
Map<String, dynamic> options,
|
||||
) =>
|
||||
banMember(userID, options);
|
||||
|
||||
/// Bans the member with given [userID] from the channel.
|
||||
Future<EmptyResponse> banMember(
|
||||
String userID,
|
||||
@@ -1352,12 +1368,6 @@ class Channel {
|
||||
return _client.banUser(userID, opts);
|
||||
}
|
||||
|
||||
/// Remove the ban for the user with given [userID] in the channel.
|
||||
@Deprecated(
|
||||
"Use 'unbanMember' instead. This method will be removed in v4.0.0",
|
||||
)
|
||||
Future<EmptyResponse> unbanUser(String userID) => unbanMember(userID);
|
||||
|
||||
/// Remove the ban for the member with given [userID] in the channel.
|
||||
Future<EmptyResponse> unbanMember(String userID) async {
|
||||
_checkInitialized();
|
||||
@@ -1558,7 +1568,7 @@ class ChannelClientState {
|
||||
|
||||
void _checkExpiredAttachmentMessages(ChannelState channelState) async {
|
||||
final expiredAttachmentMessagesId = channelState.messages
|
||||
.where((m) =>
|
||||
?.where((m) =>
|
||||
!_updatedMessagesIds.contains(m.id) &&
|
||||
m.attachments.isNotEmpty &&
|
||||
m.attachments.any((e) {
|
||||
@@ -1585,7 +1595,8 @@ class ChannelClientState {
|
||||
.map((e) => e.id)
|
||||
.toList();
|
||||
|
||||
if (expiredAttachmentMessagesId.isNotEmpty) {
|
||||
if (expiredAttachmentMessagesId != null &&
|
||||
expiredAttachmentMessagesId.isNotEmpty) {
|
||||
await _channel._initializedCompleter.future;
|
||||
_updatedMessagesIds.addAll(expiredAttachmentMessagesId);
|
||||
_channel.getMessagesById(expiredAttachmentMessagesId);
|
||||
@@ -1595,9 +1606,10 @@ class ChannelClientState {
|
||||
void _listenMemberAdded() {
|
||||
_subscriptions.add(_channel.on(EventType.memberAdded).listen((Event e) {
|
||||
final member = e.member;
|
||||
final existingMembers = channelState.members ?? [];
|
||||
updateChannelState(channelState.copyWith(
|
||||
members: [
|
||||
...channelState.members,
|
||||
...existingMembers,
|
||||
member!,
|
||||
],
|
||||
));
|
||||
@@ -1607,11 +1619,13 @@ class ChannelClientState {
|
||||
void _listenMemberRemoved() {
|
||||
_subscriptions.add(_channel.on(EventType.memberRemoved).listen((Event e) {
|
||||
final user = e.user;
|
||||
final existingMembers = channelState.members ?? [];
|
||||
final existingRead = channelState.read ?? [];
|
||||
updateChannelState(channelState.copyWith(
|
||||
members: channelState.members
|
||||
members: existingMembers
|
||||
.where((m) => m.userId != user!.id)
|
||||
.toList(growable: false),
|
||||
read: channelState.read
|
||||
read: existingRead
|
||||
.where((r) => r.user.id != user!.id)
|
||||
.toList(growable: false),
|
||||
));
|
||||
@@ -1781,9 +1795,10 @@ class ChannelClientState {
|
||||
updateMessage(message);
|
||||
|
||||
if (message.pinned) {
|
||||
final _existingPinnedMessages = _channelState.pinnedMessages ?? [];
|
||||
_channelState = _channelState.copyWith(
|
||||
pinnedMessages: [
|
||||
..._channelState.pinnedMessages,
|
||||
..._existingPinnedMessages,
|
||||
message,
|
||||
],
|
||||
);
|
||||
@@ -1821,10 +1836,6 @@ class ChannelClientState {
|
||||
}));
|
||||
}
|
||||
|
||||
/// Add a [message] to this [channelState].
|
||||
@Deprecated('Use updateMessage instead')
|
||||
void addMessage(Message message) => updateMessage(message);
|
||||
|
||||
/// Updates the [message] in the state if it exists. Adds it otherwise.
|
||||
void updateMessage(Message message) {
|
||||
if (message.parentId == null || message.showInChannel == true) {
|
||||
@@ -1919,7 +1930,7 @@ class ChannelClientState {
|
||||
)
|
||||
.listen(
|
||||
(event) {
|
||||
final readList = List<Read>.from(_channelState.read);
|
||||
final readList = List<Read>.from(_channelState.read ?? []);
|
||||
final userReadIndex =
|
||||
read.indexWhere((r) => r.user.id == event.user!.id);
|
||||
|
||||
@@ -1940,31 +1951,34 @@ class ChannelClientState {
|
||||
}
|
||||
|
||||
/// Channel message list.
|
||||
List<Message> get messages => _channelState.messages;
|
||||
List<Message> get messages => _channelState.messages ?? <Message>[];
|
||||
|
||||
/// Channel message list as a stream.
|
||||
Stream<List<Message>> get messagesStream => channelStateStream
|
||||
.map((cs) => cs.messages)
|
||||
.map((cs) => cs.messages ?? <Message>[])
|
||||
.distinct(const ListEquality().equals);
|
||||
|
||||
/// Channel pinned message list.
|
||||
List<Message> get pinnedMessages => _channelState.pinnedMessages;
|
||||
List<Message> get pinnedMessages =>
|
||||
_channelState.pinnedMessages ?? <Message>[];
|
||||
|
||||
/// Channel pinned message list as a stream.
|
||||
Stream<List<Message>> get pinnedMessagesStream => channelStateStream
|
||||
.map((cs) => cs.pinnedMessages)
|
||||
.map((cs) => cs.pinnedMessages ?? <Message>[])
|
||||
.distinct(const ListEquality().equals);
|
||||
|
||||
/// Get channel last message.
|
||||
Message? get lastMessage =>
|
||||
_channelState.messages.isNotEmpty ? _channelState.messages.last : null;
|
||||
_channelState.messages != null && _channelState.messages!.isNotEmpty
|
||||
? _channelState.messages!.last
|
||||
: null;
|
||||
|
||||
/// Get channel last message.
|
||||
Stream<Message?> get lastMessageStream =>
|
||||
messagesStream.map((event) => event.isNotEmpty ? event.last : null);
|
||||
|
||||
/// Channel members list.
|
||||
List<Member> get members => _channelState.members
|
||||
List<Member> get members => (_channelState.members ?? <Member>[])
|
||||
.map((e) => e.copyWith(user: _channel.client.state.users[e.user!.id]))
|
||||
.toList();
|
||||
|
||||
@@ -1985,7 +1999,7 @@ class ChannelClientState {
|
||||
channelStateStream.map((cs) => cs.watcherCount);
|
||||
|
||||
/// Channel watchers list.
|
||||
List<User> get watchers => _channelState.watchers
|
||||
List<User> get watchers => (_channelState.watchers ?? <User>[])
|
||||
.map((e) => _channel.client.state.users[e.id] ?? e)
|
||||
.toList();
|
||||
|
||||
@@ -1997,11 +2011,20 @@ class ChannelClientState {
|
||||
(watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(),
|
||||
);
|
||||
|
||||
/// Channel member for the current user.
|
||||
Member? get currentUserMember => members.firstWhereOrNull(
|
||||
(m) => m.user?.id == _channel.client.state.currentUser?.id,
|
||||
);
|
||||
|
||||
/// User role for the current user.
|
||||
String? get currentUserRole => currentUserMember?.role;
|
||||
|
||||
/// Channel read list.
|
||||
List<Read> get read => _channelState.read;
|
||||
List<Read> get read => _channelState.read ?? <Read>[];
|
||||
|
||||
/// Channel read list as a stream.
|
||||
Stream<List<Read>> get readStream => channelStateStream.map((cs) => cs.read);
|
||||
Stream<List<Read>> get readStream =>
|
||||
channelStateStream.map((cs) => cs.read ?? <Read>[]);
|
||||
|
||||
bool _isCurrentUserRead(Read read) =>
|
||||
read.user.id == _channel._client.state.currentUser!.id;
|
||||
@@ -2022,7 +2045,7 @@ class ChannelClientState {
|
||||
|
||||
/// Setter for unread count.
|
||||
set unreadCount(int count) {
|
||||
final reads = [..._channelState.read];
|
||||
final reads = [...read];
|
||||
final currentUserReadIndex = reads.indexWhere(_isCurrentUserRead);
|
||||
|
||||
if (currentUserReadIndex < 0) return;
|
||||
@@ -2077,31 +2100,37 @@ class ChannelClientState {
|
||||
|
||||
/// Update channelState with updated information.
|
||||
void updateChannelState(ChannelState updatedState) {
|
||||
final _existingStateMessages = _channelState.messages ?? [];
|
||||
final _updatedStateMessages = updatedState.messages ?? [];
|
||||
final newMessages = <Message>[
|
||||
...updatedState.messages,
|
||||
..._channelState.messages
|
||||
..._updatedStateMessages,
|
||||
..._existingStateMessages
|
||||
.where((m) =>
|
||||
!updatedState.messages.any((newMessage) => newMessage.id == m.id))
|
||||
!_updatedStateMessages.any((newMessage) => newMessage.id == m.id))
|
||||
.toList(),
|
||||
]..sort(_sortByCreatedAt);
|
||||
|
||||
final _existingStateWatchers = _channelState.watchers ?? [];
|
||||
final _updatedStateWatchers = updatedState.watchers ?? [];
|
||||
final newWatchers = <User>[
|
||||
...updatedState.watchers,
|
||||
..._channelState.watchers
|
||||
..._updatedStateWatchers,
|
||||
..._existingStateWatchers
|
||||
.where((w) =>
|
||||
!updatedState.watchers.any((newWatcher) => newWatcher.id == w.id))
|
||||
!_updatedStateWatchers.any((newWatcher) => newWatcher.id == w.id))
|
||||
.toList(),
|
||||
];
|
||||
|
||||
final newMembers = <Member>[
|
||||
...updatedState.members,
|
||||
...updatedState.members ?? [],
|
||||
];
|
||||
|
||||
final _existingStateRead = _channelState.read ?? [];
|
||||
final _updatedStateRead = updatedState.read ?? [];
|
||||
final newReads = <Read>[
|
||||
...updatedState.read,
|
||||
..._channelState.read
|
||||
..._updatedStateRead,
|
||||
..._existingStateRead
|
||||
.where((r) =>
|
||||
!updatedState.read.any((newRead) => newRead.user.id == r.user.id))
|
||||
!_updatedStateRead.any((newRead) => newRead.user.id == r.user.id))
|
||||
.toList(),
|
||||
];
|
||||
|
||||
@@ -2253,9 +2282,9 @@ class ChannelClientState {
|
||||
_pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
final now = DateTime.now();
|
||||
var expiredMessages = channelState.pinnedMessages
|
||||
.where((m) => m.pinExpires?.isBefore(now) == true)
|
||||
?.where((m) => m.pinExpires?.isBefore(now) == true)
|
||||
.toList();
|
||||
if (expiredMessages.isNotEmpty) {
|
||||
if (expiredMessages != null && expiredMessages.isNotEmpty) {
|
||||
expiredMessages = expiredMessages
|
||||
.map((m) => m.copyWith(
|
||||
pinExpires: null,
|
||||
|
||||
@@ -29,7 +29,6 @@ import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
|
||||
import 'package:stream_chat/src/core/util/utils.dart';
|
||||
import 'package:stream_chat/src/db/chat_persistence_client.dart';
|
||||
import 'package:stream_chat/src/event_type.dart';
|
||||
import 'package:stream_chat/src/location.dart';
|
||||
import 'package:stream_chat/src/ws/connection_status.dart';
|
||||
import 'package:stream_chat/src/ws/websocket.dart';
|
||||
import 'package:stream_chat/version.dart';
|
||||
@@ -66,10 +65,6 @@ class StreamChatClient {
|
||||
this.logLevel = Level.WARNING,
|
||||
this.logHandlerFunction = StreamChatClient.defaultLogHandler,
|
||||
RetryPolicy? retryPolicy,
|
||||
@Deprecated('''
|
||||
Location is now deprecated in favor of the new edge server. Will be removed in v4.0.0.
|
||||
Read more here: https://getstream.io/blog/chat-edge-infrastructure
|
||||
''') Location? location,
|
||||
String? baseURL,
|
||||
Duration connectTimeout = const Duration(seconds: 6),
|
||||
Duration receiveTimeout = const Duration(seconds: 6),
|
||||
@@ -426,6 +421,7 @@ class StreamChatClient {
|
||||
}
|
||||
|
||||
void _connectionStatusHandler(ConnectionStatus status) async {
|
||||
final previousState = wsConnectionStatus;
|
||||
final currentState = _wsConnectionStatus = status;
|
||||
|
||||
handleEvent(Event(
|
||||
@@ -433,7 +429,8 @@ class StreamChatClient {
|
||||
online: status == ConnectionStatus.connected,
|
||||
));
|
||||
|
||||
if (currentState == ConnectionStatus.connected) {
|
||||
if (currentState == ConnectionStatus.connected &&
|
||||
previousState != ConnectionStatus.connected) {
|
||||
// connection recovered
|
||||
final cids = state.channels.keys.toList(growable: false);
|
||||
if (cids.isNotEmpty) {
|
||||
@@ -621,7 +618,7 @@ class StreamChatClient {
|
||||
final channels = res.channels;
|
||||
|
||||
final users = channels
|
||||
.expand((it) => it.members)
|
||||
.expand((it) => it.members ?? <Member>[])
|
||||
.map((it) => it.user)
|
||||
.toList(growable: false);
|
||||
|
||||
@@ -1246,12 +1243,14 @@ class StreamChatClient {
|
||||
String channelId,
|
||||
String channelType, {
|
||||
bool skipPush = false,
|
||||
bool skipEnrichUrl = false,
|
||||
}) =>
|
||||
_chatApi.message.sendMessage(
|
||||
channelId,
|
||||
channelType,
|
||||
message,
|
||||
skipPush: skipPush,
|
||||
skipEnrichUrl: skipEnrichUrl,
|
||||
);
|
||||
|
||||
/// Lists all the message replies for the [parentId]
|
||||
@@ -1275,8 +1274,14 @@ class StreamChatClient {
|
||||
);
|
||||
|
||||
/// Update the given message
|
||||
Future<UpdateMessageResponse> updateMessage(Message message) =>
|
||||
_chatApi.message.updateMessage(message);
|
||||
Future<UpdateMessageResponse> updateMessage(
|
||||
Message message, {
|
||||
bool skipEnrichUrl = false,
|
||||
}) =>
|
||||
_chatApi.message.updateMessage(
|
||||
message,
|
||||
skipEnrichUrl: skipEnrichUrl,
|
||||
);
|
||||
|
||||
/// Partially update the given [messageId]
|
||||
/// Use [set] to define values to be set
|
||||
@@ -1285,11 +1290,13 @@ class StreamChatClient {
|
||||
String messageId, {
|
||||
Map<String, Object?>? set,
|
||||
List<String>? unset,
|
||||
bool skipEnrichUrl = false,
|
||||
}) =>
|
||||
_chatApi.message.partialUpdateMessage(
|
||||
messageId,
|
||||
set: set,
|
||||
unset: unset,
|
||||
skipEnrichUrl: skipEnrichUrl,
|
||||
);
|
||||
|
||||
/// Deletes the given message
|
||||
@@ -1555,18 +1562,6 @@ class ClientState {
|
||||
/// The current user as a stream
|
||||
Stream<OwnUser?> get currentUserStream => _currentUserController.stream;
|
||||
|
||||
// coverage:ignore-start
|
||||
|
||||
/// The current user
|
||||
@Deprecated('Use `.currentUser` instead, Will be removed in future releases')
|
||||
OwnUser? get user => _currentUserController.valueOrNull;
|
||||
|
||||
/// The current user as a stream
|
||||
@Deprecated(
|
||||
'Use `.currentUserStream` instead, Will be removed in future releases',
|
||||
)
|
||||
Stream<OwnUser?> get userStream => _currentUserController.stream;
|
||||
|
||||
// coverage:ignore-end
|
||||
|
||||
/// The current user
|
||||
|
||||
@@ -5,9 +5,9 @@ import 'package:rxdart/rxdart.dart';
|
||||
import 'package:stream_chat/src/client/retry_policy.dart';
|
||||
import 'package:stream_chat/stream_chat.dart';
|
||||
|
||||
/// The retry queue associated to a channel
|
||||
/// The retry queue associated to a channel.
|
||||
class RetryQueue {
|
||||
/// Instantiate a new RetryQueue object
|
||||
/// Instantiate a new RetryQueue object.
|
||||
RetryQueue({
|
||||
required this.channel,
|
||||
this.logger,
|
||||
@@ -17,13 +17,13 @@ class RetryQueue {
|
||||
_listenFailedEvents();
|
||||
}
|
||||
|
||||
/// The channel of this queue
|
||||
/// The channel of this queue.
|
||||
final Channel channel;
|
||||
|
||||
/// The client associated with this [channel]
|
||||
/// The client associated with this [channel].
|
||||
final StreamChatClient client;
|
||||
|
||||
/// The logger associated to this queue
|
||||
/// The logger associated to this queue.
|
||||
final Logger? logger;
|
||||
|
||||
late final RetryPolicy _retryPolicy;
|
||||
@@ -63,7 +63,7 @@ class RetryQueue {
|
||||
}).addTo(_compositeSubscription);
|
||||
}
|
||||
|
||||
/// Add a list of messages
|
||||
/// Add a list of messages.
|
||||
void add(List<Message> messages) {
|
||||
if (messages.isEmpty) return;
|
||||
if (!_messageQueue.containsAllMessage(messages)) {
|
||||
@@ -113,6 +113,7 @@ class RetryQueue {
|
||||
} catch (e) {
|
||||
if (e is! StreamChatNetworkError || !e.isRetriable) {
|
||||
_messageQueue.removeMessage(message);
|
||||
_sendFailedEvent(message);
|
||||
return true;
|
||||
}
|
||||
// retry logic
|
||||
@@ -174,10 +175,10 @@ class RetryQueue {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether our [_messageQueue] has messages or not
|
||||
/// Whether our [_messageQueue] has messages or not.
|
||||
bool get hasMessages => _messageQueue.isNotEmpty;
|
||||
|
||||
/// Call this method to dispose this object
|
||||
/// Call this method to dispose this object.
|
||||
void dispose() {
|
||||
_messageQueue.clear();
|
||||
_compositeSubscription.dispose();
|
||||
|
||||
@@ -16,12 +16,14 @@ class MessageApi {
|
||||
String channelType,
|
||||
Message message, {
|
||||
bool skipPush = false,
|
||||
bool skipEnrichUrl = false,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'/channels/$channelType/$channelId/message',
|
||||
data: {
|
||||
'message': message,
|
||||
'skip_push': skipPush,
|
||||
'skip_enrich_url': skipEnrichUrl,
|
||||
},
|
||||
);
|
||||
return SendMessageResponse.fromJson(response.data);
|
||||
@@ -51,11 +53,15 @@ class MessageApi {
|
||||
|
||||
/// Updates the given [message]
|
||||
Future<UpdateMessageResponse> updateMessage(
|
||||
Message message,
|
||||
) async {
|
||||
Message message, {
|
||||
bool skipEnrichUrl = false,
|
||||
}) async {
|
||||
final response = await _client.post(
|
||||
'/messages/${message.id}',
|
||||
data: {'message': message},
|
||||
data: {
|
||||
'message': message,
|
||||
'skip_enrich_url': skipEnrichUrl,
|
||||
},
|
||||
);
|
||||
return UpdateMessageResponse.fromJson(response.data);
|
||||
}
|
||||
@@ -67,12 +73,14 @@ class MessageApi {
|
||||
String messageId, {
|
||||
Map<String, Object?>? set,
|
||||
List<String>? unset,
|
||||
bool skipEnrichUrl = false,
|
||||
}) async {
|
||||
final response = await _client.put(
|
||||
'/messages/$messageId',
|
||||
data: {
|
||||
if (set != null) 'set': set,
|
||||
if (unset != null) 'unset': unset,
|
||||
'skip_enrich_url': skipEnrichUrl,
|
||||
},
|
||||
);
|
||||
return UpdateMessageResponse.fromJson(response.data);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:stream_chat/src/core/api/responses.dart';
|
||||
import 'package:stream_chat/src/core/models/action.dart';
|
||||
import 'package:stream_chat/src/core/models/attachment_file.dart';
|
||||
import 'package:stream_chat/src/core/util/serializer.dart';
|
||||
@@ -66,6 +67,21 @@ class Attachment extends Equatable {
|
||||
topLevelFields + dbSpecificTopLevelFields,
|
||||
));
|
||||
|
||||
factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) =>
|
||||
Attachment(
|
||||
type: ogAttachment.type,
|
||||
title: ogAttachment.title,
|
||||
titleLink: ogAttachment.titleLink,
|
||||
text: ogAttachment.text,
|
||||
imageUrl: ogAttachment.imageUrl,
|
||||
thumbUrl: ogAttachment.thumbUrl,
|
||||
authorName: ogAttachment.authorName,
|
||||
authorLink: ogAttachment.authorLink,
|
||||
assetUrl: ogAttachment.assetUrl,
|
||||
ogScrapeUrl: ogAttachment.ogScrapeUrl,
|
||||
uploadState: const UploadState.success(),
|
||||
);
|
||||
|
||||
///The attachment type based on the URL resource. This can be: audio,
|
||||
///image or video
|
||||
final String? type;
|
||||
@@ -108,8 +124,7 @@ class Attachment extends Equatable {
|
||||
final String? assetUrl;
|
||||
|
||||
/// Actions from a command
|
||||
@JsonKey(defaultValue: [])
|
||||
final List<Action> actions;
|
||||
final List<Action>? actions;
|
||||
|
||||
final Uri? localUri;
|
||||
|
||||
@@ -229,6 +244,33 @@ class Attachment extends Equatable {
|
||||
extraData: extraData ?? this.extraData,
|
||||
);
|
||||
|
||||
Attachment merge(Attachment? other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
type: other.type,
|
||||
titleLink: other.titleLink,
|
||||
title: other.title,
|
||||
thumbUrl: other.thumbUrl,
|
||||
text: other.text,
|
||||
pretext: other.pretext,
|
||||
ogScrapeUrl: other.ogScrapeUrl,
|
||||
imageUrl: other.imageUrl,
|
||||
footerIcon: other.footerIcon,
|
||||
footer: other.footer,
|
||||
fields: other.fields,
|
||||
fallback: other.fallback,
|
||||
color: other.color,
|
||||
authorName: other.authorName,
|
||||
authorLink: other.authorLink,
|
||||
authorIcon: other.authorIcon,
|
||||
assetUrl: other.assetUrl,
|
||||
actions: other.actions,
|
||||
file: other.file,
|
||||
uploadState: other.uploadState,
|
||||
extraData: other.extraData,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
|
||||
@@ -26,9 +26,8 @@ Attachment _$AttachmentFromJson(Map<String, dynamic> json) => Attachment(
|
||||
authorIcon: json['author_icon'] as String?,
|
||||
assetUrl: json['asset_url'] as String?,
|
||||
actions: (json['actions'] as List<dynamic>?)
|
||||
?.map((e) => Action.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[],
|
||||
?.map((e) => Action.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
|
||||
file: json['file'] == null
|
||||
? null
|
||||
@@ -64,7 +63,7 @@ Map<String, dynamic> _$AttachmentToJson(Attachment instance) {
|
||||
writeNotNull('author_link', instance.authorLink);
|
||||
writeNotNull('author_icon', instance.authorIcon);
|
||||
writeNotNull('asset_url', instance.assetUrl);
|
||||
val['actions'] = instance.actions.map((e) => e.toJson()).toList();
|
||||
writeNotNull('actions', instance.actions?.map((e) => e.toJson()).toList());
|
||||
writeNotNull('file', instance.file?.toJson());
|
||||
val['upload_state'] = instance.uploadState.toJson();
|
||||
val['extra_data'] = instance.extraData;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
|
||||
|
||||
part of 'attachment_file.dart';
|
||||
@@ -11,7 +12,7 @@ part of 'attachment_file.dart';
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
|
||||
|
||||
UploadState _$UploadStateFromJson(Map<String, dynamic> json) {
|
||||
switch (json['runtimeType']) {
|
||||
@@ -30,39 +31,6 @@ UploadState _$UploadStateFromJson(Map<String, dynamic> json) {
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$UploadStateTearOff {
|
||||
const _$UploadStateTearOff();
|
||||
|
||||
Preparing preparing() {
|
||||
return const Preparing();
|
||||
}
|
||||
|
||||
InProgress inProgress({required int uploaded, required int total}) {
|
||||
return InProgress(
|
||||
uploaded: uploaded,
|
||||
total: total,
|
||||
);
|
||||
}
|
||||
|
||||
Success success() {
|
||||
return const Success();
|
||||
}
|
||||
|
||||
Failed failed({required String error}) {
|
||||
return Failed(
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
|
||||
UploadState fromJson(Map<String, Object?> json) {
|
||||
return UploadState.fromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
const $UploadState = _$UploadStateTearOff();
|
||||
|
||||
/// @nodoc
|
||||
mixin _$UploadState {
|
||||
@optionalTypeArgs
|
||||
@@ -153,7 +121,7 @@ class _$PreparingCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$Preparing implements Preparing {
|
||||
const _$Preparing({String? $type}) : $type = $type ?? 'preparing';
|
||||
const _$Preparing({final String? $type}) : $type = $type ?? 'preparing';
|
||||
|
||||
factory _$Preparing.fromJson(Map<String, dynamic> json) =>
|
||||
_$$PreparingFromJson(json);
|
||||
@@ -172,6 +140,7 @@ class _$Preparing implements Preparing {
|
||||
(other.runtimeType == runtimeType && other is Preparing);
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@@ -300,7 +269,7 @@ class _$InProgressCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
||||
@JsonSerializable()
|
||||
class _$InProgress implements InProgress {
|
||||
const _$InProgress(
|
||||
{required this.uploaded, required this.total, String? $type})
|
||||
{required this.uploaded, required this.total, final String? $type})
|
||||
: $type = $type ?? 'inProgress';
|
||||
|
||||
factory _$InProgress.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -324,13 +293,16 @@ class _$InProgress implements InProgress {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is InProgress &&
|
||||
(identical(other.uploaded, uploaded) ||
|
||||
other.uploaded == uploaded) &&
|
||||
(identical(other.total, total) || other.total == total));
|
||||
const DeepCollectionEquality().equals(other.uploaded, uploaded) &&
|
||||
const DeepCollectionEquality().equals(other.total, total));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, uploaded, total);
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
const DeepCollectionEquality().hash(uploaded),
|
||||
const DeepCollectionEquality().hash(total));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@@ -418,14 +390,14 @@ class _$InProgress implements InProgress {
|
||||
}
|
||||
|
||||
abstract class InProgress implements UploadState {
|
||||
const factory InProgress({required int uploaded, required int total}) =
|
||||
_$InProgress;
|
||||
const factory InProgress(
|
||||
{required final int uploaded, required final int total}) = _$InProgress;
|
||||
|
||||
factory InProgress.fromJson(Map<String, dynamic> json) =
|
||||
_$InProgress.fromJson;
|
||||
|
||||
int get uploaded;
|
||||
int get total;
|
||||
int get uploaded => throw _privateConstructorUsedError;
|
||||
int get total => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$InProgressCopyWith<InProgress> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
@@ -450,7 +422,7 @@ class _$SuccessCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$Success implements Success {
|
||||
const _$Success({String? $type}) : $type = $type ?? 'success';
|
||||
const _$Success({final String? $type}) : $type = $type ?? 'success';
|
||||
|
||||
factory _$Success.fromJson(Map<String, dynamic> json) =>
|
||||
_$$SuccessFromJson(json);
|
||||
@@ -469,6 +441,7 @@ class _$Success implements Success {
|
||||
(other.runtimeType == runtimeType && other is Success);
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => runtimeType.hashCode;
|
||||
|
||||
@@ -590,7 +563,7 @@ class _$FailedCopyWithImpl<$Res> extends _$UploadStateCopyWithImpl<$Res>
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
class _$Failed implements Failed {
|
||||
const _$Failed({required this.error, String? $type})
|
||||
const _$Failed({required this.error, final String? $type})
|
||||
: $type = $type ?? 'failed';
|
||||
|
||||
factory _$Failed.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -612,11 +585,13 @@ class _$Failed implements Failed {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is Failed &&
|
||||
(identical(other.error, error) || other.error == error));
|
||||
const DeepCollectionEquality().equals(other.error, error));
|
||||
}
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType, error);
|
||||
int get hashCode =>
|
||||
Object.hash(runtimeType, const DeepCollectionEquality().hash(error));
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@@ -704,11 +679,11 @@ class _$Failed implements Failed {
|
||||
}
|
||||
|
||||
abstract class Failed implements UploadState {
|
||||
const factory Failed({required String error}) = _$Failed;
|
||||
const factory Failed({required final String error}) = _$Failed;
|
||||
|
||||
factory Failed.fromJson(Map<String, dynamic> json) = _$Failed.fromJson;
|
||||
|
||||
String get error;
|
||||
String get error => throw _privateConstructorUsedError;
|
||||
@JsonKey(ignore: true)
|
||||
$FailedCopyWith<Failed> get copyWith => throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class ChannelModel {
|
||||
String? id,
|
||||
String? type,
|
||||
String? cid,
|
||||
this.ownCapabilities,
|
||||
ChannelConfig? config,
|
||||
this.createdBy,
|
||||
this.frozen = false,
|
||||
@@ -51,6 +52,10 @@ class ChannelModel {
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final String cid;
|
||||
|
||||
/// List of user permissions on this channel
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final List<String>? ownCapabilities;
|
||||
|
||||
/// The channel configuration data
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final ChannelConfig config;
|
||||
@@ -101,6 +106,7 @@ class ChannelModel {
|
||||
'id',
|
||||
'type',
|
||||
'cid',
|
||||
'own_capabilities',
|
||||
'config',
|
||||
'created_by',
|
||||
'frozen',
|
||||
@@ -127,6 +133,7 @@ class ChannelModel {
|
||||
String? id,
|
||||
String? type,
|
||||
String? cid,
|
||||
List<String>? ownCapabilities,
|
||||
ChannelConfig? config,
|
||||
User? createdBy,
|
||||
bool? frozen,
|
||||
@@ -143,6 +150,7 @@ class ChannelModel {
|
||||
id: id ?? this.id,
|
||||
type: type ?? this.type,
|
||||
cid: cid ?? this.cid,
|
||||
ownCapabilities: ownCapabilities ?? this.ownCapabilities,
|
||||
config: config ?? this.config,
|
||||
createdBy: createdBy ?? this.createdBy,
|
||||
frozen: frozen ?? this.frozen,
|
||||
@@ -164,6 +172,7 @@ class ChannelModel {
|
||||
id: other.id,
|
||||
type: other.type,
|
||||
cid: other.cid,
|
||||
ownCapabilities: other.ownCapabilities,
|
||||
config: other.config,
|
||||
createdBy: other.createdBy,
|
||||
frozen: other.frozen,
|
||||
|
||||
@@ -10,6 +10,9 @@ ChannelModel _$ChannelModelFromJson(Map<String, dynamic> json) => ChannelModel(
|
||||
id: json['id'] as String?,
|
||||
type: json['type'] as String?,
|
||||
cid: json['cid'] as String?,
|
||||
ownCapabilities: (json['own_capabilities'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
config: json['config'] == null
|
||||
? null
|
||||
: ChannelConfig.fromJson(json['config'] as Map<String, dynamic>),
|
||||
@@ -48,6 +51,7 @@ Map<String, dynamic> _$ChannelModelToJson(ChannelModel instance) {
|
||||
}
|
||||
|
||||
writeNotNull('cid', readonly(instance.cid));
|
||||
writeNotNull('own_capabilities', readonly(instance.ownCapabilities));
|
||||
writeNotNull('config', readonly(instance.config));
|
||||
writeNotNull('created_by', readonly(instance.createdBy));
|
||||
val['frozen'] = instance.frozen;
|
||||
|
||||
@@ -7,42 +7,40 @@ import 'package:stream_chat/src/core/models/user.dart';
|
||||
|
||||
part 'channel_state.g.dart';
|
||||
|
||||
const _emptyPinnedMessages = <Message>[];
|
||||
|
||||
/// The class that contains the information about a channel
|
||||
@JsonSerializable()
|
||||
class ChannelState {
|
||||
/// Constructor used for json serialization
|
||||
ChannelState({
|
||||
this.channel,
|
||||
this.messages = const [],
|
||||
this.members = const [],
|
||||
this.pinnedMessages = _emptyPinnedMessages,
|
||||
this.messages,
|
||||
this.members,
|
||||
this.pinnedMessages,
|
||||
this.watcherCount,
|
||||
this.watchers = const [],
|
||||
this.read = const [],
|
||||
this.watchers,
|
||||
this.read,
|
||||
});
|
||||
|
||||
/// The channel to which this state belongs
|
||||
final ChannelModel? channel;
|
||||
|
||||
/// A paginated list of channel messages
|
||||
final List<Message> messages;
|
||||
final List<Message>? messages;
|
||||
|
||||
/// A paginated list of channel members
|
||||
final List<Member> members;
|
||||
final List<Member>? members;
|
||||
|
||||
/// A paginated list of pinned messages
|
||||
final List<Message> pinnedMessages;
|
||||
final List<Message>? pinnedMessages;
|
||||
|
||||
/// The count of users watching the channel
|
||||
final int? watcherCount;
|
||||
|
||||
/// A paginated list of users watching the channel
|
||||
final List<User> watchers;
|
||||
final List<User>? watchers;
|
||||
|
||||
/// The list of channel reads
|
||||
final List<Read> read;
|
||||
final List<Read>? read;
|
||||
|
||||
/// Create a new instance from a json
|
||||
static ChannelState fromJson(Map<String, dynamic> json) =>
|
||||
@@ -56,7 +54,7 @@ class ChannelState {
|
||||
ChannelModel? channel,
|
||||
List<Message>? messages,
|
||||
List<Member>? members,
|
||||
List<Message> pinnedMessages = _emptyPinnedMessages,
|
||||
List<Message>? pinnedMessages,
|
||||
int? watcherCount,
|
||||
List<User>? watchers,
|
||||
List<Read>? read,
|
||||
@@ -65,11 +63,7 @@ class ChannelState {
|
||||
channel: channel ?? this.channel,
|
||||
messages: messages ?? this.messages,
|
||||
members: members ?? this.members,
|
||||
// Hack to avoid using the default value in case nothing is provided.
|
||||
// FIXME: Use non-nullable by default instead of empty list.
|
||||
pinnedMessages: pinnedMessages == _emptyPinnedMessages
|
||||
? this.pinnedMessages
|
||||
: pinnedMessages,
|
||||
pinnedMessages: pinnedMessages ?? this.pinnedMessages,
|
||||
watcherCount: watcherCount ?? this.watcherCount,
|
||||
watchers: watchers ?? this.watchers,
|
||||
read: read ?? this.read,
|
||||
|
||||
@@ -11,36 +11,31 @@ ChannelState _$ChannelStateFromJson(Map<String, dynamic> json) => ChannelState(
|
||||
? null
|
||||
: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>),
|
||||
messages: (json['messages'] as List<dynamic>?)
|
||||
?.map((e) => Message.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
?.map((e) => Message.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
members: (json['members'] as List<dynamic>?)
|
||||
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
?.map((e) => Member.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
pinnedMessages: (json['pinned_messages'] as List<dynamic>?)
|
||||
?.map((e) => Message.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
_emptyPinnedMessages,
|
||||
?.map((e) => Message.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
watcherCount: json['watcher_count'] as int?,
|
||||
watchers: (json['watchers'] as List<dynamic>?)
|
||||
?.map((e) => User.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
?.map((e) => User.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
read: (json['read'] as List<dynamic>?)
|
||||
?.map((e) => Read.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
?.map((e) => Read.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ChannelStateToJson(ChannelState instance) =>
|
||||
<String, dynamic>{
|
||||
'channel': instance.channel?.toJson(),
|
||||
'messages': instance.messages.map((e) => e.toJson()).toList(),
|
||||
'members': instance.members.map((e) => e.toJson()).toList(),
|
||||
'messages': instance.messages?.map((e) => e.toJson()).toList(),
|
||||
'members': instance.members?.map((e) => e.toJson()).toList(),
|
||||
'pinned_messages':
|
||||
instance.pinnedMessages.map((e) => e.toJson()).toList(),
|
||||
instance.pinnedMessages?.map((e) => e.toJson()).toList(),
|
||||
'watcher_count': instance.watcherCount,
|
||||
'watchers': instance.watchers.map((e) => e.toJson()).toList(),
|
||||
'read': instance.read.map((e) => e.toJson()).toList(),
|
||||
'watchers': instance.watchers?.map((e) => e.toJson()).toList(),
|
||||
'read': instance.read?.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
|
||||
@@ -8,13 +8,13 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
part 'message.g.dart';
|
||||
|
||||
class _PinExpires {
|
||||
const _PinExpires();
|
||||
class _NullConst {
|
||||
const _NullConst();
|
||||
}
|
||||
|
||||
const _pinExpires = _PinExpires();
|
||||
const _nullConst = _NullConst();
|
||||
|
||||
/// Enum defining the status of a sending message
|
||||
/// Enum defining the status of a sending message.
|
||||
enum MessageSendingStatus {
|
||||
/// Message is being sent
|
||||
sending,
|
||||
@@ -40,10 +40,10 @@ enum MessageSendingStatus {
|
||||
sent,
|
||||
}
|
||||
|
||||
/// The class that contains the information about a message
|
||||
/// The class that contains the information about a message.
|
||||
@JsonSerializable()
|
||||
class Message extends Equatable {
|
||||
/// Constructor used for json serialization
|
||||
/// Constructor used for json serialization.
|
||||
Message({
|
||||
String? id,
|
||||
this.text,
|
||||
@@ -58,44 +58,47 @@ class Message extends Equatable {
|
||||
this.ownReactions,
|
||||
this.parentId,
|
||||
this.quotedMessage,
|
||||
this.quotedMessageId,
|
||||
String? quotedMessageId,
|
||||
this.replyCount = 0,
|
||||
this.threadParticipants,
|
||||
this.showInChannel,
|
||||
this.command,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
this.deletedAt,
|
||||
this.user,
|
||||
this.pinned = false,
|
||||
this.pinnedAt,
|
||||
DateTime? pinExpires,
|
||||
this.pinnedBy,
|
||||
this.extraData = const {},
|
||||
this.deletedAt,
|
||||
this.status = MessageSendingStatus.sent,
|
||||
this.status = MessageSendingStatus.sending,
|
||||
this.i18n,
|
||||
}) : id = id ?? const Uuid().v4(),
|
||||
pinExpires = pinExpires?.toUtc(),
|
||||
createdAt = createdAt ?? DateTime.now(),
|
||||
updatedAt = updatedAt ?? DateTime.now();
|
||||
_createdAt = createdAt,
|
||||
_updatedAt = updatedAt,
|
||||
_quotedMessageId = quotedMessageId;
|
||||
|
||||
/// Create a new instance from a json
|
||||
/// Create a new instance from JSON.
|
||||
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
|
||||
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
|
||||
).copyWith(
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
/// The message ID. This is either created by Stream or set client side when
|
||||
/// the message is added.
|
||||
final String id;
|
||||
|
||||
/// The text of this message
|
||||
/// The text of this message.
|
||||
final String? text;
|
||||
|
||||
/// The status of a sending message
|
||||
/// The status of a sending message.
|
||||
@JsonKey(ignore: true)
|
||||
final MessageSendingStatus status;
|
||||
|
||||
/// The message type
|
||||
/// The message type.
|
||||
@JsonKey(
|
||||
includeIfNull: false,
|
||||
toJson: Serializer.readOnly,
|
||||
@@ -107,15 +110,15 @@ class Message extends Equatable {
|
||||
@JsonKey(includeIfNull: false)
|
||||
final List<Attachment> attachments;
|
||||
|
||||
/// The list of user mentioned in the message
|
||||
/// The list of user mentioned in the message.
|
||||
@JsonKey(toJson: User.toIds)
|
||||
final List<User> mentionedUsers;
|
||||
|
||||
/// A map describing the count of number of every reaction
|
||||
/// A map describing the count of number of every reaction.
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final Map<String, int>? reactionCounts;
|
||||
|
||||
/// A map describing the count of score of every reaction
|
||||
/// A map describing the count of score of every reaction.
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final Map<String, int>? reactionScores;
|
||||
|
||||
@@ -130,12 +133,14 @@ class Message extends Equatable {
|
||||
/// The ID of the parent message, if the message is a thread reply.
|
||||
final String? parentId;
|
||||
|
||||
/// A quoted reply message
|
||||
/// A quoted reply message.
|
||||
@JsonKey(toJson: Serializer.readOnly)
|
||||
final Message? quotedMessage;
|
||||
|
||||
final String? _quotedMessageId;
|
||||
|
||||
/// The ID of the quoted message, if the message is a quoted reply.
|
||||
final String? quotedMessageId;
|
||||
String? get quotedMessageId => _quotedMessageId ?? quotedMessage?.id;
|
||||
|
||||
/// Reserved field indicating the number of replies for this message.
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
@@ -148,10 +153,10 @@ class Message extends Equatable {
|
||||
/// Check if this message needs to show in the channel.
|
||||
final bool? showInChannel;
|
||||
|
||||
/// If true the message is silent
|
||||
/// If true the message is silent.
|
||||
final bool silent;
|
||||
|
||||
/// If true the message is shadowed
|
||||
/// If true the message is shadowed.
|
||||
@JsonKey(
|
||||
includeIfNull: false,
|
||||
toJson: Serializer.readOnly,
|
||||
@@ -162,56 +167,61 @@ class Message extends Equatable {
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final String? command;
|
||||
|
||||
/// Reserved field indicating when the message was created.
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Reserved field indicating when the message was updated last time.
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// User who sent the message
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final User? user;
|
||||
|
||||
/// If true the message is pinned
|
||||
final bool pinned;
|
||||
|
||||
/// Reserved field indicating when the message was pinned
|
||||
@JsonKey(toJson: Serializer.readOnly)
|
||||
final DateTime? pinnedAt;
|
||||
|
||||
/// Reserved field indicating when the message will expire
|
||||
///
|
||||
/// if `null` message has no expiry
|
||||
final DateTime? pinExpires;
|
||||
|
||||
/// Reserved field indicating who pinned the message
|
||||
@JsonKey(toJson: Serializer.readOnly)
|
||||
final User? pinnedBy;
|
||||
|
||||
/// Message custom extraData
|
||||
@JsonKey(includeIfNull: false)
|
||||
final Map<String, Object?> extraData;
|
||||
|
||||
/// True if the message is a system info
|
||||
bool get isSystem => type == 'system';
|
||||
|
||||
/// True if the message has been deleted
|
||||
bool get isDeleted => type == 'deleted';
|
||||
|
||||
/// True if the message is ephemeral
|
||||
bool get isEphemeral => type == 'ephemeral';
|
||||
final DateTime? _createdAt;
|
||||
|
||||
/// Reserved field indicating when the message was deleted.
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final DateTime? deletedAt;
|
||||
|
||||
/// Reserved field indicating when the message was created.
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
DateTime get createdAt => _createdAt ?? DateTime.now();
|
||||
|
||||
final DateTime? _updatedAt;
|
||||
|
||||
/// Reserved field indicating when the message was updated last time.
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
DateTime get updatedAt => _updatedAt ?? DateTime.now();
|
||||
|
||||
/// User who sent the message.
|
||||
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
|
||||
final User? user;
|
||||
|
||||
/// If true the message is pinned.
|
||||
final bool pinned;
|
||||
|
||||
/// Reserved field indicating when the message was pinned.
|
||||
@JsonKey(toJson: Serializer.readOnly)
|
||||
final DateTime? pinnedAt;
|
||||
|
||||
/// Reserved field indicating when the message will expire.
|
||||
///
|
||||
/// If `null` message has no expiry.
|
||||
final DateTime? pinExpires;
|
||||
|
||||
/// Reserved field indicating who pinned the message.
|
||||
@JsonKey(toJson: Serializer.readOnly)
|
||||
final User? pinnedBy;
|
||||
|
||||
/// Message custom extraData.
|
||||
@JsonKey(includeIfNull: false)
|
||||
final Map<String, Object?> extraData;
|
||||
|
||||
/// True if the message is a system info.
|
||||
bool get isSystem => type == 'system';
|
||||
|
||||
/// True if the message has been deleted.
|
||||
bool get isDeleted => type == 'deleted';
|
||||
|
||||
/// True if the message is ephemeral.
|
||||
bool get isEphemeral => type == 'ephemeral';
|
||||
|
||||
/// A Map of translations.
|
||||
@JsonKey(includeIfNull: false)
|
||||
final Map<String, String>? i18n;
|
||||
|
||||
/// Known top level fields.
|
||||
///
|
||||
/// Useful for [Serializer] methods.
|
||||
static const topLevelFields = [
|
||||
'id',
|
||||
@@ -244,7 +254,7 @@ class Message extends Equatable {
|
||||
'i18n',
|
||||
];
|
||||
|
||||
/// Serialize to json
|
||||
/// Serialize to json.
|
||||
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
|
||||
_$MessageToJson(this),
|
||||
);
|
||||
@@ -256,18 +266,18 @@ class Message extends Equatable {
|
||||
String? type,
|
||||
List<Attachment>? attachments,
|
||||
List<User>? mentionedUsers,
|
||||
bool? silent,
|
||||
bool? shadowed,
|
||||
Map<String, int>? reactionCounts,
|
||||
Map<String, int>? reactionScores,
|
||||
List<Reaction>? latestReactions,
|
||||
List<Reaction>? ownReactions,
|
||||
String? parentId,
|
||||
Message? quotedMessage,
|
||||
String? quotedMessageId,
|
||||
Object? quotedMessage = _nullConst,
|
||||
Object? quotedMessageId = _nullConst,
|
||||
int? replyCount,
|
||||
List<User>? threadParticipants,
|
||||
bool? showInChannel,
|
||||
bool? shadowed,
|
||||
bool? silent,
|
||||
String? command,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
@@ -275,7 +285,7 @@ class Message extends Equatable {
|
||||
User? user,
|
||||
bool? pinned,
|
||||
DateTime? pinnedAt,
|
||||
Object? pinExpires = _pinExpires,
|
||||
Object? pinExpires = _nullConst,
|
||||
User? pinnedBy,
|
||||
Map<String, Object?>? extraData,
|
||||
MessageSendingStatus? status,
|
||||
@@ -284,41 +294,68 @@ class Message extends Equatable {
|
||||
assert(() {
|
||||
if (pinExpires is! DateTime &&
|
||||
pinExpires != null &&
|
||||
pinExpires is! _PinExpires) {
|
||||
pinExpires is! _NullConst) {
|
||||
throw ArgumentError('`pinExpires` can only be set as DateTime or null');
|
||||
}
|
||||
return true;
|
||||
}(), 'Validate type for pinExpires');
|
||||
|
||||
assert(() {
|
||||
if (quotedMessage is! Message &&
|
||||
quotedMessage != null &&
|
||||
quotedMessage is! _NullConst) {
|
||||
throw ArgumentError(
|
||||
'`quotedMessage` can only be set as Message or null',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}(), 'Validate type for quotedMessage');
|
||||
|
||||
assert(() {
|
||||
if (quotedMessageId is! String &&
|
||||
quotedMessageId != null &&
|
||||
quotedMessageId is! _NullConst) {
|
||||
throw ArgumentError(
|
||||
'`quotedMessage` can only be set as String or null',
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}(), 'Validate type for quotedMessage');
|
||||
|
||||
return Message(
|
||||
id: id ?? this.id,
|
||||
text: text ?? this.text,
|
||||
type: type ?? this.type,
|
||||
attachments: attachments ?? this.attachments,
|
||||
mentionedUsers: mentionedUsers ?? this.mentionedUsers,
|
||||
silent: silent ?? this.silent,
|
||||
shadowed: shadowed ?? this.shadowed,
|
||||
reactionCounts: reactionCounts ?? this.reactionCounts,
|
||||
reactionScores: reactionScores ?? this.reactionScores,
|
||||
latestReactions: latestReactions ?? this.latestReactions,
|
||||
ownReactions: ownReactions ?? this.ownReactions,
|
||||
parentId: parentId ?? this.parentId,
|
||||
quotedMessage: quotedMessage ?? this.quotedMessage,
|
||||
quotedMessageId: quotedMessageId ?? this.quotedMessageId,
|
||||
quotedMessage: quotedMessage == _nullConst
|
||||
? this.quotedMessage
|
||||
: quotedMessage as Message?,
|
||||
quotedMessageId: quotedMessageId == _nullConst
|
||||
? _quotedMessageId
|
||||
: quotedMessageId as String?,
|
||||
replyCount: replyCount ?? this.replyCount,
|
||||
threadParticipants: threadParticipants ?? this.threadParticipants,
|
||||
showInChannel: showInChannel ?? this.showInChannel,
|
||||
command: command ?? this.command,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
silent: silent ?? this.silent,
|
||||
extraData: extraData ?? this.extraData,
|
||||
user: user ?? this.user,
|
||||
shadowed: shadowed ?? this.shadowed,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
createdAt: createdAt ?? _createdAt,
|
||||
updatedAt: updatedAt ?? _updatedAt,
|
||||
deletedAt: deletedAt ?? this.deletedAt,
|
||||
status: status ?? this.status,
|
||||
user: user ?? this.user,
|
||||
pinned: pinned ?? this.pinned,
|
||||
pinnedAt: pinnedAt ?? this.pinnedAt,
|
||||
pinnedBy: pinnedBy ?? this.pinnedBy,
|
||||
pinExpires:
|
||||
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
|
||||
pinExpires == _nullConst ? this.pinExpires : pinExpires as DateTime?,
|
||||
pinnedBy: pinnedBy ?? this.pinnedBy,
|
||||
extraData: extraData ?? this.extraData,
|
||||
status: status ?? this.status,
|
||||
i18n: i18n ?? this.i18n,
|
||||
);
|
||||
}
|
||||
@@ -331,6 +368,8 @@ class Message extends Equatable {
|
||||
type: other.type,
|
||||
attachments: other.attachments,
|
||||
mentionedUsers: other.mentionedUsers,
|
||||
silent: other.silent,
|
||||
shadowed: other.shadowed,
|
||||
reactionCounts: other.reactionCounts,
|
||||
reactionScores: other.reactionScores,
|
||||
latestReactions: other.latestReactions,
|
||||
@@ -343,17 +382,15 @@ class Message extends Equatable {
|
||||
showInChannel: other.showInChannel,
|
||||
command: other.command,
|
||||
createdAt: other.createdAt,
|
||||
silent: other.silent,
|
||||
extraData: other.extraData,
|
||||
user: other.user,
|
||||
shadowed: other.shadowed,
|
||||
updatedAt: other.updatedAt,
|
||||
deletedAt: other.deletedAt,
|
||||
status: other.status,
|
||||
user: other.user,
|
||||
pinned: other.pinned,
|
||||
pinnedAt: other.pinnedAt,
|
||||
pinExpires: other.pinExpires,
|
||||
pinnedBy: other.pinnedBy,
|
||||
extraData: other.extraData,
|
||||
status: other.status,
|
||||
i18n: other.i18n,
|
||||
);
|
||||
|
||||
@@ -377,8 +414,8 @@ class Message extends Equatable {
|
||||
shadowed,
|
||||
silent,
|
||||
command,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
_createdAt,
|
||||
_updatedAt,
|
||||
deletedAt,
|
||||
user,
|
||||
pinned,
|
||||
|
||||
@@ -49,6 +49,9 @@ Message _$MessageFromJson(Map<String, dynamic> json) => Message(
|
||||
updatedAt: json['updated_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
user: json['user'] == null
|
||||
? null
|
||||
: User.fromJson(json['user'] as Map<String, dynamic>),
|
||||
@@ -63,9 +66,6 @@ Message _$MessageFromJson(Map<String, dynamic> json) => Message(
|
||||
? null
|
||||
: User.fromJson(json['pinned_by'] as Map<String, dynamic>),
|
||||
extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
i18n: (json['i18n'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
@@ -99,6 +99,7 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
|
||||
val['silent'] = instance.silent;
|
||||
writeNotNull('shadowed', readonly(instance.shadowed));
|
||||
writeNotNull('command', readonly(instance.command));
|
||||
writeNotNull('deleted_at', readonly(instance.deletedAt));
|
||||
writeNotNull('created_at', readonly(instance.createdAt));
|
||||
writeNotNull('updated_at', readonly(instance.updatedAt));
|
||||
writeNotNull('user', readonly(instance.user));
|
||||
@@ -107,7 +108,6 @@ Map<String, dynamic> _$MessageToJson(Message instance) {
|
||||
val['pin_expires'] = instance.pinExpires?.toIso8601String();
|
||||
val['pinned_by'] = readonly(instance.pinnedBy);
|
||||
val['extra_data'] = instance.extraData;
|
||||
writeNotNull('deleted_at', readonly(instance.deletedAt));
|
||||
writeNotNull('i18n', instance.i18n);
|
||||
return val;
|
||||
}
|
||||
|
||||
@@ -44,10 +44,10 @@ abstract class ChatPersistenceClient {
|
||||
Future<ChannelModel?> getChannelByCid(String cid);
|
||||
|
||||
/// Get stored channel [Member]s by providing channel [cid]
|
||||
Future<List<Member>> getMembersByCid(String cid);
|
||||
Future<List<Member>?> getMembersByCid(String cid);
|
||||
|
||||
/// Get stored channel [Read]s by providing channel [cid]
|
||||
Future<List<Read>> getReadsByCid(String cid);
|
||||
Future<List<Read>?> getReadsByCid(String cid);
|
||||
|
||||
/// Get stored [Message]s by providing channel [cid]
|
||||
///
|
||||
@@ -78,15 +78,11 @@ abstract class ChatPersistenceClient {
|
||||
getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
|
||||
]);
|
||||
return ChannelState(
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
members: data[0] as List<Member>,
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
read: data[1] as List<Read>,
|
||||
members: data[0] as List<Member>?,
|
||||
read: data[1] as List<Read>?,
|
||||
channel: data[2] as ChannelModel?,
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
messages: data[3] as List<Message>,
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
pinnedMessages: data[4] as List<Message>,
|
||||
messages: data[3] as List<Message>?,
|
||||
pinnedMessages: data[4] as List<Message>?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,7 +142,7 @@ abstract class ChatPersistenceClient {
|
||||
bulkUpdateMessages({cid: messages});
|
||||
|
||||
/// Bulk updates the message data of multiple channels.
|
||||
Future<void> bulkUpdateMessages(Map<String, List<Message>> messages);
|
||||
Future<void> bulkUpdateMessages(Map<String, List<Message>?> messages);
|
||||
|
||||
/// Updates the pinned message data of a particular channel [cid] with
|
||||
/// the new [messages] data
|
||||
@@ -154,7 +150,7 @@ abstract class ChatPersistenceClient {
|
||||
bulkUpdatePinnedMessages({cid: messages});
|
||||
|
||||
/// Bulk updates the message data of multiple channels.
|
||||
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>> messages);
|
||||
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>?> messages);
|
||||
|
||||
/// Returns all the threads by parent message of a particular channel by
|
||||
/// providing channel [cid]
|
||||
@@ -169,7 +165,7 @@ abstract class ChatPersistenceClient {
|
||||
bulkUpdateMembers({cid: members});
|
||||
|
||||
/// Bulk updates the members data of multiple channels.
|
||||
Future<void> bulkUpdateMembers(Map<String, List<Member>> members);
|
||||
Future<void> bulkUpdateMembers(Map<String, List<Member>?> members);
|
||||
|
||||
/// Updates the read data of a particular channel [cid] with
|
||||
/// the new [reads] data
|
||||
@@ -177,7 +173,7 @@ abstract class ChatPersistenceClient {
|
||||
bulkUpdateReads({cid: reads});
|
||||
|
||||
/// Bulk updates the read data of multiple channels.
|
||||
Future<void> bulkUpdateReads(Map<String, List<Read>> reads);
|
||||
Future<void> bulkUpdateReads(Map<String, List<Read>?> reads);
|
||||
|
||||
/// Updates the users data with the new [users] data
|
||||
Future<void> updateUsers(List<User> users);
|
||||
@@ -230,10 +226,10 @@ abstract class ChatPersistenceClient {
|
||||
final membersToDelete = <String>[];
|
||||
|
||||
final channels = <ChannelModel>[];
|
||||
final channelWithMessages = <String, List<Message>>{};
|
||||
final channelWithPinnedMessages = <String, List<Message>>{};
|
||||
final channelWithReads = <String, List<Read>>{};
|
||||
final channelWithMembers = <String, List<Member>>{};
|
||||
final channelWithMessages = <String, List<Message>?>{};
|
||||
final channelWithPinnedMessages = <String, List<Message>?>{};
|
||||
final channelWithReads = <String, List<Read>?>{};
|
||||
final channelWithMembers = <String, List<Member>?>{};
|
||||
|
||||
final users = <User>[];
|
||||
final reactions = <Reaction>[];
|
||||
@@ -252,8 +248,9 @@ abstract class ChatPersistenceClient {
|
||||
|
||||
// Preparing deletion data
|
||||
membersToDelete.add(cid);
|
||||
reactionsToDelete.addAll(state.messages.map((it) => it.id));
|
||||
pinnedReactionsToDelete.addAll(state.pinnedMessages.map((it) => it.id));
|
||||
reactionsToDelete.addAll(state.messages?.map((it) => it.id) ?? []);
|
||||
pinnedReactionsToDelete
|
||||
.addAll(state.pinnedMessages?.map((it) => it.id) ?? []);
|
||||
|
||||
// preparing addition data
|
||||
channelWithReads[cid] = reads;
|
||||
@@ -261,14 +258,14 @@ abstract class ChatPersistenceClient {
|
||||
channelWithMessages[cid] = messages;
|
||||
channelWithPinnedMessages[cid] = pinnedMessages;
|
||||
|
||||
reactions.addAll(messages.expand(_expandReactions));
|
||||
pinnedReactions.addAll(pinnedMessages.expand(_expandReactions));
|
||||
reactions.addAll(messages?.expand(_expandReactions) ?? []);
|
||||
pinnedReactions.addAll(pinnedMessages?.expand(_expandReactions) ?? []);
|
||||
|
||||
users.addAll([
|
||||
channel.createdBy,
|
||||
...messages.map((it) => it.user),
|
||||
...reads.map((it) => it.user),
|
||||
...members.map((it) => it.user),
|
||||
...messages?.map((it) => it.user) ?? <User>[],
|
||||
...reads?.map((it) => it.user) ?? <User>[],
|
||||
...members?.map((it) => it.user) ?? <User>[],
|
||||
...reactions.map((it) => it.user),
|
||||
...pinnedReactions.map((it) => it.user),
|
||||
].withNullifyer);
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
/// Describes capabilities of a user vis-a-vis a channel
|
||||
class PermissionType {
|
||||
/// Capability required to send a message in the channel
|
||||
/// Channel is not frozen (or user has UseFrozenChannel permission)
|
||||
/// and user has CreateMessage permission.
|
||||
static const String sendMessage = 'send-message';
|
||||
|
||||
/// Capability required to receive connect events in the channel
|
||||
static const String connectEvents = 'connect-events';
|
||||
|
||||
/// Capability required to send a message
|
||||
/// Reactions are enabled for the channel, channel is not frozen
|
||||
/// (or user has UseFrozenChannel permission) and user has
|
||||
/// CreateReaction permission
|
||||
static const String sendReaction = 'send-reaction';
|
||||
|
||||
/// Capability required to send links in a channel
|
||||
/// send-message + user has AddLinks permission
|
||||
static const String sendLinks = 'send-links';
|
||||
|
||||
/// Capability required to send thread reply
|
||||
/// send-message + channel has replies enabled
|
||||
static const String sendReply = 'send-reply';
|
||||
|
||||
/// Capability to freeze a channel
|
||||
/// User has UpdateChannelFrozen permission.
|
||||
/// The name implies freezing,
|
||||
/// but unfreezing is also allowed when this capability is present
|
||||
static const String freezeChannel = 'freeze-channel';
|
||||
|
||||
/// User has UpdateChannelCooldown permission.
|
||||
/// Allows to enable/disable slow mode in the channel
|
||||
static const String setChannelCooldown = 'set-channel-cooldown';
|
||||
|
||||
/// User has RemoveOwnChannelMembership or UpdateChannelMembers permission
|
||||
static const String leaveChannel = 'leave-channel';
|
||||
|
||||
/// User can mute channel
|
||||
static const String muteChannel = 'mute-channel';
|
||||
|
||||
/// Ability to receive read events
|
||||
static const String readEvents = 'read-events';
|
||||
|
||||
/// Capability required to pin a message in a channel
|
||||
/// Corresponds to PinMessage permission
|
||||
static const String pinMessage = 'pin-message';
|
||||
|
||||
/// Capability required to quote a message in a channel
|
||||
static const String quoteMessage = 'quote-message';
|
||||
|
||||
/// Capability required to flag a message in a channel
|
||||
static const String flagMessage = 'flag-message';
|
||||
|
||||
/// User has ability to delete any message in the channel
|
||||
/// User has DeleteMessage permission
|
||||
/// which applies to any message in the channel
|
||||
static const String deleteAnyMessage = 'delete-any-message';
|
||||
|
||||
/// User has ability to delete their own message in the channel
|
||||
/// User has DeleteMessage permission which applies only to owned messages
|
||||
static const String deleteOwnMessage = 'delete-own-message';
|
||||
|
||||
/// User has ability to update/edit any message in the channel
|
||||
/// User has UpdateMessage permission which
|
||||
/// applies to any message in the channel
|
||||
static const String updateAnyMessage = 'update-any-message';
|
||||
|
||||
/// User has ability to update/edit their own message in the channel
|
||||
/// User has UpdateMessage permission which applies only to owned messages
|
||||
static const String updateOwnMessage = 'update-own-message';
|
||||
|
||||
/// User can search for message in a channel
|
||||
/// Search feature is enabled (it will also have
|
||||
/// permission check in the future)
|
||||
static const String searchMessages = 'search-messages';
|
||||
|
||||
/// Capability required to send typing events in a channel
|
||||
/// (Typing events are enabled)
|
||||
static const String sendTypingEvents = 'send-typing-events';
|
||||
|
||||
/// Capability required to upload a file in a channel
|
||||
/// Uploads are enabled and user has UploadAttachment
|
||||
static const String uploadFile = 'upload-file';
|
||||
|
||||
/// Capability required to delete channel
|
||||
/// User has DeleteChannel permission
|
||||
static const String deleteChannel = 'delete-channel';
|
||||
|
||||
/// Capability required update/edit channel info
|
||||
/// User has UpdateChannel permission
|
||||
static const String updateChannel = 'update-channel';
|
||||
|
||||
/// Capability required to update/edit channel members
|
||||
/// Channel is not distinct and user has UpdateChannelMembers permission
|
||||
static const String updateChannelMembers = 'update-channel-members';
|
||||
}
|
||||
@@ -7,7 +7,38 @@ export 'package:dio/src/options.dart';
|
||||
export 'package:dio/src/options.dart' show ProgressCallback;
|
||||
export 'package:logging/logging.dart' show Logger, Level, LogRecord;
|
||||
export 'package:rate_limiter/rate_limiter.dart';
|
||||
export 'package:uuid/uuid.dart';
|
||||
|
||||
export './src/core/api/attachment_file_uploader.dart'
|
||||
show AttachmentFileUploader;
|
||||
export './src/core/api/requests.dart';
|
||||
export './src/core/api/requests.dart';
|
||||
export './src/core/api/responses.dart';
|
||||
export './src/core/api/stream_chat_api.dart' show PushProvider;
|
||||
export './src/core/error/error.dart';
|
||||
export './src/core/models/action.dart';
|
||||
export './src/core/models/attachment.dart';
|
||||
export './src/core/models/attachment_file.dart';
|
||||
export './src/core/models/channel_config.dart';
|
||||
export './src/core/models/channel_model.dart';
|
||||
export './src/core/models/channel_state.dart';
|
||||
export './src/core/models/command.dart';
|
||||
export './src/core/models/device.dart';
|
||||
export './src/core/models/event.dart';
|
||||
export './src/core/models/filter.dart' show Filter;
|
||||
export './src/core/models/member.dart';
|
||||
export './src/core/models/message.dart';
|
||||
export './src/core/models/mute.dart';
|
||||
export './src/core/models/own_user.dart';
|
||||
export './src/core/models/reaction.dart';
|
||||
export './src/core/models/read.dart';
|
||||
export './src/core/models/user.dart';
|
||||
export './src/core/util/extension.dart';
|
||||
export './src/db/chat_persistence_client.dart';
|
||||
export './src/event_type.dart';
|
||||
export './src/location.dart';
|
||||
export './src/permission_type.dart';
|
||||
export './src/ws/connection_status.dart';
|
||||
export 'src/client/channel.dart';
|
||||
export 'src/client/client.dart';
|
||||
export 'src/core/api/attachment_file_uploader.dart' show AttachmentFileUploader;
|
||||
|
||||
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
|
||||
/// Current package version
|
||||
/// Used in [StreamChatClient] to build the `x-stream-client` header
|
||||
// ignore: constant_identifier_names
|
||||
const PACKAGE_VERSION = '3.6.1';
|
||||
const PACKAGE_VERSION = '4.0.1';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: stream_chat
|
||||
homepage: https://getstream.io/
|
||||
description: The official Dart client for Stream Chat, a service for building chat applications.
|
||||
version: 3.6.1
|
||||
version: 4.0.1
|
||||
repository: https://github.com/GetStream/stream-chat-flutter
|
||||
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
|
||||
|
||||
|
||||
@@ -244,9 +244,13 @@ void main() {
|
||||
|
||||
group('`.sendMessage`', () {
|
||||
test('should work fine', () async {
|
||||
final message = Message(id: 'test-message-id');
|
||||
final message = Message(
|
||||
id: 'test-message-id',
|
||||
user: client.state.currentUser,
|
||||
);
|
||||
|
||||
final sendMessageResponse = SendMessageResponse()..message = message;
|
||||
final sendMessageResponse = SendMessageResponse()
|
||||
..message = message.copyWith(status: MessageSendingStatus.sent);
|
||||
|
||||
when(() => client.sendMessage(
|
||||
any(that: isSameMessageAs(message)),
|
||||
@@ -329,6 +333,7 @@ void main() {
|
||||
.map((it) =>
|
||||
it.copyWith(uploadState: const UploadState.success()))
|
||||
.toList(growable: false),
|
||||
status: MessageSendingStatus.sent,
|
||||
));
|
||||
|
||||
expectLater(
|
||||
@@ -455,7 +460,10 @@ void main() {
|
||||
|
||||
group('`.updateMessage`', () {
|
||||
test('should work fine', () async {
|
||||
final message = Message(id: 'test-message-id');
|
||||
final message = Message(
|
||||
id: 'test-message-id',
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
final updateMessageResponse = UpdateMessageResponse()
|
||||
..message = message;
|
||||
@@ -530,6 +538,7 @@ void main() {
|
||||
any(that: isSameMessageAs(message)),
|
||||
)).thenAnswer((_) async => UpdateMessageResponse()
|
||||
..message = message.copyWith(
|
||||
status: MessageSendingStatus.sent,
|
||||
attachments: attachments
|
||||
.map((it) =>
|
||||
it.copyWith(uploadState: const UploadState.success()))
|
||||
@@ -678,7 +687,7 @@ void main() {
|
||||
[
|
||||
isSameMessageAs(
|
||||
updateMessageResponse.message.copyWith(
|
||||
status: MessageSendingStatus.sent,
|
||||
status: MessageSendingStatus.sending,
|
||||
),
|
||||
matchText: true,
|
||||
matchSendingStatus: true,
|
||||
@@ -707,7 +716,10 @@ void main() {
|
||||
group('`.deleteMessage`', () {
|
||||
test('should work fine', () async {
|
||||
const messageId = 'test-message-id';
|
||||
final message = Message(id: messageId);
|
||||
final message = Message(
|
||||
id: messageId,
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
when(() => client.deleteMessage(messageId))
|
||||
.thenAnswer((_) async => EmptyResponse());
|
||||
@@ -744,7 +756,6 @@ void main() {
|
||||
const messageId = 'test-message-id';
|
||||
final message = Message(
|
||||
id: messageId,
|
||||
status: MessageSendingStatus.sending,
|
||||
);
|
||||
|
||||
expectLater(
|
||||
@@ -1077,7 +1088,10 @@ void main() {
|
||||
group('`.sendReaction`', () {
|
||||
test('should work fine', () async {
|
||||
const type = 'test-reaction-type';
|
||||
final message = Message(id: 'test-message-id');
|
||||
final message = Message(
|
||||
id: 'test-message-id',
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
final reaction = Reaction(type: type, messageId: message.id);
|
||||
|
||||
@@ -1118,7 +1132,10 @@ void main() {
|
||||
|
||||
test('should work fine with score passed explicitly', () async {
|
||||
const type = 'test-reaction-type';
|
||||
final message = Message(id: 'test-message-id');
|
||||
final message = Message(
|
||||
id: 'test-message-id',
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
const score = 5;
|
||||
final reaction = Reaction(
|
||||
@@ -1178,7 +1195,10 @@ void main() {
|
||||
test('should work fine with score passed explicitly and in extraData',
|
||||
() async {
|
||||
const type = 'test-reaction-type';
|
||||
final message = Message(id: 'test-message-id');
|
||||
final message = Message(
|
||||
id: 'test-message-id',
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
const score = 5;
|
||||
const extraDataScore = 3;
|
||||
@@ -1249,7 +1269,10 @@ void main() {
|
||||
'should restore previous message if `client.sendReaction` throws',
|
||||
() async {
|
||||
const type = 'test-reaction-type';
|
||||
final message = Message(id: 'test-message-id');
|
||||
final message = Message(
|
||||
id: 'test-message-id',
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
final reaction = Reaction(type: type, messageId: message.id);
|
||||
|
||||
@@ -1310,6 +1333,7 @@ void main() {
|
||||
latestReactions: [prevReaction],
|
||||
reactionScores: const {prevType: 1},
|
||||
reactionCounts: const {prevType: 1},
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
const type = 'test-reaction-type-2';
|
||||
@@ -1341,7 +1365,7 @@ void main() {
|
||||
emitsInOrder([
|
||||
[
|
||||
isSameMessageAs(
|
||||
newMessage.copyWith(status: MessageSendingStatus.sent),
|
||||
newMessage,
|
||||
matchReactions: true,
|
||||
matchSendingStatus: true,
|
||||
),
|
||||
@@ -1374,6 +1398,7 @@ void main() {
|
||||
final message = Message(
|
||||
id: 'test-message-id',
|
||||
parentId: 'test-parent-id', // is thread message
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
final reaction = Reaction(type: type, messageId: message.id);
|
||||
@@ -1423,6 +1448,7 @@ void main() {
|
||||
final message = Message(
|
||||
id: 'test-message-id',
|
||||
parentId: 'test-parent-id', // is thread message
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
final reaction = Reaction(type: type, messageId: message.id);
|
||||
@@ -1490,6 +1516,7 @@ void main() {
|
||||
latestReactions: [prevReaction],
|
||||
reactionScores: const {prevType: 1},
|
||||
reactionCounts: const {prevType: 1},
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
const type = 'test-reaction-type-2';
|
||||
@@ -1567,6 +1594,7 @@ void main() {
|
||||
latestReactions: [reaction],
|
||||
reactionScores: const {type: 1},
|
||||
reactionCounts: const {type: 1},
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
when(() => client.deleteReaction(messageId, type))
|
||||
@@ -1614,6 +1642,7 @@ void main() {
|
||||
latestReactions: [reaction],
|
||||
reactionScores: const {type: 1},
|
||||
reactionCounts: const {type: 1},
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
when(() => client.deleteReaction(messageId, type))
|
||||
@@ -1668,11 +1697,13 @@ void main() {
|
||||
);
|
||||
final message = Message(
|
||||
id: messageId,
|
||||
parentId: parentId, // is thread
|
||||
parentId: parentId,
|
||||
// is thread
|
||||
ownReactions: [reaction],
|
||||
latestReactions: [reaction],
|
||||
reactionScores: const {type: 1},
|
||||
reactionCounts: const {type: 1},
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
when(() => client.deleteReaction(messageId, type))
|
||||
@@ -1725,6 +1756,7 @@ void main() {
|
||||
latestReactions: [reaction],
|
||||
reactionScores: const {type: 1},
|
||||
reactionCounts: const {type: 1},
|
||||
status: MessageSendingStatus.sent,
|
||||
);
|
||||
|
||||
when(() => client.deleteReaction(messageId, type))
|
||||
|
||||
@@ -105,11 +105,11 @@ void main() {
|
||||
);
|
||||
|
||||
expect(res, isNotNull);
|
||||
expect(res.messages.length, channelState.messages.length);
|
||||
expect(res.pinnedMessages.length, channelState.pinnedMessages.length);
|
||||
expect(res.members.length, channelState.members.length);
|
||||
expect(res.read.length, channelState.read.length);
|
||||
expect(res.watchers.length, channelState.watchers.length);
|
||||
expect(res.messages?.length, channelState.messages?.length);
|
||||
expect(res.pinnedMessages?.length, channelState.pinnedMessages?.length);
|
||||
expect(res.members?.length, channelState.members?.length);
|
||||
expect(res.read?.length, channelState.read?.length);
|
||||
expect(res.watchers?.length, channelState.watchers?.length);
|
||||
expect(res.watcherCount, channelState.watcherCount);
|
||||
|
||||
verify(() => client.post(path, data: any(named: 'data'))).called(1);
|
||||
|
||||
@@ -32,6 +32,7 @@ void main() {
|
||||
data: {
|
||||
'message': message,
|
||||
'skip_push': false,
|
||||
'skip_enrich_url': false,
|
||||
},
|
||||
)).thenAnswer((_) async => successResponse(path, data: {
|
||||
'message': message.toJson(),
|
||||
@@ -58,6 +59,7 @@ void main() {
|
||||
data: {
|
||||
'message': message,
|
||||
'skip_push': true,
|
||||
'skip_enrich_url': false,
|
||||
},
|
||||
)).thenAnswer((_) async => successResponse(path, data: {
|
||||
'message': message.toJson(),
|
||||
@@ -137,7 +139,10 @@ void main() {
|
||||
|
||||
when(() => client.post(
|
||||
path,
|
||||
data: {'message': message},
|
||||
data: {
|
||||
'message': message,
|
||||
'skip_enrich_url': false,
|
||||
},
|
||||
)).thenAnswer(
|
||||
(_) async => successResponse(path, data: {'message': message.toJson()}),
|
||||
);
|
||||
@@ -162,7 +167,11 @@ void main() {
|
||||
|
||||
when(() => client.put(
|
||||
path,
|
||||
data: {'set': set, 'unset': unset},
|
||||
data: {
|
||||
'set': set,
|
||||
'unset': unset,
|
||||
'skip_enrich_url': false,
|
||||
},
|
||||
)).thenAnswer(
|
||||
(_) async => successResponse(path, data: {'message': message.toJson()}),
|
||||
);
|
||||
@@ -180,7 +189,11 @@ void main() {
|
||||
|
||||
verify(() => client.put(
|
||||
path,
|
||||
data: {'set': set, 'unset': unset},
|
||||
data: {
|
||||
'set': set,
|
||||
'unset': unset,
|
||||
'skip_enrich_url': false,
|
||||
},
|
||||
)).called(1);
|
||||
verifyNoMoreInteractions(client);
|
||||
});
|
||||
|
||||
@@ -19,8 +19,10 @@ void main() {
|
||||
attachment.thumbUrl,
|
||||
'https://media0.giphy.com/media/3o7TKnCdBx5cMg0qti/giphy.gif',
|
||||
);
|
||||
expect(attachment.actions, isNotNull);
|
||||
expect(attachment.actions, isNotEmpty);
|
||||
expect(attachment.actions, hasLength(3));
|
||||
expect(attachment.actions[0], isA<Action>());
|
||||
expect(attachment.actions![0], isA<Action>());
|
||||
});
|
||||
|
||||
test('should serialize to json correctly', () {
|
||||
|
||||
@@ -30,14 +30,16 @@ void main() {
|
||||
channelState.channel?.extraData['image'],
|
||||
'https://cdn.chrisshort.net/testing-certificate-chains-in-go/GOPHER_MIC_DROP.png',
|
||||
);
|
||||
expect(channelState.messages, isNotNull);
|
||||
expect(channelState.messages, isNotEmpty);
|
||||
expect(channelState.messages, hasLength(25));
|
||||
expect(channelState.messages[0], isA<Message>());
|
||||
expect(channelState.messages[0], isNotNull);
|
||||
expect(channelState.messages![0], isA<Message>());
|
||||
expect(channelState.messages![0], isNotNull);
|
||||
expect(
|
||||
channelState.messages[0].createdAt,
|
||||
channelState.messages![0].createdAt,
|
||||
DateTime.parse('2020-01-29T03:23:02.843948Z'),
|
||||
);
|
||||
expect(channelState.messages[0].user, isA<User>());
|
||||
expect(channelState.messages![0].user, isA<User>());
|
||||
expect(channelState.watcherCount, 5);
|
||||
});
|
||||
|
||||
|
||||
@@ -117,19 +117,20 @@ class TestPersistenceClient extends ChatPersistenceClient {
|
||||
Future<void> updateUsers(List<User> users) => Future.value();
|
||||
|
||||
@override
|
||||
Future<void> bulkUpdateMembers(Map<String, List<Member>> members) =>
|
||||
Future<void> bulkUpdateMembers(Map<String, List<Member>?> members) =>
|
||||
Future.value();
|
||||
|
||||
@override
|
||||
Future<void> bulkUpdateMessages(Map<String, List<Message>> messages) =>
|
||||
Future<void> bulkUpdateMessages(Map<String, List<Message>?> messages) =>
|
||||
Future.value();
|
||||
|
||||
@override
|
||||
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>> messages) =>
|
||||
Future<void> bulkUpdatePinnedMessages(Map<String, List<Message>?> messages) =>
|
||||
Future.value();
|
||||
|
||||
@override
|
||||
Future<void> bulkUpdateReads(Map<String, List<Read>> reads) => Future.value();
|
||||
Future<void> bulkUpdateReads(Map<String, List<Read>?> reads) =>
|
||||
Future.value();
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
||||
@@ -59,7 +59,10 @@ void main() {
|
||||
});
|
||||
|
||||
test('`connect` successfully with the provided user', () async {
|
||||
final user = OwnUser(id: 'test-user');
|
||||
final user = OwnUser(
|
||||
id: 'test-user',
|
||||
name: 'test',
|
||||
);
|
||||
const connectionId = 'test-connection-id';
|
||||
// Sends connect event to web-socket stream
|
||||
final timer = Timer(const Duration(milliseconds: 300), () {
|
||||
@@ -90,6 +93,44 @@ void main() {
|
||||
addTearDown(timer.cancel);
|
||||
});
|
||||
|
||||
test('`connect` successfully without user details', () async {
|
||||
final user = OwnUser(
|
||||
id: 'test-user',
|
||||
name: 'test',
|
||||
);
|
||||
const connectionId = 'test-connection-id';
|
||||
// Sends connect event to web-socket stream
|
||||
final timer = Timer(const Duration(milliseconds: 300), () {
|
||||
final event = Event(
|
||||
type: EventType.healthCheck,
|
||||
connectionId: connectionId,
|
||||
me: user,
|
||||
);
|
||||
webSocketSink.add(json.encode(event));
|
||||
});
|
||||
|
||||
expectLater(
|
||||
webSocket.connectionStatusStream,
|
||||
emitsInOrder([
|
||||
ConnectionStatus.disconnected,
|
||||
ConnectionStatus.connecting,
|
||||
ConnectionStatus.connected,
|
||||
]),
|
||||
);
|
||||
|
||||
final event = await webSocket.connect(
|
||||
user,
|
||||
includeUserDetails: true,
|
||||
);
|
||||
|
||||
expect(event.type, EventType.healthCheck);
|
||||
expect(event.connectionId, connectionId);
|
||||
expect(event.me, isNotNull);
|
||||
expect(event.me!.id, user.id);
|
||||
|
||||
addTearDown(timer.cancel);
|
||||
});
|
||||
|
||||
test('`connect` should throw if already in connection attempt', () async {
|
||||
final user = OwnUser(id: 'test-user');
|
||||
webSocket.connect(user);
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
## Upcoming
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Fixed attachment picker ui.
|
||||
- Fixed message widget thread indicator in reverse mode.
|
||||
|
||||
## 4.0.1
|
||||
|
||||
- Minor fixes
|
||||
- Updated `stream_chat_flutter_core` dependency to [`4.0.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||
|
||||
## 4.0.0
|
||||
|
||||
For upgrading to V4, please refer to the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migration_guide_4_0/)
|
||||
|
||||
✅ Added
|
||||
|
||||
- [[#1087]](https://github.com/GetStream/stream-chat-flutter/issues/1087): Handle limited access to camera on iOS.
|
||||
@@ -12,6 +26,38 @@
|
||||
- Loosen up url check for attachment download.
|
||||
- Use `ogScrapeUrl` for LinkAttachments.
|
||||
|
||||
## 4.0.0-beta.2
|
||||
|
||||
✅ Added
|
||||
|
||||
- Added support to pass `autoCorrect` to `StreamMessageInput` for the text input field
|
||||
- Added support to control the visibility of the default emoji suggestions overlay in `StreamMessageInput`
|
||||
- Added support to build custom widget for scrollToBottom in `StreamMessageListView`
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Minor fixes and improvements
|
||||
-[[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`.
|
||||
- Fix `MessageInputTheme.inputBackgroundColor` color not being used in some widgets of `MessageInput`
|
||||
- Removed dependency on `visibility_detector`
|
||||
- [[#1071]](https://github.com/GetStream/stream-chat-flutter/issues/1071): Fixed the way attachment actions were handled in full screen
|
||||
|
||||
## 4.0.0-beta.1
|
||||
|
||||
✅ Added
|
||||
|
||||
- Deprecated old widgets in favor of Stream-prefixed ones.
|
||||
- Use channel capabilities to show/hide actions.
|
||||
- Deprecated `ChannelListView` in favor of `StreamChannelListView`.
|
||||
- Deprecated `ChannelPreview` in favor of `StreamChannelListTile`.
|
||||
- Deprecated `ChannelAvatar` in favor of `StreamChannelAvatar`.
|
||||
- Deprecated `ChannelName` in favor of `StreamChannelName`.
|
||||
- Deprecated `MessageInput` in favor of `StreamMessageInput`.
|
||||
- Separated `MessageInput` widget in smaller components. (For example `CountDownButton`, `StreamAttachmentPicker`...)
|
||||
- Updated `stream_chat_flutter_core` dependency to [`4.0.0-beta.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||
- Added OpenGraph preview support for links in `StreamMessageInput`.
|
||||
- Removed video compression.
|
||||
|
||||
## 3.6.1
|
||||
|
||||
- Updated `stream_chat_flutter_core` dependency to [`3.6.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
|
||||
@@ -20,12 +66,18 @@
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- [[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`.
|
||||
- Minor fixes and improvements
|
||||
-[[#892]](https://github.com/GetStream/stream-chat-flutter/issues/892): Fix default `initialAlignment` in `MessageListView`.
|
||||
- Fix `MessageInputTheme.inputBackgroundColor` color not being used in some widgets of `MessageInput`
|
||||
- Removed dependency on `visibility_detector`
|
||||
|
||||
## 3.5.1
|
||||
|
||||
🛑️ Breaking Changes
|
||||
|
||||
- `pinPermissions` is no longer needed in `MessageListView`.
|
||||
- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController`
|
||||
|
||||
🐞 Fixed
|
||||
|
||||
- Mentions overlay now doesn't overflow when there is not enough height available
|
||||
@@ -58,7 +110,12 @@
|
||||
|
||||
✅ Added
|
||||
|
||||
- Videos can now be auto-played in `FullScreenMedia`, by setting the `autoplayVideos` argument to true.
|
||||
- Videos can now be auto-played in `FullScreenMedia`
|
||||
- Extra customisation options for `MessageInput`
|
||||
|
||||
🔄 Changed
|
||||
|
||||
- Add `didUpdateWidget` override in `MessageInput` widget to handle changes to `focusNode`.
|
||||
|
||||
## 3.3.2
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
- [UI Docs](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/introduction/)
|
||||
- [Chat Client Docs](https://getstream.io/chat/docs/flutter-dart/?language=dart)
|
||||
|
||||
**V4 Migration Guide**
|
||||
|
||||
For upgrading from V3 to V4, please refer to the [V4 Migration Guide](https://getstream.io/chat/docs/sdk/flutter/guides/migration_guide_4_0/)
|
||||
|
||||
### Changelog
|
||||
|
||||
Check out the [changelog on pub.dev](https://pub.dev/packages/stream_chat_flutter/changelog) to see the latest changes in the package.
|
||||
@@ -42,7 +46,8 @@ You should then run `flutter packages get`
|
||||
|
||||
### Android
|
||||
|
||||
All set ✅
|
||||
The package uses [photo_manager](https://pub.dev/packages/photo_manager) to access the device's photo library.
|
||||
Follow [this wiki](https://pub.dev/packages/photo_manager#android-10-q-29) to fulfil the Android requirements.
|
||||
|
||||
### iOS
|
||||
|
||||
@@ -71,16 +76,12 @@ If you require the maximum amount of control over the API, please use the low le
|
||||
These are the available Widgets that you can use to build your application UI.
|
||||
Every widget uses the `StreamChat` or `StreamChannel` widgets to manage the state and communicate with Stream services.
|
||||
|
||||
- [ChannelHeader](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelHeader-class.html)
|
||||
- [ChannelImage](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelImage-class.html)
|
||||
- [ChannelListView](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelListView-class.html)
|
||||
- [ChannelName](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelName-class.html)
|
||||
- [ChannelPreview](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ChannelPreview-class.html)
|
||||
- [MessageInput](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageInput-class.html)
|
||||
- [MessageListView](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageListView-class.html)
|
||||
- [MessageWidget](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/MessageWidget-class.html)
|
||||
- [StreamChatTheme](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/StreamChatTheme-class.html)
|
||||
- [ThreadHeader](https://pub.dev/documentation/stream_chat_flutter/latest/stream_chat_flutter/ThreadHeader-class.html)
|
||||
- [StreamChannelHeader](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_channel_header/)
|
||||
- [StreamChannelListView](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_channel_list_view/)
|
||||
- [StreamMessageInput](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_message_input/)
|
||||
- [StreamMessageListView](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_message_list_view/)
|
||||
- [StreamMessageWidget](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_message_widget/)
|
||||
- [StreamChatTheme](https://getstream.io/chat/docs/sdk/flutter/stream_chat_flutter/stream_chat_and_theming/)
|
||||
- ...
|
||||
|
||||
### Customizing styles
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objectVersion = 51;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -68,7 +68,6 @@
|
||||
59062C6EC2CCFE110AC70AB8 /* Pods-Runner.release.xcconfig */,
|
||||
4684439012E1DB1A82103E26 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -356,13 +355,17 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = EHV7XZLAHA;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
@@ -488,13 +491,17 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = EHV7XZLAHA;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
@@ -515,13 +522,17 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = EHV7XZLAHA;
|
||||
ENABLE_BITCODE = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
);
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)/Flutter",
|
||||
|
||||
@@ -87,7 +87,8 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
/// A list of messages sent in the current channel.
|
||||
///
|
||||
/// This is implemented using [MessageListView], a widget that provides query
|
||||
/// This is implemented using [StreamMessageListView],
|
||||
/// a widget that provides query
|
||||
/// functionalities fetching the messages from the api and showing them in a
|
||||
/// listView.
|
||||
class ChannelPage extends StatelessWidget {
|
||||
@@ -98,13 +99,13 @@ class ChannelPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const ChannelHeader(),
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(),
|
||||
child: StreamMessageListView(),
|
||||
),
|
||||
MessageInput(attachmentLimit: 3),
|
||||
StreamMessageInput(attachmentLimit: 3),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -84,7 +84,7 @@ class _SplitViewState extends State<SplitView> {
|
||||
);
|
||||
}
|
||||
|
||||
class ChannelListPage extends StatelessWidget {
|
||||
class ChannelListPage extends StatefulWidget {
|
||||
const ChannelListPage({
|
||||
Key? key,
|
||||
this.onTap,
|
||||
@@ -92,22 +92,26 @@ class ChannelListPage extends StatelessWidget {
|
||||
|
||||
final void Function(Channel)? onTap;
|
||||
|
||||
@override
|
||||
State<ChannelListPage> createState() => _ChannelListPageState();
|
||||
}
|
||||
|
||||
class _ChannelListPageState extends State<ChannelListPage> {
|
||||
late final _listController = StreamChannelListController(
|
||||
client: StreamChat.of(context).client,
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: ChannelsBloc(
|
||||
child: ChannelListView(
|
||||
onChannelTap: onTap != null
|
||||
? (channel, _) {
|
||||
onTap!(channel);
|
||||
}
|
||||
: null,
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
),
|
||||
body: StreamChannelListView(
|
||||
onChannelTap: widget.onTap,
|
||||
controller: _listController,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -121,15 +125,15 @@ class ChannelPage extends StatelessWidget {
|
||||
Widget build(BuildContext context) => Navigator(
|
||||
onGenerateRoute: (settings) => MaterialPageRoute(
|
||||
builder: (context) => Scaffold(
|
||||
appBar: const ChannelHeader(
|
||||
appBar: const StreamChannelHeader(
|
||||
showBackButton: false,
|
||||
),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(),
|
||||
child: StreamMessageListView(),
|
||||
),
|
||||
MessageInput(),
|
||||
StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -27,7 +28,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// - We make [StreamChat] the root Widget of our application
|
||||
///
|
||||
/// - We create a single [ChannelPage] widget under [StreamChat] with three
|
||||
/// widgets: [ChannelHeader], [MessageListView] and [MessageInput]
|
||||
/// widgets: [StreamChannelHeader], [StreamMessageListView]
|
||||
/// and [StreamMessageInput]
|
||||
///
|
||||
/// If you now run the simulator you will see a single channel UI.
|
||||
void main() async {
|
||||
@@ -66,10 +68,8 @@ class MyApp extends StatelessWidget {
|
||||
final Channel channel;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
// ignore: prefer_expression_function_bodies
|
||||
builder: (context, widget) {
|
||||
return StreamChat(
|
||||
client: client,
|
||||
@@ -90,18 +90,15 @@ class ChannelPage extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const ChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(),
|
||||
),
|
||||
MessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(),
|
||||
),
|
||||
StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -14,7 +16,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// > Note: the SDK uses Flutter’s [Navigator] to move from one route to
|
||||
/// another. This allows us to avoid any boiler-plate code.
|
||||
/// > Of course, you can take total control of how navigation works by
|
||||
/// customizing widgets like [Channel] and [ChannelList].
|
||||
/// customizing widgets like [StreamChannel] and [StreamChannelListView].
|
||||
///
|
||||
/// If you run the application, you will see that the first screen shows a
|
||||
/// list of conversations, you can open each by tapping and go back to the list.
|
||||
@@ -25,7 +27,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// The [ChannelListPage] widget retrieves the list of channels based on a
|
||||
/// custom query and ordering. In this case we are showing the list of
|
||||
/// channels in which the current user is a member and we order them based
|
||||
/// on the time they had a new message. [ChannelListView] handles pagination
|
||||
/// on the time they had a new message.
|
||||
/// [StreamChannelListView] handles pagination
|
||||
/// and updates automatically when new channels are created or when a new
|
||||
/// message is added to a channel.
|
||||
void main() async {
|
||||
@@ -55,40 +58,65 @@ class MyApp extends StatelessWidget {
|
||||
final StreamChatClient client;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
builder: (context, child) => StreamChat(
|
||||
client: client,
|
||||
child: child,
|
||||
),
|
||||
home: const ChannelListPage(),
|
||||
home: ChannelListPage(
|
||||
client: client,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelListPage extends StatelessWidget {
|
||||
class ChannelListPage extends StatefulWidget {
|
||||
const ChannelListPage({
|
||||
Key? key,
|
||||
required this.client,
|
||||
}) : super(key: key);
|
||||
|
||||
final StreamChatClient client;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ChannelsBloc(
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
State<ChannelListPage> createState() => _ChannelListPageState();
|
||||
}
|
||||
|
||||
class _ChannelListPageState extends State<ChannelListPage> {
|
||||
late final _controller = StreamChannelListController(
|
||||
client: widget.client,
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: RefreshIndicator(
|
||||
onRefresh: _controller.refresh,
|
||||
child: StreamChannelListView(
|
||||
controller: _controller,
|
||||
onChannelTap: (channel) => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ChannelPage extends StatelessWidget {
|
||||
@@ -97,18 +125,15 @@ class ChannelPage extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const ChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(),
|
||||
),
|
||||
MessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(),
|
||||
),
|
||||
StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
@@ -15,9 +16,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// We start by changing how channel previews are shown in the channel list
|
||||
/// and include the number of unread messages for each.
|
||||
///
|
||||
/// We're passing a custom widget to [ChannelListView.channelPreviewBuilder];
|
||||
/// this will override the default [ChannelPreview] and allows you to create
|
||||
/// one yourself.
|
||||
/// We're passing a custom widget
|
||||
/// to [StreamChannelListView.itemBuilder];
|
||||
/// this will override the default [StreamChannelListTile] and allows you
|
||||
/// to create one yourself.
|
||||
///
|
||||
/// There are a couple interesting things we do in this widget:
|
||||
///
|
||||
@@ -56,7 +58,6 @@ class MyApp extends StatelessWidget {
|
||||
final StreamChatClient client;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
builder: (context, child) => StreamChat(
|
||||
@@ -68,31 +69,57 @@ class MyApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelListPage extends StatelessWidget {
|
||||
class ChannelListPage extends StatefulWidget {
|
||||
const ChannelListPage({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ChannelsBloc(
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
channelPreviewBuilder: _channelPreviewBuilder,
|
||||
// sort: [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
State<ChannelListPage> createState() => _ChannelListPageState();
|
||||
}
|
||||
|
||||
class _ChannelListPageState extends State<ChannelListPage> {
|
||||
late final _listController = StreamChannelListController(
|
||||
client: StreamChat.of(context).client,
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _channelPreviewBuilder(BuildContext context, Channel channel) {
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: StreamChannelListView(
|
||||
controller: _listController,
|
||||
itemBuilder: _channelPreviewBuilder,
|
||||
onChannelTap: (channel) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Widget _channelPreviewBuilder(
|
||||
BuildContext context,
|
||||
List<Channel> channels,
|
||||
int index,
|
||||
StreamChannelListTile defaultTile,
|
||||
) {
|
||||
final channel = channels[index];
|
||||
final lastMessage = channel.state?.messages.reversed.firstWhereOrNull(
|
||||
(message) => !message.isDeleted,
|
||||
);
|
||||
@@ -112,16 +139,17 @@ class ChannelListPage extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
},
|
||||
leading: ChannelAvatar(
|
||||
leading: StreamChannelAvatar(
|
||||
channel: channel,
|
||||
),
|
||||
title: ChannelName(
|
||||
textStyle: ChannelPreviewTheme.of(context).titleStyle!.copyWith(
|
||||
title: StreamChannelName(
|
||||
textStyle: StreamChannelPreviewTheme.of(context).titleStyle!.copyWith(
|
||||
color: StreamChatTheme.of(context)
|
||||
.colorTheme
|
||||
.textHighEmphasis
|
||||
.withOpacity(opacity),
|
||||
),
|
||||
channel: channel,
|
||||
),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: channel.state!.unreadCount > 0
|
||||
@@ -140,16 +168,15 @@ class ChannelPage extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const ChannelHeader(),
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: const <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(),
|
||||
child: StreamMessageListView(),
|
||||
),
|
||||
MessageInput(),
|
||||
StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -8,8 +10,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// to create sub-conversations inside the same channel.
|
||||
///
|
||||
/// Using threaded conversations is very simple and mostly a matter of
|
||||
/// plugging the [MessageListView] to another widget that renders the widget.
|
||||
/// To make this simple, such a widget only needs to build [MessageListView]
|
||||
/// plugging the [StreamMessageListView]
|
||||
/// to another widget that renders the widget.
|
||||
/// To make this simple, such a widget only needs
|
||||
/// to build [StreamMessageListView]
|
||||
/// with the parent attribute set to the thread’s root message.
|
||||
///
|
||||
/// Now we can open threads and create new ones as well. If you long-press a
|
||||
@@ -41,7 +45,6 @@ class MyApp extends StatelessWidget {
|
||||
final StreamChatClient client;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
builder: (context, child) => StreamChat(
|
||||
@@ -53,28 +56,48 @@ class MyApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelListPage extends StatelessWidget {
|
||||
class ChannelListPage extends StatefulWidget {
|
||||
const ChannelListPage({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ChannelsBloc(
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
State<ChannelListPage> createState() => _ChannelListPageState();
|
||||
}
|
||||
|
||||
class _ChannelListPageState extends State<ChannelListPage> {
|
||||
late final _listController = StreamChannelListController(
|
||||
client: StreamChat.of(context).client,
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: StreamChannelListView(
|
||||
controller: _listController,
|
||||
onChannelTap: (channel) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ChannelPage extends StatelessWidget {
|
||||
@@ -83,24 +106,21 @@ class ChannelPage extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const ChannelHeader(),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(
|
||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||
parent: parentMessage,
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(
|
||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||
parent: parentMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const MessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
const StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ThreadPage extends StatelessWidget {
|
||||
@@ -112,21 +132,22 @@ class ThreadPage extends StatelessWidget {
|
||||
final Message? parent;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: ThreadHeader(
|
||||
appBar: StreamThreadHeader(
|
||||
parent: parent!,
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(
|
||||
child: StreamMessageListView(
|
||||
parentMessage: parent,
|
||||
),
|
||||
),
|
||||
MessageInput(
|
||||
parentMessage: parent,
|
||||
StreamMessageInput(
|
||||
messageInputController: StreamMessageInputController(
|
||||
message: Message(parentId: parent!.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -8,7 +9,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// the SDK supports easily.
|
||||
///
|
||||
/// Replacing the built-in message component with your own is done by passing
|
||||
/// it as a builder function to the [MessageListView] widget.
|
||||
/// it as a builder function to the [StreamMessageListView] widget.
|
||||
///
|
||||
/// The message builder function will get the usual [BuildContext] argument
|
||||
/// as well as the [Message] object and its position inside the list.
|
||||
@@ -47,7 +48,6 @@ class MyApp extends StatelessWidget {
|
||||
final StreamChatClient client;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
builder: (context, child) => StreamChat(
|
||||
@@ -59,28 +59,48 @@ class MyApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelListPage extends StatelessWidget {
|
||||
class ChannelListPage extends StatefulWidget {
|
||||
const ChannelListPage({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ChannelsBloc(
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
State<ChannelListPage> createState() => _ChannelListPageState();
|
||||
}
|
||||
|
||||
class _ChannelListPageState extends State<ChannelListPage> {
|
||||
late final _listController = StreamChannelListController(
|
||||
client: StreamChat.of(context).client,
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: StreamChannelListView(
|
||||
controller: _listController,
|
||||
onChannelTap: (channel) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ChannelPage extends StatelessWidget {
|
||||
@@ -89,18 +109,17 @@ class ChannelPage extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const ChannelHeader(),
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(
|
||||
child: StreamMessageListView(
|
||||
messageBuilder: _messageBuilder,
|
||||
),
|
||||
),
|
||||
const MessageInput(),
|
||||
const StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -110,7 +129,7 @@ class ChannelPage extends StatelessWidget {
|
||||
BuildContext context,
|
||||
MessageDetails details,
|
||||
List<Message> messages,
|
||||
MessageWidget _,
|
||||
StreamMessageWidget _,
|
||||
) {
|
||||
final message = details.message;
|
||||
final isCurrentUser =
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// ignore_for_file: public_member_api_docs
|
||||
// ignore_for_file: prefer_expression_function_bodies
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -54,32 +56,36 @@ class MyApp extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeData = ThemeData(primarySwatch: Colors.green);
|
||||
final themeData = ThemeData(
|
||||
colorScheme: ColorScheme.fromSwatch(
|
||||
accentColor: Colors.green,
|
||||
),
|
||||
);
|
||||
final defaultTheme = StreamChatThemeData.fromTheme(themeData);
|
||||
final colorTheme = defaultTheme.colorTheme;
|
||||
final customTheme = defaultTheme.merge(StreamChatThemeData(
|
||||
channelPreviewTheme: ChannelPreviewThemeData(
|
||||
avatarTheme: AvatarThemeData(
|
||||
final customTheme = StreamChatThemeData(
|
||||
channelPreviewTheme: StreamChannelPreviewThemeData(
|
||||
avatarTheme: StreamAvatarThemeData(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
messageListViewTheme: const MessageListViewThemeData(
|
||||
messageListViewTheme: const StreamMessageListViewThemeData(
|
||||
backgroundColor: Colors.grey,
|
||||
backgroundImage: DecorationImage(
|
||||
image: AssetImage('assets/background_doodle.png'),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
otherMessageTheme: MessageThemeData(
|
||||
otherMessageTheme: StreamMessageThemeData(
|
||||
messageBackgroundColor: colorTheme.textHighEmphasis,
|
||||
messageTextStyle: TextStyle(
|
||||
color: colorTheme.barsBg,
|
||||
),
|
||||
avatarTheme: AvatarThemeData(
|
||||
avatarTheme: StreamAvatarThemeData(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
));
|
||||
).merge(defaultTheme);
|
||||
|
||||
return MaterialApp(
|
||||
theme: themeData,
|
||||
@@ -93,28 +99,48 @@ class MyApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelListPage extends StatelessWidget {
|
||||
class ChannelListPage extends StatefulWidget {
|
||||
const ChannelListPage({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: ChannelsBloc(
|
||||
child: ChannelListView(
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
channelWidget: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
State<ChannelListPage> createState() => _ChannelListPageState();
|
||||
}
|
||||
|
||||
class _ChannelListPageState extends State<ChannelListPage> {
|
||||
late final _listController = StreamChannelListController(
|
||||
client: StreamChat.of(context).client,
|
||||
filter: Filter.in_(
|
||||
'members',
|
||||
[StreamChat.of(context).currentUser!.id],
|
||||
),
|
||||
sort: const [SortOption('last_message_at')],
|
||||
limit: 20,
|
||||
);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
body: StreamChannelListView(
|
||||
controller: _listController,
|
||||
onChannelTap: (channel) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelPage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ChannelPage extends StatelessWidget {
|
||||
@@ -123,24 +149,21 @@ class ChannelPage extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: const ChannelHeader(),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(
|
||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||
parent: parentMessage,
|
||||
Widget build(BuildContext context) => Scaffold(
|
||||
appBar: const StreamChannelHeader(),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: StreamMessageListView(
|
||||
threadBuilder: (_, parentMessage) => ThreadPage(
|
||||
parent: parentMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const MessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
const StreamMessageInput(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ThreadPage extends StatelessWidget {
|
||||
@@ -152,21 +175,22 @@ class ThreadPage extends StatelessWidget {
|
||||
final Message? parent;
|
||||
|
||||
@override
|
||||
// ignore: prefer_expression_function_bodies
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: ThreadHeader(
|
||||
appBar: StreamThreadHeader(
|
||||
parent: parent!,
|
||||
),
|
||||
body: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: MessageListView(
|
||||
child: StreamMessageListView(
|
||||
parentMessage: parent,
|
||||
),
|
||||
),
|
||||
MessageInput(
|
||||
parentMessage: parent,
|
||||
StreamMessageInput(
|
||||
messageInputController: StreamMessageInputController(
|
||||
message: Message(parentId: parent!.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -38,6 +38,10 @@ dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
dependency_overrides:
|
||||
stream_chat_flutter:
|
||||
path: ../
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro attachment_title}
|
||||
@Deprecated("Use 'StreamAttachmentTitle' instead")
|
||||
typedef AttachmentTitle = StreamAttachmentTitle;
|
||||
|
||||
/// {@template attachment_title}
|
||||
/// Title for attachments
|
||||
class AttachmentTitle extends StatelessWidget {
|
||||
/// {@endtemplate}
|
||||
class StreamAttachmentTitle extends StatelessWidget {
|
||||
/// Supply attachment and theme for constructing title
|
||||
const AttachmentTitle({
|
||||
const StreamAttachmentTitle({
|
||||
Key? key,
|
||||
required this.attachment,
|
||||
required this.messageTheme,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Theme to apply to text
|
||||
final MessageThemeData messageTheme;
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// Attachment data to display
|
||||
final Attachment attachment;
|
||||
|
||||
+11
-5
@@ -9,10 +9,16 @@ typedef InProgressBuilder = Widget Function(BuildContext, int, int);
|
||||
/// Widget to build on failure
|
||||
typedef FailedBuilder = Widget Function(BuildContext, String);
|
||||
|
||||
/// {@macro attachment_upload_state_builder}
|
||||
@Deprecated("Use 'StreamAttachmentsUploadStateBuilder' instead")
|
||||
typedef AttachmentUploadStateBuilder = StreamAttachmentUploadStateBuilder;
|
||||
|
||||
/// {@template attachment_upload_state_builder}
|
||||
/// Widget to display attachment upload state
|
||||
class AttachmentUploadStateBuilder extends StatelessWidget {
|
||||
/// Constructor for creating an [AttachmentUploadStateBuilder] widget
|
||||
const AttachmentUploadStateBuilder({
|
||||
/// {@endtemplate}
|
||||
class StreamAttachmentUploadStateBuilder extends StatelessWidget {
|
||||
/// Constructor for creating an [StreamAttachmentUploadStateBuilder] widget
|
||||
const StreamAttachmentUploadStateBuilder({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.attachment,
|
||||
@@ -137,7 +143,7 @@ class _PreparingState extends StatelessWidget {
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: UploadProgressIndicator(
|
||||
child: StreamUploadProgressIndicator(
|
||||
uploaded: 0,
|
||||
total: double.maxFinite.toInt(),
|
||||
),
|
||||
@@ -177,7 +183,7 @@ class _InProgressState extends StatelessWidget {
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: UploadProgressIndicator(
|
||||
child: StreamUploadProgressIndicator(
|
||||
uploaded: sent,
|
||||
total: total,
|
||||
),
|
||||
|
||||
@@ -28,10 +28,16 @@ extension AttachmentSourceX on AttachmentSource {
|
||||
}
|
||||
}
|
||||
|
||||
/// {@macro attachment_widget}
|
||||
@Deprecated("Use 'StreamAttachmentWidget' instead")
|
||||
typedef AttachmentWidget = StreamAttachmentWidget;
|
||||
|
||||
/// {@template attachment_widget}
|
||||
/// Abstract class for deriving attachment types
|
||||
abstract class AttachmentWidget extends StatelessWidget {
|
||||
/// {@endtemplate}
|
||||
abstract class StreamAttachmentWidget extends StatelessWidget {
|
||||
/// Constructor for creating attachment widget
|
||||
const AttachmentWidget({
|
||||
const StreamAttachmentWidget({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.attachment,
|
||||
|
||||
@@ -10,10 +10,16 @@ import 'package:stream_chat_flutter/src/utils.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
|
||||
|
||||
/// {@macro file_attachment}
|
||||
@Deprecated("Use 'StreamFileAttachment' instead")
|
||||
typedef FileAttachment = StreamFileAttachment;
|
||||
|
||||
/// {@template file_attachment}
|
||||
/// Widget for displaying file attachments
|
||||
class FileAttachment extends AttachmentWidget {
|
||||
/// {@endtemplate}
|
||||
class StreamFileAttachment extends StreamAttachmentWidget {
|
||||
/// Constructor for creating a widget when attachment is of type 'file'
|
||||
const FileAttachment({
|
||||
const StreamFileAttachment({
|
||||
Key? key,
|
||||
required Message message,
|
||||
required Attachment attachment,
|
||||
@@ -157,7 +163,7 @@ class FileAttachment extends AttachmentWidget {
|
||||
type: MaterialType.transparency,
|
||||
shape: _getDefaultShape(context),
|
||||
child: source.when(
|
||||
local: () => VideoThumbnailImage(
|
||||
local: () => StreamVideoThumbnailImage(
|
||||
fit: BoxFit.cover,
|
||||
video: attachment.file!.path!,
|
||||
placeholderBuilder: (_) => const Center(
|
||||
@@ -168,7 +174,7 @@ class FileAttachment extends AttachmentWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
network: () => VideoThumbnailImage(
|
||||
network: () => StreamVideoThumbnailImage(
|
||||
fit: BoxFit.cover,
|
||||
video: attachment.assetUrl!,
|
||||
placeholderBuilder: (_) => const Center(
|
||||
@@ -278,7 +284,7 @@ class FileAttachment extends AttachmentWidget {
|
||||
);
|
||||
return attachment.uploadState.when(
|
||||
preparing: () => Text(fileSize(size), style: textStyle),
|
||||
inProgress: (sent, total) => UploadProgressIndicator(
|
||||
inProgress: (sent, total) => StreamUploadProgressIndicator(
|
||||
uploaded: sent,
|
||||
total: total,
|
||||
showBackground: false,
|
||||
|
||||
@@ -5,10 +5,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro giphy_attachment}
|
||||
@Deprecated("Use 'StreamGiphyAttachment' instead")
|
||||
typedef GiphyAttachment = StreamGiphyAttachment;
|
||||
|
||||
/// {@template giphy_attachment}
|
||||
/// Widget for showing a GIF attachment
|
||||
class GiphyAttachment extends AttachmentWidget {
|
||||
/// Constructor for creating a [GiphyAttachment] widget
|
||||
const GiphyAttachment({
|
||||
/// {@endtemplate}
|
||||
class StreamGiphyAttachment extends StreamAttachmentWidget {
|
||||
/// Constructor for creating a [StreamGiphyAttachment] widget
|
||||
const StreamGiphyAttachment({
|
||||
Key? key,
|
||||
required Message message,
|
||||
required Attachment attachment,
|
||||
@@ -39,7 +45,7 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
if (imageUrl == null) {
|
||||
return const AttachmentError();
|
||||
}
|
||||
if (attachment.actions.isNotEmpty) {
|
||||
if (attachment.actions != null && attachment.actions!.isNotEmpty) {
|
||||
return _buildSendingAttachment(context, imageUrl);
|
||||
}
|
||||
return _buildSentAttachment(context, imageUrl);
|
||||
@@ -228,7 +234,7 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
alignment: Alignment.centerRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
child: VisibleFootnote(),
|
||||
child: StreamVisibleFootnote(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -243,11 +249,10 @@ class GiphyAttachment extends AttachmentWidget {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: message.attachments,
|
||||
child: StreamFullScreenMedia(
|
||||
mediaAttachmentPackages: message.getAttachmentPackageList(),
|
||||
startIndex: message.attachments.indexOf(attachment),
|
||||
userName: message.user?.name,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -5,10 +5,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_title.dart';
|
||||
import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro image_attachment}
|
||||
@Deprecated("use 'StreamImageAttachment' instead")
|
||||
typedef ImageAttachment = StreamImageAttachment;
|
||||
|
||||
/// {@template image_attachment}
|
||||
/// Widget for showing an image attachment
|
||||
class ImageAttachment extends AttachmentWidget {
|
||||
/// Constructor for creating a [ImageAttachment] widget
|
||||
const ImageAttachment({
|
||||
/// {@endtemplate}
|
||||
class StreamImageAttachment extends StreamAttachmentWidget {
|
||||
/// Constructor for creating a [StreamImageAttachment] widget
|
||||
const StreamImageAttachment({
|
||||
Key? key,
|
||||
required Message message,
|
||||
required Attachment attachment,
|
||||
@@ -25,8 +31,8 @@ class ImageAttachment extends AttachmentWidget {
|
||||
size: size,
|
||||
);
|
||||
|
||||
/// [MessageThemeData] for showing image title
|
||||
final MessageThemeData messageTheme;
|
||||
/// [StreamMessageThemeData] for showing image title
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// Flag for showing title
|
||||
final bool showTitle;
|
||||
@@ -137,12 +143,12 @@ class ImageAttachment extends AttachmentWidget {
|
||||
StreamChannel.of(context).channel;
|
||||
return StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: message.attachments,
|
||||
child: StreamFullScreenMedia(
|
||||
mediaAttachmentPackages:
|
||||
message.getAttachmentPackageList(),
|
||||
startIndex:
|
||||
message.attachments.indexOf(attachment),
|
||||
userName: message.user?.name,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
);
|
||||
@@ -155,7 +161,7 @@ class ImageAttachment extends AttachmentWidget {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AttachmentUploadStateBuilder(
|
||||
child: StreamAttachmentUploadStateBuilder(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
),
|
||||
@@ -166,7 +172,7 @@ class ImageAttachment extends AttachmentWidget {
|
||||
if (showTitle && attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
child: StreamAttachmentTitle(
|
||||
messageTheme: messageTheme,
|
||||
attachment: attachment,
|
||||
),
|
||||
|
||||
@@ -2,10 +2,16 @@ import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro url_attachment}
|
||||
@Deprecated("Use 'StreamUrlAttachment' instead")
|
||||
typedef UrlAttachment = StreamUrlAttachment;
|
||||
|
||||
/// {@template url_attachment}
|
||||
/// Widget to display URL attachment
|
||||
class UrlAttachment extends StatelessWidget {
|
||||
/// Constructor for creating a [UrlAttachment]
|
||||
const UrlAttachment({
|
||||
/// {@endtemplate}
|
||||
class StreamUrlAttachment extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamUrlAttachment]
|
||||
const StreamUrlAttachment({
|
||||
Key? key,
|
||||
required this.urlAttachment,
|
||||
required this.hostDisplayName,
|
||||
@@ -26,8 +32,8 @@ class UrlAttachment extends StatelessWidget {
|
||||
/// Padding for text
|
||||
final EdgeInsets textPadding;
|
||||
|
||||
/// [MessageThemeData] for showing image title
|
||||
final MessageThemeData messageTheme;
|
||||
/// [StreamMessageThemeData] for showing image title
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// The function called when tapping on a link
|
||||
final void Function(String)? onLinkTap;
|
||||
|
||||
@@ -4,10 +4,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro video_attachment}
|
||||
@Deprecated("Use 'StreamVideoAttachment' instead")
|
||||
typedef VideoAttachment = StreamVideoAttachment;
|
||||
|
||||
/// {@template video_attachment}
|
||||
/// Widget for showing a video attachment
|
||||
class VideoAttachment extends AttachmentWidget {
|
||||
/// Constructor for creating a [VideoAttachment] widget
|
||||
const VideoAttachment({
|
||||
/// {@endtemplate}
|
||||
class StreamVideoAttachment extends StreamAttachmentWidget {
|
||||
/// Constructor for creating a [StreamVideoAttachment] widget
|
||||
const StreamVideoAttachment({
|
||||
Key? key,
|
||||
required Message message,
|
||||
required Attachment attachment,
|
||||
@@ -23,8 +29,8 @@ class VideoAttachment extends AttachmentWidget {
|
||||
size: size,
|
||||
);
|
||||
|
||||
/// [MessageThemeData] for showing title
|
||||
final MessageThemeData messageTheme;
|
||||
/// [StreamMessageThemeData] for showing title
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// Callback when show message is tapped
|
||||
final ShowMessageCallback? onShowMessage;
|
||||
@@ -43,7 +49,7 @@ class VideoAttachment extends AttachmentWidget {
|
||||
}
|
||||
return _buildVideoAttachment(
|
||||
context,
|
||||
VideoThumbnailImage(
|
||||
StreamVideoThumbnailImage(
|
||||
video: attachment.file!.path!,
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
@@ -58,7 +64,7 @@ class VideoAttachment extends AttachmentWidget {
|
||||
}
|
||||
return _buildVideoAttachment(
|
||||
context,
|
||||
VideoThumbnailImage(
|
||||
StreamVideoThumbnailImage(
|
||||
video: attachment.assetUrl!,
|
||||
height: size?.height,
|
||||
width: size?.width,
|
||||
@@ -84,12 +90,12 @@ class VideoAttachment extends AttachmentWidget {
|
||||
MaterialPageRoute(
|
||||
builder: (_) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: message.attachments,
|
||||
child: StreamFullScreenMedia(
|
||||
mediaAttachmentPackages:
|
||||
message.getAttachmentPackageList(),
|
||||
startIndex:
|
||||
message.attachments.indexOf(attachment),
|
||||
userName: message.user?.name,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
),
|
||||
@@ -111,7 +117,7 @@ class VideoAttachment extends AttachmentWidget {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AttachmentUploadStateBuilder(
|
||||
child: StreamAttachmentUploadStateBuilder(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
),
|
||||
@@ -123,7 +129,7 @@ class VideoAttachment extends AttachmentWidget {
|
||||
if (attachment.title != null)
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: AttachmentTitle(
|
||||
child: StreamAttachmentTitle(
|
||||
messageTheme: messageTheme,
|
||||
attachment: attachment,
|
||||
),
|
||||
|
||||
@@ -9,14 +9,18 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
typedef AttachmentDownloader = Future<String> Function(
|
||||
Attachment attachment, {
|
||||
ProgressCallback? progressCallback,
|
||||
DownloadedPathCallback? downloadedPathCallback,
|
||||
});
|
||||
|
||||
/// Callback to receive the path once the attachment asset is downloaded
|
||||
typedef DownloadedPathCallback = void Function(String? path);
|
||||
|
||||
/// Widget that shows the options in the gallery view
|
||||
class AttachmentActionsModal extends StatelessWidget {
|
||||
/// Returns a new [AttachmentActionsModal]
|
||||
const AttachmentActionsModal({
|
||||
Key? key,
|
||||
required this.currentIndex,
|
||||
required this.attachment,
|
||||
required this.message,
|
||||
this.onShowMessage,
|
||||
this.imageDownloader,
|
||||
@@ -28,12 +32,12 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
this.customActions = const [],
|
||||
}) : super(key: key);
|
||||
|
||||
/// The attachment object for which the actions are to be performed
|
||||
final Attachment attachment;
|
||||
|
||||
/// The message containing the attachments
|
||||
final Message message;
|
||||
|
||||
/// Current page index
|
||||
final int currentIndex;
|
||||
|
||||
/// Callback to show the message
|
||||
final VoidCallback? onShowMessage;
|
||||
|
||||
@@ -58,10 +62,11 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
/// List of custom actions
|
||||
final List<AttachmentAction> customActions;
|
||||
|
||||
/// Creates a copy of [MessageWidget] with specified attributes overridden.
|
||||
/// Creates a copy of [StreamMessageWidget] with
|
||||
/// specified attributes overridden.
|
||||
AttachmentActionsModal copyWith({
|
||||
Key? key,
|
||||
int? currentIndex,
|
||||
Attachment? attachment,
|
||||
Message? message,
|
||||
VoidCallback? onShowMessage,
|
||||
AttachmentDownloader? imageDownloader,
|
||||
@@ -74,7 +79,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
}) =>
|
||||
AttachmentActionsModal(
|
||||
key: key ?? this.key,
|
||||
currentIndex: currentIndex ?? this.currentIndex,
|
||||
attachment: attachment ?? this.attachment,
|
||||
message: message ?? this.message,
|
||||
onShowMessage: onShowMessage ?? this.onShowMessage,
|
||||
imageDownloader: imageDownloader ?? this.imageDownloader,
|
||||
@@ -137,7 +142,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
if (showSave)
|
||||
_buildButton(
|
||||
context,
|
||||
message.attachments[currentIndex].type == 'video'
|
||||
attachment.type == 'video'
|
||||
? context.translations.saveVideoLabel
|
||||
: context.translations.saveImageLabel,
|
||||
StreamSvgIcon.iconSave(
|
||||
@@ -145,15 +150,16 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
color: theme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
() {
|
||||
final attachment = message.attachments[currentIndex];
|
||||
final isImage = attachment.type == 'image';
|
||||
final Future<String?> Function(
|
||||
Attachment, {
|
||||
void Function(int, int) progressCallback,
|
||||
DownloadedPathCallback downloadedPathCallback,
|
||||
}) saveFile = fileDownloader ?? _downloadAttachment;
|
||||
final Future<String?> Function(
|
||||
Attachment, {
|
||||
void Function(int, int) progressCallback,
|
||||
DownloadedPathCallback downloadedPathCallback,
|
||||
}) saveImage = imageDownloader ?? _downloadAttachment;
|
||||
final downloader = isImage ? saveImage : saveFile;
|
||||
|
||||
@@ -161,6 +167,9 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
ValueNotifier<_DownloadProgress?>(
|
||||
_DownloadProgress.initial(),
|
||||
);
|
||||
final downloadedPathNotifier = ValueNotifier<String?>(
|
||||
null,
|
||||
);
|
||||
|
||||
downloader(
|
||||
attachment,
|
||||
@@ -170,6 +179,9 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
received,
|
||||
);
|
||||
},
|
||||
downloadedPathCallback: (String? path) {
|
||||
downloadedPathNotifier.value = path;
|
||||
},
|
||||
).catchError((e, stk) {
|
||||
progressNotifier.value = null;
|
||||
});
|
||||
@@ -185,6 +197,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
builder: (context) => _buildDownloadProgressDialog(
|
||||
context,
|
||||
progressNotifier,
|
||||
downloadedPathNotifier,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -203,8 +216,12 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
if (message.attachments.length > 1 ||
|
||||
message.text?.isNotEmpty == true) {
|
||||
final currentAttachmentIndex =
|
||||
message.attachments.indexWhere(
|
||||
(element) => element.id == attachment.id,
|
||||
);
|
||||
final remainingAttachments = [...message.attachments]
|
||||
..removeAt(currentIndex);
|
||||
..removeAt(currentAttachmentIndex);
|
||||
channel.updateMessage(message.copyWith(
|
||||
attachments: remainingAttachments,
|
||||
));
|
||||
@@ -284,73 +301,86 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
Widget _buildDownloadProgressDialog(
|
||||
BuildContext context,
|
||||
ValueNotifier<_DownloadProgress?> progressNotifier,
|
||||
ValueNotifier<String?> downloadedFilePathNotifier,
|
||||
) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: progressNotifier,
|
||||
builder: (_, _DownloadProgress? progress, __) {
|
||||
// Pop the dialog in case the progress is null or it's completed.
|
||||
if (progress == null || progress.toProgressIndicatorValue == 1.0) {
|
||||
valueListenable: downloadedFilePathNotifier,
|
||||
builder: (_, String? path, __) {
|
||||
final _downloadComplete = path != null && path.isNotEmpty;
|
||||
// Pop the dialog in case the download has completed
|
||||
if (_downloadComplete) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() => Navigator.of(context).maybePop(),
|
||||
);
|
||||
}
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Center(
|
||||
child: Container(
|
||||
height: 182,
|
||||
width: 182,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: theme.colorTheme.barsBg,
|
||||
),
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: progressNotifier,
|
||||
builder: (_, _DownloadProgress? progress, __) {
|
||||
// Pop the dialog in case the progress is null.
|
||||
if (progress == null) {
|
||||
Future.delayed(
|
||||
const Duration(milliseconds: 500),
|
||||
() => Navigator.of(context).maybePop(),
|
||||
);
|
||||
}
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Center(
|
||||
child: progress == null
|
||||
? SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: StreamSvgIcon.error(
|
||||
color: theme.colorTheme.disabled,
|
||||
),
|
||||
)
|
||||
: progress.toProgressIndicatorValue == 1.0
|
||||
child: Container(
|
||||
height: 182,
|
||||
width: 182,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: theme.colorTheme.barsBg,
|
||||
),
|
||||
child: Center(
|
||||
child: progress == null
|
||||
? SizedBox(
|
||||
key: const Key('completedIcon'),
|
||||
height: 160,
|
||||
width: 160,
|
||||
child: StreamSvgIcon.check(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: StreamSvgIcon.error(
|
||||
color: theme.colorTheme.disabled,
|
||||
),
|
||||
)
|
||||
: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: progress.toProgressIndicatorValue,
|
||||
strokeWidth: 8,
|
||||
valueColor: AlwaysStoppedAnimation(
|
||||
theme.colorTheme.accentPrimary,
|
||||
),
|
||||
: _downloadComplete
|
||||
? SizedBox(
|
||||
key: const Key('completedIcon'),
|
||||
height: 160,
|
||||
width: 160,
|
||||
child: StreamSvgIcon.check(
|
||||
color: theme.colorTheme.disabled,
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
'${progress.toPercentage}%',
|
||||
style: theme.textTheme.headline.copyWith(
|
||||
color: theme.colorTheme.textLowEmphasis,
|
||||
)
|
||||
: SizedBox(
|
||||
height: 100,
|
||||
width: 100,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 8,
|
||||
color: theme.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: Text(
|
||||
'${progress.receivedValueInMB} MB',
|
||||
style:
|
||||
theme.textTheme.headline.copyWith(
|
||||
color:
|
||||
theme.colorTheme.textLowEmphasis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -359,6 +389,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
Future<String?> _downloadAttachment(
|
||||
Attachment attachment, {
|
||||
ProgressCallback? progressCallback,
|
||||
DownloadedPathCallback? downloadedPathCallback,
|
||||
}) async {
|
||||
String? filePath;
|
||||
final appDocDir = await getTemporaryDirectory();
|
||||
@@ -374,6 +405,7 @@ class AttachmentActionsModal extends StatelessWidget {
|
||||
onReceiveProgress: progressCallback,
|
||||
);
|
||||
final result = await ImageGallerySaver.saveFile(filePath!);
|
||||
downloadedPathCallback?.call((result as Map)['filePath']);
|
||||
return (result as Map)['filePath'];
|
||||
}
|
||||
}
|
||||
@@ -387,6 +419,8 @@ class _DownloadProgress {
|
||||
final int total;
|
||||
final int received;
|
||||
|
||||
String get receivedValueInMB => ((received / 1024) / 1024).toStringAsFixed(2);
|
||||
|
||||
double get toProgressIndicatorValue => received / total;
|
||||
|
||||
int get toPercentage => (received * 100) ~/ total;
|
||||
|
||||
@@ -50,7 +50,7 @@ class StreamBackButton extends StatelessWidget {
|
||||
Positioned(
|
||||
top: 7,
|
||||
right: 7,
|
||||
child: UnreadIndicator(
|
||||
child: StreamUnreadIndicator(
|
||||
cid: cid,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -24,7 +24,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// child: StreamChannel(
|
||||
/// channel: channel,
|
||||
/// child: Center(
|
||||
/// child: ChannelImage(
|
||||
/// child: ChannelAvatar(
|
||||
/// channel: channel,
|
||||
/// ),
|
||||
/// ),
|
||||
@@ -44,6 +44,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// The widget renders the ui based on the first ancestor of type
|
||||
/// [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
|
||||
@Deprecated(
|
||||
"'ChannelAvatar' is deprecated and shouldn't be used. "
|
||||
"Please use 'StreamChannelAvatar' instead.",
|
||||
)
|
||||
class ChannelAvatar extends StatelessWidget {
|
||||
/// Instantiate a new ChannelImage
|
||||
const ChannelAvatar({
|
||||
@@ -147,7 +152,7 @@ class ChannelAvatar extends StatelessWidget {
|
||||
return BetterStreamBuilder<User>(
|
||||
stream: streamChat.client.state.currentUserStream.map((it) => it!),
|
||||
initialData: currentUser,
|
||||
builder: (context, user) => UserAvatar(
|
||||
builder: (context, user) => StreamUserAvatar(
|
||||
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
||||
user: user,
|
||||
constraints: constraints ?? previewTheme?.constraints,
|
||||
@@ -170,7 +175,7 @@ class ChannelAvatar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
initialData: member,
|
||||
builder: (context, member) => UserAvatar(
|
||||
builder: (context, member) => StreamUserAvatar(
|
||||
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
||||
user: member.user!,
|
||||
constraints: constraints ?? previewTheme?.constraints,
|
||||
@@ -183,7 +188,7 @@ class ChannelAvatar extends StatelessWidget {
|
||||
}
|
||||
|
||||
// Group conversation
|
||||
return GroupAvatar(
|
||||
return StreamGroupAvatar(
|
||||
members: otherMembers,
|
||||
borderRadius: borderRadius ?? previewTheme?.borderRadius,
|
||||
constraints: constraints ?? previewTheme?.constraints,
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Bottom Sheet with options
|
||||
@Deprecated("Use 'StreamChannelInfoBottomSheet' instead")
|
||||
class ChannelBottomSheet extends StatefulWidget {
|
||||
/// Constructor for creating bottom sheet
|
||||
const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key);
|
||||
@@ -15,11 +16,12 @@ class ChannelBottomSheet extends StatefulWidget {
|
||||
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
|
||||
}
|
||||
|
||||
// ignore: deprecated_member_use_from_same_package
|
||||
class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
bool _showActions = true;
|
||||
|
||||
late StreamChannelState _streamChannelState;
|
||||
late ChannelPreviewThemeData _channelPreviewThemeData;
|
||||
late StreamChannelPreviewThemeData _channelPreviewThemeData;
|
||||
late StreamChatThemeData _streamChatThemeData;
|
||||
late StreamChatState _streamChatState;
|
||||
|
||||
@@ -53,7 +55,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ChannelName(
|
||||
child: StreamChannelName(
|
||||
channel: channel,
|
||||
textStyle: _streamChatThemeData.textTheme.headlineBold,
|
||||
),
|
||||
),
|
||||
@@ -62,7 +65,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
height: 5,
|
||||
),
|
||||
Center(
|
||||
child: ChannelInfo(
|
||||
child: StreamChannelInfo(
|
||||
showTypingIndicator: false,
|
||||
channel: _streamChannelState.channel,
|
||||
textStyle: _channelPreviewThemeData.subtitleStyle,
|
||||
@@ -74,7 +77,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
if (channel.isDistinct && channel.memberCount == 2)
|
||||
Column(
|
||||
children: [
|
||||
UserAvatar(
|
||||
StreamUserAvatar(
|
||||
user: members
|
||||
.firstWhere(
|
||||
(e) => e.user?.id != userAsMember.user?.id,
|
||||
@@ -117,7 +120,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
UserAvatar(
|
||||
StreamUserAvatar(
|
||||
user: members[index].user!,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 64,
|
||||
@@ -145,7 +148,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
const SizedBox(
|
||||
height: 24,
|
||||
),
|
||||
OptionListTile(
|
||||
StreamOptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: StreamSvgIcon.user(
|
||||
@@ -155,8 +158,10 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
title: context.translations.viewInfoLabel,
|
||||
onTap: widget.onViewInfoTap,
|
||||
),
|
||||
if (!channel.isDistinct)
|
||||
OptionListTile(
|
||||
if (!channel.isDistinct &&
|
||||
channel.ownCapabilities
|
||||
.contains(PermissionType.leaveChannel))
|
||||
StreamOptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: StreamSvgIcon.userRemove(
|
||||
@@ -174,8 +179,10 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
});
|
||||
},
|
||||
),
|
||||
if (isOwner)
|
||||
OptionListTile(
|
||||
if (isOwner &&
|
||||
channel.ownCapabilities
|
||||
.contains(PermissionType.deleteChannel))
|
||||
StreamOptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: StreamSvgIcon.delete(
|
||||
@@ -194,7 +201,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
});
|
||||
},
|
||||
),
|
||||
OptionListTile(
|
||||
StreamOptionListTile(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: StreamSvgIcon.closeSmall(
|
||||
@@ -215,7 +222,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
|
||||
void didChangeDependencies() {
|
||||
_streamChannelState = StreamChannel.of(context);
|
||||
_streamChatThemeData = StreamChatTheme.of(context);
|
||||
_channelPreviewThemeData = ChannelPreviewTheme.of(context);
|
||||
_channelPreviewThemeData = StreamChannelPreviewTheme.of(context);
|
||||
_streamChatState = StreamChat.of(context);
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ import 'package:stream_chat_flutter/src/channel_info.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
///{@macro template_name}
|
||||
@Deprecated("Use 'StreamChannelHeader' instead")
|
||||
typedef ChannelHeader = StreamChannelHeader;
|
||||
|
||||
/// {@template channel_header}
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
@@ -47,11 +52,14 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// with [onBackPressed].
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type
|
||||
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
|
||||
/// [StreamChatTheme] and on its [StreamChatThemeData.channelHeaderTheme]
|
||||
/// property.
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// {@endtemplate}
|
||||
class StreamChannelHeader extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
/// Creates a channel header
|
||||
const ChannelHeader({
|
||||
const StreamChannelHeader({
|
||||
Key? key,
|
||||
this.showBackButton = true,
|
||||
this.onBackPressed,
|
||||
@@ -101,13 +109,13 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
final Widget? leading;
|
||||
|
||||
/// AppBar actions
|
||||
/// By default it shows the [ChannelAvatar]
|
||||
/// By default it shows the [StreamChannelAvatar]
|
||||
final List<Widget>? actions;
|
||||
|
||||
/// The background color for this [ChannelHeader].
|
||||
/// The background color for this [StreamChannelHeader].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The elevation for this [ChannelHeader].
|
||||
/// The elevation for this [StreamChannelHeader].
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
@@ -118,7 +126,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
centerTitle: centerTitle,
|
||||
);
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final channelHeaderTheme = ChannelHeaderTheme.of(context);
|
||||
final channelHeaderTheme = StreamChannelHeaderTheme.of(context);
|
||||
|
||||
final leadingWidget = leading ??
|
||||
(showBackButton
|
||||
@@ -128,7 +136,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
)
|
||||
: const SizedBox());
|
||||
|
||||
return ConnectionStatusBuilder(
|
||||
return StreamConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var statusString = '';
|
||||
var showStatus = true;
|
||||
@@ -148,7 +156,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return InfoTile(
|
||||
return StreamInfoTile(
|
||||
showMessage: showConnectionStateTile && showStatus,
|
||||
message: statusString,
|
||||
child: AppBar(
|
||||
@@ -165,7 +173,8 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
child: Center(
|
||||
child: ChannelAvatar(
|
||||
child: StreamChannelAvatar(
|
||||
channel: channel,
|
||||
borderRadius:
|
||||
channelHeaderTheme.avatarTheme?.borderRadius,
|
||||
constraints:
|
||||
@@ -187,12 +196,13 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
title ??
|
||||
ChannelName(
|
||||
StreamChannelName(
|
||||
channel: channel,
|
||||
textStyle: channelHeaderTheme.titleStyle,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
subtitle ??
|
||||
ChannelInfo(
|
||||
StreamChannelInfo(
|
||||
showTypingIndicator: showTypingIndicator,
|
||||
channel: channel,
|
||||
textStyle: channelHeaderTheme.subtitleStyle,
|
||||
|
||||
@@ -3,10 +3,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro channel_info}
|
||||
@Deprecated("Use 'StreamChannelInfo' instead")
|
||||
typedef ChannelInfo = StreamChannelInfo;
|
||||
|
||||
/// {@template channel_info}
|
||||
/// Widget which shows channel info
|
||||
class ChannelInfo extends StatelessWidget {
|
||||
/// Constructor which creates a [ChannelInfo] widget
|
||||
const ChannelInfo({
|
||||
/// {@endtemplate}
|
||||
class StreamChannelInfo extends StatelessWidget {
|
||||
/// Constructor which creates a [StreamChannelInfo] widget
|
||||
const StreamChannelInfo({
|
||||
Key? key,
|
||||
required this.channel,
|
||||
this.textStyle,
|
||||
@@ -32,7 +38,7 @@ class ChannelInfo extends StatelessWidget {
|
||||
return BetterStreamBuilder<List<Member>>(
|
||||
stream: channel.state!.membersStream,
|
||||
initialData: channel.state!.members,
|
||||
builder: (context, data) => ConnectionStatusBuilder(
|
||||
builder: (context, data) => StreamConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
switch (status) {
|
||||
case ConnectionStatus.connected:
|
||||
@@ -60,12 +66,13 @@ class ChannelInfo extends StatelessWidget {
|
||||
var text = context.translations.membersCountText(memberCount);
|
||||
final onlineCount =
|
||||
members?.where((m) => m.user?.online == true).length ?? 0;
|
||||
if (onlineCount > 0) {
|
||||
if (channel.ownCapabilities.contains(PermissionType.connectEvents) &&
|
||||
onlineCount > 0) {
|
||||
text += ', ${context.translations.watchersCountText(onlineCount)}';
|
||||
}
|
||||
alternativeWidget = Text(
|
||||
text,
|
||||
style: ChannelHeaderTheme.of(context).subtitleStyle,
|
||||
style: StreamChannelHeaderTheme.of(context).subtitleStyle,
|
||||
);
|
||||
} else {
|
||||
final userId = StreamChat.of(context).currentUser?.id;
|
||||
@@ -93,11 +100,12 @@ class ChannelInfo extends StatelessWidget {
|
||||
return alternativeWidget ?? const Offstage();
|
||||
}
|
||||
|
||||
return TypingIndicator(
|
||||
parentId: parentId,
|
||||
alignment: Alignment.center,
|
||||
alternativeWidget: alternativeWidget,
|
||||
style: textStyle,
|
||||
return Align(
|
||||
child: StreamTypingIndicator(
|
||||
parentId: parentId,
|
||||
style: textStyle,
|
||||
alternativeWidget: alternativeWidget,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,11 @@ typedef TitleBuilder = Widget Function(
|
||||
StreamChatClient client,
|
||||
);
|
||||
|
||||
///
|
||||
/// {@macro channel_list_header}
|
||||
@Deprecated("Use 'StreamChannelListHeader' instead")
|
||||
typedef ChannelListHeader = StreamChannelListHeader;
|
||||
|
||||
/// {@template channel_list_header}
|
||||
/// It shows the current [StreamChatClient] status.
|
||||
///
|
||||
/// ```dart
|
||||
@@ -43,11 +47,13 @@ typedef TitleBuilder = Widget Function(
|
||||
/// if you don't have it in the widget tree.
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type
|
||||
/// [StreamChatTheme] and on its [ChannelListHeaderThemeData] property.
|
||||
/// [StreamChatTheme] and on its [StreamChannelListHeaderThemeData] property.
|
||||
/// Modify it to change the widget appearance.
|
||||
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// {@endtemplate}
|
||||
class StreamChannelListHeader extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
/// Instantiates a ChannelListHeader
|
||||
const ChannelListHeader({
|
||||
const StreamChannelListHeader({
|
||||
Key? key,
|
||||
this.client,
|
||||
this.titleBuilder,
|
||||
@@ -96,17 +102,17 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// By default it shows the new chat button
|
||||
final List<Widget>? actions;
|
||||
|
||||
/// The background color for this [ChannelListHeader].
|
||||
/// The background color for this [StreamChannelListHeader].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// The elevation for this [ChannelListHeader].
|
||||
/// The elevation for this [StreamChannelListHeader].
|
||||
final double elevation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final _client = client ?? StreamChat.of(context).client;
|
||||
final user = _client.state.currentUser;
|
||||
return ConnectionStatusBuilder(
|
||||
return StreamConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var statusString = '';
|
||||
var showStatus = true;
|
||||
@@ -125,9 +131,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
}
|
||||
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
final channelListHeaderThemeData = ChannelListHeaderTheme.of(context);
|
||||
final channelListHeaderThemeData =
|
||||
StreamChannelListHeaderTheme.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return InfoTile(
|
||||
return StreamInfoTile(
|
||||
showMessage: showConnectionStateTile && showStatus,
|
||||
message: statusString,
|
||||
child: AppBar(
|
||||
@@ -143,7 +150,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
leading: leading ??
|
||||
Center(
|
||||
child: user != null
|
||||
? UserAvatar(
|
||||
? StreamUserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: false,
|
||||
onTap: onUserAvatarTap ??
|
||||
@@ -164,7 +171,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
[
|
||||
StreamNeumorphicButton(
|
||||
child: IconButton(
|
||||
icon: ConnectionStatusBuilder(
|
||||
icon: StreamConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
Color? color;
|
||||
switch (status) {
|
||||
@@ -242,10 +249,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
context.translations.searchingForNetworkText,
|
||||
style: ChannelListHeaderTheme.of(context).titleStyle?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
style:
|
||||
StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -255,7 +263,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
StreamChatClient client,
|
||||
) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
final channelListHeaderTheme = ChannelListHeaderTheme.of(context);
|
||||
final channelListHeaderTheme = StreamChannelListHeaderTheme.of(context);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:collection/collection.dart';
|
||||
// ignore: lines_longer_than_80_chars
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
@@ -12,7 +13,7 @@ typedef ChannelTapCallback = void Function(Channel, Widget?);
|
||||
/// Callback called when tapping on a channel
|
||||
typedef ChannelInfoCallback = void Function(Channel);
|
||||
|
||||
/// Builder used to create a custom [ChannelPreview] from a [Channel]
|
||||
/// Builder used to create a custom [StreamChannelPreview] from a [Channel]
|
||||
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
|
||||
|
||||
/// Callback for when 'View Info' is tapped
|
||||
@@ -53,6 +54,7 @@ typedef ViewInfoCallback = void Function(Channel);
|
||||
/// The widget components render the ui based on the first ancestor of
|
||||
/// type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
@Deprecated("Use 'StreamChannelListView' instead")
|
||||
class ChannelListView extends StatefulWidget {
|
||||
/// Instantiate a new ChannelListView
|
||||
ChannelListView({
|
||||
@@ -208,8 +210,6 @@ class ChannelListView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ChannelListViewState extends State<ChannelListView> {
|
||||
final _slideController = SlidableController();
|
||||
|
||||
late final _defaultController = ChannelListController();
|
||||
|
||||
ChannelListController get _channelListController =>
|
||||
@@ -245,7 +245,8 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
child: child,
|
||||
);
|
||||
|
||||
final backgroundColor = ChannelListViewTheme.of(context).backgroundColor;
|
||||
final backgroundColor =
|
||||
StreamChannelListViewTheme.of(context).backgroundColor;
|
||||
|
||||
if (backgroundColor != null) {
|
||||
return ColoredBox(
|
||||
@@ -270,19 +271,21 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
_gridItemBuilder(context, index, channels),
|
||||
);
|
||||
}
|
||||
return ListView.separated(
|
||||
padding: widget.padding,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
// all channels + progress loader
|
||||
itemCount: channels.length + 1,
|
||||
separatorBuilder: (_, index) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder!(context, index);
|
||||
}
|
||||
return _separatorBuilder(context, index);
|
||||
},
|
||||
itemBuilder: (context, index) =>
|
||||
_listItemBuilder(context, index, channels),
|
||||
return SlidableAutoCloseBehavior(
|
||||
child: ListView.separated(
|
||||
padding: widget.padding,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
// all channels + progress loader
|
||||
itemCount: channels.length + 1,
|
||||
separatorBuilder: (_, index) {
|
||||
if (widget.separatorBuilder != null) {
|
||||
return widget.separatorBuilder!(context, index);
|
||||
}
|
||||
return _separatorBuilder(context, index);
|
||||
},
|
||||
itemBuilder: (context, index) =>
|
||||
_listItemBuilder(context, index, channels),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -510,87 +513,92 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
final backgroundColor = chatThemeData.colorTheme.inputBg;
|
||||
final channel = channels[i];
|
||||
|
||||
final canDeleteChannel =
|
||||
channel.ownCapabilities.contains(PermissionType.deleteChannel);
|
||||
|
||||
final actionPaneChildren =
|
||||
widget.swipeActions?.length ?? (canDeleteChannel ? 2 : 1);
|
||||
final actionPaneExtentRatio = actionPaneChildren > 5
|
||||
? 1 / actionPaneChildren
|
||||
: actionPaneChildren * 0.2;
|
||||
|
||||
return StreamChannel(
|
||||
key: ValueKey<String>('CHANNEL-${channel.cid}'),
|
||||
channel: channel,
|
||||
child: Slidable(
|
||||
controller: _slideController,
|
||||
enabled: widget.swipeToAction,
|
||||
actionPane: const SlidableBehindActionPane(),
|
||||
actionExtentRatio: 0.12,
|
||||
secondaryActions: widget.swipeActions
|
||||
?.map((e) => IconSlideAction(
|
||||
color: e.color,
|
||||
iconWidget: e.iconWidget,
|
||||
onTap: () {
|
||||
e.onTap?.call(channel);
|
||||
},
|
||||
))
|
||||
.toList() ??
|
||||
<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.currentUser?.id,
|
||||
)
|
||||
?.role))
|
||||
IconSlideAction(
|
||||
color: backgroundColor,
|
||||
iconWidget: StreamSvgIcon.delete(
|
||||
color: chatThemeData.colorTheme.accentError,
|
||||
),
|
||||
onTap: widget.onDeletePressed != null
|
||||
? () {
|
||||
widget.onDeletePressed?.call(channel);
|
||||
endActionPane: ActionPane(
|
||||
extentRatio: actionPaneExtentRatio,
|
||||
motion: const BehindMotion(),
|
||||
children: widget.swipeActions
|
||||
?.map((e) => CustomSlidableAction(
|
||||
backgroundColor: e.color ?? Colors.white,
|
||||
child: e.iconWidget,
|
||||
onPressed: (_) {
|
||||
e.onTap?.call(channel);
|
||||
},
|
||||
))
|
||||
.toList() ??
|
||||
<Widget>[
|
||||
CustomSlidableAction(
|
||||
backgroundColor: backgroundColor,
|
||||
onPressed: widget.onMoreDetailsPressed != null
|
||||
? (_) {
|
||||
widget.onMoreDetailsPressed!(channel);
|
||||
}
|
||||
: () async {
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
title: context.translations.deleteConversationLabel,
|
||||
question:
|
||||
context.translations.deleteConversationQuestion,
|
||||
okText: context.translations.deleteLabel,
|
||||
cancelText: context.translations.cancelLabel,
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: chatThemeData.colorTheme.accentError,
|
||||
: (_) {
|
||||
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: StreamChannelInfoBottomSheet(
|
||||
channel: channel,
|
||||
onViewInfoTap: () {
|
||||
widget.onViewInfoTap?.call(channel);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
if (res == true) {
|
||||
await channel.delete();
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.more_horiz),
|
||||
),
|
||||
],
|
||||
if (canDeleteChannel)
|
||||
CustomSlidableAction(
|
||||
backgroundColor: backgroundColor,
|
||||
onPressed: widget.onDeletePressed != null
|
||||
? (_) {
|
||||
widget.onDeletePressed?.call(channel);
|
||||
}
|
||||
: (_) async {
|
||||
final res = await showConfirmationDialog(
|
||||
context,
|
||||
title:
|
||||
context.translations.deleteConversationLabel,
|
||||
question: context
|
||||
.translations.deleteConversationQuestion,
|
||||
okText: context.translations.deleteLabel,
|
||||
cancelText: context.translations.cancelLabel,
|
||||
icon: StreamSvgIcon.delete(
|
||||
color: chatThemeData.colorTheme.accentError,
|
||||
),
|
||||
);
|
||||
if (res == true) {
|
||||
await channel.delete();
|
||||
}
|
||||
},
|
||||
child: StreamSvgIcon.delete(
|
||||
color: chatThemeData.colorTheme.accentError,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: widget.channelPreviewBuilder?.call(context, channel) ??
|
||||
DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
@@ -642,7 +650,7 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ChannelAvatar(
|
||||
StreamChannelAvatar(
|
||||
channel: channel,
|
||||
borderRadius: BorderRadius.circular(32),
|
||||
selected: selected,
|
||||
@@ -657,8 +665,9 @@ class _ChannelListViewState extends State<ChannelListView> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: StreamChannel(
|
||||
channel: channel,
|
||||
child: const ChannelName(
|
||||
textStyle: TextStyle(
|
||||
child: StreamChannelName(
|
||||
channel: channel,
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
///
|
||||
/// The widget uses a [StreamBuilder] to render the channel information
|
||||
/// image as soon as it updates.
|
||||
@Deprecated("Use 'StreamChannelName' instead")
|
||||
class ChannelName extends StatelessWidget {
|
||||
/// Instantiate a new ChannelName
|
||||
const ChannelName({
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template channel_preview}
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
@@ -13,11 +14,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// image as soon as it updates.
|
||||
///
|
||||
/// Usually you don't use this widget as it's the default channel preview
|
||||
/// used by [ChannelListView].
|
||||
/// used by [StreamChannelListView].
|
||||
///
|
||||
/// The widget renders the ui based on the first ancestor of type
|
||||
/// [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
/// {@endtemplate}
|
||||
@Deprecated("Use 'StreamChannelListTile' instead")
|
||||
class ChannelPreview extends StatelessWidget {
|
||||
/// Constructor for creating [ChannelPreview]
|
||||
const ChannelPreview({
|
||||
@@ -52,7 +55,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
final Widget? subtitle;
|
||||
|
||||
/// Widget rendering the leading element, by default
|
||||
/// it shows the [ChannelAvatar]
|
||||
/// it shows the [StreamChannelAvatar]
|
||||
final Widget? leading;
|
||||
|
||||
/// Widget rendering the trailing element,
|
||||
@@ -60,7 +63,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
final Widget? trailing;
|
||||
|
||||
/// Widget rendering the sending indicator,
|
||||
/// by default it uses the [SendingIndicator] widget
|
||||
/// by default it uses the [StreamSendingIndicator] widget
|
||||
final Widget? sendingIndicator;
|
||||
|
||||
@override
|
||||
@@ -80,13 +83,18 @@ class ChannelPreview extends StatelessWidget {
|
||||
),
|
||||
onTap: () => onTap?.call(channel),
|
||||
onLongPress: () => onLongPress?.call(channel),
|
||||
leading: leading ?? ChannelAvatar(onTap: onImageTap),
|
||||
leading: leading ??
|
||||
StreamChannelAvatar(
|
||||
channel: channel,
|
||||
onTap: onImageTap,
|
||||
),
|
||||
title: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: title ??
|
||||
ChannelName(
|
||||
StreamChannelName(
|
||||
channel: channel,
|
||||
textStyle: channelPreviewTheme.titleStyle,
|
||||
),
|
||||
),
|
||||
@@ -100,7 +108,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
e.user!.id == channel.client.state.currentUser?.id)) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return UnreadIndicator(
|
||||
return StreamUnreadIndicator(
|
||||
cid: channel.cid,
|
||||
);
|
||||
},
|
||||
@@ -136,7 +144,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
)));
|
||||
final isMessageRead = readList.length >=
|
||||
(channel.memberCount ?? 0) - 1;
|
||||
return SendingIndicator(
|
||||
return StreamSendingIndicator(
|
||||
message: lastMessage!,
|
||||
size: channelPreviewTheme.indicatorIconSize,
|
||||
isMessageRead: isMessageRead,
|
||||
@@ -204,7 +212,7 @@ class ChannelPreview extends StatelessWidget {
|
||||
],
|
||||
);
|
||||
}
|
||||
return TypingIndicator(
|
||||
return StreamTypingIndicator(
|
||||
channel: channel,
|
||||
alternativeWidget: _buildLastMessage(context),
|
||||
style: channelPreviewTheme.subtitleStyle,
|
||||
|
||||
@@ -2,10 +2,17 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro commands_overlay}
|
||||
@Deprecated("Use 'StreamCommandsOverlay' instead")
|
||||
typedef CommandsOverlay = StreamCommandsOverlay;
|
||||
|
||||
/// {@template commands_overlay}
|
||||
/// Overlay for displaying commands that can be used
|
||||
class CommandsOverlay extends StatelessWidget {
|
||||
/// Constructor for creating a [CommandsOverlay]
|
||||
const CommandsOverlay({
|
||||
/// to interact with the channel.
|
||||
/// {@endtemplate}
|
||||
class StreamCommandsOverlay extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamCommandsOverlay]
|
||||
const StreamCommandsOverlay({
|
||||
required this.text,
|
||||
required this.onCommandResult,
|
||||
required this.size,
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro connection_status_builder}
|
||||
@Deprecated("Use 'StreamConnectionStatusBuilder' instead")
|
||||
typedef ConnectionStatusBuilder = StreamConnectionStatusBuilder;
|
||||
|
||||
/// {@template connection_status_builder}
|
||||
/// Widget that builds itself based on the latest snapshot of interaction with
|
||||
/// a [Stream] of type [ConnectionStatus].
|
||||
///
|
||||
/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream]
|
||||
/// in case no stream is provided.
|
||||
class ConnectionStatusBuilder extends StatelessWidget {
|
||||
/// {@endtemplate}
|
||||
class StreamConnectionStatusBuilder extends StatelessWidget {
|
||||
/// Creates a new ConnectionStatusBuilder
|
||||
const ConnectionStatusBuilder({
|
||||
const StreamConnectionStatusBuilder({
|
||||
Key? key,
|
||||
required this.statusBuilder,
|
||||
this.connectionStatusStream,
|
||||
|
||||
@@ -3,10 +3,16 @@ import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
/// {@macro date_divider}
|
||||
@Deprecated("Use 'StreamDateDivider' instead")
|
||||
typedef DateDivider = StreamDateDivider;
|
||||
|
||||
/// {@template date_divider}
|
||||
/// It shows a date divider depending on the date difference
|
||||
class DateDivider extends StatelessWidget {
|
||||
/// Constructor for creating a [DateDivider]
|
||||
const DateDivider({
|
||||
/// {@endtemplate}
|
||||
class StreamDateDivider extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamDateDivider]
|
||||
const StreamDateDivider({
|
||||
Key? key,
|
||||
required this.dateTime,
|
||||
this.uppercase = false,
|
||||
|
||||
@@ -3,10 +3,16 @@ import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||
|
||||
/// Widget to display deleted message
|
||||
class DeletedMessage extends StatelessWidget {
|
||||
/// Constructor to create [DeletedMessage]
|
||||
const DeletedMessage({
|
||||
/// {@macro deleted_message}
|
||||
@Deprecated("Use 'StreamDeletedMessage' instead")
|
||||
typedef DeletedMessage = StreamDeletedMessage;
|
||||
|
||||
/// {@template deleted_message}
|
||||
/// Widget to display deleted message.
|
||||
/// {@endtemplate}
|
||||
class StreamDeletedMessage extends StatelessWidget {
|
||||
/// Constructor to create [StreamDeletedMessage]
|
||||
const StreamDeletedMessage({
|
||||
Key? key,
|
||||
required this.messageTheme,
|
||||
this.borderRadiusGeometry,
|
||||
@@ -16,7 +22,7 @@ class DeletedMessage extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
/// The theme of the message
|
||||
final MessageThemeData messageTheme;
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// The border radius of the message text
|
||||
final BorderRadiusGeometry? borderRadiusGeometry;
|
||||
|
||||
@@ -4,10 +4,16 @@ import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:substring_highlight/substring_highlight.dart';
|
||||
|
||||
/// {@macro emoji_overlay}
|
||||
@Deprecated("Use 'StreamEmojiOverlay' instead")
|
||||
typedef EmojiOverlay = StreamEmojiOverlay;
|
||||
|
||||
/// {@template emoji_overlay}
|
||||
/// Overlay for displaying emoji that can be used
|
||||
class EmojiOverlay extends StatelessWidget {
|
||||
/// Constructor for creating a [EmojiOverlay]
|
||||
const EmojiOverlay({
|
||||
/// {@endtemplate}
|
||||
class StreamEmojiOverlay extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamEmojiOverlay]
|
||||
const StreamEmojiOverlay({
|
||||
required this.query,
|
||||
required this.onEmojiResult,
|
||||
required this.size,
|
||||
|
||||
@@ -46,7 +46,7 @@ extension IterableX<T> on Iterable<T> {
|
||||
extension PlatformFileX on PlatformFile {
|
||||
/// Converts the [PlatformFile] into [AttachmentFile]
|
||||
AttachmentFile get toAttachmentFile => AttachmentFile(
|
||||
//ignore: avoid_redundant_argument_values
|
||||
// ignore: avoid_redundant_argument_values
|
||||
path: kIsWeb ? null : path,
|
||||
name: name,
|
||||
bytes: bytes,
|
||||
@@ -226,6 +226,53 @@ extension UserListX on List<User> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions on Message
|
||||
extension MessageX on Message {
|
||||
/// It replaces the user mentions with the actual user names.
|
||||
Message replaceMentions({bool linkify = true}) {
|
||||
var messageTextToRender = text;
|
||||
for (final user in mentionedUsers.toSet()) {
|
||||
final userId = user.id;
|
||||
final userName = user.name;
|
||||
if (linkify) {
|
||||
messageTextToRender = messageTextToRender?.replaceAll(
|
||||
'@$userId',
|
||||
'[@$userName](@${userName.replaceAll(' ', '')})',
|
||||
);
|
||||
} else {
|
||||
messageTextToRender = messageTextToRender?.replaceAll(
|
||||
'@$userId',
|
||||
'@$userName',
|
||||
);
|
||||
}
|
||||
}
|
||||
return copyWith(text: messageTextToRender);
|
||||
}
|
||||
|
||||
/// It returns the message with the translated text if available locally
|
||||
Message translate(String language) =>
|
||||
copyWith(text: i18n?['${language}_text'] ?? text);
|
||||
|
||||
/// It returns the message replacing the mentioned user names with
|
||||
/// the respective user ids
|
||||
Message replaceMentionsWithId() {
|
||||
if (mentionedUsers.isEmpty) return this;
|
||||
|
||||
var messageTextToSend = text;
|
||||
if (messageTextToSend == null) return this;
|
||||
|
||||
for (final user in mentionedUsers.toSet()) {
|
||||
final userName = user.name;
|
||||
messageTextToSend = messageTextToSend!.replaceAll(
|
||||
'@$userName',
|
||||
'@${user.id}',
|
||||
);
|
||||
}
|
||||
|
||||
return copyWith(text: messageTextToSend);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions on [Uri]
|
||||
extension UriX on Uri {
|
||||
/// Return the URI adding the http scheme if it is missing
|
||||
|
||||
@@ -21,13 +21,18 @@ enum ReturnActionType {
|
||||
/// Callback when show message is tapped
|
||||
typedef ShowMessageCallback = void Function(Message message, Channel channel);
|
||||
|
||||
/// {@macro full_screen_media}
|
||||
@Deprecated("Use 'StreamFullScreenMedia' instead")
|
||||
typedef FullScreenMedia = StreamFullScreenMedia;
|
||||
|
||||
/// {@template full_screen_media}
|
||||
/// A full screen image widget
|
||||
class FullScreenMedia extends StatefulWidget {
|
||||
/// {@endtemplate}
|
||||
class StreamFullScreenMedia extends StatefulWidget {
|
||||
/// Instantiate a new FullScreenImage
|
||||
const FullScreenMedia({
|
||||
const StreamFullScreenMedia({
|
||||
Key? key,
|
||||
required this.mediaAttachments,
|
||||
required this.message,
|
||||
required this.mediaAttachmentPackages,
|
||||
this.startIndex = 0,
|
||||
String? userName,
|
||||
this.onShowMessage,
|
||||
@@ -37,10 +42,7 @@ class FullScreenMedia extends StatefulWidget {
|
||||
super(key: key);
|
||||
|
||||
/// The url of the image
|
||||
final List<Attachment> mediaAttachments;
|
||||
|
||||
/// Message where attachments are attached
|
||||
final Message message;
|
||||
final List<StreamAttachmentPackage> mediaAttachmentPackages;
|
||||
|
||||
/// First index of media shown
|
||||
final int startIndex;
|
||||
@@ -60,10 +62,10 @@ class FullScreenMedia extends StatefulWidget {
|
||||
final bool autoplayVideos;
|
||||
|
||||
@override
|
||||
_FullScreenMediaState createState() => _FullScreenMediaState();
|
||||
_StreamFullScreenMediaState createState() => _StreamFullScreenMediaState();
|
||||
}
|
||||
|
||||
class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _animationController;
|
||||
late final PageController _pageController;
|
||||
@@ -94,8 +96,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
duration: const Duration(milliseconds: 300),
|
||||
);
|
||||
_pageController = PageController(initialPage: widget.startIndex);
|
||||
for (var i = 0; i < widget.mediaAttachments.length; i++) {
|
||||
final attachment = widget.mediaAttachments[i];
|
||||
for (var i = 0; i < widget.mediaAttachmentPackages.length; i++) {
|
||||
final attachment = widget.mediaAttachmentPackages[i].attachment;
|
||||
if (attachment.type != 'video') continue;
|
||||
final package = VideoPackage(attachment, showControls: true);
|
||||
videoPackages[attachment.id] = package;
|
||||
@@ -108,7 +110,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
return;
|
||||
}
|
||||
|
||||
final currentAttachment = widget.mediaAttachments[widget.startIndex];
|
||||
final currentAttachment =
|
||||
widget.mediaAttachmentPackages[widget.startIndex].attachment;
|
||||
|
||||
await Future.wait(videoPackages.values.map(
|
||||
(it) => it.initialize(),
|
||||
@@ -136,7 +139,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
return;
|
||||
}
|
||||
|
||||
final currentAttachment = widget.mediaAttachments[val];
|
||||
final currentAttachment =
|
||||
widget.mediaAttachmentPackages[val].attachment;
|
||||
|
||||
for (final e in videoPackages.values) {
|
||||
if (e._attachment != currentAttachment) {
|
||||
@@ -151,7 +155,9 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
}
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
final currentAttachmentPackage =
|
||||
widget.mediaAttachmentPackages[index];
|
||||
final attachment = currentAttachmentPackage.attachment;
|
||||
if (attachment.type == 'image' || attachment.type == 'giphy') {
|
||||
final imageUrl = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
@@ -168,11 +174,11 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
maxScale: PhotoViewComputedScale.covered,
|
||||
minScale: PhotoViewComputedScale.contained,
|
||||
heroAttributes: PhotoViewHeroAttributes(
|
||||
tag: widget.mediaAttachments,
|
||||
tag: widget.mediaAttachmentPackages,
|
||||
),
|
||||
backgroundDecoration: BoxDecoration(
|
||||
color: ColorTween(
|
||||
begin: ChannelHeaderTheme.of(context).color,
|
||||
begin: StreamChannelHeaderTheme.of(context).color,
|
||||
end: Colors.black,
|
||||
).lerp(_curvedAnimation.value),
|
||||
),
|
||||
@@ -212,53 +218,66 @@ class _FullScreenMediaState extends State<FullScreenMedia>
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
itemCount: widget.mediaAttachments.length,
|
||||
itemCount: widget.mediaAttachmentPackages.length,
|
||||
),
|
||||
FadeTransition(
|
||||
opacity: _opacityAnimation,
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: _currentPage,
|
||||
builder: (context, value, child) => Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GalleryHeader(
|
||||
userName: widget.userName,
|
||||
sentAt: context.translations.sentAtText(
|
||||
date: widget.message.createdAt,
|
||||
time: widget.message.createdAt,
|
||||
),
|
||||
onBackPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
message: widget.message,
|
||||
currentIndex: value,
|
||||
onShowMessage: () {
|
||||
widget.onShowMessage?.call(
|
||||
widget.message,
|
||||
StreamChannel.of(context).channel,
|
||||
);
|
||||
},
|
||||
attachmentActionsModalBuilder:
|
||||
widget.attachmentActionsModalBuilder,
|
||||
),
|
||||
if (!widget.message.isEphemeral)
|
||||
GalleryFooter(
|
||||
currentPage: value,
|
||||
totalPages: widget.mediaAttachments.length,
|
||||
mediaAttachments: widget.mediaAttachments,
|
||||
message: widget.message,
|
||||
mediaSelectedCallBack: (val) {
|
||||
_currentPage.value = val;
|
||||
_pageController.animateToPage(
|
||||
val,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
builder: (context, value, child) {
|
||||
final _currentAttachmentPackage =
|
||||
widget.mediaAttachmentPackages[value];
|
||||
final _currentMessage = _currentAttachmentPackage.message;
|
||||
final _currentAttachment =
|
||||
_currentAttachmentPackage.attachment;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
StreamGalleryHeader(
|
||||
userName: widget.userName,
|
||||
sentAt: context.translations.sentAtText(
|
||||
date: widget
|
||||
.mediaAttachmentPackages[_currentPage.value]
|
||||
.message
|
||||
.createdAt,
|
||||
time: widget
|
||||
.mediaAttachmentPackages[_currentPage.value]
|
||||
.message
|
||||
.createdAt,
|
||||
),
|
||||
onBackPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
message: _currentMessage,
|
||||
attachment: _currentAttachment,
|
||||
onShowMessage: () {
|
||||
widget.onShowMessage?.call(
|
||||
_currentMessage,
|
||||
StreamChannel.of(context).channel,
|
||||
);
|
||||
},
|
||||
attachmentActionsModalBuilder:
|
||||
widget.attachmentActionsModalBuilder,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!_currentMessage.isEphemeral)
|
||||
StreamGalleryFooter(
|
||||
currentPage: value,
|
||||
totalPages: widget.mediaAttachmentPackages.length,
|
||||
mediaAttachmentPackages:
|
||||
widget.mediaAttachmentPackages,
|
||||
mediaSelectedCallBack: (val) {
|
||||
_currentPage.value = val;
|
||||
_pageController.animateToPage(
|
||||
val,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -9,18 +9,24 @@ import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro gallery_footer}
|
||||
@Deprecated("Use 'StreamGalleryFooter' instead")
|
||||
typedef GalleryFooter = StreamGalleryFooter;
|
||||
|
||||
/// {@template gallery_footer}
|
||||
/// Footer widget for media display
|
||||
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
/// Creates a channel header
|
||||
const GalleryFooter({
|
||||
/// {@endtemplate}
|
||||
class StreamGalleryFooter extends StatefulWidget
|
||||
implements PreferredSizeWidget {
|
||||
/// Creates a StreamGalleryFooter
|
||||
const StreamGalleryFooter({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.onBackPressed,
|
||||
this.onTitleTap,
|
||||
this.onImageTap,
|
||||
this.currentPage = 0,
|
||||
this.totalPages = 0,
|
||||
this.mediaAttachments = const [],
|
||||
required this.mediaAttachmentPackages,
|
||||
this.mediaSelectedCallBack,
|
||||
this.backgroundColor,
|
||||
}) : preferredSize = const Size.fromHeight(kToolbarHeight),
|
||||
@@ -43,30 +49,27 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
|
||||
final int totalPages;
|
||||
|
||||
/// All attachments to show
|
||||
final List<Attachment> mediaAttachments;
|
||||
|
||||
/// Message which attachments are attached to
|
||||
final Message message;
|
||||
final List<StreamAttachmentPackage> mediaAttachmentPackages;
|
||||
|
||||
/// Callback when media is selected
|
||||
final ValueChanged<int>? mediaSelectedCallBack;
|
||||
|
||||
/// The background color of this [GalleryFooter].
|
||||
/// The background color of this [StreamGalleryFooter].
|
||||
final Color? backgroundColor;
|
||||
|
||||
@override
|
||||
_GalleryFooterState createState() => _GalleryFooterState();
|
||||
_StreamGalleryFooterState createState() => _StreamGalleryFooterState();
|
||||
|
||||
@override
|
||||
final Size preferredSize;
|
||||
}
|
||||
|
||||
class _GalleryFooterState extends State<GalleryFooter> {
|
||||
class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const showShareButton = !kIsWeb;
|
||||
final mediaQueryData = MediaQuery.of(context);
|
||||
final galleryFooterThemeData = GalleryFooterTheme.of(context);
|
||||
final galleryFooterThemeData = StreamGalleryFooterTheme.of(context);
|
||||
return SizedBox.fromSize(
|
||||
size: Size(
|
||||
mediaQueryData.size.width,
|
||||
@@ -92,8 +95,8 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
color: galleryFooterThemeData.shareIconColor,
|
||||
),
|
||||
onPressed: () async {
|
||||
final attachment =
|
||||
widget.mediaAttachments[widget.currentPage];
|
||||
final attachment = widget
|
||||
.mediaAttachmentPackages[widget.currentPage].attachment;
|
||||
final url = attachment.imageUrl ??
|
||||
attachment.assetUrl ??
|
||||
attachment.thumbUrl!;
|
||||
@@ -149,7 +152,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
|
||||
void _showPhotosModal(context) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
final galleryFooterThemeData = GalleryFooterTheme.of(context);
|
||||
final galleryFooterThemeData = StreamGalleryFooterTheme.of(context);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
barrierColor: galleryFooterThemeData.bottomSheetBarrierColor,
|
||||
@@ -164,7 +167,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
builder: (context) {
|
||||
const crossAxisCount = 3;
|
||||
final noOfRowToShowInitially =
|
||||
widget.mediaAttachments.length > crossAxisCount ? 2 : 1;
|
||||
widget.mediaAttachmentPackages.length > crossAxisCount ? 2 : 1;
|
||||
final size = MediaQuery.of(context).size;
|
||||
final initialChildSize =
|
||||
48 + (size.width * noOfRowToShowInitially) / crossAxisCount;
|
||||
@@ -205,7 +208,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.mediaAttachments.length,
|
||||
itemCount: widget.mediaAttachmentPackages.length,
|
||||
padding: const EdgeInsets.all(1),
|
||||
// ignore: lines_longer_than_80_chars
|
||||
gridDelegate:
|
||||
@@ -216,13 +219,16 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
Widget media;
|
||||
final attachment = widget.mediaAttachments[index];
|
||||
final attachmentPackage =
|
||||
widget.mediaAttachmentPackages[index];
|
||||
final attachment = attachmentPackage.attachment;
|
||||
final message = attachmentPackage.message;
|
||||
if (attachment.type == 'video') {
|
||||
media = InkWell(
|
||||
onTap: () => widget.mediaSelectedCallBack!(index),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.cover,
|
||||
child: VideoThumbnailImage(
|
||||
child: StreamVideoThumbnailImage(
|
||||
video: (attachment.file?.path ??
|
||||
attachment.assetUrl)!,
|
||||
),
|
||||
@@ -246,7 +252,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
return Stack(
|
||||
children: [
|
||||
media,
|
||||
if (widget.message.user != null)
|
||||
if (message.user != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Container(
|
||||
@@ -264,8 +270,8 @@ class _GalleryFooterState extends State<GalleryFooter> {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: UserAvatar(
|
||||
user: widget.message.user!,
|
||||
child: StreamUserAvatar(
|
||||
user: message.user!,
|
||||
constraints:
|
||||
BoxConstraints.tight(const Size(24, 24)),
|
||||
showOnlineStatus: false,
|
||||
|
||||
@@ -15,13 +15,20 @@ typedef AttachmentActionsBuilder = Widget Function(
|
||||
AttachmentActionsModal defaultActionsModal,
|
||||
);
|
||||
|
||||
/// {@macro gallery_header}
|
||||
@Deprecated("Use 'StreamGalleryHeader' instead")
|
||||
typedef GalleryHeader = StreamGalleryHeader;
|
||||
|
||||
/// {@template gallery_header}
|
||||
/// Header/AppBar widget for media display screen
|
||||
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// {@endtemplate}
|
||||
class StreamGalleryHeader extends StatelessWidget
|
||||
implements PreferredSizeWidget {
|
||||
/// Creates a channel header
|
||||
const GalleryHeader({
|
||||
const StreamGalleryHeader({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.currentIndex = 0,
|
||||
required this.attachment,
|
||||
this.showBackButton = true,
|
||||
this.onBackPressed,
|
||||
this.onShowMessage,
|
||||
@@ -53,16 +60,16 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
/// Message which attachments are attached to
|
||||
final Message message;
|
||||
|
||||
/// The attachment that's currently in focus
|
||||
final Attachment attachment;
|
||||
|
||||
/// Username of sender
|
||||
final String userName;
|
||||
|
||||
/// Text which connotes the time the message was sent
|
||||
final String sentAt;
|
||||
|
||||
/// Stores the current index of media shown
|
||||
final int currentIndex;
|
||||
|
||||
/// The background color of this [GalleryHeader].
|
||||
/// The background color of this [StreamGalleryHeader].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Widget builder for attachment actions modal
|
||||
@@ -72,7 +79,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
|
||||
final galleryHeaderThemeData = StreamGalleryHeaderTheme.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return AppBar(
|
||||
toolbarTextStyle: theme.textTheme.bodyText2,
|
||||
@@ -139,14 +146,14 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
|
||||
StreamChatTheme.of(context).galleryHeaderTheme;
|
||||
|
||||
final defaultModal = AttachmentActionsModal(
|
||||
attachment: attachment,
|
||||
message: message,
|
||||
currentIndex: currentIndex,
|
||||
onShowMessage: onShowMessage,
|
||||
);
|
||||
|
||||
final effectiveModal = attachmentActionsModalBuilder?.call(
|
||||
context,
|
||||
message.attachments[currentIndex],
|
||||
attachment,
|
||||
defaultModal,
|
||||
) ??
|
||||
defaultModal;
|
||||
|
||||
@@ -3,10 +3,16 @@ import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// {@macro gradient_avatar}
|
||||
@Deprecated("Use 'StreamGradientAvatar' instead")
|
||||
typedef GradientAvatar = StreamGradientAvatar;
|
||||
|
||||
/// {@template gradient_avatar}
|
||||
/// Fallback user avatar with a polygon gradient overlayed with text
|
||||
class GradientAvatar extends StatefulWidget {
|
||||
/// Constructor for [GradientAvatar]
|
||||
const GradientAvatar({
|
||||
/// {@endtemplate}
|
||||
class StreamGradientAvatar extends StatefulWidget {
|
||||
/// Constructor for [StreamGradientAvatar]
|
||||
const StreamGradientAvatar({
|
||||
Key? key,
|
||||
required this.name,
|
||||
required this.userId,
|
||||
@@ -19,10 +25,10 @@ class GradientAvatar extends StatefulWidget {
|
||||
final String userId;
|
||||
|
||||
@override
|
||||
_GradientAvatarState createState() => _GradientAvatarState();
|
||||
_StreamGradientAvatarState createState() => _StreamGradientAvatarState();
|
||||
}
|
||||
|
||||
class _GradientAvatarState extends State<GradientAvatar> {
|
||||
class _StreamGradientAvatarState extends State<StreamGradientAvatar> {
|
||||
@override
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: RepaintBoundary(
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro group_avatar}
|
||||
@Deprecated("Use 'StreamGroupAvatar' instead")
|
||||
typedef GroupAvatar = StreamGroupAvatar;
|
||||
|
||||
/// {@template group_avatar}
|
||||
/// Widget for constructing a group of images
|
||||
class GroupAvatar extends StatelessWidget {
|
||||
/// Constructor for creating a [GroupAvatar]
|
||||
const GroupAvatar({
|
||||
/// {@endtemplate}
|
||||
class StreamGroupAvatar extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamGroupAvatar]
|
||||
const StreamGroupAvatar({
|
||||
Key? key,
|
||||
this.channel,
|
||||
required this.members,
|
||||
this.constraints,
|
||||
this.onTap,
|
||||
@@ -15,6 +22,9 @@ class GroupAvatar extends StatelessWidget {
|
||||
this.selectionThickness = 4,
|
||||
}) : super(key: key);
|
||||
|
||||
/// The channel of the avatar
|
||||
final Channel? channel;
|
||||
|
||||
/// List of images to display
|
||||
final List<Member> members;
|
||||
|
||||
@@ -38,7 +48,7 @@ class GroupAvatar extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
final channel = this.channel ?? StreamChannel.of(context).channel;
|
||||
|
||||
assert(channel.state != null, 'Channel ${channel.id} is not initialized');
|
||||
|
||||
@@ -80,7 +90,7 @@ class GroupAvatar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
initialData: member,
|
||||
builder: (context, member) => UserAvatar(
|
||||
builder: (context, member) => StreamUserAvatar(
|
||||
showOnlineStatus: false,
|
||||
user: member.user!,
|
||||
borderRadius: BorderRadius.zero,
|
||||
@@ -118,7 +128,8 @@ class GroupAvatar extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
initialData: member,
|
||||
builder: (context, member) => UserAvatar(
|
||||
builder: (context, member) =>
|
||||
StreamUserAvatar(
|
||||
showOnlineStatus: false,
|
||||
user: member.user!,
|
||||
borderRadius: BorderRadius.zero,
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro image_group}
|
||||
@Deprecated("Use 'StreamImageGroup' instead")
|
||||
typedef ImageGroup = StreamImageGroup;
|
||||
|
||||
/// {@template image_group}
|
||||
/// Widget for constructing a group of images in message
|
||||
class ImageGroup extends StatelessWidget {
|
||||
/// Constructor for creating [ImageGroup] widget
|
||||
const ImageGroup({
|
||||
/// {@endtemplate}
|
||||
class StreamImageGroup extends StatelessWidget {
|
||||
/// Constructor for creating [StreamImageGroup] widget
|
||||
const StreamImageGroup({
|
||||
Key? key,
|
||||
required this.images,
|
||||
required this.message,
|
||||
@@ -27,8 +33,8 @@ class ImageGroup extends StatelessWidget {
|
||||
/// Message which images are attached to
|
||||
final Message message;
|
||||
|
||||
/// [MessageThemeData] to apply to message
|
||||
final MessageThemeData messageTheme;
|
||||
/// [StreamMessageThemeData] to apply to message
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// Size of iamges
|
||||
final Size size;
|
||||
@@ -129,11 +135,10 @@ class ImageGroup extends StatelessWidget {
|
||||
MaterialPageRoute(
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: FullScreenMedia(
|
||||
mediaAttachments: images,
|
||||
child: StreamFullScreenMedia(
|
||||
mediaAttachmentPackages: message.getAttachmentPackageList(),
|
||||
startIndex: index,
|
||||
userName: message.user?.name,
|
||||
message: message,
|
||||
onShowMessage: onShowMessage,
|
||||
),
|
||||
),
|
||||
@@ -142,7 +147,7 @@ class ImageGroup extends StatelessWidget {
|
||||
if (res != null) onReturnAction?.call(res);
|
||||
}
|
||||
|
||||
Widget _buildImage(BuildContext context, int index) => ImageAttachment(
|
||||
Widget _buildImage(BuildContext context, int index) => StreamImageAttachment(
|
||||
attachment: images[index],
|
||||
size: size,
|
||||
message: message,
|
||||
|
||||
@@ -2,10 +2,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
/// {@macro info_tile}
|
||||
@Deprecated("Use 'StreamInfoTile' instead")
|
||||
typedef InfoTile = StreamInfoTile;
|
||||
|
||||
/// {@template info_tile}
|
||||
/// Tile to display a message, used in stream chat to display connection status
|
||||
class InfoTile extends StatelessWidget {
|
||||
/// Constructor for creating an [InfoTile] widget
|
||||
const InfoTile({
|
||||
/// {@endtemplate}
|
||||
class StreamInfoTile extends StatelessWidget {
|
||||
/// Constructor for creating an [StreamInfoTile] widget
|
||||
const StreamInfoTile({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.child,
|
||||
@@ -40,11 +46,13 @@ class InfoTile extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return PortalEntry(
|
||||
return PortalTarget(
|
||||
visible: showMessage,
|
||||
portalAnchor: tileAnchor ?? Alignment.topCenter,
|
||||
childAnchor: childAnchor ?? Alignment.bottomCenter,
|
||||
portal: Container(
|
||||
anchor: Aligned(
|
||||
follower: tileAnchor ?? Alignment.topCenter,
|
||||
target: childAnchor ?? Alignment.bottomCenter,
|
||||
),
|
||||
portalFollower: Container(
|
||||
height: 25,
|
||||
color: backgroundColor ??
|
||||
chatThemeData.colorTheme.textLowEmphasis.withOpacity(0.9),
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import 'package:jiffy/jiffy.dart';
|
||||
import 'package:stream_chat_flutter/src/connection_status_builder.dart';
|
||||
import 'package:stream_chat_flutter/src/message_input.dart';
|
||||
import 'package:stream_chat_flutter/src/message_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/message_search_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/v4/message_input/stream_message_input.dart';
|
||||
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'
|
||||
show User;
|
||||
|
||||
@@ -59,7 +58,7 @@ abstract class Translations {
|
||||
/// The error shown when loading messages fails
|
||||
String get loadingMessagesError;
|
||||
|
||||
/// The text for showing the result count in [MessageSearchListView]
|
||||
/// The text for showing the result count in [StreamMessageSearchListView]
|
||||
String resultCountText(int count);
|
||||
|
||||
/// The text for showing the message is deleted
|
||||
@@ -74,44 +73,48 @@ abstract class Translations {
|
||||
/// The text for showing there are no chats
|
||||
String get emptyChatMessagesText;
|
||||
|
||||
/// The text for showing the thread separator in case [MessageListView]
|
||||
/// The text for showing the thread separator in case [StreamMessageListView]
|
||||
/// contains a parent message
|
||||
String threadSeparatorText(int replyCount);
|
||||
|
||||
/// The label for "connected" in [ConnectionStatusBuilder]
|
||||
/// The label for "connected" in [StreamConnectionStatusBuilder]
|
||||
String get connectedLabel;
|
||||
|
||||
/// The label for "disconnected" in [ConnectionStatusBuilder]
|
||||
/// The label for "disconnected" in [StreamConnectionStatusBuilder]
|
||||
String get disconnectedLabel;
|
||||
|
||||
/// The label for "reconnecting" in [ConnectionStatusBuilder]
|
||||
/// The label for "reconnecting" in [StreamConnectionStatusBuilder]
|
||||
String get reconnectingLabel;
|
||||
|
||||
/// The label for also send as direct message "checkbox"" in [MessageInput]
|
||||
/// The label for also send
|
||||
/// as direct message "checkbox"" in [StreamMessageInput]
|
||||
String get alsoSendAsDirectMessageLabel;
|
||||
|
||||
/// The label for search Gif
|
||||
String get searchGifLabel;
|
||||
|
||||
/// The label for the MessageInput hint when permission denied on sendMessage
|
||||
String get sendMessagePermissionError;
|
||||
|
||||
/// The label for add a comment or send in case of
|
||||
/// attachments inside [MessageInput]
|
||||
/// attachments inside [StreamMessageInput]
|
||||
String get addACommentOrSendLabel;
|
||||
|
||||
/// The label for write a message in [MessageInput]
|
||||
/// The label for write a message in [StreamMessageInput]
|
||||
String get writeAMessageLabel;
|
||||
|
||||
/// The label for slow mode enabled in [MessageInput]
|
||||
/// The label for slow mode enabled in [StreamMessageInput]
|
||||
String get slowModeOnLabel;
|
||||
|
||||
/// The label for instant commands in [MessageInput]
|
||||
/// The label for instant commands in [StreamMessageInput]
|
||||
String get instantCommandsLabel;
|
||||
|
||||
/// The error shown in case the fi"le is too large even after compression
|
||||
/// while uploading via [MessageInput]
|
||||
/// while uploading via [StreamMessageInput]
|
||||
String fileTooLargeAfterCompressionError(double limitInMB);
|
||||
|
||||
/// The error shown in case the file is too large
|
||||
/// while uploading via [MessageInput]
|
||||
/// while uploading via [StreamMessageInput]
|
||||
String fileTooLargeError(double limitInMB);
|
||||
|
||||
/// The text for showing the query while searching for emojis
|
||||
@@ -141,6 +144,12 @@ abstract class Translations {
|
||||
/// The label for "OK"
|
||||
String get okLabel;
|
||||
|
||||
/// The label for a link disabled error
|
||||
String get linkDisabledError;
|
||||
|
||||
/// The additional info on a link disabled error
|
||||
String get linkDisabledDetails;
|
||||
|
||||
/// The label for "add more files"
|
||||
String get addMoreFilesLabel;
|
||||
|
||||
@@ -380,6 +389,10 @@ class DefaultTranslations implements Translations {
|
||||
return 'Pinned by ${pinnedBy.name}';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sendMessagePermissionError =>
|
||||
'You don\'t have permission to send messages';
|
||||
|
||||
@override
|
||||
String get emptyMessagesText => 'There are no messages currently';
|
||||
|
||||
@@ -691,4 +704,11 @@ class DefaultTranslations implements Translations {
|
||||
@override
|
||||
String attachmentLimitExceedError(int limit) => """
|
||||
Attachment limit exceeded: it's not possible to add more than $limit attachments""";
|
||||
|
||||
@override
|
||||
String get linkDisabledDetails =>
|
||||
'Sending links is not allowed in this conversation.';
|
||||
|
||||
@override
|
||||
String get linkDisabledError => 'Links are disabled';
|
||||
}
|
||||
|
||||
@@ -8,21 +8,16 @@ import 'package:photo_manager/photo_manager.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view_controller.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
extension on Duration {
|
||||
String format() {
|
||||
final s = '$this'.split('.')[0].padLeft(8, '0');
|
||||
if (s.startsWith('00:')) {
|
||||
return s.replaceFirst('00:', '');
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
/// {@macro media_list_view}
|
||||
@Deprecated("Use 'StreamMediaListView' instead")
|
||||
typedef MediaListView = StreamMediaListView;
|
||||
|
||||
/// {@template media_list_view}
|
||||
/// Constructs a list of media
|
||||
class MediaListView extends StatefulWidget {
|
||||
/// Constructor for creating a [MediaListView] widget
|
||||
const MediaListView({
|
||||
/// {@endtemplate}
|
||||
class StreamMediaListView extends StatefulWidget {
|
||||
/// Constructor for creating a [StreamMediaListView] widget
|
||||
const StreamMediaListView({
|
||||
Key? key,
|
||||
this.selectedIds = const [],
|
||||
this.onSelect,
|
||||
@@ -39,10 +34,10 @@ class MediaListView extends StatefulWidget {
|
||||
final MediaListViewController? controller;
|
||||
|
||||
@override
|
||||
_MediaListViewState createState() => _MediaListViewState();
|
||||
_StreamMediaListViewState createState() => _StreamMediaListViewState();
|
||||
}
|
||||
|
||||
class _MediaListViewState extends State<MediaListView> {
|
||||
class _StreamMediaListViewState extends State<StreamMediaListView> {
|
||||
var _media = <AssetEntity>[];
|
||||
var _currentPage = 0;
|
||||
final _scrollController = ScrollController();
|
||||
@@ -252,3 +247,14 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
|
||||
@override
|
||||
String toString() => '$runtimeType("${media.id}")';
|
||||
}
|
||||
|
||||
extension on Duration {
|
||||
String format() {
|
||||
final s = '$this'.split('.')[0].padLeft(8, '0');
|
||||
if (s.startsWith('00:')) {
|
||||
return s.replaceFirst('00:', '');
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// This widget is used for showing user tiles for mentions
|
||||
/// Use [title], [subtitle], [leading], [trailing] for
|
||||
/// substituting widgets in respective positions
|
||||
@Deprecated('Use `UserMentionTile` instead. Will be removed in future release')
|
||||
class MentionTile extends StatelessWidget {
|
||||
/// Constructor for creating a [MentionTile] widget
|
||||
const MentionTile(
|
||||
this.member, {
|
||||
Key? key,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
}) : super(key: key);
|
||||
|
||||
/// Member to display in the tile
|
||||
final Member member;
|
||||
|
||||
/// Widget to display as title
|
||||
final Widget? title;
|
||||
|
||||
/// Widget to display below [title]
|
||||
final Widget? subtitle;
|
||||
|
||||
/// Widget at the start of the tile
|
||||
final Widget? leading;
|
||||
|
||||
/// Widget at the end of tile
|
||||
final Widget? trailing;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final chatThemeData = StreamChatTheme.of(context);
|
||||
return SizedBox(
|
||||
height: 56,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
leading ??
|
||||
UserAvatar(
|
||||
constraints: BoxConstraints.tight(
|
||||
const Size(
|
||||
40,
|
||||
40,
|
||||
),
|
||||
),
|
||||
user: member.user!,
|
||||
),
|
||||
const SizedBox(
|
||||
width: 8,
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title ??
|
||||
Text(
|
||||
member.user!.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: chatThemeData.textTheme.bodyBold,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 2,
|
||||
),
|
||||
subtitle ??
|
||||
Text(
|
||||
'@${member.userId}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: chatThemeData.textTheme.footnoteBold.copyWith(
|
||||
color: chatThemeData.colorTheme.textLowEmphasis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing ??
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: 18,
|
||||
left: 8,
|
||||
),
|
||||
child: StreamSvgIcon.mentions(
|
||||
color: chatThemeData.colorTheme.accentPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,16 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro message_action}
|
||||
@Deprecated("Use 'StreamMessageActions' instead")
|
||||
typedef MessageAction = StreamMessageAction;
|
||||
|
||||
/// {@template message_action}
|
||||
/// Class describing a message action
|
||||
class MessageAction {
|
||||
/// returns a new instance of a [MessageAction]
|
||||
MessageAction({
|
||||
/// {@endtemplate}
|
||||
class StreamMessageAction {
|
||||
/// returns a new instance of a [StreamMessageAction]
|
||||
StreamMessageAction({
|
||||
this.leading,
|
||||
this.title,
|
||||
this.onTap,
|
||||
|
||||
@@ -3,25 +3,31 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro message_actions_modal}
|
||||
@Deprecated("Use 'StreamMessageActionsModal' instead")
|
||||
typedef MessageActionsModal = StreamMessageActionsModal;
|
||||
|
||||
/// {@template message_actions_modal}
|
||||
/// Constructs a modal with actions for a message
|
||||
class MessageActionsModal extends StatefulWidget {
|
||||
/// Constructor for creating a [MessageActionsModal] widget
|
||||
const MessageActionsModal({
|
||||
/// {@endtemplate}
|
||||
class StreamMessageActionsModal extends StatefulWidget {
|
||||
/// Constructor for creating a [StreamMessageActionsModal] widget
|
||||
const StreamMessageActionsModal({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.messageWidget,
|
||||
required this.messageTheme,
|
||||
this.showReactions = true,
|
||||
this.showDeleteMessage = true,
|
||||
this.showEditMessage = true,
|
||||
this.showReactions,
|
||||
this.showDeleteMessage,
|
||||
this.showEditMessage,
|
||||
this.onReplyTap,
|
||||
this.onThreadReplyTap,
|
||||
this.showCopyMessage = true,
|
||||
this.showReplyMessage = true,
|
||||
this.showResendMessage = true,
|
||||
this.showThreadReplyMessage = true,
|
||||
this.showFlagButton = true,
|
||||
this.showPinButton = true,
|
||||
this.showThreadReplyMessage,
|
||||
this.showFlagButton,
|
||||
this.showPinButton,
|
||||
this.editMessageInputBuilder,
|
||||
this.reverse = false,
|
||||
this.customActions = const [],
|
||||
@@ -43,51 +49,54 @@ class MessageActionsModal extends StatefulWidget {
|
||||
/// Message in focus for actions
|
||||
final Message message;
|
||||
|
||||
/// [MessageThemeData] for message
|
||||
final MessageThemeData messageTheme;
|
||||
/// [StreamMessageThemeData] for message
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// Flag for showing reactions
|
||||
final bool showReactions;
|
||||
final bool? showReactions;
|
||||
|
||||
/// Callback when copy is tapped
|
||||
final OnMessageTap? onCopyTap;
|
||||
|
||||
/// Callback when delete is tapped
|
||||
final bool showDeleteMessage;
|
||||
final bool? showDeleteMessage;
|
||||
|
||||
/// Flag for showing copy action
|
||||
final bool showCopyMessage;
|
||||
|
||||
/// Flag for showing edit action
|
||||
final bool showEditMessage;
|
||||
final bool? showEditMessage;
|
||||
|
||||
/// Flag for showing resend action
|
||||
final bool showResendMessage;
|
||||
|
||||
/// Flag for showing reply action
|
||||
final bool showReplyMessage;
|
||||
final bool? showReplyMessage;
|
||||
|
||||
/// Flag for showing thread reply action
|
||||
final bool showThreadReplyMessage;
|
||||
final bool? showThreadReplyMessage;
|
||||
|
||||
/// Flag for showing flag action
|
||||
final bool showFlagButton;
|
||||
final bool? showFlagButton;
|
||||
|
||||
/// Flag for showing pin action
|
||||
final bool showPinButton;
|
||||
final bool? showPinButton;
|
||||
|
||||
/// Flag for reversing message
|
||||
final bool reverse;
|
||||
|
||||
/// List of custom actions
|
||||
final List<MessageAction> customActions;
|
||||
final List<StreamMessageAction> customActions;
|
||||
|
||||
@override
|
||||
_MessageActionsModalState createState() => _MessageActionsModalState();
|
||||
_StreamMessageActionsModalState createState() =>
|
||||
_StreamMessageActionsModalState();
|
||||
}
|
||||
|
||||
class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
class _StreamMessageActionsModalState extends State<StreamMessageActionsModal> {
|
||||
bool _showActions = true;
|
||||
late List<String> _userPermissions;
|
||||
late bool _isMyMessage;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _showMessageOptionsModal();
|
||||
@@ -122,6 +131,19 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
final shiftFactor =
|
||||
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0;
|
||||
|
||||
final hasEditPermission = _userPermissions.contains(
|
||||
PermissionType.updateAnyMessage,
|
||||
) ||
|
||||
_userPermissions.contains(PermissionType.updateOwnMessage);
|
||||
|
||||
final hasDeletePermission = _userPermissions.contains(
|
||||
PermissionType.deleteAnyMessage,
|
||||
) ||
|
||||
_userPermissions.contains(PermissionType.deleteOwnMessage);
|
||||
|
||||
final hasReactionPermission =
|
||||
_userPermissions.contains(PermissionType.sendReaction);
|
||||
|
||||
final child = Center(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
@@ -131,7 +153,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
? CrossAxisAlignment.end
|
||||
: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
if (widget.showReactions &&
|
||||
if ((widget.showReactions ?? hasReactionPermission) &&
|
||||
(widget.message.status == MessageSendingStatus.sent))
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
@@ -144,7 +166,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
: -(1.2 - divFactor)),
|
||||
0,
|
||||
),
|
||||
child: ReactionPicker(
|
||||
child: StreamReactionPicker(
|
||||
message: widget.message,
|
||||
),
|
||||
),
|
||||
@@ -168,21 +190,35 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (widget.showReplyMessage &&
|
||||
widget.message.status == MessageSendingStatus.sent)
|
||||
if (widget.showReplyMessage ??
|
||||
(_userPermissions
|
||||
.contains(PermissionType.quoteMessage) &&
|
||||
widget.message.status ==
|
||||
MessageSendingStatus.sent))
|
||||
_buildReplyButton(context),
|
||||
if (widget.showThreadReplyMessage &&
|
||||
(widget.message.status ==
|
||||
MessageSendingStatus.sent) &&
|
||||
widget.message.parentId == null)
|
||||
if (widget.showThreadReplyMessage ??
|
||||
_userPermissions
|
||||
.contains(PermissionType.sendReply) &&
|
||||
(widget.message.status ==
|
||||
MessageSendingStatus.sent) &&
|
||||
widget.message.parentId == null)
|
||||
_buildThreadReplyButton(context),
|
||||
if (widget.showResendMessage)
|
||||
_buildResendMessage(context),
|
||||
if (widget.showEditMessage) _buildEditMessage(context),
|
||||
if (widget.showEditMessage ??
|
||||
_isMyMessage && hasEditPermission)
|
||||
_buildEditMessage(context),
|
||||
if (widget.showCopyMessage) _buildCopyButton(context),
|
||||
if (widget.showFlagButton) _buildFlagButton(context),
|
||||
if (widget.showPinButton) _buildPinButton(context),
|
||||
if (widget.showDeleteMessage)
|
||||
if (widget.showFlagButton ??
|
||||
_userPermissions
|
||||
.contains(PermissionType.flagMessage))
|
||||
_buildFlagButton(context),
|
||||
if (widget.showPinButton ??
|
||||
_userPermissions
|
||||
.contains(PermissionType.pinMessage))
|
||||
_buildPinButton(context),
|
||||
if (widget.showDeleteMessage ??
|
||||
(_isMyMessage && hasDeletePermission))
|
||||
_buildDeleteButton(context),
|
||||
...widget.customActions
|
||||
.map((action) => _buildCustomAction(
|
||||
@@ -239,7 +275,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
|
||||
InkWell _buildCustomAction(
|
||||
BuildContext context,
|
||||
MessageAction messageAction,
|
||||
StreamMessageAction messageAction,
|
||||
) =>
|
||||
InkWell(
|
||||
onTap: () {
|
||||
@@ -560,7 +596,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
elevation: 2,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: MessageInputTheme.of(context).inputBackgroundColor,
|
||||
backgroundColor: StreamMessageInputTheme.of(context).inputBackgroundColor,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(16),
|
||||
@@ -602,8 +638,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
if (widget.editMessageInputBuilder != null)
|
||||
widget.editMessageInputBuilder!(context, widget.message)
|
||||
else
|
||||
MessageInput(
|
||||
editMessage: widget.message,
|
||||
StreamMessageInput(
|
||||
messageInputController: StreamMessageInputController(
|
||||
message: widget.message,
|
||||
),
|
||||
preMessageSending: (m) {
|
||||
FocusScope.of(context).unfocus();
|
||||
Navigator.pop(context);
|
||||
@@ -643,4 +681,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
final newStreamChannel = StreamChannel.of(context);
|
||||
_userPermissions = newStreamChannel.channel.ownCapabilities;
|
||||
_isMyMessage =
|
||||
widget.message.user?.id == StreamChat.of(context).currentUser?.id;
|
||||
super.didChangeDependencies();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// ignore_for_file: deprecated_member_use_from_same_package
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
@@ -16,15 +18,10 @@ import 'package:stream_chat_flutter/src/emoji_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view.dart';
|
||||
import 'package:stream_chat_flutter/src/media_list_view_controller.dart';
|
||||
import 'package:stream_chat_flutter/src/multi_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/quoted_message_widget.dart';
|
||||
import 'package:stream_chat_flutter/src/user_mentions_overlay.dart';
|
||||
import 'package:stream_chat_flutter/src/video_service.dart';
|
||||
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
import 'package:video_compress/video_compress.dart';
|
||||
|
||||
export 'package:video_compress/video_compress.dart' show VideoQuality;
|
||||
|
||||
/// A callback that can be passed to [MessageInput.onError].
|
||||
///
|
||||
@@ -60,7 +57,7 @@ typedef MentionTileBuilder = Widget Function(
|
||||
|
||||
/// Builder function for building a user mention tile.
|
||||
///
|
||||
/// Use [UserMentionTile] for the default implementation.
|
||||
/// Use [StreamUserMentionTile] for the default implementation.
|
||||
typedef UserMentionTileBuilder = Widget Function(
|
||||
BuildContext context,
|
||||
User user,
|
||||
@@ -156,12 +153,13 @@ const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You usually put this widget in the same page of a [MessageListView]
|
||||
/// You usually put this widget in the same page of a [StreamMessageListView]
|
||||
/// as the bottom widget.
|
||||
///
|
||||
/// The widget renders the ui based on the first ancestor of
|
||||
/// type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
@Deprecated("Use 'StreamMessageInput' instead")
|
||||
class MessageInput extends StatefulWidget {
|
||||
/// Instantiate a new MessageInput
|
||||
const MessageInput({
|
||||
@@ -191,8 +189,6 @@ class MessageInput extends StatefulWidget {
|
||||
this.mentionsTileBuilder,
|
||||
this.userMentionsTileBuilder,
|
||||
this.maxAttachmentSize = _kDefaultMaxAttachmentSize,
|
||||
this.compressedVideoQuality = VideoQuality.DefaultQuality,
|
||||
this.compressedVideoFrameRate = 30,
|
||||
this.onError,
|
||||
this.attachmentLimit = 10,
|
||||
this.onAttachmentLimitExceed,
|
||||
@@ -213,12 +209,6 @@ class MessageInput extends StatefulWidget {
|
||||
/// Message to edit
|
||||
final Message? editMessage;
|
||||
|
||||
/// Video quality to use when compressing the videos
|
||||
final VideoQuality compressedVideoQuality;
|
||||
|
||||
/// Frame rate to use when compressing the videos
|
||||
final int compressedVideoFrameRate;
|
||||
|
||||
/// Max attachment size in bytes
|
||||
/// Defaults to 20 MB
|
||||
/// do not set it if you're using our default CDN
|
||||
@@ -339,6 +329,7 @@ class MessageInput extends StatefulWidget {
|
||||
}
|
||||
|
||||
/// State of [MessageInput]
|
||||
@Deprecated("Use 'StreamMessageInput' instead")
|
||||
class MessageInputState extends State<MessageInput> {
|
||||
final _attachments = <String, Attachment>{};
|
||||
final List<User> _mentionedUsers = [];
|
||||
@@ -363,7 +354,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
widget.textEditingController ?? TextEditingController();
|
||||
|
||||
late StreamChatThemeData _streamChatTheme;
|
||||
late MessageInputThemeData _messageInputTheme;
|
||||
late StreamMessageInputThemeData _messageInputTheme;
|
||||
|
||||
bool get _hasQuotedMessage => widget.quotedMessage != null;
|
||||
|
||||
@@ -487,7 +478,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
}
|
||||
|
||||
return MultiOverlay(
|
||||
return StreamMultiOverlay(
|
||||
childAnchor: Alignment.topCenter,
|
||||
overlayAnchor: Alignment.bottomCenter,
|
||||
overlayOptions: [
|
||||
@@ -932,7 +923,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (renderObject == null) {
|
||||
return const Offstage();
|
||||
}
|
||||
return CommandsOverlay(
|
||||
return StreamCommandsOverlay(
|
||||
channel: StreamChannel.of(context).channel,
|
||||
size: Size(renderObject.size.width - 16, 400),
|
||||
text: text,
|
||||
@@ -1142,41 +1133,18 @@ class MessageInputState extends State<MessageInput> {
|
||||
|
||||
if (mediaFile == null) return;
|
||||
|
||||
var file = AttachmentFile(
|
||||
final file = AttachmentFile(
|
||||
path: mediaFile.path,
|
||||
size: await mediaFile.length(),
|
||||
bytes: mediaFile.readAsBytesSync(),
|
||||
);
|
||||
|
||||
if (file.size! > widget.maxAttachmentSize) {
|
||||
if (medium.type == AssetType.video && file.path != null) {
|
||||
final mediaInfo = await VideoService.compressVideo(
|
||||
file.path!,
|
||||
frameRate: widget.compressedVideoFrameRate,
|
||||
quality: widget.compressedVideoQuality,
|
||||
);
|
||||
|
||||
if (mediaInfo == null ||
|
||||
mediaInfo.filesize! > widget.maxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
context.translations.fileTooLargeAfterCompressionError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file?.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
_showErrorAlert(context.translations.fileTooLargeError(
|
||||
return _showErrorAlert(
|
||||
context.translations.fileTooLargeError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
));
|
||||
return;
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
@@ -1218,7 +1186,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, snapshot) => UserMentionsOverlay(
|
||||
builder: (context, snapshot) => StreamUserMentionsOverlay(
|
||||
query: query,
|
||||
mentionAllAppUsers: widget.mentionAllAppUsers,
|
||||
client: StreamChat.of(context).client,
|
||||
@@ -1263,7 +1231,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
final renderObject = context.findRenderObject() as RenderBox;
|
||||
|
||||
return EmojiOverlay(
|
||||
return StreamEmojiOverlay(
|
||||
size: Size(renderObject.size.width - 16, 200),
|
||||
query: query,
|
||||
onEmojiResult: (emoji) {
|
||||
@@ -1298,7 +1266,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
if (!_hasQuotedMessage) return const Offstage();
|
||||
final containsUrl = widget.quotedMessage!.attachments
|
||||
.any((element) => element.ogScrapeUrl != null);
|
||||
return QuotedMessageWidget(
|
||||
return StreamQuotedMessageWidget(
|
||||
reverse: true,
|
||||
showBorder: !containsUrl,
|
||||
message: widget.quotedMessage!,
|
||||
@@ -1329,10 +1297,8 @@ class MessageInputState extends State<MessageInput> {
|
||||
.map<Widget>(
|
||||
(e) => ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: FileAttachment(
|
||||
message: Message(
|
||||
status: MessageSendingStatus.sending,
|
||||
), // dummy message
|
||||
child: StreamFileAttachment(
|
||||
message: Message(), // dummy message
|
||||
attachment: e,
|
||||
size: Size(
|
||||
MediaQuery.of(context).size.width * 0.65,
|
||||
@@ -1453,7 +1419,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
case 'video':
|
||||
return Stack(
|
||||
children: [
|
||||
VideoThumbnailImage(
|
||||
StreamVideoThumbnailImage(
|
||||
height: 104,
|
||||
width: 104,
|
||||
video: (attachment.file?.path ?? attachment.assetUrl)!,
|
||||
@@ -1703,33 +1669,11 @@ class MessageInputState extends State<MessageInput> {
|
||||
);
|
||||
|
||||
if (file.size! > widget.maxAttachmentSize) {
|
||||
if (attachmentType == 'video' && file.path != null) {
|
||||
final mediaInfo = await (VideoService.compressVideo(
|
||||
file.path!,
|
||||
frameRate: widget.compressedVideoFrameRate,
|
||||
quality: widget.compressedVideoQuality,
|
||||
) as FutureOr<MediaInfo>);
|
||||
|
||||
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
|
||||
_showErrorAlert(
|
||||
context.translations.fileTooLargeAfterCompressionError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
file = AttachmentFile(
|
||||
name: file.name,
|
||||
size: mediaInfo.filesize,
|
||||
bytes: await mediaInfo.file!.readAsBytes(),
|
||||
path: mediaInfo.path,
|
||||
);
|
||||
} else {
|
||||
_showErrorAlert(context.translations.fileTooLargeError(
|
||||
return _showErrorAlert(
|
||||
context.translations.fileTooLargeError(
|
||||
widget.maxAttachmentSize / (1024 * 1024),
|
||||
));
|
||||
return;
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
setState(() {
|
||||
@@ -1968,7 +1912,7 @@ class MessageInputState extends State<MessageInput> {
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
_streamChatTheme = StreamChatTheme.of(context);
|
||||
_messageInputTheme = MessageInputTheme.of(context);
|
||||
_messageInputTheme = StreamMessageInputTheme.of(context);
|
||||
if (widget.editMessage == null) _startSlowMode();
|
||||
|
||||
if ((widget.editMessage != null || widget.initialMessage != null) &&
|
||||
@@ -2047,7 +1991,7 @@ class _PickerWidgetState extends State<_PickerWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
return MediaListView(
|
||||
return StreamMediaListView(
|
||||
controller: widget.mediaListViewController,
|
||||
selectedIds: widget.selectedMedias,
|
||||
onSelect: widget.onMediaSelected,
|
||||
|
||||
@@ -10,22 +10,22 @@ import 'package:stream_chat_flutter/src/swipeable.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// Widget builder for message
|
||||
/// [defaultMessageWidget] is the default [MessageWidget] configuration
|
||||
/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration
|
||||
/// Use [defaultMessageWidget.copyWith] to easily customize it
|
||||
typedef MessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
MessageDetails,
|
||||
List<Message>,
|
||||
MessageWidget defaultMessageWidget,
|
||||
StreamMessageWidget defaultMessageWidget,
|
||||
);
|
||||
|
||||
/// Widget builder for parent message
|
||||
/// [defaultMessageWidget] is the default [MessageWidget] configuration
|
||||
/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration
|
||||
/// Use [defaultMessageWidget.copyWith] to easily customize it
|
||||
typedef ParentMessageBuilder = Widget Function(
|
||||
BuildContext,
|
||||
Message?,
|
||||
MessageWidget defaultMessageWidget,
|
||||
StreamMessageWidget defaultMessageWidget,
|
||||
);
|
||||
|
||||
/// Widget builder for system message
|
||||
@@ -122,6 +122,11 @@ class MessageDetails {
|
||||
final int index;
|
||||
}
|
||||
|
||||
/// {@macro message_list_view}
|
||||
@Deprecated("Use 'StreamMessageListView' instead")
|
||||
typedef MessageListView = StreamMessageListView;
|
||||
|
||||
/// {@template message_list_view}
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
@@ -130,29 +135,25 @@ class MessageDetails {
|
||||
/// ```dart
|
||||
/// class ChannelPage extends StatelessWidget {
|
||||
/// const ChannelPage({
|
||||
/// Key key,
|
||||
/// Key? key,
|
||||
/// }) : super(key: key);
|
||||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return Scaffold(
|
||||
/// appBar: ChannelHeader(),
|
||||
/// body: Column(
|
||||
/// children: <Widget>[
|
||||
/// Expanded(
|
||||
/// child: MessageListView(
|
||||
/// threadBuilder: (_, parentMessage) {
|
||||
/// return ThreadPage(
|
||||
/// Widget build(BuildContext context) => Scaffold(
|
||||
/// appBar: const StreamChannelHeader(),
|
||||
/// body: Column(
|
||||
/// children: <Widget>[
|
||||
/// Expanded(
|
||||
/// child: StreamMessageListView(
|
||||
/// threadBuilder: (_, parentMessage) => ThreadPage(
|
||||
/// parent: parentMessage,
|
||||
/// );
|
||||
/// },
|
||||
/// ),
|
||||
/// ),
|
||||
/// ),
|
||||
/// ),
|
||||
/// MessageInput(),
|
||||
/// ],
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// const StreamMessageInput(),
|
||||
/// ],
|
||||
/// ),
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
@@ -164,11 +165,13 @@ class MessageDetails {
|
||||
/// The widget components render the ui based on the first
|
||||
/// ancestor of type [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class MessageListView extends StatefulWidget {
|
||||
/// Instantiate a new MessageListView
|
||||
const MessageListView({
|
||||
/// {@endtemplate}
|
||||
class StreamMessageListView extends StatefulWidget {
|
||||
/// Instantiate a new StreamMessageListView.
|
||||
const StreamMessageListView({
|
||||
Key? key,
|
||||
this.showScrollToBottom = true,
|
||||
this.scrollToBottomBuilder,
|
||||
this.messageBuilder,
|
||||
this.parentMessageBuilder,
|
||||
this.parentMessage,
|
||||
@@ -195,7 +198,6 @@ class MessageListView extends StatefulWidget {
|
||||
this.messageFilter,
|
||||
this.onMessageTap,
|
||||
this.onSystemMessageTap,
|
||||
this.pinPermissions = const [],
|
||||
this.showFloatingDateDivider = true,
|
||||
this.threadSeparatorBuilder,
|
||||
this.messageListController,
|
||||
@@ -241,6 +243,25 @@ class MessageListView extends StatefulWidget {
|
||||
/// messages and the scroll offset is not zero
|
||||
final bool showScrollToBottom;
|
||||
|
||||
/// Function used to build a custom scroll to bottom widget
|
||||
///
|
||||
/// Provides the current unread messages count and a reference
|
||||
/// to the function that is executed on tap of this widget by default
|
||||
///
|
||||
/// As an example:
|
||||
/// MessageListView(
|
||||
/// scrollToBottomBuilder: (unreadCount, defaultTapAction) {
|
||||
/// return InkWell(
|
||||
/// onTap: () => defaultTapAction(unreadCount),
|
||||
/// child: Text('Scroll To Bottom'),
|
||||
/// );
|
||||
/// },
|
||||
/// ),
|
||||
final Widget Function(
|
||||
int unreadCount,
|
||||
Future<void> Function(int) scrollToBottomDefaultTapAction,
|
||||
)? scrollToBottomBuilder;
|
||||
|
||||
/// Parent message in case of a thread
|
||||
final Message? parentMessage;
|
||||
|
||||
@@ -313,9 +334,6 @@ class MessageListView extends StatefulWidget {
|
||||
/// Called when system message is tapped
|
||||
final OnMessageTap? onSystemMessageTap;
|
||||
|
||||
/// 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;
|
||||
|
||||
@@ -333,10 +351,10 @@ class MessageListView extends StatefulWidget {
|
||||
final SpacingWidgetBuilder? spacingWidgetBuilder;
|
||||
|
||||
@override
|
||||
_MessageListViewState createState() => _MessageListViewState();
|
||||
_StreamMessageListViewState createState() => _StreamMessageListViewState();
|
||||
}
|
||||
|
||||
class _MessageListViewState extends State<MessageListView> {
|
||||
class _StreamMessageListViewState extends State<StreamMessageListView> {
|
||||
ItemScrollController? _scrollController;
|
||||
void Function(Message)? _onThreadTap;
|
||||
final ValueNotifier<bool> _showScrollToBottom = ValueNotifier(false);
|
||||
@@ -344,6 +362,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
int? _messageListLength;
|
||||
StreamChannelState? streamChannel;
|
||||
late StreamChatThemeData _streamTheme;
|
||||
late List<String> _userPermissions;
|
||||
|
||||
int get _initialIndex {
|
||||
final initialScrollIndex = widget.initialScrollIndex;
|
||||
@@ -463,7 +482,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final child = Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ConnectionStatusBuilder(
|
||||
StreamConnectionStatusBuilder(
|
||||
statusBuilder: (context, status) {
|
||||
var statusString = '';
|
||||
var showStatus = true;
|
||||
@@ -480,7 +499,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
break;
|
||||
}
|
||||
|
||||
return InfoTile(
|
||||
return StreamInfoTile(
|
||||
showMessage: widget.showConnectionStateTile && showStatus,
|
||||
tileAnchor: Alignment.topCenter,
|
||||
childAnchor: Alignment.topCenter,
|
||||
@@ -726,8 +745,10 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
],
|
||||
);
|
||||
|
||||
final backgroundColor = MessageListViewTheme.of(context).backgroundColor;
|
||||
final backgroundImage = MessageListViewTheme.of(context).backgroundImage;
|
||||
final backgroundColor =
|
||||
StreamMessageListViewTheme.of(context).backgroundColor;
|
||||
final backgroundImage =
|
||||
StreamMessageListViewTheme.of(context).backgroundImage;
|
||||
|
||||
if (backgroundColor != null || backgroundImage != null) {
|
||||
return Container(
|
||||
@@ -749,7 +770,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: DateDivider(
|
||||
child: StreamDateDivider(
|
||||
dateTime: message.createdAt.toLocal(),
|
||||
),
|
||||
);
|
||||
@@ -771,7 +792,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
child: Text(
|
||||
context.translations.threadSeparatorText(replyCount),
|
||||
textAlign: TextAlign.center,
|
||||
style: ChannelHeaderTheme.of(context).subtitleStyle,
|
||||
style: StreamChannelHeaderTheme.of(context).subtitleStyle,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -829,7 +850,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final message = messages[index - 2];
|
||||
return widget.dateDividerBuilder != null
|
||||
? widget.dateDividerBuilder!(message.createdAt.toLocal())
|
||||
: DateDivider(dateTime: message.createdAt.toLocal());
|
||||
: StreamDateDivider(dateTime: message.createdAt.toLocal());
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -858,6 +879,28 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
.index;
|
||||
}
|
||||
|
||||
Future<void> scrollToBottomDefaultTapAction(int unreadCount) async {
|
||||
if (unreadCount > 0) {
|
||||
streamChannel!.channel.markRead();
|
||||
}
|
||||
if (!_upToDate) {
|
||||
_bottomPaginationActive = false;
|
||||
initialAlignment = 0;
|
||||
initialIndex = 0;
|
||||
await streamChannel!.reloadChannel();
|
||||
|
||||
WidgetsBinding.instance?.addPostFrameCallback((_) {
|
||||
_scrollController!.jumpTo(index: 0);
|
||||
});
|
||||
} else {
|
||||
_scrollController!.scrollTo(
|
||||
index: 0,
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildScrollToBottom() => StreamBuilder<int>(
|
||||
stream: streamChannel!.channel.state!.unreadCountStream,
|
||||
builder: (_, snapshot) {
|
||||
@@ -867,6 +910,12 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
return const Offstage();
|
||||
}
|
||||
final unreadCount = snapshot.data!;
|
||||
if (widget.scrollToBottomBuilder != null) {
|
||||
return widget.scrollToBottomBuilder!(
|
||||
unreadCount,
|
||||
scrollToBottomDefaultTapAction,
|
||||
);
|
||||
}
|
||||
final showUnreadCount = unreadCount > 0 &&
|
||||
streamChannel!.channel.state!.members.any((e) =>
|
||||
e.userId ==
|
||||
@@ -881,27 +930,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
backgroundColor: _streamTheme.colorTheme.barsBg,
|
||||
onPressed: () async {
|
||||
if (unreadCount > 0) {
|
||||
streamChannel!.channel.markRead();
|
||||
}
|
||||
if (!_upToDate) {
|
||||
_bottomPaginationActive = false;
|
||||
initialAlignment = 0;
|
||||
initialIndex = 0;
|
||||
await streamChannel!.reloadChannel();
|
||||
|
||||
WidgetsBinding.instance?.addPostFrameCallback((_) {
|
||||
_scrollController!.jumpTo(index: 0);
|
||||
});
|
||||
} else {
|
||||
_scrollController!.scrollTo(
|
||||
index: 0,
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
},
|
||||
onPressed: () => scrollToBottomDefaultTapAction(unreadCount),
|
||||
child: widget.reverse
|
||||
? StreamSvgIcon.down(
|
||||
color: _streamTheme.colorTheme.textHighEmphasis,
|
||||
@@ -970,7 +999,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final currentUserMember =
|
||||
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
||||
|
||||
final defaultMessageWidget = MessageWidget(
|
||||
final defaultMessageWidget = StreamMessageWidget(
|
||||
showReplyMessage: false,
|
||||
showResendMessage: false,
|
||||
showThreadReplyMessage: false,
|
||||
@@ -1016,7 +1045,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
showPinButton: currentUserMember != null &&
|
||||
widget.pinPermissions.contains(currentUserMember.role),
|
||||
_userPermissions.contains(PermissionType.pinMessage),
|
||||
);
|
||||
|
||||
if (widget.parentMessageBuilder != null) {
|
||||
@@ -1034,7 +1063,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
if ((message.type == 'system' || message.type == 'error') &&
|
||||
message.text?.isNotEmpty == true) {
|
||||
return widget.systemMessageBuilder?.call(context, message) ??
|
||||
SystemMessage(
|
||||
StreamSystemMessage(
|
||||
message: message,
|
||||
onMessageTap: (message) {
|
||||
if (widget.onSystemMessageTap != null) {
|
||||
@@ -1104,7 +1133,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
final currentUserMember =
|
||||
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
|
||||
|
||||
Widget messageWidget = MessageWidget(
|
||||
Widget messageWidget = StreamMessageWidget(
|
||||
message: message,
|
||||
reverse: isMyMessage,
|
||||
showReactions: !message.isDeleted,
|
||||
@@ -1135,7 +1164,10 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
},
|
||||
showEditMessage: isMyMessage,
|
||||
showDeleteMessage: isMyMessage,
|
||||
showThreadReplyMessage: !isThreadMessage,
|
||||
showThreadReplyMessage: !isThreadMessage &&
|
||||
streamChannel?.channel.ownCapabilities
|
||||
.contains(PermissionType.sendReply) ==
|
||||
true,
|
||||
showFlagButton: !isMyMessage,
|
||||
borderSide: borderSide,
|
||||
onThreadTap: _onThreadTap,
|
||||
@@ -1204,7 +1236,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
FocusScope.of(context).unfocus();
|
||||
},
|
||||
showPinButton: currentUserMember != null &&
|
||||
widget.pinPermissions.contains(currentUserMember.role),
|
||||
_userPermissions.contains(PermissionType.pinMessage),
|
||||
);
|
||||
|
||||
if (widget.messageBuilder != null) {
|
||||
@@ -1217,7 +1249,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
index,
|
||||
),
|
||||
messages,
|
||||
messageWidget as MessageWidget,
|
||||
messageWidget as StreamMessageWidget,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1286,6 +1318,7 @@ class _MessageListViewState extends State<MessageListView> {
|
||||
void didChangeDependencies() {
|
||||
final newStreamChannel = StreamChannel.of(context);
|
||||
_streamTheme = StreamChatTheme.of(context);
|
||||
_userPermissions = newStreamChannel.channel.ownCapabilities;
|
||||
|
||||
if (newStreamChannel != streamChannel) {
|
||||
streamChannel = newStreamChannel;
|
||||
|
||||
@@ -5,15 +5,21 @@ import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro message_reactions_modal}
|
||||
@Deprecated("Use 'StreamMessageReactionsModal' instead")
|
||||
typedef MessageReactionsModal = StreamMessageReactionsModal;
|
||||
|
||||
/// {@template message_reactions_modal}
|
||||
/// Modal widget for displaying message reactions
|
||||
class MessageReactionsModal extends StatelessWidget {
|
||||
/// Constructor for creating a [MessageReactionsModal] reactions
|
||||
const MessageReactionsModal({
|
||||
/// {@endtemplate}
|
||||
class StreamMessageReactionsModal extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamMessageReactionsModal] reactions
|
||||
const StreamMessageReactionsModal({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.messageWidget,
|
||||
required this.messageTheme,
|
||||
this.showReactions = true,
|
||||
this.showReactions,
|
||||
this.reverse = false,
|
||||
this.onUserAvatarTap,
|
||||
}) : super(key: key);
|
||||
@@ -24,14 +30,14 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
/// Message to display reactions of
|
||||
final Message message;
|
||||
|
||||
/// [MessageThemeData] to apply to [message]
|
||||
final MessageThemeData messageTheme;
|
||||
/// [StreamMessageThemeData] to apply to [message]
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// Flag to reverse message
|
||||
final bool reverse;
|
||||
|
||||
/// Flag to show reactions on message
|
||||
final bool showReactions;
|
||||
final bool? showReactions;
|
||||
|
||||
/// Callback when user avatar is tapped
|
||||
final void Function(User)? onUserAvatarTap;
|
||||
@@ -40,6 +46,10 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
final size = MediaQuery.of(context).size;
|
||||
final user = StreamChat.of(context).currentUser;
|
||||
final _userPermissions = StreamChannel.of(context).channel.ownCapabilities;
|
||||
|
||||
final hasReactionPermission =
|
||||
_userPermissions.contains(PermissionType.sendReaction);
|
||||
|
||||
final roughMaxSize = size.width * 2 / 3;
|
||||
var messageTextLength = message.text!.length;
|
||||
@@ -71,7 +81,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
if (showReactions &&
|
||||
if ((showReactions ?? hasReactionPermission) &&
|
||||
(message.status == MessageSendingStatus.sent))
|
||||
Align(
|
||||
alignment: Alignment(
|
||||
@@ -84,7 +94,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
: -(1.2 - divFactor)),
|
||||
0,
|
||||
),
|
||||
child: ReactionPicker(
|
||||
child: StreamReactionPicker(
|
||||
message: message,
|
||||
),
|
||||
),
|
||||
@@ -196,7 +206,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
UserAvatar(
|
||||
StreamUserAvatar(
|
||||
onTap: onUserAvatarTap,
|
||||
user: reaction.user!,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
@@ -216,7 +226,7 @@ class MessageReactionsModal extends StatelessWidget {
|
||||
child: Align(
|
||||
alignment:
|
||||
reverse ? Alignment.centerRight : Alignment.centerLeft,
|
||||
child: ReactionBubble(
|
||||
child: StreamReactionBubble(
|
||||
reactions: [reaction],
|
||||
flipTail: !reverse,
|
||||
borderColor:
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@template message_search_item}
|
||||
/// It shows the current [Message] preview.
|
||||
///
|
||||
/// Usually you don't use this widget as it's the default item used by
|
||||
@@ -10,6 +11,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
/// The widget renders the ui based on the first ancestor of type
|
||||
/// [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
/// {@endtemplate}
|
||||
@Deprecated("Use 'StreamMessageSearchItem' instead")
|
||||
class MessageSearchItem extends StatelessWidget {
|
||||
/// Instantiate a new MessageSearchItem
|
||||
const MessageSearchItem({
|
||||
@@ -34,10 +37,10 @@ class MessageSearchItem extends StatelessWidget {
|
||||
final channel = getMessageResponse.channel;
|
||||
final channelName = channel?.extraData['name'];
|
||||
final user = message.user!;
|
||||
final channelPreviewTheme = ChannelPreviewTheme.of(context);
|
||||
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
|
||||
return ListTile(
|
||||
onTap: onTap,
|
||||
leading: UserAvatar(
|
||||
leading: StreamUserAvatar(
|
||||
user: user,
|
||||
showOnlineStatus: showOnlineStatus,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
@@ -120,7 +123,7 @@ class MessageSearchItem extends StatelessWidget {
|
||||
text = parts.join(' ');
|
||||
}
|
||||
|
||||
final channelPreviewTheme = ChannelPreviewTheme.of(context);
|
||||
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
|
||||
return Text.rich(
|
||||
_getDisplayText(
|
||||
text!,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// ignore: lines_longer_than_80_chars
|
||||
// ignore_for_file: deprecated_member_use_from_same_package, deprecated_member_use
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
@@ -17,7 +20,7 @@ typedef EmptyMessageSearchBuilder = Widget Function(
|
||||
String searchQuery,
|
||||
);
|
||||
|
||||
///
|
||||
/// {@template message_search_list_view}
|
||||
/// It shows the list of searched messages.
|
||||
///
|
||||
/// ```dart
|
||||
@@ -47,6 +50,8 @@ typedef EmptyMessageSearchBuilder = Widget Function(
|
||||
/// The widget components render the ui based on the first ancestor of type
|
||||
/// [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
/// {@endtemplate}
|
||||
@Deprecated("Use 'StreamMessageSearchListView' instead")
|
||||
class MessageSearchListView extends StatefulWidget {
|
||||
/// Instantiate a new MessageSearchListView
|
||||
const MessageSearchListView({
|
||||
@@ -168,7 +173,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
if (error is Error) {
|
||||
print(error.stackTrace);
|
||||
}
|
||||
return InfoTile(
|
||||
return StreamInfoTile(
|
||||
showMessage: widget.showErrorTile,
|
||||
tileAnchor: Alignment.topCenter,
|
||||
childAnchor: Alignment.topCenter,
|
||||
@@ -195,7 +200,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
|
||||
);
|
||||
|
||||
final backgroundColor =
|
||||
MessageSearchListViewTheme.of(context).backgroundColor;
|
||||
StreamMessageSearchListViewTheme.of(context).backgroundColor;
|
||||
|
||||
if (backgroundColor != null) {
|
||||
return ColoredBox(
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_markdown/flutter_markdown.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro message_text}
|
||||
@Deprecated("Use 'StreamMessageText' instead")
|
||||
typedef MessageText = StreamMessageText;
|
||||
|
||||
/// {@template message_text}
|
||||
/// Text widget to display in message
|
||||
class MessageText extends StatelessWidget {
|
||||
/// Constructor for creating a [MessageText] widget
|
||||
const MessageText({
|
||||
/// {@endtemplate}
|
||||
class StreamMessageText extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamMessageText] widget
|
||||
const StreamMessageText({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.messageTheme,
|
||||
@@ -23,8 +30,8 @@ class MessageText extends StatelessWidget {
|
||||
/// Callback for when link is tapped
|
||||
final void Function(String)? onLinkTap;
|
||||
|
||||
/// [MessageThemeData] whose text theme is to be applied
|
||||
final MessageThemeData messageTheme;
|
||||
/// [StreamMessageThemeData] whose text theme is to be applied
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -34,13 +41,14 @@ class MessageText extends StatelessWidget {
|
||||
stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'),
|
||||
initialData: streamChat.currentUser!.language ?? 'en',
|
||||
builder: (context, language) {
|
||||
final translatedText =
|
||||
message.i18n?['${language}_text'] ?? message.text;
|
||||
final messageText =
|
||||
_replaceMentions(translatedText ?? '').replaceAll('\n', '\n\n');
|
||||
final messageText = message
|
||||
.translate(language)
|
||||
.replaceMentions()
|
||||
.text
|
||||
?.replaceAll('\n', '\n\n');
|
||||
final themeData = Theme.of(context);
|
||||
return MarkdownBody(
|
||||
data: messageText,
|
||||
data: messageText ?? '',
|
||||
onTapLink: (
|
||||
String link,
|
||||
String? href,
|
||||
@@ -80,17 +88,4 @@ class MessageText extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _replaceMentions(String text) {
|
||||
var messageTextToRender = text;
|
||||
for (final user in message.mentionedUsers.toSet()) {
|
||||
final userId = user.id;
|
||||
final userName = user.name;
|
||||
messageTextToRender = messageTextToRender.replaceAll(
|
||||
'@$userId',
|
||||
'[@$userName](@${userName.replaceAll(' ', '')})',
|
||||
);
|
||||
}
|
||||
return messageTextToRender;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,20 +32,26 @@ enum DisplayWidget {
|
||||
show,
|
||||
}
|
||||
|
||||
/// {@macro message_widget}
|
||||
@Deprecated("Use 'StreamMessageWidget' instead")
|
||||
typedef MessageWidget = StreamMessageWidget;
|
||||
|
||||
/// {@template message_widget}
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows a message with reactions, replies and user avatar.
|
||||
///
|
||||
/// Usually you don't use this widget as it's the default message widget used by
|
||||
/// [MessageListView].
|
||||
/// [StreamMessageListView].
|
||||
///
|
||||
/// The widget components render the ui based on the first ancestor of type
|
||||
/// [StreamChatTheme].
|
||||
/// Modify it to change the widget appearance.
|
||||
class MessageWidget extends StatefulWidget {
|
||||
///
|
||||
MessageWidget({
|
||||
/// {@endtemplate}
|
||||
class StreamMessageWidget extends StatefulWidget {
|
||||
/// Creates a new instance of the message widget.
|
||||
StreamMessageWidget({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.messageTheme,
|
||||
@@ -95,14 +101,6 @@ class MessageWidget extends StatefulWidget {
|
||||
vertical: 8,
|
||||
),
|
||||
this.attachmentPadding = EdgeInsets.zero,
|
||||
@Deprecated('''
|
||||
allRead is now deprecated and it will be removed in future releases.
|
||||
The MessageWidget now listens for read events on its own.
|
||||
''') this.allRead = false,
|
||||
@Deprecated('''
|
||||
readList is now deprecated and it will be removed in future releases.
|
||||
The MessageWidget now listens for read events on its own.
|
||||
''') this.readList,
|
||||
this.onQuotedMessageTap,
|
||||
this.customActions = const [],
|
||||
this.onAttachmentTap,
|
||||
@@ -121,7 +119,7 @@ class MessageWidget extends StatefulWidget {
|
||||
context,
|
||||
Material(
|
||||
color: messageTheme.messageBackgroundColor,
|
||||
child: ImageGroup(
|
||||
child: StreamImageGroup(
|
||||
size: Size(
|
||||
mediaQueryData.size.width * 0.8,
|
||||
mediaQueryData.size.height * 0.3,
|
||||
@@ -142,7 +140,7 @@ class MessageWidget extends StatefulWidget {
|
||||
|
||||
return wrapAttachmentWidget(
|
||||
context,
|
||||
ImageAttachment(
|
||||
StreamImageAttachment(
|
||||
attachment: attachments[0],
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
@@ -172,7 +170,7 @@ class MessageWidget extends StatefulWidget {
|
||||
Column(
|
||||
children: attachments.map((attachment) {
|
||||
final mediaQueryData = MediaQuery.of(context);
|
||||
return VideoAttachment(
|
||||
return StreamVideoAttachment(
|
||||
attachment: attachment,
|
||||
messageTheme: messageTheme,
|
||||
size: Size(
|
||||
@@ -204,7 +202,7 @@ class MessageWidget extends StatefulWidget {
|
||||
Column(
|
||||
children: attachments.map((attachment) {
|
||||
final mediaQueryData = MediaQuery.of(context);
|
||||
return GiphyAttachment(
|
||||
return StreamGiphyAttachment(
|
||||
attachment: attachment,
|
||||
message: message,
|
||||
size: Size(
|
||||
@@ -240,7 +238,7 @@ class MessageWidget extends StatefulWidget {
|
||||
final mediaQueryData = MediaQuery.of(context);
|
||||
return wrapAttachmentWidget(
|
||||
context,
|
||||
FileAttachment(
|
||||
StreamFileAttachment(
|
||||
message: message,
|
||||
attachment: attachment,
|
||||
size: Size(
|
||||
@@ -300,7 +298,7 @@ class MessageWidget extends StatefulWidget {
|
||||
final Message message;
|
||||
|
||||
/// The message theme
|
||||
final MessageThemeData messageTheme;
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// If true the widget will be mirrored
|
||||
final bool reverse;
|
||||
@@ -341,9 +339,6 @@ class MessageWidget extends StatefulWidget {
|
||||
/// If true the widget will show the reactions
|
||||
final bool showReactions;
|
||||
|
||||
///
|
||||
final bool allRead;
|
||||
|
||||
/// If true the widget will show the thread reply indicator
|
||||
final bool showThreadReplyIndicator;
|
||||
|
||||
@@ -356,12 +351,9 @@ class MessageWidget extends StatefulWidget {
|
||||
/// The function called when tapping on a link
|
||||
final void Function(String)? onLinkTap;
|
||||
|
||||
/// Used in [MessageReactionsModal] and [MessageActionsModal]
|
||||
/// Used in [StreamMessageReactionsModal] and [StreamMessageActionsModal]
|
||||
final bool showReactionPickerIndicator;
|
||||
|
||||
/// List of users who read
|
||||
final List<Read>? readList;
|
||||
|
||||
/// Callback when show message is tapped
|
||||
final ShowMessageCallback? onShowMessage;
|
||||
|
||||
@@ -417,13 +409,14 @@ class MessageWidget extends StatefulWidget {
|
||||
final void Function(Message)? onMessageTap;
|
||||
|
||||
/// List of custom actions shown on message long tap
|
||||
final List<MessageAction> customActions;
|
||||
final List<StreamMessageAction> customActions;
|
||||
|
||||
/// Customize onTap on attachment
|
||||
final void Function(Message message, Attachment attachment)? onAttachmentTap;
|
||||
|
||||
/// Creates a copy of [MessageWidget] with specified attributes overridden.
|
||||
MessageWidget copyWith({
|
||||
/// Creates a copy of [StreamMessageWidget] with
|
||||
/// specified attributes overridden.
|
||||
StreamMessageWidget copyWith({
|
||||
Key? key,
|
||||
void Function(User)? onMentionTap,
|
||||
void Function(Message)? onThreadTap,
|
||||
@@ -435,7 +428,7 @@ class MessageWidget extends StatefulWidget {
|
||||
Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
|
||||
void Function(BuildContext, Message)? onMessageActions,
|
||||
Message? message,
|
||||
MessageThemeData? messageTheme,
|
||||
StreamMessageThemeData? messageTheme,
|
||||
bool? reverse,
|
||||
ShapeBorder? shape,
|
||||
ShapeBorder? attachmentShape,
|
||||
@@ -473,11 +466,11 @@ class MessageWidget extends StatefulWidget {
|
||||
bool? translateUserAvatar,
|
||||
OnQuotedMessageTap? onQuotedMessageTap,
|
||||
void Function(Message)? onMessageTap,
|
||||
List<MessageAction>? customActions,
|
||||
List<StreamMessageAction>? customActions,
|
||||
void Function(Message message, Attachment attachment)? onAttachmentTap,
|
||||
Widget Function(BuildContext, User)? userAvatarBuilder,
|
||||
}) =>
|
||||
MessageWidget(
|
||||
StreamMessageWidget(
|
||||
key: key ?? this.key,
|
||||
onMentionTap: onMentionTap ?? this.onMentionTap,
|
||||
onThreadTap: onThreadTap ?? this.onThreadTap,
|
||||
@@ -539,11 +532,11 @@ class MessageWidget extends StatefulWidget {
|
||||
);
|
||||
|
||||
@override
|
||||
_MessageWidgetState createState() => _MessageWidgetState();
|
||||
_StreamMessageWidgetState createState() => _StreamMessageWidgetState();
|
||||
}
|
||||
|
||||
class _MessageWidgetState extends State<MessageWidget>
|
||||
with AutomaticKeepAliveClientMixin<MessageWidget> {
|
||||
class _StreamMessageWidgetState extends State<StreamMessageWidget>
|
||||
with AutomaticKeepAliveClientMixin<StreamMessageWidget> {
|
||||
bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
|
||||
|
||||
bool get showSendingIndicator => widget.showSendingIndicator;
|
||||
@@ -666,9 +659,9 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
if (widget.showUserAvatar == DisplayWidget.hide)
|
||||
SizedBox(width: avatarWidth + 4),
|
||||
Flexible(
|
||||
child: PortalEntry(
|
||||
child: PortalTarget(
|
||||
visible: showReactions,
|
||||
portal: showReactions
|
||||
portalFollower: showReactions
|
||||
? Container(
|
||||
transform:
|
||||
Matrix4.translationValues(
|
||||
@@ -684,10 +677,16 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
),
|
||||
)
|
||||
: null,
|
||||
portalAnchor:
|
||||
Alignment(widget.reverse ? 1 : -1, -1),
|
||||
childAnchor:
|
||||
Alignment(widget.reverse ? -1 : 1, -1),
|
||||
anchor: Aligned(
|
||||
follower: Alignment(
|
||||
widget.reverse ? 1 : -1,
|
||||
-1,
|
||||
),
|
||||
target: Alignment(
|
||||
widget.reverse ? -1 : 1,
|
||||
-1,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
@@ -717,7 +716,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
? 0
|
||||
: 4.0,
|
||||
),
|
||||
child: DeletedMessage(
|
||||
child: StreamDeletedMessage(
|
||||
borderRadiusGeometry: widget
|
||||
.borderRadiusGeometry,
|
||||
borderSide:
|
||||
@@ -854,7 +853,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
|
||||
: null;
|
||||
final chatThemeData = _streamChatTheme;
|
||||
return QuotedMessageWidget(
|
||||
return StreamQuotedMessageWidget(
|
||||
onTap: onTap,
|
||||
message: widget.message.quotedMessage!,
|
||||
messageTheme: isMyMessage
|
||||
@@ -910,17 +909,6 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
const usernameKey = Key('username');
|
||||
|
||||
children.addAll([
|
||||
if (showInChannel || showThreadReplyIndicator) ...[
|
||||
if (showThreadParticipants)
|
||||
SizedBox.fromSize(
|
||||
size: Size((threadParticipants!.length * 8.0) + 8, 16),
|
||||
child: _buildThreadParticipantsIndicator(threadParticipants),
|
||||
),
|
||||
InkWell(
|
||||
onTap: widget.onThreadTap != null ? onThreadTap : null,
|
||||
child: Text(msg, style: widget.messageTheme.repliesStyle),
|
||||
),
|
||||
],
|
||||
if (showUsername) _buildUsername(usernameKey),
|
||||
if (showTimeStamp)
|
||||
Text(
|
||||
@@ -933,26 +921,41 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
final showThreadTail = !(hasUrlAttachments || isGiphy || isOnlyEmoji) &&
|
||||
(showThreadReplyIndicator || showInChannel);
|
||||
|
||||
final threadIndicatorWidgets = <Widget>[
|
||||
if (showThreadTail)
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: context.textScaleFactor *
|
||||
((widget.messageTheme.repliesStyle?.fontSize ?? 1) / 2),
|
||||
),
|
||||
child: CustomPaint(
|
||||
size: const Size(16, 32) * context.textScaleFactor,
|
||||
painter: _ThreadReplyPainter(
|
||||
context: context,
|
||||
color: widget.messageTheme.messageBorderColor,
|
||||
reverse: widget.reverse,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showInChannel || showThreadReplyIndicator) ...[
|
||||
if (showThreadParticipants)
|
||||
SizedBox.fromSize(
|
||||
size: Size((threadParticipants!.length * 8.0) + 8, 16),
|
||||
child: _buildThreadParticipantsIndicator(threadParticipants),
|
||||
),
|
||||
InkWell(
|
||||
onTap: widget.onThreadTap != null ? onThreadTap : null,
|
||||
child: Text(msg, style: widget.messageTheme.repliesStyle),
|
||||
),
|
||||
],
|
||||
];
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisAlignment:
|
||||
widget.reverse ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (showThreadTail && !widget.reverse)
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: context.textScaleFactor *
|
||||
((widget.messageTheme.repliesStyle?.fontSize ?? 1) / 2),
|
||||
),
|
||||
child: CustomPaint(
|
||||
size: const Size(16, 32) * context.textScaleFactor,
|
||||
painter: _ThreadReplyPainter(
|
||||
context: context,
|
||||
color: widget.messageTheme.messageBorderColor,
|
||||
reverse: widget.reverse,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showThreadTail && !widget.reverse) ...threadIndicatorWidgets,
|
||||
...children.map(
|
||||
(child) {
|
||||
Widget mappedChild = SizedBox(
|
||||
@@ -966,20 +969,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
},
|
||||
),
|
||||
if (showThreadTail && widget.reverse)
|
||||
Container(
|
||||
margin: EdgeInsets.only(
|
||||
bottom: context.textScaleFactor *
|
||||
((widget.messageTheme.repliesStyle?.fontSize ?? 1) / 2),
|
||||
),
|
||||
child: CustomPaint(
|
||||
size: const Size(16, 32) * context.textScaleFactor,
|
||||
painter: _ThreadReplyPainter(
|
||||
context: context,
|
||||
color: widget.messageTheme.messageBorderColor,
|
||||
reverse: widget.reverse,
|
||||
),
|
||||
),
|
||||
),
|
||||
...threadIndicatorWidgets.reversed,
|
||||
].insertBetween(const SizedBox(width: 8)),
|
||||
);
|
||||
}
|
||||
@@ -1008,7 +998,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
getWebsiteName(hostName.toLowerCase()) ??
|
||||
hostName.capitalize();
|
||||
|
||||
return UrlAttachment(
|
||||
return StreamUrlAttachment(
|
||||
urlAttachment: urlAttachment,
|
||||
hostDisplayName: hostDisplayName,
|
||||
textPadding: widget.textPadding,
|
||||
@@ -1042,7 +1032,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
child: _shouldShowReactions
|
||||
? GestureDetector(
|
||||
onTap: () => _showMessageReactionsModalBottomSheet(context),
|
||||
child: ReactionBubble(
|
||||
child: StreamReactionBubble(
|
||||
key: ValueKey('${widget.message.id}.reactions'),
|
||||
reverse: widget.reverse,
|
||||
flipTail: widget.reverse,
|
||||
@@ -1073,7 +1063,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
barrierColor: _streamChatTheme.colorTheme.overlay,
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: MessageActionsModal(
|
||||
child: StreamMessageActionsModal(
|
||||
messageWidget: widget.copyWith(
|
||||
key: const Key('MessageWidget'),
|
||||
message: widget.message.copyWith(
|
||||
@@ -1088,7 +1078,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
showSendingIndicator: false,
|
||||
padding: const EdgeInsets.all(0),
|
||||
showReactionPickerIndicator: widget.showReactions &&
|
||||
(widget.message.status == MessageSendingStatus.sent),
|
||||
(widget.message.status == MessageSendingStatus.sent) &&
|
||||
channel.ownCapabilities.contains(PermissionType.sendReaction),
|
||||
showPinHighlight: false,
|
||||
showUserAvatar:
|
||||
widget.message.user!.id == channel.client.state.currentUser!.id
|
||||
@@ -1099,7 +1090,6 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
Clipboard.setData(ClipboardData(text: message.text)),
|
||||
messageTheme: widget.messageTheme,
|
||||
reverse: widget.reverse,
|
||||
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
|
||||
message: widget.message,
|
||||
editMessageInputBuilder: widget.editMessageInputBuilder,
|
||||
onReplyTap: widget.onReplyTap,
|
||||
@@ -1109,11 +1099,6 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
showCopyMessage: widget.showCopyMessage &&
|
||||
!isFailedState &&
|
||||
widget.message.text?.trim().isNotEmpty == true,
|
||||
showEditMessage: widget.showEditMessage &&
|
||||
!isDeleteFailed &&
|
||||
!widget.message.attachments
|
||||
.any((element) => element.type == 'giphy'),
|
||||
showReactions: widget.showReactions,
|
||||
showReplyMessage: widget.showReplyMessage &&
|
||||
!isFailedState &&
|
||||
widget.onReplyTap != null,
|
||||
@@ -1121,7 +1106,6 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
!isFailedState &&
|
||||
widget.onThreadTap != null,
|
||||
showFlagButton: widget.showFlagButton,
|
||||
showPinButton: widget.showPinButton,
|
||||
customActions: widget.customActions,
|
||||
),
|
||||
),
|
||||
@@ -1136,7 +1120,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
barrierColor: _streamChatTheme.colorTheme.overlay,
|
||||
builder: (context) => StreamChannel(
|
||||
channel: channel,
|
||||
child: MessageReactionsModal(
|
||||
child: StreamMessageReactionsModal(
|
||||
messageWidget: widget.copyWith(
|
||||
key: const Key('MessageWidget'),
|
||||
message: widget.message.copyWith(
|
||||
@@ -1151,7 +1135,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
showSendingIndicator: false,
|
||||
padding: const EdgeInsets.all(0),
|
||||
showReactionPickerIndicator: widget.showReactions &&
|
||||
(widget.message.status == MessageSendingStatus.sent),
|
||||
(widget.message.status == MessageSendingStatus.sent) &&
|
||||
channel.ownCapabilities.contains(PermissionType.sendReaction),
|
||||
showPinHighlight: false,
|
||||
showUserAvatar:
|
||||
widget.message.user!.id == channel.client.state.currentUser!.id
|
||||
@@ -1162,7 +1147,8 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
messageTheme: widget.messageTheme,
|
||||
reverse: widget.reverse,
|
||||
message: widget.message,
|
||||
showReactions: widget.showReactions,
|
||||
showReactions: widget.showReactions &&
|
||||
channel.ownCapabilities.contains(PermissionType.sendReaction),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -1250,6 +1236,13 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
|
||||
final channel = StreamChannel.of(context).channel;
|
||||
|
||||
if (!channel.ownCapabilities.contains(PermissionType.readEvents)) {
|
||||
return StreamSendingIndicator(
|
||||
message: message,
|
||||
size: style!.fontSize,
|
||||
);
|
||||
}
|
||||
|
||||
return BetterStreamBuilder<List<Read>>(
|
||||
stream: channel.state?.readStream,
|
||||
initialData: channel.state?.read,
|
||||
@@ -1259,7 +1252,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
(it.lastRead.isAfter(message.createdAt) ||
|
||||
it.lastRead.isAtSameMomentAs(message.createdAt)));
|
||||
final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1;
|
||||
Widget child = SendingIndicator(
|
||||
Widget child = StreamSendingIndicator(
|
||||
message: message,
|
||||
isMessageRead: isMessageRead,
|
||||
size: style!.fontSize,
|
||||
@@ -1293,7 +1286,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
: 0,
|
||||
),
|
||||
child: widget.userAvatarBuilder?.call(context, widget.message.user!) ??
|
||||
UserAvatar(
|
||||
StreamUserAvatar(
|
||||
user: widget.message.user!,
|
||||
onTap: widget.onUserAvatarTap,
|
||||
constraints: widget.messageTheme.avatarTheme!.constraints,
|
||||
@@ -1311,7 +1304,7 @@ class _MessageWidgetState extends State<MessageWidget>
|
||||
padding: isOnlyEmoji ? EdgeInsets.zero : widget.textPadding,
|
||||
child: widget.textBuilder != null
|
||||
? widget.textBuilder!(context, widget.message)
|
||||
: MessageText(
|
||||
: StreamMessageText(
|
||||
onLinkTap: widget.onLinkTap,
|
||||
message: widget.message,
|
||||
onMentionTap: widget.onMentionTap,
|
||||
@@ -1428,7 +1421,7 @@ class _ThreadParticipants extends StatelessWidget {
|
||||
color: _streamChatTheme.colorTheme.barsBg,
|
||||
),
|
||||
padding: const EdgeInsets.all(1),
|
||||
child: UserAvatar(
|
||||
child: StreamUserAvatar(
|
||||
user: user,
|
||||
constraints: BoxConstraints.loose(const Size.fromRadius(7)),
|
||||
showOnlineStatus: false,
|
||||
|
||||
@@ -2,15 +2,21 @@ import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_portal/flutter_portal.dart';
|
||||
|
||||
/// {@macro multi_overlay}
|
||||
@Deprecated("Use 'StreamMultiOverlay' instead")
|
||||
typedef MultiOverlay = StreamMultiOverlay;
|
||||
|
||||
/// {@template multi_overlay}
|
||||
/// Widget that renders a single overlay widget from a list of [overlayOptions]
|
||||
/// It shows the first one that is visible
|
||||
class MultiOverlay extends StatelessWidget {
|
||||
/// {@endtemplate}
|
||||
class StreamMultiOverlay extends StatelessWidget {
|
||||
/// Constructs a new MultiOverlay widget
|
||||
/// [overlayOptions] - the list of overlay options
|
||||
/// [overlayAnchor] - the anchor relative to the overlay
|
||||
/// [childAnchor] - the anchor relative to the child
|
||||
/// [child] - the child widget
|
||||
const MultiOverlay({
|
||||
const StreamMultiOverlay({
|
||||
Key? key,
|
||||
required this.overlayOptions,
|
||||
required this.child,
|
||||
@@ -35,11 +41,13 @@ class MultiOverlay extends StatelessWidget {
|
||||
final visibleOverlay =
|
||||
overlayOptions.firstWhereOrNull((element) => element.visible);
|
||||
|
||||
return PortalEntry(
|
||||
childAnchor: childAnchor,
|
||||
portalAnchor: overlayAnchor,
|
||||
return PortalTarget(
|
||||
anchor: Aligned(
|
||||
follower: overlayAnchor ?? Alignment.center,
|
||||
target: childAnchor ?? Alignment.center,
|
||||
),
|
||||
visible: visibleOverlay != null,
|
||||
portal: visibleOverlay?.widget,
|
||||
portalFollower: visibleOverlay?.widget,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
/// {@macro option_list_tile}
|
||||
@Deprecated("Use 'StreamOptionListTile' instead")
|
||||
typedef OptionListTile = StreamOptionListTile;
|
||||
|
||||
/// {@template option_list_tile}
|
||||
/// List tile for [ChannelBottomSheet]
|
||||
class OptionListTile extends StatelessWidget {
|
||||
/// Constructor for creating [OptionListTile]
|
||||
const OptionListTile({
|
||||
/// {@endtemplate}
|
||||
class StreamOptionListTile extends StatelessWidget {
|
||||
/// Constructor for creating [StreamOptionListTile]
|
||||
const StreamOptionListTile({
|
||||
Key? key,
|
||||
this.title,
|
||||
required this.title,
|
||||
this.leading,
|
||||
this.trailing,
|
||||
this.onTap,
|
||||
@@ -17,7 +23,7 @@ class OptionListTile extends StatelessWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
/// Title for tile
|
||||
final String? title;
|
||||
final String title;
|
||||
|
||||
/// Leading widget (start)
|
||||
final Widget? leading;
|
||||
@@ -46,8 +52,8 @@ class OptionListTile extends StatelessWidget {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
color: separatorColor ?? chatThemeData.colorTheme.disabled,
|
||||
height: 1,
|
||||
color: separatorColor ?? chatThemeData.colorTheme.disabled,
|
||||
),
|
||||
Material(
|
||||
color: tileColor ?? chatThemeData.colorTheme.barsBg,
|
||||
@@ -57,15 +63,14 @@ class OptionListTile extends StatelessWidget {
|
||||
onTap: onTap,
|
||||
child: Row(
|
||||
children: [
|
||||
if (leading != null) Center(child: leading),
|
||||
if (leading == null)
|
||||
const SizedBox(
|
||||
width: 16,
|
||||
),
|
||||
if (leading != null)
|
||||
Center(child: leading)
|
||||
else
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
title!,
|
||||
title,
|
||||
style: titleTextStyle ??
|
||||
(titleColor == null
|
||||
? chatThemeData.textTheme.bodyBold
|
||||
|
||||
@@ -10,54 +10,14 @@ typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
|
||||
Attachment,
|
||||
);
|
||||
|
||||
class _VideoAttachmentThumbnail extends StatefulWidget {
|
||||
const _VideoAttachmentThumbnail({
|
||||
Key? key,
|
||||
required this.attachment,
|
||||
this.size = const Size(32, 32),
|
||||
}) : super(key: key);
|
||||
/// Widget for the quoted message.
|
||||
@Deprecated("Use 'StreamQuotedMessageWidget' instead")
|
||||
typedef QuotedMessageWidget = StreamQuotedMessageWidget;
|
||||
|
||||
final Size size;
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
_VideoAttachmentThumbnailState createState() =>
|
||||
_VideoAttachmentThumbnailState();
|
||||
}
|
||||
|
||||
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
||||
late VideoPlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.network(widget.attachment.assetUrl!)
|
||||
..initialize().then((_) {
|
||||
// ignore: no-empty-block
|
||||
setState(() {}); //when your thumbnail will show.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
height: widget.size.height,
|
||||
width: widget.size.width,
|
||||
child: _controller.value.isInitialized
|
||||
? VideoPlayer(_controller)
|
||||
: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
class QuotedMessageWidget extends StatelessWidget {
|
||||
///
|
||||
const QuotedMessageWidget({
|
||||
/// Widget for the quoted message.
|
||||
class StreamQuotedMessageWidget extends StatelessWidget {
|
||||
/// Creates a new instance of the widget.
|
||||
const StreamQuotedMessageWidget({
|
||||
Key? key,
|
||||
required this.message,
|
||||
required this.messageTheme,
|
||||
@@ -73,7 +33,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
final Message message;
|
||||
|
||||
/// The message theme
|
||||
final MessageThemeData messageTheme;
|
||||
final StreamMessageThemeData messageTheme;
|
||||
|
||||
/// If true the widget will be mirrored
|
||||
final bool reverse;
|
||||
@@ -134,7 +94,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
if (_hasAttachments) _parseAttachments(context),
|
||||
if (msg.text!.isNotEmpty)
|
||||
Flexible(
|
||||
child: MessageText(
|
||||
child: StreamMessageText(
|
||||
message: msg,
|
||||
messageTheme: isOnlyEmoji && _containsText
|
||||
? messageTheme.copyWith(
|
||||
@@ -231,7 +191,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
);
|
||||
|
||||
Widget _buildUserAvatar() => UserAvatar(
|
||||
Widget _buildUserAvatar() => StreamUserAvatar(
|
||||
user: message.user!,
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 24,
|
||||
@@ -242,7 +202,7 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
|
||||
Map<String, QuotedMessageAttachmentThumbnailBuilder>
|
||||
get _defaultAttachmentBuilder => {
|
||||
'image': (_, attachment) => ImageAttachment(
|
||||
'image': (_, attachment) => StreamImageAttachment(
|
||||
attachment: attachment,
|
||||
message: message,
|
||||
messageTheme: messageTheme,
|
||||
@@ -288,3 +248,47 @@ class QuotedMessageWidget extends StatelessWidget {
|
||||
return messageTheme.messageBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoAttachmentThumbnail extends StatefulWidget {
|
||||
const _VideoAttachmentThumbnail({
|
||||
Key? key,
|
||||
required this.attachment,
|
||||
this.size = const Size(32, 32),
|
||||
}) : super(key: key);
|
||||
|
||||
final Size size;
|
||||
final Attachment attachment;
|
||||
|
||||
@override
|
||||
_VideoAttachmentThumbnailState createState() =>
|
||||
_VideoAttachmentThumbnailState();
|
||||
}
|
||||
|
||||
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
|
||||
late VideoPlayerController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = VideoPlayerController.network(widget.attachment.assetUrl!)
|
||||
..initialize().then((_) {
|
||||
// ignore: no-empty-block
|
||||
setState(() {}); //when your thumbnail will show.
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
_controller.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => SizedBox(
|
||||
height: widget.size.height,
|
||||
width: widget.size.width,
|
||||
child: _controller.value.isInitialized
|
||||
? VideoPlayer(_controller)
|
||||
: const CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,16 @@ import 'package:collection/collection.dart' show IterableExtension;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro reaction_bubble}
|
||||
@Deprecated("Use 'StreamReactionBubble' instead")
|
||||
typedef ReactionBubble = StreamReactionBubble;
|
||||
|
||||
/// {@template reaction_bubble}
|
||||
/// Creates reaction bubble widget for displaying over messages
|
||||
class ReactionBubble extends StatelessWidget {
|
||||
/// Constructor for creating a [ReactionBubble]
|
||||
const ReactionBubble({
|
||||
/// {@endtemplate}
|
||||
class StreamReactionBubble extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamReactionBubble]
|
||||
const StreamReactionBubble({
|
||||
Key? key,
|
||||
required this.reactions,
|
||||
required this.borderColor,
|
||||
@@ -111,7 +117,7 @@ class ReactionBubble extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildReaction(
|
||||
List<ReactionIcon> reactionIcons,
|
||||
List<StreamReactionIcon> reactionIcons,
|
||||
Reaction reaction,
|
||||
BuildContext context,
|
||||
) {
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Reaction icon data
|
||||
class ReactionIcon {
|
||||
/// Constructor for creating [ReactionIcon]
|
||||
ReactionIcon({
|
||||
@Deprecated("Use 'StreamReactionIcon' instead")
|
||||
typedef ReactionIcon = StreamReactionIcon;
|
||||
|
||||
/// Reaction icon data
|
||||
class StreamReactionIcon {
|
||||
/// Constructor for creating [StreamReactionIcon]
|
||||
StreamReactionIcon({
|
||||
required this.type,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@@ -3,16 +3,22 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/extension.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro reaction_picker}
|
||||
@Deprecated("Use 'StreamReactionPicker' instead")
|
||||
typedef ReactionPicker = StreamReactionPicker;
|
||||
|
||||
/// {@template reaction_picker}
|
||||
/// 
|
||||
/// 
|
||||
///
|
||||
/// It shows a reaction picker
|
||||
///
|
||||
/// Usually you don't use this widget as it's one of the default widgets used
|
||||
/// by [MessageWidget.onMessageActions].
|
||||
class ReactionPicker extends StatefulWidget {
|
||||
/// Constructor for creating a [ReactionPicker] widget
|
||||
const ReactionPicker({
|
||||
/// by [StreamMessageWidget.onMessageActions].
|
||||
/// {@endtemplate}
|
||||
class StreamReactionPicker extends StatefulWidget {
|
||||
/// Constructor for creating a [StreamReactionPicker] widget
|
||||
const StreamReactionPicker({
|
||||
Key? key,
|
||||
required this.message,
|
||||
}) : super(key: key);
|
||||
@@ -21,10 +27,10 @@ class ReactionPicker extends StatefulWidget {
|
||||
final Message message;
|
||||
|
||||
@override
|
||||
_ReactionPickerState createState() => _ReactionPickerState();
|
||||
_StreamReactionPickerState createState() => _StreamReactionPickerState();
|
||||
}
|
||||
|
||||
class _ReactionPickerState extends State<ReactionPicker>
|
||||
class _StreamReactionPickerState extends State<StreamReactionPicker>
|
||||
with TickerProviderStateMixin {
|
||||
List<EzAnimation> animations = [];
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// {@macro sending_indicator}
|
||||
@Deprecated("Use 'StreamSendingIndicator' instead")
|
||||
typedef SendingIndicator = StreamSendingIndicator;
|
||||
|
||||
/// {@template sending_indicator}
|
||||
/// Used to show the sending status of the message
|
||||
class SendingIndicator extends StatelessWidget {
|
||||
/// Constructor for creating a [SendingIndicator] widget
|
||||
const SendingIndicator({
|
||||
/// {@endtemplate}
|
||||
class StreamSendingIndicator extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamSendingIndicator] widget
|
||||
const StreamSendingIndicator({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.isMessageRead = false,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// The [StreamAttachmentPackage] class is basically meant to wrap
|
||||
/// individual attachments with their corresponding message
|
||||
class StreamAttachmentPackage {
|
||||
/// Default constructor to prepare an [StreamAttachmentPackage] object
|
||||
StreamAttachmentPackage({
|
||||
required this.attachment,
|
||||
required this.message,
|
||||
});
|
||||
|
||||
/// This is the individual attachment
|
||||
final Attachment attachment;
|
||||
|
||||
/// This is the message that the attachment belongs to
|
||||
/// The message object may have attachemnt(s) other than the one packaged
|
||||
final Message message;
|
||||
}
|
||||
@@ -132,20 +132,6 @@ class StreamChatState extends State<StreamChat> {
|
||||
return defaultTheme.merge(themeData);
|
||||
}
|
||||
|
||||
// coverage:ignore-start
|
||||
|
||||
/// The current user
|
||||
@Deprecated('Use `.currentUser` instead, Will be removed in future releases')
|
||||
User? get user => widget.client.state.currentUser;
|
||||
|
||||
/// The current user as a stream
|
||||
@Deprecated(
|
||||
'Use `.currentUserStream` instead, Will be removed in future releases',
|
||||
)
|
||||
Stream<User?> get userStream => widget.client.state.currentUserStream;
|
||||
|
||||
// coverage:ignore-end
|
||||
|
||||
/// The current user
|
||||
User? get currentUser => widget.client.state.currentUser;
|
||||
|
||||
|
||||
@@ -38,29 +38,29 @@ class StreamChatThemeData {
|
||||
/// Create a theme from scratch
|
||||
factory StreamChatThemeData({
|
||||
Brightness? brightness,
|
||||
TextTheme? textTheme,
|
||||
ColorTheme? colorTheme,
|
||||
ChannelListHeaderThemeData? channelListHeaderTheme,
|
||||
ChannelPreviewThemeData? channelPreviewTheme,
|
||||
ChannelHeaderThemeData? channelHeaderTheme,
|
||||
MessageThemeData? otherMessageTheme,
|
||||
MessageThemeData? ownMessageTheme,
|
||||
MessageInputThemeData? messageInputTheme,
|
||||
StreamTextTheme? textTheme,
|
||||
StreamColorTheme? colorTheme,
|
||||
StreamChannelListHeaderThemeData? channelListHeaderTheme,
|
||||
StreamChannelPreviewThemeData? channelPreviewTheme,
|
||||
StreamChannelHeaderThemeData? channelHeaderTheme,
|
||||
StreamMessageThemeData? otherMessageTheme,
|
||||
StreamMessageThemeData? ownMessageTheme,
|
||||
StreamMessageInputThemeData? messageInputTheme,
|
||||
Widget Function(BuildContext, User)? defaultUserImage,
|
||||
Widget Function(BuildContext, User)? placeholderUserImage,
|
||||
IconThemeData? primaryIconTheme,
|
||||
List<ReactionIcon>? reactionIcons,
|
||||
GalleryHeaderThemeData? imageHeaderTheme,
|
||||
GalleryFooterThemeData? imageFooterTheme,
|
||||
MessageListViewThemeData? messageListViewTheme,
|
||||
ChannelListViewThemeData? channelListViewTheme,
|
||||
UserListViewThemeData? userListViewTheme,
|
||||
MessageSearchListViewThemeData? messageSearchListViewTheme,
|
||||
List<StreamReactionIcon>? reactionIcons,
|
||||
StreamGalleryHeaderThemeData? imageHeaderTheme,
|
||||
StreamGalleryFooterThemeData? imageFooterTheme,
|
||||
StreamMessageListViewThemeData? messageListViewTheme,
|
||||
StreamChannelListViewThemeData? channelListViewTheme,
|
||||
StreamUserListViewThemeData? userListViewTheme,
|
||||
StreamMessageSearchListViewThemeData? messageSearchListViewTheme,
|
||||
}) {
|
||||
brightness ??= colorTheme?.brightness ?? Brightness.light;
|
||||
final isDark = brightness == Brightness.dark;
|
||||
textTheme ??= isDark ? TextTheme.dark() : TextTheme.light();
|
||||
colorTheme ??= isDark ? ColorTheme.dark() : ColorTheme.light();
|
||||
textTheme ??= isDark ? StreamTextTheme.dark() : StreamTextTheme.light();
|
||||
colorTheme ??= isDark ? StreamColorTheme.dark() : StreamColorTheme.light();
|
||||
|
||||
final defaultData = StreamChatThemeData.fromColorAndTextTheme(
|
||||
colorTheme,
|
||||
@@ -133,14 +133,14 @@ class StreamChatThemeData {
|
||||
|
||||
/// Create theme from color and text theme
|
||||
factory StreamChatThemeData.fromColorAndTextTheme(
|
||||
ColorTheme colorTheme,
|
||||
TextTheme textTheme,
|
||||
StreamColorTheme colorTheme,
|
||||
StreamTextTheme textTheme,
|
||||
) {
|
||||
final accentColor = colorTheme.accentPrimary;
|
||||
final iconTheme =
|
||||
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5));
|
||||
final channelHeaderTheme = ChannelHeaderThemeData(
|
||||
avatarTheme: AvatarThemeData(
|
||||
final channelHeaderTheme = StreamChannelHeaderThemeData(
|
||||
avatarTheme: StreamAvatarThemeData(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
@@ -153,9 +153,9 @@ class StreamChatThemeData {
|
||||
color: const Color(0xff7A7A7A),
|
||||
),
|
||||
);
|
||||
final channelPreviewTheme = ChannelPreviewThemeData(
|
||||
final channelPreviewTheme = StreamChannelPreviewThemeData(
|
||||
unreadCounterColor: colorTheme.accentError,
|
||||
avatarTheme: AvatarThemeData(
|
||||
avatarTheme: StreamAvatarThemeData(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
@@ -176,14 +176,14 @@ class StreamChatThemeData {
|
||||
colorTheme: colorTheme,
|
||||
primaryIconTheme: iconTheme,
|
||||
defaultUserImage: (context, user) => Center(
|
||||
child: GradientAvatar(
|
||||
child: StreamGradientAvatar(
|
||||
name: user.name,
|
||||
userId: user.id,
|
||||
),
|
||||
),
|
||||
channelPreviewTheme: channelPreviewTheme,
|
||||
channelListHeaderTheme: ChannelListHeaderThemeData(
|
||||
avatarTheme: AvatarThemeData(
|
||||
channelListHeaderTheme: StreamChannelListHeaderThemeData(
|
||||
avatarTheme: StreamAvatarThemeData(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 40,
|
||||
@@ -194,7 +194,7 @@ class StreamChatThemeData {
|
||||
titleStyle: textTheme.headlineBold,
|
||||
),
|
||||
channelHeaderTheme: channelHeaderTheme,
|
||||
ownMessageTheme: MessageThemeData(
|
||||
ownMessageTheme: StreamMessageThemeData(
|
||||
messageAuthorStyle:
|
||||
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
|
||||
messageTextStyle: textTheme.body,
|
||||
@@ -206,7 +206,7 @@ class StreamChatThemeData {
|
||||
reactionsBorderColor: colorTheme.borders,
|
||||
reactionsMaskColor: colorTheme.appBg,
|
||||
messageBorderColor: colorTheme.disabled,
|
||||
avatarTheme: AvatarThemeData(
|
||||
avatarTheme: StreamAvatarThemeData(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 32,
|
||||
@@ -218,7 +218,7 @@ class StreamChatThemeData {
|
||||
),
|
||||
linkBackgroundColor: colorTheme.linkBg,
|
||||
),
|
||||
otherMessageTheme: MessageThemeData(
|
||||
otherMessageTheme: StreamMessageThemeData(
|
||||
reactionsBackgroundColor: colorTheme.disabled,
|
||||
reactionsBorderColor: colorTheme.barsBg,
|
||||
reactionsMaskColor: colorTheme.appBg,
|
||||
@@ -233,7 +233,7 @@ class StreamChatThemeData {
|
||||
),
|
||||
messageBackgroundColor: colorTheme.barsBg,
|
||||
messageBorderColor: colorTheme.borders,
|
||||
avatarTheme: AvatarThemeData(
|
||||
avatarTheme: StreamAvatarThemeData(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
constraints: const BoxConstraints.tightFor(
|
||||
height: 32,
|
||||
@@ -242,7 +242,7 @@ class StreamChatThemeData {
|
||||
),
|
||||
linkBackgroundColor: colorTheme.linkBg,
|
||||
),
|
||||
messageInputTheme: MessageInputThemeData(
|
||||
messageInputTheme: StreamMessageInputThemeData(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
sendAnimationDuration: const Duration(milliseconds: 300),
|
||||
actionButtonColor: colorTheme.accentPrimary,
|
||||
@@ -252,6 +252,7 @@ class StreamChatThemeData {
|
||||
sendButtonIdleColor: colorTheme.disabled,
|
||||
inputBackgroundColor: colorTheme.barsBg,
|
||||
inputTextStyle: textTheme.body,
|
||||
linkHighlightColor: colorTheme.accentPrimary,
|
||||
idleBorderGradient: LinearGradient(
|
||||
colors: [
|
||||
colorTheme.disabled,
|
||||
@@ -266,7 +267,7 @@ class StreamChatThemeData {
|
||||
),
|
||||
),
|
||||
reactionIcons: [
|
||||
ReactionIcon(
|
||||
StreamReactionIcon(
|
||||
type: 'love',
|
||||
builder: (context, highlighted, size) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
@@ -278,7 +279,7 @@ class StreamChatThemeData {
|
||||
);
|
||||
},
|
||||
),
|
||||
ReactionIcon(
|
||||
StreamReactionIcon(
|
||||
type: 'like',
|
||||
builder: (context, highlighted, size) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
@@ -290,7 +291,7 @@ class StreamChatThemeData {
|
||||
);
|
||||
},
|
||||
),
|
||||
ReactionIcon(
|
||||
StreamReactionIcon(
|
||||
type: 'sad',
|
||||
builder: (context, highlighted, size) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
@@ -302,7 +303,7 @@ class StreamChatThemeData {
|
||||
);
|
||||
},
|
||||
),
|
||||
ReactionIcon(
|
||||
StreamReactionIcon(
|
||||
type: 'haha',
|
||||
builder: (context, highlighted, size) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
@@ -314,7 +315,7 @@ class StreamChatThemeData {
|
||||
);
|
||||
},
|
||||
),
|
||||
ReactionIcon(
|
||||
StreamReactionIcon(
|
||||
type: 'wow',
|
||||
builder: (context, highlighted, size) {
|
||||
final theme = StreamChatTheme.of(context);
|
||||
@@ -327,7 +328,7 @@ class StreamChatThemeData {
|
||||
},
|
||||
),
|
||||
],
|
||||
galleryHeaderTheme: GalleryHeaderThemeData(
|
||||
galleryHeaderTheme: StreamGalleryHeaderThemeData(
|
||||
closeButtonColor: colorTheme.textHighEmphasis,
|
||||
backgroundColor: channelHeaderTheme.color,
|
||||
iconMenuPointColor: colorTheme.textHighEmphasis,
|
||||
@@ -335,7 +336,7 @@ class StreamChatThemeData {
|
||||
subtitleTextStyle: channelPreviewTheme.subtitleStyle,
|
||||
bottomSheetBarrierColor: colorTheme.overlay,
|
||||
),
|
||||
galleryFooterTheme: GalleryFooterThemeData(
|
||||
galleryFooterTheme: StreamGalleryFooterThemeData(
|
||||
backgroundColor: colorTheme.barsBg,
|
||||
shareIconColor: colorTheme.textHighEmphasis,
|
||||
titleTextStyle: textTheme.headlineBold,
|
||||
@@ -345,52 +346,52 @@ class StreamChatThemeData {
|
||||
bottomSheetPhotosTextStyle: textTheme.headlineBold,
|
||||
bottomSheetCloseIconColor: colorTheme.textHighEmphasis,
|
||||
),
|
||||
messageListViewTheme: MessageListViewThemeData(
|
||||
messageListViewTheme: StreamMessageListViewThemeData(
|
||||
backgroundColor: colorTheme.barsBg,
|
||||
),
|
||||
channelListViewTheme: ChannelListViewThemeData(
|
||||
channelListViewTheme: StreamChannelListViewThemeData(
|
||||
backgroundColor: colorTheme.appBg,
|
||||
),
|
||||
userListViewTheme: UserListViewThemeData(
|
||||
userListViewTheme: StreamUserListViewThemeData(
|
||||
backgroundColor: colorTheme.appBg,
|
||||
),
|
||||
messageSearchListViewTheme: MessageSearchListViewThemeData(
|
||||
messageSearchListViewTheme: StreamMessageSearchListViewThemeData(
|
||||
backgroundColor: colorTheme.appBg,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The text themes used in the widgets
|
||||
final TextTheme textTheme;
|
||||
final StreamTextTheme textTheme;
|
||||
|
||||
/// The color themes used in the widgets
|
||||
final ColorTheme colorTheme;
|
||||
final StreamColorTheme colorTheme;
|
||||
|
||||
/// Theme of the [ChannelPreview]
|
||||
final ChannelPreviewThemeData channelPreviewTheme;
|
||||
/// Theme of the [StreamChannelPreview]
|
||||
final StreamChannelPreviewThemeData channelPreviewTheme;
|
||||
|
||||
/// Theme of the [ChannelListHeader]
|
||||
final ChannelListHeaderThemeData channelListHeaderTheme;
|
||||
/// Theme of the [StreamChannelListHeader]
|
||||
final StreamChannelListHeaderThemeData channelListHeaderTheme;
|
||||
|
||||
/// Theme of the chat widgets dedicated to a channel header
|
||||
final ChannelHeaderThemeData channelHeaderTheme;
|
||||
final StreamChannelHeaderThemeData channelHeaderTheme;
|
||||
|
||||
/// The default style for [GalleryHeader]s below the overall
|
||||
/// The default style for [StreamGalleryHeader]s below the overall
|
||||
/// [StreamChatTheme].
|
||||
final GalleryHeaderThemeData galleryHeaderTheme;
|
||||
final StreamGalleryHeaderThemeData galleryHeaderTheme;
|
||||
|
||||
/// The default style for [GalleryFooter]s below the overall
|
||||
/// The default style for [StreamGalleryFooter]s below the overall
|
||||
/// [StreamChatTheme].
|
||||
final GalleryFooterThemeData galleryFooterTheme;
|
||||
final StreamGalleryFooterThemeData galleryFooterTheme;
|
||||
|
||||
/// Theme of the current user messages
|
||||
final MessageThemeData ownMessageTheme;
|
||||
final StreamMessageThemeData ownMessageTheme;
|
||||
|
||||
/// Theme of other users messages
|
||||
final MessageThemeData otherMessageTheme;
|
||||
final StreamMessageThemeData otherMessageTheme;
|
||||
|
||||
/// Theme dedicated to the [MessageInput] widget
|
||||
final MessageInputThemeData messageInputTheme;
|
||||
/// Theme dedicated to the [StreamMessageInput] widget
|
||||
final StreamMessageInputThemeData messageInputTheme;
|
||||
|
||||
/// The widget that will be built when the user image is unavailable
|
||||
final Widget Function(BuildContext, User) defaultUserImage;
|
||||
@@ -402,41 +403,41 @@ class StreamChatThemeData {
|
||||
final IconThemeData primaryIconTheme;
|
||||
|
||||
/// Assets used for rendering reactions
|
||||
final List<ReactionIcon> reactionIcons;
|
||||
final List<StreamReactionIcon> reactionIcons;
|
||||
|
||||
/// Theme configuration for the [MessageListView] widget.
|
||||
final MessageListViewThemeData messageListViewTheme;
|
||||
/// Theme configuration for the [StreamMessageListView] widget.
|
||||
final StreamMessageListViewThemeData messageListViewTheme;
|
||||
|
||||
/// Theme configuration for the [ChannelListView] widget.
|
||||
final ChannelListViewThemeData channelListViewTheme;
|
||||
/// Theme configuration for the [StreamChannelListView] widget.
|
||||
final StreamChannelListViewThemeData channelListViewTheme;
|
||||
|
||||
/// Theme configuration for the [UserListView] widget.
|
||||
final UserListViewThemeData userListViewTheme;
|
||||
/// Theme configuration for the [StreamUserListView] widget.
|
||||
final StreamUserListViewThemeData userListViewTheme;
|
||||
|
||||
/// Theme configuration for the [MessageSearchListView] widget.
|
||||
final MessageSearchListViewThemeData messageSearchListViewTheme;
|
||||
/// Theme configuration for the [StreamMessageSearchListView] widget.
|
||||
final StreamMessageSearchListViewThemeData messageSearchListViewTheme;
|
||||
|
||||
/// Creates a copy of [StreamChatThemeData] with specified attributes
|
||||
/// overridden.
|
||||
StreamChatThemeData copyWith({
|
||||
TextTheme? textTheme,
|
||||
ColorTheme? colorTheme,
|
||||
ChannelPreviewThemeData? channelPreviewTheme,
|
||||
ChannelHeaderThemeData? channelHeaderTheme,
|
||||
MessageThemeData? ownMessageTheme,
|
||||
MessageThemeData? otherMessageTheme,
|
||||
MessageInputThemeData? messageInputTheme,
|
||||
StreamTextTheme? textTheme,
|
||||
StreamColorTheme? colorTheme,
|
||||
StreamChannelPreviewThemeData? channelPreviewTheme,
|
||||
StreamChannelHeaderThemeData? channelHeaderTheme,
|
||||
StreamMessageThemeData? ownMessageTheme,
|
||||
StreamMessageThemeData? otherMessageTheme,
|
||||
StreamMessageInputThemeData? messageInputTheme,
|
||||
Widget Function(BuildContext, User)? defaultUserImage,
|
||||
Widget Function(BuildContext, User)? placeholderUserImage,
|
||||
IconThemeData? primaryIconTheme,
|
||||
ChannelListHeaderThemeData? channelListHeaderTheme,
|
||||
List<ReactionIcon>? reactionIcons,
|
||||
GalleryHeaderThemeData? galleryHeaderTheme,
|
||||
GalleryFooterThemeData? galleryFooterTheme,
|
||||
MessageListViewThemeData? messageListViewTheme,
|
||||
ChannelListViewThemeData? channelListViewTheme,
|
||||
UserListViewThemeData? userListViewTheme,
|
||||
MessageSearchListViewThemeData? messageSearchListViewTheme,
|
||||
StreamChannelListHeaderThemeData? channelListHeaderTheme,
|
||||
List<StreamReactionIcon>? reactionIcons,
|
||||
StreamGalleryHeaderThemeData? galleryHeaderTheme,
|
||||
StreamGalleryFooterThemeData? galleryFooterTheme,
|
||||
StreamMessageListViewThemeData? messageListViewTheme,
|
||||
StreamChannelListViewThemeData? channelListViewTheme,
|
||||
StreamUserListViewThemeData? userListViewTheme,
|
||||
StreamMessageSearchListViewThemeData? messageSearchListViewTheme,
|
||||
}) =>
|
||||
StreamChatThemeData.raw(
|
||||
channelListHeaderTheme:
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
|
||||
|
||||
/// It shows a date divider depending on the date difference
|
||||
class SystemMessage extends StatelessWidget {
|
||||
/// Constructor for creating a [SystemMessage]
|
||||
const SystemMessage({
|
||||
/// {@macro system_message}
|
||||
@Deprecated("Use 'StreamSystemMessage' instead")
|
||||
typedef SystemMessage = StreamSystemMessage;
|
||||
|
||||
/// {@template system_message}
|
||||
/// It shows a widget for the message with a system message type.
|
||||
/// {@endtemplate}
|
||||
class StreamSystemMessage extends StatelessWidget {
|
||||
/// Constructor for creating a [StreamSystemMessage]
|
||||
const StreamSystemMessage({
|
||||
Key? key,
|
||||
required this.message,
|
||||
this.onMessageTap,
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// {@macro avatar_theme_data}
|
||||
@Deprecated("Use 'StreamAvatarThemeData' instead")
|
||||
typedef AvatarThemeData = StreamAvatarThemeData;
|
||||
|
||||
/// {@template avatar_theme_data}
|
||||
/// A style that overrides the default appearance of various avatar widgets.
|
||||
/// {@endtemplate}
|
||||
// ignore: prefer-match-file-name
|
||||
class AvatarThemeData with Diagnosticable {
|
||||
/// Creates an [AvatarThemeData].
|
||||
const AvatarThemeData({
|
||||
class StreamAvatarThemeData with Diagnosticable {
|
||||
/// Creates an [StreamAvatarThemeData].
|
||||
const StreamAvatarThemeData({
|
||||
BoxConstraints? constraints,
|
||||
BorderRadius? borderRadius,
|
||||
}) : _constraints = constraints,
|
||||
@@ -25,12 +31,12 @@ class AvatarThemeData with Diagnosticable {
|
||||
/// Get border radius
|
||||
BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20);
|
||||
|
||||
/// Copy this [AvatarThemeData] to another.
|
||||
AvatarThemeData copyWith({
|
||||
/// Copy this [StreamAvatarThemeData] to another.
|
||||
StreamAvatarThemeData copyWith({
|
||||
BoxConstraints? constraints,
|
||||
BorderRadius? borderRadius,
|
||||
}) =>
|
||||
AvatarThemeData(
|
||||
StreamAvatarThemeData(
|
||||
constraints: constraints ?? _constraints,
|
||||
borderRadius: borderRadius ?? _borderRadius,
|
||||
);
|
||||
@@ -38,12 +44,12 @@ class AvatarThemeData with Diagnosticable {
|
||||
/// Linearly interpolate between two [UserAvatar] themes.
|
||||
///
|
||||
/// All the properties must be non-null.
|
||||
AvatarThemeData lerp(
|
||||
AvatarThemeData a,
|
||||
AvatarThemeData b,
|
||||
StreamAvatarThemeData lerp(
|
||||
StreamAvatarThemeData a,
|
||||
StreamAvatarThemeData b,
|
||||
double t,
|
||||
) =>
|
||||
AvatarThemeData(
|
||||
StreamAvatarThemeData(
|
||||
borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t),
|
||||
constraints: BoxConstraints.lerp(a.constraints, b.constraints, t),
|
||||
);
|
||||
@@ -51,7 +57,7 @@ class AvatarThemeData with Diagnosticable {
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AvatarThemeData &&
|
||||
other is StreamAvatarThemeData &&
|
||||
runtimeType == other.runtimeType &&
|
||||
_constraints == other._constraints &&
|
||||
_borderRadius == other._borderRadius;
|
||||
@@ -59,8 +65,8 @@ class AvatarThemeData with Diagnosticable {
|
||||
@override
|
||||
int get hashCode => _constraints.hashCode ^ _borderRadius.hashCode;
|
||||
|
||||
/// Merges one [AvatarThemeData] with the another
|
||||
AvatarThemeData merge(AvatarThemeData? other) {
|
||||
/// Merges one [StreamAvatarThemeData] with the another
|
||||
StreamAvatarThemeData merge(StreamAvatarThemeData? other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
constraints: other._constraints,
|
||||
|
||||
@@ -4,27 +4,33 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/themes.dart';
|
||||
|
||||
/// {@macro channel_header_theme}
|
||||
@Deprecated("Use 'StreamChannelHeaderTheme' instead")
|
||||
typedef ChannelHeaderTheme = StreamChannelHeaderTheme;
|
||||
|
||||
/// {@template channel_header_theme}
|
||||
/// Overrides the default style of [ChannelHeader] descendants.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ChannelHeaderThemeData], which is used to configure this theme.
|
||||
class ChannelHeaderTheme extends InheritedTheme {
|
||||
/// Creates a [ChannelHeaderTheme].
|
||||
/// * [StreamChannelHeaderThemeData], which is used to configure this theme.
|
||||
/// {@endtemplate}
|
||||
class StreamChannelHeaderTheme extends InheritedTheme {
|
||||
/// Creates a [StreamChannelHeaderTheme].
|
||||
///
|
||||
/// The [data] parameter must not be null.
|
||||
const ChannelHeaderTheme({
|
||||
const StreamChannelHeaderTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The configuration of this theme.
|
||||
final ChannelHeaderThemeData data;
|
||||
final StreamChannelHeaderThemeData data;
|
||||
|
||||
/// The closest instance of this class that encloses the given context.
|
||||
///
|
||||
/// If there is no enclosing [ChannelHeaderTheme] widget, then
|
||||
/// If there is no enclosing [StreamChannelHeaderTheme] widget, then
|
||||
/// [StreamChatThemeData.channelTheme.channelHeaderTheme] is used.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
@@ -32,34 +38,40 @@ class ChannelHeaderTheme extends InheritedTheme {
|
||||
/// ```dart
|
||||
/// final theme = ChannelHeaderTheme.of(context);
|
||||
/// ```
|
||||
static ChannelHeaderThemeData of(BuildContext context) {
|
||||
static StreamChannelHeaderThemeData of(BuildContext context) {
|
||||
final channelHeaderTheme =
|
||||
context.dependOnInheritedWidgetOfExactType<ChannelHeaderTheme>();
|
||||
context.dependOnInheritedWidgetOfExactType<StreamChannelHeaderTheme>();
|
||||
return channelHeaderTheme?.data ??
|
||||
StreamChatTheme.of(context).channelHeaderTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) =>
|
||||
ChannelHeaderTheme(data: data, child: child);
|
||||
StreamChannelHeaderTheme(data: data, child: child);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ChannelHeaderTheme oldWidget) =>
|
||||
bool updateShouldNotify(StreamChannelHeaderTheme oldWidget) =>
|
||||
data != oldWidget.data;
|
||||
}
|
||||
|
||||
/// {@macro channel_header_theme_data}
|
||||
@Deprecated("Use 'StreamChannelHeaderThemeData' instead")
|
||||
typedef ChannelHeaderThemeData = StreamChannelHeaderThemeData;
|
||||
|
||||
/// {@template channel_header_theme_data}
|
||||
/// A style that overrides the default appearance of [ChannelHeader]s when used
|
||||
/// with [ChannelHeaderTheme] or with the overall [StreamChatTheme]'s
|
||||
/// with [StreamChannelHeaderTheme] or with the overall [StreamChatTheme]'s
|
||||
/// [StreamChatThemeData.channelHeaderTheme].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ChannelHeaderTheme], the theme which is configured with this class.
|
||||
/// * [StreamChannelHeaderTheme], the theme which is configured with this class.
|
||||
/// * [StreamChatThemeData.channelHeaderTheme], which can be used to override
|
||||
/// the default style for [ChannelHeader]s below the overall [StreamChatTheme].
|
||||
class ChannelHeaderThemeData with Diagnosticable {
|
||||
/// Creates a [ChannelHeaderThemeData]
|
||||
const ChannelHeaderThemeData({
|
||||
/// {@endtemplate}
|
||||
class StreamChannelHeaderThemeData with Diagnosticable {
|
||||
/// Creates a [StreamChannelHeaderThemeData]
|
||||
const StreamChannelHeaderThemeData({
|
||||
this.titleStyle,
|
||||
this.subtitleStyle,
|
||||
this.avatarTheme,
|
||||
@@ -73,43 +85,43 @@ class ChannelHeaderThemeData with Diagnosticable {
|
||||
final TextStyle? subtitleStyle;
|
||||
|
||||
/// Theme for avatar
|
||||
final AvatarThemeData? avatarTheme;
|
||||
final StreamAvatarThemeData? avatarTheme;
|
||||
|
||||
/// Color for [ChannelHeaderThemeData]
|
||||
/// Color for [StreamChannelHeaderThemeData]
|
||||
final Color? color;
|
||||
|
||||
/// Copy with theme
|
||||
ChannelHeaderThemeData copyWith({
|
||||
StreamChannelHeaderThemeData copyWith({
|
||||
TextStyle? titleStyle,
|
||||
TextStyle? subtitleStyle,
|
||||
AvatarThemeData? avatarTheme,
|
||||
StreamAvatarThemeData? avatarTheme,
|
||||
Color? color,
|
||||
}) =>
|
||||
ChannelHeaderThemeData(
|
||||
StreamChannelHeaderThemeData(
|
||||
titleStyle: titleStyle ?? this.titleStyle,
|
||||
subtitleStyle: subtitleStyle ?? this.subtitleStyle,
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
color: color ?? this.color,
|
||||
);
|
||||
|
||||
/// Linearly interpolate between two [ChannelHeaderThemeData].
|
||||
/// Linearly interpolate between two [StreamChannelHeaderThemeData].
|
||||
///
|
||||
/// All the properties must be non-null.
|
||||
ChannelHeaderThemeData lerp(
|
||||
ChannelHeaderThemeData a,
|
||||
ChannelHeaderThemeData b,
|
||||
StreamChannelHeaderThemeData lerp(
|
||||
StreamChannelHeaderThemeData a,
|
||||
StreamChannelHeaderThemeData b,
|
||||
double t,
|
||||
) =>
|
||||
ChannelHeaderThemeData(
|
||||
StreamChannelHeaderThemeData(
|
||||
titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t),
|
||||
subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t),
|
||||
avatarTheme:
|
||||
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t),
|
||||
avatarTheme: const StreamAvatarThemeData()
|
||||
.lerp(a.avatarTheme!, b.avatarTheme!, t),
|
||||
color: Color.lerp(a.color, b.color, t),
|
||||
);
|
||||
|
||||
/// Merge with other [ChannelHeaderThemeData]
|
||||
ChannelHeaderThemeData merge(ChannelHeaderThemeData? other) {
|
||||
/// Merge with other [StreamChannelHeaderThemeData]
|
||||
StreamChannelHeaderThemeData merge(StreamChannelHeaderThemeData? other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle,
|
||||
@@ -123,7 +135,7 @@ class ChannelHeaderThemeData with Diagnosticable {
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ChannelHeaderThemeData &&
|
||||
other is StreamChannelHeaderThemeData &&
|
||||
runtimeType == other.runtimeType &&
|
||||
titleStyle == other.titleStyle &&
|
||||
subtitleStyle == other.subtitleStyle &&
|
||||
|
||||
@@ -3,27 +3,34 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
|
||||
|
||||
/// {@macro channel_list_header_theme}
|
||||
@Deprecated("Use 'StreamChannelListHeaderTheme' instead")
|
||||
typedef ChannelListHeaderTheme = StreamChannelListHeaderTheme;
|
||||
|
||||
/// {@template channel_list_header_theme}
|
||||
/// Overrides the default style of [ChannelListHeader] descendants.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ChannelListHeaderThemeData], which is used to configure this theme.
|
||||
class ChannelListHeaderTheme extends InheritedTheme {
|
||||
/// Creates a [ChannelListHeaderTheme].
|
||||
/// * [StreamChannelListHeaderThemeData], which is used
|
||||
/// to configure this theme.
|
||||
/// {@endtemplate}
|
||||
class StreamChannelListHeaderTheme extends InheritedTheme {
|
||||
/// Creates a [StreamChannelListHeaderTheme].
|
||||
///
|
||||
/// The [data] parameter must not be null.
|
||||
const ChannelListHeaderTheme({
|
||||
const StreamChannelListHeaderTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The configuration of this theme.
|
||||
final ChannelListHeaderThemeData data;
|
||||
final StreamChannelListHeaderThemeData data;
|
||||
|
||||
/// The closest instance of this class that encloses the given context.
|
||||
///
|
||||
/// If there is no enclosing [ChannelListHeaderTheme] widget, then
|
||||
/// If there is no enclosing [StreamChannelListHeaderTheme] widget, then
|
||||
/// [StreamChatThemeData.channelListHeaderTheme] is used.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
@@ -31,26 +38,32 @@ class ChannelListHeaderTheme extends InheritedTheme {
|
||||
/// ```dart
|
||||
/// final theme = ChannelListHeaderTheme.of(context);
|
||||
/// ```
|
||||
static ChannelListHeaderThemeData of(BuildContext context) {
|
||||
final channelListHeaderTheme =
|
||||
context.dependOnInheritedWidgetOfExactType<ChannelListHeaderTheme>();
|
||||
static StreamChannelListHeaderThemeData of(BuildContext context) {
|
||||
final channelListHeaderTheme = context
|
||||
.dependOnInheritedWidgetOfExactType<StreamChannelListHeaderTheme>();
|
||||
return channelListHeaderTheme?.data ??
|
||||
StreamChatTheme.of(context).channelListHeaderTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) =>
|
||||
ChannelListHeaderTheme(data: data, child: child);
|
||||
StreamChannelListHeaderTheme(data: data, child: child);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ChannelListHeaderTheme oldWidget) =>
|
||||
bool updateShouldNotify(StreamChannelListHeaderTheme oldWidget) =>
|
||||
data != oldWidget.data;
|
||||
}
|
||||
|
||||
/// {@macro channel_list_header_theme_data}
|
||||
@Deprecated("Use ''StreamChannelListHeaderThemeData' instead")
|
||||
typedef ChannelListHeaderThemeData = StreamChannelListHeaderThemeData;
|
||||
|
||||
/// {@template channel_list_header_theme_data}
|
||||
/// Theme dedicated to the [ChannelListHeader]
|
||||
class ChannelListHeaderThemeData with Diagnosticable {
|
||||
/// Returns a new [ChannelListHeaderThemeData]
|
||||
const ChannelListHeaderThemeData({
|
||||
/// {@endtemplate}
|
||||
class StreamChannelListHeaderThemeData with Diagnosticable {
|
||||
/// Returns a new [StreamChannelListHeaderThemeData]
|
||||
const StreamChannelListHeaderThemeData({
|
||||
this.titleStyle,
|
||||
this.avatarTheme,
|
||||
this.color,
|
||||
@@ -60,39 +73,42 @@ class ChannelListHeaderThemeData with Diagnosticable {
|
||||
final TextStyle? titleStyle;
|
||||
|
||||
/// Theme dedicated to the userAvatar
|
||||
final AvatarThemeData? avatarTheme;
|
||||
final StreamAvatarThemeData? avatarTheme;
|
||||
|
||||
/// Background color of the appbar
|
||||
final Color? color;
|
||||
|
||||
/// Returns a new [ChannelListHeaderThemeData] replacing some of its
|
||||
/// Returns a new [StreamChannelListHeaderThemeData] replacing some of its
|
||||
/// properties
|
||||
ChannelListHeaderThemeData copyWith({
|
||||
StreamChannelListHeaderThemeData copyWith({
|
||||
TextStyle? titleStyle,
|
||||
AvatarThemeData? avatarTheme,
|
||||
StreamAvatarThemeData? avatarTheme,
|
||||
Color? color,
|
||||
}) =>
|
||||
ChannelListHeaderThemeData(
|
||||
StreamChannelListHeaderThemeData(
|
||||
titleStyle: titleStyle ?? this.titleStyle,
|
||||
avatarTheme: avatarTheme ?? this.avatarTheme,
|
||||
color: color ?? this.color,
|
||||
);
|
||||
|
||||
/// Linearly interpolate from one [ChannelListHeaderThemeData] to another.
|
||||
ChannelListHeaderThemeData lerp(
|
||||
ChannelListHeaderThemeData a,
|
||||
ChannelListHeaderThemeData b,
|
||||
/// Linearly interpolate from one [StreamChannelListHeaderThemeData]
|
||||
/// to another.
|
||||
StreamChannelListHeaderThemeData lerp(
|
||||
StreamChannelListHeaderThemeData a,
|
||||
StreamChannelListHeaderThemeData b,
|
||||
double t,
|
||||
) =>
|
||||
ChannelListHeaderThemeData(
|
||||
avatarTheme:
|
||||
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t),
|
||||
StreamChannelListHeaderThemeData(
|
||||
avatarTheme: const StreamAvatarThemeData()
|
||||
.lerp(a.avatarTheme!, b.avatarTheme!, t),
|
||||
color: Color.lerp(a.color, b.color, t),
|
||||
titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t),
|
||||
);
|
||||
|
||||
/// Merges [this] [ChannelListHeaderThemeData] with the [other]
|
||||
ChannelListHeaderThemeData merge(ChannelListHeaderThemeData? other) {
|
||||
/// Merges [this] [StreamChannelListHeaderThemeData] with the [other]
|
||||
StreamChannelListHeaderThemeData merge(
|
||||
StreamChannelListHeaderThemeData? other,
|
||||
) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle,
|
||||
@@ -104,7 +120,7 @@ class ChannelListHeaderThemeData with Diagnosticable {
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ChannelListHeaderThemeData &&
|
||||
other is StreamChannelListHeaderThemeData &&
|
||||
runtimeType == other.runtimeType &&
|
||||
titleStyle == other.titleStyle &&
|
||||
avatarTheme == other.avatarTheme &&
|
||||
|
||||
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
|
||||
/// {@macro channel_list_view_theme}
|
||||
@Deprecated("Use 'StreamChannelListViewTheme' instead")
|
||||
typedef ChannelListViewTheme = StreamChannelListViewTheme;
|
||||
|
||||
/// {@template channel_list_view_theme}
|
||||
/// Overrides the default style of [ChannelListView] descendants.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ChannelListViewThemeData], which is used to configure this theme.
|
||||
class ChannelListViewTheme extends InheritedTheme {
|
||||
/// Creates a [ChannelListViewTheme].
|
||||
/// * [StreamChannelListViewThemeData], which is used to configure this theme.
|
||||
/// {@endtemplate}
|
||||
class StreamChannelListViewTheme extends InheritedTheme {
|
||||
/// Creates a [StreamChannelListViewTheme].
|
||||
///
|
||||
/// The [data] parameter must not be null.
|
||||
const ChannelListViewTheme({
|
||||
const StreamChannelListViewTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The configuration of this theme.
|
||||
final ChannelListViewThemeData data;
|
||||
final StreamChannelListViewThemeData data;
|
||||
|
||||
/// The closest instance of this class that encloses the given context.
|
||||
///
|
||||
/// If there is no enclosing [ChannelListViewTheme] widget, then
|
||||
/// If there is no enclosing [StreamChannelListViewTheme] widget, then
|
||||
/// [StreamChatThemeData.channelListViewTheme] is used.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
@@ -30,63 +36,71 @@ class ChannelListViewTheme extends InheritedTheme {
|
||||
/// ```dart
|
||||
/// ChannelListViewTheme theme = ChannelListViewTheme.of(context);
|
||||
/// ```
|
||||
static ChannelListViewThemeData of(BuildContext context) {
|
||||
final channelListViewTheme =
|
||||
context.dependOnInheritedWidgetOfExactType<ChannelListViewTheme>();
|
||||
static StreamChannelListViewThemeData of(BuildContext context) {
|
||||
final channelListViewTheme = context
|
||||
.dependOnInheritedWidgetOfExactType<StreamChannelListViewTheme>();
|
||||
return channelListViewTheme?.data ??
|
||||
StreamChatTheme.of(context).channelListViewTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) =>
|
||||
ChannelListViewTheme(data: data, child: child);
|
||||
StreamChannelListViewTheme(data: data, child: child);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ChannelListViewTheme oldWidget) =>
|
||||
bool updateShouldNotify(StreamChannelListViewTheme oldWidget) =>
|
||||
data != oldWidget.data;
|
||||
}
|
||||
|
||||
/// {@macro channel_list_view_theme_data}
|
||||
@Deprecated("Use 'StreamChannelListViewThemeData' instead")
|
||||
typedef ChannelListViewThemeData = StreamChannelListViewThemeData;
|
||||
|
||||
/// {@template channel_list_view_theme_data}
|
||||
/// A style that overrides the default appearance of [ChannelListView]s when
|
||||
/// used with [ChannelListViewTheme] or with the overall [StreamChatTheme]'s
|
||||
/// used with [StreamChannelListViewTheme]
|
||||
/// or with the overall [StreamChatTheme]'s
|
||||
/// [StreamChatThemeData.channelListViewTheme].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ChannelListViewTheme], the theme which is configured with this class.
|
||||
/// * [StreamChannelListViewTheme], the theme
|
||||
/// which is configured with this class.
|
||||
/// * [StreamChatThemeData.channelListViewTheme], which can be used to override
|
||||
/// the default style for [ChannelListView]s below the overall
|
||||
/// [StreamChatTheme].
|
||||
class ChannelListViewThemeData with Diagnosticable {
|
||||
/// Creates a [ChannelListViewThemeData].
|
||||
const ChannelListViewThemeData({
|
||||
/// {@endtemplate}
|
||||
class StreamChannelListViewThemeData with Diagnosticable {
|
||||
/// Creates a [StreamChannelListViewThemeData].
|
||||
const StreamChannelListViewThemeData({
|
||||
this.backgroundColor,
|
||||
});
|
||||
|
||||
/// The color of the [ChannelListView] background.
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// Copies this [ChannelListViewThemeData] to another.
|
||||
ChannelListViewThemeData copyWith({
|
||||
/// Copies this [StreamChannelListViewThemeData] to another.
|
||||
StreamChannelListViewThemeData copyWith({
|
||||
Color? backgroundColor,
|
||||
}) =>
|
||||
ChannelListViewThemeData(
|
||||
StreamChannelListViewThemeData(
|
||||
backgroundColor: backgroundColor ?? this.backgroundColor,
|
||||
);
|
||||
|
||||
/// Linearly interpolate between two [ChannelListViewThemeData] themes.
|
||||
/// Linearly interpolate between two [StreamChannelListViewThemeData] themes.
|
||||
///
|
||||
/// All the properties must be non-null.
|
||||
ChannelListViewThemeData lerp(
|
||||
ChannelListViewThemeData a,
|
||||
ChannelListViewThemeData b,
|
||||
StreamChannelListViewThemeData lerp(
|
||||
StreamChannelListViewThemeData a,
|
||||
StreamChannelListViewThemeData b,
|
||||
double t,
|
||||
) =>
|
||||
ChannelListViewThemeData(
|
||||
StreamChannelListViewThemeData(
|
||||
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
|
||||
);
|
||||
|
||||
/// Merges one [ChannelListViewThemeData] with another.
|
||||
ChannelListViewThemeData merge(ChannelListViewThemeData? other) {
|
||||
/// Merges one [StreamChannelListViewThemeData] with another.
|
||||
StreamChannelListViewThemeData merge(StreamChannelListViewThemeData? other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
backgroundColor: other.backgroundColor,
|
||||
@@ -96,7 +110,7 @@ class ChannelListViewThemeData with Diagnosticable {
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ChannelListViewThemeData &&
|
||||
other is StreamChannelListViewThemeData &&
|
||||
runtimeType == other.runtimeType &&
|
||||
backgroundColor == other.backgroundColor;
|
||||
|
||||
|
||||
@@ -3,27 +3,33 @@ import 'package:flutter/material.dart';
|
||||
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
|
||||
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
|
||||
|
||||
/// {@macro channel_preview_theme}
|
||||
@Deprecated("Use 'StreamChannelPreviewTheme' instead")
|
||||
typedef ChannelPreviewTheme = StreamChannelPreviewTheme;
|
||||
|
||||
/// {@template channel_preview_theme}
|
||||
/// Overrides the default style of [ChannelPreview] descendants.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ChannelPreviewThemeData], which is used to configure this theme.
|
||||
class ChannelPreviewTheme extends InheritedTheme {
|
||||
/// Creates a [ChannelPreviewTheme].
|
||||
/// * [StreamChannelPreviewThemeData], which is used to configure this theme.
|
||||
/// {@endtemplate}
|
||||
class StreamChannelPreviewTheme extends InheritedTheme {
|
||||
/// Creates a [StreamChannelPreviewTheme].
|
||||
///
|
||||
/// The [data] parameter must not be null.
|
||||
const ChannelPreviewTheme({
|
||||
const StreamChannelPreviewTheme({
|
||||
Key? key,
|
||||
required this.data,
|
||||
required Widget child,
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
/// The configuration of this theme.
|
||||
final ChannelPreviewThemeData data;
|
||||
final StreamChannelPreviewThemeData data;
|
||||
|
||||
/// The closest instance of this class that encloses the given context.
|
||||
///
|
||||
/// If there is no enclosing [ChannelPreviewTheme] widget, then
|
||||
/// If there is no enclosing [StreamChannelPreviewTheme] widget, then
|
||||
/// [StreamChatThemeData.channelPreviewTheme] is used.
|
||||
///
|
||||
/// Typical usage is as follows:
|
||||
@@ -31,34 +37,41 @@ class ChannelPreviewTheme extends InheritedTheme {
|
||||
/// ```dart
|
||||
/// final theme = ChannelPreviewTheme.of(context);
|
||||
/// ```
|
||||
static ChannelPreviewThemeData of(BuildContext context) {
|
||||
static StreamChannelPreviewThemeData of(BuildContext context) {
|
||||
final channelPreviewTheme =
|
||||
context.dependOnInheritedWidgetOfExactType<ChannelPreviewTheme>();
|
||||
context.dependOnInheritedWidgetOfExactType<StreamChannelPreviewTheme>();
|
||||
return channelPreviewTheme?.data ??
|
||||
StreamChatTheme.of(context).channelPreviewTheme;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget wrap(BuildContext context, Widget child) =>
|
||||
ChannelPreviewTheme(data: data, child: child);
|
||||
StreamChannelPreviewTheme(data: data, child: child);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(ChannelPreviewTheme oldWidget) =>
|
||||
bool updateShouldNotify(StreamChannelPreviewTheme oldWidget) =>
|
||||
data != oldWidget.data;
|
||||
}
|
||||
|
||||
/// {@macro channel_preview_theme_data}
|
||||
@Deprecated("Use 'StreamChannelPreviewThemeData' instead")
|
||||
typedef ChannelPreviewThemeData = StreamChannelPreviewThemeData;
|
||||
|
||||
/// {@template channel_preview_theme_data}
|
||||
/// A style that overrides the default appearance of [ChannelPreview]s when used
|
||||
/// with [ChannelPreviewTheme] or with the overall [StreamChatTheme]'s
|
||||
/// with [StreamChannelPreviewTheme] or with the overall [StreamChatTheme]'s
|
||||
/// [StreamChatThemeData.channelPreviewTheme].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ChannelPreviewTheme], the theme which is configured with this class.
|
||||
/// * [StreamChannelPreviewTheme], the theme
|
||||
/// which is configured with this class.
|
||||
/// * [StreamChatThemeData.channelPreviewTheme], which can be used to override
|
||||
/// the default style for [ChannelHeader]s below the overall [StreamChatTheme].
|
||||
class ChannelPreviewThemeData with Diagnosticable {
|
||||
/// Creates a [ChannelPreviewThemeData].
|
||||
const ChannelPreviewThemeData({
|
||||
/// {@endtemplate}
|
||||
class StreamChannelPreviewThemeData with Diagnosticable {
|
||||
/// Creates a [StreamChannelPreviewThemeData].
|
||||
const StreamChannelPreviewThemeData({
|
||||
this.titleStyle,
|
||||
this.subtitleStyle,
|
||||
this.lastMessageAtStyle,
|
||||
@@ -77,7 +90,7 @@ class ChannelPreviewThemeData with Diagnosticable {
|
||||
final TextStyle? lastMessageAtStyle;
|
||||
|
||||
/// Avatar theme
|
||||
final AvatarThemeData? avatarTheme;
|
||||
final StreamAvatarThemeData? avatarTheme;
|
||||
|
||||
/// Unread counter color
|
||||
final Color? unreadCounterColor;
|
||||
@@ -86,15 +99,15 @@ class ChannelPreviewThemeData with Diagnosticable {
|
||||
final double? indicatorIconSize;
|
||||
|
||||
/// Copy with theme
|
||||
ChannelPreviewThemeData copyWith({
|
||||
StreamChannelPreviewThemeData copyWith({
|
||||
TextStyle? titleStyle,
|
||||
TextStyle? subtitleStyle,
|
||||
TextStyle? lastMessageAtStyle,
|
||||
AvatarThemeData? avatarTheme,
|
||||
StreamAvatarThemeData? avatarTheme,
|
||||
Color? unreadCounterColor,
|
||||
double? indicatorIconSize,
|
||||
}) =>
|
||||
ChannelPreviewThemeData(
|
||||
StreamChannelPreviewThemeData(
|
||||
titleStyle: titleStyle ?? this.titleStyle,
|
||||
subtitleStyle: subtitleStyle ?? this.subtitleStyle,
|
||||
lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle,
|
||||
@@ -103,15 +116,15 @@ class ChannelPreviewThemeData with Diagnosticable {
|
||||
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
|
||||
);
|
||||
|
||||
/// Linearly interpolate one [ChannelPreviewThemeData] to another.
|
||||
ChannelPreviewThemeData lerp(
|
||||
ChannelPreviewThemeData a,
|
||||
ChannelPreviewThemeData b,
|
||||
/// Linearly interpolate one [StreamChannelPreviewThemeData] to another.
|
||||
StreamChannelPreviewThemeData lerp(
|
||||
StreamChannelPreviewThemeData a,
|
||||
StreamChannelPreviewThemeData b,
|
||||
double t,
|
||||
) =>
|
||||
ChannelPreviewThemeData(
|
||||
avatarTheme:
|
||||
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t),
|
||||
StreamChannelPreviewThemeData(
|
||||
avatarTheme: const StreamAvatarThemeData()
|
||||
.lerp(a.avatarTheme!, b.avatarTheme!, t),
|
||||
indicatorIconSize: a.indicatorIconSize,
|
||||
lastMessageAtStyle:
|
||||
TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t),
|
||||
@@ -122,7 +135,7 @@ class ChannelPreviewThemeData with Diagnosticable {
|
||||
);
|
||||
|
||||
/// Merge with theme
|
||||
ChannelPreviewThemeData merge(ChannelPreviewThemeData? other) {
|
||||
StreamChannelPreviewThemeData merge(StreamChannelPreviewThemeData? other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle,
|
||||
@@ -138,7 +151,7 @@ class ChannelPreviewThemeData with Diagnosticable {
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ChannelPreviewThemeData &&
|
||||
other is StreamChannelPreviewThemeData &&
|
||||
runtimeType == other.runtimeType &&
|
||||
titleStyle == other.titleStyle &&
|
||||
subtitleStyle == other.subtitleStyle &&
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// {@macro color_theme}
|
||||
@Deprecated("Use 'StreamColorTheme' instead")
|
||||
typedef ColorTheme = StreamColorTheme;
|
||||
|
||||
/// {@template color_theme}
|
||||
/// Theme that holds colors
|
||||
class ColorTheme {
|
||||
/// {@endtemplate}
|
||||
class StreamColorTheme {
|
||||
/// Initialise with light theme
|
||||
ColorTheme.light({
|
||||
StreamColorTheme.light({
|
||||
this.textHighEmphasis = const Color(0xff000000),
|
||||
this.textLowEmphasis = const Color(0xff7a7a7a),
|
||||
this.disabled = const Color(0xffdbdbdb),
|
||||
@@ -55,7 +61,7 @@ class ColorTheme {
|
||||
}) : brightness = Brightness.light;
|
||||
|
||||
/// Initialise with dark theme
|
||||
ColorTheme.dark({
|
||||
StreamColorTheme.dark({
|
||||
this.textHighEmphasis = const Color(0xffffffff),
|
||||
this.textLowEmphasis = const Color(0xff7a7a7a),
|
||||
this.disabled = const Color(0xff2d2f2f),
|
||||
@@ -169,7 +175,7 @@ class ColorTheme {
|
||||
final Brightness brightness;
|
||||
|
||||
/// Copy with theme
|
||||
ColorTheme copyWith({
|
||||
StreamColorTheme copyWith({
|
||||
Brightness brightness = Brightness.light,
|
||||
Color? textHighEmphasis,
|
||||
Color? textLowEmphasis,
|
||||
@@ -192,7 +198,7 @@ class ColorTheme {
|
||||
Gradient? bgGradient,
|
||||
}) =>
|
||||
brightness == Brightness.light
|
||||
? ColorTheme.light(
|
||||
? StreamColorTheme.light(
|
||||
textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis,
|
||||
textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis,
|
||||
disabled: disabled ?? this.disabled,
|
||||
@@ -213,7 +219,7 @@ class ColorTheme {
|
||||
overlayDark: overlayDark ?? this.overlayDark,
|
||||
bgGradient: bgGradient ?? this.bgGradient,
|
||||
)
|
||||
: ColorTheme.dark(
|
||||
: StreamColorTheme.dark(
|
||||
textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis,
|
||||
textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis,
|
||||
disabled: disabled ?? this.disabled,
|
||||
@@ -236,7 +242,7 @@ class ColorTheme {
|
||||
);
|
||||
|
||||
/// Merge color theme
|
||||
ColorTheme merge(ColorTheme? other) {
|
||||
StreamColorTheme merge(StreamColorTheme? other) {
|
||||
if (other == null) return this;
|
||||
return copyWith(
|
||||
textHighEmphasis: other.textHighEmphasis,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user