diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 10ff713e..85bf8a96 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -58,7 +58,7 @@ class Message extends Equatable { this.ownReactions, this.parentId, this.quotedMessage, - this.quotedMessageId, + String? quotedMessageId, this.replyCount = 0, this.threadParticipants, this.showInChannel, @@ -72,16 +72,19 @@ class Message extends Equatable { 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 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 @@ -134,8 +137,10 @@ class Message extends Equatable { @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) @@ -162,13 +167,17 @@ class Message extends Equatable { @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final String? command; + final DateTime? _createdAt; + /// Reserved field indicating when the message was created. @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) - final DateTime createdAt; + 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) - final DateTime updatedAt; + DateTime get updatedAt => _updatedAt ?? DateTime.now(); /// User who sent the message @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) @@ -301,17 +310,17 @@ class Message extends Equatable { ownReactions: ownReactions ?? this.ownReactions, parentId: parentId ?? this.parentId, quotedMessage: quotedMessage ?? this.quotedMessage, - quotedMessageId: quotedMessageId ?? this.quotedMessageId, + quotedMessageId: quotedMessageId ?? _quotedMessageId, replyCount: replyCount ?? this.replyCount, threadParticipants: threadParticipants ?? this.threadParticipants, showInChannel: showInChannel ?? this.showInChannel, command: command ?? this.command, - createdAt: createdAt ?? this.createdAt, + createdAt: createdAt ?? _createdAt, silent: silent ?? this.silent, extraData: extraData ?? this.extraData, user: user ?? this.user, shadowed: shadowed ?? this.shadowed, - updatedAt: updatedAt ?? this.updatedAt, + updatedAt: updatedAt ?? _updatedAt, deletedAt: deletedAt ?? this.deletedAt, status: status ?? this.status, pinned: pinned ?? this.pinned, 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..ea9afa2f 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -126,7 +126,9 @@ class ThreadPage extends StatelessWidget { ), ), MessageInput( - parentMessage: parent, + messageInputController: MessageInputController( + message: Message(parentId: parent!.id), + ), ), ], ), 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..2555e007 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -166,7 +166,9 @@ class ThreadPage extends StatelessWidget { ), ), MessageInput( - parentMessage: parent, + messageInputController: MessageInputController( + message: Message(parentId: parent!.id), + ), ), ], ), diff --git a/packages/stream_chat_flutter/example/pubspec.yaml b/packages/stream_chat_flutter/example/pubspec.yaml index 9977c198..bd785a29 100644 --- a/packages/stream_chat_flutter/example/pubspec.yaml +++ b/packages/stream_chat_flutter/example/pubspec.yaml @@ -27,9 +27,12 @@ dependencies: cupertino_icons: ^1.0.3 flutter: sdk: flutter - stream_chat_flutter: ^2.2.1 - stream_chat_localizations: ^1.1.0 - stream_chat_persistence: ^2.2.0 + stream_chat_flutter: + path: ../ + stream_chat_localizations: + path: ../../stream_chat_localizations + stream_chat_persistence: + path: ../../stream_chat_persistence dev_dependencies: flutter_test: diff --git a/packages/stream_chat_flutter/lib/src/localization/translations.dart b/packages/stream_chat_flutter/lib/src/localization/translations.dart index 8aa8d071..8039ac47 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' 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 a67004af..e85e7cd8 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -610,7 +610,9 @@ class _MessageActionsModalState extends State { widget.editMessageInputBuilder!(context, widget.message) else MessageInput( - editMessage: widget.message, + messageInputController: MessageInputController( + message: widget.message, + ), preMessageSending: (m) { FocusScope.of(context).unfocus(); Navigator.pop(context); diff --git a/packages/stream_chat_flutter/lib/src/mip/countdown_button.dart b/packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart similarity index 100% rename from packages/stream_chat_flutter/lib/src/mip/countdown_button.dart rename to packages/stream_chat_flutter/lib/src/message_input/countdown_button.dart diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart similarity index 87% rename from packages/stream_chat_flutter/lib/src/message_input.dart rename to packages/stream_chat_flutter/lib/src/message_input/message_input.dart index 2e035df0..aa18fce5 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/message_input.dart @@ -184,12 +184,9 @@ class MessageInput extends StatefulWidget { Key? key, this.onMessageSent, this.preMessageSending, - this.parentMessage, - this.editMessage, this.maxHeight = 150, this.keyboardType = TextInputType.multiline, this.disableAttachments = false, - this.initialMessage, this.messageInputController, this.actions = const [], this.actionsLocation = ActionsLocation.left, @@ -219,18 +216,11 @@ class MessageInput extends StatefulWidget { this.attachmentsPickerBuilder, this.sendButtonBuilder, this.shouldKeepFocusAfterMessage, - }) : assert( - initialMessage == null || editMessage == null, - "Can't provide both `initialMessage` and `editMessage`", - ), - super(key: key); + }) : super(key: key); /// List of options for showing overlays final List customOverlays; - /// Message to edit - final Message? editMessage; - /// Video quality to use when compressing the videos final VideoQuality compressedVideoQuality; @@ -242,9 +232,6 @@ class MessageInput extends StatefulWidget { /// do not set it if you're using our default CDN final int maxAttachmentSize; - /// Message to start with - final Message? initialMessage; - /// Function called after sending the message final void Function(Message)? onMessageSent; @@ -252,9 +239,6 @@ class MessageInput extends StatefulWidget { /// Use this to transform the message final FutureOr Function(Message)? preMessageSending; - /// Parent message in case of a thread - final Message? parentMessage; - /// Maximum Height for the TextField to grow before it starts scrolling final double maxHeight; @@ -367,11 +351,10 @@ class MessageInputState extends State { final _imagePicker = ImagePicker(); late final _focusNode = widget.focusNode ?? FocusNode(); bool _inputEnabled = true; - bool _commandEnabled = false; + bool get _commandEnabled => messageInputController.value.command != null; bool _showCommandsOverlay = false; bool _showMentionsOverlay = false; - Command? _chosenCommand; bool _actionsShrunk = false; bool _openFilePickerSection = false; @@ -382,16 +365,17 @@ class MessageInputState extends State { late StreamChatThemeData _streamChatTheme; late MessageInputThemeData _messageInputTheme; - bool get _hasQuotedMessage => widget.quotedMessage != null; + bool get _hasQuotedMessage => + messageInputController.value.quotedMessage != null; bool get _messageIsPresent => messageInputController.text.trim().isNotEmpty; + bool get _isEditing => + messageInputController.value.status != MessageSendingStatus.sending; + @override void initState() { super.initState(); - if (widget.editMessage != null || widget.initialMessage != null) { - _parseExistingMessage(widget.editMessage ?? widget.initialMessage!); - } messageInputController.textEditingController .addListener(_onChangedDebounced); _focusNode.addListener(_focusNodeListener); @@ -437,70 +421,74 @@ class MessageInputState extends State { Widget build(BuildContext context) { Widget child = ValueListenableBuilder( valueListenable: messageInputController, - builder: (context, value, wid) => DecoratedBox( - decoration: BoxDecoration( - color: _messageInputTheme.inputBackgroundColor, - ), - child: SafeArea( - child: GestureDetector( - onPanUpdate: (details) { - if (details.delta.dy > 0) { - _focusNode.unfocus(); - if (_openFilePickerSection) { - setState(() { - _openFilePickerSection = false; - }); + builder: (context, value, wid) { + print('VALUE ${value.text}'); + return DecoratedBox( + decoration: BoxDecoration( + color: _messageInputTheme.inputBackgroundColor, + ), + child: SafeArea( + 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, + }, + 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: widget.onQuotedMessageCleared, - ), - ], + Text( + context.translations.replyToMessageLabel, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + IconButton( + visualDensity: VisualDensity.compact, + icon: StreamSvgIcon.closeSmall(), + onPressed: widget.onQuotedMessageCleared, + ), + ], + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: _buildTextField(context), - ), - if (widget.parentMessage != null && !widget.hideSendAsDm) Padding( - padding: const EdgeInsets.only( - right: 12, - left: 12, - bottom: 12, - ), - child: _buildDmCheckbox(), + padding: const EdgeInsets.symmetric(vertical: 8), + child: _buildTextField(context), ), - _buildFilePickerSection(), - ], + if (messageInputController.value.parentId != null && + !widget.hideSendAsDm) + Padding( + padding: const EdgeInsets.only( + right: 12, + left: 12, + bottom: 12, + ), + child: _buildDmCheckbox(), + ), + _buildFilePickerSection(), + ], + ), ), ), - ), - ), + ); + }, ); - if (widget.editMessage == null) { + if (_isEditing) { child = Material( elevation: 8, child: child, @@ -575,10 +563,8 @@ class MessageInputState extends State { : _streamChatTheme.colorTheme.barsBg, child: InkWell( onTap: () { - setState(() { - messageInputController.showInChannel = - !messageInputController.showInChannel; - }); + messageInputController.showInChannel = + !messageInputController.showInChannel; }, child: AnimatedCrossFade( duration: const Duration(milliseconds: 300), @@ -621,7 +607,7 @@ class MessageInputState extends State { onSendMessage: sendMessage, timeOut: _timeOut, isIdle: !_messageIsPresent && messageInputController.attachments.isEmpty, - isEditEnabled: widget.editMessage != null, + isEditEnabled: _isEditing, idleSendButton: widget.idleSendButton, activeSendButton: widget.activeSendButton, ); @@ -668,7 +654,7 @@ class MessageInputState extends State { if (!widget.disableAttachments) _buildAttachmentButton(context), if (widget.showCommandsButton && - widget.editMessage == null && + !_isEditing && channel.state != null && channel.config?.commands.isNotEmpty == true) _buildCommandButton(context), @@ -791,7 +777,7 @@ class MessageInputState extends State { size: 16, ), Text( - _chosenCommand?.name.toUpperCase() ?? '', + messageInputController.value.command!.toUpperCase(), style: _streamChatTheme.textTheme.footnoteBold.copyWith( color: Colors.white, @@ -825,9 +811,7 @@ class MessageInputState extends State { height: 24, width: 24, ), - onPressed: () { - setState(() => _commandEnabled = false); - }, + onPressed: messageInputController.clear, ), ), if (!_commandEnabled && @@ -848,8 +832,10 @@ class MessageInputState extends State { final channel = StreamChannel.of(context).channel; if (value.isNotEmpty) { - // ignore: no-empty-block - channel.keyStroke(widget.parentMessage?.id).catchError((e) {}); + channel + .keyStroke(messageInputController.value.parentId) + // ignore: no-empty-block + .catchError((e) {}); } var actionsLength = widget.actions.length; @@ -869,7 +855,7 @@ class MessageInputState extends State { ); String _getHint(BuildContext context) { - if (_commandEnabled && _chosenCommand!.name == 'giphy') { + if (_commandEnabled && messageInputController.value.command == 'giphy') { return context.translations.searchGifLabel; } if (messageInputController.attachments.isNotEmpty) { @@ -1034,15 +1020,11 @@ class MessageInputState extends State { splits[splits.length - 1] = user.name; final rejoin = splits.join('@'); - messageInputController.textEditingController.value = TextEditingValue( - text: rejoin + - messageInputController.text.substring( - messageInputController.selectionStart, - ), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); + messageInputController.text = rejoin + + messageInputController.text.substring( + messageInputController.selectionStart, + ); + _onChangedDebounced.cancel(); setState(() => _showMentionsOverlay = false); }, @@ -1077,22 +1059,17 @@ class MessageInputState extends State { void _chooseEmoji(List splits, Emoji emoji) { final rejoin = splits.sublist(0, splits.length - 1).join(':') + emoji.char!; - messageInputController.textEditingController.value = TextEditingValue( - text: rejoin + - messageInputController.text.substring( - messageInputController.selectionStart, - ), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); + messageInputController.text = rejoin + + messageInputController.text.substring( + messageInputController.selectionStart, + ); } void _setCommand(Command c) { - messageInputController.clear(); + messageInputController + ..clear() + ..command = c; setState(() { - _chosenCommand = c; - _commandEnabled = true; _showCommandsOverlay = false; }); } @@ -1133,9 +1110,7 @@ class MessageInputState extends State { (e) => ClipRRect( borderRadius: BorderRadius.circular(10), child: FileAttachment( - message: Message( - status: MessageSendingStatus.sending, - ), // dummy message + message: Message(), // dummy message attachment: e, size: Size( MediaQuery.of(context).size.width * 0.65, @@ -1202,9 +1177,11 @@ class MessageInputState extends State { focusElevation: 0, hoverElevation: 0, onPressed: () { - setState( - () => messageInputController.attachments - .removeWhere((e) => e.id == attachment.id), + messageInputController.value = + messageInputController.value.copyWith( + attachments: messageInputController.attachments + .where((it) => it.id != attachment.id) + .toList(), ); }, fillColor: @@ -1412,7 +1389,7 @@ class MessageInputState extends State { /// /// Note: Only meant to be used from outside the state. void addAttachment(Attachment attachment) { - setState(() => _addAttachments([attachment])); + _addAttachments([attachment]); } /// Adds an attachment to the [messageInputController.attachments] map @@ -1539,23 +1516,20 @@ class MessageInputState extends State { } } - setState(() { - _addAttachments([ - attachment.copyWith( - file: file, - extraData: {...attachment.extraData} - ..update('file_size', ((_) => file!.size!)), - ), - ]); - }); + _addAttachments([ + attachment.copyWith( + file: file, + extraData: {...attachment.extraData} + ..update('file_size', ((_) => file!.size!)), + ), + ]); } /// Sends the current message Future sendMessage() async { - var text = messageInputController.text.trim(); - final attachments = messageInputController.attachments; + var message = messageInputController.value; - if (text.isEmpty && attachments.isEmpty) { + if (messageInputController.isValid) { return; } @@ -1563,48 +1537,9 @@ class MessageInputState extends State { shouldKeepFocus ??= !_commandEnabled; - if (_commandEnabled) { - text = '${'/${_chosenCommand!.name} '}$text'; - } - - messageInputController - ..text = '' - ..clearAttachments(); + messageInputController.reset(); widget.onQuotedMessageCleared?.call(); - setState(() { - _commandEnabled = false; - }); - - Message message; - if (widget.editMessage != null) { - message = widget.editMessage!.copyWith( - text: text, - attachments: attachments, - mentionedUsers: messageInputController.mentionedUsers - .where((u) => text.contains('@${u.name}')) - .toList(), - ); - } else { - message = (widget.initialMessage ?? Message()).copyWith( - parentId: widget.parentMessage?.id, - text: text, - attachments: attachments, - mentionedUsers: messageInputController.mentionedUsers - .where((u) => text.contains('@${u.name}')) - .toList(), - showInChannel: widget.parentMessage != null - ? messageInputController.showInChannel - : null, - ); - } - - if (widget.quotedMessage != null) { - message = message.copyWith( - quotedMessageId: widget.quotedMessage!.id, - ); - } - if (widget.preMessageSending != null) { message = await widget.preMessageSending!(message); } @@ -1615,13 +1550,9 @@ class MessageInputState extends State { await streamChannel.reloadChannel(); } - messageInputController.clearMentionedUsers(); - try { Future sendingFuture; - if (widget.editMessage == null || - widget.editMessage!.status == MessageSendingStatus.failed || - widget.editMessage!.status == MessageSendingStatus.sending) { + if (!_isEditing) { sendingFuture = channel.sendMessage(message); } else { sendingFuture = channel.updateMessage(message); @@ -1635,7 +1566,7 @@ class MessageInputState extends State { final resp = await sendingFuture; if (resp.message?.type == 'error') { - _parseExistingMessage(message); + messageInputController.value = message; } _startSlowMode(); widget.onMessageSent?.call(resp.message); @@ -1714,12 +1645,6 @@ class MessageInputState extends State { ); } - void _parseExistingMessage(Message message) { - final messageText = message.text; - if (messageText != null) messageInputController.text = messageText; - _addAttachments(message.attachments); - } - @override void dispose() { messageInputController.textEditingController @@ -1731,19 +1656,12 @@ class MessageInputState extends State { super.dispose(); } - bool _initialized = false; - @override void didChangeDependencies() { _streamChatTheme = StreamChatTheme.of(context); _messageInputTheme = MessageInputTheme.of(context); - if (widget.editMessage == null && _timeOut <= 0) _startSlowMode(); + if (!_isEditing && _timeOut <= 0) _startSlowMode(); - if ((widget.editMessage != null || widget.initialMessage != null) && - !_initialized) { - FocusScope.of(context).requestFocus(_focusNode); - _initialized = true; - } super.didChangeDependencies(); } } diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart similarity index 99% rename from packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart index f075a09c..066ed780 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_attachment_picker.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_attachment_picker.dart @@ -116,8 +116,8 @@ class _StreamAttachmentPickerState extends State { @override Widget build(BuildContext context) { - var _streamChatTheme = StreamChatTheme.of(context); - var messageInputController = widget.messageInputController; + final _streamChatTheme = StreamChatTheme.of(context); + final messageInputController = widget.messageInputController; final _attachmentContainsImage = messageInputController.attachments.any((it) => it.type == 'image'); diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart similarity index 92% rename from packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart index 5443fb18..ec03a58c 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_message_send_button.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_send_button.dart @@ -23,7 +23,7 @@ class StreamMessageSendButton extends StatelessWidget { @override Widget build(BuildContext context) { - var _streamChatTheme = StreamChatTheme.of(context); + final _streamChatTheme = StreamChatTheme.of(context); late Widget sendButton; if (timeOut > 0) { @@ -46,7 +46,7 @@ class StreamMessageSendButton extends StatelessWidget { } Widget _buildIdleSendButton(BuildContext context) { - var _messageInputTheme = MessageInputTheme.of(context); + final _messageInputTheme = MessageInputTheme.of(context); return Padding( padding: const EdgeInsets.all(8), @@ -58,7 +58,7 @@ class StreamMessageSendButton extends StatelessWidget { } Widget _buildSendButton(BuildContext context) { - var _messageInputTheme = MessageInputTheme.of(context); + final _messageInputTheme = MessageInputTheme.of(context); return Padding( padding: const EdgeInsets.all(8), diff --git a/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart similarity index 99% rename from packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart rename to packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart index 43acce31..37b4f683 100644 --- a/packages/stream_chat_flutter/lib/src/mip/stream_message_text_field.dart +++ b/packages/stream_chat_flutter/lib/src/message_input/stream_message_text_field.dart @@ -1,3 +1,5 @@ +// ignore_for_file: prefer-trailing-comma, cascade_invocations + import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; import 'package:flutter/cupertino.dart'; 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 23d913cb..f3818b2e 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -2,7 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide TextTheme; import 'package:stream_chat_flutter/src/channel_preview.dart'; import 'package:stream_chat_flutter/src/gradient_avatar.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/reaction_icon.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 78977cf4..b39a2eea 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -23,16 +23,16 @@ 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/message_input.dart'; export 'src/message_list_view.dart'; export 'src/message_search_item.dart'; export 'src/message_search_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; -export 'src/mip/countdown_button.dart'; -export 'src/mip/stream_attachment_picker.dart'; -export 'src/mip/stream_message_send_button.dart'; -export 'src/mip/stream_message_text_field.dart'; +export 'src/message_input/countdown_button.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/option_list_tile.dart'; export 'src/reaction_icon.dart'; export 'src/reaction_picker.dart'; 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 index 3339bc5b..4d1565da 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_input_controller.dart @@ -4,29 +4,82 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:stream_chat/stream_chat.dart'; +typedef MessageValidator = bool Function(Message message); + /// 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}) => - MessageInputController._(message ?? Message()); + factory MessageInputController({ + Message? message, + MessageValidator? validator, + }) => + MessageInputController._( + initialMessage: message ?? Message(), + validator: validator ?? _defaultValidator, + ); /// Creates a controller for an editable text field from an initial [text]. - factory MessageInputController.fromText(String? text) => - MessageInputController._(Message(text: text)); + factory MessageInputController.fromText( + String? text, { + MessageValidator? validator, + }) => + MessageInputController._( + initialMessage: Message(text: text), + validator: validator ?? _defaultValidator, + ); /// Creates a controller for an editable text field from an initial /// [attachments]. factory MessageInputController.fromAttachments( - List attachments, - ) => - MessageInputController._(Message(attachments: attachments)); + List attachments, { + MessageValidator? validator, + }) => + MessageInputController._( + initialMessage: Message(attachments: attachments), + validator: validator ?? _defaultValidator, + ); - MessageInputController._(Message message) - : _textEditingController = TextEditingController(text: message.text), - super(message); + MessageInputController._({ + required Message initialMessage, + this.validator = _defaultValidator, + }) : _textEditingController = + TextEditingController(text: initialMessage.text), + _initialMessage = initialMessage, + super(initialMessage) { + addListener(_textEditingSyncer); + } + + /// A callback function that validates the message. + final MessageValidator validator; + + /// Checks if the message is valid. + bool get isValid => validator(value); + + static bool _defaultValidator(Message message) => + message.text?.isNotEmpty != true && message.attachments.isEmpty; + + 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, + ); + } + } /// TextEditingController get textEditingController => _textEditingController; @@ -35,24 +88,29 @@ class MessageInputController extends ValueNotifier { /// String get text => _textEditingController.text; + final Message _initialMessage; + /// set message(Message message) { value = message; } - set text(String newText) { - value = value.copyWith(text: newText); - _textEditingController - ..text = newText - ..selection = TextSelection.fromPosition( - TextPosition(offset: _textEditingController.text.length), - ); + /// + set command(Command command) { + value = value.copyWith( + command: command.name, + text: '/${command.name} ', + ); } - /// - set textEditingValue(TextEditingValue newValue) { - _textEditingController.value = newValue; - value = value.copyWith(text: _textEditingController.text); + 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); } /// @@ -147,9 +205,13 @@ class MessageInputController extends ValueNotifier { _textEditingController.clear(); } + /// Set the [value] to the initial [Message] value. + void reset() => value = _initialMessage; + @override void dispose() { super.dispose(); + removeListener(_textEditingSyncer); _textEditingController.dispose(); } }