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: |
flutter pub global activate melos ${{ env.melos_version }}
- name: "Bootstrap Workspace"
run: melos bootstrap
run: melos bootstrap --verbose
- name: "Dart Analyze"
run: |
melos run analyze
+2 -1
View File
@@ -11,7 +11,8 @@ dependencies:
cupertino_icons: ^1.0.0
flutter:
sdk: flutter
stream_chat: ^2.2.1
stream_chat:
path: ../
dev_dependencies:
flutter_test:
@@ -294,6 +294,18 @@ class Channel {
return data;
}
/// List of user permissions on this channel
List<String> get ownCapabilities =>
state?._channelState.channel?.ownCapabilities ?? [];
/// List of user permissions on this channel
Stream<List<String>> get ownCapabilitiesStream {
_checkInitialized();
return state!.channelStateStream
.map((cs) => cs.channel?.ownCapabilities ?? [])
.distinct();
}
/// Channel extra data as a stream.
Stream<Map<String, Object?>> get extraDataStream {
_checkInitialized();
@@ -487,6 +499,7 @@ class Channel {
Future<SendMessageResponse> sendMessage(
Message message, {
bool skipPush = false,
bool skipEnrichUrl = false,
}) async {
_checkInitialized();
// Cancelling previous completer in case it's called again in the process
@@ -534,6 +547,7 @@ class Channel {
id!,
type,
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
);
state!.updateMessage(response.message);
if (cooldown > 0) cooldownStartedAt = DateTime.now();
@@ -550,7 +564,10 @@ class Channel {
///
/// Waits for a [_messageAttachmentsUploadCompleter] to complete
/// before actually updating the message.
Future<UpdateMessageResponse> updateMessage(Message message) async {
Future<UpdateMessageResponse> updateMessage(
Message message, {
bool skipEnrichUrl = false,
}) async {
final originalMessage = message;
// Cancelling previous completer in case it's called again in the process
@@ -588,7 +605,10 @@ class Channel {
message = await attachmentsUploadCompleter.future;
}
final response = await _client.updateMessage(message);
final response = await _client.updateMessage(
message,
skipEnrichUrl: skipEnrichUrl,
);
final m = response.message.copyWith(
ownReactions: message.ownReactions,
@@ -618,12 +638,14 @@ class Channel {
Message message, {
Map<String, Object?>? set,
List<String>? unset,
bool skipEnrichUrl = false,
}) async {
try {
final response = await _client.partialUpdateMessage(
message.id,
set: set,
unset: unset,
skipEnrichUrl: skipEnrichUrl,
);
final updatedMessage = response.message.copyWith(
@@ -1598,7 +1620,9 @@ class ChannelClientState {
_subscriptions.add(_channel.on(EventType.channelUpdated).listen((Event e) {
final channel = e.channel!;
updateChannelState(channelState.copyWith(
channel: channel,
channel: channel.copyWith(
ownCapabilities: channelState.channel?.ownCapabilities,
),
members: channel.members,
));
}));
@@ -1968,6 +1992,14 @@ class ChannelClientState {
(watchers, users) => watchers!.map((e) => users[e.id] ?? e).toList(),
);
/// Channel member for the current user.
Member? get currentUserMember => members.firstWhereOrNull(
(m) => m.user?.id == _channel.client.state.currentUser?.id,
);
/// User role for the current user.
String? get currentUserRole => currentUserMember?.role;
/// Channel read list.
List<Read> get read => _channelState.read;
@@ -1212,12 +1212,14 @@ class StreamChatClient {
String channelId,
String channelType, {
bool skipPush = false,
bool skipEnrichUrl = false,
}) =>
_chatApi.message.sendMessage(
channelId,
channelType,
message,
skipPush: skipPush,
skipEnrichUrl: skipEnrichUrl,
);
/// Lists all the message replies for the [parentId]
@@ -1241,8 +1243,14 @@ class StreamChatClient {
);
/// Update the given message
Future<UpdateMessageResponse> updateMessage(Message message) =>
_chatApi.message.updateMessage(message);
Future<UpdateMessageResponse> updateMessage(
Message message, {
bool skipEnrichUrl = false,
}) =>
_chatApi.message.updateMessage(
message,
skipEnrichUrl: skipEnrichUrl,
);
/// Partially update the given [messageId]
/// Use [set] to define values to be set
@@ -1251,11 +1259,13 @@ class StreamChatClient {
String messageId, {
Map<String, Object?>? set,
List<String>? unset,
bool skipEnrichUrl = false,
}) =>
_chatApi.message.partialUpdateMessage(
messageId,
set: set,
unset: unset,
skipEnrichUrl: skipEnrichUrl,
);
/// Deletes the given message
@@ -5,9 +5,9 @@ import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/client/retry_policy.dart';
import 'package:stream_chat/stream_chat.dart';
/// The retry queue associated to a channel
/// The retry queue associated to a channel.
class RetryQueue {
/// Instantiate a new RetryQueue object
/// Instantiate a new RetryQueue object.
RetryQueue({
required this.channel,
this.logger,
@@ -17,13 +17,13 @@ class RetryQueue {
_listenFailedEvents();
}
/// The channel of this queue
/// The channel of this queue.
final Channel channel;
/// The client associated with this [channel]
/// The client associated with this [channel].
final StreamChatClient client;
/// The logger associated to this queue
/// The logger associated to this queue.
final Logger? logger;
late final RetryPolicy _retryPolicy;
@@ -63,7 +63,7 @@ class RetryQueue {
}).addTo(_compositeSubscription);
}
/// Add a list of messages
/// Add a list of messages.
void add(List<Message> messages) {
if (messages.isEmpty) return;
if (!_messageQueue.containsAllMessage(messages)) {
@@ -113,6 +113,7 @@ class RetryQueue {
} catch (e) {
if (e is! StreamChatNetworkError || !e.isRetriable) {
_messageQueue.removeMessage(message);
_sendFailedEvent(message);
return true;
}
// retry logic
@@ -174,10 +175,10 @@ class RetryQueue {
}
}
/// Whether our [_messageQueue] has messages or not
/// Whether our [_messageQueue] has messages or not.
bool get hasMessages => _messageQueue.isNotEmpty;
/// Call this method to dispose this object
/// Call this method to dispose this object.
void dispose() {
_messageQueue.clear();
_compositeSubscription.dispose();
@@ -269,6 +269,7 @@ class ChannelApi {
) async {
final response = await _client.post(
'${_getChannelUrl(channelId, channelType)}/truncate',
data: {},
);
return EmptyResponse.fromJson(response.data);
}
@@ -16,12 +16,14 @@ class MessageApi {
String channelType,
Message message, {
bool skipPush = false,
bool skipEnrichUrl = false,
}) async {
final response = await _client.post(
'/channels/$channelType/$channelId/message',
data: {
'message': message,
'skip_push': skipPush,
'skip_enrich_url': skipEnrichUrl,
},
);
return SendMessageResponse.fromJson(response.data);
@@ -51,11 +53,15 @@ class MessageApi {
/// Updates the given [message]
Future<UpdateMessageResponse> updateMessage(
Message message,
) async {
Message message, {
bool skipEnrichUrl = false,
}) async {
final response = await _client.post(
'/messages/${message.id}',
data: {'message': message},
data: {
'message': message,
'skip_enrich_url': skipEnrichUrl,
},
);
return UpdateMessageResponse.fromJson(response.data);
}
@@ -67,12 +73,14 @@ class MessageApi {
String messageId, {
Map<String, Object?>? set,
List<String>? unset,
bool skipEnrichUrl = false,
}) async {
final response = await _client.put(
'/messages/$messageId',
data: {
if (set != null) 'set': set,
if (unset != null) 'unset': unset,
'skip_enrich_url': skipEnrichUrl,
},
);
return UpdateMessageResponse.fromJson(response.data);
@@ -2,6 +2,7 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/models/action.dart';
import 'package:stream_chat/src/core/models/attachment_file.dart';
import 'package:stream_chat/src/core/util/serializer.dart';
@@ -66,6 +67,21 @@ class Attachment extends Equatable {
topLevelFields + dbSpecificTopLevelFields,
));
factory Attachment.fromOGAttachment(OGAttachmentResponse ogAttachment) =>
Attachment(
type: ogAttachment.type,
title: ogAttachment.title,
titleLink: ogAttachment.titleLink,
text: ogAttachment.text,
imageUrl: ogAttachment.imageUrl,
thumbUrl: ogAttachment.thumbUrl,
authorName: ogAttachment.authorName,
authorLink: ogAttachment.authorLink,
assetUrl: ogAttachment.assetUrl,
ogScrapeUrl: ogAttachment.ogScrapeUrl,
uploadState: const UploadState.success(),
);
///The attachment type based on the URL resource. This can be: audio,
///image or video
final String? type;
@@ -229,6 +245,33 @@ class Attachment extends Equatable {
extraData: extraData ?? this.extraData,
);
Attachment merge(Attachment? other) {
if (other == null) return this;
return copyWith(
type: other.type,
titleLink: other.titleLink,
title: other.title,
thumbUrl: other.thumbUrl,
text: other.text,
pretext: other.pretext,
ogScrapeUrl: other.ogScrapeUrl,
imageUrl: other.imageUrl,
footerIcon: other.footerIcon,
footer: other.footer,
fields: other.fields,
fallback: other.fallback,
color: other.color,
authorName: other.authorName,
authorLink: other.authorLink,
authorIcon: other.authorIcon,
assetUrl: other.assetUrl,
actions: other.actions,
file: other.file,
uploadState: other.uploadState,
extraData: other.extraData,
);
}
@override
List<Object?> get props => [
id,
@@ -13,6 +13,7 @@ class ChannelModel {
String? id,
String? type,
String? cid,
this.ownCapabilities = const [],
ChannelConfig? config,
this.createdBy,
this.frozen = false,
@@ -51,6 +52,10 @@ class ChannelModel {
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final String cid;
/// List of user permissions on this channel
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final List<String> ownCapabilities;
/// The channel configuration data
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final ChannelConfig config;
@@ -101,6 +106,7 @@ class ChannelModel {
'id',
'type',
'cid',
'own_capabilities',
'config',
'created_by',
'frozen',
@@ -127,6 +133,7 @@ class ChannelModel {
String? id,
String? type,
String? cid,
List<String>? ownCapabilities,
ChannelConfig? config,
User? createdBy,
bool? frozen,
@@ -143,6 +150,7 @@ class ChannelModel {
id: id ?? this.id,
type: type ?? this.type,
cid: cid ?? this.cid,
ownCapabilities: ownCapabilities ?? this.ownCapabilities,
config: config ?? this.config,
createdBy: createdBy ?? this.createdBy,
frozen: frozen ?? this.frozen,
@@ -164,6 +172,7 @@ class ChannelModel {
id: other.id,
type: other.type,
cid: other.cid,
ownCapabilities: other.ownCapabilities,
config: other.config,
createdBy: other.createdBy,
frozen: other.frozen,
@@ -10,6 +10,10 @@ ChannelModel _$ChannelModelFromJson(Map<String, dynamic> json) => ChannelModel(
id: json['id'] as String?,
type: json['type'] as String?,
cid: json['cid'] as String?,
ownCapabilities: (json['own_capabilities'] as List<dynamic>?)
?.map((e) => e as String)
.toList() ??
const [],
config: json['config'] == null
? null
: ChannelConfig.fromJson(json['config'] as Map<String, dynamic>),
@@ -48,6 +52,7 @@ Map<String, dynamic> _$ChannelModelToJson(ChannelModel instance) {
}
writeNotNull('cid', readonly(instance.cid));
writeNotNull('own_capabilities', readonly(instance.ownCapabilities));
writeNotNull('config', readonly(instance.config));
writeNotNull('created_by', readonly(instance.createdBy));
val['frozen'] = instance.frozen;
@@ -8,13 +8,13 @@ import 'package:uuid/uuid.dart';
part 'message.g.dart';
class _PinExpires {
const _PinExpires();
class _NullConst {
const _NullConst();
}
const _pinExpires = _PinExpires();
const _nullConst = _NullConst();
/// Enum defining the status of a sending message
/// Enum defining the status of a sending message.
enum MessageSendingStatus {
/// Message is being sent
sending,
@@ -40,10 +40,10 @@ enum MessageSendingStatus {
sent,
}
/// The class that contains the information about a message
/// The class that contains the information about a message.
@JsonSerializable()
class Message extends Equatable {
/// Constructor used for json serialization
/// Constructor used for json serialization.
Message({
String? id,
this.text,
@@ -58,44 +58,47 @@ class Message extends Equatable {
this.ownReactions,
this.parentId,
this.quotedMessage,
this.quotedMessageId,
String? quotedMessageId,
this.replyCount = 0,
this.threadParticipants,
this.showInChannel,
this.command,
DateTime? createdAt,
DateTime? updatedAt,
this.deletedAt,
this.user,
this.pinned = false,
this.pinnedAt,
DateTime? pinExpires,
this.pinnedBy,
this.extraData = const {},
this.deletedAt,
this.status = MessageSendingStatus.sent,
this.status = MessageSendingStatus.sending,
this.i18n,
}) : id = id ?? const Uuid().v4(),
pinExpires = pinExpires?.toUtc(),
createdAt = createdAt ?? DateTime.now(),
updatedAt = updatedAt ?? DateTime.now();
_createdAt = createdAt,
_updatedAt = updatedAt,
_quotedMessageId = quotedMessageId;
/// Create a new instance from a json
/// Create a new instance from JSON.
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
Serializer.moveToExtraDataFromRoot(json, topLevelFields),
).copyWith(
status: MessageSendingStatus.sent,
);
/// The message ID. This is either created by Stream or set client side when
/// the message is added.
final String id;
/// The text of this message
/// The text of this message.
final String? text;
/// The status of a sending message
/// The status of a sending message.
@JsonKey(ignore: true)
final MessageSendingStatus status;
/// The message type
/// The message type.
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
@@ -107,15 +110,15 @@ class Message extends Equatable {
@JsonKey(includeIfNull: false)
final List<Attachment> attachments;
/// The list of user mentioned in the message
/// The list of user mentioned in the message.
@JsonKey(toJson: User.toIds)
final List<User> mentionedUsers;
/// A map describing the count of number of every reaction
/// A map describing the count of number of every reaction.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final Map<String, int>? reactionCounts;
/// A map describing the count of score of every reaction
/// A map describing the count of score of every reaction.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final Map<String, int>? reactionScores;
@@ -130,12 +133,14 @@ class Message extends Equatable {
/// The ID of the parent message, if the message is a thread reply.
final String? parentId;
/// A quoted reply message
/// A quoted reply message.
@JsonKey(toJson: Serializer.readOnly)
final Message? quotedMessage;
final String? _quotedMessageId;
/// The ID of the quoted message, if the message is a quoted reply.
final String? quotedMessageId;
String? get quotedMessageId => _quotedMessageId ?? quotedMessage?.id;
/// Reserved field indicating the number of replies for this message.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
@@ -148,10 +153,10 @@ class Message extends Equatable {
/// Check if this message needs to show in the channel.
final bool? showInChannel;
/// If true the message is silent
/// If true the message is silent.
final bool silent;
/// If true the message is shadowed
/// If true the message is shadowed.
@JsonKey(
includeIfNull: false,
toJson: Serializer.readOnly,
@@ -162,56 +167,61 @@ class Message extends Equatable {
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final String? command;
/// Reserved field indicating when the message was created.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final DateTime createdAt;
/// Reserved field indicating when the message was updated last time.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final DateTime updatedAt;
/// User who sent the message
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final User? user;
/// If true the message is pinned
final bool pinned;
/// Reserved field indicating when the message was pinned
@JsonKey(toJson: Serializer.readOnly)
final DateTime? pinnedAt;
/// Reserved field indicating when the message will expire
///
/// if `null` message has no expiry
final DateTime? pinExpires;
/// Reserved field indicating who pinned the message
@JsonKey(toJson: Serializer.readOnly)
final User? pinnedBy;
/// Message custom extraData
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// True if the message is a system info
bool get isSystem => type == 'system';
/// True if the message has been deleted
bool get isDeleted => type == 'deleted';
/// True if the message is ephemeral
bool get isEphemeral => type == 'ephemeral';
final DateTime? _createdAt;
/// Reserved field indicating when the message was deleted.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final DateTime? deletedAt;
/// Reserved field indicating when the message was created.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
DateTime get createdAt => _createdAt ?? DateTime.now();
final DateTime? _updatedAt;
/// Reserved field indicating when the message was updated last time.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
DateTime get updatedAt => _updatedAt ?? DateTime.now();
/// User who sent the message.
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final User? user;
/// If true the message is pinned.
final bool pinned;
/// Reserved field indicating when the message was pinned.
@JsonKey(toJson: Serializer.readOnly)
final DateTime? pinnedAt;
/// Reserved field indicating when the message will expire.
///
/// If `null` message has no expiry.
final DateTime? pinExpires;
/// Reserved field indicating who pinned the message.
@JsonKey(toJson: Serializer.readOnly)
final User? pinnedBy;
/// Message custom extraData.
@JsonKey(includeIfNull: false)
final Map<String, Object?> extraData;
/// True if the message is a system info.
bool get isSystem => type == 'system';
/// True if the message has been deleted.
bool get isDeleted => type == 'deleted';
/// True if the message is ephemeral.
bool get isEphemeral => type == 'ephemeral';
/// A Map of translations.
@JsonKey(includeIfNull: false)
final Map<String, String>? i18n;
/// Known top level fields.
///
/// Useful for [Serializer] methods.
static const topLevelFields = [
'id',
@@ -244,7 +254,7 @@ class Message extends Equatable {
'i18n',
];
/// Serialize to json
/// Serialize to json.
Map<String, dynamic> toJson() => Serializer.moveFromExtraDataToRoot(
_$MessageToJson(this),
);
@@ -256,18 +266,18 @@ class Message extends Equatable {
String? type,
List<Attachment>? attachments,
List<User>? mentionedUsers,
bool? silent,
bool? shadowed,
Map<String, int>? reactionCounts,
Map<String, int>? reactionScores,
List<Reaction>? latestReactions,
List<Reaction>? ownReactions,
String? parentId,
Message? quotedMessage,
String? quotedMessageId,
Object? quotedMessage = _nullConst,
Object? quotedMessageId = _nullConst,
int? replyCount,
List<User>? threadParticipants,
bool? showInChannel,
bool? shadowed,
bool? silent,
String? command,
DateTime? createdAt,
DateTime? updatedAt,
@@ -275,7 +285,7 @@ class Message extends Equatable {
User? user,
bool? pinned,
DateTime? pinnedAt,
Object? pinExpires = _pinExpires,
Object? pinExpires = _nullConst,
User? pinnedBy,
Map<String, Object?>? extraData,
MessageSendingStatus? status,
@@ -284,41 +294,68 @@ class Message extends Equatable {
assert(() {
if (pinExpires is! DateTime &&
pinExpires != null &&
pinExpires is! _PinExpires) {
pinExpires is! _NullConst) {
throw ArgumentError('`pinExpires` can only be set as DateTime or null');
}
return true;
}(), 'Validate type for pinExpires');
assert(() {
if (quotedMessage is! Message &&
quotedMessage != null &&
quotedMessage is! _NullConst) {
throw ArgumentError(
'`quotedMessage` can only be set as Message or null',
);
}
return true;
}(), 'Validate type for quotedMessage');
assert(() {
if (quotedMessageId is! String &&
quotedMessageId != null &&
quotedMessageId is! _NullConst) {
throw ArgumentError(
'`quotedMessage` can only be set as String or null',
);
}
return true;
}(), 'Validate type for quotedMessage');
return Message(
id: id ?? this.id,
text: text ?? this.text,
type: type ?? this.type,
attachments: attachments ?? this.attachments,
mentionedUsers: mentionedUsers ?? this.mentionedUsers,
silent: silent ?? this.silent,
shadowed: shadowed ?? this.shadowed,
reactionCounts: reactionCounts ?? this.reactionCounts,
reactionScores: reactionScores ?? this.reactionScores,
latestReactions: latestReactions ?? this.latestReactions,
ownReactions: ownReactions ?? this.ownReactions,
parentId: parentId ?? this.parentId,
quotedMessage: quotedMessage ?? this.quotedMessage,
quotedMessageId: quotedMessageId ?? this.quotedMessageId,
quotedMessage: quotedMessage == _nullConst
? this.quotedMessage
: quotedMessage as Message?,
quotedMessageId: quotedMessageId == _nullConst
? _quotedMessageId
: quotedMessageId as String?,
replyCount: replyCount ?? this.replyCount,
threadParticipants: threadParticipants ?? this.threadParticipants,
showInChannel: showInChannel ?? this.showInChannel,
command: command ?? this.command,
createdAt: createdAt ?? this.createdAt,
silent: silent ?? this.silent,
extraData: extraData ?? this.extraData,
user: user ?? this.user,
shadowed: shadowed ?? this.shadowed,
updatedAt: updatedAt ?? this.updatedAt,
createdAt: createdAt ?? _createdAt,
updatedAt: updatedAt ?? _updatedAt,
deletedAt: deletedAt ?? this.deletedAt,
status: status ?? this.status,
user: user ?? this.user,
pinned: pinned ?? this.pinned,
pinnedAt: pinnedAt ?? this.pinnedAt,
pinnedBy: pinnedBy ?? this.pinnedBy,
pinExpires:
pinExpires == _pinExpires ? this.pinExpires : pinExpires as DateTime?,
pinExpires == _nullConst ? this.pinExpires : pinExpires as DateTime?,
pinnedBy: pinnedBy ?? this.pinnedBy,
extraData: extraData ?? this.extraData,
status: status ?? this.status,
i18n: i18n ?? this.i18n,
);
}
@@ -331,6 +368,8 @@ class Message extends Equatable {
type: other.type,
attachments: other.attachments,
mentionedUsers: other.mentionedUsers,
silent: other.silent,
shadowed: other.shadowed,
reactionCounts: other.reactionCounts,
reactionScores: other.reactionScores,
latestReactions: other.latestReactions,
@@ -343,17 +382,15 @@ class Message extends Equatable {
showInChannel: other.showInChannel,
command: other.command,
createdAt: other.createdAt,
silent: other.silent,
extraData: other.extraData,
user: other.user,
shadowed: other.shadowed,
updatedAt: other.updatedAt,
deletedAt: other.deletedAt,
status: other.status,
user: other.user,
pinned: other.pinned,
pinnedAt: other.pinnedAt,
pinExpires: other.pinExpires,
pinnedBy: other.pinnedBy,
extraData: other.extraData,
status: other.status,
i18n: other.i18n,
);
@@ -377,8 +414,8 @@ class Message extends Equatable {
shadowed,
silent,
command,
createdAt,
updatedAt,
_createdAt,
_updatedAt,
deletedAt,
user,
pinned,
@@ -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:logging/logging.dart' show Logger, Level;
export 'package:rate_limiter/rate_limiter.dart';
export 'package:uuid/uuid.dart';
export './src/core/api/attachment_file_uploader.dart'
show AttachmentFileUploader;
@@ -36,6 +37,7 @@ export './src/core/util/extension.dart';
export './src/db/chat_persistence_client.dart';
export './src/event_type.dart';
export './src/location.dart';
export './src/permission_type.dart';
export './src/ws/connection_status.dart';
export 'src/client/channel.dart';
export 'src/client/client.dart';
@@ -244,9 +244,13 @@ void main() {
group('`.sendMessage`', () {
test('should work fine', () async {
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
user: client.state.currentUser,
);
final sendMessageResponse = SendMessageResponse()..message = message;
final sendMessageResponse = SendMessageResponse()
..message = message.copyWith(status: MessageSendingStatus.sent);
when(() => client.sendMessage(
any(that: isSameMessageAs(message)),
@@ -329,6 +333,7 @@ void main() {
.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
.toList(growable: false),
status: MessageSendingStatus.sent,
));
expectLater(
@@ -455,7 +460,10 @@ void main() {
group('`.updateMessage`', () {
test('should work fine', () async {
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
status: MessageSendingStatus.sent,
);
final updateMessageResponse = UpdateMessageResponse()
..message = message;
@@ -530,6 +538,7 @@ void main() {
any(that: isSameMessageAs(message)),
)).thenAnswer((_) async => UpdateMessageResponse()
..message = message.copyWith(
status: MessageSendingStatus.sent,
attachments: attachments
.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
@@ -678,7 +687,7 @@ void main() {
[
isSameMessageAs(
updateMessageResponse.message.copyWith(
status: MessageSendingStatus.sent,
status: MessageSendingStatus.sending,
),
matchText: true,
matchSendingStatus: true,
@@ -707,7 +716,10 @@ void main() {
group('`.deleteMessage`', () {
test('should work fine', () async {
const messageId = 'test-message-id';
final message = Message(id: messageId);
final message = Message(
id: messageId,
status: MessageSendingStatus.sent,
);
when(() => client.deleteMessage(messageId))
.thenAnswer((_) async => EmptyResponse());
@@ -744,7 +756,6 @@ void main() {
const messageId = 'test-message-id';
final message = Message(
id: messageId,
status: MessageSendingStatus.sending,
);
expectLater(
@@ -1077,7 +1088,10 @@ void main() {
group('`.sendReaction`', () {
test('should work fine', () async {
const type = 'test-reaction-type';
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
status: MessageSendingStatus.sent,
);
final reaction = Reaction(type: type, messageId: message.id);
@@ -1118,7 +1132,10 @@ void main() {
test('should work fine with score passed explicitly', () async {
const type = 'test-reaction-type';
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
status: MessageSendingStatus.sent,
);
const score = 5;
final reaction = Reaction(
@@ -1178,7 +1195,10 @@ void main() {
test('should work fine with score passed explicitly and in extraData',
() async {
const type = 'test-reaction-type';
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
status: MessageSendingStatus.sent,
);
const score = 5;
const extraDataScore = 3;
@@ -1249,7 +1269,10 @@ void main() {
'should restore previous message if `client.sendReaction` throws',
() async {
const type = 'test-reaction-type';
final message = Message(id: 'test-message-id');
final message = Message(
id: 'test-message-id',
status: MessageSendingStatus.sent,
);
final reaction = Reaction(type: type, messageId: message.id);
@@ -1310,6 +1333,7 @@ void main() {
latestReactions: [prevReaction],
reactionScores: const {prevType: 1},
reactionCounts: const {prevType: 1},
status: MessageSendingStatus.sent,
);
const type = 'test-reaction-type-2';
@@ -1341,7 +1365,7 @@ void main() {
emitsInOrder([
[
isSameMessageAs(
newMessage.copyWith(status: MessageSendingStatus.sent),
newMessage,
matchReactions: true,
matchSendingStatus: true,
),
@@ -1374,6 +1398,7 @@ void main() {
final message = Message(
id: 'test-message-id',
parentId: 'test-parent-id', // is thread message
status: MessageSendingStatus.sent,
);
final reaction = Reaction(type: type, messageId: message.id);
@@ -1423,6 +1448,7 @@ void main() {
final message = Message(
id: 'test-message-id',
parentId: 'test-parent-id', // is thread message
status: MessageSendingStatus.sent,
);
final reaction = Reaction(type: type, messageId: message.id);
@@ -1490,6 +1516,7 @@ void main() {
latestReactions: [prevReaction],
reactionScores: const {prevType: 1},
reactionCounts: const {prevType: 1},
status: MessageSendingStatus.sent,
);
const type = 'test-reaction-type-2';
@@ -1567,6 +1594,7 @@ void main() {
latestReactions: [reaction],
reactionScores: const {type: 1},
reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
);
when(() => client.deleteReaction(messageId, type))
@@ -1614,6 +1642,7 @@ void main() {
latestReactions: [reaction],
reactionScores: const {type: 1},
reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
);
when(() => client.deleteReaction(messageId, type))
@@ -1668,11 +1697,13 @@ void main() {
);
final message = Message(
id: messageId,
parentId: parentId, // is thread
parentId: parentId,
// is thread
ownReactions: [reaction],
latestReactions: [reaction],
reactionScores: const {type: 1},
reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
);
when(() => client.deleteReaction(messageId, type))
@@ -1725,6 +1756,7 @@ void main() {
latestReactions: [reaction],
reactionScores: const {type: 1},
reactionCounts: const {type: 1},
status: MessageSendingStatus.sent,
);
when(() => client.deleteReaction(messageId, type))
@@ -481,14 +481,21 @@ void main() {
final path = '${_getChannelUrl(channelId, channelType)}/truncate';
when(() => client.post(path)).thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
when(() => client.post(
path,
data: {},
))
.thenAnswer(
(_) async => successResponse(path, data: <String, dynamic>{}));
final res = await channelApi.truncateChannel(channelId, channelType);
expect(res, isNotNull);
verify(() => client.post(path)).called(1);
verify(() => client.post(
path,
data: {},
)).called(1);
verifyNoMoreInteractions(client);
});
@@ -32,6 +32,7 @@ void main() {
data: {
'message': message,
'skip_push': false,
'skip_enrich_url': false,
},
)).thenAnswer((_) async => successResponse(path, data: {
'message': message.toJson(),
@@ -58,6 +59,7 @@ void main() {
data: {
'message': message,
'skip_push': true,
'skip_enrich_url': false,
},
)).thenAnswer((_) async => successResponse(path, data: {
'message': message.toJson(),
@@ -137,7 +139,10 @@ void main() {
when(() => client.post(
path,
data: {'message': message},
data: {
'message': message,
'skip_enrich_url': false,
},
)).thenAnswer(
(_) async => successResponse(path, data: {'message': message.toJson()}),
);
@@ -162,7 +167,11 @@ void main() {
when(() => client.put(
path,
data: {'set': set, 'unset': unset},
data: {
'set': set,
'unset': unset,
'skip_enrich_url': false,
},
)).thenAnswer(
(_) async => successResponse(path, data: {'message': message.toJson()}),
);
@@ -180,7 +189,11 @@ void main() {
verify(() => client.put(
path,
data: {'set': set, 'unset': unset},
data: {
'set': set,
'unset': unset,
'skip_enrich_url': false,
},
)).called(1);
verifyNoMoreInteractions(client);
});
+11 -1
View File
@@ -1,5 +1,10 @@
## 3.5.1
🛑️ Breaking Changes
- `pinPermissions` is no longer needed in `MessageListView`.
- `MessageInput` now works with a `MessageInputController` instead of a `TextEditingController`
🐞 Fixed
- Mentions overlay now doesn't overflow when there is not enough height available
@@ -31,7 +36,12 @@
✅ Added
- Videos can now be auto-played in `FullScreenMedia`, by setting the `autoplayVideos` argument to true.
- Videos can now be auto-played in `FullScreenMedia`
- Extra customisation options for `MessageInput`
🔄 Changed
- Add `didUpdateWidget` override in `MessageInput` widget to handle changes to `focusNode`.
## 3.3.2
@@ -87,7 +87,7 @@ class MyApp extends StatelessWidget {
/// A list of messages sent in the current channel.
///
/// This is implemented using [MessageListView], a widget that provides query
/// This is implemented using [StreamMessageListView], a widget that provides query
/// functionalities fetching the messages from the api and showing them in a
/// listView.
class ChannelPage extends StatelessWidget {
@@ -98,13 +98,13 @@ class ChannelPage extends StatelessWidget {
@override
Widget build(BuildContext context) => Scaffold(
appBar: const ChannelHeader(),
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: MessageListView(),
child: StreamMessageListView(),
),
MessageInput(attachmentLimit: 3),
StreamMessageInput(attachmentLimit: 3),
],
),
);
@@ -84,7 +84,7 @@ class _SplitViewState extends State<SplitView> {
);
}
class ChannelListPage extends StatelessWidget {
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
Key? key,
this.onTap,
@@ -92,22 +92,26 @@ class ChannelListPage extends StatelessWidget {
final void Function(Channel)? onTap;
@override
State<ChannelListPage> createState() => _ChannelListPageState();
}
class _ChannelListPageState extends State<ChannelListPage> {
late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
);
@override
Widget build(BuildContext context) => Scaffold(
body: ChannelsBloc(
child: ChannelListView(
onChannelTap: onTap != null
? (channel, _) {
onTap!(channel);
}
: null,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
),
body: StreamChannelListView(
onChannelTap: widget.onTap,
controller: _listController,
),
);
}
@@ -121,15 +125,15 @@ class ChannelPage extends StatelessWidget {
Widget build(BuildContext context) => Navigator(
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (context) => Scaffold(
appBar: const ChannelHeader(
appBar: const StreamChannelHeader(
showBackButton: false,
),
body: Column(
children: const <Widget>[
Expanded(
child: MessageListView(),
child: StreamMessageListView(),
),
MessageInput(),
StreamMessageInput(),
],
),
),
@@ -27,7 +27,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// - We make [StreamChat] the root Widget of our application
///
/// - We create a single [ChannelPage] widget under [StreamChat] with three
/// widgets: [ChannelHeader], [MessageListView] and [MessageInput]
/// widgets: [StreamChannelHeader], [StreamMessageListView] and [StreamMessageInput]
///
/// If you now run the simulator you will see a single channel UI.
void main() async {
@@ -90,18 +90,15 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: const ChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
);
}
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
}
@@ -25,7 +25,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// The [ChannelListPage] widget retrieves the list of channels based on a
/// custom query and ordering. In this case we are showing the list of
/// channels in which the current user is a member and we order them based
/// on the time they had a new message. [ChannelListView] handles pagination
/// on the time they had a new message. [StreamChannelListView] handles pagination
/// and updates automatically when new channels are created or when a new
/// message is added to a channel.
void main() async {
@@ -62,33 +62,53 @@ class MyApp extends StatelessWidget {
client: client,
child: child,
),
home: const ChannelListPage(),
home: ChannelListPage(
client: client,
),
);
}
}
class ChannelListPage extends StatelessWidget {
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
Key? key,
required this.client,
}) : super(key: key);
final StreamChatClient client;
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
State<ChannelListPage> createState() => _ChannelListPageState();
}
class _ChannelListPageState extends State<ChannelListPage> {
late final _controller = StreamChannelListController(
client: widget.client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
);
@override
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(),
),
),
),
),
sort: const [SortOption('last_message_at')],
limit: 20,
channelWidget: const ChannelPage(),
),
),
);
}
);
}
class ChannelPage extends StatelessWidget {
@@ -97,18 +117,15 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: const ChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: MessageListView(),
),
MessageInput(),
],
),
);
}
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: StreamMessageListView(),
),
StreamMessageInput(),
],
),
);
}
@@ -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
/// and include the number of unread messages for each.
///
/// We're passing a custom widget to [ChannelListView.channelPreviewBuilder];
/// this will override the default [ChannelPreview] and allows you to create
/// We're passing a custom widget to [StreamChannelListView.channelPreviewBuilder];
/// this will override the default [StreamChannelPreview] and allows you to create
/// one yourself.
///
/// 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({
Key? key,
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
channelPreviewBuilder: _channelPreviewBuilder,
// sort: [SortOption('last_message_at')],
limit: 20,
channelWidget: const ChannelPage(),
),
),
);
}
State<ChannelListPage> createState() => _ChannelListPageState();
}
Widget _channelPreviewBuilder(BuildContext context, Channel channel) {
class _ChannelListPageState extends State<ChannelListPage> {
late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
);
@override
Widget build(BuildContext context) => Scaffold(
body: 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,
StreamChannelListTile defaultTile,
) {
final lastMessage = channel.state?.messages.reversed.firstWhereOrNull(
(message) => !message.isDeleted,
);
@@ -112,16 +130,17 @@ class ChannelListPage extends StatelessWidget {
),
);
},
leading: ChannelAvatar(
leading: StreamChannelAvatar(
channel: channel,
),
title: ChannelName(
textStyle: ChannelPreviewTheme.of(context).titleStyle!.copyWith(
title: StreamChannelName(
textStyle: StreamChannelPreviewTheme.of(context).titleStyle!.copyWith(
color: StreamChatTheme.of(context)
.colorTheme
.textHighEmphasis
.withOpacity(opacity),
),
channel: channel,
),
subtitle: Text(subtitle),
trailing: channel.state!.unreadCount > 0
@@ -143,13 +162,13 @@ class ChannelPage extends StatelessWidget {
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: const ChannelHeader(),
appBar: const StreamChannelHeader(),
body: Column(
children: const <Widget>[
Expanded(
child: MessageListView(),
child: StreamMessageListView(),
),
MessageInput(),
StreamMessageInput(),
],
),
);
@@ -8,8 +8,8 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// to create sub-conversations inside the same channel.
///
/// Using threaded conversations is very simple and mostly a matter of
/// plugging the [MessageListView] to another widget that renders the widget.
/// To make this simple, such a widget only needs to build [MessageListView]
/// plugging the [StreamMessageListView] to another widget that renders the widget.
/// To make this simple, such a widget only needs to build [StreamMessageListView]
/// with the parent attribute set to the threads root message.
///
/// Now we can open threads and create new ones as well. If you long-press a
@@ -53,28 +53,42 @@ class MyApp extends StatelessWidget {
}
}
class ChannelListPage extends StatelessWidget {
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
channelWidget: const ChannelPage(),
State<ChannelListPage> createState() => _ChannelListPageState();
}
class _ChannelListPageState extends State<ChannelListPage> {
late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
);
@override
Widget build(BuildContext context) => Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
),
);
},
),
),
);
}
);
}
class ChannelPage extends StatelessWidget {
@@ -83,24 +97,21 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: const ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
),
),
),
const MessageInput(),
],
),
);
}
const StreamMessageInput(),
],
),
);
}
class ThreadPage extends StatelessWidget {
@@ -115,18 +126,20 @@ class ThreadPage extends StatelessWidget {
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ThreadHeader(
appBar: StreamThreadHeader(
parent: parent!,
),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
child: StreamMessageListView(
parentMessage: parent,
),
),
MessageInput(
parentMessage: parent,
StreamMessageInput(
messageInputController: MessageInputController(
message: Message(parentId: parent!.id),
),
),
],
),
@@ -8,7 +8,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// the SDK supports easily.
///
/// Replacing the built-in message component with your own is done by passing
/// it as a builder function to the [MessageListView] widget.
/// it as a builder function to the [StreamMessageListView] widget.
///
/// The message builder function will get the usual [BuildContext] argument
/// as well as the [Message] object and its position inside the list.
@@ -59,28 +59,42 @@ class MyApp extends StatelessWidget {
}
}
class ChannelListPage extends StatelessWidget {
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
channelWidget: const ChannelPage(),
State<ChannelListPage> createState() => _ChannelListPageState();
}
class _ChannelListPageState extends State<ChannelListPage> {
late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
);
@override
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 {
@@ -92,15 +106,15 @@ class ChannelPage extends StatelessWidget {
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: const ChannelHeader(),
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
child: StreamMessageListView(
messageBuilder: _messageBuilder,
),
),
const MessageInput(),
const StreamMessageInput(),
],
),
);
@@ -110,7 +124,7 @@ class ChannelPage extends StatelessWidget {
BuildContext context,
MessageDetails details,
List<Message> messages,
MessageWidget _,
StreamMessageWidget _,
) {
final message = details.message;
final isCurrentUser =
@@ -58,24 +58,24 @@ class MyApp extends StatelessWidget {
final defaultTheme = StreamChatThemeData.fromTheme(themeData);
final colorTheme = defaultTheme.colorTheme;
final customTheme = defaultTheme.merge(StreamChatThemeData(
channelPreviewTheme: ChannelPreviewThemeData(
avatarTheme: AvatarThemeData(
channelPreviewTheme: StreamChannelPreviewThemeData(
avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(8),
),
),
messageListViewTheme: const MessageListViewThemeData(
messageListViewTheme: const StreamMessageListViewThemeData(
backgroundColor: Colors.grey,
backgroundImage: DecorationImage(
image: AssetImage('assets/background_doodle.png'),
fit: BoxFit.cover,
),
),
otherMessageTheme: MessageThemeData(
otherMessageTheme: StreamMessageThemeData(
messageBackgroundColor: colorTheme.textHighEmphasis,
messageTextStyle: TextStyle(
color: colorTheme.barsBg,
),
avatarTheme: AvatarThemeData(
avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(8),
),
),
@@ -93,28 +93,41 @@ class MyApp extends StatelessWidget {
}
}
class ChannelListPage extends StatelessWidget {
class ChannelListPage extends StatefulWidget {
const ChannelListPage({
Key? key,
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
body: ChannelsBloc(
child: ChannelListView(
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
channelWidget: const ChannelPage(),
State<ChannelListPage> createState() => _ChannelListPageState();
}
class _ChannelListPageState extends State<ChannelListPage> {
late final _listController = StreamChannelListController(
client: StreamChat.of(context).client,
filter: Filter.in_(
'members',
[StreamChat.of(context).currentUser!.id],
),
sort: const [SortOption('last_message_at')],
limit: 20,
);
@override
Widget build(BuildContext context) => Scaffold(
body: StreamChannelListView(
controller: _listController,
onChannelTap: (channel) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: const ChannelPage(),
),
),
);
},
),
),
);
}
);
}
class ChannelPage extends StatelessWidget {
@@ -123,24 +136,21 @@ class ChannelPage extends StatelessWidget {
}) : super(key: key);
@override
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: const ChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
Widget build(BuildContext context) => Scaffold(
appBar: const StreamChannelHeader(),
body: Column(
children: <Widget>[
Expanded(
child: StreamMessageListView(
threadBuilder: (_, parentMessage) => ThreadPage(
parent: parentMessage,
),
),
),
),
const MessageInput(),
],
),
);
}
const StreamMessageInput(),
],
),
);
}
class ThreadPage extends StatelessWidget {
@@ -155,18 +165,20 @@ class ThreadPage extends StatelessWidget {
// ignore: prefer_expression_function_bodies
Widget build(BuildContext context) {
return Scaffold(
appBar: ThreadHeader(
appBar: StreamThreadHeader(
parent: parent!,
),
body: Column(
children: <Widget>[
Expanded(
child: MessageListView(
child: StreamMessageListView(
parentMessage: parent,
),
),
MessageInput(
parentMessage: parent,
StreamMessageInput(
messageInputController: MessageInputController(
message: Message(parentId: parent!.id),
),
),
],
),
@@ -1,17 +1,23 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro attachment_title}
@Deprecated("Use 'StreamAttachmentTitle' instead")
typedef AttachmentTitle = StreamAttachmentTitle;
/// {@template attachment_title}
/// Title for attachments
class AttachmentTitle extends StatelessWidget {
/// {@endtemplate}
class StreamAttachmentTitle extends StatelessWidget {
/// Supply attachment and theme for constructing title
const AttachmentTitle({
const StreamAttachmentTitle({
Key? key,
required this.attachment,
required this.messageTheme,
}) : super(key: key);
/// Theme to apply to text
final MessageThemeData messageTheme;
final StreamMessageThemeData messageTheme;
/// Attachment data to display
final Attachment attachment;
@@ -9,10 +9,16 @@ typedef InProgressBuilder = Widget Function(BuildContext, int, int);
/// Widget to build on failure
typedef FailedBuilder = Widget Function(BuildContext, String);
/// {@macro attachment_upload_state_builder}
@Deprecated("Use 'StreamAttachmentsUploadStateBuilder' instead")
typedef AttachmentUploadStateBuilder = StreamAttachmentUploadStateBuilder;
/// {@template attachment_upload_state_builder}
/// Widget to display attachment upload state
class AttachmentUploadStateBuilder extends StatelessWidget {
/// Constructor for creating an [AttachmentUploadStateBuilder] widget
const AttachmentUploadStateBuilder({
/// {@endtemplate}
class StreamAttachmentUploadStateBuilder extends StatelessWidget {
/// Constructor for creating an [StreamAttachmentUploadStateBuilder] widget
const StreamAttachmentUploadStateBuilder({
Key? key,
required this.message,
required this.attachment,
@@ -137,7 +143,7 @@ class _PreparingState extends StatelessWidget {
),
Align(
alignment: Alignment.topRight,
child: UploadProgressIndicator(
child: StreamUploadProgressIndicator(
uploaded: 0,
total: double.maxFinite.toInt(),
),
@@ -177,7 +183,7 @@ class _InProgressState extends StatelessWidget {
),
Align(
alignment: Alignment.topRight,
child: UploadProgressIndicator(
child: StreamUploadProgressIndicator(
uploaded: sent,
total: total,
),
@@ -28,10 +28,16 @@ extension AttachmentSourceX on AttachmentSource {
}
}
/// {@macro attachment_widget}
@Deprecated("Use 'StreamAttachmentWidget' instead")
typedef AttachmentWidget = StreamAttachmentWidget;
/// {@template attachment_widget}
/// Abstract class for deriving attachment types
abstract class AttachmentWidget extends StatelessWidget {
/// {@endtemplate}
abstract class StreamAttachmentWidget extends StatelessWidget {
/// Constructor for creating attachment widget
const AttachmentWidget({
const StreamAttachmentWidget({
Key? key,
required this.message,
required this.attachment,
@@ -10,10 +10,16 @@ import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// {@macro file_attachment}
@Deprecated("Use 'StreamFileAttachment' instead")
typedef FileAttachment = StreamFileAttachment;
/// {@template file_attachment}
/// Widget for displaying file attachments
class FileAttachment extends AttachmentWidget {
/// {@endtemplate}
class StreamFileAttachment extends StreamAttachmentWidget {
/// Constructor for creating a widget when attachment is of type 'file'
const FileAttachment({
const StreamFileAttachment({
Key? key,
required Message message,
required Attachment attachment,
@@ -157,7 +163,7 @@ class FileAttachment extends AttachmentWidget {
type: MaterialType.transparency,
shape: _getDefaultShape(context),
child: source.when(
local: () => VideoThumbnailImage(
local: () => StreamVideoThumbnailImage(
fit: BoxFit.cover,
video: attachment.file!.path!,
placeholderBuilder: (_) => const Center(
@@ -168,7 +174,7 @@ class FileAttachment extends AttachmentWidget {
),
),
),
network: () => VideoThumbnailImage(
network: () => StreamVideoThumbnailImage(
fit: BoxFit.cover,
video: attachment.assetUrl!,
placeholderBuilder: (_) => const Center(
@@ -278,7 +284,7 @@ class FileAttachment extends AttachmentWidget {
);
return attachment.uploadState.when(
preparing: () => Text(fileSize(size), style: textStyle),
inProgress: (sent, total) => UploadProgressIndicator(
inProgress: (sent, total) => StreamUploadProgressIndicator(
uploaded: sent,
total: total,
showBackground: false,
@@ -5,10 +5,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro giphy_attachment}
@Deprecated("Use 'StreamGiphyAttachment' instead")
typedef GiphyAttachment = StreamGiphyAttachment;
/// {@template giphy_attachment}
/// Widget for showing a GIF attachment
class GiphyAttachment extends AttachmentWidget {
/// Constructor for creating a [GiphyAttachment] widget
const GiphyAttachment({
/// {@endtemplate}
class StreamGiphyAttachment extends StreamAttachmentWidget {
/// Constructor for creating a [StreamGiphyAttachment] widget
const StreamGiphyAttachment({
Key? key,
required Message message,
required Attachment attachment,
@@ -228,7 +234,7 @@ class GiphyAttachment extends AttachmentWidget {
alignment: Alignment.centerRight,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: VisibleFootnote(),
child: StreamVisibleFootnote(),
),
),
],
@@ -243,7 +249,7 @@ class GiphyAttachment extends AttachmentWidget {
final channel = StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
child: StreamFullScreenMedia(
mediaAttachments: message.attachments,
startIndex: message.attachments.indexOf(attachment),
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/stream_chat_flutter.dart';
/// {@macro image_attachment}
@Deprecated("use 'StreamImageAttachment' instead")
typedef ImageAttachment = StreamImageAttachment;
/// {@template image_attachment}
/// Widget for showing an image attachment
class ImageAttachment extends AttachmentWidget {
/// Constructor for creating a [ImageAttachment] widget
const ImageAttachment({
/// {@endtemplate}
class StreamImageAttachment extends StreamAttachmentWidget {
/// Constructor for creating a [StreamImageAttachment] widget
const StreamImageAttachment({
Key? key,
required Message message,
required Attachment attachment,
@@ -25,8 +31,8 @@ class ImageAttachment extends AttachmentWidget {
size: size,
);
/// [MessageThemeData] for showing image title
final MessageThemeData messageTheme;
/// [StreamMessageThemeData] for showing image title
final StreamMessageThemeData messageTheme;
/// Flag for showing title
final bool showTitle;
@@ -137,7 +143,7 @@ class ImageAttachment extends AttachmentWidget {
StreamChannel.of(context).channel;
return StreamChannel(
channel: channel,
child: FullScreenMedia(
child: StreamFullScreenMedia(
mediaAttachments: message.attachments,
startIndex:
message.attachments.indexOf(attachment),
@@ -155,7 +161,7 @@ class ImageAttachment extends AttachmentWidget {
),
Padding(
padding: const EdgeInsets.all(8),
child: AttachmentUploadStateBuilder(
child: StreamAttachmentUploadStateBuilder(
message: message,
attachment: attachment,
),
@@ -166,7 +172,7 @@ class ImageAttachment extends AttachmentWidget {
if (showTitle && attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: AttachmentTitle(
child: StreamAttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
@@ -2,10 +2,16 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro url_attachment}
@Deprecated("Use 'StreamUrlAttachment' instead")
typedef UrlAttachment = StreamUrlAttachment;
/// {@template url_attachment}
/// Widget to display URL attachment
class UrlAttachment extends StatelessWidget {
/// Constructor for creating a [UrlAttachment]
const UrlAttachment({
/// {@endtemplate}
class StreamUrlAttachment extends StatelessWidget {
/// Constructor for creating a [StreamUrlAttachment]
const StreamUrlAttachment({
Key? key,
required this.urlAttachment,
required this.hostDisplayName,
@@ -26,8 +32,8 @@ class UrlAttachment extends StatelessWidget {
/// Padding for text
final EdgeInsets textPadding;
/// [MessageThemeData] for showing image title
final MessageThemeData messageTheme;
/// [StreamMessageThemeData] for showing image title
final StreamMessageThemeData messageTheme;
/// The function called when tapping on a link
final void Function(String)? onLinkTap;
@@ -4,10 +4,16 @@ import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro video_attachment}
@Deprecated("Use 'StreamVideoAttachment' instead")
typedef VideoAttachment = StreamVideoAttachment;
/// {@template video_attachment}
/// Widget for showing a video attachment
class VideoAttachment extends AttachmentWidget {
/// Constructor for creating a [VideoAttachment] widget
const VideoAttachment({
/// {@endtemplate}
class StreamVideoAttachment extends StreamAttachmentWidget {
/// Constructor for creating a [StreamVideoAttachment] widget
const StreamVideoAttachment({
Key? key,
required Message message,
required Attachment attachment,
@@ -23,8 +29,8 @@ class VideoAttachment extends AttachmentWidget {
size: size,
);
/// [MessageThemeData] for showing title
final MessageThemeData messageTheme;
/// [StreamMessageThemeData] for showing title
final StreamMessageThemeData messageTheme;
/// Callback when show message is tapped
final ShowMessageCallback? onShowMessage;
@@ -43,7 +49,7 @@ class VideoAttachment extends AttachmentWidget {
}
return _buildVideoAttachment(
context,
VideoThumbnailImage(
StreamVideoThumbnailImage(
video: attachment.file!.path!,
height: size?.height,
width: size?.width,
@@ -58,7 +64,7 @@ class VideoAttachment extends AttachmentWidget {
}
return _buildVideoAttachment(
context,
VideoThumbnailImage(
StreamVideoThumbnailImage(
video: attachment.assetUrl!,
height: size?.height,
width: size?.width,
@@ -84,7 +90,7 @@ class VideoAttachment extends AttachmentWidget {
MaterialPageRoute(
builder: (_) => StreamChannel(
channel: channel,
child: FullScreenMedia(
child: StreamFullScreenMedia(
mediaAttachments: message.attachments,
startIndex:
message.attachments.indexOf(attachment),
@@ -111,7 +117,7 @@ class VideoAttachment extends AttachmentWidget {
),
Padding(
padding: const EdgeInsets.all(8),
child: AttachmentUploadStateBuilder(
child: StreamAttachmentUploadStateBuilder(
message: message,
attachment: attachment,
),
@@ -123,7 +129,7 @@ class VideoAttachment extends AttachmentWidget {
if (attachment.title != null)
Material(
color: messageTheme.messageBackgroundColor,
child: AttachmentTitle(
child: StreamAttachmentTitle(
messageTheme: messageTheme,
attachment: attachment,
),
@@ -58,7 +58,7 @@ class AttachmentActionsModal extends StatelessWidget {
/// List of custom actions
final List<AttachmentAction> customActions;
/// Creates a copy of [MessageWidget] with specified attributes overridden.
/// Creates a copy of [StreamMessageWidget] with specified attributes overridden.
AttachmentActionsModal copyWith({
Key? key,
int? currentIndex,
@@ -50,7 +50,7 @@ class StreamBackButton extends StatelessWidget {
Positioned(
top: 7,
right: 7,
child: UnreadIndicator(
child: StreamUnreadIndicator(
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
/// [StreamChatTheme].
/// Modify it to change the widget appearance.
@Deprecated(
"'ChannelAvatar' is deprecated and shouldn't be used. "
"Please use 'StreamChannelAvatar' instead.",
)
class ChannelAvatar extends StatelessWidget {
/// Instantiate a new ChannelImage
const ChannelAvatar({
@@ -147,7 +152,7 @@ class ChannelAvatar extends StatelessWidget {
return BetterStreamBuilder<User>(
stream: streamChat.client.state.currentUserStream.map((it) => it!),
initialData: currentUser,
builder: (context, user) => UserAvatar(
builder: (context, user) => StreamUserAvatar(
borderRadius: borderRadius ?? previewTheme?.borderRadius,
user: user,
constraints: constraints ?? previewTheme?.constraints,
@@ -170,7 +175,7 @@ class ChannelAvatar extends StatelessWidget {
),
),
initialData: member,
builder: (context, member) => UserAvatar(
builder: (context, member) => StreamUserAvatar(
borderRadius: borderRadius ?? previewTheme?.borderRadius,
user: member.user!,
constraints: constraints ?? previewTheme?.constraints,
@@ -183,7 +188,7 @@ class ChannelAvatar extends StatelessWidget {
}
// Group conversation
return GroupAvatar(
return StreamGroupAvatar(
members: otherMembers,
borderRadius: borderRadius ?? previewTheme?.borderRadius,
constraints: constraints ?? previewTheme?.constraints,
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Bottom Sheet with options
@Deprecated("Use 'StreamChannelInfoBottomSheet' instead")
class ChannelBottomSheet extends StatefulWidget {
/// Constructor for creating bottom sheet
const ChannelBottomSheet({Key? key, this.onViewInfoTap}) : super(key: key);
@@ -15,11 +16,12 @@ class ChannelBottomSheet extends StatefulWidget {
_ChannelBottomSheetState createState() => _ChannelBottomSheetState();
}
// ignore: deprecated_member_use_from_same_package
class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
bool _showActions = true;
late StreamChannelState _streamChannelState;
late ChannelPreviewThemeData _channelPreviewThemeData;
late StreamChannelPreviewThemeData _channelPreviewThemeData;
late StreamChatThemeData _streamChatThemeData;
late StreamChatState _streamChatState;
@@ -53,7 +55,8 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ChannelName(
child: StreamChannelName(
channel: channel,
textStyle: _streamChatThemeData.textTheme.headlineBold,
),
),
@@ -62,7 +65,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
height: 5,
),
Center(
child: ChannelInfo(
child: StreamChannelInfo(
showTypingIndicator: false,
channel: _streamChannelState.channel,
textStyle: _channelPreviewThemeData.subtitleStyle,
@@ -74,7 +77,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
if (channel.isDistinct && channel.memberCount == 2)
Column(
children: [
UserAvatar(
StreamUserAvatar(
user: members
.firstWhere(
(e) => e.user?.id != userAsMember.user?.id,
@@ -117,7 +120,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
children: [
UserAvatar(
StreamUserAvatar(
user: members[index].user!,
constraints: const BoxConstraints.tightFor(
height: 64,
@@ -145,7 +148,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
const SizedBox(
height: 24,
),
OptionListTile(
StreamOptionListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.user(
@@ -155,8 +158,10 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
title: context.translations.viewInfoLabel,
onTap: widget.onViewInfoTap,
),
if (!channel.isDistinct)
OptionListTile(
if (!channel.isDistinct &&
channel.ownCapabilities
.contains(PermissionType.leaveChannel))
StreamOptionListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.userRemove(
@@ -174,8 +179,10 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
});
},
),
if (isOwner)
OptionListTile(
if (isOwner &&
channel.ownCapabilities
.contains(PermissionType.deleteChannel))
StreamOptionListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.delete(
@@ -194,7 +201,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
});
},
),
OptionListTile(
StreamOptionListTile(
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: StreamSvgIcon.closeSmall(
@@ -215,7 +222,7 @@ class _ChannelBottomSheetState extends State<ChannelBottomSheet> {
void didChangeDependencies() {
_streamChannelState = StreamChannel.of(context);
_streamChatThemeData = StreamChatTheme.of(context);
_channelPreviewThemeData = ChannelPreviewTheme.of(context);
_channelPreviewThemeData = StreamChannelPreviewTheme.of(context);
_streamChatState = StreamChat.of(context);
super.didChangeDependencies();
}
@@ -4,6 +4,11 @@ import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///{@macro template_name}
@Deprecated("Use 'StreamChannelHeader' instead")
typedef ChannelHeader = StreamChannelHeader;
/// {@template channel_header}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png)
///
@@ -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
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance.
class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
/// {@endtemplate}
class StreamChannelHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Creates a channel header
const ChannelHeader({
const StreamChannelHeader({
Key? key,
this.showBackButton = true,
this.onBackPressed,
@@ -96,16 +103,16 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
final Widget? leading;
/// AppBar actions
/// By default it shows the [ChannelAvatar]
/// By default it shows the [StreamChannelAvatar]
final List<Widget>? actions;
/// The background color for this [ChannelHeader].
/// The background color for this [StreamChannelHeader].
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
final channelHeaderTheme = ChannelHeaderTheme.of(context);
final channelHeaderTheme = StreamChannelHeaderTheme.of(context);
final leadingWidget = leading ??
(showBackButton
@@ -115,7 +122,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
)
: const SizedBox());
return ConnectionStatusBuilder(
return StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
var statusString = '';
var showStatus = true;
@@ -135,7 +142,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
final theme = Theme.of(context);
return InfoTile(
return StreamInfoTile(
showMessage: showConnectionStateTile && showStatus,
message: statusString,
child: AppBar(
@@ -152,7 +159,8 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
Padding(
padding: const EdgeInsets.only(right: 10),
child: Center(
child: ChannelAvatar(
child: StreamChannelAvatar(
channel: channel,
borderRadius:
channelHeaderTheme.avatarTheme?.borderRadius,
constraints:
@@ -172,12 +180,13 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
title ??
ChannelName(
StreamChannelName(
channel: channel,
textStyle: channelHeaderTheme.titleStyle,
),
const SizedBox(height: 2),
subtitle ??
ChannelInfo(
StreamChannelInfo(
showTypingIndicator: showTypingIndicator,
channel: channel,
textStyle: channelHeaderTheme.subtitleStyle,
@@ -3,10 +3,16 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro channel_info}
@Deprecated("Use 'StreamChannelInfo' instead")
typedef ChannelInfo = StreamChannelInfo;
/// {@template channel_info}
/// Widget which shows channel info
class ChannelInfo extends StatelessWidget {
/// Constructor which creates a [ChannelInfo] widget
const ChannelInfo({
/// {@endtemplate}
class StreamChannelInfo extends StatelessWidget {
/// Constructor which creates a [StreamChannelInfo] widget
const StreamChannelInfo({
Key? key,
required this.channel,
this.textStyle,
@@ -32,7 +38,7 @@ class ChannelInfo extends StatelessWidget {
return BetterStreamBuilder<List<Member>>(
stream: channel.state!.membersStream,
initialData: channel.state!.members,
builder: (context, data) => ConnectionStatusBuilder(
builder: (context, data) => StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
switch (status) {
case ConnectionStatus.connected:
@@ -60,12 +66,13 @@ class ChannelInfo extends StatelessWidget {
var text = context.translations.membersCountText(memberCount);
final onlineCount =
members?.where((m) => m.user?.online == true).length ?? 0;
if (onlineCount > 0) {
if (channel.ownCapabilities.contains(PermissionType.connectEvents) &&
onlineCount > 0) {
text += ', ${context.translations.watchersCountText(onlineCount)}';
}
alternativeWidget = Text(
text,
style: ChannelHeaderTheme.of(context).subtitleStyle,
style: StreamChannelHeaderTheme.of(context).subtitleStyle,
);
} else {
final userId = StreamChat.of(context).currentUser?.id;
@@ -93,11 +100,12 @@ class ChannelInfo extends StatelessWidget {
return alternativeWidget ?? const Offstage();
}
return TypingIndicator(
parentId: parentId,
alignment: Alignment.center,
alternativeWidget: alternativeWidget,
style: textStyle,
return Align(
child: StreamTypingIndicator(
parentId: parentId,
style: textStyle,
alternativeWidget: alternativeWidget,
),
);
}
@@ -11,7 +11,11 @@ typedef TitleBuilder = Widget Function(
StreamChatClient client,
);
///
/// {@macro channel_list_header}
@Deprecated("Use 'StreamChannelListHeader' instead")
typedef ChannelListHeader = StreamChannelListHeader;
/// {@template channel_list_header}
/// It shows the current [StreamChatClient] status.
///
/// ```dart
@@ -43,11 +47,13 @@ typedef TitleBuilder = Widget Function(
/// if you don't have it in the widget tree.
///
/// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme] and on its [ChannelListHeaderThemeData] property.
/// [StreamChatTheme] and on its [StreamChannelListHeaderThemeData] property.
/// Modify it to change the widget appearance.
class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// {@endtemplate}
class StreamChannelListHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Instantiates a ChannelListHeader
const ChannelListHeader({
const StreamChannelListHeader({
Key? key,
this.client,
this.titleBuilder,
@@ -91,14 +97,14 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
/// By default it shows the new chat button
final List<Widget>? actions;
/// The background color for this [ChannelListHeader].
/// The background color for this [StreamChannelListHeader].
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
final _client = client ?? StreamChat.of(context).client;
final user = _client.state.currentUser;
return ConnectionStatusBuilder(
return StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
var statusString = '';
var showStatus = true;
@@ -117,9 +123,10 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
}
final chatThemeData = StreamChatTheme.of(context);
final channelListHeaderThemeData = ChannelListHeaderTheme.of(context);
final channelListHeaderThemeData =
StreamChannelListHeaderTheme.of(context);
final theme = Theme.of(context);
return InfoTile(
return StreamInfoTile(
showMessage: showConnectionStateTile && showStatus,
message: statusString,
child: AppBar(
@@ -135,7 +142,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
leading: leading ??
Center(
child: user != null
? UserAvatar(
? StreamUserAvatar(
user: user,
showOnlineStatus: false,
onTap: onUserAvatarTap ??
@@ -156,7 +163,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
[
StreamNeumorphicButton(
child: IconButton(
icon: ConnectionStatusBuilder(
icon: StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
Color? color;
switch (status) {
@@ -234,10 +241,11 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
const SizedBox(width: 10),
Text(
context.translations.searchingForNetworkText,
style: ChannelListHeaderTheme.of(context).titleStyle?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
style:
StreamChannelListHeaderTheme.of(context).titleStyle?.copyWith(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
);
@@ -247,7 +255,7 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
StreamChatClient client,
) {
final chatThemeData = StreamChatTheme.of(context);
final channelListHeaderTheme = ChannelListHeaderTheme.of(context);
final channelListHeaderTheme = StreamChannelListHeaderTheme.of(context);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@@ -1,8 +1,6 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
@@ -12,7 +10,7 @@ typedef ChannelTapCallback = void Function(Channel, Widget?);
/// Callback called when tapping on a channel
typedef ChannelInfoCallback = void Function(Channel);
/// Builder used to create a custom [ChannelPreview] from a [Channel]
/// Builder used to create a custom [StreamChannelPreview] from a [Channel]
typedef ChannelPreviewBuilder = Widget Function(BuildContext, Channel);
/// Callback for when 'View Info' is tapped
@@ -53,6 +51,7 @@ typedef ViewInfoCallback = void Function(Channel);
/// The widget components render the ui based on the first ancestor of
/// type [StreamChatTheme].
/// Modify it to change the widget appearance.
@Deprecated("Use 'StreamChannelListView' instead")
class ChannelListView extends StatefulWidget {
/// Instantiate a new ChannelListView
ChannelListView({
@@ -245,7 +244,8 @@ class _ChannelListViewState extends State<ChannelListView> {
child: child,
);
final backgroundColor = ChannelListViewTheme.of(context).backgroundColor;
final backgroundColor =
StreamChannelListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
@@ -547,7 +547,8 @@ class _ChannelListViewState extends State<ChannelListView> {
context: context,
builder: (context) => StreamChannel(
channel: channel,
child: ChannelBottomSheet(
child: StreamChannelInfoBottomSheet(
channel: channel,
onViewInfoTap: () {
widget.onViewInfoTap?.call(channel);
},
@@ -556,14 +557,8 @@ class _ChannelListViewState extends State<ChannelListView> {
);
},
),
if ([
'admin',
'owner',
].contains(channel.state!.members
.firstWhereOrNull(
(m) => m.userId == channel.client.state.currentUser?.id,
)
?.role))
if (channel.ownCapabilities
.contains(PermissionType.deleteChannel))
IconSlideAction(
color: backgroundColor,
iconWidget: StreamSvgIcon.delete(
@@ -642,7 +637,7 @@ class _ChannelListViewState extends State<ChannelListView> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ChannelAvatar(
StreamChannelAvatar(
channel: channel,
borderRadius: BorderRadius.circular(32),
selected: selected,
@@ -657,8 +652,9 @@ class _ChannelListViewState extends State<ChannelListView> {
padding: const EdgeInsets.symmetric(horizontal: 8),
child: StreamChannel(
channel: channel,
child: const ChannelName(
textStyle: TextStyle(
child: StreamChannelName(
channel: channel,
textStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
),
@@ -6,6 +6,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
///
/// The widget uses a [StreamBuilder] to render the channel information
/// image as soon as it updates.
@Deprecated("Use 'StreamChannelName' instead")
class ChannelName extends StatelessWidget {
/// Instantiate a new ChannelName
const ChannelName({
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@template channel_preview}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_preview_paint.png)
///
@@ -13,11 +14,13 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// image as soon as it updates.
///
/// Usually you don't use this widget as it's the default channel preview
/// used by [ChannelListView].
/// used by [StreamChannelListView].
///
/// The widget renders the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance.
/// {@endtemplate}
@Deprecated("Use 'StreamChannelListTile' instead")
class ChannelPreview extends StatelessWidget {
/// Constructor for creating [ChannelPreview]
const ChannelPreview({
@@ -52,7 +55,7 @@ class ChannelPreview extends StatelessWidget {
final Widget? subtitle;
/// Widget rendering the leading element, by default
/// it shows the [ChannelAvatar]
/// it shows the [StreamChannelAvatar]
final Widget? leading;
/// Widget rendering the trailing element,
@@ -60,7 +63,7 @@ class ChannelPreview extends StatelessWidget {
final Widget? trailing;
/// Widget rendering the sending indicator,
/// by default it uses the [SendingIndicator] widget
/// by default it uses the [StreamSendingIndicator] widget
final Widget? sendingIndicator;
@override
@@ -80,13 +83,18 @@ class ChannelPreview extends StatelessWidget {
),
onTap: () => onTap?.call(channel),
onLongPress: () => onLongPress?.call(channel),
leading: leading ?? ChannelAvatar(onTap: onImageTap),
leading: leading ??
StreamChannelAvatar(
channel: channel,
onTap: onImageTap,
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: title ??
ChannelName(
StreamChannelName(
channel: channel,
textStyle: channelPreviewTheme.titleStyle,
),
),
@@ -100,7 +108,7 @@ class ChannelPreview extends StatelessWidget {
e.user!.id == channel.client.state.currentUser?.id)) {
return const SizedBox();
}
return UnreadIndicator(
return StreamUnreadIndicator(
cid: channel.cid,
);
},
@@ -136,7 +144,7 @@ class ChannelPreview extends StatelessWidget {
)));
final isMessageRead = readList.length >=
(channel.memberCount ?? 0) - 1;
return SendingIndicator(
return StreamSendingIndicator(
message: lastMessage!,
size: channelPreviewTheme.indicatorIconSize,
isMessageRead: isMessageRead,
@@ -204,7 +212,7 @@ class ChannelPreview extends StatelessWidget {
],
);
}
return TypingIndicator(
return StreamTypingIndicator(
channel: channel,
alternativeWidget: _buildLastMessage(context),
style: channelPreviewTheme.subtitleStyle,
@@ -2,10 +2,17 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro commands_overlay}
@Deprecated("Use 'StreamCommandsOverlay' instead")
typedef CommandsOverlay = StreamCommandsOverlay;
/// {@template commands_overlay}
/// Overlay for displaying commands that can be used
class CommandsOverlay extends StatelessWidget {
/// Constructor for creating a [CommandsOverlay]
const CommandsOverlay({
/// to interact with the channel.
/// {@endtemplate}
class StreamCommandsOverlay extends StatelessWidget {
/// Constructor for creating a [StreamCommandsOverlay]
const StreamCommandsOverlay({
required this.text,
required this.onCommandResult,
required this.size,
@@ -1,14 +1,20 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro connection_status_builder}
@Deprecated("Use 'StreamConnectionStatusBuilder' instead")
typedef ConnectionStatusBuilder = StreamConnectionStatusBuilder;
/// {@template connection_status_builder}
/// Widget that builds itself based on the latest snapshot of interaction with
/// a [Stream] of type [ConnectionStatus].
///
/// The widget will use the closest [StreamChatClient.wsConnectionStatusStream]
/// in case no stream is provided.
class ConnectionStatusBuilder extends StatelessWidget {
/// {@endtemplate}
class StreamConnectionStatusBuilder extends StatelessWidget {
/// Creates a new ConnectionStatusBuilder
const ConnectionStatusBuilder({
const StreamConnectionStatusBuilder({
Key? key,
required this.statusBuilder,
this.connectionStatusStream,
@@ -3,10 +3,16 @@ import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro date_divider}
@Deprecated("Use 'StreamDateDivider' instead")
typedef DateDivider = StreamDateDivider;
/// {@template date_divider}
/// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget {
/// Constructor for creating a [DateDivider]
const DateDivider({
/// {@endtemplate}
class StreamDateDivider extends StatelessWidget {
/// Constructor for creating a [StreamDateDivider]
const StreamDateDivider({
Key? key,
required this.dateTime,
this.uppercase = false,
@@ -3,10 +3,16 @@ import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart';
/// Widget to display deleted message
class DeletedMessage extends StatelessWidget {
/// Constructor to create [DeletedMessage]
const DeletedMessage({
/// {@macro deleted_message}
@Deprecated("Use 'StreamDeletedMessage' instead")
typedef DeletedMessage = StreamDeletedMessage;
/// {@template deleted_message}
/// Widget to display deleted message.
/// {@endtemplate}
class StreamDeletedMessage extends StatelessWidget {
/// Constructor to create [StreamDeletedMessage]
const StreamDeletedMessage({
Key? key,
required this.messageTheme,
this.borderRadiusGeometry,
@@ -16,7 +22,7 @@ class DeletedMessage extends StatelessWidget {
}) : super(key: key);
/// The theme of the message
final MessageThemeData messageTheme;
final StreamMessageThemeData messageTheme;
/// The border radius of the message text
final BorderRadiusGeometry? borderRadiusGeometry;
@@ -4,10 +4,16 @@ import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:substring_highlight/substring_highlight.dart';
/// {@macro emoji_overlay}
@Deprecated("Use 'StreamEmojiOverlay' instead")
typedef EmojiOverlay = StreamEmojiOverlay;
/// {@template emoji_overlay}
/// Overlay for displaying emoji that can be used
class EmojiOverlay extends StatelessWidget {
/// Constructor for creating a [EmojiOverlay]
const EmojiOverlay({
/// {@endtemplate}
class StreamEmojiOverlay extends StatelessWidget {
/// Constructor for creating a [StreamEmojiOverlay]
const StreamEmojiOverlay({
required this.query,
required this.onEmojiResult,
required this.size,
@@ -46,7 +46,7 @@ extension IterableX<T> on Iterable<T> {
extension PlatformFileX on PlatformFile {
/// Converts the [PlatformFile] into [AttachmentFile]
AttachmentFile get toAttachmentFile => AttachmentFile(
//ignore: avoid_redundant_argument_values
// ignore: avoid_redundant_argument_values
path: kIsWeb ? null : path,
name: name,
bytes: bytes,
@@ -225,3 +225,50 @@ extension UserListX on List<User> {
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
typedef ShowMessageCallback = void Function(Message message, Channel channel);
/// {@macro full_screen_media}
@Deprecated("Use 'StreamFullScreenMedia' instead")
typedef FullScreenMedia = StreamFullScreenMedia;
/// {@template full_screen_media}
/// A full screen image widget
class FullScreenMedia extends StatefulWidget {
/// {@endtemplate}
class StreamFullScreenMedia extends StatefulWidget {
/// Instantiate a new FullScreenImage
const FullScreenMedia({
const StreamFullScreenMedia({
Key? key,
required this.mediaAttachments,
required this.message,
@@ -60,10 +66,10 @@ class FullScreenMedia extends StatefulWidget {
final bool autoplayVideos;
@override
_FullScreenMediaState createState() => _FullScreenMediaState();
_StreamFullScreenMediaState createState() => _StreamFullScreenMediaState();
}
class _FullScreenMediaState extends State<FullScreenMedia>
class _StreamFullScreenMediaState extends State<StreamFullScreenMedia>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late final PageController _pageController;
@@ -172,7 +178,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
),
backgroundDecoration: BoxDecoration(
color: ColorTween(
begin: ChannelHeaderTheme.of(context).color,
begin: StreamChannelHeaderTheme.of(context).color,
end: Colors.black,
).lerp(_curvedAnimation.value),
),
@@ -221,7 +227,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
builder: (context, value, child) => Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
GalleryHeader(
StreamGalleryHeader(
userName: widget.userName,
sentAt: context.translations.sentAtText(
date: widget.message.createdAt,
@@ -242,7 +248,7 @@ class _FullScreenMediaState extends State<FullScreenMedia>
widget.attachmentActionsModalBuilder,
),
if (!widget.message.isEphemeral)
GalleryFooter(
StreamGalleryFooter(
currentPage: value,
totalPages: widget.mediaAttachments.length,
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/stream_chat_flutter.dart';
/// {@macro gallery_footer}
@Deprecated("Use 'StreamGalleryFooter' instead")
typedef GalleryFooter = StreamGalleryFooter;
/// {@template gallery_footer}
/// Footer widget for media display
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
/// {@endtemplate}
class StreamGalleryFooter extends StatefulWidget
implements PreferredSizeWidget {
/// Creates a channel header
const GalleryFooter({
const StreamGalleryFooter({
Key? key,
required this.message,
this.onBackPressed,
@@ -51,22 +58,22 @@ class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
/// Callback when media is selected
final ValueChanged<int>? mediaSelectedCallBack;
/// The background color of this [GalleryFooter].
/// The background color of this [StreamGalleryFooter].
final Color? backgroundColor;
@override
_GalleryFooterState createState() => _GalleryFooterState();
_StreamGalleryFooterState createState() => _StreamGalleryFooterState();
@override
final Size preferredSize;
}
class _GalleryFooterState extends State<GalleryFooter> {
class _StreamGalleryFooterState extends State<StreamGalleryFooter> {
@override
Widget build(BuildContext context) {
const showShareButton = !kIsWeb;
final mediaQueryData = MediaQuery.of(context);
final galleryFooterThemeData = GalleryFooterTheme.of(context);
final galleryFooterThemeData = StreamGalleryFooterTheme.of(context);
return SizedBox.fromSize(
size: Size(
mediaQueryData.size.width,
@@ -149,7 +156,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
void _showPhotosModal(context) {
final chatThemeData = StreamChatTheme.of(context);
final galleryFooterThemeData = GalleryFooterTheme.of(context);
final galleryFooterThemeData = StreamGalleryFooterTheme.of(context);
showModalBottomSheet(
context: context,
barrierColor: galleryFooterThemeData.bottomSheetBarrierColor,
@@ -222,7 +229,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
onTap: () => widget.mediaSelectedCallBack!(index),
child: FittedBox(
fit: BoxFit.cover,
child: VideoThumbnailImage(
child: StreamVideoThumbnailImage(
video: (attachment.file?.path ??
attachment.assetUrl)!,
),
@@ -264,7 +271,7 @@ class _GalleryFooterState extends State<GalleryFooter> {
),
],
),
child: UserAvatar(
child: StreamUserAvatar(
user: widget.message.user!,
constraints:
BoxConstraints.tight(const Size(24, 24)),
@@ -15,10 +15,17 @@ typedef AttachmentActionsBuilder = Widget Function(
AttachmentActionsModal defaultActionsModal,
);
/// {@macro gallery_header}
@Deprecated("Use 'StreamGalleryHeader' instead")
typedef GalleryHeader = StreamGalleryHeader;
/// {@template gallery_header}
/// Header/AppBar widget for media display screen
class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
/// {@endtemplate}
class StreamGalleryHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Creates a channel header
const GalleryHeader({
const StreamGalleryHeader({
Key? key,
required this.message,
this.currentIndex = 0,
@@ -62,7 +69,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
/// Stores the current index of media shown
final int currentIndex;
/// The background color of this [GalleryHeader].
/// The background color of this [StreamGalleryHeader].
final Color? backgroundColor;
/// Widget builder for attachment actions modal
@@ -72,7 +79,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
final galleryHeaderThemeData = StreamGalleryHeaderTheme.of(context);
final theme = Theme.of(context);
return AppBar(
toolbarTextStyle: theme.textTheme.bodyText2,
@@ -3,10 +3,16 @@ import 'dart:ui' as ui;
import 'package:flutter/material.dart';
/// {@macro gradient_avatar}
@Deprecated("Use 'StreamGradientAvatar' instead")
typedef GradientAvatar = StreamGradientAvatar;
/// {@template gradient_avatar}
/// Fallback user avatar with a polygon gradient overlayed with text
class GradientAvatar extends StatefulWidget {
/// Constructor for [GradientAvatar]
const GradientAvatar({
/// {@endtemplate}
class StreamGradientAvatar extends StatefulWidget {
/// Constructor for [StreamGradientAvatar]
const StreamGradientAvatar({
Key? key,
required this.name,
required this.userId,
@@ -19,10 +25,10 @@ class GradientAvatar extends StatefulWidget {
final String userId;
@override
_GradientAvatarState createState() => _GradientAvatarState();
_StreamGradientAvatarState createState() => _StreamGradientAvatarState();
}
class _GradientAvatarState extends State<GradientAvatar> {
class _StreamGradientAvatarState extends State<StreamGradientAvatar> {
@override
Widget build(BuildContext context) => Center(
child: RepaintBoundary(
@@ -1,11 +1,18 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro group_avatar}
@Deprecated("Use 'StreamGroupAvatar' instead")
typedef GroupAvatar = StreamGroupAvatar;
/// {@template group_avatar}
/// Widget for constructing a group of images
class GroupAvatar extends StatelessWidget {
/// Constructor for creating a [GroupAvatar]
const GroupAvatar({
/// {@endtemplate}
class StreamGroupAvatar extends StatelessWidget {
/// Constructor for creating a [StreamGroupAvatar]
const StreamGroupAvatar({
Key? key,
this.channel,
required this.members,
this.constraints,
this.onTap,
@@ -15,6 +22,9 @@ class GroupAvatar extends StatelessWidget {
this.selectionThickness = 4,
}) : super(key: key);
/// The channel of the avatar
final Channel? channel;
/// List of images to display
final List<Member> members;
@@ -38,7 +48,7 @@ class GroupAvatar extends StatelessWidget {
@override
Widget build(BuildContext context) {
final channel = StreamChannel.of(context).channel;
final channel = this.channel ?? StreamChannel.of(context).channel;
assert(channel.state != null, 'Channel ${channel.id} is not initialized');
@@ -80,7 +90,7 @@ class GroupAvatar extends StatelessWidget {
),
),
initialData: member,
builder: (context, member) => UserAvatar(
builder: (context, member) => StreamUserAvatar(
showOnlineStatus: false,
user: member.user!,
borderRadius: BorderRadius.zero,
@@ -118,7 +128,8 @@ class GroupAvatar extends StatelessWidget {
),
),
initialData: member,
builder: (context, member) => UserAvatar(
builder: (context, member) =>
StreamUserAvatar(
showOnlineStatus: false,
user: member.user!,
borderRadius: BorderRadius.zero,
@@ -1,10 +1,16 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro image_group}
@Deprecated("Use 'StreamImageGroup' instead")
typedef ImageGroup = StreamImageGroup;
/// {@template image_group}
/// Widget for constructing a group of images in message
class ImageGroup extends StatelessWidget {
/// Constructor for creating [ImageGroup] widget
const ImageGroup({
/// {@endtemplate}
class StreamImageGroup extends StatelessWidget {
/// Constructor for creating [StreamImageGroup] widget
const StreamImageGroup({
Key? key,
required this.images,
required this.message,
@@ -27,8 +33,8 @@ class ImageGroup extends StatelessWidget {
/// Message which images are attached to
final Message message;
/// [MessageThemeData] to apply to message
final MessageThemeData messageTheme;
/// [StreamMessageThemeData] to apply to message
final StreamMessageThemeData messageTheme;
/// Size of iamges
final Size size;
@@ -129,7 +135,7 @@ class ImageGroup extends StatelessWidget {
MaterialPageRoute(
builder: (context) => StreamChannel(
channel: channel,
child: FullScreenMedia(
child: StreamFullScreenMedia(
mediaAttachments: images,
startIndex: index,
userName: message.user?.name,
@@ -142,7 +148,7 @@ class ImageGroup extends StatelessWidget {
if (res != null) onReturnAction?.call(res);
}
Widget _buildImage(BuildContext context, int index) => ImageAttachment(
Widget _buildImage(BuildContext context, int index) => StreamImageAttachment(
attachment: images[index],
size: size,
message: message,
@@ -2,10 +2,16 @@ import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro info_tile}
@Deprecated("Use 'StreamInfoTile' instead")
typedef InfoTile = StreamInfoTile;
/// {@template info_tile}
/// Tile to display a message, used in stream chat to display connection status
class InfoTile extends StatelessWidget {
/// Constructor for creating an [InfoTile] widget
const InfoTile({
/// {@endtemplate}
class StreamInfoTile extends StatelessWidget {
/// Constructor for creating an [StreamInfoTile] widget
const StreamInfoTile({
Key? key,
required this.message,
required this.child,
@@ -1,6 +1,6 @@
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/connection_status_builder.dart';
import 'package:stream_chat_flutter/src/message_input.dart';
import 'package:stream_chat_flutter/src/message_input/message_input.dart';
import 'package:stream_chat_flutter/src/message_list_view.dart';
import 'package:stream_chat_flutter/src/message_search_list_view.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'
@@ -59,7 +59,7 @@ abstract class Translations {
/// The error shown when loading messages fails
String get loadingMessagesError;
/// The text for showing the result count in [MessageSearchListView]
/// The text for showing the result count in [StreamMessageSearchListView]
String resultCountText(int count);
/// The text for showing the message is deleted
@@ -74,44 +74,47 @@ abstract class Translations {
/// The text for showing there are no chats
String get emptyChatMessagesText;
/// The text for showing the thread separator in case [MessageListView]
/// The text for showing the thread separator in case [StreamMessageListView]
/// contains a parent message
String threadSeparatorText(int replyCount);
/// The label for "connected" in [ConnectionStatusBuilder]
/// The label for "connected" in [StreamConnectionStatusBuilder]
String get connectedLabel;
/// The label for "disconnected" in [ConnectionStatusBuilder]
/// The label for "disconnected" in [StreamConnectionStatusBuilder]
String get disconnectedLabel;
/// The label for "reconnecting" in [ConnectionStatusBuilder]
/// The label for "reconnecting" in [StreamConnectionStatusBuilder]
String get reconnectingLabel;
/// The label for also send as direct message "checkbox"" in [MessageInput]
/// The label for also send as direct message "checkbox"" in [StreamMessageInput]
String get alsoSendAsDirectMessageLabel;
/// The label for search Gif
String get searchGifLabel;
/// The label for the MessageInput hint when permission denied on sendMessage
String get sendMessagePermissionError;
/// The label for add a comment or send in case of
/// attachments inside [MessageInput]
/// attachments inside [StreamMessageInput]
String get addACommentOrSendLabel;
/// The label for write a message in [MessageInput]
/// The label for write a message in [StreamMessageInput]
String get writeAMessageLabel;
/// The label for slow mode enabled in [MessageInput]
/// The label for slow mode enabled in [StreamMessageInput]
String get slowModeOnLabel;
/// The label for instant commands in [MessageInput]
/// The label for instant commands in [StreamMessageInput]
String get instantCommandsLabel;
/// The error shown in case the fi"le is too large even after compression
/// while uploading via [MessageInput]
/// while uploading via [StreamMessageInput]
String fileTooLargeAfterCompressionError(double limitInMB);
/// The error shown in case the file is too large
/// while uploading via [MessageInput]
/// while uploading via [StreamMessageInput]
String fileTooLargeError(double limitInMB);
/// The text for showing the query while searching for emojis
@@ -141,6 +144,12 @@ abstract class Translations {
/// The label for "OK"
String get okLabel;
/// The label for a link disabled error
String get linkDisabledError;
/// The additional info on a link disabled error
String get linkDisabledDetails;
/// The label for "add more files"
String get addMoreFilesLabel;
@@ -377,6 +386,10 @@ class DefaultTranslations implements Translations {
return 'Pinned by ${pinnedBy.name}';
}
@override
String get sendMessagePermissionError =>
'You don\'t have permission to send messages';
@override
String get emptyMessagesText => 'There are no messages currently';
@@ -685,4 +698,11 @@ class DefaultTranslations implements Translations {
@override
String attachmentLimitExceedError(int limit) => """
Attachment limit exceeded: it's not possible to add more than $limit attachments""";
@override
String get linkDisabledDetails =>
'Sending links is not allowed in this conversation.';
@override
String get linkDisabledError => 'Links are disabled';
}
@@ -7,21 +7,16 @@ import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
extension on Duration {
String format() {
final s = '$this'.split('.')[0].padLeft(8, '0');
if (s.startsWith('00:')) {
return s.replaceFirst('00:', '');
}
return s;
}
}
/// {@macro media_list_view}
@Deprecated("Use 'StreamMediaListView' instead")
typedef MediaListView = StreamMediaListView;
/// {@template media_list_view}
/// Constructs a list of media
class MediaListView extends StatefulWidget {
/// Constructor for creating a [MediaListView] widget
const MediaListView({
/// {@endtemplate}
class StreamMediaListView extends StatefulWidget {
/// Constructor for creating a [StreamMediaListView] widget
const StreamMediaListView({
Key? key,
this.selectedIds = const [],
this.onSelect,
@@ -34,10 +29,10 @@ class MediaListView extends StatefulWidget {
final void Function(AssetEntity media)? onSelect;
@override
_MediaListViewState createState() => _MediaListViewState();
_StreamMediaListViewState createState() => _StreamMediaListViewState();
}
class _MediaListViewState extends State<MediaListView> {
class _StreamMediaListViewState extends State<StreamMediaListView> {
final _media = <AssetEntity>[];
final ScrollController _scrollController = ScrollController();
int _currentPage = 0;
@@ -222,3 +217,14 @@ class MediaThumbnailProvider extends ImageProvider<MediaThumbnailProvider> {
@override
String toString() => '$runtimeType("${media.id}")';
}
extension on Duration {
String format() {
final s = '$this'.split('.')[0].padLeft(8, '0');
if (s.startsWith('00:')) {
return s.replaceFirst('00:', '');
}
return s;
}
}
@@ -1,101 +0,0 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// This widget is used for showing user tiles for mentions
/// Use [title], [subtitle], [leading], [trailing] for
/// substituting widgets in respective positions
@Deprecated('Use `UserMentionTile` instead. Will be removed in future release')
class MentionTile extends StatelessWidget {
/// Constructor for creating a [MentionTile] widget
const MentionTile(
this.member, {
Key? key,
this.title,
this.subtitle,
this.leading,
this.trailing,
}) : super(key: key);
/// Member to display in the tile
final Member member;
/// Widget to display as title
final Widget? title;
/// Widget to display below [title]
final Widget? subtitle;
/// Widget at the start of the tile
final Widget? leading;
/// Widget at the end of tile
final Widget? trailing;
@override
Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context);
return SizedBox(
height: 56,
child: Row(
children: [
const SizedBox(
width: 16,
),
leading ??
UserAvatar(
constraints: BoxConstraints.tight(
const Size(
40,
40,
),
),
user: member.user!,
),
const SizedBox(
width: 8,
),
Expanded(
child: Align(
alignment: Alignment.centerLeft,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title ??
Text(
member.user!.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: chatThemeData.textTheme.bodyBold,
),
const SizedBox(
height: 2,
),
subtitle ??
Text(
'@${member.userId}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: chatThemeData.textTheme.footnoteBold.copyWith(
color: chatThemeData.colorTheme.textLowEmphasis,
),
),
],
),
),
),
trailing ??
Padding(
padding: const EdgeInsets.only(
right: 18,
left: 8,
),
child: StreamSvgIcon.mentions(
color: chatThemeData.colorTheme.accentPrimary,
),
),
],
),
);
}
}
@@ -1,10 +1,16 @@
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro message_action}
@Deprecated("Use 'StreamMessageActions' instead")
typedef MessageAction = StreamMessageAction;
/// {@template message_action}
/// Class describing a message action
class MessageAction {
/// returns a new instance of a [MessageAction]
MessageAction({
/// {@endtemplate}
class StreamMessageAction {
/// returns a new instance of a [StreamMessageAction]
StreamMessageAction({
this.leading,
this.title,
this.onTap,
@@ -3,25 +3,31 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro message_actions_modal}
@Deprecated("Use 'StreamMessageActionsModal' instead")
typedef MessageActionsModal = StreamMessageActionsModal;
/// {@template message_actions_modal}
/// Constructs a modal with actions for a message
class MessageActionsModal extends StatefulWidget {
/// Constructor for creating a [MessageActionsModal] widget
const MessageActionsModal({
/// {@endtemplate}
class StreamMessageActionsModal extends StatefulWidget {
/// Constructor for creating a [StreamMessageActionsModal] widget
const StreamMessageActionsModal({
Key? key,
required this.message,
required this.messageWidget,
required this.messageTheme,
this.showReactions = true,
this.showDeleteMessage = true,
this.showEditMessage = true,
this.showReactions,
this.showDeleteMessage,
this.showEditMessage,
this.onReplyTap,
this.onThreadReplyTap,
this.showCopyMessage = true,
this.showReplyMessage = true,
this.showResendMessage = true,
this.showThreadReplyMessage = true,
this.showFlagButton = true,
this.showPinButton = true,
this.showThreadReplyMessage,
this.showFlagButton,
this.showPinButton,
this.editMessageInputBuilder,
this.reverse = false,
this.customActions = const [],
@@ -43,51 +49,54 @@ class MessageActionsModal extends StatefulWidget {
/// Message in focus for actions
final Message message;
/// [MessageThemeData] for message
final MessageThemeData messageTheme;
/// [StreamMessageThemeData] for message
final StreamMessageThemeData messageTheme;
/// Flag for showing reactions
final bool showReactions;
final bool? showReactions;
/// Callback when copy is tapped
final OnMessageTap? onCopyTap;
/// Callback when delete is tapped
final bool showDeleteMessage;
final bool? showDeleteMessage;
/// Flag for showing copy action
final bool showCopyMessage;
/// Flag for showing edit action
final bool showEditMessage;
final bool? showEditMessage;
/// Flag for showing resend action
final bool showResendMessage;
/// Flag for showing reply action
final bool showReplyMessage;
final bool? showReplyMessage;
/// Flag for showing thread reply action
final bool showThreadReplyMessage;
final bool? showThreadReplyMessage;
/// Flag for showing flag action
final bool showFlagButton;
final bool? showFlagButton;
/// Flag for showing pin action
final bool showPinButton;
final bool? showPinButton;
/// Flag for reversing message
final bool reverse;
/// List of custom actions
final List<MessageAction> customActions;
final List<StreamMessageAction> customActions;
@override
_MessageActionsModalState createState() => _MessageActionsModalState();
_StreamMessageActionsModalState createState() =>
_StreamMessageActionsModalState();
}
class _MessageActionsModalState extends State<MessageActionsModal> {
class _StreamMessageActionsModalState extends State<StreamMessageActionsModal> {
bool _showActions = true;
late List<String> _userPermissions;
late bool _isMyMessage;
@override
Widget build(BuildContext context) => _showMessageOptionsModal();
@@ -122,6 +131,19 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
final shiftFactor =
numberOfReactions < 5 ? (5 - numberOfReactions) * 0.1 : 0.0;
final hasEditPermission = _userPermissions.contains(
PermissionType.updateAnyMessage,
) ||
_userPermissions.contains(PermissionType.updateOwnMessage);
final hasDeletePermission = _userPermissions.contains(
PermissionType.deleteAnyMessage,
) ||
_userPermissions.contains(PermissionType.deleteOwnMessage);
final hasReactionPermission =
_userPermissions.contains(PermissionType.sendReaction);
final child = Center(
child: SingleChildScrollView(
child: Padding(
@@ -131,7 +153,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
? CrossAxisAlignment.end
: CrossAxisAlignment.start,
children: <Widget>[
if (widget.showReactions &&
if ((widget.showReactions ?? hasReactionPermission) &&
(widget.message.status == MessageSendingStatus.sent))
Align(
alignment: Alignment(
@@ -144,7 +166,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
: -(1.2 - divFactor)),
0,
),
child: ReactionPicker(
child: StreamReactionPicker(
message: widget.message,
),
),
@@ -168,21 +190,35 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (widget.showReplyMessage &&
widget.message.status == MessageSendingStatus.sent)
if (widget.showReplyMessage ??
(_userPermissions
.contains(PermissionType.quoteMessage) &&
widget.message.status ==
MessageSendingStatus.sent))
_buildReplyButton(context),
if (widget.showThreadReplyMessage &&
(widget.message.status ==
MessageSendingStatus.sent) &&
widget.message.parentId == null)
if (widget.showThreadReplyMessage ??
_userPermissions
.contains(PermissionType.sendReply) &&
(widget.message.status ==
MessageSendingStatus.sent) &&
widget.message.parentId == null)
_buildThreadReplyButton(context),
if (widget.showResendMessage)
_buildResendMessage(context),
if (widget.showEditMessage) _buildEditMessage(context),
if (widget.showEditMessage ??
_isMyMessage && hasEditPermission)
_buildEditMessage(context),
if (widget.showCopyMessage) _buildCopyButton(context),
if (widget.showFlagButton) _buildFlagButton(context),
if (widget.showPinButton) _buildPinButton(context),
if (widget.showDeleteMessage)
if (widget.showFlagButton ??
_userPermissions
.contains(PermissionType.flagMessage))
_buildFlagButton(context),
if (widget.showPinButton ??
_userPermissions
.contains(PermissionType.pinMessage))
_buildPinButton(context),
if (widget.showDeleteMessage ??
(_isMyMessage && hasDeletePermission))
_buildDeleteButton(context),
...widget.customActions
.map((action) => _buildCustomAction(
@@ -239,7 +275,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
InkWell _buildCustomAction(
BuildContext context,
MessageAction messageAction,
StreamMessageAction messageAction,
) =>
InkWell(
onTap: () {
@@ -560,7 +596,7 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
elevation: 2,
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
backgroundColor: MessageInputTheme.of(context).inputBackgroundColor,
backgroundColor: StreamMessageInputTheme.of(context).inputBackgroundColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
@@ -602,8 +638,10 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
if (widget.editMessageInputBuilder != null)
widget.editMessageInputBuilder!(context, widget.message)
else
MessageInput(
editMessage: widget.message,
StreamMessageInput(
messageInputController: MessageInputController(
message: widget.message,
),
preMessageSending: (m) {
FocusScope.of(context).unfocus();
Navigator.pop(context);
@@ -643,4 +681,13 @@ class _MessageActionsModalState extends State<MessageActionsModal> {
),
);
}
@override
void didChangeDependencies() {
final newStreamChannel = StreamChannel.of(context);
_userPermissions = newStreamChannel.channel.ownCapabilities;
_isMyMessage =
widget.message.user?.id == StreamChat.of(context).currentUser?.id;
super.didChangeDependencies();
}
}
@@ -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/quoted_message_widget.dart';
import 'package:stream_chat_flutter/src/user_mentions_overlay.dart';
import 'package:stream_chat_flutter/src/video_service.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_compress/video_compress.dart';
export 'package:video_compress/video_compress.dart' show VideoQuality;
/// A callback that can be passed to [MessageInput.onError].
///
@@ -59,7 +55,7 @@ typedef MentionTileBuilder = Widget Function(
/// Builder function for building a user mention tile.
///
/// Use [UserMentionTile] for the default implementation.
/// Use [StreamUserMentionTile] for the default implementation.
typedef UserMentionTileBuilder = Widget Function(
BuildContext context,
User user,
@@ -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.
///
/// The widget renders the ui based on the first ancestor of
/// type [StreamChatTheme].
/// Modify it to change the widget appearance.
@Deprecated("Use 'StreamMessageInput' instead")
class MessageInput extends StatefulWidget {
/// Instantiate a new MessageInput
const MessageInput({
@@ -190,8 +187,6 @@ class MessageInput extends StatefulWidget {
this.mentionsTileBuilder,
this.userMentionsTileBuilder,
this.maxAttachmentSize = _kDefaultMaxAttachmentSize,
this.compressedVideoQuality = VideoQuality.DefaultQuality,
this.compressedVideoFrameRate = 30,
this.onError,
this.attachmentLimit = 10,
this.onAttachmentLimitExceed,
@@ -213,12 +208,6 @@ class MessageInput extends StatefulWidget {
/// Message to edit
final Message? editMessage;
/// Video quality to use when compressing the videos
final VideoQuality compressedVideoQuality;
/// Frame rate to use when compressing the videos
final int compressedVideoFrameRate;
/// Max attachment size in bytes
/// Defaults to 20 MB
/// do not set it if you're using our default CDN
@@ -341,6 +330,7 @@ class MessageInput extends StatefulWidget {
}
/// State of [MessageInput]
@Deprecated("Use 'StreamMessageInput' instead")
class MessageInputState extends State<MessageInput> {
final _attachments = <String, Attachment>{};
final List<User> _mentionedUsers = [];
@@ -364,7 +354,7 @@ class MessageInputState extends State<MessageInput> {
widget.textEditingController ?? TextEditingController();
late StreamChatThemeData _streamChatTheme;
late MessageInputThemeData _messageInputTheme;
late StreamMessageInputThemeData _messageInputTheme;
bool get _hasQuotedMessage => widget.quotedMessage != null;
@@ -487,7 +477,7 @@ class MessageInputState extends State<MessageInput> {
);
}
return MultiOverlay(
return StreamMultiOverlay(
childAnchor: Alignment.topCenter,
overlayAnchor: Alignment.bottomCenter,
overlayOptions: [
@@ -932,7 +922,7 @@ class MessageInputState extends State<MessageInput> {
if (renderObject == null) {
return const Offstage();
}
return CommandsOverlay(
return StreamCommandsOverlay(
channel: StreamChannel.of(context).channel,
size: Size(renderObject.size.width - 16, 400),
text: text,
@@ -1121,41 +1111,18 @@ class MessageInputState extends State<MessageInput> {
if (mediaFile == null) return;
var file = AttachmentFile(
final file = AttachmentFile(
path: mediaFile.path,
size: await mediaFile.length(),
bytes: mediaFile.readAsBytesSync(),
);
if (file.size! > widget.maxAttachmentSize) {
if (medium.type == AssetType.video && file.path != null) {
final mediaInfo = await VideoService.compressVideo(
file.path!,
frameRate: widget.compressedVideoFrameRate,
quality: widget.compressedVideoQuality,
);
if (mediaInfo == null ||
mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert(
context.translations.fileTooLargeAfterCompressionError(
widget.maxAttachmentSize / (1024 * 1024),
),
);
return;
}
file = AttachmentFile(
name: file.name,
size: mediaInfo.filesize,
bytes: await mediaInfo.file?.readAsBytes(),
path: mediaInfo.path,
);
} else {
_showErrorAlert(context.translations.fileTooLargeError(
return _showErrorAlert(
context.translations.fileTooLargeError(
widget.maxAttachmentSize / (1024 * 1024),
));
return;
}
),
);
}
setState(() {
@@ -1197,7 +1164,7 @@ class MessageInputState extends State<MessageInput> {
}
return LayoutBuilder(
builder: (context, snapshot) => UserMentionsOverlay(
builder: (context, snapshot) => StreamUserMentionsOverlay(
query: query,
mentionAllAppUsers: widget.mentionAllAppUsers,
client: StreamChat.of(context).client,
@@ -1242,7 +1209,7 @@ class MessageInputState extends State<MessageInput> {
// ignore: cast_nullable_to_non_nullable
final renderObject = context.findRenderObject() as RenderBox;
return EmojiOverlay(
return StreamEmojiOverlay(
size: Size(renderObject.size.width - 16, 200),
query: query,
onEmojiResult: (emoji) {
@@ -1277,7 +1244,7 @@ class MessageInputState extends State<MessageInput> {
if (!_hasQuotedMessage) return const Offstage();
final containsUrl = widget.quotedMessage!.attachments
.any((element) => element.titleLink != null);
return QuotedMessageWidget(
return StreamQuotedMessageWidget(
reverse: true,
showBorder: !containsUrl,
message: widget.quotedMessage!,
@@ -1308,10 +1275,8 @@ class MessageInputState extends State<MessageInput> {
.map<Widget>(
(e) => ClipRRect(
borderRadius: BorderRadius.circular(10),
child: FileAttachment(
message: Message(
status: MessageSendingStatus.sending,
), // dummy message
child: StreamFileAttachment(
message: Message(), // dummy message
attachment: e,
size: Size(
MediaQuery.of(context).size.width * 0.65,
@@ -1432,7 +1397,7 @@ class MessageInputState extends State<MessageInput> {
case 'video':
return Stack(
children: [
VideoThumbnailImage(
StreamVideoThumbnailImage(
height: 104,
width: 104,
video: (attachment.file?.path ?? attachment.assetUrl)!,
@@ -1682,33 +1647,11 @@ class MessageInputState extends State<MessageInput> {
);
if (file.size! > widget.maxAttachmentSize) {
if (attachmentType == 'video' && file.path != null) {
final mediaInfo = await (VideoService.compressVideo(
file.path!,
frameRate: widget.compressedVideoFrameRate,
quality: widget.compressedVideoQuality,
) as FutureOr<MediaInfo>);
if (mediaInfo.filesize! > widget.maxAttachmentSize) {
_showErrorAlert(
context.translations.fileTooLargeAfterCompressionError(
widget.maxAttachmentSize / (1024 * 1024),
),
);
return;
}
file = AttachmentFile(
name: file.name,
size: mediaInfo.filesize,
bytes: await mediaInfo.file!.readAsBytes(),
path: mediaInfo.path,
);
} else {
_showErrorAlert(context.translations.fileTooLargeError(
return _showErrorAlert(
context.translations.fileTooLargeError(
widget.maxAttachmentSize / (1024 * 1024),
));
return;
}
),
);
}
setState(() {
@@ -1947,7 +1890,7 @@ class MessageInputState extends State<MessageInput> {
@override
void didChangeDependencies() {
_streamChatTheme = StreamChatTheme.of(context);
_messageInputTheme = MessageInputTheme.of(context);
_messageInputTheme = StreamMessageInputTheme.of(context);
if (widget.editMessage == null) _startSlowMode();
if ((widget.editMessage != null || widget.initialMessage != null) &&
@@ -2023,7 +1966,7 @@ class _PickerWidgetState extends State<_PickerWidget> {
),
);
}
return MediaListView(
return StreamMediaListView(
selectedIds: widget.selectedMedias,
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';
/// Widget builder for message
/// [defaultMessageWidget] is the default [MessageWidget] configuration
/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration
/// Use [defaultMessageWidget.copyWith] to easily customize it
typedef MessageBuilder = Widget Function(
BuildContext,
MessageDetails,
List<Message>,
MessageWidget defaultMessageWidget,
StreamMessageWidget defaultMessageWidget,
);
/// Widget builder for parent message
/// [defaultMessageWidget] is the default [MessageWidget] configuration
/// [defaultMessageWidget] is the default [StreamMessageWidget] configuration
/// Use [defaultMessageWidget.copyWith] to easily customize it
typedef ParentMessageBuilder = Widget Function(
BuildContext,
Message?,
MessageWidget defaultMessageWidget,
StreamMessageWidget defaultMessageWidget,
);
/// Widget builder for system message
@@ -123,6 +123,11 @@ class MessageDetails {
final int index;
}
/// {@macro message_list_view}
@Deprecated("Use 'StreamMessageListView' instead")
typedef MessageListView = StreamMessageListView;
/// {@template message_list_view}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_listview_paint.png)
///
@@ -165,9 +170,10 @@ class MessageDetails {
/// The widget components render the ui based on the first
/// ancestor of type [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageListView extends StatefulWidget {
/// {@endtemplate}
class StreamMessageListView extends StatefulWidget {
/// Instantiate a new MessageListView
const MessageListView({
const StreamMessageListView({
Key? key,
this.showScrollToBottom = true,
this.scrollToBottomBuilder,
@@ -197,7 +203,6 @@ class MessageListView extends StatefulWidget {
this.messageFilter,
this.onMessageTap,
this.onSystemMessageTap,
this.pinPermissions = const [],
this.showFloatingDateDivider = true,
this.threadSeparatorBuilder,
this.messageListController,
@@ -320,9 +325,6 @@ class MessageListView extends StatefulWidget {
/// Called when system message is tapped
final OnMessageTap? onSystemMessageTap;
/// A List of user types that have permission to pin messages
final List<String> pinPermissions;
/// Builder used to build the thread separator in case it's a thread view
final WidgetBuilder? threadSeparatorBuilder;
@@ -340,10 +342,10 @@ class MessageListView extends StatefulWidget {
final SpacingWidgetBuilder? spacingWidgetBuilder;
@override
_MessageListViewState createState() => _MessageListViewState();
_StreamMessageListViewState createState() => _StreamMessageListViewState();
}
class _MessageListViewState extends State<MessageListView> {
class _StreamMessageListViewState extends State<StreamMessageListView> {
ItemScrollController? _scrollController;
void Function(Message)? _onThreadTap;
final ValueNotifier<bool> _showScrollToBottom = ValueNotifier(false);
@@ -351,6 +353,7 @@ class _MessageListViewState extends State<MessageListView> {
int? _messageListLength;
StreamChannelState? streamChannel;
late StreamChatThemeData _streamTheme;
late List<String> _userPermissions;
int get _initialIndex {
final initialScrollIndex = widget.initialScrollIndex;
@@ -470,7 +473,7 @@ class _MessageListViewState extends State<MessageListView> {
final child = Stack(
alignment: Alignment.center,
children: [
ConnectionStatusBuilder(
StreamConnectionStatusBuilder(
statusBuilder: (context, status) {
var statusString = '';
var showStatus = true;
@@ -487,7 +490,7 @@ class _MessageListViewState extends State<MessageListView> {
break;
}
return InfoTile(
return StreamInfoTile(
showMessage: widget.showConnectionStateTile && showStatus,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
@@ -535,6 +538,7 @@ class _MessageListViewState extends State<MessageListView> {
return ((index + 2) * 2) - 1;
}
}
return null;
},
// Item Count -> 8 (1 parent, 2 header+footer, 2 top+bottom, 3 messages)
@@ -600,7 +604,7 @@ class _MessageListViewState extends State<MessageListView> {
)
: Padding(
padding: const EdgeInsets.symmetric(vertical: 12),
child: DateDivider(
child: StreamDateDivider(
dateTime: nextMessage.createdAt.toLocal(),
),
);
@@ -738,8 +742,10 @@ class _MessageListViewState extends State<MessageListView> {
],
);
final backgroundColor = MessageListViewTheme.of(context).backgroundColor;
final backgroundImage = MessageListViewTheme.of(context).backgroundImage;
final backgroundColor =
StreamMessageListViewTheme.of(context).backgroundColor;
final backgroundImage =
StreamMessageListViewTheme.of(context).backgroundImage;
if (backgroundColor != null || backgroundImage != null) {
return Container(
@@ -769,7 +775,7 @@ class _MessageListViewState extends State<MessageListView> {
child: Text(
context.translations.threadSeparatorText(replyCount),
textAlign: TextAlign.center,
style: ChannelHeaderTheme.of(context).subtitleStyle,
style: StreamChannelHeaderTheme.of(context).subtitleStyle,
),
),
);
@@ -823,7 +829,7 @@ class _MessageListViewState extends State<MessageListView> {
final message = messages[index - 2];
return widget.dateDividerBuilder != null
? widget.dateDividerBuilder!(message.createdAt.toLocal())
: DateDivider(dateTime: message.createdAt.toLocal());
: StreamDateDivider(dateTime: message.createdAt.toLocal());
},
),
);
@@ -992,7 +998,7 @@ class _MessageListViewState extends State<MessageListView> {
final currentUserMember =
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
final defaultMessageWidget = MessageWidget(
final defaultMessageWidget = StreamMessageWidget(
showReplyMessage: false,
showResendMessage: false,
showThreadReplyMessage: false,
@@ -1038,7 +1044,7 @@ class _MessageListViewState extends State<MessageListView> {
FocusScope.of(context).unfocus();
},
showPinButton: currentUserMember != null &&
widget.pinPermissions.contains(currentUserMember.role),
_userPermissions.contains(PermissionType.pinMessage),
);
if (widget.parentMessageBuilder != null) {
@@ -1056,7 +1062,7 @@ class _MessageListViewState extends State<MessageListView> {
if ((message.type == 'system' || message.type == 'error') &&
message.text?.isNotEmpty == true) {
return widget.systemMessageBuilder?.call(context, message) ??
SystemMessage(
StreamSystemMessage(
message: message,
onMessageTap: (message) {
if (widget.onSystemMessageTap != null) {
@@ -1126,7 +1132,7 @@ class _MessageListViewState extends State<MessageListView> {
final currentUserMember =
members.firstWhereOrNull((e) => e.user!.id == currentUser!.id);
Widget messageWidget = MessageWidget(
Widget messageWidget = StreamMessageWidget(
message: message,
reverse: isMyMessage,
showReactions: !message.isDeleted,
@@ -1157,7 +1163,10 @@ class _MessageListViewState extends State<MessageListView> {
},
showEditMessage: isMyMessage,
showDeleteMessage: isMyMessage,
showThreadReplyMessage: !isThreadMessage,
showThreadReplyMessage: !isThreadMessage &&
streamChannel?.channel.ownCapabilities
.contains(PermissionType.sendReply) ==
true,
showFlagButton: !isMyMessage,
borderSide: borderSide,
onThreadTap: _onThreadTap,
@@ -1226,7 +1235,7 @@ class _MessageListViewState extends State<MessageListView> {
FocusScope.of(context).unfocus();
},
showPinButton: currentUserMember != null &&
widget.pinPermissions.contains(currentUserMember.role),
_userPermissions.contains(PermissionType.pinMessage),
);
if (widget.messageBuilder != null) {
@@ -1239,7 +1248,7 @@ class _MessageListViewState extends State<MessageListView> {
index,
),
messages,
messageWidget as MessageWidget,
messageWidget as StreamMessageWidget,
);
}
@@ -1306,6 +1315,7 @@ class _MessageListViewState extends State<MessageListView> {
void didChangeDependencies() {
final newStreamChannel = StreamChannel.of(context);
_streamTheme = StreamChatTheme.of(context);
_userPermissions = newStreamChannel.channel.ownCapabilities;
if (newStreamChannel != streamChannel) {
streamChannel = newStreamChannel;
@@ -5,15 +5,21 @@ import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/reaction_bubble.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro message_reactions_modal}
@Deprecated("Use 'StreamMessageReactionsModal' instead")
typedef MessageReactionsModal = StreamMessageReactionsModal;
/// {@template message_reactions_modal}
/// Modal widget for displaying message reactions
class MessageReactionsModal extends StatelessWidget {
/// Constructor for creating a [MessageReactionsModal] reactions
const MessageReactionsModal({
/// {@endtemplate}
class StreamMessageReactionsModal extends StatelessWidget {
/// Constructor for creating a [StreamMessageReactionsModal] reactions
const StreamMessageReactionsModal({
Key? key,
required this.message,
required this.messageWidget,
required this.messageTheme,
this.showReactions = true,
this.showReactions,
this.reverse = false,
this.onUserAvatarTap,
}) : super(key: key);
@@ -24,14 +30,14 @@ class MessageReactionsModal extends StatelessWidget {
/// Message to display reactions of
final Message message;
/// [MessageThemeData] to apply to [message]
final MessageThemeData messageTheme;
/// [StreamMessageThemeData] to apply to [message]
final StreamMessageThemeData messageTheme;
/// Flag to reverse message
final bool reverse;
/// Flag to show reactions on message
final bool showReactions;
final bool? showReactions;
/// Callback when user avatar is tapped
final void Function(User)? onUserAvatarTap;
@@ -40,6 +46,10 @@ class MessageReactionsModal extends StatelessWidget {
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
final user = StreamChat.of(context).currentUser;
final _userPermissions = StreamChannel.of(context).channel.ownCapabilities;
final hasReactionPermission =
_userPermissions.contains(PermissionType.sendReaction);
final roughMaxSize = size.width * 2 / 3;
var messageTextLength = message.text!.length;
@@ -71,7 +81,7 @@ class MessageReactionsModal extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
if (showReactions &&
if ((showReactions ?? hasReactionPermission) &&
(message.status == MessageSendingStatus.sent))
Align(
alignment: Alignment(
@@ -84,7 +94,7 @@ class MessageReactionsModal extends StatelessWidget {
: -(1.2 - divFactor)),
0,
),
child: ReactionPicker(
child: StreamReactionPicker(
message: message,
),
),
@@ -196,7 +206,7 @@ class MessageReactionsModal extends StatelessWidget {
Stack(
clipBehavior: Clip.none,
children: [
UserAvatar(
StreamUserAvatar(
onTap: onUserAvatarTap,
user: reaction.user!,
constraints: const BoxConstraints.tightFor(
@@ -216,7 +226,7 @@ class MessageReactionsModal extends StatelessWidget {
child: Align(
alignment:
reverse ? Alignment.centerRight : Alignment.centerLeft,
child: ReactionBubble(
child: StreamReactionBubble(
reactions: [reaction],
flipTail: !reverse,
borderColor:
@@ -2,17 +2,23 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.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.
///
/// 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
/// [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageSearchItem extends StatelessWidget {
/// {@endtemplate}
class StreamMessageSearchItem extends StatelessWidget {
/// Instantiate a new MessageSearchItem
const MessageSearchItem({
const StreamMessageSearchItem({
Key? key,
required this.getMessageResponse,
this.onTap,
@@ -25,7 +31,7 @@ class MessageSearchItem extends StatelessWidget {
/// Function called when tapping this widget
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;
@override
@@ -34,10 +40,10 @@ class MessageSearchItem extends StatelessWidget {
final channel = getMessageResponse.channel;
final channelName = channel?.extraData['name'];
final user = message.user!;
final channelPreviewTheme = ChannelPreviewTheme.of(context);
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
return ListTile(
onTap: onTap,
leading: UserAvatar(
leading: StreamUserAvatar(
user: user,
showOnlineStatus: showOnlineStatus,
constraints: const BoxConstraints.tightFor(
@@ -120,7 +126,7 @@ class MessageSearchItem extends StatelessWidget {
text = parts.join(' ');
}
final channelPreviewTheme = ChannelPreviewTheme.of(context);
final channelPreviewTheme = StreamChannelPreviewTheme.of(context);
return Text.rich(
_getDisplayText(
text!,
@@ -11,13 +11,17 @@ typedef MessageSearchItemBuilder = Widget Function(
GetMessageResponse,
);
/// Builder used when [MessageSearchListView] is empty
/// Builder used when [StreamMessageSearchListView] is empty
typedef EmptyMessageSearchBuilder = Widget Function(
BuildContext context,
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.
///
/// ```dart
@@ -47,9 +51,10 @@ typedef EmptyMessageSearchBuilder = Widget Function(
/// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageSearchListView extends StatefulWidget {
/// {@endtemplate}
class StreamMessageSearchListView extends StatefulWidget {
/// Instantiate a new MessageSearchListView
const MessageSearchListView({
const StreamMessageSearchListView({
Key? key,
required this.filters,
this.messageQuery,
@@ -96,7 +101,7 @@ class MessageSearchListView extends StatefulWidget {
/// Builder used to create a custom item preview
final MessageSearchItemBuilder? itemBuilder;
/// Function called when tapping on a [MessageSearchItem]
/// Function called when tapping on a [StreamMessageSearchItem]
final MessageSearchItemTapCallback? onItemTap;
/// Builder used to create a custom item separator
@@ -130,10 +135,12 @@ class MessageSearchListView extends StatefulWidget {
final MessageSearchListController? messageSearchListController;
@override
_MessageSearchListViewState createState() => _MessageSearchListViewState();
_StreamMessageSearchListViewState createState() =>
_StreamMessageSearchListViewState();
}
class _MessageSearchListViewState extends State<MessageSearchListView> {
class _StreamMessageSearchListViewState
extends State<StreamMessageSearchListView> {
late final _defaultController = MessageSearchListController();
MessageSearchListController get _messageSearchListController =>
@@ -168,7 +175,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
if (error is Error) {
print(error.stackTrace);
}
return InfoTile(
return StreamInfoTile(
showMessage: widget.showErrorTile,
tileAnchor: Alignment.topCenter,
childAnchor: Alignment.topCenter,
@@ -195,7 +202,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
);
final backgroundColor =
MessageSearchListViewTheme.of(context).backgroundColor;
StreamMessageSearchListViewTheme.of(context).backgroundColor;
if (backgroundColor != null) {
return ColoredBox(
@@ -219,7 +226,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
if (widget.itemBuilder != null) {
return widget.itemBuilder!(context, getMessageResponse);
}
return MessageSearchItem(
return StreamMessageSearchItem(
getMessageResponse: getMessageResponse,
onTap: () => widget.onItemTap!(getMessageResponse),
);
@@ -1,12 +1,19 @@
import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro message_text}
@Deprecated("Use 'StreamMessageText' instead")
typedef MessageText = StreamMessageText;
/// {@template message_text}
/// Text widget to display in message
class MessageText extends StatelessWidget {
/// Constructor for creating a [MessageText] widget
const MessageText({
/// {@endtemplate}
class StreamMessageText extends StatelessWidget {
/// Constructor for creating a [StreamMessageText] widget
const StreamMessageText({
Key? key,
required this.message,
required this.messageTheme,
@@ -23,8 +30,8 @@ class MessageText extends StatelessWidget {
/// Callback for when link is tapped
final void Function(String)? onLinkTap;
/// [MessageThemeData] whose text theme is to be applied
final MessageThemeData messageTheme;
/// [StreamMessageThemeData] whose text theme is to be applied
final StreamMessageThemeData messageTheme;
@override
Widget build(BuildContext context) {
@@ -34,13 +41,14 @@ class MessageText extends StatelessWidget {
stream: streamChat.currentUserStream.map((it) => it!.language ?? 'en'),
initialData: streamChat.currentUser!.language ?? 'en',
builder: (context, language) {
final translatedText =
message.i18n?['${language}_text'] ?? message.text;
final messageText =
_replaceMentions(translatedText ?? '').replaceAll('\n', '\n\n');
final messageText = message
.translate(language)
.replaceMentions()
.text
?.replaceAll('\n', '\n\n');
final themeData = Theme.of(context);
return MarkdownBody(
data: messageText,
data: messageText ?? '',
onTapLink: (
String link,
String? href,
@@ -80,17 +88,4 @@ class MessageText extends StatelessWidget {
},
);
}
String _replaceMentions(String text) {
var messageTextToRender = text;
for (final user in message.mentionedUsers.toSet()) {
final userId = user.id;
final userName = user.name;
messageTextToRender = messageTextToRender.replaceAll(
'@$userId',
'[@$userName](@${userName.replaceAll(' ', '')})',
);
}
return messageTextToRender;
}
}
@@ -32,20 +32,26 @@ enum DisplayWidget {
show,
}
/// {@macro message_widget}
@Deprecated("Use 'StreamMessageWidget' instead")
typedef MessageWidget = StreamMessageWidget;
/// {@template message_widget}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_widget_paint.png)
///
/// It shows a message with reactions, replies and user avatar.
///
/// Usually you don't use this widget as it's the default message widget used by
/// [MessageListView].
/// [StreamMessageListView].
///
/// The widget components render the ui based on the first ancestor of type
/// [StreamChatTheme].
/// Modify it to change the widget appearance.
class MessageWidget extends StatefulWidget {
///
MessageWidget({
/// {@endtemplate}
class StreamMessageWidget extends StatefulWidget {
/// Creates a new instance of the message widget.
StreamMessageWidget({
Key? key,
required this.message,
required this.messageTheme,
@@ -121,7 +127,7 @@ class MessageWidget extends StatefulWidget {
context,
Material(
color: messageTheme.messageBackgroundColor,
child: ImageGroup(
child: StreamImageGroup(
size: Size(
mediaQueryData.size.width * 0.8,
mediaQueryData.size.height * 0.3,
@@ -142,7 +148,7 @@ class MessageWidget extends StatefulWidget {
return wrapAttachmentWidget(
context,
ImageAttachment(
StreamImageAttachment(
attachment: attachments[0],
message: message,
messageTheme: messageTheme,
@@ -172,7 +178,7 @@ class MessageWidget extends StatefulWidget {
Column(
children: attachments.map((attachment) {
final mediaQueryData = MediaQuery.of(context);
return VideoAttachment(
return StreamVideoAttachment(
attachment: attachment,
messageTheme: messageTheme,
size: Size(
@@ -204,7 +210,7 @@ class MessageWidget extends StatefulWidget {
Column(
children: attachments.map((attachment) {
final mediaQueryData = MediaQuery.of(context);
return GiphyAttachment(
return StreamGiphyAttachment(
attachment: attachment,
message: message,
size: Size(
@@ -240,7 +246,7 @@ class MessageWidget extends StatefulWidget {
final mediaQueryData = MediaQuery.of(context);
return wrapAttachmentWidget(
context,
FileAttachment(
StreamFileAttachment(
message: message,
attachment: attachment,
size: Size(
@@ -300,7 +306,7 @@ class MessageWidget extends StatefulWidget {
final Message message;
/// The message theme
final MessageThemeData messageTheme;
final StreamMessageThemeData messageTheme;
/// If true the widget will be mirrored
final bool reverse;
@@ -356,7 +362,7 @@ class MessageWidget extends StatefulWidget {
/// The function called when tapping on a link
final void Function(String)? onLinkTap;
/// Used in [MessageReactionsModal] and [MessageActionsModal]
/// Used in [StreamMessageReactionsModal] and [StreamMessageActionsModal]
final bool showReactionPickerIndicator;
/// List of users who read
@@ -417,13 +423,13 @@ class MessageWidget extends StatefulWidget {
final void Function(Message)? onMessageTap;
/// List of custom actions shown on message long tap
final List<MessageAction> customActions;
final List<StreamMessageAction> customActions;
/// Customize onTap on attachment
final void Function(Message message, Attachment attachment)? onAttachmentTap;
/// Creates a copy of [MessageWidget] with specified attributes overridden.
MessageWidget copyWith({
/// Creates a copy of [StreamMessageWidget] with specified attributes overridden.
StreamMessageWidget copyWith({
Key? key,
void Function(User)? onMentionTap,
void Function(Message)? onThreadTap,
@@ -435,7 +441,7 @@ class MessageWidget extends StatefulWidget {
Widget Function(BuildContext, Message)? deletedBottomRowBuilder,
void Function(BuildContext, Message)? onMessageActions,
Message? message,
MessageThemeData? messageTheme,
StreamMessageThemeData? messageTheme,
bool? reverse,
ShapeBorder? shape,
ShapeBorder? attachmentShape,
@@ -473,11 +479,11 @@ class MessageWidget extends StatefulWidget {
bool? translateUserAvatar,
OnQuotedMessageTap? onQuotedMessageTap,
void Function(Message)? onMessageTap,
List<MessageAction>? customActions,
List<StreamMessageAction>? customActions,
void Function(Message message, Attachment attachment)? onAttachmentTap,
Widget Function(BuildContext, User)? userAvatarBuilder,
}) =>
MessageWidget(
StreamMessageWidget(
key: key ?? this.key,
onMentionTap: onMentionTap ?? this.onMentionTap,
onThreadTap: onThreadTap ?? this.onThreadTap,
@@ -539,11 +545,11 @@ class MessageWidget extends StatefulWidget {
);
@override
_MessageWidgetState createState() => _MessageWidgetState();
_StreamMessageWidgetState createState() => _StreamMessageWidgetState();
}
class _MessageWidgetState extends State<MessageWidget>
with AutomaticKeepAliveClientMixin<MessageWidget> {
class _StreamMessageWidgetState extends State<StreamMessageWidget>
with AutomaticKeepAliveClientMixin<StreamMessageWidget> {
bool get showThreadReplyIndicator => widget.showThreadReplyIndicator;
bool get showSendingIndicator => widget.showSendingIndicator;
@@ -717,7 +723,7 @@ class _MessageWidgetState extends State<MessageWidget>
? 0
: 4.0,
),
child: DeletedMessage(
child: StreamDeletedMessage(
borderRadiusGeometry: widget
.borderRadiusGeometry,
borderSide:
@@ -854,7 +860,7 @@ class _MessageWidgetState extends State<MessageWidget>
? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId)
: null;
final chatThemeData = _streamChatTheme;
return QuotedMessageWidget(
return StreamQuotedMessageWidget(
onTap: onTap,
message: widget.message.quotedMessage!,
messageTheme: isMyMessage
@@ -1008,7 +1014,7 @@ class _MessageWidgetState extends State<MessageWidget>
getWebsiteName(hostName.toLowerCase()) ??
hostName.capitalize();
return UrlAttachment(
return StreamUrlAttachment(
urlAttachment: urlAttachment,
hostDisplayName: hostDisplayName,
textPadding: widget.textPadding,
@@ -1042,7 +1048,7 @@ class _MessageWidgetState extends State<MessageWidget>
child: _shouldShowReactions
? GestureDetector(
onTap: () => _showMessageReactionsModalBottomSheet(context),
child: ReactionBubble(
child: StreamReactionBubble(
key: ValueKey('${widget.message.id}.reactions'),
reverse: widget.reverse,
flipTail: widget.reverse,
@@ -1073,7 +1079,7 @@ class _MessageWidgetState extends State<MessageWidget>
barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel(
channel: channel,
child: MessageActionsModal(
child: StreamMessageActionsModal(
messageWidget: widget.copyWith(
key: const Key('MessageWidget'),
message: widget.message.copyWith(
@@ -1088,7 +1094,8 @@ class _MessageWidgetState extends State<MessageWidget>
showSendingIndicator: false,
padding: const EdgeInsets.all(0),
showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent),
(widget.message.status == MessageSendingStatus.sent) &&
channel.ownCapabilities.contains(PermissionType.sendReaction),
showPinHighlight: false,
showUserAvatar:
widget.message.user!.id == channel.client.state.currentUser!.id
@@ -1099,7 +1106,6 @@ class _MessageWidgetState extends State<MessageWidget>
Clipboard.setData(ClipboardData(text: message.text)),
messageTheme: widget.messageTheme,
reverse: widget.reverse,
showDeleteMessage: widget.showDeleteMessage || isDeleteFailed,
message: widget.message,
editMessageInputBuilder: widget.editMessageInputBuilder,
onReplyTap: widget.onReplyTap,
@@ -1109,11 +1115,6 @@ class _MessageWidgetState extends State<MessageWidget>
showCopyMessage: widget.showCopyMessage &&
!isFailedState &&
widget.message.text?.trim().isNotEmpty == true,
showEditMessage: widget.showEditMessage &&
!isDeleteFailed &&
!widget.message.attachments
.any((element) => element.type == 'giphy'),
showReactions: widget.showReactions,
showReplyMessage: widget.showReplyMessage &&
!isFailedState &&
widget.onReplyTap != null,
@@ -1121,7 +1122,6 @@ class _MessageWidgetState extends State<MessageWidget>
!isFailedState &&
widget.onThreadTap != null,
showFlagButton: widget.showFlagButton,
showPinButton: widget.showPinButton,
customActions: widget.customActions,
),
),
@@ -1136,7 +1136,7 @@ class _MessageWidgetState extends State<MessageWidget>
barrierColor: _streamChatTheme.colorTheme.overlay,
builder: (context) => StreamChannel(
channel: channel,
child: MessageReactionsModal(
child: StreamMessageReactionsModal(
messageWidget: widget.copyWith(
key: const Key('MessageWidget'),
message: widget.message.copyWith(
@@ -1151,7 +1151,8 @@ class _MessageWidgetState extends State<MessageWidget>
showSendingIndicator: false,
padding: const EdgeInsets.all(0),
showReactionPickerIndicator: widget.showReactions &&
(widget.message.status == MessageSendingStatus.sent),
(widget.message.status == MessageSendingStatus.sent) &&
channel.ownCapabilities.contains(PermissionType.sendReaction),
showPinHighlight: false,
showUserAvatar:
widget.message.user!.id == channel.client.state.currentUser!.id
@@ -1162,7 +1163,8 @@ class _MessageWidgetState extends State<MessageWidget>
messageTheme: widget.messageTheme,
reverse: widget.reverse,
message: widget.message,
showReactions: widget.showReactions,
showReactions: widget.showReactions &&
channel.ownCapabilities.contains(PermissionType.sendReaction),
),
),
);
@@ -1250,6 +1252,13 @@ class _MessageWidgetState extends State<MessageWidget>
final channel = StreamChannel.of(context).channel;
if (!channel.ownCapabilities.contains(PermissionType.readEvents)) {
return StreamSendingIndicator(
message: message,
size: style!.fontSize,
);
}
return BetterStreamBuilder<List<Read>>(
stream: channel.state?.readStream,
initialData: channel.state?.read,
@@ -1259,7 +1268,7 @@ class _MessageWidgetState extends State<MessageWidget>
(it.lastRead.isAfter(message.createdAt) ||
it.lastRead.isAtSameMomentAs(message.createdAt)));
final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1;
Widget child = SendingIndicator(
Widget child = StreamSendingIndicator(
message: message,
isMessageRead: isMessageRead,
size: style!.fontSize,
@@ -1293,7 +1302,7 @@ class _MessageWidgetState extends State<MessageWidget>
: 0,
),
child: widget.userAvatarBuilder?.call(context, widget.message.user!) ??
UserAvatar(
StreamUserAvatar(
user: widget.message.user!,
onTap: widget.onUserAvatarTap,
constraints: widget.messageTheme.avatarTheme!.constraints,
@@ -1311,7 +1320,7 @@ class _MessageWidgetState extends State<MessageWidget>
padding: isOnlyEmoji ? EdgeInsets.zero : widget.textPadding,
child: widget.textBuilder != null
? widget.textBuilder!(context, widget.message)
: MessageText(
: StreamMessageText(
onLinkTap: widget.onLinkTap,
message: widget.message,
onMentionTap: widget.onMentionTap,
@@ -1428,7 +1437,7 @@ class _ThreadParticipants extends StatelessWidget {
color: _streamChatTheme.colorTheme.barsBg,
),
padding: const EdgeInsets.all(1),
child: UserAvatar(
child: StreamUserAvatar(
user: user,
constraints: BoxConstraints.loose(const Size.fromRadius(7)),
showOnlineStatus: false,
@@ -2,15 +2,21 @@ import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_portal/flutter_portal.dart';
/// {@macro multi_overlay}
@Deprecated("Use 'StreamMultiOverlay' instead")
typedef MultiOverlay = StreamMultiOverlay;
/// {@template multi_overlay}
/// Widget that renders a single overlay widget from a list of [overlayOptions]
/// It shows the first one that is visible
class MultiOverlay extends StatelessWidget {
/// {@endtemplate}
class StreamMultiOverlay extends StatelessWidget {
/// Constructs a new MultiOverlay widget
/// [overlayOptions] - the list of overlay options
/// [overlayAnchor] - the anchor relative to the overlay
/// [childAnchor] - the anchor relative to the child
/// [child] - the child widget
const MultiOverlay({
const StreamMultiOverlay({
Key? key,
required this.overlayOptions,
required this.child,
@@ -1,12 +1,18 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro option_list_tile}
@Deprecated("Use 'StreamOptionListTile' instead")
typedef OptionListTile = StreamOptionListTile;
/// {@template option_list_tile}
/// List tile for [ChannelBottomSheet]
class OptionListTile extends StatelessWidget {
/// Constructor for creating [OptionListTile]
const OptionListTile({
/// {@endtemplate}
class StreamOptionListTile extends StatelessWidget {
/// Constructor for creating [StreamOptionListTile]
const StreamOptionListTile({
Key? key,
this.title,
required this.title,
this.leading,
this.trailing,
this.onTap,
@@ -17,7 +23,7 @@ class OptionListTile extends StatelessWidget {
}) : super(key: key);
/// Title for tile
final String? title;
final String title;
/// Leading widget (start)
final Widget? leading;
@@ -46,8 +52,8 @@ class OptionListTile extends StatelessWidget {
return Column(
children: [
Container(
color: separatorColor ?? chatThemeData.colorTheme.disabled,
height: 1,
color: separatorColor ?? chatThemeData.colorTheme.disabled,
),
Material(
color: tileColor ?? chatThemeData.colorTheme.barsBg,
@@ -57,15 +63,14 @@ class OptionListTile extends StatelessWidget {
onTap: onTap,
child: Row(
children: [
if (leading != null) Center(child: leading),
if (leading == null)
const SizedBox(
width: 16,
),
if (leading != null)
Center(child: leading)
else
const SizedBox(width: 16),
Expanded(
flex: 4,
child: Text(
title!,
title,
style: titleTextStyle ??
(titleColor == null
? chatThemeData.textTheme.bodyBold
@@ -10,54 +10,14 @@ typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function(
Attachment,
);
class _VideoAttachmentThumbnail extends StatefulWidget {
const _VideoAttachmentThumbnail({
Key? key,
required this.attachment,
this.size = const Size(32, 32),
}) : super(key: key);
/// Widget for the quoted message.
@Deprecated("Use 'StreamQuotedMessageWidget' instead")
typedef QuotedMessageWidget = StreamQuotedMessageWidget;
final Size size;
final Attachment attachment;
@override
_VideoAttachmentThumbnailState createState() =>
_VideoAttachmentThumbnailState();
}
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
late VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(widget.attachment.assetUrl!)
..initialize().then((_) {
// ignore: no-empty-block
setState(() {}); //when your thumbnail will show.
});
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) => SizedBox(
height: widget.size.height,
width: widget.size.width,
child: _controller.value.isInitialized
? VideoPlayer(_controller)
: const CircularProgressIndicator(),
);
}
///
class QuotedMessageWidget extends StatelessWidget {
///
const QuotedMessageWidget({
/// Widget for the quoted message.
class StreamQuotedMessageWidget extends StatelessWidget {
/// Creates a new instance of the widget.
const StreamQuotedMessageWidget({
Key? key,
required this.message,
required this.messageTheme,
@@ -73,7 +33,7 @@ class QuotedMessageWidget extends StatelessWidget {
final Message message;
/// The message theme
final MessageThemeData messageTheme;
final StreamMessageThemeData messageTheme;
/// If true the widget will be mirrored
final bool reverse;
@@ -134,7 +94,7 @@ class QuotedMessageWidget extends StatelessWidget {
if (_hasAttachments) _parseAttachments(context),
if (msg.text!.isNotEmpty)
Flexible(
child: MessageText(
child: StreamMessageText(
message: msg,
messageTheme: isOnlyEmoji && _containsText
? messageTheme.copyWith(
@@ -231,7 +191,7 @@ class QuotedMessageWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(8),
);
Widget _buildUserAvatar() => UserAvatar(
Widget _buildUserAvatar() => StreamUserAvatar(
user: message.user!,
constraints: const BoxConstraints.tightFor(
height: 24,
@@ -242,7 +202,7 @@ class QuotedMessageWidget extends StatelessWidget {
Map<String, QuotedMessageAttachmentThumbnailBuilder>
get _defaultAttachmentBuilder => {
'image': (_, attachment) => ImageAttachment(
'image': (_, attachment) => StreamImageAttachment(
attachment: attachment,
message: message,
messageTheme: messageTheme,
@@ -288,3 +248,47 @@ class QuotedMessageWidget extends StatelessWidget {
return messageTheme.messageBackgroundColor;
}
}
class _VideoAttachmentThumbnail extends StatefulWidget {
const _VideoAttachmentThumbnail({
Key? key,
required this.attachment,
this.size = const Size(32, 32),
}) : super(key: key);
final Size size;
final Attachment attachment;
@override
_VideoAttachmentThumbnailState createState() =>
_VideoAttachmentThumbnailState();
}
class _VideoAttachmentThumbnailState extends State<_VideoAttachmentThumbnail> {
late VideoPlayerController _controller;
@override
void initState() {
super.initState();
_controller = VideoPlayerController.network(widget.attachment.assetUrl!)
..initialize().then((_) {
// ignore: no-empty-block
setState(() {}); //when your thumbnail will show.
});
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
Widget build(BuildContext context) => SizedBox(
height: widget.size.height,
width: widget.size.width,
child: _controller.value.isInitialized
? VideoPlayer(_controller)
: const CircularProgressIndicator(),
);
}
@@ -4,10 +4,16 @@ import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro reaction_bubble}
@Deprecated("Use 'StreamReactionBubble' instead")
typedef ReactionBubble = StreamReactionBubble;
/// {@template reaction_bubble}
/// Creates reaction bubble widget for displaying over messages
class ReactionBubble extends StatelessWidget {
/// Constructor for creating a [ReactionBubble]
const ReactionBubble({
/// {@endtemplate}
class StreamReactionBubble extends StatelessWidget {
/// Constructor for creating a [StreamReactionBubble]
const StreamReactionBubble({
Key? key,
required this.reactions,
required this.borderColor,
@@ -111,7 +117,7 @@ class ReactionBubble extends StatelessWidget {
}
Widget _buildReaction(
List<ReactionIcon> reactionIcons,
List<StreamReactionIcon> reactionIcons,
Reaction reaction,
BuildContext context,
) {
@@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
/// Reaction icon data
class ReactionIcon {
/// Constructor for creating [ReactionIcon]
ReactionIcon({
@Deprecated("Use 'StreamReactionIcon' instead")
typedef ReactionIcon = StreamReactionIcon;
/// Reaction icon data
class StreamReactionIcon {
/// Constructor for creating [StreamReactionIcon]
StreamReactionIcon({
required this.type,
required this.builder,
});
@@ -3,16 +3,22 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro reaction_picker}
@Deprecated("Use 'StreamReactionPicker' instead")
typedef ReactionPicker = StreamReactionPicker;
/// {@template reaction_picker}
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/reaction_picker_paint.png)
///
/// It shows a reaction picker
///
/// Usually you don't use this widget as it's one of the default widgets used
/// by [MessageWidget.onMessageActions].
class ReactionPicker extends StatefulWidget {
/// Constructor for creating a [ReactionPicker] widget
const ReactionPicker({
/// by [StreamMessageWidget.onMessageActions].
/// {@endtemplate}
class StreamReactionPicker extends StatefulWidget {
/// Constructor for creating a [StreamReactionPicker] widget
const StreamReactionPicker({
Key? key,
required this.message,
}) : super(key: key);
@@ -21,10 +27,10 @@ class ReactionPicker extends StatefulWidget {
final Message message;
@override
_ReactionPickerState createState() => _ReactionPickerState();
_StreamReactionPickerState createState() => _StreamReactionPickerState();
}
class _ReactionPickerState extends State<ReactionPicker>
class _StreamReactionPickerState extends State<StreamReactionPicker>
with TickerProviderStateMixin {
List<EzAnimation> animations = [];
@@ -1,10 +1,16 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// {@macro sending_indicator}
@Deprecated("Use 'StreamSendingIndicator' instead")
typedef SendingIndicator = StreamSendingIndicator;
/// {@template sending_indicator}
/// Used to show the sending status of the message
class SendingIndicator extends StatelessWidget {
/// Constructor for creating a [SendingIndicator] widget
const SendingIndicator({
/// {@endtemplate}
class StreamSendingIndicator extends StatelessWidget {
/// Constructor for creating a [StreamSendingIndicator] widget
const StreamSendingIndicator({
Key? key,
required this.message,
this.isMessageRead = false,
@@ -38,29 +38,29 @@ class StreamChatThemeData {
/// Create a theme from scratch
factory StreamChatThemeData({
Brightness? brightness,
TextTheme? textTheme,
ColorTheme? colorTheme,
ChannelListHeaderThemeData? channelListHeaderTheme,
ChannelPreviewThemeData? channelPreviewTheme,
ChannelHeaderThemeData? channelHeaderTheme,
MessageThemeData? otherMessageTheme,
MessageThemeData? ownMessageTheme,
MessageInputThemeData? messageInputTheme,
StreamTextTheme? textTheme,
StreamColorTheme? colorTheme,
StreamChannelListHeaderThemeData? channelListHeaderTheme,
StreamChannelPreviewThemeData? channelPreviewTheme,
StreamChannelHeaderThemeData? channelHeaderTheme,
StreamMessageThemeData? otherMessageTheme,
StreamMessageThemeData? ownMessageTheme,
StreamMessageInputThemeData? messageInputTheme,
Widget Function(BuildContext, User)? defaultUserImage,
Widget Function(BuildContext, User)? placeholderUserImage,
IconThemeData? primaryIconTheme,
List<ReactionIcon>? reactionIcons,
GalleryHeaderThemeData? imageHeaderTheme,
GalleryFooterThemeData? imageFooterTheme,
MessageListViewThemeData? messageListViewTheme,
ChannelListViewThemeData? channelListViewTheme,
UserListViewThemeData? userListViewTheme,
MessageSearchListViewThemeData? messageSearchListViewTheme,
List<StreamReactionIcon>? reactionIcons,
StreamGalleryHeaderThemeData? imageHeaderTheme,
StreamGalleryFooterThemeData? imageFooterTheme,
StreamMessageListViewThemeData? messageListViewTheme,
StreamChannelListViewThemeData? channelListViewTheme,
StreamUserListViewThemeData? userListViewTheme,
StreamMessageSearchListViewThemeData? messageSearchListViewTheme,
}) {
brightness ??= colorTheme?.brightness ?? Brightness.light;
final isDark = brightness == Brightness.dark;
textTheme ??= isDark ? TextTheme.dark() : TextTheme.light();
colorTheme ??= isDark ? ColorTheme.dark() : ColorTheme.light();
textTheme ??= isDark ? StreamTextTheme.dark() : StreamTextTheme.light();
colorTheme ??= isDark ? StreamColorTheme.dark() : StreamColorTheme.light();
final defaultData = StreamChatThemeData.fromColorAndTextTheme(
colorTheme,
@@ -133,14 +133,14 @@ class StreamChatThemeData {
/// Create theme from color and text theme
factory StreamChatThemeData.fromColorAndTextTheme(
ColorTheme colorTheme,
TextTheme textTheme,
StreamColorTheme colorTheme,
StreamTextTheme textTheme,
) {
final accentColor = colorTheme.accentPrimary;
final iconTheme =
IconThemeData(color: colorTheme.textHighEmphasis.withOpacity(0.5));
final channelHeaderTheme = ChannelHeaderThemeData(
avatarTheme: AvatarThemeData(
final channelHeaderTheme = StreamChannelHeaderThemeData(
avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 40,
@@ -153,9 +153,9 @@ class StreamChatThemeData {
color: const Color(0xff7A7A7A),
),
);
final channelPreviewTheme = ChannelPreviewThemeData(
final channelPreviewTheme = StreamChannelPreviewThemeData(
unreadCounterColor: colorTheme.accentError,
avatarTheme: AvatarThemeData(
avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 40,
@@ -176,14 +176,14 @@ class StreamChatThemeData {
colorTheme: colorTheme,
primaryIconTheme: iconTheme,
defaultUserImage: (context, user) => Center(
child: GradientAvatar(
child: StreamGradientAvatar(
name: user.name,
userId: user.id,
),
),
channelPreviewTheme: channelPreviewTheme,
channelListHeaderTheme: ChannelListHeaderThemeData(
avatarTheme: AvatarThemeData(
channelListHeaderTheme: StreamChannelListHeaderThemeData(
avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 40,
@@ -194,7 +194,7 @@ class StreamChatThemeData {
titleStyle: textTheme.headlineBold,
),
channelHeaderTheme: channelHeaderTheme,
ownMessageTheme: MessageThemeData(
ownMessageTheme: StreamMessageThemeData(
messageAuthorStyle:
textTheme.footnote.copyWith(color: colorTheme.textLowEmphasis),
messageTextStyle: textTheme.body,
@@ -206,7 +206,7 @@ class StreamChatThemeData {
reactionsBorderColor: colorTheme.borders,
reactionsMaskColor: colorTheme.appBg,
messageBorderColor: colorTheme.disabled,
avatarTheme: AvatarThemeData(
avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 32,
@@ -218,7 +218,7 @@ class StreamChatThemeData {
),
linkBackgroundColor: colorTheme.linkBg,
),
otherMessageTheme: MessageThemeData(
otherMessageTheme: StreamMessageThemeData(
reactionsBackgroundColor: colorTheme.disabled,
reactionsBorderColor: colorTheme.barsBg,
reactionsMaskColor: colorTheme.appBg,
@@ -233,7 +233,7 @@ class StreamChatThemeData {
),
messageBackgroundColor: colorTheme.barsBg,
messageBorderColor: colorTheme.borders,
avatarTheme: AvatarThemeData(
avatarTheme: StreamAvatarThemeData(
borderRadius: BorderRadius.circular(20),
constraints: const BoxConstraints.tightFor(
height: 32,
@@ -242,7 +242,7 @@ class StreamChatThemeData {
),
linkBackgroundColor: colorTheme.linkBg,
),
messageInputTheme: MessageInputThemeData(
messageInputTheme: StreamMessageInputThemeData(
borderRadius: BorderRadius.circular(20),
sendAnimationDuration: const Duration(milliseconds: 300),
actionButtonColor: colorTheme.accentPrimary,
@@ -252,6 +252,7 @@ class StreamChatThemeData {
sendButtonIdleColor: colorTheme.disabled,
inputBackgroundColor: colorTheme.barsBg,
inputTextStyle: textTheme.body,
linkHighlightColor: colorTheme.accentPrimary,
idleBorderGradient: LinearGradient(
colors: [
colorTheme.disabled,
@@ -266,7 +267,7 @@ class StreamChatThemeData {
),
),
reactionIcons: [
ReactionIcon(
StreamReactionIcon(
type: 'love',
builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context);
@@ -278,7 +279,7 @@ class StreamChatThemeData {
);
},
),
ReactionIcon(
StreamReactionIcon(
type: 'like',
builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context);
@@ -290,7 +291,7 @@ class StreamChatThemeData {
);
},
),
ReactionIcon(
StreamReactionIcon(
type: 'sad',
builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context);
@@ -302,7 +303,7 @@ class StreamChatThemeData {
);
},
),
ReactionIcon(
StreamReactionIcon(
type: 'haha',
builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context);
@@ -314,7 +315,7 @@ class StreamChatThemeData {
);
},
),
ReactionIcon(
StreamReactionIcon(
type: 'wow',
builder: (context, highlighted, size) {
final theme = StreamChatTheme.of(context);
@@ -327,7 +328,7 @@ class StreamChatThemeData {
},
),
],
galleryHeaderTheme: GalleryHeaderThemeData(
galleryHeaderTheme: StreamGalleryHeaderThemeData(
closeButtonColor: colorTheme.textHighEmphasis,
backgroundColor: channelHeaderTheme.color,
iconMenuPointColor: colorTheme.textHighEmphasis,
@@ -335,7 +336,7 @@ class StreamChatThemeData {
subtitleTextStyle: channelPreviewTheme.subtitleStyle,
bottomSheetBarrierColor: colorTheme.overlay,
),
galleryFooterTheme: GalleryFooterThemeData(
galleryFooterTheme: StreamGalleryFooterThemeData(
backgroundColor: colorTheme.barsBg,
shareIconColor: colorTheme.textHighEmphasis,
titleTextStyle: textTheme.headlineBold,
@@ -345,52 +346,52 @@ class StreamChatThemeData {
bottomSheetPhotosTextStyle: textTheme.headlineBold,
bottomSheetCloseIconColor: colorTheme.textHighEmphasis,
),
messageListViewTheme: MessageListViewThemeData(
messageListViewTheme: StreamMessageListViewThemeData(
backgroundColor: colorTheme.barsBg,
),
channelListViewTheme: ChannelListViewThemeData(
channelListViewTheme: StreamChannelListViewThemeData(
backgroundColor: colorTheme.appBg,
),
userListViewTheme: UserListViewThemeData(
userListViewTheme: StreamUserListViewThemeData(
backgroundColor: colorTheme.appBg,
),
messageSearchListViewTheme: MessageSearchListViewThemeData(
messageSearchListViewTheme: StreamMessageSearchListViewThemeData(
backgroundColor: colorTheme.appBg,
),
);
}
/// The text themes used in the widgets
final TextTheme textTheme;
final StreamTextTheme textTheme;
/// The color themes used in the widgets
final ColorTheme colorTheme;
final StreamColorTheme colorTheme;
/// Theme of the [ChannelPreview]
final ChannelPreviewThemeData channelPreviewTheme;
/// Theme of the [StreamChannelPreview]
final StreamChannelPreviewThemeData channelPreviewTheme;
/// Theme of the [ChannelListHeader]
final ChannelListHeaderThemeData channelListHeaderTheme;
/// Theme of the [StreamChannelListHeader]
final StreamChannelListHeaderThemeData channelListHeaderTheme;
/// Theme of the chat widgets dedicated to a channel header
final ChannelHeaderThemeData channelHeaderTheme;
final StreamChannelHeaderThemeData channelHeaderTheme;
/// The default style for [GalleryHeader]s below the overall
/// The default style for [StreamGalleryHeader]s below the overall
/// [StreamChatTheme].
final GalleryHeaderThemeData galleryHeaderTheme;
final StreamGalleryHeaderThemeData galleryHeaderTheme;
/// The default style for [GalleryFooter]s below the overall
/// The default style for [StreamGalleryFooter]s below the overall
/// [StreamChatTheme].
final GalleryFooterThemeData galleryFooterTheme;
final StreamGalleryFooterThemeData galleryFooterTheme;
/// Theme of the current user messages
final MessageThemeData ownMessageTheme;
final StreamMessageThemeData ownMessageTheme;
/// Theme of other users messages
final MessageThemeData otherMessageTheme;
final StreamMessageThemeData otherMessageTheme;
/// Theme dedicated to the [MessageInput] widget
final MessageInputThemeData messageInputTheme;
/// Theme dedicated to the [StreamMessageInput] widget
final StreamMessageInputThemeData messageInputTheme;
/// The widget that will be built when the user image is unavailable
final Widget Function(BuildContext, User) defaultUserImage;
@@ -402,41 +403,41 @@ class StreamChatThemeData {
final IconThemeData primaryIconTheme;
/// Assets used for rendering reactions
final List<ReactionIcon> reactionIcons;
final List<StreamReactionIcon> reactionIcons;
/// Theme configuration for the [MessageListView] widget.
final MessageListViewThemeData messageListViewTheme;
/// Theme configuration for the [StreamMessageListView] widget.
final StreamMessageListViewThemeData messageListViewTheme;
/// Theme configuration for the [ChannelListView] widget.
final ChannelListViewThemeData channelListViewTheme;
/// Theme configuration for the [StreamChannelListView] widget.
final StreamChannelListViewThemeData channelListViewTheme;
/// Theme configuration for the [UserListView] widget.
final UserListViewThemeData userListViewTheme;
/// Theme configuration for the [StreamUserListView] widget.
final StreamUserListViewThemeData userListViewTheme;
/// Theme configuration for the [MessageSearchListView] widget.
final MessageSearchListViewThemeData messageSearchListViewTheme;
/// Theme configuration for the [StreamMessageSearchListView] widget.
final StreamMessageSearchListViewThemeData messageSearchListViewTheme;
/// Creates a copy of [StreamChatThemeData] with specified attributes
/// overridden.
StreamChatThemeData copyWith({
TextTheme? textTheme,
ColorTheme? colorTheme,
ChannelPreviewThemeData? channelPreviewTheme,
ChannelHeaderThemeData? channelHeaderTheme,
MessageThemeData? ownMessageTheme,
MessageThemeData? otherMessageTheme,
MessageInputThemeData? messageInputTheme,
StreamTextTheme? textTheme,
StreamColorTheme? colorTheme,
StreamChannelPreviewThemeData? channelPreviewTheme,
StreamChannelHeaderThemeData? channelHeaderTheme,
StreamMessageThemeData? ownMessageTheme,
StreamMessageThemeData? otherMessageTheme,
StreamMessageInputThemeData? messageInputTheme,
Widget Function(BuildContext, User)? defaultUserImage,
Widget Function(BuildContext, User)? placeholderUserImage,
IconThemeData? primaryIconTheme,
ChannelListHeaderThemeData? channelListHeaderTheme,
List<ReactionIcon>? reactionIcons,
GalleryHeaderThemeData? galleryHeaderTheme,
GalleryFooterThemeData? galleryFooterTheme,
MessageListViewThemeData? messageListViewTheme,
ChannelListViewThemeData? channelListViewTheme,
UserListViewThemeData? userListViewTheme,
MessageSearchListViewThemeData? messageSearchListViewTheme,
StreamChannelListHeaderThemeData? channelListHeaderTheme,
List<StreamReactionIcon>? reactionIcons,
StreamGalleryHeaderThemeData? galleryHeaderTheme,
StreamGalleryFooterThemeData? galleryFooterTheme,
StreamMessageListViewThemeData? messageListViewTheme,
StreamChannelListViewThemeData? channelListViewTheme,
StreamUserListViewThemeData? userListViewTheme,
StreamMessageSearchListViewThemeData? messageSearchListViewTheme,
}) =>
StreamChatThemeData.raw(
channelListHeaderTheme:
@@ -1,10 +1,16 @@
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// It shows a date divider depending on the date difference
class SystemMessage extends StatelessWidget {
/// Constructor for creating a [SystemMessage]
const SystemMessage({
/// {@macro system_message}
@Deprecated("Use 'StreamSystemMessage' instead")
typedef SystemMessage = StreamSystemMessage;
/// {@template system_message}
/// It shows a widget for the message with a system message type.
/// {@endtemplate}
class StreamSystemMessage extends StatelessWidget {
/// Constructor for creating a [StreamSystemMessage]
const StreamSystemMessage({
Key? key,
required this.message,
this.onMessageTap,
@@ -1,11 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// {@macro avatar_theme_data}
@Deprecated("Use 'StreamAvatarThemeData' instead")
typedef AvatarThemeData = StreamAvatarThemeData;
/// {@template avatar_theme_data}
/// A style that overrides the default appearance of various avatar widgets.
/// {@endtemplate}
// ignore: prefer-match-file-name
class AvatarThemeData with Diagnosticable {
/// Creates an [AvatarThemeData].
const AvatarThemeData({
class StreamAvatarThemeData with Diagnosticable {
/// Creates an [StreamAvatarThemeData].
const StreamAvatarThemeData({
BoxConstraints? constraints,
BorderRadius? borderRadius,
}) : _constraints = constraints,
@@ -25,12 +31,12 @@ class AvatarThemeData with Diagnosticable {
/// Get border radius
BorderRadius get borderRadius => _borderRadius ?? BorderRadius.circular(20);
/// Copy this [AvatarThemeData] to another.
AvatarThemeData copyWith({
/// Copy this [StreamAvatarThemeData] to another.
StreamAvatarThemeData copyWith({
BoxConstraints? constraints,
BorderRadius? borderRadius,
}) =>
AvatarThemeData(
StreamAvatarThemeData(
constraints: constraints ?? _constraints,
borderRadius: borderRadius ?? _borderRadius,
);
@@ -38,12 +44,12 @@ class AvatarThemeData with Diagnosticable {
/// Linearly interpolate between two [UserAvatar] themes.
///
/// All the properties must be non-null.
AvatarThemeData lerp(
AvatarThemeData a,
AvatarThemeData b,
StreamAvatarThemeData lerp(
StreamAvatarThemeData a,
StreamAvatarThemeData b,
double t,
) =>
AvatarThemeData(
StreamAvatarThemeData(
borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t),
constraints: BoxConstraints.lerp(a.constraints, b.constraints, t),
);
@@ -51,7 +57,7 @@ class AvatarThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AvatarThemeData &&
other is StreamAvatarThemeData &&
runtimeType == other.runtimeType &&
_constraints == other._constraints &&
_borderRadius == other._borderRadius;
@@ -59,8 +65,8 @@ class AvatarThemeData with Diagnosticable {
@override
int get hashCode => _constraints.hashCode ^ _borderRadius.hashCode;
/// Merges one [AvatarThemeData] with the another
AvatarThemeData merge(AvatarThemeData? other) {
/// Merges one [StreamAvatarThemeData] with the another
StreamAvatarThemeData merge(StreamAvatarThemeData? other) {
if (other == null) return this;
return copyWith(
constraints: other._constraints,
@@ -4,27 +4,33 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart';
/// {@macro channel_header_theme}
@Deprecated("Use 'StreamChannelHeaderTheme' instead")
typedef ChannelHeaderTheme = StreamChannelHeaderTheme;
/// {@template channel_header_theme}
/// Overrides the default style of [ChannelHeader] descendants.
///
/// See also:
///
/// * [ChannelHeaderThemeData], which is used to configure this theme.
class ChannelHeaderTheme extends InheritedTheme {
/// Creates a [ChannelHeaderTheme].
/// * [StreamChannelHeaderThemeData], which is used to configure this theme.
/// {@endtemplate}
class StreamChannelHeaderTheme extends InheritedTheme {
/// Creates a [StreamChannelHeaderTheme].
///
/// The [data] parameter must not be null.
const ChannelHeaderTheme({
const StreamChannelHeaderTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final ChannelHeaderThemeData data;
final StreamChannelHeaderThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [ChannelHeaderTheme] widget, then
/// If there is no enclosing [StreamChannelHeaderTheme] widget, then
/// [StreamChatThemeData.channelTheme.channelHeaderTheme] is used.
///
/// Typical usage is as follows:
@@ -32,34 +38,40 @@ class ChannelHeaderTheme extends InheritedTheme {
/// ```dart
/// final theme = ChannelHeaderTheme.of(context);
/// ```
static ChannelHeaderThemeData of(BuildContext context) {
static StreamChannelHeaderThemeData of(BuildContext context) {
final channelHeaderTheme =
context.dependOnInheritedWidgetOfExactType<ChannelHeaderTheme>();
context.dependOnInheritedWidgetOfExactType<StreamChannelHeaderTheme>();
return channelHeaderTheme?.data ??
StreamChatTheme.of(context).channelHeaderTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
ChannelHeaderTheme(data: data, child: child);
StreamChannelHeaderTheme(data: data, child: child);
@override
bool updateShouldNotify(ChannelHeaderTheme oldWidget) =>
bool updateShouldNotify(StreamChannelHeaderTheme oldWidget) =>
data != oldWidget.data;
}
/// {@macro channel_header_theme_data}
@Deprecated("Use 'StreamChannelHeaderThemeData' instead")
typedef ChannelHeaderThemeData = StreamChannelHeaderThemeData;
/// {@template channel_header_theme_data}
/// A style that overrides the default appearance of [ChannelHeader]s when used
/// with [ChannelHeaderTheme] or with the overall [StreamChatTheme]'s
/// with [StreamChannelHeaderTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.channelHeaderTheme].
///
/// See also:
///
/// * [ChannelHeaderTheme], the theme which is configured with this class.
/// * [StreamChannelHeaderTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.channelHeaderTheme], which can be used to override
/// the default style for [ChannelHeader]s below the overall [StreamChatTheme].
class ChannelHeaderThemeData with Diagnosticable {
/// Creates a [ChannelHeaderThemeData]
const ChannelHeaderThemeData({
/// {@endtemplate}
class StreamChannelHeaderThemeData with Diagnosticable {
/// Creates a [StreamChannelHeaderThemeData]
const StreamChannelHeaderThemeData({
this.titleStyle,
this.subtitleStyle,
this.avatarTheme,
@@ -73,43 +85,43 @@ class ChannelHeaderThemeData with Diagnosticable {
final TextStyle? subtitleStyle;
/// Theme for avatar
final AvatarThemeData? avatarTheme;
final StreamAvatarThemeData? avatarTheme;
/// Color for [ChannelHeaderThemeData]
/// Color for [StreamChannelHeaderThemeData]
final Color? color;
/// Copy with theme
ChannelHeaderThemeData copyWith({
StreamChannelHeaderThemeData copyWith({
TextStyle? titleStyle,
TextStyle? subtitleStyle,
AvatarThemeData? avatarTheme,
StreamAvatarThemeData? avatarTheme,
Color? color,
}) =>
ChannelHeaderThemeData(
StreamChannelHeaderThemeData(
titleStyle: titleStyle ?? this.titleStyle,
subtitleStyle: subtitleStyle ?? this.subtitleStyle,
avatarTheme: avatarTheme ?? this.avatarTheme,
color: color ?? this.color,
);
/// Linearly interpolate between two [ChannelHeaderThemeData].
/// Linearly interpolate between two [StreamChannelHeaderThemeData].
///
/// All the properties must be non-null.
ChannelHeaderThemeData lerp(
ChannelHeaderThemeData a,
ChannelHeaderThemeData b,
StreamChannelHeaderThemeData lerp(
StreamChannelHeaderThemeData a,
StreamChannelHeaderThemeData b,
double t,
) =>
ChannelHeaderThemeData(
StreamChannelHeaderThemeData(
titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t),
subtitleStyle: TextStyle.lerp(a.subtitleStyle, b.subtitleStyle, t),
avatarTheme:
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t),
avatarTheme: const StreamAvatarThemeData()
.lerp(a.avatarTheme!, b.avatarTheme!, t),
color: Color.lerp(a.color, b.color, t),
);
/// Merge with other [ChannelHeaderThemeData]
ChannelHeaderThemeData merge(ChannelHeaderThemeData? other) {
/// Merge with other [StreamChannelHeaderThemeData]
StreamChannelHeaderThemeData merge(StreamChannelHeaderThemeData? other) {
if (other == null) return this;
return copyWith(
titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle,
@@ -123,7 +135,7 @@ class ChannelHeaderThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ChannelHeaderThemeData &&
other is StreamChannelHeaderThemeData &&
runtimeType == other.runtimeType &&
titleStyle == other.titleStyle &&
subtitleStyle == other.subtitleStyle &&
@@ -3,27 +3,33 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
/// {@macro channel_list_header_theme}
@Deprecated("Use 'StreamChannelListHeaderTheme' instead")
typedef ChannelListHeaderTheme = StreamChannelListHeaderTheme;
/// {@template channel_list_header_theme}
/// Overrides the default style of [ChannelListHeader] descendants.
///
/// See also:
///
/// * [ChannelListHeaderThemeData], which is used to configure this theme.
class ChannelListHeaderTheme extends InheritedTheme {
/// Creates a [ChannelListHeaderTheme].
/// * [StreamChannelListHeaderThemeData], which is used to configure this theme.
/// {@endtemplate}
class StreamChannelListHeaderTheme extends InheritedTheme {
/// Creates a [StreamChannelListHeaderTheme].
///
/// The [data] parameter must not be null.
const ChannelListHeaderTheme({
const StreamChannelListHeaderTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final ChannelListHeaderThemeData data;
final StreamChannelListHeaderThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [ChannelListHeaderTheme] widget, then
/// If there is no enclosing [StreamChannelListHeaderTheme] widget, then
/// [StreamChatThemeData.channelListHeaderTheme] is used.
///
/// Typical usage is as follows:
@@ -31,26 +37,32 @@ class ChannelListHeaderTheme extends InheritedTheme {
/// ```dart
/// final theme = ChannelListHeaderTheme.of(context);
/// ```
static ChannelListHeaderThemeData of(BuildContext context) {
final channelListHeaderTheme =
context.dependOnInheritedWidgetOfExactType<ChannelListHeaderTheme>();
static StreamChannelListHeaderThemeData of(BuildContext context) {
final channelListHeaderTheme = context
.dependOnInheritedWidgetOfExactType<StreamChannelListHeaderTheme>();
return channelListHeaderTheme?.data ??
StreamChatTheme.of(context).channelListHeaderTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
ChannelListHeaderTheme(data: data, child: child);
StreamChannelListHeaderTheme(data: data, child: child);
@override
bool updateShouldNotify(ChannelListHeaderTheme oldWidget) =>
bool updateShouldNotify(StreamChannelListHeaderTheme oldWidget) =>
data != oldWidget.data;
}
/// {@macro channel_list_header_theme_data}
@Deprecated("Use ''StreamChannelListHeaderThemeData' instead")
typedef ChannelListHeaderThemeData = StreamChannelListHeaderThemeData;
/// {@template channel_list_header_theme_data}
/// Theme dedicated to the [ChannelListHeader]
class ChannelListHeaderThemeData with Diagnosticable {
/// Returns a new [ChannelListHeaderThemeData]
const ChannelListHeaderThemeData({
/// {@endtemplate}
class StreamChannelListHeaderThemeData with Diagnosticable {
/// Returns a new [StreamChannelListHeaderThemeData]
const StreamChannelListHeaderThemeData({
this.titleStyle,
this.avatarTheme,
this.color,
@@ -60,39 +72,41 @@ class ChannelListHeaderThemeData with Diagnosticable {
final TextStyle? titleStyle;
/// Theme dedicated to the userAvatar
final AvatarThemeData? avatarTheme;
final StreamAvatarThemeData? avatarTheme;
/// Background color of the appbar
final Color? color;
/// Returns a new [ChannelListHeaderThemeData] replacing some of its
/// Returns a new [StreamChannelListHeaderThemeData] replacing some of its
/// properties
ChannelListHeaderThemeData copyWith({
StreamChannelListHeaderThemeData copyWith({
TextStyle? titleStyle,
AvatarThemeData? avatarTheme,
StreamAvatarThemeData? avatarTheme,
Color? color,
}) =>
ChannelListHeaderThemeData(
StreamChannelListHeaderThemeData(
titleStyle: titleStyle ?? this.titleStyle,
avatarTheme: avatarTheme ?? this.avatarTheme,
color: color ?? this.color,
);
/// Linearly interpolate from one [ChannelListHeaderThemeData] to another.
ChannelListHeaderThemeData lerp(
ChannelListHeaderThemeData a,
ChannelListHeaderThemeData b,
/// Linearly interpolate from one [StreamChannelListHeaderThemeData] to another.
StreamChannelListHeaderThemeData lerp(
StreamChannelListHeaderThemeData a,
StreamChannelListHeaderThemeData b,
double t,
) =>
ChannelListHeaderThemeData(
avatarTheme:
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t),
StreamChannelListHeaderThemeData(
avatarTheme: const StreamAvatarThemeData()
.lerp(a.avatarTheme!, b.avatarTheme!, t),
color: Color.lerp(a.color, b.color, t),
titleStyle: TextStyle.lerp(a.titleStyle, b.titleStyle, t),
);
/// Merges [this] [ChannelListHeaderThemeData] with the [other]
ChannelListHeaderThemeData merge(ChannelListHeaderThemeData? other) {
/// Merges [this] [StreamChannelListHeaderThemeData] with the [other]
StreamChannelListHeaderThemeData merge(
StreamChannelListHeaderThemeData? other,
) {
if (other == null) return this;
return copyWith(
titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle,
@@ -104,7 +118,7 @@ class ChannelListHeaderThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ChannelListHeaderThemeData &&
other is StreamChannelListHeaderThemeData &&
runtimeType == other.runtimeType &&
titleStyle == other.titleStyle &&
avatarTheme == other.avatarTheme &&
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro channel_list_view_theme}
@Deprecated("Use 'StreamChannelListViewTheme' instead")
typedef ChannelListViewTheme = StreamChannelListViewTheme;
/// {@template channel_list_view_theme}
/// Overrides the default style of [ChannelListView] descendants.
///
/// See also:
///
/// * [ChannelListViewThemeData], which is used to configure this theme.
class ChannelListViewTheme extends InheritedTheme {
/// Creates a [ChannelListViewTheme].
/// * [StreamChannelListViewThemeData], which is used to configure this theme.
/// {@endtemplate}
class StreamChannelListViewTheme extends InheritedTheme {
/// Creates a [StreamChannelListViewTheme].
///
/// The [data] parameter must not be null.
const ChannelListViewTheme({
const StreamChannelListViewTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final ChannelListViewThemeData data;
final StreamChannelListViewThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [ChannelListViewTheme] widget, then
/// If there is no enclosing [StreamChannelListViewTheme] widget, then
/// [StreamChatThemeData.channelListViewTheme] is used.
///
/// Typical usage is as follows:
@@ -30,63 +36,69 @@ class ChannelListViewTheme extends InheritedTheme {
/// ```dart
/// ChannelListViewTheme theme = ChannelListViewTheme.of(context);
/// ```
static ChannelListViewThemeData of(BuildContext context) {
final channelListViewTheme =
context.dependOnInheritedWidgetOfExactType<ChannelListViewTheme>();
static StreamChannelListViewThemeData of(BuildContext context) {
final channelListViewTheme = context
.dependOnInheritedWidgetOfExactType<StreamChannelListViewTheme>();
return channelListViewTheme?.data ??
StreamChatTheme.of(context).channelListViewTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
ChannelListViewTheme(data: data, child: child);
StreamChannelListViewTheme(data: data, child: child);
@override
bool updateShouldNotify(ChannelListViewTheme oldWidget) =>
bool updateShouldNotify(StreamChannelListViewTheme oldWidget) =>
data != oldWidget.data;
}
/// {@macro channel_list_view_theme_data}
@Deprecated("Use 'StreamChannelListViewThemeData' instead")
typedef ChannelListViewThemeData = StreamChannelListViewThemeData;
/// {@template channel_list_view_theme_data}
/// A style that overrides the default appearance of [ChannelListView]s when
/// used with [ChannelListViewTheme] or with the overall [StreamChatTheme]'s
/// used with [StreamChannelListViewTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.channelListViewTheme].
///
/// See also:
///
/// * [ChannelListViewTheme], the theme which is configured with this class.
/// * [StreamChannelListViewTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.channelListViewTheme], which can be used to override
/// the default style for [ChannelListView]s below the overall
/// [StreamChatTheme].
class ChannelListViewThemeData with Diagnosticable {
/// Creates a [ChannelListViewThemeData].
const ChannelListViewThemeData({
/// {@endtemplate}
class StreamChannelListViewThemeData with Diagnosticable {
/// Creates a [StreamChannelListViewThemeData].
const StreamChannelListViewThemeData({
this.backgroundColor,
});
/// The color of the [ChannelListView] background.
final Color? backgroundColor;
/// Copies this [ChannelListViewThemeData] to another.
ChannelListViewThemeData copyWith({
/// Copies this [StreamChannelListViewThemeData] to another.
StreamChannelListViewThemeData copyWith({
Color? backgroundColor,
}) =>
ChannelListViewThemeData(
StreamChannelListViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
);
/// Linearly interpolate between two [ChannelListViewThemeData] themes.
/// Linearly interpolate between two [StreamChannelListViewThemeData] themes.
///
/// All the properties must be non-null.
ChannelListViewThemeData lerp(
ChannelListViewThemeData a,
ChannelListViewThemeData b,
StreamChannelListViewThemeData lerp(
StreamChannelListViewThemeData a,
StreamChannelListViewThemeData b,
double t,
) =>
ChannelListViewThemeData(
StreamChannelListViewThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
);
/// Merges one [ChannelListViewThemeData] with another.
ChannelListViewThemeData merge(ChannelListViewThemeData? other) {
/// Merges one [StreamChannelListViewThemeData] with another.
StreamChannelListViewThemeData merge(StreamChannelListViewThemeData? other) {
if (other == null) return this;
return copyWith(
backgroundColor: other.backgroundColor,
@@ -96,7 +108,7 @@ class ChannelListViewThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ChannelListViewThemeData &&
other is StreamChannelListViewThemeData &&
runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor;
@@ -3,27 +3,33 @@ import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/theme/avatar_theme.dart';
/// {@macro channel_preview_theme}
@Deprecated("Use 'StreamChannelPreviewTheme' instead")
typedef ChannelPreviewTheme = StreamChannelPreviewTheme;
/// {@template channel_preview_theme}
/// Overrides the default style of [ChannelPreview] descendants.
///
/// See also:
///
/// * [ChannelPreviewThemeData], which is used to configure this theme.
class ChannelPreviewTheme extends InheritedTheme {
/// Creates a [ChannelPreviewTheme].
/// * [StreamChannelPreviewThemeData], which is used to configure this theme.
/// {@endtemplate}
class StreamChannelPreviewTheme extends InheritedTheme {
/// Creates a [StreamChannelPreviewTheme].
///
/// The [data] parameter must not be null.
const ChannelPreviewTheme({
const StreamChannelPreviewTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final ChannelPreviewThemeData data;
final StreamChannelPreviewThemeData data;
/// The closest instance of this class that encloses the given context.
///
/// If there is no enclosing [ChannelPreviewTheme] widget, then
/// If there is no enclosing [StreamChannelPreviewTheme] widget, then
/// [StreamChatThemeData.channelPreviewTheme] is used.
///
/// Typical usage is as follows:
@@ -31,34 +37,40 @@ class ChannelPreviewTheme extends InheritedTheme {
/// ```dart
/// final theme = ChannelPreviewTheme.of(context);
/// ```
static ChannelPreviewThemeData of(BuildContext context) {
static StreamChannelPreviewThemeData of(BuildContext context) {
final channelPreviewTheme =
context.dependOnInheritedWidgetOfExactType<ChannelPreviewTheme>();
context.dependOnInheritedWidgetOfExactType<StreamChannelPreviewTheme>();
return channelPreviewTheme?.data ??
StreamChatTheme.of(context).channelPreviewTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
ChannelPreviewTheme(data: data, child: child);
StreamChannelPreviewTheme(data: data, child: child);
@override
bool updateShouldNotify(ChannelPreviewTheme oldWidget) =>
bool updateShouldNotify(StreamChannelPreviewTheme oldWidget) =>
data != oldWidget.data;
}
/// {@macro channel_preview_theme_data}
@Deprecated("Use 'StreamChannelPreviewThemeData' instead")
typedef ChannelPreviewThemeData = StreamChannelPreviewThemeData;
/// {@template channel_preview_theme_data}
/// A style that overrides the default appearance of [ChannelPreview]s when used
/// with [ChannelPreviewTheme] or with the overall [StreamChatTheme]'s
/// with [StreamChannelPreviewTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.channelPreviewTheme].
///
/// See also:
///
/// * [ChannelPreviewTheme], the theme which is configured with this class.
/// * [StreamChannelPreviewTheme], the theme which is configured with this class.
/// * [StreamChatThemeData.channelPreviewTheme], which can be used to override
/// the default style for [ChannelHeader]s below the overall [StreamChatTheme].
class ChannelPreviewThemeData with Diagnosticable {
/// Creates a [ChannelPreviewThemeData].
const ChannelPreviewThemeData({
/// {@endtemplate}
class StreamChannelPreviewThemeData with Diagnosticable {
/// Creates a [StreamChannelPreviewThemeData].
const StreamChannelPreviewThemeData({
this.titleStyle,
this.subtitleStyle,
this.lastMessageAtStyle,
@@ -77,7 +89,7 @@ class ChannelPreviewThemeData with Diagnosticable {
final TextStyle? lastMessageAtStyle;
/// Avatar theme
final AvatarThemeData? avatarTheme;
final StreamAvatarThemeData? avatarTheme;
/// Unread counter color
final Color? unreadCounterColor;
@@ -86,15 +98,15 @@ class ChannelPreviewThemeData with Diagnosticable {
final double? indicatorIconSize;
/// Copy with theme
ChannelPreviewThemeData copyWith({
StreamChannelPreviewThemeData copyWith({
TextStyle? titleStyle,
TextStyle? subtitleStyle,
TextStyle? lastMessageAtStyle,
AvatarThemeData? avatarTheme,
StreamAvatarThemeData? avatarTheme,
Color? unreadCounterColor,
double? indicatorIconSize,
}) =>
ChannelPreviewThemeData(
StreamChannelPreviewThemeData(
titleStyle: titleStyle ?? this.titleStyle,
subtitleStyle: subtitleStyle ?? this.subtitleStyle,
lastMessageAtStyle: lastMessageAtStyle ?? this.lastMessageAtStyle,
@@ -103,15 +115,15 @@ class ChannelPreviewThemeData with Diagnosticable {
indicatorIconSize: indicatorIconSize ?? this.indicatorIconSize,
);
/// Linearly interpolate one [ChannelPreviewThemeData] to another.
ChannelPreviewThemeData lerp(
ChannelPreviewThemeData a,
ChannelPreviewThemeData b,
/// Linearly interpolate one [StreamChannelPreviewThemeData] to another.
StreamChannelPreviewThemeData lerp(
StreamChannelPreviewThemeData a,
StreamChannelPreviewThemeData b,
double t,
) =>
ChannelPreviewThemeData(
avatarTheme:
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t),
StreamChannelPreviewThemeData(
avatarTheme: const StreamAvatarThemeData()
.lerp(a.avatarTheme!, b.avatarTheme!, t),
indicatorIconSize: a.indicatorIconSize,
lastMessageAtStyle:
TextStyle.lerp(a.lastMessageAtStyle, b.lastMessageAtStyle, t),
@@ -122,7 +134,7 @@ class ChannelPreviewThemeData with Diagnosticable {
);
/// Merge with theme
ChannelPreviewThemeData merge(ChannelPreviewThemeData? other) {
StreamChannelPreviewThemeData merge(StreamChannelPreviewThemeData? other) {
if (other == null) return this;
return copyWith(
titleStyle: titleStyle?.merge(other.titleStyle) ?? other.titleStyle,
@@ -138,7 +150,7 @@ class ChannelPreviewThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ChannelPreviewThemeData &&
other is StreamChannelPreviewThemeData &&
runtimeType == other.runtimeType &&
titleStyle == other.titleStyle &&
subtitleStyle == other.subtitleStyle &&
@@ -1,9 +1,15 @@
import 'package:flutter/material.dart';
/// {@macro color_theme}
@Deprecated("Use 'StreamColorTheme' instead")
typedef ColorTheme = StreamColorTheme;
/// {@template color_theme}
/// Theme that holds colors
class ColorTheme {
/// {@endtemplate}
class StreamColorTheme {
/// Initialise with light theme
ColorTheme.light({
StreamColorTheme.light({
this.textHighEmphasis = const Color(0xff000000),
this.textLowEmphasis = const Color(0xff7a7a7a),
this.disabled = const Color(0xffdbdbdb),
@@ -55,7 +61,7 @@ class ColorTheme {
}) : brightness = Brightness.light;
/// Initialise with dark theme
ColorTheme.dark({
StreamColorTheme.dark({
this.textHighEmphasis = const Color(0xffffffff),
this.textLowEmphasis = const Color(0xff7a7a7a),
this.disabled = const Color(0xff2d2f2f),
@@ -169,7 +175,7 @@ class ColorTheme {
final Brightness brightness;
/// Copy with theme
ColorTheme copyWith({
StreamColorTheme copyWith({
Brightness brightness = Brightness.light,
Color? textHighEmphasis,
Color? textLowEmphasis,
@@ -192,7 +198,7 @@ class ColorTheme {
Gradient? bgGradient,
}) =>
brightness == Brightness.light
? ColorTheme.light(
? StreamColorTheme.light(
textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis,
textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis,
disabled: disabled ?? this.disabled,
@@ -213,7 +219,7 @@ class ColorTheme {
overlayDark: overlayDark ?? this.overlayDark,
bgGradient: bgGradient ?? this.bgGradient,
)
: ColorTheme.dark(
: StreamColorTheme.dark(
textHighEmphasis: textHighEmphasis ?? this.textHighEmphasis,
textLowEmphasis: textLowEmphasis ?? this.textLowEmphasis,
disabled: disabled ?? this.disabled,
@@ -236,7 +242,7 @@ class ColorTheme {
);
/// Merge color theme
ColorTheme merge(ColorTheme? other) {
StreamColorTheme merge(StreamColorTheme? other) {
if (other == null) return this;
return copyWith(
textHighEmphasis: other.textHighEmphasis,
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro gallery_footer_theme}
@Deprecated("Use 'StreamGalleryFooterTheme' instead")
typedef GalleryFooterTheme = StreamGalleryFooterTheme;
/// {@template gallery_footer_theme}
/// Overrides the default style of [GalleryFooter] descendants.
///
/// See also:
///
/// * [GalleryFooterThemeData], which is used to configure this theme.
class GalleryFooterTheme extends InheritedTheme {
/// Creates an [GalleryFooterTheme].
/// * [StreamGalleryFooterThemeData], which is used to configure this theme.
/// {@endtemplate}
class StreamGalleryFooterTheme extends InheritedTheme {
/// Creates an [StreamGalleryFooterTheme].
///
/// The [data] parameter must not be null.
const GalleryFooterTheme({
const StreamGalleryFooterTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final GalleryFooterThemeData data;
final StreamGalleryFooterThemeData data;
/// 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.
///
/// Typical usage is as follows:
@@ -30,34 +36,40 @@ class GalleryFooterTheme extends InheritedTheme {
/// ```dart
/// ImageFooterTheme theme = ImageFooterTheme.of(context);
/// ```
static GalleryFooterThemeData of(BuildContext context) {
static StreamGalleryFooterThemeData of(BuildContext context) {
final imageFooterTheme =
context.dependOnInheritedWidgetOfExactType<GalleryFooterTheme>();
context.dependOnInheritedWidgetOfExactType<StreamGalleryFooterTheme>();
return imageFooterTheme?.data ??
StreamChatTheme.of(context).galleryFooterTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
GalleryFooterTheme(data: data, child: child);
StreamGalleryFooterTheme(data: data, child: child);
@override
bool updateShouldNotify(GalleryFooterTheme oldWidget) =>
bool updateShouldNotify(StreamGalleryFooterTheme oldWidget) =>
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
/// with [GalleryFooterTheme] or with the overall [StreamChatTheme]'s
/// with [StreamGalleryFooterTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.galleryFooterTheme].
///
/// 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
/// the default style for [GalleryFooter]s below the overall [StreamChatTheme].
class GalleryFooterThemeData with Diagnosticable {
/// Creates an [GalleryFooterThemeData].
const GalleryFooterThemeData({
/// {@endtemplate}
class StreamGalleryFooterThemeData with Diagnosticable {
/// Creates an [StreamGalleryFooterThemeData].
const StreamGalleryFooterThemeData({
this.backgroundColor,
this.shareIconColor,
this.titleTextStyle,
@@ -108,8 +120,8 @@ class GalleryFooterThemeData with Diagnosticable {
/// Defaults to [ColorTheme.textHighEmphasis].
final Color? bottomSheetCloseIconColor;
/// Copies this [GalleryFooterThemeData] to another.
GalleryFooterThemeData copyWith({
/// Copies this [StreamGalleryFooterThemeData] to another.
StreamGalleryFooterThemeData copyWith({
Color? backgroundColor,
Color? shareIconColor,
TextStyle? titleTextStyle,
@@ -119,7 +131,7 @@ class GalleryFooterThemeData with Diagnosticable {
TextStyle? bottomSheetPhotosTextStyle,
Color? bottomSheetCloseIconColor,
}) =>
GalleryFooterThemeData(
StreamGalleryFooterThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
shareIconColor: shareIconColor ?? this.shareIconColor,
titleTextStyle: titleTextStyle ?? this.titleTextStyle,
@@ -137,12 +149,12 @@ class GalleryFooterThemeData with Diagnosticable {
/// Linearly interpolate between two [GalleryFooter] themes.
///
/// All the properties must be non-null.
GalleryFooterThemeData lerp(
GalleryFooterThemeData a,
GalleryFooterThemeData b,
StreamGalleryFooterThemeData lerp(
StreamGalleryFooterThemeData a,
StreamGalleryFooterThemeData b,
double t,
) =>
GalleryFooterThemeData(
StreamGalleryFooterThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
shareIconColor: Color.lerp(a.shareIconColor, b.shareIconColor, t),
titleTextStyle: TextStyle.lerp(a.titleTextStyle, b.titleTextStyle, t),
@@ -167,8 +179,8 @@ class GalleryFooterThemeData with Diagnosticable {
),
);
/// Merges one [GalleryFooterThemeData] with another.
GalleryFooterThemeData merge(GalleryFooterThemeData? other) {
/// Merges one [StreamGalleryFooterThemeData] with another.
StreamGalleryFooterThemeData merge(StreamGalleryFooterThemeData? other) {
if (other == null) return this;
return copyWith(
backgroundColor: other.backgroundColor,
@@ -185,7 +197,7 @@ class GalleryFooterThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GalleryFooterThemeData &&
other is StreamGalleryFooterThemeData &&
runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor &&
shareIconColor == other.shareIconColor &&
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro gallery_header_them}
@Deprecated("Use 'StreamGalleryHeaderTheme' instead")
typedef GalleryHeaderTheme = StreamGalleryHeaderTheme;
/// {@template gallery_header_theme}
/// Overrides the default style of [GalleryHeader] descendants.
///
/// See also:
///
/// * [GalleryHeaderThemeData], which is used to configure this theme.
class GalleryHeaderTheme extends InheritedTheme {
/// Creates a [GalleryHeaderTheme].
/// * [StreamGalleryHeaderThemeData], which is used to configure this theme.
/// {@endtemplate}
class StreamGalleryHeaderTheme extends InheritedTheme {
/// Creates a [StreamGalleryHeaderTheme].
///
/// The [data] parameter must not be null.
const GalleryHeaderTheme({
const StreamGalleryHeaderTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final GalleryHeaderThemeData data;
final StreamGalleryHeaderThemeData data;
/// 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.
///
/// Typical usage is as follows:
@@ -30,34 +36,40 @@ class GalleryHeaderTheme extends InheritedTheme {
/// ```dart
/// ImageHeaderTheme theme = ImageHeaderTheme.of(context);
/// ```
static GalleryHeaderThemeData of(BuildContext context) {
static StreamGalleryHeaderThemeData of(BuildContext context) {
final galleryHeaderTheme =
context.dependOnInheritedWidgetOfExactType<GalleryHeaderTheme>();
context.dependOnInheritedWidgetOfExactType<StreamGalleryHeaderTheme>();
return galleryHeaderTheme?.data ??
StreamChatTheme.of(context).galleryHeaderTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
GalleryHeaderTheme(data: data, child: child);
StreamGalleryHeaderTheme(data: data, child: child);
@override
bool updateShouldNotify(GalleryHeaderTheme oldWidget) =>
bool updateShouldNotify(StreamGalleryHeaderTheme oldWidget) =>
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
/// with [GalleryHeaderTheme] or with the overall [StreamChatTheme]'s
/// with [StreamGalleryHeaderTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.galleryHeaderTheme].
///
/// 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
/// the default style for [GalleryHeader]s below the overall [StreamChatTheme].
class GalleryHeaderThemeData with Diagnosticable {
/// Creates an [GalleryHeaderThemeData].
const GalleryHeaderThemeData({
/// {@endtemplate}
class StreamGalleryHeaderThemeData with Diagnosticable {
/// Creates an [StreamGalleryHeaderThemeData].
const StreamGalleryHeaderThemeData({
this.closeButtonColor,
this.backgroundColor,
this.iconMenuPointColor,
@@ -92,8 +104,8 @@ class GalleryHeaderThemeData with Diagnosticable {
///
final Color? bottomSheetBarrierColor;
/// Copies this [GalleryHeaderThemeData] to another.
GalleryHeaderThemeData copyWith({
/// Copies this [StreamGalleryHeaderThemeData] to another.
StreamGalleryHeaderThemeData copyWith({
Color? closeButtonColor,
Color? backgroundColor,
Color? iconMenuPointColor,
@@ -101,7 +113,7 @@ class GalleryHeaderThemeData with Diagnosticable {
TextStyle? subtitleTextStyle,
Color? bottomSheetBarrierColor,
}) =>
GalleryHeaderThemeData(
StreamGalleryHeaderThemeData(
closeButtonColor: closeButtonColor ?? this.closeButtonColor,
backgroundColor: backgroundColor ?? this.backgroundColor,
iconMenuPointColor: iconMenuPointColor ?? this.iconMenuPointColor,
@@ -114,12 +126,12 @@ class GalleryHeaderThemeData with Diagnosticable {
/// Linearly interpolate between two [GalleryHeader] themes.
///
/// All the properties must be non-null.
GalleryHeaderThemeData lerp(
GalleryHeaderThemeData a,
GalleryHeaderThemeData b,
StreamGalleryHeaderThemeData lerp(
StreamGalleryHeaderThemeData a,
StreamGalleryHeaderThemeData b,
double t,
) =>
GalleryHeaderThemeData(
StreamGalleryHeaderThemeData(
closeButtonColor: Color.lerp(a.closeButtonColor, b.closeButtonColor, t),
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
iconMenuPointColor:
@@ -131,8 +143,8 @@ class GalleryHeaderThemeData with Diagnosticable {
Color.lerp(a.bottomSheetBarrierColor, b.bottomSheetBarrierColor, t),
);
/// Merges one [GalleryHeaderThemeData] with the another
GalleryHeaderThemeData merge(GalleryHeaderThemeData? other) {
/// Merges one [StreamGalleryHeaderThemeData] with the another
StreamGalleryHeaderThemeData merge(StreamGalleryHeaderThemeData? other) {
if (other == null) return this;
return copyWith(
closeButtonColor: other.closeButtonColor,
@@ -147,7 +159,7 @@ class GalleryHeaderThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GalleryHeaderThemeData &&
other is StreamGalleryHeaderThemeData &&
runtimeType == other.runtimeType &&
closeButtonColor == other.closeButtonColor &&
backgroundColor == other.backgroundColor &&
@@ -1,29 +1,37 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/extension.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.
///
/// See also:
///
/// * [MessageInputThemeData], which is used to configure this theme.
class MessageInputTheme extends InheritedTheme {
/// Creates a [MessageInputTheme].
/// * [StreamMessageInputThemeData], which is used to configure this theme.
/// {@endtemplate}
class StreamMessageInputTheme extends InheritedTheme {
/// Creates a [StreamMessageInputTheme].
///
/// The [data] parameter must not be null.
const MessageInputTheme({
const StreamMessageInputTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final MessageInputThemeData data;
final StreamMessageInputThemeData data;
/// 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.
///
/// Typical usage is as follows:
@@ -31,28 +39,34 @@ class MessageInputTheme extends InheritedTheme {
/// ```dart
/// final theme = MessageInputTheme.of(context);
/// ```
static MessageInputThemeData of(BuildContext context) {
static StreamMessageInputThemeData of(BuildContext context) {
final messageInputTheme =
context.dependOnInheritedWidgetOfExactType<MessageInputTheme>();
context.dependOnInheritedWidgetOfExactType<StreamMessageInputTheme>();
return messageInputTheme?.data ??
StreamChatTheme.of(context).messageInputTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
MessageInputTheme(data: data, child: child);
StreamMessageInputTheme(data: data, child: child);
@override
bool updateShouldNotify(MessageInputTheme oldWidget) =>
bool updateShouldNotify(StreamMessageInputTheme oldWidget) =>
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
/// when used with [MessageInputTheme] or with the overall [StreamChatTheme]'s
/// when used with [StreamMessageInputTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.messageInputTheme].
class MessageInputThemeData with Diagnosticable {
/// Creates a [MessageInputThemeData].
const MessageInputThemeData({
/// {@endtemplate}
class StreamMessageInputThemeData with Diagnosticable {
/// Creates a [StreamMessageInputThemeData].
const StreamMessageInputThemeData({
this.sendAnimationDuration,
this.actionButtonColor,
this.sendButtonColor,
@@ -65,6 +79,10 @@ class MessageInputThemeData with Diagnosticable {
this.idleBorderGradient,
this.borderRadius,
this.expandButtonColor,
this.linkHighlightColor,
this.enableSafeArea,
this.elevation,
this.shadow,
});
/// Duration of the [MessageInput] send button animation
@@ -73,6 +91,9 @@ class MessageInputThemeData with Diagnosticable {
/// Background color of [MessageInput] send button
final Color? sendButtonColor;
/// Color of a link
final Color? linkHighlightColor;
/// Background color of [MessageInput] action buttons
final Color? actionButtonColor;
@@ -103,13 +124,23 @@ class MessageInputThemeData with Diagnosticable {
/// Border radius of [MessageInput]
final BorderRadius? borderRadius;
/// Returns a new [MessageInputThemeData] replacing some of its properties
MessageInputThemeData copyWith({
/// Wrap [MessageInput] with a [SafeArea widget]
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,
Color? inputBackgroundColor,
Color? actionButtonColor,
Color? sendButtonColor,
Color? actionButtonIdleColor,
Color? linkHighlightColor,
Color? sendButtonIdleColor,
Color? expandButtonColor,
TextStyle? inputTextStyle,
@@ -117,8 +148,11 @@ class MessageInputThemeData with Diagnosticable {
Gradient? activeBorderGradient,
Gradient? idleBorderGradient,
BorderRadius? borderRadius,
bool? enableSafeArea,
double? elevation,
BoxShadow? shadow,
}) =>
MessageInputThemeData(
StreamMessageInputThemeData(
sendAnimationDuration:
sendAnimationDuration ?? this.sendAnimationDuration,
inputBackgroundColor: inputBackgroundColor ?? this.inputBackgroundColor,
@@ -133,15 +167,19 @@ class MessageInputThemeData with Diagnosticable {
activeBorderGradient: activeBorderGradient ?? this.activeBorderGradient,
idleBorderGradient: idleBorderGradient ?? this.idleBorderGradient,
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.
MessageInputThemeData lerp(
MessageInputThemeData a,
MessageInputThemeData b,
/// Linearly interpolate from one [StreamMessageInputThemeData] to another.
StreamMessageInputThemeData lerp(
StreamMessageInputThemeData a,
StreamMessageInputThemeData b,
double t,
) =>
MessageInputThemeData(
StreamMessageInputThemeData(
actionButtonColor:
Color.lerp(a.actionButtonColor, b.actionButtonColor, t),
actionButtonIdleColor:
@@ -161,10 +199,15 @@ class MessageInputThemeData with Diagnosticable {
Color.lerp(a.sendButtonIdleColor, b.sendButtonIdleColor, t),
sendAnimationDuration: a.sendAnimationDuration,
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]
MessageInputThemeData merge(MessageInputThemeData? other) {
/// Merges [this] [StreamMessageInputThemeData] with the [other]
StreamMessageInputThemeData merge(StreamMessageInputThemeData? other) {
if (other == null) return this;
return copyWith(
sendAnimationDuration: other.sendAnimationDuration,
@@ -181,13 +224,17 @@ class MessageInputThemeData with Diagnosticable {
idleBorderGradient: other.idleBorderGradient,
borderRadius: other.borderRadius,
expandButtonColor: other.expandButtonColor,
linkHighlightColor: other.linkHighlightColor,
enableSafeArea: other.enableSafeArea,
elevation: other.elevation,
shadow: other.shadow,
);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MessageInputThemeData &&
other is StreamMessageInputThemeData &&
runtimeType == other.runtimeType &&
sendAnimationDuration == other.sendAnimationDuration &&
sendButtonColor == other.sendButtonColor &&
@@ -200,7 +247,11 @@ class MessageInputThemeData with Diagnosticable {
inputDecoration == other.inputDecoration &&
idleBorderGradient == other.idleBorderGradient &&
activeBorderGradient == other.activeBorderGradient &&
borderRadius == other.borderRadius;
borderRadius == other.borderRadius &&
linkHighlightColor == other.linkHighlightColor &&
enableSafeArea == other.enableSafeArea &&
elevation == other.elevation &&
shadow == other.shadow;
@override
int get hashCode =>
@@ -215,7 +266,11 @@ class MessageInputThemeData with Diagnosticable {
inputDecoration.hashCode ^
idleBorderGradient.hashCode ^
activeBorderGradient.hashCode ^
borderRadius.hashCode;
borderRadius.hashCode ^
linkHighlightColor.hashCode ^
elevation.hashCode ^
shadow.hashCode ^
enableSafeArea.hashCode;
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
@@ -232,6 +287,10 @@ class MessageInputThemeData with Diagnosticable {
..add(DiagnosticsProperty('activeBorderGradient', activeBorderGradient))
..add(DiagnosticsProperty('idleBorderGradient', idleBorderGradient))
..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: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.
///
/// See also:
///
/// * [MessageListViewThemeData], which is used to configure this theme.
class MessageListViewTheme extends InheritedTheme {
/// Creates a [MessageListViewTheme].
/// * [StreamMessageListViewThemeData], which is used to configure this theme.
/// {@endtemplate}
class StreamMessageListViewTheme extends InheritedTheme {
/// Creates a [StreamMessageListViewTheme].
///
/// The [data] parameter must not be null.
const MessageListViewTheme({
const StreamMessageListViewTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final MessageListViewThemeData data;
final StreamMessageListViewThemeData data;
/// 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.
///
/// Typical usage is as follows:
@@ -30,35 +36,41 @@ class MessageListViewTheme extends InheritedTheme {
/// ```dart
/// MessageListViewTheme theme = MessageListViewTheme.of(context);
/// ```
static MessageListViewThemeData of(BuildContext context) {
final messageListViewTheme =
context.dependOnInheritedWidgetOfExactType<MessageListViewTheme>();
static StreamMessageListViewThemeData of(BuildContext context) {
final messageListViewTheme = context
.dependOnInheritedWidgetOfExactType<StreamMessageListViewTheme>();
return messageListViewTheme?.data ??
StreamChatTheme.of(context).messageListViewTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
MessageListViewTheme(data: data, child: child);
StreamMessageListViewTheme(data: data, child: child);
@override
bool updateShouldNotify(MessageListViewTheme oldWidget) =>
bool updateShouldNotify(StreamMessageListViewTheme oldWidget) =>
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
/// used with [MessageListViewTheme] or with the overall [StreamChatTheme]'s
/// used with [StreamMessageListViewTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.messageListViewTheme].
///
/// 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
/// the default style for [MessageListView]s below the overall
/// [StreamChatTheme].
class MessageListViewThemeData with Diagnosticable {
/// Creates a [MessageListViewThemeData].
const MessageListViewThemeData({
/// {@endtemplate}
class StreamMessageListViewThemeData with Diagnosticable {
/// Creates a [StreamMessageListViewThemeData].
const StreamMessageListViewThemeData({
this.backgroundColor,
this.backgroundImage,
});
@@ -69,12 +81,12 @@ class MessageListViewThemeData with Diagnosticable {
/// The image of the [MessageListView] background.
final DecorationImage? backgroundImage;
/// Copies this [MessageListViewThemeData] to another.
MessageListViewThemeData copyWith({
/// Copies this [StreamMessageListViewThemeData] to another.
StreamMessageListViewThemeData copyWith({
Color? backgroundColor,
DecorationImage? backgroundImage,
}) =>
MessageListViewThemeData(
StreamMessageListViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
backgroundImage: backgroundImage ?? this.backgroundImage,
);
@@ -82,18 +94,18 @@ class MessageListViewThemeData with Diagnosticable {
/// Linearly interpolate between two [MessageListView] themes.
///
/// All the properties must be non-null.
MessageListViewThemeData lerp(
MessageListViewThemeData a,
MessageListViewThemeData b,
StreamMessageListViewThemeData lerp(
StreamMessageListViewThemeData a,
StreamMessageListViewThemeData b,
double t,
) =>
MessageListViewThemeData(
StreamMessageListViewThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
backgroundImage: t < 0.5 ? a.backgroundImage : b.backgroundImage,
);
/// Merges one [MessageListViewThemeData] with another.
MessageListViewThemeData merge(MessageListViewThemeData? other) {
/// Merges one [StreamMessageListViewThemeData] with another.
StreamMessageListViewThemeData merge(StreamMessageListViewThemeData? other) {
if (other == null) return this;
return copyWith(
backgroundColor: other.backgroundColor,
@@ -104,7 +116,7 @@ class MessageListViewThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MessageListViewThemeData &&
other is StreamMessageListViewThemeData &&
runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor &&
backgroundImage == other.backgroundImage;
@@ -2,23 +2,29 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.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.
///
/// See also:
///
/// * [UserListViewThemeData], which is used to configure this theme.
class MessageSearchListViewTheme extends InheritedTheme {
/// {@endtemplate}
class StreamMessageSearchListViewTheme extends InheritedTheme {
/// Creates a [UserListViewTheme].
///
/// The [data] parameter must not be null.
const MessageSearchListViewTheme({
const StreamMessageSearchListViewTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final MessageSearchListViewThemeData data;
final StreamMessageSearchListViewThemeData data;
/// The closest instance of this class that encloses the given context.
///
@@ -30,64 +36,72 @@ class MessageSearchListViewTheme extends InheritedTheme {
/// ```dart
/// MessageSearchListViewTheme theme = MessageSearchListViewTheme.of(context);
/// ```
static MessageSearchListViewThemeData of(BuildContext context) {
static StreamMessageSearchListViewThemeData of(BuildContext context) {
final messageSearchListViewTheme = context
.dependOnInheritedWidgetOfExactType<MessageSearchListViewTheme>();
.dependOnInheritedWidgetOfExactType<StreamMessageSearchListViewTheme>();
return messageSearchListViewTheme?.data ??
StreamChatTheme.of(context).messageSearchListViewTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
MessageSearchListViewTheme(data: data, child: child);
StreamMessageSearchListViewTheme(data: data, child: child);
@override
bool updateShouldNotify(MessageSearchListViewTheme oldWidget) =>
bool updateShouldNotify(StreamMessageSearchListViewTheme oldWidget) =>
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
/// when used with [MessageSearchListView] or with the overall
/// [StreamChatTheme]'s [StreamChatThemeData.messageSearchListViewTheme].
///
/// See also:
///
/// * [MessageSearchListViewTheme], the theme which is configured with this
/// * [StreamMessageSearchListViewTheme], the theme which is configured with this
/// class.
/// * [StreamChatThemeData.messageSearchListViewTheme], which can be used to
/// override the default style for [UserListView]s below the overall
/// [StreamChatTheme].
class MessageSearchListViewThemeData with Diagnosticable {
/// Creates a [MessageSearchListViewThemeData].
const MessageSearchListViewThemeData({
/// {@endtemplate}
class StreamMessageSearchListViewThemeData with Diagnosticable {
/// Creates a [StreamMessageSearchListViewThemeData].
const StreamMessageSearchListViewThemeData({
this.backgroundColor,
});
/// The color of the [MessageSearchListView] background.
final Color? backgroundColor;
/// Copies this [MessageSearchListViewThemeData] to another.
MessageSearchListViewThemeData copyWith({
/// Copies this [StreamMessageSearchListViewThemeData] to another.
StreamMessageSearchListViewThemeData copyWith({
Color? backgroundColor,
}) =>
MessageSearchListViewThemeData(
StreamMessageSearchListViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
);
/// Linearly interpolate between two [UserListViewThemeData] themes.
///
/// All the properties must be non-null.
MessageSearchListViewThemeData lerp(
MessageSearchListViewThemeData a,
MessageSearchListViewThemeData b,
StreamMessageSearchListViewThemeData lerp(
StreamMessageSearchListViewThemeData a,
StreamMessageSearchListViewThemeData b,
double t,
) =>
MessageSearchListViewThemeData(
StreamMessageSearchListViewThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
);
/// Merges one [MessageSearchListViewThemeData] with another.
MessageSearchListViewThemeData merge(MessageSearchListViewThemeData? other) {
/// Merges one [StreamMessageSearchListViewThemeData] with another.
StreamMessageSearchListViewThemeData merge(
StreamMessageSearchListViewThemeData? other,
) {
if (other == null) return this;
return copyWith(
backgroundColor: other.backgroundColor,
@@ -97,7 +111,7 @@ class MessageSearchListViewThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MessageSearchListViewThemeData &&
other is StreamMessageSearchListViewThemeData &&
runtimeType == other.runtimeType &&
backgroundColor == other.backgroundColor;
@@ -2,11 +2,17 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.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
/// {@endtemplate}
// ignore: prefer-match-file-name
class MessageThemeData with Diagnosticable {
/// Creates a [MessageThemeData].
const MessageThemeData({
class StreamMessageThemeData with Diagnosticable {
/// Creates a [StreamMessageThemeData].
const StreamMessageThemeData({
this.repliesStyle,
this.messageTextStyle,
this.messageAuthorStyle,
@@ -52,13 +58,13 @@ class MessageThemeData with Diagnosticable {
final Color? reactionsMaskColor;
/// Theme of the avatar
final AvatarThemeData? avatarTheme;
final StreamAvatarThemeData? avatarTheme;
/// Background color for messages with url attachments.
final Color? linkBackgroundColor;
/// Copy with a theme
MessageThemeData copyWith({
StreamMessageThemeData copyWith({
TextStyle? messageTextStyle,
TextStyle? messageAuthorStyle,
TextStyle? messageLinksStyle,
@@ -66,13 +72,13 @@ class MessageThemeData with Diagnosticable {
TextStyle? repliesStyle,
Color? messageBackgroundColor,
Color? messageBorderColor,
AvatarThemeData? avatarTheme,
StreamAvatarThemeData? avatarTheme,
Color? reactionsBackgroundColor,
Color? reactionsBorderColor,
Color? reactionsMaskColor,
Color? linkBackgroundColor,
}) =>
MessageThemeData(
StreamMessageThemeData(
messageTextStyle: messageTextStyle ?? this.messageTextStyle,
messageAuthorStyle: messageAuthorStyle ?? this.messageAuthorStyle,
messageLinksStyle: messageLinksStyle ?? this.messageLinksStyle,
@@ -89,11 +95,15 @@ class MessageThemeData with Diagnosticable {
linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor,
);
/// Linearly interpolate from one [MessageThemeData] to another.
MessageThemeData lerp(MessageThemeData a, MessageThemeData b, double t) =>
MessageThemeData(
avatarTheme:
const AvatarThemeData().lerp(a.avatarTheme!, b.avatarTheme!, t),
/// Linearly interpolate from one [StreamMessageThemeData] to another.
StreamMessageThemeData lerp(
StreamMessageThemeData a,
StreamMessageThemeData b,
double t,
) =>
StreamMessageThemeData(
avatarTheme: const StreamAvatarThemeData()
.lerp(a.avatarTheme!, b.avatarTheme!, t),
createdAtStyle: TextStyle.lerp(a.createdAtStyle, b.createdAtStyle, t),
messageAuthorStyle:
TextStyle.lerp(a.messageAuthorStyle, b.messageAuthorStyle, t),
@@ -120,7 +130,7 @@ class MessageThemeData with Diagnosticable {
);
/// Merge with a theme
MessageThemeData merge(MessageThemeData? other) {
StreamMessageThemeData merge(StreamMessageThemeData? other) {
if (other == null) return this;
return copyWith(
messageTextStyle: messageTextStyle?.merge(other.messageTextStyle) ??
@@ -146,7 +156,7 @@ class MessageThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MessageThemeData &&
other is StreamMessageThemeData &&
runtimeType == other.runtimeType &&
messageTextStyle == other.messageTextStyle &&
messageAuthorStyle == other.messageAuthorStyle &&
@@ -1,9 +1,15 @@
import 'package:flutter/material.dart';
/// {@macro text_theme}
@Deprecated("Use 'StreamTextTheme' instead")
typedef TextTheme = StreamTextTheme;
/// {@template text_theme}
/// Class for holding text theme
class TextTheme {
/// {@endtemplate}
class StreamTextTheme {
/// Initialise light text theme
TextTheme.light({
StreamTextTheme.light({
this.title = const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
@@ -46,7 +52,7 @@ class TextTheme {
});
/// Initialise with dark theme
TextTheme.dark({
StreamTextTheme.dark({
this.title = const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
@@ -113,7 +119,7 @@ class TextTheme {
final TextStyle captionBold;
/// Copy with theme
TextTheme copyWith({
StreamTextTheme copyWith({
Brightness brightness = Brightness.light,
TextStyle? body,
TextStyle? title,
@@ -125,7 +131,7 @@ class TextTheme {
TextStyle? captionBold,
}) =>
brightness == Brightness.light
? TextTheme.light(
? StreamTextTheme.light(
body: body ?? this.body,
title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold,
@@ -135,7 +141,7 @@ class TextTheme {
footnote: footnote ?? this.footnote,
captionBold: captionBold ?? this.captionBold,
)
: TextTheme.dark(
: StreamTextTheme.dark(
body: body ?? this.body,
title: title ?? this.title,
headlineBold: headlineBold ?? this.headlineBold,
@@ -147,7 +153,7 @@ class TextTheme {
);
/// Merge text theme
TextTheme merge(TextTheme? other) {
StreamTextTheme merge(StreamTextTheme? other) {
if (other == null) return this;
return copyWith(
body: body.merge(other.body),
@@ -2,27 +2,33 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// {@macro user_list_view_theme}
@Deprecated("Use 'StreamUserListViewTheme' instead")
typedef UserListViewTheme = StreamUserListViewTheme;
/// {@template user_list_view_theme}
/// Overrides the default style of [UserListView] descendants.
///
/// See also:
///
/// * [UserListViewThemeData], which is used to configure this theme.
class UserListViewTheme extends InheritedTheme {
/// Creates a [UserListViewTheme].
/// * [StreamUserListViewThemeData], which is used to configure this theme.
/// {@endtemplate}
class StreamUserListViewTheme extends InheritedTheme {
/// Creates a [StreamUserListViewTheme].
///
/// The [data] parameter must not be null.
const UserListViewTheme({
const StreamUserListViewTheme({
Key? key,
required this.data,
required Widget child,
}) : super(key: key, child: child);
/// The configuration of this theme.
final UserListViewThemeData data;
final StreamUserListViewThemeData data;
/// 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.
///
/// Typical usage is as follows:
@@ -30,35 +36,41 @@ class UserListViewTheme extends InheritedTheme {
/// ```dart
/// UserListViewTheme theme = UserListViewTheme.of(context);
/// ```
static UserListViewThemeData of(BuildContext context) {
static StreamUserListViewThemeData of(BuildContext context) {
final userListViewTheme =
context.dependOnInheritedWidgetOfExactType<UserListViewTheme>();
context.dependOnInheritedWidgetOfExactType<StreamUserListViewTheme>();
return userListViewTheme?.data ??
StreamChatTheme.of(context).userListViewTheme;
}
@override
Widget wrap(BuildContext context, Widget child) =>
UserListViewTheme(data: data, child: child);
StreamUserListViewTheme(data: data, child: child);
@override
bool updateShouldNotify(UserListViewTheme oldWidget) =>
bool updateShouldNotify(StreamUserListViewTheme oldWidget) =>
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
/// used with [UserListViewTheme] or with the overall [StreamChatTheme]'s
/// used with [StreamUserListViewTheme] or with the overall [StreamChatTheme]'s
/// [StreamChatThemeData.userListViewTheme].
///
/// 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
/// the default style for [UserListView]s below the overall
/// [StreamChatTheme].
class UserListViewThemeData with Diagnosticable {
/// Creates a [UserListViewThemeData].
const UserListViewThemeData({
/// {@endtemplate}
class StreamUserListViewThemeData with Diagnosticable {
/// Creates a [StreamUserListViewThemeData].
const StreamUserListViewThemeData({
this.backgroundColor,
});
@@ -66,27 +78,27 @@ class UserListViewThemeData with Diagnosticable {
final Color? backgroundColor;
/// Copies this [ChannelListViewThemeData] to another.
UserListViewThemeData copyWith({
StreamUserListViewThemeData copyWith({
Color? backgroundColor,
}) =>
UserListViewThemeData(
StreamUserListViewThemeData(
backgroundColor: backgroundColor ?? this.backgroundColor,
);
/// Linearly interpolate between two [UserListViewThemeData] themes.
/// Linearly interpolate between two [StreamUserListViewThemeData] themes.
///
/// All the properties must be non-null.
UserListViewThemeData lerp(
UserListViewThemeData a,
UserListViewThemeData b,
StreamUserListViewThemeData lerp(
StreamUserListViewThemeData a,
StreamUserListViewThemeData b,
double t,
) =>
UserListViewThemeData(
StreamUserListViewThemeData(
backgroundColor: Color.lerp(a.backgroundColor, b.backgroundColor, t),
);
/// Merges one [UserListViewThemeData] with another.
UserListViewThemeData merge(UserListViewThemeData? other) {
/// Merges one [StreamUserListViewThemeData] with another.
StreamUserListViewThemeData merge(StreamUserListViewThemeData? other) {
if (other == null) return this;
return copyWith(
backgroundColor: other.backgroundColor,
@@ -96,7 +108,7 @@ class UserListViewThemeData with Diagnosticable {
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UserListViewThemeData &&
other is StreamUserListViewThemeData &&
runtimeType == other.runtimeType &&
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/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_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
/// [StreamChatTheme] and on its [ChannelTheme.channelHeaderTheme] property.
/// Modify it to change the widget appearance.
class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// {@endtemplate}
class StreamThreadHeader extends StatelessWidget
implements PreferredSizeWidget {
/// Instantiate a new ThreadHeader
const ThreadHeader({
const StreamThreadHeader({
Key? key,
required this.parent,
this.showBackButton = true,
@@ -102,12 +109,12 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
/// if a user is typing in this thread
final bool showTypingIndicator;
/// The background color of this [ThreadHeader].
/// The background color of this [StreamThreadHeader].
final Color? backgroundColor;
@override
Widget build(BuildContext context) {
final channelHeaderTheme = ChannelHeaderTheme.of(context);
final channelHeaderTheme = StreamChannelHeaderTheme.of(context);
final defaultSubtitle = subtitle ??
Row(
@@ -161,12 +168,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget {
),
const SizedBox(height: 2),
if (showTypingIndicator)
TypingIndicator(
alignment: Alignment.center,
channel: StreamChannel.of(context).channel,
style: channelHeaderTheme.subtitleStyle,
parentId: parent.id,
alternativeWidget: defaultSubtitle,
Align(
child: StreamTypingIndicator(
channel: StreamChannel.of(context).channel,
style: channelHeaderTheme.subtitleStyle,
parentId: parent.id,
alternativeWidget: defaultSubtitle,
),
)
else
defaultSubtitle,
@@ -3,15 +3,20 @@ import 'package:lottie/lottie.dart';
import 'package:stream_chat_flutter/src/extension.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
class TypingIndicator extends StatelessWidget {
/// {@endtemplate}
class StreamTypingIndicator extends StatelessWidget {
/// Instantiate a new TypingIndicator
const TypingIndicator({
const StreamTypingIndicator({
Key? key,
this.channel,
this.alternativeWidget,
this.style,
this.alignment = Alignment.centerLeft,
this.padding = const EdgeInsets.all(0),
this.parentId,
}) : super(key: key);
@@ -28,9 +33,6 @@ class TypingIndicator extends StatelessWidget {
/// The padding of this widget
final EdgeInsets padding;
/// Alignment of the typing indicator
final Alignment alignment;
/// Id of the parent message in case of a thread
final String? parentId;
@@ -46,30 +48,25 @@ class TypingIndicator extends StatelessWidget {
stream: channelState.typingEventsStream.map((typings) => typings.entries
.where((element) => element.value.parentId == parentId)
.map((e) => e.key)),
builder: (context, data) => AnimatedSwitcher(
builder: (context, users) => AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: data.isNotEmpty
child: users.isNotEmpty
? Padding(
key: const Key('main'),
padding: padding,
child: Align(
key: const Key('typings'),
alignment: alignment,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Lottie.asset(
'animations/typing_dots.json',
package: 'stream_chat_flutter',
height: 4,
),
Text(
context.translations.userTypingText(data),
maxLines: 1,
style: style,
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Lottie.asset(
'animations/typing_dots.json',
package: 'stream_chat_flutter',
height: 4,
),
Text(
context.translations.userTypingText(users),
maxLines: 1,
style: style,
),
],
),
)
: altWidget,

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