Merge branch 'develop' of github.com:GetStream/stream-chat-flutter into develop

This commit is contained in:
xsahil03x
2022-05-05 20:11:43 +05:30
411 changed files with 23761 additions and 2944 deletions
+35 -1
View File
@@ -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
+4
View File
@@ -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.
+2 -1
View File
@@ -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,
+16 -21
View File
@@ -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';
}
+31
View File
@@ -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;
+1 -1
View File
@@ -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 -1
View File
@@ -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);
+59 -2
View File
@@ -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
+12 -11
View File
@@ -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 Flutters [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 threads 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;
@@ -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}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png)
///
@@ -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}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png)
///
@@ -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}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview_paint.png)
///
@@ -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}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget_paint.png)
///
/// 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}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.png)
///
/// 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