Merge branch 'v4' of https://github.com/GetStream/stream-chat-flutter into live-event-improvements

This commit is contained in:
Ayush Shekhar
2022-03-21 06:33:24 +05:30
174 changed files with 11150 additions and 1870 deletions
@@ -35,7 +35,7 @@ jobs:
run: | run: |
flutter pub global activate melos ${{ env.melos_version }} flutter pub global activate melos ${{ env.melos_version }}
- name: "Bootstrap Workspace" - name: "Bootstrap Workspace"
run: melos bootstrap run: melos bootstrap --verbose
- name: "Dart Analyze" - name: "Dart Analyze"
run: | run: |
melos run analyze melos run analyze
+2 -1
View File
@@ -11,7 +11,8 @@ dependencies:
cupertino_icons: ^1.0.0 cupertino_icons: ^1.0.0
flutter: flutter:
sdk: flutter sdk: flutter
stream_chat: ^2.2.1 stream_chat:
path: ../
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -294,6 +294,18 @@ class Channel {
return data; 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. /// Channel extra data as a stream.
Stream<Map<String, Object?>> get extraDataStream { Stream<Map<String, Object?>> get extraDataStream {
_checkInitialized(); _checkInitialized();
@@ -487,6 +499,7 @@ class Channel {
Future<SendMessageResponse> sendMessage( Future<SendMessageResponse> sendMessage(
Message message, { Message message, {
bool skipPush = false, bool skipPush = false,
bool skipEnrichUrl = false,
}) async { }) async {
_checkInitialized(); _checkInitialized();
// Cancelling previous completer in case it's called again in the process // Cancelling previous completer in case it's called again in the process
@@ -534,6 +547,7 @@ class Channel {
id!, id!,
type, type,
skipPush: skipPush, skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
); );
state!.updateMessage(response.message); state!.updateMessage(response.message);
if (cooldown > 0) cooldownStartedAt = DateTime.now(); if (cooldown > 0) cooldownStartedAt = DateTime.now();
@@ -550,7 +564,10 @@ class Channel {
/// ///
/// Waits for a [_messageAttachmentsUploadCompleter] to complete /// Waits for a [_messageAttachmentsUploadCompleter] to complete
/// before actually updating the message. /// before actually updating the message.
Future<UpdateMessageResponse> updateMessage(Message message) async { Future<UpdateMessageResponse> updateMessage(
Message message, {
bool skipEnrichUrl = false,
}) async {
final originalMessage = message; final originalMessage = message;
// Cancelling previous completer in case it's called again in the process // Cancelling previous completer in case it's called again in the process
@@ -588,7 +605,10 @@ class Channel {
message = await attachmentsUploadCompleter.future; message = await attachmentsUploadCompleter.future;
} }
final response = await _client.updateMessage(message); final response = await _client.updateMessage(
message,
skipEnrichUrl: skipEnrichUrl,
);
final m = response.message.copyWith( final m = response.message.copyWith(
ownReactions: message.ownReactions, ownReactions: message.ownReactions,
@@ -618,12 +638,14 @@ class Channel {
Message message, { Message message, {
Map<String, Object?>? set, Map<String, Object?>? set,
List<String>? unset, List<String>? unset,
bool skipEnrichUrl = false,
}) async { }) async {
try { try {
final response = await _client.partialUpdateMessage( final response = await _client.partialUpdateMessage(
message.id, message.id,
set: set, set: set,
unset: unset, unset: unset,
skipEnrichUrl: skipEnrichUrl,
); );
final updatedMessage = response.message.copyWith( final updatedMessage = response.message.copyWith(
@@ -1598,7 +1620,9 @@ class ChannelClientState {
_subscriptions.add(_channel.on(EventType.channelUpdated).listen((Event e) { _subscriptions.add(_channel.on(EventType.channelUpdated).listen((Event e) {
final channel = e.channel!; final channel = e.channel!;
updateChannelState(channelState.copyWith( updateChannelState(channelState.copyWith(
channel: channel, channel: channel.copyWith(
ownCapabilities: channelState.channel?.ownCapabilities,
),
members: channel.members, members: channel.members,
)); ));
})); }));
@@ -1968,6 +1992,14 @@ class ChannelClientState {
(watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(), (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. /// Channel read list.
List<Read> get read => _channelState.read; List<Read> get read => _channelState.read;
@@ -1212,12 +1212,14 @@ class StreamChatClient {
String channelId, String channelId,
String channelType, { String channelType, {
bool skipPush = false, bool skipPush = false,
bool skipEnrichUrl = false,
}) => }) =>
_chatApi.message.sendMessage( _chatApi.message.sendMessage(
channelId, channelId,
channelType, channelType,
message, message,
skipPush: skipPush, skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
); );
/// Lists all the message replies for the [parentId] /// Lists all the message replies for the [parentId]
@@ -1241,8 +1243,14 @@ class StreamChatClient {
); );
/// Update the given message /// Update the given message
Future<UpdateMessageResponse> updateMessage(Message message) => Future<UpdateMessageResponse> updateMessage(
_chatApi.message.updateMessage(message); Message message, {
bool skipEnrichUrl = false,
}) =>
_chatApi.message.updateMessage(
message,
skipEnrichUrl: skipEnrichUrl,
);
/// Partially update the given [messageId] /// Partially update the given [messageId]
/// Use [set] to define values to be set /// Use [set] to define values to be set
@@ -1251,11 +1259,13 @@ class StreamChatClient {
String messageId, { String messageId, {
Map<String, Object?>? set, Map<String, Object?>? set,
List<String>? unset, List<String>? unset,
bool skipEnrichUrl = false,
}) => }) =>
_chatApi.message.partialUpdateMessage( _chatApi.message.partialUpdateMessage(
messageId, messageId,
set: set, set: set,
unset: unset, unset: unset,
skipEnrichUrl: skipEnrichUrl,
); );
/// Deletes the given message /// Deletes the given message
@@ -5,9 +5,9 @@ import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/client/retry_policy.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
/// The retry queue associated to a channel /// The retry queue associated to a channel.
class RetryQueue { class RetryQueue {
/// Instantiate a new RetryQueue object /// Instantiate a new RetryQueue object.
RetryQueue({ RetryQueue({
required this.channel, required this.channel,
this.logger, this.logger,
@@ -17,13 +17,13 @@ class RetryQueue {
_listenFailedEvents(); _listenFailedEvents();
} }
/// The channel of this queue /// The channel of this queue.
final Channel channel; final Channel channel;
/// The client associated with this [channel] /// The client associated with this [channel].
final StreamChatClient client; final StreamChatClient client;
/// The logger associated to this queue /// The logger associated to this queue.
final Logger? logger; final Logger? logger;
late final RetryPolicy _retryPolicy; late final RetryPolicy _retryPolicy;
@@ -63,7 +63,7 @@ class RetryQueue {
}).addTo(_compositeSubscription); }).addTo(_compositeSubscription);
} }
/// Add a list of messages /// Add a list of messages.
void add(List<Message> messages) { void add(List<Message> messages) {
if (messages.isEmpty) return; if (messages.isEmpty) return;
if (!_messageQueue.containsAllMessage(messages)) { if (!_messageQueue.containsAllMessage(messages)) {
@@ -113,6 +113,7 @@ class RetryQueue {
} catch (e) { } catch (e) {
if (e is! StreamChatNetworkError || !e.isRetriable) { if (e is! StreamChatNetworkError || !e.isRetriable) {
_messageQueue.removeMessage(message); _messageQueue.removeMessage(message);
_sendFailedEvent(message);
return true; return true;
} }
// retry logic // 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; bool get hasMessages => _messageQueue.isNotEmpty;
/// Call this method to dispose this object /// Call this method to dispose this object.
void dispose() { void dispose() {
_messageQueue.clear(); _messageQueue.clear();
_compositeSubscription.dispose(); _compositeSubscription.dispose();
@@ -269,6 +269,7 @@ class ChannelApi {
) async { ) async {
final response = await _client.post( final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/truncate', '${_getChannelUrl(channelId, channelType)}/truncate',
data: {},
); );
return EmptyResponse.fromJson(response.data); return EmptyResponse.fromJson(response.data);
} }
@@ -16,12 +16,14 @@ class MessageApi {
String channelType, String channelType,
Message message, { Message message, {
bool skipPush = false, bool skipPush = false,
bool skipEnrichUrl = false,
}) async { }) async {
final response = await _client.post( final response = await _client.post(
'/channels/$channelType/$channelId/message', '/channels/$channelType/$channelId/message',
data: { data: {
'message': message, 'message': message,
'skip_push': skipPush, 'skip_push': skipPush,
'skip_enrich_url': skipEnrichUrl,
}, },
); );
return SendMessageResponse.fromJson(response.data); return SendMessageResponse.fromJson(response.data);
@@ -51,11 +53,15 @@ class MessageApi {
/// Updates the given [message] /// Updates the given [message]
Future<UpdateMessageResponse> updateMessage( Future<UpdateMessageResponse> updateMessage(
Message message, Message message, {
) async { bool skipEnrichUrl = false,
}) async {
final response = await _client.post( final response = await _client.post(
'/messages/${message.id}', '/messages/${message.id}',
data: {'message': message}, data: {
'message': message,
'skip_enrich_url': skipEnrichUrl,
},
); );
return UpdateMessageResponse.fromJson(response.data); return UpdateMessageResponse.fromJson(response.data);
} }
@@ -67,12 +73,14 @@ class MessageApi {
String messageId, { String messageId, {
Map<String, Object?>? set, Map<String, Object?>? set,
List<String>? unset, List<String>? unset,
bool skipEnrichUrl = false,
}) async { }) async {
final response = await _client.put( final response = await _client.put(
'/messages/$messageId', '/messages/$messageId',
data: { data: {
if (set != null) 'set': set, if (set != null) 'set': set,
if (unset != null) 'unset': unset, if (unset != null) 'unset': unset,
'skip_enrich_url': skipEnrichUrl,
}, },
); );
return UpdateMessageResponse.fromJson(response.data); return UpdateMessageResponse.fromJson(response.data);
@@ -2,6 +2,7 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/models/action.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/models/attachment_file.dart';
import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/util/serializer.dart';
@@ -66,6 +67,21 @@ class Attachment extends Equatable {
topLevelFields + dbSpecificTopLevelFields, 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, ///The attachment type based on the URL resource. This can be: audio,
///image or video ///image or video
final String? type; final String? type;
@@ -229,6 +245,33 @@ class Attachment extends Equatable {
extraData: extraData ?? this.extraData, 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 @override
List<Object?> get props => [ List<Object?> get props => [
id, id,
@@ -13,6 +13,7 @@ class ChannelModel {
String? id, String? id,
String? type, String? type,
String? cid, String? cid,
this.ownCapabilities = const [],
ChannelConfig? config, ChannelConfig? config,
this.createdBy, this.createdBy,
this.frozen = false, this.frozen = false,
@@ -51,6 +52,10 @@ class ChannelModel {
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final String cid; final String cid;
/// List of user permissions on this channel
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final List<String> ownCapabilities;
/// The channel configuration data /// The channel configuration data
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final ChannelConfig config; final ChannelConfig config;
@@ -101,6 +106,7 @@ class ChannelModel {
'id', 'id',
'type', 'type',
'cid', 'cid',
'own_capabilities',
'config', 'config',
'created_by', 'created_by',
'frozen', 'frozen',
@@ -127,6 +133,7 @@ class ChannelModel {
String? id, String? id,
String? type, String? type,
String? cid, String? cid,
List<String>? ownCapabilities,
ChannelConfig? config, ChannelConfig? config,
User? createdBy, User? createdBy,
bool? frozen, bool? frozen,
@@ -143,6 +150,7 @@ class ChannelModel {
id: id ?? this.id, id: id ?? this.id,
type: type ?? this.type, type: type ?? this.type,
cid: cid ?? this.cid, cid: cid ?? this.cid,
ownCapabilities: ownCapabilities ?? this.ownCapabilities,
config: config ?? this.config, config: config ?? this.config,
createdBy: createdBy ?? this.createdBy, createdBy: createdBy ?? this.createdBy,
frozen: frozen ?? this.frozen, frozen: frozen ?? this.frozen,
@@ -164,6 +172,7 @@ class ChannelModel {
id: other.id, id: other.id,
type: other.type, type: other.type,
cid: other.cid, cid: other.cid,
ownCapabilities: other.ownCapabilities,
config: other.config, config: other.config,
createdBy: other.createdBy, createdBy: other.createdBy,
frozen: other.frozen, frozen: other.frozen,
@@ -10,6 +10,10 @@ ChannelModel _$ChannelModelFromJson(Map<String, dynamic> json) => ChannelModel(
id: json['id'] as String?, id: json['id'] as String?,
type: json['type'] as String?, type: json['type'] as String?,
cid: json['cid'] as String?, cid: json['cid'] as String?,
ownCapabilities: (json['own_capabilities'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
config: json['config'] == null config: json['config'] == null
? null ? null
: ChannelConfig.fromJson(json['config'] as Map<String, dynamic>), : ChannelConfig.fromJson(json['config'] as Map<String, dynamic>),
@@ -48,6 +52,7 @@ Map<String, dynamic> _$ChannelModelToJson(ChannelModel instance) {
} }
writeNotNull('cid', readonly(instance.cid)); writeNotNull('cid', readonly(instance.cid));
writeNotNull('own_capabilities', readonly(instance.ownCapabilities));
writeNotNull('config', readonly(instance.config)); writeNotNull('config', readonly(instance.config));
writeNotNull('created_by', readonly(instance.createdBy)); writeNotNull('created_by', readonly(instance.createdBy));
val['frozen'] = instance.frozen; val['frozen'] = instance.frozen;
@@ -8,13 +8,13 @@ import 'package:uuid/uuid.dart';
part 'message.g.dart'; part 'message.g.dart';
class _PinExpires { class _NullConst {
const _PinExpires(); 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 { enum MessageSendingStatus {
/// Message is being sent /// Message is being sent
sending, sending,
@@ -40,10 +40,10 @@ enum MessageSendingStatus {
sent, sent,
} }
/// The class that contains the information about a message /// The class that contains the information about a message.
@JsonSerializable() @JsonSerializable()
class Message extends Equatable { class Message extends Equatable {
/// Constructor used for json serialization /// Constructor used for json serialization.
Message({ Message({
String? id, String? id,
this.text, this.text,
@@ -58,44 +58,47 @@ class Message extends Equatable {
this.ownReactions, this.ownReactions,
this.parentId, this.parentId,
this.quotedMessage, this.quotedMessage,
this.quotedMessageId, String? quotedMessageId,
this.replyCount = 0, this.replyCount = 0,
this.threadParticipants, this.threadParticipants,
this.showInChannel, this.showInChannel,
this.command, this.command,
DateTime? createdAt, DateTime? createdAt,
DateTime? updatedAt, DateTime? updatedAt,
this.deletedAt,
this.user, this.user,
this.pinned = false, this.pinned = false,
this.pinnedAt, this.pinnedAt,
DateTime? pinExpires, DateTime? pinExpires,
this.pinnedBy, this.pinnedBy,
this.extraData = const {}, this.extraData = const {},
this.deletedAt, this.status = MessageSendingStatus.sending,
this.status = MessageSendingStatus.sent,
this.i18n, this.i18n,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
pinExpires = pinExpires?.toUtc(), pinExpires = pinExpires?.toUtc(),
createdAt = createdAt ?? DateTime.now(), _createdAt = createdAt,
updatedAt = updatedAt ?? DateTime.now(); _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( factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
Serializer.moveToExtraDataFromRoot(json, topLevelFields), Serializer.moveToExtraDataFromRoot(json, topLevelFields),
).copyWith(
status: MessageSendingStatus.sent,
); );
/// The message ID. This is either created by Stream or set client side when /// The message ID. This is either created by Stream or set client side when
/// the message is added. /// the message is added.
final String id; final String id;
/// The text of this message /// The text of this message.
final String? text; final String? text;
/// The status of a sending message /// The status of a sending message.
@JsonKey(ignore: true) @JsonKey(ignore: true)
final MessageSendingStatus status; final MessageSendingStatus status;
/// The message type /// The message type.
@JsonKey( @JsonKey(
includeIfNull: false, includeIfNull: false,
toJson: Serializer.readOnly, toJson: Serializer.readOnly,
@@ -107,15 +110,15 @@ class Message extends Equatable {
@JsonKey(includeIfNull: false) @JsonKey(includeIfNull: false)
final List<Attachment> attachments; final List<Attachment> attachments;
/// The list of user mentioned in the message /// The list of user mentioned in the message.
@JsonKey(toJson: User.toIds) @JsonKey(toJson: User.toIds)
final List<User> mentionedUsers; 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) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final Map<String, int>? reactionCounts; 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) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final Map<String, int>? reactionScores; 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. /// The ID of the parent message, if the message is a thread reply.
final String? parentId; final String? parentId;
/// A quoted reply message /// A quoted reply message.
@JsonKey(toJson: Serializer.readOnly) @JsonKey(toJson: Serializer.readOnly)
final Message? quotedMessage; final Message? quotedMessage;
final String? _quotedMessageId;
/// The ID of the quoted message, if the message is a quoted reply. /// 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. /// Reserved field indicating the number of replies for this message.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@@ -148,10 +153,10 @@ class Message extends Equatable {
/// Check if this message needs to show in the channel. /// Check if this message needs to show in the channel.
final bool? showInChannel; final bool? showInChannel;
/// If true the message is silent /// If true the message is silent.
final bool silent; final bool silent;
/// If true the message is shadowed /// If true the message is shadowed.
@JsonKey( @JsonKey(
includeIfNull: false, includeIfNull: false,
toJson: Serializer.readOnly, toJson: Serializer.readOnly,
@@ -162,56 +167,61 @@ class Message extends Equatable {
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final String? command; final String? command;
/// Reserved field indicating when the message was created. final DateTime? _createdAt;
@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';
/// Reserved field indicating when the message was deleted. /// Reserved field indicating when the message was deleted.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final DateTime? deletedAt; 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. /// A Map of translations.
@JsonKey(includeIfNull: false) @JsonKey(includeIfNull: false)
final Map<String, String>? i18n; final Map<String, String>? i18n;
/// Known top level fields. /// Known top level fields.
///
/// Useful for [Serializer] methods. /// Useful for [Serializer] methods.
static const topLevelFields = [ static const topLevelFields = [
'id', 'id',
@@ -244,7 +254,7 @@ class Message extends Equatable {
'i18n', 'i18n',
]; ];
/// Serialize to json /// Serialize to json.
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot( Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
_$MessageToJson(this), _$MessageToJson(this),
); );
@@ -256,18 +266,18 @@ class Message extends Equatable {
String? type, String? type,
List<Attachment>? attachments, List<Attachment>? attachments,
List<User>? mentionedUsers, List<User>? mentionedUsers,
bool? silent,
bool? shadowed,
Map<String, int>? reactionCounts, Map<String, int>? reactionCounts,
Map<String, int>? reactionScores, Map<String, int>? reactionScores,
List<Reaction>? latestReactions, List<Reaction>? latestReactions,
List<Reaction>? ownReactions, List<Reaction>? ownReactions,
String? parentId, String? parentId,
Message? quotedMessage, Object? quotedMessage = _nullConst,
String? quotedMessageId, Object? quotedMessageId = _nullConst,
int? replyCount, int? replyCount,
List<User>? threadParticipants, List<User>? threadParticipants,
bool? showInChannel, bool? showInChannel,
bool? shadowed,
bool? silent,
String? command, String? command,
DateTime? createdAt, DateTime? createdAt,
DateTime? updatedAt, DateTime? updatedAt,
@@ -275,7 +285,7 @@ class Message extends Equatable {
User? user, User? user,
bool? pinned, bool? pinned,
DateTime? pinnedAt, DateTime? pinnedAt,
Object? pinExpires = _pinExpires, Object? pinExpires = _nullConst,
User? pinnedBy, User? pinnedBy,
Map<String, Object?>? extraData, Map<String, Object?>? extraData,
MessageSendingStatus? status, MessageSendingStatus? status,
@@ -284,41 +294,68 @@ class Message extends Equatable {
assert(() { assert(() {
if (pinExpires is! DateTime && if (pinExpires is! DateTime &&
pinExpires != null && pinExpires != null &&
pinExpires is! _PinExpires) { pinExpires is! _NullConst) {
throw ArgumentError('`pinExpires` can only be set as DateTime or null'); throw ArgumentError('`pinExpires` can only be set as DateTime or null');
} }
return true; return true;
}(), 'Validate type for pinExpires'); }(), '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( return Message(
id: id ?? this.id, id: id ?? this.id,
text: text ?? this.text, text: text ?? this.text,
type: type ?? this.type, type: type ?? this.type,
attachments: attachments ?? this.attachments, attachments: attachments ?? this.attachments,
mentionedUsers: mentionedUsers ?? this.mentionedUsers, mentionedUsers: mentionedUsers ?? this.mentionedUsers,
silent: silent ?? this.silent,
shadowed: shadowed ?? this.shadowed,
reactionCounts: reactionCounts ?? this.reactionCounts, reactionCounts: reactionCounts ?? this.reactionCounts,
reactionScores: reactionScores ?? this.reactionScores, reactionScores: reactionScores ?? this.reactionScores,
latestReactions: latestReactions ?? this.latestReactions, latestReactions: latestReactions ?? this.latestReactions,
ownReactions: ownReactions ?? this.ownReactions, ownReactions: ownReactions ?? this.ownReactions,
parentId: parentId ?? this.parentId, parentId: parentId ?? this.parentId,
quotedMessage: quotedMessage ?? this.quotedMessage, quotedMessage: quotedMessage == _nullConst
quotedMessageId: quotedMessageId ?? this.quotedMessageId, ? this.quotedMessage
: quotedMessage as Message?,
quotedMessageId: quotedMessageId == _nullConst
? _quotedMessageId
: quotedMessageId as String?,
replyCount: replyCount ?? this.replyCount, replyCount: replyCount ?? this.replyCount,
threadParticipants: threadParticipants ?? this.threadParticipants, threadParticipants: threadParticipants ?? this.threadParticipants,
showInChannel: showInChannel ?? this.showInChannel, showInChannel: showInChannel ?? this.showInChannel,
command: command ?? this.command, command: command ?? this.command,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? _createdAt,
silent: silent ?? this.silent, updatedAt: updatedAt ?? _updatedAt,
extraData: extraData ?? this.extraData,
user: user ?? this.user,
shadowed: shadowed ?? this.shadowed,
updatedAt: updatedAt ?? this.updatedAt,
deletedAt: deletedAt ?? this.deletedAt, deletedAt: deletedAt ?? this.deletedAt,
status: status ?? this.status, user: user ?? this.user,
pinned: pinned ?? this.pinned, pinned: pinned ?? this.pinned,
pinnedAt: pinnedAt ?? this.pinnedAt, pinnedAt: pinnedAt ?? this.pinnedAt,
pinnedBy: pinnedBy ?? this.pinnedBy,
pinExpires: 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, i18n: i18n ?? this.i18n,
); );
} }
@@ -331,6 +368,8 @@ class Message extends Equatable {
type: other.type, type: other.type,
attachments: other.attachments, attachments: other.attachments,
mentionedUsers: other.mentionedUsers, mentionedUsers: other.mentionedUsers,
silent: other.silent,
shadowed: other.shadowed,
reactionCounts: other.reactionCounts, reactionCounts: other.reactionCounts,
reactionScores: other.reactionScores, reactionScores: other.reactionScores,
latestReactions: other.latestReactions, latestReactions: other.latestReactions,
@@ -343,17 +382,15 @@ class Message extends Equatable {
showInChannel: other.showInChannel, showInChannel: other.showInChannel,
command: other.command, command: other.command,
createdAt: other.createdAt, createdAt: other.createdAt,
silent: other.silent,
extraData: other.extraData,
user: other.user,
shadowed: other.shadowed,
updatedAt: other.updatedAt, updatedAt: other.updatedAt,
deletedAt: other.deletedAt, deletedAt: other.deletedAt,
status: other.status, user: other.user,
pinned: other.pinned, pinned: other.pinned,
pinnedAt: other.pinnedAt, pinnedAt: other.pinnedAt,
pinExpires: other.pinExpires, pinExpires: other.pinExpires,
pinnedBy: other.pinnedBy, pinnedBy: other.pinnedBy,
extraData: other.extraData,
status: other.status,
i18n: other.i18n, i18n: other.i18n,
); );
@@ -377,8 +414,8 @@ class Message extends Equatable {
shadowed, shadowed,
silent, silent,
command, command,
createdAt, _createdAt,
updatedAt, _updatedAt,
deletedAt, deletedAt,
user, user,
pinned, pinned,
@@ -0,0 +1,96 @@
/// Describes capabilities of a user vis-a-vis a channel
class PermissionType {
/// Capability required to send a message in the channel
/// Channel is not frozen (or user has UseFrozenChannel permission)
/// and user has CreateMessage permission.
static const String sendMessage = 'send-message';
/// Capability required to receive connect events in the channel
static const String connectEvents = 'connect-events';
/// Capability required to send a message
/// Reactions are enabled for the channel, channel is not frozen
/// (or user has UseFrozenChannel permission) and user has
/// CreateReaction permission
static const String sendReaction = 'send-reaction';
/// Capability required to send links in a channel
/// send-message + user has AddLinks permission
static const String sendLinks = 'send-links';
/// Capability required to send thread reply
/// send-message + channel has replies enabled
static const String sendReply = 'send-reply';
/// Capability to freeze a channel
/// User has UpdateChannelFrozen permission.
/// The name implies freezing,
/// but unfreezing is also allowed when this capability is present
static const String freezeChannel = 'freeze-channel';
/// User has UpdateChannelCooldown permission.
/// Allows to enable/disable slow mode in the channel
static const String setChannelCooldown = 'set-channel-cooldown';
/// User has RemoveOwnChannelMembership or UpdateChannelMembers permission
static const String leaveChannel = 'leave-channel';
/// User can mute channel
static const String muteChannel = 'mute-channel';
/// Ability to receive read events
static const String readEvents = 'read-events';
/// Capability required to pin a message in a channel
/// Corresponds to PinMessage permission
static const String pinMessage = 'pin-message';
/// Capability required to quote a message in a channel
static const String quoteMessage = 'quote-message';
/// Capability required to flag a message in a channel
static const String flagMessage = 'flag-message';
/// User has ability to delete any message in the channel
/// User has DeleteMessage permission
/// which applies to any message in the channel
static const String deleteAnyMessage = 'delete-any-message';
/// User has ability to delete their own message in the channel
/// User has DeleteMessage permission which applies only to owned messages
static const String deleteOwnMessage = 'delete-own-message';
/// User has ability to update/edit any message in the channel
/// User has UpdateMessage permission which
/// applies to any message in the channel
static const String updateAnyMessage = 'update-any-message';
/// User has ability to update/edit their own message in the channel
/// User has UpdateMessage permission which applies only to owned messages
static const String updateOwnMessage = 'update-own-message';
/// User can search for message in a channel
/// Search feature is enabled (it will also have
/// permission check in the future)
static const String searchMessages = 'search-messages';
/// Capability required to send typing events in a channel
/// (Typing events are enabled)
static const String sendTypingEvents = 'send-typing-events';
/// Capability required to upload a file in a channel
/// Uploads are enabled and user has UploadAttachment
static const String uploadFile = 'upload-file';
/// Capability required to delete channel
/// User has DeleteChannel permission
static const String deleteChannel = 'delete-channel';
/// Capability required update/edit channel info
/// User has UpdateChannel permission
static const String updateChannel = 'update-channel';
/// Capability required to update/edit channel members
/// Channel is not distinct and user has UpdateChannelMembers permission
static const String updateChannelMembers = 'update-channel-members';
}
@@ -7,6 +7,7 @@ export 'package:dio/src/options.dart';
export 'package:dio/src/options.dart' show ProgressCallback; export 'package:dio/src/options.dart' show ProgressCallback;
export 'package:logging/logging.dart' show Logger, Level; export 'package:logging/logging.dart' show Logger, Level;
export 'package:rate_limiter/rate_limiter.dart'; export 'package:rate_limiter/rate_limiter.dart';
export 'package:uuid/uuid.dart';
export './src/core/api/attachment_file_uploader.dart' export './src/core/api/attachment_file_uploader.dart'
show AttachmentFileUploader; show AttachmentFileUploader;
@@ -36,6 +37,7 @@ export './src/core/util/extension.dart';
export './src/db/chat_persistence_client.dart'; export './src/db/chat_persistence_client.dart';
export './src/event_type.dart'; export './src/event_type.dart';
export './src/location.dart'; export './src/location.dart';
export './src/permission_type.dart';
export './src/ws/connection_status.dart'; export './src/ws/connection_status.dart';
export 'src/client/channel.dart'; export 'src/client/channel.dart';
export 'src/client/client.dart'; export 'src/client/client.dart';
@@ -244,9 +244,13 @@ void main() {
group('`.sendMessage`', () { group('`.sendMessage`', () {
test('should work fine', () async { 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( when(() => client.sendMessage(
any(that: isSameMessageAs(message)), any(that: isSameMessageAs(message)),
@@ -329,6 +333,7 @@ void main() {
.map((it) => .map((it) =>
it.copyWith(uploadState: const UploadState.success())) it.copyWith(uploadState: const UploadState.success()))
.toList(growable: false), .toList(growable: false),
status: MessageSendingStatus.sent,
)); ));
expectLater( expectLater(
@@ -455,7 +460,10 @@ void main() {
group('`.updateMessage`', () { group('`.updateMessage`', () {
test('should work fine', () async { 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() final updateMessageResponse = UpdateMessageResponse()
..message = message; ..message = message;
@@ -530,6 +538,7 @@ void main() {
any(that: isSameMessageAs(message)), any(that: isSameMessageAs(message)),
)).thenAnswer((_) async => UpdateMessageResponse() )).thenAnswer((_) async => UpdateMessageResponse()
..message = message.copyWith( ..message = message.copyWith(
status: MessageSendingStatus.sent,
attachments: attachments attachments: attachments
.map((it) => .map((it) =>
it.copyWith(uploadState: const UploadState.success())) it.copyWith(uploadState: const UploadState.success()))
@@ -678,7 +687,7 @@ void main() {
[ [
isSameMessageAs( isSameMessageAs(
updateMessageResponse.message.copyWith( updateMessageResponse.message.copyWith(
status: MessageSendingStatus.sent, status: MessageSendingStatus.sending,
), ),
matchText: true, matchText: true,
matchSendingStatus: true, matchSendingStatus: true,
@@ -707,7 +716,10 @@ void main() {
group('`.deleteMessage`', () { group('`.deleteMessage`', () {
test('should work fine', () async { test('should work fine', () async {
const messageId = 'test-message-id'; const messageId = 'test-message-id';
final message = Message(id: messageId); final message = Message(
id: messageId,
status: MessageSendingStatus.sent,
);
when(() => client.deleteMessage(messageId)) when(() => client.deleteMessage(messageId))
.thenAnswer((_) async => EmptyResponse()); .thenAnswer((_) async => EmptyResponse());
@@ -744,7 +756,6 @@ void main() {
const messageId = 'test-message-id'; const messageId = 'test-message-id';
final message = Message( final message = Message(
id: messageId, id: messageId,
status: MessageSendingStatus.sending,
); );
expectLater( expectLater(
@@ -1077,7 +1088,10 @@ void main() {
group('`.sendReaction`', () { group('`.sendReaction`', () {
test('should work fine', () async { test('should work fine', () async {
const type = 'test-reaction-type'; 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); final reaction = Reaction(type: type, messageId: message.id);
@@ -1118,7 +1132,10 @@ void main() {
test('should work fine with score passed explicitly', () async { test('should work fine with score passed explicitly', () async {
const type = 'test-reaction-type'; 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 score = 5;
final reaction = Reaction( final reaction = Reaction(
@@ -1178,7 +1195,10 @@ void main() {
test('should work fine with score passed explicitly and in extraData', test('should work fine with score passed explicitly and in extraData',
() async { () async {
const type = 'test-reaction-type'; 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 score = 5;
const extraDataScore = 3; const extraDataScore = 3;
@@ -1249,7 +1269,10 @@ void main() {
'should restore previous message if `client.sendReaction` throws', 'should restore previous message if `client.sendReaction` throws',
() async { () async {
const type = 'test-reaction-type'; 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); final reaction = Reaction(type: type, messageId: message.id);
@@ -1310,6 +1333,7 @@ void main() {
latestReactions: [prevReaction], latestReactions: [prevReaction],
reactionScores: const {prevType: 1}, reactionScores: const {prevType: 1},
reactionCounts: const {prevType: 1}, reactionCounts: const {prevType: 1},
status: MessageSendingStatus.sent,
); );
const type = 'test-reaction-type-2'; const type = 'test-reaction-type-2';
@@ -1341,7 +1365,7 @@ void main() {
emitsInOrder([ emitsInOrder([
[ [
isSameMessageAs( isSameMessageAs(
newMessage.copyWith(status: MessageSendingStatus.sent), newMessage,
matchReactions: true, matchReactions: true,
matchSendingStatus: true, matchSendingStatus: true,
), ),
@@ -1374,6 +1398,7 @@ void main() {
final message = Message( final message = Message(
id: 'test-message-id', id: 'test-message-id',
parentId: 'test-parent-id', // is thread message parentId: 'test-parent-id', // is thread message
status: MessageSendingStatus.sent,
); );
final reaction = Reaction(type: type, messageId: message.id); final reaction = Reaction(type: type, messageId: message.id);
@@ -1423,6 +1448,7 @@ void main() {
final message = Message( final message = Message(
id: 'test-message-id', id: 'test-message-id',
parentId: 'test-parent-id', // is thread message parentId: 'test-parent-id', // is thread message
status: MessageSendingStatus.sent,
); );
final reaction = Reaction(type: type, messageId: message.id); final reaction = Reaction(type: type, messageId: message.id);
@@ -1490,6 +1516,7 @@ void main() {
latestReactions: [prevReaction], latestReactions: [prevReaction],
reactionScores: const {prevType: 1}, reactionScores: const {prevType: 1},
reactionCounts: const {prevType: 1}, reactionCounts: const {prevType: 1},
status: MessageSendingStatus.sent,
); );
const type = 'test-reaction-type-2'; const type = 'test-reaction-type-2';
@@ -1567,6 +1594,7 @@ void main() {
latestReactions: [reaction], latestReactions: [reaction],
reactionScores: const {type: 1}, reactionScores: const {type: 1},
reactionCounts: const {type: 1}, reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
); );
when(() => client.deleteReaction(messageId, type)) when(() => client.deleteReaction(messageId, type))
@@ -1614,6 +1642,7 @@ void main() {
latestReactions: [reaction], latestReactions: [reaction],
reactionScores: const {type: 1}, reactionScores: const {type: 1},
reactionCounts: const {type: 1}, reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
); );
when(() => client.deleteReaction(messageId, type)) when(() => client.deleteReaction(messageId, type))
@@ -1668,11 +1697,13 @@ void main() {
); );
final message = Message( final message = Message(
id: messageId, id: messageId,
parentId: parentId, // is thread parentId: parentId,
// is thread
ownReactions: [reaction], ownReactions: [reaction],
latestReactions: [reaction], latestReactions: [reaction],
reactionScores: const {type: 1}, reactionScores: const {type: 1},
reactionCounts: const {type: 1}, reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
); );
when(() => client.deleteReaction(messageId, type)) when(() => client.deleteReaction(messageId, type))
@@ -1725,6 +1756,7 @@ void main() {
latestReactions: [reaction], latestReactions: [reaction],
reactionScores: const {type: 1}, reactionScores: const {type: 1},
reactionCounts: const {type: 1}, reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
); );
when(() => client.deleteReaction(messageId, type)) when(() => client.deleteReaction(messageId, type))
@@ -481,14 +481,21 @@ void main() {
final path = '${_getChannelUrl(channelId, channelType)}/truncate'; final path = '${_getChannelUrl(channelId, channelType)}/truncate';
when(() => client.post(path)).thenAnswer( when(() => client.post(
path,
data: {},
))
.thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{})); (_) async => successResponse(path, data: <String, dynamic>{}));
final res = await channelApi.truncateChannel(channelId, channelType); final res = await channelApi.truncateChannel(channelId, channelType);
expect(res, isNotNull); expect(res, isNotNull);
verify(() => client.post(path)).called(1); verify(() => client.post(
path,
data: {},
)).called(1);
verifyNoMoreInteractions(client); verifyNoMoreInteractions(client);
}); });
@@ -32,6 +32,7 @@ void main() {
data: { data: {
'message': message, 'message': message,
'skip_push': false, 'skip_push': false,
'skip_enrich_url': false,
}, },
)).thenAnswer((_) async => successResponse(path, data: { )).thenAnswer((_) async => successResponse(path, data: {
'message': message.toJson(), 'message': message.toJson(),
@@ -58,6 +59,7 @@ void main() {
data: { data: {
'message': message, 'message': message,
'skip_push': true, 'skip_push': true,
'skip_enrich_url': false,
}, },
)).thenAnswer((_) async => successResponse(path, data: { )).thenAnswer((_) async => successResponse(path, data: {
'message': message.toJson(), 'message': message.toJson(),
@@ -137,7 +139,10 @@ void main() {
when(() => client.post( when(() => client.post(
path, path,
data: {'message': message}, data: {
'message': message,
'skip_enrich_url': false,
},
)).thenAnswer( )).thenAnswer(
(_) async => successResponse(path, data: {'message': message.toJson()}), (_) async => successResponse(path, data: {'message': message.toJson()}),
); );
@@ -162,7 +167,11 @@ void main() {
when(() => client.put( when(() => client.put(
path, path,
data: {'set': set, 'unset': unset}, data: {
'set': set,
'unset': unset,
'skip_enrich_url': false,
},
)).thenAnswer( )).thenAnswer(
(_) async => successResponse(path, data: {'message': message.toJson()}), (_) async => successResponse(path, data: {'message': message.toJson()}),
); );
@@ -180,7 +189,11 @@ void main() {
verify(() => client.put( verify(() => client.put(
path, path,
data: {'set': set, 'unset': unset}, data: {
'set': set,
'unset': unset,
'skip_enrich_url': false,
},
)).called(1); )).called(1);
verifyNoMoreInteractions(client); verifyNoMoreInteractions(client);
}); });
+11 -1
View File
@@ -1,5 +1,10 @@
## 3.5.1 ## 3.5.1
🛑️ Breaking Changes
- `pinPermissions` is no longer needed in `MessageListView`.
- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController`
🐞 Fixed 🐞 Fixed
- Mentions overlay now doesn't overflow when there is not enough height available - Mentions overlay now doesn't overflow when there is not enough height available
@@ -31,7 +36,12 @@
✅ Added ✅ 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 ## 3.3.2
@@ -87,7 +87,7 @@ class MyApp extends StatelessWidget {
/// A list of messages sent in the current channel. /// 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 /// functionalities fetching the messages from the api and showing them in a
/// listView. /// listView.
class ChannelPage extends StatelessWidget { class ChannelPage extends StatelessWidget {
@@ -98,13 +98,13 @@ class ChannelPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) => Scaffold( Widget build(BuildContext context) => Scaffold(
appBar: const ChannelHeader(), appBar: const StreamChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: const <Widget>[
Expanded( 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({ const ChannelListPage({
Key? key, Key? key,
this.onTap, this.onTap,
@@ -93,21 +93,25 @@ class ChannelListPage extends StatelessWidget {
final void Function(Channel)? onTap; final void Function(Channel)? onTap;
@override @override
Widget build(BuildContext context) => Scaffold( State<ChannelListPage> createState() => _ChannelListPageState();
body: ChannelsBloc(
child: ChannelListView(
onChannelTap: onTap != null
? (channel, _) {
onTap!(channel);
} }
: null,
class _ChannelListPageState extends State<ChannelListPage> {
late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
limit: 20, limit: 20,
), );
@override
Widget build(BuildContext context) => Scaffold(
body: StreamChannelListView(
onChannelTap: widget.onTap,
controller: _listController,
), ),
); );
} }
@@ -121,15 +125,15 @@ class ChannelPage extends StatelessWidget {
Widget build(BuildContext context) => Navigator( Widget build(BuildContext context) => Navigator(
onGenerateRoute: (settings) => MaterialPageRoute( onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => Scaffold( builder: (context) => Scaffold(
appBar: const ChannelHeader( appBar: const StreamChannelHeader(
showBackButton: false, showBackButton: false,
), ),
body: Column( body: Column(
children: const <Widget>[ children: const <Widget>[
Expanded( Expanded(
child: MessageListView(), child: StreamMessageListView(),
), ),
MessageInput(), StreamMessageInput(),
], ],
), ),
), ),
@@ -27,7 +27,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// - We make [StreamChat] the root Widget of our application /// - We make [StreamChat] the root Widget of our application
/// ///
/// - We create a single [ChannelPage] widget under [StreamChat] with three /// - 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. /// If you now run the simulator you will see a single channel UI.
void main() async { void main() async {
@@ -90,18 +90,15 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
@override @override
// ignore: prefer_expression_function_bodies Widget build(BuildContext context) => Scaffold(
Widget build(BuildContext context) { appBar: const StreamChannelHeader(),
return Scaffold(
appBar: const ChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: const <Widget>[
Expanded( Expanded(
child: MessageListView(), child: StreamMessageListView(),
), ),
MessageInput(), StreamMessageInput(),
], ],
), ),
); );
} }
}
@@ -25,7 +25,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// The [ChannelListPage] widget retrieves the list of channels based on a /// The [ChannelListPage] widget retrieves the list of channels based on a
/// custom query and ordering. In this case we are showing the list of /// 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 /// 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 /// and updates automatically when new channels are created or when a new
/// message is added to a channel. /// message is added to a channel.
void main() async { void main() async {
@@ -62,34 +62,54 @@ class MyApp extends StatelessWidget {
client: client, client: client,
child: child, child: child,
), ),
home: const ChannelListPage(), home: ChannelListPage(
client: client,
),
); );
} }
} }
class ChannelListPage extends StatelessWidget { class ChannelListPage extends StatefulWidget {
const ChannelListPage({ const ChannelListPage({
Key? key, Key? key,
required this.client,
}) : super(key: key); }) : super(key: key);
final StreamChatClient client;
@override @override
// ignore: prefer_expression_function_bodies State<ChannelListPage> createState() => _ChannelListPageState();
Widget build(BuildContext context) { }
return Scaffold(
body: ChannelsBloc( class _ChannelListPageState extends State<ChannelListPage> {
child: ChannelListView( late final _controller = StreamChannelListController(
client: widget.client,
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
limit: 20, );
channelWidget: const ChannelPage(),
@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 { class ChannelPage extends StatelessWidget {
const ChannelPage({ const ChannelPage({
@@ -97,18 +117,15 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
@override @override
// ignore: prefer_expression_function_bodies Widget build(BuildContext context) => Scaffold(
Widget build(BuildContext context) { appBar: const StreamChannelHeader(),
return Scaffold(
appBar: const ChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: const <Widget>[
Expanded( Expanded(
child: MessageListView(), child: StreamMessageListView(),
), ),
MessageInput(), StreamMessageInput(),
], ],
), ),
); );
} }
}
@@ -15,8 +15,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// We start by changing how channel previews are shown in the channel list /// We start by changing how channel previews are shown in the channel list
/// and include the number of unread messages for each. /// and include the number of unread messages for each.
/// ///
/// We're passing a custom widget to [ChannelListView.channelPreviewBuilder]; /// We're passing a custom widget to [StreamChannelListView.channelPreviewBuilder];
/// this will override the default [ChannelPreview] and allows you to create /// this will override the default [StreamChannelPreview] and allows you to create
/// one yourself. /// one yourself.
/// ///
/// There are a couple interesting things we do in this widget: /// There are a couple interesting things we do in this widget:
@@ -68,31 +68,49 @@ class MyApp extends StatelessWidget {
} }
} }
class ChannelListPage extends StatelessWidget { class ChannelListPage extends StatefulWidget {
const ChannelListPage({ const ChannelListPage({
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
// ignore: prefer_expression_function_bodies State<ChannelListPage> createState() => _ChannelListPageState();
Widget build(BuildContext context) { }
return Scaffold(
body: ChannelsBloc( class _ChannelListPageState extends State<ChannelListPage> {
child: ChannelListView( late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
channelPreviewBuilder: _channelPreviewBuilder, sort: const [SortOption('last_message_at')],
// sort: [SortOption('last_message_at')],
limit: 20, limit: 20,
channelWidget: const ChannelPage(), );
@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, Channel channel) { Widget _channelPreviewBuilder(
BuildContext context,
Channel channel,
StreamChannelListTile defaultTile,
) {
final lastMessage = channel.state?.messages.reversed.firstWhereOrNull( final lastMessage = channel.state?.messages.reversed.firstWhereOrNull(
(message) => !message.isDeleted, (message) => !message.isDeleted,
); );
@@ -112,16 +130,17 @@ class ChannelListPage extends StatelessWidget {
), ),
); );
}, },
leading: ChannelAvatar( leading: StreamChannelAvatar(
channel: channel, channel: channel,
), ),
title: ChannelName( title: StreamChannelName(
textStyle: ChannelPreviewTheme.of(context).titleStyle!.copyWith( textStyle: StreamChannelPreviewTheme.of(context).titleStyle!.copyWith(
color: StreamChatTheme.of(context) color: StreamChatTheme.of(context)
.colorTheme .colorTheme
.textHighEmphasis .textHighEmphasis
.withOpacity(opacity), .withOpacity(opacity),
), ),
channel: channel,
), ),
subtitle: Text(subtitle), subtitle: Text(subtitle),
trailing: channel.state!.unreadCount > 0 trailing: channel.state!.unreadCount > 0
@@ -143,13 +162,13 @@ class ChannelPage extends StatelessWidget {
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const ChannelHeader(), appBar: const StreamChannelHeader(),
body: Column( body: Column(
children: const <Widget>[ children: const <Widget>[
Expanded( Expanded(
child: MessageListView(), child: StreamMessageListView(),
), ),
MessageInput(), StreamMessageInput(),
], ],
), ),
); );
@@ -8,8 +8,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// to create sub-conversations inside the same channel. /// to create sub-conversations inside the same channel.
/// ///
/// Using threaded conversations is very simple and mostly a matter of /// Using threaded conversations is very simple and mostly a matter of
/// plugging the [MessageListView] to another widget that renders the widget. /// plugging the [StreamMessageListView] to another widget that renders the widget.
/// To make this simple, such a widget only needs to build [MessageListView] /// To make this simple, such a widget only needs to build [StreamMessageListView]
/// with the parent attribute set to the threads root message. /// 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 /// Now we can open threads and create new ones as well. If you long-press a
@@ -53,28 +53,42 @@ class MyApp extends StatelessWidget {
} }
} }
class ChannelListPage extends StatelessWidget { class ChannelListPage extends StatefulWidget {
const ChannelListPage({ const ChannelListPage({
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
// ignore: prefer_expression_function_bodies State<ChannelListPage> createState() => _ChannelListPageState();
Widget build(BuildContext context) { }
return Scaffold(
body: ChannelsBloc( class _ChannelListPageState extends State<ChannelListPage> {
child: ChannelListView( late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
limit: 20, limit: 20,
channelWidget: const ChannelPage(), );
@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 { class ChannelPage extends StatelessWidget {
@@ -83,25 +97,22 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
@override @override
// ignore: prefer_expression_function_bodies Widget build(BuildContext context) => Scaffold(
Widget build(BuildContext context) { appBar: const StreamChannelHeader(),
return Scaffold(
appBar: const ChannelHeader(),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: MessageListView( child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage( threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage, parent: parentMessage,
), ),
), ),
), ),
const MessageInput(), const StreamMessageInput(),
], ],
), ),
); );
} }
}
class ThreadPage extends StatelessWidget { class ThreadPage extends StatelessWidget {
const ThreadPage({ const ThreadPage({
@@ -115,18 +126,20 @@ class ThreadPage extends StatelessWidget {
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: ThreadHeader( appBar: StreamThreadHeader(
parent: parent!, parent: parent!,
), ),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: MessageListView( child: StreamMessageListView(
parentMessage: parent, parentMessage: parent,
), ),
), ),
MessageInput( StreamMessageInput(
parentMessage: parent, messageInputController: MessageInputController(
message: Message(parentId: parent!.id),
),
), ),
], ],
), ),
@@ -8,7 +8,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// the SDK supports easily. /// the SDK supports easily.
/// ///
/// Replacing the built-in message component with your own is done by passing /// 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 /// The message builder function will get the usual [BuildContext] argument
/// as well as the [Message] object and its position inside the list. /// as well as the [Message] object and its position inside the list.
@@ -59,28 +59,42 @@ class MyApp extends StatelessWidget {
} }
} }
class ChannelListPage extends StatelessWidget { class ChannelListPage extends StatefulWidget {
const ChannelListPage({ const ChannelListPage({
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
// ignore: prefer_expression_function_bodies State<ChannelListPage> createState() => _ChannelListPageState();
Widget build(BuildContext context) { }
return Scaffold(
body: ChannelsBloc( class _ChannelListPageState extends State<ChannelListPage> {
child: ChannelListView( late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
limit: 20, limit: 20,
channelWidget: const ChannelPage(), );
@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 { class ChannelPage extends StatelessWidget {
@@ -92,15 +106,15 @@ class ChannelPage extends StatelessWidget {
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: const ChannelHeader(), appBar: const StreamChannelHeader(),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: MessageListView( child: StreamMessageListView(
messageBuilder: _messageBuilder, messageBuilder: _messageBuilder,
), ),
), ),
const MessageInput(), const StreamMessageInput(),
], ],
), ),
); );
@@ -110,7 +124,7 @@ class ChannelPage extends StatelessWidget {
BuildContext context, BuildContext context,
MessageDetails details, MessageDetails details,
List<Message> messages, List<Message> messages,
MessageWidget _, StreamMessageWidget _,
) { ) {
final message = details.message; final message = details.message;
final isCurrentUser = final isCurrentUser =
@@ -58,24 +58,24 @@ class MyApp extends StatelessWidget {
final defaultTheme = StreamChatThemeData.fromTheme(themeData); final defaultTheme = StreamChatThemeData.fromTheme(themeData);
final colorTheme = defaultTheme.colorTheme; final colorTheme = defaultTheme.colorTheme;
final customTheme = defaultTheme.merge(StreamChatThemeData( final customTheme = defaultTheme.merge(StreamChatThemeData(
channelPreviewTheme: ChannelPreviewThemeData( channelPreviewTheme: StreamChannelPreviewThemeData(
avatarTheme: AvatarThemeData( avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
messageListViewTheme: const MessageListViewThemeData( messageListViewTheme: const StreamMessageListViewThemeData(
backgroundColor: Colors.grey, backgroundColor: Colors.grey,
backgroundImage: DecorationImage( backgroundImage: DecorationImage(
image: AssetImage('assets/background_doodle.png'), image: AssetImage('assets/background_doodle.png'),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
otherMessageTheme: MessageThemeData( otherMessageTheme: StreamMessageThemeData(
messageBackgroundColor: colorTheme.textHighEmphasis, messageBackgroundColor: colorTheme.textHighEmphasis,
messageTextStyle: TextStyle( messageTextStyle: TextStyle(
color: colorTheme.barsBg, color: colorTheme.barsBg,
), ),
avatarTheme: AvatarThemeData( avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
), ),
@@ -93,28 +93,41 @@ class MyApp extends StatelessWidget {
} }
} }
class ChannelListPage extends StatelessWidget { class ChannelListPage extends StatefulWidget {
const ChannelListPage({ const ChannelListPage({
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override @override
// ignore: prefer_expression_function_bodies State<ChannelListPage> createState() => _ChannelListPageState();
Widget build(BuildContext context) { }
return Scaffold(
body: ChannelsBloc( class _ChannelListPageState extends State<ChannelListPage> {
child: ChannelListView( late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_( filter: Filter.in_(
'members', 'members',
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
limit: 20, limit: 20,
channelWidget: const ChannelPage(), );
@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 { class ChannelPage extends StatelessWidget {
@@ -123,25 +136,22 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
@override @override
// ignore: prefer_expression_function_bodies Widget build(BuildContext context) => Scaffold(
Widget build(BuildContext context) { appBar: const StreamChannelHeader(),
return Scaffold(
appBar: const ChannelHeader(),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: MessageListView( child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage( threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage, parent: parentMessage,
), ),
), ),
), ),
const MessageInput(), const StreamMessageInput(),
], ],
), ),
); );
} }
}
class ThreadPage extends StatelessWidget { class ThreadPage extends StatelessWidget {
const ThreadPage({ const ThreadPage({
@@ -155,18 +165,20 @@ class ThreadPage extends StatelessWidget {
// ignore: prefer_expression_function_bodies // ignore: prefer_expression_function_bodies
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: ThreadHeader( appBar: StreamThreadHeader(
parent: parent!, parent: parent!,
), ),
body: Column( body: Column(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: MessageListView( child: StreamMessageListView(
parentMessage: parent, parentMessage: parent,
), ),
), ),
MessageInput( StreamMessageInput(
parentMessage: parent, messageInputController: MessageInputController(
message: Message(parentId: parent!.id),
),
), ),
], ],
), ),
@@ -1,17 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro attachment_title}
@Deprecated("Use 'StreamAttachmentTitle' instead")
typedef AttachmentTitle = StreamAttachmentTitle;
/// {@template attachment_title}
/// Title for attachments /// Title for attachments
class AttachmentTitle extends StatelessWidget { /// {@endtemplate}
class StreamAttachmentTitle extends StatelessWidget {
/// Supply attachment and theme for constructing title /// Supply attachment and theme for constructing title
const AttachmentTitle({ const StreamAttachmentTitle({
Key? key, Key? key,
required this.attachment, required this.attachment,
required this.messageTheme, required this.messageTheme,
}) : super(key: key); }) : super(key: key);
/// Theme to apply to text /// Theme to apply to text
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// Attachment data to display /// Attachment data to display
final Attachment attachment; final Attachment attachment;
@@ -9,10 +9,16 @@ typedef InProgressBuilder = Widget Function(BuildContext, int, int);
/// Widget to build on failure /// Widget to build on failure
typedef FailedBuilder = Widget Function(BuildContext, String); 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 /// Widget to display attachment upload state
class AttachmentUploadStateBuilder extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating an [AttachmentUploadStateBuilder] widget class StreamAttachmentUploadStateBuilder extends StatelessWidget {
const AttachmentUploadStateBuilder({ /// Constructor for creating an [StreamAttachmentUploadStateBuilder] widget
const StreamAttachmentUploadStateBuilder({
Key? key, Key? key,
required this.message, required this.message,
required this.attachment, required this.attachment,
@@ -137,7 +143,7 @@ class _PreparingState extends StatelessWidget {
), ),
Align( Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: UploadProgressIndicator( child: StreamUploadProgressIndicator(
uploaded: 0, uploaded: 0,
total: double.maxFinite.toInt(), total: double.maxFinite.toInt(),
), ),
@@ -177,7 +183,7 @@ class _InProgressState extends StatelessWidget {
), ),
Align( Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: UploadProgressIndicator( child: StreamUploadProgressIndicator(
uploaded: sent, uploaded: sent,
total: total, 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 for deriving attachment types
abstract class AttachmentWidget extends StatelessWidget { /// {@endtemplate}
abstract class StreamAttachmentWidget extends StatelessWidget {
/// Constructor for creating attachment widget /// Constructor for creating attachment widget
const AttachmentWidget({ const StreamAttachmentWidget({
Key? key, Key? key,
required this.message, required this.message,
required this.attachment, 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/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// {@macro file_attachment}
@Deprecated("Use 'StreamFileAttachment' instead")
typedef FileAttachment = StreamFileAttachment;
/// {@template file_attachment}
/// Widget for displaying file attachments /// 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' /// Constructor for creating a widget when attachment is of type 'file'
const FileAttachment({ const StreamFileAttachment({
Key? key, Key? key,
required Message message, required Message message,
required Attachment attachment, required Attachment attachment,
@@ -157,7 +163,7 @@ class FileAttachment extends AttachmentWidget {
type: MaterialType.transparency, type: MaterialType.transparency,
shape: _getDefaultShape(context), shape: _getDefaultShape(context),
child: source.when( child: source.when(
local: () => VideoThumbnailImage( local: () => StreamVideoThumbnailImage(
fit: BoxFit.cover, fit: BoxFit.cover,
video: attachment.file!.path!, video: attachment.file!.path!,
placeholderBuilder: (_) => const Center( placeholderBuilder: (_) => const Center(
@@ -168,7 +174,7 @@ class FileAttachment extends AttachmentWidget {
), ),
), ),
), ),
network: () => VideoThumbnailImage( network: () => StreamVideoThumbnailImage(
fit: BoxFit.cover, fit: BoxFit.cover,
video: attachment.assetUrl!, video: attachment.assetUrl!,
placeholderBuilder: (_) => const Center( placeholderBuilder: (_) => const Center(
@@ -278,7 +284,7 @@ class FileAttachment extends AttachmentWidget {
); );
return attachment.uploadState.when( return attachment.uploadState.when(
preparing: () => Text(fileSize(size), style: textStyle), preparing: () => Text(fileSize(size), style: textStyle),
inProgress: (sent, total) => UploadProgressIndicator( inProgress: (sent, total) => StreamUploadProgressIndicator(
uploaded: sent, uploaded: sent,
total: total, total: total,
showBackground: false, 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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 /// Widget for showing a GIF attachment
class GiphyAttachment extends AttachmentWidget { /// {@endtemplate}
/// Constructor for creating a [GiphyAttachment] widget class StreamGiphyAttachment extends StreamAttachmentWidget {
const GiphyAttachment({ /// Constructor for creating a [StreamGiphyAttachment] widget
const StreamGiphyAttachment({
Key? key, Key? key,
required Message message, required Message message,
required Attachment attachment, required Attachment attachment,
@@ -228,7 +234,7 @@ class GiphyAttachment extends AttachmentWidget {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Padding( child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: VisibleFootnote(), child: StreamVisibleFootnote(),
), ),
), ),
], ],
@@ -243,7 +249,7 @@ class GiphyAttachment extends AttachmentWidget {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
child: FullScreenMedia( child: StreamFullScreenMedia(
mediaAttachments: message.attachments, mediaAttachments: message.attachments,
startIndex: message.attachments.indexOf(attachment), startIndex: message.attachments.indexOf(attachment),
userName: message.user?.name, userName: message.user?.name,
@@ -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/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 /// Widget for showing an image attachment
class ImageAttachment extends AttachmentWidget { /// {@endtemplate}
/// Constructor for creating a [ImageAttachment] widget class StreamImageAttachment extends StreamAttachmentWidget {
const ImageAttachment({ /// Constructor for creating a [StreamImageAttachment] widget
const StreamImageAttachment({
Key? key, Key? key,
required Message message, required Message message,
required Attachment attachment, required Attachment attachment,
@@ -25,8 +31,8 @@ class ImageAttachment extends AttachmentWidget {
size: size, size: size,
); );
/// [MessageThemeData] for showing image title /// [StreamMessageThemeData] for showing image title
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// Flag for showing title /// Flag for showing title
final bool showTitle; final bool showTitle;
@@ -137,7 +143,7 @@ class ImageAttachment extends AttachmentWidget {
StreamChannel.of(context).channel; StreamChannel.of(context).channel;
return StreamChannel( return StreamChannel(
channel: channel, channel: channel,
child: FullScreenMedia( child: StreamFullScreenMedia(
mediaAttachments: message.attachments, mediaAttachments: message.attachments,
startIndex: startIndex:
message.attachments.indexOf(attachment), message.attachments.indexOf(attachment),
@@ -155,7 +161,7 @@ class ImageAttachment extends AttachmentWidget {
), ),
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: AttachmentUploadStateBuilder( child: StreamAttachmentUploadStateBuilder(
message: message, message: message,
attachment: attachment, attachment: attachment,
), ),
@@ -166,7 +172,7 @@ class ImageAttachment extends AttachmentWidget {
if (showTitle && attachment.title != null) if (showTitle && attachment.title != null)
Material( Material(
color: messageTheme.messageBackgroundColor, color: messageTheme.messageBackgroundColor,
child: AttachmentTitle( child: StreamAttachmentTitle(
messageTheme: messageTheme, messageTheme: messageTheme,
attachment: attachment, attachment: attachment,
), ),
@@ -2,10 +2,16 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro url_attachment}
@Deprecated("Use 'StreamUrlAttachment' instead")
typedef UrlAttachment = StreamUrlAttachment;
/// {@template url_attachment}
/// Widget to display URL attachment /// Widget to display URL attachment
class UrlAttachment extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating a [UrlAttachment] class StreamUrlAttachment extends StatelessWidget {
const UrlAttachment({ /// Constructor for creating a [StreamUrlAttachment]
const StreamUrlAttachment({
Key? key, Key? key,
required this.urlAttachment, required this.urlAttachment,
required this.hostDisplayName, required this.hostDisplayName,
@@ -26,8 +32,8 @@ class UrlAttachment extends StatelessWidget {
/// Padding for text /// Padding for text
final EdgeInsets textPadding; final EdgeInsets textPadding;
/// [MessageThemeData] for showing image title /// [StreamMessageThemeData] for showing image title
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// The function called when tapping on a link /// The function called when tapping on a link
final void Function(String)? onLinkTap; 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/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 /// Widget for showing a video attachment
class VideoAttachment extends AttachmentWidget { /// {@endtemplate}
/// Constructor for creating a [VideoAttachment] widget class StreamVideoAttachment extends StreamAttachmentWidget {
const VideoAttachment({ /// Constructor for creating a [StreamVideoAttachment] widget
const StreamVideoAttachment({
Key? key, Key? key,
required Message message, required Message message,
required Attachment attachment, required Attachment attachment,
@@ -23,8 +29,8 @@ class VideoAttachment extends AttachmentWidget {
size: size, size: size,
); );
/// [MessageThemeData] for showing title /// [StreamMessageThemeData] for showing title
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// Callback when show message is tapped /// Callback when show message is tapped
final ShowMessageCallback? onShowMessage; final ShowMessageCallback? onShowMessage;
@@ -43,7 +49,7 @@ class VideoAttachment extends AttachmentWidget {
} }
return _buildVideoAttachment( return _buildVideoAttachment(
context, context,
VideoThumbnailImage( StreamVideoThumbnailImage(
video: attachment.file!.path!, video: attachment.file!.path!,
height: size?.height, height: size?.height,
width: size?.width, width: size?.width,
@@ -58,7 +64,7 @@ class VideoAttachment extends AttachmentWidget {
} }
return _buildVideoAttachment( return _buildVideoAttachment(
context, context,
VideoThumbnailImage( StreamVideoThumbnailImage(
video: attachment.assetUrl!, video: attachment.assetUrl!,
height: size?.height, height: size?.height,
width: size?.width, width: size?.width,
@@ -84,7 +90,7 @@ class VideoAttachment extends AttachmentWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (_) => StreamChannel( builder: (_) => StreamChannel(
channel: channel, channel: channel,
child: FullScreenMedia( child: StreamFullScreenMedia(
mediaAttachments: message.attachments, mediaAttachments: message.attachments,
startIndex: startIndex:
message.attachments.indexOf(attachment), message.attachments.indexOf(attachment),
@@ -111,7 +117,7 @@ class VideoAttachment extends AttachmentWidget {
), ),
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: AttachmentUploadStateBuilder( child: StreamAttachmentUploadStateBuilder(
message: message, message: message,
attachment: attachment, attachment: attachment,
), ),
@@ -123,7 +129,7 @@ class VideoAttachment extends AttachmentWidget {
if (attachment.title != null) if (attachment.title != null)
Material( Material(
color: messageTheme.messageBackgroundColor, color: messageTheme.messageBackgroundColor,
child: AttachmentTitle( child: StreamAttachmentTitle(
messageTheme: messageTheme, messageTheme: messageTheme,
attachment: attachment, attachment: attachment,
), ),
@@ -58,7 +58,7 @@ class AttachmentActionsModal extends StatelessWidget {
/// List of custom actions /// List of custom actions
final List<AttachmentAction> customActions; final List<AttachmentAction> customActions;
/// Creates a copy of [MessageWidget] with specified attributes overridden. /// Creates a copy of [StreamMessageWidget] with specified attributes overridden.
AttachmentActionsModal copyWith({ AttachmentActionsModal copyWith({
Key? key, Key? key,
int? currentIndex, int? currentIndex,
@@ -50,7 +50,7 @@ class StreamBackButton extends StatelessWidget {
Positioned( Positioned(
top: 7, top: 7,
right: 7, right: 7,
child: UnreadIndicator( child: StreamUnreadIndicator(
cid: cid, cid: cid,
), ),
), ),
@@ -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 /// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme]. /// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
@Deprecated(
"'ChannelAvatar' is deprecated and shouldn't be used. "
"Please use 'StreamChannelAvatar' instead.",
)
class ChannelAvatar extends StatelessWidget { class ChannelAvatar extends StatelessWidget {
/// Instantiate a new ChannelImage /// Instantiate a new ChannelImage
const ChannelAvatar({ const ChannelAvatar({
@@ -147,7 +152,7 @@ class ChannelAvatar extends StatelessWidget {
return BetterStreamBuilder<User>( return BetterStreamBuilder<User>(
stream: streamChat.client.state.currentUserStream.map((it) => it!), stream: streamChat.client.state.currentUserStream.map((it) => it!),
initialData: currentUser, initialData: currentUser,
builder: (context, user) => UserAvatar( builder: (context, user) => StreamUserAvatar(
borderRadius: borderRadius ?? previewTheme?.borderRadius, borderRadius: borderRadius ?? previewTheme?.borderRadius,
user: user, user: user,
constraints: constraints ?? previewTheme?.constraints, constraints: constraints ?? previewTheme?.constraints,
@@ -170,7 +175,7 @@ class ChannelAvatar extends StatelessWidget {
), ),
), ),
initialData: member, initialData: member,
builder: (context, member) => UserAvatar( builder: (context, member) => StreamUserAvatar(
borderRadius: borderRadius ?? previewTheme?.borderRadius, borderRadius: borderRadius ?? previewTheme?.borderRadius,
user: member.user!, user: member.user!,
constraints: constraints ?? previewTheme?.constraints, constraints: constraints ?? previewTheme?.constraints,
@@ -183,7 +188,7 @@ class ChannelAvatar extends StatelessWidget {
} }
// Group conversation // Group conversation
return GroupAvatar( return StreamGroupAvatar(
members: otherMembers, members: otherMembers,
borderRadius: borderRadius ?? previewTheme?.borderRadius, borderRadius: borderRadius ?? previewTheme?.borderRadius,
constraints: constraints ?? previewTheme?.constraints, 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'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Bottom Sheet with options /// Bottom Sheet with options
@Deprecated("Use 'StreamChannelInfoBottomSheet' instead")
class ChannelBottomSheet extends StatefulWidget { class ChannelBottomSheet extends StatefulWidget {
/// Constructor for creating bottom sheet /// Constructor for creating bottom sheet
const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key); const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key);
@@ -15,11 +16,12 @@ class ChannelBottomSheet extends StatefulWidget {
_ChannelBottomSheetState createState() => _ChannelBottomSheetState(); _ChannelBottomSheetState createState() => _ChannelBottomSheetState();
} }
// ignore: deprecated_member_use_from_same_package
class _ChannelBottomSheetState extends State<ChannelBottomSheet> { class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
bool _showActions = true; bool _showActions = true;
late StreamChannelState _streamChannelState; late StreamChannelState _streamChannelState;
late ChannelPreviewThemeData _channelPreviewThemeData; late StreamChannelPreviewThemeData _channelPreviewThemeData;
late StreamChatThemeData _streamChatThemeData; late StreamChatThemeData _streamChatThemeData;
late StreamChatState _streamChatState; late StreamChatState _streamChatState;
@@ -53,7 +55,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: ChannelName( child: StreamChannelName(
channel: channel,
textStyle: _streamChatThemeData.textTheme.headlineBold, textStyle: _streamChatThemeData.textTheme.headlineBold,
), ),
), ),
@@ -62,7 +65,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
height: 5, height: 5,
), ),
Center( Center(
child: ChannelInfo( child: StreamChannelInfo(
showTypingIndicator: false, showTypingIndicator: false,
channel: _streamChannelState.channel, channel: _streamChannelState.channel,
textStyle: _channelPreviewThemeData.subtitleStyle, textStyle: _channelPreviewThemeData.subtitleStyle,
@@ -74,7 +77,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
if (channel.isDistinct && channel.memberCount == 2) if (channel.isDistinct && channel.memberCount == 2)
Column( Column(
children: [ children: [
UserAvatar( StreamUserAvatar(
user: members user: members
.firstWhere( .firstWhere(
(e) => e.user?.id != userAsMember.user?.id, (e) => e.user?.id != userAsMember.user?.id,
@@ -117,7 +120,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column( child: Column(
children: [ children: [
UserAvatar( StreamUserAvatar(
user: members[index].user!, user: members[index].user!,
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 64, height: 64,
@@ -145,7 +148,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
const SizedBox( const SizedBox(
height: 24, height: 24,
), ),
OptionListTile( StreamOptionListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.user( child: StreamSvgIcon.user(
@@ -155,8 +158,10 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
title: context.translations.viewInfoLabel, title: context.translations.viewInfoLabel,
onTap: widget.onViewInfoTap, onTap: widget.onViewInfoTap,
), ),
if (!channel.isDistinct) if (!channel.isDistinct &&
OptionListTile( channel.ownCapabilities
.contains(PermissionType.leaveChannel))
StreamOptionListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.userRemove( child: StreamSvgIcon.userRemove(
@@ -174,8 +179,10 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
}); });
}, },
), ),
if (isOwner) if (isOwner &&
OptionListTile( channel.ownCapabilities
.contains(PermissionType.deleteChannel))
StreamOptionListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.delete( child: StreamSvgIcon.delete(
@@ -194,7 +201,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
}); });
}, },
), ),
OptionListTile( StreamOptionListTile(
leading: Padding( leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.closeSmall( child: StreamSvgIcon.closeSmall(
@@ -215,7 +222,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
void didChangeDependencies() { void didChangeDependencies() {
_streamChannelState = StreamChannel.of(context); _streamChannelState = StreamChannel.of(context);
_streamChatThemeData = StreamChatTheme.of(context); _streamChatThemeData = StreamChatTheme.of(context);
_channelPreviewThemeData = ChannelPreviewTheme.of(context); _channelPreviewThemeData = StreamChannelPreviewTheme.of(context);
_streamChatState = StreamChat.of(context); _streamChatState = StreamChat.of(context);
super.didChangeDependencies(); 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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png)
/// ///
@@ -49,9 +54,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// The widget components render the ui based on the first ancestor of type /// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. /// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { /// {@endtemplate}
class StreamChannelHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Creates a channel header /// Creates a channel header
const ChannelHeader({ const StreamChannelHeader({
Key? key, Key? key,
this.showBackButton = true, this.showBackButton = true,
this.onBackPressed, this.onBackPressed,
@@ -96,16 +103,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
final Widget? leading; final Widget? leading;
/// AppBar actions /// AppBar actions
/// By default it shows the [ChannelAvatar] /// By default it shows the [StreamChannelAvatar]
final List<Widget>? actions; final List<Widget>? actions;
/// The background color for this [ChannelHeader]. /// The background color for this [StreamChannelHeader].
final Color? backgroundColor; final Color? backgroundColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
final channelHeaderTheme = ChannelHeaderTheme.of(context); final channelHeaderTheme = StreamChannelHeaderTheme.of(context);
final leadingWidget = leading ?? final leadingWidget = leading ??
(showBackButton (showBackButton
@@ -115,7 +122,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
) )
: const SizedBox()); : const SizedBox());
return ConnectionStatusBuilder( return StreamConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
var statusString = ''; var statusString = '';
var showStatus = true; var showStatus = true;
@@ -135,7 +142,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
return InfoTile( return StreamInfoTile(
showMessage: showConnectionStateTile && showStatus, showMessage: showConnectionStateTile && showStatus,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
@@ -152,7 +159,8 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
Padding( Padding(
padding: const EdgeInsets.only(right: 10), padding: const EdgeInsets.only(right: 10),
child: Center( child: Center(
child: ChannelAvatar( child: StreamChannelAvatar(
channel: channel,
borderRadius: borderRadius:
channelHeaderTheme.avatarTheme?.borderRadius, channelHeaderTheme.avatarTheme?.borderRadius,
constraints: constraints:
@@ -172,12 +180,13 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[ children: <Widget>[
title ?? title ??
ChannelName( StreamChannelName(
channel: channel,
textStyle: channelHeaderTheme.titleStyle, textStyle: channelHeaderTheme.titleStyle,
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
subtitle ?? subtitle ??
ChannelInfo( StreamChannelInfo(
showTypingIndicator: showTypingIndicator, showTypingIndicator: showTypingIndicator,
channel: channel, channel: channel,
textStyle: channelHeaderTheme.subtitleStyle, 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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 /// Widget which shows channel info
class ChannelInfo extends StatelessWidget { /// {@endtemplate}
/// Constructor which creates a [ChannelInfo] widget class StreamChannelInfo extends StatelessWidget {
const ChannelInfo({ /// Constructor which creates a [StreamChannelInfo] widget
const StreamChannelInfo({
Key? key, Key? key,
required this.channel, required this.channel,
this.textStyle, this.textStyle,
@@ -32,7 +38,7 @@ class ChannelInfo extends StatelessWidget {
return BetterStreamBuilder<List<Member>>( return BetterStreamBuilder<List<Member>>(
stream: channel.state!.membersStream, stream: channel.state!.membersStream,
initialData: channel.state!.members, initialData: channel.state!.members,
builder: (context, data) => ConnectionStatusBuilder( builder: (context, data) => StreamConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
switch (status) { switch (status) {
case ConnectionStatus.connected: case ConnectionStatus.connected:
@@ -60,12 +66,13 @@ class ChannelInfo extends StatelessWidget {
var text = context.translations.membersCountText(memberCount); var text = context.translations.membersCountText(memberCount);
final onlineCount = final onlineCount =
members?.where((m) => m.user?.online == true).length ?? 0; 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)}'; text += ', ${context.translations.watchersCountText(onlineCount)}';
} }
alternativeWidget = Text( alternativeWidget = Text(
text, text,
style: ChannelHeaderTheme.of(context).subtitleStyle, style: StreamChannelHeaderTheme.of(context).subtitleStyle,
); );
} else { } else {
final userId = StreamChat.of(context).currentUser?.id; final userId = StreamChat.of(context).currentUser?.id;
@@ -93,11 +100,12 @@ class ChannelInfo extends StatelessWidget {
return alternativeWidget ?? const Offstage(); return alternativeWidget ?? const Offstage();
} }
return TypingIndicator( return Align(
child: StreamTypingIndicator(
parentId: parentId, parentId: parentId,
alignment: Alignment.center,
alternativeWidget: alternativeWidget,
style: textStyle, style: textStyle,
alternativeWidget: alternativeWidget,
),
); );
} }
@@ -11,7 +11,11 @@ typedef TitleBuilder = Widget Function(
StreamChatClient client, StreamChatClient client,
); );
/// /// {@macro channel_list_header}
@Deprecated("Use 'StreamChannelListHeader' instead")
typedef ChannelListHeader = StreamChannelListHeader;
/// {@template channel_list_header}
/// It shows the current [StreamChatClient] status. /// It shows the current [StreamChatClient] status.
/// ///
/// ```dart /// ```dart
@@ -43,11 +47,13 @@ typedef TitleBuilder = Widget Function(
/// if you don't have it in the widget tree. /// if you don't have it in the widget tree.
/// ///
/// The widget components render the ui based on the first ancestor of type /// 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. /// Modify it to change the widget appearance.
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { /// {@endtemplate}
class StreamChannelListHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Instantiates a ChannelListHeader /// Instantiates a ChannelListHeader
const ChannelListHeader({ const StreamChannelListHeader({
Key? key, Key? key,
this.client, this.client,
this.titleBuilder, this.titleBuilder,
@@ -91,14 +97,14 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// By default it shows the new chat button /// By default it shows the new chat button
final List<Widget>? actions; final List<Widget>? actions;
/// The background color for this [ChannelListHeader]. /// The background color for this [StreamChannelListHeader].
final Color? backgroundColor; final Color? backgroundColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client; final _client = client ?? StreamChat.of(context).client;
final user = _client.state.currentUser; final user = _client.state.currentUser;
return ConnectionStatusBuilder( return StreamConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
var statusString = ''; var statusString = '';
var showStatus = true; var showStatus = true;
@@ -117,9 +123,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
} }
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
final channelListHeaderThemeData = ChannelListHeaderTheme.of(context); final channelListHeaderThemeData =
StreamChannelListHeaderTheme.of(context);
final theme = Theme.of(context); final theme = Theme.of(context);
return InfoTile( return StreamInfoTile(
showMessage: showConnectionStateTile && showStatus, showMessage: showConnectionStateTile && showStatus,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
@@ -135,7 +142,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
leading: leading ?? leading: leading ??
Center( Center(
child: user != null child: user != null
? UserAvatar( ? StreamUserAvatar(
user: user, user: user,
showOnlineStatus: false, showOnlineStatus: false,
onTap: onUserAvatarTap ?? onTap: onUserAvatarTap ??
@@ -156,7 +163,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
[ [
StreamNeumorphicButton( StreamNeumorphicButton(
child: IconButton( child: IconButton(
icon: ConnectionStatusBuilder( icon: StreamConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
Color? color; Color? color;
switch (status) { switch (status) {
@@ -234,7 +241,8 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
const SizedBox(width: 10), const SizedBox(width: 10),
Text( Text(
context.translations.searchingForNetworkText, context.translations.searchingForNetworkText,
style: ChannelListHeaderTheme.of(context).titleStyle?.copyWith( style:
StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -247,7 +255,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
StreamChatClient client, StreamChatClient client,
) { ) {
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
final channelListHeaderTheme = ChannelListHeaderTheme.of(context); final channelListHeaderTheme = StreamChannelListHeaderTheme.of(context);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -1,8 +1,6 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -12,7 +10,7 @@ typedef ChannelTapCallback = void Function(Channel, Widget?);
/// Callback called when tapping on a channel /// Callback called when tapping on a channel
typedef ChannelInfoCallback = void Function(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); typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
/// Callback for when 'View Info' is tapped /// Callback for when 'View Info' is tapped
@@ -53,6 +51,7 @@ typedef ViewInfoCallback = void Function(Channel);
/// The widget components render the ui based on the first ancestor of /// The widget components render the ui based on the first ancestor of
/// type [StreamChatTheme]. /// type [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
@Deprecated("Use 'StreamChannelListView' instead")
class ChannelListView extends StatefulWidget { class ChannelListView extends StatefulWidget {
/// Instantiate a new ChannelListView /// Instantiate a new ChannelListView
ChannelListView({ ChannelListView({
@@ -245,7 +244,8 @@ class _ChannelListViewState extends State<ChannelListView> {
child: child, child: child,
); );
final backgroundColor = ChannelListViewTheme.of(context).backgroundColor; final backgroundColor =
StreamChannelListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) { if (backgroundColor != null) {
return ColoredBox( return ColoredBox(
@@ -547,7 +547,8 @@ class _ChannelListViewState extends State<ChannelListView> {
context: context, context: context,
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: ChannelBottomSheet( child: StreamChannelInfoBottomSheet(
channel: channel,
onViewInfoTap: () { onViewInfoTap: () {
widget.onViewInfoTap?.call(channel); widget.onViewInfoTap?.call(channel);
}, },
@@ -556,14 +557,8 @@ class _ChannelListViewState extends State<ChannelListView> {
); );
}, },
), ),
if ([ if (channel.ownCapabilities
'admin', .contains(PermissionType.deleteChannel))
'owner',
].contains(channel.state!.members
.firstWhereOrNull(
(m) => m.userId == channel.client.state.currentUser?.id,
)
?.role))
IconSlideAction( IconSlideAction(
color: backgroundColor, color: backgroundColor,
iconWidget: StreamSvgIcon.delete( iconWidget: StreamSvgIcon.delete(
@@ -642,7 +637,7 @@ class _ChannelListViewState extends State<ChannelListView> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
ChannelAvatar( StreamChannelAvatar(
channel: channel, channel: channel,
borderRadius: BorderRadius.circular(32), borderRadius: BorderRadius.circular(32),
selected: selected, selected: selected,
@@ -657,8 +652,9 @@ class _ChannelListViewState extends State<ChannelListView> {
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
child: StreamChannel( child: StreamChannel(
channel: channel, channel: channel,
child: const ChannelName( child: StreamChannelName(
textStyle: TextStyle( channel: channel,
textStyle: const TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, 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 /// The widget uses a [StreamBuilder] to render the channel information
/// image as soon as it updates. /// image as soon as it updates.
@Deprecated("Use 'StreamChannelName' instead")
class ChannelName extends StatelessWidget { class ChannelName extends StatelessWidget {
/// Instantiate a new ChannelName /// Instantiate a new ChannelName
const 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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.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. /// image as soon as it updates.
/// ///
/// Usually you don't use this widget as it's the default channel preview /// 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 /// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme]. /// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
/// {@endtemplate}
@Deprecated("Use 'StreamChannelListTile' instead")
class ChannelPreview extends StatelessWidget { class ChannelPreview extends StatelessWidget {
/// Constructor for creating [ChannelPreview] /// Constructor for creating [ChannelPreview]
const ChannelPreview({ const ChannelPreview({
@@ -52,7 +55,7 @@ class ChannelPreview extends StatelessWidget {
final Widget? subtitle; final Widget? subtitle;
/// Widget rendering the leading element, by default /// Widget rendering the leading element, by default
/// it shows the [ChannelAvatar] /// it shows the [StreamChannelAvatar]
final Widget? leading; final Widget? leading;
/// Widget rendering the trailing element, /// Widget rendering the trailing element,
@@ -60,7 +63,7 @@ class ChannelPreview extends StatelessWidget {
final Widget? trailing; final Widget? trailing;
/// Widget rendering the sending indicator, /// Widget rendering the sending indicator,
/// by default it uses the [SendingIndicator] widget /// by default it uses the [StreamSendingIndicator] widget
final Widget? sendingIndicator; final Widget? sendingIndicator;
@override @override
@@ -80,13 +83,18 @@ class ChannelPreview extends StatelessWidget {
), ),
onTap: () => onTap?.call(channel), onTap: () => onTap?.call(channel),
onLongPress: () => onLongPress?.call(channel), onLongPress: () => onLongPress?.call(channel),
leading: leading ?? ChannelAvatar(onTap: onImageTap), leading: leading ??
StreamChannelAvatar(
channel: channel,
onTap: onImageTap,
),
title: Row( title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: title ?? child: title ??
ChannelName( StreamChannelName(
channel: channel,
textStyle: channelPreviewTheme.titleStyle, textStyle: channelPreviewTheme.titleStyle,
), ),
), ),
@@ -100,7 +108,7 @@ class ChannelPreview extends StatelessWidget {
e.user!.id == channel.client.state.currentUser?.id)) { e.user!.id == channel.client.state.currentUser?.id)) {
return const SizedBox(); return const SizedBox();
} }
return UnreadIndicator( return StreamUnreadIndicator(
cid: channel.cid, cid: channel.cid,
); );
}, },
@@ -136,7 +144,7 @@ class ChannelPreview extends StatelessWidget {
))); )));
final isMessageRead = readList.length >= final isMessageRead = readList.length >=
(channel.memberCount ?? 0) - 1; (channel.memberCount ?? 0) - 1;
return SendingIndicator( return StreamSendingIndicator(
message: lastMessage!, message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize, size: channelPreviewTheme.indicatorIconSize,
isMessageRead: isMessageRead, isMessageRead: isMessageRead,
@@ -204,7 +212,7 @@ class ChannelPreview extends StatelessWidget {
], ],
); );
} }
return TypingIndicator( return StreamTypingIndicator(
channel: channel, channel: channel,
alternativeWidget: _buildLastMessage(context), alternativeWidget: _buildLastMessage(context),
style: channelPreviewTheme.subtitleStyle, 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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 /// Overlay for displaying commands that can be used
class CommandsOverlay extends StatelessWidget { /// to interact with the channel.
/// Constructor for creating a [CommandsOverlay] /// {@endtemplate}
const CommandsOverlay({ class StreamCommandsOverlay extends StatelessWidget {
/// Constructor for creating a [StreamCommandsOverlay]
const StreamCommandsOverlay({
required this.text, required this.text,
required this.onCommandResult, required this.onCommandResult,
required this.size, required this.size,
@@ -1,14 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@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 /// Widget that builds itself based on the latest snapshot of interaction with
/// a [Stream] of type [ConnectionStatus]. /// a [Stream] of type [ConnectionStatus].
/// ///
/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] /// The widget will use the closest [StreamChatClient.wsConnectionStatusStream]
/// in case no stream is provided. /// in case no stream is provided.
class ConnectionStatusBuilder extends StatelessWidget { /// {@endtemplate}
class StreamConnectionStatusBuilder extends StatelessWidget {
/// Creates a new ConnectionStatusBuilder /// Creates a new ConnectionStatusBuilder
const ConnectionStatusBuilder({ const StreamConnectionStatusBuilder({
Key? key, Key? key,
required this.statusBuilder, required this.statusBuilder,
this.connectionStatusStream, 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/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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 /// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating a [DateDivider] class StreamDateDivider extends StatelessWidget {
const DateDivider({ /// Constructor for creating a [StreamDateDivider]
const StreamDateDivider({
Key? key, Key? key,
required this.dateTime, required this.dateTime,
this.uppercase = false, 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/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart';
/// Widget to display deleted message /// {@macro deleted_message}
class DeletedMessage extends StatelessWidget { @Deprecated("Use 'StreamDeletedMessage' instead")
/// Constructor to create [DeletedMessage] typedef DeletedMessage = StreamDeletedMessage;
const DeletedMessage({
/// {@template deleted_message}
/// Widget to display deleted message.
/// {@endtemplate}
class StreamDeletedMessage extends StatelessWidget {
/// Constructor to create [StreamDeletedMessage]
const StreamDeletedMessage({
Key? key, Key? key,
required this.messageTheme, required this.messageTheme,
this.borderRadiusGeometry, this.borderRadiusGeometry,
@@ -16,7 +22,7 @@ class DeletedMessage extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
/// The theme of the message /// The theme of the message
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// The border radius of the message text /// The border radius of the message text
final BorderRadiusGeometry? borderRadiusGeometry; 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:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:substring_highlight/substring_highlight.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 /// Overlay for displaying emoji that can be used
class EmojiOverlay extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating a [EmojiOverlay] class StreamEmojiOverlay extends StatelessWidget {
const EmojiOverlay({ /// Constructor for creating a [StreamEmojiOverlay]
const StreamEmojiOverlay({
required this.query, required this.query,
required this.onEmojiResult, required this.onEmojiResult,
required this.size, required this.size,
@@ -225,3 +225,50 @@ extension UserListX on List<User> {
return entries.map((e) => e.key).toList(growable: false); return entries.map((e) => e.key).toList(growable: false);
} }
} }
/// 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);
}
}
@@ -21,10 +21,16 @@ enum ReturnActionType {
/// Callback when show message is tapped /// Callback when show message is tapped
typedef ShowMessageCallback = void Function(Message message, Channel channel); 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 /// A full screen image widget
class FullScreenMedia extends StatefulWidget { /// {@endtemplate}
class StreamFullScreenMedia extends StatefulWidget {
/// Instantiate a new FullScreenImage /// Instantiate a new FullScreenImage
const FullScreenMedia({ const StreamFullScreenMedia({
Key? key, Key? key,
required this.mediaAttachments, required this.mediaAttachments,
required this.message, required this.message,
@@ -60,10 +66,10 @@ class FullScreenMedia extends StatefulWidget {
final bool autoplayVideos; final bool autoplayVideos;
@override @override
_FullScreenMediaState createState() => _FullScreenMediaState(); _StreamFullScreenMediaState createState() => _StreamFullScreenMediaState();
} }
class _FullScreenMediaState extends State<FullScreenMedia> class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final AnimationController _animationController; late final AnimationController _animationController;
late final PageController _pageController; late final PageController _pageController;
@@ -172,7 +178,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
), ),
backgroundDecoration: BoxDecoration( backgroundDecoration: BoxDecoration(
color: ColorTween( color: ColorTween(
begin: ChannelHeaderTheme.of(context).color, begin: StreamChannelHeaderTheme.of(context).color,
end: Colors.black, end: Colors.black,
).lerp(_curvedAnimation.value), ).lerp(_curvedAnimation.value),
), ),
@@ -221,7 +227,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
builder: (context, value, child) => Column( builder: (context, value, child) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
GalleryHeader( StreamGalleryHeader(
userName: widget.userName, userName: widget.userName,
sentAt: context.translations.sentAtText( sentAt: context.translations.sentAtText(
date: widget.message.createdAt, date: widget.message.createdAt,
@@ -242,7 +248,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
widget.attachmentActionsModalBuilder, widget.attachmentActionsModalBuilder,
), ),
if (!widget.message.isEphemeral) if (!widget.message.isEphemeral)
GalleryFooter( StreamGalleryFooter(
currentPage: value, currentPage: value,
totalPages: widget.mediaAttachments.length, totalPages: widget.mediaAttachments.length,
mediaAttachments: widget.mediaAttachments, mediaAttachments: widget.mediaAttachments,
@@ -9,10 +9,17 @@ import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 /// Footer widget for media display
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { /// {@endtemplate}
class StreamGalleryFooter extends StatefulWidget
implements PreferredSizeWidget {
/// Creates a channel header /// Creates a channel header
const GalleryFooter({ const StreamGalleryFooter({
Key? key, Key? key,
required this.message, required this.message,
this.onBackPressed, this.onBackPressed,
@@ -51,22 +58,22 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
/// Callback when media is selected /// Callback when media is selected
final ValueChanged<int>? mediaSelectedCallBack; final ValueChanged<int>? mediaSelectedCallBack;
/// The background color of this [GalleryFooter]. /// The background color of this [StreamGalleryFooter].
final Color? backgroundColor; final Color? backgroundColor;
@override @override
_GalleryFooterState createState() => _GalleryFooterState(); _StreamGalleryFooterState createState() => _StreamGalleryFooterState();
@override @override
final Size preferredSize; final Size preferredSize;
} }
class _GalleryFooterState extends State<GalleryFooter> { class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const showShareButton = !kIsWeb; const showShareButton = !kIsWeb;
final mediaQueryData = MediaQuery.of(context); final mediaQueryData = MediaQuery.of(context);
final galleryFooterThemeData = GalleryFooterTheme.of(context); final galleryFooterThemeData = StreamGalleryFooterTheme.of(context);
return SizedBox.fromSize( return SizedBox.fromSize(
size: Size( size: Size(
mediaQueryData.size.width, mediaQueryData.size.width,
@@ -149,7 +156,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
void _showPhotosModal(context) { void _showPhotosModal(context) {
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
final galleryFooterThemeData = GalleryFooterTheme.of(context); final galleryFooterThemeData = StreamGalleryFooterTheme.of(context);
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
barrierColor: galleryFooterThemeData.bottomSheetBarrierColor, barrierColor: galleryFooterThemeData.bottomSheetBarrierColor,
@@ -222,7 +229,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
onTap: () => widget.mediaSelectedCallBack!(index), onTap: () => widget.mediaSelectedCallBack!(index),
child: FittedBox( child: FittedBox(
fit: BoxFit.cover, fit: BoxFit.cover,
child: VideoThumbnailImage( child: StreamVideoThumbnailImage(
video: (attachment.file?.path ?? video: (attachment.file?.path ??
attachment.assetUrl)!, attachment.assetUrl)!,
), ),
@@ -264,7 +271,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
), ),
], ],
), ),
child: UserAvatar( child: StreamUserAvatar(
user: widget.message.user!, user: widget.message.user!,
constraints: constraints:
BoxConstraints.tight(const Size(24, 24)), BoxConstraints.tight(const Size(24, 24)),
@@ -15,10 +15,17 @@ typedef AttachmentActionsBuilder = Widget Function(
AttachmentActionsModal defaultActionsModal, AttachmentActionsModal defaultActionsModal,
); );
/// {@macro gallery_header}
@Deprecated("Use 'StreamGalleryHeader' instead")
typedef GalleryHeader = StreamGalleryHeader;
/// {@template gallery_header}
/// Header/AppBar widget for media display screen /// Header/AppBar widget for media display screen
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// {@endtemplate}
class StreamGalleryHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Creates a channel header /// Creates a channel header
const GalleryHeader({ const StreamGalleryHeader({
Key? key, Key? key,
required this.message, required this.message,
this.currentIndex = 0, this.currentIndex = 0,
@@ -62,7 +69,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
/// Stores the current index of media shown /// Stores the current index of media shown
final int currentIndex; final int currentIndex;
/// The background color of this [GalleryHeader]. /// The background color of this [StreamGalleryHeader].
final Color? backgroundColor; final Color? backgroundColor;
/// Widget builder for attachment actions modal /// Widget builder for attachment actions modal
@@ -72,7 +79,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final galleryHeaderThemeData = GalleryHeaderTheme.of(context); final galleryHeaderThemeData = StreamGalleryHeaderTheme.of(context);
final theme = Theme.of(context); final theme = Theme.of(context);
return AppBar( return AppBar(
toolbarTextStyle: theme.textTheme.bodyText2, toolbarTextStyle: theme.textTheme.bodyText2,
@@ -3,10 +3,16 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart'; 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 /// Fallback user avatar with a polygon gradient overlayed with text
class GradientAvatar extends StatefulWidget { /// {@endtemplate}
/// Constructor for [GradientAvatar] class StreamGradientAvatar extends StatefulWidget {
const GradientAvatar({ /// Constructor for [StreamGradientAvatar]
const StreamGradientAvatar({
Key? key, Key? key,
required this.name, required this.name,
required this.userId, required this.userId,
@@ -19,10 +25,10 @@ class GradientAvatar extends StatefulWidget {
final String userId; final String userId;
@override @override
_GradientAvatarState createState() => _GradientAvatarState(); _StreamGradientAvatarState createState() => _StreamGradientAvatarState();
} }
class _GradientAvatarState extends State<GradientAvatar> { class _StreamGradientAvatarState extends State<StreamGradientAvatar> {
@override @override
Widget build(BuildContext context) => Center( Widget build(BuildContext context) => Center(
child: RepaintBoundary( child: RepaintBoundary(
@@ -1,11 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro group_avatar}
@Deprecated("Use 'StreamGroupAvatar' instead")
typedef GroupAvatar = StreamGroupAvatar;
/// {@template group_avatar}
/// Widget for constructing a group of images /// Widget for constructing a group of images
class GroupAvatar extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating a [GroupAvatar] class StreamGroupAvatar extends StatelessWidget {
const GroupAvatar({ /// Constructor for creating a [StreamGroupAvatar]
const StreamGroupAvatar({
Key? key, Key? key,
this.channel,
required this.members, required this.members,
this.constraints, this.constraints,
this.onTap, this.onTap,
@@ -15,6 +22,9 @@ class GroupAvatar extends StatelessWidget {
this.selectionThickness = 4, this.selectionThickness = 4,
}) : super(key: key); }) : super(key: key);
/// The channel of the avatar
final Channel? channel;
/// List of images to display /// List of images to display
final List<Member> members; final List<Member> members;
@@ -38,7 +48,7 @@ class GroupAvatar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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'); assert(channel.state != null, 'Channel ${channel.id} is not initialized');
@@ -80,7 +90,7 @@ class GroupAvatar extends StatelessWidget {
), ),
), ),
initialData: member, initialData: member,
builder: (context, member) => UserAvatar( builder: (context, member) => StreamUserAvatar(
showOnlineStatus: false, showOnlineStatus: false,
user: member.user!, user: member.user!,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
@@ -118,7 +128,8 @@ class GroupAvatar extends StatelessWidget {
), ),
), ),
initialData: member, initialData: member,
builder: (context, member) => UserAvatar( builder: (context, member) =>
StreamUserAvatar(
showOnlineStatus: false, showOnlineStatus: false,
user: member.user!, user: member.user!,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.zero,
@@ -1,10 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro image_group}
@Deprecated("Use 'StreamImageGroup' instead")
typedef ImageGroup = StreamImageGroup;
/// {@template image_group}
/// Widget for constructing a group of images in message /// Widget for constructing a group of images in message
class ImageGroup extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating [ImageGroup] widget class StreamImageGroup extends StatelessWidget {
const ImageGroup({ /// Constructor for creating [StreamImageGroup] widget
const StreamImageGroup({
Key? key, Key? key,
required this.images, required this.images,
required this.message, required this.message,
@@ -27,8 +33,8 @@ class ImageGroup extends StatelessWidget {
/// Message which images are attached to /// Message which images are attached to
final Message message; final Message message;
/// [MessageThemeData] to apply to message /// [StreamMessageThemeData] to apply to message
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// Size of iamges /// Size of iamges
final Size size; final Size size;
@@ -129,7 +135,7 @@ class ImageGroup extends StatelessWidget {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: FullScreenMedia( child: StreamFullScreenMedia(
mediaAttachments: images, mediaAttachments: images,
startIndex: index, startIndex: index,
userName: message.user?.name, userName: message.user?.name,
@@ -142,7 +148,7 @@ class ImageGroup extends StatelessWidget {
if (res != null) onReturnAction?.call(res); if (res != null) onReturnAction?.call(res);
} }
Widget _buildImage(BuildContext context, int index) => ImageAttachment( Widget _buildImage(BuildContext context, int index) => StreamImageAttachment(
attachment: images[index], attachment: images[index],
size: size, size: size,
message: message, message: message,
@@ -2,10 +2,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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 /// Tile to display a message, used in stream chat to display connection status
class InfoTile extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating an [InfoTile] widget class StreamInfoTile extends StatelessWidget {
const InfoTile({ /// Constructor for creating an [StreamInfoTile] widget
const StreamInfoTile({
Key? key, Key? key,
required this.message, required this.message,
required this.child, required this.child,
@@ -1,6 +1,6 @@
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/connection_status_builder.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_input/message_input.dart';
import 'package:stream_chat_flutter/src/message_list_view.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/message_search_list_view.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart' import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'
@@ -59,7 +59,7 @@ abstract class Translations {
/// The error shown when loading messages fails /// The error shown when loading messages fails
String get loadingMessagesError; 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); String resultCountText(int count);
/// The text for showing the message is deleted /// The text for showing the message is deleted
@@ -74,44 +74,47 @@ abstract class Translations {
/// The text for showing there are no chats /// The text for showing there are no chats
String get emptyChatMessagesText; 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 /// contains a parent message
String threadSeparatorText(int replyCount); String threadSeparatorText(int replyCount);
/// The label for "connected" in [ConnectionStatusBuilder] /// The label for "connected" in [StreamConnectionStatusBuilder]
String get connectedLabel; String get connectedLabel;
/// The label for "disconnected" in [ConnectionStatusBuilder] /// The label for "disconnected" in [StreamConnectionStatusBuilder]
String get disconnectedLabel; String get disconnectedLabel;
/// The label for "reconnecting" in [ConnectionStatusBuilder] /// The label for "reconnecting" in [StreamConnectionStatusBuilder]
String get reconnectingLabel; 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; String get alsoSendAsDirectMessageLabel;
/// The label for search Gif /// The label for search Gif
String get searchGifLabel; 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 /// The label for add a comment or send in case of
/// attachments inside [MessageInput] /// attachments inside [StreamMessageInput]
String get addACommentOrSendLabel; String get addACommentOrSendLabel;
/// The label for write a message in [MessageInput] /// The label for write a message in [StreamMessageInput]
String get writeAMessageLabel; String get writeAMessageLabel;
/// The label for slow mode enabled in [MessageInput] /// The label for slow mode enabled in [StreamMessageInput]
String get slowModeOnLabel; String get slowModeOnLabel;
/// The label for instant commands in [MessageInput] /// The label for instant commands in [StreamMessageInput]
String get instantCommandsLabel; String get instantCommandsLabel;
/// The error shown in case the fi"le is too large even after compression /// 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); String fileTooLargeAfterCompressionError(double limitInMB);
/// The error shown in case the file is too large /// The error shown in case the file is too large
/// while uploading via [MessageInput] /// while uploading via [StreamMessageInput]
String fileTooLargeError(double limitInMB); String fileTooLargeError(double limitInMB);
/// The text for showing the query while searching for emojis /// The text for showing the query while searching for emojis
@@ -141,6 +144,12 @@ abstract class Translations {
/// The label for "OK" /// The label for "OK"
String get okLabel; 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" /// The label for "add more files"
String get addMoreFilesLabel; String get addMoreFilesLabel;
@@ -377,6 +386,10 @@ class DefaultTranslations implements Translations {
return 'Pinned by ${pinnedBy.name}'; return 'Pinned by ${pinnedBy.name}';
} }
@override
String get sendMessagePermissionError =>
'You don\'t have permission to send messages';
@override @override
String get emptyMessagesText => 'There are no messages currently'; String get emptyMessagesText => 'There are no messages currently';
@@ -685,4 +698,11 @@ class DefaultTranslations implements Translations {
@override @override
String attachmentLimitExceedError(int limit) => """ String attachmentLimitExceedError(int limit) => """
Attachment limit exceeded: it's not possible to add more than $limit attachments"""; 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';
} }
@@ -7,21 +7,16 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
extension on Duration { /// {@macro media_list_view}
String format() { @Deprecated("Use 'StreamMediaListView' instead")
final s = '$this'.split('.')[0].padLeft(8, '0'); typedef MediaListView = StreamMediaListView;
if (s.startsWith('00:')) {
return s.replaceFirst('00:', '');
}
return s;
}
}
/// {@template media_list_view}
/// Constructs a list of media /// Constructs a list of media
class MediaListView extends StatefulWidget { /// {@endtemplate}
/// Constructor for creating a [MediaListView] widget class StreamMediaListView extends StatefulWidget {
const MediaListView({ /// Constructor for creating a [StreamMediaListView] widget
const StreamMediaListView({
Key? key, Key? key,
this.selectedIds = const [], this.selectedIds = const [],
this.onSelect, this.onSelect,
@@ -34,10 +29,10 @@ class MediaListView extends StatefulWidget {
final void Function(AssetEntity media)? onSelect; final void Function(AssetEntity media)? onSelect;
@override @override
_MediaListViewState createState() => _MediaListViewState(); _StreamMediaListViewState createState() => _StreamMediaListViewState();
} }
class _MediaListViewState extends State<MediaListView> { class _StreamMediaListViewState extends State<StreamMediaListView> {
final _media = <AssetEntity>[]; final _media = <AssetEntity>[];
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
int _currentPage = 0; int _currentPage = 0;
@@ -222,3 +217,14 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
@override @override
String toString() => '$runtimeType("${media.id}")'; 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:flutter/widgets.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 describing a message action
class MessageAction { /// {@endtemplate}
/// returns a new instance of a [MessageAction] class StreamMessageAction {
MessageAction({ /// returns a new instance of a [StreamMessageAction]
StreamMessageAction({
this.leading, this.leading,
this.title, this.title,
this.onTap, this.onTap,
@@ -3,25 +3,31 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 /// Constructs a modal with actions for a message
class MessageActionsModal extends StatefulWidget { /// {@endtemplate}
/// Constructor for creating a [MessageActionsModal] widget class StreamMessageActionsModal extends StatefulWidget {
const MessageActionsModal({ /// Constructor for creating a [StreamMessageActionsModal] widget
const StreamMessageActionsModal({
Key? key, Key? key,
required this.message, required this.message,
required this.messageWidget, required this.messageWidget,
required this.messageTheme, required this.messageTheme,
this.showReactions = true, this.showReactions,
this.showDeleteMessage = true, this.showDeleteMessage,
this.showEditMessage = true, this.showEditMessage,
this.onReplyTap, this.onReplyTap,
this.onThreadReplyTap, this.onThreadReplyTap,
this.showCopyMessage = true, this.showCopyMessage = true,
this.showReplyMessage = true, this.showReplyMessage = true,
this.showResendMessage = true, this.showResendMessage = true,
this.showThreadReplyMessage = true, this.showThreadReplyMessage,
this.showFlagButton = true, this.showFlagButton,
this.showPinButton = true, this.showPinButton,
this.editMessageInputBuilder, this.editMessageInputBuilder,
this.reverse = false, this.reverse = false,
this.customActions = const [], this.customActions = const [],
@@ -43,51 +49,54 @@ class MessageActionsModal extends StatefulWidget {
/// Message in focus for actions /// Message in focus for actions
final Message message; final Message message;
/// [MessageThemeData] for message /// [StreamMessageThemeData] for message
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// Flag for showing reactions /// Flag for showing reactions
final bool showReactions; final bool? showReactions;
/// Callback when copy is tapped /// Callback when copy is tapped
final OnMessageTap? onCopyTap; final OnMessageTap? onCopyTap;
/// Callback when delete is tapped /// Callback when delete is tapped
final bool showDeleteMessage; final bool? showDeleteMessage;
/// Flag for showing copy action /// Flag for showing copy action
final bool showCopyMessage; final bool showCopyMessage;
/// Flag for showing edit action /// Flag for showing edit action
final bool showEditMessage; final bool? showEditMessage;
/// Flag for showing resend action /// Flag for showing resend action
final bool showResendMessage; final bool showResendMessage;
/// Flag for showing reply action /// Flag for showing reply action
final bool showReplyMessage; final bool? showReplyMessage;
/// Flag for showing thread reply action /// Flag for showing thread reply action
final bool showThreadReplyMessage; final bool? showThreadReplyMessage;
/// Flag for showing flag action /// Flag for showing flag action
final bool showFlagButton; final bool? showFlagButton;
/// Flag for showing pin action /// Flag for showing pin action
final bool showPinButton; final bool? showPinButton;
/// Flag for reversing message /// Flag for reversing message
final bool reverse; final bool reverse;
/// List of custom actions /// List of custom actions
final List<MessageAction> customActions; final List<StreamMessageAction> customActions;
@override @override
_MessageActionsModalState createState() => _MessageActionsModalState(); _StreamMessageActionsModalState createState() =>
_StreamMessageActionsModalState();
} }
class _MessageActionsModalState extends State<MessageActionsModal> { class _StreamMessageActionsModalState extends State<StreamMessageActionsModal> {
bool _showActions = true; bool _showActions = true;
late List<String> _userPermissions;
late bool _isMyMessage;
@override @override
Widget build(BuildContext context) => _showMessageOptionsModal(); Widget build(BuildContext context) => _showMessageOptionsModal();
@@ -122,6 +131,19 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final shiftFactor = final shiftFactor =
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0; 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( final child = Center(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Padding( child: Padding(
@@ -131,7 +153,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
? CrossAxisAlignment.end ? CrossAxisAlignment.end
: CrossAxisAlignment.start, : CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
if (widget.showReactions && if ((widget.showReactions ?? hasReactionPermission) &&
(widget.message.status == MessageSendingStatus.sent)) (widget.message.status == MessageSendingStatus.sent))
Align( Align(
alignment: Alignment( alignment: Alignment(
@@ -144,7 +166,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
: -(1.2 - divFactor)), : -(1.2 - divFactor)),
0, 0,
), ),
child: ReactionPicker( child: StreamReactionPicker(
message: widget.message, message: widget.message,
), ),
), ),
@@ -168,21 +190,35 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (widget.showReplyMessage && if (widget.showReplyMessage ??
widget.message.status == MessageSendingStatus.sent) (_userPermissions
.contains(PermissionType.quoteMessage) &&
widget.message.status ==
MessageSendingStatus.sent))
_buildReplyButton(context), _buildReplyButton(context),
if (widget.showThreadReplyMessage && if (widget.showThreadReplyMessage ??
_userPermissions
.contains(PermissionType.sendReply) &&
(widget.message.status == (widget.message.status ==
MessageSendingStatus.sent) && MessageSendingStatus.sent) &&
widget.message.parentId == null) widget.message.parentId == null)
_buildThreadReplyButton(context), _buildThreadReplyButton(context),
if (widget.showResendMessage) if (widget.showResendMessage)
_buildResendMessage(context), _buildResendMessage(context),
if (widget.showEditMessage) _buildEditMessage(context), if (widget.showEditMessage ??
_isMyMessage && hasEditPermission)
_buildEditMessage(context),
if (widget.showCopyMessage) _buildCopyButton(context), if (widget.showCopyMessage) _buildCopyButton(context),
if (widget.showFlagButton) _buildFlagButton(context), if (widget.showFlagButton ??
if (widget.showPinButton) _buildPinButton(context), _userPermissions
if (widget.showDeleteMessage) .contains(PermissionType.flagMessage))
_buildFlagButton(context),
if (widget.showPinButton ??
_userPermissions
.contains(PermissionType.pinMessage))
_buildPinButton(context),
if (widget.showDeleteMessage ??
(_isMyMessage && hasDeletePermission))
_buildDeleteButton(context), _buildDeleteButton(context),
...widget.customActions ...widget.customActions
.map((action) => _buildCustomAction( .map((action) => _buildCustomAction(
@@ -239,7 +275,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
InkWell _buildCustomAction( InkWell _buildCustomAction(
BuildContext context, BuildContext context,
MessageAction messageAction, StreamMessageAction messageAction,
) => ) =>
InkWell( InkWell(
onTap: () { onTap: () {
@@ -560,7 +596,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
elevation: 2, elevation: 2,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
isScrollControlled: true, isScrollControlled: true,
backgroundColor: MessageInputTheme.of(context).inputBackgroundColor, backgroundColor: StreamMessageInputTheme.of(context).inputBackgroundColor,
shape: const RoundedRectangleBorder( shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.circular(16), topLeft: Radius.circular(16),
@@ -602,8 +638,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
if (widget.editMessageInputBuilder != null) if (widget.editMessageInputBuilder != null)
widget.editMessageInputBuilder!(context, widget.message) widget.editMessageInputBuilder!(context, widget.message)
else else
MessageInput( StreamMessageInput(
editMessage: widget.message, messageInputController: MessageInputController(
message: widget.message,
),
preMessageSending: (m) { preMessageSending: (m) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
Navigator.pop(context); 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();
}
} }
@@ -18,12 +18,8 @@ import 'package:stream_chat_flutter/src/media_list_view.dart';
import 'package:stream_chat_flutter/src/multi_overlay.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/quoted_message_widget.dart';
import 'package:stream_chat_flutter/src/user_mentions_overlay.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/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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]. /// A callback that can be passed to [MessageInput.onError].
/// ///
@@ -59,7 +55,7 @@ typedef MentionTileBuilder = Widget Function(
/// Builder function for building a user mention tile. /// Builder function for building a user mention tile.
/// ///
/// Use [UserMentionTile] for the default implementation. /// Use [StreamUserMentionTile] for the default implementation.
typedef UserMentionTileBuilder = Widget Function( typedef UserMentionTileBuilder = Widget Function(
BuildContext context, BuildContext context,
User user, User user,
@@ -155,12 +151,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. /// as the bottom widget.
/// ///
/// The widget renders the ui based on the first ancestor of /// The widget renders the ui based on the first ancestor of
/// type [StreamChatTheme]. /// type [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
@Deprecated("Use 'StreamMessageInput' instead")
class MessageInput extends StatefulWidget { class MessageInput extends StatefulWidget {
/// Instantiate a new MessageInput /// Instantiate a new MessageInput
const MessageInput({ const MessageInput({
@@ -190,8 +187,6 @@ class MessageInput extends StatefulWidget {
this.mentionsTileBuilder, this.mentionsTileBuilder,
this.userMentionsTileBuilder, this.userMentionsTileBuilder,
this.maxAttachmentSize = _kDefaultMaxAttachmentSize, this.maxAttachmentSize = _kDefaultMaxAttachmentSize,
this.compressedVideoQuality = VideoQuality.DefaultQuality,
this.compressedVideoFrameRate = 30,
this.onError, this.onError,
this.attachmentLimit = 10, this.attachmentLimit = 10,
this.onAttachmentLimitExceed, this.onAttachmentLimitExceed,
@@ -213,12 +208,6 @@ class MessageInput extends StatefulWidget {
/// Message to edit /// Message to edit
final Message? editMessage; 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 /// Max attachment size in bytes
/// Defaults to 20 MB /// Defaults to 20 MB
/// do not set it if you're using our default CDN /// do not set it if you're using our default CDN
@@ -341,6 +330,7 @@ class MessageInput extends StatefulWidget {
} }
/// State of [MessageInput] /// State of [MessageInput]
@Deprecated("Use 'StreamMessageInput' instead")
class MessageInputState extends State<MessageInput> { class MessageInputState extends State<MessageInput> {
final _attachments = <String, Attachment>{}; final _attachments = <String, Attachment>{};
final List<User> _mentionedUsers = []; final List<User> _mentionedUsers = [];
@@ -364,7 +354,7 @@ class MessageInputState extends State<MessageInput> {
widget.textEditingController ?? TextEditingController(); widget.textEditingController ?? TextEditingController();
late StreamChatThemeData _streamChatTheme; late StreamChatThemeData _streamChatTheme;
late MessageInputThemeData _messageInputTheme; late StreamMessageInputThemeData _messageInputTheme;
bool get _hasQuotedMessage => widget.quotedMessage != null; bool get _hasQuotedMessage => widget.quotedMessage != null;
@@ -487,7 +477,7 @@ class MessageInputState extends State<MessageInput> {
); );
} }
return MultiOverlay( return StreamMultiOverlay(
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
overlayAnchor: Alignment.bottomCenter, overlayAnchor: Alignment.bottomCenter,
overlayOptions: [ overlayOptions: [
@@ -932,7 +922,7 @@ class MessageInputState extends State<MessageInput> {
if (renderObject == null) { if (renderObject == null) {
return const Offstage(); return const Offstage();
} }
return CommandsOverlay( return StreamCommandsOverlay(
channel: StreamChannel.of(context).channel, channel: StreamChannel.of(context).channel,
size: Size(renderObject.size.width - 16, 400), size: Size(renderObject.size.width - 16, 400),
text: text, text: text,
@@ -1121,41 +1111,18 @@ class MessageInputState extends State<MessageInput> {
if (mediaFile == null) return; if (mediaFile == null) return;
var file = AttachmentFile( final file = AttachmentFile(
path: mediaFile.path, path: mediaFile.path,
size: await mediaFile.length(), size: await mediaFile.length(),
bytes: mediaFile.readAsBytesSync(), bytes: mediaFile.readAsBytesSync(),
); );
if (file.size! > widget.maxAttachmentSize) { if (file.size! > widget.maxAttachmentSize) {
if (medium.type == AssetType.video && file.path != null) { return _showErrorAlert(
final mediaInfo = await VideoService.compressVideo( context.translations.fileTooLargeError(
file.path!,
frameRate: widget.compressedVideoFrameRate,
quality: widget.compressedVideoQuality,
);
if (mediaInfo == null ||
mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert(
context.translations.fileTooLargeAfterCompressionError(
widget.maxAttachmentSize / (1024 * 1024), 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(
widget.maxAttachmentSize / (1024 * 1024),
));
return;
}
} }
setState(() { setState(() {
@@ -1197,7 +1164,7 @@ class MessageInputState extends State<MessageInput> {
} }
return LayoutBuilder( return LayoutBuilder(
builder: (context, snapshot) => UserMentionsOverlay( builder: (context, snapshot) => StreamUserMentionsOverlay(
query: query, query: query,
mentionAllAppUsers: widget.mentionAllAppUsers, mentionAllAppUsers: widget.mentionAllAppUsers,
client: StreamChat.of(context).client, client: StreamChat.of(context).client,
@@ -1242,7 +1209,7 @@ class MessageInputState extends State<MessageInput> {
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
final renderObject = context.findRenderObject() as RenderBox; final renderObject = context.findRenderObject() as RenderBox;
return EmojiOverlay( return StreamEmojiOverlay(
size: Size(renderObject.size.width - 16, 200), size: Size(renderObject.size.width - 16, 200),
query: query, query: query,
onEmojiResult: (emoji) { onEmojiResult: (emoji) {
@@ -1277,7 +1244,7 @@ class MessageInputState extends State<MessageInput> {
if (!_hasQuotedMessage) return const Offstage(); if (!_hasQuotedMessage) return const Offstage();
final containsUrl = widget.quotedMessage!.attachments final containsUrl = widget.quotedMessage!.attachments
.any((element) => element.titleLink != null); .any((element) => element.titleLink != null);
return QuotedMessageWidget( return StreamQuotedMessageWidget(
reverse: true, reverse: true,
showBorder: !containsUrl, showBorder: !containsUrl,
message: widget.quotedMessage!, message: widget.quotedMessage!,
@@ -1308,10 +1275,8 @@ class MessageInputState extends State<MessageInput> {
.map<Widget>( .map<Widget>(
(e) => ClipRRect( (e) => ClipRRect(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
child: FileAttachment( child: StreamFileAttachment(
message: Message( message: Message(), // dummy message
status: MessageSendingStatus.sending,
), // dummy message
attachment: e, attachment: e,
size: Size( size: Size(
MediaQuery.of(context).size.width * 0.65, MediaQuery.of(context).size.width * 0.65,
@@ -1432,7 +1397,7 @@ class MessageInputState extends State<MessageInput> {
case 'video': case 'video':
return Stack( return Stack(
children: [ children: [
VideoThumbnailImage( StreamVideoThumbnailImage(
height: 104, height: 104,
width: 104, width: 104,
video: (attachment.file?.path ?? attachment.assetUrl)!, video: (attachment.file?.path ?? attachment.assetUrl)!,
@@ -1682,33 +1647,11 @@ class MessageInputState extends State<MessageInput> {
); );
if (file.size! > widget.maxAttachmentSize) { if (file.size! > widget.maxAttachmentSize) {
if (attachmentType == 'video' && file.path != null) { return _showErrorAlert(
final mediaInfo = await (VideoService.compressVideo( context.translations.fileTooLargeError(
file.path!,
frameRate: widget.compressedVideoFrameRate,
quality: widget.compressedVideoQuality,
) as FutureOr<MediaInfo>);
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert(
context.translations.fileTooLargeAfterCompressionError(
widget.maxAttachmentSize / (1024 * 1024), 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(
widget.maxAttachmentSize / (1024 * 1024),
));
return;
}
} }
setState(() { setState(() {
@@ -1947,7 +1890,7 @@ class MessageInputState extends State<MessageInput> {
@override @override
void didChangeDependencies() { void didChangeDependencies() {
_streamChatTheme = StreamChatTheme.of(context); _streamChatTheme = StreamChatTheme.of(context);
_messageInputTheme = MessageInputTheme.of(context); _messageInputTheme = StreamMessageInputTheme.of(context);
if (widget.editMessage == null) _startSlowMode(); if (widget.editMessage == null) _startSlowMode();
if ((widget.editMessage != null || widget.initialMessage != null) && if ((widget.editMessage != null || widget.initialMessage != null) &&
@@ -2023,7 +1966,7 @@ class _PickerWidgetState extends State<_PickerWidget> {
), ),
); );
} }
return MediaListView( return StreamMediaListView(
selectedIds: widget.selectedMedias, selectedIds: widget.selectedMedias,
onSelect: widget.onMediaSelected, onSelect: widget.onMediaSelected,
); );
@@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Button for showing visual component of slow mode.
class StreamCountdownButton extends StatelessWidget {
/// Constructor for creating [StreamCountdownButton].
const StreamCountdownButton({
Key? key,
required this.count,
}) : super(key: key);
/// Count of time remaining to show to the user.
final int count;
@override
Widget build(BuildContext context) => Padding(
padding: const EdgeInsets.all(8),
child: DecoratedBox(
decoration: BoxDecoration(
color: StreamChatTheme.of(context).colorTheme.disabled,
shape: BoxShape.circle,
),
child: SizedBox(
height: 24,
width: 24,
child: Center(
child: Text('$count'),
),
),
),
);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
/// A [SafeArea] with an enabled toggle
class SimpleSafeArea extends StatelessWidget {
/// Constructor for [SimpleSafeArea]
const SimpleSafeArea({
Key? key,
this.enabled = true,
required this.child,
}) : super(key: key);
/// Wrap [child] with [SafeArea]
final bool enabled;
/// Child widget to wrap
final Widget child;
@override
Widget build(BuildContext context) => SafeArea(
left: enabled,
top: enabled,
right: enabled,
bottom: enabled,
child: child,
);
}
@@ -0,0 +1,537 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/media_list_view.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Callback for when a file has to be picked.
typedef FilePickerCallback = void Function(
DefaultAttachmentTypes fileType, {
bool camera,
});
/// Callback for building an icon for a custom attachment type.
typedef CustomAttachmentIconBuilder = Widget Function(
BuildContext context,
bool active,
);
/// A widget that allows to pick an attachment.
class StreamAttachmentPicker extends StatefulWidget {
/// Default constructor for [StreamAttachmentPicker] which creates the Stream
/// attachment picker widget.
const StreamAttachmentPicker({
Key? key,
required this.messageInputController,
required this.onFilePicked,
this.isOpen = false,
this.pickerSize = 360.0,
this.attachmentLimit = 10,
this.onAttachmentLimitExceeded,
this.maxAttachmentSize = 20971520,
this.onError,
this.allowedAttachmentTypes = const [
DefaultAttachmentTypes.image,
DefaultAttachmentTypes.file,
DefaultAttachmentTypes.video,
],
this.customAttachmentTypes = const [],
}) : super(key: key);
/// True if the picker is open.
final bool isOpen;
/// The picker size in height.
final double pickerSize;
/// The [MessageInputController] linked to this picker.
final MessageInputController messageInputController;
/// The limit of attachments that can be picked.
final int attachmentLimit;
/// The callback for when the attachment limit is exceeded.
final AttachmentLimitExceedListener? onAttachmentLimitExceeded;
/// Callback for when an error occurs in the attachment picker.
final ValueChanged<String>? onError;
/// Callback for when file is picked.
final FilePickerCallback onFilePicked;
/// Max attachment size in bytes:
/// - Defaults to 20 MB
/// - Do not set it if you're using our default CDN
final int maxAttachmentSize;
/// The list of attachment types that can be picked.
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
/// The list of custom attachment types that can be picked.
final List<CustomAttachmentType> customAttachmentTypes;
/// Used to create a new copy of [StreamAttachmentPicker] with modified
/// properties.
StreamAttachmentPicker copyWith({
Key? key,
MessageInputController? messageInputController,
FilePickerCallback? onFilePicked,
bool? isOpen,
double? pickerSize,
int? attachmentLimit,
AttachmentLimitExceedListener? onAttachmentLimitExceeded,
int? maxAttachmentSize,
ValueChanged<bool>? onChangeInputState,
ValueChanged<String>? onError,
List<DefaultAttachmentTypes>? allowedAttachmentTypes,
List<CustomAttachmentType>? customAttachmentTypes = const [],
}) =>
StreamAttachmentPicker(
key: key ?? this.key,
messageInputController:
messageInputController ?? this.messageInputController,
onFilePicked: onFilePicked ?? this.onFilePicked,
isOpen: isOpen ?? this.isOpen,
pickerSize: pickerSize ?? this.pickerSize,
attachmentLimit: attachmentLimit ?? this.attachmentLimit,
onAttachmentLimitExceeded:
onAttachmentLimitExceeded ?? this.onAttachmentLimitExceeded,
maxAttachmentSize: maxAttachmentSize ?? this.maxAttachmentSize,
onError: onError ?? this.onError,
allowedAttachmentTypes:
allowedAttachmentTypes ?? this.allowedAttachmentTypes,
customAttachmentTypes:
customAttachmentTypes ?? this.customAttachmentTypes,
);
@override
State<StreamAttachmentPicker> createState() => _StreamAttachmentPickerState();
}
class _StreamAttachmentPickerState extends State<StreamAttachmentPicker> {
int _filePickerIndex = 0;
@override
Widget build(BuildContext context) {
final _streamChatTheme = StreamChatTheme.of(context);
final messageInputController = widget.messageInputController;
final _attachmentContainsImage =
messageInputController.attachments.any((it) => it.type == 'image');
final _attachmentContainsFile =
messageInputController.attachments.any((it) => it.type == 'file');
final _attachmentContainsVideo =
messageInputController.attachments.any((it) => it.type == 'video');
final attachmentLimitCrossed =
messageInputController.attachments.length >= widget.attachmentLimit;
Color _getIconColor(int index) {
final streamChatThemeData = _streamChatTheme;
switch (index) {
case 0:
return _filePickerIndex == 0 || _attachmentContainsImage
? streamChatThemeData.colorTheme.accentPrimary
: (_attachmentContainsImage
? streamChatThemeData.colorTheme.accentPrimary
: streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(
messageInputController.attachments.isEmpty ? 0.5 : 0.2,
));
case 1:
return _attachmentContainsFile
? streamChatThemeData.colorTheme.accentPrimary
: (messageInputController.attachments.isEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2));
case 2:
return widget.messageInputController.attachments.isNotEmpty &&
(!_attachmentContainsImage || attachmentLimitCrossed)
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5);
case 3:
return widget.messageInputController.attachments.isNotEmpty &&
(!_attachmentContainsVideo || attachmentLimitCrossed)
? streamChatThemeData.colorTheme.textHighEmphasis.withOpacity(0.2)
: _attachmentContainsFile &&
messageInputController.attachments.isNotEmpty
? streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.2)
: streamChatThemeData.colorTheme.textHighEmphasis
.withOpacity(0.5);
default:
return Colors.black;
}
}
return AnimatedContainer(
duration:
widget.isOpen ? const Duration(milliseconds: 300) : const Duration(),
curve: Curves.easeOut,
height: widget.isOpen ? widget.pickerSize : 0,
child: SingleChildScrollView(
child: SizedBox(
height: widget.pickerSize,
child: Material(
color: _streamChatTheme.colorTheme.inputBg,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
if (widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.image))
IconButton(
icon: StreamSvgIcon.pictures(
color: _getIconColor(0),
),
onPressed:
messageInputController.attachments.isNotEmpty &&
!_attachmentContainsImage
? null
: () {
setState(() {
_filePickerIndex = 0;
});
},
),
if (widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.file))
IconButton(
iconSize: 32,
icon: StreamSvgIcon.files(
color: _getIconColor(1),
),
onPressed: messageInputController
.attachments.isNotEmpty &&
!_attachmentContainsFile
? null
: () {
widget
.onFilePicked(DefaultAttachmentTypes.file);
},
),
if (widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.image))
IconButton(
icon: StreamSvgIcon.camera(
color: _getIconColor(2),
),
onPressed: attachmentLimitCrossed ||
(messageInputController
.attachments.isNotEmpty &&
!_attachmentContainsVideo)
? null
: () {
widget.onFilePicked(
DefaultAttachmentTypes.image,
camera: true,
);
},
),
if (widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.video))
IconButton(
padding: const EdgeInsets.all(0),
icon: StreamSvgIcon.record(
color: _getIconColor(3),
),
onPressed: attachmentLimitCrossed ||
(messageInputController
.attachments.isNotEmpty &&
!_attachmentContainsVideo)
? null
: () {
widget.onFilePicked(
DefaultAttachmentTypes.video,
camera: true,
);
},
),
for (int i = 0;
i < widget.customAttachmentTypes.length;
i++)
IconButton(
onPressed: () {
if (messageInputController.attachments.isNotEmpty) {
if (!messageInputController.attachments.any((e) =>
e.type ==
widget.customAttachmentTypes[i].type)) {
return;
}
}
setState(() {
_filePickerIndex = i + 1;
});
},
icon: widget.customAttachmentTypes[i]
.iconBuilder(context, _filePickerIndex == i + 1),
),
],
),
DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.barsBg,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.inputBg,
borderRadius: BorderRadius.circular(4),
),
),
),
),
),
if (widget.isOpen &&
(widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.image) ||
(widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.file))))
Expanded(
child: DecoratedBox(
decoration: BoxDecoration(
color: _streamChatTheme.colorTheme.barsBg,
borderRadius: BorderRadius.circular(8),
),
child: _PickerWidget(
filePickerIndex: _filePickerIndex,
streamChatTheme: _streamChatTheme,
containsFile: _attachmentContainsFile,
selectedMedias: messageInputController.attachments
.map((e) => e.id)
.toList(),
onAddMoreFilesClick: widget.onFilePicked,
onMediaSelected: (media) {
if (messageInputController.attachments
.any((e) => e.id == media.id)) {
messageInputController
.removeAttachmentById(media.id);
} else {
_addAssetAttachment(media);
}
},
allowedAttachmentTypes: widget.allowedAttachmentTypes,
customAttachmentTypes: widget.customAttachmentTypes,
),
),
),
],
),
),
),
),
);
}
void _addAssetAttachment(AssetEntity medium) async {
final mediaFile = await medium.originFile.timeout(
const Duration(seconds: 5),
onTimeout: () => medium.originFile,
);
if (mediaFile == null) return;
var file = AttachmentFile(
path: mediaFile.path,
size: await mediaFile.length(),
bytes: mediaFile.readAsBytesSync(),
);
if (file.size! > widget.maxAttachmentSize) {
return widget.onError?.call(
context.translations.fileTooLargeError(
widget.maxAttachmentSize / (1024 * 1024),
),
);
}
setState(() {
final attachment = Attachment(
id: medium.id,
file: file,
type: medium.type == AssetType.image ? 'image' : 'video',
);
_addAttachments([attachment]);
});
}
/// Adds an attachment to the [messageInputController.attachments] map
void _addAttachments(Iterable<Attachment> attachments) {
final limit = widget.attachmentLimit;
final length =
widget.messageInputController.attachments.length + attachments.length;
if (length > limit) {
final onAttachmentLimitExceed = widget.onAttachmentLimitExceeded;
if (onAttachmentLimitExceed != null) {
return onAttachmentLimitExceed(
widget.attachmentLimit,
context.translations.attachmentLimitExceedError(limit),
);
}
return widget.onError?.call(
context.translations.attachmentLimitExceedError(limit),
);
}
for (final attachment in attachments) {
widget.messageInputController.addAttachment(attachment);
}
}
}
class _PickerWidget extends StatefulWidget {
const _PickerWidget({
Key? key,
required this.filePickerIndex,
required this.containsFile,
required this.selectedMedias,
required this.onAddMoreFilesClick,
required this.onMediaSelected,
required this.streamChatTheme,
required this.allowedAttachmentTypes,
required this.customAttachmentTypes,
}) : super(key: key);
final int filePickerIndex;
final bool containsFile;
final List<String> selectedMedias;
final void Function(DefaultAttachmentTypes) onAddMoreFilesClick;
final void Function(AssetEntity) onMediaSelected;
final StreamChatThemeData streamChatTheme;
final List<DefaultAttachmentTypes> allowedAttachmentTypes;
final List<CustomAttachmentType> customAttachmentTypes;
@override
_PickerWidgetState createState() => _PickerWidgetState();
}
class _PickerWidgetState extends State<_PickerWidget> {
Future<bool>? requestPermission;
@override
void initState() {
super.initState();
requestPermission = PhotoManager.requestPermission();
}
@override
Widget build(BuildContext context) {
if (widget.filePickerIndex != 0) {
return widget.customAttachmentTypes[widget.filePickerIndex - 1]
.pickerBuilder(context);
}
return FutureBuilder<bool>(
future: requestPermission,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Offstage();
}
if (snapshot.data!) {
if (widget.containsFile ||
!widget.allowedAttachmentTypes
.contains(DefaultAttachmentTypes.image)) {
return GestureDetector(
onTap: () {
widget.onAddMoreFilesClick(DefaultAttachmentTypes.file);
},
child: Container(
constraints: const BoxConstraints.expand(),
color: widget.streamChatTheme.colorTheme.inputBg,
alignment: Alignment.center,
child: Text(
context.translations.addMoreFilesLabel,
style: TextStyle(
color: widget.streamChatTheme.colorTheme.accentPrimary,
fontWeight: FontWeight.bold,
),
),
),
);
}
return StreamMediaListView(
selectedIds: widget.selectedMedias,
onSelect: widget.onMediaSelected,
);
}
return InkWell(
onTap: () async {
PhotoManager.openSetting();
},
child: Container(
color: widget.streamChatTheme.colorTheme.inputBg,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SvgPicture.asset(
'svgs/icon_picture_empty_state.svg',
package: 'stream_chat_flutter',
height: 140,
color: widget.streamChatTheme.colorTheme.disabled,
),
Text(
context.translations.enablePhotoAndVideoAccessMessage,
style: widget.streamChatTheme.textTheme.body.copyWith(
color: widget.streamChatTheme.colorTheme.textLowEmphasis,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Center(
child: Text(
context.translations.allowGalleryAccessMessage,
style: widget.streamChatTheme.textTheme.bodyBold.copyWith(
color: widget.streamChatTheme.colorTheme.accentPrimary,
),
),
),
],
),
),
);
},
);
}
}
/// Class which holds data for a custom attachment type in the attachment picker
class CustomAttachmentType {
/// Default constructor for creating a custom attachment for the attachment
/// picker.
CustomAttachmentType({
required this.type,
required this.iconBuilder,
required this.pickerBuilder,
});
/// Type name.
String type;
/// Builds the icon in the attachment picker top row.
CustomAttachmentIconBuilder iconBuilder;
/// Builds content in the attachment builder when icon is selected.
WidgetBuilder pickerBuilder;
}
@@ -0,0 +1,115 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// A widget that displays a sending button.
class StreamMessageSendButton extends StatelessWidget {
/// Returns a [StreamMessageSendButton] with the given [timeOut], [isIdle],
/// [isCommandEnabled], [isEditEnabled], [idleSendButton], [activeSendButton],
/// [onSendMessage].
const StreamMessageSendButton({
Key? key,
this.timeOut = 0,
this.isIdle = true,
this.isCommandEnabled = false,
this.isEditEnabled = false,
this.idleSendButton,
this.activeSendButton,
required this.onSendMessage,
}) : super(key: key);
/// Time out related to slow mode.
final int timeOut;
/// If true the button will be disabled.
final bool isIdle;
/// True if a command is being sent.
final bool isCommandEnabled;
/// True if in editing mode.
final bool isEditEnabled;
/// The widget to display when the button is disabled.
final Widget? idleSendButton;
/// The widget to display when the button is enabled.
final Widget? activeSendButton;
/// The callback to call when the button is pressed.
final VoidCallback onSendMessage;
@override
Widget build(BuildContext context) {
final _streamChatTheme = StreamChatTheme.of(context);
late Widget sendButton;
if (timeOut > 0) {
sendButton = StreamCountdownButton(count: timeOut);
} else if (isIdle) {
sendButton = idleSendButton ?? _buildIdleSendButton(context);
} else {
sendButton = activeSendButton != null
? InkWell(
onTap: onSendMessage,
child: activeSendButton,
)
: _buildSendButton(context);
}
return AnimatedSwitcher(
duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!,
child: sendButton,
);
}
Widget _buildIdleSendButton(BuildContext context) {
final _messageInputTheme = StreamMessageInputTheme.of(context);
return Padding(
padding: const EdgeInsets.all(8),
child: StreamSvgIcon(
assetName: _getIdleSendIcon(),
color: _messageInputTheme.sendButtonIdleColor,
),
);
}
Widget _buildSendButton(BuildContext context) {
final _messageInputTheme = StreamMessageInputTheme.of(context);
return Padding(
padding: const EdgeInsets.all(8),
child: IconButton(
onPressed: onSendMessage,
padding: const EdgeInsets.all(0),
splashRadius: 24,
constraints: const BoxConstraints.tightFor(
height: 24,
width: 24,
),
icon: StreamSvgIcon(
assetName: _getSendIcon(),
color: _messageInputTheme.sendButtonColor,
),
),
);
}
String _getIdleSendIcon() {
if (isCommandEnabled) {
return 'Icon_search.svg';
} else {
return 'Icon_circle_right.svg';
}
}
String _getSendIcon() {
if (isEditEnabled) {
return 'Icon_circle_up.svg';
} else if (isCommandEnabled) {
return 'Icon_search.svg';
} else {
return 'Icon_circle_up.svg';
}
}
}
@@ -0,0 +1,766 @@
// ignore_for_file: prefer-trailing-comma, cascade_invocations, lines_longer_than_80_chars
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
export 'package:flutter/services.dart'
show
TextInputType,
TextInputAction,
TextCapitalization,
SmartQuotesType,
SmartDashesType;
/// A widget the wraps the [TextField] and adds some StreamChat specifics.
class StreamMessageTextField extends StatefulWidget {
/// Creates a Material Design text field.
///
/// If [decoration] is non-null (which is the default), the text field
/// requires one of its ancestors to be a [Material] widget.
///
/// To remove the decoration entirely (including the extra padding introduced
/// by the decoration to save space for the labels), set the [decoration] to
/// null.
///
/// The [maxLines] property can be set to null to remove the restriction on
/// the number of lines. By default, it is one, meaning this is a single-line
/// text field. [maxLines] must not be zero.
///
/// The [maxLength] property is set to null by default, which means the
/// number of characters allowed in the text field is not restricted. If
/// [maxLength] is set a character counter will be displayed below the
/// field showing how many characters have been entered. If the value is
/// set to a positive integer it will also display the maximum allowed
/// number of characters to be entered. If the value is set to
/// [TextField.noMaxLength] then only the current length is displayed.
///
/// After [maxLength] characters have been input, additional input
/// is ignored, unless [maxLengthEnforcement] is set to
/// [MaxLengthEnforcement.none].
/// The text field enforces the length with a
/// [LengthLimitingTextInputFormatter],
/// which is evaluated after the supplied [inputFormatters], if any.
/// The [maxLength] value must be either null or greater than zero.
///
/// The text cursor is not shown if [showCursor] is false or if [showCursor]
/// is null (the default) and [readOnly] is true.
///
/// The [selectionHeightStyle] and [selectionWidthStyle] properties allow
/// changing the shape of the selection highlighting. These properties default
/// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and
/// must not be null.
///
/// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect],
/// [scrollPadding], [maxLines], [maxLength],
/// [selectionHeightStyle], [selectionWidthStyle], [enableSuggestions], and
/// [enableIMEPersonalizedLearning] arguments must not be null.
///
/// See also:
///
/// * [maxLength], which discusses the precise meaning of "number of
/// characters" and how it may differ from the intuitive meaning.
const StreamMessageTextField({
Key? key,
this.controller,
this.focusNode,
this.decoration = const InputDecoration(),
TextInputType? keyboardType,
this.textInputAction,
this.textCapitalization = TextCapitalization.none,
this.style,
this.strutStyle,
this.textAlign = TextAlign.start,
this.textAlignVertical,
this.textDirection,
this.readOnly = false,
ToolbarOptions? toolbarOptions,
this.showCursor,
this.autofocus = false,
this.obscuringCharacter = '',
this.obscureText = false,
this.autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
this.enableSuggestions = true,
this.maxLines = 1,
this.minLines,
this.expands = false,
this.maxLength,
@Deprecated(
'Use maxLengthEnforcement parameter which provides more specific '
'behavior related to the maxLength limit. '
'This feature was deprecated after v1.25.0-5.0.pre.',
)
this.maxLengthEnforced = true,
this.maxLengthEnforcement,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
this.onAppPrivateCommand,
this.inputFormatters,
this.enabled,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20),
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.mouseCursor,
this.buildCounter,
this.scrollController,
this.scrollPhysics,
this.autofillHints,
this.restorationId,
this.enableIMEPersonalizedLearning = true,
}) : assert(obscuringCharacter.length == 1,
'`obscuringCharacter.length` must be 1'),
smartDashesType = smartDashesType ??
(obscureText ? SmartDashesType.disabled : SmartDashesType.enabled),
smartQuotesType = smartQuotesType ??
(obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled),
assert(
maxLengthEnforced || maxLengthEnforcement == null,
'maxLengthEnforced is deprecated, use only maxLengthEnforcement',
),
assert(maxLines == null || maxLines > 0,
'`maxLines` needs to be left as null or bigger than 0'),
assert(minLines == null || minLines > 0,
'`minLines` needs to be left as null or bigger than 0'),
assert(
(maxLines == null) || (minLines == null) || (maxLines >= minLines),
"minLines can't be greater than maxLines",
),
assert(
!expands || (maxLines == null && minLines == null),
'minLines and maxLines must be null when expands is true.',
),
assert(!obscureText || maxLines == 1,
'Obscured fields cannot be multiline.'),
assert(
maxLength == null ||
maxLength == TextField.noMaxLength ||
maxLength > 0,
'`maxLength` needs to be null or a positive integer'),
// Assert the following instead of setting it directly to avoid
// surprising the user by silently changing the value they set.
assert(
!identical(textInputAction, TextInputAction.newline) ||
maxLines == 1 ||
!identical(keyboardType, TextInputType.text),
'''Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline TextField.''',
),
keyboardType = keyboardType ??
(maxLines == 1 ? TextInputType.text : TextInputType.multiline),
toolbarOptions = toolbarOptions ??
(obscureText
? const ToolbarOptions(
selectAll: true,
paste: true,
)
: const ToolbarOptions(
copy: true,
cut: true,
selectAll: true,
paste: true,
)),
super(key: key);
/// Controls the message being edited.
///
/// If null, this widget will create its own [MessageInputController].
final MessageInputController? controller;
/// Defines the keyboard focus for this widget.
///
/// The [focusNode] is a long-lived object that's typically managed by a
/// [StatefulWidget] parent. See [FocusNode] for more information.
///
/// To give the keyboard focus to this widget, provide a [focusNode] and then
/// use the current [FocusScope] to request the focus:
///
/// ```dart
/// FocusScope.of(context).requestFocus(myFocusNode);
/// ```
///
/// This happens automatically when the widget is tapped.
///
/// To be notified when the widget gains or loses the focus, add a listener
/// to the [focusNode]:
///
/// ```dart
/// focusNode.addListener(() { print(myFocusNode.hasFocus); });
/// ```
///
/// If null, this widget will create its own [FocusNode].
///
/// ## Keyboard
///
/// Requesting the focus will typically cause the keyboard to be shown
/// if it's not showing already.
///
/// On Android, the user can hide the keyboard - without changing the focus -
/// with the system back button. They can restore the keyboard's visibility
/// by tapping on a text field. The user might hide the keyboard and
/// switch to a physical keyboard, or they might just need to get it
/// out of the way for a moment, to expose something it's
/// obscuring. In this case requesting the focus again will not
/// cause the focus to change, and will not make the keyboard visible.
///
/// This widget builds an [EditableText] and will ensure that the keyboard is
/// showing when it is tapped by calling
/// [EditableTextState.requestKeyboard()].
final FocusNode? focusNode;
/// The decoration to show around the text field.
///
/// By default, draws a horizontal line under the text field but can be
/// configured to show an icon, label, hint text, and error text.
///
/// Specify null to remove the decoration entirely (including the
/// extra padding introduced by the decoration to save space for the labels).
final InputDecoration? decoration;
/// {@macro flutter.widgets.editableText.keyboardType}
final TextInputType keyboardType;
/// The type of action button to use for the keyboard.
///
/// Defaults to [TextInputAction.newline] if [keyboardType] is
/// [TextInputType.multiline] and [TextInputAction.done] otherwise.
final TextInputAction? textInputAction;
/// {@macro flutter.widgets.editableText.textCapitalization}
final TextCapitalization textCapitalization;
/// The style to use for the text being edited.
///
/// This text style is also used as the base style for the [decoration].
///
/// If null, defaults to the `subtitle1` text style from the current [Theme].
final TextStyle? style;
/// {@macro flutter.widgets.editableText.strutStyle}
final StrutStyle? strutStyle;
/// {@macro flutter.widgets.editableText.textAlign}
final TextAlign textAlign;
/// {@macro flutter.material.InputDecorator.textAlignVertical}
final TextAlignVertical? textAlignVertical;
/// {@macro flutter.widgets.editableText.textDirection}
final TextDirection? textDirection;
/// {@macro flutter.widgets.editableText.autofocus}
final bool autofocus;
/// {@macro flutter.widgets.editableText.obscuringCharacter}
final String obscuringCharacter;
/// {@macro flutter.widgets.editableText.obscureText}
final bool obscureText;
/// {@macro flutter.widgets.editableText.autocorrect}
final bool autocorrect;
/// {@macro flutter.services.TextInputConfiguration.smartDashesType}
final SmartDashesType smartDashesType;
/// {@macro flutter.services.TextInputConfiguration.smartQuotesType}
final SmartQuotesType smartQuotesType;
/// {@macro flutter.services.TextInputConfiguration.enableSuggestions}
final bool enableSuggestions;
/// {@macro flutter.widgets.editableText.maxLines}
/// * [expands], which determines whether the field should fill the height of
/// its parent.
final int? maxLines;
/// {@macro flutter.widgets.editableText.minLines}
/// * [expands], which determines whether the field should fill the height of
/// its parent.
final int? minLines;
/// {@macro flutter.widgets.editableText.expands}
final bool expands;
/// {@macro flutter.widgets.editableText.readOnly}
final bool readOnly;
/// Configuration of toolbar options.
///
/// If not set, select all and paste will default to be enabled. Copy and cut
/// will be disabled if [obscureText] is true. If [readOnly] is true,
/// paste and cut will be disabled regardless.
final ToolbarOptions toolbarOptions;
/// {@macro flutter.widgets.editableText.showCursor}
final bool? showCursor;
/// If [maxLength] is set to this value, only the "current input length"
/// part of the character counter is shown.
static const int noMaxLength = -1;
/// The maximum number of characters (Unicode scalar values) to allow in the
/// text field.
///
/// If set, a character counter will be displayed below the
/// field showing how many characters have been entered. If set to a number
/// greater than 0, it will also display the maximum number allowed. If set
/// to [TextField.noMaxLength] then only the current character count is
/// displayed.
///
/// After [maxLength] characters have been input, additional input
/// is ignored, unless [maxLengthEnforcement] is set to
/// [MaxLengthEnforcement.none].
///
/// The text field enforces the length with a
/// [LengthLimitingTextInputFormatter], which is evaluated after the supplied
/// [inputFormatters], if any.
///
/// This value must be either null, [TextField.noMaxLength], or greater than
/// 0.
///
/// If null (the default) then there is no limit to the number of characters
/// that can be entered. If set to [TextField.noMaxLength], then no limit will
/// be enforced, but the number of characters entered will still be displayed.
///
/// Whitespace characters (e.g. newline, space, tab) are included in the
/// character count.
///
/// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength}
final int? maxLength;
/// If [maxLength] is set, [maxLengthEnforced] indicates whether or not to
/// enforce the limit, or merely provide a character counter and warning when
/// [maxLength] is exceeded.
///
/// If true, prevents the field from allowing more than [maxLength]
/// characters.
@Deprecated(
'Use maxLengthEnforcement parameter which provides more specific '
'behavior related to the maxLength limit. '
'This feature was deprecated after v1.25.0-5.0.pre.',
)
final bool maxLengthEnforced;
/// Determines how the [maxLength] limit should be enforced.
///
/// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement}
///
/// {@macro flutter.services.textFormatter.maxLengthEnforcement}
final MaxLengthEnforcement? maxLengthEnforcement;
/// {@macro flutter.widgets.editableText.onChanged}
///
/// See also:
///
/// * [inputFormatters], which are called before [onChanged]
/// runs and can validate and change ("format") the input value.
/// * [onEditingComplete], [onSubmitted]:
/// which are more specialized input change notifications.
final ValueChanged<String>? onChanged;
/// {@macro flutter.widgets.editableText.onEditingComplete}
final VoidCallback? onEditingComplete;
/// {@macro flutter.widgets.editableText.onSubmitted}
///
/// See also:
///
/// * [TextInputAction.next] and [TextInputAction.previous], which
/// automatically shift the focus to the next/previous focusable item when
/// the user is done editing.
final ValueChanged<String>? onSubmitted;
/// {@macro flutter.widgets.editableText.onAppPrivateCommand}
final AppPrivateCommandCallback? onAppPrivateCommand;
/// {@macro flutter.widgets.editableText.inputFormatters}
final List<TextInputFormatter>? inputFormatters;
/// If false the text field is "disabled": it ignores taps and its
/// [decoration] is rendered in grey.
///
/// If non-null this property overrides the [decoration]'s
/// [InputDecoration.enabled] property.
final bool? enabled;
/// {@macro flutter.widgets.editableText.cursorWidth}
final double cursorWidth;
/// {@macro flutter.widgets.editableText.cursorHeight}
final double? cursorHeight;
/// {@macro flutter.widgets.editableText.cursorRadius}
final Radius? cursorRadius;
/// The color of the cursor.
///
/// The cursor indicates the current location of text insertion point in
/// the field.
///
/// If this is null it will default to the ambient
/// [TextSelectionThemeData.cursorColor]. If that is null, and the
/// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS]
/// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use
/// the value of [ColorScheme.primary] of [ThemeData.colorScheme].
final Color? cursorColor;
/// Controls how tall the selection highlight boxes are computed to be.
///
/// See [ui.BoxHeightStyle] for details on available styles.
final ui.BoxHeightStyle selectionHeightStyle;
/// Controls how wide the selection highlight boxes are computed to be.
///
/// See [ui.BoxWidthStyle] for details on available styles.
final ui.BoxWidthStyle selectionWidthStyle;
/// The appearance of the keyboard.
///
/// This setting is only honored on iOS devices.
///
/// If unset, defaults to the brightness of
/// [ThemeData.primaryColorBrightness].
final Brightness? keyboardAppearance;
/// {@macro flutter.widgets.editableText.scrollPadding}
final EdgeInsets scrollPadding;
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
final bool enableInteractiveSelection;
/// {@macro flutter.widgets.editableText.selectionControls}
final TextSelectionControls? selectionControls;
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
final DragStartBehavior dragStartBehavior;
/// {@macro flutter.widgets.editableText.selectionEnabled}
bool get selectionEnabled => enableInteractiveSelection;
/// {@template flutter.material.textfield.onTap}
/// Called for each distinct tap except for every second tap of a double tap.
///
/// The text field builds a [GestureDetector] to handle input events like tap,
/// to trigger focus requests, to move the caret, adjust the selection, etc.
/// Handling some of those events by wrapping the text field with a competing
/// GestureDetector is problematic.
///
/// To unconditionally handle taps, without interfering with the text field's
/// internal gesture detector, provide this callback.
///
/// If the text field is created with [enabled] false, taps will not be
/// recognized.
///
/// To be notified when the text field gains or loses the focus, provide a
/// [focusNode] and add a listener to that.
///
/// To listen to arbitrary pointer events without competing with the
/// text field's internal gesture detector, use a [Listener].
/// {@endtemplate}
final GestureTapCallback? onTap;
/// The cursor for a mouse pointer when it enters or is hovering over the
/// widget.
///
/// If [mouseCursor] is a [MaterialStateProperty<MouseCursor>],
/// [MaterialStateProperty.resolve] is used for the following
/// [MaterialState]s:
///
/// * [MaterialState.error].
/// * [MaterialState.hovered].
/// * [MaterialState.focused].
/// * [MaterialState.disabled].
///
/// If this property is null, [MaterialStateMouseCursor.textable] will be
/// used.
///
/// The [mouseCursor] is the only property of [TextField] that controls the
/// appearance of the mouse pointer. All other properties related to "cursor"
/// stand for the text cursor, which is usually a blinking vertical line at
/// the editing position.
final MouseCursor? mouseCursor;
/// Callback that generates a custom [InputDecoration.counter] widget.
///
/// See [InputCounterWidgetBuilder] for an explanation of the passed in
/// arguments. The returned widget will be placed below the line in place of
/// the default widget built when [InputDecoration.counterText] is specified.
///
/// The returned widget will be wrapped in a [Semantics] widget for
/// accessibility, but it also needs to be accessible itself. For example,
/// if returning a Text widget, set the [Text.semanticsLabel] property.
///
/// {@tool snippet}
/// ```dart
/// Widget counter(
/// BuildContext context,
/// {
/// required int currentLength,
/// required int? maxLength,
/// required bool isFocused,
/// }
/// ) {
/// return Text(
/// '$currentLength of $maxLength characters',
/// semanticsLabel: 'character count',
/// );
/// }
/// ```
/// {@end-tool}
///
/// If buildCounter returns null, then no counter and no Semantics widget will
/// be created at all.
final InputCounterWidgetBuilder? buildCounter;
/// {@macro flutter.widgets.editableText.scrollPhysics}
final ScrollPhysics? scrollPhysics;
/// {@macro flutter.widgets.editableText.scrollController}
final ScrollController? scrollController;
/// {@macro flutter.widgets.editableText.autofillHints}
/// {@macro flutter.services.AutofillConfiguration.autofillHints}
final Iterable<String>? autofillHints;
/// {@template flutter.material.textfield.restorationId}
/// Restoration ID to save and restore the state of the text field.
///
/// If non-null, the text field will persist and restore its current scroll
/// offset and - if no [controller] has been provided - the content of the
/// text field. If a [controller] has been provided, it is the responsibility
/// of the owner of that controller to persist and restore it, e.g. by using
/// a [RestorableTextEditingController].
///
/// The state of this widget is persisted in a [RestorationBucket] claimed
/// from the surrounding [RestorationScope] using the provided restoration ID.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
/// {@endtemplate}
final String? restorationId;
/// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning}
final bool enableIMEPersonalizedLearning;
@override
_StreamMessageTextFieldState createState() => _StreamMessageTextFieldState();
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<FocusNode>('focusNode', focusNode,
defaultValue: null));
properties
.add(DiagnosticsProperty<bool>('enabled', enabled, defaultValue: null));
properties.add(DiagnosticsProperty<InputDecoration>(
'decoration', decoration,
defaultValue: const InputDecoration()));
properties.add(DiagnosticsProperty<TextInputType>(
'keyboardType', keyboardType,
defaultValue: TextInputType.text));
properties.add(
DiagnosticsProperty<TextStyle>('style', style, defaultValue: null));
properties.add(
DiagnosticsProperty<bool>('autofocus', autofocus, defaultValue: false));
properties.add(DiagnosticsProperty<String>(
'obscuringCharacter', obscuringCharacter,
defaultValue: ''));
properties.add(DiagnosticsProperty<bool>('obscureText', obscureText,
defaultValue: false));
properties.add(DiagnosticsProperty<bool>('autocorrect', autocorrect,
defaultValue: true));
properties.add(EnumProperty<SmartDashesType>(
'smartDashesType', smartDashesType,
defaultValue:
obscureText ? SmartDashesType.disabled : SmartDashesType.enabled));
properties.add(EnumProperty<SmartQuotesType>(
'smartQuotesType', smartQuotesType,
defaultValue:
obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled));
properties.add(DiagnosticsProperty<bool>(
'enableSuggestions', enableSuggestions,
defaultValue: true));
properties.add(IntProperty('maxLines', maxLines, defaultValue: 1));
properties.add(IntProperty('minLines', minLines, defaultValue: null));
properties.add(
DiagnosticsProperty<bool>('expands', expands, defaultValue: false));
properties.add(IntProperty('maxLength', maxLength, defaultValue: null));
properties.add(EnumProperty<MaxLengthEnforcement>(
'maxLengthEnforcement', maxLengthEnforcement,
defaultValue: null));
properties.add(EnumProperty<TextInputAction>(
'textInputAction', textInputAction,
defaultValue: null));
properties.add(EnumProperty<TextCapitalization>(
'textCapitalization', textCapitalization,
defaultValue: TextCapitalization.none));
properties.add(EnumProperty<TextAlign>('textAlign', textAlign,
defaultValue: TextAlign.start));
properties.add(DiagnosticsProperty<TextAlignVertical>(
'textAlignVertical', textAlignVertical,
defaultValue: null));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection,
defaultValue: null));
properties
.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0));
properties
.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null));
properties.add(DiagnosticsProperty<Radius>('cursorRadius', cursorRadius,
defaultValue: null));
properties
.add(ColorProperty('cursorColor', cursorColor, defaultValue: null));
properties.add(DiagnosticsProperty<Brightness>(
'keyboardAppearance', keyboardAppearance,
defaultValue: null));
properties.add(DiagnosticsProperty<EdgeInsetsGeometry>(
'scrollPadding', scrollPadding,
defaultValue: const EdgeInsets.all(20)));
properties.add(FlagProperty('selectionEnabled',
value: selectionEnabled,
defaultValue: true,
ifFalse: 'selection disabled'));
properties.add(DiagnosticsProperty<TextSelectionControls>(
'selectionControls', selectionControls,
defaultValue: null));
properties.add(DiagnosticsProperty<ScrollController>(
'scrollController', scrollController,
defaultValue: null));
properties.add(DiagnosticsProperty<ScrollPhysics>(
'scrollPhysics', scrollPhysics,
defaultValue: null));
properties.add(DiagnosticsProperty<bool>(
'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning,
defaultValue: true));
}
}
class _StreamMessageTextFieldState extends State<StreamMessageTextField>
with RestorationMixin<StreamMessageTextField> {
RestorableMessageInputController? _controller;
MessageInputController get _effectiveController =>
widget.controller ?? _controller!.value;
@override
void initState() {
super.initState();
if (widget.controller == null) {
_createLocalController();
}
}
void _createLocalController([Message? message]) {
assert(_controller == null, '');
_controller = RestorableMessageInputController(message: message);
}
@override
void didUpdateWidget(covariant StreamMessageTextField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.controller == null && oldWidget.controller != null) {
_createLocalController(oldWidget.controller!.value);
} else if (widget.controller != null && oldWidget.controller == null) {
unregisterFromRestoration(_controller!);
_controller!.dispose();
_controller = null;
}
}
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
if (_controller != null) {
_registerController();
}
}
@override
String? get restorationId => widget.restorationId;
void _registerController() {
assert(_controller != null, '');
registerForRestoration(_controller!, restorationId ?? 'controller');
}
@override
Widget build(BuildContext context) => TextField(
controller: _effectiveController.textEditingController,
onChanged: (newText) {
_effectiveController.text = newText;
},
focusNode: widget.focusNode,
decoration: widget.decoration,
keyboardType: widget.keyboardType,
textInputAction: widget.textInputAction,
textCapitalization: widget.textCapitalization,
style: widget.style,
strutStyle: widget.strutStyle,
textAlign: widget.textAlign,
textAlignVertical: widget.textAlignVertical,
textDirection: widget.textDirection,
readOnly: widget.readOnly,
toolbarOptions: widget.toolbarOptions,
showCursor: widget.showCursor,
autofocus: widget.autofocus,
obscuringCharacter: widget.obscuringCharacter,
obscureText: widget.obscureText,
autocorrect: widget.autocorrect,
smartDashesType: widget.smartDashesType,
smartQuotesType: widget.smartQuotesType,
enableSuggestions: widget.enableSuggestions,
maxLines: widget.maxLines,
minLines: widget.minLines,
expands: widget.expands,
maxLength: widget.maxLength,
maxLengthEnforcement: widget.maxLengthEnforcement,
onEditingComplete: widget.onEditingComplete,
onSubmitted: widget.onSubmitted,
onAppPrivateCommand: widget.onAppPrivateCommand,
inputFormatters: widget.inputFormatters,
enabled: widget.enabled,
cursorWidth: widget.cursorWidth,
cursorHeight: widget.cursorHeight,
cursorRadius: widget.cursorRadius,
cursorColor: widget.cursorColor,
selectionHeightStyle: widget.selectionHeightStyle,
selectionWidthStyle: widget.selectionWidthStyle,
keyboardAppearance: widget.keyboardAppearance,
scrollPadding: widget.scrollPadding,
dragStartBehavior: widget.dragStartBehavior,
enableInteractiveSelection: widget.enableInteractiveSelection,
selectionControls: widget.selectionControls,
onTap: widget.onTap,
mouseCursor: widget.mouseCursor,
buildCounter: widget.buildCounter,
scrollController: widget.scrollController,
scrollPhysics: widget.scrollPhysics,
autofillHints: widget.autofillHints,
restorationId: widget.restorationId,
enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning,
);
@override
void dispose() {
_controller?.dispose();
super.dispose();
}
}
File diff suppressed because it is too large Load Diff
@@ -11,22 +11,22 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:visibility_detector/visibility_detector.dart'; import 'package:visibility_detector/visibility_detector.dart';
/// Widget builder for message /// Widget builder for message
/// [defaultMessageWidget] is the default [MessageWidget] configuration /// [defaultMessageWidget] is the default [StreamMessageWidget] configuration
/// Use [defaultMessageWidget.copyWith] to easily customize it /// Use [defaultMessageWidget.copyWith] to easily customize it
typedef MessageBuilder = Widget Function( typedef MessageBuilder = Widget Function(
BuildContext, BuildContext,
MessageDetails, MessageDetails,
List<Message>, List<Message>,
MessageWidget defaultMessageWidget, StreamMessageWidget defaultMessageWidget,
); );
/// Widget builder for parent message /// Widget builder for parent message
/// [defaultMessageWidget] is the default [MessageWidget] configuration /// [defaultMessageWidget] is the default [StreamMessageWidget] configuration
/// Use [defaultMessageWidget.copyWith] to easily customize it /// Use [defaultMessageWidget.copyWith] to easily customize it
typedef ParentMessageBuilder = Widget Function( typedef ParentMessageBuilder = Widget Function(
BuildContext, BuildContext,
Message?, Message?,
MessageWidget defaultMessageWidget, StreamMessageWidget defaultMessageWidget,
); );
/// Widget builder for system message /// Widget builder for system message
@@ -123,6 +123,11 @@ class MessageDetails {
final int index; 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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview_paint.png)
/// ///
@@ -165,9 +170,10 @@ class MessageDetails {
/// The widget components render the ui based on the first /// The widget components render the ui based on the first
/// ancestor of type [StreamChatTheme]. /// ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MessageListView extends StatefulWidget { /// {@endtemplate}
class StreamMessageListView extends StatefulWidget {
/// Instantiate a new MessageListView /// Instantiate a new MessageListView
const MessageListView({ const StreamMessageListView({
Key? key, Key? key,
this.showScrollToBottom = true, this.showScrollToBottom = true,
this.scrollToBottomBuilder, this.scrollToBottomBuilder,
@@ -197,7 +203,6 @@ class MessageListView extends StatefulWidget {
this.messageFilter, this.messageFilter,
this.onMessageTap, this.onMessageTap,
this.onSystemMessageTap, this.onSystemMessageTap,
this.pinPermissions = const [],
this.showFloatingDateDivider = true, this.showFloatingDateDivider = true,
this.threadSeparatorBuilder, this.threadSeparatorBuilder,
this.messageListController, this.messageListController,
@@ -320,9 +325,6 @@ class MessageListView extends StatefulWidget {
/// Called when system message is tapped /// Called when system message is tapped
final OnMessageTap? onSystemMessageTap; 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 /// Builder used to build the thread separator in case it's a thread view
final WidgetBuilder? threadSeparatorBuilder; final WidgetBuilder? threadSeparatorBuilder;
@@ -340,10 +342,10 @@ class MessageListView extends StatefulWidget {
final SpacingWidgetBuilder? spacingWidgetBuilder; final SpacingWidgetBuilder? spacingWidgetBuilder;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _StreamMessageListViewState createState() => _StreamMessageListViewState();
} }
class _MessageListViewState extends State<MessageListView> { class _StreamMessageListViewState extends State<StreamMessageListView> {
ItemScrollController? _scrollController; ItemScrollController? _scrollController;
void Function(Message)? _onThreadTap; void Function(Message)? _onThreadTap;
final ValueNotifier<bool> _showScrollToBottom = ValueNotifier(false); final ValueNotifier<bool> _showScrollToBottom = ValueNotifier(false);
@@ -351,6 +353,7 @@ class _MessageListViewState extends State<MessageListView> {
int? _messageListLength; int? _messageListLength;
StreamChannelState? streamChannel; StreamChannelState? streamChannel;
late StreamChatThemeData _streamTheme; late StreamChatThemeData _streamTheme;
late List<String> _userPermissions;
int get _initialIndex { int get _initialIndex {
final initialScrollIndex = widget.initialScrollIndex; final initialScrollIndex = widget.initialScrollIndex;
@@ -470,7 +473,7 @@ class _MessageListViewState extends State<MessageListView> {
final child = Stack( final child = Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
ConnectionStatusBuilder( StreamConnectionStatusBuilder(
statusBuilder: (context, status) { statusBuilder: (context, status) {
var statusString = ''; var statusString = '';
var showStatus = true; var showStatus = true;
@@ -487,7 +490,7 @@ class _MessageListViewState extends State<MessageListView> {
break; break;
} }
return InfoTile( return StreamInfoTile(
showMessage: widget.showConnectionStateTile && showStatus, showMessage: widget.showConnectionStateTile && showStatus,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
@@ -535,6 +538,7 @@ class _MessageListViewState extends State<MessageListView> {
return ((index + 2) * 2) - 1; return ((index + 2) * 2) - 1;
} }
} }
return null;
}, },
// Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages) // Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages)
@@ -600,7 +604,7 @@ class _MessageListViewState extends State<MessageListView> {
) )
: Padding( : Padding(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
child: DateDivider( child: StreamDateDivider(
dateTime: nextMessage.createdAt.toLocal(), dateTime: nextMessage.createdAt.toLocal(),
), ),
); );
@@ -738,8 +742,10 @@ class _MessageListViewState extends State<MessageListView> {
], ],
); );
final backgroundColor = MessageListViewTheme.of(context).backgroundColor; final backgroundColor =
final backgroundImage = MessageListViewTheme.of(context).backgroundImage; StreamMessageListViewTheme.of(context).backgroundColor;
final backgroundImage =
StreamMessageListViewTheme.of(context).backgroundImage;
if (backgroundColor != null || backgroundImage != null) { if (backgroundColor != null || backgroundImage != null) {
return Container( return Container(
@@ -769,7 +775,7 @@ class _MessageListViewState extends State<MessageListView> {
child: Text( child: Text(
context.translations.threadSeparatorText(replyCount), context.translations.threadSeparatorText(replyCount),
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: ChannelHeaderTheme.of(context).subtitleStyle, style: StreamChannelHeaderTheme.of(context).subtitleStyle,
), ),
), ),
); );
@@ -823,7 +829,7 @@ class _MessageListViewState extends State<MessageListView> {
final message = messages[index - 2]; final message = messages[index - 2];
return widget.dateDividerBuilder != null return widget.dateDividerBuilder != null
? widget.dateDividerBuilder!(message.createdAt.toLocal()) ? widget.dateDividerBuilder!(message.createdAt.toLocal())
: DateDivider(dateTime: message.createdAt.toLocal()); : StreamDateDivider(dateTime: message.createdAt.toLocal());
}, },
), ),
); );
@@ -992,7 +998,7 @@ class _MessageListViewState extends State<MessageListView> {
final currentUserMember = final currentUserMember =
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
final defaultMessageWidget = MessageWidget( final defaultMessageWidget = StreamMessageWidget(
showReplyMessage: false, showReplyMessage: false,
showResendMessage: false, showResendMessage: false,
showThreadReplyMessage: false, showThreadReplyMessage: false,
@@ -1038,7 +1044,7 @@ class _MessageListViewState extends State<MessageListView> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}, },
showPinButton: currentUserMember != null && showPinButton: currentUserMember != null &&
widget.pinPermissions.contains(currentUserMember.role), _userPermissions.contains(PermissionType.pinMessage),
); );
if (widget.parentMessageBuilder != null) { if (widget.parentMessageBuilder != null) {
@@ -1056,7 +1062,7 @@ class _MessageListViewState extends State<MessageListView> {
if ((message.type == 'system' || message.type == 'error') && if ((message.type == 'system' || message.type == 'error') &&
message.text?.isNotEmpty == true) { message.text?.isNotEmpty == true) {
return widget.systemMessageBuilder?.call(context, message) ?? return widget.systemMessageBuilder?.call(context, message) ??
SystemMessage( StreamSystemMessage(
message: message, message: message,
onMessageTap: (message) { onMessageTap: (message) {
if (widget.onSystemMessageTap != null) { if (widget.onSystemMessageTap != null) {
@@ -1126,7 +1132,7 @@ class _MessageListViewState extends State<MessageListView> {
final currentUserMember = final currentUserMember =
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id); members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
Widget messageWidget = MessageWidget( Widget messageWidget = StreamMessageWidget(
message: message, message: message,
reverse: isMyMessage, reverse: isMyMessage,
showReactions: !message.isDeleted, showReactions: !message.isDeleted,
@@ -1157,7 +1163,10 @@ class _MessageListViewState extends State<MessageListView> {
}, },
showEditMessage: isMyMessage, showEditMessage: isMyMessage,
showDeleteMessage: isMyMessage, showDeleteMessage: isMyMessage,
showThreadReplyMessage: !isThreadMessage, showThreadReplyMessage: !isThreadMessage &&
streamChannel?.channel.ownCapabilities
.contains(PermissionType.sendReply) ==
true,
showFlagButton: !isMyMessage, showFlagButton: !isMyMessage,
borderSide: borderSide, borderSide: borderSide,
onThreadTap: _onThreadTap, onThreadTap: _onThreadTap,
@@ -1226,7 +1235,7 @@ class _MessageListViewState extends State<MessageListView> {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
}, },
showPinButton: currentUserMember != null && showPinButton: currentUserMember != null &&
widget.pinPermissions.contains(currentUserMember.role), _userPermissions.contains(PermissionType.pinMessage),
); );
if (widget.messageBuilder != null) { if (widget.messageBuilder != null) {
@@ -1239,7 +1248,7 @@ class _MessageListViewState extends State<MessageListView> {
index, index,
), ),
messages, messages,
messageWidget as MessageWidget, messageWidget as StreamMessageWidget,
); );
} }
@@ -1306,6 +1315,7 @@ class _MessageListViewState extends State<MessageListView> {
void didChangeDependencies() { void didChangeDependencies() {
final newStreamChannel = StreamChannel.of(context); final newStreamChannel = StreamChannel.of(context);
_streamTheme = StreamChatTheme.of(context); _streamTheme = StreamChatTheme.of(context);
_userPermissions = newStreamChannel.channel.ownCapabilities;
if (newStreamChannel != streamChannel) { if (newStreamChannel != streamChannel) {
streamChannel = newStreamChannel; 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/src/reaction_bubble.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 /// Modal widget for displaying message reactions
class MessageReactionsModal extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating a [MessageReactionsModal] reactions class StreamMessageReactionsModal extends StatelessWidget {
const MessageReactionsModal({ /// Constructor for creating a [StreamMessageReactionsModal] reactions
const StreamMessageReactionsModal({
Key? key, Key? key,
required this.message, required this.message,
required this.messageWidget, required this.messageWidget,
required this.messageTheme, required this.messageTheme,
this.showReactions = true, this.showReactions,
this.reverse = false, this.reverse = false,
this.onUserAvatarTap, this.onUserAvatarTap,
}) : super(key: key); }) : super(key: key);
@@ -24,14 +30,14 @@ class MessageReactionsModal extends StatelessWidget {
/// Message to display reactions of /// Message to display reactions of
final Message message; final Message message;
/// [MessageThemeData] to apply to [message] /// [StreamMessageThemeData] to apply to [message]
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// Flag to reverse message /// Flag to reverse message
final bool reverse; final bool reverse;
/// Flag to show reactions on message /// Flag to show reactions on message
final bool showReactions; final bool? showReactions;
/// Callback when user avatar is tapped /// Callback when user avatar is tapped
final void Function(User)? onUserAvatarTap; final void Function(User)? onUserAvatarTap;
@@ -40,6 +46,10 @@ class MessageReactionsModal extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final user = StreamChat.of(context).currentUser; 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; final roughMaxSize = size.width * 2 / 3;
var messageTextLength = message.text!.length; var messageTextLength = message.text!.length;
@@ -71,7 +81,7 @@ class MessageReactionsModal extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
if (showReactions && if ((showReactions ?? hasReactionPermission) &&
(message.status == MessageSendingStatus.sent)) (message.status == MessageSendingStatus.sent))
Align( Align(
alignment: Alignment( alignment: Alignment(
@@ -84,7 +94,7 @@ class MessageReactionsModal extends StatelessWidget {
: -(1.2 - divFactor)), : -(1.2 - divFactor)),
0, 0,
), ),
child: ReactionPicker( child: StreamReactionPicker(
message: message, message: message,
), ),
), ),
@@ -196,7 +206,7 @@ class MessageReactionsModal extends StatelessWidget {
Stack( Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
UserAvatar( StreamUserAvatar(
onTap: onUserAvatarTap, onTap: onUserAvatarTap,
user: reaction.user!, user: reaction.user!,
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
@@ -216,7 +226,7 @@ class MessageReactionsModal extends StatelessWidget {
child: Align( child: Align(
alignment: alignment:
reverse ? Alignment.centerRight : Alignment.centerLeft, reverse ? Alignment.centerRight : Alignment.centerLeft,
child: ReactionBubble( child: StreamReactionBubble(
reactions: [reaction], reactions: [reaction],
flipTail: !reverse, flipTail: !reverse,
borderColor: borderColor:
@@ -2,17 +2,23 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro message_search_item}
@Deprecated("Use 'StreamMessageSearchItem' instead")
typedef MessageSearchItem = StreamMessageSearchItem;
/// {@template message_search_item}
/// It shows the current [Message] preview. /// It shows the current [Message] preview.
/// ///
/// Usually you don't use this widget as it's the default item used by /// Usually you don't use this widget as it's the default item used by
/// [MessageSearchListView]. /// [StreamMessageSearchListView].
/// ///
/// The widget renders the ui based on the first ancestor of type /// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme]. /// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MessageSearchItem extends StatelessWidget { /// {@endtemplate}
class StreamMessageSearchItem extends StatelessWidget {
/// Instantiate a new MessageSearchItem /// Instantiate a new MessageSearchItem
const MessageSearchItem({ const StreamMessageSearchItem({
Key? key, Key? key,
required this.getMessageResponse, required this.getMessageResponse,
this.onTap, this.onTap,
@@ -25,7 +31,7 @@ class MessageSearchItem extends StatelessWidget {
/// Function called when tapping this widget /// Function called when tapping this widget
final VoidCallback? onTap; final VoidCallback? onTap;
/// If true the [MessageSearchItem] will show the current online Status /// If true the [StreamMessageSearchItem] will show the current online Status
final bool showOnlineStatus; final bool showOnlineStatus;
@override @override
@@ -34,10 +40,10 @@ class MessageSearchItem extends StatelessWidget {
final channel = getMessageResponse.channel; final channel = getMessageResponse.channel;
final channelName = channel?.extraData['name']; final channelName = channel?.extraData['name'];
final user = message.user!; final user = message.user!;
final channelPreviewTheme = ChannelPreviewTheme.of(context); final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
return ListTile( return ListTile(
onTap: onTap, onTap: onTap,
leading: UserAvatar( leading: StreamUserAvatar(
user: user, user: user,
showOnlineStatus: showOnlineStatus, showOnlineStatus: showOnlineStatus,
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
@@ -120,7 +126,7 @@ class MessageSearchItem extends StatelessWidget {
text = parts.join(' '); text = parts.join(' ');
} }
final channelPreviewTheme = ChannelPreviewTheme.of(context); final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
return Text.rich( return Text.rich(
_getDisplayText( _getDisplayText(
text!, text!,
@@ -11,13 +11,17 @@ typedef MessageSearchItemBuilder = Widget Function(
GetMessageResponse, GetMessageResponse,
); );
/// Builder used when [MessageSearchListView] is empty /// Builder used when [StreamMessageSearchListView] is empty
typedef EmptyMessageSearchBuilder = Widget Function( typedef EmptyMessageSearchBuilder = Widget Function(
BuildContext context, BuildContext context,
String searchQuery, String searchQuery,
); );
/// /// {@macro message_search_list_view}
@Deprecated("Use 'StreamMessageSearchListView' instead")
typedef MessageSearchListView = StreamMessageSearchListView;
/// {@template message_search_list_view}
/// It shows the list of searched messages. /// It shows the list of searched messages.
/// ///
/// ```dart /// ```dart
@@ -47,9 +51,10 @@ typedef EmptyMessageSearchBuilder = Widget Function(
/// The widget components render the ui based on the first ancestor of type /// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme]. /// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MessageSearchListView extends StatefulWidget { /// {@endtemplate}
class StreamMessageSearchListView extends StatefulWidget {
/// Instantiate a new MessageSearchListView /// Instantiate a new MessageSearchListView
const MessageSearchListView({ const StreamMessageSearchListView({
Key? key, Key? key,
required this.filters, required this.filters,
this.messageQuery, this.messageQuery,
@@ -96,7 +101,7 @@ class MessageSearchListView extends StatefulWidget {
/// Builder used to create a custom item preview /// Builder used to create a custom item preview
final MessageSearchItemBuilder? itemBuilder; final MessageSearchItemBuilder? itemBuilder;
/// Function called when tapping on a [MessageSearchItem] /// Function called when tapping on a [StreamMessageSearchItem]
final MessageSearchItemTapCallback? onItemTap; final MessageSearchItemTapCallback? onItemTap;
/// Builder used to create a custom item separator /// Builder used to create a custom item separator
@@ -130,10 +135,12 @@ class MessageSearchListView extends StatefulWidget {
final MessageSearchListController? messageSearchListController; final MessageSearchListController? messageSearchListController;
@override @override
_MessageSearchListViewState createState() => _MessageSearchListViewState(); _StreamMessageSearchListViewState createState() =>
_StreamMessageSearchListViewState();
} }
class _MessageSearchListViewState extends State<MessageSearchListView> { class _StreamMessageSearchListViewState
extends State<StreamMessageSearchListView> {
late final _defaultController = MessageSearchListController(); late final _defaultController = MessageSearchListController();
MessageSearchListController get _messageSearchListController => MessageSearchListController get _messageSearchListController =>
@@ -168,7 +175,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
if (error is Error) { if (error is Error) {
print(error.stackTrace); print(error.stackTrace);
} }
return InfoTile( return StreamInfoTile(
showMessage: widget.showErrorTile, showMessage: widget.showErrorTile,
tileAnchor: Alignment.topCenter, tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter,
@@ -195,7 +202,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
); );
final backgroundColor = final backgroundColor =
MessageSearchListViewTheme.of(context).backgroundColor; StreamMessageSearchListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) { if (backgroundColor != null) {
return ColoredBox( return ColoredBox(
@@ -219,7 +226,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
if (widget.itemBuilder != null) { if (widget.itemBuilder != null) {
return widget.itemBuilder!(context, getMessageResponse); return widget.itemBuilder!(context, getMessageResponse);
} }
return MessageSearchItem( return StreamMessageSearchItem(
getMessageResponse: getMessageResponse, getMessageResponse: getMessageResponse,
onTap: () => widget.onItemTap!(getMessageResponse), onTap: () => widget.onItemTap!(getMessageResponse),
); );
@@ -1,12 +1,19 @@
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.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'; 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 /// Text widget to display in message
class MessageText extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating a [MessageText] widget class StreamMessageText extends StatelessWidget {
const MessageText({ /// Constructor for creating a [StreamMessageText] widget
const StreamMessageText({
Key? key, Key? key,
required this.message, required this.message,
required this.messageTheme, required this.messageTheme,
@@ -23,8 +30,8 @@ class MessageText extends StatelessWidget {
/// Callback for when link is tapped /// Callback for when link is tapped
final void Function(String)? onLinkTap; final void Function(String)? onLinkTap;
/// [MessageThemeData] whose text theme is to be applied /// [StreamMessageThemeData] whose text theme is to be applied
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -34,13 +41,14 @@ class MessageText extends StatelessWidget {
stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'), stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'),
initialData: streamChat.currentUser!.language ?? 'en', initialData: streamChat.currentUser!.language ?? 'en',
builder: (context, language) { builder: (context, language) {
final translatedText = final messageText = message
message.i18n?['${language}_text'] ?? message.text; .translate(language)
final messageText = .replaceMentions()
_replaceMentions(translatedText ?? '').replaceAll('\n', '\n\n'); .text
?.replaceAll('\n', '\n\n');
final themeData = Theme.of(context); final themeData = Theme.of(context);
return MarkdownBody( return MarkdownBody(
data: messageText, data: messageText ?? '',
onTapLink: ( onTapLink: (
String link, String link,
String? href, 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, 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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget_paint.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. /// 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 /// 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 /// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme]. /// [StreamChatTheme].
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MessageWidget extends StatefulWidget { /// {@endtemplate}
/// class StreamMessageWidget extends StatefulWidget {
MessageWidget({ /// Creates a new instance of the message widget.
StreamMessageWidget({
Key? key, Key? key,
required this.message, required this.message,
required this.messageTheme, required this.messageTheme,
@@ -121,7 +127,7 @@ class MessageWidget extends StatefulWidget {
context, context,
Material( Material(
color: messageTheme.messageBackgroundColor, color: messageTheme.messageBackgroundColor,
child: ImageGroup( child: StreamImageGroup(
size: Size( size: Size(
mediaQueryData.size.width * 0.8, mediaQueryData.size.width * 0.8,
mediaQueryData.size.height * 0.3, mediaQueryData.size.height * 0.3,
@@ -142,7 +148,7 @@ class MessageWidget extends StatefulWidget {
return wrapAttachmentWidget( return wrapAttachmentWidget(
context, context,
ImageAttachment( StreamImageAttachment(
attachment: attachments[0], attachment: attachments[0],
message: message, message: message,
messageTheme: messageTheme, messageTheme: messageTheme,
@@ -172,7 +178,7 @@ class MessageWidget extends StatefulWidget {
Column( Column(
children: attachments.map((attachment) { children: attachments.map((attachment) {
final mediaQueryData = MediaQuery.of(context); final mediaQueryData = MediaQuery.of(context);
return VideoAttachment( return StreamVideoAttachment(
attachment: attachment, attachment: attachment,
messageTheme: messageTheme, messageTheme: messageTheme,
size: Size( size: Size(
@@ -204,7 +210,7 @@ class MessageWidget extends StatefulWidget {
Column( Column(
children: attachments.map((attachment) { children: attachments.map((attachment) {
final mediaQueryData = MediaQuery.of(context); final mediaQueryData = MediaQuery.of(context);
return GiphyAttachment( return StreamGiphyAttachment(
attachment: attachment, attachment: attachment,
message: message, message: message,
size: Size( size: Size(
@@ -240,7 +246,7 @@ class MessageWidget extends StatefulWidget {
final mediaQueryData = MediaQuery.of(context); final mediaQueryData = MediaQuery.of(context);
return wrapAttachmentWidget( return wrapAttachmentWidget(
context, context,
FileAttachment( StreamFileAttachment(
message: message, message: message,
attachment: attachment, attachment: attachment,
size: Size( size: Size(
@@ -300,7 +306,7 @@ class MessageWidget extends StatefulWidget {
final Message message; final Message message;
/// The message theme /// The message theme
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// If true the widget will be mirrored /// If true the widget will be mirrored
final bool reverse; final bool reverse;
@@ -356,7 +362,7 @@ class MessageWidget extends StatefulWidget {
/// The function called when tapping on a link /// The function called when tapping on a link
final void Function(String)? onLinkTap; final void Function(String)? onLinkTap;
/// Used in [MessageReactionsModal] and [MessageActionsModal] /// Used in [StreamMessageReactionsModal] and [StreamMessageActionsModal]
final bool showReactionPickerIndicator; final bool showReactionPickerIndicator;
/// List of users who read /// List of users who read
@@ -417,13 +423,13 @@ class MessageWidget extends StatefulWidget {
final void Function(Message)? onMessageTap; final void Function(Message)? onMessageTap;
/// List of custom actions shown on message long tap /// List of custom actions shown on message long tap
final List<MessageAction> customActions; final List<StreamMessageAction> customActions;
/// Customize onTap on attachment /// Customize onTap on attachment
final void Function(Message message, Attachment attachment)? onAttachmentTap; final void Function(Message message, Attachment attachment)? onAttachmentTap;
/// Creates a copy of [MessageWidget] with specified attributes overridden. /// Creates a copy of [StreamMessageWidget] with specified attributes overridden.
MessageWidget copyWith({ StreamMessageWidget copyWith({
Key? key, Key? key,
void Function(User)? onMentionTap, void Function(User)? onMentionTap,
void Function(Message)? onThreadTap, void Function(Message)? onThreadTap,
@@ -435,7 +441,7 @@ class MessageWidget extends StatefulWidget {
Widget Function(BuildContext, Message)? deletedBottomRowBuilder, Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
void Function(BuildContext, Message)? onMessageActions, void Function(BuildContext, Message)? onMessageActions,
Message? message, Message? message,
MessageThemeData? messageTheme, StreamMessageThemeData? messageTheme,
bool? reverse, bool? reverse,
ShapeBorder? shape, ShapeBorder? shape,
ShapeBorder? attachmentShape, ShapeBorder? attachmentShape,
@@ -473,11 +479,11 @@ class MessageWidget extends StatefulWidget {
bool? translateUserAvatar, bool? translateUserAvatar,
OnQuotedMessageTap? onQuotedMessageTap, OnQuotedMessageTap? onQuotedMessageTap,
void Function(Message)? onMessageTap, void Function(Message)? onMessageTap,
List<MessageAction>? customActions, List<StreamMessageAction>? customActions,
void Function(Message message, Attachment attachment)? onAttachmentTap, void Function(Message message, Attachment attachment)? onAttachmentTap,
Widget Function(BuildContext, User)? userAvatarBuilder, Widget Function(BuildContext, User)? userAvatarBuilder,
}) => }) =>
MessageWidget( StreamMessageWidget(
key: key ?? this.key, key: key ?? this.key,
onMentionTap: onMentionTap ?? this.onMentionTap, onMentionTap: onMentionTap ?? this.onMentionTap,
onThreadTap: onThreadTap ?? this.onThreadTap, onThreadTap: onThreadTap ?? this.onThreadTap,
@@ -539,11 +545,11 @@ class MessageWidget extends StatefulWidget {
); );
@override @override
_MessageWidgetState createState() => _MessageWidgetState(); _StreamMessageWidgetState createState() => _StreamMessageWidgetState();
} }
class _MessageWidgetState extends State<MessageWidget> class _StreamMessageWidgetState extends State<StreamMessageWidget>
with AutomaticKeepAliveClientMixin<MessageWidget> { with AutomaticKeepAliveClientMixin<StreamMessageWidget> {
bool get showThreadReplyIndicator => widget.showThreadReplyIndicator; bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
bool get showSendingIndicator => widget.showSendingIndicator; bool get showSendingIndicator => widget.showSendingIndicator;
@@ -717,7 +723,7 @@ class _MessageWidgetState extends State<MessageWidget>
? 0 ? 0
: 4.0, : 4.0,
), ),
child: DeletedMessage( child: StreamDeletedMessage(
borderRadiusGeometry: widget borderRadiusGeometry: widget
.borderRadiusGeometry, .borderRadiusGeometry,
borderSide: borderSide:
@@ -854,7 +860,7 @@ class _MessageWidgetState extends State<MessageWidget>
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
: null; : null;
final chatThemeData = _streamChatTheme; final chatThemeData = _streamChatTheme;
return QuotedMessageWidget( return StreamQuotedMessageWidget(
onTap: onTap, onTap: onTap,
message: widget.message.quotedMessage!, message: widget.message.quotedMessage!,
messageTheme: isMyMessage messageTheme: isMyMessage
@@ -1008,7 +1014,7 @@ class _MessageWidgetState extends State<MessageWidget>
getWebsiteName(hostName.toLowerCase()) ?? getWebsiteName(hostName.toLowerCase()) ??
hostName.capitalize(); hostName.capitalize();
return UrlAttachment( return StreamUrlAttachment(
urlAttachment: urlAttachment, urlAttachment: urlAttachment,
hostDisplayName: hostDisplayName, hostDisplayName: hostDisplayName,
textPadding: widget.textPadding, textPadding: widget.textPadding,
@@ -1042,7 +1048,7 @@ class _MessageWidgetState extends State<MessageWidget>
child: _shouldShowReactions child: _shouldShowReactions
? GestureDetector( ? GestureDetector(
onTap: () => _showMessageReactionsModalBottomSheet(context), onTap: () => _showMessageReactionsModalBottomSheet(context),
child: ReactionBubble( child: StreamReactionBubble(
key: ValueKey('${widget.message.id}.reactions'), key: ValueKey('${widget.message.id}.reactions'),
reverse: widget.reverse, reverse: widget.reverse,
flipTail: widget.reverse, flipTail: widget.reverse,
@@ -1073,7 +1079,7 @@ class _MessageWidgetState extends State<MessageWidget>
barrierColor: _streamChatTheme.colorTheme.overlay, barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageActionsModal( child: StreamMessageActionsModal(
messageWidget: widget.copyWith( messageWidget: widget.copyWith(
key: const Key('MessageWidget'), key: const Key('MessageWidget'),
message: widget.message.copyWith( message: widget.message.copyWith(
@@ -1088,7 +1094,8 @@ class _MessageWidgetState extends State<MessageWidget>
showSendingIndicator: false, showSendingIndicator: false,
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
showReactionPickerIndicator: widget.showReactions && showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent), (widget.message.status == MessageSendingStatus.sent) &&
channel.ownCapabilities.contains(PermissionType.sendReaction),
showPinHighlight: false, showPinHighlight: false,
showUserAvatar: showUserAvatar:
widget.message.user!.id == channel.client.state.currentUser!.id widget.message.user!.id == channel.client.state.currentUser!.id
@@ -1099,7 +1106,6 @@ class _MessageWidgetState extends State<MessageWidget>
Clipboard.setData(ClipboardData(text: message.text)), Clipboard.setData(ClipboardData(text: message.text)),
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
reverse: widget.reverse, reverse: widget.reverse,
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
message: widget.message, message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder, editMessageInputBuilder: widget.editMessageInputBuilder,
onReplyTap: widget.onReplyTap, onReplyTap: widget.onReplyTap,
@@ -1109,11 +1115,6 @@ class _MessageWidgetState extends State<MessageWidget>
showCopyMessage: widget.showCopyMessage && showCopyMessage: widget.showCopyMessage &&
!isFailedState && !isFailedState &&
widget.message.text?.trim().isNotEmpty == true, widget.message.text?.trim().isNotEmpty == true,
showEditMessage: widget.showEditMessage &&
!isDeleteFailed &&
!widget.message.attachments
.any((element) => element.type == 'giphy'),
showReactions: widget.showReactions,
showReplyMessage: widget.showReplyMessage && showReplyMessage: widget.showReplyMessage &&
!isFailedState && !isFailedState &&
widget.onReplyTap != null, widget.onReplyTap != null,
@@ -1121,7 +1122,6 @@ class _MessageWidgetState extends State<MessageWidget>
!isFailedState && !isFailedState &&
widget.onThreadTap != null, widget.onThreadTap != null,
showFlagButton: widget.showFlagButton, showFlagButton: widget.showFlagButton,
showPinButton: widget.showPinButton,
customActions: widget.customActions, customActions: widget.customActions,
), ),
), ),
@@ -1136,7 +1136,7 @@ class _MessageWidgetState extends State<MessageWidget>
barrierColor: _streamChatTheme.colorTheme.overlay, barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel( builder: (context) => StreamChannel(
channel: channel, channel: channel,
child: MessageReactionsModal( child: StreamMessageReactionsModal(
messageWidget: widget.copyWith( messageWidget: widget.copyWith(
key: const Key('MessageWidget'), key: const Key('MessageWidget'),
message: widget.message.copyWith( message: widget.message.copyWith(
@@ -1151,7 +1151,8 @@ class _MessageWidgetState extends State<MessageWidget>
showSendingIndicator: false, showSendingIndicator: false,
padding: const EdgeInsets.all(0), padding: const EdgeInsets.all(0),
showReactionPickerIndicator: widget.showReactions && showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent), (widget.message.status == MessageSendingStatus.sent) &&
channel.ownCapabilities.contains(PermissionType.sendReaction),
showPinHighlight: false, showPinHighlight: false,
showUserAvatar: showUserAvatar:
widget.message.user!.id == channel.client.state.currentUser!.id widget.message.user!.id == channel.client.state.currentUser!.id
@@ -1162,7 +1163,8 @@ class _MessageWidgetState extends State<MessageWidget>
messageTheme: widget.messageTheme, messageTheme: widget.messageTheme,
reverse: widget.reverse, reverse: widget.reverse,
message: widget.message, message: widget.message,
showReactions: widget.showReactions, showReactions: widget.showReactions &&
channel.ownCapabilities.contains(PermissionType.sendReaction),
), ),
), ),
); );
@@ -1250,6 +1252,13 @@ class _MessageWidgetState extends State<MessageWidget>
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
if (!channel.ownCapabilities.contains(PermissionType.readEvents)) {
return StreamSendingIndicator(
message: message,
size: style!.fontSize,
);
}
return BetterStreamBuilder<List<Read>>( return BetterStreamBuilder<List<Read>>(
stream: channel.state?.readStream, stream: channel.state?.readStream,
initialData: channel.state?.read, initialData: channel.state?.read,
@@ -1259,7 +1268,7 @@ class _MessageWidgetState extends State<MessageWidget>
(it.lastRead.isAfter(message.createdAt) || (it.lastRead.isAfter(message.createdAt) ||
it.lastRead.isAtSameMomentAs(message.createdAt))); it.lastRead.isAtSameMomentAs(message.createdAt)));
final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1; final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1;
Widget child = SendingIndicator( Widget child = StreamSendingIndicator(
message: message, message: message,
isMessageRead: isMessageRead, isMessageRead: isMessageRead,
size: style!.fontSize, size: style!.fontSize,
@@ -1293,7 +1302,7 @@ class _MessageWidgetState extends State<MessageWidget>
: 0, : 0,
), ),
child: widget.userAvatarBuilder?.call(context, widget.message.user!) ?? child: widget.userAvatarBuilder?.call(context, widget.message.user!) ??
UserAvatar( StreamUserAvatar(
user: widget.message.user!, user: widget.message.user!,
onTap: widget.onUserAvatarTap, onTap: widget.onUserAvatarTap,
constraints: widget.messageTheme.avatarTheme!.constraints, constraints: widget.messageTheme.avatarTheme!.constraints,
@@ -1311,7 +1320,7 @@ class _MessageWidgetState extends State<MessageWidget>
padding: isOnlyEmoji ? EdgeInsets.zero : widget.textPadding, padding: isOnlyEmoji ? EdgeInsets.zero : widget.textPadding,
child: widget.textBuilder != null child: widget.textBuilder != null
? widget.textBuilder!(context, widget.message) ? widget.textBuilder!(context, widget.message)
: MessageText( : StreamMessageText(
onLinkTap: widget.onLinkTap, onLinkTap: widget.onLinkTap,
message: widget.message, message: widget.message,
onMentionTap: widget.onMentionTap, onMentionTap: widget.onMentionTap,
@@ -1428,7 +1437,7 @@ class _ThreadParticipants extends StatelessWidget {
color: _streamChatTheme.colorTheme.barsBg, color: _streamChatTheme.colorTheme.barsBg,
), ),
padding: const EdgeInsets.all(1), padding: const EdgeInsets.all(1),
child: UserAvatar( child: StreamUserAvatar(
user: user, user: user,
constraints: BoxConstraints.loose(const Size.fromRadius(7)), constraints: BoxConstraints.loose(const Size.fromRadius(7)),
showOnlineStatus: false, showOnlineStatus: false,
@@ -2,15 +2,21 @@ import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_portal/flutter_portal.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] /// Widget that renders a single overlay widget from a list of [overlayOptions]
/// It shows the first one that is visible /// It shows the first one that is visible
class MultiOverlay extends StatelessWidget { /// {@endtemplate}
class StreamMultiOverlay extends StatelessWidget {
/// Constructs a new MultiOverlay widget /// Constructs a new MultiOverlay widget
/// [overlayOptions] - the list of overlay options /// [overlayOptions] - the list of overlay options
/// [overlayAnchor] - the anchor relative to the overlay /// [overlayAnchor] - the anchor relative to the overlay
/// [childAnchor] - the anchor relative to the child /// [childAnchor] - the anchor relative to the child
/// [child] - the child widget /// [child] - the child widget
const MultiOverlay({ const StreamMultiOverlay({
Key? key, Key? key,
required this.overlayOptions, required this.overlayOptions,
required this.child, required this.child,
@@ -1,12 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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] /// List tile for [ChannelBottomSheet]
class OptionListTile extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating [OptionListTile] class StreamOptionListTile extends StatelessWidget {
const OptionListTile({ /// Constructor for creating [StreamOptionListTile]
const StreamOptionListTile({
Key? key, Key? key,
this.title, required this.title,
this.leading, this.leading,
this.trailing, this.trailing,
this.onTap, this.onTap,
@@ -17,7 +23,7 @@ class OptionListTile extends StatelessWidget {
}) : super(key: key); }) : super(key: key);
/// Title for tile /// Title for tile
final String? title; final String title;
/// Leading widget (start) /// Leading widget (start)
final Widget? leading; final Widget? leading;
@@ -46,8 +52,8 @@ class OptionListTile extends StatelessWidget {
return Column( return Column(
children: [ children: [
Container( Container(
color: separatorColor ?? chatThemeData.colorTheme.disabled,
height: 1, height: 1,
color: separatorColor ?? chatThemeData.colorTheme.disabled,
), ),
Material( Material(
color: tileColor ?? chatThemeData.colorTheme.barsBg, color: tileColor ?? chatThemeData.colorTheme.barsBg,
@@ -57,15 +63,14 @@ class OptionListTile extends StatelessWidget {
onTap: onTap, onTap: onTap,
child: Row( child: Row(
children: [ children: [
if (leading != null) Center(child: leading), if (leading != null)
if (leading == null) Center(child: leading)
const SizedBox( else
width: 16, const SizedBox(width: 16),
),
Expanded( Expanded(
flex: 4, flex: 4,
child: Text( child: Text(
title!, title,
style: titleTextStyle ?? style: titleTextStyle ??
(titleColor == null (titleColor == null
? chatThemeData.textTheme.bodyBold ? chatThemeData.textTheme.bodyBold
@@ -10,54 +10,14 @@ typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
Attachment, Attachment,
); );
class _VideoAttachmentThumbnail extends StatefulWidget { /// Widget for the quoted message.
const _VideoAttachmentThumbnail({ @Deprecated("Use 'StreamQuotedMessageWidget' instead")
Key? key, typedef QuotedMessageWidget = StreamQuotedMessageWidget;
required this.attachment,
this.size = const Size(32, 32),
}) : super(key: key);
final Size size; /// Widget for the quoted message.
final Attachment attachment; class StreamQuotedMessageWidget extends StatelessWidget {
/// Creates a new instance of the widget.
@override const StreamQuotedMessageWidget({
_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({
Key? key, Key? key,
required this.message, required this.message,
required this.messageTheme, required this.messageTheme,
@@ -73,7 +33,7 @@ class QuotedMessageWidget extends StatelessWidget {
final Message message; final Message message;
/// The message theme /// The message theme
final MessageThemeData messageTheme; final StreamMessageThemeData messageTheme;
/// If true the widget will be mirrored /// If true the widget will be mirrored
final bool reverse; final bool reverse;
@@ -134,7 +94,7 @@ class QuotedMessageWidget extends StatelessWidget {
if (_hasAttachments) _parseAttachments(context), if (_hasAttachments) _parseAttachments(context),
if (msg.text!.isNotEmpty) if (msg.text!.isNotEmpty)
Flexible( Flexible(
child: MessageText( child: StreamMessageText(
message: msg, message: msg,
messageTheme: isOnlyEmoji && _containsText messageTheme: isOnlyEmoji && _containsText
? messageTheme.copyWith( ? messageTheme.copyWith(
@@ -231,7 +191,7 @@ class QuotedMessageWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
); );
Widget _buildUserAvatar() => UserAvatar( Widget _buildUserAvatar() => StreamUserAvatar(
user: message.user!, user: message.user!,
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 24, height: 24,
@@ -242,7 +202,7 @@ class QuotedMessageWidget extends StatelessWidget {
Map<String, QuotedMessageAttachmentThumbnailBuilder> Map<String, QuotedMessageAttachmentThumbnailBuilder>
get _defaultAttachmentBuilder => { get _defaultAttachmentBuilder => {
'image': (_, attachment) => ImageAttachment( 'image': (_, attachment) => StreamImageAttachment(
attachment: attachment, attachment: attachment,
message: message, message: message,
messageTheme: messageTheme, messageTheme: messageTheme,
@@ -288,3 +248,47 @@ class QuotedMessageWidget extends StatelessWidget {
return messageTheme.messageBackgroundColor; 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:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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 /// Creates reaction bubble widget for displaying over messages
class ReactionBubble extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating a [ReactionBubble] class StreamReactionBubble extends StatelessWidget {
const ReactionBubble({ /// Constructor for creating a [StreamReactionBubble]
const StreamReactionBubble({
Key? key, Key? key,
required this.reactions, required this.reactions,
required this.borderColor, required this.borderColor,
@@ -111,7 +117,7 @@ class ReactionBubble extends StatelessWidget {
} }
Widget _buildReaction( Widget _buildReaction(
List<ReactionIcon> reactionIcons, List<StreamReactionIcon> reactionIcons,
Reaction reaction, Reaction reaction,
BuildContext context, BuildContext context,
) { ) {
@@ -1,9 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Reaction icon data /// Reaction icon data
class ReactionIcon { @Deprecated("Use 'StreamReactionIcon' instead")
/// Constructor for creating [ReactionIcon] typedef ReactionIcon = StreamReactionIcon;
ReactionIcon({
/// Reaction icon data
class StreamReactionIcon {
/// Constructor for creating [StreamReactionIcon]
StreamReactionIcon({
required this.type, required this.type,
required this.builder, 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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.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 /// It shows a reaction picker
/// ///
/// Usually you don't use this widget as it's one of the default widgets used /// Usually you don't use this widget as it's one of the default widgets used
/// by [MessageWidget.onMessageActions]. /// by [StreamMessageWidget.onMessageActions].
class ReactionPicker extends StatefulWidget { /// {@endtemplate}
/// Constructor for creating a [ReactionPicker] widget class StreamReactionPicker extends StatefulWidget {
const ReactionPicker({ /// Constructor for creating a [StreamReactionPicker] widget
const StreamReactionPicker({
Key? key, Key? key,
required this.message, required this.message,
}) : super(key: key); }) : super(key: key);
@@ -21,10 +27,10 @@ class ReactionPicker extends StatefulWidget {
final Message message; final Message message;
@override @override
_ReactionPickerState createState() => _ReactionPickerState(); _StreamReactionPickerState createState() => _StreamReactionPickerState();
} }
class _ReactionPickerState extends State<ReactionPicker> class _StreamReactionPickerState extends State<StreamReactionPicker>
with TickerProviderStateMixin { with TickerProviderStateMixin {
List<EzAnimation> animations = []; List<EzAnimation> animations = [];
@@ -1,10 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro sending_indicator}
@Deprecated("Use 'StreamSendingIndicator' instead")
typedef SendingIndicator = StreamSendingIndicator;
/// {@template sending_indicator}
/// Used to show the sending status of the message /// Used to show the sending status of the message
class SendingIndicator extends StatelessWidget { /// {@endtemplate}
/// Constructor for creating a [SendingIndicator] widget class StreamSendingIndicator extends StatelessWidget {
const SendingIndicator({ /// Constructor for creating a [StreamSendingIndicator] widget
const StreamSendingIndicator({
Key? key, Key? key,
required this.message, required this.message,
this.isMessageRead = false, this.isMessageRead = false,
@@ -38,29 +38,29 @@ class StreamChatThemeData {
/// Create a theme from scratch /// Create a theme from scratch
factory StreamChatThemeData({ factory StreamChatThemeData({
Brightness? brightness, Brightness? brightness,
TextTheme? textTheme, StreamTextTheme? textTheme,
ColorTheme? colorTheme, StreamColorTheme? colorTheme,
ChannelListHeaderThemeData? channelListHeaderTheme, StreamChannelListHeaderThemeData? channelListHeaderTheme,
ChannelPreviewThemeData? channelPreviewTheme, StreamChannelPreviewThemeData? channelPreviewTheme,
ChannelHeaderThemeData? channelHeaderTheme, StreamChannelHeaderThemeData? channelHeaderTheme,
MessageThemeData? otherMessageTheme, StreamMessageThemeData? otherMessageTheme,
MessageThemeData? ownMessageTheme, StreamMessageThemeData? ownMessageTheme,
MessageInputThemeData? messageInputTheme, StreamMessageInputThemeData? messageInputTheme,
Widget Function(BuildContext, User)? defaultUserImage, Widget Function(BuildContext, User)? defaultUserImage,
Widget Function(BuildContext, User)? placeholderUserImage, Widget Function(BuildContext, User)? placeholderUserImage,
IconThemeData? primaryIconTheme, IconThemeData? primaryIconTheme,
List<ReactionIcon>? reactionIcons, List<StreamReactionIcon>? reactionIcons,
GalleryHeaderThemeData? imageHeaderTheme, StreamGalleryHeaderThemeData? imageHeaderTheme,
GalleryFooterThemeData? imageFooterTheme, StreamGalleryFooterThemeData? imageFooterTheme,
MessageListViewThemeData? messageListViewTheme, StreamMessageListViewThemeData? messageListViewTheme,
ChannelListViewThemeData? channelListViewTheme, StreamChannelListViewThemeData? channelListViewTheme,
UserListViewThemeData? userListViewTheme, StreamUserListViewThemeData? userListViewTheme,
MessageSearchListViewThemeData? messageSearchListViewTheme, StreamMessageSearchListViewThemeData? messageSearchListViewTheme,
}) { }) {
brightness ??= colorTheme?.brightness ?? Brightness.light; brightness ??= colorTheme?.brightness ?? Brightness.light;
final isDark = brightness == Brightness.dark; final isDark = brightness == Brightness.dark;
textTheme ??= isDark ? TextTheme.dark() : TextTheme.light(); textTheme ??= isDark ? StreamTextTheme.dark() : StreamTextTheme.light();
colorTheme ??= isDark ? ColorTheme.dark() : ColorTheme.light(); colorTheme ??= isDark ? StreamColorTheme.dark() : StreamColorTheme.light();
final defaultData = StreamChatThemeData.fromColorAndTextTheme( final defaultData = StreamChatThemeData.fromColorAndTextTheme(
colorTheme, colorTheme,
@@ -133,14 +133,14 @@ class StreamChatThemeData {
/// Create theme from color and text theme /// Create theme from color and text theme
factory StreamChatThemeData.fromColorAndTextTheme( factory StreamChatThemeData.fromColorAndTextTheme(
ColorTheme colorTheme, StreamColorTheme colorTheme,
TextTheme textTheme, StreamTextTheme textTheme,
) { ) {
final accentColor = colorTheme.accentPrimary; final accentColor = colorTheme.accentPrimary;
final iconTheme = final iconTheme =
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5)); IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5));
final channelHeaderTheme = ChannelHeaderThemeData( final channelHeaderTheme = StreamChannelHeaderThemeData(
avatarTheme: AvatarThemeData( avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 40, height: 40,
@@ -153,9 +153,9 @@ class StreamChatThemeData {
color: const Color(0xff7A7A7A), color: const Color(0xff7A7A7A),
), ),
); );
final channelPreviewTheme = ChannelPreviewThemeData( final channelPreviewTheme = StreamChannelPreviewThemeData(
unreadCounterColor: colorTheme.accentError, unreadCounterColor: colorTheme.accentError,
avatarTheme: AvatarThemeData( avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 40, height: 40,
@@ -176,14 +176,14 @@ class StreamChatThemeData {
colorTheme: colorTheme, colorTheme: colorTheme,
primaryIconTheme: iconTheme, primaryIconTheme: iconTheme,
defaultUserImage: (context, user) => Center( defaultUserImage: (context, user) => Center(
child: GradientAvatar( child: StreamGradientAvatar(
name: user.name, name: user.name,
userId: user.id, userId: user.id,
), ),
), ),
channelPreviewTheme: channelPreviewTheme, channelPreviewTheme: channelPreviewTheme,
channelListHeaderTheme: ChannelListHeaderThemeData( channelListHeaderTheme: StreamChannelListHeaderThemeData(
avatarTheme: AvatarThemeData( avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 40, height: 40,
@@ -194,7 +194,7 @@ class StreamChatThemeData {
titleStyle: textTheme.headlineBold, titleStyle: textTheme.headlineBold,
), ),
channelHeaderTheme: channelHeaderTheme, channelHeaderTheme: channelHeaderTheme,
ownMessageTheme: MessageThemeData( ownMessageTheme: StreamMessageThemeData(
messageAuthorStyle: messageAuthorStyle:
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis), textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
messageTextStyle: textTheme.body, messageTextStyle: textTheme.body,
@@ -206,7 +206,7 @@ class StreamChatThemeData {
reactionsBorderColor: colorTheme.borders, reactionsBorderColor: colorTheme.borders,
reactionsMaskColor: colorTheme.appBg, reactionsMaskColor: colorTheme.appBg,
messageBorderColor: colorTheme.disabled, messageBorderColor: colorTheme.disabled,
avatarTheme: AvatarThemeData( avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 32, height: 32,
@@ -218,7 +218,7 @@ class StreamChatThemeData {
), ),
linkBackgroundColor: colorTheme.linkBg, linkBackgroundColor: colorTheme.linkBg,
), ),
otherMessageTheme: MessageThemeData( otherMessageTheme: StreamMessageThemeData(
reactionsBackgroundColor: colorTheme.disabled, reactionsBackgroundColor: colorTheme.disabled,
reactionsBorderColor: colorTheme.barsBg, reactionsBorderColor: colorTheme.barsBg,
reactionsMaskColor: colorTheme.appBg, reactionsMaskColor: colorTheme.appBg,
@@ -233,7 +233,7 @@ class StreamChatThemeData {
), ),
messageBackgroundColor: colorTheme.barsBg, messageBackgroundColor: colorTheme.barsBg,
messageBorderColor: colorTheme.borders, messageBorderColor: colorTheme.borders,
avatarTheme: AvatarThemeData( avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor( constraints: const BoxConstraints.tightFor(
height: 32, height: 32,
@@ -242,7 +242,7 @@ class StreamChatThemeData {
), ),
linkBackgroundColor: colorTheme.linkBg, linkBackgroundColor: colorTheme.linkBg,
), ),
messageInputTheme: MessageInputThemeData( messageInputTheme: StreamMessageInputThemeData(
borderRadius: BorderRadius.circular(20), borderRadius: BorderRadius.circular(20),
sendAnimationDuration: const Duration(milliseconds: 300), sendAnimationDuration: const Duration(milliseconds: 300),
actionButtonColor: colorTheme.accentPrimary, actionButtonColor: colorTheme.accentPrimary,
@@ -252,6 +252,7 @@ class StreamChatThemeData {
sendButtonIdleColor: colorTheme.disabled, sendButtonIdleColor: colorTheme.disabled,
inputBackgroundColor: colorTheme.barsBg, inputBackgroundColor: colorTheme.barsBg,
inputTextStyle: textTheme.body, inputTextStyle: textTheme.body,
linkHighlightColor: colorTheme.accentPrimary,
idleBorderGradient: LinearGradient( idleBorderGradient: LinearGradient(
colors: [ colors: [
colorTheme.disabled, colorTheme.disabled,
@@ -266,7 +267,7 @@ class StreamChatThemeData {
), ),
), ),
reactionIcons: [ reactionIcons: [
ReactionIcon( StreamReactionIcon(
type: 'love', type: 'love',
builder: (context, highlighted, size) { builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
@@ -278,7 +279,7 @@ class StreamChatThemeData {
); );
}, },
), ),
ReactionIcon( StreamReactionIcon(
type: 'like', type: 'like',
builder: (context, highlighted, size) { builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
@@ -290,7 +291,7 @@ class StreamChatThemeData {
); );
}, },
), ),
ReactionIcon( StreamReactionIcon(
type: 'sad', type: 'sad',
builder: (context, highlighted, size) { builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
@@ -302,7 +303,7 @@ class StreamChatThemeData {
); );
}, },
), ),
ReactionIcon( StreamReactionIcon(
type: 'haha', type: 'haha',
builder: (context, highlighted, size) { builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
@@ -314,7 +315,7 @@ class StreamChatThemeData {
); );
}, },
), ),
ReactionIcon( StreamReactionIcon(
type: 'wow', type: 'wow',
builder: (context, highlighted, size) { builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context); final theme = StreamChatTheme.of(context);
@@ -327,7 +328,7 @@ class StreamChatThemeData {
}, },
), ),
], ],
galleryHeaderTheme: GalleryHeaderThemeData( galleryHeaderTheme: StreamGalleryHeaderThemeData(
closeButtonColor: colorTheme.textHighEmphasis, closeButtonColor: colorTheme.textHighEmphasis,
backgroundColor: channelHeaderTheme.color, backgroundColor: channelHeaderTheme.color,
iconMenuPointColor: colorTheme.textHighEmphasis, iconMenuPointColor: colorTheme.textHighEmphasis,
@@ -335,7 +336,7 @@ class StreamChatThemeData {
subtitleTextStyle: channelPreviewTheme.subtitleStyle, subtitleTextStyle: channelPreviewTheme.subtitleStyle,
bottomSheetBarrierColor: colorTheme.overlay, bottomSheetBarrierColor: colorTheme.overlay,
), ),
galleryFooterTheme: GalleryFooterThemeData( galleryFooterTheme: StreamGalleryFooterThemeData(
backgroundColor: colorTheme.barsBg, backgroundColor: colorTheme.barsBg,
shareIconColor: colorTheme.textHighEmphasis, shareIconColor: colorTheme.textHighEmphasis,
titleTextStyle: textTheme.headlineBold, titleTextStyle: textTheme.headlineBold,
@@ -345,52 +346,52 @@ class StreamChatThemeData {
bottomSheetPhotosTextStyle: textTheme.headlineBold, bottomSheetPhotosTextStyle: textTheme.headlineBold,
bottomSheetCloseIconColor: colorTheme.textHighEmphasis, bottomSheetCloseIconColor: colorTheme.textHighEmphasis,
), ),
messageListViewTheme: MessageListViewThemeData( messageListViewTheme: StreamMessageListViewThemeData(
backgroundColor: colorTheme.barsBg, backgroundColor: colorTheme.barsBg,
), ),
channelListViewTheme: ChannelListViewThemeData( channelListViewTheme: StreamChannelListViewThemeData(
backgroundColor: colorTheme.appBg, backgroundColor: colorTheme.appBg,
), ),
userListViewTheme: UserListViewThemeData( userListViewTheme: StreamUserListViewThemeData(
backgroundColor: colorTheme.appBg, backgroundColor: colorTheme.appBg,
), ),
messageSearchListViewTheme: MessageSearchListViewThemeData( messageSearchListViewTheme: StreamMessageSearchListViewThemeData(
backgroundColor: colorTheme.appBg, backgroundColor: colorTheme.appBg,
), ),
); );
} }
/// The text themes used in the widgets /// The text themes used in the widgets
final TextTheme textTheme; final StreamTextTheme textTheme;
/// The color themes used in the widgets /// The color themes used in the widgets
final ColorTheme colorTheme; final StreamColorTheme colorTheme;
/// Theme of the [ChannelPreview] /// Theme of the [StreamChannelPreview]
final ChannelPreviewThemeData channelPreviewTheme; final StreamChannelPreviewThemeData channelPreviewTheme;
/// Theme of the [ChannelListHeader] /// Theme of the [StreamChannelListHeader]
final ChannelListHeaderThemeData channelListHeaderTheme; final StreamChannelListHeaderThemeData channelListHeaderTheme;
/// Theme of the chat widgets dedicated to a channel header /// 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]. /// [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]. /// [StreamChatTheme].
final GalleryFooterThemeData galleryFooterTheme; final StreamGalleryFooterThemeData galleryFooterTheme;
/// Theme of the current user messages /// Theme of the current user messages
final MessageThemeData ownMessageTheme; final StreamMessageThemeData ownMessageTheme;
/// Theme of other users messages /// Theme of other users messages
final MessageThemeData otherMessageTheme; final StreamMessageThemeData otherMessageTheme;
/// Theme dedicated to the [MessageInput] widget /// Theme dedicated to the [StreamMessageInput] widget
final MessageInputThemeData messageInputTheme; final StreamMessageInputThemeData messageInputTheme;
/// The widget that will be built when the user image is unavailable /// The widget that will be built when the user image is unavailable
final Widget Function(BuildContext, User) defaultUserImage; final Widget Function(BuildContext, User) defaultUserImage;
@@ -402,41 +403,41 @@ class StreamChatThemeData {
final IconThemeData primaryIconTheme; final IconThemeData primaryIconTheme;
/// Assets used for rendering reactions /// Assets used for rendering reactions
final List<ReactionIcon> reactionIcons; final List<StreamReactionIcon> reactionIcons;
/// Theme configuration for the [MessageListView] widget. /// Theme configuration for the [StreamMessageListView] widget.
final MessageListViewThemeData messageListViewTheme; final StreamMessageListViewThemeData messageListViewTheme;
/// Theme configuration for the [ChannelListView] widget. /// Theme configuration for the [StreamChannelListView] widget.
final ChannelListViewThemeData channelListViewTheme; final StreamChannelListViewThemeData channelListViewTheme;
/// Theme configuration for the [UserListView] widget. /// Theme configuration for the [StreamUserListView] widget.
final UserListViewThemeData userListViewTheme; final StreamUserListViewThemeData userListViewTheme;
/// Theme configuration for the [MessageSearchListView] widget. /// Theme configuration for the [StreamMessageSearchListView] widget.
final MessageSearchListViewThemeData messageSearchListViewTheme; final StreamMessageSearchListViewThemeData messageSearchListViewTheme;
/// Creates a copy of [StreamChatThemeData] with specified attributes /// Creates a copy of [StreamChatThemeData] with specified attributes
/// overridden. /// overridden.
StreamChatThemeData copyWith({ StreamChatThemeData copyWith({
TextTheme? textTheme, StreamTextTheme? textTheme,
ColorTheme? colorTheme, StreamColorTheme? colorTheme,
ChannelPreviewThemeData? channelPreviewTheme, StreamChannelPreviewThemeData? channelPreviewTheme,
ChannelHeaderThemeData? channelHeaderTheme, StreamChannelHeaderThemeData? channelHeaderTheme,
MessageThemeData? ownMessageTheme, StreamMessageThemeData? ownMessageTheme,
MessageThemeData? otherMessageTheme, StreamMessageThemeData? otherMessageTheme,
MessageInputThemeData? messageInputTheme, StreamMessageInputThemeData? messageInputTheme,
Widget Function(BuildContext, User)? defaultUserImage, Widget Function(BuildContext, User)? defaultUserImage,
Widget Function(BuildContext, User)? placeholderUserImage, Widget Function(BuildContext, User)? placeholderUserImage,
IconThemeData? primaryIconTheme, IconThemeData? primaryIconTheme,
ChannelListHeaderThemeData? channelListHeaderTheme, StreamChannelListHeaderThemeData? channelListHeaderTheme,
List<ReactionIcon>? reactionIcons, List<StreamReactionIcon>? reactionIcons,
GalleryHeaderThemeData? galleryHeaderTheme, StreamGalleryHeaderThemeData? galleryHeaderTheme,
GalleryFooterThemeData? galleryFooterTheme, StreamGalleryFooterThemeData? galleryFooterTheme,
MessageListViewThemeData? messageListViewTheme, StreamMessageListViewThemeData? messageListViewTheme,
ChannelListViewThemeData? channelListViewTheme, StreamChannelListViewThemeData? channelListViewTheme,
UserListViewThemeData? userListViewTheme, StreamUserListViewThemeData? userListViewTheme,
MessageSearchListViewThemeData? messageSearchListViewTheme, StreamMessageSearchListViewThemeData? messageSearchListViewTheme,
}) => }) =>
StreamChatThemeData.raw( StreamChatThemeData.raw(
channelListHeaderTheme: channelListHeaderTheme:
@@ -1,10 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// It shows a date divider depending on the date difference /// {@macro system_message}
class SystemMessage extends StatelessWidget { @Deprecated("Use 'StreamSystemMessage' instead")
/// Constructor for creating a [SystemMessage] typedef SystemMessage = StreamSystemMessage;
const SystemMessage({
/// {@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, Key? key,
required this.message, required this.message,
this.onMessageTap, this.onMessageTap,
@@ -1,11 +1,17 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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. /// A style that overrides the default appearance of various avatar widgets.
/// {@endtemplate}
// ignore: prefer-match-file-name // ignore: prefer-match-file-name
class AvatarThemeData with Diagnosticable { class StreamAvatarThemeData with Diagnosticable {
/// Creates an [AvatarThemeData]. /// Creates an [StreamAvatarThemeData].
const AvatarThemeData({ const StreamAvatarThemeData({
BoxConstraints? constraints, BoxConstraints? constraints,
BorderRadius? borderRadius, BorderRadius? borderRadius,
}) : _constraints = constraints, }) : _constraints = constraints,
@@ -25,12 +31,12 @@ class AvatarThemeData with Diagnosticable {
/// Get border radius /// Get border radius
BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20); BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20);
/// Copy this [AvatarThemeData] to another. /// Copy this [StreamAvatarThemeData] to another.
AvatarThemeData copyWith({ StreamAvatarThemeData copyWith({
BoxConstraints? constraints, BoxConstraints? constraints,
BorderRadius? borderRadius, BorderRadius? borderRadius,
}) => }) =>
AvatarThemeData( StreamAvatarThemeData(
constraints: constraints ?? _constraints, constraints: constraints ?? _constraints,
borderRadius: borderRadius ?? _borderRadius, borderRadius: borderRadius ?? _borderRadius,
); );
@@ -38,12 +44,12 @@ class AvatarThemeData with Diagnosticable {
/// Linearly interpolate between two [UserAvatar] themes. /// Linearly interpolate between two [UserAvatar] themes.
/// ///
/// All the properties must be non-null. /// All the properties must be non-null.
AvatarThemeData lerp( StreamAvatarThemeData lerp(
AvatarThemeData a, StreamAvatarThemeData a,
AvatarThemeData b, StreamAvatarThemeData b,
double t, double t,
) => ) =>
AvatarThemeData( StreamAvatarThemeData(
borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t),
constraints: BoxConstraints.lerp(a.constraints, b.constraints, t), constraints: BoxConstraints.lerp(a.constraints, b.constraints, t),
); );
@@ -51,7 +57,7 @@ class AvatarThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is AvatarThemeData && other is StreamAvatarThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
_constraints == other._constraints && _constraints == other._constraints &&
_borderRadius == other._borderRadius; _borderRadius == other._borderRadius;
@@ -59,8 +65,8 @@ class AvatarThemeData with Diagnosticable {
@override @override
int get hashCode => _constraints.hashCode ^ _borderRadius.hashCode; int get hashCode => _constraints.hashCode ^ _borderRadius.hashCode;
/// Merges one [AvatarThemeData] with the another /// Merges one [StreamAvatarThemeData] with the another
AvatarThemeData merge(AvatarThemeData? other) { StreamAvatarThemeData merge(StreamAvatarThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
constraints: other._constraints, 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/avatar_theme.dart';
import 'package:stream_chat_flutter/src/theme/themes.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. /// Overrides the default style of [ChannelHeader] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [ChannelHeaderThemeData], which is used to configure this theme. /// * [StreamChannelHeaderThemeData], which is used to configure this theme.
class ChannelHeaderTheme extends InheritedTheme { /// {@endtemplate}
/// Creates a [ChannelHeaderTheme]. class StreamChannelHeaderTheme extends InheritedTheme {
/// Creates a [StreamChannelHeaderTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const ChannelHeaderTheme({ const StreamChannelHeaderTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final ChannelHeaderThemeData data; final StreamChannelHeaderThemeData data;
/// The closest instance of this class that encloses the given context. /// 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. /// [StreamChatThemeData.channelTheme.channelHeaderTheme] is used.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
@@ -32,34 +38,40 @@ class ChannelHeaderTheme extends InheritedTheme {
/// ```dart /// ```dart
/// final theme = ChannelHeaderTheme.of(context); /// final theme = ChannelHeaderTheme.of(context);
/// ``` /// ```
static ChannelHeaderThemeData of(BuildContext context) { static StreamChannelHeaderThemeData of(BuildContext context) {
final channelHeaderTheme = final channelHeaderTheme =
context.dependOnInheritedWidgetOfExactType<ChannelHeaderTheme>(); context.dependOnInheritedWidgetOfExactType<StreamChannelHeaderTheme>();
return channelHeaderTheme?.data ?? return channelHeaderTheme?.data ??
StreamChatTheme.of(context).channelHeaderTheme; StreamChatTheme.of(context).channelHeaderTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
ChannelHeaderTheme(data: data, child: child); StreamChannelHeaderTheme(data: data, child: child);
@override @override
bool updateShouldNotify(ChannelHeaderTheme oldWidget) => bool updateShouldNotify(StreamChannelHeaderTheme oldWidget) =>
data != oldWidget.data; 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 /// 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]. /// [StreamChatThemeData.channelHeaderTheme].
/// ///
/// See also: /// 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 /// * [StreamChatThemeData.channelHeaderTheme], which can be used to override
/// the default style for [ChannelHeader]s below the overall [StreamChatTheme]. /// the default style for [ChannelHeader]s below the overall [StreamChatTheme].
class ChannelHeaderThemeData with Diagnosticable { /// {@endtemplate}
/// Creates a [ChannelHeaderThemeData] class StreamChannelHeaderThemeData with Diagnosticable {
const ChannelHeaderThemeData({ /// Creates a [StreamChannelHeaderThemeData]
const StreamChannelHeaderThemeData({
this.titleStyle, this.titleStyle,
this.subtitleStyle, this.subtitleStyle,
this.avatarTheme, this.avatarTheme,
@@ -73,43 +85,43 @@ class ChannelHeaderThemeData with Diagnosticable {
final TextStyle? subtitleStyle; final TextStyle? subtitleStyle;
/// Theme for avatar /// Theme for avatar
final AvatarThemeData? avatarTheme; final StreamAvatarThemeData? avatarTheme;
/// Color for [ChannelHeaderThemeData] /// Color for [StreamChannelHeaderThemeData]
final Color? color; final Color? color;
/// Copy with theme /// Copy with theme
ChannelHeaderThemeData copyWith({ StreamChannelHeaderThemeData copyWith({
TextStyle? titleStyle, TextStyle? titleStyle,
TextStyle? subtitleStyle, TextStyle? subtitleStyle,
AvatarThemeData? avatarTheme, StreamAvatarThemeData? avatarTheme,
Color? color, Color? color,
}) => }) =>
ChannelHeaderThemeData( StreamChannelHeaderThemeData(
titleStyle: titleStyle ?? this.titleStyle, titleStyle: titleStyle ?? this.titleStyle,
subtitleStyle: subtitleStyle ?? this.subtitleStyle, subtitleStyle: subtitleStyle ?? this.subtitleStyle,
avatarTheme: avatarTheme ?? this.avatarTheme, avatarTheme: avatarTheme ?? this.avatarTheme,
color: color ?? this.color, color: color ?? this.color,
); );
/// Linearly interpolate between two [ChannelHeaderThemeData]. /// Linearly interpolate between two [StreamChannelHeaderThemeData].
/// ///
/// All the properties must be non-null. /// All the properties must be non-null.
ChannelHeaderThemeData lerp( StreamChannelHeaderThemeData lerp(
ChannelHeaderThemeData a, StreamChannelHeaderThemeData a,
ChannelHeaderThemeData b, StreamChannelHeaderThemeData b,
double t, double t,
) => ) =>
ChannelHeaderThemeData( StreamChannelHeaderThemeData(
titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t),
subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t), subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t),
avatarTheme: avatarTheme: const StreamAvatarThemeData()
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), .lerp(a.avatarTheme!, b.avatarTheme!, t),
color: Color.lerp(a.color, b.color, t), color: Color.lerp(a.color, b.color, t),
); );
/// Merge with other [ChannelHeaderThemeData] /// Merge with other [StreamChannelHeaderThemeData]
ChannelHeaderThemeData merge(ChannelHeaderThemeData? other) { StreamChannelHeaderThemeData merge(StreamChannelHeaderThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle,
@@ -123,7 +135,7 @@ class ChannelHeaderThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is ChannelHeaderThemeData && other is StreamChannelHeaderThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
titleStyle == other.titleStyle && titleStyle == other.titleStyle &&
subtitleStyle == other.subtitleStyle && subtitleStyle == other.subtitleStyle &&
@@ -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/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/theme/avatar_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. /// Overrides the default style of [ChannelListHeader] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [ChannelListHeaderThemeData], which is used to configure this theme. /// * [StreamChannelListHeaderThemeData], which is used to configure this theme.
class ChannelListHeaderTheme extends InheritedTheme { /// {@endtemplate}
/// Creates a [ChannelListHeaderTheme]. class StreamChannelListHeaderTheme extends InheritedTheme {
/// Creates a [StreamChannelListHeaderTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const ChannelListHeaderTheme({ const StreamChannelListHeaderTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final ChannelListHeaderThemeData data; final StreamChannelListHeaderThemeData data;
/// The closest instance of this class that encloses the given context. /// 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. /// [StreamChatThemeData.channelListHeaderTheme] is used.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
@@ -31,26 +37,32 @@ class ChannelListHeaderTheme extends InheritedTheme {
/// ```dart /// ```dart
/// final theme = ChannelListHeaderTheme.of(context); /// final theme = ChannelListHeaderTheme.of(context);
/// ``` /// ```
static ChannelListHeaderThemeData of(BuildContext context) { static StreamChannelListHeaderThemeData of(BuildContext context) {
final channelListHeaderTheme = final channelListHeaderTheme = context
context.dependOnInheritedWidgetOfExactType<ChannelListHeaderTheme>(); .dependOnInheritedWidgetOfExactType<StreamChannelListHeaderTheme>();
return channelListHeaderTheme?.data ?? return channelListHeaderTheme?.data ??
StreamChatTheme.of(context).channelListHeaderTheme; StreamChatTheme.of(context).channelListHeaderTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
ChannelListHeaderTheme(data: data, child: child); StreamChannelListHeaderTheme(data: data, child: child);
@override @override
bool updateShouldNotify(ChannelListHeaderTheme oldWidget) => bool updateShouldNotify(StreamChannelListHeaderTheme oldWidget) =>
data != oldWidget.data; 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] /// Theme dedicated to the [ChannelListHeader]
class ChannelListHeaderThemeData with Diagnosticable { /// {@endtemplate}
/// Returns a new [ChannelListHeaderThemeData] class StreamChannelListHeaderThemeData with Diagnosticable {
const ChannelListHeaderThemeData({ /// Returns a new [StreamChannelListHeaderThemeData]
const StreamChannelListHeaderThemeData({
this.titleStyle, this.titleStyle,
this.avatarTheme, this.avatarTheme,
this.color, this.color,
@@ -60,39 +72,41 @@ class ChannelListHeaderThemeData with Diagnosticable {
final TextStyle? titleStyle; final TextStyle? titleStyle;
/// Theme dedicated to the userAvatar /// Theme dedicated to the userAvatar
final AvatarThemeData? avatarTheme; final StreamAvatarThemeData? avatarTheme;
/// Background color of the appbar /// Background color of the appbar
final Color? color; final Color? color;
/// Returns a new [ChannelListHeaderThemeData] replacing some of its /// Returns a new [StreamChannelListHeaderThemeData] replacing some of its
/// properties /// properties
ChannelListHeaderThemeData copyWith({ StreamChannelListHeaderThemeData copyWith({
TextStyle? titleStyle, TextStyle? titleStyle,
AvatarThemeData? avatarTheme, StreamAvatarThemeData? avatarTheme,
Color? color, Color? color,
}) => }) =>
ChannelListHeaderThemeData( StreamChannelListHeaderThemeData(
titleStyle: titleStyle ?? this.titleStyle, titleStyle: titleStyle ?? this.titleStyle,
avatarTheme: avatarTheme ?? this.avatarTheme, avatarTheme: avatarTheme ?? this.avatarTheme,
color: color ?? this.color, color: color ?? this.color,
); );
/// Linearly interpolate from one [ChannelListHeaderThemeData] to another. /// Linearly interpolate from one [StreamChannelListHeaderThemeData] to another.
ChannelListHeaderThemeData lerp( StreamChannelListHeaderThemeData lerp(
ChannelListHeaderThemeData a, StreamChannelListHeaderThemeData a,
ChannelListHeaderThemeData b, StreamChannelListHeaderThemeData b,
double t, double t,
) => ) =>
ChannelListHeaderThemeData( StreamChannelListHeaderThemeData(
avatarTheme: avatarTheme: const StreamAvatarThemeData()
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), .lerp(a.avatarTheme!, b.avatarTheme!, t),
color: Color.lerp(a.color, b.color, t), color: Color.lerp(a.color, b.color, t),
titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t), titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t),
); );
/// Merges [this] [ChannelListHeaderThemeData] with the [other] /// Merges [this] [StreamChannelListHeaderThemeData] with the [other]
ChannelListHeaderThemeData merge(ChannelListHeaderThemeData? other) { StreamChannelListHeaderThemeData merge(
StreamChannelListHeaderThemeData? other,
) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle,
@@ -104,7 +118,7 @@ class ChannelListHeaderThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is ChannelListHeaderThemeData && other is StreamChannelListHeaderThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
titleStyle == other.titleStyle && titleStyle == other.titleStyle &&
avatarTheme == other.avatarTheme && avatarTheme == other.avatarTheme &&
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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. /// Overrides the default style of [ChannelListView] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [ChannelListViewThemeData], which is used to configure this theme. /// * [StreamChannelListViewThemeData], which is used to configure this theme.
class ChannelListViewTheme extends InheritedTheme { /// {@endtemplate}
/// Creates a [ChannelListViewTheme]. class StreamChannelListViewTheme extends InheritedTheme {
/// Creates a [StreamChannelListViewTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const ChannelListViewTheme({ const StreamChannelListViewTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final ChannelListViewThemeData data; final StreamChannelListViewThemeData data;
/// The closest instance of this class that encloses the given context. /// 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. /// [StreamChatThemeData.channelListViewTheme] is used.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
@@ -30,63 +36,69 @@ class ChannelListViewTheme extends InheritedTheme {
/// ```dart /// ```dart
/// ChannelListViewTheme theme = ChannelListViewTheme.of(context); /// ChannelListViewTheme theme = ChannelListViewTheme.of(context);
/// ``` /// ```
static ChannelListViewThemeData of(BuildContext context) { static StreamChannelListViewThemeData of(BuildContext context) {
final channelListViewTheme = final channelListViewTheme = context
context.dependOnInheritedWidgetOfExactType<ChannelListViewTheme>(); .dependOnInheritedWidgetOfExactType<StreamChannelListViewTheme>();
return channelListViewTheme?.data ?? return channelListViewTheme?.data ??
StreamChatTheme.of(context).channelListViewTheme; StreamChatTheme.of(context).channelListViewTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
ChannelListViewTheme(data: data, child: child); StreamChannelListViewTheme(data: data, child: child);
@override @override
bool updateShouldNotify(ChannelListViewTheme oldWidget) => bool updateShouldNotify(StreamChannelListViewTheme oldWidget) =>
data != oldWidget.data; 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 /// 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]. /// [StreamChatThemeData.channelListViewTheme].
/// ///
/// See also: /// 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 /// * [StreamChatThemeData.channelListViewTheme], which can be used to override
/// the default style for [ChannelListView]s below the overall /// the default style for [ChannelListView]s below the overall
/// [StreamChatTheme]. /// [StreamChatTheme].
class ChannelListViewThemeData with Diagnosticable { /// {@endtemplate}
/// Creates a [ChannelListViewThemeData]. class StreamChannelListViewThemeData with Diagnosticable {
const ChannelListViewThemeData({ /// Creates a [StreamChannelListViewThemeData].
const StreamChannelListViewThemeData({
this.backgroundColor, this.backgroundColor,
}); });
/// The color of the [ChannelListView] background. /// The color of the [ChannelListView] background.
final Color? backgroundColor; final Color? backgroundColor;
/// Copies this [ChannelListViewThemeData] to another. /// Copies this [StreamChannelListViewThemeData] to another.
ChannelListViewThemeData copyWith({ StreamChannelListViewThemeData copyWith({
Color? backgroundColor, Color? backgroundColor,
}) => }) =>
ChannelListViewThemeData( StreamChannelListViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
); );
/// Linearly interpolate between two [ChannelListViewThemeData] themes. /// Linearly interpolate between two [StreamChannelListViewThemeData] themes.
/// ///
/// All the properties must be non-null. /// All the properties must be non-null.
ChannelListViewThemeData lerp( StreamChannelListViewThemeData lerp(
ChannelListViewThemeData a, StreamChannelListViewThemeData a,
ChannelListViewThemeData b, StreamChannelListViewThemeData b,
double t, double t,
) => ) =>
ChannelListViewThemeData( StreamChannelListViewThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
); );
/// Merges one [ChannelListViewThemeData] with another. /// Merges one [StreamChannelListViewThemeData] with another.
ChannelListViewThemeData merge(ChannelListViewThemeData? other) { StreamChannelListViewThemeData merge(StreamChannelListViewThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
backgroundColor: other.backgroundColor, backgroundColor: other.backgroundColor,
@@ -96,7 +108,7 @@ class ChannelListViewThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is ChannelListViewThemeData && other is StreamChannelListViewThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor; 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/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/theme/avatar_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. /// Overrides the default style of [ChannelPreview] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [ChannelPreviewThemeData], which is used to configure this theme. /// * [StreamChannelPreviewThemeData], which is used to configure this theme.
class ChannelPreviewTheme extends InheritedTheme { /// {@endtemplate}
/// Creates a [ChannelPreviewTheme]. class StreamChannelPreviewTheme extends InheritedTheme {
/// Creates a [StreamChannelPreviewTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const ChannelPreviewTheme({ const StreamChannelPreviewTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final ChannelPreviewThemeData data; final StreamChannelPreviewThemeData data;
/// The closest instance of this class that encloses the given context. /// 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. /// [StreamChatThemeData.channelPreviewTheme] is used.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
@@ -31,34 +37,40 @@ class ChannelPreviewTheme extends InheritedTheme {
/// ```dart /// ```dart
/// final theme = ChannelPreviewTheme.of(context); /// final theme = ChannelPreviewTheme.of(context);
/// ``` /// ```
static ChannelPreviewThemeData of(BuildContext context) { static StreamChannelPreviewThemeData of(BuildContext context) {
final channelPreviewTheme = final channelPreviewTheme =
context.dependOnInheritedWidgetOfExactType<ChannelPreviewTheme>(); context.dependOnInheritedWidgetOfExactType<StreamChannelPreviewTheme>();
return channelPreviewTheme?.data ?? return channelPreviewTheme?.data ??
StreamChatTheme.of(context).channelPreviewTheme; StreamChatTheme.of(context).channelPreviewTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
ChannelPreviewTheme(data: data, child: child); StreamChannelPreviewTheme(data: data, child: child);
@override @override
bool updateShouldNotify(ChannelPreviewTheme oldWidget) => bool updateShouldNotify(StreamChannelPreviewTheme oldWidget) =>
data != oldWidget.data; 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 /// 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]. /// [StreamChatThemeData.channelPreviewTheme].
/// ///
/// See also: /// 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 /// * [StreamChatThemeData.channelPreviewTheme], which can be used to override
/// the default style for [ChannelHeader]s below the overall [StreamChatTheme]. /// the default style for [ChannelHeader]s below the overall [StreamChatTheme].
class ChannelPreviewThemeData with Diagnosticable { /// {@endtemplate}
/// Creates a [ChannelPreviewThemeData]. class StreamChannelPreviewThemeData with Diagnosticable {
const ChannelPreviewThemeData({ /// Creates a [StreamChannelPreviewThemeData].
const StreamChannelPreviewThemeData({
this.titleStyle, this.titleStyle,
this.subtitleStyle, this.subtitleStyle,
this.lastMessageAtStyle, this.lastMessageAtStyle,
@@ -77,7 +89,7 @@ class ChannelPreviewThemeData with Diagnosticable {
final TextStyle? lastMessageAtStyle; final TextStyle? lastMessageAtStyle;
/// Avatar theme /// Avatar theme
final AvatarThemeData? avatarTheme; final StreamAvatarThemeData? avatarTheme;
/// Unread counter color /// Unread counter color
final Color? unreadCounterColor; final Color? unreadCounterColor;
@@ -86,15 +98,15 @@ class ChannelPreviewThemeData with Diagnosticable {
final double? indicatorIconSize; final double? indicatorIconSize;
/// Copy with theme /// Copy with theme
ChannelPreviewThemeData copyWith({ StreamChannelPreviewThemeData copyWith({
TextStyle? titleStyle, TextStyle? titleStyle,
TextStyle? subtitleStyle, TextStyle? subtitleStyle,
TextStyle? lastMessageAtStyle, TextStyle? lastMessageAtStyle,
AvatarThemeData? avatarTheme, StreamAvatarThemeData? avatarTheme,
Color? unreadCounterColor, Color? unreadCounterColor,
double? indicatorIconSize, double? indicatorIconSize,
}) => }) =>
ChannelPreviewThemeData( StreamChannelPreviewThemeData(
titleStyle: titleStyle ?? this.titleStyle, titleStyle: titleStyle ?? this.titleStyle,
subtitleStyle: subtitleStyle ?? this.subtitleStyle, subtitleStyle: subtitleStyle ?? this.subtitleStyle,
lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle, lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle,
@@ -103,15 +115,15 @@ class ChannelPreviewThemeData with Diagnosticable {
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize, indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
); );
/// Linearly interpolate one [ChannelPreviewThemeData] to another. /// Linearly interpolate one [StreamChannelPreviewThemeData] to another.
ChannelPreviewThemeData lerp( StreamChannelPreviewThemeData lerp(
ChannelPreviewThemeData a, StreamChannelPreviewThemeData a,
ChannelPreviewThemeData b, StreamChannelPreviewThemeData b,
double t, double t,
) => ) =>
ChannelPreviewThemeData( StreamChannelPreviewThemeData(
avatarTheme: avatarTheme: const StreamAvatarThemeData()
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), .lerp(a.avatarTheme!, b.avatarTheme!, t),
indicatorIconSize: a.indicatorIconSize, indicatorIconSize: a.indicatorIconSize,
lastMessageAtStyle: lastMessageAtStyle:
TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t), TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t),
@@ -122,7 +134,7 @@ class ChannelPreviewThemeData with Diagnosticable {
); );
/// Merge with theme /// Merge with theme
ChannelPreviewThemeData merge(ChannelPreviewThemeData? other) { StreamChannelPreviewThemeData merge(StreamChannelPreviewThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle, titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle,
@@ -138,7 +150,7 @@ class ChannelPreviewThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is ChannelPreviewThemeData && other is StreamChannelPreviewThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
titleStyle == other.titleStyle && titleStyle == other.titleStyle &&
subtitleStyle == other.subtitleStyle && subtitleStyle == other.subtitleStyle &&
@@ -1,9 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// {@macro color_theme}
@Deprecated("Use 'StreamColorTheme' instead")
typedef ColorTheme = StreamColorTheme;
/// {@template color_theme}
/// Theme that holds colors /// Theme that holds colors
class ColorTheme { /// {@endtemplate}
class StreamColorTheme {
/// Initialise with light theme /// Initialise with light theme
ColorTheme.light({ StreamColorTheme.light({
this.textHighEmphasis = const Color(0xff000000), this.textHighEmphasis = const Color(0xff000000),
this.textLowEmphasis = const Color(0xff7a7a7a), this.textLowEmphasis = const Color(0xff7a7a7a),
this.disabled = const Color(0xffdbdbdb), this.disabled = const Color(0xffdbdbdb),
@@ -55,7 +61,7 @@ class ColorTheme {
}) : brightness = Brightness.light; }) : brightness = Brightness.light;
/// Initialise with dark theme /// Initialise with dark theme
ColorTheme.dark({ StreamColorTheme.dark({
this.textHighEmphasis = const Color(0xffffffff), this.textHighEmphasis = const Color(0xffffffff),
this.textLowEmphasis = const Color(0xff7a7a7a), this.textLowEmphasis = const Color(0xff7a7a7a),
this.disabled = const Color(0xff2d2f2f), this.disabled = const Color(0xff2d2f2f),
@@ -169,7 +175,7 @@ class ColorTheme {
final Brightness brightness; final Brightness brightness;
/// Copy with theme /// Copy with theme
ColorTheme copyWith({ StreamColorTheme copyWith({
Brightness brightness = Brightness.light, Brightness brightness = Brightness.light,
Color? textHighEmphasis, Color? textHighEmphasis,
Color? textLowEmphasis, Color? textLowEmphasis,
@@ -192,7 +198,7 @@ class ColorTheme {
Gradient? bgGradient, Gradient? bgGradient,
}) => }) =>
brightness == Brightness.light brightness == Brightness.light
? ColorTheme.light( ? StreamColorTheme.light(
textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis,
textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis,
disabled: disabled ?? this.disabled, disabled: disabled ?? this.disabled,
@@ -213,7 +219,7 @@ class ColorTheme {
overlayDark: overlayDark ?? this.overlayDark, overlayDark: overlayDark ?? this.overlayDark,
bgGradient: bgGradient ?? this.bgGradient, bgGradient: bgGradient ?? this.bgGradient,
) )
: ColorTheme.dark( : StreamColorTheme.dark(
textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis, textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis,
textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis, textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis,
disabled: disabled ?? this.disabled, disabled: disabled ?? this.disabled,
@@ -236,7 +242,7 @@ class ColorTheme {
); );
/// Merge color theme /// Merge color theme
ColorTheme merge(ColorTheme? other) { StreamColorTheme merge(StreamColorTheme? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
textHighEmphasis: other.textHighEmphasis, textHighEmphasis: other.textHighEmphasis,
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro gallery_footer_theme}
@Deprecated("Use 'StreamGalleryFooterTheme' instead")
typedef GalleryFooterTheme = StreamGalleryFooterTheme;
/// {@template gallery_footer_theme}
/// Overrides the default style of [GalleryFooter] descendants. /// Overrides the default style of [GalleryFooter] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [GalleryFooterThemeData], which is used to configure this theme. /// * [StreamGalleryFooterThemeData], which is used to configure this theme.
class GalleryFooterTheme extends InheritedTheme { /// {@endtemplate}
/// Creates an [GalleryFooterTheme]. class StreamGalleryFooterTheme extends InheritedTheme {
/// Creates an [StreamGalleryFooterTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const GalleryFooterTheme({ const StreamGalleryFooterTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final GalleryFooterThemeData data; final StreamGalleryFooterThemeData data;
/// The closest instance of this class that encloses the given context. /// The closest instance of this class that encloses the given context.
/// ///
/// If there is no enclosing [GalleryFooterTheme] widget, then /// If there is no enclosing [StreamGalleryFooterTheme] widget, then
/// [StreamChatThemeData.galleryFooterTheme] is used. /// [StreamChatThemeData.galleryFooterTheme] is used.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
@@ -30,34 +36,40 @@ class GalleryFooterTheme extends InheritedTheme {
/// ```dart /// ```dart
/// ImageFooterTheme theme = ImageFooterTheme.of(context); /// ImageFooterTheme theme = ImageFooterTheme.of(context);
/// ``` /// ```
static GalleryFooterThemeData of(BuildContext context) { static StreamGalleryFooterThemeData of(BuildContext context) {
final imageFooterTheme = final imageFooterTheme =
context.dependOnInheritedWidgetOfExactType<GalleryFooterTheme>(); context.dependOnInheritedWidgetOfExactType<StreamGalleryFooterTheme>();
return imageFooterTheme?.data ?? return imageFooterTheme?.data ??
StreamChatTheme.of(context).galleryFooterTheme; StreamChatTheme.of(context).galleryFooterTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
GalleryFooterTheme(data: data, child: child); StreamGalleryFooterTheme(data: data, child: child);
@override @override
bool updateShouldNotify(GalleryFooterTheme oldWidget) => bool updateShouldNotify(StreamGalleryFooterTheme oldWidget) =>
data != oldWidget.data; data != oldWidget.data;
} }
/// {@macro gallery_footer_theme_data}
@Deprecated("Use 'StreamGalleryFooterThemeData' instead")
typedef GalleryFooterThemeData = StreamGalleryFooterThemeData;
/// {@template gallery_footer_theme_data}
/// A style that overrides the default appearance of [GalleryFooter]s when used /// A style that overrides the default appearance of [GalleryFooter]s when used
/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s /// with [StreamGalleryFooterTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.galleryFooterTheme]. /// [StreamChatThemeData.galleryFooterTheme].
/// ///
/// See also: /// See also:
/// ///
/// * [GalleryFooterTheme], the theme which is configured with this class. /// * [StreamGalleryFooterTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.galleryFooterTheme], which can be used to override /// * [StreamChatThemeData.galleryFooterTheme], which can be used to override
/// the default style for [GalleryFooter]s below the overall [StreamChatTheme]. /// the default style for [GalleryFooter]s below the overall [StreamChatTheme].
class GalleryFooterThemeData with Diagnosticable { /// {@endtemplate}
/// Creates an [GalleryFooterThemeData]. class StreamGalleryFooterThemeData with Diagnosticable {
const GalleryFooterThemeData({ /// Creates an [StreamGalleryFooterThemeData].
const StreamGalleryFooterThemeData({
this.backgroundColor, this.backgroundColor,
this.shareIconColor, this.shareIconColor,
this.titleTextStyle, this.titleTextStyle,
@@ -108,8 +120,8 @@ class GalleryFooterThemeData with Diagnosticable {
/// Defaults to [ColorTheme.textHighEmphasis]. /// Defaults to [ColorTheme.textHighEmphasis].
final Color? bottomSheetCloseIconColor; final Color? bottomSheetCloseIconColor;
/// Copies this [GalleryFooterThemeData] to another. /// Copies this [StreamGalleryFooterThemeData] to another.
GalleryFooterThemeData copyWith({ StreamGalleryFooterThemeData copyWith({
Color? backgroundColor, Color? backgroundColor,
Color? shareIconColor, Color? shareIconColor,
TextStyle? titleTextStyle, TextStyle? titleTextStyle,
@@ -119,7 +131,7 @@ class GalleryFooterThemeData with Diagnosticable {
TextStyle? bottomSheetPhotosTextStyle, TextStyle? bottomSheetPhotosTextStyle,
Color? bottomSheetCloseIconColor, Color? bottomSheetCloseIconColor,
}) => }) =>
GalleryFooterThemeData( StreamGalleryFooterThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
shareIconColor: shareIconColor ?? this.shareIconColor, shareIconColor: shareIconColor ?? this.shareIconColor,
titleTextStyle: titleTextStyle ?? this.titleTextStyle, titleTextStyle: titleTextStyle ?? this.titleTextStyle,
@@ -137,12 +149,12 @@ class GalleryFooterThemeData with Diagnosticable {
/// Linearly interpolate between two [GalleryFooter] themes. /// Linearly interpolate between two [GalleryFooter] themes.
/// ///
/// All the properties must be non-null. /// All the properties must be non-null.
GalleryFooterThemeData lerp( StreamGalleryFooterThemeData lerp(
GalleryFooterThemeData a, StreamGalleryFooterThemeData a,
GalleryFooterThemeData b, StreamGalleryFooterThemeData b,
double t, double t,
) => ) =>
GalleryFooterThemeData( StreamGalleryFooterThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t), shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t),
titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t), titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t),
@@ -167,8 +179,8 @@ class GalleryFooterThemeData with Diagnosticable {
), ),
); );
/// Merges one [GalleryFooterThemeData] with another. /// Merges one [StreamGalleryFooterThemeData] with another.
GalleryFooterThemeData merge(GalleryFooterThemeData? other) { StreamGalleryFooterThemeData merge(StreamGalleryFooterThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
backgroundColor: other.backgroundColor, backgroundColor: other.backgroundColor,
@@ -185,7 +197,7 @@ class GalleryFooterThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is GalleryFooterThemeData && other is StreamGalleryFooterThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor && backgroundColor == other.backgroundColor &&
shareIconColor == other.shareIconColor && shareIconColor == other.shareIconColor &&
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro gallery_header_them}
@Deprecated("Use 'StreamGalleryHeaderTheme' instead")
typedef GalleryHeaderTheme = StreamGalleryHeaderTheme;
/// {@template gallery_header_theme}
/// Overrides the default style of [GalleryHeader] descendants. /// Overrides the default style of [GalleryHeader] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [GalleryHeaderThemeData], which is used to configure this theme. /// * [StreamGalleryHeaderThemeData], which is used to configure this theme.
class GalleryHeaderTheme extends InheritedTheme { /// {@endtemplate}
/// Creates a [GalleryHeaderTheme]. class StreamGalleryHeaderTheme extends InheritedTheme {
/// Creates a [StreamGalleryHeaderTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const GalleryHeaderTheme({ const StreamGalleryHeaderTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final GalleryHeaderThemeData data; final StreamGalleryHeaderThemeData data;
/// The closest instance of this class that encloses the given context. /// The closest instance of this class that encloses the given context.
/// ///
/// If there is no enclosing [GalleryHeaderTheme] widget, then /// If there is no enclosing [StreamGalleryHeaderTheme] widget, then
/// [StreamChatThemeData.galleryHeaderTheme] is used. /// [StreamChatThemeData.galleryHeaderTheme] is used.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
@@ -30,34 +36,40 @@ class GalleryHeaderTheme extends InheritedTheme {
/// ```dart /// ```dart
/// ImageHeaderTheme theme = ImageHeaderTheme.of(context); /// ImageHeaderTheme theme = ImageHeaderTheme.of(context);
/// ``` /// ```
static GalleryHeaderThemeData of(BuildContext context) { static StreamGalleryHeaderThemeData of(BuildContext context) {
final galleryHeaderTheme = final galleryHeaderTheme =
context.dependOnInheritedWidgetOfExactType<GalleryHeaderTheme>(); context.dependOnInheritedWidgetOfExactType<StreamGalleryHeaderTheme>();
return galleryHeaderTheme?.data ?? return galleryHeaderTheme?.data ??
StreamChatTheme.of(context).galleryHeaderTheme; StreamChatTheme.of(context).galleryHeaderTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
GalleryHeaderTheme(data: data, child: child); StreamGalleryHeaderTheme(data: data, child: child);
@override @override
bool updateShouldNotify(GalleryHeaderTheme oldWidget) => bool updateShouldNotify(StreamGalleryHeaderTheme oldWidget) =>
data != oldWidget.data; data != oldWidget.data;
} }
/// {@macro gallery_header_theme_data}
@Deprecated("Use 'StreamGalleryHeaderThemeData' instead")
typedef GalleryHeaderThemeData = StreamGalleryHeaderThemeData;
/// {@template gallery_header_theme_data}
/// A style that overrides the default appearance of [GalleryHeader]s when used /// A style that overrides the default appearance of [GalleryHeader]s when used
/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s /// with [StreamGalleryHeaderTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.galleryHeaderTheme]. /// [StreamChatThemeData.galleryHeaderTheme].
/// ///
/// See also: /// See also:
/// ///
/// * [GalleryHeaderTheme], the theme which is configured with this class. /// * [StreamGalleryHeaderTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.galleryHeaderTheme], which can be used to override /// * [StreamChatThemeData.galleryHeaderTheme], which can be used to override
/// the default style for [GalleryHeader]s below the overall [StreamChatTheme]. /// the default style for [GalleryHeader]s below the overall [StreamChatTheme].
class GalleryHeaderThemeData with Diagnosticable { /// {@endtemplate}
/// Creates an [GalleryHeaderThemeData]. class StreamGalleryHeaderThemeData with Diagnosticable {
const GalleryHeaderThemeData({ /// Creates an [StreamGalleryHeaderThemeData].
const StreamGalleryHeaderThemeData({
this.closeButtonColor, this.closeButtonColor,
this.backgroundColor, this.backgroundColor,
this.iconMenuPointColor, this.iconMenuPointColor,
@@ -92,8 +104,8 @@ class GalleryHeaderThemeData with Diagnosticable {
/// ///
final Color? bottomSheetBarrierColor; final Color? bottomSheetBarrierColor;
/// Copies this [GalleryHeaderThemeData] to another. /// Copies this [StreamGalleryHeaderThemeData] to another.
GalleryHeaderThemeData copyWith({ StreamGalleryHeaderThemeData copyWith({
Color? closeButtonColor, Color? closeButtonColor,
Color? backgroundColor, Color? backgroundColor,
Color? iconMenuPointColor, Color? iconMenuPointColor,
@@ -101,7 +113,7 @@ class GalleryHeaderThemeData with Diagnosticable {
TextStyle? subtitleTextStyle, TextStyle? subtitleTextStyle,
Color? bottomSheetBarrierColor, Color? bottomSheetBarrierColor,
}) => }) =>
GalleryHeaderThemeData( StreamGalleryHeaderThemeData(
closeButtonColor: closeButtonColor ?? this.closeButtonColor, closeButtonColor: closeButtonColor ?? this.closeButtonColor,
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor, iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor,
@@ -114,12 +126,12 @@ class GalleryHeaderThemeData with Diagnosticable {
/// Linearly interpolate between two [GalleryHeader] themes. /// Linearly interpolate between two [GalleryHeader] themes.
/// ///
/// All the properties must be non-null. /// All the properties must be non-null.
GalleryHeaderThemeData lerp( StreamGalleryHeaderThemeData lerp(
GalleryHeaderThemeData a, StreamGalleryHeaderThemeData a,
GalleryHeaderThemeData b, StreamGalleryHeaderThemeData b,
double t, double t,
) => ) =>
GalleryHeaderThemeData( StreamGalleryHeaderThemeData(
closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t), closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t),
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
iconMenuPointColor: iconMenuPointColor:
@@ -131,8 +143,8 @@ class GalleryHeaderThemeData with Diagnosticable {
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t), Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
); );
/// Merges one [GalleryHeaderThemeData] with the another /// Merges one [StreamGalleryHeaderThemeData] with the another
GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) { StreamGalleryHeaderThemeData merge(StreamGalleryHeaderThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
closeButtonColor: other.closeButtonColor, closeButtonColor: other.closeButtonColor,
@@ -147,7 +159,7 @@ class GalleryHeaderThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is GalleryHeaderThemeData && other is StreamGalleryHeaderThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
closeButtonColor == other.closeButtonColor && closeButtonColor == other.closeButtonColor &&
backgroundColor == other.backgroundColor && backgroundColor == other.backgroundColor &&
@@ -1,29 +1,37 @@
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro message_input_theme}
@Deprecated("Use 'StreamMessageInputTheme' instead")
typedef MessageInputTheme = StreamMessageInputTheme;
/// {@template message_input_theme}
/// Overrides the default style of [MessageInput] descendants. /// Overrides the default style of [MessageInput] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [MessageInputThemeData], which is used to configure this theme. /// * [StreamMessageInputThemeData], which is used to configure this theme.
class MessageInputTheme extends InheritedTheme { /// {@endtemplate}
/// Creates a [MessageInputTheme]. class StreamMessageInputTheme extends InheritedTheme {
/// Creates a [StreamMessageInputTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const MessageInputTheme({ const StreamMessageInputTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final MessageInputThemeData data; final StreamMessageInputThemeData data;
/// The closest instance of this class that encloses the given context. /// The closest instance of this class that encloses the given context.
/// ///
/// If there is no enclosing [MessageInputTheme] widget, then /// If there is no enclosing [StreamMessageInputTheme] widget, then
/// [StreamChatThemeData.messageInputTheme] is used. /// [StreamChatThemeData.messageInputTheme] is used.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
@@ -31,28 +39,34 @@ class MessageInputTheme extends InheritedTheme {
/// ```dart /// ```dart
/// final theme = MessageInputTheme.of(context); /// final theme = MessageInputTheme.of(context);
/// ``` /// ```
static MessageInputThemeData of(BuildContext context) { static StreamMessageInputThemeData of(BuildContext context) {
final messageInputTheme = final messageInputTheme =
context.dependOnInheritedWidgetOfExactType<MessageInputTheme>(); context.dependOnInheritedWidgetOfExactType<StreamMessageInputTheme>();
return messageInputTheme?.data ?? return messageInputTheme?.data ??
StreamChatTheme.of(context).messageInputTheme; StreamChatTheme.of(context).messageInputTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
MessageInputTheme(data: data, child: child); StreamMessageInputTheme(data: data, child: child);
@override @override
bool updateShouldNotify(MessageInputTheme oldWidget) => bool updateShouldNotify(StreamMessageInputTheme oldWidget) =>
data != oldWidget.data; data != oldWidget.data;
} }
/// {@macro message_input_theme_data}
@Deprecated("Use 'StreamMessageInputThemeData' instead")
typedef MessageInputThemeData = StreamMessageInputThemeData;
/// {@template message_input_theme_data}
/// A style that overrides the default appearance of [MessageInput] widgets /// A style that overrides the default appearance of [MessageInput] widgets
/// when used with [MessageInputTheme] or with the overall [StreamChatTheme]'s /// when used with [StreamMessageInputTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.messageInputTheme]. /// [StreamChatThemeData.messageInputTheme].
class MessageInputThemeData with Diagnosticable { /// {@endtemplate}
/// Creates a [MessageInputThemeData]. class StreamMessageInputThemeData with Diagnosticable {
const MessageInputThemeData({ /// Creates a [StreamMessageInputThemeData].
const StreamMessageInputThemeData({
this.sendAnimationDuration, this.sendAnimationDuration,
this.actionButtonColor, this.actionButtonColor,
this.sendButtonColor, this.sendButtonColor,
@@ -65,6 +79,10 @@ class MessageInputThemeData with Diagnosticable {
this.idleBorderGradient, this.idleBorderGradient,
this.borderRadius, this.borderRadius,
this.expandButtonColor, this.expandButtonColor,
this.linkHighlightColor,
this.enableSafeArea,
this.elevation,
this.shadow,
}); });
/// Duration of the [MessageInput] send button animation /// Duration of the [MessageInput] send button animation
@@ -73,6 +91,9 @@ class MessageInputThemeData with Diagnosticable {
/// Background color of [MessageInput] send button /// Background color of [MessageInput] send button
final Color? sendButtonColor; final Color? sendButtonColor;
/// Color of a link
final Color? linkHighlightColor;
/// Background color of [MessageInput] action buttons /// Background color of [MessageInput] action buttons
final Color? actionButtonColor; final Color? actionButtonColor;
@@ -103,13 +124,23 @@ class MessageInputThemeData with Diagnosticable {
/// Border radius of [MessageInput] /// Border radius of [MessageInput]
final BorderRadius? borderRadius; final BorderRadius? borderRadius;
/// Returns a new [MessageInputThemeData] replacing some of its properties /// Wrap [MessageInput] with a [SafeArea widget]
MessageInputThemeData copyWith({ final bool? enableSafeArea;
/// Elevation of the [MessageInput]
final double? elevation;
/// Shadow for the [MessageInput] widget
final BoxShadow? shadow;
/// Returns a new [StreamMessageInputThemeData] replacing some of its properties
StreamMessageInputThemeData copyWith({
Duration? sendAnimationDuration, Duration? sendAnimationDuration,
Color? inputBackgroundColor, Color? inputBackgroundColor,
Color? actionButtonColor, Color? actionButtonColor,
Color? sendButtonColor, Color? sendButtonColor,
Color? actionButtonIdleColor, Color? actionButtonIdleColor,
Color? linkHighlightColor,
Color? sendButtonIdleColor, Color? sendButtonIdleColor,
Color? expandButtonColor, Color? expandButtonColor,
TextStyle? inputTextStyle, TextStyle? inputTextStyle,
@@ -117,8 +148,11 @@ class MessageInputThemeData with Diagnosticable {
Gradient? activeBorderGradient, Gradient? activeBorderGradient,
Gradient? idleBorderGradient, Gradient? idleBorderGradient,
BorderRadius? borderRadius, BorderRadius? borderRadius,
bool? enableSafeArea,
double? elevation,
BoxShadow? shadow,
}) => }) =>
MessageInputThemeData( StreamMessageInputThemeData(
sendAnimationDuration: sendAnimationDuration:
sendAnimationDuration ?? this.sendAnimationDuration, sendAnimationDuration ?? this.sendAnimationDuration,
inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor, inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor,
@@ -133,15 +167,19 @@ class MessageInputThemeData with Diagnosticable {
activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient, activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient,
idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient, idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient,
borderRadius: borderRadius ?? this.borderRadius, borderRadius: borderRadius ?? this.borderRadius,
linkHighlightColor: linkHighlightColor ?? this.linkHighlightColor,
enableSafeArea: enableSafeArea ?? this.enableSafeArea,
elevation: elevation ?? this.elevation,
shadow: shadow ?? this.shadow,
); );
/// Linearly interpolate from one [MessageInputThemeData] to another. /// Linearly interpolate from one [StreamMessageInputThemeData] to another.
MessageInputThemeData lerp( StreamMessageInputThemeData lerp(
MessageInputThemeData a, StreamMessageInputThemeData a,
MessageInputThemeData b, StreamMessageInputThemeData b,
double t, double t,
) => ) =>
MessageInputThemeData( StreamMessageInputThemeData(
actionButtonColor: actionButtonColor:
Color.lerp(a.actionButtonColor, b.actionButtonColor, t), Color.lerp(a.actionButtonColor, b.actionButtonColor, t),
actionButtonIdleColor: actionButtonIdleColor:
@@ -161,10 +199,15 @@ class MessageInputThemeData with Diagnosticable {
Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t), Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t),
sendAnimationDuration: a.sendAnimationDuration, sendAnimationDuration: a.sendAnimationDuration,
inputDecoration: a.inputDecoration, inputDecoration: a.inputDecoration,
linkHighlightColor:
Color.lerp(a.linkHighlightColor, b.linkHighlightColor, t),
enableSafeArea: a.enableSafeArea,
elevation: lerpDouble(a.elevation, b.elevation, t),
shadow: BoxShadow.lerp(a.shadow, b.shadow, t),
); );
/// Merges [this] [MessageInputThemeData] with the [other] /// Merges [this] [StreamMessageInputThemeData] with the [other]
MessageInputThemeData merge(MessageInputThemeData? other) { StreamMessageInputThemeData merge(StreamMessageInputThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
sendAnimationDuration: other.sendAnimationDuration, sendAnimationDuration: other.sendAnimationDuration,
@@ -181,13 +224,17 @@ class MessageInputThemeData with Diagnosticable {
idleBorderGradient: other.idleBorderGradient, idleBorderGradient: other.idleBorderGradient,
borderRadius: other.borderRadius, borderRadius: other.borderRadius,
expandButtonColor: other.expandButtonColor, expandButtonColor: other.expandButtonColor,
linkHighlightColor: other.linkHighlightColor,
enableSafeArea: other.enableSafeArea,
elevation: other.elevation,
shadow: other.shadow,
); );
} }
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is MessageInputThemeData && other is StreamMessageInputThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
sendAnimationDuration == other.sendAnimationDuration && sendAnimationDuration == other.sendAnimationDuration &&
sendButtonColor == other.sendButtonColor && sendButtonColor == other.sendButtonColor &&
@@ -200,7 +247,11 @@ class MessageInputThemeData with Diagnosticable {
inputDecoration == other.inputDecoration && inputDecoration == other.inputDecoration &&
idleBorderGradient == other.idleBorderGradient && idleBorderGradient == other.idleBorderGradient &&
activeBorderGradient == other.activeBorderGradient && activeBorderGradient == other.activeBorderGradient &&
borderRadius == other.borderRadius; borderRadius == other.borderRadius &&
linkHighlightColor == other.linkHighlightColor &&
enableSafeArea == other.enableSafeArea &&
elevation == other.elevation &&
shadow == other.shadow;
@override @override
int get hashCode => int get hashCode =>
@@ -215,7 +266,11 @@ class MessageInputThemeData with Diagnosticable {
inputDecoration.hashCode ^ inputDecoration.hashCode ^
idleBorderGradient.hashCode ^ idleBorderGradient.hashCode ^
activeBorderGradient.hashCode ^ activeBorderGradient.hashCode ^
borderRadius.hashCode; borderRadius.hashCode ^
linkHighlightColor.hashCode ^
elevation.hashCode ^
shadow.hashCode ^
enableSafeArea.hashCode;
@override @override
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@@ -232,6 +287,10 @@ class MessageInputThemeData with Diagnosticable {
..add(DiagnosticsProperty('activeBorderGradient', activeBorderGradient)) ..add(DiagnosticsProperty('activeBorderGradient', activeBorderGradient))
..add(DiagnosticsProperty('idleBorderGradient', idleBorderGradient)) ..add(DiagnosticsProperty('idleBorderGradient', idleBorderGradient))
..add(DiagnosticsProperty('borderRadius', borderRadius)) ..add(DiagnosticsProperty('borderRadius', borderRadius))
..add(ColorProperty('expandButtonColor', expandButtonColor)); ..add(ColorProperty('expandButtonColor', expandButtonColor))
..add(ColorProperty('linkHighlightColor', linkHighlightColor))
..add(DiagnosticsProperty('elevation', elevation))
..add(DiagnosticsProperty('shadow', shadow))
..add(DiagnosticsProperty('enableSafeArea', enableSafeArea));
} }
} }
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro message_list_view_theme}
@Deprecated("Use 'StreamMessageListViewTheme' instead")
typedef MessageListViewTheme = StreamMessageListViewTheme;
/// {@template message_list_view_theme}
/// Overrides the default style of [MessageListView] descendants. /// Overrides the default style of [MessageListView] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [MessageListViewThemeData], which is used to configure this theme. /// * [StreamMessageListViewThemeData], which is used to configure this theme.
class MessageListViewTheme extends InheritedTheme { /// {@endtemplate}
/// Creates a [MessageListViewTheme]. class StreamMessageListViewTheme extends InheritedTheme {
/// Creates a [StreamMessageListViewTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const MessageListViewTheme({ const StreamMessageListViewTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final MessageListViewThemeData data; final StreamMessageListViewThemeData data;
/// The closest instance of this class that encloses the given context. /// The closest instance of this class that encloses the given context.
/// ///
/// If there is no enclosing [MessageListViewTheme] widget, then /// If there is no enclosing [StreamMessageListViewTheme] widget, then
/// [StreamChatThemeData.messageListViewTheme] is used. /// [StreamChatThemeData.messageListViewTheme] is used.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
@@ -30,35 +36,41 @@ class MessageListViewTheme extends InheritedTheme {
/// ```dart /// ```dart
/// MessageListViewTheme theme = MessageListViewTheme.of(context); /// MessageListViewTheme theme = MessageListViewTheme.of(context);
/// ``` /// ```
static MessageListViewThemeData of(BuildContext context) { static StreamMessageListViewThemeData of(BuildContext context) {
final messageListViewTheme = final messageListViewTheme = context
context.dependOnInheritedWidgetOfExactType<MessageListViewTheme>(); .dependOnInheritedWidgetOfExactType<StreamMessageListViewTheme>();
return messageListViewTheme?.data ?? return messageListViewTheme?.data ??
StreamChatTheme.of(context).messageListViewTheme; StreamChatTheme.of(context).messageListViewTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
MessageListViewTheme(data: data, child: child); StreamMessageListViewTheme(data: data, child: child);
@override @override
bool updateShouldNotify(MessageListViewTheme oldWidget) => bool updateShouldNotify(StreamMessageListViewTheme oldWidget) =>
data != oldWidget.data; data != oldWidget.data;
} }
/// {@macro message_list_view_theme_data}
@Deprecated("Use 'StreamMessageListViewThemeData' instead")
typedef MessageListViewThemeData = StreamMessageListViewThemeData;
/// {@template message_list_view_theme_data}
/// A style that overrides the default appearance of [MessageListView]s when /// A style that overrides the default appearance of [MessageListView]s when
/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s /// used with [StreamMessageListViewTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.messageListViewTheme]. /// [StreamChatThemeData.messageListViewTheme].
/// ///
/// See also: /// See also:
/// ///
/// * [MessageListViewTheme], the theme which is configured with this class. /// * [StreamMessageListViewTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.messageListViewTheme], which can be used to override /// * [StreamChatThemeData.messageListViewTheme], which can be used to override
/// the default style for [MessageListView]s below the overall /// the default style for [MessageListView]s below the overall
/// [StreamChatTheme]. /// [StreamChatTheme].
class MessageListViewThemeData with Diagnosticable { /// {@endtemplate}
/// Creates a [MessageListViewThemeData]. class StreamMessageListViewThemeData with Diagnosticable {
const MessageListViewThemeData({ /// Creates a [StreamMessageListViewThemeData].
const StreamMessageListViewThemeData({
this.backgroundColor, this.backgroundColor,
this.backgroundImage, this.backgroundImage,
}); });
@@ -69,12 +81,12 @@ class MessageListViewThemeData with Diagnosticable {
/// The image of the [MessageListView] background. /// The image of the [MessageListView] background.
final DecorationImage? backgroundImage; final DecorationImage? backgroundImage;
/// Copies this [MessageListViewThemeData] to another. /// Copies this [StreamMessageListViewThemeData] to another.
MessageListViewThemeData copyWith({ StreamMessageListViewThemeData copyWith({
Color? backgroundColor, Color? backgroundColor,
DecorationImage? backgroundImage, DecorationImage? backgroundImage,
}) => }) =>
MessageListViewThemeData( StreamMessageListViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
backgroundImage: backgroundImage ?? this.backgroundImage, backgroundImage: backgroundImage ?? this.backgroundImage,
); );
@@ -82,18 +94,18 @@ class MessageListViewThemeData with Diagnosticable {
/// Linearly interpolate between two [MessageListView] themes. /// Linearly interpolate between two [MessageListView] themes.
/// ///
/// All the properties must be non-null. /// All the properties must be non-null.
MessageListViewThemeData lerp( StreamMessageListViewThemeData lerp(
MessageListViewThemeData a, StreamMessageListViewThemeData a,
MessageListViewThemeData b, StreamMessageListViewThemeData b,
double t, double t,
) => ) =>
MessageListViewThemeData( StreamMessageListViewThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage, backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage,
); );
/// Merges one [MessageListViewThemeData] with another. /// Merges one [StreamMessageListViewThemeData] with another.
MessageListViewThemeData merge(MessageListViewThemeData? other) { StreamMessageListViewThemeData merge(StreamMessageListViewThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
backgroundColor: other.backgroundColor, backgroundColor: other.backgroundColor,
@@ -104,7 +116,7 @@ class MessageListViewThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is MessageListViewThemeData && other is StreamMessageListViewThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor && backgroundColor == other.backgroundColor &&
backgroundImage == other.backgroundImage; backgroundImage == other.backgroundImage;
@@ -2,23 +2,29 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro message_search_list_view_theme}
@Deprecated("Use 'StreamMessageSearchListViewTheme' instead")
typedef MessageSearchListViewTheme = StreamMessageSearchListViewTheme;
/// {@template message_search_list_view_theme}
/// Overrides the default style of [MessageSearchListView] descendants. /// Overrides the default style of [MessageSearchListView] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [UserListViewThemeData], which is used to configure this theme. /// * [UserListViewThemeData], which is used to configure this theme.
class MessageSearchListViewTheme extends InheritedTheme { /// {@endtemplate}
class StreamMessageSearchListViewTheme extends InheritedTheme {
/// Creates a [UserListViewTheme]. /// Creates a [UserListViewTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const MessageSearchListViewTheme({ const StreamMessageSearchListViewTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final MessageSearchListViewThemeData data; final StreamMessageSearchListViewThemeData data;
/// The closest instance of this class that encloses the given context. /// The closest instance of this class that encloses the given context.
/// ///
@@ -30,64 +36,72 @@ class MessageSearchListViewTheme extends InheritedTheme {
/// ```dart /// ```dart
/// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context); /// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context);
/// ``` /// ```
static MessageSearchListViewThemeData of(BuildContext context) { static StreamMessageSearchListViewThemeData of(BuildContext context) {
final messageSearchListViewTheme = context final messageSearchListViewTheme = context
.dependOnInheritedWidgetOfExactType<MessageSearchListViewTheme>(); .dependOnInheritedWidgetOfExactType<StreamMessageSearchListViewTheme>();
return messageSearchListViewTheme?.data ?? return messageSearchListViewTheme?.data ??
StreamChatTheme.of(context).messageSearchListViewTheme; StreamChatTheme.of(context).messageSearchListViewTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
MessageSearchListViewTheme(data: data, child: child); StreamMessageSearchListViewTheme(data: data, child: child);
@override @override
bool updateShouldNotify(MessageSearchListViewTheme oldWidget) => bool updateShouldNotify(StreamMessageSearchListViewTheme oldWidget) =>
data != oldWidget.data; data != oldWidget.data;
} }
/// {@macro message_search_list_view_theme_data}
@Deprecated("Use 'StreamMessageSearchListViewThemeData' instead")
typedef MessageSearchListViewThemeData = StreamMessageSearchListViewThemeData;
/// {@macro message_search_list_view_theme_data}
/// A style that overrides the default appearance of [MessageSearchListView]s /// A style that overrides the default appearance of [MessageSearchListView]s
/// when used with [MessageSearchListView] or with the overall /// when used with [MessageSearchListView] or with the overall
/// [StreamChatTheme]'s [StreamChatThemeData.messageSearchListViewTheme]. /// [StreamChatTheme]'s [StreamChatThemeData.messageSearchListViewTheme].
/// ///
/// See also: /// See also:
/// ///
/// * [MessageSearchListViewTheme], the theme which is configured with this /// * [StreamMessageSearchListViewTheme], the theme which is configured with this
/// class. /// class.
/// * [StreamChatThemeData.messageSearchListViewTheme], which can be used to /// * [StreamChatThemeData.messageSearchListViewTheme], which can be used to
/// override the default style for [UserListView]s below the overall /// override the default style for [UserListView]s below the overall
/// [StreamChatTheme]. /// [StreamChatTheme].
class MessageSearchListViewThemeData with Diagnosticable { /// {@endtemplate}
/// Creates a [MessageSearchListViewThemeData]. class StreamMessageSearchListViewThemeData with Diagnosticable {
const MessageSearchListViewThemeData({ /// Creates a [StreamMessageSearchListViewThemeData].
const StreamMessageSearchListViewThemeData({
this.backgroundColor, this.backgroundColor,
}); });
/// The color of the [MessageSearchListView] background. /// The color of the [MessageSearchListView] background.
final Color? backgroundColor; final Color? backgroundColor;
/// Copies this [MessageSearchListViewThemeData] to another. /// Copies this [StreamMessageSearchListViewThemeData] to another.
MessageSearchListViewThemeData copyWith({ StreamMessageSearchListViewThemeData copyWith({
Color? backgroundColor, Color? backgroundColor,
}) => }) =>
MessageSearchListViewThemeData( StreamMessageSearchListViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
); );
/// Linearly interpolate between two [UserListViewThemeData] themes. /// Linearly interpolate between two [UserListViewThemeData] themes.
/// ///
/// All the properties must be non-null. /// All the properties must be non-null.
MessageSearchListViewThemeData lerp( StreamMessageSearchListViewThemeData lerp(
MessageSearchListViewThemeData a, StreamMessageSearchListViewThemeData a,
MessageSearchListViewThemeData b, StreamMessageSearchListViewThemeData b,
double t, double t,
) => ) =>
MessageSearchListViewThemeData( StreamMessageSearchListViewThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
); );
/// Merges one [MessageSearchListViewThemeData] with another. /// Merges one [StreamMessageSearchListViewThemeData] with another.
MessageSearchListViewThemeData merge(MessageSearchListViewThemeData? other) { StreamMessageSearchListViewThemeData merge(
StreamMessageSearchListViewThemeData? other,
) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
backgroundColor: other.backgroundColor, backgroundColor: other.backgroundColor,
@@ -97,7 +111,7 @@ class MessageSearchListViewThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is MessageSearchListViewThemeData && other is StreamMessageSearchListViewThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor; backgroundColor == other.backgroundColor;
@@ -2,11 +2,17 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart'; import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
/// {@macro message_theme_data}
@Deprecated("Use 'StreamMessageThemeData' instead")
typedef MessageThemeData = StreamMessageThemeData;
/// {@template message_theme_data}
/// Class for getting message theme /// Class for getting message theme
/// {@endtemplate}
// ignore: prefer-match-file-name // ignore: prefer-match-file-name
class MessageThemeData with Diagnosticable { class StreamMessageThemeData with Diagnosticable {
/// Creates a [MessageThemeData]. /// Creates a [StreamMessageThemeData].
const MessageThemeData({ const StreamMessageThemeData({
this.repliesStyle, this.repliesStyle,
this.messageTextStyle, this.messageTextStyle,
this.messageAuthorStyle, this.messageAuthorStyle,
@@ -52,13 +58,13 @@ class MessageThemeData with Diagnosticable {
final Color? reactionsMaskColor; final Color? reactionsMaskColor;
/// Theme of the avatar /// Theme of the avatar
final AvatarThemeData? avatarTheme; final StreamAvatarThemeData? avatarTheme;
/// Background color for messages with url attachments. /// Background color for messages with url attachments.
final Color? linkBackgroundColor; final Color? linkBackgroundColor;
/// Copy with a theme /// Copy with a theme
MessageThemeData copyWith({ StreamMessageThemeData copyWith({
TextStyle? messageTextStyle, TextStyle? messageTextStyle,
TextStyle? messageAuthorStyle, TextStyle? messageAuthorStyle,
TextStyle? messageLinksStyle, TextStyle? messageLinksStyle,
@@ -66,13 +72,13 @@ class MessageThemeData with Diagnosticable {
TextStyle? repliesStyle, TextStyle? repliesStyle,
Color? messageBackgroundColor, Color? messageBackgroundColor,
Color? messageBorderColor, Color? messageBorderColor,
AvatarThemeData? avatarTheme, StreamAvatarThemeData? avatarTheme,
Color? reactionsBackgroundColor, Color? reactionsBackgroundColor,
Color? reactionsBorderColor, Color? reactionsBorderColor,
Color? reactionsMaskColor, Color? reactionsMaskColor,
Color? linkBackgroundColor, Color? linkBackgroundColor,
}) => }) =>
MessageThemeData( StreamMessageThemeData(
messageTextStyle: messageTextStyle ?? this.messageTextStyle, messageTextStyle: messageTextStyle ?? this.messageTextStyle,
messageAuthorStyle: messageAuthorStyle ?? this.messageAuthorStyle, messageAuthorStyle: messageAuthorStyle ?? this.messageAuthorStyle,
messageLinksStyle: messageLinksStyle ?? this.messageLinksStyle, messageLinksStyle: messageLinksStyle ?? this.messageLinksStyle,
@@ -89,11 +95,15 @@ class MessageThemeData with Diagnosticable {
linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor, linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor,
); );
/// Linearly interpolate from one [MessageThemeData] to another. /// Linearly interpolate from one [StreamMessageThemeData] to another.
MessageThemeData lerp(MessageThemeData a, MessageThemeData b, double t) => StreamMessageThemeData lerp(
MessageThemeData( StreamMessageThemeData a,
avatarTheme: StreamMessageThemeData b,
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t), double t,
) =>
StreamMessageThemeData(
avatarTheme: const StreamAvatarThemeData()
.lerp(a.avatarTheme!, b.avatarTheme!, t),
createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t), createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t),
messageAuthorStyle: messageAuthorStyle:
TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t), TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t),
@@ -120,7 +130,7 @@ class MessageThemeData with Diagnosticable {
); );
/// Merge with a theme /// Merge with a theme
MessageThemeData merge(MessageThemeData? other) { StreamMessageThemeData merge(StreamMessageThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
messageTextStyle: messageTextStyle?.merge(other.messageTextStyle) ?? messageTextStyle: messageTextStyle?.merge(other.messageTextStyle) ??
@@ -146,7 +156,7 @@ class MessageThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is MessageThemeData && other is StreamMessageThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
messageTextStyle == other.messageTextStyle && messageTextStyle == other.messageTextStyle &&
messageAuthorStyle == other.messageAuthorStyle && messageAuthorStyle == other.messageAuthorStyle &&
@@ -1,9 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// {@macro text_theme}
@Deprecated("Use 'StreamTextTheme' instead")
typedef TextTheme = StreamTextTheme;
/// {@template text_theme}
/// Class for holding text theme /// Class for holding text theme
class TextTheme { /// {@endtemplate}
class StreamTextTheme {
/// Initialise light text theme /// Initialise light text theme
TextTheme.light({ StreamTextTheme.light({
this.title = const TextStyle( this.title = const TextStyle(
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -46,7 +52,7 @@ class TextTheme {
}); });
/// Initialise with dark theme /// Initialise with dark theme
TextTheme.dark({ StreamTextTheme.dark({
this.title = const TextStyle( this.title = const TextStyle(
fontSize: 22, fontSize: 22,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -113,7 +119,7 @@ class TextTheme {
final TextStyle captionBold; final TextStyle captionBold;
/// Copy with theme /// Copy with theme
TextTheme copyWith({ StreamTextTheme copyWith({
Brightness brightness = Brightness.light, Brightness brightness = Brightness.light,
TextStyle? body, TextStyle? body,
TextStyle? title, TextStyle? title,
@@ -125,7 +131,7 @@ class TextTheme {
TextStyle? captionBold, TextStyle? captionBold,
}) => }) =>
brightness == Brightness.light brightness == Brightness.light
? TextTheme.light( ? StreamTextTheme.light(
body: body ?? this.body, body: body ?? this.body,
title: title ?? this.title, title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold, headlineBold: headlineBold ?? this.headlineBold,
@@ -135,7 +141,7 @@ class TextTheme {
footnote: footnote ?? this.footnote, footnote: footnote ?? this.footnote,
captionBold: captionBold ?? this.captionBold, captionBold: captionBold ?? this.captionBold,
) )
: TextTheme.dark( : StreamTextTheme.dark(
body: body ?? this.body, body: body ?? this.body,
title: title ?? this.title, title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold, headlineBold: headlineBold ?? this.headlineBold,
@@ -147,7 +153,7 @@ class TextTheme {
); );
/// Merge text theme /// Merge text theme
TextTheme merge(TextTheme? other) { StreamTextTheme merge(StreamTextTheme? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
body: body.merge(other.body), body: body.merge(other.body),
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro user_list_view_theme}
@Deprecated("Use 'StreamUserListViewTheme' instead")
typedef UserListViewTheme = StreamUserListViewTheme;
/// {@template user_list_view_theme}
/// Overrides the default style of [UserListView] descendants. /// Overrides the default style of [UserListView] descendants.
/// ///
/// See also: /// See also:
/// ///
/// * [UserListViewThemeData], which is used to configure this theme. /// * [StreamUserListViewThemeData], which is used to configure this theme.
class UserListViewTheme extends InheritedTheme { /// {@endtemplate}
/// Creates a [UserListViewTheme]. class StreamUserListViewTheme extends InheritedTheme {
/// Creates a [StreamUserListViewTheme].
/// ///
/// The [data] parameter must not be null. /// The [data] parameter must not be null.
const UserListViewTheme({ const StreamUserListViewTheme({
Key? key, Key? key,
required this.data, required this.data,
required Widget child, required Widget child,
}) : super(key: key, child: child); }) : super(key: key, child: child);
/// The configuration of this theme. /// The configuration of this theme.
final UserListViewThemeData data; final StreamUserListViewThemeData data;
/// The closest instance of this class that encloses the given context. /// The closest instance of this class that encloses the given context.
/// ///
/// If there is no enclosing [UserListViewTheme] widget, then /// If there is no enclosing [StreamUserListViewTheme] widget, then
/// [StreamChatThemeData.userListViewTheme] is used. /// [StreamChatThemeData.userListViewTheme] is used.
/// ///
/// Typical usage is as follows: /// Typical usage is as follows:
@@ -30,35 +36,41 @@ class UserListViewTheme extends InheritedTheme {
/// ```dart /// ```dart
/// UserListViewTheme theme = UserListViewTheme.of(context); /// UserListViewTheme theme = UserListViewTheme.of(context);
/// ``` /// ```
static UserListViewThemeData of(BuildContext context) { static StreamUserListViewThemeData of(BuildContext context) {
final userListViewTheme = final userListViewTheme =
context.dependOnInheritedWidgetOfExactType<UserListViewTheme>(); context.dependOnInheritedWidgetOfExactType<StreamUserListViewTheme>();
return userListViewTheme?.data ?? return userListViewTheme?.data ??
StreamChatTheme.of(context).userListViewTheme; StreamChatTheme.of(context).userListViewTheme;
} }
@override @override
Widget wrap(BuildContext context, Widget child) => Widget wrap(BuildContext context, Widget child) =>
UserListViewTheme(data: data, child: child); StreamUserListViewTheme(data: data, child: child);
@override @override
bool updateShouldNotify(UserListViewTheme oldWidget) => bool updateShouldNotify(StreamUserListViewTheme oldWidget) =>
data != oldWidget.data; data != oldWidget.data;
} }
/// {@macro user_list_view_theme_data}
@Deprecated("Use 'StreamUserListViewThemeData' instead")
typedef UserListViewThemeData = StreamUserListViewThemeData;
/// {@template user_list_view_theme_data}
/// A style that overrides the default appearance of [UserListView]s when /// A style that overrides the default appearance of [UserListView]s when
/// used with [UserListViewTheme] or with the overall [StreamChatTheme]'s /// used with [StreamUserListViewTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.userListViewTheme]. /// [StreamChatThemeData.userListViewTheme].
/// ///
/// See also: /// See also:
/// ///
/// * [UserListViewTheme], the theme which is configured with this class. /// * [StreamUserListViewTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.userListViewTheme], which can be used to override /// * [StreamChatThemeData.userListViewTheme], which can be used to override
/// the default style for [UserListView]s below the overall /// the default style for [UserListView]s below the overall
/// [StreamChatTheme]. /// [StreamChatTheme].
class UserListViewThemeData with Diagnosticable { /// {@endtemplate}
/// Creates a [UserListViewThemeData]. class StreamUserListViewThemeData with Diagnosticable {
const UserListViewThemeData({ /// Creates a [StreamUserListViewThemeData].
const StreamUserListViewThemeData({
this.backgroundColor, this.backgroundColor,
}); });
@@ -66,27 +78,27 @@ class UserListViewThemeData with Diagnosticable {
final Color? backgroundColor; final Color? backgroundColor;
/// Copies this [ChannelListViewThemeData] to another. /// Copies this [ChannelListViewThemeData] to another.
UserListViewThemeData copyWith({ StreamUserListViewThemeData copyWith({
Color? backgroundColor, Color? backgroundColor,
}) => }) =>
UserListViewThemeData( StreamUserListViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor, backgroundColor: backgroundColor ?? this.backgroundColor,
); );
/// Linearly interpolate between two [UserListViewThemeData] themes. /// Linearly interpolate between two [StreamUserListViewThemeData] themes.
/// ///
/// All the properties must be non-null. /// All the properties must be non-null.
UserListViewThemeData lerp( StreamUserListViewThemeData lerp(
UserListViewThemeData a, StreamUserListViewThemeData a,
UserListViewThemeData b, StreamUserListViewThemeData b,
double t, double t,
) => ) =>
UserListViewThemeData( StreamUserListViewThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t), backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
); );
/// Merges one [UserListViewThemeData] with another. /// Merges one [StreamUserListViewThemeData] with another.
UserListViewThemeData merge(UserListViewThemeData? other) { StreamUserListViewThemeData merge(StreamUserListViewThemeData? other) {
if (other == null) return this; if (other == null) return this;
return copyWith( return copyWith(
backgroundColor: other.backgroundColor, backgroundColor: other.backgroundColor,
@@ -96,7 +108,7 @@ class UserListViewThemeData with Diagnosticable {
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is UserListViewThemeData && other is StreamUserListViewThemeData &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor; backgroundColor == other.backgroundColor;
@@ -3,6 +3,11 @@ import 'package:flutter/services.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro thread_header}
@Deprecated("Use 'StreamThreadHeader' instead")
typedef ThreadHeader = StreamThreadHeader;
/// {@template thread_header}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header_paint.png)
/// ///
@@ -56,9 +61,11 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// The widget components render the ui based on the first ancestor of type /// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property. /// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { /// {@endtemplate}
class StreamThreadHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Instantiate a new ThreadHeader /// Instantiate a new ThreadHeader
const ThreadHeader({ const StreamThreadHeader({
Key? key, Key? key,
required this.parent, required this.parent,
this.showBackButton = true, this.showBackButton = true,
@@ -102,12 +109,12 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// if a user is typing in this thread /// if a user is typing in this thread
final bool showTypingIndicator; final bool showTypingIndicator;
/// The background color of this [ThreadHeader]. /// The background color of this [StreamThreadHeader].
final Color? backgroundColor; final Color? backgroundColor;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final channelHeaderTheme = ChannelHeaderTheme.of(context); final channelHeaderTheme = StreamChannelHeaderTheme.of(context);
final defaultSubtitle = subtitle ?? final defaultSubtitle = subtitle ??
Row( Row(
@@ -161,12 +168,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
if (showTypingIndicator) if (showTypingIndicator)
TypingIndicator( Align(
alignment: Alignment.center, child: StreamTypingIndicator(
channel: StreamChannel.of(context).channel, channel: StreamChannel.of(context).channel,
style: channelHeaderTheme.subtitleStyle, style: channelHeaderTheme.subtitleStyle,
parentId: parent.id, parentId: parent.id,
alternativeWidget: defaultSubtitle, alternativeWidget: defaultSubtitle,
),
) )
else else
defaultSubtitle, defaultSubtitle,
@@ -3,15 +3,20 @@ import 'package:lottie/lottie.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// {@macro typing_indicator}
@Deprecated("Use 'StreamTypingIndicator' instead")
typedef TypingIndicator = StreamTypingIndicator;
/// {@template typing_indicator}
/// Widget to show the current list of typing users /// Widget to show the current list of typing users
class TypingIndicator extends StatelessWidget { /// {@endtemplate}
class StreamTypingIndicator extends StatelessWidget {
/// Instantiate a new TypingIndicator /// Instantiate a new TypingIndicator
const TypingIndicator({ const StreamTypingIndicator({
Key? key, Key? key,
this.channel, this.channel,
this.alternativeWidget, this.alternativeWidget,
this.style, this.style,
this.alignment = Alignment.centerLeft,
this.padding = const EdgeInsets.all(0), this.padding = const EdgeInsets.all(0),
this.parentId, this.parentId,
}) : super(key: key); }) : super(key: key);
@@ -28,9 +33,6 @@ class TypingIndicator extends StatelessWidget {
/// The padding of this widget /// The padding of this widget
final EdgeInsets padding; final EdgeInsets padding;
/// Alignment of the typing indicator
final Alignment alignment;
/// Id of the parent message in case of a thread /// Id of the parent message in case of a thread
final String? parentId; final String? parentId;
@@ -46,15 +48,11 @@ class TypingIndicator extends StatelessWidget {
stream: channelState.typingEventsStream.map((typings) => typings.entries stream: channelState.typingEventsStream.map((typings) => typings.entries
.where((element) => element.value.parentId == parentId) .where((element) => element.value.parentId == parentId)
.map((e) => e.key)), .map((e) => e.key)),
builder: (context, data) => AnimatedSwitcher( builder: (context, users) => AnimatedSwitcher(
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: data.isNotEmpty child: users.isNotEmpty
? Padding( ? Padding(
key: const Key('main'),
padding: padding, padding: padding,
child: Align(
key: const Key('typings'),
alignment: alignment,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -64,13 +62,12 @@ class TypingIndicator extends StatelessWidget {
height: 4, height: 4,
), ),
Text( Text(
context.translations.userTypingText(data), context.translations.userTypingText(users),
maxLines: 1, maxLines: 1,
style: style, style: style,
), ),
], ],
), ),
),
) )
: altWidget, : altWidget,
), ),

Some files were not shown because too many files have changed in this diff Show More