diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 33298866..5f92c714 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -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 diff --git a/packages/stream_chat/example/pubspec.yaml b/packages/stream_chat/example/pubspec.yaml index cf1f3874..f3fad2d1 100644 --- a/packages/stream_chat/example/pubspec.yaml +++ b/packages/stream_chat/example/pubspec.yaml @@ -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: diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index f2effe04..9d0c98d2 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -294,6 +294,18 @@ class Channel { return data; } + /// List of user permissions on this channel + List get ownCapabilities => + state?._channelState.channel?.ownCapabilities ?? []; + + /// List of user permissions on this channel + Stream> get ownCapabilitiesStream { + _checkInitialized(); + return state!.channelStateStream + .map((cs) => cs.channel?.ownCapabilities ?? []) + .distinct(); + } + /// Channel extra data as a stream. Stream> get extraDataStream { _checkInitialized(); @@ -487,6 +499,7 @@ class Channel { Future 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 updateMessage(Message message) async { + Future 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? set, List? 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 get read => _channelState.read; diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index c3605ac5..cab864b9 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -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 updateMessage(Message message) => - _chatApi.message.updateMessage(message); + Future 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? set, List? unset, + bool skipEnrichUrl = false, }) => _chatApi.message.partialUpdateMessage( messageId, set: set, unset: unset, + skipEnrichUrl: skipEnrichUrl, ); /// Deletes the given message diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 5dfa9aa3..388dda57 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -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 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(); diff --git a/packages/stream_chat/lib/src/core/api/channel_api.dart b/packages/stream_chat/lib/src/core/api/channel_api.dart index 8744661a..0b299d95 100644 --- a/packages/stream_chat/lib/src/core/api/channel_api.dart +++ b/packages/stream_chat/lib/src/core/api/channel_api.dart @@ -269,6 +269,7 @@ class ChannelApi { ) async { final response = await _client.post( '${_getChannelUrl(channelId, channelType)}/truncate', + data: {}, ); return EmptyResponse.fromJson(response.data); } diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 8894e14c..20cdfc51 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -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 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? set, List? 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); diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index 09fe1083..325dbe11 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -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 get props => [ id, diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index ed7c2c2e..967fe827 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -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 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? 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, diff --git a/packages/stream_chat/lib/src/core/models/channel_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart index 94d9a81a..974ef383 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.g.dart @@ -10,6 +10,10 @@ ChannelModel _$ChannelModelFromJson(Map json) => ChannelModel( id: json['id'] as String?, type: json['type'] as String?, cid: json['cid'] as String?, + ownCapabilities: (json['own_capabilities'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], config: json['config'] == null ? null : ChannelConfig.fromJson(json['config'] as Map), @@ -48,6 +52,7 @@ Map _$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; diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 10ff713e..017bb06f 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -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 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 attachments; - /// The list of user mentioned in the message + /// The list of user mentioned in the message. @JsonKey(toJson: User.toIds) final List 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? 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? 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 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 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? 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 toJson() => Serializer.moveFromExtraDataToRoot( _$MessageToJson(this), ); @@ -256,18 +266,18 @@ class Message extends Equatable { String? type, List? attachments, List? mentionedUsers, + bool? silent, + bool? shadowed, Map? reactionCounts, Map? reactionScores, List? latestReactions, List? ownReactions, String? parentId, - Message? quotedMessage, - String? quotedMessageId, + Object? quotedMessage = _nullConst, + Object? quotedMessageId = _nullConst, int? replyCount, List? 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? 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, diff --git a/packages/stream_chat/lib/src/permission_type.dart b/packages/stream_chat/lib/src/permission_type.dart new file mode 100644 index 00000000..993351b7 --- /dev/null +++ b/packages/stream_chat/lib/src/permission_type.dart @@ -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'; +} diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 571d78f5..c0ceab3a 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -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'; diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 19657683..9452a433 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.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)) diff --git a/packages/stream_chat/test/src/core/api/channel_api_test.dart b/packages/stream_chat/test/src/core/api/channel_api_test.dart index e631fdb2..a7856c92 100644 --- a/packages/stream_chat/test/src/core/api/channel_api_test.dart +++ b/packages/stream_chat/test/src/core/api/channel_api_test.dart @@ -481,14 +481,21 @@ void main() { final path = '${_getChannelUrl(channelId, channelType)}/truncate'; - when(() => client.post(path)).thenAnswer( - (_) async => successResponse(path, data: {})); + when(() => client.post( + path, + data: {}, + )) + .thenAnswer( + (_) async => successResponse(path, data: {})); 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); }); diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index e89491a2..ed7fefd6 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -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); }); diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 78a52f64..fe8d6cd1 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -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 diff --git a/packages/stream_chat_flutter/example/lib/main.dart b/packages/stream_chat_flutter/example/lib/main.dart index 6b5a299f..4a7eabf4 100644 --- a/packages/stream_chat_flutter/example/lib/main.dart +++ b/packages/stream_chat_flutter/example/lib/main.dart @@ -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 [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(attachmentLimit: 3), + StreamMessageInput(attachmentLimit: 3), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 1fd3ccb6..6f4c51e6 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -84,7 +84,7 @@ class _SplitViewState extends State { ); } -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 createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + 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 [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart index 459d8690..cd3bb12e 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_1.dart @@ -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 [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 6abc260e..0ca2356e 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -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 createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + 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 [ - Expanded( - child: MessageListView(), - ), - MessageInput(), - ], - ), - ); - } + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: const [ + Expanded( + child: StreamMessageListView(), + ), + StreamMessageInput(), + ], + ), + ); } diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 538dbd68..8eb01fbb 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -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 createState() => _ChannelListPageState(); +} - Widget _channelPreviewBuilder(BuildContext context, Channel channel) { +class _ChannelListPageState extends State { + 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 [ Expanded( - child: MessageListView(), + child: StreamMessageListView(), ), - MessageInput(), + StreamMessageInput(), ], ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index 5a05e4f9..89aa55f7 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -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 thread’s 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 createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + 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: [ - Expanded( - child: MessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: [ + 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: [ Expanded( - child: MessageListView( + child: StreamMessageListView( parentMessage: parent, ), ), - MessageInput( - parentMessage: parent, + StreamMessageInput( + messageInputController: MessageInputController( + message: Message(parentId: parent!.id), + ), ), ], ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index 86ac2e1f..fd6081f7 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -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 createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + 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: [ Expanded( - child: MessageListView( + child: StreamMessageListView( messageBuilder: _messageBuilder, ), ), - const MessageInput(), + const StreamMessageInput(), ], ), ); @@ -110,7 +124,7 @@ class ChannelPage extends StatelessWidget { BuildContext context, MessageDetails details, List messages, - MessageWidget _, + StreamMessageWidget _, ) { final message = details.message; final isCurrentUser = diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index 19d5d1ae..6ff1bb64 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -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 createState() => _ChannelListPageState(); +} + +class _ChannelListPageState extends State { + 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: [ - Expanded( - child: MessageListView( - threadBuilder: (_, parentMessage) => ThreadPage( - parent: parentMessage, + Widget build(BuildContext context) => Scaffold( + appBar: const StreamChannelHeader(), + body: Column( + children: [ + 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: [ Expanded( - child: MessageListView( + child: StreamMessageListView( parentMessage: parent, ), ), - MessageInput( - parentMessage: parent, + StreamMessageInput( + messageInputController: MessageInputController( + message: Message(parentId: parent!.id), + ), ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index f1369d49..b1aba4a2 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -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; diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index bd0aacc3..a1aa60bc 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart @@ -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, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart index 75a890e8..0021fc76 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_widget.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index 0b5d580f..6bac62e9 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 1974b040..28e14e5e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index a46ff25c..b6de6bc1 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -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, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart index df05ba5e..25cfb5ba 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -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; diff --git a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart index 3df1f451..541f1484 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/video_attachment.dart @@ -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, ), diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index 4fedfc12..41e6fd11 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -58,7 +58,7 @@ class AttachmentActionsModal extends StatelessWidget { /// List of custom actions final List 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, diff --git a/packages/stream_chat_flutter/lib/src/back_button.dart b/packages/stream_chat_flutter/lib/src/back_button.dart index 08d8dabf..b7cd4552 100644 --- a/packages/stream_chat_flutter/lib/src/back_button.dart +++ b/packages/stream_chat_flutter/lib/src/back_button.dart @@ -50,7 +50,7 @@ class StreamBackButton extends StatelessWidget { Positioned( top: 7, right: 7, - child: UnreadIndicator( + child: StreamUnreadIndicator( cid: cid, ), ), diff --git a/packages/stream_chat_flutter/lib/src/channel_avatar.dart b/packages/stream_chat_flutter/lib/src/channel_avatar.dart index ba24a640..077302dd 100644 --- a/packages/stream_chat_flutter/lib/src/channel_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/channel_avatar.dart @@ -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( 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, diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 74661c08..66438479 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -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 { 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 { 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 { height: 5, ), Center( - child: ChannelInfo( + child: StreamChannelInfo( showTypingIndicator: false, channel: _streamChannelState.channel, textStyle: _channelPreviewThemeData.subtitleStyle, @@ -74,7 +77,7 @@ class _ChannelBottomSheetState extends State { 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 { 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 { 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 { 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 { }); }, ), - 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 { }); }, ), - OptionListTile( + StreamOptionListTile( leading: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: StreamSvgIcon.closeSmall( @@ -215,7 +222,7 @@ class _ChannelBottomSheetState extends State { void didChangeDependencies() { _streamChannelState = StreamChannel.of(context); _streamChatThemeData = StreamChatTheme.of(context); - _channelPreviewThemeData = ChannelPreviewTheme.of(context); + _channelPreviewThemeData = StreamChannelPreviewTheme.of(context); _streamChatState = StreamChat.of(context); super.didChangeDependencies(); } diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 954aa758..9da8b4f8 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -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? 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: [ title ?? - ChannelName( + StreamChannelName( + channel: channel, textStyle: channelHeaderTheme.titleStyle, ), const SizedBox(height: 2), subtitle ?? - ChannelInfo( + StreamChannelInfo( showTypingIndicator: showTypingIndicator, channel: channel, textStyle: channelHeaderTheme.subtitleStyle, diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index ff068d21..b30058b9 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -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>( 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, + ), ); } diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 6c54ad89..f814a08e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -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? 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: [ diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 648226ce..f7c39e8e 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -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 { 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 { 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 { ); }, ), - 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 { child: Column( mainAxisSize: MainAxisSize.min, children: [ - ChannelAvatar( + StreamChannelAvatar( channel: channel, borderRadius: BorderRadius.circular(32), selected: selected, @@ -657,8 +652,9 @@ class _ChannelListViewState extends State { 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, ), diff --git a/packages/stream_chat_flutter/lib/src/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index a1e81005..f496b28a 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -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({ diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 125d06fe..ed5fe9f8 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -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: [ 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, diff --git a/packages/stream_chat_flutter/lib/src/commands_overlay.dart b/packages/stream_chat_flutter/lib/src/commands_overlay.dart index 9f8aca1e..6234f696 100644 --- a/packages/stream_chat_flutter/lib/src/commands_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/commands_overlay.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index f0eca9da..6f234547 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/date_divider.dart index c3b48158..64396ceb 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/date_divider.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/deleted_message.dart b/packages/stream_chat_flutter/lib/src/deleted_message.dart index 09d725bb..667d101d 100644 --- a/packages/stream_chat_flutter/lib/src/deleted_message.dart +++ b/packages/stream_chat_flutter/lib/src/deleted_message.dart @@ -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; diff --git a/packages/stream_chat_flutter/lib/src/emoji_overlay.dart b/packages/stream_chat_flutter/lib/src/emoji_overlay.dart index 0a3cb634..b4e50974 100644 --- a/packages/stream_chat_flutter/lib/src/emoji_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/emoji_overlay.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index f955ca99..a0c9d288 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -46,7 +46,7 @@ extension IterableX on Iterable { 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 { 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); + } +} diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index aa185597..0ae3a475 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -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 +class _StreamFullScreenMediaState extends State with SingleTickerProviderStateMixin { late final AnimationController _animationController; late final PageController _pageController; @@ -172,7 +178,7 @@ class _FullScreenMediaState extends State ), 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 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 widget.attachmentActionsModalBuilder, ), if (!widget.message.isEphemeral) - GalleryFooter( + StreamGalleryFooter( currentPage: value, totalPages: widget.mediaAttachments.length, mediaAttachments: widget.mediaAttachments, diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index f189dae3..a137281c 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -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? 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 { +class _StreamGalleryFooterState extends State { @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 { 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 { 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 { ), ], ), - child: UserAvatar( + child: StreamUserAvatar( user: widget.message.user!, constraints: BoxConstraints.tight(const Size(24, 24)), diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index 2325af69..9c8650b2 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart index 12824f45..aca07c92 100644 --- a/packages/stream_chat_flutter/lib/src/gradient_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/gradient_avatar.dart @@ -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 { +class _StreamGradientAvatarState extends State { @override Widget build(BuildContext context) => Center( child: RepaintBoundary( diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/group_avatar.dart index bf599c63..f7efa32d 100644 --- a/packages/stream_chat_flutter/lib/src/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/group_avatar.dart @@ -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 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, diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index ff67b068..fa723bef 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/info_tile.dart index 965d4fed..86964bf0 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/info_tile.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 8aa8d071..2a87e76d 100644 --- a/packages/stream_chat_flutter/lib/src/localization/translations.dart +++ b/packages/stream_chat_flutter/lib/src/localization/translations.dart @@ -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'; } diff --git a/packages/stream_chat_flutter/lib/src/media_list_view.dart b/packages/stream_chat_flutter/lib/src/media_list_view.dart index 62072b0a..c2404208 100644 --- a/packages/stream_chat_flutter/lib/src/media_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/media_list_view.dart @@ -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 { +class _StreamMediaListViewState extends State { final _media = []; final ScrollController _scrollController = ScrollController(); int _currentPage = 0; @@ -222,3 +217,14 @@ class MediaThumbnailProvider extends ImageProvider { @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; + } +} diff --git a/packages/stream_chat_flutter/lib/src/mention_tile.dart b/packages/stream_chat_flutter/lib/src/mention_tile.dart deleted file mode 100644 index c302ccb8..00000000 --- a/packages/stream_chat_flutter/lib/src/mention_tile.dart +++ /dev/null @@ -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, - ), - ), - ], - ), - ); - } -} diff --git a/packages/stream_chat_flutter/lib/src/message_action.dart b/packages/stream_chat_flutter/lib/src/message_action.dart index 49a57b1e..edd732f5 100644 --- a/packages/stream_chat_flutter/lib/src/message_action.dart +++ b/packages/stream_chat_flutter/lib/src/message_action.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 907f4096..056e84fc 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -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 customActions; + final List customActions; @override - _MessageActionsModalState createState() => _MessageActionsModalState(); + _StreamMessageActionsModalState createState() => + _StreamMessageActionsModalState(); } -class _MessageActionsModalState extends State { +class _StreamMessageActionsModalState extends State { bool _showActions = true; + late List _userPermissions; + late bool _isMyMessage; @override Widget build(BuildContext context) => _showMessageOptionsModal(); @@ -122,6 +131,19 @@ class _MessageActionsModalState extends State { 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 { ? CrossAxisAlignment.end : CrossAxisAlignment.start, children: [ - if (widget.showReactions && + if ((widget.showReactions ?? hasReactionPermission) && (widget.message.status == MessageSendingStatus.sent)) Align( alignment: Alignment( @@ -144,7 +166,7 @@ class _MessageActionsModalState extends State { : -(1.2 - divFactor)), 0, ), - child: ReactionPicker( + child: StreamReactionPicker( message: widget.message, ), ), @@ -168,21 +190,35 @@ class _MessageActionsModalState extends State { 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 { InkWell _buildCustomAction( BuildContext context, - MessageAction messageAction, + StreamMessageAction messageAction, ) => InkWell( onTap: () { @@ -560,7 +596,7 @@ class _MessageActionsModalState extends State { 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 { 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 { ), ); } + + @override + void didChangeDependencies() { + final newStreamChannel = StreamChannel.of(context); + _userPermissions = newStreamChannel.channel.ownCapabilities; + _isMyMessage = + widget.message.user?.id == StreamChat.of(context).currentUser?.id; + super.didChangeDependencies(); + } } diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 1fe46c6c..19a6592d 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -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 { final _attachments = {}; final List _mentionedUsers = []; @@ -364,7 +354,7 @@ class MessageInputState extends State { 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 { ); } - return MultiOverlay( + return StreamMultiOverlay( childAnchor: Alignment.topCenter, overlayAnchor: Alignment.bottomCenter, overlayOptions: [ @@ -932,7 +922,7 @@ class MessageInputState extends State { 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 { 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 { } 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 { // 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 { 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 { .map( (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 { 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 { ); 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); - - 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 { @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, ); diff --git a/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart b/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart new file mode 100644 index 00000000..aaf016a3 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart @@ -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'), + ), + ), + ), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart new file mode 100644 index 00000000..0ffa472c --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -0,0 +1,1879 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:stream_chat_flutter/src/commands_overlay.dart'; +import 'package:stream_chat_flutter/src/emoji/emoji.dart'; +import 'package:stream_chat_flutter/src/emoji_overlay.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/message_input/simple_safe_area.dart'; +import 'package:stream_chat_flutter/src/message_input/tld.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_thumbnail_image.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// A function that returns true if the message is valid and can be sent. +typedef MessageValidator = bool Function(Message message); + +/// A callback that can be passed to [StreamMessageInput.onError]. +/// +/// This callback should not throw. +/// +/// It exists merely for error reporting, and should not be used otherwise. +typedef ErrorListener = void Function( + Object error, + StackTrace? stackTrace, +); + +/// A callback that can be passed to [StreamMessageInput.onAttachmentLimitExceed]. +/// +/// This callback should not throw. +/// +/// It exists merely for showing a custom error, and should not be used +/// otherwise. +typedef AttachmentLimitExceedListener = void Function( + int limit, + String error, +); + +/// Builder for attachment thumbnails. +typedef AttachmentThumbnailBuilder = Widget Function( + BuildContext, + Attachment, +); + +/// Builder function for building a mention tile. +typedef MentionTileBuilder = Widget Function( + BuildContext context, + Member member, +); + +/// Builder function for building a user mention tile. +/// +/// Use [StreamUserMentionTile] for the default implementation. +typedef UserMentionTileBuilder = Widget Function( + BuildContext context, + User user, +); + +/// Widget builder for action button. +/// +/// [defaultActionButton] is the default [IconButton] configuration, +/// use [defaultActionButton.copyWith] to easily customize it. +typedef ActionButtonBuilder = Widget Function( + BuildContext context, + IconButton defaultActionButton, +); + +/// Widget builder for widgets that may require data from the +/// [MessageInputController]. +typedef MessageRelatedBuilder = Widget Function( + BuildContext context, + MessageInputController messageInputController, +); + +/// Widget builder for a custom attachment picker. +typedef AttachmentsPickerBuilder = Widget Function( + BuildContext context, + MessageInputController messageInputController, + StreamAttachmentPicker defaultPicker, +); + +/// Location for actions on the [StreamMessageInput]. +enum ActionsLocation { + /// Align to left + left, + + /// Align to right + right, + + /// Align to left but inside the [TextField] + leftInside, + + /// Align to right but inside the [TextField] + rightInside, +} + +/// Default attachments for widget. +enum DefaultAttachmentTypes { + /// Image Attachment + image, + + /// Video Attachment + video, + + /// File Attachment + file, +} + +/// Available locations for the `sendMessage` button relative to the textField. +enum SendButtonLocation { + /// inside the textField + inside, + + /// outside the textField + outside, +} + +const _kMinMediaPickerSize = 360.0; + +const _kDefaultMaxAttachmentSize = 20971520; // 20MB in Bytes + +/// Inactive state: +/// +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input_paint.png) +/// +/// Focused state: +/// +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/message_input2_paint.png) +/// +/// Widget used to enter a message and add attachments: +/// +/// ```dart +/// class ChannelPage extends StatelessWidget { +/// const ChannelPage({ +/// Key key, +/// }) : super(key: key); +/// +/// @override +/// Widget build(BuildContext context) { +/// return Scaffold( +/// appBar: ChannelHeader(), +/// body: Column( +/// children: [ +/// Expanded( +/// child: MessageListView( +/// threadBuilder: (_, parentMessage) { +/// return ThreadPage( +/// parent: parentMessage, +/// ); +/// }, +/// ), +/// ), +/// MessageInput(), +/// ], +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// 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. +class StreamMessageInput extends StatefulWidget { + /// Instantiate a new MessageInput + const StreamMessageInput({ + Key? key, + this.onMessageSent, + this.preMessageSending, + this.maxHeight = 150, + this.keyboardType = TextInputType.multiline, + this.disableAttachments = false, + this.messageInputController, + this.actions = const [], + this.actionsLocation = ActionsLocation.left, + this.attachmentThumbnailBuilders, + this.focusNode, + this.sendButtonLocation = SendButtonLocation.outside, + this.autofocus = false, + this.hideSendAsDm = false, + this.idleSendButton, + this.activeSendButton, + this.showCommandsButton = true, + @Deprecated('''Use `userMentionsTileBuilder` instead. Will be removed in future release''') + this.mentionsTileBuilder, + this.userMentionsTileBuilder, + this.maxAttachmentSize = _kDefaultMaxAttachmentSize, + this.onError, + this.attachmentLimit = 10, + this.onAttachmentLimitExceed, + this.attachmentButtonBuilder, + this.commandButtonBuilder, + this.customOverlays = const [], + this.mentionAllAppUsers = false, + this.attachmentsPickerBuilder, + this.sendButtonBuilder, + this.shouldKeepFocusAfterMessage, + this.validator = _defaultValidator, + this.restorationId, + this.enableSafeArea, + this.elevation, + this.shadow, + }) : super(key: key); + + /// List of options for showing overlays. + final List customOverlays; + + /// Max attachment size in bytes: + /// - Defaults to 20 MB + /// - Do not set it if you're using our default CDN + final int maxAttachmentSize; + + /// Function called after sending the message. + final void Function(Message)? onMessageSent; + + /// Function called right before sending the message. + /// + /// Use this to transform the message. + final FutureOr Function(Message)? preMessageSending; + + /// Maximum Height for the TextField to grow before it starts scrolling. + final double maxHeight; + + /// The keyboard type assigned to the TextField. + final TextInputType keyboardType; + + /// If true the attachments button will not be displayed. + final bool disableAttachments; + + /// Use this property to hide/show the commands button. + final bool showCommandsButton; + + /// Hide send as dm checkbox. + final bool hideSendAsDm; + + /// The text controller of the TextField. + final MessageInputController? messageInputController; + + /// List of action widgets. + final List actions; + + /// The location of the custom actions. + final ActionsLocation actionsLocation; + + /// Map that defines a thumbnail builder for an attachment type. + final Map? attachmentThumbnailBuilders; + + /// The focus node associated to the TextField. + final FocusNode? focusNode; + + /// The location of the send button + final SendButtonLocation sendButtonLocation; + + /// Autofocus property passed to the TextField + final bool autofocus; + + /// Send button widget in an idle state + final Widget? idleSendButton; + + /// Send button widget in an active state + final Widget? activeSendButton; + + /// Customize the tile for the mentions overlay. + final MentionTileBuilder? mentionsTileBuilder; + + /// Customize the tile for the mentions overlay. + final UserMentionTileBuilder? userMentionsTileBuilder; + + /// A callback for error reporting + final ErrorListener? onError; + + /// A limit for the no. of attachments that can be sent with a single message. + final int attachmentLimit; + + /// A callback for when the [attachmentLimit] is exceeded. + /// + /// This will override the default error alert behaviour. + final AttachmentLimitExceedListener? onAttachmentLimitExceed; + + /// Builder for customizing the attachment button. + /// + /// The builder contains the default [IconButton] that can be customized by + /// calling `.copyWith`. + final ActionButtonBuilder? attachmentButtonBuilder; + + /// Builder for customizing the command button. + /// + /// The builder contains the default [IconButton] that can be customized by + /// calling `.copyWith`. + final ActionButtonBuilder? commandButtonBuilder; + + /// When enabled mentions search users across the entire app. + /// + /// Defaults to false. + final bool mentionAllAppUsers; + + /// Builds bottom sheet when attachment picker is opened. + final AttachmentsPickerBuilder? attachmentsPickerBuilder; + + /// Builder for creating send button + final MessageRelatedBuilder? sendButtonBuilder; + + /// Defines if the [StreamMessageInput] loses focuses after a message is sent. + /// The default behaviour keeps focus until a command is enabled. + final bool? shouldKeepFocusAfterMessage; + + /// A callback function that validates the message. + final MessageValidator validator; + + /// Restoration ID to save and restore the state of the MessageInput. + final String? restorationId; + + /// Wrap [StreamMessageInput] with a [SafeArea widget] + final bool? enableSafeArea; + + /// Elevation of the [StreamMessageInput] + final double? elevation; + + /// Shadow for the [StreamMessageInput] widget + final BoxShadow? shadow; + + static bool _defaultValidator(Message message) => + message.text?.isNotEmpty == true || message.attachments.isNotEmpty; + + @override + StreamMessageInputState createState() => StreamMessageInputState(); +} + +/// State of [StreamMessageInput] +class StreamMessageInputState extends State + with RestorationMixin { + final _imagePicker = ImagePicker(); + late FocusNode _focusNode = widget.focusNode ?? FocusNode(); + late final _isInternalFocusNode = widget.focusNode == null; + bool _inputEnabled = true; + + bool get _commandEnabled => _effectiveController.value.command != null; + bool _showCommandsOverlay = false; + bool _showMentionsOverlay = false; + + bool _actionsShrunk = false; + bool _openFilePickerSection = false; + + late StreamChatThemeData _streamChatTheme; + late StreamMessageInputThemeData _messageInputTheme; + + bool get _hasQuotedMessage => + _effectiveController.value.quotedMessage != null; + + bool get _isEditing => + _effectiveController.value.status != MessageSendingStatus.sending; + + RestorableMessageInputController? _controller; + + MessageInputController get _effectiveController => + widget.messageInputController ?? _controller!.value; + + void _createLocalController([Message? message]) { + assert(_controller == null, ''); + _controller = RestorableMessageInputController(message: message); + } + + void _registerController() { + assert(_controller != null, ''); + + registerForRestoration( + _controller!, + widget.restorationId ?? 'messageInputController', + ); + _effectiveController.textEditingController + .removeListener(_onChangedDebounced); + _effectiveController.textEditingController.addListener(_onChangedDebounced); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); + } + + @override + void initState() { + super.initState(); + if (widget.messageInputController == null) { + _createLocalController(); + } else { + _initialiseEffectiveController(); + } + _focusNode.addListener(_focusNodeListener); + } + + @override + void didUpdateWidget(covariant StreamMessageInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.messageInputController == null && + oldWidget.messageInputController != null) { + _createLocalController(oldWidget.messageInputController!.value); + } else if (widget.messageInputController != null && + oldWidget.messageInputController == null) { + unregisterFromRestoration(_controller!); + _controller!.dispose(); + _controller = null; + _initialiseEffectiveController(); + } + + // Update _focusNode + if (widget.focusNode != null && oldWidget.focusNode != widget.focusNode) { + _focusNode.removeListener(_focusNodeListener); + _focusNode = widget.focusNode!; + _focusNode.addListener(_focusNodeListener); + } + } + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + if (_controller != null) { + _registerController(); + } + } + + @override + String? get restorationId => widget.restorationId; + + void _focusNodeListener() { + if (_focusNode.hasFocus) { + _openFilePickerSection = false; + } + } + + int _timeOut = 0; + Timer? _slowModeTimer; + + void _initialiseEffectiveController() { + _effectiveController.textEditingController + .removeListener(_onChangedDebounced); + _effectiveController.textEditingController.addListener(_onChangedDebounced); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); + } + + void _startSlowMode() { + if (!mounted) { + return; + } + final channel = StreamChannel.of(context).channel; + final cooldownStartedAt = channel.cooldownStartedAt; + if (cooldownStartedAt != null) { + final diff = DateTime.now().difference(cooldownStartedAt).inSeconds; + if (diff < channel.cooldown) { + _timeOut = channel.cooldown - diff; + if (_timeOut > 0) { + _slowModeTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_timeOut == 0) { + timer.cancel(); + } else { + if (mounted) { + setState(() => _timeOut -= 1); + } + } + }); + } + } + } + } + + void _stopSlowMode() => _slowModeTimer?.cancel(); + + @override + Widget build(BuildContext context) { + if (!StreamChannel.of(context) + .channel + .ownCapabilities + .contains(PermissionType.sendMessage)) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 15, + ), + child: Text( + context.translations.sendMessagePermissionError, + style: _messageInputTheme.inputTextStyle, + ), + ), + ); + } + return MessageValueListenableBuilder( + valueListenable: _effectiveController, + builder: (context, value, _) { + Widget child = DecoratedBox( + decoration: BoxDecoration( + color: _messageInputTheme.inputBackgroundColor, + boxShadow: widget.shadow == null + ? (_streamChatTheme.messageInputTheme.shadow == null + ? [] + : [_streamChatTheme.messageInputTheme.shadow!]) + : [widget.shadow!], + ), + child: SimpleSafeArea( + enabled: widget.enableSafeArea ?? + _streamChatTheme.messageInputTheme.enableSafeArea ?? + true, + child: GestureDetector( + onPanUpdate: (details) { + if (details.delta.dy > 0) { + _focusNode.unfocus(); + if (_openFilePickerSection) { + setState(() { + _openFilePickerSection = false; + }); + } + } + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (_hasQuotedMessage) + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: StreamSvgIcon.reply( + color: _streamChatTheme.colorTheme.disabled, + ), + ), + Text( + context.translations.replyToMessageLabel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: () { + _effectiveController.clearQuotedMessage(); + _focusNode.unfocus(); + }, + ), + ], + ), + ) + else if (_effectiveController.ogAttachment != null) + OGAttachmentPreview( + attachment: _effectiveController.ogAttachment!, + onDismissPreviewPressed: () { + _effectiveController.clearOGAttachment(); + _focusNode.unfocus(); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: _buildTextField(context), + ), + if (_effectiveController.value.parentId != null && + !widget.hideSendAsDm) + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + bottom: 12, + ), + child: _buildDmCheckbox(), + ), + _buildFilePickerSection(), + ], + ), + ), + ), + ); + if (!_isEditing) { + child = Material( + elevation: widget.elevation ?? + _streamChatTheme.messageInputTheme.elevation ?? + 8, + child: child, + ); + } + return StreamMultiOverlay( + childAnchor: Alignment.topCenter, + overlayAnchor: Alignment.bottomCenter, + overlayOptions: [ + OverlayOptions( + visible: _showCommandsOverlay, + widget: _buildCommandsOverlayEntry(), + ), + OverlayOptions( + visible: _focusNode.hasFocus && + _effectiveController.text.isNotEmpty && + _effectiveController.baseOffset > 0 && + _effectiveController.text + .substring( + 0, + _effectiveController.baseOffset, + ) + .contains(':'), + widget: _buildEmojiOverlay(), + ), + OverlayOptions( + visible: _showMentionsOverlay, + widget: _buildMentionsOverlayEntry(), + ), + ...widget.customOverlays, + ], + child: child, + ); + }, + ); + } + + Flex _buildTextField(BuildContext context) => Flex( + direction: Axis.horizontal, + children: [ + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.left) + _buildExpandActionsButton(context), + _buildTextInput(context), + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.right) + _buildExpandActionsButton(context), + if (widget.sendButtonLocation == SendButtonLocation.outside) + _buildSendButton(context), + ], + ); + + Widget _buildDmCheckbox() => Row( + children: [ + Container( + height: 16, + width: 16, + foregroundDecoration: BoxDecoration( + border: _effectiveController.showInChannel + ? null + : Border.all( + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), + width: 2, + ), + borderRadius: BorderRadius.circular(3), + ), + child: Center( + child: Material( + borderRadius: BorderRadius.circular(3), + color: _effectiveController.showInChannel + ? _streamChatTheme.colorTheme.accentPrimary + : _streamChatTheme.colorTheme.barsBg, + child: InkWell( + onTap: () { + _effectiveController.showInChannel = + !_effectiveController.showInChannel; + }, + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + crossFadeState: _effectiveController.showInChannel + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: StreamSvgIcon.check( + size: 16, + color: _streamChatTheme.colorTheme.barsBg, + ), + secondChild: const SizedBox( + height: 16, + width: 16, + ), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + context.translations.alsoSendAsDirectMessageLabel, + style: _streamChatTheme.textTheme.footnote.copyWith( + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(0.5), + ), + ), + ), + ], + ); + + Widget _buildSendButton(BuildContext context) { + if (widget.sendButtonBuilder != null) { + return widget.sendButtonBuilder!(context, _effectiveController); + } + + return StreamMessageSendButton( + onSendMessage: sendMessage, + timeOut: _timeOut, + isIdle: !widget.validator(_effectiveController.message), + isEditEnabled: _isEditing, + idleSendButton: widget.idleSendButton, + activeSendButton: widget.activeSendButton, + ); + } + + Widget _buildExpandActionsButton(BuildContext context) { + final channel = StreamChannel.of(context).channel; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: AnimatedCrossFade( + crossFadeState: _actionsShrunk + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstCurve: Curves.easeOut, + secondCurve: Curves.easeIn, + firstChild: IconButton( + onPressed: () { + if (_actionsShrunk) { + setState(() => _actionsShrunk = false); + } + }, + icon: Transform.rotate( + angle: (widget.actionsLocation == ActionsLocation.right || + widget.actionsLocation == ActionsLocation.rightInside) + ? pi + : 0, + child: StreamSvgIcon.emptyCircleLeft( + color: _messageInputTheme.expandButtonColor, + ), + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + ), + secondChild: widget.disableAttachments && + !widget.showCommandsButton && + !widget.actions.isNotEmpty + ? const Offstage() + : Wrap( + children: [ + if (!widget.disableAttachments && + channel.ownCapabilities + .contains(PermissionType.uploadFile)) + _buildAttachmentButton(context), + if (widget.showCommandsButton && + !_isEditing && + channel.state != null && + channel.config?.commands.isNotEmpty == true) + _buildCommandButton(context), + ...widget.actions, + ].insertBetween(const SizedBox(width: 8)), + ), + duration: const Duration(milliseconds: 300), + alignment: Alignment.center, + ), + ); + } + + Expanded _buildTextInput(BuildContext context) { + final margin = (widget.sendButtonLocation == SendButtonLocation.inside + ? const EdgeInsets.only(right: 8) + : EdgeInsets.zero) + + (widget.actionsLocation != ActionsLocation.left || _commandEnabled + ? const EdgeInsets.only(left: 8) + : EdgeInsets.zero); + return Expanded( + child: Container( + clipBehavior: Clip.hardEdge, + margin: margin, + decoration: BoxDecoration( + borderRadius: _messageInputTheme.borderRadius, + gradient: _focusNode.hasFocus + ? _messageInputTheme.activeBorderGradient + : _messageInputTheme.idleBorderGradient, + ), + child: Padding( + padding: const EdgeInsets.all(1.5), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: _messageInputTheme.borderRadius, + color: _messageInputTheme.inputBackgroundColor, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildReplyToMessage(), + _buildAttachments(), + LimitedBox( + maxHeight: widget.maxHeight, + child: StreamMessageTextField( + key: const Key('messageInputText'), + enabled: _inputEnabled, + maxLines: null, + onSubmitted: (_) => sendMessage(), + keyboardType: widget.keyboardType, + controller: _effectiveController, + focusNode: _focusNode, + style: _messageInputTheme.inputTextStyle, + autofocus: widget.autofocus, + textAlignVertical: TextAlignVertical.center, + decoration: _getInputDecoration(context), + textCapitalization: TextCapitalization.sentences, + ), + ), + ], + ), + ), + ), + ), + ); + } + + InputDecoration _getInputDecoration(BuildContext context) { + final passedDecoration = _messageInputTheme.inputDecoration; + return InputDecoration( + isDense: true, + hintText: _getHint(context), + hintStyle: _messageInputTheme.inputTextStyle!.copyWith( + color: _streamChatTheme.colorTheme.textLowEmphasis, + ), + border: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + errorBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + disabledBorder: const OutlineInputBorder( + borderSide: BorderSide( + color: Colors.transparent, + ), + ), + contentPadding: const EdgeInsets.fromLTRB(16, 12, 13, 11), + prefixIcon: _commandEnabled + ? Row( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Container( + constraints: BoxConstraints.tight(const Size(64, 24)), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: _streamChatTheme.colorTheme.accentPrimary, + ), + alignment: Alignment.center, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamSvgIcon.lightning( + color: Colors.white, + size: 16, + ), + Text( + _effectiveController.value.command!.toUpperCase(), + style: + _streamChatTheme.textTheme.footnoteBold.copyWith( + color: Colors.white, + ), + ), + ], + ), + ), + ), + ], + ) + : (widget.actionsLocation == ActionsLocation.leftInside + ? Row( + mainAxisSize: MainAxisSize.min, + children: [_buildExpandActionsButton(context)], + ) + : null), + suffixIconConstraints: const BoxConstraints.tightFor(height: 40), + prefixIconConstraints: const BoxConstraints.tightFor(height: 40), + suffixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_commandEnabled) + Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + icon: StreamSvgIcon.closeSmall(), + splashRadius: 24, + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + onPressed: _effectiveController.clear, + ), + ), + if (!_commandEnabled && + widget.actionsLocation == ActionsLocation.rightInside) + _buildExpandActionsButton(context), + if (widget.sendButtonLocation == SendButtonLocation.inside) + _buildSendButton(context), + ], + ), + ).merge(passedDecoration); + } + + late final _onChangedDebounced = debounce( + () { + var value = _effectiveController.text; + if (!mounted) return; + value = value.trim(); + + final channel = StreamChannel.of(context).channel; + if (channel.ownCapabilities.contains(PermissionType.sendTypingEvents) && + value.isNotEmpty) { + channel + .keyStroke(_effectiveController.value.parentId) + // ignore: no-empty-block + .catchError((e) {}); + } + + var actionsLength = widget.actions.length; + if (widget.showCommandsButton) actionsLength += 1; + if (!widget.disableAttachments) actionsLength += 1; + + setState(() { + _actionsShrunk = value.isNotEmpty && actionsLength > 1; + }); + + _checkContainsUrl(value, context); + _checkCommands(value, context); + _checkMentions(value, context); + _checkEmoji(value, context); + }, + const Duration(milliseconds: 350), + leading: true, + ); + + String _getHint(BuildContext context) { + if (_commandEnabled && _effectiveController.value.command == 'giphy') { + return context.translations.searchGifLabel; + } + if (_effectiveController.attachments.isNotEmpty) { + return context.translations.addACommentOrSendLabel; + } + if (_timeOut != 0) { + return context.translations.slowModeOnLabel; + } + + return context.translations.writeAMessageLabel; + } + + String? _lastSearchedContainsUrlText; + CancelableOperation? _enrichUrlOperation; + final _urlRegex = RegExp( + r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+', + ); + + void _checkContainsUrl(String value, BuildContext context) async { + // Cancel the previous operation if it's still running + _enrichUrlOperation?.cancel(); + + // If the text is same as the last time, don't do anything + if (_lastSearchedContainsUrlText == value) return; + _lastSearchedContainsUrlText = value; + + final matchedUrls = _urlRegex.allMatches(value).toList() + ..removeWhere((it) => it.group(0)?.split('.').last.isValidTLD() == false); + + // Reset the og attachment if the text doesn't contain any url + if (matchedUrls.isEmpty || + !StreamChannel.of(context) + .channel + .ownCapabilities + .contains(PermissionType.sendLinks)) { + _effectiveController + ..text = value + ..clearOGAttachment(); + return; + } + + final firstMatchedUrl = matchedUrls.first.group(0)!; + + // If the parsed url matches the ogAttachment url, don't do anything + if (_effectiveController.ogAttachment?.titleLink == firstMatchedUrl) { + return; + } + + final client = StreamChat.of(context).client; + + _enrichUrlOperation = CancelableOperation.fromFuture( + _enrichUrl(firstMatchedUrl, client), + ).then( + (ogAttachment) { + final attachment = Attachment.fromOGAttachment(ogAttachment); + _effectiveController.setOGAttachment(attachment); + }, + onError: (error, stackTrace) { + // Reset the ogAttachment if there was an error + _effectiveController.clearOGAttachment(); + widget.onError?.call(error, stackTrace); + }, + ); + } + + final _ogAttachmentCache = {}; + + Future _enrichUrl( + String url, + StreamChatClient client, + ) async { + var response = _ogAttachmentCache[url]; + if (response == null) { + final client = StreamChat.of(context).client; + response = await client.enrichUrl(url); + _ogAttachmentCache[url] = response; + } + return response; + } + + void _checkEmoji(String value, BuildContext context) { + if (value.isNotEmpty && + _effectiveController.baseOffset > 0 && + _effectiveController.text + .substring(0, _effectiveController.baseOffset) + .contains(':')) { + final textToSelection = _effectiveController.text.substring( + 0, + _effectiveController.selectionStart, + ); + final splits = textToSelection.split(':'); + final query = splits[splits.length - 2].toLowerCase(); + final emoji = Emoji.byName(query); + + if (textToSelection.endsWith(':') && emoji != null) { + _chooseEmoji(splits.sublist(0, splits.length - 1), emoji); + } + } + } + + void _checkMentions(String value, BuildContext context) { + if (value.isNotEmpty && + _effectiveController.baseOffset > 0 && + _effectiveController.text + .substring(0, _effectiveController.baseOffset) + .split(' ') + .last + .contains('@')) { + if (!_showMentionsOverlay) { + setState(() { + _showMentionsOverlay = true; + }); + } + } else if (_showMentionsOverlay) { + setState(() { + _showMentionsOverlay = false; + }); + } + } + + void _checkCommands(String value, BuildContext context) { + if (value.startsWith('/')) { + final allCommands = StreamChannel.of(context).channel.config?.commands; + final command = + allCommands?.firstWhereOrNull((it) => it.name == value.substring(1)); + if (command != null) { + return _setCommand(command); + } else if (!_showCommandsOverlay) { + setState(() { + _showCommandsOverlay = true; + }); + } + } else if (_showCommandsOverlay) { + setState(() { + _showCommandsOverlay = false; + }); + } + } + + Widget _buildCommandsOverlayEntry() { + final text = _effectiveController.text.trimLeft(); + + final renderObject = context.findRenderObject() as RenderBox?; + if (renderObject == null) { + return const Offstage(); + } + return StreamCommandsOverlay( + channel: StreamChannel.of(context).channel, + size: Size(renderObject.size.width - 16, 400), + text: text, + onCommandResult: _setCommand, + ); + } + + Widget _buildFilePickerSection() { + final picker = StreamAttachmentPicker( + messageInputController: _effectiveController, + onFilePicked: pickFile, + isOpen: _openFilePickerSection, + pickerSize: _openFilePickerSection ? _kMinMediaPickerSize : 0, + attachmentLimit: widget.attachmentLimit, + onAttachmentLimitExceeded: widget.onAttachmentLimitExceed, + maxAttachmentSize: widget.maxAttachmentSize, + onError: _showErrorAlert, + ); + + if (_openFilePickerSection && widget.attachmentsPickerBuilder != null) { + return widget.attachmentsPickerBuilder!( + context, + _effectiveController, + picker, + ); + } + + return picker; + } + + Widget _buildMentionsOverlayEntry() { + final channel = StreamChannel.of(context).channel; + if (_effectiveController.selectionStart < 0 || channel.state == null) { + return const Offstage(); + } + + final splits = _effectiveController.text + .substring(0, _effectiveController.selectionStart) + .split('@'); + final query = splits.last.toLowerCase(); + + // ignore: cast_nullable_to_non_nullable + final renderObject = context.findRenderObject() as RenderBox; + + var tileBuilder = widget.userMentionsTileBuilder; + if (tileBuilder == null && widget.mentionsTileBuilder != null) { + tileBuilder = (context, user) { + final member = Member( + user: user, + userId: user.id, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + ); + return widget.mentionsTileBuilder!(context, member); + }; + } + + return LayoutBuilder( + builder: (context, snapshot) => StreamUserMentionsOverlay( + query: query, + mentionAllAppUsers: widget.mentionAllAppUsers, + client: StreamChat.of(context).client, + channel: channel, + size: Size( + renderObject.size.width - 16, + min(400, (snapshot.maxHeight - renderObject.size.height - 16).abs()), + ), + mentionsTileBuilder: tileBuilder, + onMentionUserTap: (user) { + _effectiveController.addMentionedUser(user); + splits[splits.length - 1] = user.name; + final rejoin = splits.join('@'); + + _effectiveController.text = + '$rejoin${_effectiveController.text.substring( + _effectiveController.selectionStart, + )}'; + + _onChangedDebounced.cancel(); + setState(() => _showMentionsOverlay = false); + }, + ), + ); + } + + Widget _buildEmojiOverlay() { + if (_effectiveController.baseOffset < 0) { + return const Offstage(); + } + + final splits = _effectiveController.text + .substring(0, _effectiveController.baseOffset) + .split(':'); + + final query = splits.last.toLowerCase(); + // ignore: cast_nullable_to_non_nullable + final renderObject = context.findRenderObject() as RenderBox; + + return StreamEmojiOverlay( + size: Size(renderObject.size.width - 16, 200), + query: query, + onEmojiResult: (emoji) { + _chooseEmoji(splits, emoji); + }, + ); + } + + void _chooseEmoji(List splits, Emoji emoji) { + final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; + + _effectiveController.text = rejoin + + _effectiveController.text.substring( + _effectiveController.selectionStart, + ); + } + + void _setCommand(Command c) { + _effectiveController + ..clear() + ..command = c; + setState(() { + _showCommandsOverlay = false; + }); + } + + Widget _buildReplyToMessage() { + if (!_hasQuotedMessage) return const Offstage(); + final containsUrl = _effectiveController.value.quotedMessage!.attachments + .any((element) => element.titleLink != null); + return StreamQuotedMessageWidget( + reverse: true, + showBorder: !containsUrl, + message: _effectiveController.value.quotedMessage!, + messageTheme: _streamChatTheme.otherMessageTheme, + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + ); + } + + Widget _buildAttachments() { + final nonOGAttachments = _effectiveController.attachments.where( + (it) => it.titleLink == null, + ); + if (nonOGAttachments.isEmpty) return const Offstage(); + final fileAttachments = nonOGAttachments + .where((it) => it.type == 'file') + .toList(growable: false); + final remainingAttachments = nonOGAttachments + .where((it) => it.type != 'file') + .toList(growable: false); + return Column( + children: [ + if (fileAttachments.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: LimitedBox( + maxHeight: 136, + child: ListView( + reverse: true, + shrinkWrap: true, + children: fileAttachments.reversed + .map( + (e) => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: StreamFileAttachment( + message: Message(), // dummy message + attachment: e, + size: Size( + MediaQuery.of(context).size.width * 0.65, + 56, + ), + trailing: Padding( + padding: const EdgeInsets.all(8), + child: _buildRemoveButton(e), + ), + ), + ), + ) + .insertBetween(const SizedBox(height: 8)), + ), + ), + ), + if (remainingAttachments.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), + child: LimitedBox( + maxHeight: 104, + child: ListView( + scrollDirection: Axis.horizontal, + children: remainingAttachments + .map( + (attachment) => ClipRRect( + borderRadius: BorderRadius.circular(10), + child: Stack( + children: [ + AspectRatio( + aspectRatio: 1, + child: SizedBox( + height: 104, + width: 104, + child: _buildAttachment(attachment), + ), + ), + Positioned( + top: 8, + right: 8, + child: _buildRemoveButton(attachment), + ), + ], + ), + ), + ) + .insertBetween(const SizedBox(width: 8)), + ), + ), + ), + ], + ); + } + + Widget _buildRemoveButton(Attachment attachment) => SizedBox( + height: 24, + width: 24, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: () { + _effectiveController.removeAttachmentById(attachment.id); + }, + fillColor: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.5), + child: Center( + child: StreamSvgIcon.close( + size: 24, + color: _streamChatTheme.colorTheme.barsBg, + ), + ), + ), + ); + + Widget _buildAttachment(Attachment attachment) { + if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) == + true) { + return widget.attachmentThumbnailBuilders![attachment.type!]!( + context, + attachment, + ); + } + + switch (attachment.type) { + case 'image': + case 'giphy': + return attachment.file != null + ? Image.memory( + attachment.file!.bytes!, + fit: BoxFit.cover, + errorBuilder: (context, _, __) => Image.asset( + 'images/placeholder.png', + package: 'stream_chat_flutter', + ), + ) + : CachedNetworkImage( + imageUrl: attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl!, + fit: BoxFit.cover, + errorWidget: (_, obj, trace) => + getFileTypeImage(attachment.extraData['other'] as String?), + placeholder: (context, _) => Shimmer.fromColors( + baseColor: _streamChatTheme.colorTheme.disabled, + highlightColor: _streamChatTheme.colorTheme.inputBg, + child: Image.asset( + 'images/placeholder.png', + fit: BoxFit.cover, + package: 'stream_chat_flutter', + ), + ), + ); + case 'video': + return Stack( + children: [ + StreamVideoThumbnailImage( + height: 104, + width: 104, + video: (attachment.file?.path ?? attachment.assetUrl)!, + fit: BoxFit.cover, + ), + Positioned( + left: 8, + bottom: 10, + child: SvgPicture.asset( + 'svgs/video_call_icon.svg', + package: 'stream_chat_flutter', + ), + ), + ], + ); + default: + return Container( + color: Colors.black26, + child: const Icon(Icons.insert_drive_file), + ); + } + } + + Widget _buildCommandButton(BuildContext context) { + final s = _effectiveController.text.trim(); + final defaultButton = IconButton( + icon: StreamSvgIcon.lightning( + color: s.isNotEmpty + ? _streamChatTheme.colorTheme.disabled + : (_showCommandsOverlay + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor), + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + if (_openFilePickerSection) { + setState(() => _openFilePickerSection = false); + await Future.delayed(const Duration(milliseconds: 300)); + } + + setState(() { + _showCommandsOverlay = !_showCommandsOverlay; + }); + }, + ); + + return widget.commandButtonBuilder?.call(context, defaultButton) ?? + defaultButton; + } + + Widget _buildAttachmentButton(BuildContext context) { + final defaultButton = IconButton( + icon: StreamSvgIcon.attach( + color: _openFilePickerSection + ? _messageInputTheme.actionButtonColor + : _messageInputTheme.actionButtonIdleColor, + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + _showCommandsOverlay = false; + _showMentionsOverlay = false; + + if (_openFilePickerSection) { + setState(() => _openFilePickerSection = false); + } else { + showAttachmentModal(); + } + }, + ); + + return widget.attachmentButtonBuilder?.call(context, defaultButton) ?? + defaultButton; + } + + /// Show the attachment modal, making the user choose where to + /// pick a media from + void showAttachmentModal() { + if (_focusNode.hasFocus) { + _focusNode.unfocus(); + } + + if (!kIsWeb) { + setState(() { + _openFilePickerSection = true; + }); + } else { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + isScrollControlled: true, + builder: (_) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + title: Text( + context.translations.addAFileLabel, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + ListTile( + leading: const Icon(Icons.image), + title: Text(context.translations.uploadAPhotoLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.image); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.video_library), + title: Text(context.translations.uploadAVideoLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.video); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.insert_drive_file), + title: Text(context.translations.uploadAFileLabel), + onTap: () { + pickFile(DefaultAttachmentTypes.file); + Navigator.pop(context); + }, + ), + ], + ), + ); + } + } + + /// Adds an attachment to the [messageInputController.attachments] map + void _addAttachments(Iterable attachments) { + final limit = widget.attachmentLimit; + final length = _effectiveController.attachments.length + attachments.length; + if (length > limit) { + final onAttachmentLimitExceed = widget.onAttachmentLimitExceed; + if (onAttachmentLimitExceed != null) { + return onAttachmentLimitExceed( + widget.attachmentLimit, + context.translations.attachmentLimitExceedError(limit), + ); + } + return _showErrorAlert( + context.translations.attachmentLimitExceedError(limit), + ); + } + for (final attachment in attachments) { + _effectiveController.addAttachment(attachment); + } + } + + /// Pick a file from the device + /// If [camera] is true then the camera will open + void pickFile( + DefaultAttachmentTypes fileType, { + bool camera = false, + }) async { + setState(() => _inputEnabled = false); + + AttachmentFile? file; + String? attachmentType; + + if (fileType == DefaultAttachmentTypes.image) { + attachmentType = 'image'; + } else if (fileType == DefaultAttachmentTypes.video) { + attachmentType = 'video'; + } else if (fileType == DefaultAttachmentTypes.file) { + attachmentType = 'file'; + } + + if (camera) { + XFile? pickedFile; + if (fileType == DefaultAttachmentTypes.image) { + pickedFile = await _imagePicker.pickImage(source: ImageSource.camera); + } else if (fileType == DefaultAttachmentTypes.video) { + pickedFile = await _imagePicker.pickVideo(source: ImageSource.camera); + } + if (pickedFile != null) { + final bytes = await pickedFile.readAsBytes(); + file = AttachmentFile( + size: bytes.length, + path: pickedFile.path, + bytes: bytes, + ); + } + } else { + late FileType type; + if (fileType == DefaultAttachmentTypes.image) { + type = FileType.image; + } else if (fileType == DefaultAttachmentTypes.video) { + type = FileType.video; + } else if (fileType == DefaultAttachmentTypes.file) { + type = FileType.any; + } + final res = await FilePicker.platform.pickFiles( + type: type, + ); + if (res?.files.isNotEmpty == true) { + file = res!.files.single.toAttachmentFile; + } + } + + setState(() => _inputEnabled = true); + + if (file == null) return; + + final mimeType = file.name?.mimeType ?? file.path!.split('/').last.mimeType; + + final extraDataMap = {}; + + if (mimeType?.subtype != null) { + extraDataMap['mime_type'] = mimeType!.subtype.toLowerCase(); + } + + extraDataMap['file_size'] = file.size!; + + final attachment = Attachment( + file: file, + type: attachmentType, + uploadState: const UploadState.preparing(), + extraData: extraDataMap, + ); + + if (file.size! > widget.maxAttachmentSize) { + return _showErrorAlert( + context.translations.fileTooLargeError( + widget.maxAttachmentSize / (1024 * 1024), + ), + ); + } + + _addAttachments([ + attachment.copyWith( + file: file, + extraData: {...attachment.extraData} + ..update('file_size', ((_) => file!.size!)), + ), + ]); + } + + /// Sends the current message + Future sendMessage() async { + final streamChannel = StreamChannel.of(context); + var message = _effectiveController.value; + if (!streamChannel.channel.ownCapabilities + .contains(PermissionType.sendLinks) && + _urlRegex.allMatches(message.text ?? '').any((element) => + element.group(0)?.split('.').last.isValidTLD() == true)) { + showInfoDialog( + context, + icon: StreamSvgIcon.error( + color: StreamChatTheme.of(context).colorTheme.accentError, + size: 24, + ), + title: 'Links are disabled', + details: 'Sending links is not allowed in this conversation.', + okText: context.translations.okLabel, + ); + return; + } + + final skipEnrichUrl = _effectiveController.ogAttachment == null; + + var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; + + shouldKeepFocus ??= !_commandEnabled; + + _effectiveController.reset(); + + if (widget.preMessageSending != null) { + message = await widget.preMessageSending!(message); + } + + final channel = streamChannel.channel; + if (!channel.state!.isUpToDate) { + await streamChannel.reloadChannel(); + } + + message = message.replaceMentionsWithId(); + + try { + Future sendingFuture; + if (_isEditing) { + sendingFuture = channel.updateMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); + } else { + sendingFuture = channel.sendMessage( + message, + skipEnrichUrl: skipEnrichUrl, + ); + } + + if (shouldKeepFocus) { + FocusScope.of(context).requestFocus(_focusNode); + } else { + FocusScope.of(context).unfocus(); + } + + final resp = await sendingFuture; + if (resp.message?.type == 'error') { + _effectiveController.value = message; + } + _startSlowMode(); + widget.onMessageSent?.call(resp.message); + } catch (e, stk) { + if (widget.onError != null) { + widget.onError?.call(e, stk); + } else { + rethrow; + } + } + } + + void _showErrorAlert(String description) { + showModalBottomSheet( + backgroundColor: _streamChatTheme.colorTheme.barsBg, + context: context, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + ), + builder: (context) => Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox( + height: 26, + ), + StreamSvgIcon.error( + color: _streamChatTheme.colorTheme.accentError, + size: 24, + ), + const SizedBox( + height: 26, + ), + Text( + context.translations.somethingWentWrongError, + style: _streamChatTheme.textTheme.headlineBold, + ), + const SizedBox( + height: 7, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + description, + textAlign: TextAlign.center, + ), + ), + const SizedBox( + height: 36, + ), + Container( + color: + _streamChatTheme.colorTheme.textHighEmphasis.withOpacity(0.08), + height: 1, + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + context.translations.okLabel, + style: _streamChatTheme.textTheme.bodyBold.copyWith( + color: _streamChatTheme.colorTheme.accentPrimary, + ), + ), + ), + ], + ), + ], + ), + ); + } + + @override + void dispose() { + _effectiveController.textEditingController + .removeListener(_onChangedDebounced); + _controller?.dispose(); + _focusNode.removeListener(_focusNodeListener); + if (_isInternalFocusNode) _focusNode.dispose(); + _stopSlowMode(); + _onChangedDebounced.cancel(); + super.dispose(); + } + + @override + void didChangeDependencies() { + _streamChatTheme = StreamChatTheme.of(context); + _messageInputTheme = StreamMessageInputTheme.of(context); + + super.didChangeDependencies(); + } +} + +/// Preview of an Open Graph attachment. +class OGAttachmentPreview extends StatelessWidget { + /// Returns a new instance of [OGAttachmentPreview] + const OGAttachmentPreview({ + Key? key, + required this.attachment, + this.onDismissPreviewPressed, + }) : super(key: key); + + /// The attachment to be rendered. + final Attachment attachment; + + /// Called when the dismiss button is pressed. + final VoidCallback? onDismissPreviewPressed; + + @override + Widget build(BuildContext context) { + final chatTheme = StreamChatTheme.of(context); + final textTheme = chatTheme.textTheme; + final colorTheme = chatTheme.colorTheme; + + final attachmentTitle = attachment.title; + final attachmentText = attachment.text; + + return Row( + children: [ + Padding( + padding: const EdgeInsets.all(8), + child: Icon( + Icons.link, + color: colorTheme.accentPrimary, + ), + ), + Expanded( + child: Container( + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: colorTheme.accentPrimary, + width: 2, + ), + ), + ), + padding: const EdgeInsets.only(left: 6), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (attachmentTitle != null) + Text( + attachmentTitle.trim(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.body.copyWith(fontWeight: FontWeight.w700), + ), + if (attachmentText != null) + Text( + attachmentText, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.body.copyWith(fontWeight: FontWeight.w400), + ), + ], + ), + ), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: onDismissPreviewPressed, + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart new file mode 100644 index 00000000..5f3d7391 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/simple_safe_area.dart @@ -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, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart new file mode 100644 index 00000000..2c254223 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart @@ -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? 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 allowedAttachmentTypes; + + /// The list of custom attachment types that can be picked. + final List 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? onChangeInputState, + ValueChanged? onError, + List? allowedAttachmentTypes, + List? 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 createState() => _StreamAttachmentPickerState(); +} + +class _StreamAttachmentPickerState extends State { + 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 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 selectedMedias; + final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; + final void Function(AssetEntity) onMediaSelected; + final StreamChatThemeData streamChatTheme; + final List allowedAttachmentTypes; + final List customAttachmentTypes; + + @override + _PickerWidgetState createState() => _PickerWidgetState(); +} + +class _PickerWidgetState extends State<_PickerWidget> { + Future? 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( + 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; +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart new file mode 100644 index 00000000..67a06f67 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart @@ -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'; + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart new file mode 100644 index 00000000..8e4a34eb --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -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? 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? onSubmitted; + + /// {@macro flutter.widgets.editableText.onAppPrivateCommand} + final AppPrivateCommandCallback? onAppPrivateCommand; + + /// {@macro flutter.widgets.editableText.inputFormatters} + final List? 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], + /// [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? 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, + defaultValue: null)); + properties + .add(DiagnosticsProperty('enabled', enabled, defaultValue: null)); + properties.add(DiagnosticsProperty( + 'decoration', decoration, + defaultValue: const InputDecoration())); + properties.add(DiagnosticsProperty( + 'keyboardType', keyboardType, + defaultValue: TextInputType.text)); + properties.add( + DiagnosticsProperty('style', style, defaultValue: null)); + properties.add( + DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); + properties.add(DiagnosticsProperty( + 'obscuringCharacter', obscuringCharacter, + defaultValue: '•')); + properties.add(DiagnosticsProperty('obscureText', obscureText, + defaultValue: false)); + properties.add(DiagnosticsProperty('autocorrect', autocorrect, + defaultValue: true)); + properties.add(EnumProperty( + 'smartDashesType', smartDashesType, + defaultValue: + obscureText ? SmartDashesType.disabled : SmartDashesType.enabled)); + properties.add(EnumProperty( + 'smartQuotesType', smartQuotesType, + defaultValue: + obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled)); + properties.add(DiagnosticsProperty( + 'enableSuggestions', enableSuggestions, + defaultValue: true)); + properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); + properties.add(IntProperty('minLines', minLines, defaultValue: null)); + properties.add( + DiagnosticsProperty('expands', expands, defaultValue: false)); + properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); + properties.add(EnumProperty( + 'maxLengthEnforcement', maxLengthEnforcement, + defaultValue: null)); + properties.add(EnumProperty( + 'textInputAction', textInputAction, + defaultValue: null)); + properties.add(EnumProperty( + 'textCapitalization', textCapitalization, + defaultValue: TextCapitalization.none)); + properties.add(EnumProperty('textAlign', textAlign, + defaultValue: TextAlign.start)); + properties.add(DiagnosticsProperty( + 'textAlignVertical', textAlignVertical, + defaultValue: null)); + properties.add(EnumProperty('textDirection', textDirection, + defaultValue: null)); + properties + .add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); + properties + .add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); + properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, + defaultValue: null)); + properties + .add(ColorProperty('cursorColor', cursorColor, defaultValue: null)); + properties.add(DiagnosticsProperty( + 'keyboardAppearance', keyboardAppearance, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollPadding', scrollPadding, + defaultValue: const EdgeInsets.all(20))); + properties.add(FlagProperty('selectionEnabled', + value: selectionEnabled, + defaultValue: true, + ifFalse: 'selection disabled')); + properties.add(DiagnosticsProperty( + 'selectionControls', selectionControls, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollController', scrollController, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'scrollPhysics', scrollPhysics, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, + defaultValue: true)); + } +} + +class _StreamMessageTextFieldState extends State + with RestorationMixin { + 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(); + } +} diff --git a/packages/stream_chat_flutter/lib/src/message_input/tld.dart b/packages/stream_chat_flutter/lib/src/message_input/tld.dart new file mode 100644 index 00000000..2bfa5257 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/message_input/tld.dart @@ -0,0 +1,1553 @@ +/// Extension on String adding utilities checking TLD validity. +extension TLDString on String { + /// Returns true if the string is a valid TLD. + bool isValidTLD() => + isNotEmpty && + tlds.containsKey(this[0].toUpperCase()) && + tlds[this[0].toUpperCase()]!.contains(toUpperCase()); +} + +/// List of valid TLDs. +/// https://data.iana.org/TLD/tlds-alpha-by-domain.txt +const tlds = { + 'A': [ + 'AAA', + 'AARP', + 'ABARTH', + 'ABB', + 'ABBOTT', + 'ABBVIE', + 'ABC', + 'ABLE', + 'ABOGADO', + 'ABUDHABI', + 'AC', + 'ACADEMY', + 'ACCENTURE', + 'ACCOUNTANT', + 'ACCOUNTANTS', + 'ACO', + 'ACTOR', + 'AD', + 'ADAC', + 'ADS', + 'ADULT', + 'AE', + 'AEG', + 'AERO', + 'AETNA', + 'AF', + 'AFL', + 'AFRICA', + 'AG', + 'AGAKHAN', + 'AGENCY', + 'AI', + 'AIG', + 'AIRBUS', + 'AIRFORCE', + 'AIRTEL', + 'AKDN', + 'AL', + 'ALFAROMEO', + 'ALIBABA', + 'ALIPAY', + 'ALLFINANZ', + 'ALLSTATE', + 'ALLY', + 'ALSACE', + 'ALSTOM', + 'AM', + 'AMAZON', + 'AMERICANEXPRESS', + 'AMERICANFAMILY', + 'AMEX', + 'AMFAM', + 'AMICA', + 'AMSTERDAM', + 'ANALYTICS', + 'ANDROID', + 'ANQUAN', + 'ANZ', + 'AO', + 'AOL', + 'APARTMENTS', + 'APP', + 'APPLE', + 'AQ', + 'AQUARELLE', + 'AR', + 'ARAB', + 'ARAMCO', + 'ARCHI', + 'ARMY', + 'ARPA', + 'ART', + 'ARTE', + 'AS', + 'ASDA', + 'ASIA', + 'ASSOCIATES', + 'AT', + 'ATHLETA', + 'ATTORNEY', + 'AU', + 'AUCTION', + 'AUDI', + 'AUDIBLE', + 'AUDIO', + 'AUSPOST', + 'AUTHOR', + 'AUTO', + 'AUTOS', + 'AVIANCA', + 'AW', + 'AWS', + 'AX', + 'AXA', + 'AZ', + 'AZURE', + ], + 'B': [ + 'BA', + 'BABY', + 'BAIDU', + 'BANAMEX', + 'BANANAREPUBLIC', + 'BAND', + 'BANK', + 'BAR', + 'BARCELONA', + 'BARCLAYCARD', + 'BARCLAYS', + 'BAREFOOT', + 'BARGAINS', + 'BASEBALL', + 'BASKETBALL', + 'BAUHAUS', + 'BAYERN', + 'BB', + 'BBC', + 'BBT', + 'BBVA', + 'BCG', + 'BCN', + 'BD', + 'BE', + 'BEATS', + 'BEAUTY', + 'BEER', + 'BENTLEY', + 'BERLIN', + 'BEST', + 'BESTBUY', + 'BET', + 'BF', + 'BG', + 'BH', + 'BHARTI', + 'BI', + 'BIBLE', + 'BID', + 'BIKE', + 'BING', + 'BINGO', + 'BIO', + 'BIZ', + 'BJ', + 'BLACK', + 'BLACKFRIDAY', + 'BLOCKBUSTER', + 'BLOG', + 'BLOOMBERG', + 'BLUE', + 'BM', + 'BMS', + 'BMW', + 'BN', + 'BNPPARIBAS', + 'BO', + 'BOATS', + 'BOEHRINGER', + 'BOFA', + 'BOM', + 'BOND', + 'BOO', + 'BOOK', + 'BOOKING', + 'BOSCH', + 'BOSTIK', + 'BOSTON', + 'BOT', + 'BOUTIQUE', + 'BOX', + 'BR', + 'BRADESCO', + 'BRIDGESTONE', + 'BROADWAY', + 'BROKER', + 'BROTHER', + 'BRUSSELS', + 'BS', + 'BT', + 'BUDAPEST', + 'BUGATTI', + 'BUILD', + 'BUILDERS', + 'BUSINESS', + 'BUY', + 'BUZZ', + 'BV', + 'BW', + 'BY', + 'BZ', + 'BZH', + ], + 'C': [ + 'CA', + 'CAB', + 'CAFE', + 'CAL', + 'CALL', + 'CALVINKLEIN', + 'CAM', + 'CAMERA', + 'CAMP', + 'CANCERRESEARCH', + 'CANON', + 'CAPETOWN', + 'CAPITAL', + 'CAPITALONE', + 'CAR', + 'CARAVAN', + 'CARDS', + 'CARE', + 'CAREER', + 'CAREERS', + 'CARS', + 'CASA', + 'CASE', + 'CASH', + 'CASINO', + 'CAT', + 'CATERING', + 'CATHOLIC', + 'CBA', + 'CBN', + 'CBRE', + 'CBS', + 'CC', + 'CD', + 'CENTER', + 'CEO', + 'CERN', + 'CF', + 'CFA', + 'CFD', + 'CG', + 'CH', + 'CHANEL', + 'CHANNEL', + 'CHARITY', + 'CHASE', + 'CHAT', + 'CHEAP', + 'CHINTAI', + 'CHRISTMAS', + 'CHROME', + 'CHURCH', + 'CI', + 'CIPRIANI', + 'CIRCLE', + 'CISCO', + 'CITADEL', + 'CITI', + 'CITIC', + 'CITY', + 'CITYEATS', + 'CK', + 'CL', + 'CLAIMS', + 'CLEANING', + 'CLICK', + 'CLINIC', + 'CLINIQUE', + 'CLOTHING', + 'CLOUD', + 'CLUB', + 'CLUBMED', + 'CM', + 'CN', + 'CO', + 'COACH', + 'CODES', + 'COFFEE', + 'COLLEGE', + 'COLOGNE', + 'COM', + 'COMCAST', + 'COMMBANK', + 'COMMUNITY', + 'COMPANY', + 'COMPARE', + 'COMPUTER', + 'COMSEC', + 'CONDOS', + 'CONSTRUCTION', + 'CONSULTING', + 'CONTACT', + 'CONTRACTORS', + 'COOKING', + 'COOKINGCHANNEL', + 'COOL', + 'COOP', + 'CORSICA', + 'COUNTRY', + 'COUPON', + 'COUPONS', + 'COURSES', + 'CPA', + 'CR', + 'CREDIT', + 'CREDITCARD', + 'CREDITUNION', + 'CRICKET', + 'CROWN', + 'CRS', + 'CRUISE', + 'CRUISES', + 'CSC', + 'CU', + 'CUISINELLA', + 'CV', + 'CW', + 'CX', + 'CY', + 'CYMRU', + 'CYOU', + 'CZ', + ], + 'D': [ + 'DABUR', + 'DAD', + 'DANCE', + 'DATA', + 'DATE', + 'DATING', + 'DATSUN', + 'DAY', + 'DCLK', + 'DDS', + 'DE', + 'DEAL', + 'DEALER', + 'DEALS', + 'DEGREE', + 'DELIVERY', + 'DELL', + 'DELOITTE', + 'DELTA', + 'DEMOCRAT', + 'DENTAL', + 'DENTIST', + 'DESI', + 'DESIGN', + 'DEV', + 'DHL', + 'DIAMONDS', + 'DIET', + 'DIGITAL', + 'DIRECT', + 'DIRECTORY', + 'DISCOUNT', + 'DISCOVER', + 'DISH', + 'DIY', + 'DJ', + 'DK', + 'DM', + 'DNP', + 'DO', + 'DOCS', + 'DOCTOR', + 'DOG', + 'DOMAINS', + 'DOT', + 'DOWNLOAD', + 'DRIVE', + 'DTV', + 'DUBAI', + 'DUNLOP', + 'DUPONT', + 'DURBAN', + 'DVAG', + 'DVR', + 'DZ', + ], + 'E': [ + 'EARTH', + 'EAT', + 'EC', + 'ECO', + 'EDEKA', + 'EDU', + 'EDUCATION', + 'EE', + 'EG', + 'EMAIL', + 'EMERCK', + 'ENERGY', + 'ENGINEER', + 'ENGINEERING', + 'ENTERPRISES', + 'EPSON', + 'EQUIPMENT', + 'ER', + 'ERICSSON', + 'ERNI', + 'ES', + 'ESQ', + 'ESTATE', + 'ET', + 'ETISALAT', + 'EU', + 'EUROVISION', + 'EUS', + 'EVENTS', + 'EXCHANGE', + 'EXPERT', + 'EXPOSED', + 'EXPRESS', + 'EXTRASPACE', + ], + 'F': [ + 'FAGE', + 'FAIL', + 'FAIRWINDS', + 'FAITH', + 'FAMILY', + 'FAN', + 'FANS', + 'FARM', + 'FARMERS', + 'FASHION', + 'FAST', + 'FEDEX', + 'FEEDBACK', + 'FERRARI', + 'FERRERO', + 'FI', + 'FIAT', + 'FIDELITY', + 'FIDO', + 'FILM', + 'FINAL', + 'FINANCE', + 'FINANCIAL', + 'FIRE', + 'FIRESTONE', + 'FIRMDALE', + 'FISH', + 'FISHING', + 'FIT', + 'FITNESS', + 'FJ', + 'FK', + 'FLICKR', + 'FLIGHTS', + 'FLIR', + 'FLORIST', + 'FLOWERS', + 'FLY', + 'FM', + 'FO', + 'FOO', + 'FOOD', + 'FOODNETWORK', + 'FOOTBALL', + 'FORD', + 'FOREX', + 'FORSALE', + 'FORUM', + 'FOUNDATION', + 'FOX', + 'FR', + 'FREE', + 'FRESENIUS', + 'FRL', + 'FROGANS', + 'FRONTDOOR', + 'FRONTIER', + 'FTR', + 'FUJITSU', + 'FUN', + 'FUND', + 'FURNITURE', + 'FUTBOL', + 'FYI', + ], + 'G': [ + 'GA', + 'GAL', + 'GALLERY', + 'GALLO', + 'GALLUP', + 'GAME', + 'GAMES', + 'GAP', + 'GARDEN', + 'GAY', + 'GB', + 'GBIZ', + 'GD', + 'GDN', + 'GE', + 'GEA', + 'GENT', + 'GENTING', + 'GEORGE', + 'GF', + 'GG', + 'GGEE', + 'GH', + 'GI', + 'GIFT', + 'GIFTS', + 'GIVES', + 'GIVING', + 'GL', + 'GLASS', + 'GLE', + 'GLOBAL', + 'GLOBO', + 'GM', + 'GMAIL', + 'GMBH', + 'GMO', + 'GMX', + 'GN', + 'GODADDY', + 'GOLD', + 'GOLDPOINT', + 'GOLF', + 'GOO', + 'GOODYEAR', + 'GOOG', + 'GOOGLE', + 'GOP', + 'GOT', + 'GOV', + 'GP', + 'GQ', + 'GR', + 'GRAINGER', + 'GRAPHICS', + 'GRATIS', + 'GREEN', + 'GRIPE', + 'GROCERY', + 'GROUP', + 'GS', + 'GT', + 'GU', + 'GUARDIAN', + 'GUCCI', + 'GUGE', + 'GUIDE', + 'GUITARS', + 'GURU', + 'GW', + 'GY', + ], + 'H': [ + 'HAIR', + 'HAMBURG', + 'HANGOUT', + 'HAUS', + 'HBO', + 'HDFC', + 'HDFCBANK', + 'HEALTH', + 'HEALTHCARE', + 'HELP', + 'HELSINKI', + 'HERE', + 'HERMES', + 'HGTV', + 'HIPHOP', + 'HISAMITSU', + 'HITACHI', + 'HIV', + 'HK', + 'HKT', + 'HM', + 'HN', + 'HOCKEY', + 'HOLDINGS', + 'HOLIDAY', + 'HOMEDEPOT', + 'HOMEGOODS', + 'HOMES', + 'HOMESENSE', + 'HONDA', + 'HORSE', + 'HOSPITAL', + 'HOST', + 'HOSTING', + 'HOT', + 'HOTELES', + 'HOTELS', + 'HOTMAIL', + 'HOUSE', + 'HOW', + 'HR', + 'HSBC', + 'HT', + 'HU', + 'HUGHES', + 'HYATT', + 'HYUNDAI', + ], + 'I': [ + 'IBM', + 'ICBC', + 'ICE', + 'ICU', + 'ID', + 'IE', + 'IEEE', + 'IFM', + 'IKANO', + 'IL', + 'IM', + 'IMAMAT', + 'IMDB', + 'IMMO', + 'IMMOBILIEN', + 'IN', + 'INC', + 'INDUSTRIES', + 'INFINITI', + 'INFO', + 'ING', + 'INK', + 'INSTITUTE', + 'INSURANCE', + 'INSURE', + 'INT', + 'INTERNATIONAL', + 'INTUIT', + 'INVESTMENTS', + 'IO', + 'IPIRANGA', + 'IQ', + 'IR', + 'IRISH', + 'IS', + 'ISMAILI', + 'IST', + 'ISTANBUL', + 'IT', + 'ITAU', + 'ITV', + ], + 'J': [ + 'JAGUAR', + 'JAVA', + 'JCB', + 'JE', + 'JEEP', + 'JETZT', + 'JEWELRY', + 'JIO', + 'JLL', + 'JM', + 'JMP', + 'JNJ', + 'JO', + 'JOBS', + 'JOBURG', + 'JOT', + 'JOY', + 'JP', + 'JPMORGAN', + 'JPRS', + 'JUEGOS', + 'JUNIPER', + ], + 'K': [ + 'KAUFEN', + 'KDDI', + 'KE', + 'KERRYHOTELS', + 'KERRYLOGISTICS', + 'KERRYPROPERTIES', + 'KFH', + 'KG', + 'KH', + 'KI', + 'KIA', + 'KIM', + 'KINDER', + 'KINDLE', + 'KITCHEN', + 'KIWI', + 'KM', + 'KN', + 'KOELN', + 'KOMATSU', + 'KOSHER', + 'KP', + 'KPMG', + 'KPN', + 'KR', + 'KRD', + 'KRED', + 'KUOKGROUP', + 'KW', + 'KY', + 'KYOTO', + 'KZ', + ], + 'L': [ + 'LA', + 'LACAIXA', + 'LAMBORGHINI', + 'LAMER', + 'LANCASTER', + 'LANCIA', + 'LAND', + 'LANDROVER', + 'LANXESS', + 'LASALLE', + 'LAT', + 'LATINO', + 'LATROBE', + 'LAW', + 'LAWYER', + 'LB', + 'LC', + 'LDS', + 'LEASE', + 'LECLERC', + 'LEFRAK', + 'LEGAL', + 'LEGO', + 'LEXUS', + 'LGBT', + 'LI', + 'LIDL', + 'LIFE', + 'LIFEINSURANCE', + 'LIFESTYLE', + 'LIGHTING', + 'LIKE', + 'LILLY', + 'LIMITED', + 'LIMO', + 'LINCOLN', + 'LINDE', + 'LINK', + 'LIPSY', + 'LIVE', + 'LIVING', + 'LK', + 'LLC', + 'LLP', + 'LOAN', + 'LOANS', + 'LOCKER', + 'LOCUS', + 'LOFT', + 'LOL', + 'LONDON', + 'LOTTE', + 'LOTTO', + 'LOVE', + 'LPL', + 'LPLFINANCIAL', + 'LR', + 'LS', + 'LT', + 'LTD', + 'LTDA', + 'LU', + 'LUNDBECK', + 'LUXE', + 'LUXURY', + 'LV', + 'LY', + ], + 'M': [ + 'MA', + 'MACYS', + 'MADRID', + 'MAIF', + 'MAISON', + 'MAKEUP', + 'MAN', + 'MANAGEMENT', + 'MANGO', + 'MAP', + 'MARKET', + 'MARKETING', + 'MARKETS', + 'MARRIOTT', + 'MARSHALLS', + 'MASERATI', + 'MATTEL', + 'MBA', + 'MC', + 'MCKINSEY', + 'MD', + 'ME', + 'MED', + 'MEDIA', + 'MEET', + 'MELBOURNE', + 'MEME', + 'MEMORIAL', + 'MEN', + 'MENU', + 'MERCKMSD', + 'MG', + 'MH', + 'MIAMI', + 'MICROSOFT', + 'MIL', + 'MINI', + 'MINT', + 'MIT', + 'MITSUBISHI', + 'MK', + 'ML', + 'MLB', + 'MLS', + 'MM', + 'MMA', + 'MN', + 'MO', + 'MOBI', + 'MOBILE', + 'MODA', + 'MOE', + 'MOI', + 'MOM', + 'MONASH', + 'MONEY', + 'MONSTER', + 'MORMON', + 'MORTGAGE', + 'MOSCOW', + 'MOTO', + 'MOTORCYCLES', + 'MOV', + 'MOVIE', + 'MP', + 'MQ', + 'MR', + 'MS', + 'MSD', + 'MT', + 'MTN', + 'MTR', + 'MU', + 'MUSEUM', + 'MUSIC', + 'MUTUAL', + 'MV', + 'MW', + 'MX', + 'MY', + 'MZ', + ], + 'N': [ + 'NA', + 'NAB', + 'NAGOYA', + 'NAME', + 'NATURA', + 'NAVY', + 'NBA', + 'NC', + 'NE', + 'NEC', + 'NET', + 'NETBANK', + 'NETFLIX', + 'NETWORK', + 'NEUSTAR', + 'NEW', + 'NEWS', + 'NEXT', + 'NEXTDIRECT', + 'NEXUS', + 'NF', + 'NFL', + 'NG', + 'NGO', + 'NHK', + 'NI', + 'NICO', + 'NIKE', + 'NIKON', + 'NINJA', + 'NISSAN', + 'NISSAY', + 'NL', + 'NO', + 'NOKIA', + 'NORTHWESTERNMUTUAL', + 'NORTON', + 'NOW', + 'NOWRUZ', + 'NOWTV', + 'NP', + 'NR', + 'NRA', + 'NRW', + 'NTT', + 'NU', + 'NYC', + 'NZ', + ], + 'O': [ + 'OBI', + 'OBSERVER', + 'OFFICE', + 'OKINAWA', + 'OLAYAN', + 'OLAYANGROUP', + 'OLDNAVY', + 'OLLO', + 'OM', + 'OMEGA', + 'ONE', + 'ONG', + 'ONL', + 'ONLINE', + 'OOO', + 'OPEN', + 'ORACLE', + 'ORANGE', + 'ORG', + 'ORGANIC', + 'ORIGINS', + 'OSAKA', + 'OTSUKA', + 'OTT', + 'OVH', + ], + 'P': [ + 'PA', + 'PAGE', + 'PANASONIC', + 'PARIS', + 'PARS', + 'PARTNERS', + 'PARTS', + 'PARTY', + 'PASSAGENS', + 'PAY', + 'PCCW', + 'PE', + 'PET', + 'PF', + 'PFIZER', + 'PG', + 'PH', + 'PHARMACY', + 'PHD', + 'PHILIPS', + 'PHONE', + 'PHOTO', + 'PHOTOGRAPHY', + 'PHOTOS', + 'PHYSIO', + 'PICS', + 'PICTET', + 'PICTURES', + 'PID', + 'PIN', + 'PING', + 'PINK', + 'PIONEER', + 'PIZZA', + 'PK', + 'PL', + 'PLACE', + 'PLAY', + 'PLAYSTATION', + 'PLUMBING', + 'PLUS', + 'PM', + 'PN', + 'PNC', + 'POHL', + 'POKER', + 'POLITIE', + 'PORN', + 'POST', + 'PR', + 'PRAMERICA', + 'PRAXI', + 'PRESS', + 'PRIME', + 'PRO', + 'PROD', + 'PRODUCTIONS', + 'PROF', + 'PROGRESSIVE', + 'PROMO', + 'PROPERTIES', + 'PROPERTY', + 'PROTECTION', + 'PRU', + 'PRUDENTIAL', + 'PS', + 'PT', + 'PUB', + 'PW', + 'PWC', + 'PY', + ], + 'Q': [ + 'QA', + 'QPON', + 'QUEBEC', + 'QUEST', + ], + 'R': [ + 'RACING', + 'RADIO', + 'RE', + 'READ', + 'REALESTATE', + 'REALTOR', + 'REALTY', + 'RECIPES', + 'RED', + 'REDSTONE', + 'REDUMBRELLA', + 'REHAB', + 'REISE', + 'REISEN', + 'REIT', + 'RELIANCE', + 'REN', + 'RENT', + 'RENTALS', + 'REPAIR', + 'REPORT', + 'REPUBLICAN', + 'REST', + 'RESTAURANT', + 'REVIEW', + 'REVIEWS', + 'REXROTH', + 'RICH', + 'RICHARDLI', + 'RICOH', + 'RIL', + 'RIO', + 'RIP', + 'RO', + 'ROCHER', + 'ROCKS', + 'RODEO', + 'ROGERS', + 'ROOM', + 'RS', + 'RSVP', + 'RU', + 'RUGBY', + 'RUHR', + 'RUN', + 'RW', + 'RWE', + 'RYUKYU', + ], + 'S': [ + 'SA', + 'SAARLAND', + 'SAFE', + 'SAFETY', + 'SAKURA', + 'SALE', + 'SALON', + 'SAMSCLUB', + 'SAMSUNG', + 'SANDVIK', + 'SANDVIKCOROMANT', + 'SANOFI', + 'SAP', + 'SARL', + 'SAS', + 'SAVE', + 'SAXO', + 'SB', + 'SBI', + 'SBS', + 'SC', + 'SCA', + 'SCB', + 'SCHAEFFLER', + 'SCHMIDT', + 'SCHOLARSHIPS', + 'SCHOOL', + 'SCHULE', + 'SCHWARZ', + 'SCIENCE', + 'SCOT', + 'SD', + 'SE', + 'SEARCH', + 'SEAT', + 'SECURE', + 'SECURITY', + 'SEEK', + 'SELECT', + 'SENER', + 'SERVICES', + 'SES', + 'SEVEN', + 'SEW', + 'SEX', + 'SEXY', + 'SFR', + 'SG', + 'SH', + 'SHANGRILA', + 'SHARP', + 'SHAW', + 'SHELL', + 'SHIA', + 'SHIKSHA', + 'SHOES', + 'SHOP', + 'SHOPPING', + 'SHOUJI', + 'SHOW', + 'SHOWTIME', + 'SI', + 'SILK', + 'SINA', + 'SINGLES', + 'SITE', + 'SJ', + 'SK', + 'SKI', + 'SKIN', + 'SKY', + 'SKYPE', + 'SL', + 'SLING', + 'SM', + 'SMART', + 'SMILE', + 'SN', + 'SNCF', + 'SO', + 'SOCCER', + 'SOCIAL', + 'SOFTBANK', + 'SOFTWARE', + 'SOHU', + 'SOLAR', + 'SOLUTIONS', + 'SONG', + 'SONY', + 'SOY', + 'SPA', + 'SPACE', + 'SPORT', + 'SPOT', + 'SR', + 'SRL', + 'SS', + 'ST', + 'STADA', + 'STAPLES', + 'STAR', + 'STATEBANK', + 'STATEFARM', + 'STC', + 'STCGROUP', + 'STOCKHOLM', + 'STORAGE', + 'STORE', + 'STREAM', + 'STUDIO', + 'STUDY', + 'STYLE', + 'SU', + 'SUCKS', + 'SUPPLIES', + 'SUPPLY', + 'SUPPORT', + 'SURF', + 'SURGERY', + 'SUZUKI', + 'SV', + 'SWATCH', + 'SWISS', + 'SX', + 'SY', + 'SYDNEY', + 'SYSTEMS', + 'SZ', + ], + 'T': [ + 'TAB', + 'TAIPEI', + 'TALK', + 'TAOBAO', + 'TARGET', + 'TATAMOTORS', + 'TATAR', + 'TATTOO', + 'TAX', + 'TAXI', + 'TC', + 'TCI', + 'TD', + 'TDK', + 'TEAM', + 'TECH', + 'TECHNOLOGY', + 'TEL', + 'TEMASEK', + 'TENNIS', + 'TEVA', + 'TF', + 'TG', + 'TH', + 'THD', + 'THEATER', + 'THEATRE', + 'TIAA', + 'TICKETS', + 'TIENDA', + 'TIFFANY', + 'TIPS', + 'TIRES', + 'TIROL', + 'TJ', + 'TJMAXX', + 'TJX', + 'TK', + 'TKMAXX', + 'TL', + 'TM', + 'TMALL', + 'TN', + 'TO', + 'TODAY', + 'TOKYO', + 'TOOLS', + 'TOP', + 'TORAY', + 'TOSHIBA', + 'TOTAL', + 'TOURS', + 'TOWN', + 'TOYOTA', + 'TOYS', + 'TR', + 'TRADE', + 'TRADING', + 'TRAINING', + 'TRAVEL', + 'TRAVELCHANNEL', + 'TRAVELERS', + 'TRAVELERSINSURANCE', + 'TRUST', + 'TRV', + 'TT', + 'TUBE', + 'TUI', + 'TUNES', + 'TUSHU', + 'TV', + 'TVS', + 'TW', + 'TZ', + ], + 'U': [ + 'UA', + 'UBANK', + 'UBS', + 'UG', + 'UK', + 'UNICOM', + 'UNIVERSITY', + 'UNO', + 'UOL', + 'UPS', + 'US', + 'UY', + 'UZ', + ], + 'V': [ + 'VA', + 'VACATIONS', + 'VANA', + 'VANGUARD', + 'VC', + 'VE', + 'VEGAS', + 'VENTURES', + 'VERISIGN', + 'VERSICHERUNG', + 'VET', + 'VG', + 'VI', + 'VIAJES', + 'VIDEO', + 'VIG', + 'VIKING', + 'VILLAS', + 'VIN', + 'VIP', + 'VIRGIN', + 'VISA', + 'VISION', + 'VIVA', + 'VIVO', + 'VLAANDEREN', + 'VN', + 'VODKA', + 'VOLKSWAGEN', + 'VOLVO', + 'VOTE', + 'VOTING', + 'VOTO', + 'VOYAGE', + 'VU', + 'VUELOS', + ], + 'W': [ + 'WALES', + 'WALMART', + 'WALTER', + 'WANG', + 'WANGGOU', + 'WATCH', + 'WATCHES', + 'WEATHER', + 'WEATHERCHANNEL', + 'WEBCAM', + 'WEBER', + 'WEBSITE', + 'WED', + 'WEDDING', + 'WEIBO', + 'WEIR', + 'WF', + 'WHOSWHO', + 'WIEN', + 'WIKI', + 'WILLIAMHILL', + 'WIN', + 'WINDOWS', + 'WINE', + 'WINNERS', + 'WME', + 'WOLTERSKLUWER', + 'WOODSIDE', + 'WORK', + 'WORKS', + 'WORLD', + 'WOW', + 'WS', + 'WTC', + 'WTF', + ], + 'X': [ + 'XBOX', + 'XEROX', + 'XFINITY', + 'XIHUAN', + 'XIN', + 'XN--11B4C3D', + 'XN--1CK2E1B', + 'XN--1QQW23A', + 'XN--2SCRJ9C', + 'XN--30RR7Y', + 'XN--3BST00M', + 'XN--3DS443G', + 'XN--3E0B707E', + 'XN--3HCRJ9C', + 'XN--3PXU8K', + 'XN--42C2D9A', + 'XN--45BR5CYL', + 'XN--45BRJ9C', + 'XN--45Q11C', + 'XN--4DBRK0CE', + 'XN--4GBRIM', + 'XN--54B7FTA0CC', + 'XN--55QW42G', + 'XN--55QX5D', + 'XN--5SU34J936BGSG', + 'XN--5TZM5G', + 'XN--6FRZ82G', + 'XN--6QQ986B3XL', + 'XN--80ADXHKS', + 'XN--80AO21A', + 'XN--80AQECDR1A', + 'XN--80ASEHDB', + 'XN--80ASWG', + 'XN--8Y0A063A', + 'XN--90A3AC', + 'XN--90AE', + 'XN--90AIS', + 'XN--9DBQ2A', + 'XN--9ET52U', + 'XN--9KRT00A', + 'XN--B4W605FERD', + 'XN--BCK1B9A5DRE4C', + 'XN--C1AVG', + 'XN--C2BR7G', + 'XN--CCK2B3B', + 'XN--CCKWCXETD', + 'XN--CG4BKI', + 'XN--CLCHC0EA0B2G2A9GCD', + 'XN--CZR694B', + 'XN--CZRS0T', + 'XN--CZRU2D', + 'XN--D1ACJ3B', + 'XN--D1ALF', + 'XN--E1A4C', + 'XN--ECKVDTC9D', + 'XN--EFVY88H', + 'XN--FCT429K', + 'XN--FHBEI', + 'XN--FIQ228C5HS', + 'XN--FIQ64B', + 'XN--FIQS8S', + 'XN--FIQZ9S', + 'XN--FJQ720A', + 'XN--FLW351E', + 'XN--FPCRJ9C3D', + 'XN--FZC2C9E2C', + 'XN--FZYS8D69UVGM', + 'XN--G2XX48C', + 'XN--GCKR3F0F', + 'XN--GECRJ9C', + 'XN--GK3AT1E', + 'XN--H2BREG3EVE', + 'XN--H2BRJ9C', + 'XN--H2BRJ9C8C', + 'XN--HXT814E', + 'XN--I1B6B1A6A2E', + 'XN--IMR513N', + 'XN--IO0A7I', + 'XN--J1AEF', + 'XN--J1AMH', + 'XN--J6W193G', + 'XN--JLQ480N2RG', + 'XN--JLQ61U9W7B', + 'XN--JVR189M', + 'XN--KCRX77D1X4A', + 'XN--KPRW13D', + 'XN--KPRY57D', + 'XN--KPUT3I', + 'XN--L1ACC', + 'XN--LGBBAT1AD8J', + 'XN--MGB9AWBF', + 'XN--MGBA3A3EJT', + 'XN--MGBA3A4F16A', + 'XN--MGBA7C0BBN0A', + 'XN--MGBAAKC7DVF', + 'XN--MGBAAM7A8H', + 'XN--MGBAB2BD', + 'XN--MGBAH1A3HJKRD', + 'XN--MGBAI9AZGQP6J', + 'XN--MGBAYH7GPA', + 'XN--MGBBH1A', + 'XN--MGBBH1A71E', + 'XN--MGBC0A9AZCG', + 'XN--MGBCA7DZDO', + 'XN--MGBCPQ6GPA1A', + 'XN--MGBERP4A5D4AR', + 'XN--MGBGU82A', + 'XN--MGBI4ECEXP', + 'XN--MGBPL2FH', + 'XN--MGBT3DHD', + 'XN--MGBTX2B', + 'XN--MGBX4CD0AB', + 'XN--MIX891F', + 'XN--MK1BU44C', + 'XN--MXTQ1M', + 'XN--NGBC5AZD', + 'XN--NGBE9E0A', + 'XN--NGBRX', + 'XN--NODE', + 'XN--NQV7F', + 'XN--NQV7FS00EMA', + 'XN--NYQY26A', + 'XN--O3CW4H', + 'XN--OGBPF8FL', + 'XN--OTU796D', + 'XN--P1ACF', + 'XN--P1AI', + 'XN--PGBS0DH', + 'XN--PSSY2U', + 'XN--Q7CE6A', + 'XN--Q9JYB4C', + 'XN--QCKA1PMC', + 'XN--QXA6A', + 'XN--QXAM', + 'XN--RHQV96G', + 'XN--ROVU88B', + 'XN--RVC1E0AM3E', + 'XN--S9BRJ9C', + 'XN--SES554G', + 'XN--T60B56A', + 'XN--TCKWE', + 'XN--TIQ49XQYJ', + 'XN--UNUP4Y', + 'XN--VERMGENSBERATER-CTB', + 'XN--VERMGENSBERATUNG-PWB', + 'XN--VHQUV', + 'XN--VUQ861B', + 'XN--W4R85EL8FHU5DNRA', + 'XN--W4RS40L', + 'XN--WGBH1C', + 'XN--WGBL6A', + 'XN--XHQ521B', + 'XN--XKC2AL3HYE2A', + 'XN--XKC2DL3A5EE0H', + 'XN--Y9A3AQ', + 'XN--YFRO4I67O', + 'XN--YGBI2AMMX', + 'XN--ZFR164B', + 'XXX', + 'XYZ', + ], + 'Y': [ + 'YACHTS', + 'YAHOO', + 'YAMAXUN', + 'YANDEX', + 'YE', + 'YODOBASHI', + 'YOGA', + 'YOKOHAMA', + 'YOU', + 'YOUTUBE', + 'YT', + 'YUN', + ], + 'Z': [ + 'ZA', + 'ZAPPOS', + 'ZARA', + 'ZERO', + 'ZIP', + 'ZM', + 'ZONE', + 'ZUERICH', + 'ZW', + ], +}; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index edd76327..e7b2d215 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -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, - 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 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 { +class _StreamMessageListViewState extends State { ItemScrollController? _scrollController; void Function(Message)? _onThreadTap; final ValueNotifier _showScrollToBottom = ValueNotifier(false); @@ -351,6 +353,7 @@ class _MessageListViewState extends State { int? _messageListLength; StreamChannelState? streamChannel; late StreamChatThemeData _streamTheme; + late List _userPermissions; int get _initialIndex { final initialScrollIndex = widget.initialScrollIndex; @@ -470,7 +473,7 @@ class _MessageListViewState extends State { 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 { break; } - return InfoTile( + return StreamInfoTile( showMessage: widget.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, @@ -535,6 +538,7 @@ class _MessageListViewState extends State { 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 { ) : Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: DateDivider( + child: StreamDateDivider( dateTime: nextMessage.createdAt.toLocal(), ), ); @@ -738,8 +742,10 @@ class _MessageListViewState extends State { ], ); - 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 { 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 { 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 { 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 { 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 { 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 { 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 { }, 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 { 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 { index, ), messages, - messageWidget as MessageWidget, + messageWidget as StreamMessageWidget, ); } @@ -1306,6 +1315,7 @@ class _MessageListViewState extends State { void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); _streamTheme = StreamChatTheme.of(context); + _userPermissions = newStreamChannel.channel.ownCapabilities; if (newStreamChannel != streamChannel) { streamChannel = newStreamChannel; diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 98e64c25..eceba3f2 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -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: [ - 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: diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index 022ec7ce..fe6d02b7 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -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!, diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index 76ae0e98..380b6545 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -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 { +class _StreamMessageSearchListViewState + extends State { late final _defaultController = MessageSearchListController(); MessageSearchListController get _messageSearchListController => @@ -168,7 +175,7 @@ class _MessageSearchListViewState extends State { 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 { ); final backgroundColor = - MessageSearchListViewTheme.of(context).backgroundColor; + StreamMessageSearchListViewTheme.of(context).backgroundColor; if (backgroundColor != null) { return ColoredBox( @@ -219,7 +226,7 @@ class _MessageSearchListViewState extends State { if (widget.itemBuilder != null) { return widget.itemBuilder!(context, getMessageResponse); } - return MessageSearchItem( + return StreamMessageSearchItem( getMessageResponse: getMessageResponse, onTap: () => widget.onItemTap!(getMessageResponse), ); diff --git a/packages/stream_chat_flutter/lib/src/message_text.dart b/packages/stream_chat_flutter/lib/src/message_text.dart index 4787bb9e..b6fee801 100644 --- a/packages/stream_chat_flutter/lib/src/message_text.dart +++ b/packages/stream_chat_flutter/lib/src/message_text.dart @@ -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; - } } diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 3ffca84a..05bff7ba 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -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 customActions; + final List 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? customActions, + List? 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 - with AutomaticKeepAliveClientMixin { +class _StreamMessageWidgetState extends State + with AutomaticKeepAliveClientMixin { bool get showThreadReplyIndicator => widget.showThreadReplyIndicator; bool get showSendingIndicator => widget.showSendingIndicator; @@ -717,7 +723,7 @@ class _MessageWidgetState extends State ? 0 : 4.0, ), - child: DeletedMessage( + child: StreamDeletedMessage( borderRadiusGeometry: widget .borderRadiusGeometry, borderSide: @@ -854,7 +860,7 @@ class _MessageWidgetState extends State ? () => 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 getWebsiteName(hostName.toLowerCase()) ?? hostName.capitalize(); - return UrlAttachment( + return StreamUrlAttachment( urlAttachment: urlAttachment, hostDisplayName: hostDisplayName, textPadding: widget.textPadding, @@ -1042,7 +1048,7 @@ class _MessageWidgetState extends State 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 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 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 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 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 !isFailedState && widget.onThreadTap != null, showFlagButton: widget.showFlagButton, - showPinButton: widget.showPinButton, customActions: widget.customActions, ), ), @@ -1136,7 +1136,7 @@ class _MessageWidgetState extends State 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 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 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 final channel = StreamChannel.of(context).channel; + if (!channel.ownCapabilities.contains(PermissionType.readEvents)) { + return StreamSendingIndicator( + message: message, + size: style!.fontSize, + ); + } + return BetterStreamBuilder>( stream: channel.state?.readStream, initialData: channel.state?.read, @@ -1259,7 +1268,7 @@ class _MessageWidgetState extends State (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 : 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 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, diff --git a/packages/stream_chat_flutter/lib/src/multi_overlay.dart b/packages/stream_chat_flutter/lib/src/multi_overlay.dart index 31b9d5e9..bd1ed387 100644 --- a/packages/stream_chat_flutter/lib/src/multi_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/multi_overlay.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/option_list_tile.dart b/packages/stream_chat_flutter/lib/src/option_list_tile.dart index 16bf9e97..185d40f8 100644 --- a/packages/stream_chat_flutter/lib/src/option_list_tile.dart +++ b/packages/stream_chat_flutter/lib/src/option_list_tile.dart @@ -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 diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 131ca60b..81c8b892 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -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 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(), + ); +} diff --git a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart index dd3d5e27..99585001 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_bubble.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_bubble.dart @@ -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 reactionIcons, + List reactionIcons, Reaction reaction, BuildContext context, ) { diff --git a/packages/stream_chat_flutter/lib/src/reaction_icon.dart b/packages/stream_chat_flutter/lib/src/reaction_icon.dart index e675128b..cf2cef81 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_icon.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_icon.dart @@ -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, }); diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reaction_picker.dart index 2991a1bb..28053c01 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_picker.dart @@ -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 +class _StreamReactionPickerState extends State with TickerProviderStateMixin { List animations = []; diff --git a/packages/stream_chat_flutter/lib/src/sending_indicator.dart b/packages/stream_chat_flutter/lib/src/sending_indicator.dart index 7f44e5b8..908c734d 100644 --- a/packages/stream_chat_flutter/lib/src/sending_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/sending_indicator.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index 563f3c50..32b4830f 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -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? reactionIcons, - GalleryHeaderThemeData? imageHeaderTheme, - GalleryFooterThemeData? imageFooterTheme, - MessageListViewThemeData? messageListViewTheme, - ChannelListViewThemeData? channelListViewTheme, - UserListViewThemeData? userListViewTheme, - MessageSearchListViewThemeData? messageSearchListViewTheme, + List? 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 reactionIcons; + final List 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? reactionIcons, - GalleryHeaderThemeData? galleryHeaderTheme, - GalleryFooterThemeData? galleryFooterTheme, - MessageListViewThemeData? messageListViewTheme, - ChannelListViewThemeData? channelListViewTheme, - UserListViewThemeData? userListViewTheme, - MessageSearchListViewThemeData? messageSearchListViewTheme, + StreamChannelListHeaderThemeData? channelListHeaderTheme, + List? reactionIcons, + StreamGalleryHeaderThemeData? galleryHeaderTheme, + StreamGalleryFooterThemeData? galleryFooterTheme, + StreamMessageListViewThemeData? messageListViewTheme, + StreamChannelListViewThemeData? channelListViewTheme, + StreamUserListViewThemeData? userListViewTheme, + StreamMessageSearchListViewThemeData? messageSearchListViewTheme, }) => StreamChatThemeData.raw( channelListHeaderTheme: diff --git a/packages/stream_chat_flutter/lib/src/system_message.dart b/packages/stream_chat_flutter/lib/src/system_message.dart index 297810b3..4ec10aa4 100644 --- a/packages/stream_chat_flutter/lib/src/system_message.dart +++ b/packages/stream_chat_flutter/lib/src/system_message.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart index 738fa0de..eb2d79bf 100644 --- a/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/avatar_theme.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart index 63e208e5..aa988c0f 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_header_theme.dart @@ -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(); + context.dependOnInheritedWidgetOfExactType(); 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 && diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart index 3846232e..7cff2b90 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_header_theme.dart @@ -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(); + static StreamChannelListHeaderThemeData of(BuildContext context) { + final channelListHeaderTheme = context + .dependOnInheritedWidgetOfExactType(); 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 && diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart index d310e2bc..4d6bd7d0 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_list_view_theme.dart @@ -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(); + static StreamChannelListViewThemeData of(BuildContext context) { + final channelListViewTheme = context + .dependOnInheritedWidgetOfExactType(); 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; diff --git a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart index ff9ef70c..3df974b2 100644 --- a/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/channel_preview_theme.dart @@ -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(); + context.dependOnInheritedWidgetOfExactType(); 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 && diff --git a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart index b32c023f..e0068d59 100644 --- a/packages/stream_chat_flutter/lib/src/theme/color_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/color_theme.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart index a227b830..f68ceb45 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_footer_theme.dart @@ -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(); + context.dependOnInheritedWidgetOfExactType(); 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 && diff --git a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart index 23402c57..f03ebe9a 100644 --- a/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/gallery_header_theme.dart @@ -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(); + context.dependOnInheritedWidgetOfExactType(); 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 && diff --git a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart index 26a995a9..c3874ef6 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_input_theme.dart @@ -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(); + context.dependOnInheritedWidgetOfExactType(); 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)); } } diff --git a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart index e4857a79..882d4708 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_list_view_theme.dart @@ -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(); + static StreamMessageListViewThemeData of(BuildContext context) { + final messageListViewTheme = context + .dependOnInheritedWidgetOfExactType(); 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; diff --git a/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart index 670e05e7..83ef032a 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_search_list_view_theme.dart @@ -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(); + .dependOnInheritedWidgetOfExactType(); 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; diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart index 386a312f..c94c8159 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -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 && diff --git a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart index ec70cf83..06ddf2d9 100644 --- a/packages/stream_chat_flutter/lib/src/theme/text_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/text_theme.dart @@ -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), diff --git a/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart index 0d99ccbd..fb57438b 100644 --- a/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/user_list_view_theme.dart @@ -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(); + context.dependOnInheritedWidgetOfExactType(); 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; diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index aae1d8f9..052e1bcb 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index d961a8ff..1cb3d8eb 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -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, diff --git a/packages/stream_chat_flutter/lib/src/unread_indicator.dart b/packages/stream_chat_flutter/lib/src/unread_indicator.dart index 958386d1..bf23db1a 100644 --- a/packages/stream_chat_flutter/lib/src/unread_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/unread_indicator.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro unread_indicator} +@Deprecated("Use 'StreamUnreadIndicator' instead") +typedef UnreadIndicator = StreamUnreadIndicator; + +/// {@template unread_indicator} /// Widget for showing an unread indicator -class UnreadIndicator extends StatelessWidget { - /// Constructor for creating an [UnreadIndicator] - const UnreadIndicator({ +/// {@endtemplate} +class StreamUnreadIndicator extends StatelessWidget { + /// Constructor for creating an [StreamUnreadIndicator] + const StreamUnreadIndicator({ Key? key, this.cid, }) : super(key: key); diff --git a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart index b3fe510d..d11e9609 100644 --- a/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/upload_progress_indicator.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro upload_progress_indicator} +@Deprecated("Use 'StreamUploadProgressIndicator' instead") +typedef UploadProgressIndicator = StreamUploadProgressIndicator; + +/// {@template upload_progress_indicator} /// Widget for showing upload progress -class UploadProgressIndicator extends StatelessWidget { - /// Constructor for creating an [UploadProgressIndicator] - const UploadProgressIndicator({ +/// {@endtemplate} +class StreamUploadProgressIndicator extends StatelessWidget { + /// Constructor for creating an [StreamUploadProgressIndicator] + const StreamUploadProgressIndicator({ Key? key, required this.uploaded, required this.total, diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index e638685c..8d634039 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -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 user_avatar} +@Deprecated("Use 'StreamUserAvatar' instead") +typedef UserAvatar = StreamUserAvatar; + +/// {@template user_avatar} /// Widget that displays a user avatar -class UserAvatar extends StatelessWidget { - /// Constructor to create a [UserAvatar] - const UserAvatar({ +/// {@endtemplate} +class StreamUserAvatar extends StatelessWidget { + /// Constructor to create a [StreamUserAvatar] + const StreamUserAvatar({ Key? key, required this.user, this.constraints, diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user_item.dart index c0e3482e..74ccacc2 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user_item.dart @@ -2,21 +2,26 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -/// +/// {@macro user_item} +@Deprecated("Use 'StreamUserItem' instead") +typedef UserItem = StreamUserItem; + +/// {@template user_item} /// It shows the current [User] preview. /// /// The widget uses a [StreamBuilder] to render the user information /// image as soon as it updates. /// /// Usually you don't use this widget as it's the default user preview used -/// by [UserListView]. +/// by [StreamUserListView]. /// /// The widget renders the ui based on the first ancestor of type /// [StreamChatTheme]. /// Modify it to change the widget appearance. -class UserItem extends StatelessWidget { +/// {@endtemplate} +class StreamUserItem extends StatelessWidget { /// Instantiate a new UserItem - const UserItem({ + const StreamUserItem({ Key? key, required this.user, this.onTap, @@ -38,10 +43,10 @@ class UserItem extends StatelessWidget { /// The function called when the image is tapped final void Function(User)? onImageTap; - /// If true the [UserItem] will show a trailing checkmark + /// If true the [StreamUserItem] will show a trailing checkmark final bool selected; - /// If true the [UserItem] will show the last seen + /// If true the [StreamUserItem] will show the last seen final bool showLastOnline; @override @@ -58,7 +63,7 @@ class UserItem extends StatelessWidget { onLongPress!(user); } }, - leading: UserAvatar( + leading: StreamUserAvatar( user: user, onTap: (user) { if (onImageTap != null) { diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 58af30a7..a604c611 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -8,7 +8,11 @@ typedef UserTapCallback = void Function(User, Widget?); /// Builder used to create a custom [ListUserItem] from a [User] typedef UserItemBuilder = Widget Function(BuildContext, User, bool); -/// +/// {@macro user_list_view} +@Deprecated("Use 'StreamUserListView' instead") +typedef UserListView = StreamUserListView; + +/// {@template user_list_view} /// It shows the list of current users. /// /// ```dart @@ -42,9 +46,10 @@ typedef UserItemBuilder = Widget Function(BuildContext, User, bool); /// The widget components render the ui based on the first ancestor of /// type [StreamChatTheme]. /// Modify it to change the widget appearance. -class UserListView extends StatefulWidget { +/// {@endtemplate} +class StreamUserListView extends StatefulWidget { /// Instantiate a new UserListView - UserListView({ + StreamUserListView({ Key? key, this.filter = const Filter.empty(), this.sort, @@ -160,10 +165,10 @@ class UserListView extends StatefulWidget { final UserListController? userListController; @override - _UserListViewState createState() => _UserListViewState(); + _StreamUserListViewState createState() => _StreamUserListViewState(); } -class _UserListViewState extends State +class _StreamUserListViewState extends State with WidgetsBindingObserver { bool get _isListView => widget.crossAxisCount == 1; @@ -203,7 +208,7 @@ class _UserListViewState extends State userListController: _userListController, ); - final backgroundColor = UserListViewTheme.of(context).backgroundColor; + final backgroundColor = StreamUserListViewTheme.of(context).backgroundColor; Widget child; @@ -332,7 +337,7 @@ class _UserListViewState extends State key: ValueKey('USER-${user.id}'), child: widget.userItemBuilder != null ? widget.userItemBuilder!(context, user, selected) - : UserItem( + : StreamUserItem( user: user, onTap: (user) => widget.onUserTap!(user, widget.userWidget), onLongPress: widget.onUserLongPress, @@ -362,7 +367,7 @@ class _UserListViewState extends State : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - UserAvatar( + StreamUserAvatar( user: user, borderRadius: BorderRadius.circular(32), selected: selected, diff --git a/packages/stream_chat_flutter/lib/src/user_mention_tile.dart b/packages/stream_chat_flutter/lib/src/user_mention_tile.dart index ff9e145b..b944d912 100644 --- a/packages/stream_chat_flutter/lib/src/user_mention_tile.dart +++ b/packages/stream_chat_flutter/lib/src/user_mention_tile.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro user_mention_tile} +@Deprecated("Use 'StreamUserMentionTile' instead") +typedef UserMentionTile = StreamUserMentionTile; + +/// {@template user_mention_tile} /// This widget is used for showing user tiles for mentions /// Use [title], [subtitle], [leading], [trailing] for /// substituting widgets in respective positions -class UserMentionTile extends StatelessWidget { - /// Constructor for creating a [UserMentionTile] widget - const UserMentionTile( +/// {@endtemplate} +class StreamUserMentionTile extends StatelessWidget { + /// Constructor for creating a [StreamUserMentionTile] widget + const StreamUserMentionTile( this.user, { Key? key, this.title, @@ -41,7 +47,7 @@ class UserMentionTile extends StatelessWidget { width: 16, ), leading ?? - UserAvatar( + StreamUserAvatar( user: user, constraints: BoxConstraints.tight(const Size(40, 40)), ), diff --git a/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart b/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart index e3830231..59c86791 100644 --- a/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart +++ b/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart @@ -6,16 +6,22 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Builder function for building a mention tile. /// -/// Use [UserMentionTile] for the default implementation. +/// Use [StreamUserMentionTile] for the default implementation. typedef MentionTileBuilder = Widget Function( BuildContext context, User user, ); +/// {@macro user_mention_tile} +@Deprecated("Use 'StreamUserMentionsOverlay' instead") +typedef UserMentionsOverlay = StreamUserMentionsOverlay; + +/// {@template user_mentions_overlay} /// Overlay for displaying users that can be mentioned. -class UserMentionsOverlay extends StatefulWidget { - /// Constructor for creating a [UserMentionsOverlay]. - UserMentionsOverlay({ +/// {@endtemplate} +class StreamUserMentionsOverlay extends StatefulWidget { + /// Constructor for creating a [StreamUserMentionsOverlay]. + StreamUserMentionsOverlay({ Key? key, required this.query, required this.channel, @@ -62,10 +68,11 @@ class UserMentionsOverlay extends StatefulWidget { final void Function(User user)? onMentionUserTap; @override - _UserMentionsOverlayState createState() => _UserMentionsOverlayState(); + _StreamUserMentionsOverlayState createState() => + _StreamUserMentionsOverlayState(); } -class _UserMentionsOverlayState extends State { +class _StreamUserMentionsOverlayState extends State { late Future> userMentionsFuture; @override @@ -75,7 +82,7 @@ class _UserMentionsOverlayState extends State { } @override - void didUpdateWidget(covariant UserMentionsOverlay oldWidget) { + void didUpdateWidget(covariant StreamUserMentionsOverlay oldWidget) { super.didUpdateWidget(oldWidget); if (widget.channel != oldWidget.channel || widget.query != oldWidget.query || @@ -116,7 +123,7 @@ class _UserMentionsOverlayState extends State { child: InkWell( onTap: () => widget.onMentionUserTap?.call(user), child: widget.mentionsTileBuilder?.call(context, user) ?? - UserMentionTile(user), + StreamUserMentionTile(user), ), ); }, diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart new file mode 100644 index 00000000..69a6cf6e --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_loading_tile.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:shimmer/shimmer.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; + +/// A shimmering list item which shows a loading effect. +/// +/// This is used by [StreamChannelListView] to show a loading effect while +/// the list is being loaded. +class StreamChannelListLoadingTile extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadingTile] widget. + const StreamChannelListLoadingTile({ + Key? key, + this.visualDensity = VisualDensity.standard, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + }) : super(key: key); + + /// Defines how compact the list tile's layout will be. + /// + /// {@macro flutter.material.themedata.visualDensity} + /// + /// See also: + /// + /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all + /// widgets within a [Theme]. + final VisualDensity visualDensity; + + /// The tile's internal padding. + /// + /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], + /// and [trailing] widgets. + /// + /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. + final EdgeInsetsGeometry contentPadding; + + @override + Widget build(BuildContext context) { + final colorTheme = StreamChatTheme.of(context).colorTheme; + + final leading = Container( + height: 49, + width: 49, + decoration: BoxDecoration( + color: colorTheme.barsBg, + shape: BoxShape.circle, + ), + ); + + final title = Container( + height: 16, + width: 66, + decoration: BoxDecoration( + color: colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + ); + + final subtitle = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Container( + height: 16, + decoration: BoxDecoration( + color: colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + const SizedBox(width: 8), + Container( + height: 16, + width: 50, + decoration: BoxDecoration( + color: colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ); + + return Shimmer.fromColors( + baseColor: colorTheme.disabled, + highlightColor: colorTheme.inputBg, + child: ListTile( + leading: leading, + title: title, + subtitle: subtitle, + visualDensity: visualDensity, + contentPadding: contentPadding, + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart new file mode 100644 index 00000000..16fcc5cc --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_tile.dart @@ -0,0 +1,454 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/sending_indicator.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/channel_preview_theme.dart'; +import 'package:stream_chat_flutter/src/typing_indicator.dart'; +import 'package:stream_chat_flutter/src/unread_indicator.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_avatar.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// A widget that displays a channel preview. +/// +/// This widget is intended to be used as a Tile in [StreamChannelListView] +/// +/// It shows the last message of the channel, the last message time, the unread +/// message count, the typing indicator, the sending indicator and the channel +/// avatar. +/// +/// See also: +/// * [StreamChannelAvatar] +/// * [StreamChannelName] +class StreamChannelListTile extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTile] widget. + StreamChannelListTile({ + Key? key, + required this.channel, + this.leading, + this.title, + this.subtitle, + this.trailing, + this.onTap, + this.onLongPress, + this.tileColor, + this.visualDensity = VisualDensity.compact, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 8), + this.unreadIndicatorBuilder, + this.sendingIndicatorBuilder, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The channel to display. + final Channel channel; + + /// A widget to display before the title. + final Widget? leading; + + /// The primary content of the list tile. + final Widget? title; + + /// Additional content displayed below the title. + final Widget? subtitle; + + /// A widget to display at the end of tile. + final Widget? trailing; + + /// Called when the user taps this list tile. + final GestureTapCallback? onTap; + + /// Called when the user long-presses on this list tile. + final GestureLongPressCallback? onLongPress; + + /// {@template flutter.material.ListTile.tileColor} + /// Defines the background color of `ListTile`. + /// + /// When the value is null, + /// the `tileColor` is set to [ListTileTheme.tileColor] + /// if it's not null and to [Colors.transparent] if it's null. + /// {@endtemplate} + final Color? tileColor; + + /// Defines how compact the list tile's layout will be. + /// + /// {@macro flutter.material.themedata.visualDensity} + /// + /// See also: + /// + /// * [ThemeData.visualDensity], which specifies the [visualDensity] for all + /// widgets within a [Theme]. + final VisualDensity visualDensity; + + /// The tile's internal padding. + /// + /// Insets a [ListTile]'s contents: its [leading], [title], [subtitle], + /// and [trailing] widgets. + /// + /// If null, `EdgeInsets.symmetric(horizontal: 16.0)` is used. + final EdgeInsetsGeometry contentPadding; + + /// The widget builder for the unread indicator. + final WidgetBuilder? unreadIndicatorBuilder; + + /// The widget builder for the sending indicator. + /// + /// `Message` is the last message in the channel, Use it to determine the + /// status using [Message.status]. + final Widget Function(BuildContext, Message)? sendingIndicatorBuilder; + + /// Creates a copy of this tile but with the given fields replaced with + /// the new values. + StreamChannelListTile copyWith({ + Key? key, + Channel? channel, + Widget? leading, + Widget? title, + Widget? subtitle, + VoidCallback? onTap, + VoidCallback? onLongPress, + VisualDensity? visualDensity, + EdgeInsetsGeometry? contentPadding, + }) => + StreamChannelListTile( + key: key ?? this.key, + channel: channel ?? this.channel, + leading: leading ?? this.leading, + title: title ?? this.title, + subtitle: subtitle ?? this.subtitle, + onTap: onTap ?? this.onTap, + onLongPress: onLongPress ?? this.onLongPress, + visualDensity: visualDensity ?? this.visualDensity, + contentPadding: contentPadding ?? this.contentPadding, + ); + + @override + Widget build(BuildContext context) { + final channelState = channel.state!; + final currentUser = channel.client.state.currentUser!; + + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + + final leading = this.leading ?? + StreamChannelAvatar( + channel: channel, + ); + + final title = this.title ?? + StreamChannelName( + channel: channel, + textStyle: channelPreviewTheme.titleStyle, + ); + + final subtitle = this.subtitle ?? + ChannelListTileSubtitle( + channel: channel, + textStyle: channelPreviewTheme.subtitleStyle, + ); + + final trailing = this.trailing ?? + ChannelLastMessageDate( + channel: channel, + textStyle: channelPreviewTheme.lastMessageAtStyle, + ); + + return BetterStreamBuilder( + stream: channel.isMutedStream, + initialData: channel.isMuted, + builder: (context, isMuted) => AnimatedOpacity( + opacity: isMuted ? 0.5 : 1, + duration: const Duration(milliseconds: 300), + child: ListTile( + onTap: onTap, + onLongPress: onLongPress, + visualDensity: visualDensity, + contentPadding: contentPadding, + leading: leading, + tileColor: tileColor, + title: Row( + children: [ + Expanded(child: title), + BetterStreamBuilder>( + stream: channelState.membersStream, + initialData: channelState.members, + comparator: const ListEquality().equals, + builder: (context, members) { + if (members.isEmpty || + !members.any((it) => it.user!.id == currentUser.id)) { + return const Offstage(); + } + return unreadIndicatorBuilder?.call(context) ?? + StreamUnreadIndicator(cid: channel.cid); + }, + ), + ], + ), + subtitle: Row( + children: [ + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: subtitle, + ), + ), + BetterStreamBuilder>( + stream: channelState.messagesStream, + initialData: channelState.messages, + comparator: const ListEquality().equals, + builder: (context, messages) { + final lastMessage = messages.lastWhereOrNull( + (m) => !m.shadowed && !m.isDeleted, + ); + + if (lastMessage == null || + (lastMessage.user?.id != currentUser.id)) { + return const Offstage(); + } + + return Padding( + padding: const EdgeInsets.only(right: 4), + child: + sendingIndicatorBuilder?.call(context, lastMessage) ?? + StreamSendingIndicator( + message: lastMessage, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: channelState + .currentUserRead!.lastRead + .isAfter(lastMessage.createdAt), + ), + ); + }, + ), + trailing, + ], + ), + ), + ), + ); + } +} + +/// A widget that displays the channel last message date. +class ChannelLastMessageDate extends StatelessWidget { + /// Creates a new instance of the [ChannelLastMessageDate] widget. + ChannelLastMessageDate({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The channel to display the last message date for. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + stream: channel.lastMessageAtStream, + initialData: channel.lastMessageAt, + builder: (context, data) { + final lastMessageAt = data.toLocal(); + + String stringDate; + final now = DateTime.now(); + + final startOfDay = DateTime(now.year, now.month, now.day); + + if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay.millisecondsSinceEpoch) { + stringDate = Jiffy(lastMessageAt.toLocal()).jm; + } else if (lastMessageAt.millisecondsSinceEpoch >= + startOfDay + .subtract(const Duration(days: 1)) + .millisecondsSinceEpoch) { + stringDate = context.translations.yesterdayLabel; + } else if (startOfDay.difference(lastMessageAt).inDays < 7) { + stringDate = Jiffy(lastMessageAt.toLocal()).EEEE; + } else { + stringDate = Jiffy(lastMessageAt.toLocal()).yMd; + } + + return Text( + stringDate, + style: textStyle, + ); + }, + ); +} + +/// A widget that displays the subtitle for [StreamChannelListTile]. +class ChannelListTileSubtitle extends StatelessWidget { + /// Creates a new instance of [StreamChannelListTileSubtitle] widget. + ChannelListTileSubtitle({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The channel to create the subtitle from. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) { + if (channel.isMuted) { + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + StreamSvgIcon.mute(size: 16), + Text( + ' ${context.translations.channelIsMutedText}', + style: textStyle, + ), + ], + ); + } + return StreamTypingIndicator( + channel: channel, + style: textStyle, + alternativeWidget: ChannelLastMessageText( + channel: channel, + textStyle: textStyle, + ), + ); + } +} + +/// A widget that displays the last message of a channel. +class ChannelLastMessageText extends StatelessWidget { + /// Creates a new instance of [ChannelLastMessageText] widget. + ChannelLastMessageText({ + Key? key, + required this.channel, + this.textStyle, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The channel to display the last message of. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + @override + Widget build(BuildContext context) => BetterStreamBuilder>( + stream: channel.state!.messagesStream, + initialData: channel.state!.messages, + builder: (context, messages) { + final lastMessage = messages.lastWhereOrNull( + (m) => !m.shadowed && !m.isDeleted, + ); + + if (lastMessage == null) return const Offstage(); + + final lastMessageText = lastMessage + .translate(channel.client.state.currentUser?.language ?? 'en') + .replaceMentions(linkify: false) + .text; + final lastMessageAttachments = lastMessage.attachments; + final lastMessageMentionedUsers = lastMessage.mentionedUsers; + + final mentionedUsersRegex = RegExp( + lastMessageMentionedUsers.map((it) => '@${it.name}').join('|'), + caseSensitive: false, + ); + + final messageTextParts = [ + ...lastMessageAttachments.map((it) { + if (it.type == 'image') { + return '📷'; + } else if (it.type == 'video') { + return '🎬'; + } else if (it.type == 'giphy') { + return '[GIF]'; + } + return it == lastMessage.attachments.last + ? (it.title ?? 'File') + : '${it.title ?? 'File'} , '; + }), + if (lastMessageText != null) + if (lastMessageMentionedUsers.isNotEmpty) + ...mentionedUsersRegex.allMatchesWithSep(lastMessageText) + else + lastMessageText, + ]; + + final fontStyle = (lastMessage.isSystem || lastMessage.isDeleted) + ? FontStyle.italic + : FontStyle.normal; + + final regularTextStyle = textStyle?.copyWith(fontStyle: fontStyle); + + final mentionsTextStyle = textStyle?.copyWith( + fontStyle: fontStyle, + fontWeight: FontWeight.bold, + ); + + final spans = [ + for (final part in messageTextParts) + if (lastMessageMentionedUsers.isNotEmpty && + lastMessageMentionedUsers.any((it) => '@${it.name}' == part)) + TextSpan( + text: part, + style: mentionsTextStyle, + ) + else if (lastMessageAttachments.isNotEmpty && + lastMessageAttachments + .where((it) => it.title != null) + .any((it) => it.title == part)) + TextSpan( + text: part, + style: regularTextStyle?.copyWith( + fontStyle: FontStyle.italic, + ), + ) + else + TextSpan( + text: part, + style: regularTextStyle, + ), + ]; + + return Text.rich( + TextSpan(children: spans), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ); + }, + ); +} + +extension _RegExpX on RegExp { + List allMatchesWithSep(String input, [int start = 0]) { + final result = []; + for (final match in allMatches(input, start)) { + result.add(input.substring(start, match.start)); + // ignore: cascade_invocations + result.add(match[0]!); + // ignore: parameter_assignments + start = match.end; + } + result.add(input.substring(start)); + return result; + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart new file mode 100644 index 00000000..1d88f354 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/channel_list_view/stream_channel_list_view.dart @@ -0,0 +1,502 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; +import 'package:stream_chat_flutter/src/v4/channel_list_view/stream_channel_list_tile.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Default separator builder for [StreamChannelListView]. +Widget defaultSeparatorBuilder(BuildContext context, int index) => + const StreamChannelListSeparator(); + +/// Signature for the item builder that creates the children of the +/// [StreamChannelListView]. +typedef StreamChannelListViewItemBuilder = Widget Function( + BuildContext context, + Channel channel, + StreamChannelListTile defaultWidget, +); + +/// A [ListView] that shows a list of [Channel]s, +/// it uses [StreamChannelListTile] as a default item. +/// +/// This is the new version of [ChannelListView] that uses +/// [StreamChannelListController]. +/// +/// Example: +/// +/// ```dart +/// StreamChannelListView( +/// controller: controller, +/// onChannelTap: (channel) { +/// // Handle channel tap event +/// }, +/// onChannelLongPress: (channel) { +/// // Handle channel long press event +/// }, +/// ) +/// ``` +/// +/// See also: +/// * [StreamChannelListTile] +/// * [StreamChannelListController] +class StreamChannelListView extends StatefulWidget { + /// Creates a new instance of [StreamChannelListView]. + const StreamChannelListView({ + Key? key, + required this.controller, + this.itemBuilder, + this.separatorBuilder = defaultSeparatorBuilder, + this.emptyBuilder, + this.loadingBuilder, + this.errorBuilder, + this.onChannelTap, + this.onChannelLongPress, + this.padding, + this.physics, + this.reverse = false, + this.scrollController, + this.primary, + this.scrollBehavior, + this.shrinkWrap = false, + this.cacheExtent, + this.dragStartBehavior = DragStartBehavior.start, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, + this.restorationId, + }) : super(key: key); + + /// The [StreamChannelListController] used to control the list of channels. + final StreamChannelListController controller; + + /// A builder that is called to build items in the [ListView]. + /// + /// The `channel` parameter is the [Channel] at this position in the list + /// and the `defaultWidget` is the default widget used + /// i.e: [StreamChannelListTile]. + final StreamChannelListViewItemBuilder? itemBuilder; + + /// A builder that is called to build the list separator. + final IndexedWidgetBuilder separatorBuilder; + + /// A builder that is called to build the empty state of the list. + /// + /// If not provider, [StreamChannelListEmptyWidget] will be used. + final WidgetBuilder? emptyBuilder; + + /// A builder that is called to build the loading state of the list. + /// + /// If not provided, [StreamChannelListLoadingTile] will be used. + final WidgetBuilder? loadingBuilder; + + /// A builder that is called to build the error state of the list. + /// + /// If not provided, [StreamChannelListErrorWidget] will be used. + final Widget Function(BuildContext, StreamChatError)? errorBuilder; + + /// Called when the user taps this list tile. + final void Function(Channel)? onChannelTap; + + /// Called when the user long-presses on this list tile. + final void Function(Channel)? onChannelLongPress; + + /// The amount of space by which to inset the children. + final EdgeInsetsGeometry? padding; + + /// {@template flutter.widgets.scroll_view.reverse} + /// Whether the scroll view scrolls in the reading direction. + /// + /// For example, if [scrollDirection] is [Axis.vertical], then the scroll view + /// scrolls from top to bottom when [reverse] is false and from bottom to top + /// when [reverse] is true. + /// + /// Defaults to false. + /// {@endtemplate} + final bool reverse; + + /// {@template flutter.widgets.scroll_view.controller} + /// An object that can be used to control the position to which this scroll + /// view is scrolled. + /// + /// Must be null if [primary] is true. + /// + /// A [ScrollController] serves several purposes. It can be used to control + /// the initial scroll position (see [ScrollController.initialScrollOffset]). + /// It can be used to control whether the scroll view should automatically + /// save and restore its scroll position in the [PageStorage] (see + /// [ScrollController.keepScrollOffset]). It can be used to read the current + /// scroll position (see [ScrollController.offset]), or change it (see + /// [ScrollController.animateTo]). + /// {@endtemplate} + final ScrollController? scrollController; + + /// {@template flutter.widgets.scroll_view.primary} + /// Whether this is the primary scroll view associated with the parent + /// [PrimaryScrollController]. + /// + /// When this is true, the scroll view is scrollable even if it does not have + /// sufficient content to actually scroll. Otherwise, by default the user can + /// only scroll the view if it has sufficient content. See [physics]. + /// + /// Also when true, the scroll view is used for default [ScrollAction]s. If a + /// ScrollAction is not handled by an otherwise focused part of the + /// application, the ScrollAction will be evaluated using this scroll view, + /// for example, when executing [Shortcuts] key events like page up and down. + /// + /// On iOS, this also identifies the scroll view that will scroll to top in + /// response to a tap in the status bar. + /// {@endtemplate} + /// + /// Defaults to true when [scrollController] is null. + final bool? primary; + + /// {@macro flutter.widgets.shadow.scrollBehavior} + /// + /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit + /// [ScrollPhysics] is provided in [physics], it will take precedence, + /// followed by [scrollBehavior], and then the inherited ancestor + /// [ScrollBehavior]. + final ScrollBehavior? scrollBehavior; + + /// {@template flutter.widgets.scroll_view.shrinkWrap} + /// Whether the extent of the scroll view in the [scrollDirection] should be + /// determined by the contents being viewed. + /// + /// If the scroll view does not shrink wrap, then the scroll view will expand + /// to the maximum allowed size in the [scrollDirection]. If the scroll view + /// has unbounded constraints in the [scrollDirection], then [shrinkWrap] must + /// be true. + /// + /// Shrink wrapping the content of the scroll view is significantly more + /// expensive than expanding to the maximum allowed size because the content + /// can expand and contract during scrolling, which means the size of the + /// scroll view needs to be recomputed whenever the scroll position changes. + /// + /// Defaults to false. + /// {@endtemplate} + final bool shrinkWrap; + + /// {@template flutter.widgets.scroll_view.physics} + /// How the scroll view should respond to user input. + /// + /// For example, determines how the scroll view continues to animate after the + /// user stops dragging the scroll view. + /// + /// Defaults to matching platform conventions. Furthermore, if [primary] is + /// false, then the user cannot scroll if there is insufficient content to + /// scroll, while if [primary] is true, they can always attempt to scroll. + /// + /// To force the scroll view to always be scrollable even if there is + /// insufficient content, as if [primary] was true but without necessarily + /// setting it to true, provide an [AlwaysScrollableScrollPhysics] physics + /// object, as in: + /// + /// ```dart + /// physics: const AlwaysScrollableScrollPhysics(), + /// ``` + /// + /// To force the scroll view to use the default platform conventions and not + /// be scrollable if there is insufficient content, regardless of the value of + /// [primary], provide an explicit [ScrollPhysics] object, as in: + /// + /// ```dart + /// physics: const ScrollPhysics(), + /// ``` + /// + /// The physics can be changed dynamically (by providing a new object in a + /// subsequent build), but new physics will only take effect if the _class_ of + /// the provided object changes. Merely constructing a new instance with a + /// different configuration is insufficient to cause the physics to be + /// reapplied. (This is because the final object used is generated + /// dynamically, which can be relatively expensive, and it would be + /// inefficient to speculatively create this object each frame to see if the + /// physics should be updated.) + /// {@endtemplate} + /// + /// If an explicit [ScrollBehavior] is provided to [scrollBehavior], the + /// [ScrollPhysics] provided by that behavior will take precedence after + /// [physics]. + final ScrollPhysics? physics; + + /// {@macro flutter.rendering.RenderViewportBase.cacheExtent} + final double? cacheExtent; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@template flutter.widgets.scroll_view.keyboardDismissBehavior} + /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will + /// dismiss the keyboard automatically. + /// {@endtemplate} + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + + /// {@macro flutter.widgets.scrollable.restorationId} + final String? restorationId; + + @override + _StreamChannelListViewState createState() => _StreamChannelListViewState(); +} + +class _StreamChannelListViewState extends State { + StreamChannelListController get _controller => widget.controller; + + // Avoids duplicate requests on rebuilds. + bool _hasRequestedNextPage = false; + + @override + void initState() { + super.initState(); + _controller.doInitialLoad(); + } + + @override + void didUpdateWidget(covariant StreamChannelListView oldWidget) { + super.didUpdateWidget(oldWidget); + if (_controller != oldWidget.controller) { + // reset duplicate requests flag + _hasRequestedNextPage = false; + _controller.doInitialLoad(); + } + } + + @override + Widget build(BuildContext context) => + PagedValueListenableBuilder( + valueListenable: widget.controller, + builder: (context, value, _) => value.when( + (channels, nextPageKey, error) { + if (channels.isEmpty) { + return widget.emptyBuilder?.call(context) ?? + const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: StreamChannelListEmptyWidget(), + ), + ); + } + + return ListView.separated( + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + controller: widget.scrollController, + primary: widget.primary, + shrinkWrap: widget.shrinkWrap, + keyboardDismissBehavior: widget.keyboardDismissBehavior, + restorationId: widget.restorationId, + dragStartBehavior: widget.dragStartBehavior, + cacheExtent: widget.cacheExtent, + itemCount: value.itemCount, + separatorBuilder: widget.separatorBuilder, + itemBuilder: (context, index) { + if (!_hasRequestedNextPage) { + final newPageRequestTriggerIndex = channels.length - 3; + final isBuildingTriggerIndexItem = + index == newPageRequestTriggerIndex; + if (nextPageKey != null && isBuildingTriggerIndexItem) { + // Schedules the request for the end of this frame. + WidgetsBinding.instance?.addPostFrameCallback((_) async { + if (error == null) { + await _controller.loadMore(nextPageKey); + } + _hasRequestedNextPage = false; + }); + _hasRequestedNextPage = true; + } + } + + if (index == channels.length) { + if (error != null) { + return StreamChannelListLoadMoreError( + onTap: _controller.retry, + ); + } + return const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: StreamChannelListLoadMoreIndicator(), + ), + ); + } + + final channel = channels[index]; + + final onTap = widget.onChannelTap; + final onLongPress = widget.onChannelLongPress; + + final streamChannelListTile = StreamChannelListTile( + channel: channel, + onTap: onTap == null ? null : () => onTap(channel), + onLongPress: + onLongPress == null ? null : () => onLongPress(channel), + ); + + final itemBuilder = widget.itemBuilder; + + if (itemBuilder != null) { + return itemBuilder( + context, + channel, + streamChannelListTile, + ); + } + + return streamChannelListTile; + }, + ); + }, + loading: () => + widget.loadingBuilder?.call(context) ?? + ListView.separated( + padding: widget.padding, + physics: widget.physics, + reverse: widget.reverse, + itemCount: 25, + separatorBuilder: widget.separatorBuilder, + itemBuilder: (_, __) => const StreamChannelListLoadingTile(), + ), + error: (error) => + widget.errorBuilder?.call(context, error) ?? + Center( + child: StreamChannelListErrorWidget( + onPressed: _controller.refresh, + ), + ), + ), + ); +} + +/// A [StreamChannelListTile] that can be used in a [ListView] to show a +/// loading tile while waiting for the [StreamChannelListController] to load +/// more channels. +class StreamChannelListLoadMoreIndicator extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadMoreIndicator]. + const StreamChannelListLoadMoreIndicator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) => const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator.adaptive(), + ); +} + +/// A [StreamChannelListTile] that is used to display the error indicator when +/// loading more channels fails. +class StreamChannelListLoadMoreError extends StatelessWidget { + /// Creates a new instance of [StreamChannelListLoadMoreError]. + const StreamChannelListLoadMoreError({ + Key? key, + this.onTap, + }) : super(key: key); + + /// The callback to invoke when the user taps on the error indicator. + final GestureTapCallback? onTap; + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + return InkWell( + onTap: onTap, + child: Container( + color: theme.colorTheme.textLowEmphasis.withOpacity(0.9), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + context.translations.loadingChannelsError, + style: theme.textTheme.body.copyWith( + color: Colors.white, + ), + ), + StreamSvgIcon.retry(color: Colors.white), + ], + ), + ), + ), + ); + } +} + +/// A widget that is used to display a separator between +/// [StreamChannelListTile] items. +class StreamChannelListSeparator extends StatelessWidget { + /// Creates a new instance of [StreamChannelListSeparator]. + const StreamChannelListSeparator({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final effect = StreamChatTheme.of(context).colorTheme.borderBottom; + return Container( + height: 1, + color: effect.color!.withOpacity(effect.alpha ?? 1.0), + ); + } +} + +/// A widget that is used to display an error screen +/// when [StreamChannelListController] fails to load initial channels. +class StreamChannelListErrorWidget extends StatelessWidget { + /// Creates a new instance of [StreamChannelListErrorWidget] widget. + const StreamChannelListErrorWidget({ + Key? key, + this.onPressed, + }) : super(key: key); + + /// The callback to invoke when the user taps on the retry button. + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text.rich( + TextSpan( + children: [ + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(right: 2), + child: Icon(Icons.error_outline), + ), + ), + TextSpan(text: context.translations.loadingChannelsError), + ], + ), + style: Theme.of(context).textTheme.headline6, + ), + TextButton( + onPressed: onPressed, + child: Text(context.translations.retryLabel), + ), + ], + ); +} + +/// A widget that is used to display an empty state when +/// [StreamChannelListController] loads zero channels. +class StreamChannelListEmptyWidget extends StatelessWidget { + /// Creates a new instance of [StreamChannelListEmptyWidget] widget. + const StreamChannelListEmptyWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.message( + size: 148, + color: chatThemeData.colorTheme.disabled, + ), + const SizedBox(height: 28), + Text( + context.translations.letsStartChattingLabel, + style: chatThemeData.textTheme.headline, + ), + ], + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart new file mode 100644 index 00000000..95cc14f2 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_avatar.dart @@ -0,0 +1,200 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/group_avatar.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image.png) +/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_image_paint.png) +/// +/// It shows the current [Channel] image. +/// +/// ```dart +/// class MyApp extends StatelessWidget { +/// final StreamChatClient client; +/// final Channel channel; +/// +/// MyApp(this.client, this.channel); +/// +/// @override +/// Widget build(BuildContext context) { +/// return MaterialApp( +/// debugShowCheckedModeBanner: false, +/// home: StreamChat( +/// client: client, +/// child: StreamChannel( +/// channel: channel, +/// child: Center( +/// child: ChannelImage( +/// channel: channel, +/// ), +/// ), +/// ), +/// ), +/// ); +/// } +/// } +/// ``` +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +/// +/// By default the widget radius size is 40x40 pixels. +/// Set the property [constraints] to set a custom dimension. +/// +/// The widget renders the ui based on the first ancestor of type +/// [StreamChatTheme]. +/// Modify it to change the widget appearance. +class StreamChannelAvatar extends StatelessWidget { + /// Instantiate a new ChannelImage + StreamChannelAvatar({ + Key? key, + required this.channel, + this.constraints, + this.onTap, + this.borderRadius, + this.selected = false, + this.selectionColor, + this.selectionThickness = 4, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// [BorderRadius] to display the widget + final BorderRadius? borderRadius; + + /// The channel to show the image of + final Channel channel; + + /// The diameter of the image + final BoxConstraints? constraints; + + /// The function called when the image is tapped + final VoidCallback? onTap; + + /// If image is selected + final bool selected; + + /// Selection color for image + final Color? selectionColor; + + /// Thickness of selection image + final double selectionThickness; + + @override + Widget build(BuildContext context) { + final client = channel.client.state; + + final chatThemeData = StreamChatTheme.of(context); + final colorTheme = chatThemeData.colorTheme; + final previewTheme = chatThemeData.channelPreviewTheme.avatarTheme; + + return BetterStreamBuilder( + stream: channel.imageStream, + initialData: channel.image, + builder: (context, channelImage) { + Widget child = ClipRRect( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + child: Container( + constraints: constraints ?? previewTheme?.constraints, + decoration: BoxDecoration(color: colorTheme.accentPrimary), + child: InkWell( + onTap: onTap, + child: CachedNetworkImage( + imageUrl: channelImage, + errorWidget: (_, __, ___) => Center( + child: Text( + channel.name?[0] ?? '', + style: TextStyle( + color: colorTheme.barsBg, + fontWeight: FontWeight.bold, + ), + ), + ), + fit: BoxFit.cover, + ), + ), + ), + ); + + if (selected) { + child = ClipRRect( + key: const Key('selectedImage'), + borderRadius: BorderRadius.circular(selectionThickness) + + (borderRadius ?? + previewTheme?.borderRadius ?? + BorderRadius.zero), + child: Container( + constraints: constraints ?? previewTheme?.constraints, + color: selectionColor ?? colorTheme.accentPrimary, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: child, + ), + ), + ); + } + return child; + }, + noDataBuilder: (context) { + final currentUser = client.currentUser!; + final otherMembers = channel.state!.members + .where((it) => it.userId != currentUser.id) + .toList(growable: false); + + // our own space, no other members + if (otherMembers.isEmpty) { + return BetterStreamBuilder( + stream: client.currentUserStream.map((it) => it!), + initialData: currentUser, + builder: (context, user) => StreamUserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: user, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // 1-1 Conversation + if (otherMembers.length == 1) { + final member = otherMembers.first; + return BetterStreamBuilder( + stream: channel.state!.membersStream.map( + (members) => members.firstWhere( + (it) => it.userId == member.userId, + orElse: () => member, + ), + ), + initialData: member, + builder: (context, member) => StreamUserAvatar( + borderRadius: borderRadius ?? previewTheme?.borderRadius, + user: member.user!, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap != null ? (_) => onTap!() : null, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ), + ); + } + + // Group conversation + return StreamGroupAvatar( + channel: channel, + members: otherMembers, + borderRadius: borderRadius ?? previewTheme?.borderRadius, + constraints: constraints ?? previewTheme?.constraints, + onTap: onTap, + selected: selected, + selectionColor: selectionColor ?? colorTheme.accentPrimary, + selectionThickness: selectionThickness, + ); + }, + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart new file mode 100644 index 00000000..1a219e91 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_info_bottom_sheet.dart @@ -0,0 +1,366 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/channel_info.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/option_list_tile.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/theme/themes.dart'; +import 'package:stream_chat_flutter/src/user_avatar.dart'; +import 'package:stream_chat_flutter/src/v4/stream_channel_name.dart'; + +/// A [BottomSheet] that shows information about a [Channel]. +class StreamChannelInfoBottomSheet extends StatelessWidget { + /// Creates a new instance [StreamChannelInfoBottomSheet] widget. + StreamChannelInfoBottomSheet({ + Key? key, + required this.channel, + this.onMemberTap, + this.onViewInfoTap, + this.onLeaveChannelTap, + this.onDeleteConversationTap, + this.onCancelTap, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The [Channel] to show information about. + final Channel channel; + + /// A callback that is called when a member is tapped. + final void Function(Member)? onMemberTap; + + /// A callback that is called when the "View Info" button is tapped. + final VoidCallback? onViewInfoTap; + + /// A callback that is called when the "Leave Channel" button is tapped. + /// + /// Only shown when the channel is a group channel. + final VoidCallback? onLeaveChannelTap; + + /// A callback that is called when the "Delete Conversation" button is tapped. + /// + /// Only shown when you are the `owner` of the channel. + final VoidCallback? onDeleteConversationTap; + + /// A callback that is called when the "Cancel" button is tapped. + final VoidCallback? onCancelTap; + + @override + Widget build(BuildContext context) { + final themeData = StreamChatTheme.of(context); + final colorTheme = themeData.colorTheme; + final channelPreviewTheme = StreamChannelPreviewTheme.of(context); + + final currentUser = channel.client.state.currentUser; + final isOneToOneChannel = channel.isDistinct && channel.memberCount == 2; + + final members = channel.state?.members ?? []; + + final isOwner = members.any( + (it) => it.user?.id == currentUser?.id && it.role == 'owner', + ); + + // remove current user in case it's 1-1 conversation + if (isOneToOneChannel) { + members.removeWhere((it) => it.user?.id == currentUser?.id); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 24), + Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamChannelName( + channel: channel, + textStyle: themeData.textTheme.headlineBold, + ), + ), + ), + const SizedBox(height: 5), + Center( + // TODO: Refactor ChannelInfo + child: StreamChannelInfo( + showTypingIndicator: false, + channel: channel, + textStyle: channelPreviewTheme.subtitleStyle, + ), + ), + const SizedBox(height: 17), + Container( + height: 94, + alignment: Alignment.center, + child: ListView.separated( + shrinkWrap: true, + itemCount: members.length, + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8), + separatorBuilder: (context, index) => const SizedBox(width: 16), + itemBuilder: (context, index) { + final member = members[index]; + final user = member.user!; + return Column( + children: [ + StreamUserAvatar( + user: user, + constraints: const BoxConstraints( + maxHeight: 64, + maxWidth: 64, + ), + borderRadius: BorderRadius.circular(32), + onlineIndicatorConstraints: BoxConstraints.tight( + const Size(12, 12), + ), + onTap: onMemberTap != null + ? (_) => onMemberTap!(member) + : null, + ), + const SizedBox(height: 6), + Text( + user.name, + style: themeData.textTheme.footnoteBold, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ); + }, + ), + ), + const SizedBox(height: 24), + StreamOptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.user( + color: colorTheme.textLowEmphasis, + ), + ), + title: context.translations.viewInfoLabel, + onTap: onViewInfoTap, + ), + if (!isOneToOneChannel) + StreamOptionListTile( + title: context.translations.leaveGroupLabel, + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.userRemove( + color: colorTheme.textLowEmphasis, + ), + ), + onTap: onLeaveChannelTap, + ), + if (isOwner) + StreamOptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.delete( + color: colorTheme.accentError, + ), + ), + title: context.translations.deleteConversationLabel, + titleColor: colorTheme.accentError, + onTap: onDeleteConversationTap, + ), + StreamOptionListTile( + leading: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: StreamSvgIcon.closeSmall( + color: colorTheme.textLowEmphasis, + ), + ), + title: context.translations.cancelLabel, + onTap: onCancelTap ?? Navigator.of(context).pop, + ), + ], + ); + } +} + +const _kDefaultChannelInfoBottomSheetShape = RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), +); + +/// Shows a modal material design bottom sheet. +/// +/// A modal bottom sheet is an alternative to a menu or a dialog and prevents +/// the user from interacting with the rest of the app. +/// +/// A closely related widget is a persistent bottom sheet, which shows +/// information that supplements the primary content of the app without +/// preventing the use from interacting with the app. Persistent bottom sheets +/// can be created and displayed with the [showBottomSheet] function or the +/// [ScaffoldState.showBottomSheet] method. +/// +/// The `context` argument is used to look up the [Navigator] and [Theme] for +/// the bottom sheet. It is only used when the method is called. Its +/// corresponding widget can be safely removed from the tree before the bottom +/// sheet is closed. +/// +/// The `isScrollControlled` parameter specifies whether this is a route for +/// a bottom sheet that will utilize [DraggableScrollableSheet]. If you wish +/// to have a bottom sheet that has a scrollable child such as a [ListView] or +/// a [GridView] and have the bottom sheet be draggable, you should set this +/// parameter to true. +/// +/// The `useRootNavigator` parameter ensures that the root navigator is used to +/// display the [BottomSheet] when set to `true`. This is useful in the case +/// that a modal [BottomSheet] needs to be displayed above all other content +/// but the caller is inside another [Navigator]. +/// +/// The [isDismissible] parameter specifies whether the bottom sheet will be +/// dismissed when user taps on the scrim. +/// +/// The [enableDrag] parameter specifies whether the bottom sheet can be +/// dragged up and down and dismissed by swiping downwards. +/// +/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], +/// [constraints] and [transitionAnimationController] +/// parameters can be passed in to customize the appearance and behavior of +/// modal bottom sheets (see the documentation for these on [BottomSheet] +/// for more details). +/// +/// The [transitionAnimationController] controls the bottom sheet's entrance and +/// exit animations if provided. +/// +/// The optional `routeSettings` parameter sets the [RouteSettings] +/// of the modal bottom sheet sheet. +/// This is particularly useful in the case that a user wants to observe +/// [PopupRoute]s within a [NavigatorObserver]. +/// +/// Returns a `Future` that resolves to the value (if any) that was passed to +/// [Navigator.pop] when the modal bottom sheet was closed. +/// +/// See also: +/// +/// * [BottomSheet], which becomes the parent of the widget returned by the +/// function passed as the `builder` argument to [showModalBottomSheet]. +/// * [showBottomSheet] and [ScaffoldState.showBottomSheet], for showing +/// non-modal bottom sheets. +/// * [DraggableScrollableSheet], which allows you to create a bottom sheet +/// that grows and then becomes scrollable once it reaches its maximum size. +/// * +Future showChannelInfoModalBottomSheet({ + required BuildContext context, + required Channel channel, + Color? backgroundColor, + double? elevation, + BoxConstraints? constraints, + Color? barrierColor, + bool isScrollControlled = true, + bool useRootNavigator = false, + bool isDismissible = true, + bool enableDrag = true, + RouteSettings? routeSettings, + AnimationController? transitionAnimationController, + Clip? clipBehavior = Clip.hardEdge, + ShapeBorder? shape = _kDefaultChannelInfoBottomSheetShape, + void Function(Member)? onMemberTap, + VoidCallback? onViewInfoTap, + VoidCallback? onLeaveChannelTap, + VoidCallback? onDeleteConversationTap, + VoidCallback? onCancelTap, +}) => + showModalBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + barrierColor: barrierColor, + isScrollControlled: isScrollControlled, + useRootNavigator: useRootNavigator, + isDismissible: isDismissible, + enableDrag: enableDrag, + routeSettings: routeSettings, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), + ); + +/// Shows a material design bottom sheet in the nearest [Scaffold] ancestor. If +/// you wish to show a persistent bottom sheet, use [Scaffold.bottomSheet]. +/// +/// Returns a controller that can be used to close and otherwise manipulate the +/// bottom sheet. +/// +/// The optional [backgroundColor], [elevation], [shape], [clipBehavior], +/// [constraints] and [transitionAnimationController] +/// parameters can be passed in to customize the appearance and behavior of +/// persistent bottom sheets (see the documentation for these on [BottomSheet] +/// for more details). +/// +/// To rebuild the bottom sheet (e.g. if it is stateful), call +/// [PersistentBottomSheetController.setState] on the controller returned by +/// this method. +/// +/// The new bottom sheet becomes a [LocalHistoryEntry] for the enclosing +/// [ModalRoute] and a back button is added to the app bar of the [Scaffold] +/// that closes the bottom sheet. +/// +/// To create a persistent bottom sheet that is not a [LocalHistoryEntry] and +/// does not add a back button to the enclosing Scaffold's app bar, use the +/// [Scaffold.bottomSheet] constructor parameter. +/// +/// A closely related widget is a modal bottom sheet, which is an alternative +/// to a menu or a dialog and prevents the user from interacting with the rest +/// of the app. Modal bottom sheets can be created and displayed with the +/// [showModalBottomSheet] function. +/// +/// The `context` argument is used to look up the [Scaffold] for the bottom +/// sheet. It is only used when the method is called. Its corresponding widget +/// can be safely removed from the tree before the bottom sheet is closed. +/// +/// See also: +/// +/// * [BottomSheet], which becomes the parent of the widget returned by the +/// `builder`. +/// * [showModalBottomSheet], which can be used to display a modal bottom +/// sheet. +/// * [Scaffold.of], for information about how to obtain the [BuildContext]. +/// * +PersistentBottomSheetController showChannelInfoBottomSheet({ + required BuildContext context, + required Channel channel, + Color? backgroundColor, + double? elevation, + BoxConstraints? constraints, + AnimationController? transitionAnimationController, + Clip? clipBehavior = Clip.hardEdge, + ShapeBorder? shape = _kDefaultChannelInfoBottomSheetShape, + void Function(Member)? onMemberTap, + VoidCallback? onViewInfoTap, + VoidCallback? onLeaveChannelTap, + VoidCallback? onDeleteConversationTap, + VoidCallback? onCancelTap, +}) => + showBottomSheet( + context: context, + backgroundColor: backgroundColor, + elevation: elevation, + shape: shape, + clipBehavior: clipBehavior, + constraints: constraints, + transitionAnimationController: transitionAnimationController, + builder: (BuildContext context) => StreamChannelInfoBottomSheet( + channel: channel, + onMemberTap: onMemberTap, + onViewInfoTap: onViewInfoTap, + onLeaveChannelTap: onLeaveChannelTap, + onDeleteConversationTap: onDeleteConversationTap, + onCancelTap: onCancelTap, + ), + ); diff --git a/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart new file mode 100644 index 00000000..c6070e0f --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/v4/stream_channel_name.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// It shows the current [Channel] name using a [Text] widget. +/// +/// The widget uses a [StreamBuilder] to render the channel information +/// image as soon as it updates. +class StreamChannelName extends StatelessWidget { + /// Instantiate a new ChannelName + StreamChannelName({ + Key? key, + required this.channel, + this.textStyle, + this.textOverflow = TextOverflow.ellipsis, + }) : assert( + channel.state != null, + 'Channel ${channel.id} is not initialized', + ), + super(key: key); + + /// The [Channel] to show the name for. + final Channel channel; + + /// The style of the text displayed + final TextStyle? textStyle; + + /// How visual overflow should be handled. + final TextOverflow textOverflow; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + stream: channel.nameStream, + initialData: channel.name, + builder: (context, channelName) => Text( + channelName, + style: textStyle, + overflow: textOverflow, + ), + noDataBuilder: (context) => _generateName( + channel.client.state.currentUser!, + channel.state!.members, + ), + ); + + Widget _generateName( + User currentUser, + List members, + ) => + LayoutBuilder( + builder: (context, constraints) { + var channelName = context.translations.noTitleText; + final otherMembers = members.where( + (member) => member.userId != currentUser.id, + ); + + if (otherMembers.isNotEmpty) { + if (otherMembers.length == 1) { + final user = otherMembers.first.user; + if (user != null) { + channelName = user.name; + } + } else { + final maxWidth = constraints.maxWidth; + final maxChars = maxWidth / (textStyle?.fontSize ?? 1); + var currentChars = 0; + final currentMembers = []; + otherMembers.forEach((element) { + final newLength = + currentChars + (element.user?.name.length ?? 0); + if (newLength < maxChars) { + currentChars = newLength; + currentMembers.add(element); + } + }); + + final exceedingMembers = + otherMembers.length - currentMembers.length; + channelName = + '${currentMembers.map((e) => e.user?.name).join(', ')} ' + '${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } + } + + return Text( + channelName, + style: textStyle, + overflow: textOverflow, + ); + }, + ); +} diff --git a/packages/stream_chat_flutter/lib/src/video_service.dart b/packages/stream_chat_flutter/lib/src/video_service.dart index a50a43c8..120460be 100644 --- a/packages/stream_chat_flutter/lib/src/video_service.dart +++ b/packages/stream_chat_flutter/lib/src/video_service.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:synchronized/synchronized.dart'; -import 'package:video_compress/video_compress.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; /// @@ -12,32 +10,6 @@ class _IVideoService { /// Singleton instance of [_IVideoService] static final _IVideoService instance = _IVideoService._(); - final _lock = Lock(); - - /// compress video from [path] - /// compress video from [path] return [Future] - /// - /// you can choose its [quality] and [frameRate] - /// - /// ## example - /// ```dart - /// final info = await _flutterVideoCompress.compressVideo( - /// file.path, - /// ); - /// debugPrint(info.toJson()); - /// ``` - Future compressVideo( - String path, { - int frameRate = 30, - VideoQuality quality = VideoQuality.DefaultQuality, - }) async => - _lock.synchronized( - () => VideoCompress.compressVideo( - path, - frameRate: frameRate, - quality: quality, - ), - ); /// Generates a thumbnail image data in memory as UInt8List, /// it can be easily used by Image.memory(...). @@ -66,5 +38,10 @@ class _IVideoService { } /// Get instance of [_IVideoService] +@Deprecated("Use 'StreamVideoService' instead") // ignore: non_constant_identifier_names _IVideoService get VideoService => _IVideoService.instance; + +/// Get instance of [_IVideoService] +// ignore: non_constant_identifier_names +_IVideoService get StreamVideoService => _IVideoService.instance; diff --git a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart index 2286bc3d..4d253da0 100644 --- a/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart +++ b/packages/stream_chat_flutter/lib/src/video_thumbnail_image.dart @@ -6,10 +6,16 @@ import 'package:stream_chat_flutter/src/video_service.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; +/// {@macro video_thumbnail_image} +@Deprecated("Use 'StreamVideoThumbnailImage' instead") +typedef VideoThumbnailImage = StreamVideoThumbnailImage; + +/// {@template video_thumbnail_image} /// Widget for creating video thumbnail image -class VideoThumbnailImage extends StatefulWidget { - /// Constructor for creating [VideoThumbnailImage] - const VideoThumbnailImage({ +/// {@endtemplate} +class StreamVideoThumbnailImage extends StatefulWidget { + /// Constructor for creating [StreamVideoThumbnailImage] + const StreamVideoThumbnailImage({ Key? key, required this.video, this.width, @@ -42,16 +48,17 @@ class VideoThumbnailImage extends StatefulWidget { final WidgetBuilder? placeholderBuilder; @override - _VideoThumbnailImageState createState() => _VideoThumbnailImageState(); + _StreamVideoThumbnailImageState createState() => + _StreamVideoThumbnailImageState(); } -class _VideoThumbnailImageState extends State { +class _StreamVideoThumbnailImageState extends State { late Future thumbnailFuture; late StreamChatThemeData _streamChatTheme; @override void initState() { - thumbnailFuture = VideoService.generateVideoThumbnail( + thumbnailFuture = StreamVideoService.generateVideoThumbnail( video: widget.video, imageFormat: widget.format, ); @@ -65,9 +72,9 @@ class _VideoThumbnailImageState extends State { } @override - void didUpdateWidget(covariant VideoThumbnailImage oldWidget) { + void didUpdateWidget(covariant StreamVideoThumbnailImage oldWidget) { if (oldWidget.video != widget.video || oldWidget.format != widget.format) { - thumbnailFuture = VideoService.generateVideoThumbnail( + thumbnailFuture = StreamVideoService.generateVideoThumbnail( video: widget.video, imageFormat: widget.format, ); diff --git a/packages/stream_chat_flutter/lib/src/visible_footnote.dart b/packages/stream_chat_flutter/lib/src/visible_footnote.dart index 700e22ad..1456de3a 100644 --- a/packages/stream_chat_flutter/lib/src/visible_footnote.dart +++ b/packages/stream_chat_flutter/lib/src/visible_footnote.dart @@ -2,10 +2,16 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +/// {@macro visible_footnote} +@Deprecated("Use 'StreamVisibleFootnote' instead") +typedef VisibleFootnote = StreamVisibleFootnote; + +/// {@template visible_footnote} /// Widget for displaying a footnote -class VisibleFootnote extends StatelessWidget { - /// Constructor for creating a [VisibleFootnote] - const VisibleFootnote({Key? key}) : super(key: key); +/// {@endtemplate} +class StreamVisibleFootnote extends StatelessWidget { + /// Constructor for creating a [StreamVisibleFootnote] + const StreamVisibleFootnote({Key? key}) : super(key: key); @override Widget build(BuildContext context) { diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index cd0d93a0..e63db686 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -21,9 +21,13 @@ export 'src/gradient_avatar.dart'; export 'src/info_tile.dart'; export 'src/localization/stream_chat_localizations.dart'; export 'src/localization/translations.dart' show DefaultTranslations; -export 'src/mention_tile.dart'; export 'src/message_action.dart'; -export 'src/message_input.dart'; +export 'src/message_input.dart' show MessageInput, MessageInputState; +export 'src/message_input/countdown_button.dart'; +export 'src/message_input/message_input.dart'; +export 'src/message_input/stream_attachment_picker.dart'; +export 'src/message_input/stream_message_send_button.dart'; +export 'src/message_input/stream_message_text_field.dart'; export 'src/message_list_view.dart'; export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; @@ -47,4 +51,12 @@ export 'src/user_item.dart'; export 'src/user_list_view.dart'; export 'src/user_mention_tile.dart'; export 'src/utils.dart'; + +// v4 +export 'src/v4/channel_list_view/stream_channel_list_loading_tile.dart'; +export 'src/v4/channel_list_view/stream_channel_list_tile.dart'; +export 'src/v4/channel_list_view/stream_channel_list_view.dart'; +export 'src/v4/stream_channel_avatar.dart'; +export 'src/v4/stream_channel_info_bottom_sheet.dart'; +export 'src/v4/stream_channel_name.dart'; export 'src/visible_footnote.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 2abc216f..9b769039 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -38,9 +38,7 @@ dependencies: shimmer: ^2.0.0 stream_chat_flutter_core: ^3.5.1 substring_highlight: ^1.0.26 - synchronized: ^3.0.0 url_launcher: ^6.0.3 - video_compress: ^3.0.0 video_player: ^2.1.0 video_thumbnail: ^0.4.3 visibility_detector: ^0.2.0 diff --git a/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart b/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart index ed4ce267..9747f11d 100644 --- a/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart +++ b/packages/stream_chat_flutter/test/src/attachment_widgets_test.dart @@ -24,7 +24,7 @@ void main() { child: StreamChannel( channel: channel, child: SizedBox( - child: FileAttachment( + child: StreamFileAttachment( size: const Size( 300, 300, diff --git a/packages/stream_chat_flutter/test/src/back_button_test.dart b/packages/stream_chat_flutter/test/src/back_button_test.dart index fca30c5b..f6ec0fc4 100644 --- a/packages/stream_chat_flutter/test/src/back_button_test.dart +++ b/packages/stream_chat_flutter/test/src/back_button_test.dart @@ -136,7 +136,7 @@ void main() { ), ); - expect(find.byType(UnreadIndicator), findsOneWidget); + expect(find.byType(StreamUnreadIndicator), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/channel_header_test.dart b/packages/stream_chat_flutter/test/src/channel_header_test.dart index 95d2e751..6590ee66 100644 --- a/packages/stream_chat_flutter/test/src/channel_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_header_test.dart @@ -58,7 +58,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader(), + body: StreamChannelHeader(), ), ), ), @@ -67,7 +67,7 @@ void main() { expect(find.text('test'), findsOneWidget); expect(find.byType(ChannelAvatar), findsOneWidget); expect(find.byType(StreamBackButton), findsOneWidget); - expect(find.byType(ChannelInfo), findsOneWidget); + expect(find.byType(StreamChannelInfo), findsOneWidget); }, ); @@ -124,7 +124,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( showConnectionStateTile: true, ), ), @@ -132,8 +132,12 @@ void main() { ), )); - expect(tester.widget(find.byType(InfoTile)).showMessage, true); - expect(tester.widget(find.byType(InfoTile)).message, + expect( + tester + .widget(find.byType(StreamInfoTile)) + .showMessage, + true); + expect(tester.widget(find.byType(StreamInfoTile)).message, 'Disconnected'); }, ); @@ -190,7 +194,7 @@ void main() { channel: channel, showLoading: false, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( showConnectionStateTile: true, ), ), @@ -200,8 +204,12 @@ void main() { await tester.pump(); - expect(tester.widget(find.byType(InfoTile)).showMessage, true); - expect(tester.widget(find.byType(InfoTile)).message, + expect( + tester + .widget(find.byType(StreamInfoTile)) + .showMessage, + true); + expect(tester.widget(find.byType(StreamInfoTile)).message, 'Reconnecting...'); }, ); @@ -257,7 +265,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( leading: Text('leading'), subtitle: Text('subtitle'), actions: [ @@ -273,7 +281,7 @@ void main() { expect(find.text('test'), findsNothing); expect(find.byType(StreamBackButton), findsNothing); expect(find.byType(ChannelAvatar), findsNothing); - expect(find.byType(ChannelInfo), findsNothing); + expect(find.byType(StreamChannelInfo), findsNothing); expect(find.text('leading'), findsOneWidget); expect(find.text('title'), findsOneWidget); expect(find.text('subtitle'), findsOneWidget); @@ -331,7 +339,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( showTypingIndicator: false, showBackButton: false, ), @@ -343,10 +351,14 @@ void main() { expect(find.byType(StreamBackButton), findsNothing); expect( tester - .widget(find.byType(ChannelInfo)) + .widget(find.byType(StreamChannelInfo)) .showTypingIndicator, false); - expect(tester.widget(find.byType(InfoTile)).showMessage, false); + expect( + tester + .widget(find.byType(StreamInfoTile)) + .showMessage, + false); }, ); @@ -405,7 +417,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ChannelHeader( + body: StreamChannelHeader( onBackPressed: () => backPressed = true, onImageTap: () => imageTapped = true, onTitleTap: () => titleTapped = true, diff --git a/packages/stream_chat_flutter/test/src/channel_image_test.dart b/packages/stream_chat_flutter/test/src/channel_image_test.dart index be2ade55..3fc1e718 100644 --- a/packages/stream_chat_flutter/test/src/channel_image_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_image_test.dart @@ -169,7 +169,8 @@ void main() { ), )); - final image = tester.widget(find.byType(GroupAvatar)); + final image = + tester.widget(find.byType(StreamGroupAvatar)); final otherMembers = members.where((it) => it.userId != currentUser.id); expect( image.members.map((it) => it.user?.id), diff --git a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart index a2783067..532d07d6 100644 --- a/packages/stream_chat_flutter/test/src/channel_list_header_test.dart +++ b/packages/stream_chat_flutter/test/src/channel_list_header_test.dart @@ -22,14 +22,15 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: ChannelListHeader(), + body: StreamChannelListHeader(), ), ), ), ); await tester.pumpAndSettle(); - final userAvatar = tester.widget(find.byType(UserAvatar)); + final userAvatar = + tester.widget(find.byType(StreamUserAvatar)); expect(userAvatar.user, clientState.currentUser); expect(find.byType(StreamNeumorphicButton), findsOneWidget); expect(find.text('Stream Chat'), findsOneWidget); @@ -52,7 +53,7 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( showConnectionStateTile: true, ), ), @@ -81,7 +82,7 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( showConnectionStateTile: true, ), ), @@ -110,7 +111,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( titleBuilder: (context, status, client) => const Text('TITLE'), subtitle: const Text('SUBTITLE'), leading: const Text('LEADING'), @@ -150,7 +151,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( preNavigationCallback: () { tapped = true; }, @@ -161,7 +162,7 @@ void main() { ); await tester.pump(); - await tester.tap(find.byType(UserAvatar)); + await tester.tap(find.byType(StreamUserAvatar)); expect(tapped, true); }, ); @@ -183,7 +184,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: ChannelListHeader( + body: StreamChannelListHeader( onUserAvatarTap: (u) { tapped++; }, @@ -197,7 +198,7 @@ void main() { ); await tester.pump(); - await tester.tap(find.byType(UserAvatar)); + await tester.tap(find.byType(StreamUserAvatar)); await tester.tap(find.byType(StreamNeumorphicButton)); expect(tapped, 2); }, diff --git a/packages/stream_chat_flutter/test/src/date_divider_test.dart b/packages/stream_chat_flutter/test/src/date_divider_test.dart index f84d4dce..59ba4be1 100644 --- a/packages/stream_chat_flutter/test/src/date_divider_test.dart +++ b/packages/stream_chat_flutter/test/src/date_divider_test.dart @@ -19,7 +19,7 @@ void main() { home: StreamChat( client: client, child: Scaffold( - body: DateDivider( + body: StreamDateDivider( dateTime: DateTime.now(), ), ), diff --git a/packages/stream_chat_flutter/test/src/deleted_message_test.dart b/packages/stream_chat_flutter/test/src/deleted_message_test.dart index 0d86e89c..171b993d 100644 --- a/packages/stream_chat_flutter/test/src/deleted_message_test.dart +++ b/packages/stream_chat_flutter/test/src/deleted_message_test.dart @@ -20,8 +20,8 @@ void main() { home: StreamChat( client: client, child: const Scaffold( - body: DeletedMessage( - messageTheme: MessageThemeData( + body: StreamDeletedMessage( + messageTheme: StreamMessageThemeData( createdAtStyle: TextStyle( color: Colors.black, ), @@ -75,7 +75,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: DeletedMessage( + child: StreamDeletedMessage( messageTheme: theme.ownMessageTheme, ), ), @@ -128,7 +128,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: DeletedMessage( + child: StreamDeletedMessage( messageTheme: theme.ownMessageTheme, ), ), @@ -181,7 +181,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: DeletedMessage( + child: StreamDeletedMessage( messageTheme: theme.ownMessageTheme, reverse: true, shape: RoundedRectangleBorder( diff --git a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart index 2d271090..c17df733 100644 --- a/packages/stream_chat_flutter/test/src/full_screen_media_test.dart +++ b/packages/stream_chat_flutter/test/src/full_screen_media_test.dart @@ -68,7 +68,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: FullScreenMedia( + child: StreamFullScreenMedia( mediaAttachments: [ Attachment( type: 'image', diff --git a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart index 6dcc0e2d..073adc55 100644 --- a/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart +++ b/packages/stream_chat_flutter/test/src/gradient_avatar_test.dart @@ -25,7 +25,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: 'demo user', userId: 'demo123'), + child: StreamGradientAvatar( + name: 'demo user', userId: 'demo123'), ), ), ), @@ -33,7 +34,7 @@ void main() { ), ); - expect(find.byType(GradientAvatar), findsOneWidget); + expect(find.byType(StreamGradientAvatar), findsOneWidget); }, ); @@ -47,7 +48,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: 'demo user', userId: 'demo123'), + child: + StreamGradientAvatar(name: 'demo user', userId: 'demo123'), ), ), ), @@ -68,7 +70,7 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: 'demo', userId: 'demo1'), + child: StreamGradientAvatar(name: 'demo', userId: 'demo1'), ), ), ), @@ -89,7 +91,7 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar( + child: StreamGradientAvatar( name: 'd123@/d de:\$as', userId: 'demo123', ), @@ -113,7 +115,8 @@ void main() { child: SizedBox( width: 100, height: 100, - child: GradientAvatar(name: '123@/d \$as', userId: 'demo123'), + child: StreamGradientAvatar( + name: '123@/d \$as', userId: 'demo123'), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/image_footer_test.dart b/packages/stream_chat_flutter/test/src/image_footer_test.dart index 434a78d4..7fe611b8 100644 --- a/packages/stream_chat_flutter/test/src/image_footer_test.dart +++ b/packages/stream_chat_flutter/test/src/image_footer_test.dart @@ -37,7 +37,7 @@ void main() { child: WillPopScope( onWillPop: () async => false, child: Scaffold( - body: GalleryFooter( + body: StreamGalleryFooter( message: Message(), ), ), diff --git a/packages/stream_chat_flutter/test/src/info_tile_test.dart b/packages/stream_chat_flutter/test/src/info_tile_test.dart index f175ca9a..e0687964 100644 --- a/packages/stream_chat_flutter/test/src/info_tile_test.dart +++ b/packages/stream_chat_flutter/test/src/info_tile_test.dart @@ -22,7 +22,7 @@ void main() { child: const Scaffold( body: Portal( child: SizedBox( - child: InfoTile( + child: StreamInfoTile( showMessage: true, message: 'message', child: Text('test'), @@ -52,7 +52,7 @@ void main() { child: const Scaffold( body: Portal( child: SizedBox( - child: InfoTile( + child: StreamInfoTile( showMessage: false, message: 'message', child: Text('test'), diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index 9210ac49..e0f8dcb3 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -18,6 +18,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -31,18 +32,25 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + messageTheme: streamTheme.ownMessageTheme, + showThreadReplyMessage: true, + showEditMessage: true, + showDeleteMessage: true, ), - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), - ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -64,6 +72,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -77,22 +86,23 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - showEditMessage: false, - showCopyMessage: false, - showDeleteMessage: false, - showReplyMessage: false, - showThreadReplyMessage: false, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + showCopyMessage: false, + showReplyMessage: false, + showThreadReplyMessage: false, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + ), + messageTheme: streamTheme.ownMessageTheme, + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), ), - ), - messageTheme: streamTheme.ownMessageTheme, - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), ), ), ), @@ -115,6 +125,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -130,24 +141,27 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + messageWidget: const Text('test'), + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), ), + messageTheme: streamTheme.ownMessageTheme, + customActions: [ + StreamMessageAction( + leading: const Icon(Icons.check), + title: const Text('title'), + onTap: (m) { + tapped = true; + }, + ), + ], ), - messageTheme: streamTheme.ownMessageTheme, - customActions: [ - MessageAction( - leading: const Icon(Icons.check), - title: const Text('title'), - onTap: (m) { - tapped = true; - }, - ), - ], ), ), ), @@ -170,6 +184,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -186,18 +201,22 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - onReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + messageWidget: const Text('test'), + onReplyTap: (m) { + tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), + messageTheme: streamTheme.ownMessageTheme, ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -216,6 +235,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); @@ -232,18 +252,23 @@ void main() { streamChatThemeData: streamTheme, client: client, child: SizedBox( - child: MessageActionsModal( - messageWidget: const Text('test'), - onThreadReplyTap: (m) { - tapped = true; - }, - message: Message( - text: 'test', - user: User( - id: 'user-id', + child: StreamChannel( + channel: channel, + child: StreamMessageActionsModal( + messageWidget: const Text('test'), + onThreadReplyTap: (m) { + tapped = true; + }, + message: Message( + text: 'test', + user: User( + id: 'user-id', + ), + status: MessageSendingStatus.sent, ), + messageTheme: streamTheme.ownMessageTheme, + showThreadReplyMessage: true, ), - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -284,7 +309,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( text: 'test', @@ -293,6 +318,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showEditMessage: true, ), ), ), @@ -304,7 +330,7 @@ void main() { await tester.pumpAndSettle(); - expect(find.byType(MessageInput), findsOneWidget); + expect(find.byType(StreamMessageInput), findsOneWidget); }, ); @@ -333,7 +359,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), editMessageInputBuilder: (context, m) => const Text('test'), message: Message( @@ -343,6 +369,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showEditMessage: true, ), ), ), @@ -385,7 +412,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), onCopyTap: (m) => tapped = true, message: Message( @@ -435,7 +462,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed, @@ -485,7 +512,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( status: MessageSendingStatus.failed_update, @@ -533,7 +560,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -543,6 +570,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -589,7 +617,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -599,6 +627,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -645,7 +674,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -655,6 +684,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showFlagButton: true, ), ), ), @@ -699,7 +729,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -709,6 +739,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showDeleteMessage: true, ), ), ), @@ -755,7 +786,7 @@ void main() { showLoading: false, channel: channel, child: SizedBox( - child: MessageActionsModal( + child: StreamMessageActionsModal( messageWidget: const Text('test'), message: Message( id: 'testid', @@ -765,6 +796,7 @@ void main() { ), ), messageTheme: streamTheme.ownMessageTheme, + showDeleteMessage: true, ), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_input_test.dart b/packages/stream_chat_flutter/test/src/message_input_test.dart index 62fe0107..cdedc818 100644 --- a/packages/stream_chat_flutter/test/src/message_input_test.dart +++ b/packages/stream_chat_flutter/test/src/message_input_test.dart @@ -59,7 +59,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: MessageInput(), + body: StreamMessageInput(), ), ), ), @@ -125,7 +125,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: MessageInput(), + body: StreamMessageInput(), ), ), ), diff --git a/packages/stream_chat_flutter/test/src/message_list_view_test.dart b/packages/stream_chat_flutter/test/src/message_list_view_test.dart index b82d53b3..08e23820 100644 --- a/packages/stream_chat_flutter/test/src/message_list_view_test.dart +++ b/packages/stream_chat_flutter/test/src/message_list_view_test.dart @@ -54,7 +54,7 @@ void main() { client: client, child: StreamChannel( channel: channel, - child: MessageListView( + child: StreamMessageListView( emptyBuilder: (_) => Container(key: emptyWidgetKey), ), ), @@ -63,7 +63,7 @@ void main() { ); await tester.pumpAndSettle(); - expect(find.byType(MessageListView), findsOneWidget); + expect(find.byType(StreamMessageListView), findsOneWidget); expect(find.byKey(emptyWidgetKey), findsOneWidget); }); @@ -92,7 +92,7 @@ void main() { child: StreamChat( client: client, streamChatThemeData: StreamChatThemeData.light().copyWith( - messageListViewTheme: const MessageListViewThemeData( + messageListViewTheme: const StreamMessageListViewThemeData( backgroundColor: Colors.grey, backgroundImage: DecorationImage( image: AssetImage('images/placeholder.png'), @@ -102,7 +102,7 @@ void main() { ), child: StreamChannel( channel: channel, - child: const MessageListView( + child: const StreamMessageListView( key: nonEmptyWidgetKey, ), ), @@ -118,7 +118,7 @@ void main() { widget.decoration is BoxDecoration && (widget.decoration! as BoxDecoration).image != null; - expect(find.byType(MessageListView), findsOneWidget); + expect(find.byType(StreamMessageListView), findsOneWidget); expect(find.byKey(nonEmptyWidgetKey), findsOneWidget); expect( find.byWidgetPredicate( diff --git a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart index 3732ca0d..31812bf2 100644 --- a/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_reactions_modal_test.dart @@ -13,6 +13,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); @@ -33,13 +34,16 @@ void main() { home: StreamChat( client: client, streamChatThemeData: streamTheme, - child: MessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), + child: StreamChannel( + channel: channel, + child: StreamMessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + message: message, + messageTheme: streamTheme.ownMessageTheme, ), - message: message, - messageTheme: streamTheme.ownMessageTheme, ), ), ), @@ -47,9 +51,9 @@ void main() { await tester.pump(const Duration(milliseconds: 1000)); - expect(find.byType(ReactionBubble), findsNothing); + expect(find.byType(StreamReactionBubble), findsNothing); - expect(find.byType(UserAvatar), findsNothing); + expect(find.byType(StreamUserAvatar), findsNothing); }, ); @@ -58,6 +62,7 @@ void main() { (WidgetTester tester) async { final client = MockClient(); final clientState = MockClientState(); + final channel = MockChannel(); final themeData = ThemeData(); when(() => client.state).thenReturn(clientState); @@ -89,16 +94,19 @@ void main() { home: StreamChat( client: client, streamChatThemeData: streamTheme, - child: MessageReactionsModal( - messageWidget: const Text( - 'test', - key: Key('MessageWidget'), + child: StreamChannel( + channel: channel, + child: StreamMessageReactionsModal( + messageWidget: const Text( + 'test', + key: Key('MessageWidget'), + ), + message: message, + messageTheme: streamTheme.ownMessageTheme, + reverse: true, + showReactions: false, + onUserAvatarTap: onUserAvatarTap, ), - message: message, - messageTheme: streamTheme.ownMessageTheme, - reverse: true, - showReactions: false, - onUserAvatarTap: onUserAvatarTap, ), ), ), @@ -108,8 +116,8 @@ void main() { expect(find.byKey(const Key('MessageWidget')), findsOneWidget); - expect(find.byType(ReactionBubble), findsOneWidget); - expect(find.byType(UserAvatar), findsOneWidget); + expect(find.byType(StreamReactionBubble), findsOneWidget); + expect(find.byType(StreamUserAvatar), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/message_text_test.dart b/packages/stream_chat_flutter/test/src/message_text_test.dart index 57f0a282..8f2d4c10 100644 --- a/packages/stream_chat_flutter/test/src/message_text_test.dart +++ b/packages/stream_chat_flutter/test/src/message_text_test.dart @@ -65,7 +65,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: Message( text: 'demo', ), @@ -84,7 +84,7 @@ void main() { final clientState = MockClientState(); final channel = MockChannel(); final channelState = MockChannelState(); - const messageTheme = MessageThemeData(); + const messageTheme = StreamMessageThemeData(); final currentUser = OwnUser( id: 'sahil', @@ -122,7 +122,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: message, messageTheme: messageTheme, ), @@ -158,7 +158,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: message, messageTheme: messageTheme, ), @@ -221,7 +221,7 @@ cool.'''; child: StreamChannel( channel: channel, child: Scaffold( - body: MessageText( + body: StreamMessageText( message: Message( text: messageText, ), diff --git a/packages/stream_chat_flutter/test/src/mocks.dart b/packages/stream_chat_flutter/test/src/mocks.dart index 443f770c..1fa1e99b 100644 --- a/packages/stream_chat_flutter/test/src/mocks.dart +++ b/packages/stream_chat_flutter/test/src/mocks.dart @@ -21,6 +21,9 @@ class MockChannel extends Mock implements Channel { Future keyStroke([String? parentId]) async { return; } + + @override + List get ownCapabilities => ['send-message']; } class MockChannelState extends Mock implements ChannelClientState { diff --git a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart index 6e823b43..50719b17 100644 --- a/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart +++ b/packages/stream_chat_flutter/test/src/reaction_bubble_test.dart @@ -25,7 +25,7 @@ void main() { streamChatThemeData: theme, connectivityStream: Stream.value(ConnectivityResult.mobile), child: SizedBox( - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -62,7 +62,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -99,7 +99,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -144,7 +144,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), child: Container( color: Colors.black, - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', @@ -187,7 +187,7 @@ void main() { connectivityStream: Stream.value(ConnectivityResult.mobile), streamChatThemeData: StreamChatThemeData.fromTheme(themeData), child: SizedBox( - child: ReactionBubble( + child: StreamReactionBubble( reactions: [ Reaction( type: 'like', diff --git a/packages/stream_chat_flutter/test/src/system_message_test.dart b/packages/stream_chat_flutter/test/src/system_message_test.dart index 24eccdf6..c723ff24 100644 --- a/packages/stream_chat_flutter/test/src/system_message_test.dart +++ b/packages/stream_chat_flutter/test/src/system_message_test.dart @@ -40,7 +40,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: SystemMessage( + body: StreamSystemMessage( onMessageTap: (m) => tapped = true, message: Message( text: 'demo message', @@ -51,7 +51,7 @@ void main() { ), )); - await tester.tap(find.byType(SystemMessage)); + await tester.tap(find.byType(StreamSystemMessage)); expect(find.text('demo message'), findsOneWidget); expect(tapped, true); @@ -94,7 +94,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: SystemMessage( + child: StreamSystemMessage( message: Message( text: 'demo message', ), @@ -146,7 +146,7 @@ void main() { showLoading: false, channel: channel, child: Center( - child: SystemMessage( + child: StreamSystemMessage( message: Message( text: 'demo message', ), diff --git a/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart index 63e6da86..8d055d52 100644 --- a/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/avatar_theme_test.dart @@ -4,22 +4,23 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('AvatarThemeData copyWith, ==, hashCode basics', () { - expect(const AvatarThemeData(), const AvatarThemeData().copyWith()); - expect(const AvatarThemeData().hashCode, - const AvatarThemeData().copyWith().hashCode); + expect(const StreamAvatarThemeData(), + const StreamAvatarThemeData().copyWith()); + expect(const StreamAvatarThemeData().hashCode, + const StreamAvatarThemeData().copyWith().hashCode); }); group('AvatarThemeData lerps correctly', () { test('Lerp completely', () { expect( - const AvatarThemeData() + const StreamAvatarThemeData() .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 1), _avatarThemeDataControl2); }); test('Lerp halfway', () { expect( - const AvatarThemeData() + const StreamAvatarThemeData() .lerp(_avatarThemeDataControl1, _avatarThemeDataControl2, 0.5), _avatarThemeDataControlMidLerp); }); @@ -31,9 +32,9 @@ void main() { }); } -const _avatarThemeDataControl1 = AvatarThemeData(); +const _avatarThemeDataControl1 = StreamAvatarThemeData(); -final _avatarThemeDataControlMidLerp = AvatarThemeData( +final _avatarThemeDataControlMidLerp = StreamAvatarThemeData( borderRadius: BorderRadius.circular(16), constraints: const BoxConstraints.tightFor( height: 33, @@ -41,7 +42,7 @@ final _avatarThemeDataControlMidLerp = AvatarThemeData( ), ); -final _avatarThemeDataControl2 = AvatarThemeData( +final _avatarThemeDataControl2 = StreamAvatarThemeData( borderRadius: BorderRadius.circular(12), constraints: const BoxConstraints.tightFor( height: 34, diff --git a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart index 475b1084..1b5499dd 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_header_theme_test.dart @@ -4,10 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelHeaderThemeData(), - const ChannelHeaderThemeData().copyWith()); - expect(const ChannelHeaderThemeData().hashCode, - const ChannelHeaderThemeData().copyWith().hashCode); + expect(const StreamChannelHeaderThemeData(), + const StreamChannelHeaderThemeData().copyWith()); + expect(const StreamChannelHeaderThemeData().hashCode, + const StreamChannelHeaderThemeData().copyWith().hashCode); }); group('ChannelHeaderThemeData lerps', () { @@ -15,7 +15,7 @@ void main() { '''Light ChannelHeaderThemeData lerps completely to dark ChannelHeaderThemeData''', () { expect( - const ChannelHeaderThemeData() + const StreamChannelHeaderThemeData() .lerp(_channelThemeControl, _channelThemeControlDark, 1), _channelThemeControlDark); }); @@ -24,7 +24,7 @@ void main() { '''Light ChannelHeaderThemeData lerps halfway to dark ChannelHeaderThemeData''', () { expect( - const ChannelHeaderThemeData() + const StreamChannelHeaderThemeData() .lerp(_channelThemeControl, _channelThemeControlDark, 0.5), _channelThemeControlMidLerp); }); @@ -33,7 +33,7 @@ void main() { '''Dark ChannelHeaderThemeData lerps completely to light ChannelHeaderThemeData''', () { expect( - const ChannelHeaderThemeData() + const StreamChannelHeaderThemeData() .lerp(_channelThemeControlDark, _channelThemeControl, 1), _channelThemeControl); }); @@ -45,8 +45,8 @@ void main() { }); } -final _channelThemeControl = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelThemeControl = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -54,16 +54,16 @@ final _channelThemeControl = ChannelHeaderThemeData( ), ), color: const Color(0xff101418), - titleStyle: TextTheme.light().headlineBold.copyWith( + titleStyle: StreamTextTheme.light().headlineBold.copyWith( color: const Color(0xffffffff), ), - subtitleStyle: TextTheme.light().footnote.copyWith( + subtitleStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7a7a7a), ), ); -final _channelThemeControlMidLerp = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelThemeControlMidLerp = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -76,22 +76,22 @@ final _channelThemeControlMidLerp = ChannelHeaderThemeData( fontWeight: FontWeight.bold, fontSize: 16, ), - subtitleStyle: TextTheme.light().footnote.copyWith( + subtitleStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7a7a7a), ), ); -final _channelThemeControlDark = ChannelHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelThemeControlDark = StreamChannelHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - color: ColorTheme.dark().barsBg, - titleStyle: TextTheme.dark().headlineBold, - subtitleStyle: TextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().barsBg, + titleStyle: StreamTextTheme.dark().headlineBold, + subtitleStyle: StreamTextTheme.dark().footnote.copyWith( color: const Color(0xff7A7A7A), ), ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart index aa378f3a..14c09706 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_header_theme_test.dart @@ -4,10 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelListHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelListHeaderThemeData(), - const ChannelListHeaderThemeData().copyWith()); - expect(const ChannelListHeaderThemeData().hashCode, - const ChannelListHeaderThemeData().copyWith().hashCode); + expect(const StreamChannelListHeaderThemeData(), + const StreamChannelListHeaderThemeData().copyWith()); + expect(const StreamChannelListHeaderThemeData().hashCode, + const StreamChannelListHeaderThemeData().copyWith().hashCode); }); group('ChannelListHeaderThemeData lerps', () { @@ -15,7 +15,7 @@ void main() { '''Light ChannelListHeaderThemeData lerps completely to dark ChannelListHeaderThemeData''', () { expect( - const ChannelListHeaderThemeData().lerp( + const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControl, _channelListHeaderThemeControlDark, 1), @@ -26,7 +26,7 @@ void main() { '''Light ChannelListHeaderThemeData lerps halfway to dark ChannelListHeaderThemeData''', () { expect( - const ChannelListHeaderThemeData().lerp( + const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControl, _channelListHeaderThemeControlDark, 0.5), @@ -37,7 +37,7 @@ void main() { '''Dark ChannelListHeaderThemeData lerps completely to light ChannelListHeaderThemeData''', () { expect( - const ChannelListHeaderThemeData().lerp( + const StreamChannelListHeaderThemeData().lerp( _channelListHeaderThemeControlDark, _channelListHeaderThemeControl, 1), @@ -53,20 +53,20 @@ void main() { }); } -final _channelListHeaderThemeControl = ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelListHeaderThemeControl = StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - color: ColorTheme.light().barsBg, - titleStyle: TextTheme.light().headlineBold, + color: StreamColorTheme.light().barsBg, + titleStyle: StreamTextTheme.light().headlineBold, ); -final _channelListHeaderThemeControlMidLerp = ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelListHeaderThemeControlMidLerp = StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -81,14 +81,14 @@ final _channelListHeaderThemeControlMidLerp = ChannelListHeaderThemeData( ), ); -final _channelListHeaderThemeControlDark = ChannelListHeaderThemeData( - avatarTheme: AvatarThemeData( +final _channelListHeaderThemeControlDark = StreamChannelListHeaderThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - color: ColorTheme.dark().barsBg, - titleStyle: TextTheme.dark().headlineBold, + color: StreamColorTheme.dark().barsBg, + titleStyle: StreamTextTheme.dark().headlineBold, ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart index 079c01e3..37de3140 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart @@ -6,16 +6,18 @@ import '../mocks.dart'; void main() { test('ChannelListViewThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelListViewThemeData(), - const ChannelListViewThemeData().copyWith()); + expect(const StreamChannelListViewThemeData(), + const StreamChannelListViewThemeData().copyWith()); }); test( '''Light ChannelListViewThemeData lerps completely to dark ChannelListViewThemeData''', () { expect( - const ChannelListViewThemeData().lerp(_channelListViewThemeDataControl, - _channelListViewThemeDataControlDark, 1), + const StreamChannelListViewThemeData().lerp( + _channelListViewThemeDataControl, + _channelListViewThemeDataControlDark, + 1), _channelListViewThemeDataControlDark); }); @@ -23,8 +25,10 @@ void main() { '''Light ChannelListViewThemeData lerps halfway to dark ChannelListViewThemeData''', () { expect( - const ChannelListViewThemeData().lerp(_channelListViewThemeDataControl, - _channelListViewThemeDataControlDark, 0.5), + const StreamChannelListViewThemeData().lerp( + _channelListViewThemeDataControl, + _channelListViewThemeDataControlDark, + 0.5), _channelListViewThemeDataControlHalfLerp); }); @@ -32,7 +36,7 @@ void main() { '''Dark ChannelListViewThemeData lerps completely to light ChannelListViewThemeData''', () { expect( - const ChannelListViewThemeData().lerp( + const StreamChannelListViewThemeData().lerp( _channelListViewThemeDataControlDark, _channelListViewThemeDataControl, 1), @@ -70,7 +74,7 @@ void main() { ), ); - final channelListViewTheme = ChannelListViewTheme.of(_context); + final channelListViewTheme = StreamChannelListViewTheme.of(_context); expect(channelListViewTheme.backgroundColor, _channelListViewThemeDataControl.backgroundColor); }); @@ -92,7 +96,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -100,20 +104,20 @@ void main() { ), ); - final channelListViewTheme = ChannelListViewTheme.of(_context); + final channelListViewTheme = StreamChannelListViewTheme.of(_context); expect(channelListViewTheme.backgroundColor, _channelListViewThemeDataControlDark.backgroundColor); }); } -final _channelListViewThemeDataControl = ChannelListViewThemeData( - backgroundColor: ColorTheme.light().appBg, +final _channelListViewThemeDataControl = StreamChannelListViewThemeData( + backgroundColor: StreamColorTheme.light().appBg, ); -const _channelListViewThemeDataControlHalfLerp = ChannelListViewThemeData( +const _channelListViewThemeDataControlHalfLerp = StreamChannelListViewThemeData( backgroundColor: Color(0xff818384), ); -final _channelListViewThemeDataControlDark = ChannelListViewThemeData( - backgroundColor: ColorTheme.dark().appBg, +final _channelListViewThemeDataControlDark = StreamChannelListViewThemeData( + backgroundColor: StreamColorTheme.dark().appBg, ); diff --git a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart index dc5c0529..782be835 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_preview_theme_test.dart @@ -4,10 +4,10 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('ChannelPreviewThemeData copyWith, ==, hashCode basics', () { - expect(const ChannelPreviewThemeData(), - const ChannelPreviewThemeData().copyWith()); - expect(const ChannelPreviewThemeData().hashCode, - const ChannelPreviewThemeData().copyWith().hashCode); + expect(const StreamChannelPreviewThemeData(), + const StreamChannelPreviewThemeData().copyWith()); + expect(const StreamChannelPreviewThemeData().hashCode, + const StreamChannelPreviewThemeData().copyWith().hashCode); }); group('ChannelPreviewThemeData lerps', () { @@ -15,7 +15,7 @@ void main() { '''Light ChannelPreviewThemeData lerps completely to dark ChannelPreviewThemeData''', () { expect( - const ChannelPreviewThemeData().lerp( + const StreamChannelPreviewThemeData().lerp( _channelPreviewThemeControl, _channelPreviewThemeControlDark, 1), _channelPreviewThemeControlDark); }); @@ -24,8 +24,10 @@ void main() { '''Light ChannelPreviewThemeData lerps halfway to dark ChannelPreviewThemeData''', () { expect( - const ChannelPreviewThemeData().lerp(_channelPreviewThemeControl, - _channelPreviewThemeControlDark, 0.5), + const StreamChannelPreviewThemeData().lerp( + _channelPreviewThemeControl, + _channelPreviewThemeControlDark, + 0.5), _channelPreviewThemeControlMidLerp); }); @@ -33,7 +35,7 @@ void main() { '''Dark ChannelPreviewThemeData lerps completely to light ChannelPreviewThemeData''', () { expect( - const ChannelPreviewThemeData().lerp( + const StreamChannelPreviewThemeData().lerp( _channelPreviewThemeControlDark, _channelPreviewThemeControl, 1), _channelPreviewThemeControl); }); @@ -45,28 +47,28 @@ void main() { }); } -final _channelPreviewThemeControl = ChannelPreviewThemeData( - unreadCounterColor: ColorTheme.light().accentError, - avatarTheme: AvatarThemeData( +final _channelPreviewThemeControl = StreamChannelPreviewThemeData( + unreadCounterColor: StreamColorTheme.light().accentError, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - titleStyle: TextTheme.light().bodyBold, - subtitleStyle: TextTheme.light().footnote.copyWith( + titleStyle: StreamTextTheme.light().bodyBold, + subtitleStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAtStyle: TextTheme.light().footnote.copyWith( - color: ColorTheme.light().textHighEmphasis.withOpacity(0.5), + lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( + color: StreamColorTheme.light().textHighEmphasis.withOpacity(0.5), ), indicatorIconSize: 16, ); -final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( +final _channelPreviewThemeControlMidLerp = StreamChannelPreviewThemeData( unreadCounterColor: const Color(0xffff3742), - avatarTheme: AvatarThemeData( + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, @@ -82,27 +84,27 @@ final _channelPreviewThemeControlMidLerp = ChannelPreviewThemeData( color: Color(0xff7a7a7a), fontSize: 12, ), - lastMessageAtStyle: TextTheme.light().footnote.copyWith( + lastMessageAtStyle: StreamTextTheme.light().footnote.copyWith( color: const Color(0x807f7f7f).withOpacity(0.5), ), indicatorIconSize: 16, ); -final _channelPreviewThemeControlDark = ChannelPreviewThemeData( - unreadCounterColor: ColorTheme.dark().accentError, - avatarTheme: AvatarThemeData( +final _channelPreviewThemeControlDark = StreamChannelPreviewThemeData( + unreadCounterColor: StreamColorTheme.dark().accentError, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 40, width: 40, ), ), - titleStyle: TextTheme.dark().bodyBold, - subtitleStyle: TextTheme.dark().footnote.copyWith( + titleStyle: StreamTextTheme.dark().bodyBold, + subtitleStyle: StreamTextTheme.dark().footnote.copyWith( color: const Color(0xff7A7A7A), ), - lastMessageAtStyle: TextTheme.dark().footnote.copyWith( - color: ColorTheme.dark().textHighEmphasis.withOpacity(0.5), + lastMessageAtStyle: StreamTextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().textHighEmphasis.withOpacity(0.5), ), indicatorIconSize: 16, ); diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart index 6a73e34e..0193cb35 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart @@ -7,18 +7,20 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('GalleryFooterThemeData copyWith, ==, hashCode basics', () { - expect(const GalleryFooterThemeData(), - const GalleryFooterThemeData().copyWith()); - expect(const GalleryFooterThemeData().hashCode, - const GalleryFooterThemeData().copyWith().hashCode); + expect(const StreamGalleryFooterThemeData(), + const StreamGalleryFooterThemeData().copyWith()); + expect(const StreamGalleryFooterThemeData().hashCode, + const StreamGalleryFooterThemeData().copyWith().hashCode); }); test( '''Light GalleryFooterThemeData lerps completely to dark GalleryFooterThemeData''', () { expect( - const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, - _galleryFooterThemeDataControlDark, 1), + const StreamGalleryFooterThemeData().lerp( + _galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, + 1), _galleryFooterThemeDataControlDark); }); @@ -26,8 +28,10 @@ void main() { '''Light GalleryFooterThemeData lerps halfway to dark GalleryFooterThemeData''', () { expect( - const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControl, - _galleryFooterThemeDataControlDark, 0.5), + const StreamGalleryFooterThemeData().lerp( + _galleryFooterThemeDataControl, + _galleryFooterThemeDataControlDark, + 0.5), _galleryFooterThemeDataControlMidLerp); }); @@ -35,8 +39,10 @@ void main() { '''Dark GalleryFooterThemeData lerps completely to light GalleryFooterThemeData''', () { expect( - const GalleryFooterThemeData().lerp(_galleryFooterThemeDataControlDark, - _galleryFooterThemeDataControl, 1), + const StreamGalleryFooterThemeData().lerp( + _galleryFooterThemeDataControlDark, + _galleryFooterThemeDataControl, + 1), _galleryFooterThemeDataControl); }); @@ -68,7 +74,7 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryFooter( + appBar: StreamGalleryFooter( message: Message(), ), ); @@ -77,7 +83,7 @@ void main() { ), ); - final imageFooterTheme = GalleryFooterTheme.of(_context); + final imageFooterTheme = StreamGalleryFooterTheme.of(_context); expect(imageFooterTheme.backgroundColor, _galleryFooterThemeDataControl.backgroundColor); expect(imageFooterTheme.shareIconColor, @@ -111,7 +117,7 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryFooter( + appBar: StreamGalleryFooter( message: Message(), ), ); @@ -120,7 +126,7 @@ void main() { ), ); - final imageFooterTheme = GalleryFooterTheme.of(_context); + final imageFooterTheme = StreamGalleryFooterTheme.of(_context); expect(imageFooterTheme.backgroundColor, _galleryFooterThemeDataControlDark.backgroundColor); expect(imageFooterTheme.shareIconColor, @@ -141,19 +147,19 @@ void main() { } // Light theme control -final _galleryFooterThemeDataControl = GalleryFooterThemeData( - backgroundColor: ColorTheme.light().barsBg, - shareIconColor: ColorTheme.light().textHighEmphasis, - titleTextStyle: TextTheme.light().headlineBold, - gridIconButtonColor: ColorTheme.light().textHighEmphasis, - bottomSheetBackgroundColor: ColorTheme.light().barsBg, - bottomSheetBarrierColor: ColorTheme.light().overlay, - bottomSheetCloseIconColor: ColorTheme.light().textHighEmphasis, - bottomSheetPhotosTextStyle: TextTheme.light().headlineBold, +final _galleryFooterThemeDataControl = StreamGalleryFooterThemeData( + backgroundColor: StreamColorTheme.light().barsBg, + shareIconColor: StreamColorTheme.light().textHighEmphasis, + titleTextStyle: StreamTextTheme.light().headlineBold, + gridIconButtonColor: StreamColorTheme.light().textHighEmphasis, + bottomSheetBackgroundColor: StreamColorTheme.light().barsBg, + bottomSheetBarrierColor: StreamColorTheme.light().overlay, + bottomSheetCloseIconColor: StreamColorTheme.light().textHighEmphasis, + bottomSheetPhotosTextStyle: StreamTextTheme.light().headlineBold, ); // Mid-lerp theme control -const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData( +const _galleryFooterThemeDataControlMidLerp = StreamGalleryFooterThemeData( backgroundColor: Color(0xff87898b), shareIconColor: Color(0xff7f7f7f), titleTextStyle: TextStyle( @@ -173,13 +179,13 @@ const _galleryFooterThemeDataControlMidLerp = GalleryFooterThemeData( ); // Dark theme control -final _galleryFooterThemeDataControlDark = GalleryFooterThemeData( - backgroundColor: ColorTheme.dark().barsBg, - shareIconColor: ColorTheme.dark().textHighEmphasis, - titleTextStyle: TextTheme.dark().headlineBold, - gridIconButtonColor: ColorTheme.dark().textHighEmphasis, - bottomSheetBackgroundColor: ColorTheme.dark().barsBg, - bottomSheetBarrierColor: ColorTheme.dark().overlay, - bottomSheetCloseIconColor: ColorTheme.dark().textHighEmphasis, - bottomSheetPhotosTextStyle: TextTheme.dark().headlineBold, +final _galleryFooterThemeDataControlDark = StreamGalleryFooterThemeData( + backgroundColor: StreamColorTheme.dark().barsBg, + shareIconColor: StreamColorTheme.dark().textHighEmphasis, + titleTextStyle: StreamTextTheme.dark().headlineBold, + gridIconButtonColor: StreamColorTheme.dark().textHighEmphasis, + bottomSheetBackgroundColor: StreamColorTheme.dark().barsBg, + bottomSheetBarrierColor: StreamColorTheme.dark().overlay, + bottomSheetCloseIconColor: StreamColorTheme.dark().textHighEmphasis, + bottomSheetPhotosTextStyle: StreamTextTheme.dark().headlineBold, ); diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart index 534008ba..0ded6bbf 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart @@ -7,18 +7,20 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('GalleryHeaderThemeData copyWith, ==, hashCode basics', () { - expect(const GalleryHeaderThemeData(), - const GalleryHeaderThemeData().copyWith()); - expect(const GalleryHeaderThemeData().hashCode, - const GalleryHeaderThemeData().copyWith().hashCode); + expect(const StreamGalleryHeaderThemeData(), + const StreamGalleryHeaderThemeData().copyWith()); + expect(const StreamGalleryHeaderThemeData().hashCode, + const StreamGalleryHeaderThemeData().copyWith().hashCode); }); test( '''Light GalleryHeaderThemeData lerps completely to dark GalleryHeaderThemeData''', () { expect( - const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, - _galleryHeaderThemeDataDarkControl, 1), + const StreamGalleryHeaderThemeData().lerp( + _galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, + 1), _galleryHeaderThemeDataDarkControl); }); @@ -26,8 +28,10 @@ void main() { '''Light GalleryHeaderThemeData lerps halfway to dark GalleryHeaderThemeData''', () { expect( - const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataControl, - _galleryHeaderThemeDataDarkControl, 0.5), + const StreamGalleryHeaderThemeData().lerp( + _galleryHeaderThemeDataControl, + _galleryHeaderThemeDataDarkControl, + 0.5), _galleryHeaderThemeDataHalfLerpControl); }); @@ -35,8 +39,10 @@ void main() { '''Dark GalleryHeaderThemeData lerps completely to light GalleryHeaderThemeData''', () { expect( - const GalleryHeaderThemeData().lerp(_galleryHeaderThemeDataDarkControl, - _galleryHeaderThemeDataControl, 1), + const StreamGalleryHeaderThemeData().lerp( + _galleryHeaderThemeDataDarkControl, + _galleryHeaderThemeDataControl, + 1), _galleryHeaderThemeDataControl); }); @@ -61,7 +67,7 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryHeader( + appBar: StreamGalleryHeader( message: Message(), ), ); @@ -70,7 +76,7 @@ void main() { ), ); - final imageHeaderTheme = GalleryHeaderTheme.of(_context); + final imageHeaderTheme = StreamGalleryHeaderTheme.of(_context); expect(imageHeaderTheme.closeButtonColor, _galleryHeaderThemeDataControl.closeButtonColor); expect(imageHeaderTheme.backgroundColor, @@ -100,7 +106,7 @@ void main() { builder: (context) { _context = context; return Scaffold( - appBar: GalleryHeader( + appBar: StreamGalleryHeader( message: Message(), ), ); @@ -109,7 +115,7 @@ void main() { ), ); - final imageHeaderTheme = GalleryHeaderTheme.of(_context); + final imageHeaderTheme = StreamGalleryHeaderTheme.of(_context); expect(imageHeaderTheme.closeButtonColor, _galleryHeaderThemeDataDarkControl.closeButtonColor); expect(imageHeaderTheme.backgroundColor, @@ -126,7 +132,7 @@ void main() { } // Light theme test control. -final _galleryHeaderThemeDataControl = GalleryHeaderThemeData( +final _galleryHeaderThemeDataControl = StreamGalleryHeaderThemeData( closeButtonColor: const Color(0xff000000), backgroundColor: const Color(0xffffffff), iconMenuPointColor: const Color(0xff000000), @@ -145,7 +151,7 @@ final _galleryHeaderThemeDataControl = GalleryHeaderThemeData( ); // Light theme test control. -final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData( +final _galleryHeaderThemeDataHalfLerpControl = StreamGalleryHeaderThemeData( closeButtonColor: const Color(0xff7f7f7f), backgroundColor: const Color(0xff87898b), iconMenuPointColor: const Color(0xff7f7f7f), @@ -164,7 +170,7 @@ final _galleryHeaderThemeDataHalfLerpControl = GalleryHeaderThemeData( ); // Dark theme test control. -final _galleryHeaderThemeDataDarkControl = GalleryHeaderThemeData( +final _galleryHeaderThemeDataDarkControl = StreamGalleryHeaderThemeData( closeButtonColor: const Color(0xffffffff), backgroundColor: const Color(0xff101418), iconMenuPointColor: const Color(0xffffffff), diff --git a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart index c618d65c..3c62baea 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_input_theme_test.dart @@ -4,30 +4,30 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('MessageInputThemeData copyWith, ==, hashCode basics', () { - expect(const MessageInputThemeData(), - const MessageInputThemeData().copyWith()); - expect(const MessageInputThemeData().hashCode, - const MessageInputThemeData().copyWith().hashCode); + expect(const StreamMessageInputThemeData(), + const StreamMessageInputThemeData().copyWith()); + expect(const StreamMessageInputThemeData().hashCode, + const StreamMessageInputThemeData().copyWith().hashCode); }); group('MessageInputThemeData lerps correctly', () { test('Lerp completely from light to dark', () { expect( - const MessageInputThemeData().lerp( + const StreamMessageInputThemeData().lerp( _messageInputThemeControl, _messageInputThemeControlDark, 1), _messageInputThemeControlDark); }); test('Lerp halfway from light to dark', () { expect( - const MessageInputThemeData().lerp( + const StreamMessageInputThemeData().lerp( _messageInputThemeControl, _messageInputThemeControlDark, 0.5), _messageInputThemeControlMidLerp); }); test('Lerp completely from dark to light', () { expect( - const MessageInputThemeData().lerp( + const StreamMessageInputThemeData().lerp( _messageInputThemeControlDark, _messageInputThemeControl, 1), _messageInputThemeControl); }); @@ -39,33 +39,33 @@ void main() { }); } -final _messageInputThemeControl = MessageInputThemeData( +final _messageInputThemeControl = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: ColorTheme.light().accentPrimary, - actionButtonIdleColor: ColorTheme.light().textLowEmphasis, - expandButtonColor: ColorTheme.light().accentPrimary, - sendButtonColor: ColorTheme.light().accentPrimary, - sendButtonIdleColor: ColorTheme.light().disabled, - inputBackgroundColor: ColorTheme.light().barsBg, - inputTextStyle: TextTheme.light().body, + actionButtonColor: StreamColorTheme.light().accentPrimary, + actionButtonIdleColor: StreamColorTheme.light().textLowEmphasis, + expandButtonColor: StreamColorTheme.light().accentPrimary, + sendButtonColor: StreamColorTheme.light().accentPrimary, + sendButtonIdleColor: StreamColorTheme.light().disabled, + inputBackgroundColor: StreamColorTheme.light().barsBg, + inputTextStyle: StreamTextTheme.light().body, idleBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.light().disabled, - ColorTheme.light().disabled, + StreamColorTheme.light().disabled, + StreamColorTheme.light().disabled, ], ), activeBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.light().disabled, - ColorTheme.light().disabled, + StreamColorTheme.light().disabled, + StreamColorTheme.light().disabled, ], ), ); -final _messageInputThemeControlMidLerp = MessageInputThemeData( +final _messageInputThemeControlMidLerp = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), inputBackgroundColor: const Color(0xff87898b), @@ -95,28 +95,28 @@ final _messageInputThemeControlMidLerp = MessageInputThemeData( ), ); -final _messageInputThemeControlDark = MessageInputThemeData( +final _messageInputThemeControlDark = StreamMessageInputThemeData( borderRadius: BorderRadius.circular(20), sendAnimationDuration: const Duration(milliseconds: 300), - actionButtonColor: ColorTheme.dark().accentPrimary, - actionButtonIdleColor: ColorTheme.dark().textLowEmphasis, - expandButtonColor: ColorTheme.dark().accentPrimary, - sendButtonColor: ColorTheme.dark().accentPrimary, - sendButtonIdleColor: ColorTheme.dark().disabled, - inputBackgroundColor: ColorTheme.dark().barsBg, - inputTextStyle: TextTheme.dark().body, + actionButtonColor: StreamColorTheme.dark().accentPrimary, + actionButtonIdleColor: StreamColorTheme.dark().textLowEmphasis, + expandButtonColor: StreamColorTheme.dark().accentPrimary, + sendButtonColor: StreamColorTheme.dark().accentPrimary, + sendButtonIdleColor: StreamColorTheme.dark().disabled, + inputBackgroundColor: StreamColorTheme.dark().barsBg, + inputTextStyle: StreamTextTheme.dark().body, idleBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.dark().disabled, - ColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, ], ), activeBorderGradient: LinearGradient( stops: const [0.0, 1.0], colors: [ - ColorTheme.dark().disabled, - ColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, + StreamColorTheme.dark().disabled, ], ), ); diff --git a/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart index 94becc06..264710ec 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_list_view_theme_test.dart @@ -9,18 +9,20 @@ class MockStreamChatClient extends Mock implements StreamChatClient {} void main() { test('MessageListViewThemeData copyWith, ==, hashCode basics', () { - expect(const MessageListViewThemeData(), - const MessageListViewThemeData().copyWith()); - expect(const MessageListViewThemeData().hashCode, - const MessageListViewThemeData().copyWith().hashCode); + expect(const StreamMessageListViewThemeData(), + const StreamMessageListViewThemeData().copyWith()); + expect(const StreamMessageListViewThemeData().hashCode, + const StreamMessageListViewThemeData().copyWith().hashCode); }); test( '''Light MessageListViewThemeData lerps completely to dark MessageListViewThemeData''', () { expect( - const MessageListViewThemeData().lerp(_messageListViewThemeDataControl, - _messageListViewThemeDataControlDark, 1), + const StreamMessageListViewThemeData().lerp( + _messageListViewThemeDataControl, + _messageListViewThemeDataControlDark, + 1), _messageListViewThemeDataControlDark); }); @@ -28,8 +30,10 @@ void main() { '''Light MessageListViewThemeData lerps halfway to dark MessageListViewThemeData''', () { expect( - const MessageListViewThemeData().lerp(_messageListViewThemeDataControl, - _messageListViewThemeDataControlDark, 0.5), + const StreamMessageListViewThemeData().lerp( + _messageListViewThemeDataControl, + _messageListViewThemeDataControlDark, + 0.5), _messageListViewThemeDataControlHalfLerp); }); @@ -37,7 +41,7 @@ void main() { '''Dark MessageListViewThemeData lerps completely to light MessageListViewThemeData''', () { expect( - const MessageListViewThemeData().lerp( + const StreamMessageListViewThemeData().lerp( _messageListViewThemeDataControlDark, _messageListViewThemeDataControl, 1), @@ -67,7 +71,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -75,7 +79,7 @@ void main() { ), ); - final messageListViewTheme = MessageListViewTheme.of(_context); + final messageListViewTheme = StreamMessageListViewTheme.of(_context); expect(messageListViewTheme.backgroundColor, _messageListViewThemeDataControl.backgroundColor); }); @@ -97,7 +101,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -105,7 +109,7 @@ void main() { ), ); - final messageListViewTheme = MessageListViewTheme.of(_context); + final messageListViewTheme = StreamMessageListViewTheme.of(_context); expect(messageListViewTheme.backgroundColor, _messageListViewThemeDataControlDark.backgroundColor); }); @@ -128,7 +132,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const MessageListView(), + child: const StreamMessageListView(), ), ); }, @@ -136,25 +140,25 @@ void main() { ), ); - final messageListViewTheme = MessageListViewTheme.of(_context); + final messageListViewTheme = StreamMessageListViewTheme.of(_context); expect(messageListViewTheme.backgroundImage, _messageListViewThemeDataImage.backgroundImage); }); } -final _messageListViewThemeDataControl = MessageListViewThemeData( - backgroundColor: ColorTheme.light().barsBg, +final _messageListViewThemeDataControl = StreamMessageListViewThemeData( + backgroundColor: StreamColorTheme.light().barsBg, ); -const _messageListViewThemeDataControlHalfLerp = MessageListViewThemeData( +const _messageListViewThemeDataControlHalfLerp = StreamMessageListViewThemeData( backgroundColor: Color(0xff87898b), ); -final _messageListViewThemeDataControlDark = MessageListViewThemeData( - backgroundColor: ColorTheme.dark().barsBg, +final _messageListViewThemeDataControlDark = StreamMessageListViewThemeData( + backgroundColor: StreamColorTheme.dark().barsBg, ); -const _messageListViewThemeDataImage = MessageListViewThemeData( +const _messageListViewThemeDataImage = StreamMessageListViewThemeData( backgroundImage: DecorationImage( image: AssetImage('example/assets/background_doodle.png'), fit: BoxFit.cover, diff --git a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart index 69c63abe..bbb27463 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart @@ -6,17 +6,17 @@ import '../mocks.dart'; void main() { test('MessageSearchListViewThemeData copyWith, ==, hashCode basics', () { - expect(const MessageSearchListViewThemeData(), - const MessageSearchListViewThemeData().copyWith()); - expect(const MessageSearchListViewThemeData().hashCode, - const MessageSearchListViewThemeData().copyWith().hashCode); + expect(const StreamMessageSearchListViewThemeData(), + const StreamMessageSearchListViewThemeData().copyWith()); + expect(const StreamMessageSearchListViewThemeData().hashCode, + const StreamMessageSearchListViewThemeData().copyWith().hashCode); }); test( '''Light MessageSearchListViewThemeData lerps completely to dark MessageSearchListViewThemeData''', () { expect( - const MessageSearchListViewThemeData().lerp( + const StreamMessageSearchListViewThemeData().lerp( _messageSearchListViewThemeDataControl, _messageSearchListViewThemeDataControlDark, 1), @@ -27,7 +27,7 @@ void main() { '''Light MessageSearchListViewThemeData lerps halfway to dark MessageSearchListViewThemeData''', () { expect( - const MessageSearchListViewThemeData().lerp( + const StreamMessageSearchListViewThemeData().lerp( _messageSearchListViewThemeDataControl, _messageSearchListViewThemeDataControlDark, 0.5), @@ -38,7 +38,7 @@ void main() { '''Dark MessageSearchListViewThemeData lerps completely to light MessageSearchListViewThemeData''', () { expect( - const MessageSearchListViewThemeData().lerp( + const StreamMessageSearchListViewThemeData().lerp( _messageSearchListViewThemeDataControlDark, _messageSearchListViewThemeDataControl, 1), @@ -67,7 +67,7 @@ void main() { _context = context; return Scaffold( body: MessageSearchBloc( - child: MessageSearchListView( + child: StreamMessageSearchListView( filters: Filter.in_('members', const ['test_id']), messageQuery: 'test query', ), @@ -78,7 +78,8 @@ void main() { ), ); - final messageSearchListViewTheme = MessageSearchListViewTheme.of(_context); + final messageSearchListViewTheme = + StreamMessageSearchListViewTheme.of(_context); expect(messageSearchListViewTheme.backgroundColor, _messageSearchListViewThemeDataControl.backgroundColor); }); @@ -99,7 +100,7 @@ void main() { _context = context; return Scaffold( body: MessageSearchBloc( - child: MessageSearchListView( + child: StreamMessageSearchListView( filters: Filter.in_('members', const ['test_id']), messageQuery: 'test query', ), @@ -110,22 +111,24 @@ void main() { ), ); - final messageSearchListViewTheme = MessageSearchListViewTheme.of(_context); + final messageSearchListViewTheme = + StreamMessageSearchListViewTheme.of(_context); expect(messageSearchListViewTheme.backgroundColor, _messageSearchListViewThemeDataControlDark.backgroundColor); }); } -final _messageSearchListViewThemeDataControl = MessageSearchListViewThemeData( - backgroundColor: ColorTheme.light().appBg, +final _messageSearchListViewThemeDataControl = + StreamMessageSearchListViewThemeData( + backgroundColor: StreamColorTheme.light().appBg, ); const _messageSearchListViewThemeDataControlHalfLerp = - MessageSearchListViewThemeData( + StreamMessageSearchListViewThemeData( backgroundColor: Color(0xff818384), ); final _messageSearchListViewThemeDataControlDark = - MessageSearchListViewThemeData( - backgroundColor: ColorTheme.dark().appBg, + StreamMessageSearchListViewThemeData( + backgroundColor: StreamColorTheme.dark().appBg, ); diff --git a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart index 502be84a..3b6c3a6f 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart @@ -4,16 +4,17 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; void main() { test('MessageThemeData copyWith, ==, hashCode basics', () { - expect(const MessageThemeData(), const MessageThemeData().copyWith()); - expect(const MessageThemeData().hashCode, - const MessageThemeData().copyWith().hashCode); + expect(const StreamMessageThemeData(), + const StreamMessageThemeData().copyWith()); + expect(const StreamMessageThemeData().hashCode, + const StreamMessageThemeData().copyWith().hashCode); }); group('MessageThemeData lerps', () { test('''Light MessageThemeData lerps completely to dark MessageThemeData''', () { expect( - const MessageThemeData() + const StreamMessageThemeData() .lerp(_messageThemeControl, _messageThemeControlDark, 1), _messageThemeControlDark); }); @@ -21,7 +22,7 @@ void main() { test('''Dark MessageThemeData lerps completely to light MessageThemeData''', () { expect( - const MessageThemeData() + const StreamMessageThemeData() .lerp(_messageThemeControlDark, _messageThemeControl, 1), _messageThemeControl); }); @@ -33,23 +34,23 @@ void main() { }); } -final _messageThemeControl = MessageThemeData( - messageAuthorStyle: TextTheme.light().footnote.copyWith( - color: ColorTheme.light().textLowEmphasis, +final _messageThemeControl = StreamMessageThemeData( + messageAuthorStyle: StreamTextTheme.light().footnote.copyWith( + color: StreamColorTheme.light().textLowEmphasis, ), - messageTextStyle: TextTheme.light().body, - createdAtStyle: TextTheme.light().footnote.copyWith( - color: ColorTheme.light().textLowEmphasis, + messageTextStyle: StreamTextTheme.light().body, + createdAtStyle: StreamTextTheme.light().footnote.copyWith( + color: StreamColorTheme.light().textLowEmphasis, ), - repliesStyle: TextTheme.light().footnoteBold.copyWith( - color: ColorTheme.light().accentPrimary, + repliesStyle: StreamTextTheme.light().footnoteBold.copyWith( + color: StreamColorTheme.light().accentPrimary, ), - messageBackgroundColor: ColorTheme.light().disabled, - reactionsBackgroundColor: ColorTheme.light().barsBg, - reactionsBorderColor: ColorTheme.light().borders, - reactionsMaskColor: ColorTheme.light().appBg, - messageBorderColor: ColorTheme.light().disabled, - avatarTheme: AvatarThemeData( + messageBackgroundColor: StreamColorTheme.light().disabled, + reactionsBackgroundColor: StreamColorTheme.light().barsBg, + reactionsBorderColor: StreamColorTheme.light().borders, + reactionsMaskColor: StreamColorTheme.light().appBg, + messageBorderColor: StreamColorTheme.light().disabled, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -57,28 +58,28 @@ final _messageThemeControl = MessageThemeData( ), ), messageLinksStyle: TextStyle( - color: ColorTheme.light().accentPrimary, + color: StreamColorTheme.light().accentPrimary, ), - linkBackgroundColor: ColorTheme.light().linkBg, + linkBackgroundColor: StreamColorTheme.light().linkBg, ); -final _messageThemeControlDark = MessageThemeData( - messageAuthorStyle: TextTheme.dark().footnote.copyWith( - color: ColorTheme.dark().textLowEmphasis, +final _messageThemeControlDark = StreamMessageThemeData( + messageAuthorStyle: StreamTextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().textLowEmphasis, ), - messageTextStyle: TextTheme.dark().body, - createdAtStyle: TextTheme.dark().footnote.copyWith( - color: ColorTheme.dark().textLowEmphasis, + messageTextStyle: StreamTextTheme.dark().body, + createdAtStyle: StreamTextTheme.dark().footnote.copyWith( + color: StreamColorTheme.dark().textLowEmphasis, ), - repliesStyle: TextTheme.dark().footnoteBold.copyWith( - color: ColorTheme.dark().accentPrimary, + repliesStyle: StreamTextTheme.dark().footnoteBold.copyWith( + color: StreamColorTheme.dark().accentPrimary, ), - messageBackgroundColor: ColorTheme.dark().disabled, - reactionsBackgroundColor: ColorTheme.dark().barsBg, - reactionsBorderColor: ColorTheme.dark().borders, - reactionsMaskColor: ColorTheme.dark().appBg, - messageBorderColor: ColorTheme.dark().disabled, - avatarTheme: AvatarThemeData( + messageBackgroundColor: StreamColorTheme.dark().disabled, + reactionsBackgroundColor: StreamColorTheme.dark().barsBg, + reactionsBorderColor: StreamColorTheme.dark().borders, + reactionsMaskColor: StreamColorTheme.dark().appBg, + messageBorderColor: StreamColorTheme.dark().disabled, + avatarTheme: StreamAvatarThemeData( borderRadius: BorderRadius.circular(20), constraints: const BoxConstraints.tightFor( height: 32, @@ -86,7 +87,7 @@ final _messageThemeControlDark = MessageThemeData( ), ), messageLinksStyle: TextStyle( - color: ColorTheme.dark().accentPrimary, + color: StreamColorTheme.dark().accentPrimary, ), - linkBackgroundColor: ColorTheme.dark().linkBg, + linkBackgroundColor: StreamColorTheme.dark().linkBg, ); diff --git a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart index 3aa20744..6937f4b9 100644 --- a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart @@ -6,15 +6,15 @@ import '../mocks.dart'; void main() { test('UserListViewThemeData copyWith, ==, hashCode basics', () { - expect(const UserListViewThemeData(), - const UserListViewThemeData().copyWith()); + expect(const StreamUserListViewThemeData(), + const StreamUserListViewThemeData().copyWith()); }); test( '''Light UserListViewThemeData lerps completely to dark UserListViewThemeData''', () { expect( - const UserListViewThemeData().lerp(_userListViewThemeDataControl, + const StreamUserListViewThemeData().lerp(_userListViewThemeDataControl, _userListViewThemeDataControlDark, 1), _userListViewThemeDataControlDark); }); @@ -23,7 +23,7 @@ void main() { '''Light UserListViewThemeData lerps halfway to dark UserListViewThemeData''', () { expect( - const UserListViewThemeData().lerp(_userListViewThemeDataControl, + const StreamUserListViewThemeData().lerp(_userListViewThemeDataControl, _userListViewThemeDataControlDark, 0.5), _userListViewThemeDataControlHalfLerp); }); @@ -32,8 +32,10 @@ void main() { '''Dark UserListViewThemeData lerps completely to light UserListViewThemeData''', () { expect( - const UserListViewThemeData().lerp(_userListViewThemeDataControlDark, - _userListViewThemeDataControl, 1), + const StreamUserListViewThemeData().lerp( + _userListViewThemeDataControlDark, + _userListViewThemeDataControl, + 1), _userListViewThemeDataControl); }); @@ -58,7 +60,7 @@ void main() { _context = context; return Scaffold( body: UsersBloc( - child: UserListView(), + child: StreamUserListView(), ), ); }, @@ -66,7 +68,7 @@ void main() { ), ); - final userListViewTheme = UserListViewTheme.of(_context); + final userListViewTheme = StreamUserListViewTheme.of(_context); expect(userListViewTheme.backgroundColor, _userListViewThemeDataControl.backgroundColor); }); @@ -87,7 +89,7 @@ void main() { _context = context; return Scaffold( body: UsersBloc( - child: UserListView(), + child: StreamUserListView(), ), ); }, @@ -95,20 +97,20 @@ void main() { ), ); - final userListViewTheme = UserListViewTheme.of(_context); + final userListViewTheme = StreamUserListViewTheme.of(_context); expect(userListViewTheme.backgroundColor, _userListViewThemeDataControlDark.backgroundColor); }); } -final _userListViewThemeDataControl = UserListViewThemeData( - backgroundColor: ColorTheme.light().appBg, +final _userListViewThemeDataControl = StreamUserListViewThemeData( + backgroundColor: StreamColorTheme.light().appBg, ); -const _userListViewThemeDataControlHalfLerp = UserListViewThemeData( +const _userListViewThemeDataControlHalfLerp = StreamUserListViewThemeData( backgroundColor: Color(0xff818384), ); -final _userListViewThemeDataControlDark = UserListViewThemeData( - backgroundColor: ColorTheme.dark().appBg, +final _userListViewThemeDataControlDark = StreamUserListViewThemeData( + backgroundColor: StreamColorTheme.dark().appBg, ); diff --git a/packages/stream_chat_flutter/test/src/thread_header_test.dart b/packages/stream_chat_flutter/test/src/thread_header_test.dart index babe4318..6d1758fa 100644 --- a/packages/stream_chat_flutter/test/src/thread_header_test.dart +++ b/packages/stream_chat_flutter/test/src/thread_header_test.dart @@ -51,7 +51,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ThreadHeader( + body: StreamThreadHeader( parent: Message(), ), ), @@ -112,7 +112,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: ThreadHeader( + body: StreamThreadHeader( parent: Message(), subtitle: const Text('subtitle'), leading: const Text('leading'), diff --git a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart index 8d790266..27b3f977 100644 --- a/packages/stream_chat_flutter/test/src/typing_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/typing_indicator_test.dart @@ -63,19 +63,23 @@ void main() { Event(type: EventType.typingStart), })); + const typingKey = Key('typing'); + await tester.pumpWidget(MaterialApp( home: StreamChat( client: client, child: StreamChannel( channel: channel, child: const Scaffold( - body: TypingIndicator(), + body: StreamTypingIndicator( + key: typingKey, + ), ), ), ), )); - expect(find.byKey(const Key('typings')), findsOneWidget); + expect(find.byKey(typingKey), findsOneWidget); }, ); } diff --git a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart index 00359089..a470fe74 100644 --- a/packages/stream_chat_flutter/test/src/unread_indicator_test.dart +++ b/packages/stream_chat_flutter/test/src/unread_indicator_test.dart @@ -37,7 +37,7 @@ void main() { child: StreamChannel( channel: channel, child: const Scaffold( - body: UnreadIndicator(), + body: StreamUnreadIndicator(), ), ), ), @@ -75,7 +75,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: UnreadIndicator( + body: StreamUnreadIndicator( cid: channel.cid, ), ), @@ -115,7 +115,7 @@ void main() { child: StreamChannel( channel: channel, child: Scaffold( - body: UnreadIndicator( + body: StreamUnreadIndicator( cid: channel.cid, ), ), diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index d18b2ca0..ab1e2504 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -9,6 +9,10 @@ ## 3.4.0 - Updated `stream_chat` dependency to [`3.4.0`](https://pub.dev/packages/stream_chat/changelog). +✅ Added + +- Added `MessageInputController` to hold `Message` related data. + 🐞 Fixed - Do not move a channel to top if the new message is from a thread. diff --git a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index fa19a90f..76bf3854 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -16,6 +16,7 @@ import 'package:stream_chat_flutter_core/src/stream_controller_extension.dart'; /// using Flutter's [BuildContext]. /// /// API docs: https://getstream.io/chat/docs/flutter-dart/query_channels/ +@Deprecated("Use 'StreamChannelListController' instead") class ChannelsBloc extends StatefulWidget { /// Creates a new [ChannelsBloc]. The parameter [child] must be supplied and /// not null. diff --git a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart new file mode 100644 index 00000000..65c6d8d8 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -0,0 +1,309 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'package:stream_chat_flutter_core/src/message_text_field_controller.dart'; + +/// A value listenable builder related to a [Message]. +/// +/// Pass in a [MessageInputController] as the `valueListenable`. +typedef MessageValueListenableBuilder = ValueListenableBuilder; + +/// Controller for storing and mutating a [Message] value. +class MessageInputController extends ValueNotifier { + /// Creates a controller for an editable text field. + /// + /// This constructor treats a null [message] argument as if it were the empty + /// message. + factory MessageInputController({ + Message? message, + Map? textPatternStyle, + }) => + MessageInputController._( + initialMessage: message ?? Message(), + textPatternStyle: textPatternStyle, + ); + + /// Creates a controller for an editable text field from an initial [text]. + factory MessageInputController.fromText( + String? text, { + Map? textPatternStyle, + }) => + MessageInputController._( + initialMessage: Message(text: text), + textPatternStyle: textPatternStyle, + ); + + /// Creates a controller for an editable text field from initial + /// [attachments]. + factory MessageInputController.fromAttachments( + List attachments, { + Map? textPatternStyle, + }) => + MessageInputController._( + initialMessage: Message(attachments: attachments), + textPatternStyle: textPatternStyle, + ); + + MessageInputController._({ + required Message initialMessage, + Map? textPatternStyle, + }) : _textEditingController = MessageTextFieldController.fromValue( + initialMessage.text == null + ? const TextEditingValue() + : TextEditingValue( + text: initialMessage.text!, + composing: TextRange.collapsed(initialMessage.text!.length), + ), + textPatternStyle: textPatternStyle, + ), + _initialMessage = initialMessage, + super(initialMessage) { + addListener(_textEditingSyncer); + } + + void _textEditingSyncer() { + final cleanText = value.command == null + ? value.text + : value.text?.replaceFirst('/${value.command} ', ''); + + if (cleanText != _textEditingController.text) { + final previousOffset = _textEditingController.value.selection.start; + final previousText = _textEditingController.text; + final diff = (cleanText?.length ?? 0) - previousText.length; + _textEditingController + ..text = cleanText ?? '' + ..selection = TextSelection.collapsed( + offset: previousOffset + diff, + ); + } + } + + /// Returns the current message associated with this controller. + Message get message => value; + + /// Returns the controller of the text field linked to this controller. + MessageTextFieldController get textEditingController => + _textEditingController; + final MessageTextFieldController _textEditingController; + + /// Returns the text of the message. + String get text => _textEditingController.text; + + Message _initialMessage; + + /// Sets the message. + set message(Message message) { + value = message; + } + + /// Sets the message that's being quoted. + set quotedMessage(Message message) { + value = value.copyWith( + quotedMessage: message, + quotedMessageId: message.id, + ); + } + + /// Clears the quoted message. + void clearQuotedMessage() { + value = value.copyWith( + quotedMessageId: null, + quotedMessage: null, + ); + } + + /// Sets a command for the message. + set command(Command command) { + value = value.copyWith( + command: command.name, + text: '/${command.name} ', + ); + } + + /// Sets the text of the message. + set text(String newText) { + var newTextWithCommand = newText; + if (value.command != null) { + if (!newText.startsWith('/${value.command}')) { + newTextWithCommand = '/${value.command} $newText'; + } + } + value = value.copyWith(text: newTextWithCommand); + } + + /// Returns the baseOffset of the text field. + int get baseOffset => textEditingController.selection.baseOffset; + + /// Returns the start of the selection of the text field. + int get selectionStart => textEditingController.selection.start; + + /// Sets the [showInChannel] flag of the message. + set showInChannel(bool newValue) { + value = value.copyWith(showInChannel: newValue); + } + + /// Returns true if the message is in a thread and + /// should be shown in the main channel as well. + bool get showInChannel => value.showInChannel ?? false; + + /// Returns the attachments of the message. + List get attachments => value.attachments; + + /// Sets the list of [attachments] for the message. + set attachments(List attachments) { + value = value.copyWith(attachments: attachments); + } + + /// Adds a new attachment to the message. + void addAttachment(Attachment attachment) { + attachments = [...attachments, attachment]; + } + + /// Adds a new attachment at the specified [index]. + void addAttachmentAt(int index, Attachment attachment) { + attachments = [...attachments]..insert(index, attachment); + } + + /// Removes the specified [attachment] from the message. + void removeAttachment(Attachment attachment) { + attachments = [...attachments]..remove(attachment); + } + + /// Remove the attachment with the given [attachmentId]. + void removeAttachmentById(String attachmentId) { + attachments = [...attachments]..removeWhere((it) => it.id == attachmentId); + } + + /// Removes the attachment at the given [index]. + void removeAttachmentAt(int index) { + attachments = [...attachments]..removeAt(index); + } + + /// Clears the message attachments. + void clearAttachments() { + attachments = []; + } + + // Only used to store the value locally in order to remove it if we call + // [clearOGAttachment] or [setOGAttachment] again. + Attachment? _ogAttachment; + + /// Returns the og attachment of the message if set + Attachment? get ogAttachment => + attachments.firstWhereOrNull((it) => it.id == _ogAttachment?.id); + + /// Sets the og attachment in the message. + void setOGAttachment(Attachment attachment) { + attachments = [...attachments] + ..remove(_ogAttachment) + ..insert(0, attachment); + _ogAttachment = attachment; + } + + /// Removes the og attachment. + void clearOGAttachment() { + if (_ogAttachment != null) { + removeAttachment(_ogAttachment!); + } + _ogAttachment = null; + } + + /// Returns the list of mentioned users in the message. + List get mentionedUsers => value.mentionedUsers; + + /// Sets the mentioned users. + set mentionedUsers(List users) { + value = value.copyWith(mentionedUsers: users); + } + + /// Adds a user to the list of mentioned users. + void addMentionedUser(User user) { + mentionedUsers = [...mentionedUsers, user]; + } + + /// Removes the specified [user] from the mentioned users list. + void removeMentionedUser(User user) { + mentionedUsers = [...mentionedUsers]..remove(user); + } + + /// Removes the mentioned user with the given [userId]. + void removeMentionedUserById(String userId) { + mentionedUsers = [...mentionedUsers]..removeWhere((it) => it.id == userId); + } + + /// Removes all mentioned users from the message. + void clearMentionedUsers() { + mentionedUsers = []; + } + + /// Sets the [message], or [value], to empty. + /// + /// After calling this function, [text], [attachments] and [mentionedUsers] + /// will all be empty. + /// + /// Calling this will notify all the listeners of this + /// [MessageInputController] that they need to update + /// (calls [notifyListeners]). For this reason, + /// this method should only be called between frames, e.g. in response to user + /// actions, not during the build, layout, or paint phases. + void clear() { + value = Message(); + _textEditingController.clear(); + } + + /// Sets the [value] to the initial [Message] value. + void reset({bool resetId = true}) { + if (resetId) { + final newId = const Uuid().v4(); + _initialMessage = _initialMessage.copyWith(id: newId); + } + value = _initialMessage; + } + + @override + void dispose() { + removeListener(_textEditingSyncer); + _textEditingController.dispose(); + super.dispose(); + } +} + +/// A [RestorableProperty] that knows how to store and restore a +/// [MessageInputController]. +/// +/// The [MessageInputController] is accessible via the [value] getter. During +/// state restoration, the property will restore [MessageInputController.value] +/// to the value it had when the restoration data it is getting restored from +/// was collected. +class RestorableMessageInputController + extends RestorableChangeNotifier { + /// Creates a [RestorableMessageInputController]. + /// + /// This constructor creates a default [Message] when no `message` argument + /// is supplied. + RestorableMessageInputController({Message? message}) + : _initialValue = message ?? Message(); + + /// Creates a [RestorableMessageInputController] from an initial + /// [text] value. + factory RestorableMessageInputController.fromText(String? text) => + RestorableMessageInputController(message: Message(text: text)); + + final Message _initialValue; + + @override + MessageInputController createDefaultValue() => + MessageInputController(message: _initialValue); + + @override + MessageInputController fromPrimitives(Object? data) { + final message = Message.fromJson(json.decode(data! as String)); + return MessageInputController(message: message); + } + + @override + String toPrimitives() => json.encode(value.value); +} diff --git a/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart new file mode 100644 index 00000000..0f9f75c6 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/message_text_field_controller.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +/// A function that takes a [BuildContext] and returns a [TextStyle]. +typedef TextStyleBuilder = TextStyle? Function( + BuildContext context, + String text, +); + +/// Controller for the [StreamTextField] widget. +class MessageTextFieldController extends TextEditingController { + /// Returns a new MessageTextFieldController + MessageTextFieldController({ + String? text, + this.textPatternStyle, + }) : super(text: text); + + /// Returns a new MessageTextFieldController with the given text [value]. + MessageTextFieldController.fromValue( + TextEditingValue? value, { + this.textPatternStyle, + }) : super.fromValue(value); + + /// A map of style to apply to the text matching the RegExp patterns. + final Map? textPatternStyle; + + /// Builds a [TextSpan] from the current text, + /// highlighting the matches for [textPatternStyle]. + @override + TextSpan buildTextSpan({ + required BuildContext context, + TextStyle? style, + required bool withComposing, + }) { + final pattern = textPatternStyle; + if (pattern == null || pattern.isEmpty) { + return super.buildTextSpan( + context: context, + style: style, + withComposing: withComposing, + ); + } + + return TextSpan(text: text, style: style).splitMapJoin( + RegExp(pattern.keys.map((it) => it.pattern).join('|')), + onMatch: (match) { + final text = match[0]!; + final key = pattern.keys.firstWhere((it) => it.hasMatch(text)); + return TextSpan( + text: text, + style: pattern[key]?.call( + context, + text, + ), + ); + }, + ); + } +} + +extension _TextSpanX on TextSpan { + TextSpan splitMapJoin( + Pattern pattern, { + TextSpan Function(Match)? onMatch, + TextSpan Function(TextSpan)? onNonMatch, + }) { + final children = []; + + toPlainText().splitMapJoin( + pattern, + onMatch: (match) { + final span = TextSpan(text: match.group(0), style: style); + final updated = onMatch?.call(match); + children.add(updated ?? span); + return span.toPlainText(); + }, + onNonMatch: (text) { + final span = TextSpan(text: text, style: style); + final updatedSpan = onNonMatch?.call(span); + children.add(updatedSpan ?? span); + return span.toPlainText(); + }, + ); + + return TextSpan(style: style, children: children); + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart new file mode 100644 index 00000000..9525fea4 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.dart @@ -0,0 +1,136 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:stream_chat/stream_chat.dart' show StreamChatError; + +part 'paged_value_notifier.freezed.dart'; + +/// Default initial page size multiplier. +const defaultInitialPagedLimitMultiplier = 3; + +/// Value listenable for paged data. +typedef PagedValueListenableBuilder + = ValueListenableBuilder>; + +/// A [PagedValueNotifier] that uses a [PagedListenable] to load data. +/// +/// This class is useful when you need to load data from a server +/// using a [PagedListenable] and want to keep the UI-driven refresh +/// signals in the [PagedListenable]. +/// +/// [PagedValueNotifier] is a [ValueNotifier] that emits a [PagedValue] +/// whenever the data is loaded or an error occurs. +abstract class PagedValueNotifier + extends ValueNotifier> { + /// Creates a [PagedValueNotifier] + PagedValueNotifier(this._initialValue) : super(_initialValue); + + /// Stores initialValue in case we need to call [refresh]. + final PagedValue _initialValue; + + /// Returns the currently loaded items + List get currentItems => value.asSuccess.items; + + /// Appends [newItems] to the previously loaded ones and replaces + /// the next page's key. + void appendPage({ + required List newItems, + required Key nextPageKey, + }) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems, nextPageKey: nextPageKey); + } + + /// Appends [newItems] to the previously loaded ones and sets the next page + /// key to `null`. + void appendLastPage(List newItems) { + final updatedItems = currentItems + newItems; + value = PagedValue(items: updatedItems); + } + + /// Retry any failed load requests. + /// + /// Unlike [refresh], this does not resets the whole [value], + /// it only retries the last failed load request. + Future retry() { + final lastValue = value.asSuccess; + assert(lastValue.hasError, ''); + + final nextPageKey = lastValue.nextPageKey; + // resetting the error + value = lastValue.copyWith(error: null); + // ignore: null_check_on_nullable_type_parameter + return loadMore(nextPageKey!); + } + + /// Refresh the data presented by this [PagedValueNotifier]. + /// + /// Resets the [value] to the initial value in case [resetValue] is true. + /// + /// Note: This API is intended for UI-driven refresh signals, + /// such as swipe-to-refresh. + Future refresh({bool resetValue = true}) { + if (resetValue) value = _initialValue; + return doInitialLoad(); + } + + /// Load initial data from the server. + Future doInitialLoad(); + + /// Load more data from the server using [nextPageKey]. + Future loadMore(Key nextPageKey); +} + +/// Paged value that can be used with [PagedValueNotifier]. +@freezed +abstract class PagedValue with _$PagedValue { + /// Represents the success state of the [PagedValue] + // @Assert( + // 'nextPageKey != null', + // 'Cannot set an error if all the pages are already fetched', + // ) + const factory PagedValue({ + /// List with all items loaded so far. + required List items, + + /// The key for the next page to be fetched. + Key? nextPageKey, + + /// The current error, if any. + StreamChatError? error, + }) = Success; + + const PagedValue._(); + + /// Represents the loading state of the [PagedValue]. + const factory PagedValue.loading() = Loading; + + /// Represents the error state of the [PagedValue]. + const factory PagedValue.error(StreamChatError error) = Error; + + /// Returns `true` if the [PagedValue] is [Success]. + bool get isSuccess => this is Success; + + /// Returns the [PagedValue] as [Success]. + Success get asSuccess { + assert( + isSuccess, + 'Cannot get asSuccess if the PagedValue is not in the Success state', + ); + return this as Success; + } + + /// Returns `true` if the [PagedValue] is [Success] + /// and has more items to load. + bool get hasNextPage => asSuccess.nextPageKey != null; + + /// Returns `true` if the [PagedValue] is [Success] and has an error. + bool get hasError => asSuccess.error != null; + + /// + int get itemCount { + final count = asSuccess.items.length; + if (hasNextPage || hasError) return count + 1; + return count; + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart new file mode 100644 index 00000000..a7ea0ea3 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/paged_value_notifier.freezed.dart @@ -0,0 +1,589 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target + +part of 'paged_value_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + +/// @nodoc +class _$PagedValueTearOff { + const _$PagedValueTearOff(); + + Success call( + {required List items, Key? nextPageKey, StreamChatError? error}) { + return Success( + items: items, + nextPageKey: nextPageKey, + error: error, + ); + } + + Loading loading() { + return Loading(); + } + + Error error(StreamChatError error) { + return Error( + error, + ); + } +} + +/// @nodoc +const $PagedValue = _$PagedValueTearOff(); + +/// @nodoc +mixin _$PagedValue { + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PagedValueCopyWith { + factory $PagedValueCopyWith(PagedValue value, + $Res Function(PagedValue) then) = + _$PagedValueCopyWithImpl; +} + +/// @nodoc +class _$PagedValueCopyWithImpl + implements $PagedValueCopyWith { + _$PagedValueCopyWithImpl(this._value, this._then); + + final PagedValue _value; + // ignore: unused_field + final $Res Function(PagedValue) _then; +} + +/// @nodoc +abstract class $SuccessCopyWith { + factory $SuccessCopyWith( + Success value, $Res Function(Success) then) = + _$SuccessCopyWithImpl; + $Res call({List items, Key? nextPageKey, StreamChatError? error}); +} + +/// @nodoc +class _$SuccessCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $SuccessCopyWith { + _$SuccessCopyWithImpl( + Success _value, $Res Function(Success) _then) + : super(_value, (v) => _then(v as Success)); + + @override + Success get _value => super._value as Success; + + @override + $Res call({ + Object? items = freezed, + Object? nextPageKey = freezed, + Object? error = freezed, + }) { + return _then(Success( + items: items == freezed + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + nextPageKey: nextPageKey == freezed + ? _value.nextPageKey + : nextPageKey // ignore: cast_nullable_to_non_nullable + as Key?, + error: error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StreamChatError?, + )); + } +} + +/// @nodoc + +class _$Success extends Success + with DiagnosticableTreeMixin { + const _$Success({required this.items, this.nextPageKey, this.error}) + : super._(); + + @override + + /// List with all items loaded so far. + final List items; + @override + + /// The key for the next page to be fetched. + final Key? nextPageKey; + @override + + /// The current error, if any. + final StreamChatError? error; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>(items: $items, nextPageKey: $nextPageKey, error: $error)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>')) + ..add(DiagnosticsProperty('items', items)) + ..add(DiagnosticsProperty('nextPageKey', nextPageKey)) + ..add(DiagnosticsProperty('error', error)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is Success && + const DeepCollectionEquality().equals(other.items, items) && + const DeepCollectionEquality() + .equals(other.nextPageKey, nextPageKey) && + const DeepCollectionEquality().equals(other.error, error)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(items), + const DeepCollectionEquality().hash(nextPageKey), + const DeepCollectionEquality().hash(error)); + + @JsonKey(ignore: true) + @override + $SuccessCopyWith> get copyWith => + _$SuccessCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return $default(items, nextPageKey, this.error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return $default?.call(items, nextPageKey, this.error); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(items, nextPageKey, this.error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return $default(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return $default?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if ($default != null) { + return $default(this); + } + return orElse(); + } +} + +abstract class Success extends PagedValue { + const factory Success( + {required List items, + Key? nextPageKey, + StreamChatError? error}) = _$Success; + const Success._() : super._(); + + /// List with all items loaded so far. + List get items; + + /// The key for the next page to be fetched. + Key? get nextPageKey; + + /// The current error, if any. + StreamChatError? get error; + @JsonKey(ignore: true) + $SuccessCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoadingCopyWith { + factory $LoadingCopyWith( + Loading value, $Res Function(Loading) then) = + _$LoadingCopyWithImpl; +} + +/// @nodoc +class _$LoadingCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $LoadingCopyWith { + _$LoadingCopyWithImpl( + Loading _value, $Res Function(Loading) _then) + : super(_value, (v) => _then(v as Loading)); + + @override + Loading get _value => super._value as Loading; +} + +/// @nodoc + +class _$Loading extends Loading + with DiagnosticableTreeMixin { + const _$Loading() : super._(); + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>.loading()'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.loading')); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is Loading); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class Loading extends PagedValue { + const factory Loading() = _$Loading; + const Loading._() : super._(); +} + +/// @nodoc +abstract class $ErrorCopyWith { + factory $ErrorCopyWith( + Error value, $Res Function(Error) then) = + _$ErrorCopyWithImpl; + $Res call({StreamChatError error}); +} + +/// @nodoc +class _$ErrorCopyWithImpl + extends _$PagedValueCopyWithImpl + implements $ErrorCopyWith { + _$ErrorCopyWithImpl( + Error _value, $Res Function(Error) _then) + : super(_value, (v) => _then(v as Error)); + + @override + Error get _value => super._value as Error; + + @override + $Res call({ + Object? error = freezed, + }) { + return _then(Error( + error == freezed + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as StreamChatError, + )); + } +} + +/// @nodoc + +class _$Error extends Error + with DiagnosticableTreeMixin { + const _$Error(this.error) : super._(); + + @override + final StreamChatError error; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'PagedValue<$Key, $Value>.error(error: $error)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'PagedValue<$Key, $Value>.error')) + ..add(DiagnosticsProperty('error', error)); + } + + @override + bool operator ==(dynamic other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is Error && + const DeepCollectionEquality().equals(other.error, error)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(error)); + + @JsonKey(ignore: true) + @override + $ErrorCopyWith> get copyWith => + _$ErrorCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error) + $default, { + required TResult Function() loading, + required TResult Function(StreamChatError error) error, + }) { + return error(this.error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + }) { + return error?.call(this.error); + } + + @override + @optionalTypeArgs + TResult maybeWhen( + TResult Function( + List items, Key? nextPageKey, StreamChatError? error)? + $default, { + TResult Function()? loading, + TResult Function(StreamChatError error)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this.error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map( + TResult Function(Success value) $default, { + required TResult Function(Loading value) loading, + required TResult Function(Error value) error, + }) { + return error(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + }) { + return error?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap( + TResult Function(Success value)? $default, { + TResult Function(Loading value)? loading, + TResult Function(Error value)? error, + required TResult orElse(), + }) { + if (error != null) { + return error(this); + } + return orElse(); + } +} + +abstract class Error extends PagedValue { + const factory Error(StreamChatError error) = _$Error; + const Error._() : super._(); + + StreamChatError get error; + @JsonKey(ignore: true) + $ErrorCopyWith> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart new file mode 100644 index 00000000..7d3a281f --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_controller.dart @@ -0,0 +1,282 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:stream_chat/stream_chat.dart' hide Success; +import 'package:stream_chat_flutter_core/src/paged_value_notifier.dart'; + +import 'package:stream_chat_flutter_core/src/stream_channel_list_event_handler.dart'; + +/// The default channel page limit to load. +const defaultChannelPagedLimit = 10; + +const _kDefaultBackendPaginationLimit = 30; + +/// A controller for a Channel list. +/// +/// This class lets you perform tasks such as: +/// * Load initial data. +/// * Use channel events handlers. +/// * Load more data using [loadMore]. +/// * Replace the previously loaded channels. +/// * Return/Create a new channel and start watching it. +/// * Pause and Resume all subscriptions added to this composite. +class StreamChannelListController extends PagedValueNotifier { + /// Creates a Stream channel list controller. + /// + /// * `client` is the Stream chat client to use for the channels list. + /// + /// * `channelEventHandlers` is the channel events to use for the channels + /// list. This class can be mixed in or extended to create custom overrides. + /// See [StreamChannelListEventHandler] for advice. + /// + /// * `filter` is the query filters to use. + /// + /// * `sort` is the sorting used for the channels matching the filters. + /// + /// * `presence` sets whether you'll receive user presence updates via the + /// websocket events. + /// + /// * `limit` is the limit to apply to the channel list. + /// + /// * `messageLimit` is the number of messages to fetch in each channel. + /// + /// * `memberLimit` is the number of members to fetch in each channel. + StreamChannelListController({ + required this.client, + StreamChannelListEventHandler? eventHandler, + this.filter, + this.sort, + this.presence = true, + this.limit = defaultChannelPagedLimit, + this.messageLimit, + this.memberLimit, + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), + super(const PagedValue.loading()); + + /// Creates a [StreamChannelListController] from the passed [value]. + StreamChannelListController.fromValue( + PagedValue value, { + required this.client, + StreamChannelListEventHandler? eventHandler, + this.filter, + this.sort, + this.presence = true, + this.limit = defaultChannelPagedLimit, + this.messageLimit, + this.memberLimit, + }) : _eventHandler = eventHandler ?? StreamChannelListEventHandler(), + super(value); + + /// The client to use for the channels list. + final StreamChatClient client; + + /// The channel event handlers to use for the channels list. + final StreamChannelListEventHandler _eventHandler; + + /// The query filters to use. + /// + /// You can query on any of the custom fields you've defined on the [Channel]. + /// + /// You can also filter other built-in channel fields. + final Filter? filter; + + /// The sorting used for the channels matching the filters. + /// + /// Sorting is based on field and direction, multiple sorting options + /// can be provided. + /// + /// You can sort based on last_updated, last_message_at, updated_at, + /// created_at or member_count. + /// + /// Direction can be ascending or descending. + final List>? sort; + + /// If true you’ll receive user presence updates via the websocket events + final bool presence; + + /// The limit to apply to the channel list. The default is set to + /// [defaultChannelPagedLimit]. + final int limit; + + /// Number of messages to fetch in each channel. + final int? messageLimit; + + /// Number of members to fetch in each channel. + final int? memberLimit; + + @override + Future doInitialLoad() async { + final limit = min( + this.limit * defaultInitialPagedLimitMultiplier, + _kDefaultBackendPaginationLimit, + ); + try { + await for (final channels in client.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + presence: presence, + paginationParams: PaginationParams(limit: limit), + )) { + final nextKey = channels.length < limit ? null : channels.length; + value = PagedValue( + items: channels, + nextPageKey: nextKey, + ); + } + // start listening to events + _subscribeToChannelListEvents(); + } on StreamChatError catch (error) { + value = PagedValue.error(error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = PagedValue.error(chatError); + } + } + + @override + Future loadMore(int nextPageKey) async { + final previousValue = value.asSuccess; + + try { + await for (final channels in client.queryChannels( + filter: filter, + sort: sort, + memberLimit: memberLimit, + messageLimit: messageLimit, + presence: presence, + paginationParams: PaginationParams(limit: limit, offset: nextPageKey), + )) { + final previousItems = previousValue.items; + final newItems = previousItems + channels; + final nextKey = channels.length < limit ? null : newItems.length; + value = PagedValue( + items: newItems, + nextPageKey: nextKey, + ); + } + } on StreamChatError catch (error) { + value = previousValue.copyWith(error: error); + } catch (error) { + final chatError = StreamChatError(error.toString()); + value = previousValue.copyWith(error: chatError); + } + } + + /// Replaces the previously loaded channels with [channels] and updates + /// the nextPageKey. + set channels(List channels) { + value = PagedValue( + items: channels, + nextPageKey: channels.length, + ); + } + + /// Returns/Creates a new Channel and starts watching it. + Future getChannel({ + required String id, + required String type, + }) async { + final channel = client.channel(type, id: id); + await channel.watch(); + return channel; + } + + /// Leaves the [channel] and updates the list. + Future leaveChannel(Channel channel) async { + final user = client.state.currentUser; + assert(user != null, 'You must be logged in to leave a channel.'); + await channel.removeMembers([user!.id]); + } + + /// Deletes the [channel] and updates the list. + Future deleteChannel(Channel channel) async { + await channel.delete(); + } + + /// Mutes the [channel] and updates the list. + Future muteChannel(Channel channel) async { + await channel.mute(); + } + + /// Un-mutes the [channel] and updates the list. + Future unmuteChannel(Channel channel) async { + await channel.unmute(); + } + + /// Event listener, which can be set in order to listen + /// [client] web-socket events. + /// + /// Return `true` if the event is handled. Return `false` to + /// allow the event to be handled internally. + bool Function(Event event)? eventListener; + + StreamSubscription? _channelEventSubscription; + + // Subscribes to the channel list events. + void _subscribeToChannelListEvents() { + if (_channelEventSubscription != null) { + _unsubscribeFromChannelListEvents(); + } + + _channelEventSubscription = client.on().listen((event) { + // Returns early if the event is already handled by the listener. + if (eventListener?.call(event) ?? false) return; + + final eventType = event.type; + if (eventType == EventType.channelDeleted) { + _eventHandler.onChannelDeleted(event, this); + } else if (eventType == EventType.channelHidden) { + _eventHandler.onChannelHidden(event, this); + } else if (eventType == EventType.channelTruncated) { + _eventHandler.onChannelTruncated(event, this); + } else if (eventType == EventType.channelUpdated) { + _eventHandler.onChannelUpdated(event, this); + } else if (eventType == EventType.channelVisible) { + _eventHandler.onChannelVisible(event, this); + } else if (eventType == EventType.connectionRecovered) { + _eventHandler.onConnectionRecovered(event, this); + } else if (eventType == EventType.connectionChanged) { + if (event.online != null) { + _eventHandler.onConnectionRecovered(event, this); + } + } else if (eventType == EventType.messageNew) { + _eventHandler.onMessageNew(event, this); + } else if (eventType == EventType.notificationAddedToChannel) { + _eventHandler.onNotificationAddedToChannel(event, this); + } else if (eventType == EventType.notificationMessageNew) { + _eventHandler.onNotificationMessageNew(event, this); + } else if (eventType == EventType.notificationRemovedFromChannel) { + _eventHandler.onNotificationRemovedFromChannel(event, this); + } else if (eventType == 'user.presence.changed' || + eventType == EventType.userUpdated) { + _eventHandler.onUserPresenceChanged(event, this); + } + }); + } + + // Unsubscribes from all channel list events. + void _unsubscribeFromChannelListEvents() { + if (_channelEventSubscription != null) { + _channelEventSubscription!.cancel(); + _channelEventSubscription = null; + } + } + + /// Pauses all subscriptions added to this composite. + void pauseEventsSubscription([Future? resumeSignal]) { + _channelEventSubscription?.pause(resumeSignal); + } + + /// Resumes all subscriptions added to this composite. + void resumeEventsSubscription() { + _channelEventSubscription?.resume(); + } + + @override + void dispose() { + _unsubscribeFromChannelListEvents(); + super.dispose(); + } +} diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart new file mode 100644 index 00000000..8d548c67 --- /dev/null +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel_list_event_handler.dart @@ -0,0 +1,213 @@ +import 'package:stream_chat/stream_chat.dart' show ChannelState, Event; +import 'package:stream_chat_flutter_core/src/stream_channel_list_controller.dart'; + +/// Contains handlers that are called from [StreamChannelListController] for +/// certain [Event]s. +/// +/// This class can be mixed in or extended to create custom overrides. +class StreamChannelListEventHandler { + /// Function which gets called for the event + /// [EventType.channelDeleted]. + /// + /// This event is fired when a channel is deleted. + /// + /// By default, this removes the channel from the list of channels. + void onChannelDeleted(Event event, StreamChannelListController controller) { + final channels = [...controller.currentItems]; + + final updatedChannels = channels + ..removeWhere( + (it) => it.cid == (event.cid ?? event.channel?.cid), + ); + + controller.channels = updatedChannels; + } + + /// Function which gets called for the event + /// [EventType.channelHidden]. + /// + /// This event is fired when a channel is hidden. + /// + /// By default, this removes the channel from the list of channels. + void onChannelHidden(Event event, StreamChannelListController controller) { + onChannelDeleted(event, controller); + } + + /// Function which gets called for the event + /// [EventType.channelTruncated]. + /// + /// This event is fired when a channel is truncated. + /// + /// By default, this refreshes the whole channel list. + void onChannelTruncated(Event event, StreamChannelListController controller) { + controller.refresh(); + } + + /// Function which gets called for the event + /// [EventType.channelUpdated]. + /// + /// This event is fired when a channel is updated. + /// + /// By default, this updates the channel received in the event. + void onChannelUpdated(Event event, StreamChannelListController controller) { + final eventChannel = event.channel; + if (eventChannel == null) return; + + final channels = [...controller.currentItems]; + final channelIndex = channels.indexWhere( + (it) => it.cid == (event.cid ?? eventChannel.cid), + ); + + if (channelIndex >= 0) { + final channelState = ChannelState(channel: eventChannel); + channels[channelIndex].state?.updateChannelState(channelState); + } + + controller.channels = channels; + } + + /// Function which gets called for the event + /// [EventType.channelVisible]. + /// + /// This event is fired when a channel is made visible. + /// + /// By default, this adds the channel to the list of channels. + void onChannelVisible( + Event event, + StreamChannelListController controller, + ) async { + final channelId = event.channelId; + final channelType = event.channelType; + + if (channelId == null || channelType == null) return; + + final channel = await controller.getChannel( + id: channelId, + type: channelType, + ); + + final currentChannels = [...controller.currentItems]; + + final updatedChannels = [ + channel, + ...currentChannels..removeWhere((it) => it.cid == channel.cid), + ]; + + controller.channels = updatedChannels; + } + + /// Function which gets called for the event + /// [EventType.connectionRecovered]. + /// + /// This event is fired when the client web-socket connection recovers. + /// + /// By default, this refreshes the whole channel list. + void onConnectionRecovered( + Event event, + StreamChannelListController controller, + ) { + controller.refresh(); + } + + /// Function which gets called for the event [EventType.messageNew]. + /// + /// This event is fired when a new message is created in one of the channels + /// we are currently watching. + /// + /// By default, this moves the channel to the top of the list. + void onMessageNew(Event event, StreamChannelListController controller) { + final channelCid = event.cid; + if (channelCid == null) return; + + final channels = [...controller.currentItems]; + + final channelIndex = channels.indexWhere((it) => it.cid == channelCid); + if (channelIndex <= 0) return; + + final channel = channels.removeAt(channelIndex); + channels.insert(0, channel); + + controller.channels = [...channels]; + } + + /// Function which gets called for the event + /// [EventType.notificationAddedToChannel]. + /// + /// This event is fired when a channel is added which we are not watching. + /// + /// By default, this adds the channel and moves it to the top of list. + void onNotificationAddedToChannel( + Event event, + StreamChannelListController controller, + ) { + onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationMessageNew]. + /// + /// This event is fired when a new message is created in a channel + /// which we are not currently watching. + /// + /// By default, this adds the channel and moves it to the top of list. + void onNotificationMessageNew( + Event event, + StreamChannelListController controller, + ) { + onChannelVisible(event, controller); + } + + /// Function which gets called for the event + /// [EventType.notificationRemovedFromChannel]. + /// + /// This event is fired when a user is removed from a channel which we are + /// not currently watching. + /// + /// By default, this removes the event channel from the list. + void onNotificationRemovedFromChannel( + Event event, + StreamChannelListController controller, + ) { + final channels = [...controller.currentItems]; + final updatedChannels = + channels.where((it) => it.cid != event.channel?.cid); + final listChanged = channels.length != updatedChannels.length; + + if (!listChanged) return; + + controller.channels = [...updatedChannels]; + } + + /// Function which gets called for the event + /// 'user.presence.changed' and [EventType.userUpdated]. + /// + /// This event is fired when a user's presence changes or gets updated. + /// + /// By default, this updates the channel member with the event user. + void onUserPresenceChanged( + Event event, + StreamChannelListController controller, + ) { + final user = event.user; + if (user == null) return; + + final channels = [...controller.currentItems]; + + final updatedChannels = channels.map((channel) { + final members = [...channel.state!.members]; + final memberIndex = members.indexWhere( + (it) => user.id == (it.userId ?? it.user?.id), + ); + + if (memberIndex < 0) return channel; + + members[memberIndex] = members[memberIndex].copyWith(user: user); + final updatedState = ChannelState(members: [...members]); + channel.state!.updateChannelState(updatedState); + + return channel; + }); + + controller.channels = [...updatedChannels]; + } +} diff --git a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart index 8bd41c76..78d82d8b 100644 --- a/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart +++ b/packages/stream_chat_flutter_core/lib/stream_chat_flutter_core.dart @@ -7,10 +7,15 @@ export 'src/better_stream_builder.dart'; export 'src/channel_list_core.dart' hide ChannelListCoreState; export 'src/channels_bloc.dart'; export 'src/lazy_load_scroll_view.dart'; +export 'src/message_input_controller.dart'; export 'src/message_list_core.dart' hide MessageListCoreState; export 'src/message_search_bloc.dart'; export 'src/message_search_list_core.dart' hide MessageSearchListCoreState; +export 'src/message_text_field_controller.dart'; +export 'src/paged_value_notifier.dart' show PagedValueListenableBuilder; export 'src/stream_channel.dart'; +export 'src/stream_channel_list_controller.dart'; +export 'src/stream_channel_list_event_handler.dart'; export 'src/stream_chat_core.dart'; export 'src/typedef.dart'; export 'src/user_list_core.dart' hide UserListCoreState; diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 3fde274b..09ce3733 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -6,7 +6,7 @@ repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.14.0 <3.0.0' flutter: ">=1.17.0" dependencies: @@ -14,14 +14,17 @@ dependencies: connectivity_plus: ^2.1.0 flutter: sdk: flutter + freezed_annotation: ^1.0.0 meta: ^1.3.0 rxdart: ^0.27.0 stream_chat: ^3.5.1 dev_dependencies: + build_runner: ^2.0.1 dart_code_metrics: ^4.4.0 fake_async: ^1.2.0 flutter_test: sdk: flutter + freezed: ^1.0.0 mocktail: ^0.3.0 diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 6fa7689b..d111ead0 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -84,6 +84,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { 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'; @@ -392,6 +396,13 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; } void main() async { diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart index 0c7ce68e..6ea2eda4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations.dart @@ -75,6 +75,7 @@ GlobalStreamChatLocalizations? getStreamChatTranslation(Locale locale) { case 'pt': return const StreamChatLocalizationsPt(); } + return null; } /// Implementation of localized strings for the stream chat widgets diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart index c1106cab..08dbb02a 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_en.dart @@ -60,6 +60,10 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { 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'; @@ -368,4 +372,11 @@ class StreamChatLocalizationsEn extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'Slow mode ON'; + + @override + String get linkDisabledDetails => + 'Sending links is not allowed in this conversation.'; + + @override + String get linkDisabledError => 'Links are disabled'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart index 49e66184..50c748cb 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_es.dart @@ -61,6 +61,10 @@ class StreamChatLocalizationsEs extends GlobalStreamChatLocalizations { return 'Fijado por ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'No tienes permiso para enviar mensajes'; + @override String get emptyMessagesText => 'Actualmente no hay mensajes'; @@ -374,4 +378,11 @@ No es posible añadir más de $limit archivos adjuntos @override String get slowModeOnLabel => 'Modo lento activado'; + + @override + String get linkDisabledDetails => + 'No se permite enviar enlaces en esta conversación.'; + + @override + String get linkDisabledError => 'Los enlaces están deshabilitados'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart index 0057b7cc..0671f5af 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_fr.dart @@ -61,6 +61,10 @@ class StreamChatLocalizationsFr extends GlobalStreamChatLocalizations { return 'Épinglé par ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'Vous n\'êtes pas autorisé à envoyer des messages'; + @override String get emptyMessagesText => "Il n'y a pas de messages actuellement"; @@ -373,4 +377,11 @@ Limite de pièces jointes dépassée : il n'est pas possible d'ajouter plus de $ @override String get slowModeOnLabel => 'Mode lent activé'; + + @override + String get linkDisabledDetails => + 'L\'envoi de liens n\'est pas autorisé dans cette conversation.'; + + @override + String get linkDisabledError => 'Les liens sont désactivés'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart index 10f1858b..1244e2ea 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_hi.dart @@ -60,6 +60,9 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { return '${pinnedBy.name} द्वारा पिन किया गया'; } + @override + String get sendMessagePermissionError => 'आपको संदेश भेजने की अनुमति नहीं है'; + @override String get emptyMessagesText => 'वर्तमान में कोई संदेश नहीं है'; @@ -368,4 +371,11 @@ class StreamChatLocalizationsHi extends GlobalStreamChatLocalizations { @override String get slowModeOnLabel => 'स्लो मोड चालू'; + + @override + String get linkDisabledDetails => + 'इस बातचीत में लिंक भेजने की अनुमति नहीं है.'; + + @override + String get linkDisabledError => 'लिंक भेजना प्रतिबंधित'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart index 18e961d6..1f7fd048 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_it.dart @@ -60,6 +60,10 @@ class StreamChatLocalizationsIt extends GlobalStreamChatLocalizations { return 'Messo in evidenza da ${pinnedBy.name}'; } + @override + String get sendMessagePermissionError => + 'Non hai l\'autorizzazione per inviare messaggi'; + @override String get emptyMessagesText => 'Non c\'é nessun messaggio al momento'; @@ -370,4 +374,11 @@ Attenzione: il limite massimo di $limit file è stato superato. @override String get slowModeOnLabel => 'Slowmode attiva'; + + @override + String get linkDisabledDetails => + 'Non è permesso condividere link in questa convesazione.'; + + @override + String get linkDisabledError => 'I links sono disattivati'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart index 3b28d867..e1eea3a4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ja.dart @@ -60,6 +60,9 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { return '${pinnedBy.name}のピン'; } + @override + String get sendMessagePermissionError => 'メッセージを送信する権限がありません'; + @override String get emptyMessagesText => '現在、メッセージはありません。'; @@ -354,4 +357,10 @@ class StreamChatLocalizationsJa extends GlobalStreamChatLocalizations { String attachmentLimitExceedError(int limit) => ''' 添付ファイルの制限を超えました:$limit個のファイル以上を添付することはできません '''; + + @override + String get linkDisabledDetails => 'この会話では、リンクの送信は許可されていません。'; + + @override + String get linkDisabledError => 'リンクが無効になっています'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart index 3693bc11..820d58e4 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_ko.dart @@ -60,6 +60,9 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { return '${pinnedBy.name}의 핀'; } + @override + String get sendMessagePermissionError => '메시지를 보낼 수 있는 권한이 없습니다'; + @override String get emptyMessagesText => '현재 메시지가 없습니다'; @@ -354,4 +357,10 @@ class StreamChatLocalizationsKo extends GlobalStreamChatLocalizations { @override String attachmentLimitExceedError(int limit) => '첨부 파일 제한 초과: $limit 이상의 첨부 파일을 추가할 수 없습니다'; + + @override + String get linkDisabledDetails => '이 대화에서는 링크를 보낼 수 없습니다.'; + + @override + String get linkDisabledError => '링크가 비활성화되었습니다.'; } diff --git a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart index 44c02081..4c1d2a58 100644 --- a/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart +++ b/packages/stream_chat_localizations/lib/src/stream_chat_localizations_pt.dart @@ -371,4 +371,15 @@ Não é possível adicionar mais de $limit arquivos de uma vez @override String get slowModeOnLabel => 'Modo lento ativado'; + + @override + String get linkDisabledDetails => + 'O envio de links não é permitido nesta conversa.'; + + @override + String get linkDisabledError => 'Os links estão desativados'; + + @override + String get sendMessagePermissionError => + 'Você não tem permissão para enviar mensagens'; } diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index d2ea8b93..8a06f02d 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -157,7 +157,6 @@ void main() { (prev, curr) => prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), ), - status: MessageSendingStatus.sending, updatedAt: DateTime.now(), extraData: const {'extra_test_data': 'extraData'}, user: user, diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index 9c2378fd..9c4d99a2 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -147,7 +147,6 @@ void main() { (prev, curr) => prev?..update(curr.type, (value) => value + 1, ifAbsent: () => 1), ), - status: MessageSendingStatus.sending, updatedAt: DateTime.now(), extraData: const {'extra_test_data': 'extraData'}, user: user,